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