Files
AbelBirdNest-Stock/scripts/backfill-purchase-realization.mjs

235 lines
6.9 KiB
JavaScript

import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
const OPENING_EVENT_TYPES = new Set(["OPENING_COST"]);
const SALE_EVENT_TYPES = new Set(["SALE_REVENUE", "CONSIGNMENT_REVENUE"]);
const RETURN_EVENT_TYPES = new Set(["SALE_RETURN", "CONSIGNMENT_RETURN"]);
const COST_ADDITIONAL_EVENT_TYPES = new Set([
"WASHING_COST",
"WASHING_SHRINKAGE",
"TRANSFORMATION_SHRINKAGE",
"SALE_SHRINKAGE",
"CONSIGNMENT_SHRINKAGE",
"STOCK_ADJUSTMENT_LOSS",
"MANUAL_ADJUSTMENT"
]);
const roundQty = (value) => Number(value.toFixed(3));
const roundAmount = (value) => Number(value.toFixed(2));
async function recalcSummary(purchaseId, agentSharePercent) {
const entries = await prisma.purchaseRealizationEntry.findMany({
where: { purchaseId }
});
const aggregate = entries.reduce(
(state, entry) => {
const qtyIn = entry.qtyIn.toNumber();
const qtyOut = entry.qtyOut.toNumber();
const qtyShrinkage = entry.qtyShrinkage.toNumber();
const amountCost = entry.amountCost.toNumber();
const amountRevenue = entry.amountRevenue.toNumber();
const amountExpense = entry.amountExpense.toNumber();
const agentAmount = entry.agentAmount.toNumber();
state.qtyRemaining += qtyIn - qtyOut - qtyShrinkage;
state.profitLossTotal += amountRevenue - amountCost - amountExpense;
state.agentProfitTotal += agentAmount;
if (OPENING_EVENT_TYPES.has(entry.eventType)) {
state.qtyOpening += qtyIn;
state.costOpeningTotal += amountCost;
}
if (SALE_EVENT_TYPES.has(entry.eventType)) {
state.qtySold += qtyOut;
state.revenueTotal += amountRevenue;
}
if (RETURN_EVENT_TYPES.has(entry.eventType)) {
state.qtyReturned += qtyIn;
}
if (COST_ADDITIONAL_EVENT_TYPES.has(entry.eventType)) {
state.costAdditionalTotal += amountCost + amountExpense;
}
state.qtyShrinkage += qtyShrinkage;
return state;
},
{
qtyOpening: 0,
qtyRemaining: 0,
qtySold: 0,
qtyReturned: 0,
qtyShrinkage: 0,
costOpeningTotal: 0,
costAdditionalTotal: 0,
revenueTotal: 0,
profitLossTotal: 0,
agentProfitTotal: 0
}
);
const status =
aggregate.qtyOpening <= 0
? "OPEN"
: aggregate.qtyRemaining <= 0
? "READY_TO_CLOSE"
: aggregate.qtySold > 0 || aggregate.qtyShrinkage > 0
? "PARTIAL"
: "OPEN";
await prisma.purchaseRealizationSummary.upsert({
where: { purchaseId },
update: {
status,
qtyOpening: roundQty(aggregate.qtyOpening),
qtyRemaining: roundQty(aggregate.qtyRemaining),
qtySold: roundQty(aggregate.qtySold),
qtyReturned: roundQty(aggregate.qtyReturned),
qtyShrinkage: roundQty(aggregate.qtyShrinkage),
costOpeningTotal: roundAmount(aggregate.costOpeningTotal),
costAdditionalTotal: roundAmount(aggregate.costAdditionalTotal),
revenueTotal: roundAmount(aggregate.revenueTotal),
profitLossTotal: roundAmount(aggregate.profitLossTotal),
agentSharePercent,
agentProfitTotal: roundAmount(aggregate.agentProfitTotal),
closedAt: null
},
create: {
purchaseId,
status,
qtyOpening: roundQty(aggregate.qtyOpening),
qtyRemaining: roundQty(aggregate.qtyRemaining),
qtySold: roundQty(aggregate.qtySold),
qtyReturned: roundQty(aggregate.qtyReturned),
qtyShrinkage: roundQty(aggregate.qtyShrinkage),
costOpeningTotal: roundAmount(aggregate.costOpeningTotal),
costAdditionalTotal: roundAmount(aggregate.costAdditionalTotal),
revenueTotal: roundAmount(aggregate.revenueTotal),
profitLossTotal: roundAmount(aggregate.profitLossTotal),
agentSharePercent,
agentProfitTotal: roundAmount(aggregate.agentProfitTotal),
closedAt: null
}
});
}
async function main() {
const purchases = await prisma.purchase.findMany({
where: {
status: "SUBMITTED",
purchaseType: {
in: ["REGULAR", "OFFICE_BUYOUT"]
}
},
include: {
profitShareScheme: {
select: {
shareAgent: true
}
},
lots: true
},
orderBy: { id: "asc" }
});
let allocationsCreated = 0;
let entriesCreated = 0;
for (const purchase of purchases) {
for (const lot of purchase.lots) {
const allocationCount = await prisma.lotPurchaseAllocation.count({
where: {
lotId: lot.id,
purchaseId: purchase.id
}
});
if (allocationCount === 0) {
await prisma.lotPurchaseAllocation.create({
data: {
lotId: lot.id,
purchaseId: purchase.id,
purchaseLineId: lot.purchaseLineId,
sourceType: lot.sourceType,
sourceRefId: lot.sourceRefId,
agentIdSnapshot: purchase.agentId,
profitShareSchemeIdSnapshot: purchase.profitShareSchemeId,
qtyAllocated: lot.availableQty,
costTotalAllocated: roundAmount(lot.availableQty.toNumber() * lot.unitCost.toNumber()),
unitCostSnapshot: lot.unitCost,
notes: "Backfill opening allocation"
}
});
allocationsCreated += 1;
}
const openingCount = await prisma.purchaseRealizationEntry.count({
where: {
purchaseId: purchase.id,
lotId: lot.id,
eventType: "OPENING_COST"
}
});
if (openingCount === 0) {
const allocation = await prisma.lotPurchaseAllocation.findFirst({
where: {
lotId: lot.id,
purchaseId: purchase.id
},
orderBy: { id: "asc" }
});
await prisma.purchaseRealizationEntry.create({
data: {
purchaseId: purchase.id,
lotId: lot.id,
allocationId: allocation?.id ?? null,
eventType: "OPENING_COST",
referenceType: "PURCHASE",
referenceId: purchase.id,
occurredAt: lot.receivedAt,
qtyIn: lot.originalQty,
qtyOut: 0,
qtyShrinkage: 0,
amountCost: roundAmount(lot.originalQty.toNumber() * lot.unitCost.toNumber()),
amountRevenue: 0,
amountExpense: 0,
amountProfit: 0,
agentSharePercentSnapshot: purchase.profitShareScheme?.shareAgent ?? null,
agentAmount: 0,
notes: "Backfill opening realization"
}
});
entriesCreated += 1;
}
}
await recalcSummary(
purchase.id,
purchase.profitShareScheme?.shareAgent?.toNumber() ?? null
);
}
console.log(
JSON.stringify(
{
purchases: purchases.length,
allocations_created: allocationsCreated,
entries_created: entriesCreated
},
null,
2
)
);
}
main()
.catch((error) => {
console.error(error);
process.exitCode = 1;
})
.finally(async () => {
await prisma.$disconnect();
});