Tokiva-backend-Node / src /services /produk.service.ts
anoderb
feat: initial commit of Tokiva POS Node backend for Hugging Face
ef874e6
Raw
History Blame Contribute Delete
11.5 kB
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;
}
}