@qrwise/pos-calculations 0.0.17 → 0.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -61,7 +61,114 @@ function getDiscountBreakdown(discountType, discountedAmount) {
61
61
  mov: includesAny(value, ["uniformed", "uniformed personnel", "mov"]) ? discountedAmount : 0
62
62
  };
63
63
  }
64
+ var splitDiscountTypes = [
65
+ { type: "SENIOR", count: "seniorCount", breakdown: "sc", vatExempt: true },
66
+ { type: "PWD", count: "pwdCount", breakdown: "pwd", vatExempt: true },
67
+ { type: "NAAC", count: "naacCount", breakdown: "naac", vatExempt: false },
68
+ { type: "MOV", count: "movCount", breakdown: "mov", vatExempt: false },
69
+ { type: "SOLO", count: "soloParentCount", breakdown: "solo", vatExempt: false }
70
+ ];
71
+ function normalizedCount(value) {
72
+ return Math.max(0, Math.floor(toMoneyNumber(value)));
73
+ }
74
+ function calculateSplitTotals(input) {
75
+ if (!input.paxDiscountCounts) return null;
76
+ const counts = input.paxDiscountCounts;
77
+ const regularCount = normalizedCount(counts.regularCount);
78
+ const concessionCount = splitDiscountTypes.reduce((total, item) => total + normalizedCount(counts[item.count]), 0);
79
+ const paxCount = regularCount + concessionCount;
80
+ if (paxCount === 0) return null;
81
+ const vatDivisor = Number(input.vatRate || 0) / 100;
82
+ const vatRate = Math.max(0, vatDivisor - 1);
83
+ const scRate = Number(input.serviceChargeRate || 0) / 100;
84
+ const discountableAmount = Math.max(0, toMoneyNumber(input.discountableTotalAmount));
85
+ const nonDiscountableAmount = Math.max(0, toMoneyNumber(input.nonDiscountableTotalAmount));
86
+ const totalOrderAmount = discountableAmount + nonDiscountableAmount;
87
+ const voucherRate = input.voucherType === "FIXED" ? input.voucherRate : Number(input.voucherRate || 0) / 100;
88
+ let voucherDiscounted = 0;
89
+ if (voucherRate > 0) {
90
+ voucherDiscounted = input.voucherType === "FIXED" ? -format2(Math.min(toMoneyNumber(voucherRate), totalOrderAmount)) : -format2(totalOrderAmount * voucherRate);
91
+ }
92
+ const subtotal = format2(totalOrderAmount + voucherDiscounted);
93
+ const discountableSubtotal = Math.max(0, discountableAmount - Math.abs(voucherDiscounted));
94
+ const nonDiscountableSubtotal = Math.max(0, subtotal - discountableSubtotal);
95
+ const discountableShare = discountableSubtotal / paxCount;
96
+ const discountBreakdown = { sc: 0, pwd: 0, solo: 0, naac: 0, mov: 0 };
97
+ let vatSales = (nonDiscountableSubtotal + discountableShare * regularCount) / vatDivisor;
98
+ let vatExemptSales = 0;
99
+ let totalDiscount = 0;
100
+ for (const item of splitDiscountTypes) {
101
+ const count = normalizedCount(counts[item.count]);
102
+ if (count === 0) continue;
103
+ const netShare = discountableShare * count / vatDivisor;
104
+ const rate = Number(input.discountRates?.[item.type] ?? input.discountRate ?? 0) / 100;
105
+ const discount = format2(netShare * Math.max(0, rate));
106
+ if (item.vatExempt) vatExemptSales += netShare;
107
+ else vatSales += netShare;
108
+ discountBreakdown[item.breakdown] = -discount;
109
+ totalDiscount += discount;
110
+ }
111
+ vatSales = format2(vatSales);
112
+ vatExemptSales = format2(vatExemptSales);
113
+ const discounted = -format2(totalDiscount);
114
+ const vat = format2(vatSales * vatRate);
115
+ const vatNet = format2(vatSales + vatExemptSales + discounted);
116
+ const serviceChargeBase = Math.min(Math.max(0, toMoneyNumber(input.serviceChargeableTotalAmount)), subtotal);
117
+ const serviceCharge = input.serviceChargeRate > 0 ? format2(serviceChargeBase / vatDivisor * scRate) : 0;
118
+ const togoCharge = input.togoableTotalAmount > 0 ? input.togoCharge : 0;
119
+ const totalAmount = format2(Math.max(0, vatNet + vat + serviceCharge + togoCharge));
120
+ const voucherVatNet = format2(totalOrderAmount / vatDivisor);
121
+ const voucherVat = format2(voucherVatNet * vatRate);
122
+ return {
123
+ quantity: input.quantity,
124
+ subtotal,
125
+ isAddVat: splitDiscountTypes.some((item) => !item.vatExempt && normalizedCount(counts[item.count]) > 0),
126
+ vatNet,
127
+ vat,
128
+ discounted,
129
+ discountBreakdown,
130
+ voucherDiscounted,
131
+ serviceCharge,
132
+ togoCharge,
133
+ totalAmount,
134
+ receipt: {
135
+ gross: totalOrderAmount,
136
+ hasVoucher: voucherRate > 0,
137
+ voucher: {
138
+ discount: voucherDiscounted,
139
+ net: subtotal,
140
+ vatNet: voucherVatNet,
141
+ vat: voucherVat,
142
+ forBirDiscount: -(input.voucherType === "FIXED" ? voucherVatNet - Number(voucherRate.toFixed(2)) : voucherVatNet * Number(voucherRate.toFixed(2)))
143
+ },
144
+ hasDiscount: totalDiscount > 0,
145
+ discount: {
146
+ discount: discounted,
147
+ net: vatNet
148
+ },
149
+ netOfVat: format2(vatSales + vatExemptSales),
150
+ vat,
151
+ serviceCharge,
152
+ togoCharge,
153
+ totalAmount,
154
+ bir: {
155
+ vatSales,
156
+ vatExemptSales,
157
+ vatZeroRatedSales: 0,
158
+ nonTaxableSales: 0,
159
+ vat,
160
+ totalSales: subtotal,
161
+ discount: discounted,
162
+ serviceCharge,
163
+ togoCharge,
164
+ amountPayable: totalAmount
165
+ }
166
+ }
167
+ };
168
+ }
64
169
  function calculateTotals(input) {
170
+ const splitTotals = calculateSplitTotals(input);
171
+ if (splitTotals) return splitTotals;
65
172
  const vatRate = Number(input.vatRate || 0) / 100;
66
173
  const scRate = Number(input.serviceChargeRate || 0) / 100;
67
174
  const discountRate = Number(input.discountRate || 0) / 100;
@@ -110,6 +217,7 @@ function calculateTotals(input) {
110
217
  vatNet,
111
218
  vat,
112
219
  discounted,
220
+ discountBreakdown: getDiscountBreakdown(input.discountType, discounted),
113
221
  voucherDiscounted,
114
222
  serviceCharge,
115
223
  togoCharge,
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,4 @@ declare function getDiscountBreakdown(discountType: string | null | undefined, d
74
97
  };
75
98
  declare function calculateTotals(input: CalculateTotalsInput): CalculateTotalsResult;
76
99
 
77
- export { type CalculateTotalsInput, type CalculateTotalsResult, type DiscountType, type VoucherType, calculateTotals, clamp, format2, getDiscountBreakdown, isAddVatAfterDiscount, isVatExemptDiscount, toMoneyNumber };
100
+ export { type CalculateTotalsInput, type CalculateTotalsResult, type DiscountBreakdown, type DiscountRates, type DiscountType, type PaxDiscountCounts, type VoucherType, 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,4 @@ declare function getDiscountBreakdown(discountType: string | null | undefined, d
74
97
  };
75
98
  declare function calculateTotals(input: CalculateTotalsInput): CalculateTotalsResult;
76
99
 
77
- export { type CalculateTotalsInput, type CalculateTotalsResult, type DiscountType, type VoucherType, calculateTotals, clamp, format2, getDiscountBreakdown, isAddVatAfterDiscount, isVatExemptDiscount, toMoneyNumber };
100
+ export { type CalculateTotalsInput, type CalculateTotalsResult, type DiscountBreakdown, type DiscountRates, type DiscountType, type PaxDiscountCounts, type VoucherType, 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 voucherVatNet = format2(totalOrderAmount / vatDivisor);
89
+ const voucherVat = format2(voucherVatNet * 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: voucherVatNet,
109
+ vat: voucherVat,
110
+ forBirDiscount: -(input.voucherType === "FIXED" ? voucherVatNet - Number(voucherRate.toFixed(2)) : voucherVatNet * 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;
@@ -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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qrwise/pos-calculations",
3
- "version": "0.0.17",
3
+ "version": "0.0.18",
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
+ "test": "npm run build && node --test test/*.test.mjs",
22
23
  "prepublishOnly": "npm run build",
23
24
  "release:patch": "npm version patch && npm publish",
24
25
  "release:minor": "npm version minor && npm publish",