Spaces:
Sleeping
Sleeping
| import prisma from '../utils/db'; | |
| import { APIError } from '../utils/errors'; | |
| export class DiskonService { | |
| static async getActiveDiscounts(isMember: boolean) { | |
| const now = new Date(); | |
| // Filter diskon yang aktif dan periode tanggalnya cocok | |
| const discounts = await prisma.diskon.findMany({ | |
| where: { | |
| is_aktif: 1, | |
| tgl_mulai: { lte: now }, | |
| tgl_selesai: { gte: now }, | |
| ...(isMember ? {} : { member_only: 0 }), | |
| }, | |
| }); | |
| const currentTimeString = now.toTimeString().split(' ')[0]; // "HH:MM:SS" | |
| const activeDiscounts = discounts.filter((d) => { | |
| if (d.tipe === 'flash_sale') { | |
| if (d.jam_mulai && d.jam_selesai) { | |
| const jamMulai = new Date(d.jam_mulai).toTimeString().split(' ')[0]; | |
| const jamSelesai = new Date(d.jam_selesai).toTimeString().split(' ')[0]; | |
| return currentTimeString >= jamMulai && currentTimeString <= jamSelesai; | |
| } | |
| return true; | |
| } | |
| return true; | |
| }); | |
| return activeDiscounts; | |
| } | |
| static async hitungDiskonDanHarga(items: Array<{ produk_id: number; qty: number }>, memberId?: number | null) { | |
| const isMember = memberId !== undefined && memberId !== null; | |
| const activePromos = await this.getActiveDiscounts(isMember); | |
| // 1. Hitung harga dasar bertingkat per item (volume pricing) | |
| const cartItems: any[] = []; | |
| let subtotalDasar = 0; | |
| for (const item of items) { | |
| const produkId = item.produk_id; | |
| const qty = item.qty; | |
| const produk = await prisma.produk.findUnique({ | |
| where: { id: produkId }, | |
| include: { harga_tingkat: true }, | |
| }); | |
| if (!produk) { | |
| throw new APIError(`Produk ID ${produkId} tidak ditemukan.`, 404); | |
| } | |
| // Tentukan harga dasar berdasarkan volume pricing (harga_tingkat) | |
| let hargaSatuan = Number(produk.harga_jual); // Default ecer | |
| if (produk.harga_tingkat && produk.harga_tingkat.length > 0) { | |
| // Urutkan berdasarkan min_qty desc untuk mencari batas tertinggi yang terpenuhi | |
| const sortedTingkat = [...produk.harga_tingkat].sort((a, b) => Number(b.min_qty) - Number(a.min_qty)); | |
| for (const ht of sortedTingkat) { | |
| if (qty >= Number(ht.min_qty)) { | |
| hargaSatuan = Number(ht.harga); | |
| break; | |
| } | |
| } | |
| } | |
| const itemSubtotal = qty * hargaSatuan; | |
| subtotalDasar += itemSubtotal; | |
| cartItems.push({ | |
| produk, | |
| qty, | |
| harga_original: hargaSatuan, | |
| diskon_item: 0, | |
| diskon_id: null, | |
| subtotal: itemSubtotal, | |
| }); | |
| } | |
| // 2. Terapkan diskon per item (per_item_persen, per_item_nominal, flash_sale) | |
| for (const item of cartItems) { | |
| const produk = item.produk; | |
| const qty = item.qty; | |
| const hargaOriginal = item.harga_original; | |
| let bestDiscount = 0; | |
| let bestDiskonId: number | null = null; | |
| for (const promo of activePromos) { | |
| // Cek apakah promo berlaku untuk produk ini | |
| let isApplicable = false; | |
| if (!promo.produk_ids) { | |
| // Berlaku untuk semua produk | |
| isApplicable = true; | |
| } else { | |
| try { | |
| let pIds: number[] = []; | |
| if (typeof promo.produk_ids === 'string') { | |
| pIds = JSON.parse(promo.produk_ids); | |
| } else if (Array.isArray(promo.produk_ids)) { | |
| pIds = promo.produk_ids as number[]; | |
| } | |
| if (pIds.includes(produk.id)) { | |
| isApplicable = true; | |
| } | |
| } catch (e) { | |
| isApplicable = false; | |
| } | |
| } | |
| if (!isApplicable) continue; | |
| // Hitung nilai diskon | |
| let currentDiscount = 0; | |
| if (promo.tipe === 'per_item_persen' || promo.tipe === 'flash_sale') { | |
| currentDiscount = hargaOriginal * (Number(promo.nilai) / 100) * qty; | |
| } else if (promo.tipe === 'per_item_nominal') { | |
| currentDiscount = Number(promo.nilai) * qty; | |
| } | |
| if (currentDiscount > bestDiscount) { | |
| bestDiscount = currentDiscount; | |
| bestDiskonId = promo.id; | |
| } | |
| } | |
| if (bestDiscount > 0) { | |
| item.diskon_item = bestDiscount; | |
| item.diskon_id = bestDiskonId; | |
| item.subtotal = (hargaOriginal * qty) - bestDiscount; | |
| } | |
| } | |
| // Kalkulasi subtotal setelah item-level discounts | |
| let subtotalSetelahItem = cartItems.reduce((acc, item) => acc + item.subtotal, 0); | |
| const totalDiskonItem = cartItems.reduce((acc, item) => acc + item.diskon_item, 0); | |
| // 3. Terapkan diskon bundling (paket promo) | |
| let totalDiskonBundling = 0; | |
| const bundlingPromos = activePromos.filter((p) => p.tipe === 'bundling'); | |
| for (const promo of bundlingPromos) { | |
| const bundleItems = await prisma.diskon_bundling.findMany({ | |
| where: { diskon_id: promo.id }, | |
| }); | |
| if (bundleItems.length === 0) continue; | |
| // Hitung berapa banyak paket bundle yang bisa dibuat dari keranjang | |
| let maxPossibleBundles = 999999; | |
| for (const bItem of bundleItems) { | |
| const matchingCartItem = cartItems.find((item) => item.produk.id === bItem.produk_id); | |
| if (!matchingCartItem) { | |
| maxPossibleBundles = 0; | |
| break; | |
| } | |
| const availableMultiples = Math.floor(matchingCartItem.qty / Number(bItem.qty)); | |
| if (availableMultiples < maxPossibleBundles) { | |
| maxPossibleBundles = availableMultiples; | |
| } | |
| } | |
| if (maxPossibleBundles > 0) { | |
| const diskonBundleTotal = Number(promo.nilai) * maxPossibleBundles; | |
| totalDiskonBundling += diskonBundleTotal; | |
| subtotalSetelahItem -= diskonBundleTotal; | |
| } | |
| } | |
| // 4. Terapkan diskon tingkat transaksi (total_persen, total_nominal, member) | |
| const transactionPromos = activePromos.filter((p) => | |
| ['total_persen', 'total_nominal', 'member'].includes(p.tipe) | |
| ); | |
| let bestTransDiscount = 0; | |
| for (const promo of transactionPromos) { | |
| // Cek minimal belanja | |
| if (subtotalSetelahItem < Number(promo.min_belanja)) { | |
| continue; | |
| } | |
| let currentTransDiscount = 0; | |
| if (promo.tipe === 'total_persen' || promo.tipe === 'member') { | |
| currentTransDiscount = subtotalSetelahItem * (Number(promo.nilai) / 100); | |
| } else if (promo.tipe === 'total_nominal') { | |
| currentTransDiscount = Number(promo.nilai); | |
| } | |
| if (currentTransDiscount > bestTransDiscount) { | |
| bestTransDiscount = currentTransDiscount; | |
| } | |
| } | |
| const totalDiskonTransaksi = bestTransDiscount; | |
| // Hitung total akhir | |
| let totalAkhir = subtotalSetelahItem - totalDiskonTransaksi; | |
| if (totalAkhir < 0) { | |
| totalAkhir = 0; | |
| } | |
| return { | |
| cart_items: cartItems, | |
| subtotal_dasar: subtotalDasar, | |
| total_diskon_item: totalDiskonItem, | |
| total_diskon_bundling: totalDiskonBundling, | |
| total_diskon_transaksi: totalDiskonTransaksi, | |
| total_diskon_keseluruhan: totalDiskonItem + totalDiskonBundling + totalDiskonTransaksi, | |
| total_akhir: totalAkhir, | |
| }; | |
| } | |
| } | |