@teleporthq/teleport-plugin-next-workflows 0.44.5 → 0.44.7
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__/credential-columns-reply.test.ts +286 -0
- package/__tests__/invoice-paid-status.test.ts +382 -0
- package/__tests__/invoice-tender.test.ts +5 -3
- package/dist/cjs/api-route-generator.d.ts.map +1 -1
- package/dist/cjs/api-route-generator.js +3 -2
- package/dist/cjs/api-route-generator.js.map +1 -1
- package/dist/cjs/credential-columns-code.d.ts +17 -0
- package/dist/cjs/credential-columns-code.d.ts.map +1 -0
- package/dist/cjs/credential-columns-code.js +61 -0
- package/dist/cjs/credential-columns-code.js.map +1 -0
- package/dist/cjs/data-api-route-generator.d.ts +1 -0
- package/dist/cjs/data-api-route-generator.d.ts.map +1 -1
- package/dist/cjs/data-api-route-generator.js +13 -5
- package/dist/cjs/data-api-route-generator.js.map +1 -1
- package/dist/cjs/executor-generator.d.ts.map +1 -1
- package/dist/cjs/executor-generator.js +12 -2
- package/dist/cjs/executor-generator.js.map +1 -1
- package/dist/cjs/invoice/invoice-assembly-code.d.ts.map +1 -1
- package/dist/cjs/invoice/invoice-assembly-code.js +19 -2
- 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 +100 -71
- package/dist/cjs/invoice/invoice-html-code.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/api-route-generator.d.ts.map +1 -1
- package/dist/esm/api-route-generator.js +3 -2
- package/dist/esm/api-route-generator.js.map +1 -1
- package/dist/esm/credential-columns-code.d.ts +17 -0
- package/dist/esm/credential-columns-code.d.ts.map +1 -0
- package/dist/esm/credential-columns-code.js +57 -0
- package/dist/esm/credential-columns-code.js.map +1 -0
- package/dist/esm/data-api-route-generator.d.ts +1 -0
- package/dist/esm/data-api-route-generator.d.ts.map +1 -1
- package/dist/esm/data-api-route-generator.js +11 -3
- package/dist/esm/data-api-route-generator.js.map +1 -1
- package/dist/esm/executor-generator.d.ts.map +1 -1
- package/dist/esm/executor-generator.js +12 -2
- package/dist/esm/executor-generator.js.map +1 -1
- package/dist/esm/invoice/invoice-assembly-code.d.ts.map +1 -1
- package/dist/esm/invoice/invoice-assembly-code.js +19 -2
- 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 +100 -71
- package/dist/esm/invoice/invoice-html-code.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/src/api-route-generator.ts +3 -2
- package/src/credential-columns-code.ts +59 -0
- package/src/data-api-route-generator.ts +11 -3
- package/src/executor-generator.ts +12 -2
- package/src/invoice/invoice-assembly-code.ts +19 -2
- package/src/invoice/invoice-html-code.ts +100 -71
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@teleporthq/teleport-plugin-next-workflows",
|
|
3
|
-
"version": "0.44.
|
|
3
|
+
"version": "0.44.7",
|
|
4
4
|
"description": "A plugin for workflow code generation in Next.js projects.",
|
|
5
5
|
"author": "teleportHQ",
|
|
6
6
|
"license": "MIT",
|
|
@@ -66,8 +66,8 @@
|
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
68
68
|
"@babel/types": "^7.5.5",
|
|
69
|
-
"@teleporthq/teleport-shared": "^0.44.
|
|
69
|
+
"@teleporthq/teleport-shared": "^0.44.7",
|
|
70
70
|
"@teleporthq/teleport-types": "^0.44.0"
|
|
71
71
|
},
|
|
72
|
-
"gitHead": "
|
|
72
|
+
"gitHead": "a279c1bd1e268a8b7302e2c4fafd45748a1d242a"
|
|
73
73
|
}
|
|
@@ -527,11 +527,12 @@ function __isStreamingAI(node, config) {
|
|
|
527
527
|
return config && config.streaming && __streamingAITypes.indexOf(node.type) >= 0;
|
|
528
528
|
}
|
|
529
529
|
|
|
530
|
-
// One node's result to the browser — never one kept on the server
|
|
530
|
+
// One node's result to the browser — never one kept on the server, and a data
|
|
531
|
+
// node's rows without the users table's credentials (see segmentReply).
|
|
531
532
|
function __streamNodeResult(res, nodeId, result) {
|
|
532
533
|
const kept = SEGMENT_CONFIG.nodes.find(function(nd) { return nd.id === nodeId; });
|
|
533
534
|
if (kept && kept.config && kept.config.keepResultOnServer === true && !utils.isBranchingNodeType(kept.type)) return;
|
|
534
|
-
res.write('data: ' + JSON.stringify({ type: 'node-result', nodeId: nodeId, result: result }) + '\\n\\n');
|
|
535
|
+
res.write('data: ' + JSON.stringify({ type: 'node-result', nodeId: nodeId, result: utils.withoutCredentialColumns(kept, result) }) + '\\n\\n');
|
|
535
536
|
}
|
|
536
537
|
|
|
537
538
|
function __collectSegBranchNodes(startId, excludeId) {
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { TableAccess } from '@teleporthq/teleport-shared'
|
|
2
|
+
import { DATA_NODE_TYPES } from './data-api-route-generator'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Runtime helpers that leave the auth users table's credential columns out of
|
|
6
|
+
* the rows a server segment answers the browser with — the columns
|
|
7
|
+
* `TableAccess.AUTH_CREDENTIAL_COLUMNS` names, the ones every browser read of
|
|
8
|
+
* the data route is already served without. A data node of a server segment
|
|
9
|
+
* reads through the data route as the app's own server code, so its rows come
|
|
10
|
+
* back whole, and the segment's reply (or a streamed node result) is the
|
|
11
|
+
* browser's copy of them.
|
|
12
|
+
*
|
|
13
|
+
* Only the results of the nodes that read rows (`DATA_NODE_TYPES`) are
|
|
14
|
+
* touched: all the credentials of a row of the users table, the unambiguous
|
|
15
|
+
* ones of a raw statement's row, whose table is not known. A row is the
|
|
16
|
+
* result itself (a create or update spreads its row into it), an object it
|
|
17
|
+
* holds, or an element of an array it holds; column values are not walked.
|
|
18
|
+
*/
|
|
19
|
+
export const generateCredentialColumnsCode = (): string => `
|
|
20
|
+
var __CREDENTIAL_COLUMNS = ${JSON.stringify(TableAccess.AUTH_CREDENTIAL_COLUMNS)};
|
|
21
|
+
var __UNAMBIGUOUS_CREDENTIAL_COLUMNS = ${JSON.stringify(
|
|
22
|
+
TableAccess.UNAMBIGUOUS_CREDENTIAL_COLUMNS
|
|
23
|
+
)};
|
|
24
|
+
var __CREDENTIAL_TABLES = ${JSON.stringify(TableAccess.AUTH_CREDENTIAL_TABLES)};
|
|
25
|
+
var __ROW_NODE_TYPES = ${JSON.stringify(Array.from(DATA_NODE_TYPES))};
|
|
26
|
+
|
|
27
|
+
function credentialColumnsOf(node) {
|
|
28
|
+
if (!node || __ROW_NODE_TYPES.indexOf(node.type) === -1) return [];
|
|
29
|
+
var tableName = node.config && typeof node.config.tableName === 'string' ? node.config.tableName : '';
|
|
30
|
+
if (!tableName) return __UNAMBIGUOUS_CREDENTIAL_COLUMNS;
|
|
31
|
+
var parts = tableName.trim().toLowerCase().replace(/"/g, '').split('.');
|
|
32
|
+
return __CREDENTIAL_TABLES.indexOf(parts[parts.length - 1]) !== -1 ? __CREDENTIAL_COLUMNS : [];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function withoutColumns(row, columns) {
|
|
36
|
+
if (!row || typeof row !== 'object' || Array.isArray(row)) return row;
|
|
37
|
+
var kept = {};
|
|
38
|
+
Object.keys(row).forEach(function(key) {
|
|
39
|
+
if (columns.indexOf(String(key).toLowerCase()) === -1) kept[key] = row[key];
|
|
40
|
+
});
|
|
41
|
+
return kept;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function withoutRowsColumns(value, columns) {
|
|
45
|
+
if (Array.isArray(value)) return value.map(function(row) { return withoutColumns(row, columns); });
|
|
46
|
+
return withoutColumns(value, columns);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function withoutCredentialColumns(node, result) {
|
|
50
|
+
var columns = credentialColumnsOf(node);
|
|
51
|
+
if (columns.length === 0 || !result || typeof result !== 'object') return result;
|
|
52
|
+
if (Array.isArray(result)) return withoutRowsColumns(result, columns);
|
|
53
|
+
var kept = withoutColumns(result, columns);
|
|
54
|
+
Object.keys(kept).forEach(function(key) {
|
|
55
|
+
if (kept[key] && typeof kept[key] === 'object') kept[key] = withoutRowsColumns(kept[key], columns);
|
|
56
|
+
});
|
|
57
|
+
return kept;
|
|
58
|
+
}
|
|
59
|
+
`
|
|
@@ -15,7 +15,7 @@ import { generateSqlValidatorCode } from './sql-validator'
|
|
|
15
15
|
import { generateCommonJsSessionTokenResolverCode } from './session-cookie-resolver'
|
|
16
16
|
import { ContentTables, TableAccess } from '@teleporthq/teleport-shared'
|
|
17
17
|
|
|
18
|
-
const DATA_NODE_TYPES = new Set([
|
|
18
|
+
export const DATA_NODE_TYPES: ReadonlySet<string> = new Set([
|
|
19
19
|
'data-select',
|
|
20
20
|
'data-create-item',
|
|
21
21
|
'data-update-item',
|
|
@@ -677,6 +677,13 @@ function dropMissingOptionalColumns(tableName, entries, colTypes) {
|
|
|
677
677
|
return kept;
|
|
678
678
|
}
|
|
679
679
|
|
|
680
|
+
// The row a create or update answers with, whoever asked, without the auth
|
|
681
|
+
// users table's credentials (the columns a browser read is served without):
|
|
682
|
+
// a server-side caller hands that answer on to the browser as its result.
|
|
683
|
+
function withoutCredentialColumns(row, tableName) {
|
|
684
|
+
return row ? __taWithoutCredentials([row], tableName)[0] : row;
|
|
685
|
+
}
|
|
686
|
+
|
|
680
687
|
async function handleCreate(client, body) {
|
|
681
688
|
var tableName = body.tableName;
|
|
682
689
|
var columnMappings = body.columnMappings || {};
|
|
@@ -751,7 +758,7 @@ async function handleCreate(client, body) {
|
|
|
751
758
|
var item = result.rows && result.rows[0] ? result.rows[0] : null;
|
|
752
759
|
var idIdx = columns.indexOf('id');
|
|
753
760
|
var resolvedId = item ? (item.id || null) : (idIdx >= 0 ? values[idIdx] : null);
|
|
754
|
-
return { item: item, id: resolvedId };
|
|
761
|
+
return { item: withoutCredentialColumns(item, tableName), id: resolvedId };
|
|
755
762
|
}
|
|
756
763
|
|
|
757
764
|
async function handleUpdate(client, body) {
|
|
@@ -803,7 +810,8 @@ async function handleUpdate(client, body) {
|
|
|
803
810
|
sql += ' RETURNING *';
|
|
804
811
|
|
|
805
812
|
var result = await safeQuery(client, sql, queryParams, 'update');
|
|
806
|
-
|
|
813
|
+
var updated = result.rows[0] || null;
|
|
814
|
+
return { updatedCount: result.rowCount || 0, item: withoutCredentialColumns(updated, tableName), id: (updated && updated.id) || null };
|
|
807
815
|
}
|
|
808
816
|
|
|
809
817
|
async function handleDelete(client, body) {
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
generateWorkflowLocaleHelpersCode,
|
|
5
5
|
} from './email-locale'
|
|
6
6
|
import { generateSignedInDestinationCode } from './auth-return/signed-in-destination-code'
|
|
7
|
+
import { generateCredentialColumnsCode } from './credential-columns-code'
|
|
7
8
|
|
|
8
9
|
export interface SharedRuntimeUtilsOptions {
|
|
9
10
|
/**
|
|
@@ -1414,6 +1415,10 @@ var SEGMENT_REPLY_PRIVATE_KEYS = {
|
|
|
1414
1415
|
// nodes after it in the same segment and never leaves the server (a statement's
|
|
1415
1416
|
// rows read only to build what the browser may see, a sender's recipients).
|
|
1416
1417
|
// Nothing in a later segment can read it.
|
|
1418
|
+
//
|
|
1419
|
+
// A data node's rows are replied without the auth users table's credentials
|
|
1420
|
+
// (see credential-columns-code.ts).
|
|
1421
|
+
${generateCredentialColumnsCode()}
|
|
1417
1422
|
// The nodes whose result the client branches on: never kept on the server.
|
|
1418
1423
|
function isBranchingNodeType(type) {
|
|
1419
1424
|
return type === 'general-if-statement' || type === 'general-switch';
|
|
@@ -1425,7 +1430,7 @@ function segmentReply(segmentConfig, context, snapshot) {
|
|
|
1425
1430
|
var kept = {};
|
|
1426
1431
|
var nodes = (segmentConfig && segmentConfig.nodes) || [];
|
|
1427
1432
|
for (var ni = 0; ni < nodes.length; ni++) {
|
|
1428
|
-
own[nodes[ni].id] =
|
|
1433
|
+
own[nodes[ni].id] = nodes[ni];
|
|
1429
1434
|
if (nodes[ni].config && nodes[ni].config.keepResultOnServer === true && !isBranchingNodeType(nodes[ni].type)) {
|
|
1430
1435
|
kept[nodes[ni].id] = true;
|
|
1431
1436
|
}
|
|
@@ -1436,7 +1441,11 @@ function segmentReply(segmentConfig, context, snapshot) {
|
|
|
1436
1441
|
if (SEGMENT_REPLY_PRIVATE_KEYS[key] || kept[key]) continue;
|
|
1437
1442
|
var value = context[key];
|
|
1438
1443
|
if (value === undefined) continue;
|
|
1439
|
-
if (own[key]
|
|
1444
|
+
if (own[key]) {
|
|
1445
|
+
reply[key] = withoutCredentialColumns(own[key], value);
|
|
1446
|
+
continue;
|
|
1447
|
+
}
|
|
1448
|
+
if (!snapshot || !Object.prototype.hasOwnProperty.call(snapshot, key)) {
|
|
1440
1449
|
reply[key] = value;
|
|
1441
1450
|
continue;
|
|
1442
1451
|
}
|
|
@@ -1504,6 +1513,7 @@ module.exports = {
|
|
|
1504
1513
|
loopIterationContext,
|
|
1505
1514
|
snapshotIncomingContext,
|
|
1506
1515
|
segmentReply,
|
|
1516
|
+
withoutCredentialColumns,
|
|
1507
1517
|
evaluateCondition,
|
|
1508
1518
|
evaluateSingleComparison,
|
|
1509
1519
|
normalizeComparisonOperator,
|
|
@@ -93,6 +93,16 @@ var CURRENCY_SYMBOLS = {
|
|
|
93
93
|
'CLP': 'CLP$', 'PEN': 'S/.', 'VND': '\\u20AB', 'UAH': '\\u20B4',
|
|
94
94
|
};
|
|
95
95
|
|
|
96
|
+
// \`teleport_orders.payment_status\` values that mean the order's money was
|
|
97
|
+
// collected — the editor's PAID_PAYMENT_STATUS_VALUES (teleport-gui
|
|
98
|
+
// e-commerce/constants/order-statuses.ts), provider spellings included.
|
|
99
|
+
var PAID_ORDER_PAYMENT_STATUSES = ['paid', 'partially_refunded', 'succeeded', 'success', 'completed', 'captured'];
|
|
100
|
+
|
|
101
|
+
function isPaidOrder(order) {
|
|
102
|
+
var paymentStatus = String((order && order.payment_status) || '').trim().toLowerCase();
|
|
103
|
+
return PAID_ORDER_PAYMENT_STATUSES.indexOf(paymentStatus) !== -1;
|
|
104
|
+
}
|
|
105
|
+
|
|
96
106
|
function isLoopbackHost(host) {
|
|
97
107
|
var name = String(host || '').replace(/:[0-9]+$/, '').toLowerCase();
|
|
98
108
|
return name === 'localhost' || name === '127.0.0.1' || name === '[::1]';
|
|
@@ -473,15 +483,22 @@ function assembleInvoiceData(body, hydratedOrder, hydratedItems) {
|
|
|
473
483
|
var fallbackPaymentIntentId = orderRow.payment_intent_id || '';
|
|
474
484
|
var fallbackNotes = orderRow.notes || '';
|
|
475
485
|
|
|
486
|
+
// Paid or not, when the caller does not say: the order's money state. The
|
|
487
|
+
// payment webhook marks an order paid and then asks for its invoice with just
|
|
488
|
+
// the order id; a cash-on-delivery order is invoiced unpaid when it is placed.
|
|
489
|
+
// The orders table keeps no payment time of its own — the mark-paid write
|
|
490
|
+
// stamps \`updated_at\`, just before the webhook asks.
|
|
491
|
+
var paidByOrder = !body.status && isPaidOrder(orderRow);
|
|
492
|
+
|
|
476
493
|
var invoiceData = {
|
|
477
494
|
id: body.id || require('crypto').randomUUID(),
|
|
478
495
|
// Numbered by the route: an invoice issued at payment takes the next
|
|
479
496
|
// number, a regenerated one keeps its own.
|
|
480
497
|
invoiceNumber: '',
|
|
481
|
-
status: body.status || 'issued',
|
|
498
|
+
status: body.status || (paidByOrder ? 'paid' : 'issued'),
|
|
482
499
|
issueDate: issueDate,
|
|
483
500
|
dueDate: dueDate,
|
|
484
|
-
paidAt: body.paidAt || null,
|
|
501
|
+
paidAt: body.paidAt || (paidByOrder ? orderRow.updated_at || new Date().toISOString() : null),
|
|
485
502
|
customerName: body.customerName || fallbackCustomerName || '',
|
|
486
503
|
customerEmail: body.customerEmail || fallbackCustomerEmail || '',
|
|
487
504
|
customerAddress: body.customerAddress || fallbackCustomerAddress || '',
|
|
@@ -12,12 +12,12 @@ import { UIDLInvoiceSettings } from '@teleporthq/teleport-types'
|
|
|
12
12
|
//
|
|
13
13
|
// The UIDL tree has several features that need runtime handling:
|
|
14
14
|
//
|
|
15
|
-
// 1. `<context>` elements carry
|
|
16
|
-
// (Company, Invoice, Customer, Products, Payment).
|
|
17
|
-
// that data through expressions like
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
15
|
+
// 1. `<context>` elements carry the design canvas's sample invoice on
|
|
16
|
+
// their attributes (Company, Invoice, Customer, Products, Payment).
|
|
17
|
+
// Children reference that data through expressions like
|
|
18
|
+
// `invoiceData?.Company?.name`. Every render builds the scope from the
|
|
19
|
+
// real invoice alone, so every expression resolves against the buyer's
|
|
20
|
+
// actual order and an empty field prints nothing — never the sample.
|
|
21
21
|
//
|
|
22
22
|
// 2. `cms-list-repeater` iterates `invoiceData.Products` with a
|
|
23
23
|
// per-iteration variable (the `renderPropIdentifier`). Each line item
|
|
@@ -153,11 +153,12 @@ var COMPONENT_DEFINITIONS = ${componentDefinitionsJson};
|
|
|
153
153
|
var MAX_COMPONENT_DEPTH = 12;
|
|
154
154
|
|
|
155
155
|
// ---------------------------------------------------------------------------
|
|
156
|
-
// Scope construction —
|
|
157
|
-
// the
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
//
|
|
156
|
+
// Scope construction — the real invoice, the store's company details and the
|
|
157
|
+
// buyer, never the sample on the template's <context>: an empty notes field,
|
|
158
|
+
// VAT number or paid date printed the design canvas's "Payment is due within
|
|
159
|
+
// 30 days", "US123456789" or "Jan 20, 2025" on a real invoice. The one value
|
|
160
|
+
// read from the context is the company logo URL, which the editor writes there
|
|
161
|
+
// from the store's own logo (the sample's is empty).
|
|
161
162
|
// ---------------------------------------------------------------------------
|
|
162
163
|
|
|
163
164
|
function extractStaticAttrsFromContext(uidl) {
|
|
@@ -194,23 +195,9 @@ function findFirstContextNode(node) {
|
|
|
194
195
|
return null;
|
|
195
196
|
}
|
|
196
197
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
if (typeof v === 'number') return !isNaN(v);
|
|
201
|
-
return true;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
function mergeWithDefaults(primary, defaults) {
|
|
205
|
-
var out = Object.assign({}, defaults || {});
|
|
206
|
-
if (primary) {
|
|
207
|
-
Object.keys(primary).forEach(function (k) {
|
|
208
|
-
if (nonEmpty(primary[k])) out[k] = primary[k];
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
return out;
|
|
212
|
-
}
|
|
213
|
-
|
|
198
|
+
// Money on the invoice, in the PDF and the email alike. A symbol that ends in
|
|
199
|
+
// a letter is a word ("lei", "CHF", "kr") and stands apart from the number
|
|
200
|
+
// ("lei 119.00"); a sign sits against it ("$119.00", "\u20AC119.00").
|
|
214
201
|
function formatCurrencyValue(amount, sym, decimals) {
|
|
215
202
|
var num = Number(amount);
|
|
216
203
|
if (!isFinite(num)) num = 0;
|
|
@@ -218,7 +205,17 @@ function formatCurrencyValue(amount, sym, decimals) {
|
|
|
218
205
|
var fixed = num.toFixed(dec);
|
|
219
206
|
var parts = fixed.split('.');
|
|
220
207
|
parts[0] = parts[0].replace(/\\B(?=(\\d{3})+(?!\\d))/g, ',');
|
|
221
|
-
|
|
208
|
+
var symbol = String(sym == null ? '' : sym).trim();
|
|
209
|
+
var separator = /\\p{L}$/u.test(symbol) ? ' ' : '';
|
|
210
|
+
return symbol + separator + parts.join('.');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Invoice statuses of money that was collected: a refund later moves a paid
|
|
214
|
+
// invoice to one of the last two, and it was paid all the same.
|
|
215
|
+
var SETTLED_INVOICE_STATUSES = ['paid', 'partially_refunded', 'refunded'];
|
|
216
|
+
|
|
217
|
+
function isSettledInvoice(status) {
|
|
218
|
+
return SETTLED_INVOICE_STATUSES.indexOf(String(status || '').toLowerCase()) !== -1;
|
|
222
219
|
}
|
|
223
220
|
|
|
224
221
|
// Per-line VAT formulas. These MIRROR the GUI helpers in
|
|
@@ -269,7 +266,7 @@ function formatDateValue(raw) {
|
|
|
269
266
|
}
|
|
270
267
|
|
|
271
268
|
function buildInvoiceDataScope(invoiceData) {
|
|
272
|
-
var
|
|
269
|
+
var contextCompany = extractStaticAttrsFromContext(TEMPLATE_COMPONENT_UIDL).Company || {};
|
|
273
270
|
var sym = invoiceData.currencySymbol || '$';
|
|
274
271
|
|
|
275
272
|
var realCompany = {
|
|
@@ -283,7 +280,7 @@ function buildInvoiceDataScope(invoiceData) {
|
|
|
283
280
|
registrationNumber: COMPANY_DETAILS.companyRegNumber,
|
|
284
281
|
email: COMPANY_DETAILS.companyEmail,
|
|
285
282
|
phone: COMPANY_DETAILS.companyPhone,
|
|
286
|
-
logoUrl: invoiceData.companyLogoUrl || '',
|
|
283
|
+
logoUrl: invoiceData.companyLogoUrl || contextCompany.logoUrl || '',
|
|
287
284
|
website: COMPANY_DETAILS.companyWebsite,
|
|
288
285
|
};
|
|
289
286
|
|
|
@@ -319,12 +316,20 @@ function buildInvoiceDataScope(invoiceData) {
|
|
|
319
316
|
amountDueNumber = Math.max(0, totalNumber - giftCardAmountNumber);
|
|
320
317
|
}
|
|
321
318
|
|
|
319
|
+
// A paid invoice asks for nothing: where the due date would print, it says
|
|
320
|
+
// when it was paid. The order keeps no payment method finer than its
|
|
321
|
+
// provider, so the line names none.
|
|
322
|
+
var paid = isSettledInvoice(invoiceData.status);
|
|
323
|
+
var paidDate = formatDateValue(invoiceData.paidAt || (paid ? invoiceData.issueDate : ''));
|
|
324
|
+
var paidLine = paid ? 'Paid' + (paidDate ? ' on ' + paidDate : '') : '';
|
|
325
|
+
|
|
322
326
|
var realInvoice = {
|
|
323
327
|
invoiceNumber: invoiceData.invoiceNumber ? '#' + invoiceData.invoiceNumber : '',
|
|
324
328
|
status: invoiceData.status || '',
|
|
325
329
|
issueDate: formatDateValue(invoiceData.issueDate),
|
|
326
|
-
dueDate: formatDateValue(invoiceData.dueDate),
|
|
327
|
-
paidDate:
|
|
330
|
+
dueDate: paidLine || formatDateValue(invoiceData.dueDate),
|
|
331
|
+
paidDate: paidDate,
|
|
332
|
+
paidLine: paidLine,
|
|
328
333
|
subtotal: formatCurrencyValue(invoiceData.subtotal, sym),
|
|
329
334
|
taxRate: invoiceData.taxRate != null ? String(invoiceData.taxRate) : '',
|
|
330
335
|
taxAmount: formatCurrencyValue(invoiceData.taxAmount, sym),
|
|
@@ -351,13 +356,6 @@ function buildInvoiceDataScope(invoiceData) {
|
|
|
351
356
|
notes: invoiceData.notes || '',
|
|
352
357
|
pdfUrl: invoiceData.pdfUrl || '',
|
|
353
358
|
};
|
|
354
|
-
// The merge below lets an EMPTY real value fall back to the template's
|
|
355
|
-
// sample — right for a company address, wrong for a code or a card tail: an
|
|
356
|
-
// order without a voucher must not print the designer's "SAVE20".
|
|
357
|
-
var realInvoiceNeverSampled = { voucherCode: voucherCode, giftCardLast4: giftCardLast4 };
|
|
358
|
-
// The buyer and their payment are never sampled either: an order with no
|
|
359
|
-
// city (a booking, a pickup, a download) must not print the designer's
|
|
360
|
-
// sample customer's, nor a card it was not paid with.
|
|
361
359
|
|
|
362
360
|
var realCustomer = {
|
|
363
361
|
name: invoiceData.customerName || '',
|
|
@@ -470,11 +468,11 @@ function buildInvoiceDataScope(invoiceData) {
|
|
|
470
468
|
|
|
471
469
|
return {
|
|
472
470
|
invoiceData: {
|
|
473
|
-
Company:
|
|
474
|
-
Invoice:
|
|
471
|
+
Company: realCompany,
|
|
472
|
+
Invoice: realInvoice,
|
|
475
473
|
Customer: realCustomer,
|
|
476
474
|
Payment: realPayment,
|
|
477
|
-
Products: realProducts
|
|
475
|
+
Products: realProducts,
|
|
478
476
|
},
|
|
479
477
|
};
|
|
480
478
|
}
|
|
@@ -975,7 +973,12 @@ function buildFallbackInvoiceHtml(scope) {
|
|
|
975
973
|
var parts = [];
|
|
976
974
|
parts.push('<div style="padding:32px;font-family:Helvetica,Arial,sans-serif;color:#333;font-size:10pt;">');
|
|
977
975
|
parts.push('<h1 style="text-align:right;font-size:24pt;margin:0 0 4px;">INVOICE</h1>');
|
|
978
|
-
|
|
976
|
+
if (inv.paidLine) {
|
|
977
|
+
parts.push('<p style="text-align:right;color:#666;margin:0 0 4px;">' + escapeHtml(inv.invoiceNumber || '') + '</p>');
|
|
978
|
+
parts.push('<p style="text-align:right;color:#666;margin:0 0 24px;">' + escapeHtml(inv.paidLine) + '</p>');
|
|
979
|
+
} else {
|
|
980
|
+
parts.push('<p style="text-align:right;color:#666;margin:0 0 24px;">' + escapeHtml(inv.invoiceNumber || '') + '</p>');
|
|
981
|
+
}
|
|
979
982
|
parts.push('<div style="display:flex;gap:40px;margin-bottom:24px;">');
|
|
980
983
|
parts.push('<div style="flex:1;"><strong>' + escapeHtml(comp.name || '') + '</strong><br/>' +
|
|
981
984
|
escapeHtml(comp.address || '') + '<br/>' +
|
|
@@ -1074,25 +1077,47 @@ function resolveTextSpans(spans, scope) {
|
|
|
1074
1077
|
}).join('');
|
|
1075
1078
|
}
|
|
1076
1079
|
|
|
1077
|
-
//
|
|
1078
|
-
// pickup order, "Discount −$0.00" without a
|
|
1079
|
-
//
|
|
1080
|
-
//
|
|
1081
|
-
//
|
|
1082
|
-
var OPTIONAL_AMOUNT_TOKENS = ['shippingAmount', 'discountAmount', 'giftCardAmount'];
|
|
1083
|
-
|
|
1084
|
-
function
|
|
1085
|
-
var
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1080
|
+
// The summary lines the email leaves out. An amount whose line means nothing at
|
|
1081
|
+
// zero: "Delivery $0.00" on a pickup order, "Discount −$0.00" without a
|
|
1082
|
+
// voucher, "Tax $0.00" in an untaxed store. And what a paid invoice no longer
|
|
1083
|
+
// asks for: its due date and the amount due. As on the PDF, the amount due is
|
|
1084
|
+
// printed only beside a gift card; without one it is the total again.
|
|
1085
|
+
var OPTIONAL_AMOUNT_TOKENS = ['shippingAmount', 'discountAmount', 'taxAmount', 'giftCardAmount'];
|
|
1086
|
+
|
|
1087
|
+
function omittedLineTokens(inv) {
|
|
1088
|
+
var tokens = OPTIONAL_AMOUNT_TOKENS.filter(function (key) {
|
|
1089
|
+
return !(Number(inv[key] || 0) > 0);
|
|
1090
|
+
});
|
|
1091
|
+
var paid = isSettledInvoice(inv.status);
|
|
1092
|
+
if (paid) tokens.push('dueDate');
|
|
1093
|
+
if (paid || !(Number(inv.giftCardAmount || 0) > 0)) tokens.push('amountDue');
|
|
1094
|
+
return tokens;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
// A line is the table row, list item or paragraph carrying the token, matched
|
|
1098
|
+
// innermost — a row inside a row keeps its parent. The editor's designs publish
|
|
1099
|
+
// each line as a <div> of <span>s instead, so a token outside those drops the
|
|
1100
|
+
// innermost <div> holding it, and only when that <div> is the line alone: one
|
|
1101
|
+
// that also holds a block or another token is the merchant's own, and stays.
|
|
1102
|
+
var NO_BLOCK_OR_TOKEN = '(?!<\\\\/?(?:div|p|tr|li|table|ul|ol|h[1-6])\\\\b|\\\\{\\\\{)';
|
|
1103
|
+
|
|
1104
|
+
function stripLinesHolding(template, key) {
|
|
1105
|
+
var token = '\\\\{\\\\{' + key + '\\\\}\\\\}';
|
|
1106
|
+
var row = new RegExp(
|
|
1107
|
+
'<(tr|li|p)\\\\b(?:(?!<\\\\/?\\\\1\\\\b)[\\\\s\\\\S])*?' + token + '(?:(?!<\\\\/?\\\\1\\\\b)[\\\\s\\\\S])*?<\\\\/\\\\1\\\\s*>',
|
|
1108
|
+
'g'
|
|
1109
|
+
);
|
|
1110
|
+
var lineDiv = new RegExp(
|
|
1111
|
+
'<div\\\\b(?:' + NO_BLOCK_OR_TOKEN + '[\\\\s\\\\S])*?' + token + '(?:' + NO_BLOCK_OR_TOKEN + '[\\\\s\\\\S])*?<\\\\/div\\\\s*>',
|
|
1112
|
+
'g'
|
|
1113
|
+
);
|
|
1114
|
+
return template.replace(row, '').replace(lineDiv, '');
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
function stripOmittedLines(template, inv) {
|
|
1118
|
+
return omittedLineTokens(inv).reduce(function (out, key) {
|
|
1119
|
+
return stripLinesHolding(out, key);
|
|
1120
|
+
}, template);
|
|
1096
1121
|
}
|
|
1097
1122
|
|
|
1098
1123
|
function replacePlaceholders(template, data) {
|
|
@@ -1100,7 +1125,8 @@ function replacePlaceholders(template, data) {
|
|
|
1100
1125
|
var inv = data.invoice || {};
|
|
1101
1126
|
var cust = data.customer || {};
|
|
1102
1127
|
var comp = data.company || {};
|
|
1103
|
-
|
|
1128
|
+
var sym = inv.currencySymbol;
|
|
1129
|
+
return stripOmittedLines(template, inv).replace(/\\{\\{(\\w+)\\}\\}/g, function (match, key) {
|
|
1104
1130
|
var map = {
|
|
1105
1131
|
invoiceNumber: inv.number || '',
|
|
1106
1132
|
customerName: cust.name || '',
|
|
@@ -1111,14 +1137,14 @@ function replacePlaceholders(template, data) {
|
|
|
1111
1137
|
// so the mail and its attachment agree.
|
|
1112
1138
|
invoiceDate: formatDateValue(inv.issueDate),
|
|
1113
1139
|
dueDate: formatDateValue(inv.dueDate),
|
|
1114
|
-
totalAmount: (inv.
|
|
1115
|
-
subtotal: (inv.
|
|
1116
|
-
taxAmount: (inv.
|
|
1117
|
-
shippingAmount: (inv.
|
|
1118
|
-
discountAmount: (inv.
|
|
1119
|
-
giftCardAmount: (inv.
|
|
1140
|
+
totalAmount: formatCurrencyValue(inv.total, sym),
|
|
1141
|
+
subtotal: formatCurrencyValue(inv.subtotal, sym),
|
|
1142
|
+
taxAmount: formatCurrencyValue(inv.taxAmount, sym),
|
|
1143
|
+
shippingAmount: formatCurrencyValue(inv.shippingAmount, sym),
|
|
1144
|
+
discountAmount: formatCurrencyValue(inv.discountAmount, sym),
|
|
1145
|
+
giftCardAmount: formatCurrencyValue(inv.giftCardAmount, sym),
|
|
1120
1146
|
// What is left after a gift card; the whole total when none was used.
|
|
1121
|
-
amountDue: (inv.
|
|
1147
|
+
amountDue: formatCurrencyValue(inv.amountDue != null ? inv.amountDue : inv.total, sym),
|
|
1122
1148
|
companyName: comp.name || '',
|
|
1123
1149
|
companyEmail: comp.email || '',
|
|
1124
1150
|
invoiceUrl: inv.pdfUrl || '',
|
|
@@ -1136,6 +1162,9 @@ function buildDataContext(invoiceData) {
|
|
|
1136
1162
|
return {
|
|
1137
1163
|
invoice: {
|
|
1138
1164
|
number: invoiceData.invoiceNumber || '',
|
|
1165
|
+
// Paid or not decides which lines the email keeps (see omittedLineTokens).
|
|
1166
|
+
status: invoiceData.status || '',
|
|
1167
|
+
paidAt: invoiceData.paidAt || null,
|
|
1139
1168
|
issueDate: invoiceData.issueDate || '',
|
|
1140
1169
|
dueDate: invoiceData.dueDate || '',
|
|
1141
1170
|
subtotal: invoiceData.subtotal || 0,
|