[object Object]

← 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

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 →