@classytic/ledger-bd 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1353 @@
1
+ //#region src/posting-recipes/inventory-transfer.recipes.ts
2
+ const ADJUSTMENT_LABELS = {
3
+ loss: "Inventory loss",
4
+ gain: "Inventory gain",
5
+ correction: "Inventory correction",
6
+ opening: "Opening stock"
7
+ };
8
+ function transitOf(i) {
9
+ return Math.max(0, Number(i.transitCost ?? 0));
10
+ }
11
+ function landedCostClearingSlot(costLineCode) {
12
+ const c = costLineCode.toUpperCase();
13
+ if (/DUTY|CUSTOM|TARIFF/.test(c)) return "customsDuty";
14
+ if (/CF|AGENT|FORWARD|CLEARANCE/.test(c)) return "cfAgentCommission";
15
+ if (/FREIGHT|CARRIAGE|TRANSPORT|SHIP|CARTAGE/.test(c)) return "carriageInward";
16
+ return "importLandedCost";
17
+ }
18
+ function landedCostGroups(i) {
19
+ const groups = /* @__PURE__ */ new Map();
20
+ for (const a of i.allocations) {
21
+ const slot = landedCostClearingSlot(a.costLineCode);
22
+ groups.set(slot, (groups.get(slot) ?? 0) + a.allocatedPaisa);
23
+ }
24
+ return [...groups.entries()].map(([slot, amount]) => ({
25
+ slot,
26
+ amount
27
+ }));
28
+ }
29
+ function buildInventoryTransferRecipes() {
30
+ return {
31
+ stockAdjustment: {
32
+ name: "bd.inventory.adjustment",
33
+ journalType: "INVENTORY",
34
+ idempotencyKey: (i) => `adj-${i.adjustmentId}`,
35
+ date: (i) => i.date,
36
+ label: (i) => {
37
+ if (i.description) return i.description;
38
+ const base = ADJUSTMENT_LABELS[i.type];
39
+ return i.referenceNumber ? `${base} — ${i.referenceNumber}` : base;
40
+ },
41
+ autoPost: false,
42
+ sourceRef: (i) => ({
43
+ sourceModel: i.sourceModel ?? "StockAdjustment",
44
+ sourceId: i.sourceId ?? i.adjustmentId
45
+ }),
46
+ legs: [{
47
+ id: "debit-side",
48
+ account: { route: (i) => i.type === "loss" ? "shrinkage" : "merchandise" },
49
+ side: "debit",
50
+ amount: (i) => i.amount,
51
+ when: () => true,
52
+ label: (i) => i.type === "loss" ? "Inventory shrinkage" : "Inventory adjustment (increase)"
53
+ }, {
54
+ id: "credit-side",
55
+ account: { route: (i) => i.type === "loss" ? "merchandise" : i.type === "opening" ? "retainedEarnings" : "inventoryGain" },
56
+ side: "credit",
57
+ amount: (i) => i.amount,
58
+ when: () => true,
59
+ label: (i) => i.type === "loss" ? "Inventory reduction" : i.type === "opening" ? "Opening stock (retained earnings)" : "Inventory gain"
60
+ }]
61
+ },
62
+ cogs: {
63
+ name: "bd.inventory.cogs",
64
+ journalType: "INVENTORY",
65
+ idempotencyKey: (i) => `cogs-${i.orderId}`,
66
+ date: (i) => i.date,
67
+ label: (i) => `COGS — order ${i.orderReferenceNumber ?? i.orderId}`,
68
+ autoPost: (i) => i.costAmount > 0,
69
+ sourceRef: (i) => ({
70
+ sourceModel: "Order",
71
+ sourceId: i.orderId
72
+ }),
73
+ metadata: (i) => i.metadata,
74
+ legs: [{
75
+ id: "cogs",
76
+ account: { slot: "cogsMaterials" },
77
+ side: "debit",
78
+ amount: (i) => i.costAmount,
79
+ when: () => true,
80
+ label: () => "Cost of goods sold"
81
+ }, {
82
+ id: "inventory",
83
+ account: { slot: "merchandise" },
84
+ side: "credit",
85
+ amount: (i) => i.costAmount,
86
+ when: () => true,
87
+ label: () => "Inventory consumed"
88
+ }]
89
+ },
90
+ cogsReversal: {
91
+ name: "bd.inventory.cogs-reversal",
92
+ journalType: "INVENTORY",
93
+ idempotencyKey: (i) => `cogs-reversal-${i.returnId}`,
94
+ date: (i) => i.date,
95
+ label: (i) => i.description || `COGS reversal — Return ${i.returnReferenceNumber ?? i.returnId}`,
96
+ autoPost: true,
97
+ sourceRef: (i) => ({
98
+ sourceModel: "Return",
99
+ sourceId: i.returnId
100
+ }),
101
+ metadata: (i) => i.metadata,
102
+ legs: [{
103
+ id: "inventory",
104
+ account: { slot: "merchandise" },
105
+ side: "debit",
106
+ amount: (i) => i.costAmount,
107
+ when: () => true,
108
+ label: () => "Inventory restored from return"
109
+ }, {
110
+ id: "cogs",
111
+ account: { slot: "cogsMaterials" },
112
+ side: "credit",
113
+ amount: (i) => i.costAmount,
114
+ when: () => true,
115
+ label: () => "COGS reversal"
116
+ }]
117
+ },
118
+ transferDispatch: {
119
+ name: "bd.transfer.dispatch",
120
+ journalType: "INVENTORY",
121
+ idempotencyKey: (i) => `transfer-${i.transferId}-dispatch`,
122
+ date: (i) => i.date,
123
+ label: (i) => `Transfer Dispatch — ${i.documentNumber}`,
124
+ autoPost: true,
125
+ organizationId: (i) => i.organizationId,
126
+ sourceRef: (i) => ({
127
+ sourceModel: "Transfer",
128
+ sourceId: i.transferId
129
+ }),
130
+ metadata: (i) => i.metadata,
131
+ legs: [{
132
+ id: "in-transit",
133
+ account: { slot: "inventoryInTransit" },
134
+ side: "debit",
135
+ amount: (i) => i.goodsCost,
136
+ when: () => true,
137
+ label: () => "Stock dispatched (in-transit)"
138
+ }, {
139
+ id: "inventory",
140
+ account: { slot: "merchandise" },
141
+ side: "credit",
142
+ amount: (i) => i.goodsCost,
143
+ when: () => true,
144
+ label: () => "Inventory reduction (sender)"
145
+ }]
146
+ },
147
+ transferReceive: {
148
+ name: "bd.transfer.receive",
149
+ journalType: "INVENTORY",
150
+ idempotencyKey: (i) => `transfer-${i.transferId}-receive`,
151
+ date: (i) => i.date,
152
+ label: (i) => `Transfer Receive — ${i.documentNumber}`,
153
+ autoPost: true,
154
+ organizationId: (i) => i.organizationId,
155
+ sourceRef: (i) => ({
156
+ sourceModel: "Transfer",
157
+ sourceId: i.transferId
158
+ }),
159
+ metadata: (i) => i.metadata,
160
+ legs: [
161
+ {
162
+ id: "inventory",
163
+ account: { slot: "merchandise" },
164
+ side: "debit",
165
+ amount: (i) => i.goodsCost + transitOf(i),
166
+ when: () => true,
167
+ label: (i) => transitOf(i) > 0 ? "Inventory addition (receiver, incl. capitalized transit cost)" : "Inventory addition (receiver)"
168
+ },
169
+ {
170
+ id: "in-transit",
171
+ account: { slot: "inventoryInTransit" },
172
+ side: "credit",
173
+ amount: (i) => i.goodsCost,
174
+ when: () => true,
175
+ label: () => "In-transit cleared"
176
+ },
177
+ {
178
+ id: "transit-cost",
179
+ account: { slot: "transferCostClearing" },
180
+ side: "credit",
181
+ amount: (i) => transitOf(i),
182
+ label: () => "Transit cost clearing (host clears against freight invoice)"
183
+ }
184
+ ]
185
+ },
186
+ transferDispatchReversal: {
187
+ name: "bd.transfer.dispatch-reversal",
188
+ journalType: "INVENTORY",
189
+ idempotencyKey: (i) => `transfer-${i.transferId}-dispatch-reversed`,
190
+ date: (i) => i.date,
191
+ label: (i) => `Transfer Dispatch REVERSED — ${i.documentNumber}${i.reason ? ` — ${i.reason}` : ""}`,
192
+ autoPost: true,
193
+ organizationId: (i) => i.organizationId,
194
+ sourceRef: (i) => ({
195
+ sourceModel: "Transfer",
196
+ sourceId: i.transferId
197
+ }),
198
+ metadata: (i) => i.metadata,
199
+ legs: [{
200
+ id: "inventory",
201
+ account: { slot: "merchandise" },
202
+ side: "debit",
203
+ amount: (i) => i.goodsCost,
204
+ when: () => true,
205
+ label: () => "Stock returned to sender (dispatch reversed)"
206
+ }, {
207
+ id: "in-transit",
208
+ account: { slot: "inventoryInTransit" },
209
+ side: "credit",
210
+ amount: (i) => i.goodsCost,
211
+ when: () => true,
212
+ label: () => "In-transit reversed"
213
+ }]
214
+ },
215
+ transferReceiveReversal: {
216
+ name: "bd.transfer.receive-reversal",
217
+ journalType: "INVENTORY",
218
+ idempotencyKey: (i) => `transfer-${i.transferId}-receive-reversed`,
219
+ date: (i) => i.date,
220
+ label: (i) => `Transfer Receive REVERSED — ${i.documentNumber}${i.reason ? ` — ${i.reason}` : ""}`,
221
+ autoPost: true,
222
+ organizationId: (i) => i.organizationId,
223
+ sourceRef: (i) => ({
224
+ sourceModel: "Transfer",
225
+ sourceId: i.transferId
226
+ }),
227
+ metadata: (i) => i.metadata,
228
+ legs: [
229
+ {
230
+ id: "in-transit",
231
+ account: { slot: "inventoryInTransit" },
232
+ side: "debit",
233
+ amount: (i) => i.goodsCost,
234
+ when: () => true,
235
+ label: () => "In-transit reasserted (receive reversed)"
236
+ },
237
+ {
238
+ id: "transit-cost",
239
+ account: { slot: "transferCostClearing" },
240
+ side: "debit",
241
+ amount: (i) => transitOf(i),
242
+ label: () => "Transit cost clearing reversed"
243
+ },
244
+ {
245
+ id: "inventory",
246
+ account: { slot: "merchandise" },
247
+ side: "credit",
248
+ amount: (i) => i.goodsCost + transitOf(i),
249
+ when: () => true,
250
+ label: () => "Stock removed from receiver (goods + transit)"
251
+ }
252
+ ]
253
+ },
254
+ landedCost: {
255
+ name: "bd.landed-cost",
256
+ journalType: "INVENTORY",
257
+ idempotencyKey: (i) => `landed-cost-${i.landedCostId}`,
258
+ date: (i) => i.date,
259
+ label: () => "Landed cost capitalized to inventory",
260
+ autoPost: true,
261
+ sourceRef: (i) => ({
262
+ sourceModel: "LandedCost",
263
+ sourceId: i.landedCostId
264
+ }),
265
+ legs: [{
266
+ id: "inventory",
267
+ account: { slot: "merchandise" },
268
+ side: "debit",
269
+ amount: (i) => i.allocations.reduce((s, a) => s + a.allocatedPaisa, 0),
270
+ label: () => "Landed cost capitalized to inventory"
271
+ }, {
272
+ id: "clearing",
273
+ account: { route: (g) => g.slot },
274
+ side: "credit",
275
+ amount: (g) => g.amount,
276
+ label: () => "Landed cost clearing",
277
+ expand: (i) => landedCostGroups(i)
278
+ }]
279
+ },
280
+ landedCostReversal: {
281
+ name: "bd.landed-cost.reversal",
282
+ journalType: "INVENTORY",
283
+ idempotencyKey: (i) => `landed-cost-${i.landedCostId}-reversal`,
284
+ date: (i) => i.date,
285
+ label: () => "Landed cost reversal — de-capitalize inventory",
286
+ autoPost: true,
287
+ sourceRef: (i) => ({
288
+ sourceModel: "LandedCost",
289
+ sourceId: i.landedCostId
290
+ }),
291
+ legs: [{
292
+ id: "clearing",
293
+ account: { route: (g) => g.slot },
294
+ side: "debit",
295
+ amount: (g) => g.amount,
296
+ label: () => "Landed cost clearing (reversal)",
297
+ expand: (i) => landedCostGroups(i)
298
+ }, {
299
+ id: "inventory",
300
+ account: { slot: "merchandise" },
301
+ side: "credit",
302
+ amount: (i) => i.allocations.reduce((s, a) => s + a.allocatedPaisa, 0),
303
+ label: () => "Landed cost reversal — de-capitalize inventory"
304
+ }]
305
+ }
306
+ };
307
+ }
308
+ //#endregion
309
+ //#region src/posting-recipes/resolvers.ts
310
+ /** Inventory-type → slot name (mirrors be-prod INVENTORY_ACCOUNTS keys). */
311
+ const INVENTORY_SLOT = {
312
+ raw_materials: "rawMaterials",
313
+ finished_goods: "finishedGoods",
314
+ merchandise: "merchandise",
315
+ packing: "packingMaterials",
316
+ default: "merchandise"
317
+ };
318
+ function inventorySlotFor(inventoryType) {
319
+ return INVENTORY_SLOT[inventoryType ?? "default"] ?? INVENTORY_SLOT.default;
320
+ }
321
+ /** `displayRef` parity: human ref when present, else the id. */
322
+ function displayRef(referenceNumber, id) {
323
+ return referenceNumber || id;
324
+ }
325
+ //#endregion
326
+ //#region src/posting-recipes/misc.recipes.ts
327
+ /** settlement.contract.ts balance invariant. */
328
+ function validateSettlement(i) {
329
+ if (i.totalNet + i.totalFee + i.totalWriteoff !== i.totalGross) throw new Error(`Settlement legs do not balance: net ${i.totalNet} + fee ${i.totalFee} + writeoff ${i.totalWriteoff} ≠ gross ${i.totalGross}`);
330
+ }
331
+ /** credit-debit-note.contract.ts fintech rules (positive int, reason, reference). */
332
+ function validateNoteInput(i) {
333
+ if (!Number.isInteger(i.amount) || i.amount <= 0) throw new Error(`Note amount must be a positive integer (paisa); got ${i.amount}`);
334
+ if (!i.reason || i.reason.trim().length < 3) throw new Error("Note reason is required (min 3 chars) — audit trail");
335
+ if (!i.reference || !i.reference.trim()) throw new Error("Note reference is required — drives idempotency");
336
+ }
337
+ function buildMiscRecipes(resolvers) {
338
+ return {
339
+ settlement: {
340
+ name: "bd.settlement",
341
+ journalType: "GATEWAY_SETTLEMENT",
342
+ idempotencyKey: (i) => `settlement-${i.importId}`,
343
+ date: (i) => i.date,
344
+ label: (i) => i.notes || `${i.provider} settlement — ${i.externalRef}`,
345
+ autoPost: false,
346
+ sourceRef: (i) => ({
347
+ sourceModel: "SettlementImport",
348
+ sourceId: i.importId
349
+ }),
350
+ legs: [
351
+ {
352
+ id: "bank",
353
+ account: { code: (i) => i.bankAccountCode },
354
+ side: "debit",
355
+ amount: (i) => i.totalNet,
356
+ label: (i) => `${i.provider} payout — ${i.externalRef}`
357
+ },
358
+ {
359
+ id: "fee",
360
+ account: { code: (i) => i.feeAccountCode },
361
+ side: "debit",
362
+ amount: (i) => i.totalFee,
363
+ label: (i) => `${i.provider} processing fee — ${i.externalRef}`
364
+ },
365
+ {
366
+ id: "writeoff",
367
+ account: { code: (i) => i.writeoffAccountCode },
368
+ side: "debit",
369
+ amount: (i) => i.totalWriteoff,
370
+ label: (i) => `${i.provider} write-off (short-pay / refused) — ${i.externalRef}`
371
+ },
372
+ {
373
+ id: "clearing",
374
+ account: { code: (i) => i.clearingAccountCode },
375
+ side: "credit",
376
+ amount: (i) => i.totalGross,
377
+ when: () => true,
378
+ label: (i) => `Clear ${i.provider} clearing — ${i.externalRef}`
379
+ }
380
+ ]
381
+ },
382
+ customerInvoice: {
383
+ name: "bd.customer-invoice",
384
+ journalType: "SALES",
385
+ idempotencyKey: (i) => `customer-invoice-${i.orderId}`,
386
+ date: (i) => i.issuedAt,
387
+ label: (i) => `Invoice ${displayRef(i.invoiceNumber, i.orderId)}`,
388
+ autoPost: false,
389
+ sourceRef: (i) => ({
390
+ sourceModel: "Order",
391
+ sourceId: i.orderId
392
+ }),
393
+ legs: [
394
+ {
395
+ id: "ar",
396
+ account: { slot: "ar" },
397
+ side: "debit",
398
+ amount: (i) => {
399
+ const vds = resolvers.resolveVatWithheldAtSource(i.vatAmount ?? 0, {
400
+ applicable: i.withholdVds,
401
+ rate: i.vdsRate
402
+ });
403
+ return i.totalAmount - vds;
404
+ },
405
+ when: () => true,
406
+ label: (i) => {
407
+ return resolvers.resolveVatWithheldAtSource(i.vatAmount ?? 0, {
408
+ applicable: i.withholdVds,
409
+ rate: i.vdsRate
410
+ }) > 0 ? "A/R from customer (net of VDS withheld)" : "A/R from customer";
411
+ },
412
+ partner: (i) => ({
413
+ partnerId: i.customerId,
414
+ partnerType: "customer"
415
+ }),
416
+ maturityDate: (i) => i.dueDate
417
+ },
418
+ {
419
+ id: "vds-receivable",
420
+ account: { slot: "vdsReceivable" },
421
+ side: "debit",
422
+ amount: (i) => resolvers.resolveVatWithheldAtSource(i.vatAmount ?? 0, {
423
+ applicable: i.withholdVds,
424
+ rate: i.vdsRate
425
+ }),
426
+ label: (i) => `VDS Receivable — buyer withheld ${(i.vdsRate ?? .5) * 100}% of output VAT`
427
+ },
428
+ {
429
+ id: "revenue",
430
+ account: { slot: "revenue" },
431
+ side: "credit",
432
+ amount: (i) => i.totalAmount - (i.vatAmount ?? 0),
433
+ when: () => true,
434
+ label: (i) => (i.vatAmount ?? 0) > 0 ? "Sales revenue (net of output VAT)" : "Sales revenue"
435
+ },
436
+ {
437
+ id: "vat",
438
+ account: { slot: "taxPayable" },
439
+ side: "credit",
440
+ amount: (i) => i.vatAmount ?? 0,
441
+ label: () => "Output VAT collected (payable to NBR)"
442
+ }
443
+ ]
444
+ },
445
+ customerReceipt: {
446
+ name: "bd.customer-receipt",
447
+ journalType: "CASH_RECEIPTS",
448
+ idempotencyKey: (i) => `customer-receipt-${i.orderId}-${i.date.getTime()}-${i.amount}`,
449
+ date: (i) => i.date,
450
+ label: (i) => i.reference ? `Receipt ${i.reference}` : `Payment received — ${i.customerName ?? i.customerId}`,
451
+ autoPost: true,
452
+ sourceRef: (i) => ({
453
+ sourceModel: "Order",
454
+ sourceId: i.orderId
455
+ }),
456
+ legs: [{
457
+ id: "cash",
458
+ account: { code: (i) => i.toAccountCode },
459
+ side: "debit",
460
+ amount: (i) => i.amount,
461
+ label: (i) => i.reference ? `Receipt ${i.reference}` : "Payment received"
462
+ }, {
463
+ id: "ar",
464
+ account: { slot: "ar" },
465
+ side: "credit",
466
+ amount: (i) => i.amount,
467
+ label: () => "A/R settlement",
468
+ partner: (i) => ({
469
+ partnerId: i.customerId,
470
+ partnerType: "customer"
471
+ })
472
+ }]
473
+ },
474
+ vendorCreditNote: {
475
+ name: "bd.vendor-credit-note",
476
+ journalType: "PURCHASES",
477
+ idempotencyKey: (i) => `vendor-credit-note-${i.sourceId}-${i.reference}-${i.amount}`,
478
+ date: (i) => i.date,
479
+ label: (i) => `Credit note ${i.reference} — ${i.reason}`,
480
+ autoPost: false,
481
+ sourceRef: (i) => ({
482
+ sourceModel: "PurchaseOrder",
483
+ sourceId: i.sourceId
484
+ }),
485
+ legs: [{
486
+ id: "payable",
487
+ account: { slot: "ap" },
488
+ side: "debit",
489
+ amount: (i) => i.amount,
490
+ label: (i) => `Credit note ${i.reference} — ${i.reason}`,
491
+ partner: (i) => ({
492
+ partnerId: i.supplierId,
493
+ partnerType: "supplier"
494
+ }),
495
+ fx: (i) => i.currency && i.currency !== "BDT" && i.exchangeRate && i.foreignAmount ? {
496
+ currency: i.currency,
497
+ exchangeRate: i.exchangeRate,
498
+ foreignAmount: i.foreignAmount
499
+ } : void 0
500
+ }, {
501
+ id: "contra",
502
+ account: { code: (i) => i.contraAccountCode },
503
+ side: "credit",
504
+ amount: (i) => i.amount,
505
+ label: (i) => `Credit note ${i.reference}`
506
+ }]
507
+ },
508
+ customerDebitNote: {
509
+ name: "bd.customer-debit-note",
510
+ journalType: "SALES",
511
+ idempotencyKey: (i) => `customer-debit-note-${i.sourceId}-${i.reference}-${i.amount}`,
512
+ date: (i) => i.date,
513
+ label: (i) => `Debit note ${i.reference} — ${i.reason}`,
514
+ autoPost: false,
515
+ sourceRef: (i) => ({
516
+ sourceModel: "Order",
517
+ sourceId: i.sourceId
518
+ }),
519
+ legs: [{
520
+ id: "contra",
521
+ account: { code: (i) => i.contraAccountCode },
522
+ side: "debit",
523
+ amount: (i) => i.amount,
524
+ label: (i) => `Debit note ${i.reference}`
525
+ }, {
526
+ id: "ar",
527
+ account: { slot: "ar" },
528
+ side: "credit",
529
+ amount: (i) => i.amount,
530
+ label: (i) => `Debit note ${i.reference} — ${i.reason}`,
531
+ partner: (i) => ({
532
+ partnerId: i.customerId,
533
+ partnerType: "customer"
534
+ })
535
+ }]
536
+ },
537
+ openingBalance: {
538
+ name: "bd.opening-balance",
539
+ journalType: "GENERAL",
540
+ idempotencyKey: (i) => `opening-balance-${i.side}-${i.partnerId}`,
541
+ date: (i) => i.asOf,
542
+ label: (i) => i.reason || `Opening ${i.side === "supplier" ? "payable" : "receivable"} — ${i.partnerName ?? i.partnerId}`,
543
+ autoPost: false,
544
+ sourceRef: (i) => ({
545
+ sourceModel: i.side === "supplier" ? "Supplier" : "Customer",
546
+ sourceId: i.partnerId
547
+ }),
548
+ legs: [{
549
+ id: "debit-side",
550
+ account: { route: (i) => i.side === "supplier" ? "retainedEarnings" : "ar" },
551
+ side: "debit",
552
+ amount: (i) => i.amount,
553
+ when: () => true,
554
+ label: (i) => i.side === "supplier" ? "Opening balance — retained earnings" : "Opening A/R",
555
+ partner: (i) => i.side === "customer" ? {
556
+ partnerId: i.partnerId,
557
+ partnerType: "customer"
558
+ } : void 0
559
+ }, {
560
+ id: "credit-side",
561
+ account: { route: (i) => i.side === "supplier" ? "ap" : "retainedEarnings" },
562
+ side: "credit",
563
+ amount: (i) => i.amount,
564
+ when: () => true,
565
+ label: (i) => i.side === "supplier" ? "Opening A/P" : "Opening balance — retained earnings",
566
+ partner: (i) => i.side === "supplier" ? {
567
+ partnerId: i.partnerId,
568
+ partnerType: "supplier"
569
+ } : void 0
570
+ }]
571
+ }
572
+ };
573
+ }
574
+ //#endregion
575
+ //#region src/posting-recipes/purchases.recipes.ts
576
+ function claimableVatOf(resolvers, data) {
577
+ const rateCode = data.vatRateCode ?? (data.vatRate !== void 0 ? resolvers.rateCodeForRate(data.vatRate) : "STANDARD");
578
+ const inputAccount = resolvers.inputVatAccount(rateCode, data.regime ?? "standard");
579
+ const tax = data.tax ?? 0;
580
+ return {
581
+ claimable: tax > 0 && inputAccount !== null ? tax : 0,
582
+ inputAccount
583
+ };
584
+ }
585
+ function buildPurchaseRecipes(resolvers) {
586
+ return {
587
+ purchase: {
588
+ name: "bd.purchase",
589
+ journalType: "PURCHASES",
590
+ idempotencyKey: (i) => `purchase-${i.purchaseId}`,
591
+ date: (i) => i.date,
592
+ label: (i) => i.description || `Purchase ${displayRef(i.purchaseReferenceNumber, i.purchaseId)}`,
593
+ autoPost: false,
594
+ sourceRef: (i) => ({
595
+ sourceModel: "PurchaseOrder",
596
+ sourceId: i.purchaseId
597
+ }),
598
+ legs: [
599
+ {
600
+ id: "inventory",
601
+ account: { route: (i) => inventorySlotFor(i.inventoryType) },
602
+ side: "debit",
603
+ amount: (i) => i.totalAmount - claimableVatOf(resolvers, i).claimable,
604
+ when: () => true,
605
+ label: () => "Inventory received (net of VAT)",
606
+ fx: (i) => {
607
+ if (!i.currency || i.currency === "BDT" || !i.exchangeRate || !i.foreignTotal) return void 0;
608
+ const { claimable } = claimableVatOf(resolvers, i);
609
+ const inventoryNet = i.totalAmount - claimable;
610
+ return {
611
+ currency: i.currency,
612
+ exchangeRate: i.exchangeRate,
613
+ foreignAmount: Math.round(i.foreignTotal * (inventoryNet / i.totalAmount))
614
+ };
615
+ }
616
+ },
617
+ {
618
+ id: "input-vat",
619
+ account: { code: (i) => claimableVatOf(resolvers, i).inputAccount ?? "" },
620
+ side: "debit",
621
+ amount: (i) => claimableVatOf(resolvers, i).claimable,
622
+ label: (i) => `Input VAT @ ${i.vatRate ?? "?"}% (claimable)`,
623
+ fx: (i) => {
624
+ if (!i.currency || i.currency === "BDT" || !i.exchangeRate || !i.foreignTotal) return void 0;
625
+ const { claimable } = claimableVatOf(resolvers, i);
626
+ return {
627
+ currency: i.currency,
628
+ exchangeRate: i.exchangeRate,
629
+ foreignAmount: Math.round(i.foreignTotal * (claimable / i.totalAmount))
630
+ };
631
+ }
632
+ },
633
+ {
634
+ id: "credit",
635
+ account: { route: (i) => i.isPaid ? "cash" : "ap" },
636
+ side: "credit",
637
+ amount: (i) => i.totalAmount,
638
+ label: (i) => i.isPaid ? "Bank payment" : "Supplier payable",
639
+ fx: (i) => i.currency && i.currency !== "BDT" && i.exchangeRate && i.foreignTotal ? {
640
+ currency: i.currency,
641
+ exchangeRate: i.exchangeRate,
642
+ foreignAmount: i.foreignTotal
643
+ } : void 0
644
+ }
645
+ ]
646
+ },
647
+ goodsReceipt: {
648
+ name: "bd.goods-receipt",
649
+ journalType: "PURCHASES",
650
+ idempotencyKey: (i) => `goods-receipt-${i.orderId}-${i.receiptKey}`,
651
+ date: (i) => i.date,
652
+ label: (i) => `Goods received — PO ${displayRef(i.orderNumber, i.orderId)}`,
653
+ autoPost: true,
654
+ sourceRef: (i) => ({
655
+ sourceModel: "ProcurementOrder",
656
+ sourceId: i.orderId
657
+ }),
658
+ legs: [{
659
+ id: "inventory",
660
+ account: { slot: "merchandise" },
661
+ side: "debit",
662
+ amount: (i) => i.goodsValuePaisa,
663
+ label: () => "Inventory received"
664
+ }, {
665
+ id: "grir",
666
+ account: { slot: "grIrClearing" },
667
+ side: "credit",
668
+ amount: (i) => i.goodsValuePaisa,
669
+ label: () => "GR/IR — goods received not invoiced"
670
+ }]
671
+ },
672
+ vendorBill: {
673
+ name: "bd.vendor-bill",
674
+ journalType: "PURCHASES",
675
+ idempotencyKey: (i) => `vendor-bill-${i.purchaseId}`,
676
+ date: (i) => i.receivedAt,
677
+ label: (i) => `Vendor bill ${displayRef(i.billNumber, i.purchaseId)}`,
678
+ autoPost: false,
679
+ sourceRef: (i) => ({
680
+ sourceModel: "PurchaseOrder",
681
+ sourceId: i.purchaseId
682
+ }),
683
+ legs: [
684
+ {
685
+ id: "grir",
686
+ account: { slot: "grIrClearing" },
687
+ side: "debit",
688
+ amount: (i) => i.totalAmount - claimableVatOf(resolvers, i).claimable,
689
+ when: () => true,
690
+ label: () => "GR/IR clearing (goods received not invoiced)"
691
+ },
692
+ {
693
+ id: "input-vat",
694
+ account: { code: (i) => claimableVatOf(resolvers, i).inputAccount ?? "" },
695
+ side: "debit",
696
+ amount: (i) => claimableVatOf(resolvers, i).claimable,
697
+ label: (i) => `Input VAT @ ${i.vatRate ?? "?"}% (claimable)`
698
+ },
699
+ {
700
+ id: "payable",
701
+ account: { slot: "ap" },
702
+ side: "credit",
703
+ amount: (i) => {
704
+ const { claimable } = claimableVatOf(resolvers, i);
705
+ const vds = resolvers.resolveVatWithheldAtSource(claimable, {
706
+ applicable: i.withholdVds,
707
+ rate: i.vdsRate
708
+ });
709
+ return i.totalAmount - vds;
710
+ },
711
+ when: () => true,
712
+ label: (i) => {
713
+ const { claimable } = claimableVatOf(resolvers, i);
714
+ return resolvers.resolveVatWithheldAtSource(claimable, {
715
+ applicable: i.withholdVds,
716
+ rate: i.vdsRate
717
+ }) > 0 ? "A/P to supplier (net of VDS withheld)" : "A/P to supplier";
718
+ },
719
+ partner: (i) => ({
720
+ partnerId: i.supplierId,
721
+ partnerType: "supplier"
722
+ }),
723
+ maturityDate: (i) => i.dueDate,
724
+ fx: (i) => {
725
+ if (!i.currency || i.currency === "BDT" || !i.exchangeRate || !i.foreignTotal) return void 0;
726
+ const { claimable } = claimableVatOf(resolvers, i);
727
+ const vds = resolvers.resolveVatWithheldAtSource(claimable, {
728
+ applicable: i.withholdVds,
729
+ rate: i.vdsRate
730
+ });
731
+ return {
732
+ currency: i.currency,
733
+ exchangeRate: i.exchangeRate,
734
+ foreignAmount: Math.round(i.foreignTotal * ((i.totalAmount - vds) / i.totalAmount))
735
+ };
736
+ }
737
+ },
738
+ {
739
+ id: "vds",
740
+ account: { slot: "vdsPayable" },
741
+ side: "credit",
742
+ amount: (i) => {
743
+ const { claimable } = claimableVatOf(resolvers, i);
744
+ return resolvers.resolveVatWithheldAtSource(claimable, {
745
+ applicable: i.withholdVds,
746
+ rate: i.vdsRate
747
+ });
748
+ },
749
+ label: (i) => `VDS withheld @ ${(i.vdsRate ?? .5) * 100}% of input VAT — remit to NBR`
750
+ }
751
+ ]
752
+ },
753
+ vendorPayment: {
754
+ name: "bd.vendor-payment",
755
+ journalType: "CASH_PAYMENTS",
756
+ idempotencyKey: (i) => `vendor-payment-${i.purchaseId}-${i.date.getTime()}-${i.amount}`,
757
+ date: (i) => i.date,
758
+ label: (i) => i.reference ? `Payment ${i.reference}` : `Payment to ${i.supplierName ?? i.supplierId}`,
759
+ autoPost: true,
760
+ sourceRef: (i) => ({
761
+ sourceModel: "PurchaseOrder",
762
+ sourceId: i.purchaseId
763
+ }),
764
+ legs: [
765
+ {
766
+ id: "payable",
767
+ account: { slot: "ap" },
768
+ side: "debit",
769
+ amount: (i) => i.amount,
770
+ label: () => "A/P settlement",
771
+ partner: (i) => ({
772
+ partnerId: i.supplierId,
773
+ partnerType: "supplier"
774
+ }),
775
+ fx: (i) => i.currency && i.currency !== "BDT" && i.exchangeRate && i.foreignAmount ? {
776
+ currency: i.currency,
777
+ exchangeRate: i.exchangeRate,
778
+ foreignAmount: i.foreignAmount
779
+ } : void 0
780
+ },
781
+ {
782
+ id: "cash",
783
+ account: { code: (i) => i.fromAccountCode },
784
+ side: "credit",
785
+ amount: (i) => i.settlementAmount ?? i.amount,
786
+ label: (i) => i.reference ? `Payment ${i.reference}` : "Payment out",
787
+ fx: (i) => i.currency && i.currency !== "BDT" && i.exchangeRate && i.foreignAmount ? {
788
+ currency: i.currency,
789
+ exchangeRate: i.exchangeRate,
790
+ foreignAmount: i.foreignAmount
791
+ } : void 0
792
+ },
793
+ {
794
+ id: "fx-gain",
795
+ account: { code: (i) => i.realizedGainAccountCode ?? "4306" },
796
+ side: "credit",
797
+ amount: (i) => Math.max(0, i.amount - (i.settlementAmount ?? i.amount)),
798
+ label: () => "Realized foreign exchange gain"
799
+ },
800
+ {
801
+ id: "fx-loss",
802
+ account: { code: (i) => i.realizedLossAccountCode ?? "6510" },
803
+ side: "debit",
804
+ amount: (i) => Math.max(0, (i.settlementAmount ?? i.amount) - i.amount),
805
+ label: () => "Realized foreign exchange loss"
806
+ }
807
+ ]
808
+ },
809
+ vendorBillReversal: {
810
+ name: "bd.vendor-bill.reversal",
811
+ journalType: "PURCHASES",
812
+ idempotencyKey: (i) => `vendor-bill-${i.purchaseId}-reverse`,
813
+ date: (i) => i.date,
814
+ label: (i) => i.reason ? `Vendor bill reversal (Purchase ${i.purchaseId}) — ${i.reason}` : `Vendor bill reversal (Purchase ${i.purchaseId})`,
815
+ autoPost: false,
816
+ sourceRef: (i) => ({
817
+ sourceModel: "PurchaseOrder",
818
+ sourceId: i.purchaseId
819
+ }),
820
+ legs: [
821
+ {
822
+ id: "payable",
823
+ account: { slot: "ap" },
824
+ side: "debit",
825
+ amount: (i) => {
826
+ const { claimable } = claimableVatOf(resolvers, i);
827
+ const vds = resolvers.resolveVatWithheldAtSource(claimable, {
828
+ applicable: i.withholdVds,
829
+ rate: i.vdsRate
830
+ });
831
+ return i.totalAmount - vds;
832
+ },
833
+ when: () => true,
834
+ label: (i) => i.reason ? `A/P reversal — ${i.reason}` : "A/P reversal",
835
+ partner: (i) => ({
836
+ partnerId: i.supplierId,
837
+ partnerType: "supplier"
838
+ })
839
+ },
840
+ {
841
+ id: "inventory",
842
+ account: { route: (i) => inventorySlotFor(i.inventoryType) },
843
+ side: "credit",
844
+ amount: (i) => i.totalAmount - claimableVatOf(resolvers, i).claimable,
845
+ when: () => true,
846
+ label: () => "Inventory reversal (net of VAT)"
847
+ },
848
+ {
849
+ id: "input-vat",
850
+ account: { code: (i) => claimableVatOf(resolvers, i).inputAccount ?? "" },
851
+ side: "credit",
852
+ amount: (i) => claimableVatOf(resolvers, i).claimable,
853
+ label: (i) => `Input VAT reversal @ ${i.vatRate ?? "?"}%`
854
+ },
855
+ {
856
+ id: "vds",
857
+ account: { slot: "vdsPayable" },
858
+ side: "debit",
859
+ amount: (i) => {
860
+ const { claimable } = claimableVatOf(resolvers, i);
861
+ return resolvers.resolveVatWithheldAtSource(claimable, {
862
+ applicable: i.withholdVds,
863
+ rate: i.vdsRate
864
+ });
865
+ },
866
+ label: () => "VDS Payable reversal"
867
+ }
868
+ ]
869
+ },
870
+ supplierReturn: {
871
+ name: "bd.supplier-return",
872
+ journalType: "PURCHASES",
873
+ idempotencyKey: (i) => `supplier-return-${i.purchaseId}-${i.moveGroupId}`,
874
+ date: (i) => i.date,
875
+ label: (i) => i.reason ? `Supplier return (PO ${i.purchaseId}) — ${i.reason}` : `Supplier return (PO ${i.purchaseId})`,
876
+ autoPost: false,
877
+ sourceRef: (i) => ({
878
+ sourceModel: "PurchaseOrder",
879
+ sourceId: i.purchaseId
880
+ }),
881
+ legs: [{
882
+ id: "payable",
883
+ account: { slot: "ap" },
884
+ side: "debit",
885
+ amount: (i) => i.totalPaisa,
886
+ label: (i) => i.reason ? `Supplier return — ${i.reason}` : "Supplier return — A/P offset",
887
+ partner: (i) => ({
888
+ partnerId: i.supplierId,
889
+ partnerType: "supplier"
890
+ })
891
+ }, {
892
+ id: "inventory",
893
+ account: { route: (i) => inventorySlotFor(i.inventoryType) },
894
+ side: "credit",
895
+ amount: (i) => i.totalPaisa,
896
+ label: () => "Inventory returned to supplier"
897
+ }]
898
+ },
899
+ importClearance: {
900
+ name: "bd.import-clearance",
901
+ journalType: "PURCHASES",
902
+ idempotencyKey: (i) => `import-clearance-${i.clearanceId}`,
903
+ date: (i) => i.date,
904
+ label: (i) => i.description || `Customs clearance — BoE ${displayRef(i.billOfEntry, i.clearanceId)}`,
905
+ autoPost: false,
906
+ sourceRef: (i) => ({
907
+ sourceModel: "ImportClearance",
908
+ sourceId: i.clearanceId
909
+ }),
910
+ legs: [
911
+ {
912
+ id: "inventory",
913
+ account: { route: (i) => inventorySlotFor(i.inventoryType) },
914
+ side: "debit",
915
+ amount: (i) => {
916
+ const stack = resolvers.computeImportStack(i);
917
+ const claimable = resolvers.inputVatAccount("STANDARD", i.regime ?? "importer") ? stack.recoverableTax : 0;
918
+ return stack.landedCost - i.assessableValue + (claimable ? 0 : stack.recoverableTax);
919
+ },
920
+ when: () => true,
921
+ label: (i) => {
922
+ return `Import duties capitalized into inventory (CD + SD${resolvers.inputVatAccount("STANDARD", i.regime ?? "importer") !== null ? "" : " + VAT folded-in"})`;
923
+ },
924
+ partner: (i) => ({
925
+ partnerId: i.supplierId,
926
+ partnerType: "supplier"
927
+ })
928
+ },
929
+ {
930
+ id: "input-vat",
931
+ account: { code: (i) => resolvers.inputVatAccount("STANDARD", i.regime ?? "importer") ?? "" },
932
+ side: "debit",
933
+ amount: (i) => resolvers.inputVatAccount("STANDARD", i.regime ?? "importer") ? resolvers.computeImportStack(i).recoverableTax : 0,
934
+ label: () => "Input VAT @ import (claimable)"
935
+ },
936
+ {
937
+ id: "advance-tax",
938
+ account: { slot: "advanceTaxPaid" },
939
+ side: "debit",
940
+ amount: (i) => resolvers.computeImportStack(i).prepaidTax,
941
+ label: () => "Advance tax at import (AT + AIT)"
942
+ },
943
+ {
944
+ id: "bank",
945
+ account: { slot: "cash" },
946
+ side: "credit",
947
+ amount: (i) => resolvers.computeImportStack(i).totalPayable,
948
+ label: (i) => `Customs clearance payment — BoE ${displayRef(i.billOfEntry, i.clearanceId)}`
949
+ }
950
+ ]
951
+ }
952
+ };
953
+ }
954
+ //#endregion
955
+ //#region src/posting-recipes/sales-cod.recipes.ts
956
+ /** sales.contract.ts:48-61 — method → activeCodes slot name. */
957
+ const PAYMENT_METHOD_SLOT = {
958
+ cash: "pettyCash",
959
+ card: "gatewayClearing",
960
+ bkash: "mobileMoneyMerchant",
961
+ nagad: "mobileMoneyMerchant",
962
+ rocket: "mobileMoneyMerchant",
963
+ mfs: "mobileMoneyMerchant",
964
+ bank_transfer: "cash",
965
+ manual: "pettyCash"
966
+ };
967
+ /** restocking-fee.contract.ts:32-40 adds the 'cod' key (fee retained as cash). */
968
+ const RESTOCKING_METHOD_SLOT = {
969
+ ...PAYMENT_METHOD_SLOT,
970
+ cod: "pettyCash"
971
+ };
972
+ function paymentMethodSlot(method) {
973
+ return PAYMENT_METHOD_SLOT[method ?? ""] ?? "pettyCash";
974
+ }
975
+ /**
976
+ * cod-settlement.contract.ts:121-134 — result-style pre-condition (ported
977
+ * verbatim, messages included). Handlers call BEFORE evaluate.
978
+ */
979
+ function validateCodSettlementInputs(data) {
980
+ if (data.grossAmount <= 0) return {
981
+ ok: false,
982
+ reason: "grossAmount must be positive"
983
+ };
984
+ if (data.actualReceived < 0) return {
985
+ ok: false,
986
+ reason: "actualReceived cannot be negative"
987
+ };
988
+ if (data.courierCommission < 0) return {
989
+ ok: false,
990
+ reason: "courierCommission cannot be negative"
991
+ };
992
+ if (data.writeoff < 0) return {
993
+ ok: false,
994
+ reason: "writeoff cannot be negative"
995
+ };
996
+ const sum = data.actualReceived + data.courierCommission + data.writeoff;
997
+ if (sum !== data.grossAmount) return {
998
+ ok: false,
999
+ reason: `actualReceived + courierCommission + writeoff (${sum}) must equal grossAmount (${data.grossAmount})`
1000
+ };
1001
+ return { ok: true };
1002
+ }
1003
+ /** Throw-style form of the same pre-condition (for guard-clause call sites). */
1004
+ function validateCodSettlement(i) {
1005
+ const result = validateCodSettlementInputs(i);
1006
+ if (!result.ok) throw new Error(result.reason);
1007
+ }
1008
+ function buildSalesCodRecipes() {
1009
+ return {
1010
+ salesTransaction: {
1011
+ name: "bd.sales.transaction",
1012
+ journalType: (i) => i.source === "pos" ? "POS_SALES" : "ECOM_SALES",
1013
+ idempotencyKey: (i) => `sale-${i.transactionId}`,
1014
+ date: (i) => i.date,
1015
+ label: (i) => i.description || `Sale ${displayRef(i.orderReferenceNumber, i.transactionId)}`,
1016
+ autoPost: true,
1017
+ sourceRef: (i) => i.orderId ? {
1018
+ sourceModel: "Order",
1019
+ sourceId: i.orderId
1020
+ } : {
1021
+ sourceModel: "Transaction",
1022
+ sourceId: i.transactionId
1023
+ },
1024
+ metadata: (i) => i.gatewayTransactionId || i.gatewayProvider ? {
1025
+ ...i.gatewayTransactionId ? { gatewayTransactionId: i.gatewayTransactionId } : {},
1026
+ ...i.gatewayProvider ? { gatewayProvider: i.gatewayProvider } : {}
1027
+ } : void 0,
1028
+ legs: [
1029
+ {
1030
+ id: "receipt",
1031
+ account: { route: (i) => paymentMethodSlot(i.method) },
1032
+ side: "debit",
1033
+ amount: (i) => i.amount,
1034
+ label: (i) => `${i.source ?? "web"} — ${i.method}`
1035
+ },
1036
+ {
1037
+ id: "promo",
1038
+ account: { slot: "salesDiscount" },
1039
+ side: "debit",
1040
+ amount: (i) => i.promoDiscount ?? 0,
1041
+ label: () => "Promo discount"
1042
+ },
1043
+ {
1044
+ id: "revenue",
1045
+ account: { slot: "revenue" },
1046
+ side: "credit",
1047
+ amount: (i) => i.amount - i.tax + (i.promoDiscount ?? 0),
1048
+ when: () => true,
1049
+ label: () => "Sales revenue"
1050
+ },
1051
+ {
1052
+ id: "vat",
1053
+ account: { slot: "taxPayable" },
1054
+ side: "credit",
1055
+ amount: (i) => i.tax,
1056
+ label: () => "VAT collected"
1057
+ }
1058
+ ]
1059
+ },
1060
+ dailyPosSummary: {
1061
+ name: "bd.sales.daily-pos-summary",
1062
+ journalType: "POS_SALES",
1063
+ idempotencyKey: (i) => `pos-daily-${i.branchId}-${i.date}`,
1064
+ date: (i) => new Date(i.date),
1065
+ label: (i) => `POS Daily Sales — ${i.branchCode} — ${i.date}`,
1066
+ autoPost: true,
1067
+ legs: [
1068
+ {
1069
+ id: "per-method",
1070
+ account: { route: (m) => paymentMethodSlot(m.method) },
1071
+ side: "debit",
1072
+ amount: (m) => m.amount,
1073
+ label: (m) => `POS ${m.method} receipts`,
1074
+ expand: (i) => i.byMethod.filter((m) => m.amount > 0)
1075
+ },
1076
+ {
1077
+ id: "revenue",
1078
+ account: { slot: "revenue" },
1079
+ side: "credit",
1080
+ amount: (i) => i.totalAmount - i.totalTax,
1081
+ label: (i) => `POS sales (${i.transactionCount} transactions)`
1082
+ },
1083
+ {
1084
+ id: "vat",
1085
+ account: { slot: "taxPayable" },
1086
+ side: "credit",
1087
+ amount: (i) => i.totalTax,
1088
+ label: () => "VAT collected (POS)"
1089
+ }
1090
+ ]
1091
+ },
1092
+ refund: {
1093
+ name: "bd.sales.refund",
1094
+ journalType: "ECOM_SALES",
1095
+ idempotencyKey: (i) => `refund-${i.transactionId}`,
1096
+ date: (i) => i.date,
1097
+ label: (i) => i.reason ? `Refund — ${i.reason}` : `Refund ${i.transactionId}`,
1098
+ autoPost: true,
1099
+ sourceRef: (i) => i.orderId ? {
1100
+ sourceModel: "Order",
1101
+ sourceId: i.orderId
1102
+ } : {
1103
+ sourceModel: "Transaction",
1104
+ sourceId: i.transactionId
1105
+ },
1106
+ legs: [
1107
+ {
1108
+ id: "revenue",
1109
+ account: { slot: "revenue" },
1110
+ side: "debit",
1111
+ amount: (i) => i.refundAmount - i.tax,
1112
+ when: () => true,
1113
+ label: () => "Sales refund — revenue reversal"
1114
+ },
1115
+ {
1116
+ id: "vat",
1117
+ account: { slot: "taxPayable" },
1118
+ side: "debit",
1119
+ amount: (i) => i.tax,
1120
+ label: () => "VAT refund — liability reduction"
1121
+ },
1122
+ {
1123
+ id: "cash",
1124
+ account: { route: (i) => paymentMethodSlot(i.method) },
1125
+ side: "credit",
1126
+ amount: (i) => i.refundAmount,
1127
+ label: (i) => `Refund — ${i.method}`
1128
+ }
1129
+ ]
1130
+ },
1131
+ codPlacement: {
1132
+ name: "bd.cod.placement",
1133
+ journalType: "ECOM_SALES_COD",
1134
+ idempotencyKey: (i) => `cod-placed-${i.transactionId}`,
1135
+ date: (i) => i.date,
1136
+ label: (i) => i.description || `COD order ${displayRef(i.orderReferenceNumber, i.orderId)}`,
1137
+ autoPost: true,
1138
+ sourceRef: (i) => ({
1139
+ sourceModel: "Order",
1140
+ sourceId: i.orderId
1141
+ }),
1142
+ legs: [
1143
+ {
1144
+ id: "ar",
1145
+ account: { slot: "ar" },
1146
+ side: "debit",
1147
+ amount: (i) => i.amount,
1148
+ label: (i) => `COD receivable — order ${displayRef(i.orderReferenceNumber, i.orderId)}`,
1149
+ partner: (i) => ({
1150
+ partnerId: i.customerId ?? i.orderId,
1151
+ partnerType: "customer"
1152
+ }),
1153
+ maturityDate: (i) => i.maturityDate
1154
+ },
1155
+ {
1156
+ id: "promo",
1157
+ account: { slot: "salesDiscount" },
1158
+ side: "debit",
1159
+ amount: (i) => i.promoDiscount ?? 0,
1160
+ label: () => "Promo discount"
1161
+ },
1162
+ {
1163
+ id: "revenue",
1164
+ account: { slot: "revenue" },
1165
+ side: "credit",
1166
+ amount: (i) => i.amount - i.tax + (i.promoDiscount ?? 0),
1167
+ when: () => true,
1168
+ label: () => "Sales revenue (COD)"
1169
+ },
1170
+ {
1171
+ id: "vat",
1172
+ account: { slot: "taxPayable" },
1173
+ side: "credit",
1174
+ amount: (i) => i.tax,
1175
+ label: () => "VAT collected (COD)"
1176
+ }
1177
+ ]
1178
+ },
1179
+ codSettlement: {
1180
+ name: "bd.cod.settlement",
1181
+ journalType: "ECOM_SALES_COD_SETTLEMENT",
1182
+ idempotencyKey: (i) => `cod-settled-${i.settlementId}`,
1183
+ date: (i) => i.date,
1184
+ label: (i) => i.notes || `COD settlement — order ${displayRef(i.orderReferenceNumber, i.orderId)}`,
1185
+ autoPost: true,
1186
+ sourceRef: (i) => ({
1187
+ sourceModel: "Order",
1188
+ sourceId: i.orderId
1189
+ }),
1190
+ legs: [
1191
+ {
1192
+ id: "cash",
1193
+ account: { code: (i) => i.cashAccountCode },
1194
+ side: "debit",
1195
+ amount: (i) => i.actualReceived,
1196
+ label: (i) => `COD settlement received — order ${displayRef(i.orderReferenceNumber, i.orderId)}`
1197
+ },
1198
+ {
1199
+ id: "commission",
1200
+ account: { slot: "courierCommission" },
1201
+ side: "debit",
1202
+ amount: (i) => i.courierCommission,
1203
+ label: () => "Courier COD commission"
1204
+ },
1205
+ {
1206
+ id: "writeoff",
1207
+ account: { slot: "badDebt" },
1208
+ side: "debit",
1209
+ amount: (i) => i.writeoff,
1210
+ label: () => "COD write-off (short-pay / refused)"
1211
+ },
1212
+ {
1213
+ id: "ar",
1214
+ account: { slot: "ar" },
1215
+ side: "credit",
1216
+ amount: (i) => i.grossAmount,
1217
+ when: () => true,
1218
+ label: (i) => `Clear COD A/R — order ${displayRef(i.orderReferenceNumber, i.orderId)}`,
1219
+ partner: (i) => ({
1220
+ partnerId: i.customerId ?? i.orderId,
1221
+ partnerType: "customer"
1222
+ })
1223
+ }
1224
+ ]
1225
+ },
1226
+ codCancellation: {
1227
+ name: "bd.cod.cancellation",
1228
+ journalType: "ECOM_SALES_COD_REVERSAL",
1229
+ idempotencyKey: (i) => `cod-cancelled-${i.orderId}`,
1230
+ date: (i) => i.date,
1231
+ label: (i) => i.reason ? `COD cancellation — ${i.reason}` : `COD cancellation — order ${displayRef(i.orderReferenceNumber, i.orderId)}`,
1232
+ autoPost: true,
1233
+ sourceRef: (i) => ({
1234
+ sourceModel: "Order",
1235
+ sourceId: i.orderId
1236
+ }),
1237
+ legs: [
1238
+ {
1239
+ id: "revenue",
1240
+ account: { slot: "revenue" },
1241
+ side: "debit",
1242
+ amount: (i) => i.grossAmount - i.tax + (i.promoDiscount ?? 0),
1243
+ when: () => true,
1244
+ label: () => "Revenue reversal (COD cancelled)"
1245
+ },
1246
+ {
1247
+ id: "vat",
1248
+ account: { slot: "taxPayable" },
1249
+ side: "debit",
1250
+ amount: (i) => i.tax,
1251
+ label: () => "VAT reversal (COD cancelled)"
1252
+ },
1253
+ {
1254
+ id: "promo",
1255
+ account: { slot: "salesDiscount" },
1256
+ side: "credit",
1257
+ amount: (i) => i.promoDiscount ?? 0,
1258
+ label: () => "Promo discount reversal"
1259
+ },
1260
+ {
1261
+ id: "ar",
1262
+ account: { slot: "ar" },
1263
+ side: "credit",
1264
+ amount: (i) => i.grossAmount,
1265
+ when: () => true,
1266
+ label: (i) => `Clear A/R on cancellation — order ${displayRef(i.orderReferenceNumber, i.orderId)}`,
1267
+ partner: (i) => ({
1268
+ partnerId: i.customerId ?? i.orderId,
1269
+ partnerType: "customer"
1270
+ })
1271
+ }
1272
+ ]
1273
+ },
1274
+ restockingFee: {
1275
+ name: "bd.restocking-fee",
1276
+ journalType: "ECOM_SALES",
1277
+ idempotencyKey: (i) => `restocking-fee-${i.changeNumber}`,
1278
+ date: (i) => i.date,
1279
+ label: (i) => i.reason ? `Restocking fee — ${i.reason}` : `Restocking fee ${i.changeNumber}`,
1280
+ autoPost: true,
1281
+ sourceRef: (i) => ({
1282
+ sourceModel: "OrderChange",
1283
+ sourceId: i.changeNumber
1284
+ }),
1285
+ legs: [{
1286
+ id: "receipt",
1287
+ account: { route: (i) => RESTOCKING_METHOD_SLOT[i.paymentMethod ?? ""] ?? "pettyCash" },
1288
+ side: "debit",
1289
+ amount: (i) => i.amount,
1290
+ label: (i) => `Restocking fee retained — ${i.paymentMethod ?? "cash"}`
1291
+ }, {
1292
+ id: "income",
1293
+ account: { slot: "restockingFeeIncome" },
1294
+ side: "credit",
1295
+ amount: (i) => i.amount,
1296
+ label: () => "Restocking fee income"
1297
+ }]
1298
+ }
1299
+ };
1300
+ }
1301
+ //#endregion
1302
+ //#region src/posting-recipes/pack.ts
1303
+ /**
1304
+ * Every chart slot any BD recipe references — statically (slot mode) or at
1305
+ * evaluation time (route mode). The registry validates the host chart
1306
+ * provides ALL of them at boot (fail-loud; anti-silent-default).
1307
+ */
1308
+ const BD_REQUIRED_POSTING_SLOTS = [
1309
+ "pettyCash",
1310
+ "cash",
1311
+ "gatewayClearing",
1312
+ "mobileMoneyMerchant",
1313
+ "ar",
1314
+ "vdsReceivable",
1315
+ "advanceTaxPaid",
1316
+ "merchandise",
1317
+ "rawMaterials",
1318
+ "finishedGoods",
1319
+ "packingMaterials",
1320
+ "inventoryInTransit",
1321
+ "ap",
1322
+ "grIrClearing",
1323
+ "transferCostClearing",
1324
+ "taxPayable",
1325
+ "vdsPayable",
1326
+ "revenue",
1327
+ "salesDiscount",
1328
+ "restockingFeeIncome",
1329
+ "inventoryGain",
1330
+ "cogsMaterials",
1331
+ "shrinkage",
1332
+ "courierCommission",
1333
+ "badDebt",
1334
+ "importLandedCost",
1335
+ "carriageInward",
1336
+ "customsDuty",
1337
+ "cfAgentCommission",
1338
+ "retainedEarnings"
1339
+ ];
1340
+ function buildBdPostingRecipes(resolvers) {
1341
+ const purchases = buildPurchaseRecipes(resolvers);
1342
+ const salesCod = buildSalesCodRecipes();
1343
+ const inventoryTransfer = buildInventoryTransferRecipes();
1344
+ const misc = buildMiscRecipes(resolvers);
1345
+ return [
1346
+ ...Object.values(purchases),
1347
+ ...Object.values(salesCod),
1348
+ ...Object.values(inventoryTransfer),
1349
+ ...Object.values(misc)
1350
+ ];
1351
+ }
1352
+ //#endregion
1353
+ export { BD_REQUIRED_POSTING_SLOTS, INVENTORY_SLOT, PAYMENT_METHOD_SLOT, RESTOCKING_METHOD_SLOT, buildBdPostingRecipes, buildInventoryTransferRecipes, buildMiscRecipes, buildPurchaseRecipes, buildSalesCodRecipes, displayRef, inventorySlotFor, landedCostClearingSlot, paymentMethodSlot, validateCodSettlement, validateCodSettlementInputs, validateNoteInput, validateSettlement };