{ "kind": "open-flow-workflow-bundle", "version": 1, "entryFlowId": "hebrew-whatsapp-invoice-reminders", "workflows": [ { "flowId": "hebrew-whatsapp-invoice-reminders", "file": { "kind": "open-flow-workflow", "version": 1, "name": "Hebrew WhatsApp Invoice Reminders", "content": { "modelVersion": 14, "document": { "bindings": {}, "graph": { "nodes": { "n_a6c421c5e867cea4": { "name": "Daily at 09:00 Israel time", "kind": "cron", "cronTimes": [ { "type": "cron", "expression": "0 9 * * *", "timezone": "Asia/Jerusalem" } ] }, "n_631427101ee4325d": { "inputs": { "spreadsheetId": { "kind": "value", "value": "YOUR_SPREADSHEET_ID" }, "sheetName": { "kind": "value", "value": "Invoices" }, "timeZone": { "kind": "value", "value": "Asia/Jerusalem" } }, "name": "Configure invoice settings", "kind": "task", "task": { "inputs": [ { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "spreadsheetId" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "sheetName" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "timeZone" } ], "outputs": [ { "jsonSchema": { "type": "string", "minLength": 1 }, "nullable": false, "handle": "spreadsheetId" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "sheetName" }, { "jsonSchema": { "type": "string", "minLength": 1 }, "nullable": false, "handle": "range" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "timeZone" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "today" } ], "name": "Configure invoice settings", "moduleId": "n_ec829ac99976ecc7", "capabilities": [ { "kind": "connector", "mode": "independent", "actions": [] } ] } }, "n_716f6fb00a16df1e": { "inputs": { "spreadsheetId": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_631427101ee4325d", "output": "spreadsheetId" } ] }, "range": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_631427101ee4325d", "output": "range" } ] }, "majorDimension": { "kind": "value", "value": "ROWS" }, "valueRenderOption": { "kind": "value", "value": "UNFORMATTED_VALUE" }, "dateTimeRenderOption": { "kind": "value", "value": "FORMATTED_STRING" } }, "name": "Read complete invoices table", "kind": "task", "task": { "inputs": [ { "description": "The spreadsheet ID.", "jsonSchema": { "type": "string", "minLength": 1, "description": "The spreadsheet ID." }, "nullable": true, "handle": "spreadsheetId", "value": null }, { "description": "The A1 notation of the range to retrieve.", "jsonSchema": { "type": "string", "minLength": 1, "description": "The A1 notation of the range to retrieve." }, "nullable": false, "handle": "range" }, { "description": "The major dimension for the returned values.", "jsonSchema": { "type": "string", "enum": [ "DIMENSION_UNSPECIFIED", "ROWS", "COLUMNS" ], "description": "The major dimension for the returned values." }, "nullable": true, "handle": "majorDimension", "value": null }, { "description": "How values should be rendered in the output.", "jsonSchema": { "type": "string", "enum": [ "FORMATTED_VALUE", "UNFORMATTED_VALUE", "FORMULA" ], "description": "How values should be rendered in the output." }, "nullable": true, "handle": "valueRenderOption", "value": null }, { "description": "How dates and times should be rendered in the output.", "jsonSchema": { "type": "string", "enum": [ "SERIAL_NUMBER", "FORMATTED_STRING" ], "description": "How dates and times should be rendered in the output." }, "nullable": true, "handle": "dateTimeRenderOption", "value": null } ], "outputs": [ { "description": "The A1 notation of the range.", "jsonSchema": { "type": "string", "description": "The A1 notation of the range." }, "nullable": false, "handle": "range" }, { "description": "The major dimension of the values (ROWS or COLUMNS).", "jsonSchema": { "type": "string", "description": "The major dimension of the values (ROWS or COLUMNS)." }, "nullable": false, "handle": "majorDimension" }, { "description": "The data values in the range.", "jsonSchema": { "type": "array", "items": { "type": "array", "items": { "anyOf": [ { "type": "string", "description": "A union variant." }, { "type": "number", "description": "A union variant." }, { "type": "boolean", "description": "A union variant." }, { "const": null, "description": "A union variant." } ], "description": "The cell value schema." }, "description": "An array item." }, "description": "The data values in the range." }, "nullable": false, "handle": "values" } ], "name": "Read complete invoices table", "executor": { "kind": "connector", "action": "googlesheets.values_get" } } }, "n_f95d1b3ca3647b5d": { "inputs": { "values": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_716f6fb00a16df1e", "output": "values" } ] }, "majorDimension": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_716f6fb00a16df1e", "output": "majorDimension" } ] }, "today": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_631427101ee4325d", "output": "today" } ] }, "timeZone": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_631427101ee4325d", "output": "timeZone" } ] } }, "name": "Select exact 7, 14 and 30 day tiers", "kind": "task", "task": { "inputs": [ { "jsonSchema": { "type": "array", "items": { "type": "array", "items": { "anyOf": [ { "type": "string", "description": "A union variant." }, { "type": "number", "description": "A union variant." }, { "type": "boolean", "description": "A union variant." }, { "const": null, "description": "A union variant." } ], "description": "The cell value schema." }, "description": "An array item." }, "description": "The data values in the range." }, "nullable": false, "handle": "values" }, { "jsonSchema": { "type": "string", "description": "The major dimension of the values (ROWS or COLUMNS)." }, "nullable": false, "handle": "majorDimension" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "today" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "timeZone" } ], "outputs": [ { "jsonSchema": { "type": "array", "items": { "type": "object", "properties": { "InvoiceID": { "type": "string", "minLength": 1 }, "tier": { "type": "string" }, "date": { "type": "string" }, "daysOverdue": { "type": "integer", "enum": [ 7, 14, 30 ] }, "chatId": { "type": "string" }, "message": { "type": "string" } }, "required": [ "InvoiceID", "tier", "date", "daysOverdue", "chatId", "message" ], "additionalProperties": false } }, "nullable": false, "handle": "reminders" }, { "jsonSchema": { "type": "integer", "minimum": 0 }, "nullable": false, "handle": "invalidDates" }, { "jsonSchema": { "type": "integer", "minimum": 0 }, "nullable": false, "handle": "selectedCount" } ], "name": "Select exact 7, 14 and 30 day tiers", "moduleId": "n_a7bfd25a22d014e8", "capabilities": [ { "kind": "connector", "mode": "independent", "actions": [] } ] } }, "n_48fb9eb88dbbe5a4": { "inputs": { "reminders": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_f95d1b3ca3647b5d", "output": "reminders" } ] }, "previousState": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_48fb9eb88dbbe5a4", "output": "state" } ] }, "receipt": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "kath8pmcr5", "output": "receipt" } ] } }, "name": "Collect completed reminders", "kind": "task", "task": { "inputs": [ { "jsonSchema": { "type": "array", "items": { "type": "object", "properties": { "InvoiceID": { "type": "string", "minLength": 1 }, "tier": { "type": "string" }, "date": { "type": "string" }, "daysOverdue": { "type": "integer", "enum": [ 7, 14, 30 ] }, "chatId": { "type": "string" }, "message": { "type": "string" } }, "required": [ "InvoiceID", "tier", "date", "daysOverdue", "chatId", "message" ], "additionalProperties": false } }, "nullable": false, "handle": "reminders" }, { "jsonSchema": { "type": "object" }, "nullable": true, "handle": "previousState" }, { "jsonSchema": { "type": "object" }, "nullable": true, "handle": "receipt" } ], "outputs": [ { "jsonSchema": { "type": "object", "properties": { "index": { "type": "integer", "minimum": 0 }, "receipts": { "type": "array", "items": { "type": "object" } } }, "required": [ "index", "receipts" ], "additionalProperties": false }, "nullable": false, "handle": "state" }, { "jsonSchema": { "type": "integer", "minimum": 0 }, "nullable": false, "handle": "index" }, { "jsonSchema": { "type": "boolean" }, "nullable": false, "handle": "more" }, { "jsonSchema": { "type": "boolean" }, "nullable": false, "handle": "needsPause" }, { "jsonSchema": { "type": "integer", "minimum": 0 }, "nullable": false, "handle": "sentCount" } ], "name": "Collect completed reminders", "moduleId": "n_99c6d837c598d1c3", "capabilities": [ { "kind": "connector", "mode": "independent", "actions": [] } ] } }, "n_b28b8024a582834a": { "inputs": {}, "name": "More invoice reminders", "kind": "condition", "matchMode": "first", "cases": [ { "output": "more", "groups": [ { "expressions": [ { "left": { "kind": "source", "source": { "kind": "node", "nodeId": "n_48fb9eb88dbbe5a4", "output": "more" } }, "operator": "==", "right": { "kind": "value", "value": true } } ] } ] } ] }, "n_a955657c33ac7b8e": { "inputs": {}, "name": "Earlier reminder completed", "kind": "condition", "matchMode": "first", "cases": [ { "output": "pause", "groups": [ { "expressions": [ { "left": { "kind": "source", "source": { "kind": "node", "nodeId": "n_48fb9eb88dbbe5a4", "output": "needsPause" } }, "operator": "==", "right": { "kind": "value", "value": true } } ] } ] } ] }, "n_d34084ef22b73edb": { "inputs": {}, "name": "Wait three seconds", "kind": "wait", "inputDefinitions": [], "prompt": "Wait three seconds", "autoContinue": { "kind": "after", "durationMs": 3000 } }, "n_26d9a7395748c15f": { "name": "Sent reminders and skipped dates", "kind": "outputs", "outputDefs": [ { "jsonSchema": { "type": "array", "items": { "type": "object" } }, "nullable": false, "handle": "receipts" }, { "jsonSchema": { "type": "integer", "minimum": 0 }, "nullable": false, "handle": "sentCount" }, { "jsonSchema": { "type": "integer", "minimum": 0 }, "nullable": false, "handle": "invalidDates" } ], "outputMappings": { "invalidDates": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_f95d1b3ca3647b5d", "output": "invalidDates" } ] }, "receipts": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_48fb9eb88dbbe5a4", "output": "state", "field": "receipts" } ] }, "sentCount": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_48fb9eb88dbbe5a4", "output": "sentCount" } ] } } }, "kath8pmcr5": { "inputs": { "reminders": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_f95d1b3ca3647b5d", "output": "reminders" } ] }, "index": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_48fb9eb88dbbe5a4", "output": "index" } ] }, "spreadsheetId": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_631427101ee4325d", "output": "spreadsheetId" } ] }, "range": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_631427101ee4325d", "output": "range" } ] } }, "name": "Send and log one reminder", "timeoutMs": 7200000, "kind": "task", "task": { "inputs": [ { "jsonSchema": { "type": "array", "items": { "type": "object", "properties": { "InvoiceID": { "type": "string", "minLength": 1 }, "tier": { "type": "string" }, "date": { "type": "string" }, "daysOverdue": { "type": "integer", "enum": [ 7, 14, 30 ] }, "chatId": { "type": "string" }, "message": { "type": "string" } }, "required": [ "InvoiceID", "tier", "date", "daysOverdue", "chatId", "message" ], "additionalProperties": false } }, "nullable": false, "handle": "reminders" }, { "jsonSchema": { "type": "integer", "minimum": 0 }, "nullable": false, "handle": "index" }, { "jsonSchema": { "type": "string", "minLength": 1 }, "nullable": false, "handle": "spreadsheetId" }, { "jsonSchema": { "type": "string", "minLength": 1 }, "nullable": false, "handle": "range" } ], "outputs": [ { "jsonSchema": { "type": "object" }, "nullable": false, "handle": "receipt" } ], "name": "Send and log one reminder", "moduleId": "zst8pgw6rz", "capabilities": [ { "kind": "workflow", "name": "send-and-log-invoice-reminder", "flowId": "send-and-log-one-invoice-reminder", "revisionId": "", "inputs": [ { "jsonSchema": { "type": "object", "properties": { "InvoiceID": { "type": "string", "minLength": 1 }, "tier": { "type": "string" }, "date": { "type": "string" }, "daysOverdue": { "type": "integer", "enum": [ 7, 14, 30 ] }, "chatId": { "type": "string" }, "message": { "type": "string" } }, "required": [ "InvoiceID", "tier", "date", "daysOverdue", "chatId", "message" ], "additionalProperties": false }, "nullable": false, "handle": "reminder" }, { "jsonSchema": { "type": "string", "minLength": 1 }, "nullable": false, "handle": "spreadsheetId" }, { "jsonSchema": { "type": "string", "minLength": 1 }, "nullable": false, "handle": "range" } ], "outputs": [ { "jsonSchema": { "type": "object" }, "nullable": false, "handle": "receipt" } ] } ] } } }, "edges": [ { "source": "n_a6c421c5e867cea4", "target": "n_631427101ee4325d" }, { "source": "n_631427101ee4325d", "target": "n_716f6fb00a16df1e" }, { "source": "n_716f6fb00a16df1e", "target": "n_f95d1b3ca3647b5d" }, { "source": "n_f95d1b3ca3647b5d", "target": "n_48fb9eb88dbbe5a4" }, { "source": "n_48fb9eb88dbbe5a4", "target": "n_b28b8024a582834a" }, { "source": "n_b28b8024a582834a", "target": "n_a955657c33ac7b8e", "sourceHandle": "more" }, { "source": "n_b28b8024a582834a", "target": "n_26d9a7395748c15f", "sourceHandle": "otherwise" }, { "source": "n_a955657c33ac7b8e", "target": "n_d34084ef22b73edb", "sourceHandle": "pause" }, { "source": "n_a955657c33ac7b8e", "target": "kath8pmcr5", "sourceHandle": "otherwise" }, { "source": "n_d34084ef22b73edb", "target": "kath8pmcr5", "sourceHandle": "continue" }, { "source": "kath8pmcr5", "target": "n_48fb9eb88dbbe5a4" } ] } }, "modules": { "n_ec829ac99976ecc7": { "name": "Configure invoice settings", "imports": [], "source": "export default function prepareInvoiceReminderSettings({ spreadsheetId, sheetName, timeZone }) {\n for (const [key, value] of Object.entries({ spreadsheetId, sheetName, timeZone })) if (typeof value !== 'string' || !value.trim() || value.includes('YOUR_')) throw new Error('Configure ' + key + '.');\n if (!/^[a-zA-Z0-9_-]+$/.test(spreadsheetId)) throw new Error('Use your Google spreadsheet ID.');\n const now = new Date(), parts = new Intl.DateTimeFormat('en-US', { timeZone, year: 'numeric', month: '2-digit', day: '2-digit' }).formatToParts(now), get = name => parts.find(part => part.type === name).value;\n const today = get('year') + '-' + get('month') + '-' + get('day');\n return { spreadsheetId: spreadsheetId.trim(), sheetName, range: \"'\" + sheetName.replaceAll(\"'\", \"''\") + \"'\", timeZone, today };\n}\n" }, "n_a7bfd25a22d014e8": { "name": "Select exact 7, 14 and 30 day tiers", "imports": [], "source": "function sheetTable({ values, majorDimension = \"ROWS\" }) {\n if (majorDimension !== \"ROWS\" || !Array.isArray(values) || values.some((row) => !Array.isArray(row)))\n throw Error(\"Google Sheets must return row-oriented cell values.\");\n const isEmpty = (value) => value == null || value === \"\", nonempty = (row) => row.some((value) => !isEmpty(value)), headerIndex = values.findIndex(nonempty);\n if (headerIndex < 0)\n return { columns: [], records: [] };\n const headers = values[headerIndex];\n if (headers.some((name) => typeof name !== \"string\" || !name.trim() || name === \"row_number\") || new Set(headers).size !== headers.length)\n throw Error(\"Use unique nonempty text headers; row_number is reserved.\");\n const columns = [\"row_number\", ...headers], records = [];\n for (let index = headerIndex + 1;index < values.length; index++) {\n const cells = values[index];\n if (!nonempty(cells))\n continue;\n if (cells.slice(headers.length).some((value) => !isEmpty(value)))\n throw Error(\"Every populated column needs a header.\");\n records.push(Object.fromEntries([[\"row_number\", index + 1], ...headers.map((name, column) => [name, cells[column] ?? \"\"])]));\n }\n return { columns, records };\n}\nconst reminderMessages = {\n 7: inv => 'היי ' + inv.name + ', תזכורת ידידותית: חשבונית מספר ' + inv.id + ' על סך ' + inv.amount + ' ₪ ממתינה לתשלום כבר שבוע. נשמח להסדרה בהקדם, תודה!',\n 14: inv => 'שלום ' + inv.name + ', חשבונית מספר ' + inv.id + ' על סך ' + inv.amount + ' ₪ פתוחה כבר 14 יום. נודה לטיפולך. אם התשלום כבר בוצע, אפשר להתעלם מהודעה זו.',\n 30: inv => 'שלום ' + inv.name + ', תזכורת אחרונה: חשבונית מספר ' + inv.id + ' על סך ' + inv.amount + ' ₪ פתוחה מזה 30 יום. נבקש להסדיר את התשלום עוד היום. לשאלות אנחנו זמינים כאן.',\n};\n\nfunction invoiceCalendarDate(value, timeZone) {\n const parts = new Intl.DateTimeFormat('en-US', { timeZone, year: 'numeric', month: '2-digit', day: '2-digit' }).formatToParts(value);\n const get = name => parts.find(part => part.type === name).value;\n return get('year') + '-' + get('month') + '-' + get('day');\n}\n\nfunction invoiceDateOrdinal(value, timeZone) {\n if (typeof value !== 'string' || !value.trim()) return null;\n const text = value.trim();\n const ordinal = (year, month, day) => {\n const date = new Date(Date.UTC(year, month - 1, day));\n return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day ? date.getTime() / 86400000 : null;\n };\n const iso = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(text);\n if (iso) return ordinal(Number(iso[1]), Number(iso[2]), Number(iso[3]));\n const timestamp = /^(\\d{4})-(\\d{2})-(\\d{2})T(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d{1,9})?)?(?:Z|[+-](?:[01]\\d|2[0-3]):?[0-5]\\d)$/.exec(text);\n if (timestamp && ordinal(Number(timestamp[1]), Number(timestamp[2]), Number(timestamp[3])) !== null) {\n const date = new Date(text);\n if (!Number.isFinite(date.getTime())) return null;\n return invoiceDateOrdinal(invoiceCalendarDate(date, timeZone), timeZone);\n }\n const slash = /^(\\d{1,2})\\/(\\d{1,2})\\/(\\d{4})$/.exec(text);\n if (!slash) return null;\n return ordinal(Number(slash[3]), Number(slash[2]), Number(slash[1])) ?? ordinal(Number(slash[3]), Number(slash[1]), Number(slash[2]));\n}\n\nfunction invoiceIdentity(value) {\n if (typeof value !== 'string' && !(typeof value === 'number' && Number.isFinite(value))) throw new Error('Use nonempty text or numeric InvoiceID values.');\n const id = String(value).trim();\n if (!id) throw new Error('Use a nonempty InvoiceID.');\n return id;\n}\n\nfunction invoiceChatId(value) {\n if (typeof value === 'number' && (!Number.isSafeInteger(value) || value <= 0)) throw new Error('Use a valid customer phone number.');\n if ((typeof value !== 'string' && typeof value !== 'number') || !/^[+\\d\\s().-]+$/.test(String(value))) throw new Error('Use an Israeli customer phone number.');\n let digits = String(value).replace(/\\D/g, '');\n if (digits.startsWith('0')) digits = '972' + digits.slice(1);\n if (!digits.startsWith('972')) digits = '972' + digits;\n if (!/^972[2-9]\\d{7,8}$/.test(digits)) throw new Error('Use an Israeli national number or a number with the 972 country code.');\n return digits + '@c.us';\n}\n\nfunction planInvoiceReminders({ rows, today, timeZone }) {\n const current = invoiceDateOrdinal(today, timeZone);\n if (!Array.isArray(rows) || current === null) throw new Error('Provide invoice rows and the processing calendar date.');\n const reminders = [];\n const idCounts = new Map();\n for (const row of rows) { try { const id = invoiceIdentity(row?.InvoiceID); idCounts.set(id, (idCounts.get(id) ?? 0) + 1); } catch {} }\n let invalidDates = 0;\n for (const row of rows) {\n if (!row || row.Status !== 'open') continue;\n const due = invoiceDateOrdinal(row.DueDate, timeZone);\n if (due === null) { invalidDates++; continue; }\n const daysOverdue = current - due, message = reminderMessages[daysOverdue];\n if (!message) continue;\n const InvoiceID = invoiceIdentity(row.InvoiceID);\n if (idCounts.get(InvoiceID) !== 1) throw new Error('Each selected InvoiceID must identify exactly one sheet row.');\n const name = row.CustomerName ?? '', amount = row.Amount ?? '';\n if (!['string', 'number'].includes(typeof name) || !['string', 'number'].includes(typeof amount)) throw new Error('CustomerName and Amount must be text or numbers.');\n reminders.push({ InvoiceID, tier: 'reminder-' + daysOverdue, daysOverdue, date: today, chatId: invoiceChatId(row.Phone), message: message({ id: InvoiceID, name, amount }) });\n }\n return { reminders, invalidDates, selectedCount: reminders.length };\n}\n\nexport default function(input) { const table=sheetTable(input); const expected=[\"InvoiceID\",\"CustomerName\",\"Amount\",\"DueDate\",\"Phone\",\"Status\",\"LastReminder\",\"LastReminderDate\"]; if(table.columns.length && expected.some(name=>!table.columns.includes(name))) throw new Error(\"The required invoice columns are missing.\"); return planInvoiceReminders({rows:table.records,today:input.today,timeZone:input.timeZone}); }\n" }, "n_99c6d837c598d1c3": { "name": "Collect completed reminders", "imports": [], "source": "export default function collectInvoiceReminders({ reminders, previousState, receipt }) {\n if (!Array.isArray(reminders)) throw new Error('Expected the validated invoice reminder collection.');\n let state;\n if (previousState === null) {\n if (receipt !== null) throw new Error('Unexpected receipt before invoice processing starts.');\n state = { index: 0, receipts: [] };\n } else {\n if (!Number.isSafeInteger(previousState.index) || previousState.index < 0 || previousState.index >= reminders.length || !Array.isArray(previousState.receipts) || previousState.receipts.length !== previousState.index) throw new Error('Invalid invoice reminder collection state.');\n const reminder = reminders[previousState.index];\n if (receipt?.InvoiceID !== reminder.InvoiceID || receipt.tier !== reminder.tier) throw new Error('The reminder receipt does not match this invoice.');\n state = { index: previousState.index + 1, receipts: [...previousState.receipts, receipt] };\n }\n return { state, index: state.index, more: state.index < reminders.length, needsPause: state.index > 0, sentCount: state.index };\n}\n" }, "zst8pgw6rz": { "name": "Send and log one reminder", "imports": [], "source": "export async function sendOneInvoiceReminder({ reminders, index, spreadsheetId, range }, context) {\n if (!Array.isArray(reminders) || !Number.isSafeInteger(index) || index < 0 || index >= reminders.length) throw new Error('A selected invoice is required.');\n context.signal?.throwIfAborted();\n const reminder = reminders[index];\n const result = await context.workflows.call('send-and-log-invoice-reminder', { reminder, spreadsheetId, range });\n if (result?.receipt?.InvoiceID !== reminder.InvoiceID || result.receipt.tier !== reminder.tier) throw new Error('The reminder receipt does not match this invoice.');\n return { receipt: result.receipt };\n}\nexport default async function run(input, context) { return sendOneInvoiceReminder(input, context); }\n" } } }, "presentation": { "designer": { "version": 1, "flow": { "direction": "vertical" } } } } }, { "flowId": "send-and-log-one-invoice-reminder", "file": { "kind": "open-flow-workflow", "version": 1, "name": "Send and Log One Invoice Reminder", "content": { "modelVersion": 14, "document": { "bindings": {}, "graph": { "nodes": { "n_77e195713a21433e": { "name": "One validated invoice reminder", "kind": "start", "inputDefs": [ { "jsonSchema": { "type": "object", "properties": { "InvoiceID": { "type": "string", "minLength": 1 }, "tier": { "type": "string" }, "date": { "type": "string" }, "daysOverdue": { "type": "integer", "enum": [ 7, 14, 30 ] }, "chatId": { "type": "string" }, "message": { "type": "string" } }, "required": [ "InvoiceID", "tier", "date", "daysOverdue", "chatId", "message" ], "additionalProperties": false }, "nullable": false, "handle": "reminder" }, { "jsonSchema": { "type": "string", "minLength": 1 }, "nullable": false, "handle": "spreadsheetId" }, { "jsonSchema": { "type": "string", "minLength": 1 }, "nullable": false, "handle": "range" } ] }, "n_e00d9514d51c359f": { "inputs": { "reminder": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_77e195713a21433e", "output": "reminder" } ] }, "endpoint": { "kind": "value", "value": "https://your-waha-server.example/api/sendText" }, "session": { "kind": "value", "value": "default" } }, "name": "Configure WAHA delivery", "kind": "task", "task": { "inputs": [ { "jsonSchema": { "type": "object", "properties": { "InvoiceID": { "type": "string", "minLength": 1 }, "tier": { "type": "string" }, "date": { "type": "string" }, "daysOverdue": { "type": "integer", "enum": [ 7, 14, 30 ] }, "chatId": { "type": "string" }, "message": { "type": "string" } }, "required": [ "InvoiceID", "tier", "date", "daysOverdue", "chatId", "message" ], "additionalProperties": false }, "nullable": false, "handle": "reminder" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "endpoint" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "session" } ], "outputs": [ { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "endpoint" }, { "jsonSchema": { "type": "object" }, "nullable": false, "handle": "payload" } ], "name": "Configure WAHA delivery", "moduleId": "n_212c178f2a4af820", "capabilities": [ { "kind": "connector", "mode": "independent", "actions": [] } ] } }, "n_10705ef2c34afc66": { "inputs": { "url": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_e00d9514d51c359f", "output": "endpoint" } ] }, "body": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_e00d9514d51c359f", "output": "payload" } ] }, "auth.token": { "kind": "unset" } }, "name": "Send Hebrew WhatsApp reminder", "kind": "task", "task": { "inputs": [ { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "url" }, { "jsonSchema": {}, "nullable": true, "handle": "body" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "auth.token" } ], "outputs": [ { "jsonSchema": {}, "nullable": true, "handle": "body" }, { "jsonSchema": { "type": "integer" }, "nullable": false, "handle": "statusCode" }, { "jsonSchema": { "type": "object", "additionalProperties": { "type": "string" } }, "nullable": false, "handle": "headers" } ], "name": "Send Hebrew WhatsApp reminder", "executor": { "method": "POST", "query": { "mode": "fields", "names": [] }, "headers": { "mode": "fields", "names": [] }, "bodyFormat": "json", "authentication": { "type": "apiKey", "name": "X-Api-Key", "in": "header" }, "responseFormat": "json", "statusPolicy": "success", "kind": "http-request" } } }, "n_913b51067ec25168": { "inputs": { "body": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_10705ef2c34afc66", "output": "body" } ] }, "statusCode": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_10705ef2c34afc66", "output": "statusCode" } ] } }, "name": "Verify WAHA acceptance", "kind": "task", "task": { "inputs": [ { "jsonSchema": {}, "nullable": true, "handle": "body" }, { "jsonSchema": { "type": "integer" }, "nullable": false, "handle": "statusCode" } ], "outputs": [ { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "messageId" } ], "name": "Verify WAHA acceptance", "moduleId": "n_2ee36b7f598e1f3d", "capabilities": [ { "kind": "connector", "mode": "independent", "actions": [] } ] } }, "n_108369014bcdf5c3": { "inputs": { "spreadsheetId": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_77e195713a21433e", "output": "spreadsheetId" } ] }, "range": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_77e195713a21433e", "output": "range" } ] }, "majorDimension": { "kind": "value", "value": "ROWS" }, "valueRenderOption": { "kind": "value", "value": "UNFORMATTED_VALUE" }, "dateTimeRenderOption": { "kind": "value", "value": "FORMATTED_STRING" } }, "name": "Find invoice in current sheet", "kind": "task", "task": { "inputs": [ { "description": "The spreadsheet ID.", "jsonSchema": { "type": "string", "minLength": 1, "description": "The spreadsheet ID." }, "nullable": true, "handle": "spreadsheetId", "value": null }, { "description": "The A1 notation of the range to retrieve.", "jsonSchema": { "type": "string", "minLength": 1, "description": "The A1 notation of the range to retrieve." }, "nullable": false, "handle": "range" }, { "description": "The major dimension for the returned values.", "jsonSchema": { "type": "string", "enum": [ "DIMENSION_UNSPECIFIED", "ROWS", "COLUMNS" ], "description": "The major dimension for the returned values." }, "nullable": true, "handle": "majorDimension", "value": null }, { "description": "How values should be rendered in the output.", "jsonSchema": { "type": "string", "enum": [ "FORMATTED_VALUE", "UNFORMATTED_VALUE", "FORMULA" ], "description": "How values should be rendered in the output." }, "nullable": true, "handle": "valueRenderOption", "value": null }, { "description": "How dates and times should be rendered in the output.", "jsonSchema": { "type": "string", "enum": [ "SERIAL_NUMBER", "FORMATTED_STRING" ], "description": "How dates and times should be rendered in the output." }, "nullable": true, "handle": "dateTimeRenderOption", "value": null } ], "outputs": [ { "description": "The A1 notation of the range.", "jsonSchema": { "type": "string", "description": "The A1 notation of the range." }, "nullable": false, "handle": "range" }, { "description": "The major dimension of the values (ROWS or COLUMNS).", "jsonSchema": { "type": "string", "description": "The major dimension of the values (ROWS or COLUMNS)." }, "nullable": false, "handle": "majorDimension" }, { "description": "The data values in the range.", "jsonSchema": { "type": "array", "items": { "type": "array", "items": { "anyOf": [ { "type": "string", "description": "A union variant." }, { "type": "number", "description": "A union variant." }, { "type": "boolean", "description": "A union variant." }, { "const": null, "description": "A union variant." } ], "description": "The cell value schema." }, "description": "An array item." }, "description": "The data values in the range." }, "nullable": false, "handle": "values" } ], "name": "Find invoice in current sheet", "executor": { "kind": "connector", "action": "googlesheets.values_get" } } }, "n_9b9b8645f496e4b0": { "inputs": { "values": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_108369014bcdf5c3", "output": "values" } ] }, "majorDimension": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_108369014bcdf5c3", "output": "majorDimension" } ] }, "range": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_77e195713a21433e", "output": "range" } ] }, "reminder": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_77e195713a21433e", "output": "reminder" } ] } }, "name": "Prepare two reminder log cells", "kind": "task", "task": { "inputs": [ { "jsonSchema": { "type": "array", "items": { "type": "array", "items": { "anyOf": [ { "type": "string", "description": "A union variant." }, { "type": "number", "description": "A union variant." }, { "type": "boolean", "description": "A union variant." }, { "const": null, "description": "A union variant." } ], "description": "The cell value schema." }, "description": "An array item." }, "description": "The data values in the range." }, "nullable": false, "handle": "values" }, { "jsonSchema": { "type": "string", "description": "The major dimension of the values (ROWS or COLUMNS)." }, "nullable": false, "handle": "majorDimension" }, { "jsonSchema": { "type": "string", "minLength": 1 }, "nullable": false, "handle": "range" }, { "jsonSchema": { "type": "object", "properties": { "InvoiceID": { "type": "string", "minLength": 1 }, "tier": { "type": "string" }, "date": { "type": "string" }, "daysOverdue": { "type": "integer", "enum": [ 7, 14, 30 ] }, "chatId": { "type": "string" }, "message": { "type": "string" } }, "required": [ "InvoiceID", "tier", "date", "daysOverdue", "chatId", "message" ], "additionalProperties": false }, "nullable": false, "handle": "reminder" } ], "outputs": [ { "jsonSchema": { "type": "array", "items": { "type": "object", "properties": { "range": { "type": "string", "minLength": 1, "description": "The A1 notation of the range to write to." }, "values": { "type": "array", "items": { "type": "array", "items": { "anyOf": [ { "type": "string", "description": "A union variant." }, { "type": "number", "description": "A union variant." }, { "type": "boolean", "description": "A union variant." }, { "const": null, "description": "A union variant." } ], "description": "The cell value schema." }, "description": "An array item." }, "description": "The data to write into the range." }, "majorDimension": { "type": "string", "enum": [ "ROWS", "COLUMNS" ], "description": "The major dimension of the values array." } }, "required": [ "range", "values" ], "additionalProperties": false, "description": "A batch values update item." }, "minItems": 1, "description": "The list of value ranges to update." }, "nullable": false, "handle": "data" } ], "name": "Prepare two reminder log cells", "moduleId": "n_a3555dca87ff14c6", "capabilities": [ { "kind": "connector", "mode": "independent", "actions": [] } ] } }, "n_f43cbf7f92525f8d": { "inputs": { "spreadsheetId": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_77e195713a21433e", "output": "spreadsheetId" } ] }, "data": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_9b9b8645f496e4b0", "output": "data" } ] }, "valueInputOption": { "kind": "value", "value": "RAW" }, "includeValuesInResponse": { "kind": "value", "value": true }, "responseValueRenderOption": { "kind": "value", "value": "UNFORMATTED_VALUE" }, "responseDateTimeRenderOption": { "kind": "value", "value": "FORMATTED_STRING" } }, "name": "Write reminder tier and date", "kind": "task", "task": { "inputs": [ { "description": "The spreadsheet ID.", "jsonSchema": { "type": "string", "minLength": 1, "description": "The spreadsheet ID." }, "nullable": true, "handle": "spreadsheetId", "value": null }, { "description": "The list of value ranges to update.", "jsonSchema": { "type": "array", "items": { "type": "object", "properties": { "range": { "type": "string", "minLength": 1, "description": "The A1 notation of the range to write to." }, "values": { "type": "array", "items": { "type": "array", "items": { "anyOf": [ { "type": "string", "description": "A union variant." }, { "type": "number", "description": "A union variant." }, { "type": "boolean", "description": "A union variant." }, { "const": null, "description": "A union variant." } ], "description": "The cell value schema." }, "description": "An array item." }, "description": "The data to write into the range." }, "majorDimension": { "type": "string", "enum": [ "ROWS", "COLUMNS" ], "description": "The major dimension of the values array." } }, "required": [ "range", "values" ], "additionalProperties": false, "description": "A batch values update item." }, "minItems": 1, "description": "The list of value ranges to update." }, "nullable": false, "handle": "data" }, { "description": "How the input data should be interpreted.", "jsonSchema": { "type": "string", "enum": [ "INPUT_VALUE_OPTION_UNSPECIFIED", "RAW", "USER_ENTERED" ], "description": "How the input data should be interpreted." }, "nullable": true, "handle": "valueInputOption", "value": null }, { "description": "Whether to include the written values in the response.", "jsonSchema": { "type": "boolean", "description": "Whether to include the written values in the response." }, "nullable": true, "handle": "includeValuesInResponse", "value": null }, { "description": "How values in the response should be rendered.", "jsonSchema": { "type": "string", "enum": [ "FORMATTED_VALUE", "UNFORMATTED_VALUE", "FORMULA" ], "description": "How values in the response should be rendered." }, "nullable": true, "handle": "responseValueRenderOption", "value": null }, { "description": "How dates in the response should be rendered.", "jsonSchema": { "type": "string", "enum": [ "SERIAL_NUMBER", "FORMATTED_STRING" ], "description": "How dates in the response should be rendered." }, "nullable": true, "handle": "responseDateTimeRenderOption", "value": null } ], "outputs": [ { "description": "The unique ID of the spreadsheet.", "jsonSchema": { "type": "string", "description": "The unique ID of the spreadsheet." }, "nullable": false, "handle": "spreadsheetId" }, { "description": "The total number of rows updated across all ranges.", "jsonSchema": { "type": "integer", "description": "The total number of rows updated across all ranges." }, "nullable": false, "handle": "totalUpdatedRows" }, { "description": "The total number of columns updated across all ranges.", "jsonSchema": { "type": "integer", "description": "The total number of columns updated across all ranges." }, "nullable": false, "handle": "totalUpdatedColumns" }, { "description": "The total number of cells updated across all ranges.", "jsonSchema": { "type": "integer", "description": "The total number of cells updated across all ranges." }, "nullable": false, "handle": "totalUpdatedCells" }, { "description": "The per-range update responses.", "jsonSchema": { "type": "array", "items": { "type": "object", "properties": { "updatedRange": { "type": "string", "description": "The A1 notation of the range that was updated." }, "updatedRows": { "type": "integer", "description": "The number of rows that were updated." }, "updatedColumns": { "type": "integer", "description": "The number of columns that were updated." }, "updatedCells": { "type": "integer", "description": "The number of cells that were updated." }, "updatedData": { "type": "object", "properties": { "range": { "type": "string", "description": "The A1 notation of the range." }, "majorDimension": { "type": "string", "description": "The major dimension of the values (ROWS or COLUMNS)." }, "values": { "type": "array", "items": { "type": "array", "items": { "anyOf": [ { "type": "string", "description": "A union variant." }, { "type": "number", "description": "A union variant." }, { "type": "boolean", "description": "A union variant." }, { "const": null, "description": "A union variant." } ], "description": "The cell value schema." }, "description": "An array item." }, "description": "The data values in the range." } }, "required": [ "range", "majorDimension", "values" ], "additionalProperties": false, "description": "The values of the cells after the update." } }, "required": [ "updatedRange", "updatedRows", "updatedColumns", "updatedCells" ], "additionalProperties": false, "description": "The batch update item schema." }, "description": "The per-range update responses." }, "nullable": false, "handle": "responses" } ], "name": "Write reminder tier and date", "executor": { "kind": "connector", "action": "googlesheets.update_values_batch" } } }, "n_9279321ebf11ab00": { "inputs": { "spreadsheetId": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_f43cbf7f92525f8d", "output": "spreadsheetId" } ] }, "totalUpdatedCells": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_f43cbf7f92525f8d", "output": "totalUpdatedCells" } ] }, "responses": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_f43cbf7f92525f8d", "output": "responses" } ] }, "expectedSpreadsheetId": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_77e195713a21433e", "output": "spreadsheetId" } ] }, "data": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_9b9b8645f496e4b0", "output": "data" } ] }, "reminder": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_77e195713a21433e", "output": "reminder" } ] }, "messageId": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_913b51067ec25168", "output": "messageId" } ] } }, "name": "Confirm both log cells", "kind": "task", "task": { "inputs": [ { "jsonSchema": { "type": "string", "description": "The unique ID of the spreadsheet." }, "nullable": false, "handle": "spreadsheetId" }, { "jsonSchema": { "type": "integer", "description": "The total number of cells updated across all ranges." }, "nullable": false, "handle": "totalUpdatedCells" }, { "jsonSchema": { "type": "array", "items": { "type": "object", "properties": { "updatedRange": { "type": "string", "description": "The A1 notation of the range that was updated." }, "updatedRows": { "type": "integer", "description": "The number of rows that were updated." }, "updatedColumns": { "type": "integer", "description": "The number of columns that were updated." }, "updatedCells": { "type": "integer", "description": "The number of cells that were updated." }, "updatedData": { "type": "object", "properties": { "range": { "type": "string", "description": "The A1 notation of the range." }, "majorDimension": { "type": "string", "description": "The major dimension of the values (ROWS or COLUMNS)." }, "values": { "type": "array", "items": { "type": "array", "items": { "anyOf": [ { "type": "string", "description": "A union variant." }, { "type": "number", "description": "A union variant." }, { "type": "boolean", "description": "A union variant." }, { "const": null, "description": "A union variant." } ], "description": "The cell value schema." }, "description": "An array item." }, "description": "The data values in the range." } }, "required": [ "range", "majorDimension", "values" ], "additionalProperties": false, "description": "The values of the cells after the update." } }, "required": [ "updatedRange", "updatedRows", "updatedColumns", "updatedCells" ], "additionalProperties": false, "description": "The batch update item schema." }, "description": "The per-range update responses." }, "nullable": false, "handle": "responses" }, { "jsonSchema": { "type": "string", "minLength": 1 }, "nullable": false, "handle": "expectedSpreadsheetId" }, { "jsonSchema": { "type": "array", "items": { "type": "object", "properties": { "range": { "type": "string", "minLength": 1, "description": "The A1 notation of the range to write to." }, "values": { "type": "array", "items": { "type": "array", "items": { "anyOf": [ { "type": "string", "description": "A union variant." }, { "type": "number", "description": "A union variant." }, { "type": "boolean", "description": "A union variant." }, { "const": null, "description": "A union variant." } ], "description": "The cell value schema." }, "description": "An array item." }, "description": "The data to write into the range." }, "majorDimension": { "type": "string", "enum": [ "ROWS", "COLUMNS" ], "description": "The major dimension of the values array." } }, "required": [ "range", "values" ], "additionalProperties": false, "description": "A batch values update item." }, "minItems": 1, "description": "The list of value ranges to update." }, "nullable": false, "handle": "data" }, { "jsonSchema": { "type": "object", "properties": { "InvoiceID": { "type": "string", "minLength": 1 }, "tier": { "type": "string" }, "date": { "type": "string" }, "daysOverdue": { "type": "integer", "enum": [ 7, 14, 30 ] }, "chatId": { "type": "string" }, "message": { "type": "string" } }, "required": [ "InvoiceID", "tier", "date", "daysOverdue", "chatId", "message" ], "additionalProperties": false }, "nullable": false, "handle": "reminder" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "messageId" } ], "outputs": [ { "jsonSchema": { "type": "object" }, "nullable": false, "handle": "receipt" } ], "name": "Confirm both log cells", "moduleId": "n_c35b641f90869ddf", "capabilities": [ { "kind": "connector", "mode": "independent", "actions": [] } ] } }, "n_2d102125bd66e35a": { "name": "Logged reminder receipt", "kind": "outputs", "outputDefs": [ { "jsonSchema": { "type": "object" }, "nullable": false, "handle": "receipt" } ], "outputMappings": { "receipt": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_9279321ebf11ab00", "output": "receipt" } ] } } } }, "edges": [ { "source": "n_77e195713a21433e", "target": "n_e00d9514d51c359f" }, { "source": "n_e00d9514d51c359f", "target": "n_10705ef2c34afc66" }, { "source": "n_10705ef2c34afc66", "target": "n_913b51067ec25168" }, { "source": "n_913b51067ec25168", "target": "n_108369014bcdf5c3" }, { "source": "n_108369014bcdf5c3", "target": "n_9b9b8645f496e4b0" }, { "source": "n_9b9b8645f496e4b0", "target": "n_f43cbf7f92525f8d" }, { "source": "n_f43cbf7f92525f8d", "target": "n_9279321ebf11ab00" }, { "source": "n_9279321ebf11ab00", "target": "n_2d102125bd66e35a" } ] } }, "modules": { "n_212c178f2a4af820": { "name": "Configure WAHA delivery", "imports": [], "source": "export default function prepareWhatsAppInvoiceDelivery({ reminder, endpoint, session }) {\n // The native secure Code VM has no URL global. The host/path are deployment configuration.\n if (typeof endpoint !== 'string' || !/^https:\\/\\/[a-zA-Z0-9.-]+(?::\\d+)?\\/api\\/sendText$/.test(endpoint) || /(?:^|[/.])(?:localhost|example)(?:[/:.]|$)/i.test(endpoint) || endpoint.includes('YOUR_')) throw new Error('Configure your HTTPS WAHA /api/sendText endpoint.');\n if (typeof session !== 'string' || !session.trim()) throw new Error('Configure your WAHA session.');\n if (!reminder || !/^972[2-9]\\d{7,8}@c\\.us$/.test(reminder.chatId) || typeof reminder.message !== 'string' || !reminder.message) throw new Error('Provide a validated invoice reminder.');\n return { endpoint, payload: { session: session.trim(), chatId: reminder.chatId, text: reminder.message } };\n}\n" }, "n_2ee36b7f598e1f3d": { "name": "Verify WAHA acceptance", "imports": [], "source": "export default function verifyWhatsAppInvoiceAcceptance({ body, statusCode }) {\n if (statusCode !== 201 || !body || typeof body !== 'object' || Array.isArray(body) || typeof body.id !== 'string' || !body.id.trim() || body.fromMe !== true || body.ack === -1) throw new Error('WAHA did not confirm acceptance of the reminder.');\n return { messageId: body.id };\n}\n" }, "n_a3555dca87ff14c6": { "name": "Prepare two reminder log cells", "imports": [], "source": "function sheetTable({ values, majorDimension = \"ROWS\" }) {\n if (majorDimension !== \"ROWS\" || !Array.isArray(values) || values.some((row) => !Array.isArray(row)))\n throw Error(\"Google Sheets must return row-oriented cell values.\");\n const isEmpty = (value) => value == null || value === \"\", nonempty = (row) => row.some((value) => !isEmpty(value)), headerIndex = values.findIndex(nonempty);\n if (headerIndex < 0)\n return { columns: [], records: [] };\n const headers = values[headerIndex];\n if (headers.some((name) => typeof name !== \"string\" || !name.trim() || name === \"row_number\") || new Set(headers).size !== headers.length)\n throw Error(\"Use unique nonempty text headers; row_number is reserved.\");\n const columns = [\"row_number\", ...headers], records = [];\n for (let index = headerIndex + 1;index < values.length; index++) {\n const cells = values[index];\n if (!nonempty(cells))\n continue;\n if (cells.slice(headers.length).some((value) => !isEmpty(value)))\n throw Error(\"Every populated column needs a header.\");\n records.push(Object.fromEntries([[\"row_number\", index + 1], ...headers.map((name, column) => [name, cells[column] ?? \"\"])]));\n }\n return { columns, records };\n}\nfunction invoiceIdentity(value) {\n if (typeof value !== \"string\" && !(typeof value === \"number\" && Number.isFinite(value)))\n throw Error(\"Use nonempty text or numeric InvoiceID values.\");\n const id = String(value).trim();\n if (!id)\n throw Error(\"Use a nonempty InvoiceID.\");\n return id;\n}\n\nexport default function buildInvoiceReminderLog({ values, majorDimension, range, reminder }) {\n const { columns, records } = sheetTable({ values, majorDimension });\n const required = ['InvoiceID', 'LastReminder', 'LastReminderDate'];\n if (required.some(name => !columns.includes(name))) throw new Error('The invoice log columns are missing.');\n const matches = records.filter(row => { try { return invoiceIdentity(row.InvoiceID) === reminder.InvoiceID; } catch { return false; } });\n if (matches.length !== 1) throw new Error('The sent invoice no longer identifies exactly one row.');\n const columnName = zeroBased => {\n let index = zeroBased + 1, name = '';\n while (index > 0) { index--; name = String.fromCharCode(65 + index % 26) + name; index = Math.floor(index / 26); }\n return name;\n };\n const data = ['LastReminder', 'LastReminderDate'].map(name => ({ range: range + '!' + columnName(columns.indexOf(name) - 1) + matches[0].row_number, majorDimension: 'ROWS', values: [[name === 'LastReminder' ? reminder.tier : reminder.date]] }));\n return { data };\n}\n" }, "n_c35b641f90869ddf": { "name": "Confirm both log cells", "imports": [], "source": "export default function verifyInvoiceReminderLog({ spreadsheetId, expectedSpreadsheetId, totalUpdatedCells, responses, data, reminder, messageId }) {\n const cell = value => {\n if (typeof value !== 'string') return null;\n const match = /^('(?:[^']|'')+'|[^!]+)!\\$?([A-Z]+)\\$?(\\d+)(?::\\$?([A-Z]+)\\$?(\\d+))?$/.exec(value);\n if (!match || (match[4] && (match[4] !== match[2] || match[5] !== match[3]))) return null;\n const sheet = match[1].startsWith(\"'\") ? match[1].slice(1, -1).replaceAll(\"''\", \"'\") : match[1];\n return JSON.stringify([sheet, match[2], Number(match[3])]);\n };\n const sameCell = (first, second) => cell(first) !== null && cell(first) === cell(second);\n if (spreadsheetId !== expectedSpreadsheetId || totalUpdatedCells !== 2 || !Array.isArray(responses) || responses.length !== 2 || responses.some((response, index) => response.updatedCells !== 1 || !sameCell(response.updatedRange, data[index].range) || !sameCell(response.updatedData?.range, data[index].range) || response.updatedData?.values?.length !== 1 || response.updatedData.values[0]?.length !== 1 || response.updatedData.values[0][0] !== data[index].values[0][0])) throw new Error('Google Sheets did not confirm both reminder log cells.');\n return { receipt: { InvoiceID: reminder.InvoiceID, tier: reminder.tier, date: reminder.date, messageId } };\n}\n" } } }, "presentation": { "designer": { "version": 1, "flow": { "direction": "vertical" } } } } } ] }