feat: add dayjs for date formatting and implement invoice partner functionality
Deploy Application / deploy (push) Successful in 39s Details

This commit is contained in:
faisolavolut 2025-11-24 15:33:53 +07:00
parent d7031052ea
commit faa30f3d7f
10 changed files with 337 additions and 96 deletions

7
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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

View File

@ -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`);
}

View File

@ -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",

View File

@ -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,
};
}

View File

@ -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,
};
}

View File

@ -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,
};
}

View File

@ -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");
}

View File

@ -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);
};