@tallyui/pos 3.0.1 → 3.0.3
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 +31 -4
- package/dist/index.js +166 -69
- package/package.json +6 -6
- package/src/index.ts +1 -0
- package/src/order/index.ts +1 -0
- package/src/order/order-builder.ts +12 -0
- package/src/order/order-drafts.ts +125 -0
- package/src/order/order-manager.ts +6 -90
- package/src/sale/catalogue.ts +12 -4
- package/src/sale/use-sale.ts +50 -3
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import { ReactNode } from 'react';
|
|
3
|
-
import { Money, TaxRounding, TallyConnector, SyncContext, StoreSettingsChoice, StoreSettings, StoreSettingsChoices, ProductTraits, PaymentMethodKind, CommandServerRefs, CommandWarning, CommandError, ServerCapabilities, OrderCreateEnvelope, RegisterCommandType, RegisterCommandResult, VariantSummary, CommandResult, AnyCommandEnvelope, CommandEnvelope, OrderCreatePayload, RegisterCommandEnvelope } from '@tallyui/core';
|
|
3
|
+
import { Money, TaxRounding, TallyConnector, SyncContext, StoreSettingsChoice, StoreSettings, StoreSettingsChoices, ProductTraits, PaymentMethodKind, CommandServerRefs, CommandWarning, CommandError, ServerCapabilities, OrderCreateEnvelope, RegisterCommandType, RegisterCommandResult, VariantSummary, TraitContext, CommandResult, AnyCommandEnvelope, CommandEnvelope, OrderCreatePayload, RegisterCommandEnvelope } from '@tallyui/core';
|
|
4
4
|
export { getProductStock, withStockOverlay } from '@tallyui/core';
|
|
5
5
|
export { countFresh, readFresh, watchFresh } from '@tallyui/core/rxdb';
|
|
6
6
|
import * as rxdb from 'rxdb';
|
|
@@ -376,6 +376,8 @@ interface OrderBuilder {
|
|
|
376
376
|
}): string;
|
|
377
377
|
addLine(input: AddLineInput): string;
|
|
378
378
|
updateQuantity(lineId: string, quantity: number): void;
|
|
379
|
+
/** Sets a line's unit price (a till price edit), in minor units of the order's currency and the line's own tax mode; its discounts and tax are recomputed. */
|
|
380
|
+
setUnitPrice(lineId: string, amountMinor: number): void;
|
|
379
381
|
removeItem(lineId: string): void;
|
|
380
382
|
applyLineDiscount(lineId: string, discount: Discount): void;
|
|
381
383
|
applyOrderDiscount(discount: Discount): void;
|
|
@@ -424,6 +426,15 @@ declare function taxFiguresForBasket(currency: string, pricesIncludeTax: boolean
|
|
|
424
426
|
*/
|
|
425
427
|
declare function allocateOrderDiscount(lineAmountsMinor: readonly number[], totalMinor: number): number[];
|
|
426
428
|
|
|
429
|
+
/** Apps create their drafts collection with this schema. */
|
|
430
|
+
declare const orderDraftSchema: RxJsonSchema<{
|
|
431
|
+
id: string;
|
|
432
|
+
data: string;
|
|
433
|
+
customerName?: string;
|
|
434
|
+
itemCount?: number;
|
|
435
|
+
total?: number;
|
|
436
|
+
parkedAt?: string;
|
|
437
|
+
}>;
|
|
427
438
|
interface ParkedOrderSummary {
|
|
428
439
|
id: string;
|
|
429
440
|
customerName?: string;
|
|
@@ -432,6 +443,13 @@ interface ParkedOrderSummary {
|
|
|
432
443
|
parkedAt: string;
|
|
433
444
|
source: 'local' | 'server';
|
|
434
445
|
}
|
|
446
|
+
declare function writeOrderDraft(drafts: RxCollection, snapshot: Order): Promise<string>;
|
|
447
|
+
declare function restoreOrderDraft(saved: Order, options: {
|
|
448
|
+
currency: string;
|
|
449
|
+
taxContext: TaxContext;
|
|
450
|
+
}): OrderBuilder;
|
|
451
|
+
declare function parkedOrderSummaries$(drafts: RxCollection): Observable<ParkedOrderSummary[]>;
|
|
452
|
+
|
|
435
453
|
interface OrderManagerOptions {
|
|
436
454
|
currency: string;
|
|
437
455
|
taxContext: TaxContext;
|
|
@@ -1790,8 +1808,9 @@ type CatalogueEntry<Doc> = {
|
|
|
1790
1808
|
product: Doc;
|
|
1791
1809
|
variant: VariantSummary;
|
|
1792
1810
|
};
|
|
1793
|
-
/**
|
|
1794
|
-
|
|
1811
|
+
/** Variants in product/variant order, passing context through; without getVariants,
|
|
1812
|
+
* simple products use their own traits and products with variants contribute no entries. */
|
|
1813
|
+
declare function catalogueEntries<Doc>(products: Doc[], traits: ProductTraits<Doc>, context?: TraitContext): CatalogueEntry<Doc>[];
|
|
1795
1814
|
/** Barcode-then-SKU lookup across all products. */
|
|
1796
1815
|
declare function findEntryByCode<Doc>(entries: CatalogueEntry<Doc>[], code: string): CatalogueEntry<Doc> | undefined;
|
|
1797
1816
|
/** Display the resolved price in the requested currency. */
|
|
@@ -1818,6 +1837,8 @@ declare function useSale(settings: Pick<StoreSettings, 'currency'>, opts: {
|
|
|
1818
1837
|
registerId: string;
|
|
1819
1838
|
cashierRef: string;
|
|
1820
1839
|
capabilities?: ServerCapabilities;
|
|
1840
|
+
/** Where `park()` keeps parked carts; create it with `orderDraftSchema` */
|
|
1841
|
+
drafts?: RxCollection;
|
|
1821
1842
|
/**
|
|
1822
1843
|
* When set, `complete()` stamps the finalized order with this session before `onSaleCompleted`.
|
|
1823
1844
|
* `complete()` runs after the money is taken, so a refused stamp (the session closed or went
|
|
@@ -1850,6 +1871,8 @@ declare function useSale(settings: Pick<StoreSettings, 'currency'>, opts: {
|
|
|
1850
1871
|
canContinue: boolean;
|
|
1851
1872
|
add(entry: CatalogueEntry<any>, traits: ProductTraits<any>): void;
|
|
1852
1873
|
setQuantity(lineId: string, quantity: number): void;
|
|
1874
|
+
/** Sets a line's unit price in minor units of its own tax mode, recomputing discounts and tax; returns a refusal or null. */
|
|
1875
|
+
setUnitPrice(lineId: string, amountMinor: number): string | null;
|
|
1853
1876
|
remove(lineId: string): void;
|
|
1854
1877
|
/** A line's discount, or the order's without a line; returns the refusal to show, or null once applied. */
|
|
1855
1878
|
applyDiscount(lineId: string | null, discount: Discount): string | null;
|
|
@@ -1903,6 +1926,10 @@ declare function useSale(settings: Pick<StoreSettings, 'currency'>, opts: {
|
|
|
1903
1926
|
* updates or requeues `pos_orders` itself.
|
|
1904
1927
|
*/
|
|
1905
1928
|
newSale(): void;
|
|
1929
|
+
/** Parks the cart in `drafts`; resolves to a refusal message, or null once parked. */
|
|
1930
|
+
park(): Promise<string | null>;
|
|
1931
|
+
/** Resumes a draft into an empty cart; resolves to a refusal message, or null once resumed. */
|
|
1932
|
+
resume(draftId: string): Promise<string | null>;
|
|
1906
1933
|
/**
|
|
1907
1934
|
* Continue after a failed save whose order is confirmed stored (`canContinue`): exactly `newSale()`.
|
|
1908
1935
|
* The order stays pending in the outbox, which will send it; it isn't handed to `onSaleCompleted` again, and
|
|
@@ -2277,4 +2304,4 @@ declare function planLegs(plan: TenderPlan, rowsSinceFrom: {
|
|
|
2277
2304
|
};
|
|
2278
2305
|
};
|
|
2279
2306
|
|
|
2280
|
-
export { type Actor, type AddLineInput, type AppliedDiscount, type BackendNotFound, type BasketLine, CartError, type CashMovement, type CashMovementCollection, type CatalogueEntry, type Closure, type ClosureCollection, type ClosureContext, type ClosureScope, type CommandTransport, type Correction, CurrencyProvider, type CurrencyProviderProps, type CustomerSummary, DISCOUNTS_UNSUPPORTED, type Discount, type DisplayLine, type DisplayTotals, type FinalizeOptions, type HttpTransportOptions, type LedgerRow, type LineItem, type LineTaxLine, type LogEntry, type LogLevel, type LogSink, type Logger, MAX_TENDER_MINOR, MICROS_PER_MINOR, type Movement$1 as Movement, type MovementType, type Order, type OrderBuilder, type OrderBuilderOptions, OrderContentMismatchError, type OrderManager, type OrderManagerOptions, type OrderOutbox, type OrderOutboxOptions, type OrderTaxTotals, type OutboxState, type ParkedOrderSummary, type Payment, type PaymentMethod, type PaymentTransport, type PlanLeg, type PosOrder, type PosOrderLine, type PosOrderLocalWarning, PosOrderOpenClosedError, type PosOrderPayment, type PosOrderServerFailures, type PosOrderSyncStatus, type RateTaxLine, type ReceiptConfig, type ReceiptData, type ReceiptLineItem, type RecordedFigures, RegisterApprovalRequiredError, type RegisterBucket, RegisterCloseIncompleteError, type RegisterCommand, type RegisterCommandCollection, type RegisterCounters, type RegisterDocument, type RegisterFact, type RegisterHost, RegisterIdInvalidError, RegisterMovementAmountError, RegisterMovementReasonError, RegisterMovementStrandedError, type RegisterOutbox, type RegisterOutboxOptions, type RegisterSession, RegisterSessionAlreadyOpenError, RegisterSessionClosedError, type RegisterSessionCollection, RegisterSessionRequiredError, type RegisterStore, RegisterTenderInProgressError, type Repository, type ResolveStoreSettingsOptions, SALE_SAVING, type SaleStage, type SentOrder, type SplitTab, type StoreSettingsResolution, type StoreSettingsState, TAX_ROUNDING_MIXED_NOTE, type TaxContext, type TaxLineInput, TaxProvider, type TaxProviderProps, type TaxRateMap, type TenderAction, type TenderKey, type TenderLineId, type TenderPlan, type TenderState, type TenderView, type TransportOutcome, UnsupportedOrderVersionError, type UseOrderOutboxOptions, type UseOrderOutboxResult, type UseRegisterOutboxOptions, type UseRegisterOutboxResult, type UseRegisterSessionOptions, activePlan, addEntryToCart, addPosOrderCollection, advancePerpetual, allocateOrderDiscount, appliedMinor, backToSelling, bindRegister, buildClosureDocument, buildReceiptData, buildXReportDocument, callbackSink, cashMovementSchema, catalogueEntries, changeMinor, clampClosureScope, closeNeedsApproval, closeSession, closureCommand, closureSchema, computeOrderTax, consoleSink, countVariance, createBackendNotFound, createHttpCommandTransport, createLogger, createOrderBuilder, createOrderManager, createOrderOutbox, createRegisterOutbox, createRepository, denominationTotal, denominations, deriveExpected, deriveSettled, ensureRegister, evenSplitShareMinor, exportCsv, finalizeOrder, findEntryByCode, formatClosureDate, getBoundRegisterId, getDeviceId, initTenderState, initialTenderState, isServerDecimal, labelKeys, minorToDecimal, mintClosureNumber, mintUuid, movementCommand, movementFieldError, needsAttention, nextSaleCounter, normalizeAmount, observeRegister$, openSession, openSessionSelector, outboxLogger, overThreshold, parseMinor, planLegs, posOrderCollection, posOrderSchema, posOrdersLogger, quickTenderedAmounts, ratePpmFromPercent, readBoundRegister, readRegister, reconcileRegisterCommands, recordMovement, recordRegisterFact, registerCommandCollection, registerCommandSchema, registerCommandsLogger, registerFactsLogger, registerSessionCollection, registerSessionSchema, requireOpenSession, resolveStoreSettings, roundMicrosToMinor, saleLogger, sameSale, searchProducts, selectClosureRows, sessionOpenCommand, sessionTransitionCommand, stampSession, startCounting, stockOverlay$, stockOverlayAsOf$, taxFiguresForBasket, taxLinesByRate, taxLogger, taxMicros, taxProviderProps, tenderReducer, toOrderCreateEnvelope, unbindRegister, useCurrencyCode, useCurrencyFormatter, useOrderOutbox, useRegisterOutbox, useRegisterSession, useSale, useStoreSettings, useTax, uuidv7, validAmount, varianceText, variantPriceLabel, voidMovement, withPricingContext, writeClosure };
|
|
2307
|
+
export { type Actor, type AddLineInput, type AppliedDiscount, type BackendNotFound, type BasketLine, CartError, type CashMovement, type CashMovementCollection, type CatalogueEntry, type Closure, type ClosureCollection, type ClosureContext, type ClosureScope, type CommandTransport, type Correction, CurrencyProvider, type CurrencyProviderProps, type CustomerSummary, DISCOUNTS_UNSUPPORTED, type Discount, type DisplayLine, type DisplayTotals, type FinalizeOptions, type HttpTransportOptions, type LedgerRow, type LineItem, type LineTaxLine, type LogEntry, type LogLevel, type LogSink, type Logger, MAX_TENDER_MINOR, MICROS_PER_MINOR, type Movement$1 as Movement, type MovementType, type Order, type OrderBuilder, type OrderBuilderOptions, OrderContentMismatchError, type OrderManager, type OrderManagerOptions, type OrderOutbox, type OrderOutboxOptions, type OrderTaxTotals, type OutboxState, type ParkedOrderSummary, type Payment, type PaymentMethod, type PaymentTransport, type PlanLeg, type PosOrder, type PosOrderLine, type PosOrderLocalWarning, PosOrderOpenClosedError, type PosOrderPayment, type PosOrderServerFailures, type PosOrderSyncStatus, type RateTaxLine, type ReceiptConfig, type ReceiptData, type ReceiptLineItem, type RecordedFigures, RegisterApprovalRequiredError, type RegisterBucket, RegisterCloseIncompleteError, type RegisterCommand, type RegisterCommandCollection, type RegisterCounters, type RegisterDocument, type RegisterFact, type RegisterHost, RegisterIdInvalidError, RegisterMovementAmountError, RegisterMovementReasonError, RegisterMovementStrandedError, type RegisterOutbox, type RegisterOutboxOptions, type RegisterSession, RegisterSessionAlreadyOpenError, RegisterSessionClosedError, type RegisterSessionCollection, RegisterSessionRequiredError, type RegisterStore, RegisterTenderInProgressError, type Repository, type ResolveStoreSettingsOptions, SALE_SAVING, type SaleStage, type SentOrder, type SplitTab, type StoreSettingsResolution, type StoreSettingsState, TAX_ROUNDING_MIXED_NOTE, type TaxContext, type TaxLineInput, TaxProvider, type TaxProviderProps, type TaxRateMap, type TenderAction, type TenderKey, type TenderLineId, type TenderPlan, type TenderState, type TenderView, type TransportOutcome, UnsupportedOrderVersionError, type UseOrderOutboxOptions, type UseOrderOutboxResult, type UseRegisterOutboxOptions, type UseRegisterOutboxResult, type UseRegisterSessionOptions, activePlan, addEntryToCart, addPosOrderCollection, advancePerpetual, allocateOrderDiscount, appliedMinor, backToSelling, bindRegister, buildClosureDocument, buildReceiptData, buildXReportDocument, callbackSink, cashMovementSchema, catalogueEntries, changeMinor, clampClosureScope, closeNeedsApproval, closeSession, closureCommand, closureSchema, computeOrderTax, consoleSink, countVariance, createBackendNotFound, createHttpCommandTransport, createLogger, createOrderBuilder, createOrderManager, createOrderOutbox, createRegisterOutbox, createRepository, denominationTotal, denominations, deriveExpected, deriveSettled, ensureRegister, evenSplitShareMinor, exportCsv, finalizeOrder, findEntryByCode, formatClosureDate, getBoundRegisterId, getDeviceId, initTenderState, initialTenderState, isServerDecimal, labelKeys, minorToDecimal, mintClosureNumber, mintUuid, movementCommand, movementFieldError, needsAttention, nextSaleCounter, normalizeAmount, observeRegister$, openSession, openSessionSelector, orderDraftSchema, outboxLogger, overThreshold, parkedOrderSummaries$, parseMinor, planLegs, posOrderCollection, posOrderSchema, posOrdersLogger, quickTenderedAmounts, ratePpmFromPercent, readBoundRegister, readRegister, reconcileRegisterCommands, recordMovement, recordRegisterFact, registerCommandCollection, registerCommandSchema, registerCommandsLogger, registerFactsLogger, registerSessionCollection, registerSessionSchema, requireOpenSession, resolveStoreSettings, restoreOrderDraft, roundMicrosToMinor, saleLogger, sameSale, searchProducts, selectClosureRows, sessionOpenCommand, sessionTransitionCommand, stampSession, startCounting, stockOverlay$, stockOverlayAsOf$, taxFiguresForBasket, taxLinesByRate, taxLogger, taxMicros, taxProviderProps, tenderReducer, toOrderCreateEnvelope, unbindRegister, useCurrencyCode, useCurrencyFormatter, useOrderOutbox, useRegisterOutbox, useRegisterSession, useSale, useStoreSettings, useTax, uuidv7, validAmount, varianceText, variantPriceLabel, voidMovement, withPricingContext, writeClosure, writeOrderDraft };
|
package/dist/index.js
CHANGED
|
@@ -745,6 +745,15 @@ function createOrderBuilder(options) {
|
|
|
745
745
|
}
|
|
746
746
|
emit();
|
|
747
747
|
},
|
|
748
|
+
setUnitPrice(lineId, amountMinor) {
|
|
749
|
+
if (!Number.isInteger(amountMinor)) throw new RangeError("Price must be integer minor units");
|
|
750
|
+
if (amountMinor < 0) throw new RangeError("Price must be >= 0");
|
|
751
|
+
if (!lineItems.some((li) => li.id === lineId)) throw new Error(`Unknown line ${lineId}`);
|
|
752
|
+
lineItems = lineItems.map(
|
|
753
|
+
(li) => li.id === lineId ? recalculateLine({ ...li, unitPriceMinor: amountMinor }) : li
|
|
754
|
+
);
|
|
755
|
+
emit();
|
|
756
|
+
},
|
|
748
757
|
removeItem(lineId) {
|
|
749
758
|
lineItems = lineItems.filter((li) => li.id !== lineId);
|
|
750
759
|
emit();
|
|
@@ -833,7 +842,10 @@ function taxFiguresForBasket(currency, pricesIncludeTax, lines, rounding) {
|
|
|
833
842
|
}
|
|
834
843
|
|
|
835
844
|
// src/order/order-manager.ts
|
|
836
|
-
import { BehaviorSubject as BehaviorSubject2
|
|
845
|
+
import { BehaviorSubject as BehaviorSubject2 } from "rxjs";
|
|
846
|
+
|
|
847
|
+
// src/order/order-drafts.ts
|
|
848
|
+
import { map as map2 } from "rxjs";
|
|
837
849
|
|
|
838
850
|
// src/pos-order/command.ts
|
|
839
851
|
var UnsupportedOrderVersionError = class extends Error {
|
|
@@ -934,13 +946,86 @@ function toOrderCreateEnvelope(order, deviceId, attempt2 = 1, options) {
|
|
|
934
946
|
};
|
|
935
947
|
}
|
|
936
948
|
|
|
937
|
-
// src/order/order-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
949
|
+
// src/order/order-drafts.ts
|
|
950
|
+
var orderDraftSchema = {
|
|
951
|
+
version: 0,
|
|
952
|
+
primaryKey: "id",
|
|
953
|
+
type: "object",
|
|
954
|
+
properties: {
|
|
955
|
+
id: { type: "string", maxLength: 100 },
|
|
956
|
+
data: { type: "string" },
|
|
957
|
+
customerName: { type: "string" },
|
|
958
|
+
itemCount: { type: "integer", minimum: 0, maximum: 2147483647, multipleOf: 1 },
|
|
959
|
+
total: { type: "number" },
|
|
960
|
+
parkedAt: { type: "string" }
|
|
961
|
+
},
|
|
962
|
+
required: ["id", "data"]
|
|
963
|
+
};
|
|
964
|
+
async function writeOrderDraft(drafts, snapshot) {
|
|
965
|
+
await drafts.upsert({
|
|
966
|
+
id: snapshot.id,
|
|
967
|
+
data: JSON.stringify(snapshot),
|
|
968
|
+
customerName: snapshot.customer?.name ?? "",
|
|
969
|
+
itemCount: snapshot.lineItems.length,
|
|
970
|
+
total: snapshot.totalMinor,
|
|
971
|
+
parkedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
972
|
+
});
|
|
973
|
+
return snapshot.id;
|
|
974
|
+
}
|
|
975
|
+
function restoreOrderDraft(saved, options) {
|
|
976
|
+
const { currency, taxContext } = options;
|
|
977
|
+
const builder = createOrderBuilder({
|
|
978
|
+
currency,
|
|
979
|
+
taxContext,
|
|
980
|
+
id: saved.id
|
|
981
|
+
});
|
|
982
|
+
if (saved.customer && !customerRefusal(saved.customer)) {
|
|
983
|
+
builder.setCustomer(saved.customer);
|
|
984
|
+
}
|
|
985
|
+
if (saved.note) {
|
|
986
|
+
builder.setNote(saved.note);
|
|
987
|
+
}
|
|
988
|
+
for (const line of saved.lineItems) {
|
|
989
|
+
const lineId = builder.addLine({
|
|
990
|
+
productId: line.productId,
|
|
991
|
+
variantId: line.variantId,
|
|
992
|
+
name: line.name,
|
|
993
|
+
sku: line.sku,
|
|
994
|
+
imageUrl: line.imageUrl,
|
|
995
|
+
unitPrice: { amount: line.unitPriceMinor, currency, taxInclusive: line.taxInclusive },
|
|
996
|
+
quantity: line.quantity,
|
|
997
|
+
taxRates: line.taxLines.map(({ code, ratePpm }) => ({ code, ratePpm }))
|
|
998
|
+
});
|
|
999
|
+
for (const discount of line.discounts) {
|
|
1000
|
+
builder.applyLineDiscount(lineId, {
|
|
1001
|
+
type: discount.type,
|
|
1002
|
+
value: discount.value,
|
|
1003
|
+
label: discount.label,
|
|
1004
|
+
couponCode: discount.couponCode
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
for (const discount of saved.discounts) {
|
|
1009
|
+
builder.applyOrderDiscount({
|
|
1010
|
+
type: discount.type,
|
|
1011
|
+
value: discount.value,
|
|
1012
|
+
label: discount.label,
|
|
1013
|
+
couponCode: discount.couponCode
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
for (const payment of saved.payments) {
|
|
1017
|
+
builder.addPayment({
|
|
1018
|
+
method: payment.method,
|
|
1019
|
+
amountMinor: payment.amountMinor,
|
|
1020
|
+
tenderedMinor: payment.tenderedMinor,
|
|
1021
|
+
changeMinor: payment.changeMinor,
|
|
1022
|
+
reference: payment.reference
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
return builder;
|
|
1026
|
+
}
|
|
1027
|
+
function parkedOrderSummaries$(drafts) {
|
|
1028
|
+
return drafts.find().$.pipe(
|
|
944
1029
|
map2(
|
|
945
1030
|
(docs) => docs.map((doc) => {
|
|
946
1031
|
const json = doc.toJSON();
|
|
@@ -955,6 +1040,15 @@ function createOrderManager(options) {
|
|
|
955
1040
|
})
|
|
956
1041
|
)
|
|
957
1042
|
);
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// src/order/order-manager.ts
|
|
1046
|
+
function createOrderManager(options) {
|
|
1047
|
+
const { taxContext, draftsCollection } = options;
|
|
1048
|
+
const currency = options.currency.toUpperCase();
|
|
1049
|
+
let activeBuilder = createOrderBuilder({ currency, taxContext });
|
|
1050
|
+
const activeSubject = new BehaviorSubject2(activeBuilder);
|
|
1051
|
+
const parkedOrders$ = parkedOrderSummaries$(draftsCollection);
|
|
958
1052
|
return {
|
|
959
1053
|
activeOrder$: activeSubject.asObservable(),
|
|
960
1054
|
parkedOrders$,
|
|
@@ -965,14 +1059,7 @@ function createOrderManager(options) {
|
|
|
965
1059
|
},
|
|
966
1060
|
async parkCurrentOrder() {
|
|
967
1061
|
const snapshot = activeBuilder.getSnapshot();
|
|
968
|
-
await draftsCollection
|
|
969
|
-
id: snapshot.id,
|
|
970
|
-
data: JSON.stringify(snapshot),
|
|
971
|
-
customerName: snapshot.customer?.name ?? "",
|
|
972
|
-
itemCount: snapshot.lineItems.length,
|
|
973
|
-
total: snapshot.totalMinor,
|
|
974
|
-
parkedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
975
|
-
});
|
|
1062
|
+
await writeOrderDraft(draftsCollection, snapshot);
|
|
976
1063
|
activeBuilder = createOrderBuilder({ currency, taxContext });
|
|
977
1064
|
activeSubject.next(activeBuilder);
|
|
978
1065
|
return snapshot.id;
|
|
@@ -982,54 +1069,7 @@ function createOrderManager(options) {
|
|
|
982
1069
|
if (!doc) throw new Error(`Parked order ${orderId} not found`);
|
|
983
1070
|
const json = doc.toJSON();
|
|
984
1071
|
const savedOrder = JSON.parse(json.data);
|
|
985
|
-
const builder =
|
|
986
|
-
currency,
|
|
987
|
-
taxContext,
|
|
988
|
-
id: savedOrder.id
|
|
989
|
-
});
|
|
990
|
-
if (savedOrder.customer && !customerRefusal(savedOrder.customer)) {
|
|
991
|
-
builder.setCustomer(savedOrder.customer);
|
|
992
|
-
}
|
|
993
|
-
if (savedOrder.note) {
|
|
994
|
-
builder.setNote(savedOrder.note);
|
|
995
|
-
}
|
|
996
|
-
for (const line of savedOrder.lineItems) {
|
|
997
|
-
const lineId = builder.addLine({
|
|
998
|
-
productId: line.productId,
|
|
999
|
-
variantId: line.variantId,
|
|
1000
|
-
name: line.name,
|
|
1001
|
-
sku: line.sku,
|
|
1002
|
-
imageUrl: line.imageUrl,
|
|
1003
|
-
unitPrice: { amount: line.unitPriceMinor, currency, taxInclusive: line.taxInclusive },
|
|
1004
|
-
quantity: line.quantity,
|
|
1005
|
-
taxRates: line.taxLines.map(({ code, ratePpm }) => ({ code, ratePpm }))
|
|
1006
|
-
});
|
|
1007
|
-
for (const discount of line.discounts) {
|
|
1008
|
-
builder.applyLineDiscount(lineId, {
|
|
1009
|
-
type: discount.type,
|
|
1010
|
-
value: discount.value,
|
|
1011
|
-
label: discount.label,
|
|
1012
|
-
couponCode: discount.couponCode
|
|
1013
|
-
});
|
|
1014
|
-
}
|
|
1015
|
-
}
|
|
1016
|
-
for (const discount of savedOrder.discounts) {
|
|
1017
|
-
builder.applyOrderDiscount({
|
|
1018
|
-
type: discount.type,
|
|
1019
|
-
value: discount.value,
|
|
1020
|
-
label: discount.label,
|
|
1021
|
-
couponCode: discount.couponCode
|
|
1022
|
-
});
|
|
1023
|
-
}
|
|
1024
|
-
for (const payment of savedOrder.payments) {
|
|
1025
|
-
builder.addPayment({
|
|
1026
|
-
method: payment.method,
|
|
1027
|
-
amountMinor: payment.amountMinor,
|
|
1028
|
-
tenderedMinor: payment.tenderedMinor,
|
|
1029
|
-
changeMinor: payment.changeMinor,
|
|
1030
|
-
reference: payment.reference
|
|
1031
|
-
});
|
|
1032
|
-
}
|
|
1072
|
+
const builder = restoreOrderDraft(savedOrder, { currency, taxContext });
|
|
1033
1073
|
await doc.remove();
|
|
1034
1074
|
activeBuilder = builder;
|
|
1035
1075
|
activeSubject.next(activeBuilder);
|
|
@@ -3540,7 +3580,7 @@ function useSale(settings, opts) {
|
|
|
3540
3580
|
if (active) setError(SALE_SAVING);
|
|
3541
3581
|
return active;
|
|
3542
3582
|
}
|
|
3543
|
-
function resetSale(customer) {
|
|
3583
|
+
function resetSale(customer, restored) {
|
|
3544
3584
|
if (inFlight.current && !pending.current) return setError(SALE_SAVING);
|
|
3545
3585
|
if (pending.current && confirmed.current !== pending.current) {
|
|
3546
3586
|
if (inFlight.current) checkStored(pending.current);
|
|
@@ -3553,7 +3593,7 @@ function useSale(settings, opts) {
|
|
|
3553
3593
|
droppedReference.current = null;
|
|
3554
3594
|
setSaving(false);
|
|
3555
3595
|
madeWith.current = { taxContext, currency: settings.currency };
|
|
3556
|
-
const next = createOrderBuilder({ currency: settings.currency, taxContext });
|
|
3596
|
+
const next = restored ?? createOrderBuilder({ currency: settings.currency, taxContext });
|
|
3557
3597
|
if (customer !== null) next.setCustomer(customer);
|
|
3558
3598
|
builderNow.current = next;
|
|
3559
3599
|
setBuilder(next);
|
|
@@ -3683,6 +3723,17 @@ function useSale(settings, opts) {
|
|
|
3683
3723
|
setQuantity(lineId, quantity) {
|
|
3684
3724
|
if (!locked()) builderNow.current.updateQuantity(lineId, quantity);
|
|
3685
3725
|
},
|
|
3726
|
+
/** Sets a line's unit price in minor units of its own tax mode, recomputing discounts and tax; returns a refusal or null. */
|
|
3727
|
+
setUnitPrice(lineId, amountMinor) {
|
|
3728
|
+
if (locked()) return SALE_SAVING;
|
|
3729
|
+
try {
|
|
3730
|
+
builderNow.current.setUnitPrice(lineId, amountMinor);
|
|
3731
|
+
return null;
|
|
3732
|
+
} catch (error2) {
|
|
3733
|
+
if (error2 instanceof RangeError || error2 instanceof Error && error2.message === `Unknown line ${lineId}`) return error2.message;
|
|
3734
|
+
throw error2;
|
|
3735
|
+
}
|
|
3736
|
+
},
|
|
3686
3737
|
remove(lineId) {
|
|
3687
3738
|
if (!locked()) builderNow.current.removeItem(lineId);
|
|
3688
3739
|
},
|
|
@@ -3814,6 +3865,39 @@ function useSale(settings, opts) {
|
|
|
3814
3865
|
newSale() {
|
|
3815
3866
|
resetSale(null);
|
|
3816
3867
|
},
|
|
3868
|
+
/** Parks the cart in `drafts`; resolves to a refusal message, or null once parked. */
|
|
3869
|
+
async park() {
|
|
3870
|
+
if (locked()) return SALE_SAVING;
|
|
3871
|
+
if (!opts.drafts) throw new Error("useSale: park() needs the drafts option");
|
|
3872
|
+
if (stageNow.current.kind !== "cart") return "Finish or cancel the payment before parking the sale";
|
|
3873
|
+
const builder = builderNow.current;
|
|
3874
|
+
const snapshot = builder.getSnapshot();
|
|
3875
|
+
if (!snapshot.lineItems.length) return "There is nothing to park";
|
|
3876
|
+
await writeOrderDraft(opts.drafts, snapshot);
|
|
3877
|
+
if (builderNow.current !== builder || builder.getSnapshot() !== snapshot) return "The sale changed while it was being parked; park it again";
|
|
3878
|
+
resetSale(null);
|
|
3879
|
+
return null;
|
|
3880
|
+
},
|
|
3881
|
+
/** Resumes a draft into an empty cart; resolves to a refusal message, or null once resumed. */
|
|
3882
|
+
async resume(draftId) {
|
|
3883
|
+
if (locked()) return SALE_SAVING;
|
|
3884
|
+
if (!opts.drafts) throw new Error("useSale: resume() needs the drafts option");
|
|
3885
|
+
const live = builderNow.current;
|
|
3886
|
+
const liveSnapshot = live.getSnapshot();
|
|
3887
|
+
if (stageNow.current.kind !== "cart" || liveSnapshot.lineItems.length) return "Park or clear the current sale first";
|
|
3888
|
+
const doc = await opts.drafts.findOne(draftId).exec();
|
|
3889
|
+
if (!doc) return "That parked sale is no longer there";
|
|
3890
|
+
const saved = JSON.parse(doc.toJSON().data);
|
|
3891
|
+
if (saved.currency.toUpperCase() !== settings.currency.toUpperCase()) return "That parked sale is in another currency";
|
|
3892
|
+
const builder = restoreOrderDraft(saved, { currency: settings.currency, taxContext });
|
|
3893
|
+
await doc.remove();
|
|
3894
|
+
if (builderNow.current !== live || live.getSnapshot() !== liveSnapshot) {
|
|
3895
|
+
await writeOrderDraft(opts.drafts, saved);
|
|
3896
|
+
return "The sale changed while it was being resumed; resume it again";
|
|
3897
|
+
}
|
|
3898
|
+
resetSale(null, builder);
|
|
3899
|
+
return null;
|
|
3900
|
+
},
|
|
3817
3901
|
/**
|
|
3818
3902
|
* Continue after a failed save whose order is confirmed stored (`canContinue`): exactly `newSale()`.
|
|
3819
3903
|
* The order stays pending in the outbox, which will send it; it isn't handed to `onSaleCompleted` again, and
|
|
@@ -3828,8 +3912,17 @@ function useSale(settings, opts) {
|
|
|
3828
3912
|
|
|
3829
3913
|
// src/sale/catalogue.ts
|
|
3830
3914
|
import { findVariantByCode, formatMoney as formatMoney2, resolvePrice as resolvePrice3 } from "@tallyui/core";
|
|
3831
|
-
function catalogueEntries(products, traits) {
|
|
3832
|
-
return products.flatMap((product) =>
|
|
3915
|
+
function catalogueEntries(products, traits, context) {
|
|
3916
|
+
return products.flatMap((product) => {
|
|
3917
|
+
const variants = traits.getVariants ? traits.getVariants(product, context) : traits.hasVariants(product) ? [] : [{
|
|
3918
|
+
id: traits.getId(product),
|
|
3919
|
+
sku: traits.getSku(product),
|
|
3920
|
+
barcode: traits.getBarcode(product),
|
|
3921
|
+
prices: traits.getPrices(product, context),
|
|
3922
|
+
stock: traits.getStock(product)
|
|
3923
|
+
}];
|
|
3924
|
+
return variants.map((variant) => ({ product, variant }));
|
|
3925
|
+
});
|
|
3833
3926
|
}
|
|
3834
3927
|
function findEntryByCode(entries, code) {
|
|
3835
3928
|
const variant = findVariantByCode(entries.map((entry) => entry.variant), code);
|
|
@@ -5077,8 +5170,10 @@ export {
|
|
|
5077
5170
|
observeRegister$,
|
|
5078
5171
|
openSession,
|
|
5079
5172
|
openSessionSelector,
|
|
5173
|
+
orderDraftSchema,
|
|
5080
5174
|
outboxLogger,
|
|
5081
5175
|
overThreshold,
|
|
5176
|
+
parkedOrderSummaries$,
|
|
5082
5177
|
parseMinor,
|
|
5083
5178
|
planLegs,
|
|
5084
5179
|
posOrderCollection,
|
|
@@ -5100,6 +5195,7 @@ export {
|
|
|
5100
5195
|
registerSessionSchema,
|
|
5101
5196
|
requireOpenSession,
|
|
5102
5197
|
resolveStoreSettings,
|
|
5198
|
+
restoreOrderDraft,
|
|
5103
5199
|
roundMicrosToMinor,
|
|
5104
5200
|
saleLogger,
|
|
5105
5201
|
sameSale,
|
|
@@ -5135,5 +5231,6 @@ export {
|
|
|
5135
5231
|
watchFresh,
|
|
5136
5232
|
withPricingContext,
|
|
5137
5233
|
withStockOverlay,
|
|
5138
|
-
writeClosure
|
|
5234
|
+
writeClosure,
|
|
5235
|
+
writeOrderDraft
|
|
5139
5236
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tallyui/pos",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.3",
|
|
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",
|
|
@@ -27,20 +27,20 @@
|
|
|
27
27
|
"directory": "packages/pos"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
|
-
"@tallyui/core": "^3.0.
|
|
30
|
+
"@tallyui/core": "^3.0.3",
|
|
31
31
|
"react": ">=18",
|
|
32
32
|
"rxdb": "~17.5.0",
|
|
33
33
|
"rxjs": ">=7"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"@tallyui/core": "3.0.
|
|
36
|
+
"@tallyui/core": "3.0.3",
|
|
37
37
|
"@types/react": ">=18",
|
|
38
38
|
"react": "^19.2.4",
|
|
39
39
|
"rxdb": "17.5.0",
|
|
40
40
|
"rxjs": "7.8.2",
|
|
41
|
-
"@tallyui/connector-medusa": "3.0.
|
|
42
|
-
"@tallyui/connector-woocommerce": "3.0.
|
|
43
|
-
"@tallyui/database": "3.0.
|
|
41
|
+
"@tallyui/connector-medusa": "3.0.3",
|
|
42
|
+
"@tallyui/connector-woocommerce": "3.0.3",
|
|
43
|
+
"@tallyui/database": "3.0.3"
|
|
44
44
|
},
|
|
45
45
|
"scripts": {
|
|
46
46
|
"build": "tsup",
|
package/src/index.ts
CHANGED
|
@@ -26,6 +26,7 @@ export type { Repository } from './repository';
|
|
|
26
26
|
|
|
27
27
|
// Order
|
|
28
28
|
export { createOrderBuilder, createOrderManager, allocateOrderDiscount, taxFiguresForBasket } from './order';
|
|
29
|
+
export { orderDraftSchema, writeOrderDraft, restoreOrderDraft, parkedOrderSummaries$ } from './order';
|
|
29
30
|
export type { BasketLine } from './order';
|
|
30
31
|
export type { OrderBuilder, OrderBuilderOptions } from './order';
|
|
31
32
|
export type { OrderManager, OrderManagerOptions, ParkedOrderSummary } from './order';
|
package/src/order/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ export type { BasketLine } from './tax-figures';
|
|
|
4
4
|
export { allocateOrderDiscount } from './allocate-order-discount';
|
|
5
5
|
export type { OrderBuilder, OrderBuilderOptions } from './order-builder';
|
|
6
6
|
export { createOrderManager } from './order-manager';
|
|
7
|
+
export { orderDraftSchema, writeOrderDraft, restoreOrderDraft, parkedOrderSummaries$ } from './order-drafts';
|
|
7
8
|
export type { OrderManager, OrderManagerOptions, ParkedOrderSummary } from './order-manager';
|
|
8
9
|
export type {
|
|
9
10
|
Order,
|
|
@@ -71,6 +71,8 @@ export interface OrderBuilder {
|
|
|
71
71
|
addProduct(doc: any, traits: ProductTraits, options?: { variantId?: string; quantity?: number }): string;
|
|
72
72
|
addLine(input: AddLineInput): string;
|
|
73
73
|
updateQuantity(lineId: string, quantity: number): void;
|
|
74
|
+
/** Sets a line's unit price (a till price edit), in minor units of the order's currency and the line's own tax mode; its discounts and tax are recomputed. */
|
|
75
|
+
setUnitPrice(lineId: string, amountMinor: number): void;
|
|
74
76
|
removeItem(lineId: string): void;
|
|
75
77
|
applyLineDiscount(lineId: string, discount: Discount): void;
|
|
76
78
|
applyOrderDiscount(discount: Discount): void;
|
|
@@ -355,6 +357,16 @@ export function createOrderBuilder(options: OrderBuilderOptions): OrderBuilder {
|
|
|
355
357
|
emit();
|
|
356
358
|
},
|
|
357
359
|
|
|
360
|
+
setUnitPrice(lineId, amountMinor) {
|
|
361
|
+
if (!Number.isInteger(amountMinor)) throw new RangeError('Price must be integer minor units');
|
|
362
|
+
if (amountMinor < 0) throw new RangeError('Price must be >= 0');
|
|
363
|
+
if (!lineItems.some((li) => li.id === lineId)) throw new Error(`Unknown line ${lineId}`);
|
|
364
|
+
lineItems = lineItems.map((li) =>
|
|
365
|
+
li.id === lineId ? recalculateLine({ ...li, unitPriceMinor: amountMinor }) : li,
|
|
366
|
+
);
|
|
367
|
+
emit();
|
|
368
|
+
},
|
|
369
|
+
|
|
358
370
|
removeItem(lineId) {
|
|
359
371
|
lineItems = lineItems.filter((li) => li.id !== lineId);
|
|
360
372
|
emit();
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { map, type Observable } from 'rxjs';
|
|
2
|
+
import type { RxCollection, RxJsonSchema } from 'rxdb';
|
|
3
|
+
import type { TaxContext } from '../tax/types';
|
|
4
|
+
import { customerRefusal } from '../pos-order/command';
|
|
5
|
+
import { createOrderBuilder, type OrderBuilder } from './order-builder';
|
|
6
|
+
import type { Order } from './types';
|
|
7
|
+
|
|
8
|
+
/** Apps create their drafts collection with this schema. */
|
|
9
|
+
export const orderDraftSchema: RxJsonSchema<{
|
|
10
|
+
id: string; data: string; customerName?: string; itemCount?: number; total?: number; parkedAt?: string;
|
|
11
|
+
}> = {
|
|
12
|
+
version: 0,
|
|
13
|
+
primaryKey: 'id',
|
|
14
|
+
type: 'object',
|
|
15
|
+
properties: {
|
|
16
|
+
id: { type: 'string', maxLength: 100 },
|
|
17
|
+
data: { type: 'string' },
|
|
18
|
+
customerName: { type: 'string' },
|
|
19
|
+
itemCount: { type: 'integer', minimum: 0, maximum: 2147483647, multipleOf: 1 },
|
|
20
|
+
total: { type: 'number' },
|
|
21
|
+
parkedAt: { type: 'string' },
|
|
22
|
+
},
|
|
23
|
+
required: ['id', 'data'],
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export interface ParkedOrderSummary {
|
|
27
|
+
id: string;
|
|
28
|
+
customerName?: string;
|
|
29
|
+
itemCount: number;
|
|
30
|
+
totalMinor: number;
|
|
31
|
+
parkedAt: string;
|
|
32
|
+
source: 'local' | 'server';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function writeOrderDraft(drafts: RxCollection, snapshot: Order): Promise<string> {
|
|
36
|
+
await drafts.upsert({
|
|
37
|
+
id: snapshot.id,
|
|
38
|
+
data: JSON.stringify(snapshot),
|
|
39
|
+
customerName: snapshot.customer?.name ?? '',
|
|
40
|
+
itemCount: snapshot.lineItems.length,
|
|
41
|
+
total: snapshot.totalMinor,
|
|
42
|
+
parkedAt: new Date().toISOString(),
|
|
43
|
+
});
|
|
44
|
+
return snapshot.id;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function restoreOrderDraft(saved: Order, options: { currency: string; taxContext: TaxContext }): OrderBuilder {
|
|
48
|
+
const { currency, taxContext } = options;
|
|
49
|
+
const builder = createOrderBuilder({
|
|
50
|
+
currency,
|
|
51
|
+
taxContext,
|
|
52
|
+
id: saved.id,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Restore state from saved order
|
|
56
|
+
// A customer whose email or id order.create would refuse (see useSale's setCustomer) isn't restored.
|
|
57
|
+
if (saved.customer && !customerRefusal(saved.customer)) {
|
|
58
|
+
builder.setCustomer(saved.customer);
|
|
59
|
+
}
|
|
60
|
+
if (saved.note) {
|
|
61
|
+
builder.setNote(saved.note);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Restore line items and their discounts
|
|
65
|
+
for (const line of saved.lineItems) {
|
|
66
|
+
const lineId = builder.addLine({
|
|
67
|
+
productId: line.productId,
|
|
68
|
+
variantId: line.variantId,
|
|
69
|
+
name: line.name,
|
|
70
|
+
sku: line.sku,
|
|
71
|
+
imageUrl: line.imageUrl,
|
|
72
|
+
unitPrice: { amount: line.unitPriceMinor, currency, taxInclusive: line.taxInclusive },
|
|
73
|
+
quantity: line.quantity,
|
|
74
|
+
taxRates: line.taxLines.map(({ code, ratePpm }) => ({ code, ratePpm })),
|
|
75
|
+
});
|
|
76
|
+
for (const discount of line.discounts) {
|
|
77
|
+
builder.applyLineDiscount(lineId, {
|
|
78
|
+
type: discount.type,
|
|
79
|
+
value: discount.value,
|
|
80
|
+
label: discount.label,
|
|
81
|
+
couponCode: discount.couponCode,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Restore order-level discounts
|
|
87
|
+
for (const discount of saved.discounts) {
|
|
88
|
+
builder.applyOrderDiscount({
|
|
89
|
+
type: discount.type,
|
|
90
|
+
value: discount.value,
|
|
91
|
+
label: discount.label,
|
|
92
|
+
couponCode: discount.couponCode,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Restore payments
|
|
97
|
+
for (const payment of saved.payments) {
|
|
98
|
+
builder.addPayment({
|
|
99
|
+
method: payment.method,
|
|
100
|
+
amountMinor: payment.amountMinor,
|
|
101
|
+
tenderedMinor: payment.tenderedMinor,
|
|
102
|
+
changeMinor: payment.changeMinor,
|
|
103
|
+
reference: payment.reference,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return builder;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function parkedOrderSummaries$(drafts: RxCollection): Observable<ParkedOrderSummary[]> {
|
|
110
|
+
return drafts.find().$.pipe(
|
|
111
|
+
map((docs) =>
|
|
112
|
+
docs.map((doc) => {
|
|
113
|
+
const json = doc.toJSON() as any;
|
|
114
|
+
return {
|
|
115
|
+
id: json.id,
|
|
116
|
+
customerName: json.customerName || undefined,
|
|
117
|
+
itemCount: json.itemCount ?? 0,
|
|
118
|
+
totalMinor: json.total ?? 0,
|
|
119
|
+
parkedAt: json.parkedAt ?? '',
|
|
120
|
+
source: 'local' as const,
|
|
121
|
+
};
|
|
122
|
+
}),
|
|
123
|
+
),
|
|
124
|
+
);
|
|
125
|
+
}
|
|
@@ -1,18 +1,11 @@
|
|
|
1
|
-
import { BehaviorSubject,
|
|
1
|
+
import { BehaviorSubject, type Observable } from 'rxjs';
|
|
2
2
|
import type { RxCollection } from 'rxdb';
|
|
3
3
|
import type { TaxContext } from '../tax/types';
|
|
4
|
-
import { customerRefusal } from '../pos-order/command';
|
|
5
4
|
import { createOrderBuilder, type OrderBuilder } from './order-builder';
|
|
6
5
|
import type { Order } from './types';
|
|
6
|
+
import { writeOrderDraft, restoreOrderDraft, parkedOrderSummaries$, type ParkedOrderSummary } from './order-drafts';
|
|
7
7
|
|
|
8
|
-
export
|
|
9
|
-
id: string;
|
|
10
|
-
customerName?: string;
|
|
11
|
-
itemCount: number;
|
|
12
|
-
totalMinor: number;
|
|
13
|
-
parkedAt: string;
|
|
14
|
-
source: 'local' | 'server';
|
|
15
|
-
}
|
|
8
|
+
export type { ParkedOrderSummary } from './order-drafts';
|
|
16
9
|
|
|
17
10
|
export interface OrderManagerOptions {
|
|
18
11
|
currency: string;
|
|
@@ -36,21 +29,7 @@ export function createOrderManager(options: OrderManagerOptions): OrderManager {
|
|
|
36
29
|
let activeBuilder = createOrderBuilder({ currency, taxContext });
|
|
37
30
|
const activeSubject = new BehaviorSubject<OrderBuilder>(activeBuilder);
|
|
38
31
|
|
|
39
|
-
const parkedOrders
|
|
40
|
-
map((docs) =>
|
|
41
|
-
docs.map((doc) => {
|
|
42
|
-
const json = doc.toJSON() as any;
|
|
43
|
-
return {
|
|
44
|
-
id: json.id,
|
|
45
|
-
customerName: json.customerName || undefined,
|
|
46
|
-
itemCount: json.itemCount ?? 0,
|
|
47
|
-
totalMinor: json.total ?? 0,
|
|
48
|
-
parkedAt: json.parkedAt ?? '',
|
|
49
|
-
source: 'local' as const,
|
|
50
|
-
};
|
|
51
|
-
}),
|
|
52
|
-
),
|
|
53
|
-
);
|
|
32
|
+
const parkedOrders$ = parkedOrderSummaries$(draftsCollection);
|
|
54
33
|
|
|
55
34
|
return {
|
|
56
35
|
activeOrder$: activeSubject.asObservable(),
|
|
@@ -64,14 +43,7 @@ export function createOrderManager(options: OrderManagerOptions): OrderManager {
|
|
|
64
43
|
|
|
65
44
|
async parkCurrentOrder() {
|
|
66
45
|
const snapshot = activeBuilder.getSnapshot();
|
|
67
|
-
await draftsCollection
|
|
68
|
-
id: snapshot.id,
|
|
69
|
-
data: JSON.stringify(snapshot),
|
|
70
|
-
customerName: snapshot.customer?.name ?? '',
|
|
71
|
-
itemCount: snapshot.lineItems.length,
|
|
72
|
-
total: snapshot.totalMinor,
|
|
73
|
-
parkedAt: new Date().toISOString(),
|
|
74
|
-
});
|
|
46
|
+
await writeOrderDraft(draftsCollection, snapshot);
|
|
75
47
|
|
|
76
48
|
activeBuilder = createOrderBuilder({ currency, taxContext });
|
|
77
49
|
activeSubject.next(activeBuilder);
|
|
@@ -85,63 +57,7 @@ export function createOrderManager(options: OrderManagerOptions): OrderManager {
|
|
|
85
57
|
const json = doc.toJSON() as any;
|
|
86
58
|
const savedOrder: Order = JSON.parse(json.data);
|
|
87
59
|
|
|
88
|
-
const builder =
|
|
89
|
-
currency,
|
|
90
|
-
taxContext,
|
|
91
|
-
id: savedOrder.id,
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
// Restore state from saved order
|
|
95
|
-
// A customer whose email or id order.create would refuse (see useSale's setCustomer) isn't restored.
|
|
96
|
-
if (savedOrder.customer && !customerRefusal(savedOrder.customer)) {
|
|
97
|
-
builder.setCustomer(savedOrder.customer);
|
|
98
|
-
}
|
|
99
|
-
if (savedOrder.note) {
|
|
100
|
-
builder.setNote(savedOrder.note);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Restore line items and their discounts
|
|
104
|
-
for (const line of savedOrder.lineItems) {
|
|
105
|
-
const lineId = builder.addLine({
|
|
106
|
-
productId: line.productId,
|
|
107
|
-
variantId: line.variantId,
|
|
108
|
-
name: line.name,
|
|
109
|
-
sku: line.sku,
|
|
110
|
-
imageUrl: line.imageUrl,
|
|
111
|
-
unitPrice: { amount: line.unitPriceMinor, currency, taxInclusive: line.taxInclusive },
|
|
112
|
-
quantity: line.quantity,
|
|
113
|
-
taxRates: line.taxLines.map(({ code, ratePpm }) => ({ code, ratePpm })),
|
|
114
|
-
});
|
|
115
|
-
for (const discount of line.discounts) {
|
|
116
|
-
builder.applyLineDiscount(lineId, {
|
|
117
|
-
type: discount.type,
|
|
118
|
-
value: discount.value,
|
|
119
|
-
label: discount.label,
|
|
120
|
-
couponCode: discount.couponCode,
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Restore order-level discounts
|
|
126
|
-
for (const discount of savedOrder.discounts) {
|
|
127
|
-
builder.applyOrderDiscount({
|
|
128
|
-
type: discount.type,
|
|
129
|
-
value: discount.value,
|
|
130
|
-
label: discount.label,
|
|
131
|
-
couponCode: discount.couponCode,
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Restore payments
|
|
136
|
-
for (const payment of savedOrder.payments) {
|
|
137
|
-
builder.addPayment({
|
|
138
|
-
method: payment.method,
|
|
139
|
-
amountMinor: payment.amountMinor,
|
|
140
|
-
tenderedMinor: payment.tenderedMinor,
|
|
141
|
-
changeMinor: payment.changeMinor,
|
|
142
|
-
reference: payment.reference,
|
|
143
|
-
});
|
|
144
|
-
}
|
|
60
|
+
const builder = restoreOrderDraft(savedOrder, { currency, taxContext });
|
|
145
61
|
|
|
146
62
|
await doc.remove();
|
|
147
63
|
|
package/src/sale/catalogue.ts
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import { findVariantByCode, formatMoney, resolvePrice } from '@tallyui/core';
|
|
2
|
-
import type { ProductTraits, VariantSummary } from '@tallyui/core';
|
|
2
|
+
import type { ProductTraits, TraitContext, VariantSummary } from '@tallyui/core';
|
|
3
3
|
|
|
4
4
|
export type CatalogueEntry<Doc> = { product: Doc; variant: VariantSummary };
|
|
5
5
|
|
|
6
|
-
/**
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
/** Variants in product/variant order, passing context through; without getVariants,
|
|
7
|
+
* simple products use their own traits and products with variants contribute no entries. */
|
|
8
|
+
export function catalogueEntries<Doc>(products: Doc[], traits: ProductTraits<Doc>, context?: TraitContext): CatalogueEntry<Doc>[] {
|
|
9
|
+
return products.flatMap((product) => {
|
|
10
|
+
const variants = traits.getVariants ? traits.getVariants(product, context)
|
|
11
|
+
: traits.hasVariants(product) ? [] : [{
|
|
12
|
+
id: traits.getId(product), sku: traits.getSku(product), barcode: traits.getBarcode(product),
|
|
13
|
+
prices: traits.getPrices(product, context), stock: traits.getStock(product),
|
|
14
|
+
}];
|
|
15
|
+
return variants.map((variant) => ({ product, variant }));
|
|
16
|
+
});
|
|
9
17
|
}
|
|
10
18
|
|
|
11
19
|
/** Barcode-then-SKU lookup across all products. */
|
package/src/sale/use-sale.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
2
|
+
import type { RxCollection } from 'rxdb';
|
|
2
3
|
import type { ProductTraits, ServerCapabilities, StoreSettings } from '@tallyui/core';
|
|
3
|
-
import { createOrderBuilder, type CustomerSummary, type Discount, type Order, type SentOrder } from '../order';
|
|
4
|
+
import { createOrderBuilder, writeOrderDraft, restoreOrderDraft, type OrderBuilder, type CustomerSummary, type Discount, type Order, type SentOrder } from '../order';
|
|
4
5
|
import { finalizeOrder, type PosOrder } from '../pos-order';
|
|
5
6
|
import { MESSAGE_NAME_MAX, referenceError, referenceReason, withSentForm } from '../pos-order/finalize';
|
|
6
7
|
import { CUSTOMER_REFUSALS, customerRefusal, cutText, PAYLOAD_STRING_MAX } from '../pos-order/command';
|
|
@@ -30,6 +31,8 @@ export const HUNG_SAVE_CHECK_MS = 5000;
|
|
|
30
31
|
/** Call under a `TaxProvider`: its tax context and the settings' currency price every sale. */
|
|
31
32
|
export function useSale(settings: Pick<StoreSettings, 'currency'>, opts: {
|
|
32
33
|
registerId: string; cashierRef: string; capabilities?: ServerCapabilities;
|
|
34
|
+
/** Where `park()` keeps parked carts; create it with `orderDraftSchema` */
|
|
35
|
+
drafts?: RxCollection;
|
|
33
36
|
/**
|
|
34
37
|
* When set, `complete()` stamps the finalized order with this session before `onSaleCompleted`.
|
|
35
38
|
* `complete()` runs after the money is taken, so a refused stamp (the session closed or went
|
|
@@ -103,7 +106,7 @@ export function useSale(settings: Pick<StoreSettings, 'currency'>, opts: {
|
|
|
103
106
|
return active;
|
|
104
107
|
}
|
|
105
108
|
// newSale()'s body (see its doc comment for the guards): `newSale()` is `resetSale(null)`.
|
|
106
|
-
function resetSale(customer: CustomerSummary | null) {
|
|
109
|
+
function resetSale(customer: CustomerSummary | null, restored?: OrderBuilder) {
|
|
107
110
|
if (inFlight.current && !pending.current) return setError(SALE_SAVING);
|
|
108
111
|
if (pending.current && confirmed.current !== pending.current) {
|
|
109
112
|
if (inFlight.current) checkStored(pending.current);
|
|
@@ -115,7 +118,7 @@ export function useSale(settings: Pick<StoreSettings, 'currency'>, opts: {
|
|
|
115
118
|
droppedReference.current = null;
|
|
116
119
|
setSaving(false);
|
|
117
120
|
madeWith.current = { taxContext, currency: settings.currency };
|
|
118
|
-
const next = createOrderBuilder({ currency: settings.currency, taxContext });
|
|
121
|
+
const next = restored ?? createOrderBuilder({ currency: settings.currency, taxContext });
|
|
119
122
|
if (customer !== null) next.setCustomer(customer);
|
|
120
123
|
builderNow.current = next;
|
|
121
124
|
setBuilder(next);
|
|
@@ -258,6 +261,17 @@ export function useSale(settings: Pick<StoreSettings, 'currency'>, opts: {
|
|
|
258
261
|
}
|
|
259
262
|
},
|
|
260
263
|
setQuantity(lineId: string, quantity: number) { if (!locked()) builderNow.current.updateQuantity(lineId, quantity); },
|
|
264
|
+
/** Sets a line's unit price in minor units of its own tax mode, recomputing discounts and tax; returns a refusal or null. */
|
|
265
|
+
setUnitPrice(lineId: string, amountMinor: number): string | null {
|
|
266
|
+
if (locked()) return SALE_SAVING;
|
|
267
|
+
try {
|
|
268
|
+
builderNow.current.setUnitPrice(lineId, amountMinor);
|
|
269
|
+
return null;
|
|
270
|
+
} catch (error) {
|
|
271
|
+
if (error instanceof RangeError || (error instanceof Error && error.message === `Unknown line ${lineId}`)) return error.message;
|
|
272
|
+
throw error;
|
|
273
|
+
}
|
|
274
|
+
},
|
|
261
275
|
remove(lineId: string) { if (!locked()) builderNow.current.removeItem(lineId); },
|
|
262
276
|
/** A line's discount, or the order's without a line; returns the refusal to show, or null once applied. */
|
|
263
277
|
applyDiscount(lineId: string | null, discount: Discount): string | null {
|
|
@@ -381,6 +395,39 @@ export function useSale(settings: Pick<StoreSettings, 'currency'>, opts: {
|
|
|
381
395
|
* updates or requeues `pos_orders` itself.
|
|
382
396
|
*/
|
|
383
397
|
newSale() { resetSale(null); },
|
|
398
|
+
/** Parks the cart in `drafts`; resolves to a refusal message, or null once parked. */
|
|
399
|
+
async park(): Promise<string | null> {
|
|
400
|
+
if (locked()) return SALE_SAVING;
|
|
401
|
+
if (!opts.drafts) throw new Error('useSale: park() needs the drafts option');
|
|
402
|
+
if (stageNow.current.kind !== 'cart') return 'Finish or cancel the payment before parking the sale';
|
|
403
|
+
const builder = builderNow.current;
|
|
404
|
+
const snapshot = builder.getSnapshot();
|
|
405
|
+
if (!snapshot.lineItems.length) return 'There is nothing to park';
|
|
406
|
+
await writeOrderDraft(opts.drafts, snapshot);
|
|
407
|
+
if (builderNow.current !== builder || builder.getSnapshot() !== snapshot) return 'The sale changed while it was being parked; park it again';
|
|
408
|
+
resetSale(null);
|
|
409
|
+
return null;
|
|
410
|
+
},
|
|
411
|
+
/** Resumes a draft into an empty cart; resolves to a refusal message, or null once resumed. */
|
|
412
|
+
async resume(draftId: string): Promise<string | null> {
|
|
413
|
+
if (locked()) return SALE_SAVING;
|
|
414
|
+
if (!opts.drafts) throw new Error('useSale: resume() needs the drafts option');
|
|
415
|
+
const live = builderNow.current;
|
|
416
|
+
const liveSnapshot = live.getSnapshot();
|
|
417
|
+
if (stageNow.current.kind !== 'cart' || liveSnapshot.lineItems.length) return 'Park or clear the current sale first';
|
|
418
|
+
const doc = await opts.drafts.findOne(draftId).exec();
|
|
419
|
+
if (!doc) return 'That parked sale is no longer there';
|
|
420
|
+
const saved: Order = JSON.parse(doc.toJSON().data);
|
|
421
|
+
if (saved.currency.toUpperCase() !== settings.currency.toUpperCase()) return 'That parked sale is in another currency';
|
|
422
|
+
const builder = restoreOrderDraft(saved, { currency: settings.currency, taxContext });
|
|
423
|
+
await doc.remove();
|
|
424
|
+
if (builderNow.current !== live || live.getSnapshot() !== liveSnapshot) {
|
|
425
|
+
await writeOrderDraft(opts.drafts, saved);
|
|
426
|
+
return 'The sale changed while it was being resumed; resume it again';
|
|
427
|
+
}
|
|
428
|
+
resetSale(null, builder);
|
|
429
|
+
return null;
|
|
430
|
+
},
|
|
384
431
|
/**
|
|
385
432
|
* Continue after a failed save whose order is confirmed stored (`canContinue`): exactly `newSale()`.
|
|
386
433
|
* The order stays pending in the outbox, which will send it; it isn't handed to `onSaleCompleted` again, and
|