@teleporthq/teleport-plugin-next-workflows 0.44.0 → 0.44.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/__tests__/_helpers/fake-store-database.ts +428 -0
- package/__tests__/_helpers/invoice-store.ts +382 -0
- package/__tests__/email-locale-senders.test.ts +3 -1
- package/__tests__/email-sender-name.test.ts +513 -0
- package/__tests__/graph-utils-topological-order.test.ts +155 -1
- package/__tests__/invoice-billing-details.test.ts +107 -1
- package/__tests__/invoice-data-access-replace.test.ts +236 -2
- package/__tests__/invoice-order-items-settle.test.ts +1 -1
- package/__tests__/invoice-payment-email.test.ts +191 -0
- package/__tests__/invoice-regenerate-email.test.ts +288 -0
- package/__tests__/invoice-regenerate-route.test.ts +9 -7
- package/__tests__/invoice-reservation.test.ts +1 -1
- package/__tests__/invoice-tender.test.ts +1 -1
- package/__tests__/segment-splitter-checkout-order.test.ts +306 -0
- package/__tests__/streaming-on-end-env-filter.test.ts +18 -18
- package/__tests__/streaming-on-end-order.test.ts +387 -0
- package/dist/cjs/account-delete-route-generator.d.ts +1 -0
- package/dist/cjs/account-delete-route-generator.d.ts.map +1 -1
- package/dist/cjs/account-delete-route-generator.js +2 -1
- package/dist/cjs/account-delete-route-generator.js.map +1 -1
- package/dist/cjs/auth-generator.d.ts +1 -0
- package/dist/cjs/auth-generator.d.ts.map +1 -1
- package/dist/cjs/auth-generator.js +2 -1
- package/dist/cjs/auth-generator.js.map +1 -1
- package/dist/cjs/executor-generator.d.ts.map +1 -1
- package/dist/cjs/executor-generator.js +9 -26
- package/dist/cjs/executor-generator.js.map +1 -1
- package/dist/cjs/graph-utils.d.ts +31 -1
- package/dist/cjs/graph-utils.d.ts.map +1 -1
- package/dist/cjs/graph-utils.js +112 -10
- package/dist/cjs/graph-utils.js.map +1 -1
- package/dist/cjs/invoice/api-routes-code.d.ts +12 -3
- package/dist/cjs/invoice/api-routes-code.d.ts.map +1 -1
- package/dist/cjs/invoice/api-routes-code.js +152 -21
- package/dist/cjs/invoice/api-routes-code.js.map +1 -1
- package/dist/cjs/invoice/data-access-code.d.ts.map +1 -1
- package/dist/cjs/invoice/data-access-code.js +133 -21
- package/dist/cjs/invoice/data-access-code.js.map +1 -1
- package/dist/cjs/invoice/email-sender-code.d.ts.map +1 -1
- package/dist/cjs/invoice/email-sender-code.js +15 -25
- package/dist/cjs/invoice/email-sender-code.js.map +1 -1
- package/dist/cjs/invoice/index.js +1 -1
- package/dist/cjs/invoice/index.js.map +1 -1
- package/dist/cjs/invoice/invoice-assembly-code.d.ts.map +1 -1
- package/dist/cjs/invoice/invoice-assembly-code.js +10 -6
- package/dist/cjs/invoice/invoice-assembly-code.js.map +1 -1
- package/dist/cjs/invoice/invoice-html-code.d.ts.map +1 -1
- package/dist/cjs/invoice/invoice-html-code.js +3 -0
- package/dist/cjs/invoice/invoice-html-code.js.map +1 -1
- package/dist/cjs/nodes/email/email-mailgun.d.ts.map +1 -1
- package/dist/cjs/nodes/email/email-mailgun.js +4 -2
- package/dist/cjs/nodes/email/email-mailgun.js.map +1 -1
- package/dist/cjs/nodes/email/email-postmark.d.ts.map +1 -1
- package/dist/cjs/nodes/email/email-postmark.js +5 -2
- package/dist/cjs/nodes/email/email-postmark.js.map +1 -1
- package/dist/cjs/nodes/email/email-resend.d.ts.map +1 -1
- package/dist/cjs/nodes/email/email-resend.js +5 -2
- package/dist/cjs/nodes/email/email-resend.js.map +1 -1
- package/dist/cjs/nodes/email/email-sendgrid.d.ts.map +1 -1
- package/dist/cjs/nodes/email/email-sendgrid.js +8 -2
- package/dist/cjs/nodes/email/email-sendgrid.js.map +1 -1
- package/dist/cjs/nodes/storage/storage-local-remove.d.ts.map +1 -1
- package/dist/cjs/nodes/storage/storage-local-remove.js +2 -0
- package/dist/cjs/nodes/storage/storage-local-remove.js.map +1 -1
- package/dist/cjs/nodes/storage/storage-local-set.d.ts.map +1 -1
- package/dist/cjs/nodes/storage/storage-local-set.js +4 -0
- package/dist/cjs/nodes/storage/storage-local-set.js.map +1 -1
- package/dist/cjs/segment-splitter.d.ts.map +1 -1
- package/dist/cjs/segment-splitter.js +65 -27
- package/dist/cjs/segment-splitter.js.map +1 -1
- package/dist/cjs/transactional-email-code.d.ts.map +1 -1
- package/dist/cjs/transactional-email-code.js +32 -18
- package/dist/cjs/transactional-email-code.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/cjs/workflow-project-plugin.d.ts.map +1 -1
- package/dist/cjs/workflow-project-plugin.js +2 -0
- package/dist/cjs/workflow-project-plugin.js.map +1 -1
- package/dist/esm/account-delete-route-generator.d.ts +1 -0
- package/dist/esm/account-delete-route-generator.d.ts.map +1 -1
- package/dist/esm/account-delete-route-generator.js +2 -1
- package/dist/esm/account-delete-route-generator.js.map +1 -1
- package/dist/esm/auth-generator.d.ts +1 -0
- package/dist/esm/auth-generator.d.ts.map +1 -1
- package/dist/esm/auth-generator.js +2 -1
- package/dist/esm/auth-generator.js.map +1 -1
- package/dist/esm/executor-generator.d.ts.map +1 -1
- package/dist/esm/executor-generator.js +9 -26
- package/dist/esm/executor-generator.js.map +1 -1
- package/dist/esm/graph-utils.d.ts +31 -1
- package/dist/esm/graph-utils.d.ts.map +1 -1
- package/dist/esm/graph-utils.js +109 -9
- package/dist/esm/graph-utils.js.map +1 -1
- package/dist/esm/invoice/api-routes-code.d.ts +12 -3
- package/dist/esm/invoice/api-routes-code.d.ts.map +1 -1
- package/dist/esm/invoice/api-routes-code.js +152 -21
- package/dist/esm/invoice/api-routes-code.js.map +1 -1
- package/dist/esm/invoice/data-access-code.d.ts.map +1 -1
- package/dist/esm/invoice/data-access-code.js +133 -21
- package/dist/esm/invoice/data-access-code.js.map +1 -1
- package/dist/esm/invoice/email-sender-code.d.ts.map +1 -1
- package/dist/esm/invoice/email-sender-code.js +15 -25
- package/dist/esm/invoice/email-sender-code.js.map +1 -1
- package/dist/esm/invoice/index.js +1 -1
- package/dist/esm/invoice/index.js.map +1 -1
- package/dist/esm/invoice/invoice-assembly-code.d.ts.map +1 -1
- package/dist/esm/invoice/invoice-assembly-code.js +10 -6
- package/dist/esm/invoice/invoice-assembly-code.js.map +1 -1
- package/dist/esm/invoice/invoice-html-code.d.ts.map +1 -1
- package/dist/esm/invoice/invoice-html-code.js +3 -0
- package/dist/esm/invoice/invoice-html-code.js.map +1 -1
- package/dist/esm/nodes/email/email-mailgun.d.ts.map +1 -1
- package/dist/esm/nodes/email/email-mailgun.js +4 -2
- package/dist/esm/nodes/email/email-mailgun.js.map +1 -1
- package/dist/esm/nodes/email/email-postmark.d.ts.map +1 -1
- package/dist/esm/nodes/email/email-postmark.js +5 -2
- package/dist/esm/nodes/email/email-postmark.js.map +1 -1
- package/dist/esm/nodes/email/email-resend.d.ts.map +1 -1
- package/dist/esm/nodes/email/email-resend.js +5 -2
- package/dist/esm/nodes/email/email-resend.js.map +1 -1
- package/dist/esm/nodes/email/email-sendgrid.d.ts.map +1 -1
- package/dist/esm/nodes/email/email-sendgrid.js +8 -2
- package/dist/esm/nodes/email/email-sendgrid.js.map +1 -1
- package/dist/esm/nodes/storage/storage-local-remove.d.ts.map +1 -1
- package/dist/esm/nodes/storage/storage-local-remove.js +2 -0
- package/dist/esm/nodes/storage/storage-local-remove.js.map +1 -1
- package/dist/esm/nodes/storage/storage-local-set.d.ts.map +1 -1
- package/dist/esm/nodes/storage/storage-local-set.js +4 -0
- package/dist/esm/nodes/storage/storage-local-set.js.map +1 -1
- package/dist/esm/segment-splitter.d.ts.map +1 -1
- package/dist/esm/segment-splitter.js +66 -28
- package/dist/esm/segment-splitter.js.map +1 -1
- package/dist/esm/transactional-email-code.d.ts.map +1 -1
- package/dist/esm/transactional-email-code.js +32 -18
- package/dist/esm/transactional-email-code.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/workflow-project-plugin.d.ts.map +1 -1
- package/dist/esm/workflow-project-plugin.js +2 -0
- package/dist/esm/workflow-project-plugin.js.map +1 -1
- package/package.json +3 -3
- package/src/account-delete-route-generator.ts +3 -1
- package/src/auth-generator.ts +3 -1
- package/src/executor-generator.ts +9 -26
- package/src/graph-utils.ts +143 -11
- package/src/invoice/api-routes-code.ts +166 -21
- package/src/invoice/data-access-code.ts +139 -21
- package/src/invoice/email-sender-code.ts +15 -25
- package/src/invoice/index.ts +1 -1
- package/src/invoice/invoice-assembly-code.ts +10 -6
- package/src/invoice/invoice-html-code.ts +3 -0
- package/src/nodes/email/email-mailgun.ts +11 -2
- package/src/nodes/email/email-postmark.ts +12 -2
- package/src/nodes/email/email-resend.ts +12 -2
- package/src/nodes/email/email-sendgrid.ts +15 -2
- package/src/nodes/storage/storage-local-remove.ts +4 -0
- package/src/nodes/storage/storage-local-set.ts +6 -0
- package/src/segment-splitter.ts +90 -36
- package/src/transactional-email-code.ts +32 -21
- package/src/workflow-project-plugin.ts +2 -0
|
@@ -57,6 +57,42 @@ async function invoiceCaller(req) {
|
|
|
57
57
|
};
|
|
58
58
|
}`
|
|
59
59
|
|
|
60
|
+
/**
|
|
61
|
+
* The order-history line of an invoice email the provider accepted: an
|
|
62
|
+
* `order-email-sent` line of the email's purpose, keyed on the invoice, so the
|
|
63
|
+
* email is on its order's history once, whichever route sent it. The strings
|
|
64
|
+
* are data in the merchant's database, owned by teleport-gui's
|
|
65
|
+
* `order-event-types.ts` and `EmailTemplatePurpose.INVOICE_EMAIL`.
|
|
66
|
+
*/
|
|
67
|
+
const INVOICE_EMAIL_EVENT = { type: 'order-email-sent', purpose: 'invoice-email' }
|
|
68
|
+
|
|
69
|
+
/** Who sent an invoice email, as the order's history names them. */
|
|
70
|
+
const INVOICE_EMAIL_SENDERS = {
|
|
71
|
+
// The store's own code: the payment webhook's invoice step, the checkout.
|
|
72
|
+
store: { actor: '', actorType: 'system' },
|
|
73
|
+
// A signed-in admin: the admin panel's Regenerate, or an invoice issued by hand.
|
|
74
|
+
admin: { actor: 'Store admin', actorType: 'merchant' },
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** `recordInvoiceEmailSent(invoiceData, sender)`, as emitted source; needs `dataAccess`. */
|
|
78
|
+
const RECORD_INVOICE_EMAIL_CODE = `
|
|
79
|
+
var INVOICE_EMAIL_EVENT = ${JSON.stringify(INVOICE_EMAIL_EVENT)};
|
|
80
|
+
var INVOICE_EMAIL_SENDERS = ${JSON.stringify(INVOICE_EMAIL_SENDERS)};
|
|
81
|
+
|
|
82
|
+
// Writes the order's history line for an invoice email the provider accepted.
|
|
83
|
+
// The key is the invoice's, so the line is written once per invoice email.
|
|
84
|
+
async function recordInvoiceEmailSent(invoiceData, sender) {
|
|
85
|
+
if (!invoiceData.orderId) return;
|
|
86
|
+
await dataAccess.recordOrderEvent({
|
|
87
|
+
type: INVOICE_EMAIL_EVENT.type,
|
|
88
|
+
dedupeKey: INVOICE_EMAIL_EVENT.type + ':' + invoiceData.id + ':' + INVOICE_EMAIL_EVENT.purpose,
|
|
89
|
+
orderId: invoiceData.orderId,
|
|
90
|
+
payload: { purpose: INVOICE_EMAIL_EVENT.purpose, to: invoiceData.customerEmail },
|
|
91
|
+
actor: sender.actor,
|
|
92
|
+
actorType: sender.actorType,
|
|
93
|
+
});
|
|
94
|
+
}`
|
|
95
|
+
|
|
60
96
|
export const generateInvoiceGenerateRouteCode = (settings: UIDLInvoiceSettings): string => {
|
|
61
97
|
const prefix = settings.invoicePrefix || 'INV-'
|
|
62
98
|
const autoGenerate = settings.autoGenerateOnPayment !== false
|
|
@@ -92,6 +128,7 @@ var INVOICE_PREFIX = ${JSON.stringify(prefix)};
|
|
|
92
128
|
var UNFINISHED_INVOICE_STALE_SECONDS = 900;
|
|
93
129
|
|
|
94
130
|
${INVOICE_CALLER_CODE}
|
|
131
|
+
${emailEnabled ? RECORD_INVOICE_EMAIL_CODE : ''}
|
|
95
132
|
|
|
96
133
|
// ---------------------------------------------------------------------------
|
|
97
134
|
// Waiting for the order's line items to be fully written.
|
|
@@ -127,6 +164,21 @@ function sleepMs(ms) {
|
|
|
127
164
|
return new Promise(function (resolve) { setTimeout(resolve, ms); });
|
|
128
165
|
}
|
|
129
166
|
|
|
167
|
+
// The answer for an invoice the order already has: its number and its PDF.
|
|
168
|
+
function issuedInvoiceAnswer(invoice, currency) {
|
|
169
|
+
var pdfUrl = invoice.pdf_url || ('/api/invoices/' + invoice.id + '/pdf');
|
|
170
|
+
return {
|
|
171
|
+
success: true,
|
|
172
|
+
alreadyIssued: true,
|
|
173
|
+
invoiceId: invoice.id,
|
|
174
|
+
invoiceNumber: invoice.invoice_number,
|
|
175
|
+
storageUrl: pdfUrl.indexOf('/api/invoices/') === 0 ? '' : pdfUrl,
|
|
176
|
+
pdfUrl: pdfUrl,
|
|
177
|
+
total: Number(invoice.total) || 0,
|
|
178
|
+
currency: invoice.currency || currency,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
130
182
|
async function hydrateOrderWhenSettled(orderId) {
|
|
131
183
|
var deadline = Date.now() + SETTLE_TIMEOUT_MS;
|
|
132
184
|
var lastCount = -1;
|
|
@@ -267,17 +319,7 @@ module.exports = async function handler(req, res) {
|
|
|
267
319
|
if (!reservation.created) {
|
|
268
320
|
if (Number(held.pdf_size_bytes) > 0) {
|
|
269
321
|
console.info('[invoice] Order ' + body.orderId + ' already has invoice ' + held.invoice_number + ' — returning it');
|
|
270
|
-
|
|
271
|
-
res.status(200).json({
|
|
272
|
-
success: true,
|
|
273
|
-
alreadyIssued: true,
|
|
274
|
-
invoiceId: held.id,
|
|
275
|
-
invoiceNumber: held.invoice_number,
|
|
276
|
-
storageUrl: heldPdfUrl.indexOf('/api/invoices/') === 0 ? '' : heldPdfUrl,
|
|
277
|
-
pdfUrl: heldPdfUrl,
|
|
278
|
-
total: Number(held.total) || 0,
|
|
279
|
-
currency: held.currency || currency,
|
|
280
|
-
});
|
|
322
|
+
res.status(200).json(issuedInvoiceAnswer(held, currency));
|
|
281
323
|
return;
|
|
282
324
|
}
|
|
283
325
|
// Reserved, never finished: another request is rendering it now, or the
|
|
@@ -313,7 +355,18 @@ ${
|
|
|
313
355
|
console.info('[invoice] PDF generated: ' + pdfBuffer.length + ' bytes for ' + invoiceData.invoiceNumber);
|
|
314
356
|
|
|
315
357
|
invoiceData.pdfUrl = '/api/invoices/' + invoiceData.id + '/pdf';
|
|
316
|
-
|
|
358
|
+
// The PDF is written only on an invoice that has none, and the invoice's
|
|
359
|
+
// first PDF is what its email is claimed by (see storeInvoicePdf). A
|
|
360
|
+
// regeneration that gave a reserved invoice its PDF while this request
|
|
361
|
+
// rendered it — a retried payment finishing a stale reservation — emailed
|
|
362
|
+
// the buyer itself, so this request answers with that invoice and sends
|
|
363
|
+
// nothing.
|
|
364
|
+
var stored = await dataAccess.storeInvoicePdf(invoiceData.id, pdfBuffer, invoiceData.pdfUrl);
|
|
365
|
+
if (!stored.firstPdf) {
|
|
366
|
+
console.info('[invoice] Invoice ' + invoiceNumber + ' was given its PDF by a regeneration meanwhile — returning it');
|
|
367
|
+
res.status(200).json(issuedInvoiceAnswer((await dataAccess.getInvoiceById(invoiceData.id)) || held, currency));
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
317
370
|
console.info('[invoice] PDF stored — teleport_invoices.id=' + invoiceData.id + ', number=' + invoiceData.invoiceNumber + ', pdf_size_bytes=' + pdfBuffer.length + ', items=' + items.length + ' (accessible at ' + invoiceData.pdfUrl + ')');
|
|
318
371
|
|
|
319
372
|
var safeFileName = String(invoiceNumber || invoiceData.id).replace(/[^a-zA-Z0-9._-]/g, '_') + '.pdf';
|
|
@@ -382,9 +435,11 @@ ${
|
|
|
382
435
|
emailEnabled
|
|
383
436
|
? `
|
|
384
437
|
if (invoiceData.customerEmail) {
|
|
438
|
+
var emailed = false;
|
|
385
439
|
try {
|
|
386
440
|
var emailResult = await emailSender.sendInvoiceEmail(invoiceData, pdfBuffer);
|
|
387
|
-
|
|
441
|
+
emailed = !!(emailResult && emailResult.success);
|
|
442
|
+
if (emailed) {
|
|
388
443
|
console.info('[invoice] Email delivery OK — to=' + invoiceData.customerEmail + ', messageId=' + (emailResult.messageId || '(empty)'));
|
|
389
444
|
} else {
|
|
390
445
|
console.error('[invoice] Email delivery FAILED — to=' + invoiceData.customerEmail + ', error=' + (emailResult && emailResult.error ? emailResult.error : 'unknown'));
|
|
@@ -392,6 +447,13 @@ ${
|
|
|
392
447
|
} catch (emailErr) {
|
|
393
448
|
console.error('[invoice] Email delivery threw — to=' + invoiceData.customerEmail + ', error=' + (emailErr && emailErr.message ? emailErr.message : emailErr));
|
|
394
449
|
}
|
|
450
|
+
if (emailed) {
|
|
451
|
+
try {
|
|
452
|
+
await recordInvoiceEmailSent(invoiceData, caller.admin ? INVOICE_EMAIL_SENDERS.admin : INVOICE_EMAIL_SENDERS.store);
|
|
453
|
+
} catch (recordErr) {
|
|
454
|
+
console.error('[invoice] Invoice ' + invoiceNumber + ' was emailed, but the order history could not record it — ' + (recordErr && recordErr.message));
|
|
455
|
+
}
|
|
456
|
+
}
|
|
395
457
|
} else {
|
|
396
458
|
console.info('[invoice] Skipping email delivery — invoiceData.customerEmail is empty. (Check teleport_orders.billing_email for order ' + (body.orderId || '(no orderId)') + ')');
|
|
397
459
|
}
|
|
@@ -452,6 +514,37 @@ ${
|
|
|
452
514
|
/** The roles allowed to rebuild an invoice (the admin panel is the only caller). */
|
|
453
515
|
const REGENERATE_INVOICE_ROLES = INVOICE_ADMIN_ROLES
|
|
454
516
|
|
|
517
|
+
/** The regenerate route's `emailFirstInvoice`, as emitted source. */
|
|
518
|
+
const generateFirstInvoiceEmailCode = (): string => `${RECORD_INVOICE_EMAIL_CODE}
|
|
519
|
+
|
|
520
|
+
var REGENERATE_SOURCE_REF = 'api/invoices/[id]/regenerate';
|
|
521
|
+
|
|
522
|
+
// The invoice email the payment would have sent: the same sender, template,
|
|
523
|
+
// attachment and link, in the buyer's language — the one stamped on the order
|
|
524
|
+
// at checkout, else the store's main language, never the language of this
|
|
525
|
+
// request, which is the merchant's. The sender records it in the sent-email
|
|
526
|
+
// ledger; the order's history records it once the provider accepted it. A
|
|
527
|
+
// failure is logged and goes no further: the merchant has the PDF either way.
|
|
528
|
+
async function emailFirstInvoice(invoiceData, pdfBuffer, order) {
|
|
529
|
+
if (!invoiceData.customerEmail) return;
|
|
530
|
+
invoiceData.locale = emailLocale.normalizeEmailLocale(order && order.locale) || emailLocale.DEFAULT_EMAIL_LOCALE;
|
|
531
|
+
try {
|
|
532
|
+
var sent = await emailSender.sendInvoiceEmail(invoiceData, pdfBuffer, REGENERATE_SOURCE_REF);
|
|
533
|
+
if (!sent || !sent.success) {
|
|
534
|
+
throw new Error((sent && sent.error) || 'the email provider did not accept it');
|
|
535
|
+
}
|
|
536
|
+
} catch (sendErr) {
|
|
537
|
+
console.error('[invoice] Regenerate: invoice ' + invoiceData.invoiceNumber + ' could not be emailed to its customer — ' + (sendErr && sendErr.message));
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
try {
|
|
541
|
+
await recordInvoiceEmailSent(invoiceData, INVOICE_EMAIL_SENDERS.admin);
|
|
542
|
+
} catch (recordErr) {
|
|
543
|
+
console.error('[invoice] Regenerate: invoice ' + invoiceData.invoiceNumber + ' was emailed, but the order history could not record it — ' + (recordErr && recordErr.message));
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
`
|
|
547
|
+
|
|
455
548
|
/**
|
|
456
549
|
* POST /api/invoices/[id]/regenerate — the admin panel's "Regenerate" action.
|
|
457
550
|
*
|
|
@@ -461,23 +554,47 @@ const REGENERATE_INVOICE_ROLES = INVOICE_ADMIN_ROLES
|
|
|
461
554
|
* against it keep pointing at a valid invoice. What a merchant corrected on the
|
|
462
555
|
* invoice itself (customer details, notes, payment method) wins over the order;
|
|
463
556
|
* the lines and every figure are recomputed from the order. The old PDF is
|
|
464
|
-
* replaced — and its runtime-storage copy deleted
|
|
465
|
-
*
|
|
557
|
+
* replaced — and its runtime-storage copy deleted.
|
|
558
|
+
*
|
|
559
|
+
* The customer is emailed once: by the regeneration that gives an invoice its
|
|
560
|
+
* FIRST PDF — one whose PDF failed at payment, so the payment's invoice email
|
|
561
|
+
* never went out — and only when the store emails invoices (Invoice Email
|
|
562
|
+
* Delivery). It is the payment's email: the same sender, template, attachment,
|
|
563
|
+
* link and language, recorded in the sent-email ledger and on the order's
|
|
564
|
+
* history. Which write gives the first PDF, the database decides (see
|
|
565
|
+
* `replaceInvoice` and `storeInvoicePdf`), so a second regeneration, a double
|
|
566
|
+
* click, two requests at once or a retried payment finishing the same invoice
|
|
567
|
+
* never send it again; a failed send costs the merchant nothing.
|
|
466
568
|
*
|
|
467
569
|
* Unlike `/generate`, which the payment webhook calls server-to-server, this
|
|
468
570
|
* route is reached from the merchant's browser only, so it checks the session
|
|
469
571
|
* itself: signed in, with an admin role.
|
|
470
572
|
*/
|
|
471
|
-
export const generateInvoiceRegenerateRouteCode = (): string => {
|
|
573
|
+
export const generateInvoiceRegenerateRouteCode = (settings: UIDLInvoiceSettings): string => {
|
|
574
|
+
const emailEnabled = settings.emailDelivery?.enabled === true
|
|
575
|
+
|
|
472
576
|
return `/**
|
|
473
577
|
* POST /api/invoices/[id]/regenerate
|
|
474
578
|
* Rebuilds one invoice from its order, in place, for the admin panel. Same
|
|
475
|
-
* number, id and dates; a freshly rendered PDF replaces the old one
|
|
579
|
+
* number, id and dates; a freshly rendered PDF replaces the old one.${
|
|
580
|
+
emailEnabled
|
|
581
|
+
? `
|
|
582
|
+
* The first PDF an invoice ever gets is emailed to its customer, once.`
|
|
583
|
+
: `
|
|
584
|
+
* No email: this store does not email invoices.`
|
|
585
|
+
}
|
|
476
586
|
*/
|
|
477
587
|
|
|
478
588
|
var dataAccess = require('../../../../utils/invoices/data-access');
|
|
479
589
|
var pdfGenerator = require('../../../../utils/invoices/pdf-generator');
|
|
480
590
|
var invoiceAssembly = require('../../../../utils/invoices/invoice-assembly');
|
|
591
|
+
${
|
|
592
|
+
emailEnabled
|
|
593
|
+
? `var emailSender = require('../../../../utils/invoices/email-sender');
|
|
594
|
+
var sentEmailLog = require('../../../../utils/email/sent-email-log');
|
|
595
|
+
var emailLocale = require('../../../../utils/email/email-locale');`
|
|
596
|
+
: ''
|
|
597
|
+
}
|
|
481
598
|
|
|
482
599
|
${generateCommonJsSessionTokenResolverCode()}
|
|
483
600
|
${ROLE_OF_TOKEN_CODE}
|
|
@@ -563,7 +680,7 @@ async function findInvoice(invoiceId) {
|
|
|
563
680
|
throw err;
|
|
564
681
|
}
|
|
565
682
|
}
|
|
566
|
-
|
|
683
|
+
${emailEnabled ? generateFirstInvoiceEmailCode() : ''}
|
|
567
684
|
module.exports = async function handler(req, res) {
|
|
568
685
|
if (req.method !== 'POST') {
|
|
569
686
|
res.setHeader('Allow', 'POST');
|
|
@@ -622,7 +739,14 @@ module.exports = async function handler(req, res) {
|
|
|
622
739
|
invoiceData.pdfSizeBytes = pdfBuffer.length;
|
|
623
740
|
invoiceData.pdfUrl = '/api/invoices/' + invoiceData.id + '/pdf';
|
|
624
741
|
|
|
625
|
-
|
|
742
|
+
${
|
|
743
|
+
emailEnabled
|
|
744
|
+
? ` var replaced = await dataAccess.replaceInvoice(invoiceData.id, invoiceData, invoiceData.items);
|
|
745
|
+
// This regeneration gave the invoice its first PDF, so the payment's
|
|
746
|
+
// invoice email never went out: this one sends it.
|
|
747
|
+
var emailsCustomer = !!(replaced && replaced.firstPdf);`
|
|
748
|
+
: ` await dataAccess.replaceInvoice(invoiceData.id, invoiceData, invoiceData.items);`
|
|
749
|
+
}
|
|
626
750
|
console.info('[invoice] Regenerated ' + invoiceData.invoiceNumber + ' (id=' + invoiceData.id + ', items=' + invoiceData.items.length + ', pdf_size_bytes=' + pdfBuffer.length + ')');
|
|
627
751
|
|
|
628
752
|
// The new PDF's public copy. The row already serves the new bytes through
|
|
@@ -645,7 +769,17 @@ module.exports = async function handler(req, res) {
|
|
|
645
769
|
if (storageKey) {
|
|
646
770
|
await invoiceAssembly.deleteStoredInvoicePdf(storageKey);
|
|
647
771
|
}
|
|
648
|
-
|
|
772
|
+
${
|
|
773
|
+
emailEnabled
|
|
774
|
+
? ` // The invoice has its first PDF all the same — served from the database,
|
|
775
|
+
// its public copy just went — and no later regeneration will send it.
|
|
776
|
+
if (emailsCustomer) {
|
|
777
|
+
await emailFirstInvoice(invoiceData, pdfBuffer, hydrated.order);
|
|
778
|
+
}
|
|
779
|
+
await sentEmailLog.settleSentEmailLog();
|
|
780
|
+
`
|
|
781
|
+
: ''
|
|
782
|
+
} res.status(500).json({ success: false, error: MESSAGES.orderNotUpdated });
|
|
649
783
|
return;
|
|
650
784
|
}
|
|
651
785
|
|
|
@@ -663,7 +797,18 @@ module.exports = async function handler(req, res) {
|
|
|
663
797
|
if (previousStorageKey && previousStorageKey !== storageKey) {
|
|
664
798
|
await invoiceAssembly.deleteStoredInvoicePdf(previousStorageKey);
|
|
665
799
|
}
|
|
666
|
-
|
|
800
|
+
${
|
|
801
|
+
emailEnabled
|
|
802
|
+
? `
|
|
803
|
+
if (emailsCustomer) {
|
|
804
|
+
await emailFirstInvoice(invoiceData, pdfBuffer, hydrated.order);
|
|
805
|
+
}
|
|
806
|
+
// The email's ledger row is written in the background — land it before
|
|
807
|
+
// replying, a serverless function may be frozen the instant it responds.
|
|
808
|
+
await sentEmailLog.settleSentEmailLog();
|
|
809
|
+
`
|
|
810
|
+
: ''
|
|
811
|
+
}
|
|
667
812
|
res.status(200).json({
|
|
668
813
|
success: true,
|
|
669
814
|
invoiceId: invoiceData.id,
|
|
@@ -27,7 +27,7 @@ ${generateGetInvoiceByIdCode(dsType, invoicesTable)}
|
|
|
27
27
|
|
|
28
28
|
${generateUpdateInvoiceCode(dsType, invoicesTable)}
|
|
29
29
|
|
|
30
|
-
${generateStoreInvoicePdfCode(dsType)}
|
|
30
|
+
${generateStoreInvoicePdfCode(dsType, invoicesTable)}
|
|
31
31
|
|
|
32
32
|
${generateGetOrderWithItemsCode(dsType)}
|
|
33
33
|
|
|
@@ -35,6 +35,8 @@ ${generateReplaceInvoiceCode(dsType, invoicesTable, invoiceItemsTable)}
|
|
|
35
35
|
|
|
36
36
|
${generateLinkOrderToInvoiceCode(dsType)}
|
|
37
37
|
|
|
38
|
+
${generateRecordOrderEventCode(dsType)}
|
|
39
|
+
|
|
38
40
|
module.exports = {
|
|
39
41
|
getClient: getClient,
|
|
40
42
|
reserveInvoice: reserveInvoice,
|
|
@@ -45,6 +47,7 @@ module.exports = {
|
|
|
45
47
|
getOrderWithItems: getOrderWithItems,
|
|
46
48
|
replaceInvoice: replaceInvoice,
|
|
47
49
|
linkOrderToInvoice: linkOrderToInvoice,
|
|
50
|
+
recordOrderEvent: recordOrderEvent,
|
|
48
51
|
};
|
|
49
52
|
`
|
|
50
53
|
}
|
|
@@ -356,18 +359,47 @@ ${cutoff}
|
|
|
356
359
|
}`
|
|
357
360
|
}
|
|
358
361
|
|
|
359
|
-
// Puts the rendered PDF on the reserved row
|
|
360
|
-
// the invoice
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
362
|
+
// Puts the rendered PDF on the reserved row — only while the row has none —
|
|
363
|
+
// and resolves to `{ firstPdf }`: whether this write gave the invoice its
|
|
364
|
+
// first PDF. `pdf_size_bytes` marks the invoice finished (see
|
|
365
|
+
// `claimStaleInvoice`), and the invoice's first PDF is what its email is
|
|
366
|
+
// claimed by: `replaceInvoice` writes first under the same condition, so of a
|
|
367
|
+
// retried payment finishing a stale invoice and a regeneration racing it, one
|
|
368
|
+
// writes the PDF and emails the buyer, and the other is told it was not first.
|
|
369
|
+
function generateStoreInvoicePdfCode(dsType: string, table: string): string {
|
|
370
|
+
if (dsType === 'supabase') {
|
|
371
|
+
return `
|
|
364
372
|
async function storeInvoicePdf(invoiceId, pdfBuffer, pdfUrl) {
|
|
365
|
-
|
|
366
|
-
pdf_data:
|
|
373
|
+
var result = await getClient().from('${table}').update({
|
|
374
|
+
pdf_data: pdfBuffer.toString('base64'),
|
|
367
375
|
pdf_size_bytes: pdfBuffer.length,
|
|
368
376
|
pdf_content_type: 'application/pdf',
|
|
369
377
|
pdf_url: pdfUrl,
|
|
370
|
-
|
|
378
|
+
updated_at: new Date().toISOString(),
|
|
379
|
+
}).eq('id', invoiceId).is('pdf_size_bytes', null).select('id');
|
|
380
|
+
if (result.error) throw new Error(result.error.message);
|
|
381
|
+
return { firstPdf: !!(result.data && result.data.length > 0) };
|
|
382
|
+
}`
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (dsType === 'mysql' || dsType === 'mariadb' || dsType === 'tidb') {
|
|
386
|
+
return `
|
|
387
|
+
async function storeInvoicePdf(invoiceId, pdfBuffer, pdfUrl) {
|
|
388
|
+
var [result] = await getClient().execute(
|
|
389
|
+
'UPDATE ${table} SET pdf_data = ?, pdf_size_bytes = ?, pdf_content_type = ?, pdf_url = ?, updated_at = ? WHERE id = ? AND pdf_size_bytes IS NULL',
|
|
390
|
+
[pdfBuffer, pdfBuffer.length, 'application/pdf', pdfUrl, new Date().toISOString(), invoiceId]
|
|
391
|
+
);
|
|
392
|
+
return { firstPdf: !!(result && result.affectedRows > 0) };
|
|
393
|
+
}`
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return `
|
|
397
|
+
async function storeInvoicePdf(invoiceId, pdfBuffer, pdfUrl) {
|
|
398
|
+
var result = await getClient().query(
|
|
399
|
+
'UPDATE ${table} SET pdf_data = $1, pdf_size_bytes = $2, pdf_content_type = $3, pdf_url = $4, updated_at = $5 WHERE id = $6 AND pdf_size_bytes IS NULL',
|
|
400
|
+
[pdfBuffer, pdfBuffer.length, 'application/pdf', pdfUrl, new Date().toISOString(), invoiceId]
|
|
401
|
+
);
|
|
402
|
+
return { firstPdf: result.rowCount > 0 };
|
|
371
403
|
}`
|
|
372
404
|
}
|
|
373
405
|
|
|
@@ -565,6 +597,15 @@ async function getOrderWithItems(orderId) {
|
|
|
565
597
|
// points at it (the order, the sent-email ledger) stays valid. Postgres and
|
|
566
598
|
// MySQL do it in one transaction, and Supabase undoes its steps, so a failure
|
|
567
599
|
// leaves the old invoice whole.
|
|
600
|
+
//
|
|
601
|
+
// Resolves to `{ firstPdf }`: whether this write gave the invoice its first
|
|
602
|
+
// PDF — one whose PDF never came at payment, so its buyer was never emailed
|
|
603
|
+
// it. The database decides: the row is first written only where it has no PDF
|
|
604
|
+
// (the `pdf_size_bytes IS NULL` that `claimStaleInvoice` reads as
|
|
605
|
+
// "unfinished"), and of two regenerations racing for the same invoice the
|
|
606
|
+
// second waits for the first's row lock, then finds the PDF and matches
|
|
607
|
+
// nothing. `storeInvoicePdf` writes the payment's PDF under the same
|
|
608
|
+
// condition, so exactly one write is ever told `firstPdf: true`.
|
|
568
609
|
function generateReplaceInvoiceCode(dsType: string, table: string, itemsTable: string): string {
|
|
569
610
|
if (dsType === 'supabase') {
|
|
570
611
|
return `
|
|
@@ -584,18 +625,23 @@ async function replaceInvoice(invoiceId, invoiceData, items) {
|
|
|
584
625
|
var insertedIds = (inserted || []).map(function (row) { return row.id; });
|
|
585
626
|
var written = null;
|
|
586
627
|
try {
|
|
587
|
-
var
|
|
588
|
-
var
|
|
589
|
-
if (
|
|
590
|
-
|
|
628
|
+
var outcome = await writeInvoiceRecord(mapInvoiceReplacementToRecord(invoiceData), async function (fields) {
|
|
629
|
+
var first = await client.from('${table}').update(fields).eq('id', invoiceId).is('pdf_size_bytes', null).select('id');
|
|
630
|
+
if (first.error) throw new Error(first.error.message);
|
|
631
|
+
var firstPdf = !!(first.data && first.data.length > 0);
|
|
632
|
+
if (!firstPdf) {
|
|
633
|
+
var result = await client.from('${table}').update(fields).eq('id', invoiceId).select('id');
|
|
634
|
+
if (result.error) throw new Error(result.error.message);
|
|
635
|
+
if (!result.data || result.data.length === 0) throw new Error('Invoice ' + invoiceId + ' no longer exists');
|
|
636
|
+
}
|
|
591
637
|
written = fields;
|
|
592
|
-
return
|
|
638
|
+
return { firstPdf: firstPdf };
|
|
593
639
|
});
|
|
594
640
|
if (previousIds.length > 0) {
|
|
595
641
|
var removed = await client.from('${itemsTable}').delete().in('id', previousIds);
|
|
596
642
|
if (removed.error) throw new Error(removed.error.message);
|
|
597
643
|
}
|
|
598
|
-
return
|
|
644
|
+
return outcome;
|
|
599
645
|
} catch (err) {
|
|
600
646
|
if (written) {
|
|
601
647
|
var restored = {};
|
|
@@ -624,12 +670,17 @@ async function replaceInvoice(invoiceId, invoiceData, items) {
|
|
|
624
670
|
var setClauses = columns.map(function(k) { return quoteIdent(k) + ' = ?'; }).join(', ');
|
|
625
671
|
var values = columns.map(function(k) { return fields[k]; });
|
|
626
672
|
values.push(invoiceId);
|
|
627
|
-
var
|
|
628
|
-
|
|
673
|
+
var update = 'UPDATE ${table} SET ' + setClauses + ' WHERE id = ?';
|
|
674
|
+
var [first] = await conn.execute(update + ' AND pdf_size_bytes IS NULL', values);
|
|
675
|
+
var firstPdf = !!(first && first.affectedRows > 0);
|
|
676
|
+
if (!firstPdf) {
|
|
677
|
+
var [updated] = await conn.execute(update, values);
|
|
678
|
+
if (!updated || !updated.affectedRows) throw new Error('Invoice ' + invoiceId + ' no longer exists');
|
|
679
|
+
}
|
|
629
680
|
await conn.execute('DELETE FROM ${itemsTable} WHERE invoice_id = ?', [invoiceId]);
|
|
630
681
|
await insertInvoiceItemRows(conn, invoiceId, items);
|
|
631
682
|
await conn.commit();
|
|
632
|
-
return
|
|
683
|
+
return { firstPdf: firstPdf };
|
|
633
684
|
} catch (err) {
|
|
634
685
|
try { await conn.rollback(); } catch (_rollbackErr) {}
|
|
635
686
|
throw err;
|
|
@@ -653,12 +704,16 @@ async function replaceInvoice(invoiceId, invoiceData, items) {
|
|
|
653
704
|
values.push(invoiceId);
|
|
654
705
|
// The UPDATE takes the row lock first, so two regenerations of the same
|
|
655
706
|
// invoice run one after the other instead of interleaving their lines.
|
|
656
|
-
var
|
|
657
|
-
|
|
707
|
+
var update = 'UPDATE ${table} SET ' + setClauses + ' WHERE id = $' + values.length;
|
|
708
|
+
var firstPdf = (await client.query(update + ' AND pdf_size_bytes IS NULL', values)).rowCount > 0;
|
|
709
|
+
if (!firstPdf) {
|
|
710
|
+
var updated = await client.query(update, values);
|
|
711
|
+
if (!updated.rowCount) throw new Error('Invoice ' + invoiceId + ' no longer exists');
|
|
712
|
+
}
|
|
658
713
|
await client.query('DELETE FROM ${itemsTable} WHERE invoice_id = $1', [invoiceId]);
|
|
659
714
|
await insertInvoiceItemRows(client, invoiceId, items);
|
|
660
715
|
await client.query('COMMIT');
|
|
661
|
-
return
|
|
716
|
+
return { firstPdf: firstPdf };
|
|
662
717
|
} catch (err) {
|
|
663
718
|
try { await client.query('ROLLBACK'); } catch (_rollbackErr) {}
|
|
664
719
|
throw err;
|
|
@@ -719,6 +774,69 @@ async function linkOrderToInvoice(orderId, invoiceId, invoiceNumber, pdfUrl) {
|
|
|
719
774
|
}`
|
|
720
775
|
}
|
|
721
776
|
|
|
777
|
+
// One line of an order's history: a row of the activity log the editor
|
|
778
|
+
// provisions (`teleport_events`) and both order timelines read. Written like
|
|
779
|
+
// every line the store's workflows write — teleport-gui's
|
|
780
|
+
// `constants/order-event-types.ts` owns the vocabulary — so a replay collides
|
|
781
|
+
// on the UNIQUE `dedupe_key` and adds nothing. `event` is
|
|
782
|
+
// `{ type, dedupeKey, orderId, payload, actor, actorType }`.
|
|
783
|
+
function generateRecordOrderEventCode(dsType: string): string {
|
|
784
|
+
const row = `
|
|
785
|
+
var ORDER_EVENTS_TABLE = 'teleport_events';
|
|
786
|
+
var ORDER_EVENT_DEDUPE_KEY_MAX_LENGTH = 255;
|
|
787
|
+
|
|
788
|
+
function orderEventRow(event) {
|
|
789
|
+
return {
|
|
790
|
+
id: require('crypto').randomUUID(),
|
|
791
|
+
event_type: event.type,
|
|
792
|
+
dedupe_key: String(event.dedupeKey).slice(0, ORDER_EVENT_DEDUPE_KEY_MAX_LENGTH),
|
|
793
|
+
order_id: event.orderId || null,
|
|
794
|
+
payload: JSON.stringify(event.payload || {}),
|
|
795
|
+
actor: event.actor || '',
|
|
796
|
+
actor_type: event.actorType,
|
|
797
|
+
// Logged; a dispatcher claims the rows it delivers itself.
|
|
798
|
+
status: 'recorded',
|
|
799
|
+
};
|
|
800
|
+
}`
|
|
801
|
+
|
|
802
|
+
if (dsType === 'supabase') {
|
|
803
|
+
return `${row}
|
|
804
|
+
|
|
805
|
+
async function recordOrderEvent(event) {
|
|
806
|
+
var result = await getClient()
|
|
807
|
+
.from(ORDER_EVENTS_TABLE)
|
|
808
|
+
.upsert(orderEventRow(event), { onConflict: 'dedupe_key', ignoreDuplicates: true });
|
|
809
|
+
if (result.error) throw new Error(result.error.message);
|
|
810
|
+
}`
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
if (dsType === 'mysql' || dsType === 'mariadb' || dsType === 'tidb') {
|
|
814
|
+
return `${row}
|
|
815
|
+
|
|
816
|
+
async function recordOrderEvent(event) {
|
|
817
|
+
var record = orderEventRow(event);
|
|
818
|
+
var columns = Object.keys(record);
|
|
819
|
+
var placeholders = columns.map(function() { return '?'; }).join(', ');
|
|
820
|
+
await getClient().execute(
|
|
821
|
+
'INSERT INTO ' + ORDER_EVENTS_TABLE + ' (' + columns.join(', ') + ') VALUES (' + placeholders + ') ON DUPLICATE KEY UPDATE dedupe_key = dedupe_key',
|
|
822
|
+
columns.map(function(k) { return record[k]; })
|
|
823
|
+
);
|
|
824
|
+
}`
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
return `${row}
|
|
828
|
+
|
|
829
|
+
async function recordOrderEvent(event) {
|
|
830
|
+
var record = orderEventRow(event);
|
|
831
|
+
var columns = Object.keys(record);
|
|
832
|
+
var placeholders = columns.map(function(_, i) { return '$' + (i + 1); }).join(', ');
|
|
833
|
+
await getClient().query(
|
|
834
|
+
'INSERT INTO ' + ORDER_EVENTS_TABLE + ' (' + columns.join(', ') + ') VALUES (' + placeholders + ') ON CONFLICT (dedupe_key) DO NOTHING',
|
|
835
|
+
columns.map(function(k) { return record[k]; })
|
|
836
|
+
);
|
|
837
|
+
}`
|
|
838
|
+
}
|
|
839
|
+
|
|
722
840
|
export const getRecordMappingCode = (dsType?: string): string => `
|
|
723
841
|
function mapInvoiceToRecord(data) {
|
|
724
842
|
return {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { UIDLInvoiceEmailDelivery } from '@teleporthq/teleport-types'
|
|
2
|
+
import { EmailSender } from '@teleporthq/teleport-shared'
|
|
2
3
|
|
|
3
4
|
export const generateEmailSenderCode = (emailDelivery: UIDLInvoiceEmailDelivery): string => {
|
|
4
5
|
if (!emailDelivery || !emailDelivery.enabled || !emailDelivery.provider) {
|
|
@@ -33,6 +34,8 @@ var BODY_TEMPLATE = ${JSON.stringify(bodyTemplate)};
|
|
|
33
34
|
// Per-language copies of the subject/body (main language excluded), chosen by
|
|
34
35
|
// the locale the generate route resolved for the invoice (the order's).
|
|
35
36
|
var LOCALIZED_TEMPLATES = ${JSON.stringify(localizedTemplates)};
|
|
37
|
+
${EmailSender.generateEmailSenderHelperCode()}
|
|
38
|
+
var SENDER = __emailSender(${JSON.stringify({ from: fromEmail, name: fromName })});
|
|
36
39
|
|
|
37
40
|
function resolveSecretKey(keyName) {
|
|
38
41
|
var envName = SECRET_KEYS[keyName];
|
|
@@ -52,7 +55,8 @@ function buildEmailData(invoiceData) {
|
|
|
52
55
|
subject: replacePlaceholders(copy.subject, data),
|
|
53
56
|
body: replacePlaceholders(copy.body, data),
|
|
54
57
|
to: invoiceData.customerEmail || '',
|
|
55
|
-
from:
|
|
58
|
+
from: SENDER.header,
|
|
59
|
+
sender: SENDER,
|
|
56
60
|
templateData: data,
|
|
57
61
|
};
|
|
58
62
|
}
|
|
@@ -63,7 +67,9 @@ function attachmentName(invoiceData) {
|
|
|
63
67
|
|
|
64
68
|
${generateProviderSendFunction(emailDelivery.provider)}
|
|
65
69
|
|
|
66
|
-
|
|
70
|
+
// sourceRef names the route that sent it in the ledger: the payment's
|
|
71
|
+
// generate route unless the caller says otherwise.
|
|
72
|
+
async function sendInvoiceEmail(invoiceData, pdfBuffer, sourceRef) {
|
|
67
73
|
var emailData = buildEmailData(invoiceData);
|
|
68
74
|
if (!emailData.to) return { success: false, error: 'No customer email provided' };
|
|
69
75
|
var result;
|
|
@@ -77,6 +83,7 @@ async function sendInvoiceEmail(invoiceData, pdfBuffer) {
|
|
|
77
83
|
audience: 'customer',
|
|
78
84
|
to: emailData.to,
|
|
79
85
|
from: emailData.from,
|
|
86
|
+
fromName: emailData.sender.name,
|
|
80
87
|
subject: emailData.subject,
|
|
81
88
|
html: emailData.body,
|
|
82
89
|
payload: emailData.templateData,
|
|
@@ -91,7 +98,7 @@ async function sendInvoiceEmail(invoiceData, pdfBuffer) {
|
|
|
91
98
|
status: result && result.success ? 'sent' : 'failed',
|
|
92
99
|
error: result && !result.success ? result.error : null,
|
|
93
100
|
source: 'invoice',
|
|
94
|
-
sourceRef: 'api/invoices/generate',
|
|
101
|
+
sourceRef: sourceRef || 'api/invoices/generate',
|
|
95
102
|
orderId: invoiceData.orderId,
|
|
96
103
|
referenceType: 'invoice',
|
|
97
104
|
referenceId: invoiceData.id,
|
|
@@ -139,9 +146,12 @@ async function sendInvoiceEmailWithProvider(emailData, invoiceData, pdfBuffer) {
|
|
|
139
146
|
var apiKey = resolveSecretKey('apiKey');
|
|
140
147
|
if (!apiKey) return { success: false, error: 'SendGrid API key not configured' };
|
|
141
148
|
sgMail.setApiKey(apiKey);
|
|
149
|
+
// SendGrid reads a "Name <email>" string's name verbatim, quotes included,
|
|
150
|
+
// so a named sender goes as its two parts.
|
|
151
|
+
var sender = emailData.sender;
|
|
142
152
|
var msg = {
|
|
143
153
|
to: emailData.to,
|
|
144
|
-
from:
|
|
154
|
+
from: sender.name ? { email: sender.email, name: sender.name } : sender.email,
|
|
145
155
|
subject: emailData.subject,
|
|
146
156
|
html: emailData.body,
|
|
147
157
|
attachments: [{
|
|
@@ -217,26 +227,6 @@ async function sendInvoiceEmailWithProvider(emailData, invoiceData, pdfBuffer) {
|
|
|
217
227
|
|
|
218
228
|
case 'mailersend':
|
|
219
229
|
return `
|
|
220
|
-
// Pulls "name" and "email" out of the RFC-5322-ish \`from\` string the
|
|
221
|
-
// builder assembles. Three shapes in the wild:
|
|
222
|
-
// 1. "Acme Billing <billing@acme.com>" → name="Acme Billing", email="billing@acme.com"
|
|
223
|
-
// 2. "billing@acme.com" → name="", email="billing@acme.com"
|
|
224
|
-
// 3. "" → {}, caller handles the fallback
|
|
225
|
-
// The old implementation used naive \`.replace()\` chains that produced
|
|
226
|
-
// \`name="billing@acme.com"\` in case (2) — MailerSend accepts that but the
|
|
227
|
-
// email then appears in the recipient's client with itself as the sender
|
|
228
|
-
// name, which is ugly. Kept as a pure helper (no fallbacks baked in) so
|
|
229
|
-
// the caller decides what to do when a field is missing.
|
|
230
|
-
function parseFromString(from) {
|
|
231
|
-
var str = String(from || '').trim();
|
|
232
|
-
if (!str) return { name: '', email: '' };
|
|
233
|
-
var m = /^(.*)<([^<>]+)>\\s*$/.exec(str);
|
|
234
|
-
if (m) {
|
|
235
|
-
return { name: m[1].trim(), email: m[2].trim() };
|
|
236
|
-
}
|
|
237
|
-
return { name: '', email: str };
|
|
238
|
-
}
|
|
239
|
-
|
|
240
230
|
async function sendInvoiceEmailWithProvider(emailData, invoiceData, pdfBuffer) {
|
|
241
231
|
var invoiceNumber = invoiceData && invoiceData.invoiceNumber ? invoiceData.invoiceNumber : '(no number)';
|
|
242
232
|
try {
|
|
@@ -256,7 +246,7 @@ async function sendInvoiceEmailWithProvider(emailData, invoiceData, pdfBuffer) {
|
|
|
256
246
|
|
|
257
247
|
var mailerSend = new MailerSend({ apiKey: apiKey });
|
|
258
248
|
|
|
259
|
-
var parsedFrom =
|
|
249
|
+
var parsedFrom = emailData.sender;
|
|
260
250
|
if (!parsedFrom.email) {
|
|
261
251
|
console.error('[invoice-email][mailersend] skip — from address is empty (set invoiceSettings.emailDelivery.fromEmail)');
|
|
262
252
|
return { success: false, error: 'Sender email not configured' };
|
package/src/invoice/index.ts
CHANGED