Spaces:
Sleeping
Sleeping
File size: 6,266 Bytes
ef874e6 48013ee ef874e6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 | import prisma from '../utils/db';
import { catatAudit } from '../utils/audit';
import { APIError } from '../utils/errors';
export class BonService {
static async getBon(skip = 0, limit = 10, statusFilter?: 'aktif' | 'lunas' | 'macet', memberId?: number) {
const where: any = {};
if (statusFilter) where.status = statusFilter;
if (memberId) where.member_id = memberId;
return await prisma.bon.findMany({
where,
orderBy: { created_at: 'desc' },
skip,
take: limit,
include: {
member: true,
transaksi: true,
},
});
}
static async getBonById(id: number) {
const bon = await prisma.bon.findUnique({
where: { id },
include: {
member: true,
transaksi: true,
bon_cicilan: true,
},
});
if (!bon) {
throw new APIError('Pencatatan kasbon tidak ditemukan', 404);
}
return bon;
}
static async bayarCicilan(params: {
bonId: number;
nominal: number;
metode: 'tunai' | 'transfer';
actorId: number;
ip: string;
userAgent: string;
}) {
const { bonId, nominal, metode, actorId, ip, userAgent } = params;
const bon = await this.getBonById(bonId);
if (bon.status === 'lunas') {
throw new APIError('Kasbon ini sudah lunas didebet sebelumnya', 400);
}
const sisaBon = Number(bon.sisa_bon);
if (nominal > sisaBon) {
throw new APIError(`Nominal pembayaran Rp ${nominal.toLocaleString('id-ID')} melebihi sisa tagihan Rp ${sisaBon.toLocaleString('id-ID')}`, 400);
}
// Pastikan kasir memiliki shift aktif
const shift = await prisma.shift.findFirst({
where: {
user_id: actorId,
status: 'buka',
},
});
if (!shift) {
throw new APIError('Shift kasir Anda belum dibuka. Harus membuka shift terlebih dahulu untuk menerima pembayaran.', 400);
}
// 1. Buat record cicilan
await prisma.bon_cicilan.create({
data: {
bon_id: bon.id,
nominal: nominal,
metode: metode,
user_id: actorId,
},
});
// 2. Kurangi sisa bon
const sisaLama = sisaBon;
const sisaBaru = sisaLama - nominal;
const statusBaru = sisaBaru <= 0 ? 'lunas' : bon.status;
await prisma.bon.update({
where: { id: bonId },
data: {
sisa_bon: sisaBaru,
status: statusBaru as any,
},
});
// 3. Kurangi total bon member
const totalBonMemberLama = Number(bon.member.total_bon);
const totalBonMemberBaru = Math.max(0, totalBonMemberLama - nominal);
await prisma.member.update({
where: { id: bon.member_id },
data: { total_bon: totalBonMemberBaru },
});
// 4. Masukkan ke kas shift
const updateShift: any = {};
if (metode === 'tunai') {
updateShift.total_tunai = Number(shift.total_tunai) + nominal;
} else if (metode === 'transfer') {
updateShift.total_transfer = Number(shift.total_transfer) + nominal;
}
await prisma.shift.update({
where: { id: shift.id },
data: updateShift,
});
// 5. Audit log
await catatAudit({
userId: actorId,
aksi: 'update',
entitas: 'bon',
entitasId: bon.id,
dataLama: { sisa_bon: sisaLama, status: bon.status },
dataBaru: { sisa_bon: sisaBaru, status: statusBaru, pembayaran_cicilan: nominal },
ipAddress: ip,
userAgent,
});
return await this.getBonById(bonId);
}
static async kirimReminderWA(bonId: number) {
const bon = await prisma.bon.findUnique({
where: { id: bonId },
include: { member: true },
});
if (!bon || !bon.member) {
throw new APIError('Gagal membuat pengingat WhatsApp. Data member tidak ditemukan.', 400);
}
const { nama, nomor_hp } = bon.member;
if (!nomor_hp) {
throw new APIError('Gagal membuat pengingat WhatsApp. Nomor HP member tidak terisi.', 400);
}
// Bersihkan nomor HP
let cleanedNo = nomor_hp.trim();
if (cleanedNo.startsWith('0')) {
cleanedNo = '62' + cleanedNo.slice(1);
} else if (cleanedNo.startsWith('+')) {
cleanedNo = cleanedNo.slice(1);
}
// Format pesan
const jatuhTempoStr = bon.jatuh_tempo
? new Date(bon.jatuh_tempo).toLocaleDateString('id-ID', { day: '2-digit', month: '2-digit', year: 'numeric' })
: 'secepatnya';
const sisaRupiah = Math.floor(Number(bon.sisa_bon)).toLocaleString('id-ID').replace(/,/g, '.');
const pesan =
`Halo *${nama}*,\n\n` +
`Kami dari *Tokiva POS* ingin mengingatkan bahwa Anda memiliki tagihan kasbon ` +
`sebesar *Rp ${sisaRupiah}* yang jatuh tempo pada tanggal *{jatuh_tempo_str}*.\n\n` +
`Silakan melakukan pembayaran cicilan atau pelunasan melalui kasir Tokiva. ` +
`Terima kasih atas kerjasamanya!`;
const formatPesan = pesan.replace('{jatuh_tempo_str}', jatuhTempoStr);
const encodedMessage = encodeURIComponent(formatPesan);
const waLink = `https://wa.me/${cleanedNo}?text=${encodedMessage}`;
// Tandai reminder terkirim
await prisma.bon.update({
where: { id: bonId },
data: { reminder_sent: 1 },
});
// Catat notifikasi
await prisma.notifikasi.create({
data: {
tipe: 'bon_jatuh_tempo',
judul: `WhatsApp Tagihan Dikirim: ${nama}`,
pesan: `Link WhatsApp pengingat tagihan Rp ${sisaRupiah} untuk ${nama} telah dibuat.`,
referensi_id: bon.id,
},
});
return waLink;
}
static async bayarCicilanByMemberId(params: {
memberId: number;
nominal: number;
metode: 'tunai' | 'transfer';
actorId: number;
ip: string;
userAgent: string;
}) {
const { memberId, nominal, metode, actorId, ip, userAgent } = params;
// Cari bon teraktif milik member
const bon = await prisma.bon.findFirst({
where: {
member_id: memberId,
status: 'aktif',
},
orderBy: {
created_at: 'asc', // bayar dari bon paling lama terutang
},
});
if (!bon) {
throw new APIError('Tidak ada pencatatan kasbon aktif untuk member ini.', 404);
}
return await this.bayarCicilan({
bonId: bon.id,
nominal,
metode,
actorId,
ip,
userAgent,
});
}
}
|