@teincfood/core 0.10.9 → 0.10.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -198,9 +198,26 @@ export async function transitionLocalOrder(businessId, orderId, action) {
198
198
  throw new TransitionError(TRANSITION_ERROR_CODES.INVALID_TRANSITION, `Cannot ${action} from ${order.status}`);
199
199
  }
200
200
  const map = { accept: "accepted", reject: "cancelled", start_preparing: "preparing", mark_ready: "ready", complete_pickup: "delivered", cancel: "cancelled", assign_rider: "assigned", confirm_handover: "delivered", confirm_cash: order.status, pay: order.status };
201
- const toStatus = map[action] ?? order.status;
201
+ let toStatus = map[action] ?? order.status;
202
202
  if (action === "confirm_cash" || action === "pay") {
203
+ // A paid :awaiting_payment order must leave the payment queue locally too,
204
+ // using the same promotion the backend applies: buyer order → pending,
205
+ // POS staged → accepted, POS direct → delivered. Previously this local
206
+ // write only set payment fields, so the row stayed "awaiting payment"
207
+ // until a later pull even after the cash/payment was recorded.
208
+ if (order.status === "awaiting_payment") {
209
+ if (!order.is_pos) {
210
+ toStatus = "pending";
211
+ }
212
+ else {
213
+ const flowMode = await getBusinessOrderFlowMode(businessId, db);
214
+ toStatus = flowMode === "direct" ? "delivered" : "accepted";
215
+ }
216
+ }
203
217
  await updateOrderPayment(db, businessId, orderId, { is_paid: true, paid_at: new Date().toISOString() });
218
+ if (toStatus !== order.status) {
219
+ await updateOrderStatus(db, businessId, orderId, toStatus);
220
+ }
204
221
  }
205
222
  else if (toStatus !== order.status) {
206
223
  await updateOrderStatus(db, businessId, orderId, toStatus);
@@ -12,7 +12,7 @@ import { REFERENCE_CHANGED_EVENT_TYPE } from "../reference/events";
12
12
  import { applyReferenceChangeEvent } from "../reference/events";
13
13
  import { referenceSyncService } from "../reference/service";
14
14
  import { openSyncDatabase } from "../db/connection";
15
- import { insertOrder, listOrdersWithHlc } from "../db/operations";
15
+ import { getUnsyncedEvents, insertOrder, listOrdersWithHlc } from "../db/operations";
16
16
  import { getPendingOrdersSnapshot } from "../adapters/pending-orders";
17
17
  import { generateEventId } from "../utils/uuid";
18
18
  import { createLocalOrder, transitionLocalOrder } from "../domain/orders/service";
@@ -116,7 +116,18 @@ class LocalNodeServiceCore {
116
116
  for (const order of rows)
117
117
  byId.set(order.id, order);
118
118
  // Queued offline creations have no `orders` row yet; they live in the
119
- // pending overlay. Include them so a pre-linkup sale is shared too.
119
+ // pending overlay and the durable event journal. Include both so a
120
+ // pre-linkup sale is shared even after an app restart.
121
+ for (const origin of ["local", "lan"]) {
122
+ const events = await getUnsyncedEvents(db, origin, Number.MAX_SAFE_INTEGER);
123
+ for (const event of events) {
124
+ if (event.event_type !== "order.create_queued")
125
+ continue;
126
+ const order = event.payload;
127
+ if (order?.id && order.order_number)
128
+ byId.set(order.id, order);
129
+ }
130
+ }
120
131
  for (const order of getPendingOrdersSnapshot().orders) {
121
132
  if (order.business_id === this.businessId)
122
133
  byId.set(order.id, order);
@@ -536,6 +536,23 @@ export async function drainOne(businessId, entityId) {
536
536
  // exact (no message matching).
537
537
  const isBusinessClosed = rest.code === "BUSINESS_CLOSED" || rest.code === "HOURS_NOT_SYNCED";
538
538
  const is4xx = !!rest.status && rest.status >= 400 && rest.status < 500;
539
+ // ALREADY_PAID is a settled payment, not a failed transition. The backend
540
+ // may have stamped paid_at before the status promotion (legacy cash flow)
541
+ // or another terminal may have collected the money. Pull the server row,
542
+ // apply it over the local intent, and complete the job so the terminal
543
+ // stops showing a Pay action and retrying forever.
544
+ if (rest.status === 409 && rest.code === "ALREADY_PAID" && isOrderTransition(row.command_type)) {
545
+ try {
546
+ await pullRemoteOrders(businessId);
547
+ await db.runAsync(`UPDATE sync_jobs SET status='completed', updated_at=? WHERE id=?;`, new Date().toISOString(), row.id);
548
+ syncLogger.info("SyncJobs", "ALREADY_PAID — reconciled server order and completed job", { businessId, entityId, command_type: row.command_type });
549
+ clearRetry(businessId);
550
+ return;
551
+ }
552
+ catch (error) {
553
+ syncLogger.warn("SyncJobs", "ALREADY_PAID reconciliation failed", { businessId, entityId, error: String(error) });
554
+ }
555
+ }
539
556
  // Already-applied absorb for transitions: the server already moved the
540
557
  // order (duplicate tap, earlier drain won the race) — complete the job
541
558
  // instead of failing it. Server truth only (hlc-guarded pull), never the
@@ -18,6 +18,10 @@ const ORDER_ENDPOINTS = {
18
18
  "order:cancel": "cancel",
19
19
  "order:confirm_cash": "confirm-cash-payment",
20
20
  "order:pay": "confirm-cash-payment",
21
+ // Repository/queue alias. The domain transition queues `order:confirm_cash`,
22
+ // but an offline repository command is enqueued as `payment:confirm_cash`;
23
+ // both must resolve to the same backend route.
24
+ "payment:confirm_cash": "confirm-cash-payment",
21
25
  };
22
26
  /** Command types fulfilled by the delivery endpoints (strictly online). */
23
27
  export const DELIVERY_COMMANDS = new Set([
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teincfood/core",
3
- "version": "0.10.9",
3
+ "version": "0.10.11",
4
4
  "description": "TeincFood shared offline-first core — types, sync engine, local DB, reference data, and repositories for mobile + desktop",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",