@munchi_oy/cart-engine 0.1.0

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.js ADDED
@@ -0,0 +1,852 @@
1
+ // src/cart/Cart.ts
2
+ import {
3
+ DiscountScope as DiscountScope2,
4
+ KitchenStatus as KitchenStatus3,
5
+ LocatorType,
6
+ OrderFormat,
7
+ OrderRefundStatus,
8
+ OrderStatusEnum,
9
+ OrderTypePOS,
10
+ PosPaymentStatus
11
+ } from "@munchi_oy/core";
12
+ import { createId } from "@paralleldrive/cuid2";
13
+ import dayjs from "dayjs";
14
+ import lodash from "lodash";
15
+
16
+ // src/pricing/calculator.ts
17
+ import { DiscountScope, KitchenStatus } from "@munchi_oy/core";
18
+
19
+ // src/discount/calculator.ts
20
+ import { DiscountType } from "@munchi_oy/core";
21
+
22
+ // src/utils/index.ts
23
+ var withSafeIntegers = (fn, ignoreIndexes = []) => {
24
+ return (...args) => {
25
+ args.forEach((arg, index) => {
26
+ if (typeof arg === "number" && !ignoreIndexes.includes(index) && !Number.isInteger(arg)) {
27
+ throw new Error(
28
+ `[CartEngine SafeLock] Argument at position ${index} MUST be an integer (minor unit). Received: ${arg}`
29
+ );
30
+ }
31
+ });
32
+ return fn(...args);
33
+ };
34
+ };
35
+ function mergeDefinedValue(currentValue, nextValue, hasChanges) {
36
+ if (nextValue === void 0 || currentValue === nextValue) {
37
+ return [currentValue, hasChanges];
38
+ }
39
+ return [nextValue, true];
40
+ }
41
+
42
+ // src/discount/calculator.ts
43
+ var discountStrategies = {
44
+ [DiscountType.Fixed]: withSafeIntegers(
45
+ (value, totalAmount) => {
46
+ return Math.min(value, totalAmount);
47
+ }
48
+ ),
49
+ [DiscountType.Percentage]: withSafeIntegers(
50
+ (value, totalAmount) => {
51
+ return Math.floor(totalAmount * value / 100);
52
+ },
53
+ [0]
54
+ ),
55
+ default: () => 0
56
+ };
57
+ function calculateDiscountAmount(type, value, totalAmountBeforeDiscounts) {
58
+ const strategy = discountStrategies[type] ?? discountStrategies.default;
59
+ return strategy(value, totalAmountBeforeDiscounts);
60
+ }
61
+ function calculateSequentialDiscountTotal(discounts, initialAmount) {
62
+ let remainingAmount = initialAmount;
63
+ let totalDiscountAmount = 0;
64
+ for (const discount of discounts) {
65
+ const discountAmount = Math.min(
66
+ calculateDiscountAmount(discount.type, discount.value, remainingAmount),
67
+ remainingAmount
68
+ );
69
+ totalDiscountAmount += discountAmount;
70
+ remainingAmount -= discountAmount;
71
+ }
72
+ return {
73
+ totalDiscountAmount,
74
+ remainingAmount
75
+ };
76
+ }
77
+
78
+ // src/pricing/calculator.ts
79
+ var assertMinorUnit = (value, fieldName) => {
80
+ if (!Number.isInteger(value)) {
81
+ throw new Error(
82
+ `[CartEngine SafeLock] ${fieldName} MUST be an integer (minor unit). Received: ${value}`
83
+ );
84
+ }
85
+ };
86
+ function calculateItemPrice(details) {
87
+ assertMinorUnit(details.basePriceInMinorUnit, "details.basePriceInMinorUnit");
88
+ assertMinorUnit(details.quantity, "details.quantity");
89
+ const totalOptionsPrice = details.options.reduce((sum, opt) => {
90
+ return sum + opt.suboptions.reduce((subSum, sub) => {
91
+ assertMinorUnit(sub.price.amount, "option.price.amount");
92
+ assertMinorUnit(sub.quantity, "option.quantity");
93
+ return subSum + sub.price.amount * sub.quantity;
94
+ }, 0);
95
+ }, 0);
96
+ const singleUnitPriceAmount = details.basePriceInMinorUnit + totalOptionsPrice;
97
+ const totalAmountBeforeDiscounts = singleUnitPriceAmount * details.quantity;
98
+ let discountAmount = 0;
99
+ if (details.discount) {
100
+ discountAmount = calculateDiscountAmount(
101
+ details.discount.type,
102
+ details.discount.value,
103
+ totalAmountBeforeDiscounts
104
+ );
105
+ }
106
+ const totalAfterDiscount = totalAmountBeforeDiscounts - discountAmount;
107
+ return {
108
+ unitPrice: { amount: singleUnitPriceAmount, currency: details.currency },
109
+ basePrice: {
110
+ amount: details.basePriceInMinorUnit,
111
+ currency: details.currency
112
+ },
113
+ vatPercentage: details.taxRate,
114
+ total: { amount: totalAfterDiscount, currency: details.currency },
115
+ priceBreakdown: {
116
+ totalBeforeDiscounts: {
117
+ amount: totalAmountBeforeDiscounts,
118
+ currency: details.currency
119
+ },
120
+ totalDiscounts: { amount: discountAmount, currency: details.currency },
121
+ subtotalBasketDiscounts: { amount: 0, currency: details.currency },
122
+ subtotalItemDiscounts: { amount: 0, currency: details.currency },
123
+ basePriceBeforeDiscounts: {
124
+ amount: details.basePriceInMinorUnit * details.quantity,
125
+ currency: details.currency
126
+ },
127
+ unitPriceBeforeDiscounts: {
128
+ amount: singleUnitPriceAmount,
129
+ currency: details.currency
130
+ },
131
+ subtotalOptionsBasketDiscounts: { amount: 0, currency: details.currency },
132
+ subtotalOptionsItemDiscounts: { amount: 0, currency: details.currency }
133
+ }
134
+ };
135
+ }
136
+ function calculateCartPriceSnapshot(details) {
137
+ const activeItems = details.items.filter(
138
+ (item) => item.kitchenStatus !== KitchenStatus.Cancelled
139
+ );
140
+ const totalBeforeDiscountsAmount = activeItems.reduce(
141
+ (sum, item) => sum + (item.itemPrice.priceBreakdown.totalBeforeDiscounts.amount ?? 0),
142
+ 0
143
+ );
144
+ const subtotalItemDiscountsAmount = activeItems.reduce(
145
+ (sum, item) => sum + (item.itemPrice.priceBreakdown.totalDiscounts.amount ?? 0),
146
+ 0
147
+ );
148
+ const cartDiscounts = details.discounts.filter((discount) => discount.scope === DiscountScope.Cart).sort(
149
+ (left, right) => new Date(left.createdAt).getTime() - new Date(right.createdAt).getTime()
150
+ );
151
+ const grossAfterItemDiscounts = Math.max(
152
+ 0,
153
+ totalBeforeDiscountsAmount - subtotalItemDiscountsAmount
154
+ );
155
+ const cartDiscountCalculation = calculateSequentialDiscountTotal(
156
+ cartDiscounts,
157
+ grossAfterItemDiscounts
158
+ );
159
+ const totalDiscountsAmount = subtotalItemDiscountsAmount + cartDiscountCalculation.totalDiscountAmount;
160
+ const price = {
161
+ priceBreakdown: {
162
+ totalBeforeDiscounts: {
163
+ amount: totalBeforeDiscountsAmount,
164
+ currency: details.currency
165
+ },
166
+ subtotalItemDiscounts: {
167
+ amount: subtotalItemDiscountsAmount,
168
+ currency: details.currency
169
+ },
170
+ subtotalBasketDiscounts: {
171
+ amount: cartDiscountCalculation.totalDiscountAmount,
172
+ currency: details.currency
173
+ },
174
+ totalDiscounts: {
175
+ amount: totalDiscountsAmount,
176
+ currency: details.currency
177
+ }
178
+ },
179
+ total: {
180
+ amount: cartDiscountCalculation.remainingAmount,
181
+ currency: details.currency
182
+ }
183
+ };
184
+ return {
185
+ activeItems,
186
+ totalBeforeDiscountsAmount,
187
+ subtotalItemDiscountsAmount,
188
+ subtotalBasketDiscountsAmount: cartDiscountCalculation.totalDiscountAmount,
189
+ totalDiscountsAmount,
190
+ totalAmount: cartDiscountCalculation.remainingAmount,
191
+ price
192
+ };
193
+ }
194
+
195
+ // src/tax/index.ts
196
+ import { KitchenStatus as KitchenStatus2 } from "@munchi_oy/core";
197
+
198
+ // src/discount/modulo.ts
199
+ var allocateProportionalMinorUnits = withSafeIntegers(
200
+ (weights, totalAmount) => {
201
+ for (let i = 0; i < weights.length; i++) {
202
+ if (!Number.isInteger(weights[i])) {
203
+ throw new Error(
204
+ `[CartEngine SafeLock] Weight at index ${i} MUST be an integer. Received: ${weights[i]}`
205
+ );
206
+ }
207
+ }
208
+ if (weights.length === 0 || totalAmount <= 0) {
209
+ return new Array(weights.length).fill(0);
210
+ }
211
+ const totalWeight = weights.reduce((sum, weight) => sum + weight, 0);
212
+ if (totalWeight <= 0) {
213
+ return new Array(weights.length).fill(0);
214
+ }
215
+ const baseAllocations = weights.map(
216
+ (weight) => Math.floor(weight * totalAmount / totalWeight)
217
+ );
218
+ const remainderOrder = weights.map((weight, index) => ({
219
+ index,
220
+ remainder: weight * totalAmount % totalWeight
221
+ })).sort(
222
+ (left, right) => right.remainder - left.remainder || left.index - right.index
223
+ );
224
+ let remainingAmount = totalAmount - baseAllocations.reduce((sum, amount) => sum + amount, 0);
225
+ for (const { index } of remainderOrder) {
226
+ if (remainingAmount <= 0) break;
227
+ baseAllocations[index] = (baseAllocations[index] ?? 0) + 1;
228
+ remainingAmount -= 1;
229
+ }
230
+ return baseAllocations;
231
+ }
232
+ );
233
+
234
+ // src/tax/index.ts
235
+ function assertMinorUnit2(value, fieldName) {
236
+ if (!Number.isInteger(value)) {
237
+ throw new Error(
238
+ `[CartEngine SafeLock] ${fieldName} MUST be an integer. Received: ${value}`
239
+ );
240
+ }
241
+ }
242
+ function assertValidTaxRate(value, fieldName) {
243
+ if (!Number.isFinite(value) || value < 0) {
244
+ throw new Error(
245
+ `[CartEngine SafeLock] ${fieldName} MUST be a finite non-negative number. Received: ${value}`
246
+ );
247
+ }
248
+ }
249
+ function toTaxRateBasisPoints(taxRate) {
250
+ const normalized = taxRate.toString();
251
+ if (!/^\d+(\.\d+)?$/.test(normalized)) {
252
+ throw new Error(
253
+ `[CartEngine SafeLock] taxRate MUST be a decimal number with dot notation. Received: ${taxRate}`
254
+ );
255
+ }
256
+ const [wholePart, decimalPart = ""] = normalized.split(".");
257
+ const paddedDecimals = `${decimalPart}00`.slice(0, 2);
258
+ return Number(wholePart) * 100 + Number(paddedDecimals);
259
+ }
260
+ function splitTaxInclusiveAmount(totalWithTax, taxRate) {
261
+ assertMinorUnit2(totalWithTax, "totalWithTax");
262
+ assertValidTaxRate(taxRate, "taxRate");
263
+ if (taxRate <= 0) {
264
+ return {
265
+ taxRate,
266
+ totalWithTax,
267
+ totalWithoutTax: totalWithTax,
268
+ totalTax: 0
269
+ };
270
+ }
271
+ const taxRateBasisPoints = toTaxRateBasisPoints(taxRate);
272
+ const totalWithoutTax = Math.floor(
273
+ totalWithTax * 1e4 / (1e4 + taxRateBasisPoints)
274
+ );
275
+ return {
276
+ taxRate,
277
+ totalWithTax,
278
+ totalWithoutTax,
279
+ totalTax: totalWithTax - totalWithoutTax
280
+ };
281
+ }
282
+ function calculateOrderTaxSummary(details) {
283
+ const priceSnapshot = calculateCartPriceSnapshot({
284
+ items: details.items,
285
+ discounts: details.discounts,
286
+ currency: details.currency
287
+ });
288
+ const activeItems = details.items.filter(
289
+ (item) => item.kitchenStatus !== KitchenStatus2.Cancelled
290
+ );
291
+ if (activeItems.length === 0 || priceSnapshot.totalAmount <= 0) {
292
+ return {
293
+ taxSummaries: [],
294
+ totalWithoutTax: 0,
295
+ totalTax: 0,
296
+ grandTotal: 0
297
+ };
298
+ }
299
+ const basketDiscountAllocations = allocateProportionalMinorUnits(
300
+ activeItems.map((item) => item.itemPrice.total.amount),
301
+ priceSnapshot.subtotalBasketDiscountsAmount
302
+ );
303
+ const taxRateMap = /* @__PURE__ */ new Map();
304
+ activeItems.forEach((item, index) => {
305
+ if (item.taxRate <= 0) {
306
+ return;
307
+ }
308
+ const totalWithTax = Math.max(
309
+ 0,
310
+ item.itemPrice.total.amount - (basketDiscountAllocations[index] ?? 0)
311
+ );
312
+ const taxBreakdown = splitTaxInclusiveAmount(totalWithTax, item.taxRate);
313
+ const existing = taxRateMap.get(item.taxRate) ?? {
314
+ totalWithTax: 0,
315
+ totalTax: 0,
316
+ totalWithoutTax: 0
317
+ };
318
+ existing.totalWithTax += taxBreakdown.totalWithTax;
319
+ existing.totalTax += taxBreakdown.totalTax;
320
+ existing.totalWithoutTax += taxBreakdown.totalWithoutTax;
321
+ taxRateMap.set(item.taxRate, existing);
322
+ });
323
+ const taxSummaries = Array.from(taxRateMap.entries()).sort((left, right) => left[0] - right[0]).map(([taxRate, values]) => ({
324
+ taxRate,
325
+ totalWithoutTax: values.totalWithoutTax,
326
+ totalTax: values.totalTax,
327
+ totalWithTax: values.totalWithTax
328
+ }));
329
+ return {
330
+ taxSummaries,
331
+ totalWithoutTax: taxSummaries.reduce(
332
+ (sum, item) => sum + item.totalWithoutTax,
333
+ 0
334
+ ),
335
+ totalTax: taxSummaries.reduce((sum, item) => sum + item.totalTax, 0),
336
+ grandTotal: priceSnapshot.totalAmount
337
+ };
338
+ }
339
+
340
+ // src/cart/Cart.ts
341
+ var { isEqual } = lodash;
342
+ var Cart = class _Cart {
343
+ id;
344
+ businessId;
345
+ currency;
346
+ createdAt;
347
+ orderNumber;
348
+ orderType;
349
+ orderFormat;
350
+ spotNumber;
351
+ comments;
352
+ orderRefundStatus;
353
+ updatedAt;
354
+ status;
355
+ _loyaltyTransactionIds;
356
+ _loyaltyProgramId;
357
+ _items = [];
358
+ _locatorType;
359
+ _business;
360
+ _discounts = [];
361
+ _customer = null;
362
+ _refunds = [];
363
+ _taxSummary;
364
+ _newlyCancelledItemIds;
365
+ _invoiceCompany;
366
+ _staffId;
367
+ _shiftId;
368
+ version = "1.0.2";
369
+ constructor(id, createdAt, options) {
370
+ this.id = id;
371
+ this.createdAt = createdAt;
372
+ this.businessId = options.businessId;
373
+ this.orderType = options.orderType;
374
+ this.orderNumber = options.orderNumber;
375
+ this._business = options.business;
376
+ this.currency = options.currency;
377
+ this.updatedAt = createdAt;
378
+ this.spotNumber = null;
379
+ this.comments = null;
380
+ this._loyaltyTransactionIds = [];
381
+ this._loyaltyProgramId = null;
382
+ this.orderRefundStatus = OrderRefundStatus.None;
383
+ this.status = OrderStatusEnum.Draft;
384
+ this.orderFormat = OrderFormat.Instant;
385
+ this._locatorType = LocatorType.Table;
386
+ this._newlyCancelledItemIds = /* @__PURE__ */ new Set();
387
+ this._staffId = options.staffId ?? null;
388
+ this._shiftId = options.shiftId ?? null;
389
+ this._invoiceCompany = null;
390
+ }
391
+ static create(options) {
392
+ return new _Cart(createId(), options.createdAt ?? dayjs().toDate(), {
393
+ businessId: options.businessId,
394
+ orderType: options.orderType ?? OrderTypePOS.DineIn,
395
+ orderNumber: options.orderNumber,
396
+ currency: options.currency,
397
+ business: options.business,
398
+ staffId: options.staffId,
399
+ shiftId: options.shiftId
400
+ });
401
+ }
402
+ static fromData(orderDto) {
403
+ const cart = new _Cart(orderDto.id, dayjs(orderDto.createdAt).toDate(), {
404
+ businessId: orderDto.businessId,
405
+ orderType: orderDto.orderType,
406
+ orderNumber: orderDto.orderNumber,
407
+ currency: orderDto.currency,
408
+ business: orderDto.business,
409
+ staffId: orderDto.staffId ?? null,
410
+ shiftId: orderDto.shiftId ?? null
411
+ });
412
+ cart.updatedAt = dayjs(orderDto.updatedAt).toDate();
413
+ cart.spotNumber = orderDto.spotNumber;
414
+ cart.comments = orderDto.comments;
415
+ cart.orderRefundStatus = orderDto.orderRefundStatus;
416
+ cart._locatorType = orderDto.locatorType;
417
+ cart._items = [...orderDto.items];
418
+ cart.status = orderDto.status;
419
+ cart.orderFormat = orderDto.orderFormat ?? OrderFormat.Instant;
420
+ cart._discounts = (orderDto.discounts ?? []).map((discount) => ({
421
+ ...discount,
422
+ createdAt: dayjs(discount.createdAt).toDate().toISOString()
423
+ }));
424
+ cart._customer = orderDto.customer ? { ...orderDto.customer } : null;
425
+ cart._refunds = orderDto.refunds ? [...orderDto.refunds] : [];
426
+ cart._loyaltyTransactionIds = [...orderDto.loyaltyTransactionIds];
427
+ cart._loyaltyProgramId = orderDto.loyaltyProgramId ?? null;
428
+ cart._newlyCancelledItemIds = /* @__PURE__ */ new Set();
429
+ cart._taxSummary = orderDto.taxSummary;
430
+ cart._invoiceCompany = orderDto.invoiceCompany ?? null;
431
+ return cart;
432
+ }
433
+ get items() {
434
+ return this._items;
435
+ }
436
+ get locatorType() {
437
+ return this._locatorType;
438
+ }
439
+ get staffId() {
440
+ return this._staffId;
441
+ }
442
+ get shiftId() {
443
+ return this._shiftId;
444
+ }
445
+ get discounts() {
446
+ return this._discounts;
447
+ }
448
+ get customer() {
449
+ return this._customer;
450
+ }
451
+ get refunds() {
452
+ return this._refunds;
453
+ }
454
+ get loyaltyProgramId() {
455
+ return this._loyaltyProgramId;
456
+ }
457
+ get loyaltyTransactionIds() {
458
+ return this._loyaltyTransactionIds;
459
+ }
460
+ get invoiceCompany() {
461
+ return this._invoiceCompany;
462
+ }
463
+ get price() {
464
+ return calculateCartPriceSnapshot({
465
+ items: this._items,
466
+ discounts: this._discounts,
467
+ currency: this.currency
468
+ }).price;
469
+ }
470
+ setInvoiceCompany(company) {
471
+ if (this._invoiceCompany?.id === company?.id) {
472
+ return;
473
+ }
474
+ this._invoiceCompany = company;
475
+ this.touch();
476
+ }
477
+ addItem(itemData, requiresPrep = false) {
478
+ const lockedStatuses = [
479
+ KitchenStatus3.DispatchedToKitchen,
480
+ KitchenStatus3.SentToOm,
481
+ KitchenStatus3.Completed,
482
+ KitchenStatus3.Cancelled
483
+ ];
484
+ const existingModifiableItem = this._items.find(
485
+ (item) => item.posId === itemData.posId && isEqual(item.options, itemData.options) && item.comments === itemData.comments && !lockedStatuses.includes(item.kitchenStatus) && item.discount === null
486
+ );
487
+ if (existingModifiableItem) {
488
+ this._items = this._items.map((item) => {
489
+ if (item.lineItemId !== existingModifiableItem.lineItemId) {
490
+ return item;
491
+ }
492
+ const quantity = item.quantity + itemData.quantity;
493
+ const itemPrice = calculateItemPrice({
494
+ basePriceInMinorUnit: item.basePrice.amount,
495
+ quantity,
496
+ taxRate: item.taxRate,
497
+ options: item.options,
498
+ currency: this.currency
499
+ });
500
+ return {
501
+ ...item,
502
+ quantity,
503
+ itemPrice
504
+ };
505
+ });
506
+ this.touch();
507
+ return;
508
+ }
509
+ this._items.push({
510
+ ...itemData,
511
+ lineItemId: createId(),
512
+ kitchenStatus: requiresPrep ? KitchenStatus3.Pending : KitchenStatus3.NotApplicable,
513
+ paymentStatus: PosPaymentStatus.Unpaid,
514
+ discount: null
515
+ });
516
+ this.touch();
517
+ }
518
+ updateItem(lineItemId, updatedItem) {
519
+ const itemIndex = this._items.findIndex(
520
+ (item) => item.lineItemId === lineItemId
521
+ );
522
+ if (itemIndex === -1) {
523
+ return;
524
+ }
525
+ this._items[itemIndex] = updatedItem;
526
+ this.touch();
527
+ }
528
+ removeItem(lineItemId) {
529
+ this._items = this._items.filter((item) => item.lineItemId !== lineItemId);
530
+ this.touch();
531
+ }
532
+ updateItemStatus(lineItemId, newStatus) {
533
+ const itemIndex = this._items.findIndex(
534
+ (item2) => item2.lineItemId === lineItemId
535
+ );
536
+ if (itemIndex === -1 || !this._items[itemIndex]) {
537
+ return;
538
+ }
539
+ const item = this._items[itemIndex];
540
+ const isKitchenStatus = Object.values(KitchenStatus3).includes(
541
+ newStatus
542
+ );
543
+ if (isKitchenStatus && newStatus === KitchenStatus3.Cancelled && item.kitchenStatus !== KitchenStatus3.Cancelled) {
544
+ this._newlyCancelledItemIds.add(lineItemId);
545
+ }
546
+ this._items = this._items.map((currentItem) => {
547
+ if (currentItem.lineItemId !== lineItemId) {
548
+ return currentItem;
549
+ }
550
+ if (isKitchenStatus) {
551
+ return {
552
+ ...currentItem,
553
+ kitchenStatus: newStatus
554
+ };
555
+ }
556
+ return {
557
+ ...currentItem,
558
+ paymentStatus: newStatus
559
+ };
560
+ });
561
+ this.touch();
562
+ }
563
+ getNewlyCancelledItemIds() {
564
+ return Array.from(this._newlyCancelledItemIds);
565
+ }
566
+ clearCancellationTracking() {
567
+ this._newlyCancelledItemIds.clear();
568
+ }
569
+ applyDiscountToCart(discountData) {
570
+ this._discounts.push({
571
+ ...discountData,
572
+ scope: DiscountScope2.Cart,
573
+ lineItemId: null,
574
+ createdAt: dayjs().toDate().toISOString()
575
+ });
576
+ this.touch();
577
+ }
578
+ applyDiscountToItem(lineItemId, discountData) {
579
+ const newDiscount = {
580
+ ...discountData,
581
+ scope: DiscountScope2.Item,
582
+ lineItemId,
583
+ createdAt: dayjs().toDate().toISOString()
584
+ };
585
+ const existingDiscountIndex = this._discounts.findIndex(
586
+ (discount) => discount.lineItemId === lineItemId && discount.scope === DiscountScope2.Item
587
+ );
588
+ if (existingDiscountIndex !== -1) {
589
+ this._discounts[existingDiscountIndex] = newDiscount;
590
+ } else {
591
+ this._discounts.push(newDiscount);
592
+ }
593
+ this._items = this._items.map((item) => {
594
+ if (item.lineItemId !== lineItemId) {
595
+ return item;
596
+ }
597
+ return {
598
+ ...item,
599
+ itemPrice: calculateItemPrice({
600
+ basePriceInMinorUnit: item.basePrice.amount,
601
+ quantity: item.quantity,
602
+ taxRate: item.taxRate,
603
+ options: item.options,
604
+ currency: this.currency,
605
+ discount: {
606
+ type: newDiscount.type,
607
+ value: newDiscount.value
608
+ }
609
+ }),
610
+ discount: newDiscount
611
+ };
612
+ });
613
+ this.touch();
614
+ }
615
+ removeItemDiscount(lineItemId) {
616
+ this._items = this._items.map((item) => {
617
+ if (item.lineItemId !== lineItemId) {
618
+ return item;
619
+ }
620
+ return {
621
+ ...item,
622
+ itemPrice: calculateItemPrice({
623
+ basePriceInMinorUnit: item.basePrice.amount,
624
+ quantity: item.quantity,
625
+ taxRate: item.taxRate,
626
+ options: item.options,
627
+ currency: this.currency
628
+ }),
629
+ discount: null
630
+ };
631
+ });
632
+ this._discounts = this._discounts.filter(
633
+ (discount) => discount.lineItemId !== lineItemId || discount.scope !== DiscountScope2.Item
634
+ );
635
+ this.touch();
636
+ }
637
+ removeDiscount(discountId) {
638
+ this._discounts = this._discounts.filter(
639
+ (discount) => discount.id !== discountId
640
+ );
641
+ this.touch();
642
+ }
643
+ setCustomer(customer) {
644
+ if (this._customer?.id === customer?.id) {
645
+ return;
646
+ }
647
+ this._customer = customer;
648
+ this.touch();
649
+ }
650
+ updateDetails(details) {
651
+ let hasChanges = false;
652
+ [this.orderNumber, hasChanges] = mergeDefinedValue(
653
+ this.orderNumber,
654
+ details.orderNumber,
655
+ hasChanges
656
+ );
657
+ [this.spotNumber, hasChanges] = mergeDefinedValue(
658
+ this.spotNumber,
659
+ details.spotNumber,
660
+ hasChanges
661
+ );
662
+ [this.orderType, hasChanges] = mergeDefinedValue(
663
+ this.orderType,
664
+ details.orderType,
665
+ hasChanges
666
+ );
667
+ [this.comments, hasChanges] = mergeDefinedValue(
668
+ this.comments,
669
+ details.comments,
670
+ hasChanges
671
+ );
672
+ [this._locatorType, hasChanges] = mergeDefinedValue(
673
+ this._locatorType,
674
+ details._locatorType,
675
+ hasChanges
676
+ );
677
+ if (hasChanges) {
678
+ this.touch();
679
+ }
680
+ }
681
+ setComment(details) {
682
+ if (details.scope === "order") {
683
+ if (this.comments === details.comment) {
684
+ return;
685
+ }
686
+ this.comments = details.comment;
687
+ this.touch();
688
+ return;
689
+ }
690
+ if (!details.lineItemId) {
691
+ return;
692
+ }
693
+ let hasChanges = false;
694
+ this._items = this._items.map((item) => {
695
+ if (item.lineItemId !== details.lineItemId || item.comments === details.comment) {
696
+ return item;
697
+ }
698
+ hasChanges = true;
699
+ return {
700
+ ...item,
701
+ comments: details.comment
702
+ };
703
+ });
704
+ if (hasChanges) {
705
+ this.touch();
706
+ }
707
+ }
708
+ setStaffContext(staffId, shiftId) {
709
+ if (this._staffId === staffId && this._shiftId === shiftId) {
710
+ return;
711
+ }
712
+ this._staffId = staffId;
713
+ this._shiftId = shiftId;
714
+ this.touch();
715
+ }
716
+ updateOrderStatus(newStatus) {
717
+ if (this.status === newStatus) {
718
+ return;
719
+ }
720
+ this.status = newStatus;
721
+ this.touch();
722
+ }
723
+ updateOrderFormat(newFormat) {
724
+ if (this.orderFormat === newFormat) {
725
+ return;
726
+ }
727
+ this.orderFormat = newFormat;
728
+ this.touch();
729
+ }
730
+ updateLoyaltyProgramId(id) {
731
+ if (this._loyaltyProgramId === id) {
732
+ return;
733
+ }
734
+ this._loyaltyProgramId = id;
735
+ this.touch();
736
+ }
737
+ updateLoyaltyTransactionId(id) {
738
+ if (!id || this._loyaltyTransactionIds.includes(id)) {
739
+ return;
740
+ }
741
+ this._loyaltyTransactionIds.push(id);
742
+ this.touch();
743
+ }
744
+ setLoyaltyContext(customer, loyaltyProgramId) {
745
+ let hasChanges = false;
746
+ if (this._customer?.id !== customer?.id) {
747
+ this._customer = customer;
748
+ hasChanges = true;
749
+ }
750
+ if (this._loyaltyProgramId !== loyaltyProgramId) {
751
+ this._loyaltyProgramId = loyaltyProgramId;
752
+ hasChanges = true;
753
+ }
754
+ if (hasChanges) {
755
+ this.touch();
756
+ }
757
+ }
758
+ toJSON(paymentEvents = []) {
759
+ return {
760
+ id: this.id,
761
+ businessId: this.businessId,
762
+ business: this._business,
763
+ currency: this.currency,
764
+ createdAt: this.createdAt.toISOString(),
765
+ updatedAt: this.updatedAt.toISOString(),
766
+ orderNumber: this.orderNumber,
767
+ orderFormat: this.orderFormat,
768
+ orderType: this.orderType,
769
+ locatorType: this._locatorType,
770
+ spotNumber: this.spotNumber,
771
+ comments: this.comments,
772
+ orderRefundStatus: this.orderRefundStatus,
773
+ items: this._items,
774
+ status: this.status,
775
+ discounts: this._discounts.map((discount) => ({
776
+ ...discount,
777
+ createdAt: discount.createdAt
778
+ })),
779
+ customer: this._customer,
780
+ refunds: this._refunds,
781
+ basketPrice: this.price,
782
+ paymentEvents,
783
+ loyaltyTransactionIds: this._loyaltyTransactionIds,
784
+ loyaltyProgramId: this._loyaltyProgramId,
785
+ taxSummary: this.calculateOrderTaxSummary(),
786
+ version: this.version,
787
+ staffId: this._staffId,
788
+ shiftId: this._shiftId,
789
+ invoiceCompany: this._invoiceCompany
790
+ };
791
+ }
792
+ clone() {
793
+ const newCart = new _Cart(this.id, this.createdAt, {
794
+ businessId: this.businessId,
795
+ orderType: this.orderType,
796
+ orderNumber: this.orderNumber,
797
+ currency: this.currency,
798
+ business: this._business,
799
+ staffId: this._staffId,
800
+ shiftId: this._shiftId
801
+ });
802
+ newCart.updatedAt = this.updatedAt;
803
+ newCart.spotNumber = this.spotNumber;
804
+ newCart.comments = this.comments;
805
+ newCart.orderRefundStatus = this.orderRefundStatus;
806
+ newCart._locatorType = this._locatorType;
807
+ newCart._items = [...this._items];
808
+ newCart.status = this.status;
809
+ newCart.orderFormat = this.orderFormat;
810
+ newCart._discounts = [...this._discounts];
811
+ newCart._customer = this._customer ? { ...this._customer } : null;
812
+ newCart._refunds = [...this._refunds];
813
+ newCart._loyaltyProgramId = this._loyaltyProgramId;
814
+ newCart._loyaltyTransactionIds = [...this._loyaltyTransactionIds];
815
+ newCart._taxSummary = this._taxSummary;
816
+ newCart._newlyCancelledItemIds = new Set(this._newlyCancelledItemIds);
817
+ newCart._invoiceCompany = this._invoiceCompany ? { ...this._invoiceCompany } : null;
818
+ return newCart;
819
+ }
820
+ calculateOrderTaxSummary() {
821
+ return calculateOrderTaxSummary({
822
+ items: this._items,
823
+ discounts: this._discounts,
824
+ currency: this.currency
825
+ });
826
+ }
827
+ touch() {
828
+ this.updatedAt = dayjs().toDate();
829
+ }
830
+ };
831
+
832
+ // src/types/index.ts
833
+ var ApplyTo = /* @__PURE__ */ ((ApplyTo2) => {
834
+ ApplyTo2["Order"] = "order";
835
+ ApplyTo2["Item"] = "item";
836
+ return ApplyTo2;
837
+ })(ApplyTo || {});
838
+
839
+ // src/version.ts
840
+ var VERSION = "0.1.0";
841
+ export {
842
+ ApplyTo,
843
+ Cart,
844
+ VERSION,
845
+ allocateProportionalMinorUnits,
846
+ calculateCartPriceSnapshot,
847
+ calculateDiscountAmount,
848
+ calculateItemPrice,
849
+ calculateOrderTaxSummary,
850
+ calculateSequentialDiscountTotal,
851
+ splitTaxInclusiveAmount
852
+ };