{ "kind": "open-flow-workflow", "version": 1, "name": "Pennylane Invoice Creator", "content": { "modelVersion": 14, "document": { "bindings": {}, "graph": { "nodes": { "n_b364ebaa3f99cc17": { "name": "Protected invoice request", "kind": "webhook", "bodyFields": [ { "jsonSchema": {}, "nullable": true, "handle": "customer_name" }, { "jsonSchema": {}, "nullable": true, "handle": "customer_email" }, { "jsonSchema": {}, "nullable": true, "handle": "customer_address" }, { "jsonSchema": {}, "nullable": true, "handle": "customer_postal_code" }, { "jsonSchema": {}, "nullable": true, "handle": "customer_city" }, { "jsonSchema": {}, "nullable": true, "handle": "customer_country" }, { "jsonSchema": {}, "nullable": true, "handle": "items" }, { "jsonSchema": {}, "nullable": true, "handle": "due_date" }, { "jsonSchema": {}, "nullable": true, "handle": "currency" }, { "jsonSchema": {}, "nullable": true, "handle": "reference" }, { "jsonSchema": {}, "nullable": true, "handle": "send_email" } ], "method": "POST", "options": { "responseBody": { "kind": "workflow-outputs", "output": "body", "timeoutMs": 60000 }, "responseHeadersSource": { "kind": "workflow-outputs", "output": "headers" }, "responseStatusCodeOutput": "statusCode" } }, "n_ed5c7bae0a40b4b0": { "inputs": { "body": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_b364ebaa3f99cc17", "output": "body" } ] }, "headers": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_b364ebaa3f99cc17", "output": "headers" } ] }, "expectedAuthorization": { "kind": "unset" } }, "name": "Authorize and validate invoice", "kind": "task", "task": { "inputs": [ { "jsonSchema": { "type": "object" }, "nullable": false, "handle": "body" }, { "jsonSchema": { "type": "object" }, "nullable": false, "handle": "headers" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "expectedAuthorization" } ], "outputs": [ { "jsonSchema": { "type": "boolean" }, "nullable": false, "handle": "accepted" }, { "jsonSchema": { "type": "object" }, "nullable": false, "handle": "data" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "filter" }, { "jsonSchema": { "type": "object" }, "nullable": false, "handle": "customer" }, { "jsonSchema": { "type": "object" }, "nullable": false, "handle": "body" }, { "jsonSchema": { "type": "integer" }, "nullable": false, "handle": "statusCode" } ], "name": "Authorize and validate invoice", "moduleId": "n_e6138a6247ac9129", "capabilities": [ { "kind": "connector", "mode": "independent", "actions": [] } ] } }, "n_ca57b516e46c432c": { "inputs": {}, "name": "Invoice request accepted?", "kind": "condition", "matchMode": "first", "cases": [ { "output": "rejected", "groups": [ { "expressions": [ { "left": { "kind": "source", "source": { "kind": "node", "nodeId": "n_ed5c7bae0a40b4b0", "output": "accepted" } }, "operator": "==", "right": { "kind": "value", "value": false } } ] } ] } ] }, "n_d2c6fc64a3060261": { "inputs": { "body": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_ed5c7bae0a40b4b0", "output": "body" } ] }, "statusCode": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_ed5c7bae0a40b4b0", "output": "statusCode" } ] }, "data": { "kind": "value", "value": {} }, "rejected": { "kind": "value", "value": true } }, "name": "Return rejected request", "kind": "task", "task": { "inputs": [ { "jsonSchema": {}, "nullable": true, "handle": "body" }, { "jsonSchema": { "type": "integer" }, "nullable": false, "handle": "statusCode" }, { "jsonSchema": { "type": "object" }, "nullable": false, "handle": "data" }, { "jsonSchema": { "type": "boolean" }, "nullable": false, "handle": "rejected" } ], "outputs": [ { "jsonSchema": { "type": "object" }, "nullable": false, "handle": "body" }, { "jsonSchema": { "type": "integer" }, "nullable": false, "handle": "statusCode" }, { "jsonSchema": { "type": "object", "additionalProperties": { "type": "string" } }, "nullable": false, "handle": "headers" } ], "name": "Return rejected request", "moduleId": "n_407d3fb857f77a7f", "capabilities": [ { "kind": "connector", "mode": "independent", "actions": [] } ] } }, "n_b55f5bd57e413faf": { "inputs": { "url": { "kind": "value", "value": "https://app.pennylane.com/api/external/v2/customers" }, "auth.token": { "kind": "unset" }, "query.filter": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_ed5c7bae0a40b4b0", "output": "filter" } ] } }, "name": "Find customer by email", "timeoutMs": 15000, "kind": "task", "task": { "inputs": [ { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "url" }, { "jsonSchema": { "anyOf": [ { "type": [ "string", "number", "boolean" ] }, { "type": "array", "items": { "type": [ "string", "number", "boolean" ] } } ] }, "nullable": true, "handle": "query.filter" }, { "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": "Find customer by email", "executor": { "method": "GET", "query": { "mode": "fields", "names": [ "filter" ] }, "headers": { "mode": "fields", "names": [] }, "bodyFormat": "none", "authentication": { "type": "bearer" }, "responseFormat": "json", "statusPolicy": "success", "kind": "http-request" } } }, "n_256c32e2ca3f572d": { "inputs": { "body": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_b55f5bd57e413faf", "output": "body" } ] }, "statusCode": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_b55f5bd57e413faf", "output": "statusCode" } ] }, "data": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_ed5c7bae0a40b4b0", "output": "data" } ] } }, "name": "Select first matching customer", "kind": "task", "task": { "inputs": [ { "jsonSchema": {}, "nullable": true, "handle": "body" }, { "jsonSchema": { "type": "integer" }, "nullable": false, "handle": "statusCode" }, { "jsonSchema": { "type": "object" }, "nullable": false, "handle": "data" } ], "outputs": [ { "jsonSchema": { "type": "boolean" }, "nullable": false, "handle": "found" }, { "jsonSchema": { "type": "integer" }, "nullable": false, "handle": "customerId" } ], "name": "Select first matching customer", "moduleId": "n_64e33166ffaad981", "capabilities": [ { "kind": "connector", "mode": "independent", "actions": [] } ] } }, "n_344984781edb3510": { "inputs": {}, "name": "Customer already exists?", "kind": "condition", "matchMode": "first", "cases": [ { "output": "existing_customer", "groups": [ { "expressions": [ { "left": { "kind": "source", "source": { "kind": "node", "nodeId": "n_256c32e2ca3f572d", "output": "found" } }, "operator": "==", "right": { "kind": "value", "value": true } } ] } ] } ] }, "n_d928ff98ba8214d5": { "inputs": { "url": { "kind": "value", "value": "https://app.pennylane.com/api/external/v2/company_customers" }, "auth.token": { "kind": "unset" }, "body": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_ed5c7bae0a40b4b0", "output": "customer" } ] } }, "name": "Create company customer", "timeoutMs": 15000, "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": "Create company customer", "executor": { "method": "POST", "query": { "mode": "fields", "names": [] }, "headers": { "mode": "fields", "names": [] }, "bodyFormat": "json", "authentication": { "type": "bearer" }, "responseFormat": "json", "statusPolicy": "success", "kind": "http-request" } } }, "n_2cac987cf61a005f": { "inputs": { "data": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_ed5c7bae0a40b4b0", "output": "data" } ] }, "mode": { "kind": "value", "value": "created" }, "body": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_d928ff98ba8214d5", "output": "body" } ] }, "statusCode": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_d928ff98ba8214d5", "output": "statusCode" } ] }, "customerId": { "kind": "value", "value": 0 } }, "name": "Build invoice for new customer", "kind": "task", "task": { "inputs": [ { "jsonSchema": { "type": "object" }, "nullable": false, "handle": "data" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "mode" }, { "jsonSchema": {}, "nullable": true, "handle": "body" }, { "jsonSchema": { "type": "integer" }, "nullable": false, "handle": "statusCode" }, { "jsonSchema": { "type": "integer" }, "nullable": false, "handle": "customerId" } ], "outputs": [ { "jsonSchema": { "type": "object" }, "nullable": false, "handle": "payload" } ], "name": "Build invoice for new customer", "moduleId": "n_86b2feb992d093af", "capabilities": [ { "kind": "connector", "mode": "independent", "actions": [] } ] } }, "n_68a556552e499c89": { "inputs": { "data": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_ed5c7bae0a40b4b0", "output": "data" } ] }, "mode": { "kind": "value", "value": "existing" }, "body": { "kind": "value", "value": {} }, "statusCode": { "kind": "value", "value": 200 }, "customerId": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_256c32e2ca3f572d", "output": "customerId" } ] } }, "name": "Build invoice for existing customer", "kind": "task", "task": { "inputs": [ { "jsonSchema": { "type": "object" }, "nullable": false, "handle": "data" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "mode" }, { "jsonSchema": {}, "nullable": true, "handle": "body" }, { "jsonSchema": { "type": "integer" }, "nullable": false, "handle": "statusCode" }, { "jsonSchema": { "type": "integer" }, "nullable": false, "handle": "customerId" } ], "outputs": [ { "jsonSchema": { "type": "object" }, "nullable": false, "handle": "payload" } ], "name": "Build invoice for existing customer", "moduleId": "n_4d331ee91a94db04", "capabilities": [ { "kind": "connector", "mode": "independent", "actions": [] } ] } }, "n_318f00afcef7b523": { "inputs": { "url": { "kind": "value", "value": "https://app.pennylane.com/api/external/v2/customer_invoices" }, "auth.token": { "kind": "unset" }, "body": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_2cac987cf61a005f", "output": "payload" }, { "kind": "node", "nodeId": "n_68a556552e499c89", "output": "payload" } ] } }, "name": "Create finalized invoice", "timeoutMs": 15000, "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": "Create finalized invoice", "executor": { "method": "POST", "query": { "mode": "fields", "names": [] }, "headers": { "mode": "fields", "names": [] }, "bodyFormat": "json", "authentication": { "type": "bearer" }, "responseFormat": "json", "statusPolicy": "success", "kind": "http-request" } } }, "n_8b5915d56854a78e": { "inputs": { "body": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_318f00afcef7b523", "output": "body" } ] }, "statusCode": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_318f00afcef7b523", "output": "statusCode" } ] }, "data": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_ed5c7bae0a40b4b0", "output": "data" } ] }, "rejected": { "kind": "value", "value": false } }, "name": "Confirm created invoice", "kind": "task", "task": { "inputs": [ { "jsonSchema": {}, "nullable": true, "handle": "body" }, { "jsonSchema": { "type": "integer" }, "nullable": false, "handle": "statusCode" }, { "jsonSchema": { "type": "object" }, "nullable": false, "handle": "data" }, { "jsonSchema": { "type": "boolean" }, "nullable": false, "handle": "rejected" } ], "outputs": [ { "jsonSchema": { "type": "object" }, "nullable": false, "handle": "body" }, { "jsonSchema": { "type": "integer" }, "nullable": false, "handle": "statusCode" }, { "jsonSchema": { "type": "object", "additionalProperties": { "type": "string" } }, "nullable": false, "handle": "headers" } ], "name": "Confirm created invoice", "moduleId": "n_d1851c46066670a1", "capabilities": [ { "kind": "connector", "mode": "independent", "actions": [] } ] } }, "n_3a5f4403dea2aa34": { "name": "Invoice response", "kind": "outputs", "outputDefs": [ { "jsonSchema": { "type": "object" }, "nullable": false, "handle": "body" }, { "jsonSchema": { "type": "integer" }, "nullable": false, "handle": "statusCode" }, { "jsonSchema": { "type": "object", "additionalProperties": { "type": "string" } }, "nullable": false, "handle": "headers" } ], "outputMappings": { "body": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_8b5915d56854a78e", "output": "body" }, { "kind": "node", "nodeId": "n_d2c6fc64a3060261", "output": "body" } ] }, "statusCode": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_8b5915d56854a78e", "output": "statusCode" }, { "kind": "node", "nodeId": "n_d2c6fc64a3060261", "output": "statusCode" } ] }, "headers": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_8b5915d56854a78e", "output": "headers" }, { "kind": "node", "nodeId": "n_d2c6fc64a3060261", "output": "headers" } ] } } } }, "edges": [ { "source": "n_b364ebaa3f99cc17", "target": "n_ed5c7bae0a40b4b0" }, { "source": "n_ed5c7bae0a40b4b0", "target": "n_ca57b516e46c432c" }, { "source": "n_ca57b516e46c432c", "target": "n_b55f5bd57e413faf", "sourceHandle": "otherwise" }, { "source": "n_ca57b516e46c432c", "target": "n_d2c6fc64a3060261", "sourceHandle": "rejected" }, { "source": "n_d2c6fc64a3060261", "target": "n_3a5f4403dea2aa34" }, { "source": "n_b55f5bd57e413faf", "target": "n_256c32e2ca3f572d" }, { "source": "n_256c32e2ca3f572d", "target": "n_344984781edb3510" }, { "source": "n_344984781edb3510", "target": "n_d928ff98ba8214d5", "sourceHandle": "otherwise" }, { "source": "n_344984781edb3510", "target": "n_68a556552e499c89", "sourceHandle": "existing_customer" }, { "source": "n_d928ff98ba8214d5", "target": "n_2cac987cf61a005f" }, { "source": "n_2cac987cf61a005f", "target": "n_318f00afcef7b523" }, { "source": "n_68a556552e499c89", "target": "n_318f00afcef7b523" }, { "source": "n_318f00afcef7b523", "target": "n_8b5915d56854a78e" }, { "source": "n_8b5915d56854a78e", "target": "n_3a5f4403dea2aa34" } ] } }, "modules": { "n_e6138a6247ac9129": { "name": "Authorize and validate invoice", "imports": [], "source": "import { Buffer } from 'node:buffer';\n/*\nMIT License\nCopyright (c) 2026 Gauthier Huguenin\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n*/\n\nexport function validatePennylaneInvoice(input, now = Date.now()) {\n const reject = (error, details, statusCode = 400) => ({ accepted: false, data: {}, filter: '', customer: {}, body: { success: false, error, details }, statusCode });\n if (!input || typeof input !== 'object' || Array.isArray(input)) return reject('invalid_payload', ['A JSON object is required.']);\n if (input.send_email !== undefined && input.send_email !== false) return reject('email_not_supported', ['Omit send_email or set it to false. This workflow creates invoices without sending email.'], 422);\n const errors = [];\n if (typeof input.customer_name !== 'string' || !input.customer_name.trim()) errors.push('customer_name is required');\n if (typeof input.customer_email !== 'string' || !/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(input.customer_email)) errors.push('customer_email must be a valid email address');\n if (!Array.isArray(input.items) || !input.items.length) errors.push('items must be a nonempty array');\n else input.items.forEach((item, index) => {\n if (!item || typeof item !== 'object' || Array.isArray(item)) { errors.push('items[' + index + '] must be an object'); return; }\n if (typeof item.label !== 'string' || !item.label.trim()) errors.push('items[' + index + '].label is required');\n if (typeof item.quantity !== 'number' || !Number.isFinite(item.quantity) || item.quantity <= 0) errors.push('items[' + index + '].quantity must be a number > 0');\n if (typeof item.unit_price !== 'number' || !Number.isFinite(item.unit_price) || item.unit_price <= 0 || !/^\\d+\\.\\d{2}$/.test(item.unit_price.toFixed(2))) errors.push('items[' + index + '].unit_price must be a positive finite number representable with two decimal places');\n if (item.vat_rate !== undefined && (typeof item.vat_rate !== 'string' || !item.vat_rate.trim())) errors.push('items[' + index + '].vat_rate must be a nonempty Pennylane VAT code');\n });\n if (input.customer_country !== undefined && (typeof input.customer_country !== 'string' || !/^[A-Z]{2}$/.test(input.customer_country))) errors.push('customer_country must be an uppercase two-letter country code');\n if (input.currency !== undefined && (typeof input.currency !== 'string' || !/^[A-Z]{3}$/.test(input.currency))) errors.push('currency must be an uppercase three-letter currency code');\n if (input.reference !== undefined && input.reference !== null && (typeof input.reference !== 'string' || !input.reference.trim())) errors.push('reference must be nonempty text when supplied');\n if (input.due_date !== undefined && (typeof input.due_date !== 'string' || !/^\\d{4}-\\d{2}-\\d{2}$/.test(input.due_date) || !Number.isFinite(Date.parse(input.due_date + 'T00:00:00Z')) || new Date(input.due_date + 'T00:00:00Z').toISOString().slice(0, 10) !== input.due_date)) errors.push('due_date must be a real date in YYYY-MM-DD format');\n if (errors.length) return reject('invalid_payload', errors);\n const data = {\n customer_name: input.customer_name.trim(), customer_email: input.customer_email.toLowerCase(),\n customer_address: typeof input.customer_address === 'string' ? input.customer_address : '',\n customer_postal_code: typeof input.customer_postal_code === 'string' ? input.customer_postal_code : '00000',\n customer_city: typeof input.customer_city === 'string' ? input.customer_city : 'Non renseigné',\n customer_country: input.customer_country ?? 'FR', currency: input.currency ?? 'EUR', reference: input.reference ?? null,\n items: input.items.map(item => ({ label: item.label.trim(), quantity: item.quantity, unit_price: item.unit_price, vat_rate: item.vat_rate ?? 'FR_200' })),\n due_date: input.due_date ?? new Date(now + 30 * 86400000).toISOString().slice(0, 10), invoice_date: new Date(now).toISOString().slice(0, 10), send_email: false,\n };\n return { accepted: true, data, filter: JSON.stringify([{ field: 'emails', operator: 'in', value: [data.customer_email] }]), customer: { name: data.customer_name, emails: [data.customer_email], billing_address: { address: data.customer_address, postal_code: data.customer_postal_code, city: data.customer_city, country_alpha2: data.customer_country } }, body: {}, statusCode: 200 };\n}\n\nexport default function authorizePennylaneInvoice({ body, headers, expectedAuthorization }) {\n const credentials = header => {\n if (typeof header !== 'string') return '';\n const match = /^ *Basic +([A-Za-z0-9+/]+={0,2}) *$/i.exec(header);\n if (!match) return '';\n const decoded = Buffer.from(match[1], 'base64').toString('utf8');\n const separator = decoded.indexOf(':');\n return separator > 0 && separator < decoded.length - 1 && !/[\\r\\n]/.test(decoded) ? decoded : '';\n };\n const expected = credentials(expectedAuthorization);\n if (!expected) throw new Error('Configure the protected webhook Basic authorization secret with a nonempty username and password before enabling this workflow.');\n const values = Object.entries(headers ?? {}).filter(([name]) => name.toLowerCase() === 'authorization');\n const received = values.length === 1 ? credentials(values[0][1]) : '';\n // Compare decoded username/password without returning either credential.\n let difference = received.length ^ expected.length;\n for (let index = 0; index < Math.max(received.length, expected.length); index++) difference |= (received.charCodeAt(index) || 0) ^ (expected.charCodeAt(index) || 0);\n if (difference !== 0) return { accepted: false, data: {}, filter: '', customer: {}, body: { success: false, error: 'unauthorized' }, statusCode: 401 };\n return validatePennylaneInvoice(body);\n}\n" }, "n_407d3fb857f77a7f": { "name": "Return rejected request", "imports": [], "source": "export default function pennylaneInvoiceReply({ body, statusCode, data, rejected }) {\n const headers = { 'Cache-Control': 'no-store', ...(statusCode === 401 ? { 'WWW-Authenticate': 'Basic realm=\"Webhook\"' } : {}) };\n if (rejected) return { body, statusCode, headers };\n if (statusCode !== 201 || !body || typeof body !== 'object' || !Number.isSafeInteger(body.id) || body.id <= 0 || typeof body.invoice_number !== 'string' || !body.invoice_number.trim() || typeof body.currency_amount !== 'string' || !body.currency_amount.trim() || (body.public_file_url !== null && typeof body.public_file_url !== 'string')) throw new Error('Pennylane did not confirm a completed invoice with its documented HTTP 201 fields.');\n return { body: { success: true, invoice_id: String(body.id), invoice_number: body.invoice_number, amount: body.currency_amount, customer: data.customer_name, public_url: body.public_file_url || null, email_sent: false }, statusCode: 200, headers };\n}\n" }, "n_64e33166ffaad981": { "name": "Select first matching customer", "imports": [], "source": "export default function selectPennylaneCustomer({ body, statusCode, data }) {\n if (statusCode !== 200 || !body || typeof body !== 'object' || !Array.isArray(body.items)) throw new Error('Pennylane customer lookup did not return its documented list response.');\n if (!body.items.length) return { found: false, customerId: 0 };\n const id = body.items[0]?.id;\n if (!Number.isSafeInteger(id) || id <= 0) throw new Error('The first matching Pennylane customer has no valid integer ID.');\n const emails = body.items[0].emails;\n if (!Array.isArray(emails) || !emails.some(email => typeof email === 'string' && email.trim().toLowerCase() === data.customer_email)) throw new Error('The first returned Pennylane customer does not contain the requested email. No invoice was created.');\n return { found: true, customerId: id };\n}\n" }, "n_86b2feb992d093af": { "name": "Build invoice for new customer", "imports": [], "source": "export default function buildPennylaneInvoice({ data, mode, body, statusCode, customerId }) {\n let id = customerId;\n if (mode === 'created') {\n if (statusCode !== 201 || !body || typeof body !== 'object') throw new Error('Pennylane did not confirm customer creation with HTTP 201.');\n id = body.id;\n } else if (mode !== 'existing') throw new Error('Unknown customer selection path.');\n if (!Number.isSafeInteger(id) || id <= 0) throw new Error('A valid Pennylane customer ID is required before creating an invoice.');\n const payload = {\n customer_id: id, date: data.invoice_date, deadline: data.due_date, currency: data.currency,\n invoice_lines: data.items.map(item => ({ label: item.label, quantity: item.quantity, unit: 'piece', raw_currency_unit_price: String(item.unit_price.toFixed(2)), vat_rate: item.vat_rate })), draft: false,\n };\n if (data.reference) payload.external_reference = data.reference;\n return { payload };\n}\n" }, "n_4d331ee91a94db04": { "name": "Build invoice for existing customer", "imports": [], "source": "export default function buildPennylaneInvoice({ data, mode, body, statusCode, customerId }) {\n let id = customerId;\n if (mode === 'created') {\n if (statusCode !== 201 || !body || typeof body !== 'object') throw new Error('Pennylane did not confirm customer creation with HTTP 201.');\n id = body.id;\n } else if (mode !== 'existing') throw new Error('Unknown customer selection path.');\n if (!Number.isSafeInteger(id) || id <= 0) throw new Error('A valid Pennylane customer ID is required before creating an invoice.');\n const payload = {\n customer_id: id, date: data.invoice_date, deadline: data.due_date, currency: data.currency,\n invoice_lines: data.items.map(item => ({ label: item.label, quantity: item.quantity, unit: 'piece', raw_currency_unit_price: String(item.unit_price.toFixed(2)), vat_rate: item.vat_rate })), draft: false,\n };\n if (data.reference) payload.external_reference = data.reference;\n return { payload };\n}\n" }, "n_d1851c46066670a1": { "name": "Confirm created invoice", "imports": [], "source": "export default function pennylaneInvoiceReply({ body, statusCode, data, rejected }) {\n const headers = { 'Cache-Control': 'no-store', ...(statusCode === 401 ? { 'WWW-Authenticate': 'Basic realm=\"Webhook\"' } : {}) };\n if (rejected) return { body, statusCode, headers };\n if (statusCode !== 201 || !body || typeof body !== 'object' || !Number.isSafeInteger(body.id) || body.id <= 0 || typeof body.invoice_number !== 'string' || !body.invoice_number.trim() || typeof body.currency_amount !== 'string' || !body.currency_amount.trim() || (body.public_file_url !== null && typeof body.public_file_url !== 'string')) throw new Error('Pennylane did not confirm a completed invoice with its documented HTTP 201 fields.');\n return { body: { success: true, invoice_id: String(body.id), invoice_number: body.invoice_number, amount: body.currency_amount, customer: data.customer_name, public_url: body.public_file_url || null, email_sent: false }, statusCode: 200, headers };\n}\n" } } }, "presentation": { "designer": { "version": 1, "flow": { "direction": "vertical" } } } }