From 29579c2d089295fac01802ad80d01211288b54bb Mon Sep 17 00:00:00 2001 From: Sridhar Bala Date: Thu, 17 Sep 2026 16:47:51 +0530 Subject: [PATCH 1/3] A sync that existed on paper only The waiter-call feature marked its collection synced the day it shipped. Nothing built the lane: the sync agent has none, the sync-gateway allows none, and no build since has carried one. A call from a table on the ordering page lands in the cloud and a till with its own database never sees it. The packaging guard compared the promise with the bundle and refused the build, which is what it is for. This records the truth: waitercalls is undecided, with what it will take to sync it written next to it. The classification test now names it among the undecided instead of the promised. --- api/src/sync/collections.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/sync/collections.json b/api/src/sync/collections.json index c4aa706d8..fdc93af12 100644 --- a/api/src/sync/collections.json +++ b/api/src/sync/collections.json @@ -12,7 +12,6 @@ ], "synced": { "recycle_bin": "branch", - "waitercalls": "branch", "sales": "branch", "stocklogs": "branch", "cashregister": "branch", @@ -54,6 +53,7 @@ }, "undecided": { "_note": "Shop data that today stays where it was written. Each needs a decision and, if synced, a check that rows carry branch_id and license and that deletes leave a tombstone. Until decided, a till and the cloud can disagree on these exactly as they did on tables.", + "waitercalls": "a table's call from the ordering page lands in the cloud; a till with its own database never sees it. This was marked synced on the day the feature shipped, but no lane was ever built in the sync agent or allowed by the sync-gateway, so it synced on paper only until the packaging guard refused the build. To sync it: a lane in the agent, the collection allowed by the gateway, a check that rows carry branch_id and license, and a tombstone on close", "quotes": "a quote made on the till does not exist in the cloud, and the reverse", "coupons": "a coupon created in the cloud cannot be redeemed on a till", "coupon_redemptions": "a redemption on the till is never known to the cloud", From a69283e2cf4cb6495e0f7324738166b98c4fd1b1 Mon Sep 17 00:00:00 2001 From: Sridhar Bala Date: Thu, 17 Sep 2026 18:38:56 +0530 Subject: [PATCH 2/3] A call carries its date The waiter-call row was a native insert with no updated_date. The sync agent finds its work with { updated_date: { $exists: true } } and the gateway sends a till only rows whose updated_date moved, so even with the lane built (Gateway "A call reaches the till") the row would have stayed in the cloud for ever. Same trap as the settings save (#838). The row is stamped on the way in, and again when the till marks it seen, so the cloud learns the call was answered and the ordering page stops telling the customer "already calling". waitercalls goes back to synced, per branch. It was moved to undecided when the packaging guard found the promise had no lane behind it; the guard keeps checking, against the agent actually bundled. --- api/src/repositories/sale.repository.js | 19 ++++++- api/src/sync/collections.json | 2 +- tests/a-call-carries-its-date.test.js | 67 +++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 2 deletions(-) create mode 100644 tests/a-call-carries-its-date.test.js diff --git a/api/src/repositories/sale.repository.js b/api/src/repositories/sale.repository.js index 4bdf72376..054d01206 100644 --- a/api/src/repositories/sale.repository.js +++ b/api/src/repositories/sale.repository.js @@ -8796,6 +8796,17 @@ class SalesRepository { table_number: wanted, called_at: at, seen_at: null, + /* + * THE ROW MUST CARRY ITS DATE OR IT NEVER LEAVES THIS DATABASE. + * + * This is a native insert, so nothing stamps it. The sync agent finds + * work with { updated_date: { $exists: true } } and the gateway sends + * a till only rows whose updated_date moved. A call written without one + * sits in the cloud for ever, and the person at the table keeps + * waving. Same trap as the settings save (#838). + */ + created_date: at, + updated_date: at, /* What the device was, for a shop wondering later where a run of calls came from. Never anything that identifies the person. */ client: client && typeof client === 'object' ? client : null, @@ -8892,7 +8903,13 @@ class SalesRepository { if (BaseModel.license) filter.license = BaseModel.license; const done = await db.collection('waitercalls').updateOne(filter, { - $set: { seen_at: new Date(), seen_by: BaseModel.loggedUserName || '' }, + /* updated_date moves too, or the cloud never learns the call was + answered and the ordering page keeps saying "already calling". */ + $set: { + seen_at: new Date(), + seen_by: BaseModel.loggedUserName || '', + updated_date: new Date(), + }, }); if (!done.matchedCount) return { status: false, message: 'not_found', data: null }; diff --git a/api/src/sync/collections.json b/api/src/sync/collections.json index fdc93af12..c4aa706d8 100644 --- a/api/src/sync/collections.json +++ b/api/src/sync/collections.json @@ -12,6 +12,7 @@ ], "synced": { "recycle_bin": "branch", + "waitercalls": "branch", "sales": "branch", "stocklogs": "branch", "cashregister": "branch", @@ -53,7 +54,6 @@ }, "undecided": { "_note": "Shop data that today stays where it was written. Each needs a decision and, if synced, a check that rows carry branch_id and license and that deletes leave a tombstone. Until decided, a till and the cloud can disagree on these exactly as they did on tables.", - "waitercalls": "a table's call from the ordering page lands in the cloud; a till with its own database never sees it. This was marked synced on the day the feature shipped, but no lane was ever built in the sync agent or allowed by the sync-gateway, so it synced on paper only until the packaging guard refused the build. To sync it: a lane in the agent, the collection allowed by the gateway, a check that rows carry branch_id and license, and a tombstone on close", "quotes": "a quote made on the till does not exist in the cloud, and the reverse", "coupons": "a coupon created in the cloud cannot be redeemed on a till", "coupon_redemptions": "a redemption on the till is never known to the cloud", diff --git a/tests/a-call-carries-its-date.test.js b/tests/a-call-carries-its-date.test.js new file mode 100644 index 000000000..7dc787b1c --- /dev/null +++ b/tests/a-call-carries-its-date.test.js @@ -0,0 +1,67 @@ +'use strict'; + +/* + * A table's call reaches the till, and the answer reaches the table. + * + * "A table can call somebody over" (7a0aac6e) wrote the call into whichever + * database the ordering page was talking to - the cloud's - and marked the + * collection synced. Two things stood between that row and the till's request + * dock, and neither was visible from the feature's own tests: + * + * NO LANE. The sync agent had no entry for waitercalls and the gateway + * accepted none. Built in Posnic/Gateway ("A call reaches the till"); the + * packaging guard in scripts/prepare-sync-agent.js is what noticed, by + * refusing a build on 2026-09-17. + * + * NO DATE. The row was a native insert with no updated_date. The agent + * finds its work with { updated_date: { $exists: true } } and the gateway + * sends a till only rows whose updated_date moved. A lane would have + * carried nothing. Same trap as the settings save (#838), same family as + * the handset module nothing required: written, tested, read by nobody. + * + * Both writes are pinned here, because the second one is the one that gets + * lost: marking a call seen at the till must move updated_date too, or the + * cloud never learns the call was answered and the ordering page keeps + * telling the customer "already calling". + */ + +const test = require('node:test'); +const assert = require('node:assert'); +const fs = require('node:fs'); +const path = require('node:path'); + +const ROOT = path.join(__dirname, '..'); +const REPO = fs.readFileSync(path.join(ROOT, 'api', 'src', 'repositories', 'sale.repository.js'), 'utf8'); +const DECISIONS = JSON.parse(fs.readFileSync(path.join(ROOT, 'api', 'src', 'sync', 'collections.json'), 'utf8')); + +/** The source of one method, by name, so an assertion cannot match a neighbour. */ +function methodSource(name) { + const at = REPO.indexOf('async ' + name + '('); + assert.ok(at > 0, name + ' is not in sale.repository.js'); + const next = REPO.indexOf(String.fromCharCode(10) + ' async ', at + 1); + return REPO.slice(at, next > 0 ? next : undefined); +} + +test('a new call carries created_date and updated_date', () => { + const src = methodSource('callWaiter'); + assert.ok(src.includes('called_at: at,'), 'the call is no longer stamped with when it was made'); + assert.ok(src.includes('created_date: at,'), 'a call has no created_date'); + assert.ok(src.includes('updated_date: at,'), 'a call has no updated_date, so it never leaves the cloud'); +}); + +test('answering a call moves updated_date, so the cloud learns it was answered', () => { + const src = methodSource('seeWaiterCall'); + const set = src.slice(src.indexOf('$set:'), src.indexOf('}', src.indexOf('$set:')) + 1); + assert.ok(set.includes('seen_at: new Date()'), 'seen no longer stamps seen_at'); + assert.ok(set.includes('updated_date: new Date()'), 'seen does not move updated_date; the answer never syncs back'); +}); + +test('waitercalls is a synced, branch-scoped collection again', () => { + /* + * It was moved to undecided (#851) when the guard found the promise had no + * lane behind it. With the lane built, the promise is true again - and the + * guard keeps checking, against the agent actually bundled. + */ + assert.strictEqual(DECISIONS.synced.waitercalls, 'branch'); + assert.ok(!(DECISIONS.undecided && DECISIONS.undecided.waitercalls), 'still listed as undecided'); +}); From 8d70485dbd511232d0a729bb27630e84a6a52c28 Mon Sep 17 00:00:00 2001 From: Sridhar Bala Date: Thu, 17 Sep 2026 18:40:10 +0530 Subject: [PATCH 3/3] The test names the method the repository actually has --- tests/a-call-carries-its-date.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/a-call-carries-its-date.test.js b/tests/a-call-carries-its-date.test.js index 7dc787b1c..299fdf950 100644 --- a/tests/a-call-carries-its-date.test.js +++ b/tests/a-call-carries-its-date.test.js @@ -43,7 +43,7 @@ function methodSource(name) { } test('a new call carries created_date and updated_date', () => { - const src = methodSource('callWaiter'); + const src = methodSource('callTheWaiter'); assert.ok(src.includes('called_at: at,'), 'the call is no longer stamped with when it was made'); assert.ok(src.includes('created_date: at,'), 'a call has no created_date'); assert.ok(src.includes('updated_date: at,'), 'a call has no updated_date, so it never leaves the cloud');