@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 +288 -13
- package/dist/index.d.cts +102 -1
- package/dist/index.d.ts +102 -1
- package/dist/index.js +287 -13
- package/package.json +2 -1
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 =
|
|
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
|
|
121
|
-
const voucherVat = format2(
|
|
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:
|
|
150
|
+
vatNet: voucherVatNet2,
|
|
141
151
|
vat: voucherVat,
|
|
142
|
-
forBirDiscount: -(input.voucherType === "FIXED" ?
|
|
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 =
|
|
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
|
|
187
|
-
const voucherVat = format2(
|
|
197
|
+
const voucherVatNet2 = format2(totalOrderAmount / vatRate);
|
|
198
|
+
const voucherVat = format2(voucherVatNet2 * vatPercent);
|
|
188
199
|
let vatNet = format2(subtotal / vatRate);
|
|
189
|
-
let vat = format2(vatNet *
|
|
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:
|
|
242
|
+
vatNet: voucherVatNet2,
|
|
232
243
|
vat: voucherVat,
|
|
233
|
-
forBirDiscount: -(input.voucherType === "FIXED" ?
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
89
|
-
const voucherVat = format2(
|
|
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:
|
|
117
|
+
vatNet: voucherVatNet2,
|
|
109
118
|
vat: voucherVat,
|
|
110
|
-
forBirDiscount: -(input.voucherType === "FIXED" ?
|
|
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 =
|
|
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
|
|
155
|
-
const voucherVat = format2(
|
|
164
|
+
const voucherVatNet2 = format2(totalOrderAmount / vatRate);
|
|
165
|
+
const voucherVat = format2(voucherVatNet2 * vatPercent);
|
|
156
166
|
let vatNet = format2(subtotal / vatRate);
|
|
157
|
-
let vat = format2(vatNet *
|
|
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:
|
|
209
|
+
vatNet: voucherVatNet2,
|
|
200
210
|
vat: voucherVat,
|
|
201
|
-
forBirDiscount: -(input.voucherType === "FIXED" ?
|
|
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.
|
|
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",
|