@aglyn/plugins-commerce 1.0.0-beta.289 → 1.0.0-beta.290

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.
Files changed (66) hide show
  1. package/README.md +2 -2
  2. package/package.json +13 -13
  3. package/src/lib/components/console/commerce-pdf-download.d.ts +30 -0
  4. package/src/lib/components/console/commerce-pdf-download.js +53 -0
  5. package/src/lib/components/console/commerce-pdf-download.js.map +1 -0
  6. package/src/lib/components/console/host-quotes-card.component.js +20 -4
  7. package/src/lib/components/console/host-quotes-card.component.js.map +1 -1
  8. package/src/lib/components/console/order-invoice.component.d.ts +6 -4
  9. package/src/lib/components/console/order-invoice.component.js +67 -11
  10. package/src/lib/components/console/order-invoice.component.js.map +1 -1
  11. package/src/lib/components/console/quote-editor-dialog.component.js +53 -2
  12. package/src/lib/components/console/quote-editor-dialog.component.js.map +1 -1
  13. package/src/lib/components/console/quote-proofs.component.d.ts +37 -0
  14. package/src/lib/components/console/quote-proofs.component.js +626 -0
  15. package/src/lib/components/console/quote-proofs.component.js.map +1 -0
  16. package/src/lib/components/quote-page-body.js +29 -1
  17. package/src/lib/components/quote-page-body.js.map +1 -1
  18. package/src/lib/components/quote-proof-panel.d.ts +42 -0
  19. package/src/lib/components/quote-proof-panel.js +436 -0
  20. package/src/lib/components/quote-proof-panel.js.map +1 -0
  21. package/src/lib/model/commerce-quote-pay.d.ts +27 -0
  22. package/src/lib/model/commerce-quote-pay.js +36 -3
  23. package/src/lib/model/commerce-quote-pay.js.map +1 -1
  24. package/src/lib/model/commerce-quote-proofs.d.ts +159 -0
  25. package/src/lib/model/commerce-quote-proofs.js +348 -0
  26. package/src/lib/model/commerce-quote-proofs.js.map +1 -0
  27. package/src/lib/model/commerce-quotes.d.ts +59 -3
  28. package/src/lib/model/commerce-quotes.js.map +1 -1
  29. package/src/lib/model/index.d.ts +1 -0
  30. package/src/lib/model/index.js +1 -0
  31. package/src/lib/model/index.js.map +1 -1
  32. package/src/lib/server/commerce-pdf.d.ts +100 -0
  33. package/src/lib/server/commerce-pdf.js +283 -0
  34. package/src/lib/server/commerce-pdf.js.map +1 -0
  35. package/src/lib/server/order-print.d.ts +6 -0
  36. package/src/lib/server/order-print.js +22 -5
  37. package/src/lib/server/order-print.js.map +1 -1
  38. package/src/lib/server/pdf-response.d.ts +31 -0
  39. package/src/lib/server/pdf-response.js +30 -0
  40. package/src/lib/server/pdf-response.js.map +1 -0
  41. package/src/lib/server/pos-reader-splash.d.ts +2 -18
  42. package/src/lib/server/pos-reader-splash.js +8 -93
  43. package/src/lib/server/pos-reader-splash.js.map +1 -1
  44. package/src/lib/server/quote-pay.d.ts +31 -4
  45. package/src/lib/server/quote-pay.js +11 -11
  46. package/src/lib/server/quote-pay.js.map +1 -1
  47. package/src/lib/server/quote-proofs.d.ts +51 -0
  48. package/src/lib/server/quote-proofs.js +500 -0
  49. package/src/lib/server/quote-proofs.js.map +1 -0
  50. package/src/lib/server/quote-uploads.d.ts +33 -0
  51. package/src/lib/server/quote-uploads.js +29 -0
  52. package/src/lib/server/quote-uploads.js.map +1 -0
  53. package/src/lib/server/quote.d.ts +14 -0
  54. package/src/lib/server/quote.js +336 -3
  55. package/src/lib/server/quote.js.map +1 -1
  56. package/src/lib/server/site-image-source.d.ts +38 -0
  57. package/src/lib/server/site-image-source.js +130 -0
  58. package/src/lib/server/site-image-source.js.map +1 -0
  59. package/src/lib/server.js +19 -1
  60. package/src/lib/server.js.map +1 -1
  61. package/src/lib/utils/commerce-document-pdf.d.ts +77 -0
  62. package/src/lib/utils/commerce-document-pdf.js +608 -0
  63. package/src/lib/utils/commerce-document-pdf.js.map +1 -0
  64. package/src/lib/utils/order-invoice.d.ts +52 -3
  65. package/src/lib/utils/order-invoice.js +116 -64
  66. package/src/lib/utils/order-invoice.js.map +1 -1
@@ -0,0 +1,348 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { appendOrderEvent } from "./commerce-orders.js";
18
+ import { formatQuoteNumber, isQuote, isUnpaidQuote } from "./commerce-quotes.js";
19
+ /*==========================================
20
+ * PROOFS, REVISIONS AND THE CUSTOMER'S ANSWER (AGL-4096).
21
+ *
22
+ * A quote's REVISION is the version the customer is looking at. The business
23
+ * attaches proofs to the current revision; the customer approves them under
24
+ * a typed name or asks for changes; the business then starts the next
25
+ * revision, which keeps the one before exactly as it stood in
26
+ * `hosts/{hostId}/orders/{id}/revisions/{n}` and starts the new one with no
27
+ * proofs and no answer. Every revision is kept.
28
+ *
29
+ * Whether paying waits on an approval is the business's control
30
+ * (`requireProofApproval`, AGL-4093) and `quotePaymentBlockedByApproval`
31
+ * answers it; nothing here makes paying wait when it is off.
32
+ *
33
+ * Pure: the console route, the customer's route and (slice 5) the send that
34
+ * starts a revision all call these, and write what they answer.
35
+ *=========================================*/ /** The most proofs one revision holds. */ export const QUOTE_MAX_PROOFS = 10;
36
+ /** The most answers the customer gives one revision; plenty for a change of mind. */ export const QUOTE_MAX_RESPONSES = 20;
37
+ /** The longest comment a customer may leave. */ export const QUOTE_MAX_COMMENT = 5000;
38
+ /** The longest name an approval is signed with. */ export const QUOTE_MAX_APPROVER_NAME = 120;
39
+ /** What the visitor uploads are called for a quote: its proofs, and the customer's artwork. */ export const QUOTE_PROOF_PURPOSE = 'quote-proof';
40
+ export const QUOTE_ARTWORK_PURPOSE = 'quote-artwork';
41
+ /** The current revision's proofs. */ export function currentQuoteProofs(quote) {
42
+ var _ref, _ref1;
43
+ const revision = Number((_ref = quote == null ? void 0 : quote.revision) != null ? _ref : 1);
44
+ return ((_ref1 = quote == null ? void 0 : quote.proofs) != null ? _ref1 : []).filter((proof)=>Number(proof == null ? void 0 : proof.revision) === revision);
45
+ }
46
+ /** The customer's latest answer to the current revision, if they gave one. */ export function latestQuoteResponse(quote) {
47
+ var _ref, _ref1;
48
+ const revision = Number((_ref = quote == null ? void 0 : quote.revision) != null ? _ref : 1);
49
+ const answers = ((_ref1 = quote == null ? void 0 : quote.responses) != null ? _ref1 : []).filter((response)=>Number(response == null ? void 0 : response.revision) === revision);
50
+ return answers.length ? answers[answers.length - 1] : null;
51
+ }
52
+ /** Whether the customer has answered the current revision. */ export function quoteRevisionAnswered(quote) {
53
+ return latestQuoteResponse(quote) !== null;
54
+ }
55
+ /** Why the current revision's proofs cannot be changed now, if they cannot. */ export function quoteProofChangeRefusal(order, change) {
56
+ if (!order || !isQuote(order) || !order.quote) return 'not-a-quote';
57
+ if (!isUnpaidQuote(order)) return 'paid';
58
+ if (quoteRevisionAnswered(order.quote)) return 'answered';
59
+ if (change === 'add' && currentQuoteProofs(order.quote).length >= QUOTE_MAX_PROOFS) return 'full';
60
+ return null;
61
+ }
62
+ export function quoteProofRefusalMessage(reason) {
63
+ switch(reason){
64
+ case 'not-a-quote':
65
+ return 'Unknown quote';
66
+ case 'paid':
67
+ return 'This quote has been paid, so its proofs can no longer be changed.';
68
+ case 'answered':
69
+ return 'The customer has answered this revision. Start a new revision to send different proofs.';
70
+ case 'full':
71
+ return `A revision holds up to ${QUOTE_MAX_PROOFS} proofs. Remove one first.`;
72
+ case 'unknown-proof':
73
+ return 'That proof is no longer on this quote.';
74
+ }
75
+ }
76
+ /** The quote with a proof added to its current revision, edited by `by` at `atMs`. */ export function withQuoteProofAdded(order, proof, options) {
77
+ var _quote_revision;
78
+ const refused = quoteProofChangeRefusal(order, 'add');
79
+ if (refused) return {
80
+ ok: false,
81
+ reason: refused
82
+ };
83
+ const quote = order.quote;
84
+ const added = {
85
+ id: proof.id,
86
+ revision: Number((_quote_revision = quote.revision) != null ? _quote_revision : 1),
87
+ name: proof.name,
88
+ contentType: proof.contentType,
89
+ size: proof.size,
90
+ path: proof.path,
91
+ addedAtMs: options.atMs,
92
+ addedBy: options.by
93
+ };
94
+ return {
95
+ ok: true,
96
+ proof: added,
97
+ order: _extends({}, order, {
98
+ timeline: appendOrderEvent(order, 'quote-proof-added', `Proof ${added.name} added to revision ${added.revision} by ${options.by}`, options.atMs),
99
+ quote: _extends({}, quote, {
100
+ proofs: [
101
+ ...currentQuoteProofs(quote).filter((entry)=>entry.id !== added.id),
102
+ added
103
+ ],
104
+ updatedAtMs: options.atMs,
105
+ updatedBy: options.by
106
+ })
107
+ })
108
+ };
109
+ }
110
+ /** The quote with one of the current revision's proofs taken off. */ export function withQuoteProofRemoved(order, proofId, options) {
111
+ const refused = quoteProofChangeRefusal(order, 'remove');
112
+ if (refused) return {
113
+ ok: false,
114
+ reason: refused
115
+ };
116
+ const quote = order.quote;
117
+ const proofs = currentQuoteProofs(quote);
118
+ const proof = proofs.find((entry)=>entry.id === proofId);
119
+ if (!proof) return {
120
+ ok: false,
121
+ reason: 'unknown-proof'
122
+ };
123
+ return {
124
+ ok: true,
125
+ proof,
126
+ order: _extends({}, order, {
127
+ timeline: appendOrderEvent(order, 'quote-proof-removed', `Proof ${proof.name} removed from revision ${proof.revision} by ${options.by}`, options.atMs),
128
+ quote: _extends({}, quote, {
129
+ proofs: proofs.filter((entry)=>entry.id !== proofId),
130
+ updatedAtMs: options.atMs,
131
+ updatedBy: options.by
132
+ })
133
+ })
134
+ };
135
+ }
136
+ /**
137
+ * Starts the quote's next revision. The current one is SNAPSHOTTED as it
138
+ * stands (its lines, totals, notes, terms, proofs and the customer's
139
+ * answers); the quote moves to revision N+1 with no proofs and no answer,
140
+ * back at Draft, edited by `by` at `atMs`. A paid quote starts none.
141
+ *
142
+ * The caller writes `snapshot` to `hosts/{hostId}/orders/{id}/revisions/{revision}`
143
+ * (`String(snapshot.revision)`, created, never overwritten) and `order` over
144
+ * the quote, in one transaction. The pay link's version is not touched: the
145
+ * send that hands the customer the new revision decides that.
146
+ */ export function startQuoteRevision(order, options) {
147
+ var _quote_revision, _quote_responses, _quote_stage, _order_lineItems, _order_totals, _order_taxMode, _quote_tax, _quote_discount, _quote_shipping, _quote_notes, _quote_terms, _quote_customer, _quote_billTo, _quote_shipTo;
148
+ if (!isQuote(order) || !order.quote) return {
149
+ ok: false,
150
+ reason: 'not-a-quote',
151
+ error: 'Unknown quote'
152
+ };
153
+ if (!isUnpaidQuote(order)) {
154
+ return {
155
+ ok: false,
156
+ reason: 'paid',
157
+ error: 'This quote has been paid, so it takes no new revision.'
158
+ };
159
+ }
160
+ const quote = order.quote;
161
+ const current = Math.max(1, Math.floor(Number((_quote_revision = quote.revision) != null ? _quote_revision : 1)) || 1);
162
+ const responses = ((_quote_responses = quote.responses) != null ? _quote_responses : []).filter((response)=>Number(response == null ? void 0 : response.revision) === current);
163
+ const approval = quote.approval && Number(quote.approval.revision) === current ? quote.approval : null;
164
+ const snapshot = {
165
+ revision: current,
166
+ stage: (_quote_stage = quote.stage) != null ? _quote_stage : 'draft',
167
+ lineItems: (_order_lineItems = order.lineItems) != null ? _order_lineItems : [],
168
+ totals: (_order_totals = order.totals) != null ? _order_totals : null,
169
+ taxMode: (_order_taxMode = order.taxMode) != null ? _order_taxMode : null,
170
+ tax: (_quote_tax = quote.tax) != null ? _quote_tax : null,
171
+ discount: (_quote_discount = quote.discount) != null ? _quote_discount : null,
172
+ shipping: (_quote_shipping = quote.shipping) != null ? _quote_shipping : null,
173
+ notes: (_quote_notes = quote.notes) != null ? _quote_notes : '',
174
+ terms: (_quote_terms = quote.terms) != null ? _quote_terms : '',
175
+ customer: (_quote_customer = quote.customer) != null ? _quote_customer : null,
176
+ billTo: (_quote_billTo = quote.billTo) != null ? _quote_billTo : null,
177
+ shipTo: (_quote_shipTo = quote.shipTo) != null ? _quote_shipTo : null,
178
+ proofs: currentQuoteProofs(quote),
179
+ responses,
180
+ approval,
181
+ updatedAtMs: Number(quote.updatedAtMs) || 0,
182
+ closedAtMs: options.atMs,
183
+ closedBy: options.by
184
+ };
185
+ const next = current + 1;
186
+ return {
187
+ ok: true,
188
+ revision: current,
189
+ snapshot,
190
+ order: _extends({}, order, {
191
+ timeline: appendOrderEvent(order, 'quote-revision-started', `Revision ${next} started by ${options.by}; revision ${current} is kept with its proofs and answers`, options.atMs),
192
+ quote: _extends({}, quote, {
193
+ revision: next,
194
+ stage: 'draft',
195
+ proofs: [],
196
+ responses: [],
197
+ approval: null,
198
+ updatedAtMs: options.atMs,
199
+ updatedBy: options.by
200
+ })
201
+ })
202
+ };
203
+ }
204
+ /** Why the customer cannot answer the current revision now, if they cannot. */ export function quoteRespondRefusal(order) {
205
+ var _order_quote_revision, _order_quote_responses;
206
+ if (!order || !isQuote(order) || !order.quote) return 'not-a-quote';
207
+ if (!isUnpaidQuote(order)) return 'paid';
208
+ if (currentQuoteProofs(order.quote).length === 0) return 'no-proof';
209
+ const revision = Number((_order_quote_revision = order.quote.revision) != null ? _order_quote_revision : 1);
210
+ const given = ((_order_quote_responses = order.quote.responses) != null ? _order_quote_responses : []).filter((response)=>Number(response == null ? void 0 : response.revision) === revision);
211
+ if (given.length >= QUOTE_MAX_RESPONSES) return 'too-many';
212
+ return null;
213
+ }
214
+ export function quoteRespondRefusalMessage(reason, storeName) {
215
+ const business = storeName || 'the business';
216
+ switch(reason){
217
+ case 'not-a-quote':
218
+ return 'We could not find that quote. Check the link you were sent.';
219
+ case 'paid':
220
+ return 'This quote has been paid, so it takes no more answers.';
221
+ case 'no-proof':
222
+ return `There is no proof on this quote to answer yet. ${storeName || 'The business'} will send one.`;
223
+ case 'too-many':
224
+ return `This proof has had a lot of answers already. Contact ${business} directly.`;
225
+ }
226
+ }
227
+ /** The customer's answer as the route received it, checked and trimmed, or what to fix. */ export function parseQuoteResponseInput(raw) {
228
+ const source = raw && typeof raw === 'object' ? raw : {};
229
+ if (source['action'] === 'approve') {
230
+ const name = typeof source['name'] === 'string' ? source['name'].replace(/\s+/g, ' ').trim() : '';
231
+ if (name.length < 2) return {
232
+ ok: false,
233
+ error: 'Type your name to approve the proof.'
234
+ };
235
+ return {
236
+ ok: true,
237
+ input: {
238
+ action: 'approve',
239
+ name: name.slice(0, QUOTE_MAX_APPROVER_NAME)
240
+ }
241
+ };
242
+ }
243
+ if (source['action'] === 'changes') {
244
+ const comment = typeof source['comment'] === 'string' ? source['comment'].trim() : '';
245
+ if (!comment) return {
246
+ ok: false,
247
+ error: 'Tell us what to change.'
248
+ };
249
+ const raws = Array.isArray(source['fileIds']) ? source['fileIds'] : [];
250
+ const named = raws.map((value)=>String(value != null ? value : '')).filter((value)=>/^[A-Za-z0-9_-]{1,128}$/.test(value));
251
+ if (named.length !== raws.length) return {
252
+ ok: false,
253
+ error: 'One of the files could not be attached. Send it again.'
254
+ };
255
+ const fileIds = [
256
+ ...new Set(named)
257
+ ];
258
+ if (fileIds.length > QUOTE_MAX_PROOFS) return {
259
+ ok: false,
260
+ error: `Send up to ${QUOTE_MAX_PROOFS} files at a time.`
261
+ };
262
+ return {
263
+ ok: true,
264
+ input: {
265
+ action: 'changes',
266
+ comment: comment.slice(0, QUOTE_MAX_COMMENT),
267
+ fileIds
268
+ }
269
+ };
270
+ }
271
+ return {
272
+ ok: false,
273
+ error: 'Approve the proof or ask for changes.'
274
+ };
275
+ }
276
+ /**
277
+ * The quote with the customer's answer recorded on its current revision.
278
+ * Approving names the revision approved and moves the quote to Approved;
279
+ * asking for changes moves it to Changes requested and withdraws an approval
280
+ * of the same revision. The latest answer is the one that counts. The
281
+ * business's edit stamp (`updatedAtMs`) is not touched: the quote's content
282
+ * did not change, and the page that sent the answer can still pay it.
283
+ */ export function applyQuoteResponse(order, input, context) {
284
+ var _quote_revision, _quote_responses, _response_name, _quote_approvedRevision;
285
+ var _input_files, _response_files;
286
+ const refused = quoteRespondRefusal(order);
287
+ if (refused) return {
288
+ ok: false,
289
+ reason: refused
290
+ };
291
+ const quote = order.quote;
292
+ const revision = Number((_quote_revision = quote.revision) != null ? _quote_revision : 1);
293
+ const ip = context.ip ? context.ip.slice(0, 64) : null;
294
+ const userAgent = context.userAgent ? context.userAgent.slice(0, 400) : null;
295
+ const response = input.action === 'approve' ? {
296
+ action: 'approve',
297
+ revision,
298
+ atMs: context.atMs,
299
+ name: input.name,
300
+ ip,
301
+ userAgent
302
+ } : _extends({
303
+ action: 'changes',
304
+ revision,
305
+ atMs: context.atMs,
306
+ comment: input.comment
307
+ }, ((_input_files = input.files) == null ? void 0 : _input_files.length) ? {
308
+ files: input.files
309
+ } : {}, {
310
+ ip,
311
+ userAgent
312
+ });
313
+ const responses = [
314
+ ...((_quote_responses = quote.responses) != null ? _quote_responses : []).filter((entry)=>Number(entry == null ? void 0 : entry.revision) === revision),
315
+ response
316
+ ];
317
+ const number = formatQuoteNumber(order);
318
+ const next = response.action === 'approve' ? _extends({}, quote, {
319
+ responses,
320
+ stage: 'approved',
321
+ approvedRevision: revision,
322
+ approval: {
323
+ revision,
324
+ name: (_response_name = response.name) != null ? _response_name : '',
325
+ atMs: context.atMs,
326
+ ip,
327
+ userAgent
328
+ }
329
+ }) : _extends({}, quote, {
330
+ responses,
331
+ stage: 'changes_requested',
332
+ approvedRevision: Number(quote.approvedRevision) === revision ? null : (_quote_approvedRevision = quote.approvedRevision) != null ? _quote_approvedRevision : null,
333
+ approval: null
334
+ });
335
+ return {
336
+ ok: true,
337
+ response,
338
+ order: _extends({}, order, {
339
+ timeline: appendOrderEvent(order, response.action === 'approve' ? 'quote-approved' : 'quote-changes-requested', response.action === 'approve' ? `${number} revision ${revision} approved by the customer as ${response.name}` : `The customer asked for changes to ${number} revision ${revision}` + (((_response_files = response.files) == null ? void 0 : _response_files.length) ? ` and sent ${response.files.length} file${response.files.length === 1 ? '' : 's'}` : ''), context.atMs),
340
+ quote: next
341
+ })
342
+ };
343
+ }
344
+ /** One line on what the customer last said: `Approved by Jordan Lee`, `Changes requested`. */ export function describeQuoteResponse(response) {
345
+ return response.action === 'approve' ? `Approved by ${response.name || 'the customer'}` : 'Changes requested';
346
+ }
347
+
348
+ //# sourceMappingURL=commerce-quote-proofs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/model/commerce-quote-proofs.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { appendOrderEvent, type HostOrder, type OrderAddress, type OrderLineItem, type OrderTotals } from './commerce-orders'\nimport {\n formatQuoteNumber,\n isQuote,\n isUnpaidQuote,\n type OrderQuote,\n type QuoteApproval,\n type QuoteCustomer,\n type QuoteCustomerResponse,\n type QuoteDiscount,\n type QuoteProof,\n type QuoteResponseFile,\n type QuoteShipping,\n type QuoteStage,\n type QuoteTaxBasis,\n} from './commerce-quotes'\nimport type { StorefrontTaxMode } from './commerce-tax-decision'\n\n/*==========================================\n * PROOFS, REVISIONS AND THE CUSTOMER'S ANSWER (AGL-4096).\n *\n * A quote's REVISION is the version the customer is looking at. The business\n * attaches proofs to the current revision; the customer approves them under\n * a typed name or asks for changes; the business then starts the next\n * revision, which keeps the one before exactly as it stood in\n * `hosts/{hostId}/orders/{id}/revisions/{n}` and starts the new one with no\n * proofs and no answer. Every revision is kept.\n *\n * Whether paying waits on an approval is the business's control\n * (`requireProofApproval`, AGL-4093) and `quotePaymentBlockedByApproval`\n * answers it; nothing here makes paying wait when it is off.\n *\n * Pure: the console route, the customer's route and (slice 5) the send that\n * starts a revision all call these, and write what they answer.\n *=========================================*/\n\n/** The most proofs one revision holds. */\nexport const QUOTE_MAX_PROOFS = 10\n\n/** The most answers the customer gives one revision; plenty for a change of mind. */\nexport const QUOTE_MAX_RESPONSES = 20\n\n/** The longest comment a customer may leave. */\nexport const QUOTE_MAX_COMMENT = 5_000\n\n/** The longest name an approval is signed with. */\nexport const QUOTE_MAX_APPROVER_NAME = 120\n\n/** What the visitor uploads are called for a quote: its proofs, and the customer's artwork. */\nexport const QUOTE_PROOF_PURPOSE = 'quote-proof'\nexport const QUOTE_ARTWORK_PURPOSE = 'quote-artwork'\n\n/** A revision as it stood when the next one started. */\nexport interface QuoteRevisionSnapshot {\n revision: number\n /** Where it stood with the business when it closed. */\n stage: QuoteStage\n lineItems: OrderLineItem[]\n totals: OrderTotals | null\n taxMode: StorefrontTaxMode | null\n tax: QuoteTaxBasis | null\n discount: QuoteDiscount | null\n shipping: QuoteShipping | null\n notes: string\n terms: string\n customer: QuoteCustomer | null\n billTo: OrderAddress | null\n shipTo: OrderAddress | null\n proofs: QuoteProof[]\n responses: QuoteCustomerResponse[]\n approval: QuoteApproval | null\n /** When the revision was last written by the business. */\n updatedAtMs: number\n /** When the next revision started, and who started it. */\n closedAtMs: number\n closedBy: string\n}\n\n/** The current revision's proofs. */\nexport function currentQuoteProofs(quote: Pick<OrderQuote, 'proofs' | 'revision'> | null | undefined): QuoteProof[] {\n const revision = Number(quote?.revision ?? 1)\n return (quote?.proofs ?? []).filter((proof) => Number(proof?.revision) === revision)\n}\n\n/** The customer's latest answer to the current revision, if they gave one. */\nexport function latestQuoteResponse(\n quote: Pick<OrderQuote, 'responses' | 'revision'> | null | undefined,\n): QuoteCustomerResponse | null {\n const revision = Number(quote?.revision ?? 1)\n const answers = (quote?.responses ?? []).filter((response) => Number(response?.revision) === revision)\n return answers.length ? answers[answers.length - 1] : null\n}\n\n/** Whether the customer has answered the current revision. */\nexport function quoteRevisionAnswered(quote: Pick<OrderQuote, 'responses' | 'revision'> | null | undefined): boolean {\n return latestQuoteResponse(quote) !== null\n}\n\n/*------------------------------------------\n * The business's proofs\n *-----------------------------------------*/\n\nexport type QuoteProofRefusal = 'not-a-quote' | 'paid' | 'answered' | 'full' | 'unknown-proof'\n\n/** Why the current revision's proofs cannot be changed now, if they cannot. */\nexport function quoteProofChangeRefusal(\n order: Pick<HostOrder, 'channel' | 'status' | 'quote'> | null | undefined,\n change: 'add' | 'remove',\n): QuoteProofRefusal | null {\n if (!order || !isQuote(order) || !order.quote) return 'not-a-quote'\n if (!isUnpaidQuote(order)) return 'paid'\n if (quoteRevisionAnswered(order.quote)) return 'answered'\n if (change === 'add' && currentQuoteProofs(order.quote).length >= QUOTE_MAX_PROOFS) return 'full'\n return null\n}\n\nexport function quoteProofRefusalMessage(reason: QuoteProofRefusal): string {\n switch (reason) {\n case 'not-a-quote':\n return 'Unknown quote'\n case 'paid':\n return 'This quote has been paid, so its proofs can no longer be changed.'\n case 'answered':\n return 'The customer has answered this revision. Start a new revision to send different proofs.'\n case 'full':\n return `A revision holds up to ${QUOTE_MAX_PROOFS} proofs. Remove one first.`\n case 'unknown-proof':\n return 'That proof is no longer on this quote.'\n }\n}\n\n/** The quote with a proof added to its current revision, edited by `by` at `atMs`. */\nexport function withQuoteProofAdded(\n order: HostOrder,\n proof: Omit<QuoteProof, 'revision' | 'addedAtMs' | 'addedBy'>,\n options: { atMs: number; by: string },\n): { ok: true; order: HostOrder; proof: QuoteProof } | { ok: false; reason: QuoteProofRefusal } {\n const refused = quoteProofChangeRefusal(order, 'add')\n if (refused) return { ok: false, reason: refused }\n const quote = order.quote as OrderQuote\n const added: QuoteProof = {\n id: proof.id,\n revision: Number(quote.revision ?? 1),\n name: proof.name,\n contentType: proof.contentType,\n size: proof.size,\n path: proof.path,\n addedAtMs: options.atMs,\n addedBy: options.by,\n }\n return {\n ok: true,\n proof: added,\n order: {\n ...order,\n timeline: appendOrderEvent(\n order,\n 'quote-proof-added',\n `Proof ${added.name} added to revision ${added.revision} by ${options.by}`,\n options.atMs,\n ),\n quote: {\n ...quote,\n proofs: [...currentQuoteProofs(quote).filter((entry) => entry.id !== added.id), added],\n updatedAtMs: options.atMs,\n updatedBy: options.by,\n },\n },\n }\n}\n\n/** The quote with one of the current revision's proofs taken off. */\nexport function withQuoteProofRemoved(\n order: HostOrder,\n proofId: string,\n options: { atMs: number; by: string },\n): { ok: true; order: HostOrder; proof: QuoteProof } | { ok: false; reason: QuoteProofRefusal } {\n const refused = quoteProofChangeRefusal(order, 'remove')\n if (refused) return { ok: false, reason: refused }\n const quote = order.quote as OrderQuote\n const proofs = currentQuoteProofs(quote)\n const proof = proofs.find((entry) => entry.id === proofId)\n if (!proof) return { ok: false, reason: 'unknown-proof' }\n return {\n ok: true,\n proof,\n order: {\n ...order,\n timeline: appendOrderEvent(\n order,\n 'quote-proof-removed',\n `Proof ${proof.name} removed from revision ${proof.revision} by ${options.by}`,\n options.atMs,\n ),\n quote: {\n ...quote,\n proofs: proofs.filter((entry) => entry.id !== proofId),\n updatedAtMs: options.atMs,\n updatedBy: options.by,\n },\n },\n }\n}\n\n/*------------------------------------------\n * Revisions\n *-----------------------------------------*/\n\nexport type QuoteRevisionRefusal = 'not-a-quote' | 'paid'\n\n/**\n * Starts the quote's next revision. The current one is SNAPSHOTTED as it\n * stands (its lines, totals, notes, terms, proofs and the customer's\n * answers); the quote moves to revision N+1 with no proofs and no answer,\n * back at Draft, edited by `by` at `atMs`. A paid quote starts none.\n *\n * The caller writes `snapshot` to `hosts/{hostId}/orders/{id}/revisions/{revision}`\n * (`String(snapshot.revision)`, created, never overwritten) and `order` over\n * the quote, in one transaction. The pay link's version is not touched: the\n * send that hands the customer the new revision decides that.\n */\nexport function startQuoteRevision(\n order: HostOrder,\n options: { atMs: number; by: string },\n):\n | { ok: true; snapshot: QuoteRevisionSnapshot; order: HostOrder; revision: number }\n | { ok: false; reason: QuoteRevisionRefusal; error: string } {\n if (!isQuote(order) || !order.quote) return { ok: false, reason: 'not-a-quote', error: 'Unknown quote' }\n if (!isUnpaidQuote(order)) {\n return { ok: false, reason: 'paid', error: 'This quote has been paid, so it takes no new revision.' }\n }\n const quote = order.quote\n const current = Math.max(1, Math.floor(Number(quote.revision ?? 1)) || 1)\n const responses = (quote.responses ?? []).filter((response) => Number(response?.revision) === current)\n const approval = quote.approval && Number(quote.approval.revision) === current ? quote.approval : null\n const snapshot: QuoteRevisionSnapshot = {\n revision: current,\n stage: quote.stage ?? 'draft',\n lineItems: order.lineItems ?? [],\n totals: order.totals ?? null,\n taxMode: order.taxMode ?? null,\n tax: quote.tax ?? null,\n discount: quote.discount ?? null,\n shipping: quote.shipping ?? null,\n notes: quote.notes ?? '',\n terms: quote.terms ?? '',\n customer: quote.customer ?? null,\n billTo: quote.billTo ?? null,\n shipTo: quote.shipTo ?? null,\n proofs: currentQuoteProofs(quote),\n responses,\n approval,\n updatedAtMs: Number(quote.updatedAtMs) || 0,\n closedAtMs: options.atMs,\n closedBy: options.by,\n }\n const next = current + 1\n return {\n ok: true,\n revision: current,\n snapshot,\n order: {\n ...order,\n timeline: appendOrderEvent(\n order,\n 'quote-revision-started',\n `Revision ${next} started by ${options.by}; revision ${current} is kept with its proofs and answers`,\n options.atMs,\n ),\n quote: {\n ...quote,\n revision: next,\n stage: 'draft',\n proofs: [],\n responses: [],\n approval: null,\n updatedAtMs: options.atMs,\n updatedBy: options.by,\n },\n },\n }\n}\n\n/*------------------------------------------\n * The customer's answer\n *-----------------------------------------*/\n\nexport type QuoteRespondRefusal = 'not-a-quote' | 'paid' | 'no-proof' | 'too-many'\n\n/** Why the customer cannot answer the current revision now, if they cannot. */\nexport function quoteRespondRefusal(\n order: Pick<HostOrder, 'channel' | 'status' | 'quote'> | null | undefined,\n): QuoteRespondRefusal | null {\n if (!order || !isQuote(order) || !order.quote) return 'not-a-quote'\n if (!isUnpaidQuote(order)) return 'paid'\n if (currentQuoteProofs(order.quote).length === 0) return 'no-proof'\n const revision = Number(order.quote.revision ?? 1)\n const given = (order.quote.responses ?? []).filter((response) => Number(response?.revision) === revision)\n if (given.length >= QUOTE_MAX_RESPONSES) return 'too-many'\n return null\n}\n\nexport function quoteRespondRefusalMessage(reason: QuoteRespondRefusal, storeName: string): string {\n const business = storeName || 'the business'\n switch (reason) {\n case 'not-a-quote':\n return 'We could not find that quote. Check the link you were sent.'\n case 'paid':\n return 'This quote has been paid, so it takes no more answers.'\n case 'no-proof':\n return `There is no proof on this quote to answer yet. ${storeName || 'The business'} will send one.`\n case 'too-many':\n return `This proof has had a lot of answers already. Contact ${business} directly.`\n }\n}\n\n/** What the customer sends: approve under a name, or changes with a comment and any files. */\nexport type QuoteResponseInput =\n | { action: 'approve'; name: string }\n | { action: 'changes'; comment: string; fileIds: string[] }\n\n/** The customer's answer as the route received it, checked and trimmed, or what to fix. */\nexport function parseQuoteResponseInput(raw: unknown): { ok: true; input: QuoteResponseInput } | { ok: false; error: string } {\n const source = (raw && typeof raw === 'object' ? raw : {}) as Record<string, unknown>\n if (source['action'] === 'approve') {\n const name = typeof source['name'] === 'string' ? source['name'].replace(/\\s+/g, ' ').trim() : ''\n if (name.length < 2) return { ok: false, error: 'Type your name to approve the proof.' }\n return { ok: true, input: { action: 'approve', name: name.slice(0, QUOTE_MAX_APPROVER_NAME) } }\n }\n if (source['action'] === 'changes') {\n const comment = typeof source['comment'] === 'string' ? source['comment'].trim() : ''\n if (!comment) return { ok: false, error: 'Tell us what to change.' }\n const raws = Array.isArray(source['fileIds']) ? (source['fileIds'] as unknown[]) : []\n const named = raws.map((value) => String(value ?? '')).filter((value) => /^[A-Za-z0-9_-]{1,128}$/.test(value))\n if (named.length !== raws.length) return { ok: false, error: 'One of the files could not be attached. Send it again.' }\n const fileIds = [...new Set(named)]\n if (fileIds.length > QUOTE_MAX_PROOFS) return { ok: false, error: `Send up to ${QUOTE_MAX_PROOFS} files at a time.` }\n return { ok: true, input: { action: 'changes', comment: comment.slice(0, QUOTE_MAX_COMMENT), fileIds } }\n }\n return { ok: false, error: 'Approve the proof or ask for changes.' }\n}\n\n/**\n * The quote with the customer's answer recorded on its current revision.\n * Approving names the revision approved and moves the quote to Approved;\n * asking for changes moves it to Changes requested and withdraws an approval\n * of the same revision. The latest answer is the one that counts. The\n * business's edit stamp (`updatedAtMs`) is not touched: the quote's content\n * did not change, and the page that sent the answer can still pay it.\n */\nexport function applyQuoteResponse(\n order: HostOrder,\n input: QuoteResponseInput & { files?: QuoteResponseFile[] },\n context: { atMs: number; ip: string | null; userAgent: string | null },\n): { ok: true; order: HostOrder; response: QuoteCustomerResponse } | { ok: false; reason: QuoteRespondRefusal } {\n const refused = quoteRespondRefusal(order)\n if (refused) return { ok: false, reason: refused }\n const quote = order.quote as OrderQuote\n const revision = Number(quote.revision ?? 1)\n const ip = context.ip ? context.ip.slice(0, 64) : null\n const userAgent = context.userAgent ? context.userAgent.slice(0, 400) : null\n const response: QuoteCustomerResponse =\n input.action === 'approve'\n ? { action: 'approve', revision, atMs: context.atMs, name: input.name, ip, userAgent }\n : {\n action: 'changes',\n revision,\n atMs: context.atMs,\n comment: input.comment,\n ...(input.files?.length ? { files: input.files } : {}),\n ip,\n userAgent,\n }\n const responses = [...(quote.responses ?? []).filter((entry) => Number(entry?.revision) === revision), response]\n const number = formatQuoteNumber(order)\n const next: OrderQuote =\n response.action === 'approve'\n ? {\n ...quote,\n responses,\n stage: 'approved',\n approvedRevision: revision,\n approval: { revision, name: response.name ?? '', atMs: context.atMs, ip, userAgent },\n }\n : {\n ...quote,\n responses,\n stage: 'changes_requested',\n approvedRevision: Number(quote.approvedRevision) === revision ? null : (quote.approvedRevision ?? null),\n approval: null,\n }\n return {\n ok: true,\n response,\n order: {\n ...order,\n timeline: appendOrderEvent(\n order,\n response.action === 'approve' ? 'quote-approved' : 'quote-changes-requested',\n response.action === 'approve'\n ? `${number} revision ${revision} approved by the customer as ${response.name}`\n : `The customer asked for changes to ${number} revision ${revision}` +\n (response.files?.length ? ` and sent ${response.files.length} file${response.files.length === 1 ? '' : 's'}` : ''),\n context.atMs,\n ),\n quote: next,\n },\n }\n}\n\n/** One line on what the customer last said: `Approved by Jordan Lee`, `Changes requested`. */\nexport function describeQuoteResponse(response: Pick<QuoteCustomerResponse, 'action' | 'name'>): string {\n return response.action === 'approve' ? `Approved by ${response.name || 'the customer'}` : 'Changes requested'\n}\n"],"names":["appendOrderEvent","formatQuoteNumber","isQuote","isUnpaidQuote","QUOTE_MAX_PROOFS","QUOTE_MAX_RESPONSES","QUOTE_MAX_COMMENT","QUOTE_MAX_APPROVER_NAME","QUOTE_PROOF_PURPOSE","QUOTE_ARTWORK_PURPOSE","currentQuoteProofs","quote","revision","Number","proofs","filter","proof","latestQuoteResponse","answers","responses","response","length","quoteRevisionAnswered","quoteProofChangeRefusal","order","change","quoteProofRefusalMessage","reason","withQuoteProofAdded","options","refused","ok","added","id","name","contentType","size","path","addedAtMs","atMs","addedBy","by","timeline","entry","updatedAtMs","updatedBy","withQuoteProofRemoved","proofId","find","startQuoteRevision","error","current","Math","max","floor","approval","snapshot","stage","lineItems","totals","taxMode","tax","discount","shipping","notes","terms","customer","billTo","shipTo","closedAtMs","closedBy","next","quoteRespondRefusal","given","quoteRespondRefusalMessage","storeName","business","parseQuoteResponseInput","raw","source","replace","trim","input","action","slice","comment","raws","Array","isArray","named","map","value","String","test","fileIds","Set","applyQuoteResponse","context","ip","userAgent","files","number","approvedRevision","describeQuoteResponse"],"mappings":"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"}
@@ -67,11 +67,60 @@ export interface QuoteTaxBasis {
67
67
  providerLabel?: string;
68
68
  reason?: QuoteNoTaxReason;
69
69
  }
70
- /** A proof attached to one revision of a quote. Written by the proofs feature. */
70
+ /** A proof attached to one revision of a quote: what names it, and the revision it is for. */
71
71
  export interface QuoteProofRef {
72
72
  id: string;
73
73
  revision: number;
74
74
  }
75
+ /**
76
+ * A proof the business attached to a revision (AGL-4096): an image or a
77
+ * design file in the site's private storage, under the quote.
78
+ */
79
+ export interface QuoteProof extends QuoteProofRef {
80
+ name: string;
81
+ contentType: string;
82
+ size: number;
83
+ /** The private Storage object. Never sent to the customer. */
84
+ path: string;
85
+ addedAtMs: number;
86
+ /** Who attached it: an email, else a uid. */
87
+ addedBy: string;
88
+ }
89
+ /** A file the customer sent with a response. */
90
+ export interface QuoteResponseFile {
91
+ id: string;
92
+ name: string;
93
+ contentType: string;
94
+ size: number;
95
+ /** The private Storage object. Never sent to the customer. */
96
+ path: string;
97
+ }
98
+ /**
99
+ * The customer's answer to a revision's proofs (AGL-4096): approved under a
100
+ * typed name, or changes asked for in a comment, with artwork if they had
101
+ * some. The address and browser are kept as the record of who answered.
102
+ */
103
+ export interface QuoteCustomerResponse {
104
+ action: 'approve' | 'changes';
105
+ revision: number;
106
+ atMs: number;
107
+ /** With `approve`: the name they typed. */
108
+ name?: string;
109
+ /** With `changes`: what to change. */
110
+ comment?: string;
111
+ /** With `changes`: files they sent. */
112
+ files?: QuoteResponseFile[];
113
+ ip: string | null;
114
+ userAgent: string | null;
115
+ }
116
+ /** The record of a revision's approval. */
117
+ export interface QuoteApproval {
118
+ revision: number;
119
+ name: string;
120
+ atMs: number;
121
+ ip: string | null;
122
+ userAgent: string | null;
123
+ }
75
124
  /**
76
125
  * A payment that reached a quote it could not pay: one made through a pay
77
126
  * link the business replaced or for a price it has since changed
@@ -101,7 +150,12 @@ export interface OrderQuote {
101
150
  tax: QuoteTaxBasis;
102
151
  /** The customer approves the current revision's proof before they can pay. Absent reads as off. */
103
152
  requireProofApproval?: boolean;
104
- proofs?: QuoteProofRef[];
153
+ /** The current revision's proofs; earlier revisions keep theirs in `revisions/{n}`. */
154
+ proofs?: QuoteProof[];
155
+ /** The customer's answers to the current revision, oldest first; the last one counts. */
156
+ responses?: QuoteCustomerResponse[];
157
+ /** The current revision's approval, while its latest answer is one. */
158
+ approval?: QuoteApproval | null;
105
159
  /** The revision the customer approved, when they have. */
106
160
  approvedRevision?: number | null;
107
161
  /**
@@ -132,7 +186,9 @@ export declare function quoteRequiresProofApprovalByDefault(settings: QuoteStore
132
186
  * approval, its current revision has a proof, and that revision is not the
133
187
  * one the customer approved. A revision with no proof never holds a payment.
134
188
  */
135
- export declare function quotePaymentBlockedByApproval(quote: Pick<OrderQuote, 'requireProofApproval' | 'revision' | 'proofs' | 'approvedRevision'> | null | undefined): boolean;
189
+ export declare function quotePaymentBlockedByApproval(quote: (Pick<OrderQuote, 'requireProofApproval' | 'revision' | 'approvedRevision'> & {
190
+ proofs?: readonly QuoteProofRef[];
191
+ }) | null | undefined): boolean;
136
192
  /** Whether an order document is a quote, paid or not. */
137
193
  export declare function isQuote(order: Pick<Partial<HostOrder>, 'channel'> | null | undefined): boolean;
138
194
  /** Whether an order document is a quote nobody has paid yet, which no order list counts. */