← back to Dw Photo Capture
feat: FileMaker sample-request lookup + print-sticker handoff
31dbd1217cadb9ec404b1e86dff7d7c8b93c5e77 · 2026-07-06 17:12:36 -0700 · Steve Abrams
- fm-client.js: FileMaker Cloud (Claris/Cognito) client mirroring filemaker-mcp — idToken→session
auth, fmFind/fmGet/fmUpdate (dryRun-gated); guarded require so missing dep/creds degrade gracefully.
- /api/sample-request: identify-first (unified) → find most-recent OPEN sample request in FileMaker
(today for client filled + vendor-order filled + WP-sample-sent empty), OR(combo sku, Mfr Pattern),
sort client-ordered desc. Returns identity+client name+dates. Verified: AV-3X17-S→Haley McKinley.
- /api/print-sticker: flags the record (FM_PRINT_FLAG_FIELD) for the Mac poller; dryRun unless FM_WRITE=1.
- dep amazon-cognito-identity-js.
Files touched
A fm-client.jsM package-lock.jsonM package.jsonM server.js
Diff
commit 31dbd1217cadb9ec404b1e86dff7d7c8b93c5e77
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Jul 6 17:12:36 2026 -0700
feat: FileMaker sample-request lookup + print-sticker handoff
- fm-client.js: FileMaker Cloud (Claris/Cognito) client mirroring filemaker-mcp — idToken→session
auth, fmFind/fmGet/fmUpdate (dryRun-gated); guarded require so missing dep/creds degrade gracefully.
- /api/sample-request: identify-first (unified) → find most-recent OPEN sample request in FileMaker
(today for client filled + vendor-order filled + WP-sample-sent empty), OR(combo sku, Mfr Pattern),
sort client-ordered desc. Returns identity+client name+dates. Verified: AV-3X17-S→Haley McKinley.
- /api/print-sticker: flags the record (FM_PRINT_FLAG_FIELD) for the Mac poller; dryRun unless FM_WRITE=1.
- dep amazon-cognito-identity-js.
---
fm-client.js | 93 +++++++++++++++++++++++
package-lock.json | 223 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
package.json | 3 +
server.js | 68 +++++++++++++++++
4 files changed, 387 insertions(+)
diff --git a/fm-client.js b/fm-client.js
new file mode 100644
index 0000000..66bbe92
--- /dev/null
+++ b/fm-client.js
@@ -0,0 +1,93 @@
+// FileMaker Cloud (Claris ID / Cognito) client for dwphoto — mirrors ~/Projects/filemaker-mcp.
+// Auth flow: Claris email+password --SRP--> Cognito idToken (~1h) --> per-db Data API session (15m).
+// FileMaker Cloud does NOT accept Basic auth on the Data API; it needs an FMID token from Cognito.
+// Reads are safe; the single write (fmUpdate) defaults to dryRun so it can't touch live data by accident.
+const cognito = require('amazon-cognito-identity-js');
+
+const HOST = () => process.env.FM_CLOUD_HOST || '';
+const base = (db) => `https://${HOST()}/fmi/data/vLatest/databases/${encodeURIComponent(db)}`;
+
+// --- Cognito idToken (cache ~1h, refresh 60s early) ---
+let idCache = { token: null, exp: 0 };
+function getIdToken() {
+ const now = Date.now();
+ if (idCache.token && now < idCache.exp - 60000) return Promise.resolve(idCache.token);
+ const Username = process.env.FM_CLARIS_EMAIL, Password = process.env.FM_CLARIS_PASSWORD;
+ if (!Username || !Password) return Promise.reject(new Error('FM_CLARIS_EMAIL / FM_CLARIS_PASSWORD not set'));
+ const pool = new cognito.CognitoUserPool({
+ UserPoolId: process.env.FM_COGNITO_USERPOOL || 'us-west-2_NqkuZcXQY',
+ ClientId: process.env.FM_COGNITO_CLIENTID || '4l9rvl4mv5es1eep1qe97cautn',
+ });
+ const user = new cognito.CognitoUser({ Username, Pool: pool });
+ const details = new cognito.AuthenticationDetails({ Username, Password });
+ return new Promise((resolve, reject) => {
+ user.authenticateUser(details, {
+ onSuccess: (s) => { idCache = { token: s.getIdToken().getJwtToken(), exp: now + 60 * 60 * 1000 }; resolve(idCache.token); },
+ onFailure: (e) => reject(new Error('Claris ID auth failed: ' + (e && e.message || e))),
+ mfaRequired: () => reject(new Error('Claris ID account has MFA enabled — disable it on the API service account.')),
+ totpRequired: () => reject(new Error('Claris ID account requires TOTP — disable MFA on the service account.')),
+ newPasswordRequired: () => reject(new Error('Claris ID account requires a new (permanent) password.')),
+ });
+ });
+}
+
+// --- per-database Data API session token (cache 14m) ---
+const sessions = new Map();
+async function getSession(db) {
+ const now = Date.now();
+ const s = sessions.get(db);
+ if (s && now < s.exp) return s.token;
+ const idToken = await getIdToken();
+ const res = await fetch(`${base(db)}/sessions`, {
+ method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `FMID ${idToken}` }, body: '{}',
+ });
+ const json = await res.json().catch(() => ({}));
+ const code = json && json.messages && json.messages[0] && json.messages[0].code;
+ if (!res.ok || (code && code !== '0')) throw new Error(`FileMaker session failed (${db}): HTTP ${res.status} ${JSON.stringify(json.messages || json)}`);
+ const token = json.response.token;
+ sessions.set(db, { token, exp: now + 14 * 60 * 1000 });
+ return token;
+}
+
+async function fm(db, path, { method = 'GET', body } = {}) {
+ const token = await getSession(db);
+ const res = await fetch(`${base(db)}${path}`, {
+ method, headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
+ body: body ? JSON.stringify(body) : undefined,
+ });
+ const json = await res.json().catch(() => ({}));
+ const msg = json && json.messages && json.messages[0];
+ if (msg && msg.code && msg.code !== '0') {
+ // code 401 = "no records match" — not an error for our purposes; surface as empty.
+ if (msg.code === '401') return { data: [], dataInfo: { foundCount: 0 } };
+ const e = new Error(`FileMaker [${msg.code}] ${msg.message} (${db}${path})`); e.fmCode = msg.code; throw e;
+ }
+ if (!res.ok) throw new Error(`FileMaker HTTP ${res.status} (${db}${path})`);
+ return json.response;
+}
+
+async function fmFind(db, layout, query, { limit = 20, offset = 1, sort } = {}) {
+ const body = { query: Array.isArray(query) ? query : [query], limit: String(limit), offset: String(offset) };
+ if (sort) body.sort = sort;
+ const r = await fm(db, `/layouts/${encodeURIComponent(layout)}/_find`, { method: 'POST', body });
+ return { records: r.data || [], dataInfo: r.dataInfo };
+}
+
+async function fmGet(db, layout, recordId) {
+ const r = await fm(db, `/layouts/${encodeURIComponent(layout)}/records/${encodeURIComponent(recordId)}`);
+ return (r.data && r.data[0]) || null;
+}
+
+// Update ONE record. dryRun (default) returns the before→after diff WITHOUT committing.
+async function fmUpdate(db, layout, recordId, fieldData, { dryRun = true } = {}) {
+ const cur = await fmGet(db, layout, recordId);
+ if (!cur) throw new Error(`record ${recordId} not found in ${db}/${layout}`);
+ const before = cur.fieldData || {}, diff = {};
+ for (const [k, v] of Object.entries(fieldData)) if (String(before[k] ?? '') !== String(v ?? '')) diff[k] = { from: before[k], to: v };
+ if (dryRun) return { committed: false, dryRun: true, recordId, changes: diff };
+ if (!Object.keys(diff).length) return { committed: false, recordId, changes: {}, note: 'no change' };
+ await fm(db, `/layouts/${encodeURIComponent(layout)}/records/${encodeURIComponent(recordId)}`, { method: 'PATCH', body: { fieldData } });
+ return { committed: true, recordId, changes: diff };
+}
+
+module.exports = { getIdToken, fmFind, fmGet, fmUpdate };
diff --git a/package-lock.json b/package-lock.json
index 8da36e1..7ad3241 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7,6 +7,9 @@
"": {
"name": "dw-photo-capture",
"version": "1.2.0",
+ "dependencies": {
+ "amazon-cognito-identity-js": "^6.3.20"
+ },
"devDependencies": {
"@eslint/js": "^9.0.0",
"eslint": "^9.0.0",
@@ -14,6 +17,62 @@
"globals": "^15.0.0"
}
},
+ "node_modules/@aws-crypto/sha256-js": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-1.2.2.tgz",
+ "integrity": "sha512-Nr1QJIbW/afYYGzYvrF70LtaHrIRtd4TNAglX8BvlfxJLZ45SAmueIKYl5tWoNBPzp65ymXGFK0Bb1vZUpuc9g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/util": "^1.2.2",
+ "@aws-sdk/types": "^3.1.0",
+ "tslib": "^1.11.1"
+ }
+ },
+ "node_modules/@aws-crypto/util": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-1.2.2.tgz",
+ "integrity": "sha512-H8PjG5WJ4wz0UXAFXeJjWCW1vkvIJ3qUUD+rGRwJ2/hj+xT58Qle2MTql/2MGzkU+1JLAFuR6aJpLAjHwhmwwg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "^3.1.0",
+ "@aws-sdk/util-utf8-browser": "^3.0.0",
+ "tslib": "^1.11.1"
+ }
+ },
+ "node_modules/@aws-sdk/types": {
+ "version": "3.973.15",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.15.tgz",
+ "integrity": "sha512-IULn8uBV/SMtmOIANsm4WHXIOtVPBWfOWs3WGL0j/sI+KhaYehvOw0ET+9urnn8MBpiijuU/0JOpuwKOE451PQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.15.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/types/node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
+ "node_modules/@aws-sdk/util-utf8-browser": {
+ "version": "3.259.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.259.0.tgz",
+ "integrity": "sha512-UvFa/vR+e19XookZF8RzFZBrw2EUkQWxiBW0yYQAhvk3C+QVGl0H3ouca8LDBlBfQKXwmW3huo/59H8rwb1wJw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.3.1"
+ }
+ },
+ "node_modules/@aws-sdk/util-utf8-browser/node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
"node_modules/@eslint-community/eslint-utils": {
"version": "4.9.1",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
@@ -237,6 +296,24 @@
"url": "https://github.com/sponsors/nzakas"
}
},
+ "node_modules/@smithy/types": {
+ "version": "4.15.1",
+ "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.15.1.tgz",
+ "integrity": "sha512-x3L0XSACF6UYzKpa9biqiRMgvH5+wnFFew9Tm/grFYqgaupPwx/+ojDPpPJM8dZON3S9tjz5U+PQYsCBd1Mw5Q==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/types/node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
"node_modules/@types/estree": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
@@ -291,6 +368,19 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
+ "node_modules/amazon-cognito-identity-js": {
+ "version": "6.3.20",
+ "resolved": "https://registry.npmjs.org/amazon-cognito-identity-js/-/amazon-cognito-identity-js-6.3.20.tgz",
+ "integrity": "sha512-akaaLpDqz4i0m2XG4+x+XcHAlboRt3rydVrSiEFCKvaGZSmZdvnYW4SXqm2OGeVdZK0R1nIXjp8yEpID3rHC5Q==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/sha256-js": "1.2.2",
+ "buffer": "4.9.2",
+ "fast-base64-decode": "^1.0.0",
+ "isomorphic-unfetch": "^3.0.0",
+ "js-cookie": "^3.0.7"
+ }
+ },
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
@@ -321,6 +411,26 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/brace-expansion": {
"version": "1.1.15",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz",
@@ -332,6 +442,17 @@
"concat-map": "0.0.1"
}
},
+ "node_modules/buffer": {
+ "version": "4.9.2",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz",
+ "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==",
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.0.2",
+ "ieee754": "^1.1.4",
+ "isarray": "^1.0.0"
+ }
+ },
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -691,6 +812,12 @@
"node": ">=0.10.0"
}
},
+ "node_modules/fast-base64-decode": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fast-base64-decode/-/fast-base64-decode-1.0.0.tgz",
+ "integrity": "sha512-qwaScUgUGBYeDNRnbc/KyllVU88Jk1pRHPStuF/lO7B0/RTRLj7U0lkdTAutlBblY08rwZDff6tNU9cjv6j//Q==",
+ "license": "MIT"
+ },
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -819,6 +946,26 @@
"entities": "^7.0.1"
}
},
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -879,6 +1026,12 @@
"node": ">=0.10.0"
}
},
+ "node_modules/isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+ "license": "MIT"
+ },
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -886,6 +1039,22 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/isomorphic-unfetch": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/isomorphic-unfetch/-/isomorphic-unfetch-3.1.0.tgz",
+ "integrity": "sha512-geDJjpoZ8N0kWexiwkX8F9NkTsXhetLPVbZFQ+JTW239QNOwvB0gniuR1Wc6f0AMTn7/mFGyXvHTifrCp/GH8Q==",
+ "license": "MIT",
+ "dependencies": {
+ "node-fetch": "^2.6.1",
+ "unfetch": "^4.2.0"
+ }
+ },
+ "node_modules/js-cookie": {
+ "version": "3.0.8",
+ "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.8.tgz",
+ "integrity": "sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw==",
+ "license": "MIT"
+ },
"node_modules/js-yaml": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz",
@@ -1004,6 +1173,26 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/node-fetch": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-url": "^5.0.0"
+ },
+ "engines": {
+ "node": "4.x || >=6.0.0"
+ },
+ "peerDependencies": {
+ "encoding": "^0.1.0"
+ },
+ "peerDependenciesMeta": {
+ "encoding": {
+ "optional": true
+ }
+ }
+ },
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -1166,6 +1355,18 @@
"node": ">=8"
}
},
+ "node_modules/tr46": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
+ "license": "MIT"
+ },
+ "node_modules/tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
+ "license": "0BSD"
+ },
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -1179,6 +1380,12 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/unfetch": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.2.0.tgz",
+ "integrity": "sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==",
+ "license": "MIT"
+ },
"node_modules/uri-js": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
@@ -1189,6 +1396,22 @@
"punycode": "^2.1.0"
}
},
+ "node_modules/webidl-conversions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/whatwg-url": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ }
+ },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
diff --git a/package.json b/package.json
index 8481e60..728b4cf 100644
--- a/package.json
+++ b/package.json
@@ -13,5 +13,8 @@
"eslint": "^9.0.0",
"eslint-plugin-html": "^8.1.1",
"globals": "^15.0.0"
+ },
+ "dependencies": {
+ "amazon-cognito-identity-js": "^6.3.20"
}
}
diff --git a/server.js b/server.js
index ad77206..5e9875b 100644
--- a/server.js
+++ b/server.js
@@ -14,6 +14,15 @@ const fs = require('fs');
const path = require('path');
const { execFile } = require('child_process');
+// FileMaker Cloud client (sample-request lookup + sticker-print flag). Loaded guarded so a missing
+// dep / unset creds degrade gracefully instead of crashing the scanner.
+let FM = null;
+try { FM = require('./fm-client.js'); } catch (e) { console.log('FileMaker client unavailable:', e.message); }
+const FM_DB = process.env.FM_DB || 'WALLPAPER';
+const FM_LAYOUT = process.env.FM_LAYOUT || 'Sample Requests via email';
+const FM_PRINT_FLAG = process.env.FM_PRINT_FLAG_FIELD || ''; // Steve designates a flag field the Mac poller reads
+const FM_ENABLED = () => !!(FM && process.env.FM_CLARIS_EMAIL && process.env.FM_CLARIS_PASSWORD && process.env.FM_CLOUD_HOST);
+
const ROOT = __dirname;
const DATA = path.join(ROOT, 'data');
const PHOTOS = path.join(ROOT, 'photos');
@@ -1032,6 +1041,65 @@ const appHandler = (req, res) => {
return;
}
+ // Sample-request lookup: BEST-ID the scanned item against the unified catalog, then find the
+ // most-recent OPEN sample request for it in FileMaker (client-ordered filled + vendor-ordered
+ // filled + WP-sample-sent EMPTY). Returns identity + client name + dates for the print popup.
+ if (u.pathname === '/api/sample-request' && (req.method === 'GET' || req.method === 'POST')) {
+ const run = (p) => {
+ const code = (p.code || p.sku || '').toString().trim();
+ const mfr = (p.mfr || '').toString().trim();
+ if (!code && !mfr) return send(res, 400, { err: 'code required' });
+ if (!FM_ENABLED()) return send(res, 200, { ok: false, found: false, err: 'FileMaker not configured' });
+ (async () => {
+ // 1) identify-first against the unified catalog → canonical house/mfr codes (= FileMaker keys)
+ let id = await identifyUnified(code).catch(() => null);
+ if (!id && mfr) id = await identifyUnified(mfr).catch(() => null);
+ const internal = id && id.internal_sku;
+ const mfrCode = (id && id.mfr_code) || mfr || code;
+ // 2) FileMaker: OR(combo sku exact, Mfr Pattern begins-with), each AND'd with the 3 date conditions
+ const dc = { 'today for client': '*', 'Date Sample Request printed for vendor': '*', 'Date WP Sample Sent': '=' };
+ const query = [];
+ if (internal) query.push(Object.assign({ 'combo sku': '==' + internal }, dc));
+ if (mfrCode) query.push(Object.assign({ 'Mfr Pattern': mfrCode + '*' }, dc));
+ if (!query.length) return send(res, 200, { ok: true, found: false, identity: id });
+ let r;
+ try { r = await FM.fmFind(FM_DB, FM_LAYOUT, query, { limit: 1, sort: [{ fieldName: 'today for client', sortOrder: 'descend' }] }); }
+ catch (e) { return send(res, 200, { ok: false, found: false, identity: id, err: e.message }); }
+ const rec = r.records[0];
+ if (!rec) return send(res, 200, { ok: true, found: false, identity: id });
+ const f = rec.fieldData;
+ send(res, 200, { ok: true, found: true, recordId: rec.recordId, identity: id,
+ item: { combo_sku: f['combo sku'] || null, mfr_pattern: f['Mfr Pattern'] || null, series: f['Series'] || null, vid: f['vid'] || null },
+ client: { name: f['company for client fileS'] || null },
+ dates: { client_ordered: f['today for client'] || null, vendor_ordered: f['Date Sample Request printed for vendor'] || null, wp_sent: f['Date WP Sample Sent'] || null } });
+ })();
+ };
+ if (req.method === 'GET') return run(Object.fromEntries(u.searchParams));
+ let body = ''; req.on('data', c => { body += c; if (body.length > 64 * 1024) req.destroy(); });
+ req.on('end', () => { let p; try { p = JSON.parse(body); } catch (e) { return send(res, 400, { err: 'bad json' }); } run(p); });
+ return;
+ }
+
+ // Print-sticker handoff: flag the FileMaker record so the Mac's FileMaker Pro poller runs the
+ // EXISTING print-sticker + stamp scripts (stamp Date WP Sample Sent = today + print the Zebra
+ // label). Write is dryRun unless FM_WRITE=1 (protects the live business file).
+ if (u.pathname === '/api/print-sticker' && req.method === 'POST') {
+ let body = ''; req.on('data', c => { body += c; if (body.length > 16 * 1024) req.destroy(); });
+ req.on('end', () => {
+ let p; try { p = JSON.parse(body); } catch (e) { return send(res, 400, { err: 'bad json' }); }
+ const recordId = (p.recordId || '').toString().trim();
+ const sticker = p.sticker === 'address' ? 'address' : 'sku';
+ if (!recordId) return send(res, 400, { err: 'recordId required' });
+ if (!FM_ENABLED()) return send(res, 200, { ok: false, err: 'FileMaker not configured' });
+ if (!FM_PRINT_FLAG) return send(res, 200, { ok: false, err: 'FM_PRINT_FLAG_FIELD not set — designate a flag field the FileMaker poller reads' });
+ const dryRun = process.env.FM_WRITE !== '1';
+ FM.fmUpdate(FM_DB, FM_LAYOUT, recordId, { [FM_PRINT_FLAG]: sticker }, { dryRun })
+ .then(r => send(res, 200, { ok: true, queued: !dryRun, dryRun, sticker, result: r }))
+ .catch(e => send(res, 200, { ok: false, err: e.message }));
+ });
+ return;
+ }
+
// Ask a question about a SKU — local Ollama answers from the product's own Shopify facts. $0.
if (u.pathname === '/api/ask' && req.method === 'POST') {
let body = '';
← a985673 feat: best-ID identify-first against unified catalog — ident
·
back to Dw Photo Capture
·
feat: sample-request sticker popup + scan cost throttle 7f00ab4 →