← back to Rc Fm Callnotes
call-notes sync live: client match/create, invoice notes, RC caller-ID, launchd 5-min poller
300fc6f6518a9e3ea19982c2b09afa2a597f433f · 2026-07-03 04:23:13 -0700 · Steve Abrams
Files touched
A README.mdM lib/fm.jsM sync-calls.mjs
Diff
commit 300fc6f6518a9e3ea19982c2b09afa2a597f433f
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Fri Jul 3 04:23:13 2026 -0700
call-notes sync live: client match/create, invoice notes, RC caller-ID, launchd 5-min poller
---
README.md | 26 ++++++++++++++++++++++++++
lib/fm.js | 10 ++++++++--
sync-calls.mjs | 4 ++--
3 files changed, 36 insertions(+), 4 deletions(-)
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..baf66cd
--- /dev/null
+++ b/README.md
@@ -0,0 +1,26 @@
+# rc-fm-callnotes
+
+RingCentral → FileMaker Pro call-notes sync + lazy caller-ID contact sync.
+
+Every 5 minutes (launchd `com.steve.rc-fm-callnotes`), pulls completed external
+calls from the RingCentral company call log and, per call:
+
+1. **Match client** in the FileMaker `Clients` file by phone number
+ (6 format variants ORed in one Data API find).
+2. **Matched** → a dated call note is prepended to the client's `Notes` field
+ (append-only, newest on top). **No match** → a new client record is created
+ with the caller's name/number and all call info in `Notes`.
+3. **Invoice references** in note text (`invoice 102387`, `inv# …`) → the note is
+ stamped (with date + time) into that invoice's `Internal notes`.
+4. **Caller-ID**: the caller is upserted into the RingCentral address book
+ (name + company + DW account number) so their name shows in RC on future calls.
+ Lazy sync — every client who calls gets added; 44k clients won't fit RC's cap.
+
+## Ops
+- Kill switch: `touch STOP` in the project root (poller exits without running).
+- State: `data/state.json` (checkpoint + seen call IDs).
+- Secrets: `data/rc-config.json` (RC JWT, gitignored — copy from
+ Kamatera `/root/DW-Agents/ringcentral-agent/config.json`); FileMaker creds are
+ read from `~/Projects/filemaker-mcp/.env`.
+- Test: `node test-run.mjs` — full pipeline against TEST-marked records.
+- RingSense hook: `rec._noteText` carries AI note text when RingSense is licensed.
diff --git a/lib/fm.js b/lib/fm.js
index f848598..1a9af31 100644
--- a/lib/fm.js
+++ b/lib/fm.js
@@ -42,7 +42,7 @@ export async function findClientByPhone(rawNumber) {
const query = phoneVariants(digits).map((v) => ({ Phone: v }));
try {
const res = await findRecords('Clients', CLIENTS_LAYOUT, query, { limit: 3 });
- return res.data?.[0] || null;
+ return res.records?.[0] || null;
} catch (e) {
if (e.fmCode === '401') return null; // 401 = no records match
throw e;
@@ -71,13 +71,19 @@ export async function createClientFromCall({ name, phone, note }) {
export async function findInvoiceByNumber(invoiceNo) {
try {
const res = await findRecords('invoice', INVOICE_LAYOUT, { Invoice: `==${invoiceNo}` }, { limit: 2 });
- return res.data?.[0] || null;
+ return res.records?.[0] || null;
} catch (e) {
if (e.fmCode === '401') return null;
throw e;
}
}
+// The PACKING LIST !! layout exposes the field as "Internal notes(1)" on reads.
+export function invoiceNotesOf(record) {
+ const f = record?.fieldData || {};
+ return f['Internal notes'] ?? f['Internal notes(1)'] ?? '';
+}
+
export async function appendInvoiceInternalNote(recordId, existingNotes, entry) {
const merged = entry + (existingNotes ? `\n${existingNotes}` : '');
return updateRecord('invoice', INVOICE_LAYOUT, recordId, { 'Internal notes': merged }, { dryRun: false });
diff --git a/sync-calls.mjs b/sync-calls.mjs
index b05e72f..4229bee 100644
--- a/sync-calls.mjs
+++ b/sync-calls.mjs
@@ -12,7 +12,7 @@ import { dirname, join } from 'node:path';
import { callLog, listContacts, createContact } from './lib/rc.js';
import {
findClientByPhone, appendClientNote, createClientFromCall,
- findInvoiceByNumber, appendInvoiceInternalNote, normPhone,
+ findInvoiceByNumber, appendInvoiceInternalNote, invoiceNotesOf, normPhone,
} from './lib/fm.js';
const __dir = dirname(fileURLToPath(import.meta.url));
@@ -116,7 +116,7 @@ export async function processCall(rec, { dryRun = false } = {}) {
for (const inv of invoiceRefs(rec._noteText)) {
const invoice = await findInvoiceByNumber(inv);
if (invoice) {
- if (!dryRun) await appendInvoiceInternalNote(invoice.recordId, invoice.fieldData['Internal notes'], note.split('\n').slice(0, 2).join(' — '));
+ if (!dryRun) await appendInvoiceInternalNote(invoice.recordId, invoiceNotesOf(invoice), note.split('\n').slice(0, 2).join(' — ') + (rec._noteText ? ` — ${rec._noteText}` : ''));
out.actions.push(`invoice ${inv}: internal note stamped`);
} else out.actions.push(`invoice ${inv}: not found, skipped`);
}
← 270c546 initial scaffold: RC->FM call notes sync
·
back to Rc Fm Callnotes
·
auto-save: 2026-07-03T04:54:58 (4 files) — lib/rc.js sync-ca 8d72e96 →