@teincfood/core 0.10.10 → 0.10.12
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
|
-
|
|
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);
|
package/dist/sync/rate-limit.js
CHANGED
|
@@ -18,13 +18,21 @@ let rateLimitHitsInWindow = 0;
|
|
|
18
18
|
/** Record a 429: pause all sync until retryAfterMs (default 60s). */
|
|
19
19
|
export function noteRateLimited(retryAfterMs) {
|
|
20
20
|
const delay = retryAfterMs && retryAfterMs > 0 ? retryAfterMs : DEFAULT_RETRY_AFTER_MS;
|
|
21
|
-
const
|
|
21
|
+
const now = Date.now();
|
|
22
|
+
const until = now + delay;
|
|
22
23
|
if (until > rateLimitedUntil)
|
|
23
24
|
rateLimitedUntil = until;
|
|
24
|
-
|
|
25
|
+
// A new reporting window starts only after the previous one has expired.
|
|
26
|
+
// Extending an active window (another 429, a longer Retry-After, or a
|
|
27
|
+
// synthesized gate rejection) must not reset the counter, otherwise every
|
|
28
|
+
// request reports another toast.
|
|
29
|
+
if (now >= reportedWindowUntil) {
|
|
25
30
|
reportedWindowUntil = until;
|
|
26
31
|
rateLimitHitsInWindow = 0;
|
|
27
32
|
}
|
|
33
|
+
else if (until > reportedWindowUntil) {
|
|
34
|
+
reportedWindowUntil = until;
|
|
35
|
+
}
|
|
28
36
|
rateLimitHitsInWindow += 1;
|
|
29
37
|
}
|
|
30
38
|
/** Milliseconds remaining in the current cooldown (0 when not throttled). */
|
|
@@ -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