{ "kind": "open-flow-workflow", "version": 1, "name": "Invoice Payment Reminders", "content": { "modelVersion": 14, "document": { "bindings": {}, "graph": { "nodes": { "n_0eb4eaa990c6dcbc": { "name": "Weekday morning", "kind": "cron", "cronTimes": [ { "type": "cron", "expression": "0 8 * * 1-5", "timezone": "UTC" } ] }, "n_5be5585b09c8353a": { "inputs": { "spreadsheetId": { "kind": "value", "value": "YOUR_SPREADSHEET_ID" }, "range": { "kind": "value", "value": "'Invoices'!A:F" }, "valueRenderOption": { "kind": "value", "value": "FORMATTED_VALUE" }, "majorDimension": { "kind": "value", "value": "ROWS" } }, "name": "Read invoices", "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 invoices", "executor": { "kind": "connector", "action": "googlesheets.values_get" } } }, "n_e18f577e9f8888a1": { "inputs": { "values": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_5be5585b09c8353a", "output": "values" } ] }, "timezone": { "kind": "value", "value": "UTC" }, "signature": { "kind": "value", "value": "[LA TUA AZIENDA]" }, "now": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_0eb4eaa990c6dcbc", "output": "scheduledAt" } ] } }, "name": "Prepare reminders", "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" }, "nullable": false, "handle": "timezone" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "signature" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "now" } ], "outputs": [ { "jsonSchema": { "type": "array", "items": { "type": "object" } }, "nullable": false, "handle": "reminders" }, { "jsonSchema": { "type": "integer" }, "nullable": false, "handle": "count" } ], "name": "Prepare reminders", "moduleId": "n_abe2bc937513c0ae", "capabilities": [ { "kind": "connector", "mode": "independent", "actions": [] } ] } }, "n_afd7f6d5cad0e1fe": { "inputs": { "reminders": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_e18f577e9f8888a1", "output": "reminders" } ] }, "previousState": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_afd7f6d5cad0e1fe", "output": "state" } ] }, "lastMessageId": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_d4d822900bba1321", "output": "messageId" } ] } }, "name": "Prepare next reminder", "kind": "task", "task": { "inputs": [ { "jsonSchema": { "type": "array", "items": { "type": "object" } }, "nullable": false, "handle": "reminders" }, { "jsonSchema": { "type": "object" }, "nullable": true, "handle": "previousState" }, { "jsonSchema": { "type": "string", "description": "Gmail message ID." }, "nullable": true, "handle": "lastMessageId" } ], "outputs": [ { "jsonSchema": { "type": "object" }, "nullable": false, "handle": "state" }, { "jsonSchema": { "type": "array", "items": { "type": "object" } }, "nullable": false, "handle": "sent" }, { "jsonSchema": { "type": "integer" }, "nullable": false, "handle": "count" }, { "jsonSchema": { "type": "boolean" }, "nullable": false, "handle": "more" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "to" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "subject" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "body" } ], "name": "Prepare next reminder", "moduleId": "n_dbe2dd78f837b72f", "capabilities": [ { "kind": "connector", "mode": "independent", "actions": [] } ] } }, "n_bf91a11f9d805c3a": { "inputs": {}, "name": "More reminders", "kind": "condition", "matchMode": "first", "cases": [ { "output": "more", "groups": [ { "expressions": [ { "left": { "kind": "source", "source": { "kind": "node", "nodeId": "n_afd7f6d5cad0e1fe", "output": "more" } }, "operator": "==", "right": { "kind": "value", "value": true } } ] } ] } ] }, "n_d4d822900bba1321": { "inputs": { "to": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_afd7f6d5cad0e1fe", "output": "to" } ] }, "subject": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_afd7f6d5cad0e1fe", "output": "subject" } ] }, "body": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_afd7f6d5cad0e1fe", "output": "body" } ] }, "isHtml": { "kind": "value", "value": false } }, "name": "Send reminder", "kind": "task", "task": { "inputs": [ { "description": "Primary recipient email address.", "jsonSchema": { "type": "string", "description": "Primary recipient email address." }, "nullable": true, "handle": "to", "value": null }, { "description": "Additional To recipients.", "jsonSchema": { "type": "array", "items": { "type": "string", "description": "An array item." }, "description": "Additional To recipients." }, "nullable": true, "handle": "extraRecipients", "value": null }, { "description": "Cc recipients.", "jsonSchema": { "anyOf": [ { "type": "string", "description": "A union variant." }, { "type": "array", "items": { "type": "string", "description": "An array item." }, "description": "A union variant." } ], "description": "Cc recipients." }, "nullable": true, "handle": "cc", "value": null }, { "description": "Bcc recipients.", "jsonSchema": { "anyOf": [ { "type": "string", "description": "A union variant." }, { "type": "array", "items": { "type": "string", "description": "An array item." }, "description": "A union variant." } ], "description": "Bcc recipients." }, "nullable": true, "handle": "bcc", "value": null }, { "description": "Email subject line.", "jsonSchema": { "type": "string", "description": "Email subject line." }, "nullable": true, "handle": "subject", "value": null }, { "description": "Email body content.", "jsonSchema": { "type": "string", "description": "Email body content." }, "nullable": true, "handle": "body", "value": null }, { "description": "Set to true when the body content is already HTML.", "jsonSchema": { "type": "boolean", "description": "Set to true when the body content is already HTML." }, "nullable": true, "handle": "isHtml", "value": null }, { "description": "Attachments and CID inline images. Up to 25,000,000 decoded bytes in total and 35,000,000 MIME bytes.", "jsonSchema": { "type": "array", "items": { "type": "object", "properties": { "filename": { "type": "string", "description": "Attachment filename; omit for an unnamed attachment." }, "mimeType": { "type": "string", "description": "MIME type without parameters; defaults to application/octet-stream." }, "contentBase64": { "type": "string", "description": "Standard padded Base64 content. Empty string represents a zero-byte file. Supply this or fileUrl." }, "fileUrl": { "type": "string", "format": "uri", "description": "Public URL to download the attachment. Supply this or contentBase64." }, "contentId": { "type": "string", "minLength": 1, "description": "Bare Content-ID without cid: or angle brackets; reference it as cid: in HTML." }, "disposition": { "type": "string", "enum": [ "inline", "attachment" ], "description": "Defaults to inline when contentId is set, otherwise attachment. Inline requires contentId." } }, "additionalProperties": false, "description": "An attachment or CID inline resource.", "oneOf": [ { "required": [ "contentBase64" ] }, { "required": [ "fileUrl" ] } ] }, "maxItems": 100, "description": "Attachments and CID inline images. Up to 25,000,000 decoded bytes in total and 35,000,000 MIME bytes." }, "nullable": true, "handle": "attachments", "value": null }, { "description": "Verified Gmail send-as alias to use in the From header.", "jsonSchema": { "type": "string", "description": "Verified Gmail send-as alias to use in the From header." }, "nullable": true, "handle": "fromEmail", "value": null } ], "outputs": [ { "description": "Gmail message ID.", "jsonSchema": { "type": "string", "description": "Gmail message ID." }, "nullable": false, "handle": "messageId" }, { "description": "Gmail thread ID.", "jsonSchema": { "type": "string", "description": "Gmail thread ID." }, "nullable": true, "handle": "threadId" } ], "name": "Send reminder", "executor": { "kind": "connector", "action": "gmail.send_email" } } }, "n_e5ef11bc69d7e2e6": { "name": "Reminder results", "kind": "outputs", "outputDefs": [ { "jsonSchema": { "type": "array", "items": { "type": "object" } }, "nullable": false, "handle": "sent" }, { "jsonSchema": { "type": "integer" }, "nullable": false, "handle": "count" } ], "outputMappings": { "sent": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_afd7f6d5cad0e1fe", "output": "sent" } ] }, "count": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_afd7f6d5cad0e1fe", "output": "count" } ] } } } }, "edges": [ { "source": "n_0eb4eaa990c6dcbc", "target": "n_5be5585b09c8353a" }, { "source": "n_5be5585b09c8353a", "target": "n_e18f577e9f8888a1" }, { "source": "n_e18f577e9f8888a1", "target": "n_afd7f6d5cad0e1fe" }, { "source": "n_afd7f6d5cad0e1fe", "target": "n_bf91a11f9d805c3a" }, { "source": "n_bf91a11f9d805c3a", "target": "n_d4d822900bba1321", "sourceHandle": "more" }, { "source": "n_d4d822900bba1321", "target": "n_afd7f6d5cad0e1fe" }, { "source": "n_bf91a11f9d805c3a", "target": "n_e5ef11bc69d7e2e6", "sourceHandle": "otherwise" } ] } }, "modules": { "n_abe2bc937513c0ae": { "name": "Prepare reminders", "imports": [], "source": "export default function invoiceReminders({ values, timezone, signature, now }) {\n if (!Array.isArray(values)) throw new Error('Invoice values must be an array.');\n if (!values.length) return { reminders: [], count: 0 };\n const headers = values[0];\n const required = ['Numero', 'Cliente', 'Email', 'Importo', 'Scadenza', 'Stato'];\n if (!Array.isArray(headers) || required.some((header) => !headers.includes(header))) throw new Error('Invoice sheet headers must include ' + required.join(', ') + '.');\n for (const header of required) if (headers.filter((name) => name === header).length !== 1) throw new Error('Duplicate invoice header: ' + header);\n const dateParts = new Intl.DateTimeFormat('en-GB', { timeZone: timezone, year: 'numeric', month: '2-digit', day: '2-digit' }).formatToParts(now ? new Date(now) : new Date());\n const part = (name) => Number(dateParts.find((value) => value.type === name).value);\n const today = Date.UTC(part('year'), part('month') - 1, part('day'));\n const reminders = [];\n for (const [sourceIndex, row] of values.slice(1).entries()) {\n if (!Array.isArray(row)) throw new Error('Invoice row must be an array.');\n const invoice = Object.fromEntries(headers.map((header, index) => [header, row[index] ?? '']));\n if (String(invoice.Stato).trim().toLowerCase() === 'pagata') continue;\n const match = /^(\\d{1,2})\\/(\\d{1,2})\\/(\\d{4})$/.exec(String(invoice.Scadenza).trim());\n if (!match) continue;\n const [, day, month, year] = match.map(Number);\n const due = new Date(Date.UTC(year, month - 1, day));\n if (due.getUTCFullYear() !== year || due.getUTCMonth() !== month - 1 || due.getUTCDate() !== day) continue;\n const daysRemaining = Math.round((due.getTime() - today) / 86400000);\n if (daysRemaining > 3) continue;\n const to = String(invoice.Email).trim();\n if (!/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(to)) throw new Error('Invalid email for invoice ' + invoice.Numero + '.');\n const overdue = daysRemaining < 0;\n const dueText = `${String(day).padStart(2, '0')}/${String(month).padStart(2, '0')}/${year}`;\n reminders.push({\n to, daysRemaining, sourceIndex,\n subject: `Promemoria: fattura ${invoice.Numero} ${overdue ? 'scaduta' : 'in scadenza'}`,\n body: `Gentile ${invoice.Cliente},\\n\\nle ricordiamo che la fattura n. ${invoice.Numero} di € ${invoice.Importo} ${overdue ? 'risulta scaduta il' : 'è in scadenza il'} ${dueText}.\\n\\nSe il pagamento è già stato effettuato, la preghiamo di ignorare questo messaggio.\\n\\nPer qualsiasi chiarimento siamo a disposizione.\\n\\nCordiali saluti,\\n${signature}`,\n });\n }\n return { reminders, count: reminders.length };\n}\n" }, "n_dbe2dd78f837b72f": { "name": "Prepare next reminder", "imports": [], "source": "export default function nextReminder({ reminders, previousState, lastMessageId }) {\n if (!Array.isArray(reminders)) throw new Error('Expected the complete prepared reminder array.');\n let sent = [];\n if (previousState === null) {\n if (lastMessageId !== null) throw new Error('Unexpected Gmail receipt before delivery starts.');\n } else {\n if (!previousState || !Array.isArray(previousState.sent) || previousState.sent.length >= reminders.length) throw new Error('Invalid reminder delivery state.');\n if (typeof lastMessageId !== 'string' || !lastMessageId.trim()) throw new Error('Gmail returned no sent message identifier; inspect Sent before retrying.');\n sent = [...previousState.sent, { to: reminders[previousState.sent.length].to, messageId: lastMessageId }];\n }\n const reminder = reminders[sent.length];\n return { state: { sent }, sent, count: sent.length, more: sent.length < reminders.length, to: reminder?.to ?? '', subject: reminder?.subject ?? '', body: reminder?.body ?? '' };\n}\n" } } }, "presentation": { "designer": { "version": 1, "flow": { "direction": "vertical" } } } }