@munchi_oy/cart-engine 0.1.2 → 0.1.5

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 CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  OrderRefundStatus,
9
9
  OrderStatusEnum,
10
10
  OrderTypePOS,
11
+ PosDiscountTargetType as PosDiscountTargetType2,
11
12
  PosPaymentStatus,
12
13
  ProviderEnum as Provider
13
14
  } from "@munchi_oy/core";
@@ -16,7 +17,12 @@ import dayjs from "dayjs";
16
17
  import lodash from "lodash";
17
18
 
18
19
  // src/pricing/calculator.ts
19
- import { DiscountScope, KitchenStatus } from "@munchi_oy/core";
20
+ import {
21
+ CheckoutModelEnum,
22
+ DiscountScope,
23
+ KitchenStatus,
24
+ PosDiscountTargetType
25
+ } from "@munchi_oy/core";
20
26
 
21
27
  // src/discount/calculator.ts
22
28
  import { DiscountType } from "@munchi_oy/core";
@@ -77,7 +83,49 @@ function calculateSequentialDiscountTotal(discounts, initialAmount) {
77
83
  };
78
84
  }
79
85
 
86
+ // src/discount/modulo.ts
87
+ var allocateProportionalMinorUnits = withSafeIntegers(
88
+ (weights, totalAmount) => {
89
+ for (let i = 0; i < weights.length; i++) {
90
+ if (!Number.isInteger(weights[i])) {
91
+ throw new Error(
92
+ `[CartEngine SafeLock] Weight at index ${i} MUST be an integer. Received: ${weights[i]}`
93
+ );
94
+ }
95
+ }
96
+ if (weights.length === 0 || totalAmount <= 0) {
97
+ return new Array(weights.length).fill(0);
98
+ }
99
+ const totalWeight = weights.reduce((sum, weight) => sum + weight, 0);
100
+ if (totalWeight <= 0) {
101
+ return new Array(weights.length).fill(0);
102
+ }
103
+ const baseAllocations = weights.map(
104
+ (weight) => Math.floor(weight * totalAmount / totalWeight)
105
+ );
106
+ const remainderOrder = weights.map((weight, index) => ({
107
+ index,
108
+ remainder: weight * totalAmount % totalWeight
109
+ })).sort(
110
+ (left, right) => right.remainder - left.remainder || left.index - right.index
111
+ );
112
+ let remainingAmount = totalAmount - baseAllocations.reduce((sum, amount) => sum + amount, 0);
113
+ for (const { index } of remainderOrder) {
114
+ if (remainingAmount <= 0) break;
115
+ baseAllocations[index] = (baseAllocations[index] ?? 0) + 1;
116
+ remainingAmount -= 1;
117
+ }
118
+ return baseAllocations;
119
+ }
120
+ );
121
+
80
122
  // src/pricing/calculator.ts
123
+ var EMPTY_SPLIT_PRICING_RESULT = {
124
+ targets: [],
125
+ totalGrossAmount: 0,
126
+ totalDiscountAmount: 0,
127
+ totalNetAmount: 0
128
+ };
81
129
  var assertMinorUnit = (value, fieldName) => {
82
130
  if (!Number.isInteger(value)) {
83
131
  throw new Error(
@@ -85,6 +133,93 @@ var assertMinorUnit = (value, fieldName) => {
85
133
  );
86
134
  }
87
135
  };
136
+ var sortDiscountsByCreatedAt = (discounts) => {
137
+ return [...discounts].sort(
138
+ (left, right) => new Date(left.createdAt).getTime() - new Date(right.createdAt).getTime()
139
+ );
140
+ };
141
+ var calculateTargetPricing = (id, grossAmount, discounts) => {
142
+ assertMinorUnit(grossAmount, `target gross amount (${id})`);
143
+ const { totalDiscountAmount, remainingAmount } = calculateSequentialDiscountTotal(discounts, grossAmount);
144
+ return {
145
+ id,
146
+ grossAmount,
147
+ discountAmount: totalDiscountAmount,
148
+ netAmount: remainingAmount
149
+ };
150
+ };
151
+ var sumAmounts = (targets, field) => {
152
+ return targets.reduce((sum, target) => sum + target[field], 0);
153
+ };
154
+ var buildSplitPricingResult = (targets) => {
155
+ return {
156
+ targets,
157
+ totalGrossAmount: sumAmounts(targets, "grossAmount"),
158
+ totalDiscountAmount: sumAmounts(targets, "discountAmount"),
159
+ totalNetAmount: sumAmounts(targets, "netAmount")
160
+ };
161
+ };
162
+ var filterTargetedDiscounts = (discounts, targetType, targetId) => {
163
+ return sortDiscountsByCreatedAt(
164
+ discounts.filter(
165
+ (discount) => discount.targetType === targetType && discount.targetId === targetId
166
+ )
167
+ );
168
+ };
169
+ var getSeatGrossAmount = (seat) => {
170
+ return seat.assignments.reduce((sum, assignment) => {
171
+ assertMinorUnit(assignment.amount, `seat assignment amount (${seat.id})`);
172
+ return sum + assignment.amount;
173
+ }, 0);
174
+ };
175
+ var getEqualSplitPartIds = (equalSplitState) => {
176
+ const partCount = Math.max(equalSplitState.partCount, equalSplitState.parts.length);
177
+ const ids = equalSplitState.parts.map((part) => part.id);
178
+ for (let index = ids.length; index < partCount; index += 1) {
179
+ ids.push(`__generated_equal_split_part_${index + 1}`);
180
+ }
181
+ return ids;
182
+ };
183
+ function calculateItemSplitSeatPricing(details) {
184
+ if (!details.tableService) {
185
+ return EMPTY_SPLIT_PRICING_RESULT;
186
+ }
187
+ const targets = details.tableService.itemSplitSeats.map(
188
+ (seat) => calculateTargetPricing(
189
+ seat.id,
190
+ getSeatGrossAmount(seat),
191
+ filterTargetedDiscounts(
192
+ details.discounts,
193
+ PosDiscountTargetType.ItemSplitSeat,
194
+ seat.id
195
+ )
196
+ )
197
+ );
198
+ return buildSplitPricingResult(targets);
199
+ }
200
+ function calculateEqualSplitPartPricing(details) {
201
+ assertMinorUnit(details.totalAmount, "details.totalAmount");
202
+ if (!details.equalSplitState || details.equalSplitState.partCount <= 0) {
203
+ return EMPTY_SPLIT_PRICING_RESULT;
204
+ }
205
+ const partIds = getEqualSplitPartIds(details.equalSplitState);
206
+ const grossAmounts = allocateProportionalMinorUnits(
207
+ new Array(partIds.length).fill(1),
208
+ details.totalAmount
209
+ );
210
+ const targets = partIds.map(
211
+ (partId, index) => calculateTargetPricing(
212
+ partId,
213
+ grossAmounts[index] ?? 0,
214
+ filterTargetedDiscounts(
215
+ details.discounts,
216
+ PosDiscountTargetType.EqualSplitPart,
217
+ partId
218
+ )
219
+ )
220
+ );
221
+ return buildSplitPricingResult(targets);
222
+ }
88
223
  function calculateItemPrice(details) {
89
224
  assertMinorUnit(details.basePriceInMinorUnit, "details.basePriceInMinorUnit");
90
225
  assertMinorUnit(details.quantity, "details.quantity");
@@ -147,7 +282,9 @@ function calculateCartPriceSnapshot(details) {
147
282
  (sum, item) => sum + (item.itemPrice.priceBreakdown.totalDiscounts.amount ?? 0),
148
283
  0
149
284
  );
150
- const cartDiscounts = details.discounts.filter((discount) => discount.scope === DiscountScope.Cart).sort(
285
+ const cartDiscounts = details.discounts.filter(
286
+ (discount) => discount.scope === DiscountScope.Cart && (discount.targetType === null || discount.targetType === void 0 || discount.targetType === PosDiscountTargetType.Order)
287
+ ).sort(
151
288
  (left, right) => new Date(left.createdAt).getTime() - new Date(right.createdAt).getTime()
152
289
  );
153
290
  const grossAfterItemDiscounts = Math.max(
@@ -158,7 +295,21 @@ function calculateCartPriceSnapshot(details) {
158
295
  cartDiscounts,
159
296
  grossAfterItemDiscounts
160
297
  );
161
- const totalDiscountsAmount = subtotalItemDiscountsAmount + cartDiscountCalculation.totalDiscountAmount;
298
+ const itemSplitSeatPricing = details.checkoutModel === CheckoutModelEnum.ItemSplit ? calculateItemSplitSeatPricing({
299
+ tableService: details.tableService,
300
+ discounts: details.discounts
301
+ }) : null;
302
+ const equalSplitPartPricing = details.checkoutModel === CheckoutModelEnum.EqualSplit ? calculateEqualSplitPartPricing({
303
+ totalAmount: cartDiscountCalculation.remainingAmount,
304
+ equalSplitState: details.equalSplitState,
305
+ discounts: details.discounts
306
+ }) : null;
307
+ const subtotalTargetedDiscountsAmount = itemSplitSeatPricing?.totalDiscountAmount ?? equalSplitPartPricing?.totalDiscountAmount ?? 0;
308
+ const totalAmount = Math.max(
309
+ 0,
310
+ cartDiscountCalculation.remainingAmount - subtotalTargetedDiscountsAmount
311
+ );
312
+ const totalDiscountsAmount = subtotalItemDiscountsAmount + cartDiscountCalculation.totalDiscountAmount + subtotalTargetedDiscountsAmount;
162
313
  const price = {
163
314
  priceBreakdown: {
164
315
  totalBeforeDiscounts: {
@@ -179,7 +330,7 @@ function calculateCartPriceSnapshot(details) {
179
330
  }
180
331
  },
181
332
  total: {
182
- amount: cartDiscountCalculation.remainingAmount,
333
+ amount: totalAmount,
183
334
  currency: details.currency
184
335
  }
185
336
  };
@@ -188,52 +339,17 @@ function calculateCartPriceSnapshot(details) {
188
339
  totalBeforeDiscountsAmount,
189
340
  subtotalItemDiscountsAmount,
190
341
  subtotalBasketDiscountsAmount: cartDiscountCalculation.totalDiscountAmount,
342
+ subtotalTargetedDiscountsAmount,
191
343
  totalDiscountsAmount,
192
- totalAmount: cartDiscountCalculation.remainingAmount,
344
+ totalAmount,
345
+ itemSplitSeatPricing,
346
+ equalSplitPartPricing,
193
347
  price
194
348
  };
195
349
  }
196
350
 
197
351
  // src/tax/index.ts
198
352
  import { KitchenStatus as KitchenStatus2 } from "@munchi_oy/core";
199
-
200
- // src/discount/modulo.ts
201
- var allocateProportionalMinorUnits = withSafeIntegers(
202
- (weights, totalAmount) => {
203
- for (let i = 0; i < weights.length; i++) {
204
- if (!Number.isInteger(weights[i])) {
205
- throw new Error(
206
- `[CartEngine SafeLock] Weight at index ${i} MUST be an integer. Received: ${weights[i]}`
207
- );
208
- }
209
- }
210
- if (weights.length === 0 || totalAmount <= 0) {
211
- return new Array(weights.length).fill(0);
212
- }
213
- const totalWeight = weights.reduce((sum, weight) => sum + weight, 0);
214
- if (totalWeight <= 0) {
215
- return new Array(weights.length).fill(0);
216
- }
217
- const baseAllocations = weights.map(
218
- (weight) => Math.floor(weight * totalAmount / totalWeight)
219
- );
220
- const remainderOrder = weights.map((weight, index) => ({
221
- index,
222
- remainder: weight * totalAmount % totalWeight
223
- })).sort(
224
- (left, right) => right.remainder - left.remainder || left.index - right.index
225
- );
226
- let remainingAmount = totalAmount - baseAllocations.reduce((sum, amount) => sum + amount, 0);
227
- for (const { index } of remainderOrder) {
228
- if (remainingAmount <= 0) break;
229
- baseAllocations[index] = (baseAllocations[index] ?? 0) + 1;
230
- remainingAmount -= 1;
231
- }
232
- return baseAllocations;
233
- }
234
- );
235
-
236
- // src/tax/index.ts
237
353
  function assertMinorUnit2(value, fieldName) {
238
354
  if (!Number.isInteger(value)) {
239
355
  throw new Error(
@@ -341,6 +457,76 @@ function calculateOrderTaxSummary(details) {
341
457
 
342
458
  // src/cart/Cart.ts
343
459
  var { isEqual } = lodash;
460
+ function cloneEqualSplitPart(part) {
461
+ return {
462
+ ...part,
463
+ name: part.name ?? null,
464
+ loyaltyId: part.loyaltyId ?? null,
465
+ loyaltyUserName: part.loyaltyUserName ?? null,
466
+ appliedDiscountIds: [...part.appliedDiscountIds ?? []]
467
+ };
468
+ }
469
+ function cloneEqualSplitState(equalSplitState) {
470
+ if (!equalSplitState) {
471
+ return null;
472
+ }
473
+ return {
474
+ partCount: equalSplitState.partCount,
475
+ parts: (equalSplitState.parts ?? []).map(cloneEqualSplitPart)
476
+ };
477
+ }
478
+ function createEmptyEqualSplitState() {
479
+ return {
480
+ partCount: 0,
481
+ parts: []
482
+ };
483
+ }
484
+ function cloneItemSplitSeatAssignment(assignment) {
485
+ return {
486
+ ...assignment,
487
+ appliedDiscountIds: [...assignment.appliedDiscountIds ?? []]
488
+ };
489
+ }
490
+ function cloneItemSplitSeat(seat) {
491
+ return {
492
+ ...seat,
493
+ name: seat.name ?? null,
494
+ loyaltyId: seat.loyaltyId ?? null,
495
+ loyaltyUserName: seat.loyaltyUserName ?? null,
496
+ appliedDiscountIds: [...seat.appliedDiscountIds ?? []],
497
+ assignments: (seat.assignments ?? []).map(cloneItemSplitSeatAssignment)
498
+ };
499
+ }
500
+ function cloneTableService(tableService) {
501
+ if (!tableService) {
502
+ return null;
503
+ }
504
+ return {
505
+ name: tableService.name ?? null,
506
+ seats: tableService.seats,
507
+ itemSplitSeats: (tableService.itemSplitSeats ?? []).map(cloneItemSplitSeat)
508
+ };
509
+ }
510
+ function createEmptyTableService() {
511
+ return {
512
+ name: null,
513
+ seats: 0,
514
+ itemSplitSeats: []
515
+ };
516
+ }
517
+ function validateEqualSplitState(equalSplitState) {
518
+ if (!equalSplitState) {
519
+ return;
520
+ }
521
+ if (!Number.isInteger(equalSplitState.partCount) || equalSplitState.partCount < 0) {
522
+ throw new Error("Equal split part count must be a non-negative integer");
523
+ }
524
+ if (equalSplitState.parts.length > equalSplitState.partCount) {
525
+ throw new Error(
526
+ "Equal split state cannot contain more parts than partCount"
527
+ );
528
+ }
529
+ }
344
530
  var Cart = class _Cart {
345
531
  id;
346
532
  businessId;
@@ -365,11 +551,13 @@ var Cart = class _Cart {
365
551
  _customer = null;
366
552
  _refunds = [];
367
553
  _tableService;
554
+ _equalSplitState;
368
555
  _taxSummary;
369
556
  _newlyCancelledItemIds;
370
557
  _invoiceCompany;
371
558
  _staffId;
372
559
  _shiftId;
560
+ _aggregateMatchingItemsOnAdd;
373
561
  version = "1.0.2";
374
562
  constructor(id, createdAt, options) {
375
563
  this.id = id;
@@ -378,6 +566,7 @@ var Cart = class _Cart {
378
566
  this.provider = options.provider;
379
567
  this.orderType = options.orderType;
380
568
  this.orderNumber = options.orderNumber;
569
+ this._aggregateMatchingItemsOnAdd = options.aggregateMatchingItemsOnAdd;
381
570
  this.checkoutModel = options.checkoutModel;
382
571
  this._business = options.business;
383
572
  this.currency = options.currency;
@@ -390,7 +579,8 @@ var Cart = class _Cart {
390
579
  this.status = OrderStatusEnum.Draft;
391
580
  this.orderFormat = OrderFormat.Instant;
392
581
  this._locatorType = LocatorType.Table;
393
- this._tableService = options.tableService ?? null;
582
+ this._tableService = cloneTableService(options.tableService);
583
+ this._equalSplitState = cloneEqualSplitState(options.equalSplitState);
394
584
  this._newlyCancelledItemIds = /* @__PURE__ */ new Set();
395
585
  this._staffId = options.staffId ?? null;
396
586
  this._shiftId = options.shiftId ?? null;
@@ -402,8 +592,10 @@ var Cart = class _Cart {
402
592
  provider: options.provider ?? Provider.MunchiPos,
403
593
  orderType: options.orderType ?? OrderTypePOS.DineIn,
404
594
  orderNumber: options.orderNumber,
595
+ aggregateMatchingItemsOnAdd: options.aggregateMatchingItemsOnAdd ?? false,
405
596
  checkoutModel: options.checkoutModel ?? CheckoutModel.Full,
406
597
  tableService: options.tableService ?? null,
598
+ equalSplitState: options.equalSplitState ?? null,
407
599
  currency: options.currency,
408
600
  business: options.business,
409
601
  staffId: options.staffId,
@@ -416,8 +608,10 @@ var Cart = class _Cart {
416
608
  provider: orderDto.provider,
417
609
  orderType: orderDto.orderType,
418
610
  orderNumber: orderDto.orderNumber,
611
+ aggregateMatchingItemsOnAdd: false,
419
612
  checkoutModel: orderDto.checkoutModel,
420
613
  tableService: orderDto.tableService ?? null,
614
+ equalSplitState: orderDto.equalSplitState ?? null,
421
615
  currency: orderDto.currency,
422
616
  business: orderDto.business,
423
617
  staffId: orderDto.staffId ?? null,
@@ -439,7 +633,8 @@ var Cart = class _Cart {
439
633
  cart._refunds = orderDto.refunds ? [...orderDto.refunds] : [];
440
634
  cart._loyaltyTransactionIds = [...orderDto.loyaltyTransactionIds];
441
635
  cart._loyaltyProgramId = orderDto.loyaltyProgramId ?? null;
442
- cart._tableService = orderDto.tableService ? { ...orderDto.tableService } : null;
636
+ cart._tableService = cloneTableService(orderDto.tableService);
637
+ cart._equalSplitState = cloneEqualSplitState(orderDto.equalSplitState);
443
638
  cart._newlyCancelledItemIds = /* @__PURE__ */ new Set();
444
639
  cart._taxSummary = orderDto.taxSummary;
445
640
  cart._invoiceCompany = orderDto.invoiceCompany ?? null;
@@ -475,17 +670,29 @@ var Cart = class _Cart {
475
670
  get invoiceCompany() {
476
671
  return this._invoiceCompany;
477
672
  }
673
+ get aggregateMatchingItemsOnAdd() {
674
+ return this._aggregateMatchingItemsOnAdd;
675
+ }
478
676
  get guestCount() {
479
677
  return this._tableService?.seats ?? null;
480
678
  }
481
679
  get tableService() {
482
- return this._tableService;
680
+ return this.getTableService();
681
+ }
682
+ get equalSplitState() {
683
+ return this.getEqualSplitState();
684
+ }
685
+ get equalSplitPartCount() {
686
+ return this._equalSplitState?.partCount ?? null;
483
687
  }
484
688
  get price() {
485
689
  return calculateCartPriceSnapshot({
486
690
  items: this._items,
487
691
  discounts: this._discounts,
488
- currency: this.currency
692
+ currency: this.currency,
693
+ checkoutModel: this.checkoutModel,
694
+ tableService: this._tableService,
695
+ equalSplitState: this._equalSplitState
489
696
  }).price;
490
697
  }
491
698
  setInvoiceCompany(company) {
@@ -495,6 +702,9 @@ var Cart = class _Cart {
495
702
  this._invoiceCompany = company;
496
703
  this.touch();
497
704
  }
705
+ setAggregateMatchingItemsOnAdd(enabled) {
706
+ this._aggregateMatchingItemsOnAdd = enabled;
707
+ }
498
708
  addItem(itemData, requiresPrep = false) {
499
709
  const lockedStatuses = [
500
710
  KitchenStatus3.DispatchedToKitchen,
@@ -502,9 +712,9 @@ var Cart = class _Cart {
502
712
  KitchenStatus3.Completed,
503
713
  KitchenStatus3.Cancelled
504
714
  ];
505
- const existingModifiableItem = this._items.find(
715
+ const existingModifiableItem = this._aggregateMatchingItemsOnAdd ? this._items.find(
506
716
  (item) => item.posId === itemData.posId && isEqual(item.options, itemData.options) && item.comments === itemData.comments && !lockedStatuses.includes(item.kitchenStatus) && item.discount === null
507
- );
717
+ ) : void 0;
508
718
  if (existingModifiableItem) {
509
719
  this._items = this._items.map((item) => {
510
720
  if (item.lineItemId !== existingModifiableItem.lineItemId) {
@@ -527,13 +737,24 @@ var Cart = class _Cart {
527
737
  this.touch();
528
738
  return;
529
739
  }
530
- this._items.push({
531
- ...itemData,
532
- lineItemId: createId(),
533
- kitchenStatus: requiresPrep ? KitchenStatus3.Pending : KitchenStatus3.NotApplicable,
534
- paymentStatus: PosPaymentStatus.Unpaid,
535
- discount: null
536
- });
740
+ const quantityPerLine = this._aggregateMatchingItemsOnAdd ? [itemData.quantity] : Array.from({ length: itemData.quantity }, () => 1);
741
+ for (const quantity of quantityPerLine) {
742
+ this._items.push({
743
+ ...itemData,
744
+ quantity,
745
+ itemPrice: calculateItemPrice({
746
+ basePriceInMinorUnit: itemData.basePrice.amount,
747
+ quantity,
748
+ taxRate: itemData.taxRate,
749
+ options: itemData.options,
750
+ currency: this.currency
751
+ }),
752
+ lineItemId: createId(),
753
+ kitchenStatus: requiresPrep ? KitchenStatus3.Pending : KitchenStatus3.NotApplicable,
754
+ paymentStatus: PosPaymentStatus.Unpaid,
755
+ discount: null
756
+ });
757
+ }
537
758
  this.touch();
538
759
  }
539
760
  updateItem(lineItemId, updatedItem) {
@@ -588,21 +809,25 @@ var Cart = class _Cart {
588
809
  this._newlyCancelledItemIds.clear();
589
810
  }
590
811
  applyDiscountToCart(discountData) {
591
- this._discounts.push({
592
- ...discountData,
593
- scope: DiscountScope2.Cart,
594
- lineItemId: null,
595
- createdAt: dayjs().toDate().toISOString()
596
- });
812
+ this._discounts.push(
813
+ this.createDiscountRecord(
814
+ discountData,
815
+ DiscountScope2.Cart,
816
+ null,
817
+ PosDiscountTargetType2.Order,
818
+ null
819
+ )
820
+ );
597
821
  this.touch();
598
822
  }
599
823
  applyDiscountToItem(lineItemId, discountData) {
600
- const newDiscount = {
601
- ...discountData,
602
- scope: DiscountScope2.Item,
824
+ const newDiscount = this.createDiscountRecord(
825
+ discountData,
826
+ DiscountScope2.Item,
603
827
  lineItemId,
604
- createdAt: dayjs().toDate().toISOString()
605
- };
828
+ PosDiscountTargetType2.LineItem,
829
+ lineItemId
830
+ );
606
831
  const existingDiscountIndex = this._discounts.findIndex(
607
832
  (discount) => discount.lineItemId === lineItemId && discount.scope === DiscountScope2.Item
608
833
  );
@@ -633,6 +858,42 @@ var Cart = class _Cart {
633
858
  });
634
859
  this.touch();
635
860
  }
861
+ applyDiscountToItemSplitSeat(seatId, discountData) {
862
+ this._discounts.push(
863
+ this.createDiscountRecord(
864
+ discountData,
865
+ DiscountScope2.Cart,
866
+ null,
867
+ PosDiscountTargetType2.ItemSplitSeat,
868
+ seatId
869
+ )
870
+ );
871
+ this.touch();
872
+ }
873
+ applyDiscountToItemSplitAssignment(assignmentId, discountData) {
874
+ this._discounts.push(
875
+ this.createDiscountRecord(
876
+ discountData,
877
+ DiscountScope2.Cart,
878
+ null,
879
+ PosDiscountTargetType2.ItemSplitAssignment,
880
+ assignmentId
881
+ )
882
+ );
883
+ this.touch();
884
+ }
885
+ applyDiscountToEqualSplitPart(partId, discountData) {
886
+ this._discounts.push(
887
+ this.createDiscountRecord(
888
+ discountData,
889
+ DiscountScope2.Cart,
890
+ null,
891
+ PosDiscountTargetType2.EqualSplitPart,
892
+ partId
893
+ )
894
+ );
895
+ this.touch();
896
+ }
636
897
  removeItemDiscount(lineItemId) {
637
898
  this._items = this._items.map((item) => {
638
899
  if (item.lineItemId !== lineItemId) {
@@ -745,15 +1006,277 @@ var Cart = class _Cart {
745
1006
  this.setCheckoutModel(checkoutModel);
746
1007
  }
747
1008
  setGuestCount(guestCount) {
748
- if ((this._tableService?.seats ?? null) === guestCount) {
1009
+ if (guestCount === null) {
1010
+ if (this._tableService === null) {
1011
+ return;
1012
+ }
1013
+ this._tableService = null;
1014
+ this.touch();
749
1015
  return;
750
1016
  }
751
- this._tableService = guestCount === null ? null : { seats: guestCount };
752
- this.touch();
1017
+ this.setSeatsCount(guestCount);
753
1018
  }
754
1019
  setSeatNumber(guestCount) {
755
1020
  this.setGuestCount(guestCount);
756
1021
  }
1022
+ getTableService() {
1023
+ return cloneTableService(this._tableService);
1024
+ }
1025
+ setTableService(tableService) {
1026
+ const nextTableService = cloneTableService(tableService);
1027
+ if (isEqual(this._tableService, nextTableService)) {
1028
+ return this;
1029
+ }
1030
+ this._tableService = nextTableService;
1031
+ this.touch();
1032
+ return this;
1033
+ }
1034
+ clearTableService() {
1035
+ return this.setTableService(null);
1036
+ }
1037
+ getTableName() {
1038
+ return this._tableService?.name ?? null;
1039
+ }
1040
+ setTableName(name) {
1041
+ const tableService = this.getTableService() ?? createEmptyTableService();
1042
+ if (tableService.name === name) {
1043
+ return this;
1044
+ }
1045
+ tableService.name = name;
1046
+ return this.setTableService(tableService);
1047
+ }
1048
+ getSeatsCount() {
1049
+ return this._tableService?.seats ?? null;
1050
+ }
1051
+ setSeatsCount(seats) {
1052
+ if (!Number.isInteger(seats) || seats < 0) {
1053
+ throw new Error("Seats count must be a non-negative integer");
1054
+ }
1055
+ const tableService = this.getTableService() ?? createEmptyTableService();
1056
+ if (tableService.seats === seats) {
1057
+ return this;
1058
+ }
1059
+ tableService.seats = seats;
1060
+ return this.setTableService(tableService);
1061
+ }
1062
+ getItemSplitSeats() {
1063
+ return this._tableService ? this._tableService.itemSplitSeats.map(cloneItemSplitSeat) : [];
1064
+ }
1065
+ setItemSplitSeats(seats) {
1066
+ const tableService = this.getTableService() ?? createEmptyTableService();
1067
+ tableService.itemSplitSeats = seats.map(cloneItemSplitSeat);
1068
+ return this.setTableService(tableService);
1069
+ }
1070
+ clearItemSplitSeats() {
1071
+ return this.setItemSplitSeats([]);
1072
+ }
1073
+ addItemSplitSeat(seat) {
1074
+ if (this.getItemSplitSeatById(seat.id)) {
1075
+ return this;
1076
+ }
1077
+ const seats = this.getItemSplitSeats();
1078
+ seats.push(cloneItemSplitSeat(seat));
1079
+ return this.setItemSplitSeats(seats);
1080
+ }
1081
+ updateItemSplitSeat(seatId, updater) {
1082
+ const seats = this.getItemSplitSeats();
1083
+ const seatIndex = seats.findIndex((seat) => seat.id === seatId);
1084
+ if (seatIndex === -1 || !seats[seatIndex]) {
1085
+ return this;
1086
+ }
1087
+ seats[seatIndex] = cloneItemSplitSeat(updater(cloneItemSplitSeat(seats[seatIndex])));
1088
+ return this.setItemSplitSeats(seats);
1089
+ }
1090
+ removeItemSplitSeat(seatId) {
1091
+ const seats = this.getItemSplitSeats();
1092
+ const nextSeats = seats.filter((seat) => seat.id !== seatId);
1093
+ if (nextSeats.length === seats.length) {
1094
+ return this;
1095
+ }
1096
+ return this.setItemSplitSeats(nextSeats);
1097
+ }
1098
+ getItemSplitSeatById(seatId) {
1099
+ const seat = this._tableService?.itemSplitSeats.find(
1100
+ (itemSplitSeat) => itemSplitSeat.id === seatId
1101
+ );
1102
+ return seat ? cloneItemSplitSeat(seat) : null;
1103
+ }
1104
+ setItemSplitSeatAssignments(seatId, assignments) {
1105
+ return this.updateItemSplitSeat(seatId, (seat) => ({
1106
+ ...seat,
1107
+ assignments: assignments.map(cloneItemSplitSeatAssignment)
1108
+ }));
1109
+ }
1110
+ addItemSplitSeatAssignment(seatId, assignment) {
1111
+ return this.updateItemSplitSeat(seatId, (seat) => ({
1112
+ ...seat,
1113
+ assignments: [...seat.assignments, cloneItemSplitSeatAssignment(assignment)]
1114
+ }));
1115
+ }
1116
+ removeItemSplitSeatAssignmentsByLineItem(seatId, lineItemId) {
1117
+ return this.updateItemSplitSeat(seatId, (seat) => ({
1118
+ ...seat,
1119
+ assignments: seat.assignments.filter(
1120
+ (assignment) => assignment.lineItemId !== lineItemId
1121
+ )
1122
+ }));
1123
+ }
1124
+ clearItemSplitSeatAssignments(seatId) {
1125
+ return this.setItemSplitSeatAssignments(seatId, []);
1126
+ }
1127
+ setItemSplitSeatLoyalty(seatId, loyaltyId, loyaltyUserName) {
1128
+ return this.updateItemSplitSeat(seatId, (seat) => ({
1129
+ ...seat,
1130
+ loyaltyId,
1131
+ loyaltyUserName
1132
+ }));
1133
+ }
1134
+ clearItemSplitSeatLoyalty(seatId) {
1135
+ return this.setItemSplitSeatLoyalty(seatId, null, null);
1136
+ }
1137
+ setItemSplitSeatAppliedDiscountIds(seatId, appliedDiscountIds) {
1138
+ return this.updateItemSplitSeat(seatId, (seat) => ({
1139
+ ...seat,
1140
+ appliedDiscountIds: [...appliedDiscountIds]
1141
+ }));
1142
+ }
1143
+ addItemSplitSeatAppliedDiscountId(seatId, discountId) {
1144
+ return this.updateItemSplitSeat(seatId, (seat) => {
1145
+ if (seat.appliedDiscountIds.includes(discountId)) {
1146
+ return seat;
1147
+ }
1148
+ return {
1149
+ ...seat,
1150
+ appliedDiscountIds: [...seat.appliedDiscountIds, discountId]
1151
+ };
1152
+ });
1153
+ }
1154
+ removeItemSplitSeatAppliedDiscountId(seatId, discountId) {
1155
+ return this.updateItemSplitSeat(seatId, (seat) => ({
1156
+ ...seat,
1157
+ appliedDiscountIds: seat.appliedDiscountIds.filter(
1158
+ (appliedDiscountId) => appliedDiscountId !== discountId
1159
+ )
1160
+ }));
1161
+ }
1162
+ clearItemSplitSeatAppliedDiscountIds(seatId) {
1163
+ return this.setItemSplitSeatAppliedDiscountIds(seatId, []);
1164
+ }
1165
+ getEqualSplitState() {
1166
+ return cloneEqualSplitState(this._equalSplitState);
1167
+ }
1168
+ setEqualSplitState(equalSplitState) {
1169
+ const nextEqualSplitState = cloneEqualSplitState(equalSplitState);
1170
+ validateEqualSplitState(nextEqualSplitState);
1171
+ if (isEqual(this._equalSplitState, nextEqualSplitState)) {
1172
+ return this;
1173
+ }
1174
+ this._equalSplitState = nextEqualSplitState;
1175
+ this.touch();
1176
+ return this;
1177
+ }
1178
+ clearEqualSplitState() {
1179
+ return this.setEqualSplitState(null);
1180
+ }
1181
+ getEqualSplitParts() {
1182
+ return this._equalSplitState ? this._equalSplitState.parts.map(cloneEqualSplitPart) : [];
1183
+ }
1184
+ setEqualSplitParts(parts) {
1185
+ if (parts.length === 0) {
1186
+ return this.clearEqualSplitState();
1187
+ }
1188
+ const equalSplitState = this.getEqualSplitState() ?? createEmptyEqualSplitState();
1189
+ equalSplitState.parts = parts.map(cloneEqualSplitPart);
1190
+ equalSplitState.partCount = parts.length;
1191
+ return this.setEqualSplitState(equalSplitState);
1192
+ }
1193
+ clearEqualSplitParts() {
1194
+ return this.clearEqualSplitState();
1195
+ }
1196
+ addEqualSplitPart(part) {
1197
+ if (this.getEqualSplitPartById(part.id)) {
1198
+ return this;
1199
+ }
1200
+ const parts = this.getEqualSplitParts();
1201
+ parts.push(cloneEqualSplitPart(part));
1202
+ return this.setEqualSplitParts(parts);
1203
+ }
1204
+ updateEqualSplitPart(partId, updater) {
1205
+ const parts = this.getEqualSplitParts();
1206
+ const partIndex = parts.findIndex((part) => part.id === partId);
1207
+ if (partIndex === -1 || !parts[partIndex]) {
1208
+ return this;
1209
+ }
1210
+ parts[partIndex] = cloneEqualSplitPart(updater(cloneEqualSplitPart(parts[partIndex])));
1211
+ return this.setEqualSplitParts(parts);
1212
+ }
1213
+ removeEqualSplitPart(partId) {
1214
+ const parts = this.getEqualSplitParts();
1215
+ const nextParts = parts.filter((part) => part.id !== partId);
1216
+ if (nextParts.length === parts.length) {
1217
+ return this;
1218
+ }
1219
+ return nextParts.length === 0 ? this.clearEqualSplitState() : this.setEqualSplitParts(nextParts);
1220
+ }
1221
+ getEqualSplitPartById(partId) {
1222
+ const part = this._equalSplitState?.parts.find(
1223
+ (equalSplitPart) => equalSplitPart.id === partId
1224
+ );
1225
+ return part ? cloneEqualSplitPart(part) : null;
1226
+ }
1227
+ getEqualSplitPartCount() {
1228
+ return this._equalSplitState?.partCount ?? null;
1229
+ }
1230
+ setEqualSplitPartCount(partCount) {
1231
+ if (partCount === null) {
1232
+ return this.clearEqualSplitState();
1233
+ }
1234
+ if (!Number.isInteger(partCount) || partCount <= 0) {
1235
+ throw new Error("Equal split part count must be a positive integer or null");
1236
+ }
1237
+ const equalSplitState = this.getEqualSplitState() ?? createEmptyEqualSplitState();
1238
+ if (equalSplitState.parts.length > partCount) {
1239
+ throw new Error(
1240
+ "Equal split part count cannot be less than persisted parts length"
1241
+ );
1242
+ }
1243
+ if (equalSplitState.partCount === partCount) {
1244
+ return this;
1245
+ }
1246
+ equalSplitState.partCount = partCount;
1247
+ return this.setEqualSplitState(equalSplitState);
1248
+ }
1249
+ clearEqualSplitPartCount() {
1250
+ return this.clearEqualSplitState();
1251
+ }
1252
+ setEqualSplitPartAppliedDiscountIds(partId, appliedDiscountIds) {
1253
+ return this.updateEqualSplitPart(partId, (part) => ({
1254
+ ...part,
1255
+ appliedDiscountIds: [...appliedDiscountIds]
1256
+ }));
1257
+ }
1258
+ addEqualSplitPartAppliedDiscountId(partId, discountId) {
1259
+ return this.updateEqualSplitPart(partId, (part) => {
1260
+ if (part.appliedDiscountIds.includes(discountId)) {
1261
+ return part;
1262
+ }
1263
+ return {
1264
+ ...part,
1265
+ appliedDiscountIds: [...part.appliedDiscountIds, discountId]
1266
+ };
1267
+ });
1268
+ }
1269
+ removeEqualSplitPartAppliedDiscountId(partId, discountId) {
1270
+ return this.updateEqualSplitPart(partId, (part) => ({
1271
+ ...part,
1272
+ appliedDiscountIds: part.appliedDiscountIds.filter(
1273
+ (appliedDiscountId) => appliedDiscountId !== discountId
1274
+ )
1275
+ }));
1276
+ }
1277
+ clearEqualSplitPartAppliedDiscountIds(partId) {
1278
+ return this.setEqualSplitPartAppliedDiscountIds(partId, []);
1279
+ }
757
1280
  updateOrderStatus(newStatus) {
758
1281
  if (this.status === newStatus) {
759
1282
  return;
@@ -812,7 +1335,8 @@ var Cart = class _Cart {
812
1335
  locatorType: this._locatorType,
813
1336
  spotNumber: this.spotNumber,
814
1337
  comments: this.comments,
815
- tableService: this._tableService,
1338
+ tableService: cloneTableService(this._tableService),
1339
+ equalSplitState: cloneEqualSplitState(this._equalSplitState),
816
1340
  orderRefundStatus: this.orderRefundStatus,
817
1341
  items: this._items,
818
1342
  status: this.status,
@@ -839,8 +1363,10 @@ var Cart = class _Cart {
839
1363
  provider: this.provider,
840
1364
  orderType: this.orderType,
841
1365
  orderNumber: this.orderNumber,
1366
+ aggregateMatchingItemsOnAdd: this._aggregateMatchingItemsOnAdd,
842
1367
  checkoutModel: this.checkoutModel,
843
1368
  tableService: this._tableService,
1369
+ equalSplitState: this._equalSplitState,
844
1370
  currency: this.currency,
845
1371
  business: this._business,
846
1372
  staffId: this._staffId,
@@ -859,7 +1385,8 @@ var Cart = class _Cart {
859
1385
  newCart._refunds = [...this._refunds];
860
1386
  newCart._loyaltyProgramId = this._loyaltyProgramId;
861
1387
  newCart._loyaltyTransactionIds = [...this._loyaltyTransactionIds];
862
- newCart._tableService = this._tableService ? { ...this._tableService } : null;
1388
+ newCart._tableService = cloneTableService(this._tableService);
1389
+ newCart._equalSplitState = cloneEqualSplitState(this._equalSplitState);
863
1390
  newCart._taxSummary = this._taxSummary;
864
1391
  newCart._newlyCancelledItemIds = new Set(this._newlyCancelledItemIds);
865
1392
  newCart._invoiceCompany = this._invoiceCompany ? { ...this._invoiceCompany } : null;
@@ -872,6 +1399,16 @@ var Cart = class _Cart {
872
1399
  currency: this.currency
873
1400
  });
874
1401
  }
1402
+ createDiscountRecord(discountData, scope, lineItemId, targetType, targetId) {
1403
+ return {
1404
+ ...discountData,
1405
+ scope,
1406
+ lineItemId,
1407
+ targetType,
1408
+ targetId,
1409
+ createdAt: dayjs().toDate().toISOString()
1410
+ };
1411
+ }
875
1412
  touch() {
876
1413
  this.updatedAt = dayjs().toDate();
877
1414
  }
@@ -885,7 +1422,7 @@ var ApplyTo = /* @__PURE__ */ ((ApplyTo2) => {
885
1422
  })(ApplyTo || {});
886
1423
 
887
1424
  // src/version.ts
888
- var VERSION = "0.1.2";
1425
+ var VERSION = "0.1.5";
889
1426
  export {
890
1427
  ApplyTo,
891
1428
  Cart,
@@ -893,7 +1430,9 @@ export {
893
1430
  allocateProportionalMinorUnits,
894
1431
  calculateCartPriceSnapshot,
895
1432
  calculateDiscountAmount,
1433
+ calculateEqualSplitPartPricing,
896
1434
  calculateItemPrice,
1435
+ calculateItemSplitSeatPricing,
897
1436
  calculateOrderTaxSummary,
898
1437
  calculateSequentialDiscountTotal,
899
1438
  splitTaxInclusiveAmount