diff --git a/package-lock.json b/package-lock.json index f7ce162..512a8d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "axios": "^1.12.2", "bcryptjs": "^3.0.2", "cors": "^2.8.5", + "dayjs": "^1.11.19", "decimal.js": "^10.6.0", "express": "^4.17.1", "jsonwebtoken": "^9.0.2", @@ -2321,6 +2322,12 @@ "node": ">=10" } }, + "node_modules/dayjs": { + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "license": "MIT" + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", diff --git a/package.json b/package.json index 030d279..576f8aa 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "axios": "^1.12.2", "bcryptjs": "^3.0.2", "cors": "^2.8.5", + "dayjs": "^1.11.19", "decimal.js": "^10.6.0", "express": "^4.17.1", "jsonwebtoken": "^9.0.2", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index ffeeec5..73201bc 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -187,81 +187,82 @@ model sessions { /// The underlying table does not contain a valid unique identifier and can therefore currently not be handled by Prisma Client. model rv_openitem { - ad_org_id Decimal? @db.Decimal(10, 0) - ad_client_id Decimal? @db.Decimal(10, 0) - documentno String? @db.VarChar(30) - c_invoice_id Decimal? @db.Decimal(10, 0) - c_order_id Decimal? @db.Decimal(10, 0) - c_bpartner_id Decimal? @db.Decimal(10, 0) - issotrx String? @db.Char(1) - dateinvoiced DateTime? @db.Timestamp(6) - dateacct DateTime? @db.Timestamp(6) - netdays Decimal? @db.Decimal - duedate DateTime? @db.Timestamptz(6) + ad_org_id Decimal? @db.Decimal(10, 0) + ad_client_id Decimal? @db.Decimal(10, 0) + documentno String? @db.VarChar(30) + c_invoice_id Decimal? @db.Decimal(10, 0) + c_order_id Decimal? @db.Decimal(10, 0) + c_bpartner_id Decimal? @db.Decimal(10, 0) + issotrx String? @db.Char(1) + dateinvoiced DateTime? @db.Timestamp(6) + dateacct DateTime? @db.Timestamp(6) + netdays Decimal? @db.Decimal + duedate DateTime? @db.Timestamptz(6) daysdue Int? - discountdate DateTime? @db.Timestamp(6) - discountamt Decimal? @db.Decimal - grandtotal Decimal? @db.Decimal - paidamt Decimal? @db.Decimal - openamt Decimal? @db.Decimal - c_currency_id Decimal? @db.Decimal(10, 0) - c_conversiontype_id Decimal? @db.Decimal(10, 0) - c_paymentterm_id Decimal? @db.Decimal(10, 0) - ispayschedulevalid String? @db.Char(1) - c_invoicepayschedule_id Decimal? @db.Decimal - invoicecollectiontype String? @db.Char(1) - c_campaign_id Decimal? @db.Decimal(10, 0) - c_project_id Decimal? @db.Decimal(10, 0) - c_activity_id Decimal? @db.Decimal(10, 0) - ad_orgtrx_id Decimal? @db.Decimal(10, 0) - ad_user_id Decimal? @db.Decimal(10, 0) - c_bpartner_location_id Decimal? @db.Decimal(10, 0) - c_charge_id Decimal? @db.Decimal(10, 0) - c_doctype_id Decimal? @db.Decimal(10, 0) - c_doctypetarget_id Decimal? @db.Decimal(10, 0) - c_dunninglevel_id Decimal? @db.Decimal(10, 0) - chargeamt Decimal? @db.Decimal - c_payment_id Decimal? @db.Decimal(10, 0) - created DateTime? @db.Timestamp(6) - createdby Decimal? @db.Decimal(10, 0) - dateordered DateTime? @db.Timestamp(6) - dateprinted DateTime? @db.Timestamp(6) - description String? @db.VarChar(255) - docaction String? @db.Char(2) - docstatus String? @db.Char(2) - dunninggrace DateTime? @db.Date - generateto String? @db.Char(1) - isactive String? @db.Char(1) - isapproved String? @db.Char(1) - isdiscountprinted String? @db.Char(1) - isindispute String? @db.Char(1) - ispaid String? @db.Char(1) - isprinted String? @db.Char(1) - isselfservice String? @db.Char(1) - istaxincluded String? @db.Char(1) - istransferred String? @db.Char(1) - m_pricelist_id Decimal? @db.Decimal(10, 0) - m_rma_id Decimal? @db.Decimal(10, 0) - paymentrule String? @db.Char(1) - poreference String? @db.VarChar(50) - posted String? @db.Char(1) - processedon Decimal? @db.Decimal - processing String? @db.Char(1) - ref_invoice_id Decimal? @db.Decimal(10, 0) - reversal_id Decimal? @db.Decimal(10, 0) - salesrep_id Decimal? @db.Decimal(10, 0) - sendemail String? @db.Char(1) - totallines Decimal? @db.Decimal - updated DateTime? @db.Timestamp(6) - updatedby Decimal? @db.Decimal(10, 0) - user1_id Decimal? @db.Decimal(10, 0) - user2_id Decimal? @db.Decimal(10, 0) - kodebp String? @db.VarChar(40) - noorder String? @db.VarChar(30) - orderdesc String? @db.VarChar(255) - db_id String? @db.Uuid - id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid - is_pay String? @default("N") @db.Char(1) + discountdate DateTime? @db.Timestamp(6) + discountamt Decimal? @db.Decimal + grandtotal Decimal? @db.Decimal + paidamt Decimal? @db.Decimal + openamt Decimal? @db.Decimal + c_currency_id Decimal? @db.Decimal(10, 0) + c_conversiontype_id Decimal? @db.Decimal(10, 0) + c_paymentterm_id Decimal? @db.Decimal(10, 0) + ispayschedulevalid String? @db.Char(1) + c_invoicepayschedule_id Decimal? @db.Decimal + invoicecollectiontype String? @db.Char(1) + c_campaign_id Decimal? @db.Decimal(10, 0) + c_project_id Decimal? @db.Decimal(10, 0) + c_activity_id Decimal? @db.Decimal(10, 0) + ad_orgtrx_id Decimal? @db.Decimal(10, 0) + ad_user_id Decimal? @db.Decimal(10, 0) + c_bpartner_location_id Decimal? @db.Decimal(10, 0) + c_charge_id Decimal? @db.Decimal(10, 0) + c_doctype_id Decimal? @db.Decimal(10, 0) + c_doctypetarget_id Decimal? @db.Decimal(10, 0) + c_dunninglevel_id Decimal? @db.Decimal(10, 0) + chargeamt Decimal? @db.Decimal + c_payment_id Decimal? @db.Decimal(10, 0) + created DateTime? @db.Timestamp(6) + createdby Decimal? @db.Decimal(10, 0) + dateordered DateTime? @db.Timestamp(6) + dateprinted DateTime? @db.Timestamp(6) + description String? @db.VarChar(255) + docaction String? @db.Char(2) + docstatus String? @db.Char(2) + dunninggrace DateTime? @db.Date + generateto String? @db.Char(1) + isactive String? @db.Char(1) + isapproved String? @db.Char(1) + isdiscountprinted String? @db.Char(1) + isindispute String? @db.Char(1) + ispaid String? @db.Char(1) + isprinted String? @db.Char(1) + isselfservice String? @db.Char(1) + istaxincluded String? @db.Char(1) + istransferred String? @db.Char(1) + m_pricelist_id Decimal? @db.Decimal(10, 0) + m_rma_id Decimal? @db.Decimal(10, 0) + paymentrule String? @db.Char(1) + poreference String? @db.VarChar(50) + posted String? @db.Char(1) + processedon Decimal? @db.Decimal + processing String? @db.Char(1) + ref_invoice_id Decimal? @db.Decimal(10, 0) + reversal_id Decimal? @db.Decimal(10, 0) + salesrep_id Decimal? @db.Decimal(10, 0) + sendemail String? @db.Char(1) + totallines Decimal? @db.Decimal + updated DateTime? @db.Timestamp(6) + updatedby Decimal? @db.Decimal(10, 0) + user1_id Decimal? @db.Decimal(10, 0) + user2_id Decimal? @db.Decimal(10, 0) + kodebp String? @db.VarChar(40) + noorder String? @db.VarChar(30) + orderdesc String? @db.VarChar(255) + db_id String? @db.Uuid + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + is_pay String? @default("N") @db.Char(1) + invoicePartners invoicePartner[] } model transactions { @@ -334,6 +335,23 @@ model bank_code { banks banks[] } +model invoicePartner { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + partnerServiceId String @db.VarChar(50) + c_bpartner_id Decimal? @db.Decimal(10, 0) + db_id String? @db.Uuid + c_invoice_id Decimal? @db.Decimal(10, 0) + invoiceAmount Decimal? @db.Decimal(20, 2) + payedAmount Decimal? @db.Decimal(20, 2) + amount Decimal? @db.Decimal(20, 2) + is_active Boolean @default(true) + is_pay Boolean @default(false) + rv_openitem_id String? @db.Uuid + created_at DateTime @default(now()) @db.Timestamptz(6) + updated_at DateTime @default(now()) @db.Timestamptz(6) + rv_openitem rv_openitem? @relation(fields: [rv_openitem_id], references: [id], onDelete: NoAction, onUpdate: NoAction) +} + model invoice { id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid user_id String? @db.Uuid diff --git a/src/controllers/signController.ts b/src/controllers/signController.ts index 9843e36..514e4eb 100644 --- a/src/controllers/signController.ts +++ b/src/controllers/signController.ts @@ -141,6 +141,7 @@ export async function createSignature({ throw new Error("No private key file associated with the user"); } const dir = path.join(__dirname, "..", "..", user.private_key_file); + console.log(dir); if (!fs.existsSync(dir)) { throw new Error(`Private key file not found`); } diff --git a/src/controllers/syncController.ts b/src/controllers/syncController.ts index 712d59f..176a161 100644 --- a/src/controllers/syncController.ts +++ b/src/controllers/syncController.ts @@ -84,6 +84,17 @@ function mapRowToRvOpenitem(row: any) { // Function to process a single database sync async function processDatabaseSync(database: any) { + if (!database.db_id) { + await db.activity_logs.create({ + data: { + action: "sync_rv_openitem", + status: "error", + message: `Database ${database.name} has no db_id, skipping sync`, + extra_json: database, + }, + }); + return { inserted: 0, updated: 0 }; // Exit the function early if db_id is missing + } await db.activity_logs.create({ data: { action: "sync_rv_openitem", @@ -118,6 +129,13 @@ async function processDatabaseSync(database: any) { const batchSize = 200; // Process in batches to avoid memory issues + + const existingRecords = await db.rv_openitem.findMany({ + where: { + db_id: database.db_id, + }, + select: { id: true, c_invoice_id: true }, + }); for (let i = 0; i < rows.length; i += batchSize) { const batch = rows.slice(i, i + batchSize); const ops = batch.map((r: any) => mapRowToRvOpenitem(r)); @@ -133,12 +151,6 @@ async function processDatabaseSync(database: any) { ); dbInserted += ops.length; } else { - const existingRecords = await db.rv_openitem.findMany({ - where: { - db_id: database.db_id, - }, - select: { id: true, c_invoice_id: true }, - }); await db.activity_logs.create({ data: { action: "sync_rv_openitem", diff --git a/src/lib/bank/mandiri/getInvoiceVirtualAccount.ts b/src/lib/bank/mandiri/getInvoiceVirtualAccount.ts index 961c963..a8d39b3 100644 --- a/src/lib/bank/mandiri/getInvoiceVirtualAccount.ts +++ b/src/lib/bank/mandiri/getInvoiceVirtualAccount.ts @@ -4,6 +4,8 @@ import { get } from "lodash"; import db from "../../../db"; import lo from "../../lodash"; import Decimal from "decimal.js"; +import invoicePartner from "./invoicePartner"; +import { formatDate, formatDateIndonesia } from "../../formatTimestamp"; const twoDigits = (n: number) => String(n).padStart(2, "0"); export const formatMoney = (v: any, decimal?: boolean) => { const d = new Decimal(v ?? 0); @@ -54,16 +56,31 @@ export default async function ({ message: "Try Get Outstanding Amount", }, }); - const grandtotal = await db.rv_openitem.aggregate({ - _sum: { - grandtotal: true, - }, + + const rv = await db.rv_openitem.findMany({ where: { c_bpartner_id: c_bpartner.c_bpartner_id, db_id: user.database.db_id, is_pay: "N", }, }); + console.log({ + c_bpartner_id: c_bpartner.c_bpartner_id, + db_id: user.database.db_id, + is_pay: "N", + }); + const invoiceBPartner = await invoicePartner({ + data: { + partnerServiceId: customerNo, + invoices: rv, + db_id: user.database.db_id, + }, + }); + const grandtotal = { + _sum: { + grandtotal: invoiceBPartner?.totalAmount, + }, + }; await db.activity_logs.create({ data: { @@ -73,16 +90,6 @@ export default async function ({ "Outstanding Amount Found: " + formatMoney(grandtotal._sum.grandtotal), }, }); - // console.log({ - // _sum: { - // grandtotal: true, - // }, - // where: { - // c_bpartner_id: c_bpartner.c_bpartner_id, - // db_id: user.database.db_id, - // is_pay: "N", - // }, - // }); if (lo.get(grandtotal, "_sum.grandtotal") === null) { throw new Error( "No outstanding amount found for the given Virtual Account No" @@ -127,7 +134,35 @@ export default async function ({ if (!invoice) throw new Error("Failed to create invoice record"); const transactions: any[] = []; const billDetails: any[] = []; - lines.map((item, index) => { + const deskripsi: any[] = []; + const keterangan: any[] = []; + const jatuhTempo: any[] = []; + const status: any[] = []; + const refTransaksi: any[] = []; + const linesInvoice = invoiceBPartner?.invoice || []; + linesInvoice.map((inv, index) => { + const item = get(inv, "rv_openitem"); + keterangan.push({ + english: get(item, "description"), + indonesia: get(item, "description"), + }); + deskripsi.push({ + english: `Invoice ${get(item, "documentno")}`, + indonesia: `Faktur ${get(item, "documentno")}`, + }); + jatuhTempo.push({ + english: formatDate(), + indonesia: formatDateIndonesia(), + }); + status.push({ + english: "Unpaid", + indonesia: "Belum Lunas", + }); + refTransaksi.push({ + english: get(item, "documentno"), + indonesia: get(item, "documentno"), + }); + billDetails.push({ billCode: twoDigits(index + 1), billName: get(item, "documentno"), @@ -144,7 +179,7 @@ export default async function ({ billcode: twoDigits(index + 1), line_no: index + 1, billname: get(item, "documentno"), - amount: lo.get(item, "grandtotal", 0), + amount: lo.get(item, "amount", 0), c_invoice_id: get(item, "c_invoice_id"), db_id: lo.get(user, "database.db_id"), bank_id: get(user, "bank_id"), @@ -182,5 +217,10 @@ export default async function ({ value: "0.00", currency: "IDR", }, + deskripsi, + keterangan, + jatuhTempo, + status, + refTransaksi, }; } diff --git a/src/lib/bank/mandiri/invoicePartner.ts b/src/lib/bank/mandiri/invoicePartner.ts new file mode 100644 index 0000000..5cf5d7b --- /dev/null +++ b/src/lib/bank/mandiri/invoicePartner.ts @@ -0,0 +1,74 @@ +import db from "../../../db"; + +export default async function ({ data }: { data: any }) { + const partnerServiceId = data.partnerServiceId; + const db_id = data.db_id; + const invoices: any[] = data.invoices || []; + const invoiceExists = await db.invoicePartner.findMany({ + where: { + partnerServiceId, + db_id, + is_pay: false, + }, + }); + + const removeSpace = (string: string) => { + return string + .replace( + /[\s\u00A0\u1680\u2000-\u200F\u2028\u2029\u202F\u205F\u3000\uFEFF]/g, + " " + ) + .replace(/\s+/g, " ") + .trim(); + }; + const invoicePartnerCreate = invoices.filter((invoice: any) => { + return !invoiceExists.find( + (inv) => + removeSpace(JSON.stringify(inv.c_invoice_id)) === + removeSpace(JSON.stringify(invoice.c_invoice_id)) + ); + }); + const transactions = [] as any[]; + if (invoicePartnerCreate.length > 0) + for (const inv of invoicePartnerCreate) { + transactions.push( + db.invoicePartner.create({ + data: { + partnerServiceId, + db_id, + c_bpartner_id: inv.c_bpartner_id, + c_invoice_id: inv.c_invoice_id, + amount: Number(inv.grandtotal) || 0, + rv_openitem_id: inv.id, + }, + }) + ); + } + if (transactions.length > 0) { + await db.$transaction(transactions); + } + const sumAmount = await db.invoicePartner.aggregate({ + _sum: { + amount: true, + }, + where: { + db_id, + partnerServiceId, + is_pay: false, + }, + }); + const invoice = await db.invoicePartner.findMany({ + where: { + db_id, + partnerServiceId, + is_pay: false, + }, + include: { + rv_openitem: true, + }, + }); + return { + invoice, + totalAmount: sumAmount._sum.amount || 0, + }; +} diff --git a/src/lib/bank/mandiri/invoicePartnerPayment.ts b/src/lib/bank/mandiri/invoicePartnerPayment.ts new file mode 100644 index 0000000..584f5cd --- /dev/null +++ b/src/lib/bank/mandiri/invoicePartnerPayment.ts @@ -0,0 +1,61 @@ +import db from "../../../db"; + +export default async function ({ data }: { data: any }) { + const partnerServiceId = data.partnerServiceId; + let paymentAmount = data.paymentAmount; + const db_id = data.db_id; + const invoices: any[] = data.invoices || []; + const invoiceExists = await db.invoicePartner.findMany({ + where: { + partnerServiceId, + db_id, + is_pay: false, + amount: { + gt: 0, + }, + }, + }); + const transactions = [] as any[]; + if (paymentAmount > 0) { + invoiceExists.map((inv) => { + if (paymentAmount > 0) { + let sisaInvoice = Number(inv.amount) || 0; + sisaInvoice = + paymentAmount > sisaInvoice ? 0 : sisaInvoice - paymentAmount; + const amountInvoice = Number(inv.amount) || 0; + transactions.push( + db.invoicePartner.update({ + where: { + id: inv.id, + }, + data: { + amount: sisaInvoice < 0 ? 0 : sisaInvoice, + }, + }) + ); + if (paymentAmount >= amountInvoice) { + paymentAmount -= amountInvoice; + } else { + paymentAmount = 0; + } + } + }); + } + await db.$transaction(transactions); + const sumAmount = await db.invoicePartner.aggregate({ + _sum: { amount: true }, + }); + if (!sumAmount._sum.amount) { + await db.invoicePartner.updateMany({ + where: { + partnerServiceId, + }, + data: { + is_pay: true, + }, + }); + } + return { + totalUnpaid: sumAmount._sum.amount || 0, + }; +} diff --git a/src/lib/bank/mandiri/verifySignatureV2.ts b/src/lib/bank/mandiri/verifySignatureV2.ts index 08ddc7a..43f7bd6 100644 --- a/src/lib/bank/mandiri/verifySignatureV2.ts +++ b/src/lib/bank/mandiri/verifySignatureV2.ts @@ -59,6 +59,7 @@ export default async function ({ data }: { data: any }) { throw new Error("No public key file associated with the user"); } const fullPath = path.join(__dirname, "..", "..", "..", "..", publicKeyFile); + console.log(fullPath); if (!fs.existsSync(fullPath)) { throw new Error("Public key file not found"); } diff --git a/src/lib/formatTimestamp.ts b/src/lib/formatTimestamp.ts index bda1b42..b815719 100644 --- a/src/lib/formatTimestamp.ts +++ b/src/lib/formatTimestamp.ts @@ -1,3 +1,5 @@ +import dayjs from "dayjs"; + export function formatTimestamp(d = new Date()): string { const yyyy = d.getFullYear(); const mm = String(d.getMonth() + 1).padStart(2, "0"); @@ -12,3 +14,27 @@ export function formatTimestamp(d = new Date()): string { const om = String(abs % 60).padStart(2, "0"); return `${yyyy}-${mm}-${dd}T${hh}:${min}:${ss}${sign}${oh}:${om}`; } + +export const formatDateIndonesia = (d = new Date()) => { + // 15 November 2025 + const bulanIndo = [ + "Januari", + "Februari", + "Maret", + "April", + "Mei", + "Juni", + "Juli", + "Agustus", + "September", + "Oktober", + "November", + "Desember", + ]; + return `${d.getDate()} ${bulanIndo[d.getMonth()]} ${d.getFullYear()}`; +}; + +export const formatDate = (d = new Date(), format = "DD MMMM YYYY") => { + // YYYY-MMM-DD + return dayjs(d).format(format); +};