import prisma from '../utils/db'; import { catatAudit } from '../utils/audit'; import { moveToTrash } from '../utils/trash'; import { StokService } from './stok.service'; import { deleteFromSupabase } from '../utils/supabase'; import { APIError } from '../utils/errors'; export class ProdukService { static async getProduk(skip = 0, limit = 10, kategoriId?: number, supplierId?: number, pencarian?: string) { const where: any = { deleted_at: null, }; if (kategoriId) where.kategori_id = kategoriId; if (supplierId) where.supplier_id = supplierId; if (pencarian) { where.OR = [ { kode: { contains: pencarian } }, { barcode: { contains: pencarian } }, { nama: { contains: pencarian } }, ]; } const total = await prisma.produk.count({ where }); const products = await prisma.produk.findMany({ where, skip, take: limit, include: { kategori: true, supplier: true, harga_tingkat: true, }, }); return { products, total }; } static async getProdukById(id: number) { const product = await prisma.produk.findFirst({ where: { id, deleted_at: null }, include: { kategori: true, supplier: true, harga_tingkat: true, }, }); if (!product) { throw new APIError('Produk tidak ditemukan', 404); } return product; } static async getProdukByBarcodeOrKode(term: string) { const product = await prisma.produk.findFirst({ where: { OR: [ { barcode: term }, { kode: term }, ], deleted_at: null, }, include: { kategori: true, supplier: true, harga_tingkat: true, }, }); if (!product) { throw new APIError('Produk dengan kode/barcode tersebut tidak ditemukan', 404); } return product; } static async create(data: any, actorId: number, ip: string, userAgent: string) { // Check duplicate const existingCode = await prisma.produk.findFirst({ where: { kode: data.kode, deleted_at: null }, }); const existingBarcode = data.barcode ? await prisma.produk.findFirst({ where: { barcode: data.barcode, deleted_at: null }, }) : null; const existing = existingCode || existingBarcode; if (existing) { const fieldType = existing.id === existingCode?.id ? 'Kode' : 'Barcode'; const fieldVal = existing.id === existingCode?.id ? data.kode : data.barcode; throw new APIError(`${fieldType} produk '${fieldVal}' sudah digunakan`, 400); } // Check soft-deleted code or barcode const softDeletedCode = await prisma.produk.findFirst({ where: { kode: data.kode, NOT: { deleted_at: null } }, }); const softDeletedBarcode = data.barcode ? await prisma.produk.findFirst({ where: { barcode: data.barcode, NOT: { deleted_at: null } }, }) : null; const softDeleted = softDeletedCode || softDeletedBarcode; if (softDeleted) { // Reactivate const restored = await prisma.produk.update({ where: { id: softDeleted.id }, data: { deleted_at: null, is_aktif: 1, nama: data.nama, kategori_id: data.kategori_id, supplier_id: data.supplier_id ?? null, satuan: data.satuan ?? 'pcs', harga_beli: data.harga_beli, harga_jual: data.harga_jual, stok_min: data.stok_min ?? 5, expired_date: data.expired_date ?? null, foto_url: data.foto_url ?? null, qr_url: data.qr_url ?? null, deskripsi: data.deskripsi ?? null, is_ecer: data.is_ecer ?? 1, }, }); // Reset pricing levels await prisma.harga_tingkat.deleteMany({ where: { produk_id: restored.id }, }); if (data.harga_tingkat && data.harga_tingkat.length > 0) { for (const ht of data.harga_tingkat) { await prisma.harga_tingkat.create({ data: { produk_id: restored.id, tingkat: ht.tingkat, min_qty: ht.min_qty, harga: ht.harga, }, }); } } await catatAudit({ userId: actorId, aksi: 'restore', entitas: 'produk', entitasId: restored.id, dataBaru: { kode: restored.kode, nama: restored.nama, harga_beli: Number(restored.harga_beli), harga_jual: Number(restored.harga_jual), }, ipAddress: ip, userAgent, }); return restored; } // Create new product const product = await prisma.produk.create({ data: { kode: data.kode, barcode: data.barcode ?? null, nama: data.nama, kategori_id: data.kategori_id, supplier_id: data.supplier_id ?? null, satuan: data.satuan ?? 'pcs', harga_beli: data.harga_beli, harga_jual: data.harga_jual, stok: 0.00, stok_min: data.stok_min ?? 5, expired_date: data.expired_date ?? null, foto_url: data.foto_url ?? null, qr_url: data.qr_url ?? null, deskripsi: data.deskripsi ?? null, is_ecer: data.is_ecer ?? 1, is_aktif: data.is_aktif ?? 1, }, }); // Create pricing levels if (data.harga_tingkat && data.harga_tingkat.length > 0) { for (const ht of data.harga_tingkat) { await prisma.harga_tingkat.create({ data: { produk_id: product.id, tingkat: ht.tingkat, min_qty: ht.min_qty, harga: ht.harga, }, }); } } // Tambah stok awal jika disediakan > 0 if (data.stok && data.stok > 0) { await StokService.tambahStok({ produkId: product.id, qty: data.stok, hargaBeli: data.harga_beli, batchNo: 'BATCH-AWAL', expiredDate: data.expired_date ?? null, supplierId: data.supplier_id ?? null, userId: actorId, catatan: 'Stok awal produk baru', referensi: 'STOK-AWAL', }); } await catatAudit({ userId: actorId, aksi: 'create', entitas: 'produk', entitasId: product.id, dataBaru: { kode: product.kode, nama: product.nama, harga_jual: Number(product.harga_jual), stok: Number(data.stok ?? 0), }, ipAddress: ip, userAgent, }); return product; } static async update(id: number, data: any, actorId: number, ip: string, userAgent: string) { const product = await this.getProdukById(id); const dataLama = { kode: product.kode, barcode: product.barcode, nama: product.nama, kategori_id: product.kategori_id, supplier_id: product.supplier_id, satuan: product.satuan, harga_beli: Number(product.harga_beli), harga_jual: Number(product.harga_jual), stok: Number(product.stok), stok_min: Number(product.stok_min), is_ecer: product.is_ecer, is_aktif: product.is_aktif, }; const updateData: any = {}; if (data.kode !== undefined) { const dup = await prisma.produk.findFirst({ where: { kode: data.kode, id: { not: id }, deleted_at: null }, }); if (dup) throw new APIError(`Kode produk '${data.kode}' sudah digunakan oleh produk lain`, 400); updateData.kode = data.kode; } if (data.barcode !== undefined) { if (data.barcode) { const dup = await prisma.produk.findFirst({ where: { barcode: data.barcode, id: { not: id }, deleted_at: null }, }); if (dup) throw new APIError(`Barcode '${data.barcode}' sudah digunakan oleh produk lain`, 400); } updateData.barcode = data.barcode; } if (data.nama !== undefined) updateData.nama = data.nama; if (data.kategori_id !== undefined) updateData.kategori_id = data.kategori_id; if (data.supplier_id !== undefined) updateData.supplier_id = data.supplier_id; if (data.satuan !== undefined) updateData.satuan = data.satuan; if (data.harga_beli !== undefined) updateData.harga_beli = data.harga_beli; if (data.harga_jual !== undefined) updateData.harga_jual = data.harga_jual; if (data.stok_min !== undefined) updateData.stok_min = data.stok_min; if (data.expired_date !== undefined) updateData.expired_date = data.expired_date; if (data.qr_url !== undefined) updateData.qr_url = data.qr_url; if (data.deskripsi !== undefined) updateData.deskripsi = data.deskripsi; if (data.is_ecer !== undefined) updateData.is_ecer = data.is_ecer; if (data.is_aktif !== undefined) updateData.is_aktif = data.is_aktif; // Handle foto_url change and delete old images from Supabase if (data.foto_url !== undefined && data.foto_url !== product.foto_url) { const oldFoto = product.foto_url; if (oldFoto) { let urls: string[] = []; try { const parsed = JSON.parse(oldFoto); if (Array.isArray(parsed)) { urls = parsed; } else { urls = [String(parsed)]; } } catch (e) { urls = [oldFoto]; } const searchStr = '/storage/v1/object/public/produk/'; for (const url of urls) { if (url.includes(searchStr)) { const filePath = url.split(searchStr).pop(); if (filePath) { await deleteFromSupabase('produk', filePath); } } } } updateData.foto_url = data.foto_url; } const updated = await prisma.produk.update({ where: { id }, data: updateData, }); // Update pricing levels if provided if (data.harga_tingkat !== undefined) { await prisma.harga_tingkat.deleteMany({ where: { produk_id: id }, }); if (data.harga_tingkat && data.harga_tingkat.length > 0) { for (const ht of data.harga_tingkat) { await prisma.harga_tingkat.create({ data: { produk_id: id, tingkat: ht.tingkat, min_qty: ht.min_qty, harga: ht.harga, }, }); } } } const dataBaru = { kode: updated.kode, barcode: updated.barcode, nama: updated.nama, kategori_id: updated.kategori_id, supplier_id: updated.supplier_id, satuan: updated.satuan, harga_beli: Number(updated.harga_beli), harga_jual: Number(updated.harga_jual), stok: Number(updated.stok), stok_min: Number(updated.stok_min), is_ecer: updated.is_ecer, is_aktif: updated.is_aktif, }; await catatAudit({ userId: actorId, aksi: 'update', entitas: 'produk', entitasId: updated.id, dataLama, dataBaru, ipAddress: ip, userAgent, }); return updated; } static async delete(id: number, actorId: number, ip: string, userAgent: string) { const product = await this.getProdukById(id); await moveToTrash({ instance: product, entityType: 'produk', actorId, }); await catatAudit({ userId: actorId, aksi: 'delete', entitas: 'produk', entitasId: product.id, dataLama: { kode: product.kode, nama: product.nama, deleted: false }, dataBaru: { kode: product.kode, nama: product.nama, deleted: true }, ipAddress: ip, userAgent, }); return true; } }