@tallyui/pos 3.8.0 → 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
 
@@ -3050,6 +3156,7 @@ async function writeClosure({
3050
3156
  otherTenders,
3051
3157
  movements,
3052
3158
  orders,
3159
+ refunds,
3053
3160
  tillExpected,
3054
3161
  labels,
3055
3162
  resolveCashierName,
@@ -3067,6 +3174,7 @@ async function writeClosure({
3067
3174
  return existing;
3068
3175
  }
3069
3176
  const bound = orders.filter((order) => order.sessionId === session.id);
3177
+ const countedRefunds = (refunds ?? []).filter((refund) => refund.status === "applied" && refund.sessionId === session.id);
3070
3178
  const rows = bound.flatMap(
3071
3179
  (order) => order.payments.map((payment) => ({
3072
3180
  session_id: order.sessionId,
@@ -3080,7 +3188,8 @@ async function writeClosure({
3080
3188
  const till_expected = tillExpected ?? deriveExpected({
3081
3189
  session: { id: session.id, countedFloatMinor: session.counted_float_minor },
3082
3190
  movements: entries,
3083
- ledgerRowsBySession: rows
3191
+ ledgerRowsBySession: rows,
3192
+ refunds
3084
3193
  });
3085
3194
  const counts = { cash: counted, ...otherTenders };
3086
3195
  const sum = (values) => values.reduce((total, value) => total + value, 0);
@@ -3089,6 +3198,11 @@ async function writeClosure({
3089
3198
  const method = row2.kind === "cash" ? "cash" : row2.method_id;
3090
3199
  payment_methods[method] = { sales_minor: (payment_methods[method]?.sales_minor ?? 0) + row2.amountMinor, refunds_minor: 0 };
3091
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
+ }
3092
3206
  const taxRates = /* @__PURE__ */ new Map();
3093
3207
  const roundings = /* @__PURE__ */ new Set();
3094
3208
  for (const order of bound) {
@@ -3124,7 +3238,7 @@ async function writeClosure({
3124
3238
  Object.entries(counts).map(([method, value]) => [method, countVariance(value, till_expected[method] ?? 0)])
3125
3239
  ),
3126
3240
  period_sales_total_minor: sum(rows.map((row2) => row2.amountMinor)),
3127
- period_refunds_total_minor: 0,
3241
+ period_refunds_total_minor: sum(countedRefunds.flatMap((refund) => refundAmounts(refund).map(([, amount]) => amount))),
3128
3242
  perpetual_sales_total_minor: 0,
3129
3243
  perpetual_refunds_total_minor: 0,
3130
3244
  unsynced_count: unsynced.length,
@@ -3161,7 +3275,11 @@ async function writeClosure({
3161
3275
  voided_by: voided_by ?? null
3162
3276
  })),
3163
3277
  transaction_count: bound.length,
3164
- 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
+ } : {},
3165
3283
  cashiers: [...new Set(bound.map((order) => order.cashierRef ?? "").filter(Boolean))].map((id2) => ({
3166
3284
  id: id2,
3167
3285
  name: resolveCashierName?.(id2) || id2
@@ -3788,7 +3906,8 @@ async function adoptRegisterResults({ commands, sessions, registerId }) {
3788
3906
  chains2.set(commands, run.then(() => void 0, () => void 0));
3789
3907
  return run;
3790
3908
  }
3791
- async function takeOverSession({ commands, sessions, sessionId, now }) {
3909
+ async function takeOverSession({ commands, sessions, sessionId, now, registerContract }) {
3910
+ if ((registerContract ?? 0) < 2) throw new RegisterTakeOverError("REGISTER_TAKEOVER_UNSUPPORTED");
3792
3911
  const run = (chains2.get(commands) ?? Promise.resolve()).then(async () => {
3793
3912
  const [session] = await readFresh(sessions, { selector: { id: sessionId } });
3794
3913
  const row = await commands.findOne(`session.open:${sessionId}`).exec();
@@ -3888,7 +4007,7 @@ function ledgerRows(orders) {
3888
4007
  }
3889
4008
  function useRegisterSession(options) {
3890
4009
  const { sessions, movements, closures, orders, register, storeKey, registerId, enabled, actor, timezone, tenderInProgress, expectedCloseTime, labels } = options;
3891
- const { commands } = options;
4010
+ const { commands, refunds } = options;
3892
4011
  const commandEnabled = (options.capabilities?.register ?? 0) >= 1;
3893
4012
  const commandTarget = useMemo3(
3894
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,
@@ -3915,21 +4034,22 @@ function useRegisterSession(options) {
3915
4034
  return () => subscription.unsubscribe();
3916
4035
  }, [commandTarget]);
3917
4036
  const source = useMemo3(() => {
3918
- if (!enabled || !sessions || !movements || !closures || !orders || !registerId || !register) return null;
4037
+ if (!enabled || !sessions || !movements || !closures || !orders || !registerId || !register || refunds === null) return null;
3919
4038
  return combineLatest([
3920
4039
  watchFresh(sessions, { selector: { register_id: registerId } }),
3921
4040
  watchFresh(closures, { selector: { register_id: registerId } }),
3922
4041
  observeRegister$(register).pipe(map5((document) => reservationOf(document, storeKey, registerId)))
3923
4042
  ]).pipe(switchMap(([rows, closureRows, reservation]) => {
3924
4043
  const current2 = currentSession(rows, closureRows, reservation);
3925
- if (!current2) return of({ rows, closureRows, reservation, entries: [], sales: [] });
4044
+ if (!current2) return of({ rows, closureRows, reservation, entries: [], sales: [], refundRows: [] });
3926
4045
  return combineLatest([
3927
4046
  watchFresh(movements, { selector: { session_id: current2.id } }),
3928
4047
  watchFresh(orders, { selector: { sessionId: current2.id } }),
3929
- commandTarget ? watchFresh(commandTarget.commands, { selector: { key: `session.open:${current2.id}` } }) : of([])
3930
- ]).pipe(map5(([entries2, sales2, opens]) => ({ rows, closureRows, reservation, entries: entries2, sales: sales2, open: opens[0] })));
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 })));
3931
4051
  }));
3932
- }, [enabled, sessions, movements, closures, orders, register, storeKey, registerId, commandTarget]);
4052
+ }, [enabled, sessions, movements, closures, orders, register, storeKey, registerId, commandTarget, refunds]);
3933
4053
  const tender = useRef2(tenderInProgress);
3934
4054
  tender.current = tenderInProgress;
3935
4055
  const closing = useSyncExternalStore(subscribeClosing, () => closingByRegister.has(registerId ?? ""));
@@ -3988,7 +4108,7 @@ function useRegisterSession(options) {
3988
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;
3989
4109
  const entries = data?.entries ?? [];
3990
4110
  const sales = data?.sales ?? [];
3991
- 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 ?? [] }) : {};
3992
4112
  const now = /* @__PURE__ */ new Date();
3993
4113
  const [hour, minute] = String(expectedCloseTime ?? "").split(":").map(Number);
3994
4114
  const overdue = session?.status === "open" && !!expectedCloseTime && now.getTime() > new Date(now.getFullYear(), now.getMonth(), now.getDate(), hour, minute).getTime();
@@ -3998,8 +4118,8 @@ function useRegisterSession(options) {
3998
4118
  return id2 === null ? null : { id: id2, sessions };
3999
4119
  };
4000
4120
  const live = () => {
4001
- if (!enabled || !sessions || !movements || !closures || !orders || !registerId || !register) throw new RegisterSessionRequiredError();
4002
- 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 };
4003
4123
  };
4004
4124
  const current = () => {
4005
4125
  if (!session) throw new RegisterSessionRequiredError();
@@ -4041,8 +4161,7 @@ function useRegisterSession(options) {
4041
4161
  reservationOf(await readRegister(commandTarget.host), commandTarget.storeKey, commandTarget.registerId)
4042
4162
  );
4043
4163
  if (row?.status !== "conflict") throw new RegisterSessionRequiredError();
4044
- if ((options.capabilities?.register ?? 0) < 2) throw new RegisterTakeOverError("REGISTER_TAKEOVER_UNSUPPORTED");
4045
- await takeOverSession({ ...commandTarget, sessionId: row.id });
4164
+ await takeOverSession({ ...commandTarget, sessionId: row.id, registerContract: options.capabilities?.register });
4046
4165
  },
4047
4166
  chooseAnotherRegister: async () => {
4048
4167
  if (!commandTarget) throw new RegisterSessionRequiredError();
@@ -4137,7 +4256,7 @@ function useRegisterSession(options) {
4137
4256
  const key = registerId ?? "";
4138
4257
  if (closingByRegister.has(key)) return closingByRegister.get(key);
4139
4258
  const run = (async () => {
4140
- 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();
4141
4260
  const open = current();
4142
4261
  const stored = await readSession(sessions2, open.id);
4143
4262
  if (!stored) throw new RegisterSessionRequiredError();
@@ -4145,7 +4264,8 @@ function useRegisterSession(options) {
4145
4264
  const fresh = deriveExpected({
4146
4265
  session: { id: stored.id, countedFloatMinor: stored.counted_float_minor },
4147
4266
  movements: await readFresh(movements2, { selector: { session_id: stored.id } }),
4148
- 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
4149
4269
  });
4150
4270
  if (closeNeedsApproval(input.counted.cash ?? 0, fresh.cash ?? 0, options.varianceThreshold)) throw new RegisterApprovalRequiredError();
4151
4271
  }
@@ -4163,6 +4283,7 @@ function useRegisterSession(options) {
4163
4283
  softwareVersion: options.softwareVersion,
4164
4284
  movements: await readFresh(movements2, { selector: { session_id: closed.id } }),
4165
4285
  orders: await readFresh(orders2, { selector: { sessionId: closed.id } }),
4286
+ refunds: refunds2 ? await readFresh(refunds2, { selector: { sessionId: closed.id } }) : void 0,
4166
4287
  resolveCashierName: labels?.resolveCashierName,
4167
4288
  labels: {
4168
4289
  register_name: labels?.registerName ?? "",
@@ -5771,6 +5892,148 @@ function useOrderOutbox(options) {
5771
5892
  };
5772
5893
  }
5773
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
+
5774
6037
  // src/tender/tender-state.ts
5775
6038
  var MAX_TENDER_MINOR = 999999999;
5776
6039
  var initialTenderState = {
@@ -6016,6 +6279,7 @@ export {
6016
6279
  PosOrderOpenClosedError,
6017
6280
  RECEIPT_SCHEMA_VERSION,
6018
6281
  REGISTER_SESSION_MIGRATION_CLOSE_WAIT_MS,
6282
+ RefundAnswerPendingError,
6019
6283
  RegisterApprovalRequiredError,
6020
6284
  RegisterCloseIncompleteError,
6021
6285
  RegisterIdInvalidError,
@@ -6111,6 +6375,8 @@ export {
6111
6375
  posOrderCollection,
6112
6376
  posOrderSchema,
6113
6377
  posOrdersLogger,
6378
+ posRefundCollection,
6379
+ posRefundSchema,
6114
6380
  productCategories,
6115
6381
  productSortValue,
6116
6382
  quickTenderedAmounts,
@@ -6137,6 +6403,7 @@ export {
6137
6403
  sameSale,
6138
6404
  searchProducts,
6139
6405
  selectClosureRows,
6406
+ sendOrderRefund,
6140
6407
  sessionOpenCommand,
6141
6408
  sessionTransitionCommand,
6142
6409
  sortProducts,
@@ -6144,6 +6411,7 @@ export {
6144
6411
  startCounting,
6145
6412
  stockOverlay$,
6146
6413
  stockOverlayAsOf$,
6414
+ submitOrderRefund,
6147
6415
  takeOverSession,
6148
6416
  taxFiguresForBasket,
6149
6417
  taxLinesByRate,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tallyui/pos",
3
- "version": "3.8.0",
3
+ "version": "3.9.0",
4
4
  "type": "module",
5
5
  "description": "Shared POS business logic for Tally UI — tax, orders, payments, discounts, receipts",
6
6
  "main": "./dist/index.js",
@@ -10,7 +10,8 @@
10
10
  ".": {
11
11
  "types": "./dist/index.d.ts",
12
12
  "import": "./dist/index.js",
13
- "source": "./src/index.ts"
13
+ "source": "./src/index.ts",
14
+ "default": "./dist/index.js"
14
15
  }
15
16
  },
16
17
  "files": [
@@ -27,20 +28,20 @@
27
28
  "directory": "packages/pos"
28
29
  },
29
30
  "peerDependencies": {
30
- "@tallyui/core": "^3.8.0",
31
+ "@tallyui/core": "^3.9.0",
31
32
  "react": ">=18",
32
33
  "rxdb": "~17.5.0",
33
34
  "rxjs": ">=7"
34
35
  },
35
36
  "devDependencies": {
36
- "@tallyui/core": "3.8.0",
37
+ "@tallyui/core": "3.9.0",
37
38
  "@types/react": ">=18",
38
39
  "react": "^19.2.4",
39
40
  "rxdb": "17.5.0",
40
41
  "rxjs": "7.8.2",
41
- "@tallyui/connector-medusa": "3.8.0",
42
- "@tallyui/connector-woocommerce": "3.8.0",
43
- "@tallyui/database": "3.8.0"
42
+ "@tallyui/connector-medusa": "3.9.0",
43
+ "@tallyui/connector-woocommerce": "3.9.0",
44
+ "@tallyui/database": "3.9.0"
44
45
  },
45
46
  "scripts": {
46
47
  "build": "tsup",