fix: standardize invoice partner naming and enhance sequence generation logic
Deploy Application / deploy (push) Successful in 32s Details

This commit is contained in:
faisolavolut 2025-11-25 14:28:23 +07:00
parent faa30f3d7f
commit e4a60acc58
9 changed files with 309 additions and 166 deletions

View File

@ -262,7 +262,7 @@ model rv_openitem {
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[]
invoicepartners invoicepartner[]
}
model transactions {
@ -309,20 +309,23 @@ model transactions {
}
model transactions_lines {
transaction_id String @db.Uuid
description String? @db.VarChar(255)
amount Decimal? @db.Decimal(20, 2)
db_id String? @db.Uuid
c_invoice_id Decimal? @db.Decimal(10, 0)
created_at DateTime? @default(now()) @db.Timestamptz(6)
updated_at DateTime? @default(now()) @db.Timestamptz(6)
tenant_id String @db.Uuid
line_no Int @default(1)
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
c_payment_id Decimal? @db.Decimal(10, 0)
documentno String? @db.VarChar(255)
database database? @relation(fields: [db_id], references: [db_id], onDelete: NoAction, onUpdate: NoAction)
transactions transactions @relation(fields: [transaction_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
transaction_id String @db.Uuid
description String? @db.VarChar(255)
amount Decimal? @db.Decimal(20, 2)
db_id String? @db.Uuid
c_invoice_id Decimal? @db.Decimal(10, 0)
created_at DateTime? @default(now()) @db.Timestamptz(6)
updated_at DateTime? @default(now()) @db.Timestamptz(6)
tenant_id String @db.Uuid
line_no Int @default(1)
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
c_payment_id Decimal? @db.Decimal(10, 0)
date DateTime? @db.Timestamptz(6)
invoicepartner_id String? @db.Uuid
documentno String? @db.VarChar(255)
invoicepartner invoicepartner? @relation(fields: [invoicepartner_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
database database? @relation(fields: [db_id], references: [db_id], onDelete: NoAction, onUpdate: NoAction)
transactions transactions @relation(fields: [transaction_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
}
model bank_code {
@ -335,21 +338,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 invoicepartner {
id String @id(map: "invoicePartner_pkey") @default(dbgenerated("gen_random_uuid()")) @db.Uuid
costumerno String @db.VarChar(50)
c_bpartner_id Decimal? @db.Decimal(10, 0)
documentno String? @db.VarChar(30)
db_id String? @db.Uuid
c_invoice_id Decimal? @db.Decimal(10, 0)
is_active Boolean @default(true)
created_at DateTime @default(now()) @db.Timestamptz(6)
updated_at DateTime @default(now()) @db.Timestamptz(6)
is_pay Boolean @default(false)
grandtotal Decimal? @db.Decimal(20, 2)
amount Decimal? @db.Decimal(20, 2)
rv_openitem_id String? @db.Uuid
rv_openitem rv_openitem? @relation(fields: [rv_openitem_id], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "invoicePartner_rv_openitem_id_fkey")
transactionsLines transactions_lines[]
invoiceLines invoice_lines[]
}
model invoice {
@ -381,21 +386,23 @@ model invoice {
}
model invoice_lines {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
description String? @db.VarChar(255)
amount Decimal? @db.Decimal(20, 2)
c_invoice_id Decimal? @db.Decimal(10, 0)
invoice_id String? @db.Uuid
db_id String? @db.Uuid
created_at DateTime? @default(now()) @db.Timestamptz(6)
updated_at DateTime? @default(now()) @db.Timestamptz(6)
bank_id String @db.Uuid
tenant_id String @db.Uuid
line_no Int @default(1)
billcode String? @db.VarChar(10)
billname String? @db.VarChar(50)
database database? @relation(fields: [db_id], references: [db_id], onDelete: NoAction, onUpdate: NoAction)
invoice invoice? @relation(fields: [invoice_id], references: [id])
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
description String? @db.VarChar(255)
amount Decimal? @db.Decimal(20, 2)
c_invoice_id Decimal? @db.Decimal(10, 0)
invoicepartner_id String? @db.Uuid
invoice_id String? @db.Uuid
db_id String? @db.Uuid
created_at DateTime? @default(now()) @db.Timestamptz(6)
updated_at DateTime? @default(now()) @db.Timestamptz(6)
bank_id String @db.Uuid
tenant_id String @db.Uuid
line_no Int @default(1)
billcode String? @db.VarChar(10)
billname String? @db.VarChar(50)
invoicepartner invoicepartner? @relation(fields: [invoicepartner_id], references: [id])
database database? @relation(fields: [db_id], references: [db_id], onDelete: NoAction, onUpdate: NoAction)
invoice invoice? @relation(fields: [invoice_id], references: [id])
}
model sequences {

View File

@ -202,7 +202,7 @@ export async function MidsuitPayment(req: Request, res: Response) {
tenant_id: user.tenant_id,
bank_id: get(bankTo, "bank_id") as any,
bankto_id: get(bankFrom, "bank_id") as any,
external_id: external_id,
external_id: getParameter(user, "external_id"),
partnerReferenceNo: partnerReferenceNo,
amount: get(data, "amount"),
description: `Payment for ${get(data, "documentno")}`,

View File

@ -159,10 +159,9 @@ export default async function ({
indonesia: "Belum Lunas",
});
refTransaksi.push({
english: get(item, "documentno"),
indonesia: get(item, "documentno"),
english: get(inv, "documentno"),
indonesia: get(inv, "documentno"),
});
billDetails.push({
billCode: twoDigits(index + 1),
billName: get(item, "documentno"),
@ -174,6 +173,7 @@ export default async function ({
transactions.push(
db.invoice_lines.create({
data: {
invoicepartner_id: inv.id,
invoice_id: invoice.id,
description: get(item, "description"),
billcode: twoDigits(index + 1),
@ -221,6 +221,6 @@ export default async function ({
keterangan,
jatuhTempo,
status,
refTransaksi,
// refTransaksi,
};
}

View File

@ -1,12 +1,13 @@
import db from "../../../db";
import sequence from "../../sequence";
export default async function ({ data }: { data: any }) {
const partnerServiceId = data.partnerServiceId;
const costumerno = data.partnerServiceId;
const db_id = data.db_id;
const invoices: any[] = data.invoices || [];
const invoiceExists = await db.invoicePartner.findMany({
const invoiceExists = await db.invoicepartner.findMany({
where: {
partnerServiceId,
costumerno,
db_id,
is_pay: false,
},
@ -29,16 +30,26 @@ export default async function ({ data }: { data: any }) {
);
});
const transactions = [] as any[];
const documentno = await sequence(
"invoicePartner",
10,
invoicePartnerCreate.length
);
let idx = 0;
if (invoicePartnerCreate.length > 0)
for (const inv of invoicePartnerCreate) {
const docno = documentno[idx];
idx++;
transactions.push(
db.invoicePartner.create({
db.invoicepartner.create({
data: {
partnerServiceId,
costumerno,
documentno: docno,
db_id,
c_bpartner_id: inv.c_bpartner_id,
c_invoice_id: inv.c_invoice_id,
amount: Number(inv.grandtotal) || 0,
grandtotal: Number(inv.grandtotal) || 0,
rv_openitem_id: inv.id,
},
})
@ -47,20 +58,20 @@ export default async function ({ data }: { data: any }) {
if (transactions.length > 0) {
await db.$transaction(transactions);
}
const sumAmount = await db.invoicePartner.aggregate({
const sumAmount = await db.invoicepartner.aggregate({
_sum: {
amount: true,
},
where: {
db_id,
partnerServiceId,
costumerno,
is_pay: false,
},
});
const invoice = await db.invoicePartner.findMany({
const invoice = await db.invoicepartner.findMany({
where: {
db_id,
partnerServiceId,
costumerno,
is_pay: false,
},
include: {

View File

@ -1,30 +1,87 @@
import db from "../../../db";
import { formatMoney } from "./getInvoiceVirtualAccount";
export default async function ({ data }: { data: any }) {
const partnerServiceId = data.partnerServiceId;
const costumerno = data.costumerno;
let paymentAmount = data.paymentAmount;
const db_id = data.db_id;
const invoices: any[] = data.invoices || [];
const invoiceExists = await db.invoicePartner.findMany({
const invoiceExists = await db.invoicepartner.findMany({
where: {
partnerServiceId,
c_invoice_id: {
in: invoices.map((inv) => inv.c_invoice_id),
},
costumerno,
db_id,
is_pay: false,
amount: {
gt: 0,
},
},
include: {
rv_openitem: true,
},
});
const transactions = [] as any[];
const payment = [] as any[];
const unpaid = [] as any[];
if (paymentAmount > 0) {
const invoiceByAmount = await db.invoicepartner.findFirst({
where: {
c_invoice_id: {
in: invoices.map((inv) => inv.c_invoice_id),
},
amount: paymentAmount,
is_pay: false,
costumerno,
db_id,
AND: [
{
amount: {
gt: 0,
},
},
],
},
});
invoiceExists.map((inv) => {
if (paymentAmount > 0) {
if (invoiceByAmount) {
if (invoiceByAmount.id !== inv.id) {
payment.push({
id: inv.id,
c_invoice_id: inv.c_invoice_id,
costumerno,
c_bpartner_id: inv.c_bpartner_id,
amount: 0,
remainingAmount: formatMoney(inv.amount),
grandtotal: formatMoney(inv.amount),
rv_openitem: inv.rv_openitem,
status: "unpaid",
});
return;
}
}
let sisaInvoice = Number(inv.amount) || 0;
sisaInvoice =
paymentAmount > sisaInvoice ? 0 : sisaInvoice - paymentAmount;
const amountInvoice = Number(inv.amount) || 0;
payment.push({
id: inv.id,
c_invoice_id: inv.c_invoice_id,
costumerno,
c_bpartner_id: inv.c_bpartner_id,
amount:
sisaInvoice <= 0
? formatMoney(paymentAmount)
: formatMoney(amountInvoice - sisaInvoice),
remainingAmount: formatMoney(sisaInvoice),
grandtotal: formatMoney(inv.amount),
status: sisaInvoice <= 0 ? "paid" : "unpaid",
rv_openitem: inv.rv_openitem,
});
transactions.push(
db.invoicePartner.update({
db.invoicepartner.update({
where: {
id: inv.id,
},
@ -33,29 +90,41 @@ export default async function ({ data }: { data: any }) {
},
})
);
if (sisaInvoice <= 0) {
transactions.push(
db.rv_openitem.updateMany({
where: {
c_invoice_id: inv.c_invoice_id,
db_id,
},
data: { is_pay: "Y" },
})
);
}
if (paymentAmount >= amountInvoice) {
paymentAmount -= amountInvoice;
} else {
paymentAmount = 0;
}
} else {
payment.push({
id: inv.id,
c_invoice_id: inv.c_invoice_id,
costumerno,
c_bpartner_id: inv.c_bpartner_id,
amount: 0,
remainingAmount: formatMoney(inv.amount),
grandtotal: formatMoney(inv.amount),
status: "unpaid",
rv_openitem: inv.rv_openitem,
});
}
});
}
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,
transactions,
payment,
paid: payment.filter((p) => p.amount > 0),
unpaid: payment.filter((p) => p.amount === 0),
};
}

View File

@ -4,6 +4,8 @@ import db from "../../../db";
import lo from "../../lodash";
import Decimal from "decimal.js";
import completeTransaction from "../../completeTransaction";
import invoicePartnerPayment from "./invoicePartnerPayment";
import sequence from "../../sequence";
const twoDigits = (n: number) => String(n).padStart(2, "0");
export const formatMoney = (v: any) => {
const d = new Decimal(v ?? 0);
@ -32,59 +34,91 @@ export default async function ({
}
console.log("invoice.amount:", invoice.amount);
console.log("paidAmount.value:", get(data, "paidAmount.value"));
if (formatMoney(invoice.amount) !== get(data, "paidAmount.value")) {
if (
Number(get(data, "paidAmount.value")) <= 0 ||
Number(get(data, "paidAmount.value")) > Number(get(invoice, "amount", 0))
) {
throw new Error("Paid amount does not match invoice amount");
}
if (!user.database) throw new Error("User database information is missing");
const lines = get(invoice, "invoice_lines", []);
const paymentInvoicePartner = await invoicePartnerPayment({
data: {
costumerno: get(data, "customerNo"),
paymentAmount: Number(get(data, "paidAmount.value")),
db_id: get(user, "database.db_id"),
invoices: get(invoice, "invoice_lines", []),
},
});
const lines = get(paymentInvoicePartner, "paid", []);
const flagAdvise = get(data, "flagAdvise") === "Y" ? true : false;
if (!flagAdvise) {
const transaction = await db.transactions.create({
data: {
tenant_id: user.tenant_id,
bank_id: user.bank_id,
amount: invoice.amount as any,
user_id: user.user_id,
description: `Payment for Virtual Account ${invoice.inquiryRequestId}`,
status: "DRAFT",
c_bpartner_id: get(invoice, "c_bpartner_id"),
paymentRequestId: get(data, "paymentRequestId"),
paidBills: get(data, "paidBills"),
hashedSourceAccountNo: get(data, "hashedSourceAccountNo"),
flagAdvise: get(data, "flagAdvise"),
partnerServiceId: get(data, "partnerServiceId"),
customerNo: get(data, "customerNo"),
virtualAccountNo: get(data, "virtualAccountNo"),
referenceNo: get(data, "referenceNo"),
channelCode: get(data, "channelCode"),
db_id: get(user, "database.db_id"),
trxDateTime: get(data, "trxDateTime")
? new Date(get(data, "trxDateTime"))
: null,
},
const deskripsi: any[] = [];
const keterangan: any[] = [];
const status: any[] = [];
const refTransaksi: any[] = [];
const transaction = await db.transactions.create({
data: {
tenant_id: user.tenant_id,
bank_id: user.bank_id,
amount: get(data, "paidAmount.value") as any,
user_id: user.user_id,
description: `Payment for Virtual Account ${invoice.inquiryRequestId}`,
status: "DRAFT",
c_bpartner_id: get(invoice, "c_bpartner_id"),
paymentRequestId: get(data, "paymentRequestId"),
paidBills: get(data, "paidBills"),
hashedSourceAccountNo: get(data, "hashedSourceAccountNo"),
flagAdvise: get(data, "flagAdvise"),
partnerServiceId: get(data, "partnerServiceId"),
customerNo: get(data, "customerNo"),
virtualAccountNo: get(data, "virtualAccountNo"),
referenceNo: get(data, "referenceNo"),
channelCode: get(data, "channelCode"),
db_id: get(user, "database.db_id"),
trxDateTime: get(data, "trxDateTime")
? new Date(get(data, "trxDateTime"))
: null,
},
});
if (!transaction) throw new Error("Failed to create invoice record");
const documentnoLine = await sequence("transactionLines", 10, lines.length);
const transactions: any[] = [];
lines.map((item, index) => {
const inv = item.rv_openitem;
keterangan.push({
english: get(inv, "description"),
indonesia: get(inv, "description"),
});
if (!transaction) throw new Error("Failed to create invoice record");
const transactions: any[] = [];
lines.map((item, index) => {
transactions.push(
db.transactions_lines.create({
data: {
transaction_id: transaction.id,
description: get(item, "description"),
line_no: get(item, "line_no"),
amount: lo.get(item, "grandtotal", 0),
c_invoice_id: get(item, "c_invoice_id"),
db_id: lo.get(user, "database.db_id"),
tenant_id: get(user, "tenant_id"),
},
})
);
deskripsi.push({
english: `Invoice ${get(inv, "documentno")}`,
indonesia: `Faktur ${get(inv, "documentno")}`,
});
if (transactions.length > 0) await db.$transaction(transactions);
await completeTransaction(transaction.id);
}
status.push({
english: "Received",
indonesia: "Diterima",
});
refTransaksi.push({
english: documentnoLine[index],
indonesia: documentnoLine[index],
});
transactions.push(
db.transactions_lines.create({
data: {
transaction_id: transaction.id,
invoicepartner_id: get(item, "id"),
documentno: documentnoLine[index],
line_no: index + 1,
amount: lo.get(item, "amount", 0),
c_invoice_id: get(item, "c_invoice_id"),
db_id: lo.get(user, "database.db_id"),
tenant_id: get(user, "tenant_id"),
},
})
);
});
if (transactions.length > 0) await db.$transaction(transactions);
await completeTransaction(transaction.id);
await db.$transaction(get(paymentInvoicePartner, "transactions", []));
return {
paymentFlagStatus: "00",
@ -98,9 +132,13 @@ export default async function ({
virtualAccountNo: get(data, "virtualAccountNo"),
virtualAccountName: get(invoice, "virtualAccountName"),
totalAmount: {
value: formatMoney(invoice.amount),
value: formatMoney(get(data, "paidAmount.value")),
currency: "IDR",
},
trxDateTime: get(data, "trxDateTime"),
refTransaksi,
deskripsi,
keterangan,
status,
};
}

View File

@ -17,22 +17,22 @@ export default async function (transaction_id: string) {
data: { status: "COMPLETED" },
})
);
const results = await db.transactions_lines.findMany({
where: { transaction_id: transaction_id },
});
if (results.length > 0) {
results.map((item: any) => {
transactions.push(
db.rv_openitem.updateMany({
where: {
c_invoice_id: item.c_invoice_id,
db_id: item.db_id,
},
data: { is_pay: "Y" },
})
);
});
}
// const results = await db.transactions_lines.findMany({
// where: { transaction_id: transaction_id },
// });
// if (results.length > 0) {
// results.map((item: any) => {
// transactions.push(
// db.rv_openitem.updateMany({
// where: {
// c_invoice_id: item.c_invoice_id,
// db_id: item.db_id,
// },
// data: { is_pay: "Y" },
// })
// );
// });
// }
await db.$transaction(transactions);
return true;
}

View File

@ -5,42 +5,60 @@ import db from "../db";
// atomic increment (updateMany) and falling back to create. If create races
// with another process, we catch the unique-constraint error and retry a few
// times. This avoids unsafe find-then-create/update patterns.
export default async function (name: string, padStartLength = 10) {
export default async function (name: string, padStartLength = 10, count = 1) {
const yyyymmdd = new Date().toISOString().slice(0, 10).replace(/-/g, "");
const results: string[] = [];
// Try a few times to handle races when many processes call concurrently.
const maxAttempts = 5;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
// If a row already exists, this updateMany will atomically increment it.
const updateResult = await db.sequences.updateMany({
where: { name, yyyymmdd },
data: { value: { increment: 1 }, updated_at: new Date() },
});
if (updateResult.count && updateResult.count > 0) {
// Read back the new value and return it.
const sequence = await db.sequences.findFirst({
try {
// If a row already exists, this updateMany will atomically increment it by count
const updateResult = await db.sequences.updateMany({
where: { name, yyyymmdd },
data: { value: { increment: count }, updated_at: new Date() },
});
if (sequence) {
return `${yyyymmdd}${String(sequence.value).padStart(
if (updateResult.count && updateResult.count > 0) {
// Read back the new value and return it.
const sequence = await db.sequences.findFirst({
where: { name, yyyymmdd },
});
if (sequence) {
// Generate sequences for the requested count
for (let i = 0; i < count; i++) {
const currentValue = sequence.value - count + i + 1;
const sequenceString = `${yyyymmdd}${String(currentValue).padStart(
padStartLength,
"0"
)}`;
results.push(sequenceString);
}
// Return object for single sequence, array for multiple
return count === 1 ? results[0] : results;
}
// If for some reason the row disappeared, loop and retry.
continue;
}
// No existing row — try to create it with initial value equal to count
const created = await db.sequences.create({
data: { name, yyyymmdd, value: count },
});
// Generate sequences for the requested count starting from 1
for (let i = 0; i < count; i++) {
const currentValue = i + 1;
const sequenceString = `${yyyymmdd}${String(currentValue).padStart(
padStartLength,
"0"
)}`;
results.push(sequenceString);
}
// If for some reason the row disappeared, loop and retry.
continue;
}
// No existing row — try to create it with initial value 1.
try {
const created = await db.sequences.create({
data: { name, yyyymmdd, value: 1 },
});
return `${yyyymmdd}${String(created.value).padStart(
padStartLength,
"0"
)}`;
// Return object for single sequence, array for multiple
return count === 1 ? results[0] : results;
} catch (err: any) {
// Prisma unique-constraint error code P2002 indicates another process
// created the row in the meantime; retry the loop. Re-throw unexpected errors.

View File

@ -118,7 +118,7 @@ export const setRoutes = () => {
if (!seqName) {
return res.status(400).json({ error: "Sequence name is required" });
}
const seqValue = await sequence(seqName);
const seqValue = await sequence(seqName, 10, 2);
return res.json({ result: seqValue });
} catch (err) {
console.error("Get sequence error", err);