@qrwise/pos-calculations 0.0.18 → 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 +273 -8
- package/dist/index.d.cts +101 -1
- package/dist/index.d.ts +101 -1
- package/dist/index.js +272 -8
- 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,
|
|
@@ -117,8 +118,8 @@ function calculateSplitTotals(input) {
|
|
|
117
118
|
const serviceCharge = input.serviceChargeRate > 0 ? format2(serviceChargeBase / vatDivisor * scRate) : 0;
|
|
118
119
|
const togoCharge = input.togoableTotalAmount > 0 ? input.togoCharge : 0;
|
|
119
120
|
const totalAmount = format2(Math.max(0, vatNet + vat + serviceCharge + togoCharge));
|
|
120
|
-
const
|
|
121
|
-
const voucherVat = format2(
|
|
121
|
+
const voucherVatNet2 = format2(totalOrderAmount / vatDivisor);
|
|
122
|
+
const voucherVat = format2(voucherVatNet2 * vatRate);
|
|
122
123
|
return {
|
|
123
124
|
quantity: input.quantity,
|
|
124
125
|
subtotal,
|
|
@@ -137,9 +138,9 @@ function calculateSplitTotals(input) {
|
|
|
137
138
|
voucher: {
|
|
138
139
|
discount: voucherDiscounted,
|
|
139
140
|
net: subtotal,
|
|
140
|
-
vatNet:
|
|
141
|
+
vatNet: voucherVatNet2,
|
|
141
142
|
vat: voucherVat,
|
|
142
|
-
forBirDiscount: -(input.voucherType === "FIXED" ?
|
|
143
|
+
forBirDiscount: -(input.voucherType === "FIXED" ? voucherVatNet2 - Number(voucherRate.toFixed(2)) : voucherVatNet2 * Number(voucherRate.toFixed(2)))
|
|
143
144
|
},
|
|
144
145
|
hasDiscount: totalDiscount > 0,
|
|
145
146
|
discount: {
|
|
@@ -183,8 +184,8 @@ function calculateTotals(input) {
|
|
|
183
184
|
}
|
|
184
185
|
}
|
|
185
186
|
const subtotal = totalOrderAmount + voucherDiscounted;
|
|
186
|
-
const
|
|
187
|
-
const voucherVat = format2(
|
|
187
|
+
const voucherVatNet2 = format2(totalOrderAmount / vatRate);
|
|
188
|
+
const voucherVat = format2(voucherVatNet2 * 0.12);
|
|
188
189
|
let vatNet = format2(subtotal / vatRate);
|
|
189
190
|
let vat = format2(vatNet * 0.12);
|
|
190
191
|
const isSpecial = discountRate > 0;
|
|
@@ -228,9 +229,9 @@ function calculateTotals(input) {
|
|
|
228
229
|
voucher: {
|
|
229
230
|
discount: voucherDiscounted,
|
|
230
231
|
net: subtotal,
|
|
231
|
-
vatNet:
|
|
232
|
+
vatNet: voucherVatNet2,
|
|
232
233
|
vat: voucherVat,
|
|
233
|
-
forBirDiscount: -(input.voucherType === "FIXED" ?
|
|
234
|
+
forBirDiscount: -(input.voucherType === "FIXED" ? voucherVatNet2 - Number(voucherRate.toFixed(2)) : voucherVatNet2 * Number(voucherRate.toFixed(2)))
|
|
234
235
|
},
|
|
235
236
|
hasDiscount: discountRate > 0,
|
|
236
237
|
discount: {
|
|
@@ -257,8 +258,272 @@ function calculateTotals(input) {
|
|
|
257
258
|
}
|
|
258
259
|
};
|
|
259
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
|
+
}
|
|
260
524
|
// Annotate the CommonJS export names for ESM import in node:
|
|
261
525
|
0 && (module.exports = {
|
|
526
|
+
buildTransactionProjection,
|
|
262
527
|
calculateTotals,
|
|
263
528
|
clamp,
|
|
264
529
|
format2,
|
package/dist/index.d.cts
CHANGED
|
@@ -97,4 +97,104 @@ declare function getDiscountBreakdown(discountType: string | null | undefined, d
|
|
|
97
97
|
};
|
|
98
98
|
declare function calculateTotals(input: CalculateTotalsInput): CalculateTotalsResult;
|
|
99
99
|
|
|
100
|
-
|
|
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
|
@@ -97,4 +97,104 @@ declare function getDiscountBreakdown(discountType: string | null | undefined, d
|
|
|
97
97
|
};
|
|
98
98
|
declare function calculateTotals(input: CalculateTotalsInput): CalculateTotalsResult;
|
|
99
99
|
|
|
100
|
-
|
|
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
|
@@ -85,8 +85,8 @@ function calculateSplitTotals(input) {
|
|
|
85
85
|
const serviceCharge = input.serviceChargeRate > 0 ? format2(serviceChargeBase / vatDivisor * scRate) : 0;
|
|
86
86
|
const togoCharge = input.togoableTotalAmount > 0 ? input.togoCharge : 0;
|
|
87
87
|
const totalAmount = format2(Math.max(0, vatNet + vat + serviceCharge + togoCharge));
|
|
88
|
-
const
|
|
89
|
-
const voucherVat = format2(
|
|
88
|
+
const voucherVatNet2 = format2(totalOrderAmount / vatDivisor);
|
|
89
|
+
const voucherVat = format2(voucherVatNet2 * vatRate);
|
|
90
90
|
return {
|
|
91
91
|
quantity: input.quantity,
|
|
92
92
|
subtotal,
|
|
@@ -105,9 +105,9 @@ function calculateSplitTotals(input) {
|
|
|
105
105
|
voucher: {
|
|
106
106
|
discount: voucherDiscounted,
|
|
107
107
|
net: subtotal,
|
|
108
|
-
vatNet:
|
|
108
|
+
vatNet: voucherVatNet2,
|
|
109
109
|
vat: voucherVat,
|
|
110
|
-
forBirDiscount: -(input.voucherType === "FIXED" ?
|
|
110
|
+
forBirDiscount: -(input.voucherType === "FIXED" ? voucherVatNet2 - Number(voucherRate.toFixed(2)) : voucherVatNet2 * Number(voucherRate.toFixed(2)))
|
|
111
111
|
},
|
|
112
112
|
hasDiscount: totalDiscount > 0,
|
|
113
113
|
discount: {
|
|
@@ -151,8 +151,8 @@ function calculateTotals(input) {
|
|
|
151
151
|
}
|
|
152
152
|
}
|
|
153
153
|
const subtotal = totalOrderAmount + voucherDiscounted;
|
|
154
|
-
const
|
|
155
|
-
const voucherVat = format2(
|
|
154
|
+
const voucherVatNet2 = format2(totalOrderAmount / vatRate);
|
|
155
|
+
const voucherVat = format2(voucherVatNet2 * 0.12);
|
|
156
156
|
let vatNet = format2(subtotal / vatRate);
|
|
157
157
|
let vat = format2(vatNet * 0.12);
|
|
158
158
|
const isSpecial = discountRate > 0;
|
|
@@ -196,9 +196,9 @@ function calculateTotals(input) {
|
|
|
196
196
|
voucher: {
|
|
197
197
|
discount: voucherDiscounted,
|
|
198
198
|
net: subtotal,
|
|
199
|
-
vatNet:
|
|
199
|
+
vatNet: voucherVatNet2,
|
|
200
200
|
vat: voucherVat,
|
|
201
|
-
forBirDiscount: -(input.voucherType === "FIXED" ?
|
|
201
|
+
forBirDiscount: -(input.voucherType === "FIXED" ? voucherVatNet2 - Number(voucherRate.toFixed(2)) : voucherVatNet2 * Number(voucherRate.toFixed(2)))
|
|
202
202
|
},
|
|
203
203
|
hasDiscount: discountRate > 0,
|
|
204
204
|
discount: {
|
|
@@ -225,7 +225,271 @@ function calculateTotals(input) {
|
|
|
225
225
|
}
|
|
226
226
|
};
|
|
227
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
|
+
}
|
|
228
491
|
export {
|
|
492
|
+
buildTransactionProjection,
|
|
229
493
|
calculateTotals,
|
|
230
494
|
clamp,
|
|
231
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,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",
|