@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.d.ts +112 -17
- package/dist/index.js +303 -35
- package/package.json +8 -7
- package/src/index.ts +3 -1
- package/src/order/index.ts +2 -1
- package/src/order/order-builder.ts +41 -9
- package/src/order/order-drafts.ts +3 -1
- package/src/order/types.ts +11 -1
- package/src/order/woocommerce-tax.ts +61 -4
- package/src/outbox/types.test-d.ts +5 -0
- package/src/refund/index.ts +3 -0
- package/src/refund/pos-refund.ts +35 -0
- package/src/refund/send-order-refund.ts +43 -0
- package/src/refund/submit-order-refund.ts +85 -0
- package/src/register/expected.ts +23 -6
- package/src/register/register-adoption.ts +7 -4
- package/src/register/schemas.ts +1 -1
- package/src/register/session-store.ts +21 -5
- package/src/register/use-register-session.ts +18 -13
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((
|
|
128
|
-
const rate = rates.find((rate2) => rate2.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(
|
|
134
|
+
taxMicros: String(Math.round(tax.total * factor * 1e6))
|
|
135
135
|
};
|
|
136
136
|
});
|
|
137
|
-
|
|
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
|
|
678
|
-
const base =
|
|
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(
|
|
686
|
-
const
|
|
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 =
|
|
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 =
|
|
811
|
+
const converted = displaySourceLines.flatMap((li, index) => li.taxInclusive === taxInclusive ? [] : [index]);
|
|
733
812
|
const conversions = converted.reduce((count, index) => count + [
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
...
|
|
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(
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
42
|
-
"@tallyui/connector-woocommerce": "3.
|
|
43
|
-
"@tallyui/database": "3.
|
|
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",
|