@qrwise/pos-calculations 0.0.18 → 0.0.20

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.
package/dist/index.cjs CHANGED
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ buildTransactionProjection: () => buildTransactionProjection,
23
24
  calculateTotals: () => calculateTotals,
24
25
  clamp: () => clamp,
25
26
  format2: () => format2,
@@ -68,6 +69,11 @@ var splitDiscountTypes = [
68
69
  { type: "MOV", count: "movCount", breakdown: "mov", vatExempt: false },
69
70
  { type: "SOLO", count: "soloParentCount", breakdown: "solo", vatExempt: false }
70
71
  ];
72
+ function resolvedVatDivisor(input) {
73
+ if (input.isVatRegistered === false) return 1;
74
+ const divisor = Number(input.vatRate || 0) / 100;
75
+ return divisor > 0 ? divisor : 1;
76
+ }
71
77
  function normalizedCount(value) {
72
78
  return Math.max(0, Math.floor(toMoneyNumber(value)));
73
79
  }
@@ -78,7 +84,7 @@ function calculateSplitTotals(input) {
78
84
  const concessionCount = splitDiscountTypes.reduce((total, item) => total + normalizedCount(counts[item.count]), 0);
79
85
  const paxCount = regularCount + concessionCount;
80
86
  if (paxCount === 0) return null;
81
- const vatDivisor = Number(input.vatRate || 0) / 100;
87
+ const vatDivisor = resolvedVatDivisor(input);
82
88
  const vatRate = Math.max(0, vatDivisor - 1);
83
89
  const scRate = Number(input.serviceChargeRate || 0) / 100;
84
90
  const discountableAmount = Math.max(0, toMoneyNumber(input.discountableTotalAmount));
@@ -108,6 +114,10 @@ function calculateSplitTotals(input) {
108
114
  discountBreakdown[item.breakdown] = -discount;
109
115
  totalDiscount += discount;
110
116
  }
117
+ if (input.isVatRegistered === false) {
118
+ vatExemptSales += vatSales;
119
+ vatSales = 0;
120
+ }
111
121
  vatSales = format2(vatSales);
112
122
  vatExemptSales = format2(vatExemptSales);
113
123
  const discounted = -format2(totalDiscount);
@@ -117,12 +127,12 @@ function calculateSplitTotals(input) {
117
127
  const serviceCharge = input.serviceChargeRate > 0 ? format2(serviceChargeBase / vatDivisor * scRate) : 0;
118
128
  const togoCharge = input.togoableTotalAmount > 0 ? input.togoCharge : 0;
119
129
  const totalAmount = format2(Math.max(0, vatNet + vat + serviceCharge + togoCharge));
120
- const voucherVatNet = format2(totalOrderAmount / vatDivisor);
121
- const voucherVat = format2(voucherVatNet * vatRate);
130
+ const voucherVatNet2 = format2(totalOrderAmount / vatDivisor);
131
+ const voucherVat = format2(voucherVatNet2 * vatRate);
122
132
  return {
123
133
  quantity: input.quantity,
124
134
  subtotal,
125
- isAddVat: splitDiscountTypes.some((item) => !item.vatExempt && normalizedCount(counts[item.count]) > 0),
135
+ isAddVat: input.isVatRegistered !== false && splitDiscountTypes.some((item) => !item.vatExempt && normalizedCount(counts[item.count]) > 0),
126
136
  vatNet,
127
137
  vat,
128
138
  discounted,
@@ -137,9 +147,9 @@ function calculateSplitTotals(input) {
137
147
  voucher: {
138
148
  discount: voucherDiscounted,
139
149
  net: subtotal,
140
- vatNet: voucherVatNet,
150
+ vatNet: voucherVatNet2,
141
151
  vat: voucherVat,
142
- forBirDiscount: -(input.voucherType === "FIXED" ? voucherVatNet - Number(voucherRate.toFixed(2)) : voucherVatNet * Number(voucherRate.toFixed(2)))
152
+ forBirDiscount: -(input.voucherType === "FIXED" ? voucherVatNet2 - Number(voucherRate.toFixed(2)) : voucherVatNet2 * Number(voucherRate.toFixed(2)))
143
153
  },
144
154
  hasDiscount: totalDiscount > 0,
145
155
  discount: {
@@ -169,7 +179,8 @@ function calculateSplitTotals(input) {
169
179
  function calculateTotals(input) {
170
180
  const splitTotals = calculateSplitTotals(input);
171
181
  if (splitTotals) return splitTotals;
172
- const vatRate = Number(input.vatRate || 0) / 100;
182
+ const vatRate = resolvedVatDivisor(input);
183
+ const vatPercent = Math.max(0, vatRate - 1);
173
184
  const scRate = Number(input.serviceChargeRate || 0) / 100;
174
185
  const discountRate = Number(input.discountRate || 0) / 100;
175
186
  const voucherRate = input.voucherType === "FIXED" ? input.voucherRate : Number(input.voucherRate || 0) / 100;
@@ -183,12 +194,12 @@ function calculateTotals(input) {
183
194
  }
184
195
  }
185
196
  const subtotal = totalOrderAmount + voucherDiscounted;
186
- const voucherVatNet = format2(totalOrderAmount / vatRate);
187
- const voucherVat = format2(voucherVatNet * 0.12);
197
+ const voucherVatNet2 = format2(totalOrderAmount / vatRate);
198
+ const voucherVat = format2(voucherVatNet2 * vatPercent);
188
199
  let vatNet = format2(subtotal / vatRate);
189
- let vat = format2(vatNet * 0.12);
200
+ let vat = format2(vatNet * vatPercent);
190
201
  const isSpecial = discountRate > 0;
191
- const isAddVat = isAddVatAfterDiscount(input.discountType);
202
+ const isAddVat = input.isVatRegistered === false ? false : isAddVatAfterDiscount(input.discountType);
192
203
  const discounted = format2(discountRate * -vatNet);
193
204
  if (isSpecial) {
194
205
  vatNet = format2(vatNet + discounted);
@@ -228,9 +239,9 @@ function calculateTotals(input) {
228
239
  voucher: {
229
240
  discount: voucherDiscounted,
230
241
  net: subtotal,
231
- vatNet: voucherVatNet,
242
+ vatNet: voucherVatNet2,
232
243
  vat: voucherVat,
233
- forBirDiscount: -(input.voucherType === "FIXED" ? voucherVatNet - Number(voucherRate.toFixed(2)) : voucherVatNet * Number(voucherRate.toFixed(2)))
244
+ forBirDiscount: -(input.voucherType === "FIXED" ? voucherVatNet2 - Number(voucherRate.toFixed(2)) : voucherVatNet2 * Number(voucherRate.toFixed(2)))
234
245
  },
235
246
  hasDiscount: discountRate > 0,
236
247
  discount: {
@@ -257,8 +268,272 @@ function calculateTotals(input) {
257
268
  }
258
269
  };
259
270
  }
271
+
272
+ // src/transaction-projection.ts
273
+ var emptyWeights = () => ({ sc: 0, pwd: 0, naac: 0, solo: 0, mov: 0 });
274
+ function categoryFor(value) {
275
+ const normalized = value.toLowerCase();
276
+ if (normalized.includes("senior")) return "sc";
277
+ if (normalized.includes("pwd") || normalized.includes("disability")) return "pwd";
278
+ if (normalized.includes("naac") || normalized.includes("athlete")) return "naac";
279
+ if (normalized.includes("solo")) return "solo";
280
+ if (normalized.includes("mov") || normalized.includes("valor") || normalized.includes("uniformed")) return "mov";
281
+ return null;
282
+ }
283
+ function allocate(amount, weights) {
284
+ const allocations = emptyWeights();
285
+ const categories = Object.keys(weights).filter((category) => weights[category] > 0);
286
+ if (categories.length === 1) {
287
+ allocations[categories[0]] = format2(amount);
288
+ return allocations;
289
+ }
290
+ const totalWeight = categories.reduce((total, category) => total + weights[category], 0);
291
+ let allocated = 0;
292
+ categories.forEach((category, index) => {
293
+ const share = index === categories.length - 1 ? format2(amount - allocated) : format2(amount * weights[category] / totalWeight);
294
+ allocations[category] = share;
295
+ allocated += share;
296
+ });
297
+ return allocations;
298
+ }
299
+ function itemizedWeights(transaction, options) {
300
+ const splits = transaction.metadata?.split_discounts;
301
+ if (!Array.isArray(splits)) return null;
302
+ const discountWeights = emptyWeights();
303
+ const vatExemptWeights = emptyWeights();
304
+ for (const rawSplit of splits) {
305
+ if (!rawSplit || typeof rawSplit !== "object") continue;
306
+ const discounts = rawSplit.discounts;
307
+ if (!Array.isArray(discounts)) continue;
308
+ for (const rawDiscount of discounts) {
309
+ if (!rawDiscount || typeof rawDiscount !== "object") continue;
310
+ const discount = rawDiscount;
311
+ const category = categoryFor(typeof discount.discount_name === "string" ? discount.discount_name : "");
312
+ if (!category) continue;
313
+ discountWeights[category] += Math.abs(toMoneyNumber(discount.discount_amount));
314
+ vatExemptWeights[category] += Math.abs(toMoneyNumber(discount.vat_exempt_sales));
315
+ }
316
+ }
317
+ if (Object.values(discountWeights).some((weight) => weight > 0)) {
318
+ return {
319
+ discountWeights,
320
+ vatExemptWeights: Object.values(vatExemptWeights).some((weight) => weight > 0) ? vatExemptWeights : { ...emptyWeights(), sc: discountWeights.sc, pwd: discountWeights.pwd }
321
+ };
322
+ }
323
+ const items = (transaction.transaction_items ?? []).filter((item) => item.item_status !== "cancelled");
324
+ const itemsById = new Map(items.filter((item) => item.id).map((item) => [item.id, item]));
325
+ splits.forEach((rawSplit, splitIndex) => {
326
+ if (!rawSplit || typeof rawSplit !== "object") return;
327
+ const split = rawSplit;
328
+ if (!Array.isArray(split.discounts)) return;
329
+ const itemIds = [split.transaction_item_ids, split.item_ids].find(Array.isArray)?.filter((itemId) => typeof itemId === "string");
330
+ const assignedItems = itemIds?.length ? itemIds.flatMap((itemId) => {
331
+ const item = itemsById.get(itemId);
332
+ return item ? [item] : [];
333
+ }) : splits.length === items.length ? [items[splitIndex]] : [];
334
+ if (!assignedItems.length) return;
335
+ const discounts = split.discounts.flatMap((rawDiscount) => {
336
+ if (!rawDiscount || typeof rawDiscount !== "object") return [];
337
+ const discount = rawDiscount;
338
+ const id = typeof discount.discount_id === "string" ? discount.discount_id : "";
339
+ const name = typeof discount.discount_name === "string" ? discount.discount_name : "";
340
+ const category = categoryFor(name);
341
+ if (!category) return [];
342
+ const customer = transaction.discount_customers?.find(
343
+ (entry) => id && entry.discount_id === id || !id && entry.discount_name === name
344
+ );
345
+ if (!customer) return [];
346
+ const rate = toMoneyNumber(discount.discount_rate) || toMoneyNumber(customer.discount_rate) || (id ? options.discountRatesById?.[id] : 0) || toMoneyNumber(transaction.discount_rate);
347
+ return rate > 0 ? [{ category, rate }] : [];
348
+ });
349
+ if (!discounts.length) return;
350
+ const discountableAmount = assignedItems.reduce((total, item) => {
351
+ const addOns = item.transaction_item_add_ons ?? [];
352
+ const addOnAmount = addOns.reduce((sum, addOn) => sum + toMoneyNumber(addOn.total_amount), 0);
353
+ const itemAmount = Math.max(0, toMoneyNumber(item.total_amount) - addOnAmount);
354
+ return total + (item.is_discountable === false ? 0 : itemAmount) + addOns.reduce(
355
+ (sum, addOn) => sum + (addOn.is_discountable === false ? 0 : toMoneyNumber(addOn.total_amount)),
356
+ 0
357
+ );
358
+ }, 0);
359
+ const netShare = discountableAmount / discounts.length / 1.12;
360
+ for (const entry of discounts) {
361
+ discountWeights[entry.category] += netShare * (entry.rate / 100);
362
+ if (entry.category === "sc" || entry.category === "pwd") vatExemptWeights[entry.category] += netShare;
363
+ }
364
+ });
365
+ return Object.values(discountWeights).some((weight) => weight > 0) ? { discountWeights, vatExemptWeights } : null;
366
+ }
367
+ function discountAllocation(transaction, options) {
368
+ const counts = emptyWeights();
369
+ const rateWeights = emptyWeights();
370
+ for (const customer of transaction.discount_customers ?? []) {
371
+ const category = categoryFor(customer.discount_name ?? "");
372
+ if (!category) continue;
373
+ counts[category] += 1;
374
+ rateWeights[category] += toMoneyNumber(customer.discount_rate) || (customer.discount_id ? options.discountRatesById?.[customer.discount_id] : 0) || toMoneyNumber(transaction.discount_rate) || 1;
375
+ }
376
+ if (Object.values(counts).every((count) => count === 0)) {
377
+ const category = categoryFor(`${transaction.discount_name ?? ""} ${transaction.discount_code ?? ""}`);
378
+ if (category) {
379
+ counts[category] = 1;
380
+ rateWeights[category] = toMoneyNumber(transaction.discount_rate) || 1;
381
+ }
382
+ }
383
+ const itemized = itemizedWeights(transaction, options);
384
+ return {
385
+ counts,
386
+ discountWeights: itemized?.discountWeights ?? rateWeights,
387
+ vatExemptWeights: itemized?.vatExemptWeights
388
+ };
389
+ }
390
+ function voucherVatNet(transaction, voucher, discount) {
391
+ const gross = toMoneyNumber(transaction.gross_amount);
392
+ if (voucher <= 0 || discount <= 0) return format2(gross / 1.12);
393
+ const itemsById = new Map(
394
+ (transaction.transaction_items ?? []).filter((item) => item.id && item.item_status !== "cancelled").map((item) => [item.id, toMoneyNumber(item.total_amount)])
395
+ );
396
+ let itemizedGross = 0;
397
+ for (const rawSplit of Array.isArray(transaction.metadata?.split_discounts) ? transaction.metadata.split_discounts : []) {
398
+ if (!rawSplit || typeof rawSplit !== "object") continue;
399
+ const split = rawSplit;
400
+ if (!split.voucher) continue;
401
+ const shares = Array.isArray(split.item_shares) ? split.item_shares : [];
402
+ if (shares.length) {
403
+ for (const rawShare of shares) {
404
+ if (!rawShare || typeof rawShare !== "object") continue;
405
+ const share = rawShare;
406
+ if (typeof share.item_id !== "string") continue;
407
+ itemizedGross += (itemsById.get(share.item_id) ?? 0) * Math.max(0, Math.min(1, toMoneyNumber(share.share)));
408
+ }
409
+ } else {
410
+ const itemIds = [split.transaction_item_ids, split.item_ids].find(Array.isArray)?.filter((itemId) => typeof itemId === "string");
411
+ for (const itemId of itemIds ?? []) itemizedGross += itemsById.get(itemId) ?? 0;
412
+ }
413
+ }
414
+ const rate = toMoneyNumber(transaction.voucher_rate);
415
+ const percentageGross = transaction.voucher_type?.toLowerCase() === "percentage" && rate > 0 ? voucher / (rate / 100) : 0;
416
+ const eligibleGross = itemizedGross > 0 ? itemizedGross : percentageGross > 0 ? percentageGross : gross;
417
+ return format2(Math.min(gross, eligibleGross) / 1.12);
418
+ }
419
+ function buildTransactionProjection(transaction, options = {}) {
420
+ const status = `${transaction.status} ${transaction.payment_status}`.toLowerCase();
421
+ const state = status.includes("refund") || status.includes("return") ? "RETURNED" : status.includes("cancel") || status.includes("void") ? "CANCELLED" : "SUCCESS";
422
+ const isSuccessful = state === "SUCCESS";
423
+ const gross = toMoneyNumber(transaction.gross_amount);
424
+ const discount = Math.abs(toMoneyNumber(transaction.discount_amount));
425
+ const voucher = Math.abs(toMoneyNumber(transaction.voucher_discount_amount));
426
+ const voucherRate = toMoneyNumber(transaction.voucher_rate);
427
+ const hasVoucher = voucherRate > 0;
428
+ const hasVoucherAndDiscount = isSuccessful && voucher > 0 && discount > 0;
429
+ const scopedVoucherVatNet = voucherVatNet(transaction, voucher, discount);
430
+ const voucherForBirDiscount = -format2(
431
+ transaction.voucher_type?.toLowerCase() === "fixed" ? scopedVoucherVatNet - format2(voucherRate) : scopedVoucherVatNet * format2(voucherRate / 100)
432
+ );
433
+ const otherDiscount = isSuccessful && hasVoucher ? format2(voucherForBirDiscount) : 0;
434
+ const otherDiscountAdjustment = -format2(Math.abs(otherDiscount * 0.12));
435
+ const storedVat = Math.abs(toMoneyNumber(transaction.vat_amount));
436
+ const storedVatSales = toMoneyNumber(transaction.vat_sales);
437
+ const storedVatExemptSales = toMoneyNumber(transaction.vat_exempt_sales);
438
+ const storedZeroRatedSales = toMoneyNumber(transaction.zero_rated_sales);
439
+ const storedNonTaxableSales = toMoneyNumber(transaction.non_taxable_sales);
440
+ const netOfVat = storedVatSales + storedVatExemptSales + storedZeroRatedSales + storedNonTaxableSales || Math.max(0, toMoneyNumber(transaction.amount_payable) - storedVat);
441
+ const returned = state === "RETURNED" ? -format2(netOfVat) : 0;
442
+ const cancelled = state === "CANCELLED" ? -format2(netOfVat) : 0;
443
+ const { counts, discountWeights, vatExemptWeights } = discountAllocation(transaction, options);
444
+ const allocatedDiscount = allocate(discount, discountWeights);
445
+ const deductions = {
446
+ scDiscount: isSuccessful ? -allocatedDiscount.sc : 0,
447
+ pwdDiscount: isSuccessful ? -allocatedDiscount.pwd : 0,
448
+ naacDiscount: isSuccessful ? -allocatedDiscount.naac : 0,
449
+ soloDiscount: isSuccessful ? -allocatedDiscount.solo : 0,
450
+ movDiscount: isSuccessful ? -allocatedDiscount.mov : 0,
451
+ otherDiscount,
452
+ returnAmount: returned,
453
+ voidAmount: cancelled,
454
+ totalDeductions: 0
455
+ };
456
+ deductions.totalDeductions = Object.values(deductions).reduce((total, amount) => total + amount, 0);
457
+ const vatExemptSales = isSuccessful ? storedVatExemptSales : 0;
458
+ const allocatedVatAdjustment = allocate(vatExemptSales * 0.12, {
459
+ ...vatExemptWeights ?? counts,
460
+ naac: 0,
461
+ solo: 0,
462
+ mov: 0
463
+ });
464
+ const adjustments = {
465
+ scDiscountAdjustment: isSuccessful ? -allocatedVatAdjustment.sc : 0,
466
+ pwdDiscountAdjustment: isSuccessful ? -allocatedVatAdjustment.pwd : 0,
467
+ naacDiscountAdjustment: 0,
468
+ soloDiscountAdjustment: 0,
469
+ movDiscountAdjustment: 0,
470
+ otherDiscountAdjustment,
471
+ vatOnReturns: 0,
472
+ vatOnCancelled: 0,
473
+ others: 0,
474
+ totalAdjustments: 0
475
+ };
476
+ const regularVat = storedVat || format2(Math.abs(storedVatExemptSales * 0.12));
477
+ let vatAmount = format2(
478
+ isSuccessful ? voucher > 0 ? Math.abs(scopedVoucherVatNet * 0.12) : regularVat : netOfVat * 0.12
479
+ );
480
+ if (hasVoucherAndDiscount) {
481
+ vatAmount = format2(
482
+ storedVat + Math.abs(adjustments.scDiscountAdjustment) + Math.abs(adjustments.pwdDiscountAdjustment) + Math.abs(adjustments.otherDiscountAdjustment)
483
+ );
484
+ }
485
+ adjustments.vatOnReturns = returned ? -vatAmount : 0;
486
+ adjustments.vatOnCancelled = cancelled ? -vatAmount : 0;
487
+ adjustments.totalAdjustments = Object.values(adjustments).reduce((total, amount) => total + amount, 0);
488
+ const vatableSales = isSuccessful ? hasVoucherAndDiscount ? format2(storedVatSales + Math.abs(otherDiscount)) : voucher > 0 ? scopedVoucherVatNet : storedVatSales : netOfVat;
489
+ const zeroRatedSales = isSuccessful ? storedZeroRatedSales : 0;
490
+ const netSales = isSuccessful ? format2(
491
+ vatableSales + vatExemptSales + zeroRatedSales + storedNonTaxableSales + deductions.scDiscount + deductions.pwdDiscount + deductions.naacDiscount + deductions.soloDiscount + deductions.movDiscount + deductions.otherDiscount
492
+ ) : 0;
493
+ const serviceCharge = toMoneyNumber(transaction.service_charge_amount);
494
+ const takeoutCharge = toMoneyNumber(transaction.takeout_charge_amount);
495
+ return {
496
+ state,
497
+ receipt: {
498
+ grossSales: gross,
499
+ promoDiscount: voucher,
500
+ netOfPromo: format2(gross - voucher),
501
+ netOfVat: format2(storedVatSales + storedVatExemptSales + storedZeroRatedSales + storedNonTaxableSales),
502
+ statutoryDiscount: discount,
503
+ netSales: format2(netOfVat - discount),
504
+ vatSales: storedVatSales,
505
+ vatExemptSales: storedVatExemptSales,
506
+ zeroRatedSales: storedZeroRatedSales,
507
+ nonTaxableSales: storedNonTaxableSales,
508
+ vatAmount: storedVat,
509
+ totalSales: format2(
510
+ storedVatSales + storedVatExemptSales + storedZeroRatedSales + storedNonTaxableSales + storedVat
511
+ ),
512
+ serviceCharge,
513
+ takeoutCharge,
514
+ amountPayable: toMoneyNumber(transaction.amount_payable)
515
+ },
516
+ report: {
517
+ grossSales: gross,
518
+ vatableSales,
519
+ voucherDiscount: voucher,
520
+ vatAmount,
521
+ vatExemptSales,
522
+ zeroRatedSales,
523
+ deductions,
524
+ serviceCharge,
525
+ takeoutCharge,
526
+ adjustments,
527
+ vatPayable: isSuccessful ? Math.max(0, format2(vatAmount - Math.abs(adjustments.totalAdjustments))) : 0,
528
+ netSales,
529
+ totalIncome: netSales
530
+ },
531
+ discountCounts: counts
532
+ };
533
+ }
260
534
  // Annotate the CommonJS export names for ESM import in node:
261
535
  0 && (module.exports = {
536
+ buildTransactionProjection,
262
537
  calculateTotals,
263
538
  clamp,
264
539
  format2,
package/dist/index.d.cts CHANGED
@@ -33,6 +33,7 @@ interface CalculateTotalsInput {
33
33
  */
34
34
  paxDiscountCounts?: PaxDiscountCounts;
35
35
  vatRate: number;
36
+ isVatRegistered?: boolean;
36
37
  serviceChargeRate: number;
37
38
  togoCharge: number;
38
39
  }
@@ -97,4 +98,104 @@ declare function getDiscountBreakdown(discountType: string | null | undefined, d
97
98
  };
98
99
  declare function calculateTotals(input: CalculateTotalsInput): CalculateTotalsResult;
99
100
 
100
- export { type CalculateTotalsInput, type CalculateTotalsResult, type DiscountBreakdown, type DiscountRates, type DiscountType, type PaxDiscountCounts, type VoucherType, calculateTotals, clamp, format2, getDiscountBreakdown, isAddVatAfterDiscount, isVatExemptDiscount, toMoneyNumber };
101
+ type ReportDiscountCategory = "sc" | "pwd" | "naac" | "solo" | "mov";
102
+ type DiscountWeights = Record<ReportDiscountCategory, number>;
103
+ interface FinancialTransactionInput {
104
+ status: string;
105
+ payment_status: string;
106
+ gross_amount: number | string;
107
+ amount_payable: number | string;
108
+ discount_amount?: number | string | null;
109
+ discount_code?: string | null;
110
+ discount_name?: string | null;
111
+ discount_rate?: number | string | null;
112
+ voucher_discount_amount?: number | string | null;
113
+ voucher_rate?: number | string | null;
114
+ voucher_type?: string | null;
115
+ vat_sales?: number | string | null;
116
+ vat_amount?: number | string | null;
117
+ vat_exempt_sales?: number | string | null;
118
+ zero_rated_sales?: number | string | null;
119
+ non_taxable_sales?: number | string | null;
120
+ service_charge_amount?: number | string | null;
121
+ takeout_charge_amount?: number | string | null;
122
+ discount_customers?: Array<{
123
+ discount_id?: string | null;
124
+ discount_name?: string | null;
125
+ discount_rate?: number | string | null;
126
+ }> | null;
127
+ transaction_items?: Array<{
128
+ id?: string;
129
+ total_amount?: number | string | null;
130
+ is_discountable?: boolean | null;
131
+ item_status?: string | null;
132
+ transaction_item_add_ons?: Array<{
133
+ add_on_name?: string | null;
134
+ total_amount?: number | string | null;
135
+ is_discountable?: boolean | null;
136
+ }> | null;
137
+ }> | null;
138
+ metadata?: Record<string, unknown> | null;
139
+ }
140
+ interface TransactionProjectionOptions {
141
+ discountRatesById?: Record<string, number>;
142
+ }
143
+ declare function buildTransactionProjection(transaction: FinancialTransactionInput, options?: TransactionProjectionOptions): {
144
+ state: string;
145
+ receipt: {
146
+ grossSales: number;
147
+ promoDiscount: number;
148
+ netOfPromo: number;
149
+ netOfVat: number;
150
+ statutoryDiscount: number;
151
+ netSales: number;
152
+ vatSales: number;
153
+ vatExemptSales: number;
154
+ zeroRatedSales: number;
155
+ nonTaxableSales: number;
156
+ vatAmount: number;
157
+ totalSales: number;
158
+ serviceCharge: number;
159
+ takeoutCharge: number;
160
+ amountPayable: number;
161
+ };
162
+ report: {
163
+ grossSales: number;
164
+ vatableSales: number;
165
+ voucherDiscount: number;
166
+ vatAmount: number;
167
+ vatExemptSales: number;
168
+ zeroRatedSales: number;
169
+ deductions: {
170
+ scDiscount: number;
171
+ pwdDiscount: number;
172
+ naacDiscount: number;
173
+ soloDiscount: number;
174
+ movDiscount: number;
175
+ otherDiscount: number;
176
+ returnAmount: number;
177
+ voidAmount: number;
178
+ totalDeductions: number;
179
+ };
180
+ serviceCharge: number;
181
+ takeoutCharge: number;
182
+ adjustments: {
183
+ scDiscountAdjustment: number;
184
+ pwdDiscountAdjustment: number;
185
+ naacDiscountAdjustment: number;
186
+ soloDiscountAdjustment: number;
187
+ movDiscountAdjustment: number;
188
+ otherDiscountAdjustment: number;
189
+ vatOnReturns: number;
190
+ vatOnCancelled: number;
191
+ others: number;
192
+ totalAdjustments: number;
193
+ };
194
+ vatPayable: number;
195
+ netSales: number;
196
+ totalIncome: number;
197
+ };
198
+ discountCounts: DiscountWeights;
199
+ };
200
+
201
+ export { type CalculateTotalsInput, type CalculateTotalsResult, type DiscountBreakdown, type DiscountRates, type DiscountType, type FinancialTransactionInput, type PaxDiscountCounts, type ReportDiscountCategory, type TransactionProjectionOptions, type VoucherType, buildTransactionProjection, calculateTotals, clamp, format2, getDiscountBreakdown, isAddVatAfterDiscount, isVatExemptDiscount, toMoneyNumber };
package/dist/index.d.ts CHANGED
@@ -33,6 +33,7 @@ interface CalculateTotalsInput {
33
33
  */
34
34
  paxDiscountCounts?: PaxDiscountCounts;
35
35
  vatRate: number;
36
+ isVatRegistered?: boolean;
36
37
  serviceChargeRate: number;
37
38
  togoCharge: number;
38
39
  }
@@ -97,4 +98,104 @@ declare function getDiscountBreakdown(discountType: string | null | undefined, d
97
98
  };
98
99
  declare function calculateTotals(input: CalculateTotalsInput): CalculateTotalsResult;
99
100
 
100
- export { type CalculateTotalsInput, type CalculateTotalsResult, type DiscountBreakdown, type DiscountRates, type DiscountType, type PaxDiscountCounts, type VoucherType, calculateTotals, clamp, format2, getDiscountBreakdown, isAddVatAfterDiscount, isVatExemptDiscount, toMoneyNumber };
101
+ type ReportDiscountCategory = "sc" | "pwd" | "naac" | "solo" | "mov";
102
+ type DiscountWeights = Record<ReportDiscountCategory, number>;
103
+ interface FinancialTransactionInput {
104
+ status: string;
105
+ payment_status: string;
106
+ gross_amount: number | string;
107
+ amount_payable: number | string;
108
+ discount_amount?: number | string | null;
109
+ discount_code?: string | null;
110
+ discount_name?: string | null;
111
+ discount_rate?: number | string | null;
112
+ voucher_discount_amount?: number | string | null;
113
+ voucher_rate?: number | string | null;
114
+ voucher_type?: string | null;
115
+ vat_sales?: number | string | null;
116
+ vat_amount?: number | string | null;
117
+ vat_exempt_sales?: number | string | null;
118
+ zero_rated_sales?: number | string | null;
119
+ non_taxable_sales?: number | string | null;
120
+ service_charge_amount?: number | string | null;
121
+ takeout_charge_amount?: number | string | null;
122
+ discount_customers?: Array<{
123
+ discount_id?: string | null;
124
+ discount_name?: string | null;
125
+ discount_rate?: number | string | null;
126
+ }> | null;
127
+ transaction_items?: Array<{
128
+ id?: string;
129
+ total_amount?: number | string | null;
130
+ is_discountable?: boolean | null;
131
+ item_status?: string | null;
132
+ transaction_item_add_ons?: Array<{
133
+ add_on_name?: string | null;
134
+ total_amount?: number | string | null;
135
+ is_discountable?: boolean | null;
136
+ }> | null;
137
+ }> | null;
138
+ metadata?: Record<string, unknown> | null;
139
+ }
140
+ interface TransactionProjectionOptions {
141
+ discountRatesById?: Record<string, number>;
142
+ }
143
+ declare function buildTransactionProjection(transaction: FinancialTransactionInput, options?: TransactionProjectionOptions): {
144
+ state: string;
145
+ receipt: {
146
+ grossSales: number;
147
+ promoDiscount: number;
148
+ netOfPromo: number;
149
+ netOfVat: number;
150
+ statutoryDiscount: number;
151
+ netSales: number;
152
+ vatSales: number;
153
+ vatExemptSales: number;
154
+ zeroRatedSales: number;
155
+ nonTaxableSales: number;
156
+ vatAmount: number;
157
+ totalSales: number;
158
+ serviceCharge: number;
159
+ takeoutCharge: number;
160
+ amountPayable: number;
161
+ };
162
+ report: {
163
+ grossSales: number;
164
+ vatableSales: number;
165
+ voucherDiscount: number;
166
+ vatAmount: number;
167
+ vatExemptSales: number;
168
+ zeroRatedSales: number;
169
+ deductions: {
170
+ scDiscount: number;
171
+ pwdDiscount: number;
172
+ naacDiscount: number;
173
+ soloDiscount: number;
174
+ movDiscount: number;
175
+ otherDiscount: number;
176
+ returnAmount: number;
177
+ voidAmount: number;
178
+ totalDeductions: number;
179
+ };
180
+ serviceCharge: number;
181
+ takeoutCharge: number;
182
+ adjustments: {
183
+ scDiscountAdjustment: number;
184
+ pwdDiscountAdjustment: number;
185
+ naacDiscountAdjustment: number;
186
+ soloDiscountAdjustment: number;
187
+ movDiscountAdjustment: number;
188
+ otherDiscountAdjustment: number;
189
+ vatOnReturns: number;
190
+ vatOnCancelled: number;
191
+ others: number;
192
+ totalAdjustments: number;
193
+ };
194
+ vatPayable: number;
195
+ netSales: number;
196
+ totalIncome: number;
197
+ };
198
+ discountCounts: DiscountWeights;
199
+ };
200
+
201
+ export { type CalculateTotalsInput, type CalculateTotalsResult, type DiscountBreakdown, type DiscountRates, type DiscountType, type FinancialTransactionInput, type PaxDiscountCounts, type ReportDiscountCategory, type TransactionProjectionOptions, type VoucherType, buildTransactionProjection, calculateTotals, clamp, format2, getDiscountBreakdown, isAddVatAfterDiscount, isVatExemptDiscount, toMoneyNumber };
package/dist/index.js CHANGED
@@ -36,6 +36,11 @@ var splitDiscountTypes = [
36
36
  { type: "MOV", count: "movCount", breakdown: "mov", vatExempt: false },
37
37
  { type: "SOLO", count: "soloParentCount", breakdown: "solo", vatExempt: false }
38
38
  ];
39
+ function resolvedVatDivisor(input) {
40
+ if (input.isVatRegistered === false) return 1;
41
+ const divisor = Number(input.vatRate || 0) / 100;
42
+ return divisor > 0 ? divisor : 1;
43
+ }
39
44
  function normalizedCount(value) {
40
45
  return Math.max(0, Math.floor(toMoneyNumber(value)));
41
46
  }
@@ -46,7 +51,7 @@ function calculateSplitTotals(input) {
46
51
  const concessionCount = splitDiscountTypes.reduce((total, item) => total + normalizedCount(counts[item.count]), 0);
47
52
  const paxCount = regularCount + concessionCount;
48
53
  if (paxCount === 0) return null;
49
- const vatDivisor = Number(input.vatRate || 0) / 100;
54
+ const vatDivisor = resolvedVatDivisor(input);
50
55
  const vatRate = Math.max(0, vatDivisor - 1);
51
56
  const scRate = Number(input.serviceChargeRate || 0) / 100;
52
57
  const discountableAmount = Math.max(0, toMoneyNumber(input.discountableTotalAmount));
@@ -76,6 +81,10 @@ function calculateSplitTotals(input) {
76
81
  discountBreakdown[item.breakdown] = -discount;
77
82
  totalDiscount += discount;
78
83
  }
84
+ if (input.isVatRegistered === false) {
85
+ vatExemptSales += vatSales;
86
+ vatSales = 0;
87
+ }
79
88
  vatSales = format2(vatSales);
80
89
  vatExemptSales = format2(vatExemptSales);
81
90
  const discounted = -format2(totalDiscount);
@@ -85,12 +94,12 @@ function calculateSplitTotals(input) {
85
94
  const serviceCharge = input.serviceChargeRate > 0 ? format2(serviceChargeBase / vatDivisor * scRate) : 0;
86
95
  const togoCharge = input.togoableTotalAmount > 0 ? input.togoCharge : 0;
87
96
  const totalAmount = format2(Math.max(0, vatNet + vat + serviceCharge + togoCharge));
88
- const voucherVatNet = format2(totalOrderAmount / vatDivisor);
89
- const voucherVat = format2(voucherVatNet * vatRate);
97
+ const voucherVatNet2 = format2(totalOrderAmount / vatDivisor);
98
+ const voucherVat = format2(voucherVatNet2 * vatRate);
90
99
  return {
91
100
  quantity: input.quantity,
92
101
  subtotal,
93
- isAddVat: splitDiscountTypes.some((item) => !item.vatExempt && normalizedCount(counts[item.count]) > 0),
102
+ isAddVat: input.isVatRegistered !== false && splitDiscountTypes.some((item) => !item.vatExempt && normalizedCount(counts[item.count]) > 0),
94
103
  vatNet,
95
104
  vat,
96
105
  discounted,
@@ -105,9 +114,9 @@ function calculateSplitTotals(input) {
105
114
  voucher: {
106
115
  discount: voucherDiscounted,
107
116
  net: subtotal,
108
- vatNet: voucherVatNet,
117
+ vatNet: voucherVatNet2,
109
118
  vat: voucherVat,
110
- forBirDiscount: -(input.voucherType === "FIXED" ? voucherVatNet - Number(voucherRate.toFixed(2)) : voucherVatNet * Number(voucherRate.toFixed(2)))
119
+ forBirDiscount: -(input.voucherType === "FIXED" ? voucherVatNet2 - Number(voucherRate.toFixed(2)) : voucherVatNet2 * Number(voucherRate.toFixed(2)))
111
120
  },
112
121
  hasDiscount: totalDiscount > 0,
113
122
  discount: {
@@ -137,7 +146,8 @@ function calculateSplitTotals(input) {
137
146
  function calculateTotals(input) {
138
147
  const splitTotals = calculateSplitTotals(input);
139
148
  if (splitTotals) return splitTotals;
140
- const vatRate = Number(input.vatRate || 0) / 100;
149
+ const vatRate = resolvedVatDivisor(input);
150
+ const vatPercent = Math.max(0, vatRate - 1);
141
151
  const scRate = Number(input.serviceChargeRate || 0) / 100;
142
152
  const discountRate = Number(input.discountRate || 0) / 100;
143
153
  const voucherRate = input.voucherType === "FIXED" ? input.voucherRate : Number(input.voucherRate || 0) / 100;
@@ -151,12 +161,12 @@ function calculateTotals(input) {
151
161
  }
152
162
  }
153
163
  const subtotal = totalOrderAmount + voucherDiscounted;
154
- const voucherVatNet = format2(totalOrderAmount / vatRate);
155
- const voucherVat = format2(voucherVatNet * 0.12);
164
+ const voucherVatNet2 = format2(totalOrderAmount / vatRate);
165
+ const voucherVat = format2(voucherVatNet2 * vatPercent);
156
166
  let vatNet = format2(subtotal / vatRate);
157
- let vat = format2(vatNet * 0.12);
167
+ let vat = format2(vatNet * vatPercent);
158
168
  const isSpecial = discountRate > 0;
159
- const isAddVat = isAddVatAfterDiscount(input.discountType);
169
+ const isAddVat = input.isVatRegistered === false ? false : isAddVatAfterDiscount(input.discountType);
160
170
  const discounted = format2(discountRate * -vatNet);
161
171
  if (isSpecial) {
162
172
  vatNet = format2(vatNet + discounted);
@@ -196,9 +206,9 @@ function calculateTotals(input) {
196
206
  voucher: {
197
207
  discount: voucherDiscounted,
198
208
  net: subtotal,
199
- vatNet: voucherVatNet,
209
+ vatNet: voucherVatNet2,
200
210
  vat: voucherVat,
201
- forBirDiscount: -(input.voucherType === "FIXED" ? voucherVatNet - Number(voucherRate.toFixed(2)) : voucherVatNet * Number(voucherRate.toFixed(2)))
211
+ forBirDiscount: -(input.voucherType === "FIXED" ? voucherVatNet2 - Number(voucherRate.toFixed(2)) : voucherVatNet2 * Number(voucherRate.toFixed(2)))
202
212
  },
203
213
  hasDiscount: discountRate > 0,
204
214
  discount: {
@@ -225,7 +235,271 @@ function calculateTotals(input) {
225
235
  }
226
236
  };
227
237
  }
238
+
239
+ // src/transaction-projection.ts
240
+ var emptyWeights = () => ({ sc: 0, pwd: 0, naac: 0, solo: 0, mov: 0 });
241
+ function categoryFor(value) {
242
+ const normalized = value.toLowerCase();
243
+ if (normalized.includes("senior")) return "sc";
244
+ if (normalized.includes("pwd") || normalized.includes("disability")) return "pwd";
245
+ if (normalized.includes("naac") || normalized.includes("athlete")) return "naac";
246
+ if (normalized.includes("solo")) return "solo";
247
+ if (normalized.includes("mov") || normalized.includes("valor") || normalized.includes("uniformed")) return "mov";
248
+ return null;
249
+ }
250
+ function allocate(amount, weights) {
251
+ const allocations = emptyWeights();
252
+ const categories = Object.keys(weights).filter((category) => weights[category] > 0);
253
+ if (categories.length === 1) {
254
+ allocations[categories[0]] = format2(amount);
255
+ return allocations;
256
+ }
257
+ const totalWeight = categories.reduce((total, category) => total + weights[category], 0);
258
+ let allocated = 0;
259
+ categories.forEach((category, index) => {
260
+ const share = index === categories.length - 1 ? format2(amount - allocated) : format2(amount * weights[category] / totalWeight);
261
+ allocations[category] = share;
262
+ allocated += share;
263
+ });
264
+ return allocations;
265
+ }
266
+ function itemizedWeights(transaction, options) {
267
+ const splits = transaction.metadata?.split_discounts;
268
+ if (!Array.isArray(splits)) return null;
269
+ const discountWeights = emptyWeights();
270
+ const vatExemptWeights = emptyWeights();
271
+ for (const rawSplit of splits) {
272
+ if (!rawSplit || typeof rawSplit !== "object") continue;
273
+ const discounts = rawSplit.discounts;
274
+ if (!Array.isArray(discounts)) continue;
275
+ for (const rawDiscount of discounts) {
276
+ if (!rawDiscount || typeof rawDiscount !== "object") continue;
277
+ const discount = rawDiscount;
278
+ const category = categoryFor(typeof discount.discount_name === "string" ? discount.discount_name : "");
279
+ if (!category) continue;
280
+ discountWeights[category] += Math.abs(toMoneyNumber(discount.discount_amount));
281
+ vatExemptWeights[category] += Math.abs(toMoneyNumber(discount.vat_exempt_sales));
282
+ }
283
+ }
284
+ if (Object.values(discountWeights).some((weight) => weight > 0)) {
285
+ return {
286
+ discountWeights,
287
+ vatExemptWeights: Object.values(vatExemptWeights).some((weight) => weight > 0) ? vatExemptWeights : { ...emptyWeights(), sc: discountWeights.sc, pwd: discountWeights.pwd }
288
+ };
289
+ }
290
+ const items = (transaction.transaction_items ?? []).filter((item) => item.item_status !== "cancelled");
291
+ const itemsById = new Map(items.filter((item) => item.id).map((item) => [item.id, item]));
292
+ splits.forEach((rawSplit, splitIndex) => {
293
+ if (!rawSplit || typeof rawSplit !== "object") return;
294
+ const split = rawSplit;
295
+ if (!Array.isArray(split.discounts)) return;
296
+ const itemIds = [split.transaction_item_ids, split.item_ids].find(Array.isArray)?.filter((itemId) => typeof itemId === "string");
297
+ const assignedItems = itemIds?.length ? itemIds.flatMap((itemId) => {
298
+ const item = itemsById.get(itemId);
299
+ return item ? [item] : [];
300
+ }) : splits.length === items.length ? [items[splitIndex]] : [];
301
+ if (!assignedItems.length) return;
302
+ const discounts = split.discounts.flatMap((rawDiscount) => {
303
+ if (!rawDiscount || typeof rawDiscount !== "object") return [];
304
+ const discount = rawDiscount;
305
+ const id = typeof discount.discount_id === "string" ? discount.discount_id : "";
306
+ const name = typeof discount.discount_name === "string" ? discount.discount_name : "";
307
+ const category = categoryFor(name);
308
+ if (!category) return [];
309
+ const customer = transaction.discount_customers?.find(
310
+ (entry) => id && entry.discount_id === id || !id && entry.discount_name === name
311
+ );
312
+ if (!customer) return [];
313
+ const rate = toMoneyNumber(discount.discount_rate) || toMoneyNumber(customer.discount_rate) || (id ? options.discountRatesById?.[id] : 0) || toMoneyNumber(transaction.discount_rate);
314
+ return rate > 0 ? [{ category, rate }] : [];
315
+ });
316
+ if (!discounts.length) return;
317
+ const discountableAmount = assignedItems.reduce((total, item) => {
318
+ const addOns = item.transaction_item_add_ons ?? [];
319
+ const addOnAmount = addOns.reduce((sum, addOn) => sum + toMoneyNumber(addOn.total_amount), 0);
320
+ const itemAmount = Math.max(0, toMoneyNumber(item.total_amount) - addOnAmount);
321
+ return total + (item.is_discountable === false ? 0 : itemAmount) + addOns.reduce(
322
+ (sum, addOn) => sum + (addOn.is_discountable === false ? 0 : toMoneyNumber(addOn.total_amount)),
323
+ 0
324
+ );
325
+ }, 0);
326
+ const netShare = discountableAmount / discounts.length / 1.12;
327
+ for (const entry of discounts) {
328
+ discountWeights[entry.category] += netShare * (entry.rate / 100);
329
+ if (entry.category === "sc" || entry.category === "pwd") vatExemptWeights[entry.category] += netShare;
330
+ }
331
+ });
332
+ return Object.values(discountWeights).some((weight) => weight > 0) ? { discountWeights, vatExemptWeights } : null;
333
+ }
334
+ function discountAllocation(transaction, options) {
335
+ const counts = emptyWeights();
336
+ const rateWeights = emptyWeights();
337
+ for (const customer of transaction.discount_customers ?? []) {
338
+ const category = categoryFor(customer.discount_name ?? "");
339
+ if (!category) continue;
340
+ counts[category] += 1;
341
+ rateWeights[category] += toMoneyNumber(customer.discount_rate) || (customer.discount_id ? options.discountRatesById?.[customer.discount_id] : 0) || toMoneyNumber(transaction.discount_rate) || 1;
342
+ }
343
+ if (Object.values(counts).every((count) => count === 0)) {
344
+ const category = categoryFor(`${transaction.discount_name ?? ""} ${transaction.discount_code ?? ""}`);
345
+ if (category) {
346
+ counts[category] = 1;
347
+ rateWeights[category] = toMoneyNumber(transaction.discount_rate) || 1;
348
+ }
349
+ }
350
+ const itemized = itemizedWeights(transaction, options);
351
+ return {
352
+ counts,
353
+ discountWeights: itemized?.discountWeights ?? rateWeights,
354
+ vatExemptWeights: itemized?.vatExemptWeights
355
+ };
356
+ }
357
+ function voucherVatNet(transaction, voucher, discount) {
358
+ const gross = toMoneyNumber(transaction.gross_amount);
359
+ if (voucher <= 0 || discount <= 0) return format2(gross / 1.12);
360
+ const itemsById = new Map(
361
+ (transaction.transaction_items ?? []).filter((item) => item.id && item.item_status !== "cancelled").map((item) => [item.id, toMoneyNumber(item.total_amount)])
362
+ );
363
+ let itemizedGross = 0;
364
+ for (const rawSplit of Array.isArray(transaction.metadata?.split_discounts) ? transaction.metadata.split_discounts : []) {
365
+ if (!rawSplit || typeof rawSplit !== "object") continue;
366
+ const split = rawSplit;
367
+ if (!split.voucher) continue;
368
+ const shares = Array.isArray(split.item_shares) ? split.item_shares : [];
369
+ if (shares.length) {
370
+ for (const rawShare of shares) {
371
+ if (!rawShare || typeof rawShare !== "object") continue;
372
+ const share = rawShare;
373
+ if (typeof share.item_id !== "string") continue;
374
+ itemizedGross += (itemsById.get(share.item_id) ?? 0) * Math.max(0, Math.min(1, toMoneyNumber(share.share)));
375
+ }
376
+ } else {
377
+ const itemIds = [split.transaction_item_ids, split.item_ids].find(Array.isArray)?.filter((itemId) => typeof itemId === "string");
378
+ for (const itemId of itemIds ?? []) itemizedGross += itemsById.get(itemId) ?? 0;
379
+ }
380
+ }
381
+ const rate = toMoneyNumber(transaction.voucher_rate);
382
+ const percentageGross = transaction.voucher_type?.toLowerCase() === "percentage" && rate > 0 ? voucher / (rate / 100) : 0;
383
+ const eligibleGross = itemizedGross > 0 ? itemizedGross : percentageGross > 0 ? percentageGross : gross;
384
+ return format2(Math.min(gross, eligibleGross) / 1.12);
385
+ }
386
+ function buildTransactionProjection(transaction, options = {}) {
387
+ const status = `${transaction.status} ${transaction.payment_status}`.toLowerCase();
388
+ const state = status.includes("refund") || status.includes("return") ? "RETURNED" : status.includes("cancel") || status.includes("void") ? "CANCELLED" : "SUCCESS";
389
+ const isSuccessful = state === "SUCCESS";
390
+ const gross = toMoneyNumber(transaction.gross_amount);
391
+ const discount = Math.abs(toMoneyNumber(transaction.discount_amount));
392
+ const voucher = Math.abs(toMoneyNumber(transaction.voucher_discount_amount));
393
+ const voucherRate = toMoneyNumber(transaction.voucher_rate);
394
+ const hasVoucher = voucherRate > 0;
395
+ const hasVoucherAndDiscount = isSuccessful && voucher > 0 && discount > 0;
396
+ const scopedVoucherVatNet = voucherVatNet(transaction, voucher, discount);
397
+ const voucherForBirDiscount = -format2(
398
+ transaction.voucher_type?.toLowerCase() === "fixed" ? scopedVoucherVatNet - format2(voucherRate) : scopedVoucherVatNet * format2(voucherRate / 100)
399
+ );
400
+ const otherDiscount = isSuccessful && hasVoucher ? format2(voucherForBirDiscount) : 0;
401
+ const otherDiscountAdjustment = -format2(Math.abs(otherDiscount * 0.12));
402
+ const storedVat = Math.abs(toMoneyNumber(transaction.vat_amount));
403
+ const storedVatSales = toMoneyNumber(transaction.vat_sales);
404
+ const storedVatExemptSales = toMoneyNumber(transaction.vat_exempt_sales);
405
+ const storedZeroRatedSales = toMoneyNumber(transaction.zero_rated_sales);
406
+ const storedNonTaxableSales = toMoneyNumber(transaction.non_taxable_sales);
407
+ const netOfVat = storedVatSales + storedVatExemptSales + storedZeroRatedSales + storedNonTaxableSales || Math.max(0, toMoneyNumber(transaction.amount_payable) - storedVat);
408
+ const returned = state === "RETURNED" ? -format2(netOfVat) : 0;
409
+ const cancelled = state === "CANCELLED" ? -format2(netOfVat) : 0;
410
+ const { counts, discountWeights, vatExemptWeights } = discountAllocation(transaction, options);
411
+ const allocatedDiscount = allocate(discount, discountWeights);
412
+ const deductions = {
413
+ scDiscount: isSuccessful ? -allocatedDiscount.sc : 0,
414
+ pwdDiscount: isSuccessful ? -allocatedDiscount.pwd : 0,
415
+ naacDiscount: isSuccessful ? -allocatedDiscount.naac : 0,
416
+ soloDiscount: isSuccessful ? -allocatedDiscount.solo : 0,
417
+ movDiscount: isSuccessful ? -allocatedDiscount.mov : 0,
418
+ otherDiscount,
419
+ returnAmount: returned,
420
+ voidAmount: cancelled,
421
+ totalDeductions: 0
422
+ };
423
+ deductions.totalDeductions = Object.values(deductions).reduce((total, amount) => total + amount, 0);
424
+ const vatExemptSales = isSuccessful ? storedVatExemptSales : 0;
425
+ const allocatedVatAdjustment = allocate(vatExemptSales * 0.12, {
426
+ ...vatExemptWeights ?? counts,
427
+ naac: 0,
428
+ solo: 0,
429
+ mov: 0
430
+ });
431
+ const adjustments = {
432
+ scDiscountAdjustment: isSuccessful ? -allocatedVatAdjustment.sc : 0,
433
+ pwdDiscountAdjustment: isSuccessful ? -allocatedVatAdjustment.pwd : 0,
434
+ naacDiscountAdjustment: 0,
435
+ soloDiscountAdjustment: 0,
436
+ movDiscountAdjustment: 0,
437
+ otherDiscountAdjustment,
438
+ vatOnReturns: 0,
439
+ vatOnCancelled: 0,
440
+ others: 0,
441
+ totalAdjustments: 0
442
+ };
443
+ const regularVat = storedVat || format2(Math.abs(storedVatExemptSales * 0.12));
444
+ let vatAmount = format2(
445
+ isSuccessful ? voucher > 0 ? Math.abs(scopedVoucherVatNet * 0.12) : regularVat : netOfVat * 0.12
446
+ );
447
+ if (hasVoucherAndDiscount) {
448
+ vatAmount = format2(
449
+ storedVat + Math.abs(adjustments.scDiscountAdjustment) + Math.abs(adjustments.pwdDiscountAdjustment) + Math.abs(adjustments.otherDiscountAdjustment)
450
+ );
451
+ }
452
+ adjustments.vatOnReturns = returned ? -vatAmount : 0;
453
+ adjustments.vatOnCancelled = cancelled ? -vatAmount : 0;
454
+ adjustments.totalAdjustments = Object.values(adjustments).reduce((total, amount) => total + amount, 0);
455
+ const vatableSales = isSuccessful ? hasVoucherAndDiscount ? format2(storedVatSales + Math.abs(otherDiscount)) : voucher > 0 ? scopedVoucherVatNet : storedVatSales : netOfVat;
456
+ const zeroRatedSales = isSuccessful ? storedZeroRatedSales : 0;
457
+ const netSales = isSuccessful ? format2(
458
+ vatableSales + vatExemptSales + zeroRatedSales + storedNonTaxableSales + deductions.scDiscount + deductions.pwdDiscount + deductions.naacDiscount + deductions.soloDiscount + deductions.movDiscount + deductions.otherDiscount
459
+ ) : 0;
460
+ const serviceCharge = toMoneyNumber(transaction.service_charge_amount);
461
+ const takeoutCharge = toMoneyNumber(transaction.takeout_charge_amount);
462
+ return {
463
+ state,
464
+ receipt: {
465
+ grossSales: gross,
466
+ promoDiscount: voucher,
467
+ netOfPromo: format2(gross - voucher),
468
+ netOfVat: format2(storedVatSales + storedVatExemptSales + storedZeroRatedSales + storedNonTaxableSales),
469
+ statutoryDiscount: discount,
470
+ netSales: format2(netOfVat - discount),
471
+ vatSales: storedVatSales,
472
+ vatExemptSales: storedVatExemptSales,
473
+ zeroRatedSales: storedZeroRatedSales,
474
+ nonTaxableSales: storedNonTaxableSales,
475
+ vatAmount: storedVat,
476
+ totalSales: format2(
477
+ storedVatSales + storedVatExemptSales + storedZeroRatedSales + storedNonTaxableSales + storedVat
478
+ ),
479
+ serviceCharge,
480
+ takeoutCharge,
481
+ amountPayable: toMoneyNumber(transaction.amount_payable)
482
+ },
483
+ report: {
484
+ grossSales: gross,
485
+ vatableSales,
486
+ voucherDiscount: voucher,
487
+ vatAmount,
488
+ vatExemptSales,
489
+ zeroRatedSales,
490
+ deductions,
491
+ serviceCharge,
492
+ takeoutCharge,
493
+ adjustments,
494
+ vatPayable: isSuccessful ? Math.max(0, format2(vatAmount - Math.abs(adjustments.totalAdjustments))) : 0,
495
+ netSales,
496
+ totalIncome: netSales
497
+ },
498
+ discountCounts: counts
499
+ };
500
+ }
228
501
  export {
502
+ buildTransactionProjection,
229
503
  calculateTotals,
230
504
  clamp,
231
505
  format2,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qrwise/pos-calculations",
3
- "version": "0.0.18",
3
+ "version": "0.0.20",
4
4
  "description": "Shared POS calculations for QR Wise POS",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -19,6 +19,7 @@
19
19
  },
20
20
  "scripts": {
21
21
  "build": "tsup src/index.ts --format esm,cjs --dts --clean --tsconfig tsconfig.json",
22
+ "prepare": "npm run build",
22
23
  "test": "npm run build && node --test test/*.test.mjs",
23
24
  "prepublishOnly": "npm run build",
24
25
  "release:patch": "npm version patch && npm publish",