Changelog
Released changes to the @tallyui packages, newest first.
3.0.0
@tallyui/core
Major Changes
- 668f71f: Breaking:
precheckCommandnow takes the server's own supported versions as a required second argument,{ orderCreate, register }, and checks against them; itsunsupported_versionmessage anddataname the server's list, not core's (#297). Plugins MUST pass their supported versions:precheckCommand(envelope, { orderCreate: [1, 2, 3], register: [1] }); an empty list throws aTypeError. This is part of 3.0.0's breaking changes and adds no extra major.SUPPORTED_ORDER_CREATE_VERSIONSandSUPPORTED_REGISTER_VERSIONSstay exported as the till's capability (whattoOrderCreateEnvelopecan produce), not what a server supports.
Minor Changes
-
4de75c2: A batch over 50 commands is answered
413with{ code: 'batch_too_large', maxCommands: 50, message: 'At most 50 commands are allowed' }, never400(ADR-038, Front desk ruling 18):@tallyui/core/server'svalidateBatchreturns thatbodyon its413failure so plugins send it as is.@tallyui/core/servernow also exportsMAX_COMMANDS_PER_BATCH, and@tallyui/coreand@tallyui/core/serverexport theBatchTooLargeBodytype. -
894b6ae: One catalogue reconcile runner replaces the id and fingerprint reconcile runners (#248, part A).
startIdReconcileandstartFingerprintReconcileremain as thin wrappers with their options and results.startCatalogueReconcile(new) compares the backend's product listing with the till in one pass and hands what differs to the collection's pull. It:- stays within a request budget (30 a minute by default) instead of a page cap, so large catalogues never truncate;
- keeps its daily gate and a resume cursor in RxDB local documents, so it does not run on every start, and it resumes after an interruption;
- deletes only in an uninterrupted pass, and only what the connector confirms gone. The mass-delete brake is checked on the candidates before the connector is asked, and the connector is asked in chunks (
confirmChunk, default 100), each within the budget; - stops or skips by
errorKind; - logs what it did through an optional
logcallback.
- Behaviour changes for the existing runners:
- there is no pass 5 s after every start: the daily gate is checked at the start delay and then hourly;
maxPagesis ignored;- requests are paced by the budget;
- differences are refetched page by page;
- apps that run more than one runner on the same collection pass a distinct
stateId.
createReconcileFeed:- it takes an optional
keyto match fetched documents by the local primary key, so it works wheredoc.idis not the primary key;fetchByIdsthen receives the queued entries; - a
tombstoneentry is deleted without a fetch; - a fetched document keeps a
_deletedthe connector set.
- it takes an optional
@tallyui/coreaddsCatalogueReconcileAdapterandTallyConnector.reconcile.catalogue.- Connector collections enable RxDB local documents.
-
04905ef:
Cataloguenow applies the provider's reconciled stock overlay itself (tiles, search and the variant chooser agree), anduseStockOverlaidanduseStockOverlayAsOfare new. -
faa7cda: Expose ConnectorUnauthorizedError for expired or rejected stored credentials in Vendure and Medusa requests.
-
9f34416:
@tallyui/core/serveradds the batch envelope check, the per-command version pre-check, the supported versions,totalWarningsandparseCommandResult(throwingCommandResultError), byte-identical to the medusapos plugin's. -
fb57e1d:
@tallyui/core/serveradds the register payload check (registerPayloadErrors), the expected-cash derivation (deriveSessionFigures,deriveVariance) and the register conflict codes (RegisterConflictCode,RegisterOutcome), byte-identical to the medusapos plugin's. -
898e98b: Add
@tallyui/core/server, the plugins' shared contract: money, fingerprint, theorder.createpayload shape and fiscal figures, byte-identical to the medusapos plugin's. -
75c5dce:
@tallyui/core/server's envelope types now admit register commands and any validated version, and it exportsBatchOutcomewithinProgressOutcomeandtransientOutcome. It also exportsCommandRejectionCodeandplatformErrorResult(a newplatform_errorcode). -
ba63f04:
CommandWarninggains{ code: 'customer_ignored'; customerId: string }(#266): a sale whosecustomerIddoesn't resolve is kept as a guest sale.knownWarningskeeps it andparseCommandResultaccepts it whencustomerIdis 1 to 64 characters; the orders list renders it. -
0d04d13: Add neutral Customer, CustomerInput and CustomerServiceError exports and optional online-only customer search, create and get connector methods.
Implement customer search, create and get for Medusa's admin-user connector.
-
78d324e: Add useSale.setCustomer and ReceiptData.header.customer, customerTraits, CustomerPicker, generic CustomerSelect/CustomerCard with traits overrides, CustomerForm.showAddress, and the receipt's customer line.
CustomerSelect rows are now pressable, so choosing a result works on the web (it previously did nothing).
-
7fee0c1: A WooCommerce product whose uuid changed in the store is replaced on the till in one pass (#331). The till delivers it under its new uuid and removes the old copy. Before this fix, it removed the old copy and dropped the new one, so the product was missing until the next daily check, and it was removed without the usual by-id check.
- The reconcile feed: when an entry that has a local copy is fetched back under a different primary key but the same remote id, the feed delivers that document as well as removing the old copy.
combinePullAdapterstakes an optionalkeyfor resolving duplicates across its sub-adapters. It defaults todoc.id, as before. WooCommerce passes the uuid (its primary key): two documents that share a store id, such as a product's new copy and its old copy's removal, must both reach the collection.
-
24b74fd:
CommandWarninggains{ code: 'figures_mismatch'; fields: Array<{ field: 'subtotalMinor' | 'taxMinor' | 'discountMinor' | (string & {}); tillMinor: number; serverMinor: number }> }(#257): one warning per sale listing each of the till's figures that differs from the server's own computation.parseCommandResultaccepts it whenfieldsis non-empty, eachfieldis one of the three names with none repeated, and each entry's two values are different safe integers.knownWarningsapplies the same rules but keeps a field name it doesn't know (any non-empty string), since a newer store may send one; the orders list renders it, an unknown field by its raw name. -
eb5a032: A
/tally/v1/infoanswer that says nothing about the store no longer means the default tax rounding (a follow-up to #339).- Unknown: a 2xx that is not JSON, and a
taxRoundingvalue that is present but malformed, now read as "unknown" (undefined), like a network failure or a 5xx. The till's store settings wait and retry instead of selling on a guessed rounding. - Unchanged: a 404 still means an older plugin (
orderCreate: 1, the default rounding), and so does a well-formed body with notaxRoundingkey. - Type change:
parseInfoCapabilitiesnow returnsServerCapabilities | undefined. It isundefinedwhen the body carries a malformedtaxRounding.
- Unknown: a 2xx that is not JSON, and a
-
54ee98a:
@tallyui/core/serveradds theinternal_errorrejection code andinternalErrorResult. -
27d736e:
CommandWarninggainsbridgeMinorontotal_mismatchand a newtax_rate_mismatchcode, and the till now ignores warning codes it doesn't know (knownWarnings), instead of showing them as a store total. -
e59ebec: Each line is taxed at its product's tax class, not the store's default (#288).
ProductTraitsgains an optionalgetTaxClass(doc, variantId?), the backend's tax class id, a key ofStoreSettings.taxRatesPpm.addProductandaddEntryToCartpass it toaddLinethrough the newAddLineInput.taxClass, which the tax context resolves; a connector without the accessor is unchanged (the default rate).TaxProvidertaxes a class with no rate at the default rate and warns once per class through the newtaxLogger. connector-vendure replicates each variant'staxCategory { id }and implements the accessor, and its store settings give every tax category with no enabled rate in the default zone an explicit 0 rate, as Vendure charges; its product schema goes to version 2, so the products collection is dropped and downloaded again on the first sync after the upgrade. -
2ecaa36: A replication adapter can set
pull.batchSize, and the Medusa connector pulls 500 products per page. -
901fa66: Add
order.createenvelope version 3, its display and tax-rate wire types, and the payload'ssessionIdand customer reference.At capability 3,
finalizeOrdercopies the receipt'sdisplayandtaxByRateinto the sale. Version 3 sends those figures and the sale's session (stamped or late) assessionId. Older orders keep their existing envelope version.Move
pos_ordersto schema version 3 with asessionIdindex and the optionalsentVersionanddowngradedFromfields (declared for the outbox's version fallback, not yet written). Apps must openpos_orderswithaddPosOrderCollection, which migrates it. -
bf2d805:
order.createversion 4 (#286): everydiscountMinor, the order's and each line's, is tax-exclusive, so their sum still holds. Core accepts version 4 with version 3's fields. The till's envelope builder (toOrderCreateEnvelope) produces version 4 when capped at 4 or more and the order'ssentVersiondoesn't hold it lower. The till doesn't send version 4 yet: the outbox doesn't pass the server's max, so it still sends version 3 or lower with the same figures, byte-identical. -
ca0beac: Fall back to the server's supported order.create version while preserving stored fiscal figures and command IDs. Record the sent version and downgrade in the order audit, and expose command error details.
-
af623c9: The order.create string lengths move from
payloadShapeErrorsto the newpayloadBoundErrors, whichprecheckCommandcalls after the replay lookup, so an applied order resent with a long title replays asduplicate;payloadShapeErrorskeeps the types, thecustomerIdandsessionIdbounds and the NUL check.useSaleapplies a tender before logging a dropped reference, andadd()refuses a product whose id or v3 tax code finalize would refuse; the tender's reference field caps at 255 characters.finalizeOrdernow freezes the sent form (names and discount labels cut, an unsendable customer email or id left out) andtoOrderCreateEnvelopesends the stored order unchanged, so every resend is byte-identical. -
ef2f64e: The shared order.create shape check bounds string lengths and refuses NUL, and so does the v3 fiscal-figures check for its display and tax-code strings. The till cuts long names when it stores the order, refuses over-long pass-through references at finalize (and a payment reference as it's entered), refuses a searched or parked customer whose email or id the server would refuse, and validates the customer email at entry. Tills should ship this clamp before plugins adopt the new bounds, so no till sends a sale the server would now refuse.
-
457162d: One reconcile feed per store session (#307, a release gate). The WooCommerce and Medusa reconcile feeds were module-level singletons, so after a store switch in one runtime, store A's queued tombstones and refetches could reach store B's database.
- New factories.
createWooCommerceConnector(),createMedusaConnector()andcreateMedusaAdminUserConnector()each build their own feed;createVendureConnector(options)already did. Build a connector per store session, anew on each sign-in or store change. - Deprecated exports.
woocommerceConnector,medusaConnector,medusaAdminUserConnectorandvendureConnectorare deprecated: one instance for the whole app can leak queued reconcile work across stores. They are removed in 4.0. - A development warning.
startReplicationwarns once when the same adapter object replicates into two collections at once. - Refetch budget by requests.
refetchBatchSizeon the reconcile adapters makes a page that enqueuesnrefetches takeceil(n / refetchBatchSize)request-budget slots (WooCommerce 100, Medusa 100, Vendure 1,000). - WooCommerce 426 errors. A foreign (non-WCPOS) 426 keeps the store's
codebeside its message, and the message is capped at 200 characters.
- New factories.
-
222543b: Add
RegisterCommandType,RegisterCommandEnvelopeandAnyCommandEnvelope, register payloads and results, and the register server capability.CommandTypeandCommandEnvelopeare unchanged.Record the local
register_commandsledger throughreconcileRegisterCommands, gated inuseRegisterSessionby its newcommandsandcapabilitiesoptions. Commands are recorded but not sent. Medusa reads theregistercontract. -
8141c1c: Guard register commands, movement reasons and register ids, exporting RegisterMovementReasonError and RegisterIdInvalidError. Harden register outbox result handling and batch limits.
Make CommandBatchRequest generic while preserving its existing default envelope type.
-
ce4f796: Replication pull errors are handled according to who can fix them, instead of every error being retried every 5 s forever. A till repeating a rejected token is the traffic a store's security plugin blocks.
@tallyui/core:- An error class declares
fixedBy: 'till' | 'store'with a stringcode;errorKind(error)returns'till','store'or'transient'. SyncNotice({ code, since, fixedBy, software?, minVersion?, fix? }) describes a stopped pull.ConnectorUnauthorizedErroris fixed by the till.
- An error class declares
@tallyui/databasestartReplicationhandles the three kinds and returns RxDB's state plusnotice$andresume():- till: one request, one notice, then the pull stays stopped until the app calls
resume(), after sign-in. The pull stays stopped even when RxDB restarts the loop on page visibility. - store: one notice, then one attempt every 5 minutes (or the error's
retryAfterMs, up to 1 hour). The notice clears itself on the first success, so a till recovers within 5 minutes of the owner's fix. - transient: a doubling delay from
retryTimeto 5 minutes. It waits at least a validretryAfterMs(a finite number of zero or more), capped at 1 hour.
- till: one request, one notice, then the pull stays stopped until the app calls
@tallyui/components:SyncStatustakes an optionalpullNoticeand tells the cashier in plain words that they can keep selling and who needs to act. It never shows a code, a backend name or a version the notice doesn't carry.@tallyui/connector-woocommerce:WooDateFilterErroris fixed by the store, and carriessoftwareandminVersion.WooMissingUuidErrorgainscode: 'missing_plugin'and is fixed by the store.
@tallyui/connector-vendure: a newVendureTimezoneConfigError(store_misconfigured, with a plainfix) replaces the plain error when theupdatedAtprobe shows a server that isn't in UTC.
-
5ed6281: The till computes tax with the store's rounding strategy (#287, ADR-071).
ServerCapabilitiesgainstaxRounding(core exportsTaxRounding):per_order,per_line_itemsorper_rate_group_items, each withhalf_away_from_zeroorhalf_up, orcustom. Absent, andcustom, mean today'sper_orderwith half away from zero.TaxProvidertakesroundingandrateCodes(tax class → the backend's rate name, forper_rate_group_items); the order records the strategy astaxRounding, andtaxLinesByRatetakes it as a fourth argument. The algorithms are indocs/contract/field-kinds.md. -
5a204a9:
StoreSettingsgains a derivedtaxRounding.useStoreSettingsfills it from the context's capabilities, or else from one read of the connector'scapabilities()made before the settings are ready, so each sign-in emits the settings once with the rounding known and no later change holds a sale; a failed read or a connector withoutcapabilitiesgives the default rounding.taxProviderPropspasses it toTaxProviderasrounding, so the till rounds tax like the store with no app code (#324).custompasses no rounding, and an explicitroundingprop still wins. -
7d1bc98: Add
parseTaxRoundingandparseInfoCapabilities, which read/tally/v1/infoincluding its top-leveltaxRounding(#287). The Medusa connector's capability read now carries the store'staxRounding. -
8cf3ea4: A till tells "sign in again" apart from "signed in, but not allowed" (found by the Medusa POS app's adoption).
ConnectorUnauthorizedError.statusis now required, typed401 | 403, and set by meaning at every connector.401: the credentials are not accepted, so sign in again.code: 'unauthorized', fixed by the till.403: the till is signed in but not allowed.code: 'forbidden', fixed by the store.- Vendure answers a signed-out session with 403 too. Its connector checks who is signed in first, so a confirmed sign-out is always
401.
- A 403 on the pull gives the
forbiddennotice, never a sign-out. The pull retries on the store schedule and clears by itself once the store owner grants the permission. SyncStatus shows "Products aren't updating: your account isn't allowed to do this on this store." with "You can keep selling. Ask the store owner." - The customer picker shows "Your account isn't allowed to do this on this store. Ask the store owner." for a 403, instead of asking the cashier to sign in again.
-
e15f389: The Vendure connector supplies its tax rate names, so a
per_rate_group_itemsstore groups a sale's tax the way Vendure does (#324). Apps no longer fetch the names themselves.StoreSettings.taxRateCodes(core, optional): the backend's tax rate name per tax class, keyed liketaxRatesPpm, includingdefault.vendureStoreSettingsreads each rate'snamein the tax-rate query it already runs, so no extra request is made. Only the ratestaxRatesPpmuses count, anddefaultfollows the same default-category rule.taxRateCodesis left out when no names come back.taxProviderProps(settings)passestaxRateCodesto<TaxProvider>asrateCodes.
-
ddd9e85: The WooCommerce catalogue reconcile uses the WCPOS products fast path (#313). When
wcpos/v2/statuslistsproducts_id_fast_pathincapabilities, the whole catalogue is listed in one request (per_page=-1,_fields=id,date_modified_gmt,stock_quantity,stock_status) instead of pages of 100. A store that refuses it, or answers with something that is not a list, is listed page by page in the same pass.- Keyed on the remote id: both WooCommerce listings key on the numeric product id, never the till-local uuid.
CatalogueReconcileAdapter.matchKey(core, optional): an adapter whose listing carries no primary key declares how to match a local document. The catalogue runner indexes the local documents by it for each pass. Deletion is unchanged:confirmGone, then the mass-delete brake, by primary key.remoteon the keyed reconcile feed (core, optional): a listed product the till does not hold yet is matched back by its remote id, so it is delivered rather than dropped.
Patch Changes
-
5c90aed:
parseCommandResultnow accepts atotal_mismatch'sbridgeMinorand thetax_rate_mismatchwarning code, so a plugin replaying a stored v3 result no longer fails.knownWarningsis lenient about a bad optionalbridgeMinor(dropping just that field, not the whole warning) and about a non-arraywarningsvalue.OrdersList's rounding line now reads "Store calculated …; a rounding line of … brought it to …".Cataloguekeeps its input array's identity when the stock overlay changes nothing, and takes the latest oflastStockCheckAt, the provider'sstockOverlayAsOfandlastSyncedAtfor its "stock as of" time. -
6673faf: RxDB 17.5.0.
@tallyui/storage-sqlite:- Its
rxdb-premiumpeer is now17.5.0. Apps installrxdb-premium@17.5.0together withrxdb@17.5.0. - Its storages set RxDB 17's premium flag at import and when called, so the 13-collection cap never applies.
- Its
@tallyui/pos:- Its
rxdbpeer is now~17.5.0. - Opening
pos_ordersrejects withPosOrderOpenClosedErrorwhen the database closes during a migration: RxDB 17.5.0 cancels the migration on close. The open first waits for any write already in flight, so none reaches a closed store. - An open that needs no migration resolves only once RxDB allows writes, so a sale saved straight after it is never refused with COL25.
- Its
@tallyui/database:createTallyDatabasereturns an RxDB 17 database.- In development it adds RxDB's dev-mode plugin when a database is created, not at import.
- Stored data: a till's SQLite data written by RxDB 16.21.1 opens unchanged under 17.5.0, and migrates its schema versions.
Upgrade notes
- Storage is one-way. Once a till has opened this version,
pos_ordersis at schema version 4, and an older build (such as@tallyui/pos2.0.0 on RxDB 16.21.1) opens it without an error but shows no orders, so it sends none of the pending ones until the till is upgraded again. Nothing is deleted: the next upgrade recovers every order, including a sale rung during the rollback. Never roll an app back across this version, and never re-ring sales it hides: a re-rung sale is a second sale, and the upgrade sends both. See ADR-069 indocs/DECISIONS.md. - Web apps ship the 17.5.0 storage worker with the 17.5.0 main thread. A cached 16.x worker with a 17.5.0 main thread is untested and unsupported.
- Apps pin
rxdbandrxdb-premiumto exactly17.5.0. - RxDB 17 defaults a replication's
toggleOnDocumentVisibleto true (16.21.1: false). It then resyncs when the tab becomes visible, and no longer simulates activity to keep a hidden tab awake, so a browser may throttle a hidden tab's pull. RxDB pauses a hidden tab's replication only when that tab isn't the leader; a single-instance database is always the leader (read in 17.5.0'splugins/replicationsource, not tested).
-
c48e1dd: Store settings follow-ups to #339 and #341 (#340):
- A signed-out till is asked to sign in, not shown "Retrying…". When the capabilities read fails with an error only the till can fix,
useStoreSettingsgiveserrorwithoutnextRetryAtand doesn't retry by itself, so the app prompts. That covers a till-class error (a 401, or a till that needs updating) and aSignInErrorwithcode: 'invalid_credentials'; any other sign-in error still waits and retries. Every other failure still waits and retries. - A
/tally/v1/infoJSON body that isn't an object (null, an array or a scalar) is unknown, not the default rounding.
- A signed-out till is asked to sign in, not shown "Retrying…". When the capabilities read fails with an error only the till can fix,
-
1f4d0ab:
isStorageWorkerStartErrorandisStorageWorkerFailurealso recognise RxDB's RM1, a stale storage worker built on another RxDB version (for example a cached old worker after an upgrade), so apps show their reload advice for it. Both call the newisRxdbRemoteVersionMismatchin@tallyui/core, which recognises RM1 by structure only: an RxError's owncode, or the remote storage'scould not create instancewrapping of an RxError's JSON.@tallyui/storage-sqlitenow has@tallyui/coreas a peer dependency. -
3cf5452: Follow-ups to the 401/403 split (#345):
- Vendure: a signed-in user missing a permission (confirmed by the session probe) is now
ConnectorUnauthorizedErrorwithstatus: 403, theforbiddennotice, instead of a plain transient error. - WooCommerce: a 403 from the JWT-auth plugin (
jwt_auth_*) reaches the till only with a valid token on WCPOS 1.10.0–1.10.7 (wcpos/woocommerce-pos#1863). It is nowWooPluginUpdateRequiredError(unsupported_store, WCPOS 1.10.8): the store owner updates WCPOS, and the till is never sent into a sign-in loop. ConnectorUnauthorizedError: only a 403 isforbidden. A caller that omitsstatusgetsunauthorized, as before 3.0.- Docs: the customer picker's
onError, the replication guide's error classes, and the connector comments now say that only a 401 means sign in again.
- Vendure: a signed-in user missing a permission (confirmed by the session probe) is now
@tallyui/database
Major Changes
-
6673faf: RxDB 17.5.0.
@tallyui/storage-sqlite:- Its
rxdb-premiumpeer is now17.5.0. Apps installrxdb-premium@17.5.0together withrxdb@17.5.0. - Its storages set RxDB 17's premium flag at import and when called, so the 13-collection cap never applies.
- Its
@tallyui/pos:- Its
rxdbpeer is now~17.5.0. - Opening
pos_ordersrejects withPosOrderOpenClosedErrorwhen the database closes during a migration: RxDB 17.5.0 cancels the migration on close. The open first waits for any write already in flight, so none reaches a closed store. - An open that needs no migration resolves only once RxDB allows writes, so a sale saved straight after it is never refused with COL25.
- Its
@tallyui/database:createTallyDatabasereturns an RxDB 17 database.- In development it adds RxDB's dev-mode plugin when a database is created, not at import.
- Stored data: a till's SQLite data written by RxDB 16.21.1 opens unchanged under 17.5.0, and migrates its schema versions.
Upgrade notes
- Storage is one-way. Once a till has opened this version,
pos_ordersis at schema version 4, and an older build (such as@tallyui/pos2.0.0 on RxDB 16.21.1) opens it without an error but shows no orders, so it sends none of the pending ones until the till is upgraded again. Nothing is deleted: the next upgrade recovers every order, including a sale rung during the rollback. Never roll an app back across this version, and never re-ring sales it hides: a re-rung sale is a second sale, and the upgrade sends both. See ADR-069 indocs/DECISIONS.md. - Web apps ship the 17.5.0 storage worker with the 17.5.0 main thread. A cached 16.x worker with a 17.5.0 main thread is untested and unsupported.
- Apps pin
rxdbandrxdb-premiumto exactly17.5.0. - RxDB 17 defaults a replication's
toggleOnDocumentVisibleto true (16.21.1: false). It then resyncs when the tab becomes visible, and no longer simulates activity to keep a hidden tab awake, so a browser may throttle a hidden tab's pull. RxDB pauses a hidden tab's replication only when that tab isn't the leader; a single-instance database is always the leader (read in 17.5.0'splugins/replicationsource, not tested).
Minor Changes
-
894b6ae: One catalogue reconcile runner replaces the id and fingerprint reconcile runners (#248, part A).
startIdReconcileandstartFingerprintReconcileremain as thin wrappers with their options and results.startCatalogueReconcile(new) compares the backend's product listing with the till in one pass and hands what differs to the collection's pull. It:- stays within a request budget (30 a minute by default) instead of a page cap, so large catalogues never truncate;
- keeps its daily gate and a resume cursor in RxDB local documents, so it does not run on every start, and it resumes after an interruption;
- deletes only in an uninterrupted pass, and only what the connector confirms gone. The mass-delete brake is checked on the candidates before the connector is asked, and the connector is asked in chunks (
confirmChunk, default 100), each within the budget; - stops or skips by
errorKind; - logs what it did through an optional
logcallback.
- Behaviour changes for the existing runners:
- there is no pass 5 s after every start: the daily gate is checked at the start delay and then hourly;
maxPagesis ignored;- requests are paced by the budget;
- differences are refetched page by page;
- apps that run more than one runner on the same collection pass a distinct
stateId.
createReconcileFeed:- it takes an optional
keyto match fetched documents by the local primary key, so it works wheredoc.idis not the primary key;fetchByIdsthen receives the queued entries; - a
tombstoneentry is deleted without a fetch; - a fetched document keeps a
_deletedthe connector set.
- it takes an optional
@tallyui/coreaddsCatalogueReconcileAdapterandTallyConnector.reconcile.catalogue.- Connector collections enable RxDB local documents.
-
2ecaa36: A replication adapter can set
pull.batchSize, and the Medusa connector pulls 500 products per page. -
e77d552: A reconcile result now says whether its pass was complete, so a till never shows a partial pass's "0 not sold" as current (found by the Medusa POS app's 3.0.0-next.0 adoption).
completeonCatalogueReconcileSummary,FingerprintReconcileResultandIdReconcileResult: true when the pass ran from its first page to its last in one go, sounlistedandunreportedare real counts. It is false for a resumed pass, whose counts are 0. For the fingerprint result, the pass must also have read at least one page.lastCompleteAton the runner's and the fingerprint wrapper's state: when the last complete pass finished. It is persisted with the runner's gate, and is available before the first pass after a restart.- A failed pass that finished no page is restarted rather than resumed. It re-reads from the first page anyway, so it now runs as a complete pass.
-
457162d: One reconcile feed per store session (#307, a release gate). The WooCommerce and Medusa reconcile feeds were module-level singletons, so after a store switch in one runtime, store A's queued tombstones and refetches could reach store B's database.
- New factories.
createWooCommerceConnector(),createMedusaConnector()andcreateMedusaAdminUserConnector()each build their own feed;createVendureConnector(options)already did. Build a connector per store session, anew on each sign-in or store change. - Deprecated exports.
woocommerceConnector,medusaConnector,medusaAdminUserConnectorandvendureConnectorare deprecated: one instance for the whole app can leak queued reconcile work across stores. They are removed in 4.0. - A development warning.
startReplicationwarns once when the same adapter object replicates into two collections at once. - Refetch budget by requests.
refetchBatchSizeon the reconcile adapters makes a page that enqueuesnrefetches takeceil(n / refetchBatchSize)request-budget slots (WooCommerce 100, Medusa 100, Vendure 1,000). - WooCommerce 426 errors. A foreign (non-WCPOS) 426 keeps the store's
codebeside its message, and the message is capped at 200 characters.
- New factories.
-
ce4f796: Replication pull errors are handled according to who can fix them, instead of every error being retried every 5 s forever. A till repeating a rejected token is the traffic a store's security plugin blocks.
@tallyui/core:- An error class declares
fixedBy: 'till' | 'store'with a stringcode;errorKind(error)returns'till','store'or'transient'. SyncNotice({ code, since, fixedBy, software?, minVersion?, fix? }) describes a stopped pull.ConnectorUnauthorizedErroris fixed by the till.
- An error class declares
@tallyui/databasestartReplicationhandles the three kinds and returns RxDB's state plusnotice$andresume():- till: one request, one notice, then the pull stays stopped until the app calls
resume(), after sign-in. The pull stays stopped even when RxDB restarts the loop on page visibility. - store: one notice, then one attempt every 5 minutes (or the error's
retryAfterMs, up to 1 hour). The notice clears itself on the first success, so a till recovers within 5 minutes of the owner's fix. - transient: a doubling delay from
retryTimeto 5 minutes. It waits at least a validretryAfterMs(a finite number of zero or more), capped at 1 hour.
- till: one request, one notice, then the pull stays stopped until the app calls
@tallyui/components:SyncStatustakes an optionalpullNoticeand tells the cashier in plain words that they can keep selling and who needs to act. It never shows a code, a backend name or a version the notice doesn't carry.@tallyui/connector-woocommerce:WooDateFilterErroris fixed by the store, and carriessoftwareandminVersion.WooMissingUuidErrorgainscode: 'missing_plugin'and is fixed by the store.
@tallyui/connector-vendure: a newVendureTimezoneConfigError(store_misconfigured, with a plainfix) replaces the plain error when theupdatedAtprobe shows a server that isn't in UTC.
-
ddd9e85: The WooCommerce catalogue reconcile uses the WCPOS products fast path (#313). When
wcpos/v2/statuslistsproducts_id_fast_pathincapabilities, the whole catalogue is listed in one request (per_page=-1,_fields=id,date_modified_gmt,stock_quantity,stock_status) instead of pages of 100. A store that refuses it, or answers with something that is not a list, is listed page by page in the same pass.- Keyed on the remote id: both WooCommerce listings key on the numeric product id, never the till-local uuid.
CatalogueReconcileAdapter.matchKey(core, optional): an adapter whose listing carries no primary key declares how to match a local document. The catalogue runner indexes the local documents by it for each pass. Deletion is unchanged:confirmGone, then the mass-delete brake, by primary key.remoteon the keyed reconcile feed (core, optional): a listed product the till does not hold yet is matched back by its remote id, so it is delivered rather than dropped.
Patch Changes
-
539d2ff: The stock, id and fingerprint reconciles read and write in bounded chunks, so app queries don't wait behind a whole pass.
-
d225c58: Internal
@tallyui/*peer dependencies are published as a caret range (for example^2.1.0) instead of an exact version. The packages still release together at one version. -
6f83dc5: The catalogue reconcile's mass-delete brake now explains itself in plain words. Its
keptevent withreason: 'brake'carries amessagea till can show to the store owner, for example: "12 products the online store no longer lists were kept on this till: removing that many at once needs a check. If they were hidden or removed on purpose, the person who manages this till can allow the removal." The console warning uses the same words, plus a hint for developers (allowMassDelete: true).The WooCommerce tests now model WCPOS's "POS only products" setting, and pin that a product hidden from the POS after sync is removed from the till by the next reconcile pass, while a bulk hide is held by the brake.
-
1223353: A stale duplicate copy of a store product no longer stays on the till for good (#369). When two local products share one store id, the catalogue check now makes the copy its index did not pick a deletion candidate, and logs a
duplicateevent with the codeduplicate_match_key. The copy is tombstoned only whenconfirmGoneproves the store doesn't back it, and the mass-delete brake still applies. WooCommerce'sconfirmGonenow also confirms a local whose id is live but whose uuid isn't the store's for that id. The store listing is the source of truth, and a later pull restores anything the store still backs. -
a8b58a4: The catalogue and fingerprint reconciles keep
lastCompleteAthonest (#338).- The fingerprint reconcile's
state$moveslastCompleteAtonly on a pass whose result is complete. A pass that read no pages, which givescomplete: false, no longer stamps it. - A new runner still shows the persisted value before its first pass.
- The catalogue runner's start-up load now updates
lastCompleteAtonly when the stored value is newer, so a load that resolves after a pass has completed can't roll it back.
- The fingerprint reconcile's
-
20b6321: A catalogue pass that yields no pages at all no longer counts (#368). It is not complete, it moves neither
lastCompleteAtnor the schedule's last completed time, and the gate runs it again at the next check. A page with no entries still counts: that is an adapter reporting an empty catalogue. Before this, a restarted fingerprint reconcile could show a zero-page pass's time as its last complete one. -
1f4d0ab:
isStorageWorkerStartErrorandisStorageWorkerFailurealso recognise RxDB's RM1, a stale storage worker built on another RxDB version (for example a cached old worker after an upgrade), so apps show their reload advice for it. Both call the newisRxdbRemoteVersionMismatchin@tallyui/core, which recognises RM1 by structure only: an RxError's owncode, or the remote storage'scould not create instancewrapping of an RxError's JSON.@tallyui/storage-sqlitenow has@tallyui/coreas a peer dependency. -
8cf3ea4: A till tells "sign in again" apart from "signed in, but not allowed" (found by the Medusa POS app's adoption).
ConnectorUnauthorizedError.statusis now required, typed401 | 403, and set by meaning at every connector.401: the credentials are not accepted, so sign in again.code: 'unauthorized', fixed by the till.403: the till is signed in but not allowed.code: 'forbidden', fixed by the store.- Vendure answers a signed-out session with 403 too. Its connector checks who is signed in first, so a confirmed sign-out is always
401.
- A 403 on the pull gives the
forbiddennotice, never a sign-out. The pull retries on the store schedule and clears by itself once the store owner grants the permission. SyncStatus shows "Products aren't updating: your account isn't allowed to do this on this store." with "You can keep selling. Ask the store owner." - The customer picker shows "Your account isn't allowed to do this on this store. Ask the store owner." for a 403, instead of asking the cashier to sign in again.
-
136343c: The WooCommerce connector gets a daily reconciliation pass (#248, part B), a safety net for edits the incremental pull can miss: the spring-forward hour, an over-excluding filter, a same-second edit, a shift without
X-WP-Total, trashed or unpublished products, and stock written without a modified-time bump.reconcile.cataloguelists the published catalogue with no date filter, comparing date, stock quantity and stock status. It re-reads deletion candidates by id and removes only those that are gone, trashed or unpublished. Everything else it re-pulls through the collection's own pull.replication.productsnow combines the product pull with the reconcile feed, withlegacyKey: 'products', so existing installs keep their checkpoint.- A product the store cannot be asked about (no numeric id) is never deleted.
- The WCPOS bulk-ID fast path is read from
wcpos/v2/statuscapabilities(products_id_fast_path). It stays dormant until wcpos/woocommerce-pos#2113 ships. @tallyui/database: the catalogue runner's gate check has a 60-second floor, so a bad interval can no longer re-arm it on every tick.
@tallyui/components
Minor Changes
-
04905ef:
Cataloguenow applies the provider's reconciled stock overlay itself (tiles, search and the variant chooser agree), anduseStockOverlaidanduseStockOverlayAsOfare new. -
78d324e: Add useSale.setCustomer and ReceiptData.header.customer, customerTraits, CustomerPicker, generic CustomerSelect/CustomerCard with traits overrides, CustomerForm.showAddress, and the receipt's customer line.
CustomerSelect rows are now pressable, so choosing a result works on the web (it previously did nothing).
-
130d28e: A store that keeps answering 404 is no longer silent. After 3 consecutive 404 answers the order and register outboxes set
OutboxState.backendMissing: { since }(when the first of them arrived), andSyncStatustells the cashier in plain words that sales aren't reaching the online store and are saved on the till, with a detail line for the store owner, instead of showingretrying (status_404); the stuck line shows the same words, with no raw code.SyncStatustakes an optionalpluginName(default'the POS plugin') for that detail. The outboxes keep retrying on their normal backoff, and orders stay pending, so a 404 during a deploy blip recovers on its own. The next answer that isn't a 404 clears it; an offline (network) retry changes nothing. Pass onecreateBackendNotFound()tracker asbackendNotFoundto bothcreateOrderOutboxandcreateRegisterOutboxso their 404s count together and the notice shows once, whichever outbox meets it first; without it, each outbox keeps its own. -
9e1032f: One order the store keeps failing no longer stops every later sale. After 5 server-answered failures (offline never counts) the order outbox probes the pending queue one order per backoff interval, oldest first; once the store takes one, the orders whose probes failed are isolated and retried alone, and batching resumes. If no probe gets through, the store is down: nothing is isolated. Retries alternate between the batch (or the probe) and one due isolated order, one request per interval, so neither can starve the other, and isolated orders take turns. An order the store has kept failing for 15 minutes of answered time, on its own clock, is flagged as stuck and stays pending:
OutboxState.stuck, with a per-order entry instuck.orders,useOrderOutbox'sstuckCommandIds,needsAttention'sstuckCommandIdsoption,OrdersList'sstuckprop (each order with its own time and reason), andSyncStatus's "Not syncing" line. An offline failure pauses every clock, and the store's next answer of any kind resumes them all; a flagged order stays flagged through an offline spell, since a paused clock keeps its answered time. The HTTP transport now reports a request that got no answer in time astimeout, which counts like a 503 and never pauses a clock, and keepsnetworkfor a store it could not reach. -
a9cdfc0: Migrate pos_orders to version 4 with optional localWarnings and serverFailures. Record omitted customer details and dropped payment references on the stored order, and show these warnings in the orders list; serverFailures is declared for the next outbox update.
-
c92e96e: ProductGrid renders a virtualized FlatList, with its props unchanged.
-
ce4f796: Replication pull errors are handled according to who can fix them, instead of every error being retried every 5 s forever. A till repeating a rejected token is the traffic a store's security plugin blocks.
@tallyui/core:- An error class declares
fixedBy: 'till' | 'store'with a stringcode;errorKind(error)returns'till','store'or'transient'. SyncNotice({ code, since, fixedBy, software?, minVersion?, fix? }) describes a stopped pull.ConnectorUnauthorizedErroris fixed by the till.
- An error class declares
@tallyui/databasestartReplicationhandles the three kinds and returns RxDB's state plusnotice$andresume():- till: one request, one notice, then the pull stays stopped until the app calls
resume(), after sign-in. The pull stays stopped even when RxDB restarts the loop on page visibility. - store: one notice, then one attempt every 5 minutes (or the error's
retryAfterMs, up to 1 hour). The notice clears itself on the first success, so a till recovers within 5 minutes of the owner's fix. - transient: a doubling delay from
retryTimeto 5 minutes. It waits at least a validretryAfterMs(a finite number of zero or more), capped at 1 hour.
- till: one request, one notice, then the pull stays stopped until the app calls
@tallyui/components:SyncStatustakes an optionalpullNoticeand tells the cashier in plain words that they can keep selling and who needs to act. It never shows a code, a backend name or a version the notice doesn't carry.@tallyui/connector-woocommerce:WooDateFilterErroris fixed by the store, and carriessoftwareandminVersion.WooMissingUuidErrorgainscode: 'missing_plugin'and is fixed by the store.
@tallyui/connector-vendure: a newVendureTimezoneConfigError(store_misconfigured, with a plainfix) replaces the plain error when theupdatedAtprobe shows a server that isn't in UTC.
-
a94b255:
OutboxStategainsrejected?: number: the order outbox publishes the count ofpos_ordersthe store refused (syncStatus: 'rejected') wherever it publishespending, so it rises when a batch result rejects an order and falls whenrequeue()sends one again. The register outbox leaves it unset. Whilerejectedis above 0,SyncStatusnever says "Sales are up to date.": its whole status line (label, polite live region and iOS announcement) is "1 sale needs attention · The online store refused it. Ask the store owner to look at the till's sync log." or "{n} sales need attention · The online store refused them. Ask the store owner to look at the till's sync log.", in place of the stuck and backend-missing sentences and the sending or retrying line. What else waits stays in the count: "1 sale needs attention, 5 waiting to sync · …" with other sales pending, "…, 2 till updates waiting to sync · …" with till updates, and "…, 5 sales and 2 till updates waiting to sync · …" with both. Below it, "Refused sales stay on this till under Needs attention, each with what to do next.", then the backend-missing detail when the store is missing. Withrejected0 or unset, nothing changes. When the store refused a whole batch (refusedset, no sale carrying a code), the status line is the waiting count followed by " · The online store refused the last send. This till will try again with the next sale, or when the app is reopened." in place of the stuck or backend-missing sentence, and the sending or retrying line (which read "Retrying in 0 s.") is hidden; refused sales still outrank it. With only till updates waiting and the register outbox refused, it ends "…try again with the next till update." instead; with a sale waiting, the sales line wins. -
df80ead:
SyncStatustakes an optionalregisterState(the register outbox's state): waiting till updates are counted ("1 till update waiting to sync", or named beside the sales), so it never says the sales are up to date while any wait, and with no sale waiting the backend-missing sentence says till updates aren't reaching the online store, or, onceregisterState.stuckis set, "Till updates haven't reached the online store since {time}. …". The backend-missing detail now reads "This till couldn't find {pluginName} on the online store. …". With nothing waiting, the line is only "Sales are up to date." (it replaces "All sales synced"), with no sending, retrying or problem text after it; if the store is missing, the detail reads "This till couldn't find {pluginName} on the online store the last time it checked. …". The status line's accessibility label is the whole visible line instead of "Sync status". The order and register outboxes let go of a sharedbackendNotFoundtracker onstop()and take it up again onstart()orflush(). -
8cf3ea4: A till tells "sign in again" apart from "signed in, but not allowed" (found by the Medusa POS app's adoption).
ConnectorUnauthorizedError.statusis now required, typed401 | 403, and set by meaning at every connector.401: the credentials are not accepted, so sign in again.code: 'unauthorized', fixed by the till.403: the till is signed in but not allowed.code: 'forbidden', fixed by the store.- Vendure answers a signed-out session with 403 too. Its connector checks who is signed in first, so a confirmed sign-out is always
401.
- A 403 on the pull gives the
forbiddennotice, never a sign-out. The pull retries on the store schedule and clears by itself once the store owner grants the permission. SyncStatus shows "Products aren't updating: your account isn't allowed to do this on this store." with "You can keep selling. Ask the store owner." - The customer picker shows "Your account isn't allowed to do this on this store. Ask the store owner." for a 403, instead of asking the cashier to sign in again.
Patch Changes
-
ba63f04:
CommandWarninggains{ code: 'customer_ignored'; customerId: string }(#266): a sale whosecustomerIddoesn't resolve is kept as a guest sale.knownWarningskeeps it andparseCommandResultaccepts it whencustomerIdis 1 to 64 characters; the orders list renders it. -
24b74fd:
CommandWarninggains{ code: 'figures_mismatch'; fields: Array<{ field: 'subtotalMinor' | 'taxMinor' | 'discountMinor' | (string & {}); tillMinor: number; serverMinor: number }> }(#257): one warning per sale listing each of the till's figures that differs from the server's own computation.parseCommandResultaccepts it whenfieldsis non-empty, eachfieldis one of the three names with none repeated, and each entry's two values are different safe integers.knownWarningsapplies the same rules but keeps a field name it doesn't know (any non-empty string), since a newer store may send one; the orders list renders it, an unknown field by its raw name. -
d6a5073: The outbox freezes an order an older till stored before it first sends it (
freezeSentForm, whichfinalizeOrderuses too):- line names, discount labels and payment references are cut to 255 characters, but ids never are;
- a customer email or id that
order.createwould refuse is left out; - the frozen form is written back, so the receipt and the store see the same bytes.
So an order stored before the upgrade and still unsent is never refused as
invalid_payload.New type:
SentOrder, anOrderwhose customer id may be missing. The receipt stage,buildReceiptDataandReceipttake it, and a plainOrderstill fits. -
27d736e:
CommandWarninggainsbridgeMinorontotal_mismatchand a newtax_rate_mismatchcode, and the till now ignores warning codes it doesn't know (knownWarnings), instead of showing them as a store total. -
fd882bc: The stuck line says "no answer from the store" for a timeout.
-
eb203b4: Review follow-ups with no behaviour change (#356, #358):
SyncStatusandOrdersListbuild their "since {time}" and "since about {time}" text with one shared helper.useRegisterOutbox's docs now say whentransport()is called, and that the latestisEnabledandonResultare used without restarting the outbox.
-
af623c9: The order.create string lengths move from
payloadShapeErrorsto the newpayloadBoundErrors, whichprecheckCommandcalls after the replay lookup, so an applied order resent with a long title replays asduplicate;payloadShapeErrorskeeps the types, thecustomerIdandsessionIdbounds and the NUL check.useSaleapplies a tender before logging a dropped reference, andadd()refuses a product whose id or v3 tax code finalize would refuse; the tender's reference field caps at 255 characters.finalizeOrdernow freezes the sent form (names and discount labels cut, an unsendable customer email or id left out) andtoOrderCreateEnvelopesends the stored order unchanged, so every resend is byte-identical. -
ef2f64e: The shared order.create shape check bounds string lengths and refuses NUL, and so does the v3 fiscal-figures check for its display and tax-code strings. The till cuts long names when it stores the order, refuses over-long pass-through references at finalize (and a payment reference as it's entered), refuses a searched or parked customer whose email or id the server would refuse, and validates the customer email at entry. Tills should ship this clamp before plugins adopt the new bounds, so no till sends a sale the server would now refuse.
-
d225c58: Internal
@tallyui/*peer dependencies are published as a caret range (for example^2.1.0) instead of an exact version. The packages still release together at one version. -
6bbd1ba: Each sale records the tax rounding its figures were computed with (#287):
finalizeOrderwritestaxRoundingon the stored order, the default (per_order,half_away_from_zero) included, andcustomas{ granularity: 'custom' }. It is the till's own record and is never sent inorder.create. The Z report splits each sale's tax by rate with the strategy that sale recorded, so its rows are the receipts' rows, and itsbreakdowns.tax_rounding_mixedistruewhen a session's sales used more than one strategy (acustomsale counts as the default it applied);ClosureSheetthen says so, andbuildClosureDocumentcarries the same line ready to print asclosure.tax_rounding_note(TAX_ROUNDING_MIXED_NOTE), for the apps' closure templates.PosOrder.taxRoundingis now required in the type.pos_ordersmoves to schema version 6:taxRoundingis required, and the migration records the default on every older sale, the only rounding any earlier build used. Like version 5, this storage is one-way: an older build opens it but shows no orders, so never roll an app back across it (ADR-069). Before 3.0.0 ships, #242's OPFS upgrade proof is rerun against version 6. -
5c90aed:
parseCommandResultnow accepts atotal_mismatch'sbridgeMinorand thetax_rate_mismatchwarning code, so a plugin replaying a stored v3 result no longer fails.knownWarningsis lenient about a bad optionalbridgeMinor(dropping just that field, not the whole warning) and about a non-arraywarningsvalue.OrdersList's rounding line now reads "Store calculated …; a rounding line of … brought it to …".Cataloguekeeps its input array's identity when the stock overlay changes nothing, and takes the latest oflastStockCheckAt, the provider'sstockOverlayAsOfandlastSyncedAtfor its "stock as of" time. -
c00e1ea:
OrdersListshows a rejected sale's refusal in the cashier's words, one sentence per error code (#269), in both Needs attention and Recent, and never the store's own message. An unknown code, or a rejected sale with no error, showsplatform_error's sentence: "The online store refused this sale. Ask the store owner to look at the till's sync log." Anidempotency_mismatchshows its sentence ("… Don't send it again; ask the store owner to compare the two.") in place of the old "This sale needs checking against the store before it can be sent again." line, and still has no Retry. The order outbox logs every refusal once to the sync log as "Order refused by the store" with the order id, the code and the store's message: a warning, or an error forunsupported_version(whose log previously used the message itself as its text). -
6673faf: RxDB 17.5.0.
@tallyui/storage-sqlite:- Its
rxdb-premiumpeer is now17.5.0. Apps installrxdb-premium@17.5.0together withrxdb@17.5.0. - Its storages set RxDB 17's premium flag at import and when called, so the 13-collection cap never applies.
- Its
@tallyui/pos:- Its
rxdbpeer is now~17.5.0. - Opening
pos_ordersrejects withPosOrderOpenClosedErrorwhen the database closes during a migration: RxDB 17.5.0 cancels the migration on close. The open first waits for any write already in flight, so none reaches a closed store. - An open that needs no migration resolves only once RxDB allows writes, so a sale saved straight after it is never refused with COL25.
- Its
@tallyui/database:createTallyDatabasereturns an RxDB 17 database.- In development it adds RxDB's dev-mode plugin when a database is created, not at import.
- Stored data: a till's SQLite data written by RxDB 16.21.1 opens unchanged under 17.5.0, and migrates its schema versions.
Upgrade notes
- Storage is one-way. Once a till has opened this version,
pos_ordersis at schema version 4, and an older build (such as@tallyui/pos2.0.0 on RxDB 16.21.1) opens it without an error but shows no orders, so it sends none of the pending ones until the till is upgraded again. Nothing is deleted: the next upgrade recovers every order, including a sale rung during the rollback. Never roll an app back across this version, and never re-ring sales it hides: a re-rung sale is a second sale, and the upgrade sends both. See ADR-069 indocs/DECISIONS.md. - Web apps ship the 17.5.0 storage worker with the 17.5.0 main thread. A cached 16.x worker with a 17.5.0 main thread is untested and unsupported.
- Apps pin
rxdbandrxdb-premiumto exactly17.5.0. - RxDB 17 defaults a replication's
toggleOnDocumentVisibleto true (16.21.1: false). It then resyncs when the tab becomes visible, and no longer simulates activity to keep a hidden tab awake, so a browser may throttle a hidden tab's pull. RxDB pauses a hidden tab's replication only when that tab isn't the leader; a single-instance database is always the leader (read in 17.5.0'splugins/replicationsource, not tested).
-
0e4c9cc: The "since" a cashier reads is a real time (#253).
OutboxState.stuck(both outboxes) gainsfirstFailedAt: the wall-clock time the first failure of the current stuck run was answered, so an offline gap no longer moves it.sincekeeps its meaning, the clock's virtual start, and still drives the 15-minute threshold.firstFailedAtis kept in memory only. After a restart it is absent, andSyncStatusandOrdersListshow the stored time instead, worded "since about 2:49 AM". -
d6079b4:
SyncStatusinserts the plugin name and times literally (a$&,$1or$$in them is kept as written), and with only till updates waiting shows the register outbox's sending and retrying text and countdown. It shows no raw reason code: a stuck order now shows the "Sales haven't reached the online store since {time}." sentence (for till updates alone, "Till updates haven't …"), store missing or not. Sending and retrying are a short line of their own below the status line: "Sending…" or "Retrying in {n} s.". The status line and each pull-notice line are polite live regions (aria-live="polite"on web,accessibilityLiveRegionon Android) and on iOS are announced when their text changes, both in one announcement when they change together. What is announced is only the substance (counts, the sentence, the notice): the sending or retrying line is outside any live region, so it is never announced.OrdersListshows no reason code either: a stuck order reads "Hasn't reached the online store since {time}." with the hour numeric, and a rejected order shows the store's message alone (nothing when it has none), never its error code. -
e51f1b7: Times shown to a cashier no longer force a leading zero on the hour (#252):
ProductStockBadge's "as of" time and the catalogue's time label now read "2:49 AM", not "02:49 AM", on a 12-hour clock, likeSyncStatusand the orders list. -
3cf5452: Follow-ups to the 401/403 split (#345):
- Vendure: a signed-in user missing a permission (confirmed by the session probe) is now
ConnectorUnauthorizedErrorwithstatus: 403, theforbiddennotice, instead of a plain transient error. - WooCommerce: a 403 from the JWT-auth plugin (
jwt_auth_*) reaches the till only with a valid token on WCPOS 1.10.0–1.10.7 (wcpos/woocommerce-pos#1863). It is nowWooPluginUpdateRequiredError(unsupported_store, WCPOS 1.10.8): the store owner updates WCPOS, and the till is never sent into a sign-in loop. ConnectorUnauthorizedError: only a 403 isforbidden. A caller that omitsstatusgetsunauthorized, as before 3.0.- Docs: the customer picker's
onError, the replication guide's error classes, and the connector comments now say that only a 401 means sign in again.
- Vendure: a signed-in user missing a permission (confirmed by the session probe) is now
-
6278d8d: The register outbox can start when its store opens, as the order outbox does (#290). The new
useRegisterOutbox({ commands, transport, deviceId, isEnabled?, onResult?, backendNotFound? })runscreateRegisterOutboxover an already-openregister_commandscollection, and callsstart()so that till updates left pending (after a refused batch, for instance) go out when the app reopens. It returns{ state, flush }. A new collection or device id restarts the outbox, andcommands: nullleaves it idle. Apps that create the register outbox themselves should switch to this hook.SyncStatus's till-updates refusal line now ends "…with the next till update, or when the app is reopened.", matching the sales line. -
cbf26fd: The WooCommerce connector sends WCPOS's protocol signal, so a WCPOS 2.0 store does not refuse it (#296). Every request carries
X-WCPOS-Protocol: 2andX-WCPOS-Client: tallyui/<connector version>. WCPOS's 2.0 gate refuses POS-markedwcpos/v2requests without protocol 2, and protocol 2 is a pure declaration the connector already conforms to. The headers are harmless on WCPOS 1.x.If a store still answers 426 (
wcpos_update_required), the newWooTillUpdateRequiredError(till_update_required, fixed by the till) stops the product pull after one request.SyncStatusthen tells the cashier: "Products aren't updating: this till needs updating."
@tallyui/storage-sqlite
Major Changes
-
6673faf: RxDB 17.5.0.
@tallyui/storage-sqlite:- Its
rxdb-premiumpeer is now17.5.0. Apps installrxdb-premium@17.5.0together withrxdb@17.5.0. - Its storages set RxDB 17's premium flag at import and when called, so the 13-collection cap never applies.
- Its
@tallyui/pos:- Its
rxdbpeer is now~17.5.0. - Opening
pos_ordersrejects withPosOrderOpenClosedErrorwhen the database closes during a migration: RxDB 17.5.0 cancels the migration on close. The open first waits for any write already in flight, so none reaches a closed store. - An open that needs no migration resolves only once RxDB allows writes, so a sale saved straight after it is never refused with COL25.
- Its
@tallyui/database:createTallyDatabasereturns an RxDB 17 database.- In development it adds RxDB's dev-mode plugin when a database is created, not at import.
- Stored data: a till's SQLite data written by RxDB 16.21.1 opens unchanged under 17.5.0, and migrates its schema versions.
Upgrade notes
- Storage is one-way. Once a till has opened this version,
pos_ordersis at schema version 4, and an older build (such as@tallyui/pos2.0.0 on RxDB 16.21.1) opens it without an error but shows no orders, so it sends none of the pending ones until the till is upgraded again. Nothing is deleted: the next upgrade recovers every order, including a sale rung during the rollback. Never roll an app back across this version, and never re-ring sales it hides: a re-rung sale is a second sale, and the upgrade sends both. See ADR-069 indocs/DECISIONS.md. - Web apps ship the 17.5.0 storage worker with the 17.5.0 main thread. A cached 16.x worker with a 17.5.0 main thread is untested and unsupported.
- Apps pin
rxdbandrxdb-premiumto exactly17.5.0. - RxDB 17 defaults a replication's
toggleOnDocumentVisibleto true (16.21.1: false). It then resyncs when the tab becomes visible, and no longer simulates activity to keep a hidden tab awake, so a browser may throttle a hidden tab's pull. RxDB pauses a hidden tab's replication only when that tab isn't the leader; a single-instance database is always the leader (read in 17.5.0'splugins/replicationsource, not tested).
Minor Changes
- 3332558: Tell the three start failures apart (#293), each with its own sentence for the cashier. Storage unavailable, as in a Safari private window where the browser gives the worker no usable OPFS, is the new
StorageUnavailableError, recognised withisStorageUnavailableError. Another tab holding the database is recognised with the newisStorageHeldError. A stale worker staysisRxdbRemoteVersionMismatchfrom@tallyui/core(RM1).isStorageWorkerStartErrorstill means any failed start except storage unavailable, and every start error carries its cause's name and message in its own message.
Patch Changes
- 1f4d0ab:
isStorageWorkerStartErrorandisStorageWorkerFailurealso recognise RxDB's RM1, a stale storage worker built on another RxDB version (for example a cached old worker after an upgrade), so apps show their reload advice for it. Both call the newisRxdbRemoteVersionMismatchin@tallyui/core, which recognises RM1 by structure only: an RxError's owncode, or the remote storage'scould not create instancewrapping of an RxError's JSON.@tallyui/storage-sqlitenow has@tallyui/coreas a peer dependency. - f96d185: Make the SQLite handle type the minimal interface used by the adapter, accepting expo-sqlite 16's
SQLiteDatabase.
@tallyui/pos
Major Changes
-
6673faf: RxDB 17.5.0.
@tallyui/storage-sqlite:- Its
rxdb-premiumpeer is now17.5.0. Apps installrxdb-premium@17.5.0together withrxdb@17.5.0. - Its storages set RxDB 17's premium flag at import and when called, so the 13-collection cap never applies.
- Its
@tallyui/pos:- Its
rxdbpeer is now~17.5.0. - Opening
pos_ordersrejects withPosOrderOpenClosedErrorwhen the database closes during a migration: RxDB 17.5.0 cancels the migration on close. The open first waits for any write already in flight, so none reaches a closed store. - An open that needs no migration resolves only once RxDB allows writes, so a sale saved straight after it is never refused with COL25.
- Its
@tallyui/database:createTallyDatabasereturns an RxDB 17 database.- In development it adds RxDB's dev-mode plugin when a database is created, not at import.
- Stored data: a till's SQLite data written by RxDB 16.21.1 opens unchanged under 17.5.0, and migrates its schema versions.
Upgrade notes
- Storage is one-way. Once a till has opened this version,
pos_ordersis at schema version 4, and an older build (such as@tallyui/pos2.0.0 on RxDB 16.21.1) opens it without an error but shows no orders, so it sends none of the pending ones until the till is upgraded again. Nothing is deleted: the next upgrade recovers every order, including a sale rung during the rollback. Never roll an app back across this version, and never re-ring sales it hides: a re-rung sale is a second sale, and the upgrade sends both. See ADR-069 indocs/DECISIONS.md. - Web apps ship the 17.5.0 storage worker with the 17.5.0 main thread. A cached 16.x worker with a 17.5.0 main thread is untested and unsupported.
- Apps pin
rxdbandrxdb-premiumto exactly17.5.0. - RxDB 17 defaults a replication's
toggleOnDocumentVisibleto true (16.21.1: false). It then resyncs when the tab becomes visible, and no longer simulates activity to keep a hidden tab awake, so a browser may throttle a hidden tab's pull. RxDB pauses a hidden tab's replication only when that tab isn't the leader; a single-instance database is always the leader (read in 17.5.0'splugins/replicationsource, not tested).
Minor Changes
-
78d324e: Add useSale.setCustomer and ReceiptData.header.customer, customerTraits, CustomerPicker, generic CustomerSelect/CustomerCard with traits overrides, CustomerForm.showAddress, and the receipt's customer line.
CustomerSelect rows are now pressable, so choosing a result works on the web (it previously did nothing).
-
901fa66: Add
order.createenvelope version 3, its display and tax-rate wire types, and the payload'ssessionIdand customer reference.At capability 3,
finalizeOrdercopies the receipt'sdisplayandtaxByRateinto the sale. Version 3 sends those figures and the sale's session (stamped or late) assessionId. Older orders keep their existing envelope version.Move
pos_ordersto schema version 3 with asessionIdindex and the optionalsentVersionanddowngradedFromfields (declared for the outbox's version fallback, not yet written). Apps must openpos_orderswithaddPosOrderCollection, which migrates it. -
bf2d805:
order.createversion 4 (#286): everydiscountMinor, the order's and each line's, is tax-exclusive, so their sum still holds. Core accepts version 4 with version 3's fields. The till's envelope builder (toOrderCreateEnvelope) produces version 4 when capped at 4 or more and the order'ssentVersiondoesn't hold it lower. The till doesn't send version 4 yet: the outbox doesn't pass the server's max, so it still sends version 3 or lower with the same figures, byte-identical. -
130d28e: A store that keeps answering 404 is no longer silent. After 3 consecutive 404 answers the order and register outboxes set
OutboxState.backendMissing: { since }(when the first of them arrived), andSyncStatustells the cashier in plain words that sales aren't reaching the online store and are saved on the till, with a detail line for the store owner, instead of showingretrying (status_404); the stuck line shows the same words, with no raw code.SyncStatustakes an optionalpluginName(default'the POS plugin') for that detail. The outboxes keep retrying on their normal backoff, and orders stay pending, so a 404 during a deploy blip recovers on its own. The next answer that isn't a 404 clears it; an offline (network) retry changes nothing. Pass onecreateBackendNotFound()tracker asbackendNotFoundto bothcreateOrderOutboxandcreateRegisterOutboxso their 404s count together and the notice shows once, whichever outbox meets it first; without it, each outbox keeps its own. -
9e1032f: One order the store keeps failing no longer stops every later sale. After 5 server-answered failures (offline never counts) the order outbox probes the pending queue one order per backoff interval, oldest first; once the store takes one, the orders whose probes failed are isolated and retried alone, and batching resumes. If no probe gets through, the store is down: nothing is isolated. Retries alternate between the batch (or the probe) and one due isolated order, one request per interval, so neither can starve the other, and isolated orders take turns. An order the store has kept failing for 15 minutes of answered time, on its own clock, is flagged as stuck and stays pending:
OutboxState.stuck, with a per-order entry instuck.orders,useOrderOutbox'sstuckCommandIds,needsAttention'sstuckCommandIdsoption,OrdersList'sstuckprop (each order with its own time and reason), andSyncStatus's "Not syncing" line. An offline failure pauses every clock, and the store's next answer of any kind resumes them all; a flagged order stays flagged through an offline spell, since a paused clock keeps its answered time. The HTTP transport now reports a request that got no answer in time astimeout, which counts like a 503 and never pauses a clock, and keepsnetworkfor a store it could not reach. -
ca0beac: Fall back to the server's supported order.create version while preserving stored fiscal figures and command IDs. Record the sent version and downgrade in the order audit, and expose command error details.
-
a9cdfc0: Migrate pos_orders to version 4 with optional localWarnings and serverFailures. Record omitted customer details and dropped payment references on the stored order, and show these warnings in the orders list; serverFailures is declared for the next outbox update.
-
9ffa7c0: The till sends
order.createversion 4 (#286) to a server that advertises 4. Each order is resent at the version it first went out at: the outbox recordssentVersionbefore an order's first send, so a retry after the store upgrades is byte-identical. With the server's max unknown, the till sends at most 3 and records that.requeue()clearssentVersionanddowngradedFrom, since the newcommandIdchooses afresh.pos_ordersmoves to schema version 5:sentVersionanddowngradedFromaccept 1 to 4, and the migration records each order without asentVersionat its content version (3 withdisplayandtaxByRate, else 2 when discounted, else 1). Like version 4, this storage is one-way: an older build opens it but shows no orders (ADR-069). -
6bbd1ba: Each sale records the tax rounding its figures were computed with (#287):
finalizeOrderwritestaxRoundingon the stored order, the default (per_order,half_away_from_zero) included, andcustomas{ granularity: 'custom' }. It is the till's own record and is never sent inorder.create. The Z report splits each sale's tax by rate with the strategy that sale recorded, so its rows are the receipts' rows, and itsbreakdowns.tax_rounding_mixedistruewhen a session's sales used more than one strategy (acustomsale counts as the default it applied);ClosureSheetthen says so, andbuildClosureDocumentcarries the same line ready to print asclosure.tax_rounding_note(TAX_ROUNDING_MIXED_NOTE), for the apps' closure templates.PosOrder.taxRoundingis now required in the type.pos_ordersmoves to schema version 6:taxRoundingis required, and the migration records the default on every older sale, the only rounding any earlier build used. Like version 5, this storage is one-way: an older build opens it but shows no orders, so never roll an app back across it (ADR-069). Before 3.0.0 ships, #242's OPFS upgrade proof is rerun against version 6. -
222543b: Add
RegisterCommandType,RegisterCommandEnvelopeandAnyCommandEnvelope, register payloads and results, and the register server capability.CommandTypeandCommandEnvelopeare unchanged.Record the local
register_commandsledger throughreconcileRegisterCommands, gated inuseRegisterSessionby its newcommandsandcapabilitiesoptions. Commands are recorded but not sent. Medusa reads theregistercontract. -
8141c1c: Guard register commands, movement reasons and register ids, exporting RegisterMovementReasonError and RegisterIdInvalidError. Harden register outbox result handling and batch limits.
Make CommandBatchRequest generic while preserving its existing default envelope type.
-
c9798a3: The register outbox now sets
OutboxState.stuckwhen the store has kept failing a sent register command for 15 minutes of answered time (STUCK_AFTER_MS, shared with the order outbox; offline gaps pause the clock), so the app can say since when till updates haven't reached the online store. The clock clears when the command is applied or rejected. It is kept in memory only: a restart starts it afresh. -
581472f: Add createRegisterOutbox to send stored register commands serially per register. The app starts it only for a store with the register capability.
-
c26ead6: A till no longer sells on a guessed tax rounding. When the store's capabilities read fails (it throws, or answers "unknown", as Vendure's does for a network error or a 5xx),
useStoreSettingskeeps the settings unresolved instead of falling back to the default rounding. It retries by itself after 5 seconds, then 10, doubling to at most 5 minutes. While it waits, the state iserrorwith anextRetryAt, and an app shows "Can't reach the store's settings yet. Retrying…". Only a store that reports no rounding, or a connector withoutcapabilities, gets the default. Before this, a failed read on a Vendure store set toper_rate_group_itemsmeant every sale raisedfigures_mismatch. -
0e4c9cc: The "since" a cashier reads is a real time (#253).
OutboxState.stuck(both outboxes) gainsfirstFailedAt: the wall-clock time the first failure of the current stuck run was answered, so an offline gap no longer moves it.sincekeeps its meaning, the clock's virtual start, and still drives the 15-minute threshold.firstFailedAtis kept in memory only. After a restart it is absent, andSyncStatusandOrdersListshow the stored time instead, worded "since about 2:49 AM". -
a94b255:
OutboxStategainsrejected?: number: the order outbox publishes the count ofpos_ordersthe store refused (syncStatus: 'rejected') wherever it publishespending, so it rises when a batch result rejects an order and falls whenrequeue()sends one again. The register outbox leaves it unset. Whilerejectedis above 0,SyncStatusnever says "Sales are up to date.": its whole status line (label, polite live region and iOS announcement) is "1 sale needs attention · The online store refused it. Ask the store owner to look at the till's sync log." or "{n} sales need attention · The online store refused them. Ask the store owner to look at the till's sync log.", in place of the stuck and backend-missing sentences and the sending or retrying line. What else waits stays in the count: "1 sale needs attention, 5 waiting to sync · …" with other sales pending, "…, 2 till updates waiting to sync · …" with till updates, and "…, 5 sales and 2 till updates waiting to sync · …" with both. Below it, "Refused sales stay on this till under Needs attention, each with what to do next.", then the backend-missing detail when the store is missing. Withrejected0 or unset, nothing changes. When the store refused a whole batch (refusedset, no sale carrying a code), the status line is the waiting count followed by " · The online store refused the last send. This till will try again with the next sale, or when the app is reopened." in place of the stuck or backend-missing sentence, and the sending or retrying line (which read "Retrying in 0 s.") is hidden; refused sales still outrank it. With only till updates waiting and the register outbox refused, it ends "…try again with the next till update." instead; with a sale waiting, the sales line wins. -
5ed6281: The till computes tax with the store's rounding strategy (#287, ADR-071).
ServerCapabilitiesgainstaxRounding(core exportsTaxRounding):per_order,per_line_itemsorper_rate_group_items, each withhalf_away_from_zeroorhalf_up, orcustom. Absent, andcustom, mean today'sper_orderwith half away from zero.TaxProvidertakesroundingandrateCodes(tax class → the backend's rate name, forper_rate_group_items); the order records the strategy astaxRounding, andtaxLinesByRatetakes it as a fourth argument. The algorithms are indocs/contract/field-kinds.md. -
5a204a9:
StoreSettingsgains a derivedtaxRounding.useStoreSettingsfills it from the context's capabilities, or else from one read of the connector'scapabilities()made before the settings are ready, so each sign-in emits the settings once with the rounding known and no later change holds a sale; a failed read or a connector withoutcapabilitiesgives the default rounding.taxProviderPropspasses it toTaxProviderasrounding, so the till rounds tax like the store with no app code (#324).custompasses no rounding, and an explicitroundingprop still wins. -
6278d8d: The register outbox can start when its store opens, as the order outbox does (#290). The new
useRegisterOutbox({ commands, transport, deviceId, isEnabled?, onResult?, backendNotFound? })runscreateRegisterOutboxover an already-openregister_commandscollection, and callsstart()so that till updates left pending (after a refused batch, for instance) go out when the app reopens. It returns{ state, flush }. A new collection or device id restarts the outbox, andcommands: nullleaves it idle. Apps that create the register outbox themselves should switch to this hook.SyncStatus's till-updates refusal line now ends "…with the next till update, or when the app is reopened.", matching the sales line. -
e15f389: The Vendure connector supplies its tax rate names, so a
per_rate_group_itemsstore groups a sale's tax the way Vendure does (#324). Apps no longer fetch the names themselves.StoreSettings.taxRateCodes(core, optional): the backend's tax rate name per tax class, keyed liketaxRatesPpm, includingdefault.vendureStoreSettingsreads each rate'snamein the tax-rate query it already runs, so no extra request is made. Only the ratestaxRatesPpmuses count, anddefaultfollows the same default-category rule.taxRateCodesis left out when no names come back.taxProviderProps(settings)passestaxRateCodesto<TaxProvider>asrateCodes.
Patch Changes
-
d6a5073: The outbox freezes an order an older till stored before it first sends it (
freezeSentForm, whichfinalizeOrderuses too):- line names, discount labels and payment references are cut to 255 characters, but ids never are;
- a customer email or id that
order.createwould refuse is left out; - the frozen form is written back, so the receipt and the store see the same bytes.
So an order stored before the upgrade and still unsent is never refused as
invalid_payload.New type:
SentOrder, anOrderwhose customer id may be missing. The receipt stage,buildReceiptDataandReceipttake it, and a plainOrderstill fits. -
27d736e:
CommandWarninggainsbridgeMinorontotal_mismatchand a newtax_rate_mismatchcode, and the till now ignores warning codes it doesn't know (knownWarnings), instead of showing them as a store total. -
e59ebec: Each line is taxed at its product's tax class, not the store's default (#288).
ProductTraitsgains an optionalgetTaxClass(doc, variantId?), the backend's tax class id, a key ofStoreSettings.taxRatesPpm.addProductandaddEntryToCartpass it toaddLinethrough the newAddLineInput.taxClass, which the tax context resolves; a connector without the accessor is unchanged (the default rate).TaxProvidertaxes a class with no rate at the default rate and warns once per class through the newtaxLogger. connector-vendure replicates each variant'staxCategory { id }and implements the accessor, and its store settings give every tax category with no enabled rate in the default zone an explicit 0 rate, as Vendure charges; its product schema goes to version 2, so the products collection is dropped and downloaded again on the first sync after the upgrade. -
eb203b4: Review follow-ups with no behaviour change (#356, #358):
SyncStatusandOrdersListbuild their "since {time}" and "since about {time}" text with one shared helper.useRegisterOutbox's docs now say whentransport()is called, and that the latestisEnabledandonResultare used without restarting the outbox.
-
8cd7860: A sale the store refuses on its first send is sent once, not twice (found by the Medusa POS app's 3.0.0-next.0 adoption). Before it sends, the outbox stores the order's sent form and version (#300). That write had re-armed the flush, so a refused batch went out again. A write that only records the sent form, for a new sale or for an older one carried over by the pos_orders migrations, is no longer counted as new work. Any other change to a pending sale still sends it.
-
af623c9: The order.create string lengths move from
payloadShapeErrorsto the newpayloadBoundErrors, whichprecheckCommandcalls after the replay lookup, so an applied order resent with a long title replays asduplicate;payloadShapeErrorskeeps the types, thecustomerIdandsessionIdbounds and the NUL check.useSaleapplies a tender before logging a dropped reference, andadd()refuses a product whose id or v3 tax code finalize would refuse; the tender's reference field caps at 255 characters.finalizeOrdernow freezes the sent form (names and discount labels cut, an unsendable customer email or id left out) andtoOrderCreateEnvelopesends the stored order unchanged, so every resend is byte-identical. -
ef2f64e: The shared order.create shape check bounds string lengths and refuses NUL, and so does the v3 fiscal-figures check for its display and tax-code strings. The till cuts long names when it stores the order, refuses over-long pass-through references at finalize (and a payment reference as it's entered), refuses a searched or parked customer whose email or id the server would refuse, and validates the customer email at entry. Tills should ship this clamp before plugins adopt the new bounds, so no till sends a sale the server would now refuse.
-
d225c58: Internal
@tallyui/*peer dependencies are published as a caret range (for example^2.1.0) instead of an exact version. The packages still release together at one version. -
d329193: The order outbox stores each pending order's stuck clock and isolation in
serverFailuresand restores them when it starts, so after a restart an order the store keeps refusing no longer holds up the other sales, and its stuck flag keeps its start time. -
c00e1ea:
OrdersListshows a rejected sale's refusal in the cashier's words, one sentence per error code (#269), in both Needs attention and Recent, and never the store's own message. An unknown code, or a rejected sale with no error, showsplatform_error's sentence: "The online store refused this sale. Ask the store owner to look at the till's sync log." Anidempotency_mismatchshows its sentence ("… Don't send it again; ask the store owner to compare the two.") in place of the old "This sale needs checking against the store before it can be sent again." line, and still has no Retry. The order outbox logs every refusal once to the sync log as "Order refused by the store" with the order id, the code and the store's message: a warning, or an error forunsupported_version(whose log previously used the message itself as its text). -
8ae3c53: A register session transition's ledger key now includes its status (
session.transition:<sessionId>:<status>:<at>), so a close in the same millisecond as the count before it is queued with itscounted,closedByandapprovedByinstead of being skipped (#258). -
9885075: Match variant barcodes and SKUs in product search, and skip disabled Vendure variants when reading the product barcode.
-
c48e1dd: Store settings follow-ups to #339 and #341 (#340):
- A signed-out till is asked to sign in, not shown "Retrying…". When the capabilities read fails with an error only the till can fix,
useStoreSettingsgiveserrorwithoutnextRetryAtand doesn't retry by itself, so the app prompts. That covers a till-class error (a 401, or a till that needs updating) and aSignInErrorwithcode: 'invalid_credentials'; any other sign-in error still waits and retries. Every other failure still waits and retries. - A
/tally/v1/infoJSON body that isn't an object (null, an array or a scalar) is unknown, not the default rounding.
- A signed-out till is asked to sign in, not shown "Retrying…". When the capabilities read fails with an error only the till can fix,
-
df80ead:
SyncStatustakes an optionalregisterState(the register outbox's state): waiting till updates are counted ("1 till update waiting to sync", or named beside the sales), so it never says the sales are up to date while any wait, and with no sale waiting the backend-missing sentence says till updates aren't reaching the online store, or, onceregisterState.stuckis set, "Till updates haven't reached the online store since {time}. …". The backend-missing detail now reads "This till couldn't find {pluginName} on the online store. …". With nothing waiting, the line is only "Sales are up to date." (it replaces "All sales synced"), with no sending, retrying or problem text after it; if the store is missing, the detail reads "This till couldn't find {pluginName} on the online store the last time it checked. …". The status line's accessibility label is the whole visible line instead of "Sync status". The order and register outboxes let go of a sharedbackendNotFoundtracker onstop()and take it up again onstart()orflush(). -
37aad35:
useSalenever drops a line tapped while new store settings land (#301). The new sale that new tax settings or currency start on an idle cart now begins in a layout effect, inside the commit that brings those settings, and only when the sale is idle at that moment (no line, cart stage, no save in flight or pending), not as of the last render. Every sale change reads the current order builder, so a call from an older render never reaches a builder thatnewSale()has replaced. A line tapped once the new settings have committed lands on the new sale, priced with the new settings; a line that reaches the sale before its new sale starts keeps that sale, on its old settings. -
4122dc8: order.create v3 omits a malformed session ID (empty or longer than 36 characters) instead of sending it, so the plugin never refuses the sale for it. At capability 3,
finalizeOrderrefuses a sale whose display lines don't join the order's lines by id and count, or whose display tax mode differs from the order's.
@tallyui/connector-woocommerce
Minor Changes
-
457162d: One reconcile feed per store session (#307, a release gate). The WooCommerce and Medusa reconcile feeds were module-level singletons, so after a store switch in one runtime, store A's queued tombstones and refetches could reach store B's database.
- New factories.
createWooCommerceConnector(),createMedusaConnector()andcreateMedusaAdminUserConnector()each build their own feed;createVendureConnector(options)already did. Build a connector per store session, anew on each sign-in or store change. - Deprecated exports.
woocommerceConnector,medusaConnector,medusaAdminUserConnectorandvendureConnectorare deprecated: one instance for the whole app can leak queued reconcile work across stores. They are removed in 4.0. - A development warning.
startReplicationwarns once when the same adapter object replicates into two collections at once. - Refetch budget by requests.
refetchBatchSizeon the reconcile adapters makes a page that enqueuesnrefetches takeceil(n / refetchBatchSize)request-budget slots (WooCommerce 100, Medusa 100, Vendure 1,000). - WooCommerce 426 errors. A foreign (non-WCPOS) 426 keeps the store's
codebeside its message, and the message is capped at 200 characters.
- New factories.
-
ce4f796: Replication pull errors are handled according to who can fix them, instead of every error being retried every 5 s forever. A till repeating a rejected token is the traffic a store's security plugin blocks.
@tallyui/core:- An error class declares
fixedBy: 'till' | 'store'with a stringcode;errorKind(error)returns'till','store'or'transient'. SyncNotice({ code, since, fixedBy, software?, minVersion?, fix? }) describes a stopped pull.ConnectorUnauthorizedErroris fixed by the till.
- An error class declares
@tallyui/databasestartReplicationhandles the three kinds and returns RxDB's state plusnotice$andresume():- till: one request, one notice, then the pull stays stopped until the app calls
resume(), after sign-in. The pull stays stopped even when RxDB restarts the loop on page visibility. - store: one notice, then one attempt every 5 minutes (or the error's
retryAfterMs, up to 1 hour). The notice clears itself on the first success, so a till recovers within 5 minutes of the owner's fix. - transient: a doubling delay from
retryTimeto 5 minutes. It waits at least a validretryAfterMs(a finite number of zero or more), capped at 1 hour.
- till: one request, one notice, then the pull stays stopped until the app calls
@tallyui/components:SyncStatustakes an optionalpullNoticeand tells the cashier in plain words that they can keep selling and who needs to act. It never shows a code, a backend name or a version the notice doesn't carry.@tallyui/connector-woocommerce:WooDateFilterErroris fixed by the store, and carriessoftwareandminVersion.WooMissingUuidErrorgainscode: 'missing_plugin'and is fixed by the store.
@tallyui/connector-vendure: a newVendureTimezoneConfigError(store_misconfigured, with a plainfix) replaces the plain error when theupdatedAtprobe shows a server that isn't in UTC.
-
ad18929: The WooCommerce connector names its auth failures, so the app can tell "sign in again" apart from "store broken": a 401 or 403 from the product pull rejects with
ConnectorUnauthorizedError(re-exported from the connector, as Medusa does), andauth.getHeaderswithout a WCPOS token throwsWooMissingTokenError, a subclass ofConnectorUnauthorizedError, instead of sendingBearer undefined. Other HTTP errors keep their message and class. -
136343c: The WooCommerce connector gets a daily reconciliation pass (#248, part B), a safety net for edits the incremental pull can miss: the spring-forward hour, an over-excluding filter, a same-second edit, a shift without
X-WP-Total, trashed or unpublished products, and stock written without a modified-time bump.reconcile.cataloguelists the published catalogue with no date filter, comparing date, stock quantity and stock status. It re-reads deletion candidates by id and removes only those that are gone, trashed or unpublished. Everything else it re-pulls through the collection's own pull.replication.productsnow combines the product pull with the reconcile feed, withlegacyKey: 'products', so existing installs keep their checkpoint.- A product the store cannot be asked about (no numeric id) is never deleted.
- The WCPOS bulk-ID fast path is read from
wcpos/v2/statuscapabilities(products_id_fast_path). It stays dormant until wcpos/woocommerce-pos#2113 ships. @tallyui/database: the catalogue runner's gate check has a 60-second floor, so a bad interval can no longer re-arm it on every tick.
-
ddd9e85: The WooCommerce catalogue reconcile uses the WCPOS products fast path (#313). When
wcpos/v2/statuslistsproducts_id_fast_pathincapabilities, the whole catalogue is listed in one request (per_page=-1,_fields=id,date_modified_gmt,stock_quantity,stock_status) instead of pages of 100. A store that refuses it, or answers with something that is not a list, is listed page by page in the same pass.- Keyed on the remote id: both WooCommerce listings key on the numeric product id, never the till-local uuid.
CatalogueReconcileAdapter.matchKey(core, optional): an adapter whose listing carries no primary key declares how to match a local document. The catalogue runner indexes the local documents by it for each pass. Deletion is unchanged:confirmGone, then the mass-delete brake, by primary key.remoteon the keyed reconcile feed (core, optional): a listed product the till does not hold yet is matched back by its remote id, so it is delivered rather than dropped.
-
fb4b983: A
jwt_auth_*403 is nowWooTokenRefusedError(store_misconfigured,fixedBy: 'store', with afix), replacingWooPluginUpdateRequiredError(#360). The old error told the store owner to update to WCPOS 1.10.8, but the same 403 also arrives on 1.10.8 and later, so the new one names no version: "The store refused the sign-in token (403). Ask the store owner to check the JWT Authentication plugin's settings, or pair the till again."SyncStatusshows "a setting on the online store needs changing" with the fix. Breaking for importers:WooPluginUpdateRequiredErroris no longer exported. -
508876e: The WooCommerce connector now requires WooCommerce 5.8 or later (for
modified_afteron the products route;dates_are_gmtarrived in 5.4).- The GMT question goes to the store. The product pull asks the store whether anything changed since the last pass, in GMT: the mark request sends
modified_after=<last mark>&dates_are_gmt=true, and an empty answer ends the poll. Before, the pull compared the first row of a local-time sort, so in a daylight-saving fall-back hour an edit could wait until the next one. - Stores that ignore the filter are refused. If a store returns a product outside the requested window (WooCommerce before 5.8, or a proxy that drops the parameter), the pull throws the new
WooDateFilterError(code: 'unsupported_store') instead of trusting it. - Dates carry no offset. Dates are sent as GMT digits without an offset, because WordPress parses an offset-bearing date in the site's timezone before it compares it with the GMT column.
- The connector has a README.
- The GMT question goes to the store. The product pull asks the store whether anything changed since the last pass, in GMT: the mark request sends
-
3b206d6: The connector now authenticates with a WCPOS bearer token and the
X-WCPOS: 1header against<site>/wp-json/wcpos/v2(WCPOS Free 1.10.0 or later); the consumer key and secret fields are removed; a pulled product without a uuid throwsWooMissingUuidError. -
cbf26fd: The WooCommerce connector sends WCPOS's protocol signal, so a WCPOS 2.0 store does not refuse it (#296). Every request carries
X-WCPOS-Protocol: 2andX-WCPOS-Client: tallyui/<connector version>. WCPOS's 2.0 gate refuses POS-markedwcpos/v2requests without protocol 2, and protocol 2 is a pure declaration the connector already conforms to. The headers are harmless on WCPOS 1.x.If a store still answers 426 (
wcpos_update_required), the newWooTillUpdateRequiredError(till_update_required, fixed by the till) stops the product pull after one request.SyncStatusthen tells the cashier: "Products aren't updating: this till needs updating."
Patch Changes
-
7fee0c1: A WooCommerce product whose uuid changed in the store is replaced on the till in one pass (#331). The till delivers it under its new uuid and removes the old copy. Before this fix, it removed the old copy and dropped the new one, so the product was missing until the next daily check, and it was removed without the usual by-id check.
- The reconcile feed: when an entry that has a local copy is fetched back under a different primary key but the same remote id, the feed delivers that document as well as removing the old copy.
combinePullAdapterstakes an optionalkeyfor resolving duplicates across its sub-adapters. It defaults todoc.id, as before. WooCommerce passes the uuid (its primary key): two documents that share a store id, such as a product's new copy and its old copy's removal, must both reach the collection.
-
d225c58: Internal
@tallyui/*peer dependencies are published as a caret range (for example^2.1.0) instead of an exact version. The packages still release together at one version. -
1223353: A stale duplicate copy of a store product no longer stays on the till for good (#369). When two local products share one store id, the catalogue check now makes the copy its index did not pick a deletion candidate, and logs a
duplicateevent with the codeduplicate_match_key. The copy is tombstoned only whenconfirmGoneproves the store doesn't back it, and the mass-delete brake still applies. WooCommerce'sconfirmGonenow also confirms a local whose id is live but whose uuid isn't the store's for that id. The store listing is the source of truth, and a later pull restores anything the store still backs. -
6673faf: RxDB 17.5.0.
@tallyui/storage-sqlite:- Its
rxdb-premiumpeer is now17.5.0. Apps installrxdb-premium@17.5.0together withrxdb@17.5.0. - Its storages set RxDB 17's premium flag at import and when called, so the 13-collection cap never applies.
- Its
@tallyui/pos:- Its
rxdbpeer is now~17.5.0. - Opening
pos_ordersrejects withPosOrderOpenClosedErrorwhen the database closes during a migration: RxDB 17.5.0 cancels the migration on close. The open first waits for any write already in flight, so none reaches a closed store. - An open that needs no migration resolves only once RxDB allows writes, so a sale saved straight after it is never refused with COL25.
- Its
@tallyui/database:createTallyDatabasereturns an RxDB 17 database.- In development it adds RxDB's dev-mode plugin when a database is created, not at import.
- Stored data: a till's SQLite data written by RxDB 16.21.1 opens unchanged under 17.5.0, and migrates its schema versions.
Upgrade notes
- Storage is one-way. Once a till has opened this version,
pos_ordersis at schema version 4, and an older build (such as@tallyui/pos2.0.0 on RxDB 16.21.1) opens it without an error but shows no orders, so it sends none of the pending ones until the till is upgraded again. Nothing is deleted: the next upgrade recovers every order, including a sale rung during the rollback. Never roll an app back across this version, and never re-ring sales it hides: a re-rung sale is a second sale, and the upgrade sends both. See ADR-069 indocs/DECISIONS.md. - Web apps ship the 17.5.0 storage worker with the 17.5.0 main thread. A cached 16.x worker with a 17.5.0 main thread is untested and unsupported.
- Apps pin
rxdbandrxdb-premiumto exactly17.5.0. - RxDB 17 defaults a replication's
toggleOnDocumentVisibleto true (16.21.1: false). It then resyncs when the tab becomes visible, and no longer simulates activity to keep a hidden tab awake, so a browser may throttle a hidden tab's pull. RxDB pauses a hidden tab's replication only when that tab isn't the leader; a single-instance database is always the leader (read in 17.5.0'splugins/replicationsource, not tested).
-
3cf5452: Follow-ups to the 401/403 split (#345):
- Vendure: a signed-in user missing a permission (confirmed by the session probe) is now
ConnectorUnauthorizedErrorwithstatus: 403, theforbiddennotice, instead of a plain transient error. - WooCommerce: a 403 from the JWT-auth plugin (
jwt_auth_*) reaches the till only with a valid token on WCPOS 1.10.0–1.10.7 (wcpos/woocommerce-pos#1863). It is nowWooPluginUpdateRequiredError(unsupported_store, WCPOS 1.10.8): the store owner updates WCPOS, and the till is never sent into a sign-in loop. ConnectorUnauthorizedError: only a 403 isforbidden. A caller that omitsstatusgetsunauthorized, as before 3.0.- Docs: the customer picker's
onError, the replication guide's error classes, and the connector comments now say that only a 401 means sign in again.
- Vendure: a signed-in user missing a permission (confirmed by the session probe) is now
-
8cf3ea4: A till tells "sign in again" apart from "signed in, but not allowed" (found by the Medusa POS app's adoption).
ConnectorUnauthorizedError.statusis now required, typed401 | 403, and set by meaning at every connector.401: the credentials are not accepted, so sign in again.code: 'unauthorized', fixed by the till.403: the till is signed in but not allowed.code: 'forbidden', fixed by the store.- Vendure answers a signed-out session with 403 too. Its connector checks who is signed in first, so a confirmed sign-out is always
401.
- A 403 on the pull gives the
forbiddennotice, never a sign-out. The pull retries on the store schedule and clears by itself once the store owner grants the permission. SyncStatus shows "Products aren't updating: your account isn't allowed to do this on this store." with "You can keep selling. Ask the store owner." - The customer picker shows "Your account isn't allowed to do this on this store. Ask the store owner." for a 403, instead of asking the cashier to sign in again.
-
d9ecbb8: Only WCPOS's own protocol gate counts as "this till needs updating" (#302):
- The plugin's gate: a 426 whose body carries
code: "wcpos_update_required"still raisesWooTillUpdateRequiredError. - Any other 426 (from a proxy or another plugin, or with no or another body) is now a transient error, retried with backoff, so it never tells a cashier to update the till.
The built connector now bundles only its version from
package.json, not the whole file. - The plugin's gate: a 426 whose body carries
-
154e552: The product pull sends
dates_are_gmt=truewith everymodified_after, so WooCommerce compares the GMT checkpoint againstpost_modified_gmtinstead of the store's local time; on a store west of UTC the next pull no longer skips edits made in between. -
87087f1: When the catalogue check's fast path fails, or answers with something that is not a list, the page-by-page fallback now starts with an empty page (#331). Its first request then takes its own request-budget slot, as the status read does, instead of sharing the failed fast-path request's slot. A pass that falls back reports one more page.
-
e0f0afb: The WooCommerce product pull makes one request per quiet poll in two more cases: after the most recently edited product is trashed (the store's newest product is then older than the pull's lower bound; it was 3 requests), and on a store with no products (it was 4). The stored
restartscounter is gone: every shrink of the window restarts the pass, bounded by the per-call request budget. A stored checkpoint that still carriesrestartskeeps working. -
a0256e1: The product pull no longer skips products that share a
date_modified_gmtsecond across a page boundary. It pulls in passes, like the Medusa connector: each pass fixes an inclusive lower bound (modified_afterone second earlier), pages that window by product id with an offset, restarts ifX-WP-Totaldrops between pages (after three restarts, a fresh pass starts in the same call), ends on a short or empty page when a proxy stripsX-WP-Total, and moves the lower bound to the newestdate_modified_gmtin the store when the pass began. Because RxDB does not store the checkpoint of a pull that returns no documents, the handler never carries state in an empty result: a pass that ends on an empty page chains into the next pass in the same call, and each call makes at most four requests. When nothing has changed since the last pass, the pull makes one small request and returns nothing.WooProductCheckpointis now{ modified, offset, pass_mark?, pass_count?, restarts? }; a stored{ id, modified }checkpoint is read as the start of a pass. -
b8aba84: The product pull marks every product whose
statusis notpublish(draft, pending, private, or none) as_deleted, so RxDB removes it from the POS catalogue, and a product that is published again comes back. The pull still reads every status through the modified-date cursor and sends nostatusparameter, so a product that goes from published to draft is seen and removed rather than left on the till.
@tallyui/connector-medusa
Minor Changes
-
faa7cda: Expose ConnectorUnauthorizedError for expired or rejected stored credentials in Vendure and Medusa requests.
-
0d04d13: Add neutral Customer, CustomerInput and CustomerServiceError exports and optional online-only customer search, create and get connector methods.
Implement customer search, create and get for Medusa's admin-user connector.
-
457162d: One reconcile feed per store session (#307, a release gate). The WooCommerce and Medusa reconcile feeds were module-level singletons, so after a store switch in one runtime, store A's queued tombstones and refetches could reach store B's database.
- New factories.
createWooCommerceConnector(),createMedusaConnector()andcreateMedusaAdminUserConnector()each build their own feed;createVendureConnector(options)already did. Build a connector per store session, anew on each sign-in or store change. - Deprecated exports.
woocommerceConnector,medusaConnector,medusaAdminUserConnectorandvendureConnectorare deprecated: one instance for the whole app can leak queued reconcile work across stores. They are removed in 4.0. - A development warning.
startReplicationwarns once when the same adapter object replicates into two collections at once. - Refetch budget by requests.
refetchBatchSizeon the reconcile adapters makes a page that enqueuesnrefetches takeceil(n / refetchBatchSize)request-budget slots (WooCommerce 100, Medusa 100, Vendure 1,000). - WooCommerce 426 errors. A foreign (non-WCPOS) 426 keeps the store's
codebeside its message, and the message is capped at 200 characters.
- New factories.
-
7d1bc98: Add
parseTaxRoundingandparseInfoCapabilities, which read/tally/v1/infoincluding its top-leveltaxRounding(#287). The Medusa connector's capability read now carries the store'staxRounding.
Patch Changes
-
eb5a032: A
/tally/v1/infoanswer that says nothing about the store no longer means the default tax rounding (a follow-up to #339).- Unknown: a 2xx that is not JSON, and a
taxRoundingvalue that is present but malformed, now read as "unknown" (undefined), like a network failure or a 5xx. The till's store settings wait and retry instead of selling on a guessed rounding. - Unchanged: a 404 still means an older plugin (
orderCreate: 1, the default rounding), and so does a well-formed body with notaxRoundingkey. - Type change:
parseInfoCapabilitiesnow returnsServerCapabilities | undefined. It isundefinedwhen the body carries a malformedtaxRounding.
- Unknown: a 2xx that is not JSON, and a
-
2ecaa36: A replication adapter can set
pull.batchSize, and the Medusa connector pulls 500 products per page. -
d225c58: Internal
@tallyui/*peer dependencies are published as a caret range (for example^2.1.0) instead of an exact version. The packages still release together at one version. -
222543b: Add
RegisterCommandType,RegisterCommandEnvelopeandAnyCommandEnvelope, register payloads and results, and the register server capability.CommandTypeandCommandEnvelopeare unchanged.Record the local
register_commandsledger throughreconcileRegisterCommands, gated inuseRegisterSessionby its newcommandsandcapabilitiesoptions. Commands are recorded but not sent. Medusa reads theregistercontract. -
6673faf: RxDB 17.5.0.
@tallyui/storage-sqlite:- Its
rxdb-premiumpeer is now17.5.0. Apps installrxdb-premium@17.5.0together withrxdb@17.5.0. - Its storages set RxDB 17's premium flag at import and when called, so the 13-collection cap never applies.
- Its
@tallyui/pos:- Its
rxdbpeer is now~17.5.0. - Opening
pos_ordersrejects withPosOrderOpenClosedErrorwhen the database closes during a migration: RxDB 17.5.0 cancels the migration on close. The open first waits for any write already in flight, so none reaches a closed store. - An open that needs no migration resolves only once RxDB allows writes, so a sale saved straight after it is never refused with COL25.
- Its
@tallyui/database:createTallyDatabasereturns an RxDB 17 database.- In development it adds RxDB's dev-mode plugin when a database is created, not at import.
- Stored data: a till's SQLite data written by RxDB 16.21.1 opens unchanged under 17.5.0, and migrates its schema versions.
Upgrade notes
- Storage is one-way. Once a till has opened this version,
pos_ordersis at schema version 4, and an older build (such as@tallyui/pos2.0.0 on RxDB 16.21.1) opens it without an error but shows no orders, so it sends none of the pending ones until the till is upgraded again. Nothing is deleted: the next upgrade recovers every order, including a sale rung during the rollback. Never roll an app back across this version, and never re-ring sales it hides: a re-rung sale is a second sale, and the upgrade sends both. See ADR-069 indocs/DECISIONS.md. - Web apps ship the 17.5.0 storage worker with the 17.5.0 main thread. A cached 16.x worker with a 17.5.0 main thread is untested and unsupported.
- Apps pin
rxdbandrxdb-premiumto exactly17.5.0. - RxDB 17 defaults a replication's
toggleOnDocumentVisibleto true (16.21.1: false). It then resyncs when the tab becomes visible, and no longer simulates activity to keep a hidden tab awake, so a browser may throttle a hidden tab's pull. RxDB pauses a hidden tab's replication only when that tab isn't the leader; a single-instance database is always the leader (read in 17.5.0'splugins/replicationsource, not tested).
-
8cf3ea4: A till tells "sign in again" apart from "signed in, but not allowed" (found by the Medusa POS app's adoption).
ConnectorUnauthorizedError.statusis now required, typed401 | 403, and set by meaning at every connector.401: the credentials are not accepted, so sign in again.code: 'unauthorized', fixed by the till.403: the till is signed in but not allowed.code: 'forbidden', fixed by the store.- Vendure answers a signed-out session with 403 too. Its connector checks who is signed in first, so a confirmed sign-out is always
401.
- A 403 on the pull gives the
forbiddennotice, never a sign-out. The pull retries on the store schedule and clears by itself once the store owner grants the permission. SyncStatus shows "Products aren't updating: your account isn't allowed to do this on this store." with "You can keep selling. Ask the store owner." - The customer picker shows "Your account isn't allowed to do this on this store. Ask the store owner." for a 403, instead of asking the cashier to sign in again.
@tallyui/connector-shopify
Patch Changes
-
d225c58: Internal
@tallyui/*peer dependencies are published as a caret range (for example^2.1.0) instead of an exact version. The packages still release together at one version. -
6673faf: RxDB 17.5.0.
@tallyui/storage-sqlite:- Its
rxdb-premiumpeer is now17.5.0. Apps installrxdb-premium@17.5.0together withrxdb@17.5.0. - Its storages set RxDB 17's premium flag at import and when called, so the 13-collection cap never applies.
- Its
@tallyui/pos:- Its
rxdbpeer is now~17.5.0. - Opening
pos_ordersrejects withPosOrderOpenClosedErrorwhen the database closes during a migration: RxDB 17.5.0 cancels the migration on close. The open first waits for any write already in flight, so none reaches a closed store. - An open that needs no migration resolves only once RxDB allows writes, so a sale saved straight after it is never refused with COL25.
- Its
@tallyui/database:createTallyDatabasereturns an RxDB 17 database.- In development it adds RxDB's dev-mode plugin when a database is created, not at import.
- Stored data: a till's SQLite data written by RxDB 16.21.1 opens unchanged under 17.5.0, and migrates its schema versions.
Upgrade notes
- Storage is one-way. Once a till has opened this version,
pos_ordersis at schema version 4, and an older build (such as@tallyui/pos2.0.0 on RxDB 16.21.1) opens it without an error but shows no orders, so it sends none of the pending ones until the till is upgraded again. Nothing is deleted: the next upgrade recovers every order, including a sale rung during the rollback. Never roll an app back across this version, and never re-ring sales it hides: a re-rung sale is a second sale, and the upgrade sends both. See ADR-069 indocs/DECISIONS.md. - Web apps ship the 17.5.0 storage worker with the 17.5.0 main thread. A cached 16.x worker with a 17.5.0 main thread is untested and unsupported.
- Apps pin
rxdbandrxdb-premiumto exactly17.5.0. - RxDB 17 defaults a replication's
toggleOnDocumentVisibleto true (16.21.1: false). It then resyncs when the tab becomes visible, and no longer simulates activity to keep a hidden tab awake, so a browser may throttle a hidden tab's pull. RxDB pauses a hidden tab's replication only when that tab isn't the leader; a single-instance database is always the leader (read in 17.5.0'splugins/replicationsource, not tested).
@tallyui/connector-vendure
Minor Changes
-
faa7cda: Expose ConnectorUnauthorizedError for expired or rejected stored credentials in Vendure and Medusa requests.
-
457162d: One reconcile feed per store session (#307, a release gate). The WooCommerce and Medusa reconcile feeds were module-level singletons, so after a store switch in one runtime, store A's queued tombstones and refetches could reach store B's database.
- New factories.
createWooCommerceConnector(),createMedusaConnector()andcreateMedusaAdminUserConnector()each build their own feed;createVendureConnector(options)already did. Build a connector per store session, anew on each sign-in or store change. - Deprecated exports.
woocommerceConnector,medusaConnector,medusaAdminUserConnectorandvendureConnectorare deprecated: one instance for the whole app can leak queued reconcile work across stores. They are removed in 4.0. - A development warning.
startReplicationwarns once when the same adapter object replicates into two collections at once. - Refetch budget by requests.
refetchBatchSizeon the reconcile adapters makes a page that enqueuesnrefetches takeceil(n / refetchBatchSize)request-budget slots (WooCommerce 100, Medusa 100, Vendure 1,000). - WooCommerce 426 errors. A foreign (non-WCPOS) 426 keeps the store's
codebeside its message, and the message is capped at 200 characters.
- New factories.
-
ce4f796: Replication pull errors are handled according to who can fix them, instead of every error being retried every 5 s forever. A till repeating a rejected token is the traffic a store's security plugin blocks.
@tallyui/core:- An error class declares
fixedBy: 'till' | 'store'with a stringcode;errorKind(error)returns'till','store'or'transient'. SyncNotice({ code, since, fixedBy, software?, minVersion?, fix? }) describes a stopped pull.ConnectorUnauthorizedErroris fixed by the till.
- An error class declares
@tallyui/databasestartReplicationhandles the three kinds and returns RxDB's state plusnotice$andresume():- till: one request, one notice, then the pull stays stopped until the app calls
resume(), after sign-in. The pull stays stopped even when RxDB restarts the loop on page visibility. - store: one notice, then one attempt every 5 minutes (or the error's
retryAfterMs, up to 1 hour). The notice clears itself on the first success, so a till recovers within 5 minutes of the owner's fix. - transient: a doubling delay from
retryTimeto 5 minutes. It waits at least a validretryAfterMs(a finite number of zero or more), capped at 1 hour.
- till: one request, one notice, then the pull stays stopped until the app calls
@tallyui/components:SyncStatustakes an optionalpullNoticeand tells the cashier in plain words that they can keep selling and who needs to act. It never shows a code, a backend name or a version the notice doesn't carry.@tallyui/connector-woocommerce:WooDateFilterErroris fixed by the store, and carriessoftwareandminVersion.WooMissingUuidErrorgainscode: 'missing_plugin'and is fixed by the store.
@tallyui/connector-vendure: a newVendureTimezoneConfigError(store_misconfigured, with a plainfix) replaces the plain error when theupdatedAtprobe shows a server that isn't in UTC.
-
7d1bc98: Read the store's capabilities and
taxRoundingfrom the Vendure plugin's/tally/v1/info(#287):vendureSignInreturnscapabilities, and the connector gainscapabilities(context)for a restored session or an API key. -
e15f389: The Vendure connector supplies its tax rate names, so a
per_rate_group_itemsstore groups a sale's tax the way Vendure does (#324). Apps no longer fetch the names themselves.StoreSettings.taxRateCodes(core, optional): the backend's tax rate name per tax class, keyed liketaxRatesPpm, includingdefault.vendureStoreSettingsreads each rate'snamein the tax-rate query it already runs, so no extra request is made. Only the ratestaxRatesPpmuses count, anddefaultfollows the same default-category rule.taxRateCodesis left out when no names come back.taxProviderProps(settings)passestaxRateCodesto<TaxProvider>asrateCodes.
Patch Changes
-
eb5a032: A
/tally/v1/infoanswer that says nothing about the store no longer means the default tax rounding (a follow-up to #339).- Unknown: a 2xx that is not JSON, and a
taxRoundingvalue that is present but malformed, now read as "unknown" (undefined), like a network failure or a 5xx. The till's store settings wait and retry instead of selling on a guessed rounding. - Unchanged: a 404 still means an older plugin (
orderCreate: 1, the default rounding), and so does a well-formed body with notaxRoundingkey. - Type change:
parseInfoCapabilitiesnow returnsServerCapabilities | undefined. It isundefinedwhen the body carries a malformedtaxRounding.
- Unknown: a 2xx that is not JSON, and a
-
e59ebec: Each line is taxed at its product's tax class, not the store's default (#288).
ProductTraitsgains an optionalgetTaxClass(doc, variantId?), the backend's tax class id, a key ofStoreSettings.taxRatesPpm.addProductandaddEntryToCartpass it toaddLinethrough the newAddLineInput.taxClass, which the tax context resolves; a connector without the accessor is unchanged (the default rate).TaxProvidertaxes a class with no rate at the default rate and warns once per class through the newtaxLogger. connector-vendure replicates each variant'staxCategory { id }and implements the accessor, and its store settings give every tax category with no enabled rate in the default zone an explicit 0 rate, as Vendure charges; its product schema goes to version 2, so the products collection is dropped and downloaded again on the first sync after the upgrade. -
d225c58: Internal
@tallyui/*peer dependencies are published as a caret range (for example^2.1.0) instead of an exact version. The packages still release together at one version. -
6673faf: RxDB 17.5.0.
@tallyui/storage-sqlite:- Its
rxdb-premiumpeer is now17.5.0. Apps installrxdb-premium@17.5.0together withrxdb@17.5.0. - Its storages set RxDB 17's premium flag at import and when called, so the 13-collection cap never applies.
- Its
@tallyui/pos:- Its
rxdbpeer is now~17.5.0. - Opening
pos_ordersrejects withPosOrderOpenClosedErrorwhen the database closes during a migration: RxDB 17.5.0 cancels the migration on close. The open first waits for any write already in flight, so none reaches a closed store. - An open that needs no migration resolves only once RxDB allows writes, so a sale saved straight after it is never refused with COL25.
- Its
@tallyui/database:createTallyDatabasereturns an RxDB 17 database.- In development it adds RxDB's dev-mode plugin when a database is created, not at import.
- Stored data: a till's SQLite data written by RxDB 16.21.1 opens unchanged under 17.5.0, and migrates its schema versions.
Upgrade notes
- Storage is one-way. Once a till has opened this version,
pos_ordersis at schema version 4, and an older build (such as@tallyui/pos2.0.0 on RxDB 16.21.1) opens it without an error but shows no orders, so it sends none of the pending ones until the till is upgraded again. Nothing is deleted: the next upgrade recovers every order, including a sale rung during the rollback. Never roll an app back across this version, and never re-ring sales it hides: a re-rung sale is a second sale, and the upgrade sends both. See ADR-069 indocs/DECISIONS.md. - Web apps ship the 17.5.0 storage worker with the 17.5.0 main thread. A cached 16.x worker with a 17.5.0 main thread is untested and unsupported.
- Apps pin
rxdbandrxdb-premiumto exactly17.5.0. - RxDB 17 defaults a replication's
toggleOnDocumentVisibleto true (16.21.1: false). It then resyncs when the tab becomes visible, and no longer simulates activity to keep a hidden tab awake, so a browser may throttle a hidden tab's pull. RxDB pauses a hidden tab's replication only when that tab isn't the leader; a single-instance database is always the leader (read in 17.5.0'splugins/replicationsource, not tested).
-
9885075: Match variant barcodes and SKUs in product search, and skip disabled Vendure variants when reading the product barcode.
-
3cf5452: Follow-ups to the 401/403 split (#345):
- Vendure: a signed-in user missing a permission (confirmed by the session probe) is now
ConnectorUnauthorizedErrorwithstatus: 403, theforbiddennotice, instead of a plain transient error. - WooCommerce: a 403 from the JWT-auth plugin (
jwt_auth_*) reaches the till only with a valid token on WCPOS 1.10.0–1.10.7 (wcpos/woocommerce-pos#1863). It is nowWooPluginUpdateRequiredError(unsupported_store, WCPOS 1.10.8): the store owner updates WCPOS, and the till is never sent into a sign-in loop. ConnectorUnauthorizedError: only a 403 isforbidden. A caller that omitsstatusgetsunauthorized, as before 3.0.- Docs: the customer picker's
onError, the replication guide's error classes, and the connector comments now say that only a 401 means sign in again.
- Vendure: a signed-in user missing a permission (confirmed by the session probe) is now
-
8cf3ea4: A till tells "sign in again" apart from "signed in, but not allowed" (found by the Medusa POS app's adoption).
ConnectorUnauthorizedError.statusis now required, typed401 | 403, and set by meaning at every connector.401: the credentials are not accepted, so sign in again.code: 'unauthorized', fixed by the till.403: the till is signed in but not allowed.code: 'forbidden', fixed by the store.- Vendure answers a signed-out session with 403 too. Its connector checks who is signed in first, so a confirmed sign-out is always
401.
- A 403 on the pull gives the
forbiddennotice, never a sign-out. The pull retries on the store schedule and clears by itself once the store owner grants the permission. SyncStatus shows "Products aren't updating: your account isn't allowed to do this on this store." with "You can keep selling. Ask the store owner." - The customer picker shows "Your account isn't allowed to do this on this store. Ask the store owner." for a 403, instead of asking the cashier to sign in again.
-
9cd4024: A GraphQL
FORBIDDENanswer throwsConnectorUnauthorizedErroronly when a probe ({ activeAdministrator { id } }, same headers) finds nobody signed in (#274). With a live administrator it is a plainErrornaming the missing permission, and a failed probe is a plain, transientError, so a missing permission no longer signs the till out into a sign-in loop.
2.0.0
@tallyui/core
Minor Changes
-
#23
53af670Thanks @kilbot! - Breaking: the cart and checkout components are presentational and takeMoney.CartLinetakesname,quantity,unitPriceandlineTotal.CartTotaltakessubtotal,taxLines,discountandtotal.CashTenderedandChangeDisplaytakeMoneyamounts. All of them format withformatMoney, so there is nogetPrice, float arithmetic or hard-coded'$'.CartPanelis generic, andCartLineItemis removed. Totals come from@tallyui/pos; the components no longer compute tax. The cash input keeps the text as typed and emits integer minor units.@tallyui/coreaddsmoneyFromDecimalString, which parses typed decimal text intoMoneyusing integer arithmetic. -
#54
50317c8Thanks @kilbot! - Connectors can sign a user in:ConnectorAuthgains an optionalsignIn(baseUrl, { email, password }, init?)that resolves to aSignInResult(token, optional ISO 8601expiresAt) and rejects with aSignInErrorwhosecodeisinvalid_credentials,unsupportedorfailed. The app stores the token and passes it back togetHeadersastoken.Vendure's auth gains a sign-in flow: it runs the Admin API
loginmutation and takes the token from thevendure-auth-tokenheader (the server'stokenMethodmust include'bearer'). Its fields are nowurl,email,passwordand an optionalchannel_token.getHeaderssendscredentials.api_keyasvendure-api-key, otherwisecredentials.tokenas a Bearer token, plusvendure-tokenwhenchannel_tokenis set. The oldauth_tokencredential is still accepted as a deprecated alias fortoken.Medusa's
medusaAdminUserAuthsigns in throughPOST /auth/user/emailpassand readsexpiresAtfrom the JWT'sexp.medusaSecretKeyAuthhas no sign-in. -
#58
3e09957Thanks @kilbot! - AddcombinePullAdaptersto@tallyui/core: it combines several pull adapters into the one adapter a collection replicates with, calling them one after another with a checkpoint per sub-adapter. Two replications on one collection can skip each other's pulled versions, so run one per collection.Vendure's
replication.productsnow includes a variant feed that re-delivers parent products whose variants changed. Vendure does not bumpProduct.updatedAton a variant price or stock edit, so the product feed alone misses those changes. An existing install's product-feed checkpoint carries over; the variant feed runs one full pass on first sync. -
#18
a219ae0Thanks @kilbot! - Adds the TallyUI Sync Protocol command contract: theCommandEnvelope,CommandResult,CommandWarningandOrderCreatePayloadtypes (with their line and payment types), the batch request and response types, the constantsCOMMANDS_PATH,PROTOCOL_HEADER,PROTOCOL_VERSIONandMAX_COMMANDS_PER_BATCH, and theisCommandBatchResponseguard. These are the shapes pinned in ADR-038 and ADR-039, shared by the POS outbox and backend plugins. -
#115
4df9be4Thanks @kilbot! - Discounts are pre-tax (ADR-062). An order discount is allocated across the lines in proportion to their own-mode amounts (the newallocateOrderDiscount, largest-remainder rounding), each line carries its share inorderDiscountMinorand is taxed after it, so an order discount now lowers the tax instead of coming off the total after tax. A discountedorder.createis version 2, withdiscountMinoron each discounted line and on the payload; a discount-free payload stays version 1, byte-identical.finalizestill rejects discounts until the plugins honour version 2. Receipt lines show theirdiscountMinor. Stacked percentage order discounts are additive, each computed on the pre-order-discount base rather than compounding on what an earlier discount leaves, and every discount (line or order, percentage or fixed) is clamped to 0 so a negative value can never raise a price. -
#99
9649259Thanks @kilbot! -ReconcileFeedEntry(core) gainsrefreshOnly?: boolean: a missing product is skipped instead of tombstoned when its entry is refresh-only, so only the id reconcile's braked entries can delete (ADR-060, backlog 43). Merging inenqueuekeepsrefreshOnlytrue only when every entry queued for that id was refresh-only, so a deletable id-reconcile entry is never downgraded by a later refresh-only one. The fingerprint runner (startFingerprintReconcile, database) now enqueues its entries this way; the id runner is unchanged.FingerprintReconcileState(database) gainslastResultAt/lastErrorAt, stamped from an injectablenow(defaultDate.now), so a keptlastResultnext to a newerlastErrorcan be told apart from a current one. The newisFingerprintResultCurrent(state)helper does that comparison (backlog 46). -
#85
609ebd8Thanks @kilbot! - Add the fingerprint reconcile (ADR-060 amendment 8): a neutral runner that compares a remote fingerprint per product against the local documents and re-delivers products whose fingerprint differs, through the collection's pull.@tallyui/coreadds theFingerprintReconcileAdaptercontract (fetchPages, a purefingerprintandenqueue) and an optionalreconcile.pricesonTallyConnector.@tallyui/databaseaddsstartFingerprintReconcile, which runs no pass at start by default and otherwise mirrors the id reconcile: a complete, successful pass only,state$(running,lastResult,lastError), andstop().@tallyui/connector-medusaaddsreconcile.prices, a nightly base-price backstop (MEDUSA_PRICE_RECONCILE_INTERVAL_MS) for the variant feed (ADR-060 job D1): it fingerprints each product's base prices (variant id, currency and amount, sorted, price-list prices excluded) from/admin/product-variants. Nothing is written locally; corrections arrive only through the reconcile feed's pull. -
#97
f8b0dacThanks @kilbot! - A fresh install downloads the catalogue once. A pull adapter can now declarepull.seedCheckpoint; on a fresh install (no stored checkpoint)combinePullAdaptersreads every seed before any feed runs and starts that feed from it. The Medusa and Vendure variant feeds seed their cursor at the newest variant'supdated_at, so their first pass no longer re-delivers every product the product feed has just delivered, and a variant edit made during the product feed's first pass still arrives. An install upgrading from a stored checkpoint is never seeded and keeps the variant feed's full healing pass. -
#65
a0b7981Thanks @kilbot! -@tallyui/coreaddsresolvePriceRange(variants, currency?), the lowest and highest current price across a product's variants.ProductPriceuses it to showfrom <lowest price>when a product's variants are priced differently, instead of just the default variant's price. NewshowFromPrice(defaulttrue) andfromLabel(default'from') props control and opt out of this. -
#61
540044cThanks @kilbot! - Add the id reconcile (ADR-060): a periodic pass that reads every live product id and its live variant ids, so a deleted product or a deleted variant (whose parent'supdatedAtdoes not change) reaches the local copy.@tallyui/coreadds theIdReconcileAdaptercontract andcreateReconcileFeed, which turns queued corrections into a pull-only adapter meant as the last key ofcombinePullAdapters.@tallyui/databaseadds thestartIdReconcilerunner.@tallyui/connector-vendureimplements the Vendure side and wires it intoreplication.productsandreconcile.ids. Nothing is written locally into the replicated collection; corrections arrive only through the collection's own pull. -
#92
945bb83Thanks @kilbot! - Medusa prices as Medusa charges them (ADR-060 D2b).SyncContextgains an optionalpricingContext(fromstoreSettings()),ProductPricean optionaltaxInclusive, andTallyConnector.reconcileacalculatedPricesslot. With a pricing context, every Medusa product document build fills each variant'scalculated_pricefrom the store API (nullwhen the sales channel or region does not sell it), and the traits price from it: sale lists as a sale against the original price, override lists as the base price,nullas unsellable.reconcile.calculatedPricesre-delivers products whose calculated prices changed with no timestamp bump; run it everyMEDUSA_CALCULATED_PRICE_RECONCILE_INTERVAL_MS(30 minutes) withmaxPages: 1000. Without a pricing context, documents and prices are unchanged. -
#11
f90e59dThanks @kilbot! - Add backend-neutral price and stock traits.ProductTraitsgainsgetPrices(a price list of integer minor-unitMoneyentries,baseorsale, per currency) andgetStock(in_stock | out_of_stock | backorder | unknownplus an optional quantity). Core addsresolvePrice,moneyFromMajor,moneyToMajorandminorUnitDigits. Every connector maps its own shape into them; WooCommerce'sinstock/outofstock/onbackorderstrings now stay inside the WooCommerce connector. The string-price and WooCommerce-style stock accessors remain and are deprecated. -
#11
2a05ecbThanks @kilbot! - Build the product components on the neutral traits.ProductPriceresolves the price list and formats it with Intl in the price's own currency (newcurrencyandlocaleprops;currencySymbolis now only the fallback for an unknown currency).ProductStockBadgereadsgetStock.ProductImagegainsshowPlaceholder, an initial tile for products without images. Core addsformatMoney, atraitContextprop onConnectorProviderfor store-level facts like the store currency, anduseTraitContext.@tallyui/posaddssearchProducts, name/SKU/barcode search through traits that works the same on every backend. -
#120
e0062ceThanks @kilbot! - A per-storeorder.createcapability check replaces the global discount guard (ADR-062).@tallyui/coregainsServerCapabilities,SignInResult.capabilities,SyncContext.capabilities,TallyConnector.capabilities?()andresolveCapabilities(fresh, stored).@tallyui/connector-medusareads the store's supportedorder.createversions fromGET /tally/v1/info: a 404 or a malformed response means an old plugin (version 1), a network failure or a 5xx is unknown and keeps the last known value, and a 401 throws.medusaSignInreturns the read capabilities, and both Medusa connectors exposecapabilities(context)for a restored session.finalizeOrderin@tallyui/posnow rejects a discount only when the store's capability is below 2, so a store whose plugin has caught up finalizes a discounted order asorder.createversion 2. -
#95
f1af98cThanks @kilbot! -OrderCreateLinegains an optionaltaxInclusive(ADR-038 amendment 2). It is the line's own tax mode, sent only when that mode differs from the order'spricesIncludeTax. Single-mode orders produce byte-identical payloads.finalizestill rejects converted lines until the Medusa plugin honours the field. -
#157
125c85aThanks @kilbot! -readFresh,countFreshandwatchFreshmove to a new, side-effect-free subpath,@tallyui/core/rxdb. Core now listsrxdb(>=16) andrxjs(>=7) as optional peer dependencies, needed only by that subpath; core's main entry stays free of both.@tallyui/posre-exports the helpers unchanged. The id and fingerprint reconciles in@tallyui/databaseread the local products withreadFreshinstead of a cachedfind(), so a product the pull inserts or deletes while a pass reads them no longer leaves every later pass reading a stale list (RxDB 16.21.1 bug 4): an inserted product is now checked, and tombstoned or re-fetched, on the next pass, and a deleted one is no longer re-enqueued or counted towards the mass-delete brake. -
#164
24b0563Thanks @kilbot! - Register screens (WCPOSnextport, ADR-032), driven byuseRegisterSession:RegisterPicker,OpenRegisterCard,RegisterBar(one status pill; "Register ›"),MovementSheet(labelled "Amount" and "Reason"; a reason for every movement; same-tick taps coalesced),RegisterPanel(expected in the drawer; Undo by reversal; blind mode hides amounts) andRegisterColumn.@tallyui/coreaddscurrencySymbol(currency, locale?). -
#11
bc0a224Thanks @kilbot! - AddisSellableandgetVariantCountproduct traits to core and all four connectors. -
#57
55ae68fThanks @kilbot! -SignInErrorCodesplits the oldfailedin two:failednow means no response arrived (a network error), and the newserver_errormeans a response arrived but was unusable (a bad status, a malformed body, or a missing token).SignInErrorgains an optionalstatusfrom a third constructor argument. Callers that switch oncodeshould handleserver_error.Medusa's sign-in now treats
mfa_required: trueandverification_required: truethe same as alocationbody:unsupported, and no token is ever returned from a body like that. A malformed response body, any other non-OK status and a missing or non-string token are nowserver_errorwith the HTTP status.Vendure's sign-in now treats
NATIVE_AUTH_STRATEGY_ERRORasunsupported, since native email/password auth is disabled on the server. A malformed response body, a non-OK status, GraphQL errors, a missingdata.loginand any otherErrorResultare nowserver_errorwith the HTTP status. -
#64
402ec36Thanks @kilbot! - Both connectors now store a product's variants sorted by id, since neither Medusa nor Vendure guarantees variant order across requests:@tallyui/coreaddscompareIds, and the Medusa and Vendure product projections (toDocument,toProductDocument) sortvariantswith it before the document is stored. Traits that readvariants[0](getPrices,getSku,getPrice,getStockQuantity,getBarcodeand others) now see a stable variant across runs.Already-stored documents take the new order the next time they are delivered. Vendure's variant feed re-delivers every product on its first pass anyway, so it heals immediately.
-
#55
350967dThanks @kilbot! - Stock reads use the reconciled overlay (ADR-060) and show how fresh it is.@tallyui/corenow holdswithStockOverlayandgetProductStock(@tallyui/posre-exports them), addsSTOCK_LEVELS_LAST_PASS,stockOverlayandstockOverlayAsOfprops onConnectorProvider, and auseProductStock(doc)hook that returns overlay stock plusasOf, orgetStock(doc)when no overlay is given.@tallyui/database:startStockReconcilealso returnsstate$(running,truncated,lastError,lastCompletedAt),reconcileStock()resolves withcompletedAt, and each successful pass stores{ completedAt }in thelast-passlocal document ofstock_levels, whichcreateTallyDatabasenow creates with local documents (apps that create the collection themselves use the newstockLevelsCollectionconfig; without local documents a pass rejects with a clear error); a restarted runner seedslastCompletedAtfrom it.@tallyui/posaddsstockOverlayAsOf$.ProductStockBadgereads stock throughuseProductStockand appends " · as of <time>" when an overlay is given (showAsOf={false}hides it). -
#53
e3b8686Thanks @kilbot! - Add a stock reconcile pass (ADR-060).@tallyui/coreadds theStockReconcileAdaptercontract (fetchPagesand a pureoverlay) and an optionalreconcile.stockonTallyConnector.@tallyui/databaseadds the local-onlystock_levelscollection (STOCK_LEVELS_COLLECTION,stockLevelsSchema), whichcreateTallyDatabasecreates for connectors withreconcile.stock, andstartStockReconcile, which re-reads stock every 5 minutes (and on demand throughreconcileStock()) into that collection: it writes only changed rows, removes keys the backend no longer returns, writes nothing after a failed, truncated or stopped read, and never writes the replicated products.@tallyui/posaddsstockOverlay$,withStockOverlayandgetProductStock, which read stock from the overlay where it has an entry and from the replicated product otherwise. The Vendure connector reconciles variantstockLevels, and the Medusa connector reconciles inventory item location levels, so stock changes that bump no product timestamp reach the POS. -
#87
ac2a24aThanks @kilbot! -TallyConnectorgains an optionalstoreSettings(context, choice?)(TV4): one read-only call for the store's currency,pricesIncludeTax,taxRatesPpmand an opaque connector-specificpricingContext, so the app can feed the connector's own tax-inclusivity option and the POSTaxProviderfrom a single source of truth instead of two hand-matched settings. Rejects with aStoreSettingsError(choice_requiredwithchoices, orfailed).Vendure's
storeSettingsreads the active channel's currency andpricesIncludeTax, and the default tax zone's enabled, non-customer-group rates keyed by tax category id (rounded to integer ppm once, at the connector's edge).defaultis theisDefaultcategory's rate, or, when none is flagged, the first category Vendure's owntaxCategorieslists — the same fallback Vendure uses for a variant created without a category — and is 0 when that category has no rate in the zone.createVendureConnector'spricesIncludeTaxoption is unchanged; passsettings.pricesIncludeTaxfromstoreSettingsinstead of hand-matching it to the POS. -
#19
8df0569Thanks @kilbot! - Adds variant traits.VariantSummary(id,title,sku,barcode,prices,stock) and the optionalProductTraits.getVariantsdescribe every purchasable variant of a product, andfindVariantByCodefinds a variant by barcode or SKU for scanning. The Medusa connector implementsgetVariants; its product-levelgetPricesandgetStockresults are unchanged.
Patch Changes
-
#94
373e438Thanks @kilbot! -resolvePricekeeps a price'staxInclusiveflag oncurrentandwas. Each order-builder line keeps its price's own tax mode (LineItem.taxInclusive, pluspriceTaxModeConvertedwhen it differs from the store'spricesIncludeTax), so a customer pays exactly the shelf price and an inclusive price in an exclusive store is no longer taxed twice. Orders whose prices carry no flag, or one that agrees with the store, total exactly as before. The receipt shows a converted line in the order's mode, by its share of the order's once-rounded tax, so the lines still add up.In priced mode, the Medusa traits' deprecated
getPriceandgetRegularPricereturn the resolved calculated price instead of the admin prices, andisSellableis false when no variant yields a price (for example acalculated_pricewith null amounts). -
#11
b1b6e30Thanks @kilbot! - Report product-level stock across all variants, showing the total quantity only when every variant has tracked, known stock. Draw the search magnifier with an attached, rounded handle and a larger ring.
@tallyui/database
Major Changes
-
#71
14871a2Thanks @kilbot! - Breaking:getStorage()on the web no longer returns Dexie. It throws with guidance to pass RxDB Premium's SQLite-wasm storage explicitly (@tallyui/storage-sqlite/web), bundling its worker entry — the web engine ADR-061 pins ascreateTallyDatabase's target. Any app that relied on the Dexie default breaks; switching is a cold resync, not a data migration.Breaking:
multiInstance: truenow throws for every storage (ADR-061): the pinned web engine (opfs-sahpool) cannot share exclusive OPFS handles between tabs, so multi-instance is unsupported until job 3 removes the option along with #42's outbox code.Breaking: the storage deadline is now a watchdog.
withWriteDeadline,STORAGE_WRITE_DEADLINE_MS,StorageWorkerTimeoutErrorandisStorageWorkerTimeoutare removed;isStorageWorkerFailurenow recognises onlyStorageWorkerStartError.createTallyDatabasewraps a storage markedtallyEngine: 'sqlite-sahpool'withwithStorageWatchdog, following WCPOS (ADR-061), and no storage call is ever settled on a clock, because a timed-out write may still commit:- A write pending longer than 10s (
STORAGE_WRITE_STALL_MS) is flagged asstalled, never rejected; its promise stays pending until the worker answers, and the status returns tookonce no stalled writes remain. - Reads are watched: two consecutive silent 30s windows (
STORAGE_READ_WATCHDOG_MS), with reads pending and no storage call settling, set the status todead, which is sticky. The recovery is to reload. - Creating the storage has no deadline, since the worker and wasm can be slow to download.
getStorageHealth(db)returns theObservable<StorageHealth>({ status: 'ok' | 'stalled' | 'dead', stalledWrites, stalledSince? }) for a database on that storage, andundefinedfor any other, so an app can show "saving is slow…" or "storage stopped, reload". - A write pending longer than 10s (
Minor Changes
-
#99
9649259Thanks @kilbot! -ReconcileFeedEntry(core) gainsrefreshOnly?: boolean: a missing product is skipped instead of tombstoned when its entry is refresh-only, so only the id reconcile's braked entries can delete (ADR-060, backlog 43). Merging inenqueuekeepsrefreshOnlytrue only when every entry queued for that id was refresh-only, so a deletable id-reconcile entry is never downgraded by a later refresh-only one. The fingerprint runner (startFingerprintReconcile, database) now enqueues its entries this way; the id runner is unchanged.FingerprintReconcileState(database) gainslastResultAt/lastErrorAt, stamped from an injectablenow(defaultDate.now), so a keptlastResultnext to a newerlastErrorcan be told apart from a current one. The newisFingerprintResultCurrent(state)helper does that comparison (backlog 46). -
#85
609ebd8Thanks @kilbot! - Add the fingerprint reconcile (ADR-060 amendment 8): a neutral runner that compares a remote fingerprint per product against the local documents and re-delivers products whose fingerprint differs, through the collection's pull.@tallyui/coreadds theFingerprintReconcileAdaptercontract (fetchPages, a purefingerprintandenqueue) and an optionalreconcile.pricesonTallyConnector.@tallyui/databaseaddsstartFingerprintReconcile, which runs no pass at start by default and otherwise mirrors the id reconcile: a complete, successful pass only,state$(running,lastResult,lastError), andstop().@tallyui/connector-medusaaddsreconcile.prices, a nightly base-price backstop (MEDUSA_PRICE_RECONCILE_INTERVAL_MS) for the variant feed (ADR-060 job D1): it fingerprints each product's base prices (variant id, currency and amount, sorted, price-list prices excluded) from/admin/product-variants. Nothing is written locally; corrections arrive only through the reconcile feed's pull. -
#61
540044cThanks @kilbot! - Add the id reconcile (ADR-060): a periodic pass that reads every live product id and its live variant ids, so a deleted product or a deleted variant (whose parent'supdatedAtdoes not change) reaches the local copy.@tallyui/coreadds theIdReconcileAdaptercontract andcreateReconcileFeed, which turns queued corrections into a pull-only adapter meant as the last key ofcombinePullAdapters.@tallyui/databaseadds thestartIdReconcilerunner.@tallyui/connector-vendureimplements the Vendure side and wires it intoreplication.productsandreconcile.ids. Nothing is written locally into the replicated collection; corrections arrive only through the collection's own pull. -
#69
0ef1c7eThanks @kilbot! - ADR-061:startLiveTab(@tallyui/database) coordinates exactly one live tab per store over a Web Lock and aBroadcastChannel. A new tab asks the live tab to hand over; the live tab may delay while busy, then parks and releases the lock; a tab that gets no acknowledgement is blocked and must be closed.LiveTabScreen(@tallyui/components) renders the parked and blocked screens, with translatable label props. On platforms without Web Locks (React Native, Node), a tab is simply live at once. -
#92
945bb83Thanks @kilbot! - Medusa prices as Medusa charges them (ADR-060 D2b).SyncContextgains an optionalpricingContext(fromstoreSettings()),ProductPricean optionaltaxInclusive, andTallyConnector.reconcileacalculatedPricesslot. With a pricing context, every Medusa product document build fills each variant'scalculated_pricefrom the store API (nullwhen the sales channel or region does not sell it), and the traits price from it: sale lists as a sale against the original price, override lists as the base price,nullas unsellable.reconcile.calculatedPricesre-delivers products whose calculated prices changed with no timestamp bump; run it everyMEDUSA_CALCULATED_PRICE_RECONCILE_INTERVAL_MS(30 minutes) withmaxPages: 1000. Without a pricing context, documents and prices are unchanged. -
#63
d9fe1e3Thanks @kilbot! - Fix the Medusa connector's incremental pull: Medusa 2.21 honours only the operator formupdated_at[$gte], and silently ignored the connector'supdated_at[gte], so every pass read the whole catalogue. Add the Medusa id reconcile (ADR-060), so a deleted product or a deleted variant (whose parent'supdated_atdoes not change) now reaches the local copy throughreplication.productsandreconcile.ids.@tallyui/databaseadds a mass-deletion brake tostartIdReconcile: a pass that would tombstone more thanmaxDeleteShare(default 20%) of local products, and more than 10 of them, queues nothing and warns instead, unlessallowMassDeleteis set. -
#102
7490a3fThanks @kilbot! - A connector schema version bump now drops and resyncs its collection (ADR-060 amendment 9).createTallyDatabaseadds RxDB's migration-schema plugin and gives each connector collection above version 0 av => nullstrategy per earlier version, andstartReplicationappends-v<version>to the replication identifier above version 0, so the pull starts from no checkpoint. Version 0 collections keep their identifier and never resync.stock_levelsandpos_ordersare untouched.The Medusa products schema is now version 1 and declares
variants[].calculated_price(object ornull). The first sync after upgrading resyncs the Medusa catalogue: the stored products are dropped when the database opens and download again, once, on the first sync. A collection created withmedusaProductSchemaoutsidecreateTallyDatabasemust useconnectorCollection(medusaProductSchema)from@tallyui/database, which supplies the strategies and the migration plugin; otherwise RxDB throws COL12. -
#74
0121559Thanks @kilbot! - Removes the unreleased multi-tab database machinery in favour of one live tab per store (ADR-061):CreateDatabaseOptions.multiInstance(createTallyDatabasealways passesmultiInstance: false), thetally-outbox-flushandtally-outbox-statelocal documents, and follower forwarding between tabs are all gone. The outbox's public API (flush,requeue,start,stop,state$) and its single-instance behaviour are unchanged. Apps enforce one live tab withstartLiveTab.@tallyui/storage-sqlite's worker now swallows thereadypromise's rejection so a pool install failure before anycreateStorageInstancecall doesn't surface as an unhandled rejection, and itsfileslist no longer publishes test files, matching@tallyui/databaseand@tallyui/pos. -
#55
350967dThanks @kilbot! - Stock reads use the reconciled overlay (ADR-060) and show how fresh it is.@tallyui/corenow holdswithStockOverlayandgetProductStock(@tallyui/posre-exports them), addsSTOCK_LEVELS_LAST_PASS,stockOverlayandstockOverlayAsOfprops onConnectorProvider, and auseProductStock(doc)hook that returns overlay stock plusasOf, orgetStock(doc)when no overlay is given.@tallyui/database:startStockReconcilealso returnsstate$(running,truncated,lastError,lastCompletedAt),reconcileStock()resolves withcompletedAt, and each successful pass stores{ completedAt }in thelast-passlocal document ofstock_levels, whichcreateTallyDatabasenow creates with local documents (apps that create the collection themselves use the newstockLevelsCollectionconfig; without local documents a pass rejects with a clear error); a restarted runner seedslastCompletedAtfrom it.@tallyui/posaddsstockOverlayAsOf$.ProductStockBadgereads stock throughuseProductStockand appends " · as of <time>" when an overlay is given (showAsOf={false}hides it). -
#53
e3b8686Thanks @kilbot! - Add a stock reconcile pass (ADR-060).@tallyui/coreadds theStockReconcileAdaptercontract (fetchPagesand a pureoverlay) and an optionalreconcile.stockonTallyConnector.@tallyui/databaseadds the local-onlystock_levelscollection (STOCK_LEVELS_COLLECTION,stockLevelsSchema), whichcreateTallyDatabasecreates for connectors withreconcile.stock, andstartStockReconcile, which re-reads stock every 5 minutes (and on demand throughreconcileStock()) into that collection: it writes only changed rows, removes keys the backend no longer returns, writes nothing after a failed, truncated or stopped read, and never writes the replicated products.@tallyui/posaddsstockOverlay$,withStockOverlayandgetProductStock, which read stock from the overlay where it has an entry and from the replicated product otherwise. The Vendure connector reconciles variantstockLevels, and the Medusa connector reconciles inventory item location levels, so stock changes that bump no product timestamp reach the POS.
Patch Changes
-
#66
b029839Thanks @kilbot! - The id reconcile's mass-delete brake now also trips when every local product would be tombstoned, whatever the count, unlessallowMassDeleteis set. Previously the brake applied only aboveMASS_DELETE_MINIMUM(10) would-be tombstones, so a wrong channel token that made a shop of 10 or fewer products look empty could tombstone its whole catalogue. -
#11
184901fThanks @kilbot! -createTallyDatabasenow wraps its storage in the AJV schema validator in development. RxDB dev-mode refuses to create a database without one (error DVM1), so every app using the default options failed at startup. -
#14
c9b0bb8Thanks @kilbot! -createTallyDatabasenow works in production builds. It passedignoreDuplicate: trueunconditionally, which RxDB rejects with error DB9 whenever dev mode is off, so no production app could open its database. The option is now only set in dev mode, where hot reload still re-creates a database with the same name. -
#78
9168278Thanks @kilbot! - The live tab's busy-defer now caps by wall-clock time elapsed since the hand-over ack, not by countingsleep(100)calls. The live tab is in the background exactly when a new tab asks it to hand over, and browsers throttle background timers to about 1 s or more, so a sleep count could stretch the intendedmaxDeferMs(10 s by default) far beyond that, leaving the new tab stuck inacquiring. -
#157
125c85aThanks @kilbot! -readFresh,countFreshandwatchFreshmove to a new, side-effect-free subpath,@tallyui/core/rxdb. Core now listsrxdb(>=16) andrxjs(>=7) as optional peer dependencies, needed only by that subpath; core's main entry stays free of both.@tallyui/posre-exports the helpers unchanged. The id and fingerprint reconciles in@tallyui/databaseread the local products withreadFreshinstead of a cachedfind(), so a product the pull inserts or deletes while a pass reads them no longer leaves every later pass reading a stale list (RxDB 16.21.1 bug 4): an inserted product is now checked, and tombstoned or re-fetched, on the next pass, and a deleted one is no longer re-enqueued or counted towards the mass-delete brake. -
#108
caa1fd2Thanks @kilbot! - A reconcile trigger during a pass now runs one follow-up pass instead of being dropped.
@tallyui/primitives
Minor Changes
Patch Changes
@tallyui/components
Major Changes
-
#23
53af670Thanks @kilbot! - Breaking: the cart and checkout components are presentational and takeMoney.CartLinetakesname,quantity,unitPriceandlineTotal.CartTotaltakessubtotal,taxLines,discountandtotal.CashTenderedandChangeDisplaytakeMoneyamounts. All of them format withformatMoney, so there is nogetPrice, float arithmetic or hard-coded'$'.CartPanelis generic, andCartLineItemis removed. Totals come from@tallyui/pos; the components no longer compute tax. The cash input keeps the text as typed and emits integer minor units.@tallyui/coreaddsmoneyFromDecimalString, which parses typed decimal text intoMoneyusing integer arithmetic.
Minor Changes
-
#136
0b1237fThanks @kilbot! -CartPanel's footer (totals, pay button) now stays pinned to the bottom of the panel at every height, on web and native, instead of being pushed off screen by a tall list of lines.CartPanelalso gains anafterItemsslot, rendered inside the scrolling region after the last line, for content like discount chips. -
#148
a80e053Thanks @kilbot! -Cataloguetakes an optionalminCodeLengthprop (a till's barcode-scanner setting). When set, Enter on a search query shorter thanminCodeLength(after trimming) no longer does a barcode/SKU lookup — it leaves the typed text as a plain search instead of selecting an entry. Unset, behaviour is unchanged. A scanner's timing threshold stays the app's own concern, in its unfocused wedge listener. -
#175
a9b77feThanks @kilbot! - One close in flight per register.useRegisterSession'scloseSessionjoins a close already running for the same register, in any hook instance, and returns its closure (the joining call'scounted,approvedByandapprovedByNameare ignored), so a tap during a close no longer starts a second, overlapping one. The hook returns a newclosingflag, true while that close is in flight.RegisterColumnkeeps the count slot up while closing instead of flashing the Finish-closing card, whose button now hasnativeID="register-column-finish-close-button".describeRegisterBarPilltakesclosingand returns the new'Close not finished'pill for a closed session that isn't closing, right after 'Choose a register' and ahead of 'Offline';RegisterBarpassesregister.closing. -
#174
160252cThanks @kilbot! -ClosureSheetshows who approved the close (Approved by {name}, falling back to the approver id without a name) under the figures, blind or not — it's provenance, not a counted figure.RegisterColumnoffers "Finish closing" whenuseRegisterSession's session is closed but its closure row was never written (an interrupted close), resuming the close (the store keeps the count persisted on the session) instead of falling through to the cart, whereopenSessionwould otherwise refuse withRegisterCloseIncompleteError.buildClosureDocument's return type now carriesclosure.unsynced_count: numberand each movement'sid/reasonasstring, without a cast; no runtime output changed. -
#107
f132a68Thanks @kilbot! -ConnectorStatusgainsunsoldCount,unsoldStaleandformatUnsoldprops, showing how many products the sales channel doesn't sell (the calculated-price runner'sunreported) belowlastSync, using thewarningtoken when current andmuted-foregroundwhen stale. -
#149
1e2ee56Thanks @kilbot! - A failed save can now end in Continue once its order is confirmed stored.useOrderOutboxgainsisStored(order), andrecordnow treats an order stored with the sameidand money-bearing content (sameSale, new) as stored whatever itscommandId, so a Retry after a requeue no longer fails forever; other content throws the newOrderContentMismatchError.useSaletakes an optionalisStoredand exposescanContinueandcontinueSale();Tenderrenders Continue whencanContinueis true.newSale()is now refused while a failed or running save's order isn't confirmed stored; a refusal during a running save asksisStoredagain, so a hung save whose order is stored can still Continue. A confirmed order is never handed toonSaleCompletedagain (#147's background re-hand is gone).saleLoggerandoutboxLoggerare now exported. -
#132
7c69fceThanks @kilbot! -order.displaygainslinesandorderDiscountMinor(ADR-063). Each line shows its amount before any discount, with its own discounts as sub-rows, all in the display mode. The order discounts appear as one row, not allocated to the lines. Every discount row is its own-mode amount converted on its own, so it's exact.display.subtotalMinoris now derived from the total and the discount rows, soΣ lines === subtotalMinorandΣ sub-rows + orderDiscountMinor === discountMinorhold exactly. Single-mode carts show the same figures as before; mixed-mode carts can shift by about a cent, carried by the last converted line's amount.ReceiptLineItemgainsdisplayAmountMinoranddisplayDiscounts, andReceiptDatagainsorderDiscountMinor. Print these above the subtotal.lineTotalMinoris unchanged: it's after every discount and is kept for existing readers.CartTotalnow orders its rows Subtotal / Discount / Tax / Total, matching the receipt, and takes an optionaltaxInclusive, which labels the tax rows "incl." instead of adding them. -
#65
a0b7981Thanks @kilbot! -@tallyui/coreaddsresolvePriceRange(variants, currency?), the lowest and highest current price across a product's variants.ProductPriceuses it to showfrom <lowest price>when a product's variants are priced differently, instead of just the default variant's price. NewshowFromPrice(defaulttrue) andfromLabel(default'from') props control and opt out of this. -
#141
6a4e80dThanks @kilbot! - Lifted medusapos's product catalogue, receipt, print-style hook and sync status into@tallyui/components(Catalogue,Receipt,injectPrintStyle,SyncStatus), so every platform POS gets the same screens (ADR-052, TV6b).Cataloguetakes an optionalhour12?: boolean(undefined keeps the locale default) instead of readingexpo-localization.Receipttakesstore: { name: string; address?: string }instead of a Medusa-shaped settings type, an optionaltopInset?: number(default 0) instead of an app-local strip-height context, an optionalformatDate?: (iso: string) => stringdefaulting to anIntl.DateTimeFormatformatter, and an optionaltaxLabel?: (ratePpm: number) => stringwith the same default asCart's (TV6a), keeping theincl.prefix rule.searchProducts,catalogueEntries,findEntryByCodeandvariantPriceLabeljoinbuildReceiptDataon the pure-function allow-list components may import from@tallyui/pos(ADR-064). -
#142
60a2218Thanks @kilbot! - Lifted medusapos's neutral outbox core so every platform POS records and sends sales the same way (ADR-052, TV7).@tallyui/posgainsgetDeviceId(storage, key), which keeps a UUIDv7 device id in web storage under the given key and falls back to one id per process;needsAttention(orders), which picks rejected and applied-with-warnings orders, newest first; anduseOrderOutbox({ storeKey, open, transport, deviceId, onBusy?, onOpenError? }), which opens the order store forstoreKey, runs its outbox and returns{ orders, state, recent, record, flush, requeue }.@tallyui/componentsgainsOrdersList, the "Needs attention" and "Recent" orders with a Retry button, takingorders,onRetry, an optionalformatDate(defaultIntl.DateTimeFormat) and an optionalfooter.needsAttentionjoins the pure-function allow-list components may import from@tallyui/pos(ADR-064). -
#139
3dd11f6Thanks @kilbot! - Lifted medusapos's cart, phone cart bar, discount form and tender screen into@tallyui/components(Cart,CartBar,Tender,DiscountForm,DiscountChips,parseDiscount,discountLabel), so every platform POS gets the same sale-column UI (ADR-052, TV6a).Carttakes an optionaltaxLabel?: (ratePpm: number) => string(default`Tax ${ratePpm / 10000}%`), since VAT isn't universal.@tallyui/componentsgains a runtime dependency on@tallyui/pos, for types and the purebuildReceiptDataonly — components still render from props alone. -
#69
0ef1c7eThanks @kilbot! - ADR-061:startLiveTab(@tallyui/database) coordinates exactly one live tab per store over a Web Lock and aBroadcastChannel. A new tab asks the live tab to hand over; the live tab may delay while busy, then parks and releases the lock; a tab that gets no acknowledgement is blocked and must be closed.LiveTabScreen(@tallyui/components) renders the parked and blocked screens, with translatable label props. On platforms without Web Locks (React Native, Node), a tab is simply live at once. -
#11
2a05ecbThanks @kilbot! - Build the product components on the neutral traits.ProductPriceresolves the price list and formats it with Intl in the price's own currency (newcurrencyandlocaleprops;currencySymbolis now only the fallback for an unknown currency).ProductStockBadgereadsgetStock.ProductImagegainsshowPlaceholder, an initial tile for products without images. Core addsformatMoney, atraitContextprop onConnectorProviderfor store-level facts like the store currency, anduseTraitContext.@tallyui/posaddssearchProducts, name/SKU/barcode search through traits that works the same on every backend. -
#144
e692c40Thanks @kilbot! -pos_ordersgoes to schema version 2 (ADR-032, ADR-065). It adds three optional fields:lateSessionId, and ADR-065'sdisplayandtaxByRate, which nothing writes yet. Apps must adopt this release'saddPosOrderCollection, which migratespos_ordersto version 2 from version 0 or 1 without dropping an order.A sale whose session refuses the stamp in
useSale().complete()(the session closed or went missing) is no longer stopped, because the money has been taken. It goes on toonSaleCompletedand the receipt withlateSessionIdset and nosessionId, so no closure counts it, and alate-saleregister fact is recorded.needsAttentionnow also selects any order withlateSessionId, andOrdersListexplains it: "Taken after the register closed. It is not in that register's closure." -
#76
e225222Thanks @kilbot! -ProductPricegains aformatFromprop,(price: string) => string, for languages whose word order puts the "from" label after the price (formatFrom={(p) => \${p} ab\}renders€10.00 ab);fromLabelstays as a deprecated alias. Visible change: every price — the regular price, the "from" range and the sale price — now uses the theme'stext-price(ortext-sale) token instead oftext-foreground. Apps with screenshot tests coveringProductPricewill need to re-shoot them. -
#167
62eef0fThanks @kilbot! - Register-selection screen nits (ADR-032 amendment 1, medusapos adopting451a0ca):RegisterPicker's rows now size to their content, with a minimum height, instead of clipping the "Not opened" second line at a fixedh-11;RegisterPickerandOpenRegisterCardno longer hard-codeflex-1, taking their existingclassNamefrom the caller instead (TallyUI's ownRegisterColumnstill passesflex-1where it mounts them);RegisterPanel's sales count now pluralises correctly ("1 sale this session", not "1 sales"); andOpenRegisterCard's amount label names the currency, matchingMovementSheet's "Amount (€)" ("Cash in the drawer to start (€)").Two new optional props for apps that need to put register controls elsewhere on the screen:
RegisterBartakes anonPressPill?: () => voidthat turns its status pill into a button (opening the gate/picker or the panel), andCataloguetakes astatusAccessory?: ReactNoderendered at the end of its status line, alongside the status text, so a register control can sit there at phone width. -
#164
24b0563Thanks @kilbot! - Register screens (WCPOSnextport, ADR-032), driven byuseRegisterSession:RegisterPicker,OpenRegisterCard,RegisterBar(one status pill; "Register ›"),MovementSheet(labelled "Amount" and "Reason"; a reason for every movement; same-tick taps coalesced),RegisterPanel(expected in the drawer; Undo by reversal; blind mode hides amounts) andRegisterColumn.@tallyui/coreaddscurrencySymbol(currency, locale?). -
#166
c65da52Thanks @kilbot! -RegisterCountandClosureSheet(WCPOSnextport, ADR-032 amendment 1): denomination tiles counted in minor units (tap adds one, a 400ms hold adds ten), typing a cash amount clears the tiles, a live variance line hidden while blind, and Close gated by an optionalapproveprop abovevarianceThreshold(refused with an exact message without one).ClosureSheetshows the local closure number and, unless blind, the counted figures and variance per tender. -
#55
350967dThanks @kilbot! - Stock reads use the reconciled overlay (ADR-060) and show how fresh it is.@tallyui/corenow holdswithStockOverlayandgetProductStock(@tallyui/posre-exports them), addsSTOCK_LEVELS_LAST_PASS,stockOverlayandstockOverlayAsOfprops onConnectorProvider, and auseProductStock(doc)hook that returns overlay stock plusasOf, orgetStock(doc)when no overlay is given.@tallyui/database:startStockReconcilealso returnsstate$(running,truncated,lastError,lastCompletedAt),reconcileStock()resolves withcompletedAt, and each successful pass stores{ completedAt }in thelast-passlocal document ofstock_levels, whichcreateTallyDatabasenow creates with local documents (apps that create the collection themselves use the newstockLevelsCollectionconfig; without local documents a pass rejects with a clear error); a restarted runner seedslastCompletedAtfrom it.@tallyui/posaddsstockOverlayAsOf$.ProductStockBadgereads stock throughuseProductStockand appends " · as of <time>" when an overlay is given (showAsOf={false}hides it). -
#96
93658cdThanks @kilbot! -StoreSettingsChoiceScreen: the picker the app shows whenstoreSettingsrejects withchoice_required(TV4), for a store with several regions, countries or sales channels — medusa-dev's one region with 7 countries always hits this. Renders a radio-row section per choice offered, pre-selects a single-option section or a matchinginitialvalue, and callsonSubmitwith the picked fields once every shown section has a selection. -
#12
fe65b33Thanks @kilbot! - Add createSvgIcon and SearchIcon, and replace the SearchInput text glyph with an SVG magnifier. react-native-svg >=15 is now a peer dependency. -
#104
bc46d99Thanks @kilbot! -ProductGridhides products this channel doesn't sell (traits.isSellablefalse, for example a Medusa product outside the sales channel) unlessshowUnsellableis set, andProductCardshows such a product in a muted "Not sold here" state instead of its price.OrderBuilder.addProductnow refuses an unsellable product before looking up its price, instead of throwing the unrelated "No price in …" error.
Patch Changes
-
#168
670d3b8Thanks @kilbot! - The register approval gate is enforced inuseRegisterSession'scloseSession, not only inRegisterCount: overvarianceThreshold, a close withoutapprovedBythrows the newRegisterApprovalRequiredErrorbefore any write, blind mode included.closeSessiontakesapprovedByandapprovedByName, and they reach the Z (breakdowns.approved_by,approved_by_name); the store'scloseSessiontakesapprovedBy.useRegisterSession'sregisteroption acceptsnullwhile its host opens.closeNeedsApprovalis the shared "over threshold" rule.RegisterCountpassesapprove()'sapprovedByandapprovedByNametocloseSession, and shows the hook's refusal with the same copy. -
#137
314d1fcThanks @kilbot! -CartPanelrows now default to keying by the item'sid(string or number), falling back to index only for items without one, instead of always keying by index — so removing a line above another with an open inline form (e.g. a per-line discount editor) no longer remounts and loses that form's state. PasskeyExtractorto override. -
#169
6c9a176Thanks @kilbot! -Catalogue's status text now truncates to a single line (with an ellipsis) whenstatusAccessoryis set, instead of wrapping to three lines at phone width and pushing the accessory (e.g. a register pill) out of place. WithoutstatusAccessory, the status text still wraps as before. -
#43
f1e2a02Thanks @kilbot! - Status badges use foreground text on their tint (the dot carries the colour), since coloured text on a 15% tint of itself fails WCAG AA; the refunded order badge uses the destructive token instead of the undefineddanger. Search, cart-note and customer inputs take their placeholder colour from themuted-foregroundtoken. -
#37
12b7723Thanks @kilbot! - Order cards, receipt preview, register summary, settings groups, connector status, variant picker, cash-tendered input, cart note and customer form used the undefinedbg-surfaceclass and rendered transparent; they now usebg-card. -
#88
5053527Thanks @kilbot! - no longer publishes test files -
#11
b1b6e30Thanks @kilbot! - Report product-level stock across all variants, showing the total quantity only when every variant has tracked, known stock. Draw the search magnifier with an attached, rounded handle and a larger ring. -
#36
d1bc892Thanks @kilbot! - Light theme contrast: primary darkened to #5b5ef0 (white text 4.88:1), muted-foreground to #656c79 and input to #848a94 (control borders ≥ 3:1). Product cards and quick-tender buttons get borders, and components no longer use the undefinedbg-surface-altortext-mutedtext classes, so labels such as "Change Due" are legible.
@tallyui/storage-sqlite
Minor Changes
-
#33
a0d2b90Thanks @kilbot! -getRxStorageSQLite(database)now returns RxDB Premium's SQLite storage instead of TallyUI's own engine, so writes are transactional and queries run on real SQLite. The call is unchanged: pass a synchronous SQLite handle such as expo-sqlite'sopenDatabaseSync(...).rxdb-premium@16.21.1is now a peer dependency that your app installs under its own RxDB Premium licence. Use one handle per RxDB database; the storage never closes the handle, your app does. -
#70
c15c04dThanks @kilbot! - Adds the web storage (ADR-061):getRxStorageSQLiteWasmfrom@tallyui/storage-sqlite/webruns RxDB Premium's SQLite storage on@sqlite.org/sqlite-wasm's opfs-sahpool VFS, in one dedicated worker owned by the live tab, reached through premium'sgetRxStorageWorkerin mode'one'. The app supplies the worker input, for example() => new Worker(new URL('@tallyui/storage-sqlite/web-worker', import.meta.url), { type: 'module' }).@sqlite.org/sqlite-wasmis now a peer dependency that your app installs. The returned storage carriestallyEngine: 'sqlite-sahpool'so callers can recognise it. The root export is unchanged and does not pull in wasm. -
#71
14871a2Thanks @kilbot! - Addedtallyui-build-sqlite-worker, a bin script that prebuilds the web-worker entry (with esbuild) and copiessqlite3.wasmnext to it, for Metro/Expo web apps that can't bundle a module worker at runtime. -
#100
94e1f09Thanks @kilbot! -getRxStorageSQLiteWasmreturns a storage withterminate(), which stops its worker and clears the cached channel, so a fresh open after the live-tab park no longer hangs.
Patch Changes
-
#74
0121559Thanks @kilbot! - Removes the unreleased multi-tab database machinery in favour of one live tab per store (ADR-061):CreateDatabaseOptions.multiInstance(createTallyDatabasealways passesmultiInstance: false), thetally-outbox-flushandtally-outbox-statelocal documents, and follower forwarding between tabs are all gone. The outbox's public API (flush,requeue,start,stop,state$) and its single-instance behaviour are unchanged. Apps enforce one live tab withstartLiveTab.@tallyui/storage-sqlite's worker now swallows thereadypromise's rejection so a pool install failure before anycreateStorageInstancecall doesn't surface as an unhandled rejection, and itsfileslist no longer publishes test files, matching@tallyui/databaseand@tallyui/pos. -
#72
91d9169Thanks @kilbot! - The worker listens before start-up finishes, fixing a hang on every cold start in real browsers. -
#91
fec2ee7Thanks @kilbot! - The worker build bin no longer needs a built dist.
@tallyui/pos
Major Changes
- #24
e7b2fa5Thanks @kilbot! - Breaking: removes the floating-point money and rate paths.calculateTax,extractTax,addTax,TaxResultandformatCurrencyare deleted. Use the exact tax API (computeOrderTax,taxMicros) andformatMoney.TaxContext.getTaxRate(), which returned a fraction, is replaced bygetTaxRatePpm(), which returns integer parts per million.TaxProvidertakesratesPpmand validates it.useCurrencyFormatter()now formatsMoney, anduseCurrencyCode()is added.
Minor Changes
-
#168
670d3b8Thanks @kilbot! - The register approval gate is enforced inuseRegisterSession'scloseSession, not only inRegisterCount: overvarianceThreshold, a close withoutapprovedBythrows the newRegisterApprovalRequiredErrorbefore any write, blind mode included.closeSessiontakesapprovedByandapprovedByName, and they reach the Z (breakdowns.approved_by,approved_by_name); the store'scloseSessiontakesapprovedBy.useRegisterSession'sregisteroption acceptsnullwhile its host opens.closeNeedsApprovalis the shared "over threshold" rule.RegisterCountpassesapprove()'sapprovedByandapprovedByNametocloseSession, and shows the hook's refusal with the same copy. -
#175
a9b77feThanks @kilbot! - One close in flight per register.useRegisterSession'scloseSessionjoins a close already running for the same register, in any hook instance, and returns its closure (the joining call'scounted,approvedByandapprovedByNameare ignored), so a tap during a close no longer starts a second, overlapping one. The hook returns a newclosingflag, true while that close is in flight.RegisterColumnkeeps the count slot up while closing instead of flashing the Finish-closing card, whose button now hasnativeID="register-column-finish-close-button".describeRegisterBarPilltakesclosingand returns the new'Close not finished'pill for a closed session that isn't closing, right after 'Choose a register' and ahead of 'Offline';RegisterBarpassesregister.closing. -
#149
1e2ee56Thanks @kilbot! - A failed save can now end in Continue once its order is confirmed stored.useOrderOutboxgainsisStored(order), andrecordnow treats an order stored with the sameidand money-bearing content (sameSale, new) as stored whatever itscommandId, so a Retry after a requeue no longer fails forever; other content throws the newOrderContentMismatchError.useSaletakes an optionalisStoredand exposescanContinueandcontinueSale();Tenderrenders Continue whencanContinueis true.newSale()is now refused while a failed or running save's order isn't confirmed stored; a refusal during a running save asksisStoredagain, so a hung save whose order is stored can still Continue. A confirmed order is never handed toonSaleCompletedagain (#147's background re-hand is gone).saleLoggerandoutboxLoggerare now exported. -
#115
4df9be4Thanks @kilbot! - Discounts are pre-tax (ADR-062). An order discount is allocated across the lines in proportion to their own-mode amounts (the newallocateOrderDiscount, largest-remainder rounding), each line carries its share inorderDiscountMinorand is taxed after it, so an order discount now lowers the tax instead of coming off the total after tax. A discountedorder.createis version 2, withdiscountMinoron each discounted line and on the payload; a discount-free payload stays version 1, byte-identical.finalizestill rejects discounts until the plugins honour version 2. Receipt lines show theirdiscountMinor. Stacked percentage order discounts are additive, each computed on the pre-order-discount base rather than compounding on what an earlier discount leaves, and every discount (line or order, percentage or fixed) is clamped to 0 so a negative value can never raise a price. -
#132
7c69fceThanks @kilbot! -order.displaygainslinesandorderDiscountMinor(ADR-063). Each line shows its amount before any discount, with its own discounts as sub-rows, all in the display mode. The order discounts appear as one row, not allocated to the lines. Every discount row is its own-mode amount converted on its own, so it's exact.display.subtotalMinoris now derived from the total and the discount rows, soΣ lines === subtotalMinorandΣ sub-rows + orderDiscountMinor === discountMinorhold exactly. Single-mode carts show the same figures as before; mixed-mode carts can shift by about a cent, carried by the last converted line's amount.ReceiptLineItemgainsdisplayAmountMinoranddisplayDiscounts, andReceiptDatagainsorderDiscountMinor. Print these above the subtotal.lineTotalMinoris unchanged: it's after every discount and is kept for existing readers.CartTotalnow orders its rows Subtotal / Discount / Tax / Total, matching the receipt, and takes an optionaltaxInclusive, which labels the tax rows "incl." instead of adding them. -
#127
2a0ca7fThanks @kilbot! -Ordergainsdisplay: DisplayTotals(ADR-063): the cart's subtotal before discounts, the discount, the tax and the total in the store's display mode, which add up on screen even in a mixed-mode cart. The settlement figures (subtotalMinor,discountMinor,taxMinor,totalMinor) and theorder.createpayload are unchanged;displayis never sent to the server; a parked draft may carry it, but it is recomputed on resume. -
#163
714b4bdThanks @kilbot! -useSale's hung-save check guard is now scoped per completion instead of a shared boolean: a sale whoseisStorednever settles can no longer block a later sale's own hung-save poll from ever confirming and offering Continue (medusapos's #85 review).useOrderOutbox'sisStorednow logs "A stored order has this id with different content" at most once per order id for the hook's lifetime, instead of on every 5 s poll of a hung save.useOrderOutboxalso exposessavesInFlight: number, the count ofrecord()calls not yet settled, so an app can hold sign-out while a save is in flight. -
#161
f4a4d74Thanks @kilbot! - A hung save — one whose order is built and whose save neither resolves nor throws — now re-asksisStoredby itself every 5 s while it stays unconfirmed, souseSalesetscanContinueandTendercan offer Continue with no user action. This covers an app whose tender has no New sale control while saving (medusapos), which never triggered the existing refused-newSale()check. The poll clears when the save settles, on confirmation, onnewSale()/continueSale()and on unmount; at most one runs at a time.SALE_SAVINGis now exported from@tallyui/pos. -
#142
60a2218Thanks @kilbot! - Lifted medusapos's neutral outbox core so every platform POS records and sends sales the same way (ADR-052, TV7).@tallyui/posgainsgetDeviceId(storage, key), which keeps a UUIDv7 device id in web storage under the given key and falls back to one id per process;needsAttention(orders), which picks rejected and applied-with-warnings orders, newest first; anduseOrderOutbox({ storeKey, open, transport, deviceId, onBusy?, onOpenError? }), which opens the order store forstoreKey, runs its outbox and returns{ orders, state, recent, record, flush, requeue }.@tallyui/componentsgainsOrdersList, the "Needs attention" and "Recent" orders with a Retry button, takingorders,onRetry, an optionalformatDate(defaultIntl.DateTimeFormat) and an optionalfooter.needsAttentionjoins the pure-function allow-list components may import from@tallyui/pos(ADR-064). -
#138
f09dbbcThanks @kilbot! -@tallyui/posgainsuseSale,addEntryToCart/CartErrorandcatalogueEntries/findEntryByCode/variantPriceLabel(ADR-052, TV5), lifted from medusapos/appa1b981d'suse-sale,lib/cartandlib/cataloguewith the same behaviour.useSaletakes an optionalsession, and stamps a completed sale withstampSessionbeforeonSaleCompleted; without it, behaviour is unchanged. -
#151
c664d4bThanks @kilbot! - New exportwatchFresh(collection, query): a live list onreadFreshinstead of a cachedfind().$, so a write during a query's storage read (RxDB 16.21.1 bug 4) still shows up once its change event arrives.useRegisterSessionanduseOrderOutbox's recent list now use it. -
#11
2a05ecbThanks @kilbot! - Build the product components on the neutral traits.ProductPriceresolves the price list and formats it with Intl in the price's own currency (newcurrencyandlocaleprops;currencySymbolis now only the fallback for an unknown currency).ProductStockBadgereadsgetStock.ProductImagegainsshowPlaceholder, an initial tile for products without images. Core addsformatMoney, atraitContextprop onConnectorProviderfor store-level facts like the store currency, anduseTraitContext.@tallyui/posaddssearchProducts, name/SKU/barcode search through traits that works the same on every backend. -
#120
e0062ceThanks @kilbot! - A per-storeorder.createcapability check replaces the global discount guard (ADR-062).@tallyui/coregainsServerCapabilities,SignInResult.capabilities,SyncContext.capabilities,TallyConnector.capabilities?()andresolveCapabilities(fresh, stored).@tallyui/connector-medusareads the store's supportedorder.createversions fromGET /tally/v1/info: a 404 or a malformed response means an old plugin (version 1), a network failure or a 5xx is unknown and keeps the last known value, and a 401 throws.medusaSignInreturns the read capabilities, and both Medusa connectors exposecapabilities(context)for a restored session.finalizeOrderin@tallyui/posnow rejects a discount only when the store's capability is below 2, so a store whose plugin has caught up finalizes a discounted order asorder.createversion 2. -
#17
47b9b4eThanks @kilbot! - Adds an exact, integer-only tax API:ratePpmFromPercent,taxMicros,roundMicrosToMinorandcomputeOrderTax. Line tax is kept exactly in micro-minor-units (as abigint) and rounded once, half away from zero, at the order total. This matches Medusa, which keeps line tax unrounded and rounds only at payment. The existing float tax functions are unchanged for now. -
#21
64cb5e0Thanks @kilbot! - Adds thePosOrderdocument, the neutral record of a completed sale.finalizeOrderturns a fully paid builderOrderinto a pendingPosOrder: UUIDv7 ids, cash change allocated so that payments reconcile to the total exactly, and a refusal of discounted orders until the command contract supports discounts.toOrderCreateEnvelopemaps it to the TallyUI Sync Protocolorder.createcommand,posOrderSchemastores it in apos_ordersRxDB collection, anduuidv7generates RFC 9562 ids. -
#20
98ea990Thanks @kilbot! - Breaking (pre-1.0): the order model now uses integer minor units throughout.Order,LineItem,Payment, discounts andReceiptDatamoney fields carry aMinorsuffix (totalMinor,unitPriceMinor,amountMinor, ...). Tax is exact and rounded once per order (thetax/exactAPI). Lines carry stackedtaxLines. The receipt's per-rate tax lines always sum to the charged tax (largest remainder).addProductprices fromgetPricesandresolvePricerather than the deprecatedgetPrice. The newaddLineadds a specific variant at a given price with optional tax rates. Parked orders resume throughaddLine, with no synthetic traits. -
#22
55d52faThanks @kilbot! - Adds the order outbox.createOrderOutboxsends pendingPosOrderdocuments from thepos_orderscollection through the TallyUI Sync Protocol, in batches of up to 10. It applies each result (applied,duplicateorrejected) with guarded patches and never drops a sale: transport failures and responses that make no progress retry forever, with jittered exponential backoff.createHttpCommandTransportposts to/tally/v1/commandswith the protocol header, and classifies every non-200 or malformed reply as retryable. -
#131
fbcaf59Thanks @kilbot! -addPosOrderCollection(db)is now the way to openpos_orders. It resolves only once every version-0 order has migrated, and on DM4 it rejects after the migration has stopped, keeping every order. RxDB's own open path could report the collection ready before orders written after a rollback had moved, and after a DM4 it rejected at once while the migration carried on, so on SQLite a close could interrupt it on every open. -
#144
e692c40Thanks @kilbot! -pos_ordersgoes to schema version 2 (ADR-032, ADR-065). It adds three optional fields:lateSessionId, and ADR-065'sdisplayandtaxByRate, which nothing writes yet. Apps must adopt this release'saddPosOrderCollection, which migratespos_ordersto version 2 from version 0 or 1 without dropping an order.A sale whose session refuses the stamp in
useSale().complete()(the session closed or went missing) is no longer stopped, because the money has been taken. It goes on toonSaleCompletedand the receipt withlateSessionIdset and nosessionId, so no closure counts it, and alate-saleregister fact is recorded.needsAttentionnow also selects any order withlateSessionId, andOrdersListexplains it: "Taken after the register closed. It is not in that register's closure." -
#39
f2f386cThanks @kilbot! - The command outbox no longer retries every HTTP error forever. After 3 consecutive 401s it pauses and setsauthRequiredin its state so the app can ask the cashier to sign in, then resumes on the nextflush(). A permanent refusal of a whole batch (400, 403, 413, 415, 422) changes no order: sending pauses withrefused: { status, reason }in the state until the nextflush().requeue(orderIds?)moves rejected orders back to pending with a new commandId, except those rejected withidempotency_mismatch, which are left for reconciliation.TransportOutcomegainsunauthorizedandrefusedkinds. -
#94
373e438Thanks @kilbot! -resolvePricekeeps a price'staxInclusiveflag oncurrentandwas. Each order-builder line keeps its price's own tax mode (LineItem.taxInclusive, pluspriceTaxModeConvertedwhen it differs from the store'spricesIncludeTax), so a customer pays exactly the shelf price and an inclusive price in an exclusive store is no longer taxed twice. Orders whose prices carry no flag, or one that agrees with the store, total exactly as before. The receipt shows a converted line in the order's mode, by its share of the order's once-rounded tax, so the lines still add up.In priced mode, the Medusa traits' deprecated
getPriceandgetRegularPricereturn the resolved calculated price instead of the admin prices, andisSellableis false when no variant yields a price (for example acalculated_pricewith null amounts). -
#130
516850fThanks @kilbot! -ReceiptData.totalsnow comes fromorder.display(ADR-063), not the settlement fields:subtotalMinoris before discounts anddiscountMinoris every discount, both in the store's display mode, and a newtaxInclusiveflag says whether the tax is added or already included.taxMinorandtotalMinorare unchanged in value. This is a behaviour change for anything that readsReceiptData.totals: the receipt's invariant is nowΣ lineTotalMinor === totals.subtotalMinor − totals.discountMinor, plus+ totals.taxMinor === totals.totalMinorwhen exclusive, or=== totals.totalMinorwhen inclusive. Receipt lines and each line's own discount row are unchanged, still in the line's own mode. -
#125
0988fc3Thanks @kilbot! - Adds the register closure's pure report maths (ADR-032, registers job b1), ported from WCPOSnextat3b5331b5c: settled figures after corrections (deriveSettled,Correction,RecordedFigures), a CSV export of the shown closures (exportCsv), the offline document label keys (labelKeys), and the closures list's scope clamp and row selector (clampClosureScope,selectClosureRows,ClosureScope). Money is a2's integer minor units throughout, andclampClosureScope's history window is ahistoryDaysparameter (default 92, WCPOS'sHISTORY_DAYS) instead of a@wcpos/sync-coreimport. -
#134
1d13699Thanks @kilbot! - Add the register's closure and X-report documents (buildClosureDocument,buildXReportDocument,formatClosureDate,ClosureContext) and register facts (RegisterFact,recordRegisterFact), ported from WCPOSnext(ADR-032). The documents use WCPOS's own receipt-template envelope shape, so its shipped templates and renderer can be copied in later; a pinned key-tree snapshot against WCPOS's closure fixture guards that shape until then. Facts log through TallyUI's own logger (@tallyui/pos'sloggingmodule) instead of@wcpos/utils/logger.minorToDecimalmoves out ofexportCsvinto a sharedregisterhelper so both reuse it. -
#121
9441c26Thanks @kilbot! - Adds@tallyui/pos's register maths (packages/pos/src/register), ported from WCPOSnextat3b5331b5c(ADR-032, registers job a1): typed-movement validation against the server's paid-in/paid-out/no-sale grammar (movementFieldError,normalizeAmount,isServerDecimal), the register count's variance, threshold, denomination and amount maths (countVariance,overThreshold,denominationTotal,varianceText,validAmount,parseMinor,denominations), andderiveExpectedfor the float, captured session payments and non-voided paid-in/paid-out cash movements. Refund attribution is deferred until TallyUI has a refund model, soderiveExpecteddoes not yet net refunds against the drawer. All money is TallyUI's integer-minor-units convention, with a requiredexponentparameter wherever a cashier's typed text becomes minor units (0 for JPY, 2 for GBP, 3 for KWD). -
#126
45e0b12Thanks @kilbot! -stampSession(order, sessionId, sessions)is the only way to set aPosOrder'ssessionId: it verifies the session is stillopenorcountingbefore stamping, so a caller that skipsrequireOpenSessioncan no longer leave a sale off every Z with an unchecked, closed session id.finalizeOrderno longer takes asessionIdoption; stamp its result withstampSessioninstead.recordMovementnow takes the closures collection:recordMovement(sessions, movements, closures, input). After inserting a movement, it re-reads the session. If the session closed in the gap, the movement is removed andRegisterSessionClosedErroris thrown only when the session's closure row is already frozen without it. If that closure row lists it, the movement is returned as counted. With no closure row yet, the movement is kept and the new, exportedRegisterMovementStrandedErroris thrown: it carries the movement'sidandsession_id, and its message can be shown to the cashier as it is. The caller must not record that movement again; registers job c's server resolves stranded movements. A failedremove()throwsRegisterMovementStrandedErrortoo, and a failed re-read returns the movement as recorded. -
#123
a58fccaThanks @kilbot! - Adds register sessions (ADR-032, registers job a2), ported from WCPOSnextat3b5331b5c: three local-only collections (registerSessionSchemawithregisterSessionCollection,cashMovementSchema,closureSchema), the session write path (openSession,startCounting,backToSelling,closeSession,recordMovement,voidMovement,requireOpenSession,writeClosure), and the register document for the till's identity, store binding and counters (ensureRegister,bindRegister,nextSaleCounter,mintClosureNumber,advancePerpetual). APosOrdercarries an optionalsessionId, set bystampSessionand never sent.posOrderSchemais now version 1 (the optionalsessionId): createpos_orderswith the newposOrderCollection(), which carries its identity migration; withposOrderSchemaalone, RxDB refuses the collection.A closed session is final: nothing moves it out of
closed(RegisterSessionClosedError), a repeatcloseSessionis a no-op,recordMovementandvoidMovementtake the sessions collection first and refuse a missing or closed session, andwriteClosurerefuses a session that is not closed. -
#105
6281345Thanks @kilbot! - Orders with a line in its own tax mode now finalize; the server charges each line in its own mode (ADR-038 amendment 2). -
#74
0121559Thanks @kilbot! - Removes the unreleased multi-tab database machinery in favour of one live tab per store (ADR-061):CreateDatabaseOptions.multiInstance(createTallyDatabasealways passesmultiInstance: false), thetally-outbox-flushandtally-outbox-statelocal documents, and follower forwarding between tabs are all gone. The outbox's public API (flush,requeue,start,stop,state$) and its single-instance behaviour are unchanged. Apps enforce one live tab withstartLiveTab.@tallyui/storage-sqlite's worker now swallows thereadypromise's rejection so a pool install failure before anycreateStorageInstancecall doesn't surface as an unhandled rejection, and itsfileslist no longer publishes test files, matching@tallyui/databaseand@tallyui/pos. -
#55
350967dThanks @kilbot! - Stock reads use the reconciled overlay (ADR-060) and show how fresh it is.@tallyui/corenow holdswithStockOverlayandgetProductStock(@tallyui/posre-exports them), addsSTOCK_LEVELS_LAST_PASS,stockOverlayandstockOverlayAsOfprops onConnectorProvider, and auseProductStock(doc)hook that returns overlay stock plusasOf, orgetStock(doc)when no overlay is given.@tallyui/database:startStockReconcilealso returnsstate$(running,truncated,lastError,lastCompletedAt),reconcileStock()resolves withcompletedAt, and each successful pass stores{ completedAt }in thelast-passlocal document ofstock_levels, whichcreateTallyDatabasenow creates with local documents (apps that create the collection themselves use the newstockLevelsCollectionconfig; without local documents a pass rejects with a clear error); a restarted runner seedslastCompletedAtfrom it.@tallyui/posaddsstockOverlayAsOf$.ProductStockBadgereads stock throughuseProductStockand appends " · as of <time>" when an overlay is given (showAsOf={false}hides it). -
#53
e3b8686Thanks @kilbot! - Add a stock reconcile pass (ADR-060).@tallyui/coreadds theStockReconcileAdaptercontract (fetchPagesand a pureoverlay) and an optionalreconcile.stockonTallyConnector.@tallyui/databaseadds the local-onlystock_levelscollection (STOCK_LEVELS_COLLECTION,stockLevelsSchema), whichcreateTallyDatabasecreates for connectors withreconcile.stock, andstartStockReconcile, which re-reads stock every 5 minutes (and on demand throughreconcileStock()) into that collection: it writes only changed rows, removes keys the backend no longer returns, writes nothing after a failed, truncated or stopped read, and never writes the replicated products.@tallyui/posaddsstockOverlay$,withStockOverlayandgetProductStock, which read stock from the overlay where it has an entry and from the replicated product otherwise. The Vendure connector reconciles variantstockLevels, and the Medusa connector reconciles inventory item location levels, so stock changes that bump no product timestamp reach the POS. -
#106
e7d3033Thanks @kilbot! -@tallyui/posadds the neutral store-settings bootstrap every platform POS shares.resolveStoreSettingsreads the app's stored choice, callsconnector.storeSettings, returnschoosewith the choices onchoice_required(the tried choice asinitial), and saves a new pick only once it resolves.useStoreSettingswraps it as a hook that re-resolves on a store switch and discards stale results, withchoose(choice)andretry().withPricingContextandtaxProviderPropsmap the settings into the replication'sSyncContextand<TaxProvider>. The app keeps persistence throughloadChoiceandsaveChoice. -
#118
5bcabfbThanks @kilbot! - Add the tender reducer, ported from WCPOSnextat test parity: an integer-minor-unit keypad, cash change, quick tender amounts, and even/fixed/percentage/item split plans.tenderReducer,initTenderState,initialTenderState,appliedMinor,changeMinor,quickTenderedAmounts,evenSplitShareMinor,activePlan,planLegsandMAX_TENDER_MINORare exported from@tallyui/pos, along with their state and action types. WCPOS's WooCommerce-only legacy tab (TenderTab, thetabfield,set-tab) is dropped as platform-specific;PaymentTransportis redefined locally as the same hardware transport union. No screens: those come with the TV6 components lift and the app. -
#104
bc46d99Thanks @kilbot! -ProductGridhides products this channel doesn't sell (traits.isSellablefalse, for example a Medusa product outside the sales channel) unlessshowUnsellableis set, andProductCardshows such a product in a muted "Not sold here" state instead of its price.OrderBuilder.addProductnow refuses an unsellable product before looking up its price, instead of throwing the unrelated "No price in …" error. -
#143
fb6c2e3Thanks @kilbot! - AdduseRegisterSession, the till's register session as React state with its actions (open, count, back to selling, close, cash movements, voids). The app supplies every input. Expected cash and the sales count are derived locally frompos_orders, and nothing is sent to a server.requireOpen()gates tender.RegisterTenderInProgressErrorstops counting or closing during a sale at tender,RegisterSessionAlreadyOpenErrorstops a second open, andRegisterCloseIncompleteErrorstops an open until an interrupted close is finished. A resumed close uses the count saved on the session.
Patch Changes
-
#155
96bf8f7Thanks @kilbot! -addPosOrderCollection(db): when a close stops waiting (afterPOS_ORDER_MIGRATION_CLOSE_WAIT_MS) while the migration still runs, the migration no longer writes into the stores the close closes. On SQLite it used to fail with a rawSQLite.bulkWrite() already closedand an unhandled rejection, and left the SQLite handle unable to write until restart. The open now rejects withPosOrderOpenClosedError(code: 'POS_ORDER_OPEN_CLOSED'), and the next open on the same database migrates every order.The status writes such an open drops are logged at warn through the new exported
posOrdersLogger(scopepos-orders), and a close that gives up once the migration is done, while the open reads its status, also rejects withPosOrderOpenClosedError. -
#174
160252cThanks @kilbot! -ClosureSheetshows who approved the close (Approved by {name}, falling back to the approver id without a name) under the figures, blind or not — it's provenance, not a counted figure.RegisterColumnoffers "Finish closing" whenuseRegisterSession's session is closed but its closure row was never written (an interrupted close), resuming the close (the store keeps the count persisted on the session) instead of falling through to the cart, whereopenSessionwould otherwise refuse withRegisterCloseIncompleteError.buildClosureDocument's return type now carriesclosure.unsynced_count: numberand each movement'sid/reasonasstring, without a cast; no runtime output changed. -
#147
cbda7e0Thanks @kilbot! -useSale()follow-ups to #145's idempotentcomplete(): the sale now locks fromcomplete()'s entry (savingtrue, every change refused with SALE_SAVING), not only once the order is built and stamped — so Back, an edit orcancelTender()during the session stamp is refused, and afinalizeOrderrefusal before any order is built still unlocks the sale with the refusal's error, as before.newSale()now also clears the in-flight save's tracking and stamps a new generation: a hung save's late outcome can no longer make the next sale'scomplete()quietly share its promise, and an attempt still building or stamping whennewSale()lands can no longer install its order as the new sale's pending completion — that order still reachesonSaleCompletedonce it's built, since the money is already taken; only a throw from that background call is new, logged aterrorrather than shown to the new sale. The order outbox'srun()now checks the stored order'scommandIdbefore marking it sent or rejected (not only itssyncStatus), and reads it with a primary-key lookup straight on the storage instance instead of a full index scan, so an order requeued under a newcommandIdwhile its old command was in flight can't take the old result. -
#145
e103c71Thanks @kilbot! -useSale().complete()is idempotent for one tender. It builds the order once per tender attempt and keeps it as the pending completion. A retry afteronSaleCompletedthrows hands over the same order (the sameid,commandIdandcreatedAt, with no second session stamp or late-sale fact), so a save that failed after storing the order no longer queues a duplicate sale. A secondcomplete()while one is in flight (a double tap) returns the first call's promise instead of building a second order, andcomplete()on the receipt does nothing. While a completion is pending, the newsavingflag is true and the sale is locked:add,setQuantity,remove,applyDiscount,removeDiscount,setTender,startTenderandcancelTenderchange nothing and set the error "This sale is being saved. Retry to finish it."newSale()abandons it, and leaves any order already stored inpos_ordersuntouched.useOrderOutbox().recordtreats an order already stored under the samecommandIdas stored, and still flushes; the sameidunder anothercommandIdstill rejects. -
#129
350dd72Thanks @kilbot! - Stacked line discounts now record what each one actually removed, capped at what the earlier discounts on the same line left, instead of the discount's raw computed amount. A line'sdiscountMinoris unchanged; only the per-discountamountMinorbreakdown the receipt will print is corrected (#63). -
#152
7e489e0Thanks @kilbot! -addPosOrderCollection(db): a close now waits for the whole open (up toPOS_ORDER_MIGRATION_CLOSE_WAIT_MS), not only its migration. A close that landed while the open added the collection or reset the migration checkpoint used to close storage under it; on SQLite the open then failed with rxdb-premium's rawReferenceError: context is not defined. An open called on a database whose close has begun, or one the close stopped waiting for, now stops before any further write and rejects with the new exportedPosOrderOpenClosedError(code: 'POS_ORDER_OPEN_CLOSED'): reopen and call it again. -
#133
f67535dThanks @kilbot! -addPosOrderCollection(db)follow-ups from the #131 review: a close no longer waits forever on a stuck migration (it gives up afterPOS_ORDER_MIGRATION_CLOSE_WAIT_MS, 10s, leaving the worst case anERRORstatus the next open safely resets and retries); it refuses amultiInstancedatabase up front, before any reset, since the reset can race a second tab's migration (TallyUI is single-instance, ADR-061); and repeated DM4 retries on the same database no longer add anotherdb.onClosehandler each time, only ever one. -
#135
d921415Thanks @kilbot! -addPosOrderCollectionno longer lets a failed migration run's replication outlive the run (RxDB'scancel()never stops it), so rapid DM4 retries on the same database raise no unhandledremoved alreadyrejection. A version-0 order whose version-1 copy is stale (for example, sent by a rolled-back build after a failed run copied it as pending) now migrates with its newer version-0 state instead of losing to the stale copy or looping in RxDB's conflict handling. -
#95
f1af98cThanks @kilbot! -OrderCreateLinegains an optionaltaxInclusive(ADR-038 amendment 2). It is the line's own tax mode, sent only when that mode differs from the order'spricesIncludeTax. Single-mode orders produce byte-identical payloads.finalizestill rejects converted lines until the Medusa plugin honours the field. -
#158
a7fdde8Thanks @kilbot! - A sale stamped with a register session that then closed, and stored after that session's closure was frozen without it, no longer sits on no Z. The newsweepOrphanStampsturns each such order into a late sale: it removessessionId, setslateSessionIdand logs onelate-salefact. It never changes the closure, an order the closure lists, an order whose session has no closure yet, a late order or another register's orders.useRegisterSessionruns it on start, after each close's closure, and whenever a new closure appears.voidMovementtakes an optionalclosurescollection and re-reads the session after its writes. If the session closed meanwhile and no closure lists the reversal, it throwsRegisterMovementStrandedError, and it always does so whenclosuresisn't passed. The reversal is kept either way. -
#146
5e5ba11Thanks @kilbot! - The order outbox reads its pending batch, its pending count and the rejected orders to requeue straight from the storage, past RxDB's query cache. In RxDB 16.21.1 a sale inserted while a cached query's storage read was in flight never reached that query, so the outbox left it unsent until the app restarted. Before patching a sent order, the outbox now checks the order's stored state the same way, because afindOne(id)can go stale too. New exportsreadFresh(collection, query)andcountFresh(collection, selector)do these reads. -
#38
6444868Thanks @kilbot! -uuidv7()with no arguments is now monotonic: ids made in the same millisecond, or after the clock steps back, still sort strictly after the previous one (RFC 9562 monotonic random counter). Calls with an explicit timestamp or random source are unchanged. -
#51
7502d61Thanks @kilbot! - addProduct charges the chosen variant's price (and SKU) instead of the first variant's; an unknown variant id throws. requeue() re-checks each order's status inside the write, so it never re-pends an order that is no longer rejected. -
#157
125c85aThanks @kilbot! -readFresh,countFreshandwatchFreshmove to a new, side-effect-free subpath,@tallyui/core/rxdb. Core now listsrxdb(>=16) andrxjs(>=7) as optional peer dependencies, needed only by that subpath; core's main entry stays free of both.@tallyui/posre-exports the helpers unchanged. The id and fingerprint reconciles in@tallyui/databaseread the local products withreadFreshinstead of a cachedfind(), so a product the pull inserts or deletes while a pass reads them no longer leaves every later pass reading a stale list (RxDB 16.21.1 bug 4): an inserted product is now checked, and tombstoned or re-fetched, on the next pass, and a deleted one is no longer re-enqueued or counted towards the mass-delete brake. -
#128
b32d1b4Thanks @kilbot! - Resuming a parked order now keeps each line's own tax mode (taxInclusive) instead of falling back to the store's mode. Previously a parked mixed cart came back with every line priced in the store's mode, changing both the settlement figures (subtotalMinor,discountMinor,taxMinor,totalMinor) and the display figures from the ones the cashier parked. -
#170
93ed2ccThanks @kilbot! -useSalenow stamps the session in force when the tender started (startTender), pinned for that tender, instead of reading itssessionoption atcomplete()time. A session closed betweenstartTenderandcomplete()(so the app'ssaleSessionwent undefined) previously skipped the stamp entirely: the order got neithersessionIdnorlateSessionIdand nolate-salefact. It now becomes a late sale on the pinned session. A tender started with no session stays unstamped, even if a session appears beforecomplete(). The pin is dropped bycancelTender()andnewSale(). -
#156
244bb46Thanks @kilbot! -stampSession,recordMovementandvoidMovementcheck that the session is live with a primary-key storage read instead of a cachedfindOne, and so doesrecordMovement's re-read after its insert. A session closed by a write that skips that cached query, as a server sync would, is no longer taken as open until the app restarts. -
#150
a9a4525Thanks @kilbot! -useSale().newSale()now refuses (with the saving error, changing nothing) while acomplete()attempt is still building or stamping its order — not only once a pending completion exists. Previously that window letnewSale()abandon the attempt and start a new sale at once, handing the built order toonSaleCompletedin the background once it finished; a failure there was money taken with only an error log. Now the cashier waits for the short stamp, then gets Retry or Continue as usual, and abandoning a save is possible only once its order is confirmed stored (continueSale()) or from the receipt. The generation-mismatch hand-over this replaces, andhandOverAbandoned, are removed. Also fixes three gaps the #149 review found no test caught:complete()now clears a stalecanContinueat every new attempt's entry (including a Retry after Continue was offered), and a confirmation fromisStoredthat arrives after the completion is no longer pending, or after a newer attempt has started, no longer setscanContinue. -
#160
787cadaThanks @kilbot! - The orphan-stamp sweep (sweepOrphanStamps, ADR-032) is now bounded by aswept_closure_idsset on the register document: a closure it has already checked costs nopos_ordersquery, and the set needs no schema bump. It now takesregisterandstoreKey, aswriteClosuredoes. A closure joins the set only once it is older than the newSWEEP_GRACE_MS, so an insert racing a close still gets caught;useRegisterSessionruns afullsweep (ignoring the set) once on start to repair anything a save that outlasts the grace missed.closeSessionalready awaited its own sweep before returning; that is now covered by a test.voidMovement's closure lookup is now a primary-key storage read, for consistency withreadSession. -
#172
3e63452Thanks @kilbot! -useSale'sstartTendertakes the confirmed session (startTender(method, { session })) and pins it instead of the renderedsessionoption, which can lag a session opened just before the tender.useRegisterSessionaddsrequireSaleSession():requireOpen(), returning{ id, sessions }to pass tostartTender. A tender that pinned no session, with a session rendered bycomplete(), now stamps that current session and logs a warning, instead of leaving the order unstamped. -
#166
07e0198Thanks @kilbot! -varianceTextputs the direction in the word only: "€100.00 short", "€5.00 over", "Exact", without a leading sign (a signed "−€100.00 short" read as a double negative). -
#153
38df38aThanks @kilbot! -watchFreshre-reads once per bulk write instead of once per document: it listens tocollection.eventBulks$rather than the per-documentcollection.$, so abulkInsertof 100 documents costs one storage read, not 100. (RxDB Premium's SQLite storage splits a write into batches of 199 documents and emits one event per batch, so a larger write costs one read per batch.)
@tallyui/theme
Patch Changes
-
#88
5053527Thanks @kilbot! - no longer publishes test files -
#36
d1bc892Thanks @kilbot! - Light theme contrast: primary darkened to #5b5ef0 (white text 4.88:1), muted-foreground to #656c79 and input to #848a94 (control borders ≥ 3:1). Product cards and quick-tender buttons get borders, and components no longer use the undefinedbg-surface-altortext-mutedtext classes, so labels such as "Change Due" are legible. -
#37
12b7723Thanks @kilbot! - Status colours pass WCAG AA with their foregrounds: light success #047857, warning #b45309, info #2563eb (and price #047857). Dark theme: input borders #636c76 (≥ 3:1), border #3d444d, and destructive text is dark (#0d1117) on the red. The contrast test now covers status colours and the dark theme. -
#31
78cada7Thanks @kilbot! -tokens.cssnow defines the light theme in a@variant lightblock, alongside@variant dark. Uniwind requires every theme to set the same variables, so apps using Uniwind no longer print 27 "Theme light is missing variable" errors per bundle. The file also declares thelightcustom variant, so it still compiles with plain Tailwind 4. Colour values are unchanged.
@tallyui/connector-woocommerce
Major Changes
-
#15
807d8daThanks @kilbot! - Product replication adapters are now pull-only. Thepushhandler is removed frommedusaProductReplication,wooProductReplication,vendureProductReplicationandshopifyProductReplication. Catalogue data is server-owned, so the POS never writes products. The old push handlers also turned every HTTP or network error into a fake conflict, which made RxDB silently revert local edits.This removes a public member. Nothing in TallyUI called it, but code that called
adapter.pushdirectly must stop doing so.
Minor Changes
-
#11
f90e59dThanks @kilbot! - Add backend-neutral price and stock traits.ProductTraitsgainsgetPrices(a price list of integer minor-unitMoneyentries,baseorsale, per currency) andgetStock(in_stock | out_of_stock | backorder | unknownplus an optional quantity). Core addsresolvePrice,moneyFromMajor,moneyToMajorandminorUnitDigits. Every connector maps its own shape into them; WooCommerce'sinstock/outofstock/onbackorderstrings now stay inside the WooCommerce connector. The string-price and WooCommerce-style stock accessors remain and are deprecated. -
#11
bc0a224Thanks @kilbot! - AddisSellableandgetVariantCountproduct traits to core and all four connectors.
Patch Changes
@tallyui/connector-medusa
Major Changes
-
#15
807d8daThanks @kilbot! - Product replication adapters are now pull-only. Thepushhandler is removed frommedusaProductReplication,wooProductReplication,vendureProductReplicationandshopifyProductReplication. Catalogue data is server-owned, so the POS never writes products. The old push handlers also turned every HTTP or network error into a fake conflict, which made RxDB silently revert local edits.This removes a public member. Nothing in TallyUI called it, but code that called
adapter.pushdirectly must stop doing so.
Minor Changes
-
#54
50317c8Thanks @kilbot! - Connectors can sign a user in:ConnectorAuthgains an optionalsignIn(baseUrl, { email, password }, init?)that resolves to aSignInResult(token, optional ISO 8601expiresAt) and rejects with aSignInErrorwhosecodeisinvalid_credentials,unsupportedorfailed. The app stores the token and passes it back togetHeadersastoken.Vendure's auth gains a sign-in flow: it runs the Admin API
loginmutation and takes the token from thevendure-auth-tokenheader (the server'stokenMethodmust include'bearer'). Its fields are nowurl,email,passwordand an optionalchannel_token.getHeaderssendscredentials.api_keyasvendure-api-key, otherwisecredentials.tokenas a Bearer token, plusvendure-tokenwhenchannel_tokenis set. The oldauth_tokencredential is still accepted as a deprecated alias fortoken.Medusa's
medusaAdminUserAuthsigns in throughPOST /auth/user/emailpassand readsexpiresAtfrom the JWT'sexp.medusaSecretKeyAuthhas no sign-in. -
#85
609ebd8Thanks @kilbot! - Add the fingerprint reconcile (ADR-060 amendment 8): a neutral runner that compares a remote fingerprint per product against the local documents and re-delivers products whose fingerprint differs, through the collection's pull.@tallyui/coreadds theFingerprintReconcileAdaptercontract (fetchPages, a purefingerprintandenqueue) and an optionalreconcile.pricesonTallyConnector.@tallyui/databaseaddsstartFingerprintReconcile, which runs no pass at start by default and otherwise mirrors the id reconcile: a complete, successful pass only,state$(running,lastResult,lastError), andstop().@tallyui/connector-medusaaddsreconcile.prices, a nightly base-price backstop (MEDUSA_PRICE_RECONCILE_INTERVAL_MS) for the variant feed (ADR-060 job D1): it fingerprints each product's base prices (variant id, currency and amount, sorted, price-list prices excluded) from/admin/product-variants. Nothing is written locally; corrections arrive only through the reconcile feed's pull. -
#35
63f11faThanks @kilbot! - Add a Bearer credential type for Medusa admin users:medusaAdminUserAuthsends the JWT from emailpass sign-in asAuthorization: Bearer <jwt>, andmedusaAdminUserConnectorismedusaConnectorwith that auth. The secret-key auth is now also exported asmedusaSecretKeyAuth;medusaConnectoris unchanged. -
#92
945bb83Thanks @kilbot! - Medusa prices as Medusa charges them (ADR-060 D2b).SyncContextgains an optionalpricingContext(fromstoreSettings()),ProductPricean optionaltaxInclusive, andTallyConnector.reconcileacalculatedPricesslot. With a pricing context, every Medusa product document build fills each variant'scalculated_pricefrom the store API (nullwhen the sales channel or region does not sell it), and the traits price from it: sale lists as a sale against the original price, override lists as the base price,nullas unsellable.reconcile.calculatedPricesre-delivers products whose calculated prices changed with no timestamp bump; run it everyMEDUSA_CALCULATED_PRICE_RECONCILE_INTERVAL_MS(30 minutes) withmaxPages: 1000. Without a pricing context, documents and prices are unchanged. -
#63
d9fe1e3Thanks @kilbot! - Fix the Medusa connector's incremental pull: Medusa 2.21 honours only the operator formupdated_at[$gte], and silently ignored the connector'supdated_at[gte], so every pass read the whole catalogue. Add the Medusa id reconcile (ADR-060), so a deleted product or a deleted variant (whose parent'supdated_atdoes not change) now reaches the local copy throughreplication.productsandreconcile.ids.@tallyui/databaseadds a mass-deletion brake tostartIdReconcile: a pass that would tombstone more thanmaxDeleteShare(default 20%) of local products, and more than 10 of them, queues nothing and warns instead, unlessallowMassDeleteis set. -
#102
7490a3fThanks @kilbot! - A connector schema version bump now drops and resyncs its collection (ADR-060 amendment 9).createTallyDatabaseadds RxDB's migration-schema plugin and gives each connector collection above version 0 av => nullstrategy per earlier version, andstartReplicationappends-v<version>to the replication identifier above version 0, so the pull starts from no checkpoint. Version 0 collections keep their identifier and never resync.stock_levelsandpos_ordersare untouched.The Medusa products schema is now version 1 and declares
variants[].calculated_price(object ornull). The first sync after upgrading resyncs the Medusa catalogue: the stored products are dropped when the database opens and download again, once, on the first sync. A collection created withmedusaProductSchemaoutsidecreateTallyDatabasemust useconnectorCollection(medusaProductSchema)from@tallyui/database, which supplies the strategies and the migration plugin; otherwise RxDB throws COL12. -
#89
4c2cf8fThanks @kilbot! -medusaConnector.storeSettings(TV4b) reads Medusa's admin API only: the resolved region's currency and price-preference tax inclusivity, the resolved country's tax region default rate (rounded to integer ppm once, at the connector's edge; 0 with no tax region or no default rate, matching what Medusa's system provider itself charges), and apricingContext(region_id,currency_code,publishable_key) for pricing through the store API. Region, then country, then channel (the publishable key, excluding revoked ones): the first ambiguity reports every choice known at that point, asStoreSettingsError('choice_required'), so the app asks once; a store with no publishable key at all rejects withStoreSettingsError('failed'). The publishable key is a public credential and may sit inpricingContext, but never appears in an error message,choices, or console output. -
#81
03cd385Thanks @kilbot! - Medusa'sreplication.productsnow includes a variant feed, so price edits arrive incrementally. Medusa 2.21 bumps a variant'supdated_aton a price-only edit but not its product's, so the product feed alone missed those changes. The variant feed pages changed variants and re-delivers their parent products. The first sync after upgrading re-delivers every product once. -
#11
f90e59dThanks @kilbot! - Add backend-neutral price and stock traits.ProductTraitsgainsgetPrices(a price list of integer minor-unitMoneyentries,baseorsale, per currency) andgetStock(in_stock | out_of_stock | backorder | unknownplus an optional quantity). Core addsresolvePrice,moneyFromMajor,moneyToMajorandminorUnitDigits. Every connector maps its own shape into them; WooCommerce'sinstock/outofstock/onbackorderstrings now stay inside the WooCommerce connector. The string-price and WooCommerce-style stock accessors remain and are deprecated. -
#120
e0062ceThanks @kilbot! - A per-storeorder.createcapability check replaces the global discount guard (ADR-062).@tallyui/coregainsServerCapabilities,SignInResult.capabilities,SyncContext.capabilities,TallyConnector.capabilities?()andresolveCapabilities(fresh, stored).@tallyui/connector-medusareads the store's supportedorder.createversions fromGET /tally/v1/info: a 404 or a malformed response means an old plugin (version 1), a network failure or a 5xx is unknown and keeps the last known value, and a 401 throws.medusaSignInreturns the read capabilities, and both Medusa connectors exposecapabilities(context)for a restored session.finalizeOrderin@tallyui/posnow rejects a discount only when the store's capability is below 2, so a store whose plugin has caught up finalizes a discounted order asorder.createversion 2. -
#11
bc0a224Thanks @kilbot! - AddisSellableandgetVariantCountproduct traits to core and all four connectors. -
#57
55ae68fThanks @kilbot! -SignInErrorCodesplits the oldfailedin two:failednow means no response arrived (a network error), and the newserver_errormeans a response arrived but was unusable (a bad status, a malformed body, or a missing token).SignInErrorgains an optionalstatusfrom a third constructor argument. Callers that switch oncodeshould handleserver_error.Medusa's sign-in now treats
mfa_required: trueandverification_required: truethe same as alocationbody:unsupported, and no token is ever returned from a body like that. A malformed response body, any other non-OK status and a missing or non-string token are nowserver_errorwith the HTTP status.Vendure's sign-in now treats
NATIVE_AUTH_STRATEGY_ERRORasunsupported, since native email/password auth is disabled on the server. A malformed response body, a non-OK status, GraphQL errors, a missingdata.loginand any otherErrorResultare nowserver_errorwith the HTTP status. -
#64
402ec36Thanks @kilbot! - Both connectors now store a product's variants sorted by id, since neither Medusa nor Vendure guarantees variant order across requests:@tallyui/coreaddscompareIds, and the Medusa and Vendure product projections (toDocument,toProductDocument) sortvariantswith it before the document is stored. Traits that readvariants[0](getPrices,getSku,getPrice,getStockQuantity,getBarcodeand others) now see a stable variant across runs.Already-stored documents take the new order the next time they are delivered. Vendure's variant feed re-delivers every product on its first pass anyway, so it heals immediately.
-
#53
e3b8686Thanks @kilbot! - Add a stock reconcile pass (ADR-060).@tallyui/coreadds theStockReconcileAdaptercontract (fetchPagesand a pureoverlay) and an optionalreconcile.stockonTallyConnector.@tallyui/databaseadds the local-onlystock_levelscollection (STOCK_LEVELS_COLLECTION,stockLevelsSchema), whichcreateTallyDatabasecreates for connectors withreconcile.stock, andstartStockReconcile, which re-reads stock every 5 minutes (and on demand throughreconcileStock()) into that collection: it writes only changed rows, removes keys the backend no longer returns, writes nothing after a failed, truncated or stopped read, and never writes the replicated products.@tallyui/posaddsstockOverlay$,withStockOverlayandgetProductStock, which read stock from the overlay where it has an entry and from the replicated product otherwise. The Vendure connector reconciles variantstockLevels, and the Medusa connector reconciles inventory item location levels, so stock changes that bump no product timestamp reach the POS. -
#19
8df0569Thanks @kilbot! - Adds variant traits.VariantSummary(id,title,sku,barcode,prices,stock) and the optionalProductTraits.getVariantsdescribe every purchasable variant of a product, andfindVariantByCodefinds a variant by barcode or SKU for scanning. The Medusa connector implementsgetVariants; its product-levelgetPricesandgetStockresults are unchanged.
Patch Changes
-
#48
6b1b0b7Thanks @kilbot! - Product replication no longer loses updates inside RxDB's replication loop: a pass ends on the list count, the next pass starts from a high-water mark read at the pass start, an unchanged mark ends the loop, and a pass restarts if rows vanish mid-pass. -
#97
f8b0dacThanks @kilbot! - A fresh install downloads the catalogue once. A pull adapter can now declarepull.seedCheckpoint; on a fresh install (no stored checkpoint)combinePullAdaptersreads every seed before any feed runs and starts that feed from it. The Medusa and Vendure variant feeds seed their cursor at the newest variant'supdated_at, so their first pass no longer re-delivers every product the product feed has just delivered, and a variant edit made during the product feed's first pass still arrives. An install upgrading from a stored checkpoint is never seeded and keeps the variant feed's full healing pass. -
#98
9cd78cdThanks @kilbot! -medusaStoreSettings'schoice_requiredchannel choices are named after the publishable key's sales channel(s) (joined with ", " when there is more than one), not the key's own developer-facing title. A key with no sales channel, or only blank channel names, still falls back to its title. -
#11
4e6261fThanks @kilbot! - Fix product replication skipping a page of products per batch: the checkpoint now keeps itsupdated_atfilter fixed while paging and only advances it at the end of a pass. -
#11
ffa9f19Thanks @kilbot! - Fix the Medusa connector against a real Medusa v2 (2.21) backend: send the secret API key over HTTP Basic auth (Medusa rejects it as a Bearer token), read prices as major units (v2 does not store cents), derive stock from inventory levels (the Admin API does not computeinventory_quantity), and pull products inupdated_atorder so the checkpoint is valid. -
#111
35a3fbaThanks @kilbot! - A chosen country outside the region is reported aschoice_requiredinstead of silently using the region's only country. -
#11
60230f0Thanks @kilbot! - Make the Medusa product schema load under RxDB dev-mode (indexedhandleandstatusare now required with amaxLength), keep pulled documents to the schema's fields so new Medusa API fields never fail validation, and page each replication pass inidorder, since many products share anupdated_at. -
#88
5053527Thanks @kilbot! - no longer publishes test files -
#94
373e438Thanks @kilbot! -resolvePricekeeps a price'staxInclusiveflag oncurrentandwas. Each order-builder line keeps its price's own tax mode (LineItem.taxInclusive, pluspriceTaxModeConvertedwhen it differs from the store'spricesIncludeTax), so a customer pays exactly the shelf price and an inclusive price in an exclusive store is no longer taxed twice. Orders whose prices carry no flag, or one that agrees with the store, total exactly as before. The receipt shows a converted line in the order's mode, by its share of the order's once-rounded tax, so the lines still add up.In priced mode, the Medusa traits' deprecated
getPriceandgetRegularPricereturn the resolved calculated price instead of the admin prices, andisSellableis false when no variant yields a price (for example acalculated_pricewith null amounts). -
#11
b1b6e30Thanks @kilbot! - Report product-level stock across all variants, showing the total quantity only when every variant has tracked, known stock. Draw the search magnifier with an attached, rounded handle and a larger ring.
@tallyui/connector-shopify
Major Changes
-
#15
807d8daThanks @kilbot! - Product replication adapters are now pull-only. Thepushhandler is removed frommedusaProductReplication,wooProductReplication,vendureProductReplicationandshopifyProductReplication. Catalogue data is server-owned, so the POS never writes products. The old push handlers also turned every HTTP or network error into a fake conflict, which made RxDB silently revert local edits.This removes a public member. Nothing in TallyUI called it, but code that called
adapter.pushdirectly must stop doing so.
Minor Changes
-
#11
f90e59dThanks @kilbot! - Add backend-neutral price and stock traits.ProductTraitsgainsgetPrices(a price list of integer minor-unitMoneyentries,baseorsale, per currency) andgetStock(in_stock | out_of_stock | backorder | unknownplus an optional quantity). Core addsresolvePrice,moneyFromMajor,moneyToMajorandminorUnitDigits. Every connector maps its own shape into them; WooCommerce'sinstock/outofstock/onbackorderstrings now stay inside the WooCommerce connector. The string-price and WooCommerce-style stock accessors remain and are deprecated. -
#11
bc0a224Thanks @kilbot! - AddisSellableandgetVariantCountproduct traits to core and all four connectors.
Patch Changes
-
#88
5053527Thanks @kilbot! - no longer publishes test files -
#11
b1b6e30Thanks @kilbot! - Report product-level stock across all variants, showing the total quantity only when every variant has tracked, known stock. Draw the search magnifier with an attached, rounded handle and a larger ring.
@tallyui/connector-vendure
Major Changes
-
#15
807d8daThanks @kilbot! - Product replication adapters are now pull-only. Thepushhandler is removed frommedusaProductReplication,wooProductReplication,vendureProductReplicationandshopifyProductReplication. Catalogue data is server-owned, so the POS never writes products. The old push handlers also turned every HTTP or network error into a fake conflict, which made RxDB silently revert local edits.This removes a public member. Nothing in TallyUI called it, but code that called
adapter.pushdirectly must stop doing so.
Minor Changes
-
#54
50317c8Thanks @kilbot! - Connectors can sign a user in:ConnectorAuthgains an optionalsignIn(baseUrl, { email, password }, init?)that resolves to aSignInResult(token, optional ISO 8601expiresAt) and rejects with aSignInErrorwhosecodeisinvalid_credentials,unsupportedorfailed. The app stores the token and passes it back togetHeadersastoken.Vendure's auth gains a sign-in flow: it runs the Admin API
loginmutation and takes the token from thevendure-auth-tokenheader (the server'stokenMethodmust include'bearer'). Its fields are nowurl,email,passwordand an optionalchannel_token.getHeaderssendscredentials.api_keyasvendure-api-key, otherwisecredentials.tokenas a Bearer token, plusvendure-tokenwhenchannel_tokenis set. The oldauth_tokencredential is still accepted as a deprecated alias fortoken.Medusa's
medusaAdminUserAuthsigns in throughPOST /auth/user/emailpassand readsexpiresAtfrom the JWT'sexp.medusaSecretKeyAuthhas no sign-in. -
#45
14620d9Thanks @kilbot! - Fix Vendure product pagination with fixed timestamp windows and ID ordering. Add an opt-in barcode field and stock-location configuration, use available stock from stockLevels, and support the Admin API in the mock.Complete pull passes using totalItems, restart empty mid-pass pages, and use a pass-start high-water mark with idle detection to preserve updates in RxDB replication. Include GraphQL error messages on failed HTTP responses. Existing users must set barcodeField to read barcodes: getBarcode now returns undefined unless barcodeField is configured.
Guard each pull pass against skewed Vendure updatedAt filters and add updatedAtSkewMs to widen lower bounds when the server cannot run with TZ=UTC.
-
#58
3e09957Thanks @kilbot! - AddcombinePullAdaptersto@tallyui/core: it combines several pull adapters into the one adapter a collection replicates with, calling them one after another with a checkpoint per sub-adapter. Two replications on one collection can skip each other's pulled versions, so run one per collection.Vendure's
replication.productsnow includes a variant feed that re-delivers parent products whose variants changed. Vendure does not bumpProduct.updatedAton a variant price or stock edit, so the product feed alone misses those changes. An existing install's product-feed checkpoint carries over; the variant feed runs one full pass on first sync. -
#50
2424107Thanks @kilbot! - Add variant summaries for barcode scanning and variant pickers. Add a pricesIncludeTax option, defaulting to false, so product and variant prices use net amounts by default and gross amounts for tax-inclusive channels. -
#61
540044cThanks @kilbot! - Add the id reconcile (ADR-060): a periodic pass that reads every live product id and its live variant ids, so a deleted product or a deleted variant (whose parent'supdatedAtdoes not change) reaches the local copy.@tallyui/coreadds theIdReconcileAdaptercontract andcreateReconcileFeed, which turns queued corrections into a pull-only adapter meant as the last key ofcombinePullAdapters.@tallyui/databaseadds thestartIdReconcilerunner.@tallyui/connector-vendureimplements the Vendure side and wires it intoreplication.productsandreconcile.ids. Nothing is written locally into the replicated collection; corrections arrive only through the collection's own pull. -
#11
f90e59dThanks @kilbot! - Add backend-neutral price and stock traits.ProductTraitsgainsgetPrices(a price list of integer minor-unitMoneyentries,baseorsale, per currency) andgetStock(in_stock | out_of_stock | backorder | unknownplus an optional quantity). Core addsresolvePrice,moneyFromMajor,moneyToMajorandminorUnitDigits. Every connector maps its own shape into them; WooCommerce'sinstock/outofstock/onbackorderstrings now stay inside the WooCommerce connector. The string-price and WooCommerce-style stock accessors remain and are deprecated. -
#11
bc0a224Thanks @kilbot! - AddisSellableandgetVariantCountproduct traits to core and all four connectors. -
#57
55ae68fThanks @kilbot! -SignInErrorCodesplits the oldfailedin two:failednow means no response arrived (a network error), and the newserver_errormeans a response arrived but was unusable (a bad status, a malformed body, or a missing token).SignInErrorgains an optionalstatusfrom a third constructor argument. Callers that switch oncodeshould handleserver_error.Medusa's sign-in now treats
mfa_required: trueandverification_required: truethe same as alocationbody:unsupported, and no token is ever returned from a body like that. A malformed response body, any other non-OK status and a missing or non-string token are nowserver_errorwith the HTTP status.Vendure's sign-in now treats
NATIVE_AUTH_STRATEGY_ERRORasunsupported, since native email/password auth is disabled on the server. A malformed response body, a non-OK status, GraphQL errors, a missingdata.loginand any otherErrorResultare nowserver_errorwith the HTTP status. -
#64
402ec36Thanks @kilbot! - Both connectors now store a product's variants sorted by id, since neither Medusa nor Vendure guarantees variant order across requests:@tallyui/coreaddscompareIds, and the Medusa and Vendure product projections (toDocument,toProductDocument) sortvariantswith it before the document is stored. Traits that readvariants[0](getPrices,getSku,getPrice,getStockQuantity,getBarcodeand others) now see a stable variant across runs.Already-stored documents take the new order the next time they are delivered. Vendure's variant feed re-delivers every product on its first pass anyway, so it heals immediately.
-
#53
e3b8686Thanks @kilbot! - Add a stock reconcile pass (ADR-060).@tallyui/coreadds theStockReconcileAdaptercontract (fetchPagesand a pureoverlay) and an optionalreconcile.stockonTallyConnector.@tallyui/databaseadds the local-onlystock_levelscollection (STOCK_LEVELS_COLLECTION,stockLevelsSchema), whichcreateTallyDatabasecreates for connectors withreconcile.stock, andstartStockReconcile, which re-reads stock every 5 minutes (and on demand throughreconcileStock()) into that collection: it writes only changed rows, removes keys the backend no longer returns, writes nothing after a failed, truncated or stopped read, and never writes the replicated products.@tallyui/posaddsstockOverlay$,withStockOverlayandgetProductStock, which read stock from the overlay where it has an entry and from the replicated product otherwise. The Vendure connector reconciles variantstockLevels, and the Medusa connector reconciles inventory item location levels, so stock changes that bump no product timestamp reach the POS. -
#87
ac2a24aThanks @kilbot! -TallyConnectorgains an optionalstoreSettings(context, choice?)(TV4): one read-only call for the store's currency,pricesIncludeTax,taxRatesPpmand an opaque connector-specificpricingContext, so the app can feed the connector's own tax-inclusivity option and the POSTaxProviderfrom a single source of truth instead of two hand-matched settings. Rejects with aStoreSettingsError(choice_requiredwithchoices, orfailed).Vendure's
storeSettingsreads the active channel's currency andpricesIncludeTax, and the default tax zone's enabled, non-customer-group rates keyed by tax category id (rounded to integer ppm once, at the connector's edge).defaultis theisDefaultcategory's rate, or, when none is flagged, the first category Vendure's owntaxCategorieslists — the same fallback Vendure uses for a variant created without a category — and is 0 when that category has no rate in the zone.createVendureConnector'spricesIncludeTaxoption is unchanged; passsettings.pricesIncludeTaxfromstoreSettingsinstead of hand-matching it to the POS. -
#112
8a3f323Thanks @kilbot! - Adds a nightly price reconcile pass (reconcile.prices), a fingerprint backstop for tax-rate changes: a zone's rate change for a category moves every affected variant'spriceWithTaxwithout bumping the variant'supdatedAt, so neither the product feed nor the variant feed re-delivers it (ADR-060). -
#110
b814bf3Thanks @kilbot! - Fixes three Vendure stock gaps (backlog 28, from the #45 stock review): a variant withtrackInventoryFALSE, orINHERITwith the global setting off, is now always in stock;outOfStockThreshold(per variant, or the global one throughuseGlobalOutOfStockThreshold) is now subtracted from available stock, matching Vendure's own saleable rule;getStockStatusandgetStockQuantitynow aggregate every variant, asgetStockalready did, instead of reading variant 0 only.Adds
vendureGlobalStockSettings(context)for the channel's stock defaults, andcreateVendureConnectoroptionsglobalTrackInventoryandglobalOutOfStockThreshold.The Vendure product schema is now version 1, declaring
variants[].trackInventory,outOfStockThreshold,useGlobalOutOfStockThresholdandenabled. The first sync after upgrading resyncs the Vendure catalogue once: a schema version bump drops the stored products and downloads them again (ADR-060 amendment 9). A collection created withvendureProductSchemaoutsidecreateTallyDatabasemust useconnectorCollection(vendureProductSchema)from@tallyui/database.
Patch Changes
-
#97
f8b0dacThanks @kilbot! - A fresh install downloads the catalogue once. A pull adapter can now declarepull.seedCheckpoint; on a fresh install (no stored checkpoint)combinePullAdaptersreads every seed before any feed runs and starts that feed from it. The Medusa and Vendure variant feeds seed their cursor at the newest variant'supdated_at, so their first pass no longer re-delivers every product the product feed has just delivered, and a variant edit made during the product feed's first pass still arrives. An install upgrading from a stored checkpoint is never seeded and keeps the variant feed's full healing pass. -
#88
5053527Thanks @kilbot! - no longer publishes test files -
#11
b1b6e30Thanks @kilbot! - Report product-level stock across all variants, showing the total quantity only when every variant has tracked, known stock. Draw the search magnifier with an attached, rounded handle and a larger ring. -
#113
5885373Thanks @kilbot! - A variant disabled in Vendure (enabled: false, replicated since #110) is no longer offered, priced or counted:getVariants,getPrices,getPrice,getRegularPrice,getStockandgetVariantCountnow consider live variants only, andisSellablealso requires at least one when the product has variants at all. A product with none left is not sellable, soaddProductrefuses it (#104). -
#114
51d4818Thanks @kilbot! - Vendure pull robustness (backlog 29, 30, 31). The high-water mark and both skew probes now select onlyid updatedAt, not the full product query. A mid-pass checkpoint in the shape saved before #45's pass-state fields (skipandupdatedAtalone) is recognised and normalised into a clean restart of the pass from offset 0, instead of resuming at an offset that no longer lines up with this pull's fixed-window paging. The skew guard's probe re-reads the high-water mark once before throwing, so a product deleted between the mark read and the probe settles into a clean pass instead of an error RxDB then has to retry.