@tallyui/pos 3.7.1 → 3.9.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 CHANGED
@@ -110,7 +110,7 @@ function useCurrencyCode() {
110
110
  }
111
111
 
112
112
  // src/order/woocommerce-tax.ts
113
- import { minorUnitDigits, woocommerceTax } from "@tallyui/core";
113
+ import { minorUnitDigits, woocommerceTax, woocommerceCoupons } from "@tallyui/core";
114
114
  function woocommerceLine(amountMinor, rates, inclusive, currency, roundAtSubtotal, pricesIncludeTax = inclusive) {
115
115
  const dp = minorUnitDigits(currency), factor = 10 ** dp;
116
116
  const precision = woocommerceTax.getRoundingPrecision(dp);
@@ -124,17 +124,21 @@ function woocommerceLine(amountMinor, rates, inclusive, currency, roundAtSubtota
124
124
  let result = woocommerceTax.calculateTaxes(input);
125
125
  const net = woocommerceTax.roundHalfUp(input.amount - (inclusive ? result.total : 0), precision);
126
126
  if (inclusive && rates.length) result = woocommerceTax.calculateTaxes({ ...input, amount: net, amountIncludesTax: false });
127
- const taxLines = result.taxes.map((tax2) => {
128
- const rate = rates.find((rate2) => rate2.id === tax2.id);
127
+ const taxLines = result.taxes.map((tax) => {
128
+ const rate = rates.find((rate2) => rate2.id === tax.id);
129
129
  return {
130
130
  code: rate.code,
131
131
  rateId: rate.id,
132
132
  compound: rate.compound,
133
133
  ratePpm: Math.round(Number(rate.rate) * 1e4),
134
- taxMicros: String(Math.round(tax2.total * factor * 1e6))
134
+ taxMicros: String(Math.round(tax.total * factor * 1e6))
135
135
  };
136
136
  });
137
- const tax = result.taxes.reduce((sum, rate) => sum + (roundAtSubtotal ? rate.total : woocommerceTax.roundTaxTotal(rate.total, dp, pricesIncludeTax)), 0);
137
+ return lineAmounts(net, result.taxes, taxLines, dp, roundAtSubtotal, pricesIncludeTax);
138
+ }
139
+ function lineAmounts(net, taxes, taxLines, dp, roundAtSubtotal, pricesIncludeTax) {
140
+ const factor = 10 ** dp;
141
+ const tax = taxes.reduce((sum, rate) => sum + (roundAtSubtotal ? rate.total : woocommerceTax.roundTaxTotal(rate.total, dp, pricesIncludeTax)), 0);
138
142
  return {
139
143
  netMinor: Math.round(woocommerceTax.roundHalfUp(net, dp) * factor),
140
144
  netMicros: String(Math.round(net * factor * 1e6)),
@@ -143,6 +147,69 @@ function woocommerceLine(amountMinor, rates, inclusive, currency, roundAtSubtota
143
147
  taxMicros: taxLines.reduce((sum, rate) => sum + BigInt(rate.taxMicros), 0n).toString()
144
148
  };
145
149
  }
150
+ function woocommerceCouponReplay(lines, codes, context, getTaxRates, currency, roundAtSubtotal, pricesIncludeTax) {
151
+ const dp = minorUnitDigits(currency), factor = 10 ** dp;
152
+ const inputLines = lines.filter((line) => line.unitPriceMinor >= 0);
153
+ const lineItems = inputLines.map((line) => {
154
+ const productId = Number(line.productId), variationId = Number(line.variantId);
155
+ const net = (Number(line.netMicros) / (factor * 1e6)).toFixed(6);
156
+ const taxes = line.taxLines.map((tax2) => {
157
+ const amount = (Number(tax2.taxMicros) / (factor * 1e6)).toFixed(6);
158
+ return { id: tax2.rateId, subtotal: amount, total: amount };
159
+ });
160
+ const tax = taxes.reduce((sum, row) => sum + Number(row.total), 0).toFixed(6);
161
+ return {
162
+ product_id: !line.custom && Number.isSafeInteger(productId) && productId >= 0 ? productId : 0,
163
+ ...Number.isSafeInteger(variationId) && variationId >= 0 ? { variation_id: variationId } : {},
164
+ quantity: line.quantity,
165
+ tax_class: line.taxClass ?? "",
166
+ ...line.taxStatus === "none" ? { tax_status: "none" } : {},
167
+ subtotal: net,
168
+ total: net,
169
+ taxes,
170
+ subtotal_tax: tax,
171
+ total_tax: tax
172
+ };
173
+ });
174
+ const taxRates = /* @__PURE__ */ new Map();
175
+ for (const line of inputLines) for (const rate of getTaxRates(line.taxClass)) {
176
+ const taxClass = woocommerceTax.normalizeTaxClass(line.taxClass);
177
+ taxRates.set(JSON.stringify([rate.id, taxClass]), {
178
+ id: rate.id,
179
+ rate: rate.rate,
180
+ compound: rate.compound,
181
+ order: rate.priority,
182
+ priority: rate.priority,
183
+ class: taxClass
184
+ });
185
+ }
186
+ const result = woocommerceCoupons.recalculateCoupons({
187
+ lineItems,
188
+ couponLines: codes.map((code) => ({ code })),
189
+ couponConfigs: new Map(codes.map((code) => [code, context.configs.get(code)])),
190
+ pricesIncludeTax,
191
+ calcDiscountsSequentially: context.calcDiscountsSequentially,
192
+ productCategories: new Map(context.productCategories),
193
+ taxRates: [...taxRates.values()],
194
+ taxRoundAtSubtotal: roundAtSubtotal,
195
+ dp
196
+ });
197
+ let index = 0;
198
+ return {
199
+ lines: lines.map((line) => {
200
+ if (line.unitPriceMinor < 0) return line;
201
+ const item = result.lineItems[index++];
202
+ const taxes = line.taxLines.map((tax) => ({ total: Number(item.taxes?.find((row) => row.id === tax.rateId)?.total ?? "0") }));
203
+ const taxLines = line.taxLines.map((tax, index2) => ({ ...tax, taxMicros: String(Math.round(taxes[index2].total * factor * 1e6)) }));
204
+ return { ...line, ...lineAmounts(Number(item.total), taxes, taxLines, dp, roundAtSubtotal, pricesIncludeTax) };
205
+ }),
206
+ coupons: result.couponLines.map((coupon, index2) => ({
207
+ code: codes[index2],
208
+ discountMinor: Math.round(woocommerceTax.roundHalfUp(Number(coupon.discount), dp) * factor),
209
+ discountTaxMinor: Math.round(woocommerceTax.roundHalfUp(Number(coupon.discount_tax), dp) * factor)
210
+ }))
211
+ };
212
+ }
146
213
  function woocommerceTotals(lines, fees, shipping, currency, roundAtSubtotal, inclusive) {
147
214
  const dp = minorUnitDigits(currency), factor = 10 ** dp;
148
215
  const net = (line) => Number(line.netMicros) / (factor * 1e6);
@@ -611,7 +678,7 @@ function assertDisplayResidue(residue, convertedLines, conversions) {
611
678
  if (Math.abs(residue) > bound) throw new Error(`Display residue ${residue} exceeds its bound of ${bound} (ADR-063)`);
612
679
  }
613
680
  function createOrderBuilder(options) {
614
- const { taxContext } = options;
681
+ const { taxContext, couponContext } = options;
615
682
  const currency = options.currency.toUpperCase();
616
683
  const orderId = options.id ?? uid();
617
684
  const woo = taxContext.rounding?.granularity === "woocommerce" && taxContext.getTaxRates ? taxContext.rounding : void 0;
@@ -628,6 +695,7 @@ function createOrderBuilder(options) {
628
695
  let payments = [];
629
696
  let customer = null;
630
697
  let note = "";
698
+ let couponCodes = [];
631
699
  const now = (/* @__PURE__ */ new Date()).toISOString();
632
700
  const subject = new BehaviorSubject(buildOrder());
633
701
  function recalculateLine(line, orderDiscountMinor = 0) {
@@ -674,16 +742,26 @@ function createOrderBuilder(options) {
674
742
  return Math.max(0, Math.min(discount.value, base));
675
743
  }
676
744
  function buildOrder() {
677
- const lineAmounts = lineItems.map((li) => woo ? li.unitPriceMinor * li.quantity - li.discountMinor : li.netMinor);
678
- const base = lineAmounts.reduce((sum, amount) => sum + Math.max(0, amount), 0);
745
+ const lineAmounts2 = lineItems.map((li) => woo ? li.unitPriceMinor * li.quantity - li.discountMinor : li.netMinor);
746
+ const base = lineAmounts2.reduce((sum, amount) => sum + Math.max(0, amount), 0);
679
747
  let remaining = base;
680
748
  const recalcedOrderDiscounts = orderDiscounts.map((d) => {
681
749
  const amountMinor = Math.min(remaining, computeDiscountAmount(d, d.type === "percentage" ? base : remaining));
682
750
  remaining -= amountMinor;
683
751
  return { ...d, amountMinor };
684
752
  });
685
- const shares = allocateOrderDiscount(lineAmounts, recalcedOrderDiscounts.reduce((sum, d) => sum + d.amountMinor, 0));
686
- const lines = lineItems.map((li, index) => recalculateLine(li, shares[index]));
753
+ const shares = allocateOrderDiscount(lineAmounts2, recalcedOrderDiscounts.reduce((sum, d) => sum + d.amountMinor, 0));
754
+ const displaySourceLines = lineItems.map((li, index) => recalculateLine(li, shares[index]));
755
+ const replay = woo && couponCodes.length ? woocommerceCouponReplay(
756
+ displaySourceLines,
757
+ couponCodes,
758
+ couponContext,
759
+ taxContext.getTaxRates,
760
+ currency,
761
+ woo.roundAtSubtotal,
762
+ taxContext.pricesIncludeTax
763
+ ) : void 0;
764
+ const lines = replay?.lines ?? displaySourceLines;
687
765
  if (woo) shipping = shipping.map((charge) => ({ ...charge, ...recalculateCharge(charge.id, charge, true) }));
688
766
  const wooTotals = woo ? woocommerceTotals(lines, fees, shipping, currency, woo.roundAtSubtotal, taxContext.pricesIncludeTax) : void 0;
689
767
  const { subtotalMinor } = wooTotals ?? sumLines(lines, taxContext.rounding);
@@ -700,7 +778,7 @@ function createOrderBuilder(options) {
700
778
  }
701
779
  const discountMinor = lines.reduce((sum, li) => sum + li.discountMinor, 0);
702
780
  const taxInclusive = taxContext.pricesIncludeTax;
703
- const figures = lines.map((li) => {
781
+ const figures = displaySourceLines.map((li) => {
704
782
  const returned = li.unitPriceMinor < 0;
705
783
  const discounts = returned ? [] : li.discounts.map((d) => ({
706
784
  discountId: d.id,
@@ -714,10 +792,11 @@ function createOrderBuilder(options) {
714
792
  });
715
793
  const displayLines = figures.map((f) => f.line);
716
794
  const orderDiscountDisplay = figures.reduce((sum, f) => sum + f.shareMinor, 0);
795
+ const displayCoupons = replay?.coupons.map(({ code, discountMinor: discountMinor2, discountTaxMinor }) => ({ code, amountMinor: discountMinor2 + (taxInclusive ? discountTaxMinor : 0) }));
717
796
  const displayDiscount = displayLines.reduce(
718
797
  (sum, line) => line.discounts.reduce((lineSum, d) => lineSum + d.amountMinor, sum),
719
798
  orderDiscountDisplay
720
- );
799
+ ) + (displayCoupons?.reduce((sum, row) => sum + row.amountMinor, 0) ?? 0);
721
800
  const chargeRows = (items) => items.map(({ id: id2, name, amountMinor, netMinor, totalMinor: totalMinor2 }) => ({ id: id2, name, amountMinor: woo ? taxInclusive ? totalMinor2 : netMinor : amountMinor }));
722
801
  const displayFees = chargeRows(fees), displayShipping = chargeRows(shipping);
723
802
  let displaySubtotal = (taxInclusive ? totalMinor : totalMinor - taxMinor) + displayDiscount - [...displayFees, ...displayShipping].reduce((sum, charge) => sum + charge.amountMinor, 0);
@@ -729,11 +808,11 @@ function createOrderBuilder(options) {
729
808
  if (charge) displaySubtotal -= residue;
730
809
  residue = displaySubtotal - displayLines.reduce((sum, line) => sum + line.amountMinor, 0);
731
810
  }
732
- const converted = lines.flatMap((li, index) => li.taxInclusive === taxInclusive ? [] : [index]);
811
+ const converted = displaySourceLines.flatMap((li, index) => li.taxInclusive === taxInclusive ? [] : [index]);
733
812
  const conversions = converted.reduce((count, index) => count + [
734
- lines[index].netMinor,
735
- lines[index].orderDiscountMinor,
736
- ...lines[index].discounts.map((d) => d.amountMinor)
813
+ displaySourceLines[index].netMinor,
814
+ displaySourceLines[index].orderDiscountMinor,
815
+ ...displaySourceLines[index].discounts.map((d) => d.amountMinor)
737
816
  ].filter((x) => x !== 0).length, 0);
738
817
  assertDisplayResidue(residue, converted.length, conversions);
739
818
  let left = residue;
@@ -762,6 +841,7 @@ function createOrderBuilder(options) {
762
841
  totalMinor,
763
842
  lines: displayLines,
764
843
  orderDiscountMinor: orderDiscountDisplay,
844
+ ...displayCoupons ? { coupons: displayCoupons } : {},
765
845
  ...fees.length ? { fees: displayFees } : {},
766
846
  ...shipping.length ? { shipping: displayShipping } : {}
767
847
  };
@@ -772,6 +852,7 @@ function createOrderBuilder(options) {
772
852
  id: orderId,
773
853
  status: "draft",
774
854
  lineItems: lines,
855
+ ...replay ? { coupons: replay.coupons } : {},
775
856
  ...fees.length ? { fees: [...fees] } : {},
776
857
  ...shipping.length ? { shipping: [...shipping] } : {},
777
858
  discounts: recalcedOrderDiscounts,
@@ -1038,6 +1119,17 @@ function createOrderBuilder(options) {
1038
1119
  note = n;
1039
1120
  emit();
1040
1121
  },
1122
+ setCoupons(codes) {
1123
+ const normalized = [...new Set(codes.map((code) => code.trim().toLowerCase()))];
1124
+ if (normalized.length && !couponContext) throw new Error("Coupon context is required");
1125
+ for (const code of normalized) {
1126
+ const config = couponContext.configs.get(code);
1127
+ if (!config) throw new RangeError(`Unknown coupon ${code}`);
1128
+ if (config.exclude_sale_items) throw new RangeError(`Coupon ${code} excludes sale items`);
1129
+ }
1130
+ couponCodes = normalized;
1131
+ emit();
1132
+ },
1041
1133
  clear() {
1042
1134
  lineItems = [];
1043
1135
  fees = [];
@@ -1046,6 +1138,7 @@ function createOrderBuilder(options) {
1046
1138
  payments = [];
1047
1139
  customer = null;
1048
1140
  note = "";
1141
+ couponCodes = [];
1049
1142
  emit();
1050
1143
  },
1051
1144
  getSnapshot() {
@@ -1218,9 +1311,10 @@ var orderDraftSchema = {
1218
1311
  required: ["id", "data"]
1219
1312
  };
1220
1313
  async function writeOrderDraft(drafts, snapshot) {
1314
+ const { coupons: _coupons, ...draft } = snapshot;
1221
1315
  await drafts.upsert({
1222
1316
  id: snapshot.id,
1223
- data: JSON.stringify(snapshot),
1317
+ data: JSON.stringify(draft),
1224
1318
  customerName: snapshot.customer?.name ?? "",
1225
1319
  itemCount: snapshot.lineItems.length,
1226
1320
  total: snapshot.totalMinor,
@@ -2361,10 +2455,18 @@ function movementFieldError(input) {
2361
2455
  }
2362
2456
 
2363
2457
  // src/register/expected.ts
2458
+ function refundAmounts(refund) {
2459
+ const result = refund.result;
2460
+ if (typeof result !== "object" || result === null || Object.getPrototypeOf(result) !== Object.prototype && Object.getPrototypeOf(result) !== null) return [];
2461
+ const byMethod = result.byMethod;
2462
+ if (typeof byMethod !== "object" || byMethod === null || Object.getPrototypeOf(byMethod) !== Object.prototype && Object.getPrototypeOf(byMethod) !== null) return [];
2463
+ return Object.entries(byMethod).filter(([method, amount]) => method.length > 0 && Number.isSafeInteger(amount) && amount >= 0);
2464
+ }
2364
2465
  function deriveExpected({
2365
2466
  session,
2366
2467
  movements,
2367
- ledgerRowsBySession
2468
+ ledgerRowsBySession,
2469
+ refunds
2368
2470
  }) {
2369
2471
  const totals = { cash: session.countedFloatMinor };
2370
2472
  for (const row of ledgerRowsBySession) {
@@ -2380,6 +2482,10 @@ function deriveExpected({
2380
2482
  if (row.type === "paid_in") totals.cash += row.amountMinor;
2381
2483
  if (row.type === "paid_out") totals.cash -= row.amountMinor;
2382
2484
  }
2485
+ for (const refund of refunds ?? []) {
2486
+ if (refund.status !== "applied" || refund.sessionId !== session.id) continue;
2487
+ for (const [method, amount] of refundAmounts(refund)) totals[method] = (totals[method] ?? 0) - amount;
2488
+ }
2383
2489
  return totals;
2384
2490
  }
2385
2491
 
@@ -2813,6 +2919,20 @@ var RegisterNeedsUpgradeError = class extends Error {
2813
2919
  this.name = "RegisterNeedsUpgradeError";
2814
2920
  }
2815
2921
  };
2922
+ var RegisterSessionConflictError = class extends Error {
2923
+ code = "REGISTER_SESSION_CONFLICT";
2924
+ constructor() {
2925
+ super("This register is open on another till. Take it over or choose another register.");
2926
+ this.name = "RegisterSessionConflictError";
2927
+ }
2928
+ };
2929
+ var RegisterTakeOverError = class extends Error {
2930
+ constructor(code) {
2931
+ super(code === "REGISTER_TAKEOVER_UNSUPPORTED" ? "This store cannot take over a register. Choose another register." : "Taking over this register is still waiting for the store.");
2932
+ this.code = code;
2933
+ this.name = "RegisterTakeOverError";
2934
+ }
2935
+ };
2816
2936
  var RegisterMovementAmountError = class extends Error {
2817
2937
  constructor(type, amountMinor) {
2818
2938
  super(`movement_amount_invalid:${type}:${amountMinor}`);
@@ -2840,7 +2960,8 @@ var TRANSITIONS = {
2840
2960
  counting: ["open", "closed"],
2841
2961
  closed: [],
2842
2962
  conflict: [],
2843
- superseded: []
2963
+ superseded: [],
2964
+ abandoned: []
2844
2965
  };
2845
2966
  function isKnownSessionStatus(status) {
2846
2967
  return Object.hasOwn(TRANSITIONS, status);
@@ -2858,6 +2979,7 @@ async function requireLiveSession(sessions, id2) {
2858
2979
  if (!session) throw new RegisterSessionRequiredError();
2859
2980
  if (!isKnownSessionStatus(session.status)) throw new RegisterNeedsUpgradeError();
2860
2981
  if (session.status === "closed") throw new RegisterSessionClosedError();
2982
+ if (session.status === "abandoned") throw new RegisterSessionClosedError();
2861
2983
  return session;
2862
2984
  }
2863
2985
  var openSessionSelector = { status: "open" };
@@ -3034,6 +3156,7 @@ async function writeClosure({
3034
3156
  otherTenders,
3035
3157
  movements,
3036
3158
  orders,
3159
+ refunds,
3037
3160
  tillExpected,
3038
3161
  labels,
3039
3162
  resolveCashierName,
@@ -3051,6 +3174,7 @@ async function writeClosure({
3051
3174
  return existing;
3052
3175
  }
3053
3176
  const bound = orders.filter((order) => order.sessionId === session.id);
3177
+ const countedRefunds = (refunds ?? []).filter((refund) => refund.status === "applied" && refund.sessionId === session.id);
3054
3178
  const rows = bound.flatMap(
3055
3179
  (order) => order.payments.map((payment) => ({
3056
3180
  session_id: order.sessionId,
@@ -3064,7 +3188,8 @@ async function writeClosure({
3064
3188
  const till_expected = tillExpected ?? deriveExpected({
3065
3189
  session: { id: session.id, countedFloatMinor: session.counted_float_minor },
3066
3190
  movements: entries,
3067
- ledgerRowsBySession: rows
3191
+ ledgerRowsBySession: rows,
3192
+ refunds
3068
3193
  });
3069
3194
  const counts = { cash: counted, ...otherTenders };
3070
3195
  const sum = (values) => values.reduce((total, value) => total + value, 0);
@@ -3073,6 +3198,11 @@ async function writeClosure({
3073
3198
  const method = row2.kind === "cash" ? "cash" : row2.method_id;
3074
3199
  payment_methods[method] = { sales_minor: (payment_methods[method]?.sales_minor ?? 0) + row2.amountMinor, refunds_minor: 0 };
3075
3200
  }
3201
+ for (const refund of countedRefunds) {
3202
+ for (const [method, amount] of refundAmounts(refund)) {
3203
+ payment_methods[method] = { sales_minor: payment_methods[method]?.sales_minor ?? 0, refunds_minor: (payment_methods[method]?.refunds_minor ?? 0) + amount };
3204
+ }
3205
+ }
3076
3206
  const taxRates = /* @__PURE__ */ new Map();
3077
3207
  const roundings = /* @__PURE__ */ new Set();
3078
3208
  for (const order of bound) {
@@ -3108,7 +3238,7 @@ async function writeClosure({
3108
3238
  Object.entries(counts).map(([method, value]) => [method, countVariance(value, till_expected[method] ?? 0)])
3109
3239
  ),
3110
3240
  period_sales_total_minor: sum(rows.map((row2) => row2.amountMinor)),
3111
- period_refunds_total_minor: 0,
3241
+ period_refunds_total_minor: sum(countedRefunds.flatMap((refund) => refundAmounts(refund).map(([, amount]) => amount))),
3112
3242
  perpetual_sales_total_minor: 0,
3113
3243
  perpetual_refunds_total_minor: 0,
3114
3244
  unsynced_count: unsynced.length,
@@ -3145,7 +3275,11 @@ async function writeClosure({
3145
3275
  voided_by: voided_by ?? null
3146
3276
  })),
3147
3277
  transaction_count: bound.length,
3148
- refund_count: 0,
3278
+ refund_count: countedRefunds.length,
3279
+ ...refunds !== void 0 ? {
3280
+ refund_ids: countedRefunds.map((refund) => refund.id),
3281
+ pending_refund_ids: refunds.filter((refund) => refund.status === "pending" && refund.sessionId === session.id).map((refund) => refund.id)
3282
+ } : {},
3149
3283
  cashiers: [...new Set(bound.map((order) => order.cashierRef ?? "").filter(Boolean))].map((id2) => ({
3150
3284
  id: id2,
3151
3285
  name: resolveCashierName?.(id2) || id2
@@ -3603,8 +3737,17 @@ var registerCommandSchema = {
3603
3737
  indexes: [["registerId", "seq"], ["syncStatus", "registerId", "seq"]]
3604
3738
  };
3605
3739
  var registerCommandCollection = () => ({ schema: registerCommandSchema });
3606
- function sessionOpenCommand(s) {
3607
- return { key: `session.open:${s.id}`, type: "register.session.open", version: 1, payload: {
3740
+ function deviceNameOf(name) {
3741
+ let trimmed = name?.trim();
3742
+ if (trimmed && trimmed.length > 64) {
3743
+ const last = trimmed.charCodeAt(63);
3744
+ trimmed = trimmed.slice(0, last >= 55296 && last <= 56319 ? 63 : 64).trimEnd();
3745
+ }
3746
+ return trimmed || void 0;
3747
+ }
3748
+ function sessionOpenCommand(s, v2) {
3749
+ const deviceName = deviceNameOf(v2?.deviceName);
3750
+ return { key: `session.open:${s.id}`, type: "register.session.open", version: v2 ? 2 : 1, payload: {
3608
3751
  sessionId: s.id,
3609
3752
  registerId: s.register_id,
3610
3753
  openedAt: s.opened_at_gmt,
@@ -3613,7 +3756,8 @@ function sessionOpenCommand(s) {
3613
3756
  ...s.business_day == null ? {} : { businessDay: s.business_day },
3614
3757
  ...s.opened_by == null ? {} : { openedBy: s.opened_by },
3615
3758
  ...s.expected_float_minor == null ? {} : { expectedFloatMinor: s.expected_float_minor },
3616
- ...s.opening_variance_minor == null ? {} : { openingVarianceMinor: s.opening_variance_minor }
3759
+ ...s.opening_variance_minor == null ? {} : { openingVarianceMinor: s.opening_variance_minor },
3760
+ ...deviceName === void 0 ? {} : { deviceName }
3617
3761
  } };
3618
3762
  }
3619
3763
  function sessionTransitionCommand(s) {
@@ -3670,7 +3814,7 @@ function closureCommand(c) {
3670
3814
  } };
3671
3815
  }
3672
3816
  var chains = /* @__PURE__ */ new WeakMap();
3673
- function reconcileRegisterCommands({ commands, sessions, movements, closures, host, storeKey, registerId, now, observed }) {
3817
+ function reconcileRegisterCommands({ commands, sessions, movements, closures, host, storeKey, registerId, now, observed, registerContract, deviceName }) {
3674
3818
  const sentStatus = (s) => s.status === "open" || s.status === "counting" || s.status === "closed";
3675
3819
  let registers = chains.get(commands);
3676
3820
  if (!registers) chains.set(commands, registers = /* @__PURE__ */ new Map());
@@ -3681,7 +3825,7 @@ function reconcileRegisterCommands({ commands, sessions, movements, closures, ho
3681
3825
  const transitioned = new Set((await readFresh(commands, { selector: { registerId, type: "register.session.transition" } })).map(({ payload }) => payload.sessionId));
3682
3826
  const rows = await readFresh(sessions, { selector: { register_id: registerId } });
3683
3827
  const complete = new Set((await commands.storageInstance.findDocumentsById(rows.flatMap((session) => [`session.open:${session.id}`, ...session.status === "closed" && session.closure_id ? [`closure.submit:${session.closure_id}`] : []]), false)).map(({ key }) => key));
3684
- const facts = await Promise.all(rows.filter((session) => session.status !== "closed" || (session.status_at == null || session.status_at >= since || complete.has(`session.open:${session.id}`)) && !complete.has(`closure.submit:${session.closure_id}`)).map(async (session) => {
3828
+ const facts = await Promise.all(rows.filter((session) => session.status !== "abandoned" && (session.status !== "closed" || (session.status_at == null || session.status_at >= since || complete.has(`session.open:${session.id}`)) && !complete.has(`closure.submit:${session.closure_id}`))).map(async (session) => {
3685
3829
  const [entries, closureRows] = await Promise.all([
3686
3830
  readFresh(movements, { selector: { session_id: session.id } }),
3687
3831
  session.closure_id ? closures.storageInstance.findDocumentsById([session.closure_id], false) : Promise.resolve([])
@@ -3693,7 +3837,7 @@ function reconcileRegisterCommands({ commands, sessions, movements, closures, ho
3693
3837
  );
3694
3838
  if (session.status_at != null && sentStatus(session)) events.push(sessionTransitionCommand({ ...session, status_at: session.status_at }));
3695
3839
  return { session, number: closureRows[0]?.number, built: [
3696
- sessionOpenCommand(session),
3840
+ (registerContract ?? 0) >= 2 ? sessionOpenCommand(session, { deviceName }) : sessionOpenCommand(session),
3697
3841
  ...events,
3698
3842
  ...closureRows.map((closure) => closureCommand(closure))
3699
3843
  ] };
@@ -3721,6 +3865,95 @@ function reconcileRegisterCommands({ commands, sessions, movements, closures, ho
3721
3865
  return run;
3722
3866
  }
3723
3867
 
3868
+ // src/register/register-adoption.ts
3869
+ var chains2 = /* @__PURE__ */ new WeakMap();
3870
+ async function adoptRegisterResults({ commands, sessions, registerId }) {
3871
+ const run = (chains2.get(commands) ?? Promise.resolve()).then(async () => {
3872
+ const [answers, transitions, rows] = await Promise.all([
3873
+ readFresh(commands, { selector: { registerId, syncStatus: { $in: ["applied", "rejected"] } }, sort: [{ seq: "asc" }, { key: "asc" }] }),
3874
+ readFresh(commands, { selector: { registerId, type: "register.session.transition" }, sort: [{ seq: "desc" }, { key: "desc" }] }),
3875
+ readFresh(sessions, { selector: { register_id: registerId } })
3876
+ ]);
3877
+ for (const session of rows) {
3878
+ if (session.status === "conflict" && answers.some((answer) => answer.payload.sessionId === session.id && answer.syncStatus === "rejected" && answer.error?.code === "register_session_abandoned")) {
3879
+ await finishAbandon({ commands, sessions, sessionId: session.id });
3880
+ continue;
3881
+ }
3882
+ const latest = transitions.find(({ payload }) => payload.sessionId === session.id);
3883
+ const modify = (doc) => {
3884
+ let next = doc;
3885
+ for (const answer of answers) {
3886
+ if (!isKnownSessionStatus(next.status) || next.status === "closed" || next.status === "superseded" || next.status === "abandoned") break;
3887
+ if (answer.payload.sessionId !== doc.id) continue;
3888
+ if (answer.syncStatus === "rejected") {
3889
+ if (answer.error?.code === "register_session_superseded" && (next.status === "open" || next.status === "counting")) next = { ...next, status: "superseded" };
3890
+ else if (answer.type === "register.session.open" && answer.error?.code === "register_session_already_open" && (next.status === "open" || next.status === "counting")) next = { ...next, status: "conflict" };
3891
+ } else if (answer.type === "register.session.open") {
3892
+ const resumed = answer.result?.resumed?.fromSessionId;
3893
+ if (typeof resumed === "string" && next.server_session_id == null) next = { ...next, server_session_id: resumed };
3894
+ if (next.status === "conflict") next = { ...next, status: latest?.payload.status === "counting" ? "counting" : "open" };
3895
+ }
3896
+ }
3897
+ return next;
3898
+ };
3899
+ if (modify(session) === session) continue;
3900
+ const row = await sessions.findOne(session.id).exec();
3901
+ if (row) await row.incrementalModify(modify);
3902
+ }
3903
+ }).catch((error) => {
3904
+ registerCommandsLogger.error("Register result adoption failed", { context: { registerId, error: String(error) } });
3905
+ });
3906
+ chains2.set(commands, run.then(() => void 0, () => void 0));
3907
+ return run;
3908
+ }
3909
+ async function takeOverSession({ commands, sessions, sessionId, now, registerContract }) {
3910
+ if ((registerContract ?? 0) < 2) throw new RegisterTakeOverError("REGISTER_TAKEOVER_UNSUPPORTED");
3911
+ const run = (chains2.get(commands) ?? Promise.resolve()).then(async () => {
3912
+ const [session] = await readFresh(sessions, { selector: { id: sessionId } });
3913
+ const row = await commands.findOne(`session.open:${sessionId}`).exec();
3914
+ if (session?.status !== "conflict" || !row) throw new RegisterSessionRequiredError();
3915
+ await row.incrementalModify((doc) => {
3916
+ if (doc.syncStatus !== "rejected" || doc.error?.code !== "register_session_already_open" || typeof doc.error.data?.sessionId !== "string") throw new RegisterSessionRequiredError();
3917
+ const { error, ...rest } = doc;
3918
+ return {
3919
+ ...rest,
3920
+ commandId: uuidv7(),
3921
+ version: 2,
3922
+ syncStatus: "pending",
3923
+ payload: { ...doc.payload, supersedes: error.data.sessionId },
3924
+ updatedAt: now ?? (/* @__PURE__ */ new Date()).toISOString()
3925
+ };
3926
+ });
3927
+ });
3928
+ chains2.set(commands, run.then(() => void 0, () => void 0));
3929
+ return run;
3930
+ }
3931
+ async function finishAbandon({ commands, sessions, sessionId, now }) {
3932
+ const rows = await readFresh(commands, { selector: { "payload.sessionId": sessionId, syncStatus: "pending" } });
3933
+ for (const row of rows) {
3934
+ await (await commands.findOne(row.key).exec(true)).incrementalModify((doc) => doc.syncStatus !== "pending" ? doc : {
3935
+ ...doc,
3936
+ syncStatus: "rejected",
3937
+ error: { code: "register_session_abandoned", message: "The cashier chose another register." },
3938
+ updatedAt: now ?? (/* @__PURE__ */ new Date()).toISOString()
3939
+ });
3940
+ }
3941
+ await (await sessions.findOne(sessionId).exec(true)).incrementalModify((doc) => doc.status === "conflict" ? { ...doc, status: "abandoned" } : doc);
3942
+ }
3943
+ async function abandonSession(input) {
3944
+ const { commands, sessions, sessionId } = input;
3945
+ const run = (chains2.get(commands) ?? Promise.resolve()).then(async () => {
3946
+ const [session] = await readFresh(sessions, { selector: { id: sessionId } });
3947
+ if (session?.status === "abandoned") return;
3948
+ if (session?.status !== "conflict") throw new RegisterSessionRequiredError();
3949
+ const [open] = await readFresh(commands, { selector: { key: `session.open:${sessionId}` } });
3950
+ if (open?.syncStatus === "pending") throw new RegisterTakeOverError("REGISTER_TAKEOVER_PENDING");
3951
+ await finishAbandon(input);
3952
+ });
3953
+ chains2.set(commands, run.then(() => void 0, () => void 0));
3954
+ return run;
3955
+ }
3956
+
3724
3957
  // src/register/use-register-session.ts
3725
3958
  var RegisterTenderInProgressError = class extends Error {
3726
3959
  constructor() {
@@ -3761,7 +3994,7 @@ var reservationOf = (register, storeKey, registerId) => register?.stores[storeKe
3761
3994
  var unwritten = (row, closureRows) => row.status === "closed" && row.closure_id === row.id && !closureRows.some((closure) => closure.id === row.id);
3762
3995
  function currentSession(rows, closureRows, reservation) {
3763
3996
  if (rows.some((row) => !isKnownSessionStatus(row.status))) return null;
3764
- return (reservation && !reservation.applied ? rows.find((row) => row.id === reservation.row.session_id) : void 0) ?? rows.find((row) => row.status !== "closed") ?? rows.find((row) => unwritten(row, closureRows)) ?? null;
3997
+ return (reservation && !reservation.applied ? rows.find((row) => row.id === reservation.row.session_id) : void 0) ?? rows.find((row) => row.status === "open" || row.status === "counting") ?? rows.find((row) => row.status === "conflict") ?? rows.find((row) => unwritten(row, closureRows)) ?? null;
3765
3998
  }
3766
3999
  function ledgerRows(orders) {
3767
4000
  return orders.flatMap((order) => order.payments.map((payment) => ({
@@ -3774,11 +4007,11 @@ function ledgerRows(orders) {
3774
4007
  }
3775
4008
  function useRegisterSession(options) {
3776
4009
  const { sessions, movements, closures, orders, register, storeKey, registerId, enabled, actor, timezone, tenderInProgress, expectedCloseTime, labels } = options;
3777
- const { commands } = options;
4010
+ const { commands, refunds } = options;
3778
4011
  const commandEnabled = (options.capabilities?.register ?? 0) >= 1;
3779
4012
  const commandTarget = useMemo3(
3780
- () => enabled && commandEnabled && commands && sessions && movements && closures && register && registerId ? { commands, sessions, movements, closures, host: register, storeKey, registerId } : null,
3781
- [enabled, commandEnabled, commands, sessions, movements, closures, register, storeKey, registerId]
4013
+ () => enabled && commandEnabled && commands && sessions && movements && closures && register && registerId ? { commands, sessions, movements, closures, host: register, storeKey, registerId, registerContract: options.capabilities?.register, deviceName: options.deviceName } : null,
4014
+ [enabled, commandEnabled, commands, sessions, movements, closures, register, storeKey, registerId, options.capabilities?.register, options.deviceName]
3782
4015
  );
3783
4016
  const commandTargetRef = useRef2(commandTarget);
3784
4017
  commandTargetRef.current = commandTarget;
@@ -3793,21 +4026,30 @@ function useRegisterSession(options) {
3793
4026
  });
3794
4027
  };
3795
4028
  useEffect3(reconcile, [commandTarget]);
4029
+ useEffect3(() => {
4030
+ if (!commandTarget) return;
4031
+ const subscription = commandTarget.commands.find({ selector: { registerId: commandTarget.registerId } }).$.subscribe(() => {
4032
+ void adoptRegisterResults(commandTarget);
4033
+ });
4034
+ return () => subscription.unsubscribe();
4035
+ }, [commandTarget]);
3796
4036
  const source = useMemo3(() => {
3797
- if (!enabled || !sessions || !movements || !closures || !orders || !registerId || !register) return null;
4037
+ if (!enabled || !sessions || !movements || !closures || !orders || !registerId || !register || refunds === null) return null;
3798
4038
  return combineLatest([
3799
4039
  watchFresh(sessions, { selector: { register_id: registerId } }),
3800
4040
  watchFresh(closures, { selector: { register_id: registerId } }),
3801
4041
  observeRegister$(register).pipe(map5((document) => reservationOf(document, storeKey, registerId)))
3802
4042
  ]).pipe(switchMap(([rows, closureRows, reservation]) => {
3803
4043
  const current2 = currentSession(rows, closureRows, reservation);
3804
- if (!current2) return of({ rows, closureRows, reservation, entries: [], sales: [] });
4044
+ if (!current2) return of({ rows, closureRows, reservation, entries: [], sales: [], refundRows: [] });
3805
4045
  return combineLatest([
3806
4046
  watchFresh(movements, { selector: { session_id: current2.id } }),
3807
- watchFresh(orders, { selector: { sessionId: current2.id } })
3808
- ]).pipe(map5(([entries2, sales2]) => ({ rows, closureRows, reservation, entries: entries2, sales: sales2 })));
4047
+ watchFresh(orders, { selector: { sessionId: current2.id } }),
4048
+ commandTarget ? watchFresh(commandTarget.commands, { selector: { key: `session.open:${current2.id}` } }) : of([]),
4049
+ refunds ? watchFresh(refunds, { selector: { sessionId: current2.id } }) : of([])
4050
+ ]).pipe(map5(([entries2, sales2, opens, refundRows]) => ({ rows, closureRows, reservation, entries: entries2, sales: sales2, open: opens[0], refundRows })));
3809
4051
  }));
3810
- }, [enabled, sessions, movements, closures, orders, register, storeKey, registerId]);
4052
+ }, [enabled, sessions, movements, closures, orders, register, storeKey, registerId, commandTarget, refunds]);
3811
4053
  const tender = useRef2(tenderInProgress);
3812
4054
  tender.current = tenderInProgress;
3813
4055
  const closing = useSyncExternalStore(subscribeClosing, () => closingByRegister.has(registerId ?? ""));
@@ -3862,9 +4104,11 @@ function useRegisterSession(options) {
3862
4104
  }
3863
4105
  }, [data, source]);
3864
4106
  const session = data ? currentSession(data.rows, data.closureRows, data.reservation) : null;
4107
+ const refusal = data?.open?.error?.data;
4108
+ const conflict = session?.status !== "conflict" || !commandTarget ? void 0 : data?.open?.syncStatus === "pending" && typeof data.open.payload.supersedes === "string" ? { sessionId: data.open.payload.supersedes, takingOver: true } : data?.open?.syncStatus === "rejected" && refusal ? { sessionId: refusal.sessionId, openedAt: refusal.openedAt, openedBy: refusal.openedBy, deviceName: refusal.deviceName, takingOver: false } : void 0;
3865
4109
  const entries = data?.entries ?? [];
3866
4110
  const sales = data?.sales ?? [];
3867
- const expected = session ? deriveExpected({ session: { id: session.id, countedFloatMinor: session.counted_float_minor }, movements: entries, ledgerRowsBySession: ledgerRows(sales) }) : {};
4111
+ const expected = session ? deriveExpected({ session: { id: session.id, countedFloatMinor: session.counted_float_minor }, movements: entries, ledgerRowsBySession: ledgerRows(sales), refunds: data?.refundRows ?? [] }) : {};
3868
4112
  const now = /* @__PURE__ */ new Date();
3869
4113
  const [hour, minute] = String(expectedCloseTime ?? "").split(":").map(Number);
3870
4114
  const overdue = session?.status === "open" && !!expectedCloseTime && now.getTime() > new Date(now.getFullYear(), now.getMonth(), now.getDate(), hour, minute).getTime();
@@ -3874,8 +4118,8 @@ function useRegisterSession(options) {
3874
4118
  return id2 === null ? null : { id: id2, sessions };
3875
4119
  };
3876
4120
  const live = () => {
3877
- if (!enabled || !sessions || !movements || !closures || !orders || !registerId || !register) throw new RegisterSessionRequiredError();
3878
- return { sessions, movements, closures, orders, registerId, register };
4121
+ if (!enabled || !sessions || !movements || !closures || !orders || !registerId || !register || refunds === null) throw new RegisterSessionRequiredError();
4122
+ return { sessions, movements, closures, orders, registerId, register, refunds };
3879
4123
  };
3880
4124
  const current = () => {
3881
4125
  if (!session) throw new RegisterSessionRequiredError();
@@ -3887,6 +4131,8 @@ function useRegisterSession(options) {
3887
4131
  const nameOf = (id2) => !id2 ? "" : id2 === actor.id ? actor.name : labels?.resolveCashierName?.(id2) ?? "";
3888
4132
  return {
3889
4133
  session,
4134
+ /** The other till's live session, or the session this till is waiting to take over. */
4135
+ conflict,
3890
4136
  /** Any unknown session status in the current snapshot means the register needs upgrade. */
3891
4137
  needsUpgrade: data?.rows.some((row) => !isKnownSessionStatus(row.status)) ?? false,
3892
4138
  movements: entries,
@@ -3901,17 +4147,38 @@ function useRegisterSession(options) {
3901
4147
  enabled,
3902
4148
  blind: options.blind ?? false,
3903
4149
  /** Pass straight to useSale's `session` option: `complete()` then stamps through `stampSession`. */
3904
- saleSession: session && sessions && session.status !== "closed" ? { id: session.id, sessions } : void 0,
4150
+ saleSession: session && sessions && (session.status === "open" || session.status === "counting") ? { id: session.id, sessions } : void 0,
3905
4151
  /** The open session's id, `null` when sessions are off, else `RegisterSessionRequiredError`. Call it when tender starts and before a card terminal captures. */
3906
4152
  requireOpen,
3907
4153
  /** `requireOpen()`, returning `{ id, sessions }`: pass it to useSale's `startTender`, which pins it (the rendered `saleSession` can lag). */
3908
4154
  requireSaleSession,
3909
4155
  actions: {
4156
+ takeOver: async () => {
4157
+ if (!commandTarget) throw new RegisterSessionRequiredError();
4158
+ const row = currentSession(
4159
+ await readFresh(commandTarget.sessions, { selector: { register_id: commandTarget.registerId } }),
4160
+ await readFresh(commandTarget.closures, { selector: { register_id: commandTarget.registerId } }),
4161
+ reservationOf(await readRegister(commandTarget.host), commandTarget.storeKey, commandTarget.registerId)
4162
+ );
4163
+ if (row?.status !== "conflict") throw new RegisterSessionRequiredError();
4164
+ await takeOverSession({ ...commandTarget, sessionId: row.id, registerContract: options.capabilities?.register });
4165
+ },
4166
+ chooseAnotherRegister: async () => {
4167
+ if (!commandTarget) throw new RegisterSessionRequiredError();
4168
+ const row = currentSession(
4169
+ await readFresh(commandTarget.sessions, { selector: { register_id: commandTarget.registerId } }),
4170
+ await readFresh(commandTarget.closures, { selector: { register_id: commandTarget.registerId } }),
4171
+ reservationOf(await readRegister(commandTarget.host), commandTarget.storeKey, commandTarget.registerId)
4172
+ );
4173
+ if (row?.status !== "conflict") throw new RegisterSessionRequiredError();
4174
+ await abandonSession({ ...commandTarget, sessionId: row.id });
4175
+ },
3910
4176
  /**
3911
4177
  * Refuses with `RegisterSessionAlreadyOpenError` while the register has a live session or
3912
4178
  * another open is running (in any hook instance), and with `RegisterCloseIncompleteError`
3913
4179
  * while an earlier close hasn't finished: a new session would lock the till behind it.
3914
4180
  * An unknown stored status refuses with `RegisterNeedsUpgradeError`.
4181
+ * A conflict session refuses with `RegisterSessionConflictError`; superseded and abandoned sessions do not block.
3915
4182
  */
3916
4183
  openSession: async (input) => {
3917
4184
  const key = registerId ?? "";
@@ -3920,7 +4187,8 @@ function useRegisterSession(options) {
3920
4187
  const { sessions: sessions2, closures: closures2, registerId: registerId2, register: register2 } = live();
3921
4188
  const rows = await readFresh(sessions2, { selector: { register_id: registerId2 } });
3922
4189
  if (rows.some((row2) => !isKnownSessionStatus(row2.status))) throw new RegisterNeedsUpgradeError();
3923
- if (rows.some((row2) => row2.status !== "closed")) throw new RegisterSessionAlreadyOpenError();
4190
+ if (rows.some((row2) => row2.status === "conflict")) throw new RegisterSessionConflictError();
4191
+ if (rows.some((row2) => row2.status === "open" || row2.status === "counting")) throw new RegisterSessionAlreadyOpenError();
3924
4192
  const closureRows = await readFresh(closures2, { selector: { register_id: registerId2 } });
3925
4193
  const reservation = reservationOf(await readRegister(register2), storeKey, registerId2);
3926
4194
  if (reservation && !reservation.applied || rows.some((row2) => unwritten(row2, closureRows))) {
@@ -3988,7 +4256,7 @@ function useRegisterSession(options) {
3988
4256
  const key = registerId ?? "";
3989
4257
  if (closingByRegister.has(key)) return closingByRegister.get(key);
3990
4258
  const run = (async () => {
3991
- const { sessions: sessions2, movements: movements2, closures: closures2, orders: orders2, register: register2 } = live();
4259
+ const { sessions: sessions2, movements: movements2, closures: closures2, orders: orders2, register: register2, refunds: refunds2 } = live();
3992
4260
  const open = current();
3993
4261
  const stored = await readSession(sessions2, open.id);
3994
4262
  if (!stored) throw new RegisterSessionRequiredError();
@@ -3996,7 +4264,8 @@ function useRegisterSession(options) {
3996
4264
  const fresh = deriveExpected({
3997
4265
  session: { id: stored.id, countedFloatMinor: stored.counted_float_minor },
3998
4266
  movements: await readFresh(movements2, { selector: { session_id: stored.id } }),
3999
- ledgerRowsBySession: ledgerRows(await readFresh(orders2, { selector: { sessionId: stored.id } }))
4267
+ ledgerRowsBySession: ledgerRows(await readFresh(orders2, { selector: { sessionId: stored.id } })),
4268
+ refunds: refunds2 ? await readFresh(refunds2, { selector: { sessionId: stored.id } }) : void 0
4000
4269
  });
4001
4270
  if (closeNeedsApproval(input.counted.cash ?? 0, fresh.cash ?? 0, options.varianceThreshold)) throw new RegisterApprovalRequiredError();
4002
4271
  }
@@ -4014,6 +4283,7 @@ function useRegisterSession(options) {
4014
4283
  softwareVersion: options.softwareVersion,
4015
4284
  movements: await readFresh(movements2, { selector: { session_id: closed.id } }),
4016
4285
  orders: await readFresh(orders2, { selector: { sessionId: closed.id } }),
4286
+ refunds: refunds2 ? await readFresh(refunds2, { selector: { sessionId: closed.id } }) : void 0,
4017
4287
  resolveCashierName: labels?.resolveCashierName,
4018
4288
  labels: {
4019
4289
  register_name: labels?.registerName ?? "",
@@ -5203,7 +5473,7 @@ function plainCommand({
5203
5473
  });
5204
5474
  }
5205
5475
  function createRegisterOutbox(options) {
5206
- const { collection, transport, deviceId } = options;
5476
+ const { collection, sessions, transport, deviceId } = options;
5207
5477
  const size = options.batchSize;
5208
5478
  const batchSize = Math.max(1, Math.min(typeof size === "number" && Number.isFinite(size) ? Math.floor(size) : 10, 10));
5209
5479
  const initialBackoff = options.initialBackoffMs ?? 1e3;
@@ -5223,6 +5493,7 @@ function createRegisterOutbox(options) {
5223
5493
  let running;
5224
5494
  let timer;
5225
5495
  let subscription;
5496
+ let sessionsSubscription;
5226
5497
  let stopped = false;
5227
5498
  let insertedDuringRun = false;
5228
5499
  let stoppedDuringRun = false;
@@ -5244,7 +5515,11 @@ function createRegisterOutbox(options) {
5244
5515
  const at2 = now();
5245
5516
  for (const { commandId } of commands) clocks.set(commandId, { ...clocks.get(commandId) ?? { since: at2, firstFailedAt: at2 }, seq: ++failureSeq, reason });
5246
5517
  }
5518
+ function isNonBlocking(command, abandoned) {
5519
+ return NON_BLOCKING_REJECTIONS.has(command.error?.code ?? "") || command.error?.code === "register_session_already_open" && typeof command.payload.sessionId === "string" && abandoned.has(command.payload.sessionId);
5520
+ }
5247
5521
  async function updateState(patch = {}) {
5522
+ const abandoned = new Set(sessions && clocks.size ? (await readFresh(sessions, { selector: { status: "abandoned" } })).map((session) => session.id) : []);
5248
5523
  const pending = await countFresh(collection, { syncStatus: "pending" });
5249
5524
  const sendable2 = /* @__PURE__ */ new Set();
5250
5525
  const blocked = /* @__PURE__ */ new Set();
@@ -5253,7 +5528,7 @@ function createRegisterOutbox(options) {
5253
5528
  sort: [{ seq: "asc" }, { key: "asc" }]
5254
5529
  })) {
5255
5530
  if (command.syncStatus === "rejected") {
5256
- if (!NON_BLOCKING_REJECTIONS.has(command.error?.code ?? "")) blocked.add(command.registerId);
5531
+ if (!isNonBlocking(command, abandoned)) blocked.add(command.registerId);
5257
5532
  } else if (!blocked.has(command.registerId)) sendable2.add(command.commandId);
5258
5533
  }
5259
5534
  for (const id2 of clocks.keys()) if (!sendable2.has(id2)) clocks.delete(id2);
@@ -5282,6 +5557,7 @@ function createRegisterOutbox(options) {
5282
5557
  if (options.isEnabled?.() === false) return;
5283
5558
  const pending = await readFresh(collection, { selector: { syncStatus: "pending" } });
5284
5559
  const registerIds = [...new Set(pending.map((command) => command.registerId))].sort();
5560
+ const abandoned = new Set(sessions ? (await readFresh(sessions, { selector: { status: "abandoned" } })).map((session) => session.id) : []);
5285
5561
  let sent = false;
5286
5562
  for (const registerId of registerIds) {
5287
5563
  const ledger = await readFresh(collection, {
@@ -5290,7 +5566,7 @@ function createRegisterOutbox(options) {
5290
5566
  });
5291
5567
  const commands = [];
5292
5568
  for (const command of ledger) {
5293
- if (command.syncStatus === "rejected" && NON_BLOCKING_REJECTIONS.has(command.error?.code ?? "")) continue;
5569
+ if (command.syncStatus === "rejected" && isNonBlocking(command, abandoned)) continue;
5294
5570
  if (command.syncStatus === "rejected") break;
5295
5571
  commands.push(command);
5296
5572
  if (command.type === "register.session.open" || commands.length >= batchSize) break;
@@ -5426,6 +5702,13 @@ function createRegisterOutbox(options) {
5426
5702
  });
5427
5703
  }
5428
5704
  });
5705
+ if (sessions && !sessionsSubscription) sessionsSubscription = sessions.$.subscribe((event) => {
5706
+ if (event.documentData?.status === "abandoned") {
5707
+ insertedDuringRun = true;
5708
+ flush().catch(() => {
5709
+ });
5710
+ }
5711
+ });
5429
5712
  if (!stopped) flush().catch(() => {
5430
5713
  });
5431
5714
  },
@@ -5435,6 +5718,8 @@ function createRegisterOutbox(options) {
5435
5718
  timer = void 0;
5436
5719
  subscription?.unsubscribe();
5437
5720
  subscription = void 0;
5721
+ sessionsSubscription?.unsubscribe();
5722
+ sessionsSubscription = void 0;
5438
5723
  missingSubscription?.unsubscribe();
5439
5724
  missingSubscription = void 0;
5440
5725
  state$.next({ ...state$.value, nextAttemptAt: void 0 });
@@ -5446,7 +5731,7 @@ function createRegisterOutbox(options) {
5446
5731
  import { useEffect as useEffect5, useRef as useRef4, useState as useState5 } from "react";
5447
5732
  var idle = { pending: 0, sending: false };
5448
5733
  function useRegisterOutbox(options) {
5449
- const { commands, deviceId } = options;
5734
+ const { commands, sessions, deviceId } = options;
5450
5735
  const latest = useRef4(options);
5451
5736
  latest.current = options;
5452
5737
  const current = useRef4(null);
@@ -5458,6 +5743,7 @@ function useRegisterOutbox(options) {
5458
5743
  collection: commands,
5459
5744
  deviceId,
5460
5745
  transport: latest.current.transport(),
5746
+ sessions: sessions ?? void 0,
5461
5747
  isEnabled: latest.current.isEnabled ? () => latest.current.isEnabled?.() ?? true : void 0,
5462
5748
  onResult: latest.current.onResult ? async (command, result) => {
5463
5749
  await latest.current.onResult?.(command, result);
@@ -5473,7 +5759,7 @@ function useRegisterOutbox(options) {
5473
5759
  current.current = null;
5474
5760
  setState(idle);
5475
5761
  };
5476
- }, [commands, deviceId]);
5762
+ }, [commands, sessions, deviceId]);
5477
5763
  const opened = current.current;
5478
5764
  return {
5479
5765
  state: opened?.commands === commands && opened.deviceId === deviceId ? state : idle,
@@ -5606,6 +5892,148 @@ function useOrderOutbox(options) {
5606
5892
  };
5607
5893
  }
5608
5894
 
5895
+ // src/refund/send-order-refund.ts
5896
+ import { parseCommandResult } from "@tallyui/core/server";
5897
+ async function sendOrderRefund(transport, envelope2) {
5898
+ let outcome;
5899
+ try {
5900
+ outcome = await transport.send([envelope2]);
5901
+ } catch {
5902
+ return { kind: "unknown", reason: "transport_error" };
5903
+ }
5904
+ if (outcome.kind === "retry") {
5905
+ return {
5906
+ kind: "unknown",
5907
+ reason: outcome.reason,
5908
+ ...outcome.retryAfterMs !== void 0 ? { retryAfterMs: outcome.retryAfterMs } : {}
5909
+ };
5910
+ }
5911
+ if (outcome.kind === "refused" || outcome.kind === "unauthorized") return outcome;
5912
+ const matches = outcome.results.filter((result2) => result2.id === envelope2.id);
5913
+ if (matches.length !== 1) return { kind: "unknown", reason: "missing_result" };
5914
+ let result;
5915
+ try {
5916
+ result = parseCommandResult(matches[0]);
5917
+ } catch {
5918
+ return { kind: "unknown", reason: "bad_result" };
5919
+ }
5920
+ if (result.status === "applied" || result.status === "duplicate") {
5921
+ if (!result.refund) return { kind: "unknown", reason: "bad_result" };
5922
+ return { kind: "applied", refund: result.refund, duplicate: result.status === "duplicate" };
5923
+ }
5924
+ return { kind: "rejected", error: result.error };
5925
+ }
5926
+
5927
+ // src/refund/pos-refund.ts
5928
+ var posRefundSchema = {
5929
+ title: "POS refunds",
5930
+ version: 0,
5931
+ primaryKey: "id",
5932
+ type: "object",
5933
+ additionalProperties: false,
5934
+ properties: {
5935
+ id: { type: "string", maxLength: 64 },
5936
+ commandId: { type: "string", maxLength: 64 },
5937
+ orderId: { type: "string", maxLength: 64 },
5938
+ sessionId: { type: "string", maxLength: 64 },
5939
+ registerId: { type: "string", maxLength: 64 },
5940
+ envelope: { type: "object", additionalProperties: true },
5941
+ status: { type: "string", enum: ["pending", "applied", "rejected", "unsent"], maxLength: 10 },
5942
+ result: { type: "object", additionalProperties: true },
5943
+ duplicate: { type: "boolean" },
5944
+ error: { type: "object", properties: {
5945
+ code: { type: "string" },
5946
+ message: { type: "string" },
5947
+ data: { type: "object", additionalProperties: true }
5948
+ }, required: ["code", "message"], additionalProperties: false },
5949
+ createdAt: { type: "string" },
5950
+ updatedAt: { type: "string" }
5951
+ },
5952
+ required: ["id", "commandId", "orderId", "sessionId", "registerId", "envelope", "status", "createdAt", "updatedAt"],
5953
+ indexes: [["sessionId"]]
5954
+ };
5955
+ var posRefundCollection = () => ({ schema: posRefundSchema });
5956
+
5957
+ // src/refund/submit-order-refund.ts
5958
+ import { RxError } from "rxdb";
5959
+ var RefundAnswerPendingError = class extends Error {
5960
+ constructor(record) {
5961
+ super("This refund's answer is still unknown. Send it again before trying a new refund attempt.");
5962
+ this.record = record;
5963
+ this.name = "RefundAnswerPendingError";
5964
+ }
5965
+ code = "REFUND_ANSWER_PENDING";
5966
+ };
5967
+ async function submitOrderRefund({ refunds, transport, envelope: envelope2, now = () => (/* @__PURE__ */ new Date()).toISOString() }) {
5968
+ const id2 = envelope2.payload.clientRefundId;
5969
+ let doc = await refunds.findOne(id2).exec();
5970
+ if (!doc) {
5971
+ const at2 = now();
5972
+ const { orderId, sessionId, registerId } = envelope2.payload;
5973
+ try {
5974
+ doc = await refunds.insert({
5975
+ id: id2,
5976
+ commandId: envelope2.id,
5977
+ orderId,
5978
+ sessionId,
5979
+ registerId,
5980
+ envelope: envelope2,
5981
+ status: "pending",
5982
+ createdAt: at2,
5983
+ updatedAt: at2
5984
+ });
5985
+ } catch (error) {
5986
+ if (!(error instanceof RxError) || error.code !== "CONFLICT") throw error;
5987
+ doc = await refunds.findOne(id2).exec();
5988
+ }
5989
+ }
5990
+ let record = doc.toJSON();
5991
+ for (let pass = 0; ; pass++) {
5992
+ if (pass === 2) throw new RefundAnswerPendingError(record);
5993
+ if (record.status === "applied") {
5994
+ return { outcome: { kind: "applied", refund: record.result, duplicate: true }, record };
5995
+ }
5996
+ if (record.status === "pending") {
5997
+ if (record.commandId !== envelope2.id) throw new RefundAnswerPendingError(record);
5998
+ break;
5999
+ }
6000
+ if (record.status === "rejected" && record.commandId === envelope2.id) {
6001
+ return { outcome: { kind: "rejected", error: record.error }, record };
6002
+ }
6003
+ if (record.status === "unsent" && record.commandId === envelope2.id) {
6004
+ doc = await doc.incrementalModify((stored) => {
6005
+ if (stored.status !== "unsent" || stored.commandId !== envelope2.id) return stored;
6006
+ const { error, ...rest } = stored;
6007
+ return { ...rest, status: "pending", updatedAt: now() };
6008
+ });
6009
+ record = doc.toJSON();
6010
+ continue;
6011
+ }
6012
+ doc = await doc.incrementalModify((stored) => {
6013
+ if (stored.status !== "rejected" && stored.status !== "unsent") return stored;
6014
+ const { result, duplicate, error, ...rest } = stored;
6015
+ return { ...rest, commandId: envelope2.id, envelope: envelope2, status: "pending", updatedAt: now() };
6016
+ });
6017
+ record = doc.toJSON();
6018
+ if (record.commandId === envelope2.id) break;
6019
+ }
6020
+ const outcome = await sendOrderRefund(transport, envelope2);
6021
+ if (outcome.kind !== "unknown") {
6022
+ doc = await doc.incrementalModify((stored) => {
6023
+ if (stored.status !== "pending" || stored.commandId !== envelope2.id) return stored;
6024
+ const updatedAt = now();
6025
+ if (outcome.kind === "applied") {
6026
+ const { error: error2, ...rest } = stored;
6027
+ return { ...rest, status: "applied", result: outcome.refund, duplicate: outcome.duplicate, updatedAt };
6028
+ }
6029
+ if (outcome.kind === "rejected") return { ...stored, status: "rejected", error: outcome.error, updatedAt };
6030
+ const error = outcome.kind === "refused" ? { code: "refused", message: outcome.reason, data: { status: outcome.status } } : { code: "unauthorized", message: "unauthorized" };
6031
+ return { ...stored, status: "unsent", error, updatedAt };
6032
+ });
6033
+ }
6034
+ return { outcome, record: doc.getLatest().toJSON() };
6035
+ }
6036
+
5609
6037
  // src/tender/tender-state.ts
5610
6038
  var MAX_TENDER_MINOR = 999999999;
5611
6039
  var initialTenderState = {
@@ -5851,6 +6279,7 @@ export {
5851
6279
  PosOrderOpenClosedError,
5852
6280
  RECEIPT_SCHEMA_VERSION,
5853
6281
  REGISTER_SESSION_MIGRATION_CLOSE_WAIT_MS,
6282
+ RefundAnswerPendingError,
5854
6283
  RegisterApprovalRequiredError,
5855
6284
  RegisterCloseIncompleteError,
5856
6285
  RegisterIdInvalidError,
@@ -5860,17 +6289,21 @@ export {
5860
6289
  RegisterNeedsUpgradeError,
5861
6290
  RegisterSessionAlreadyOpenError,
5862
6291
  RegisterSessionClosedError,
6292
+ RegisterSessionConflictError,
5863
6293
  RegisterSessionOpenClosedError,
5864
6294
  RegisterSessionRequiredError,
6295
+ RegisterTakeOverError,
5865
6296
  RegisterTenderInProgressError,
5866
6297
  SALE_SAVING,
5867
6298
  TAX_ROUNDING_MIXED_NOTE,
5868
6299
  TaxProvider,
5869
6300
  UnsupportedOrderVersionError,
6301
+ abandonSession,
5870
6302
  activePlan,
5871
6303
  addEntryToCart,
5872
6304
  addPosOrderCollection,
5873
6305
  addRegisterSessionCollection,
6306
+ adoptRegisterResults,
5874
6307
  advancePerpetual,
5875
6308
  allocateOrderDiscount,
5876
6309
  appliedMinor,
@@ -5942,6 +6375,8 @@ export {
5942
6375
  posOrderCollection,
5943
6376
  posOrderSchema,
5944
6377
  posOrdersLogger,
6378
+ posRefundCollection,
6379
+ posRefundSchema,
5945
6380
  productCategories,
5946
6381
  productSortValue,
5947
6382
  quickTenderedAmounts,
@@ -5968,6 +6403,7 @@ export {
5968
6403
  sameSale,
5969
6404
  searchProducts,
5970
6405
  selectClosureRows,
6406
+ sendOrderRefund,
5971
6407
  sessionOpenCommand,
5972
6408
  sessionTransitionCommand,
5973
6409
  sortProducts,
@@ -5975,6 +6411,8 @@ export {
5975
6411
  startCounting,
5976
6412
  stockOverlay$,
5977
6413
  stockOverlayAsOf$,
6414
+ submitOrderRefund,
6415
+ takeOverSession,
5978
6416
  taxFiguresForBasket,
5979
6417
  taxLinesByRate,
5980
6418
  taxLogger,