@aglyn/aglyn 1.0.0-beta.270 → 1.0.0-beta.271

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 (75) hide show
  1. package/package.json +11 -11
  2. package/src/lib/app-utils/activity-labels.js +2 -0
  3. package/src/lib/app-utils/activity-labels.js.map +1 -1
  4. package/src/lib/app-utils/analytics-events.d.ts +13 -0
  5. package/src/lib/app-utils/analytics-events.js +2 -0
  6. package/src/lib/app-utils/analytics-events.js.map +1 -1
  7. package/src/lib/app-utils/append-only-history.d.ts +94 -0
  8. package/src/lib/app-utils/append-only-history.js +73 -0
  9. package/src/lib/app-utils/append-only-history.js.map +1 -0
  10. package/src/lib/app-utils/bundle-intent.d.ts +186 -0
  11. package/src/lib/app-utils/bundle-intent.js +386 -0
  12. package/src/lib/app-utils/bundle-intent.js.map +1 -0
  13. package/src/lib/app-utils/client-build.d.ts +204 -0
  14. package/src/lib/app-utils/client-build.js +237 -0
  15. package/src/lib/app-utils/client-build.js.map +1 -0
  16. package/src/lib/app-utils/console-routes.d.ts +4 -0
  17. package/src/lib/app-utils/console-routes.js +2 -0
  18. package/src/lib/app-utils/console-routes.js.map +1 -1
  19. package/src/lib/app-utils/docs-help-sections.generated.js +7 -5
  20. package/src/lib/app-utils/docs-help-sections.generated.js.map +1 -1
  21. package/src/lib/app-utils/docs-help.generated.d.ts +5 -5
  22. package/src/lib/app-utils/docs-help.generated.js +9 -2
  23. package/src/lib/app-utils/docs-help.generated.js.map +1 -1
  24. package/src/lib/app-utils/docs-index.generated.js +101 -31
  25. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  26. package/src/lib/app-utils/margin-utilization.js +6 -4
  27. package/src/lib/app-utils/margin-utilization.js.map +1 -1
  28. package/src/lib/app-utils/media-filter.d.ts +26 -0
  29. package/src/lib/app-utils/media-filter.js +32 -0
  30. package/src/lib/app-utils/media-filter.js.map +1 -1
  31. package/src/lib/app-utils/onboarding-deep-link.d.ts +36 -2
  32. package/src/lib/app-utils/onboarding-deep-link.js +48 -5
  33. package/src/lib/app-utils/onboarding-deep-link.js.map +1 -1
  34. package/src/lib/app-utils/operator-alerts.js +21 -0
  35. package/src/lib/app-utils/operator-alerts.js.map +1 -1
  36. package/src/lib/app-utils/owner-approval-record.d.ts +102 -0
  37. package/src/lib/app-utils/owner-approval-record.js +148 -0
  38. package/src/lib/app-utils/owner-approval-record.js.map +1 -0
  39. package/src/lib/app-utils/plan-entitlements.d.ts +183 -67
  40. package/src/lib/app-utils/plan-entitlements.js +213 -58
  41. package/src/lib/app-utils/plan-entitlements.js.map +1 -1
  42. package/src/lib/app-utils/release-flags.d.ts +1 -1
  43. package/src/lib/app-utils/release-flags.js +10 -0
  44. package/src/lib/app-utils/release-flags.js.map +1 -1
  45. package/src/lib/app-utils/server.d.ts +4 -0
  46. package/src/lib/app-utils/server.js +7 -0
  47. package/src/lib/app-utils/server.js.map +1 -1
  48. package/src/lib/app-utils/site-care-project.d.ts +227 -0
  49. package/src/lib/app-utils/site-care-project.js +423 -0
  50. package/src/lib/app-utils/site-care-project.js.map +1 -0
  51. package/src/lib/app-utils/site-care.d.ts +268 -0
  52. package/src/lib/app-utils/site-care.js +283 -0
  53. package/src/lib/app-utils/site-care.js.map +1 -0
  54. package/src/lib/app-utils/upgrade-proposal.d.ts +29 -4
  55. package/src/lib/app-utils/upgrade-proposal.js +60 -9
  56. package/src/lib/app-utils/upgrade-proposal.js.map +1 -1
  57. package/src/lib/foundation/definitions/org-billing.types.d.ts +40 -10
  58. package/src/lib/foundation/definitions/org-billing.types.js.map +1 -1
  59. package/src/lib/foundation/definitions/organization.types.d.ts +3 -1
  60. package/src/lib/foundation/definitions/organization.types.js.map +1 -1
  61. package/src/lib/foundation/index.d.ts +1 -1
  62. package/src/lib/foundation/index.js.map +1 -1
  63. package/src/lib/plugin-manager/feature-plugins.d.ts +48 -0
  64. package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
  65. package/src/lib/plugin-manager/plugin-ai-capabilities.d.ts +19 -0
  66. package/src/lib/plugin-manager/plugin-ai-capabilities.js.map +1 -1
  67. package/src/lib/plugin-manager/plugin-domain-search.d.ts +81 -0
  68. package/src/lib/plugin-manager/plugin-domain-search.js +63 -0
  69. package/src/lib/plugin-manager/plugin-domain-search.js.map +1 -0
  70. package/src/lib/plugin-manager/plugin-entitlements.d.ts +23 -5
  71. package/src/lib/plugin-manager/plugin-entitlements.js +10 -0
  72. package/src/lib/plugin-manager/plugin-entitlements.js.map +1 -1
  73. package/src/lib/plugin-manager/purchase-gate-preclearance.d.ts +61 -0
  74. package/src/lib/plugin-manager/purchase-gate-preclearance.js +102 -0
  75. package/src/lib/plugin-manager/purchase-gate-preclearance.js.map +1 -0
@@ -0,0 +1,423 @@
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 { OWNER_APPROVAL_CHANNEL_WORDS, readOwnerApprovalRecord } from "./owner-approval-record.js";
18
+ import { SITE_CARE_EXTRA_HOUR_USD, SITE_CARE_PROJECT_BRAND_KIT_FROM_USD, SITE_CARE_PROJECT_LANDING_PAGE_FROM_USD, SITE_CARE_PROJECT_REDESIGN_FROM_USD, SITE_CARE_PROJECT_SPLIT_ABOVE_USD } from "./plan-entitlements.js";
19
+ import { formatSiteCareUsd, SITE_CARE_EXTRA_TIME_TAX_CODE } from "./site-care.js";
20
+ /**
21
+ * SITE CARE PROJECTS: THE ARITHMETIC AND THE RECORD (AGL-3970).
22
+ *
23
+ * Site Care covers changes to what a site already has. Work that adds
24
+ * something — a page, a form, a design, a campaign — is a PROJECT, billed as
25
+ * one flat quote from the staff "Charge for work" on the staff organization
26
+ * page, on a one-off Stripe invoice the owner pays from their phone. This
27
+ * module is everything about that which needs no Firestore and no Stripe:
28
+ * the quote, the split, where a project stands, and the sentences the owner
29
+ * reads. The server module (`@aglyn/tenant-data-admin/server/site-care-projects`),
30
+ * the staff card, the owner's Billing card and the reconciliation all read
31
+ * it, so they cannot disagree.
32
+ *
33
+ * ## The rules, from the Decision Log of 2026-10-19
34
+ *
35
+ * - The quote is the estimated hours × `SITE_CARE_EXTRA_HOUR_USD`, the
36
+ * hours rounded UP to the whole hour, and never below the published
37
+ * starting price of the project's kind. Staff may raise it, never lower
38
+ * it. It is one number, given before work starts and fixed once given.
39
+ * - Priced above `SITE_CARE_PROJECT_SPLIT_ABOVE_USD`, it is billed half up
40
+ * front and half at launch: two invoices from the same record. At or
41
+ * below, in full up front.
42
+ * - Nothing is invoiced without the owner's recorded yes to THIS quote
43
+ * (`owner-approval-record`): change the quote and the yes must be
44
+ * recorded again.
45
+ * - Scope and price are edited, with a reason, only until the first invoice
46
+ * is sent. After that a wrong invoice is voided, credited or refunded from
47
+ * the record, and a new price is a new project.
48
+ */ /** The kinds a project can be, each with its published starting price. */ export const SITE_CARE_PROJECT_KINDS = [
49
+ 'landingPage',
50
+ 'redesign',
51
+ 'brandKit',
52
+ 'other'
53
+ ];
54
+ /**
55
+ * The floor each kind's quote may not go below. `other` (a form, photos,
56
+ * copy, ads) has no published starting price, so its floor is the hourly
57
+ * rate alone: one hour at least.
58
+ */ export const SITE_CARE_PROJECT_FLOOR_USD = {
59
+ landingPage: SITE_CARE_PROJECT_LANDING_PAGE_FROM_USD,
60
+ redesign: SITE_CARE_PROJECT_REDESIGN_FROM_USD,
61
+ brandKit: SITE_CARE_PROJECT_BRAND_KIT_FROM_USD,
62
+ other: 0
63
+ };
64
+ /** The kind as a picker labels it. */ export const SITE_CARE_PROJECT_KIND_LABELS = {
65
+ landingPage: 'Landing page',
66
+ redesign: 'Redesign',
67
+ brandKit: 'Logo or brand kit',
68
+ other: 'Other project (no starting price)'
69
+ };
70
+ /** Every project line carries the code every live Aglyn product carries. */ export const SITE_CARE_PROJECT_TAX_CODE = SITE_CARE_EXTRA_TIME_TAX_CODE;
71
+ /** The longest estimate a quote takes, in hours. */ export const SITE_CARE_PROJECT_MAX_HOURS = 400;
72
+ /** The highest price a quote takes, in whole dollars. */ export const SITE_CARE_PROJECT_MAX_USD = 50000;
73
+ export const SITE_CARE_PROJECT_TITLE_MAX = 120;
74
+ export const SITE_CARE_PROJECT_SCOPE_LINES_MAX = 20;
75
+ export const SITE_CARE_PROJECT_SCOPE_LINE_MAX = 200;
76
+ /**
77
+ * How long the owner has to pay a sent invoice. Stripe requires a due date
78
+ * on an invoice it does not charge automatically; the pay link works from
79
+ * the moment it is sent.
80
+ */ export const SITE_CARE_PROJECT_INVOICE_DAYS_UNTIL_DUE = 14;
81
+ export const SITE_CARE_PROJECT_STATUS_LABELS = {
82
+ quoted: 'Quoted',
83
+ accepted: 'Accepted, not invoiced',
84
+ invoiced: 'Invoice sent',
85
+ 'deposit-paid': 'Deposit paid',
86
+ launched: 'Launched, balance invoiced',
87
+ paid: 'Paid',
88
+ refunded: 'Refunded'
89
+ };
90
+ /** A project with a price above the line is billed half and half. */ export function siteCareProjectSplit(priceUsd) {
91
+ return priceUsd > SITE_CARE_PROJECT_SPLIT_ABOVE_USD ? 'half' : 'full';
92
+ }
93
+ /** The estimate rounded up to the whole hour, ignoring float dust (2.0000001 is 2). */ export function siteCareProjectBilledHours(estimatedHours) {
94
+ return Math.max(1, Math.ceil(Number(estimatedHours) - 1e-9));
95
+ }
96
+ /**
97
+ * The quote from what staff typed: hours, a kind, and optionally a price
98
+ * above the computed one. A price below it is refused, never lowered to.
99
+ */ export function siteCareProjectQuote(input) {
100
+ var _input_kind;
101
+ const kind = String((_input_kind = input.kind) != null ? _input_kind : '');
102
+ if (!SITE_CARE_PROJECT_KINDS.includes(kind)) return {
103
+ ok: false,
104
+ error: 'Choose what kind of project it is.'
105
+ };
106
+ const estimatedHours = Number(input.estimatedHours);
107
+ if (!Number.isFinite(estimatedHours) || estimatedHours <= 0 || estimatedHours > SITE_CARE_PROJECT_MAX_HOURS) {
108
+ return {
109
+ ok: false,
110
+ error: `Enter the estimated hours, more than 0 and at most ${SITE_CARE_PROJECT_MAX_HOURS}.`
111
+ };
112
+ }
113
+ const billedHours = siteCareProjectBilledHours(estimatedHours);
114
+ const floorUsd = SITE_CARE_PROJECT_FLOOR_USD[kind];
115
+ const computedUsd = Math.max(billedHours * SITE_CARE_EXTRA_HOUR_USD, floorUsd);
116
+ const typed = input.priceUsd;
117
+ let priceUsd = computedUsd;
118
+ if (typed !== undefined && typed !== null && typed !== '') {
119
+ const price = Number(typed);
120
+ if (!Number.isInteger(price) || price < 1) return {
121
+ ok: false,
122
+ error: 'Enter the price as a whole number of dollars.'
123
+ };
124
+ if (price < computedUsd) {
125
+ return {
126
+ ok: false,
127
+ error: `The price can only be rounded up: it is at least ${formatSiteCareUsd(computedUsd * 100)} for this estimate.`
128
+ };
129
+ }
130
+ priceUsd = price;
131
+ }
132
+ if (priceUsd > SITE_CARE_PROJECT_MAX_USD) {
133
+ return {
134
+ ok: false,
135
+ error: `A project is at most ${formatSiteCareUsd(SITE_CARE_PROJECT_MAX_USD * 100)}.`
136
+ };
137
+ }
138
+ return {
139
+ ok: true,
140
+ quote: {
141
+ kind,
142
+ estimatedHours,
143
+ billedHours,
144
+ rateUsd: SITE_CARE_EXTRA_HOUR_USD,
145
+ floorUsd,
146
+ computedUsd,
147
+ priceUsd,
148
+ split: siteCareProjectSplit(priceUsd)
149
+ }
150
+ };
151
+ }
152
+ /** The invoices a quote is billed on, in order, with their amounts in cents. */ export function siteCareProjectInvoicePlan(quote) {
153
+ const cents = Math.round(quote.priceUsd * 100);
154
+ if (quote.split === 'full') return [
155
+ {
156
+ kind: 'full',
157
+ amountCents: cents
158
+ }
159
+ ];
160
+ const deposit = Math.ceil(cents / 2);
161
+ return [
162
+ {
163
+ kind: 'deposit',
164
+ amountCents: deposit
165
+ },
166
+ {
167
+ kind: 'final',
168
+ amountCents: cents - deposit
169
+ }
170
+ ];
171
+ }
172
+ /** The amount of one invoice of a quote, in cents. */ export function siteCareProjectInvoiceAmount(quote, kind) {
173
+ var _ref;
174
+ var _siteCareProjectInvoicePlan_find;
175
+ return (_ref = (_siteCareProjectInvoicePlan_find = siteCareProjectInvoicePlan(quote).find((row)=>row.kind === kind)) == null ? void 0 : _siteCareProjectInvoicePlan_find.amountCents) != null ? _ref : 0;
176
+ }
177
+ /** A trimmed title, or an error. */ export function readSiteCareProjectTitle(raw) {
178
+ const title = typeof raw === 'string' ? raw.trim().replace(/\s+/g, ' ').slice(0, SITE_CARE_PROJECT_TITLE_MAX) : '';
179
+ return title ? {
180
+ ok: true,
181
+ title
182
+ } : {
183
+ ok: false,
184
+ error: 'Give the project a name the owner will recognize.'
185
+ };
186
+ }
187
+ /**
188
+ * Scope lines from a list or from text with one deliverable per line: each
189
+ * trimmed, blanks dropped, bullets stripped, at least one.
190
+ */ export function readSiteCareProjectScope(raw) {
191
+ const rows = Array.isArray(raw) ? raw : typeof raw === 'string' ? raw.split(/\r?\n/) : [];
192
+ const scope = rows.map((row)=>String(row != null ? row : '').replace(/^\s*(?:[-*•]|\d+[.)])\s+/, '').trim().slice(0, SITE_CARE_PROJECT_SCOPE_LINE_MAX)).filter(Boolean);
193
+ if (!scope.length) return {
194
+ ok: false,
195
+ error: 'List what the price covers, one deliverable per line.'
196
+ };
197
+ if (scope.length > SITE_CARE_PROJECT_SCOPE_LINES_MAX) {
198
+ return {
199
+ ok: false,
200
+ error: `List at most ${SITE_CARE_PROJECT_SCOPE_LINES_MAX} deliverables.`
201
+ };
202
+ }
203
+ return {
204
+ ok: true,
205
+ scope
206
+ };
207
+ }
208
+ const INVOICE_STATUSES = [
209
+ 'creating',
210
+ 'failed',
211
+ 'open',
212
+ 'paid',
213
+ 'voided',
214
+ 'credited',
215
+ 'refunded'
216
+ ];
217
+ function readInvoice(value) {
218
+ var _row_description, _row_idempotencyKey;
219
+ if (!value || typeof value !== 'object') return null;
220
+ const row = value;
221
+ const kind = row.kind === 'deposit' || row.kind === 'final' ? row.kind : row.kind === 'full' ? 'full' : null;
222
+ if (!kind) return null;
223
+ const text = (v)=>typeof v === 'string' && v ? v : null;
224
+ return {
225
+ seq: Number(row.seq) || 0,
226
+ kind,
227
+ status: INVOICE_STATUSES.includes(row.status) ? row.status : 'failed',
228
+ amountCents: Number(row.amountCents) || 0,
229
+ description: String((_row_description = row.description) != null ? _row_description : ''),
230
+ idempotencyKey: String((_row_idempotencyKey = row.idempotencyKey) != null ? _row_idempotencyKey : ''),
231
+ invoiceId: text(row.invoiceId),
232
+ invoiceItemId: text(row.invoiceItemId),
233
+ number: text(row.number),
234
+ hostedInvoiceUrl: text(row.hostedInvoiceUrl),
235
+ totalCents: Number(row.totalCents) || 0,
236
+ amountPaidCents: Number(row.amountPaidCents) || 0,
237
+ creditNoteId: text(row.creditNoteId),
238
+ refundId: text(row.refundId),
239
+ error: text(row.error),
240
+ createdAtMs: Number(row.createdAtMs) || 0,
241
+ updatedAtMs: Number(row.updatedAtMs) || 0,
242
+ paidAtMs: Number(row.paidAtMs) || null
243
+ };
244
+ }
245
+ /** A stored project read back with every field present, or null when it is not one. */ export function readSiteCareProject(value) {
246
+ var _stored_orgId, _stored_title, _stored_launchedByUid, _stored_createdByUid, _stored_createdByEmail;
247
+ if (!value || typeof value !== 'object') return null;
248
+ const stored = value;
249
+ const quote = stored.quote;
250
+ if (!quote || !SITE_CARE_PROJECT_KINDS.includes(quote.kind)) return null;
251
+ const priceUsd = Number(quote.priceUsd) || 0;
252
+ const acceptanceRecord = readOwnerApprovalRecord(stored.acceptance);
253
+ const acceptance = acceptanceRecord ? _extends({}, acceptanceRecord, {
254
+ quoteVersion: Number(stored.acceptance.quoteVersion) || 0,
255
+ priceUsd: Number(stored.acceptance.priceUsd) || 0
256
+ }) : null;
257
+ const project = {
258
+ orgId: String((_stored_orgId = stored.orgId) != null ? _stored_orgId : ''),
259
+ title: String((_stored_title = stored.title) != null ? _stored_title : ''),
260
+ scope: Array.isArray(stored.scope) ? stored.scope.map(String) : [],
261
+ quote: {
262
+ kind: quote.kind,
263
+ estimatedHours: Number(quote.estimatedHours) || 0,
264
+ billedHours: Number(quote.billedHours) || 0,
265
+ rateUsd: Number(quote.rateUsd) || SITE_CARE_EXTRA_HOUR_USD,
266
+ floorUsd: Number(quote.floorUsd) || 0,
267
+ computedUsd: Number(quote.computedUsd) || 0,
268
+ priceUsd,
269
+ split: quote.split === 'half' ? 'half' : 'full'
270
+ },
271
+ quoteVersion: Number(stored.quoteVersion) || 1,
272
+ acceptance,
273
+ invoices: (Array.isArray(stored.invoices) ? stored.invoices : []).map(readInvoice).filter((row)=>Boolean(row)),
274
+ invoiceSeq: Number(stored.invoiceSeq) || 0,
275
+ firstInvoiceSentAtMs: Number(stored.firstInvoiceSentAtMs) || null,
276
+ launchedAtMs: Number(stored.launchedAtMs) || null,
277
+ launchedByUid: (_stored_launchedByUid = stored.launchedByUid) != null ? _stored_launchedByUid : null,
278
+ status: 'quoted',
279
+ createdAtMs: Number(stored.createdAtMs) || 0,
280
+ createdByUid: (_stored_createdByUid = stored.createdByUid) != null ? _stored_createdByUid : null,
281
+ createdByEmail: (_stored_createdByEmail = stored.createdByEmail) != null ? _stored_createdByEmail : null,
282
+ updatedAtMs: Number(stored.updatedAtMs) || 0
283
+ };
284
+ project.status = siteCareProjectStatus(project);
285
+ return project;
286
+ }
287
+ /** The newest invoice of a kind, whatever its status. */ export function latestSiteCareProjectInvoice(project, kind) {
288
+ var _project_invoices_filter_sort_;
289
+ return (_project_invoices_filter_sort_ = project.invoices.filter((row)=>row.kind === kind).sort((a, b)=>b.seq - a.seq)[0]) != null ? _project_invoices_filter_sort_ : null;
290
+ }
291
+ /** An invoice that is out, or on its way out: one of a kind at a time. */ function isLive(invoice) {
292
+ return Boolean(invoice) && (invoice == null ? void 0 : invoice.status) !== 'voided' && (invoice == null ? void 0 : invoice.status) !== 'credited';
293
+ }
294
+ /** Whether the owner's recorded yes is to the quote as it stands. */ export function siteCareProjectAcceptanceIsCurrent(project) {
295
+ var _project_acceptance;
296
+ return Boolean(project.acceptance) && ((_project_acceptance = project.acceptance) == null ? void 0 : _project_acceptance.quoteVersion) === project.quoteVersion;
297
+ }
298
+ /** Where a project stands, from its acceptance, invoices and launch. */ export function siteCareProjectStatus(project) {
299
+ const before = siteCareProjectAcceptanceIsCurrent(project) ? 'accepted' : 'quoted';
300
+ const outstanding = (status)=>status === 'creating' || status === 'failed' || status === 'open';
301
+ if (project.quote.split === 'full') {
302
+ const full = latestSiteCareProjectInvoice(project, 'full');
303
+ if (!isLive(full) || !full) return before;
304
+ if (full.status === 'paid') return 'paid';
305
+ if (full.status === 'refunded') return 'refunded';
306
+ return outstanding(full.status) ? 'invoiced' : before;
307
+ }
308
+ const final = latestSiteCareProjectInvoice(project, 'final');
309
+ if (final && isLive(final)) {
310
+ if (final.status === 'paid') return 'paid';
311
+ if (final.status === 'refunded') return 'refunded';
312
+ return 'launched';
313
+ }
314
+ const deposit = latestSiteCareProjectInvoice(project, 'deposit');
315
+ if (!deposit || !isLive(deposit)) return before;
316
+ if (deposit.status === 'refunded') return 'refunded';
317
+ if (deposit.status === 'paid') return project.launchedAtMs ? 'launched' : 'deposit-paid';
318
+ return 'invoiced';
319
+ }
320
+ /**
321
+ * Whether scope and price may still change: until the first invoice
322
+ * reaches the owner, and never while one is being made.
323
+ */ export function siteCareProjectQuoteIsEditable(project) {
324
+ if (project.firstInvoiceSentAtMs) return false;
325
+ return !project.invoices.some((row)=>row.status === 'creating' || row.status === 'failed' && row.invoiceId);
326
+ }
327
+ /**
328
+ * The invoice the project may raise now, or why it may not. The first is
329
+ * raised on the owner's current yes; the balance of a half-and-half project
330
+ * only once it is launched and its deposit is paid.
331
+ */ export function siteCareProjectNextInvoice(project) {
332
+ if (project.invoices.some((row)=>row.status === 'creating' || row.status === 'failed')) {
333
+ return {
334
+ ok: false,
335
+ error: 'An invoice is still being made. Retry it before raising another.'
336
+ };
337
+ }
338
+ const first = project.quote.split === 'full' ? 'full' : 'deposit';
339
+ const firstInvoice = latestSiteCareProjectInvoice(project, first);
340
+ if (!isLive(firstInvoice)) {
341
+ if (!siteCareProjectAcceptanceIsCurrent(project)) {
342
+ return {
343
+ ok: false,
344
+ error: project.acceptance ? 'The quote changed after the owner said yes. Record their yes to this quote first.' : 'Record how and when the owner said yes before anything is invoiced.'
345
+ };
346
+ }
347
+ return {
348
+ ok: true,
349
+ kind: first,
350
+ amountCents: siteCareProjectInvoiceAmount(project.quote, first)
351
+ };
352
+ }
353
+ if (first === 'full') return {
354
+ ok: false,
355
+ error: 'This project is billed in full on one invoice, already sent.'
356
+ };
357
+ if (isLive(latestSiteCareProjectInvoice(project, 'final'))) {
358
+ return {
359
+ ok: false,
360
+ error: 'Both halves of this project are already invoiced.'
361
+ };
362
+ }
363
+ if ((firstInvoice == null ? void 0 : firstInvoice.status) !== 'paid') return {
364
+ ok: false,
365
+ error: 'The balance is invoiced once the deposit is paid.'
366
+ };
367
+ if (!project.launchedAtMs) return {
368
+ ok: false,
369
+ error: 'The balance is invoiced when the project is marked launched.'
370
+ };
371
+ return {
372
+ ok: true,
373
+ kind: 'final',
374
+ amountCents: siteCareProjectInvoiceAmount(project.quote, 'final')
375
+ };
376
+ }
377
+ /** "deposit", "balance", "invoice": what an invoice is to the owner. */ export function siteCareProjectInvoiceWords(kind) {
378
+ return kind === 'deposit' ? 'deposit' : kind === 'final' ? 'balance' : 'invoice';
379
+ }
380
+ /** How the quote is billed, in one sentence the owner reads. */ export function siteCareProjectSplitSentence(quote) {
381
+ const price = formatSiteCareUsd(quote.priceUsd * 100);
382
+ if (quote.split === 'full') return `${price}, paid in full before work starts.`;
383
+ const [deposit, final] = siteCareProjectInvoicePlan(quote);
384
+ return `${price}: half (${formatSiteCareUsd(deposit.amountCents)}) before work starts and half (${formatSiteCareUsd(final.amountCents)}) at launch.`;
385
+ }
386
+ /** The Stripe line's description for one invoice of a project. */ export function siteCareProjectInvoiceDescription(project, kind) {
387
+ const price = formatSiteCareUsd(project.quote.priceUsd * 100);
388
+ const what = kind === 'deposit' ? `deposit, half of the ${price} quote` : kind === 'final' ? `balance at launch, half of the ${price} quote` : `the ${price} quote, in full`;
389
+ return `${project.title}: ${what}`.slice(0, 480);
390
+ }
391
+ /** The invoice's memo: what the price covers, one deliverable per line. */ export function siteCareProjectInvoiceMemo(project) {
392
+ return [
393
+ 'What this covers:',
394
+ ...project.scope.map((line)=>`- ${line}`)
395
+ ].join('\n').slice(0, 1500);
396
+ }
397
+ /**
398
+ * The acceptance confirmation's one sentence, in the owner's words: what
399
+ * they agreed to, how and when.
400
+ */ export function siteCareProjectAcceptanceSentence(record, project, formatDate) {
401
+ return `You accepted our quote for “${project.title}”: ${siteCareProjectSplitSentence(project.quote)} ` + `You said yes ${OWNER_APPROVAL_CHANNEL_WORDS[record.channel]} on ${formatDate(record.approvedAtMs)}.`;
402
+ }
403
+ /** What the owner sees for one invoice on Billing. */ export function siteCareProjectInvoiceLine(invoice) {
404
+ const amount = formatSiteCareUsd(invoice.amountCents);
405
+ const what = siteCareProjectInvoiceWords(invoice.kind);
406
+ const capital = what.charAt(0).toUpperCase() + what.slice(1);
407
+ switch(invoice.status){
408
+ case 'paid':
409
+ return `${capital} ${amount}: paid`;
410
+ case 'open':
411
+ return `${capital} ${amount}: waiting for payment`;
412
+ case 'voided':
413
+ return `${capital} ${amount}: canceled, nothing is owed`;
414
+ case 'credited':
415
+ return `${capital} ${amount}: credited, nothing is owed`;
416
+ case 'refunded':
417
+ return `${capital} ${amount}: refunded`;
418
+ default:
419
+ return `${capital} ${amount}: being prepared`;
420
+ }
421
+ }
422
+
423
+ //# sourceMappingURL=site-care-project.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/site-care-project.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 {\n OWNER_APPROVAL_CHANNEL_WORDS,\n readOwnerApprovalRecord,\n type OwnerApprovalRecord,\n} from './owner-approval-record'\nimport {\n SITE_CARE_EXTRA_HOUR_USD,\n SITE_CARE_PROJECT_BRAND_KIT_FROM_USD,\n SITE_CARE_PROJECT_LANDING_PAGE_FROM_USD,\n SITE_CARE_PROJECT_REDESIGN_FROM_USD,\n SITE_CARE_PROJECT_SPLIT_ABOVE_USD,\n} from './plan-entitlements'\nimport { formatSiteCareUsd, SITE_CARE_EXTRA_TIME_TAX_CODE } from './site-care'\n\n/**\n * SITE CARE PROJECTS: THE ARITHMETIC AND THE RECORD (AGL-3970).\n *\n * Site Care covers changes to what a site already has. Work that adds\n * something — a page, a form, a design, a campaign — is a PROJECT, billed as\n * one flat quote from the staff \"Charge for work\" on the staff organization\n * page, on a one-off Stripe invoice the owner pays from their phone. This\n * module is everything about that which needs no Firestore and no Stripe:\n * the quote, the split, where a project stands, and the sentences the owner\n * reads. The server module (`@aglyn/tenant-data-admin/server/site-care-projects`),\n * the staff card, the owner's Billing card and the reconciliation all read\n * it, so they cannot disagree.\n *\n * ## The rules, from the Decision Log of 2026-10-19\n *\n * - The quote is the estimated hours × `SITE_CARE_EXTRA_HOUR_USD`, the\n * hours rounded UP to the whole hour, and never below the published\n * starting price of the project's kind. Staff may raise it, never lower\n * it. It is one number, given before work starts and fixed once given.\n * - Priced above `SITE_CARE_PROJECT_SPLIT_ABOVE_USD`, it is billed half up\n * front and half at launch: two invoices from the same record. At or\n * below, in full up front.\n * - Nothing is invoiced without the owner's recorded yes to THIS quote\n * (`owner-approval-record`): change the quote and the yes must be\n * recorded again.\n * - Scope and price are edited, with a reason, only until the first invoice\n * is sent. After that a wrong invoice is voided, credited or refunded from\n * the record, and a new price is a new project.\n */\n\n/** The kinds a project can be, each with its published starting price. */\nexport const SITE_CARE_PROJECT_KINDS = ['landingPage', 'redesign', 'brandKit', 'other'] as const\n\nexport type SiteCareProjectKind = (typeof SITE_CARE_PROJECT_KINDS)[number]\n\n/**\n * The floor each kind's quote may not go below. `other` (a form, photos,\n * copy, ads) has no published starting price, so its floor is the hourly\n * rate alone: one hour at least.\n */\nexport const SITE_CARE_PROJECT_FLOOR_USD: Readonly<Record<SiteCareProjectKind, number>> = {\n landingPage: SITE_CARE_PROJECT_LANDING_PAGE_FROM_USD,\n redesign: SITE_CARE_PROJECT_REDESIGN_FROM_USD,\n brandKit: SITE_CARE_PROJECT_BRAND_KIT_FROM_USD,\n other: 0,\n}\n\n/** The kind as a picker labels it. */\nexport const SITE_CARE_PROJECT_KIND_LABELS: Readonly<Record<SiteCareProjectKind, string>> = {\n landingPage: 'Landing page',\n redesign: 'Redesign',\n brandKit: 'Logo or brand kit',\n other: 'Other project (no starting price)',\n}\n\n/** Every project line carries the code every live Aglyn product carries. */\nexport const SITE_CARE_PROJECT_TAX_CODE = SITE_CARE_EXTRA_TIME_TAX_CODE\n\n/** The longest estimate a quote takes, in hours. */\nexport const SITE_CARE_PROJECT_MAX_HOURS = 400\n\n/** The highest price a quote takes, in whole dollars. */\nexport const SITE_CARE_PROJECT_MAX_USD = 50_000\n\nexport const SITE_CARE_PROJECT_TITLE_MAX = 120\nexport const SITE_CARE_PROJECT_SCOPE_LINES_MAX = 20\nexport const SITE_CARE_PROJECT_SCOPE_LINE_MAX = 200\n\n/**\n * How long the owner has to pay a sent invoice. Stripe requires a due date\n * on an invoice it does not charge automatically; the pay link works from\n * the moment it is sent.\n */\nexport const SITE_CARE_PROJECT_INVOICE_DAYS_UNTIL_DUE = 14\n\nexport type SiteCareProjectSplit = 'full' | 'half'\n\n/** Which invoice of a project: the whole price, or one of its halves. */\nexport type SiteCareProjectInvoiceKind = 'full' | 'deposit' | 'final'\n\n/**\n * Where one invoice stands, as the platform recorded it. `creating` is\n * written BEFORE the Stripe calls, so a call that succeeded with no row\n * behind it shows as `creating` — which the reconciliation reports and a\n * retry settles under the same idempotency key.\n */\nexport type SiteCareProjectInvoiceStatus =\n | 'creating'\n | 'failed'\n | 'open'\n | 'paid'\n | 'voided'\n | 'credited'\n | 'refunded'\n\n/** Where the project stands, derived from its record (never typed in). */\nexport type SiteCareProjectStatus =\n | 'quoted'\n | 'accepted'\n | 'invoiced'\n | 'deposit-paid'\n | 'launched'\n | 'paid'\n | 'refunded'\n\nexport const SITE_CARE_PROJECT_STATUS_LABELS: Readonly<Record<SiteCareProjectStatus, string>> = {\n quoted: 'Quoted',\n accepted: 'Accepted, not invoiced',\n invoiced: 'Invoice sent',\n 'deposit-paid': 'Deposit paid',\n launched: 'Launched, balance invoiced',\n paid: 'Paid',\n refunded: 'Refunded',\n}\n\nexport interface SiteCareProjectQuote {\n kind: SiteCareProjectKind\n /** The hours staff estimated, as typed. */\n estimatedHours: number\n /** The estimate rounded up to the whole hour. */\n billedHours: number\n rateUsd: number\n floorUsd: number\n /** `max(billedHours × rate, floor)`: the least the quote may be. */\n computedUsd: number\n /** The flat price: `computedUsd`, or more if staff rounded it up. */\n priceUsd: number\n split: SiteCareProjectSplit\n}\n\nexport interface SiteCareProjectInvoice {\n /** 1 for the first invoice the project raised; each later one is higher. */\n seq: number\n kind: SiteCareProjectInvoiceKind\n status: SiteCareProjectInvoiceStatus\n /** Before tax, in cents. */\n amountCents: number\n /** The line's description, exactly as it was sent. */\n description: string\n /** The key every Stripe write for this invoice derives from. */\n idempotencyKey: string\n invoiceId: string | null\n invoiceItemId: string | null\n number: string | null\n hostedInvoiceUrl: string | null\n /** The finalized total, tax included, in cents. */\n totalCents: number\n amountPaidCents: number\n creditNoteId: string | null\n refundId: string | null\n error: string | null\n createdAtMs: number\n updatedAtMs: number\n paidAtMs: number | null\n}\n\n/** The owner's yes to a quote: the shared record, pinned to the quote it agreed to. */\nexport type SiteCareProjectAcceptance = OwnerApprovalRecord & {\n /** The quote version the owner said yes to. */\n quoteVersion: number\n priceUsd: number\n}\n\nexport interface SiteCareProject {\n orgId: string\n title: string\n /** One line per deliverable, plain text. */\n scope: string[]\n quote: SiteCareProjectQuote\n /** 1 for the first quote; each edit is one higher. */\n quoteVersion: number\n acceptance: SiteCareProjectAcceptance | null\n invoices: SiteCareProjectInvoice[]\n invoiceSeq: number\n /** When the first invoice reached the owner; the quote is fixed from then. */\n firstInvoiceSentAtMs: number | null\n launchedAtMs: number | null\n launchedByUid: string | null\n status: SiteCareProjectStatus\n createdAtMs: number\n createdByUid: string | null\n createdByEmail: string | null\n updatedAtMs: number\n}\n\nexport type SiteCareProjectResult<T> = ({ ok: true } & T) | { ok: false; error: string }\n\n/** A project with a price above the line is billed half and half. */\nexport function siteCareProjectSplit(priceUsd: number): SiteCareProjectSplit {\n return priceUsd > SITE_CARE_PROJECT_SPLIT_ABOVE_USD ? 'half' : 'full'\n}\n\n/** The estimate rounded up to the whole hour, ignoring float dust (2.0000001 is 2). */\nexport function siteCareProjectBilledHours(estimatedHours: number): number {\n return Math.max(1, Math.ceil(Number(estimatedHours) - 1e-9))\n}\n\n/**\n * The quote from what staff typed: hours, a kind, and optionally a price\n * above the computed one. A price below it is refused, never lowered to.\n */\nexport function siteCareProjectQuote(input: {\n kind: unknown\n estimatedHours: unknown\n priceUsd?: unknown\n}): SiteCareProjectResult<{ quote: SiteCareProjectQuote }> {\n const kind = String(input.kind ?? '') as SiteCareProjectKind\n if (!SITE_CARE_PROJECT_KINDS.includes(kind)) return { ok: false, error: 'Choose what kind of project it is.' }\n const estimatedHours = Number(input.estimatedHours)\n if (!Number.isFinite(estimatedHours) || estimatedHours <= 0 || estimatedHours > SITE_CARE_PROJECT_MAX_HOURS) {\n return { ok: false, error: `Enter the estimated hours, more than 0 and at most ${SITE_CARE_PROJECT_MAX_HOURS}.` }\n }\n const billedHours = siteCareProjectBilledHours(estimatedHours)\n const floorUsd = SITE_CARE_PROJECT_FLOOR_USD[kind]\n const computedUsd = Math.max(billedHours * SITE_CARE_EXTRA_HOUR_USD, floorUsd)\n const typed = input.priceUsd\n let priceUsd = computedUsd\n if (typed !== undefined && typed !== null && typed !== '') {\n const price = Number(typed)\n if (!Number.isInteger(price) || price < 1) return { ok: false, error: 'Enter the price as a whole number of dollars.' }\n if (price < computedUsd) {\n return {\n ok: false,\n error: `The price can only be rounded up: it is at least ${formatSiteCareUsd(computedUsd * 100)} for this estimate.`,\n }\n }\n priceUsd = price\n }\n if (priceUsd > SITE_CARE_PROJECT_MAX_USD) {\n return { ok: false, error: `A project is at most ${formatSiteCareUsd(SITE_CARE_PROJECT_MAX_USD * 100)}.` }\n }\n return {\n ok: true,\n quote: {\n kind,\n estimatedHours,\n billedHours,\n rateUsd: SITE_CARE_EXTRA_HOUR_USD,\n floorUsd,\n computedUsd,\n priceUsd,\n split: siteCareProjectSplit(priceUsd),\n },\n }\n}\n\n/** The invoices a quote is billed on, in order, with their amounts in cents. */\nexport function siteCareProjectInvoicePlan(\n quote: Pick<SiteCareProjectQuote, 'priceUsd' | 'split'>,\n): Array<{ kind: SiteCareProjectInvoiceKind; amountCents: number }> {\n const cents = Math.round(quote.priceUsd * 100)\n if (quote.split === 'full') return [{ kind: 'full', amountCents: cents }]\n const deposit = Math.ceil(cents / 2)\n return [\n { kind: 'deposit', amountCents: deposit },\n { kind: 'final', amountCents: cents - deposit },\n ]\n}\n\n/** The amount of one invoice of a quote, in cents. */\nexport function siteCareProjectInvoiceAmount(\n quote: Pick<SiteCareProjectQuote, 'priceUsd' | 'split'>,\n kind: SiteCareProjectInvoiceKind,\n): number {\n return siteCareProjectInvoicePlan(quote).find((row) => row.kind === kind)?.amountCents ?? 0\n}\n\n/** A trimmed title, or an error. */\nexport function readSiteCareProjectTitle(raw: unknown): SiteCareProjectResult<{ title: string }> {\n const title = typeof raw === 'string' ? raw.trim().replace(/\\s+/g, ' ').slice(0, SITE_CARE_PROJECT_TITLE_MAX) : ''\n return title ? { ok: true, title } : { ok: false, error: 'Give the project a name the owner will recognize.' }\n}\n\n/**\n * Scope lines from a list or from text with one deliverable per line: each\n * trimmed, blanks dropped, bullets stripped, at least one.\n */\nexport function readSiteCareProjectScope(raw: unknown): SiteCareProjectResult<{ scope: string[] }> {\n const rows = Array.isArray(raw) ? raw : typeof raw === 'string' ? raw.split(/\\r?\\n/) : []\n const scope = rows\n .map((row) =>\n String(row ?? '')\n .replace(/^\\s*(?:[-*•]|\\d+[.)])\\s+/, '')\n .trim()\n .slice(0, SITE_CARE_PROJECT_SCOPE_LINE_MAX),\n )\n .filter(Boolean)\n if (!scope.length) return { ok: false, error: 'List what the price covers, one deliverable per line.' }\n if (scope.length > SITE_CARE_PROJECT_SCOPE_LINES_MAX) {\n return { ok: false, error: `List at most ${SITE_CARE_PROJECT_SCOPE_LINES_MAX} deliverables.` }\n }\n return { ok: true, scope }\n}\n\nconst INVOICE_STATUSES: readonly SiteCareProjectInvoiceStatus[] = [\n 'creating',\n 'failed',\n 'open',\n 'paid',\n 'voided',\n 'credited',\n 'refunded',\n]\n\nfunction readInvoice(value: unknown): SiteCareProjectInvoice | null {\n if (!value || typeof value !== 'object') return null\n const row = value as Partial<SiteCareProjectInvoice>\n const kind = row.kind === 'deposit' || row.kind === 'final' ? row.kind : row.kind === 'full' ? 'full' : null\n if (!kind) return null\n const text = (v: unknown) => (typeof v === 'string' && v ? v : null)\n return {\n seq: Number(row.seq) || 0,\n kind,\n status: INVOICE_STATUSES.includes(row.status as SiteCareProjectInvoiceStatus)\n ? (row.status as SiteCareProjectInvoiceStatus)\n : 'failed',\n amountCents: Number(row.amountCents) || 0,\n description: String(row.description ?? ''),\n idempotencyKey: String(row.idempotencyKey ?? ''),\n invoiceId: text(row.invoiceId),\n invoiceItemId: text(row.invoiceItemId),\n number: text(row.number),\n hostedInvoiceUrl: text(row.hostedInvoiceUrl),\n totalCents: Number(row.totalCents) || 0,\n amountPaidCents: Number(row.amountPaidCents) || 0,\n creditNoteId: text(row.creditNoteId),\n refundId: text(row.refundId),\n error: text(row.error),\n createdAtMs: Number(row.createdAtMs) || 0,\n updatedAtMs: Number(row.updatedAtMs) || 0,\n paidAtMs: Number(row.paidAtMs) || null,\n }\n}\n\n/** A stored project read back with every field present, or null when it is not one. */\nexport function readSiteCareProject(value: unknown): SiteCareProject | null {\n if (!value || typeof value !== 'object') return null\n const stored = value as Partial<SiteCareProject>\n const quote = stored.quote as Partial<SiteCareProjectQuote> | undefined\n if (!quote || !SITE_CARE_PROJECT_KINDS.includes(quote.kind as SiteCareProjectKind)) return null\n const priceUsd = Number(quote.priceUsd) || 0\n const acceptanceRecord = readOwnerApprovalRecord(stored.acceptance)\n const acceptance: SiteCareProjectAcceptance | null = acceptanceRecord\n ? {\n ...acceptanceRecord,\n quoteVersion: Number((stored.acceptance as Partial<SiteCareProjectAcceptance>).quoteVersion) || 0,\n priceUsd: Number((stored.acceptance as Partial<SiteCareProjectAcceptance>).priceUsd) || 0,\n }\n : null\n const project: SiteCareProject = {\n orgId: String(stored.orgId ?? ''),\n title: String(stored.title ?? ''),\n scope: Array.isArray(stored.scope) ? stored.scope.map(String) : [],\n quote: {\n kind: quote.kind as SiteCareProjectKind,\n estimatedHours: Number(quote.estimatedHours) || 0,\n billedHours: Number(quote.billedHours) || 0,\n rateUsd: Number(quote.rateUsd) || SITE_CARE_EXTRA_HOUR_USD,\n floorUsd: Number(quote.floorUsd) || 0,\n computedUsd: Number(quote.computedUsd) || 0,\n priceUsd,\n split: quote.split === 'half' ? 'half' : 'full',\n },\n quoteVersion: Number(stored.quoteVersion) || 1,\n acceptance,\n invoices: (Array.isArray(stored.invoices) ? stored.invoices : [])\n .map(readInvoice)\n .filter((row): row is SiteCareProjectInvoice => Boolean(row)),\n invoiceSeq: Number(stored.invoiceSeq) || 0,\n firstInvoiceSentAtMs: Number(stored.firstInvoiceSentAtMs) || null,\n launchedAtMs: Number(stored.launchedAtMs) || null,\n launchedByUid: stored.launchedByUid ?? null,\n status: 'quoted',\n createdAtMs: Number(stored.createdAtMs) || 0,\n createdByUid: stored.createdByUid ?? null,\n createdByEmail: stored.createdByEmail ?? null,\n updatedAtMs: Number(stored.updatedAtMs) || 0,\n }\n project.status = siteCareProjectStatus(project)\n return project\n}\n\n/** The newest invoice of a kind, whatever its status. */\nexport function latestSiteCareProjectInvoice(\n project: Pick<SiteCareProject, 'invoices'>,\n kind: SiteCareProjectInvoiceKind,\n): SiteCareProjectInvoice | null {\n return (\n project.invoices\n .filter((row) => row.kind === kind)\n .sort((a, b) => b.seq - a.seq)[0] ?? null\n )\n}\n\n/** An invoice that is out, or on its way out: one of a kind at a time. */\nfunction isLive(invoice: SiteCareProjectInvoice | null): boolean {\n return Boolean(invoice) && invoice?.status !== 'voided' && invoice?.status !== 'credited'\n}\n\n/** Whether the owner's recorded yes is to the quote as it stands. */\nexport function siteCareProjectAcceptanceIsCurrent(\n project: Pick<SiteCareProject, 'acceptance' | 'quoteVersion'>,\n): boolean {\n return Boolean(project.acceptance) && project.acceptance?.quoteVersion === project.quoteVersion\n}\n\n/** Where a project stands, from its acceptance, invoices and launch. */\nexport function siteCareProjectStatus(\n project: Pick<SiteCareProject, 'acceptance' | 'quoteVersion' | 'invoices' | 'launchedAtMs' | 'quote'>,\n): SiteCareProjectStatus {\n const before: SiteCareProjectStatus = siteCareProjectAcceptanceIsCurrent(project) ? 'accepted' : 'quoted'\n const outstanding = (status: SiteCareProjectInvoiceStatus) =>\n status === 'creating' || status === 'failed' || status === 'open'\n if (project.quote.split === 'full') {\n const full = latestSiteCareProjectInvoice(project, 'full')\n if (!isLive(full) || !full) return before\n if (full.status === 'paid') return 'paid'\n if (full.status === 'refunded') return 'refunded'\n return outstanding(full.status) ? 'invoiced' : before\n }\n const final = latestSiteCareProjectInvoice(project, 'final')\n if (final && isLive(final)) {\n if (final.status === 'paid') return 'paid'\n if (final.status === 'refunded') return 'refunded'\n return 'launched'\n }\n const deposit = latestSiteCareProjectInvoice(project, 'deposit')\n if (!deposit || !isLive(deposit)) return before\n if (deposit.status === 'refunded') return 'refunded'\n if (deposit.status === 'paid') return project.launchedAtMs ? 'launched' : 'deposit-paid'\n return 'invoiced'\n}\n\n/**\n * Whether scope and price may still change: until the first invoice\n * reaches the owner, and never while one is being made.\n */\nexport function siteCareProjectQuoteIsEditable(\n project: Pick<SiteCareProject, 'firstInvoiceSentAtMs' | 'invoices'>,\n): boolean {\n if (project.firstInvoiceSentAtMs) return false\n return !project.invoices.some((row) => row.status === 'creating' || (row.status === 'failed' && row.invoiceId))\n}\n\n/**\n * The invoice the project may raise now, or why it may not. The first is\n * raised on the owner's current yes; the balance of a half-and-half project\n * only once it is launched and its deposit is paid.\n */\nexport function siteCareProjectNextInvoice(\n project: Pick<SiteCareProject, 'acceptance' | 'quoteVersion' | 'invoices' | 'launchedAtMs' | 'quote'>,\n): SiteCareProjectResult<{ kind: SiteCareProjectInvoiceKind; amountCents: number }> {\n if (project.invoices.some((row) => row.status === 'creating' || row.status === 'failed')) {\n return { ok: false, error: 'An invoice is still being made. Retry it before raising another.' }\n }\n const first: SiteCareProjectInvoiceKind = project.quote.split === 'full' ? 'full' : 'deposit'\n const firstInvoice = latestSiteCareProjectInvoice(project, first)\n if (!isLive(firstInvoice)) {\n if (!siteCareProjectAcceptanceIsCurrent(project)) {\n return {\n ok: false,\n error: project.acceptance\n ? 'The quote changed after the owner said yes. Record their yes to this quote first.'\n : 'Record how and when the owner said yes before anything is invoiced.',\n }\n }\n return { ok: true, kind: first, amountCents: siteCareProjectInvoiceAmount(project.quote, first) }\n }\n if (first === 'full') return { ok: false, error: 'This project is billed in full on one invoice, already sent.' }\n if (isLive(latestSiteCareProjectInvoice(project, 'final'))) {\n return { ok: false, error: 'Both halves of this project are already invoiced.' }\n }\n if (firstInvoice?.status !== 'paid') return { ok: false, error: 'The balance is invoiced once the deposit is paid.' }\n if (!project.launchedAtMs) return { ok: false, error: 'The balance is invoiced when the project is marked launched.' }\n return { ok: true, kind: 'final', amountCents: siteCareProjectInvoiceAmount(project.quote, 'final') }\n}\n\n/** \"deposit\", \"balance\", \"invoice\": what an invoice is to the owner. */\nexport function siteCareProjectInvoiceWords(kind: SiteCareProjectInvoiceKind): string {\n return kind === 'deposit' ? 'deposit' : kind === 'final' ? 'balance' : 'invoice'\n}\n\n/** How the quote is billed, in one sentence the owner reads. */\nexport function siteCareProjectSplitSentence(quote: Pick<SiteCareProjectQuote, 'priceUsd' | 'split'>): string {\n const price = formatSiteCareUsd(quote.priceUsd * 100)\n if (quote.split === 'full') return `${price}, paid in full before work starts.`\n const [deposit, final] = siteCareProjectInvoicePlan(quote)\n return `${price}: half (${formatSiteCareUsd(deposit.amountCents)}) before work starts and half (${formatSiteCareUsd(final.amountCents)}) at launch.`\n}\n\n/** The Stripe line's description for one invoice of a project. */\nexport function siteCareProjectInvoiceDescription(\n project: Pick<SiteCareProject, 'title' | 'quote'>,\n kind: SiteCareProjectInvoiceKind,\n): string {\n const price = formatSiteCareUsd(project.quote.priceUsd * 100)\n const what =\n kind === 'deposit'\n ? `deposit, half of the ${price} quote`\n : kind === 'final'\n ? `balance at launch, half of the ${price} quote`\n : `the ${price} quote, in full`\n return `${project.title}: ${what}`.slice(0, 480)\n}\n\n/** The invoice's memo: what the price covers, one deliverable per line. */\nexport function siteCareProjectInvoiceMemo(project: Pick<SiteCareProject, 'scope'>): string {\n return ['What this covers:', ...project.scope.map((line) => `- ${line}`)].join('\\n').slice(0, 1500)\n}\n\n/**\n * The acceptance confirmation's one sentence, in the owner's words: what\n * they agreed to, how and when.\n */\nexport function siteCareProjectAcceptanceSentence(\n record: Pick<OwnerApprovalRecord, 'channel' | 'approvedAtMs'>,\n project: Pick<SiteCareProject, 'title' | 'quote'>,\n formatDate: (ms: number) => string,\n): string {\n return (\n `You accepted our quote for “${project.title}”: ${siteCareProjectSplitSentence(project.quote)} ` +\n `You said yes ${OWNER_APPROVAL_CHANNEL_WORDS[record.channel]} on ${formatDate(record.approvedAtMs)}.`\n )\n}\n\n/** What the owner sees for one invoice on Billing. */\nexport function siteCareProjectInvoiceLine(invoice: SiteCareProjectInvoice): string {\n const amount = formatSiteCareUsd(invoice.amountCents)\n const what = siteCareProjectInvoiceWords(invoice.kind)\n const capital = what.charAt(0).toUpperCase() + what.slice(1)\n switch (invoice.status) {\n case 'paid':\n return `${capital} ${amount}: paid`\n case 'open':\n return `${capital} ${amount}: waiting for payment`\n case 'voided':\n return `${capital} ${amount}: canceled, nothing is owed`\n case 'credited':\n return `${capital} ${amount}: credited, nothing is owed`\n case 'refunded':\n return `${capital} ${amount}: refunded`\n default:\n return `${capital} ${amount}: being prepared`\n }\n}\n"],"names":["OWNER_APPROVAL_CHANNEL_WORDS","readOwnerApprovalRecord","SITE_CARE_EXTRA_HOUR_USD","SITE_CARE_PROJECT_BRAND_KIT_FROM_USD","SITE_CARE_PROJECT_LANDING_PAGE_FROM_USD","SITE_CARE_PROJECT_REDESIGN_FROM_USD","SITE_CARE_PROJECT_SPLIT_ABOVE_USD","formatSiteCareUsd","SITE_CARE_EXTRA_TIME_TAX_CODE","SITE_CARE_PROJECT_KINDS","SITE_CARE_PROJECT_FLOOR_USD","landingPage","redesign","brandKit","other","SITE_CARE_PROJECT_KIND_LABELS","SITE_CARE_PROJECT_TAX_CODE","SITE_CARE_PROJECT_MAX_HOURS","SITE_CARE_PROJECT_MAX_USD","SITE_CARE_PROJECT_TITLE_MAX","SITE_CARE_PROJECT_SCOPE_LINES_MAX","SITE_CARE_PROJECT_SCOPE_LINE_MAX","SITE_CARE_PROJECT_INVOICE_DAYS_UNTIL_DUE","SITE_CARE_PROJECT_STATUS_LABELS","quoted","accepted","invoiced","launched","paid","refunded","siteCareProjectSplit","priceUsd","siteCareProjectBilledHours","estimatedHours","Math","max","ceil","Number","siteCareProjectQuote","input","kind","String","includes","ok","error","isFinite","billedHours","floorUsd","computedUsd","typed","undefined","price","isInteger","quote","rateUsd","split","siteCareProjectInvoicePlan","cents","round","amountCents","deposit","siteCareProjectInvoiceAmount","find","row","readSiteCareProjectTitle","raw","title","trim","replace","slice","readSiteCareProjectScope","rows","Array","isArray","scope","map","filter","Boolean","length","INVOICE_STATUSES","readInvoice","value","text","v","seq","status","description","idempotencyKey","invoiceId","invoiceItemId","number","hostedInvoiceUrl","totalCents","amountPaidCents","creditNoteId","refundId","createdAtMs","updatedAtMs","paidAtMs","readSiteCareProject","stored","acceptanceRecord","acceptance","quoteVersion","project","orgId","invoices","invoiceSeq","firstInvoiceSentAtMs","launchedAtMs","launchedByUid","createdByUid","createdByEmail","siteCareProjectStatus","latestSiteCareProjectInvoice","sort","a","b","isLive","invoice","siteCareProjectAcceptanceIsCurrent","before","outstanding","full","final","siteCareProjectQuoteIsEditable","some","siteCareProjectNextInvoice","first","firstInvoice","siteCareProjectInvoiceWords","siteCareProjectSplitSentence","siteCareProjectInvoiceDescription","what","siteCareProjectInvoiceMemo","line","join","siteCareProjectAcceptanceSentence","record","formatDate","channel","approvedAtMs","siteCareProjectInvoiceLine","amount","capital","charAt","toUpperCase"],"mappings":"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"}