@aglyn/aglyn 1.0.0-beta.269 → 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 (121) hide show
  1. package/package.json +11 -11
  2. package/src/lib/app-utils/account-preferences.d.ts +248 -0
  3. package/src/lib/app-utils/account-preferences.js +434 -0
  4. package/src/lib/app-utils/account-preferences.js.map +1 -0
  5. package/src/lib/app-utils/activity-labels.js +2 -0
  6. package/src/lib/app-utils/activity-labels.js.map +1 -1
  7. package/src/lib/app-utils/analytics-events.d.ts +13 -0
  8. package/src/lib/app-utils/analytics-events.js +2 -0
  9. package/src/lib/app-utils/analytics-events.js.map +1 -1
  10. package/src/lib/app-utils/append-only-history.d.ts +94 -0
  11. package/src/lib/app-utils/append-only-history.js +73 -0
  12. package/src/lib/app-utils/append-only-history.js.map +1 -0
  13. package/src/lib/app-utils/bundle-intent.d.ts +186 -0
  14. package/src/lib/app-utils/bundle-intent.js +386 -0
  15. package/src/lib/app-utils/bundle-intent.js.map +1 -0
  16. package/src/lib/app-utils/client-build.d.ts +204 -0
  17. package/src/lib/app-utils/client-build.js +237 -0
  18. package/src/lib/app-utils/client-build.js.map +1 -0
  19. package/src/lib/app-utils/console-routes.d.ts +4 -0
  20. package/src/lib/app-utils/console-routes.js +2 -0
  21. package/src/lib/app-utils/console-routes.js.map +1 -1
  22. package/src/lib/app-utils/country-options.d.ts +27 -0
  23. package/src/lib/app-utils/country-options.js +96 -0
  24. package/src/lib/app-utils/country-options.js.map +1 -0
  25. package/src/lib/app-utils/docs-help-sections.generated.js +7 -5
  26. package/src/lib/app-utils/docs-help-sections.generated.js.map +1 -1
  27. package/src/lib/app-utils/docs-help.generated.d.ts +11 -5
  28. package/src/lib/app-utils/docs-help.generated.js +22 -2
  29. package/src/lib/app-utils/docs-help.generated.js.map +1 -1
  30. package/src/lib/app-utils/docs-index.generated.js +116 -39
  31. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  32. package/src/lib/app-utils/margin-utilization.js +6 -4
  33. package/src/lib/app-utils/margin-utilization.js.map +1 -1
  34. package/src/lib/app-utils/media-filter.d.ts +26 -0
  35. package/src/lib/app-utils/media-filter.js +32 -0
  36. package/src/lib/app-utils/media-filter.js.map +1 -1
  37. package/src/lib/app-utils/mobile-push.d.ts +8 -0
  38. package/src/lib/app-utils/mobile-push.js +4 -1
  39. package/src/lib/app-utils/mobile-push.js.map +1 -1
  40. package/src/lib/app-utils/native-app-association.d.ts +3 -2
  41. package/src/lib/app-utils/native-app-association.js +4 -2
  42. package/src/lib/app-utils/native-app-association.js.map +1 -1
  43. package/src/lib/app-utils/native-nav.d.ts +265 -0
  44. package/src/lib/app-utils/native-nav.js +741 -0
  45. package/src/lib/app-utils/native-nav.js.map +1 -0
  46. package/src/lib/app-utils/native-themes.d.ts +249 -0
  47. package/src/lib/app-utils/native-themes.js +732 -0
  48. package/src/lib/app-utils/native-themes.js.map +1 -0
  49. package/src/lib/app-utils/notifications.d.ts +1 -1
  50. package/src/lib/app-utils/notifications.js +10 -0
  51. package/src/lib/app-utils/notifications.js.map +1 -1
  52. package/src/lib/app-utils/onboarding-deep-link.d.ts +36 -2
  53. package/src/lib/app-utils/onboarding-deep-link.js +48 -5
  54. package/src/lib/app-utils/onboarding-deep-link.js.map +1 -1
  55. package/src/lib/app-utils/one-box-search.d.ts +108 -0
  56. package/src/lib/app-utils/one-box-search.js +167 -0
  57. package/src/lib/app-utils/one-box-search.js.map +1 -0
  58. package/src/lib/app-utils/operator-alerts.js +21 -0
  59. package/src/lib/app-utils/operator-alerts.js.map +1 -1
  60. package/src/lib/app-utils/org-permissions.js +10 -61
  61. package/src/lib/app-utils/org-permissions.js.map +1 -1
  62. package/src/lib/app-utils/org-role-grants.d.ts +95 -0
  63. package/src/lib/app-utils/org-role-grants.js +193 -0
  64. package/src/lib/app-utils/org-role-grants.js.map +1 -0
  65. package/src/lib/app-utils/owner-approval-record.d.ts +102 -0
  66. package/src/lib/app-utils/owner-approval-record.js +148 -0
  67. package/src/lib/app-utils/owner-approval-record.js.map +1 -0
  68. package/src/lib/app-utils/plan-entitlements.d.ts +224 -68
  69. package/src/lib/app-utils/plan-entitlements.js +278 -61
  70. package/src/lib/app-utils/plan-entitlements.js.map +1 -1
  71. package/src/lib/app-utils/release-flags.d.ts +1 -1
  72. package/src/lib/app-utils/release-flags.js +10 -0
  73. package/src/lib/app-utils/release-flags.js.map +1 -1
  74. package/src/lib/app-utils/server.d.ts +4 -0
  75. package/src/lib/app-utils/server.js +7 -0
  76. package/src/lib/app-utils/server.js.map +1 -1
  77. package/src/lib/app-utils/site-care-project.d.ts +227 -0
  78. package/src/lib/app-utils/site-care-project.js +423 -0
  79. package/src/lib/app-utils/site-care-project.js.map +1 -0
  80. package/src/lib/app-utils/site-care.d.ts +268 -0
  81. package/src/lib/app-utils/site-care.js +283 -0
  82. package/src/lib/app-utils/site-care.js.map +1 -0
  83. package/src/lib/app-utils/tenant-dns-targets.d.ts +62 -0
  84. package/src/lib/app-utils/tenant-dns-targets.js +80 -0
  85. package/src/lib/app-utils/tenant-dns-targets.js.map +1 -0
  86. package/src/lib/app-utils/upgrade-proposal.d.ts +29 -4
  87. package/src/lib/app-utils/upgrade-proposal.js +60 -9
  88. package/src/lib/app-utils/upgrade-proposal.js.map +1 -1
  89. package/src/lib/foundation/definitions/org-billing.types.d.ts +54 -10
  90. package/src/lib/foundation/definitions/org-billing.types.js.map +1 -1
  91. package/src/lib/foundation/definitions/organization.types.d.ts +3 -1
  92. package/src/lib/foundation/definitions/organization.types.js.map +1 -1
  93. package/src/lib/foundation/definitions/platform.types.d.ts +9 -0
  94. package/src/lib/foundation/definitions/platform.types.js.map +1 -1
  95. package/src/lib/foundation/index.d.ts +1 -1
  96. package/src/lib/foundation/index.js.map +1 -1
  97. package/src/lib/plugin-manager/feature-plugins.d.ts +86 -0
  98. package/src/lib/plugin-manager/feature-plugins.js +20 -0
  99. package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
  100. package/src/lib/plugin-manager/first-party-plugins.generated.js +22 -1
  101. package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
  102. package/src/lib/plugin-manager/plugin-ai-capabilities.d.ts +19 -0
  103. package/src/lib/plugin-manager/plugin-ai-capabilities.js.map +1 -1
  104. package/src/lib/plugin-manager/plugin-domain-events.d.ts +4 -3
  105. package/src/lib/plugin-manager/plugin-domain-events.js.map +1 -1
  106. package/src/lib/plugin-manager/plugin-domain-search.d.ts +81 -0
  107. package/src/lib/plugin-manager/plugin-domain-search.js +63 -0
  108. package/src/lib/plugin-manager/plugin-domain-search.js.map +1 -0
  109. package/src/lib/plugin-manager/plugin-entitlements.d.ts +23 -5
  110. package/src/lib/plugin-manager/plugin-entitlements.js +10 -0
  111. package/src/lib/plugin-manager/plugin-entitlements.js.map +1 -1
  112. package/src/lib/plugin-manager/plugin-media-ingest.d.ts +25 -0
  113. package/src/lib/plugin-manager/plugin-media-ingest.js.map +1 -1
  114. package/src/lib/plugin-manager/plugin-search-sources.d.ts +85 -0
  115. package/src/lib/plugin-manager/plugin-search-sources.js +91 -0
  116. package/src/lib/plugin-manager/plugin-search-sources.js.map +1 -0
  117. package/src/lib/plugin-manager/plugin-services.js +6 -0
  118. package/src/lib/plugin-manager/plugin-services.js.map +1 -1
  119. package/src/lib/plugin-manager/purchase-gate-preclearance.d.ts +61 -0
  120. package/src/lib/plugin-manager/purchase-gate-preclearance.js +102 -0
  121. package/src/lib/plugin-manager/purchase-gate-preclearance.js.map +1 -0
@@ -0,0 +1,268 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import { type OwnerApprovalRecord } from './owner-approval-record';
18
+ /**
19
+ * SITE CARE REQUESTS: THE ARITHMETIC (AGL-3959).
20
+ *
21
+ * A Site Care owner asks for a change by email or from the console, and the
22
+ * request is a support ticket tagged `siteCare`. Staff open it (a timer
23
+ * starts), make the change, and close it with one number: the minutes. This
24
+ * module is everything about that which needs no Firestore and no Stripe —
25
+ * the month a request counts in, how its minutes are counted against the
26
+ * included hour, what the close allows, what extra time bills, and the
27
+ * statement the month adds up to. The routes, the staff panel, the owner's
28
+ * Billing card and the reconciliation all read it, so they cannot disagree.
29
+ *
30
+ * ## The rules, from the Decision Log of 2026-10-19
31
+ *
32
+ * - The included hour is `SITE_CARE_INCLUDED_MINUTES` a month, no rollover,
33
+ * plus any goodwill minutes staff add to that month. Each request counts
34
+ * against it in `SITE_CARE_INCLUDED_BLOCK_MINUTES` blocks, rounded UP.
35
+ * - A request's share of the hour is fixed when it closes, against what the
36
+ * month's earlier requests left. Correcting one request's minutes later
37
+ * recomputes that request alone, against the same remainder, so a fix
38
+ * never moves a charge on a request nobody touched.
39
+ * - Past the hour, extra time bills at `SITE_CARE_EXTRA_HOUR_USD` per
40
+ * STARTED hour — whole hours, rounded up — and never less than the hours
41
+ * the owner approved. With no approval on file, or more time than was
42
+ * approved, the close is blocked: ask the owner first, or absorb it (no
43
+ * charge, the minutes still count).
44
+ */
45
+ /** The `kind` a Site Care support ticket carries. */
46
+ export declare const SITE_CARE_TICKET_KIND = "siteCare";
47
+ /** Site Care's promise: done within two business days. */
48
+ export declare const SITE_CARE_RESPONSE_BUSINESS_DAYS = 2;
49
+ /** The zone a Site Care month is drawn in: where the work is done. */
50
+ export declare const SITE_CARE_TIME_ZONE = "America/Chicago";
51
+ /** The close's minutes are rounded up to this step. */
52
+ export declare const SITE_CARE_CLOSE_STEP_MINUTES = 5;
53
+ /** The most minutes one request may log. */
54
+ export declare const SITE_CARE_MAX_REQUEST_MINUTES: number;
55
+ /**
56
+ * The tax code the extra-time item is billed under: the code every Aglyn
57
+ * product carries (the Decision Log of 2026-10-18), read off the Site Care
58
+ * product, which is checked to carry it before anything is charged.
59
+ */
60
+ export declare const SITE_CARE_EXTRA_TIME_TAX_CODE = "txcd_10103001";
61
+ /** The local part of the inbound address: `care@<inbound mail domain>`. */
62
+ export declare const SITE_CARE_INBOUND_LOCAL_PART = "care";
63
+ /** The longest title and message a request keeps. */
64
+ export declare const SITE_CARE_SUBJECT_MAX = 150;
65
+ export declare const SITE_CARE_BODY_MAX = 5000;
66
+ /** Stripe's limit on an invoice item's description is generous; ours is not. */
67
+ export declare const SITE_CARE_LINE_DESCRIPTION_MAX = 480;
68
+ /** Where a request came from. */
69
+ export type SiteCareSource = 'email' | 'console';
70
+ /**
71
+ * Where an extra-time charge stands, as the platform recorded it.
72
+ *
73
+ * `creating` is written BEFORE the Stripe call and `pending` after it, so a
74
+ * call that succeeded with no row behind it shows as `creating` with no item
75
+ * id — which the reconciliation reports. Whether a `pending` item has been
76
+ * invoiced yet is read from Stripe when it matters, never assumed.
77
+ */
78
+ export type SiteCareChargeStatus = 'creating' | 'pending' | 'failed' | 'voided' | 'credited' | 'refunded';
79
+ /** Charges that still bill, or are about to. */
80
+ export declare const SITE_CARE_LIVE_CHARGE_STATUSES: readonly SiteCareChargeStatus[];
81
+ export interface SiteCareCharge {
82
+ status: SiteCareChargeStatus;
83
+ /** Whole hours billed. */
84
+ hours: number;
85
+ /** `hours × rate`, in cents, before tax. */
86
+ amountCents: number;
87
+ /** The invoice line description, exactly as it was sent. */
88
+ description: string;
89
+ /** The key the create was sent under, so a retry makes nothing twice. */
90
+ idempotencyKey: string;
91
+ invoiceItemId: string | null;
92
+ /** The invoice the item landed on, once read. */
93
+ invoiceId: string | null;
94
+ creditNoteId: string | null;
95
+ refundId: string | null;
96
+ /** What Stripe said, when a call failed. */
97
+ error: string | null;
98
+ updatedAtMs: number;
99
+ }
100
+ /** The site publish that shipped the change, from the site's activity log. */
101
+ export interface SiteCarePublishLink {
102
+ hostId: string;
103
+ activityId: string;
104
+ action: string;
105
+ targetName: string | null;
106
+ targetId: string | null;
107
+ atMs: number;
108
+ }
109
+ /** The `siteCare` map on a Site Care ticket. */
110
+ export interface SiteCareRequest {
111
+ source: SiteCareSource;
112
+ hostId: string | null;
113
+ requesterEmail: string | null;
114
+ state: 'open' | 'closed';
115
+ /** The month the request counts in: opened, until it closes; then closed. */
116
+ month: string;
117
+ timerStartedAtMs: number | null;
118
+ timerStartedByUid: string | null;
119
+ closedAtMs: number | null;
120
+ /** The one number staff entered at close, or the latest correction of it. */
121
+ minutesLogged: number | null;
122
+ /** The timer's own minutes at close, unrounded, for the record. */
123
+ timerMinutes: number | null;
124
+ countedMinutes: number;
125
+ includedMinutesUsed: number;
126
+ overMinutes: number;
127
+ billedHours: number;
128
+ absorbed: boolean;
129
+ approval: OwnerApprovalRecord | null;
130
+ charge: SiteCareCharge | null;
131
+ /** How many charges this request has ever started; keys each one. */
132
+ chargeSeq: number;
133
+ publish: SiteCarePublishLink | null;
134
+ }
135
+ /** Whether a value is a `YYYY-MM` month key. */
136
+ export declare function isSiteCareMonthKey(value: unknown): value is string;
137
+ /** The month an instant falls in, in Site Care's zone: `2026-10`. */
138
+ export declare function siteCareMonthKey(ms: number): string;
139
+ /** The month before a month key. */
140
+ export declare function previousSiteCareMonthKey(key: string): string;
141
+ /** "October 2026". */
142
+ export declare function siteCareMonthLabel(key: string): string;
143
+ /** The document id of an org's month: `{orgId}_{YYYY-MM}`, and the ticket field. */
144
+ export declare function siteCareOrgMonth(orgId: string, month: string): string;
145
+ /** "45 min", "1 h", "1 h 15 min". */
146
+ export declare function formatSiteCareMinutes(minutes: number): string;
147
+ /** "$79", "$158.00" → always whole-dollar style when whole. */
148
+ export declare function formatSiteCareUsd(cents: number): string;
149
+ /**
150
+ * The close's number from the timer: elapsed minutes rounded UP to the next
151
+ * five, and never less than five — a request somebody opened and closed was
152
+ * still a request worked.
153
+ */
154
+ export declare function closeMinutesFromTimer(startedAtMs: number | null, nowMs: number): number;
155
+ /** Staff's number read back, or null when it is not a usable count of minutes. */
156
+ export declare function readSiteCareMinutes(value: unknown): number | null;
157
+ /** Minutes as they count against the included hour: whole blocks, rounded up. */
158
+ export declare function countedSiteCareMinutes(minutes: number): number;
159
+ /** The month's included minutes: the hour plus goodwill. */
160
+ export declare function siteCareIncludedPool(goodwillMinutes: number): number;
161
+ export interface SiteCareAllocation {
162
+ countedMinutes: number;
163
+ includedMinutesUsed: number;
164
+ overMinutes: number;
165
+ /** Whole hours the over-time needs, rounded up; 0 when within the hour. */
166
+ requiredHours: number;
167
+ /** Included minutes the month had left before this request. */
168
+ remainingBefore: number;
169
+ }
170
+ /**
171
+ * One request's minutes against what the month's OTHER closed requests left
172
+ * of the included pool.
173
+ */
174
+ export declare function allocateSiteCareMinutes(input: {
175
+ minutes: number;
176
+ otherIncludedUsed: number;
177
+ goodwillMinutes: number;
178
+ }): SiteCareAllocation;
179
+ export type SiteCareCloseVerdict = {
180
+ ok: true;
181
+ billedHours: number;
182
+ absorbed: boolean;
183
+ } | {
184
+ ok: false;
185
+ /** No approval on file, or one for fewer hours than the work took. */
186
+ reason: 'no-approval' | 'approval-exceeded';
187
+ requiredHours: number;
188
+ approvedHours: number | null;
189
+ };
190
+ /**
191
+ * What a close (or a correction) may do with a request's extra time.
192
+ *
193
+ * Within the hour nothing bills. Past it, `absorb` closes with no charge;
194
+ * otherwise an approval must cover the whole hours needed, and the bill is
195
+ * the larger of those and the approved hours — the approved amount is the
196
+ * floor. Nothing bills without the owner's recorded yes.
197
+ */
198
+ export declare function siteCareCloseVerdict(input: {
199
+ requiredHours: number;
200
+ approval: Pick<OwnerApprovalRecord, 'hours'> | null;
201
+ absorb: boolean;
202
+ }): SiteCareCloseVerdict;
203
+ /** Extra time in cents, before tax. */
204
+ export declare function siteCareExtraTimeCents(hours: number): number;
205
+ /** One request as the month's statement counts it. */
206
+ export interface SiteCareStatementRequest {
207
+ requestId: string;
208
+ title: string;
209
+ state: 'open' | 'closed';
210
+ closedAtMs: number | null;
211
+ minutesLogged: number | null;
212
+ countedMinutes: number;
213
+ includedMinutesUsed: number;
214
+ overMinutes: number;
215
+ billedHours: number;
216
+ absorbed: boolean;
217
+ chargeStatus: SiteCareChargeStatus | null;
218
+ chargeCents: number;
219
+ }
220
+ export interface SiteCareMonthStatement {
221
+ month: string;
222
+ monthLabel: string;
223
+ includedMinutes: number;
224
+ goodwillMinutes: number;
225
+ poolMinutes: number;
226
+ requests: number;
227
+ openRequests: number;
228
+ minutesLogged: number;
229
+ countedMinutes: number;
230
+ includedMinutesUsed: number;
231
+ minutesLeft: number;
232
+ /** Hours on charges that still bill. */
233
+ extraHoursBilled: number;
234
+ extraBilledCents: number;
235
+ /** Cents voided, credited or refunded. */
236
+ extraReversedCents: number;
237
+ }
238
+ /** A request's facts as the statement reads them, from the stored map. */
239
+ export declare function siteCareStatementRequest(requestId: string, title: string, request: Partial<SiteCareRequest> | null | undefined): SiteCareStatementRequest;
240
+ /** The month, summed. */
241
+ export declare function siteCareMonthStatement(input: {
242
+ month: string;
243
+ requests: readonly SiteCareStatementRequest[];
244
+ goodwillMinutes: number;
245
+ }): SiteCareMonthStatement;
246
+ /** The month's totals in one line, as the statement and the invoice line say them. */
247
+ export declare function siteCareMonthSentence(statement: SiteCareMonthStatement): string;
248
+ /**
249
+ * The extra-time invoice line's description: the request, its minutes, the
250
+ * hours billed and why, and the month's statement — what the owner reads on
251
+ * the invoice, and what the reconciliation compares Stripe's copy against.
252
+ */
253
+ export declare function siteCareExtraTimeDescription(input: {
254
+ title: string;
255
+ minutesLogged: number;
256
+ overMinutes: number;
257
+ billedHours: number;
258
+ approvedHours: number | null;
259
+ statement: SiteCareMonthStatement;
260
+ }): string;
261
+ /** A charge's standing in the owner's words. */
262
+ export declare function siteCareChargeWords(status: SiteCareChargeStatus | null): string;
263
+ /** Whether a site activity row is a publish a request can be linked to. */
264
+ export declare function isSiteCarePublishAction(action: unknown): boolean;
265
+ /** A publish link as a sentence: "Published “Home” on Oct 19, 2026, 3:04 PM". */
266
+ export declare function siteCarePublishSentence(publish: SiteCarePublishLink | null, formatDate: (ms: number) => string): string | null;
267
+ /** A date as the owner's emails and statement write it, in Site Care's zone. */
268
+ export declare function formatSiteCareDate(ms: number): string;
@@ -0,0 +1,283 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import { SITE_CARE_EXTRA_HOUR_USD, SITE_CARE_INCLUDED_BLOCK_MINUTES, SITE_CARE_INCLUDED_MINUTES } from "./plan-entitlements.js";
17
+ import { describeHours } from "./owner-approval-record.js";
18
+ /**
19
+ * SITE CARE REQUESTS: THE ARITHMETIC (AGL-3959).
20
+ *
21
+ * A Site Care owner asks for a change by email or from the console, and the
22
+ * request is a support ticket tagged `siteCare`. Staff open it (a timer
23
+ * starts), make the change, and close it with one number: the minutes. This
24
+ * module is everything about that which needs no Firestore and no Stripe —
25
+ * the month a request counts in, how its minutes are counted against the
26
+ * included hour, what the close allows, what extra time bills, and the
27
+ * statement the month adds up to. The routes, the staff panel, the owner's
28
+ * Billing card and the reconciliation all read it, so they cannot disagree.
29
+ *
30
+ * ## The rules, from the Decision Log of 2026-10-19
31
+ *
32
+ * - The included hour is `SITE_CARE_INCLUDED_MINUTES` a month, no rollover,
33
+ * plus any goodwill minutes staff add to that month. Each request counts
34
+ * against it in `SITE_CARE_INCLUDED_BLOCK_MINUTES` blocks, rounded UP.
35
+ * - A request's share of the hour is fixed when it closes, against what the
36
+ * month's earlier requests left. Correcting one request's minutes later
37
+ * recomputes that request alone, against the same remainder, so a fix
38
+ * never moves a charge on a request nobody touched.
39
+ * - Past the hour, extra time bills at `SITE_CARE_EXTRA_HOUR_USD` per
40
+ * STARTED hour — whole hours, rounded up — and never less than the hours
41
+ * the owner approved. With no approval on file, or more time than was
42
+ * approved, the close is blocked: ask the owner first, or absorb it (no
43
+ * charge, the minutes still count).
44
+ */ /** The `kind` a Site Care support ticket carries. */ export const SITE_CARE_TICKET_KIND = 'siteCare';
45
+ /** Site Care's promise: done within two business days. */ export const SITE_CARE_RESPONSE_BUSINESS_DAYS = 2;
46
+ /** The zone a Site Care month is drawn in: where the work is done. */ export const SITE_CARE_TIME_ZONE = 'America/Chicago';
47
+ /** The close's minutes are rounded up to this step. */ export const SITE_CARE_CLOSE_STEP_MINUTES = 5;
48
+ /** The most minutes one request may log. */ export const SITE_CARE_MAX_REQUEST_MINUTES = 60 * 40;
49
+ /**
50
+ * The tax code the extra-time item is billed under: the code every Aglyn
51
+ * product carries (the Decision Log of 2026-10-18), read off the Site Care
52
+ * product, which is checked to carry it before anything is charged.
53
+ */ export const SITE_CARE_EXTRA_TIME_TAX_CODE = 'txcd_10103001';
54
+ /** The local part of the inbound address: `care@<inbound mail domain>`. */ export const SITE_CARE_INBOUND_LOCAL_PART = 'care';
55
+ /** The longest title and message a request keeps. */ export const SITE_CARE_SUBJECT_MAX = 150;
56
+ export const SITE_CARE_BODY_MAX = 5000;
57
+ /** Stripe's limit on an invoice item's description is generous; ours is not. */ export const SITE_CARE_LINE_DESCRIPTION_MAX = 480;
58
+ /** Charges that still bill, or are about to. */ export const SITE_CARE_LIVE_CHARGE_STATUSES = [
59
+ 'creating',
60
+ 'pending'
61
+ ];
62
+ /** Whether a value is a `YYYY-MM` month key. */ export function isSiteCareMonthKey(value) {
63
+ return typeof value === 'string' && /^\d{4}-(0[1-9]|1[0-2])$/.test(value);
64
+ }
65
+ /** The month an instant falls in, in Site Care's zone: `2026-10`. */ export function siteCareMonthKey(ms) {
66
+ var _ref, _ref1;
67
+ var _parts_find, _parts_find1;
68
+ const parts = new Intl.DateTimeFormat('en-US', {
69
+ timeZone: SITE_CARE_TIME_ZONE,
70
+ year: 'numeric',
71
+ month: '2-digit'
72
+ }).formatToParts(new Date(ms));
73
+ const year = (_ref = (_parts_find = parts.find((part)=>part.type === 'year')) == null ? void 0 : _parts_find.value) != null ? _ref : '1970';
74
+ const month = (_ref1 = (_parts_find1 = parts.find((part)=>part.type === 'month')) == null ? void 0 : _parts_find1.value) != null ? _ref1 : '01';
75
+ return `${year}-${month}`;
76
+ }
77
+ /** The month before a month key. */ export function previousSiteCareMonthKey(key) {
78
+ const [year, month] = key.split('-').map(Number);
79
+ return month === 1 ? `${year - 1}-12` : `${year}-${String(month - 1).padStart(2, '0')}`;
80
+ }
81
+ /** "October 2026". */ export function siteCareMonthLabel(key) {
82
+ if (!isSiteCareMonthKey(key)) return key;
83
+ const [year, month] = key.split('-').map(Number);
84
+ return new Date(Date.UTC(year, month - 1, 15)).toLocaleString('en-US', {
85
+ month: 'long',
86
+ year: 'numeric',
87
+ timeZone: 'UTC'
88
+ });
89
+ }
90
+ /** The document id of an org's month: `{orgId}_{YYYY-MM}`, and the ticket field. */ export function siteCareOrgMonth(orgId, month) {
91
+ return `${orgId}_${month}`;
92
+ }
93
+ /** "45 min", "1 h", "1 h 15 min". */ export function formatSiteCareMinutes(minutes) {
94
+ const total = Math.max(0, Math.round(Number(minutes) || 0));
95
+ if (total < 60) return `${total} min`;
96
+ const hours = Math.floor(total / 60);
97
+ const rest = total % 60;
98
+ return rest ? `${hours} h ${rest} min` : `${hours} h`;
99
+ }
100
+ /** "$79", "$158.00" → always whole-dollar style when whole. */ export function formatSiteCareUsd(cents) {
101
+ const dollars = Math.round(Number(cents) || 0) / 100;
102
+ return Number.isInteger(dollars) ? `$${dollars}` : `$${dollars.toFixed(2)}`;
103
+ }
104
+ /**
105
+ * The close's number from the timer: elapsed minutes rounded UP to the next
106
+ * five, and never less than five — a request somebody opened and closed was
107
+ * still a request worked.
108
+ */ export function closeMinutesFromTimer(startedAtMs, nowMs) {
109
+ if (!startedAtMs || nowMs <= startedAtMs) return SITE_CARE_CLOSE_STEP_MINUTES;
110
+ const elapsed = (nowMs - startedAtMs) / 60000;
111
+ const rounded = Math.ceil(elapsed / SITE_CARE_CLOSE_STEP_MINUTES) * SITE_CARE_CLOSE_STEP_MINUTES;
112
+ return Math.min(SITE_CARE_MAX_REQUEST_MINUTES, Math.max(SITE_CARE_CLOSE_STEP_MINUTES, rounded));
113
+ }
114
+ /** Staff's number read back, or null when it is not a usable count of minutes. */ export function readSiteCareMinutes(value) {
115
+ const minutes = Number(value);
116
+ if (!Number.isInteger(minutes) || minutes < 1 || minutes > SITE_CARE_MAX_REQUEST_MINUTES) {
117
+ return null;
118
+ }
119
+ return minutes;
120
+ }
121
+ /** Minutes as they count against the included hour: whole blocks, rounded up. */ export function countedSiteCareMinutes(minutes) {
122
+ const value = Math.max(0, Number(minutes) || 0);
123
+ return Math.ceil(value / SITE_CARE_INCLUDED_BLOCK_MINUTES) * SITE_CARE_INCLUDED_BLOCK_MINUTES;
124
+ }
125
+ /** The month's included minutes: the hour plus goodwill. */ export function siteCareIncludedPool(goodwillMinutes) {
126
+ return SITE_CARE_INCLUDED_MINUTES + Math.max(0, Math.floor(Number(goodwillMinutes) || 0));
127
+ }
128
+ /**
129
+ * One request's minutes against what the month's OTHER closed requests left
130
+ * of the included pool.
131
+ */ export function allocateSiteCareMinutes(input) {
132
+ const countedMinutes = countedSiteCareMinutes(input.minutes);
133
+ const remainingBefore = Math.max(0, siteCareIncludedPool(input.goodwillMinutes) - Math.max(0, input.otherIncludedUsed));
134
+ const includedMinutesUsed = Math.min(countedMinutes, remainingBefore);
135
+ const overMinutes = countedMinutes - includedMinutesUsed;
136
+ return {
137
+ countedMinutes,
138
+ includedMinutesUsed,
139
+ overMinutes,
140
+ requiredHours: overMinutes > 0 ? Math.ceil(overMinutes / 60) : 0,
141
+ remainingBefore
142
+ };
143
+ }
144
+ /**
145
+ * What a close (or a correction) may do with a request's extra time.
146
+ *
147
+ * Within the hour nothing bills. Past it, `absorb` closes with no charge;
148
+ * otherwise an approval must cover the whole hours needed, and the bill is
149
+ * the larger of those and the approved hours — the approved amount is the
150
+ * floor. Nothing bills without the owner's recorded yes.
151
+ */ export function siteCareCloseVerdict(input) {
152
+ if (input.requiredHours <= 0) return {
153
+ ok: true,
154
+ billedHours: 0,
155
+ absorbed: false
156
+ };
157
+ if (input.absorb) return {
158
+ ok: true,
159
+ billedHours: 0,
160
+ absorbed: true
161
+ };
162
+ if (!input.approval) {
163
+ return {
164
+ ok: false,
165
+ reason: 'no-approval',
166
+ requiredHours: input.requiredHours,
167
+ approvedHours: null
168
+ };
169
+ }
170
+ if (input.approval.hours < input.requiredHours) {
171
+ return {
172
+ ok: false,
173
+ reason: 'approval-exceeded',
174
+ requiredHours: input.requiredHours,
175
+ approvedHours: input.approval.hours
176
+ };
177
+ }
178
+ return {
179
+ ok: true,
180
+ billedHours: Math.max(input.requiredHours, input.approval.hours),
181
+ absorbed: false
182
+ };
183
+ }
184
+ /** Extra time in cents, before tax. */ export function siteCareExtraTimeCents(hours) {
185
+ return Math.max(0, Math.floor(hours)) * SITE_CARE_EXTRA_HOUR_USD * 100;
186
+ }
187
+ /** A request's facts as the statement reads them, from the stored map. */ export function siteCareStatementRequest(requestId, title, request) {
188
+ var _ref, _ref1, _ref2, _ref3;
189
+ const charge = (_ref = request == null ? void 0 : request.charge) != null ? _ref : null;
190
+ return {
191
+ requestId,
192
+ title,
193
+ state: (request == null ? void 0 : request.state) === 'closed' ? 'closed' : 'open',
194
+ closedAtMs: (_ref1 = request == null ? void 0 : request.closedAtMs) != null ? _ref1 : null,
195
+ minutesLogged: (_ref2 = request == null ? void 0 : request.minutesLogged) != null ? _ref2 : null,
196
+ countedMinutes: Number(request == null ? void 0 : request.countedMinutes) || 0,
197
+ includedMinutesUsed: Number(request == null ? void 0 : request.includedMinutesUsed) || 0,
198
+ overMinutes: Number(request == null ? void 0 : request.overMinutes) || 0,
199
+ billedHours: Number(request == null ? void 0 : request.billedHours) || 0,
200
+ absorbed: (request == null ? void 0 : request.absorbed) === true,
201
+ chargeStatus: (_ref3 = charge == null ? void 0 : charge.status) != null ? _ref3 : null,
202
+ chargeCents: Number(charge == null ? void 0 : charge.amountCents) || 0
203
+ };
204
+ }
205
+ /** The month, summed. */ export function siteCareMonthStatement(input) {
206
+ const closed = input.requests.filter((request)=>request.state === 'closed');
207
+ const goodwillMinutes = Math.max(0, Math.floor(Number(input.goodwillMinutes) || 0));
208
+ const poolMinutes = siteCareIncludedPool(goodwillMinutes);
209
+ const includedMinutesUsed = closed.reduce((sum, r)=>sum + r.includedMinutesUsed, 0);
210
+ const live = closed.filter((r)=>r.chargeStatus !== null && SITE_CARE_LIVE_CHARGE_STATUSES.includes(r.chargeStatus));
211
+ const reversed = closed.filter((r)=>r.chargeStatus === 'voided' || r.chargeStatus === 'credited' || r.chargeStatus === 'refunded');
212
+ return {
213
+ month: input.month,
214
+ monthLabel: siteCareMonthLabel(input.month),
215
+ includedMinutes: SITE_CARE_INCLUDED_MINUTES,
216
+ goodwillMinutes,
217
+ poolMinutes,
218
+ requests: input.requests.length,
219
+ openRequests: input.requests.length - closed.length,
220
+ minutesLogged: closed.reduce((sum, r)=>{
221
+ var _r_minutesLogged;
222
+ return sum + ((_r_minutesLogged = r.minutesLogged) != null ? _r_minutesLogged : 0);
223
+ }, 0),
224
+ countedMinutes: closed.reduce((sum, r)=>sum + r.countedMinutes, 0),
225
+ includedMinutesUsed,
226
+ minutesLeft: Math.max(0, poolMinutes - includedMinutesUsed),
227
+ extraHoursBilled: live.reduce((sum, r)=>sum + r.billedHours, 0),
228
+ extraBilledCents: live.reduce((sum, r)=>sum + r.chargeCents, 0),
229
+ extraReversedCents: reversed.reduce((sum, r)=>sum + r.chargeCents, 0)
230
+ };
231
+ }
232
+ /** The month's totals in one line, as the statement and the invoice line say them. */ export function siteCareMonthSentence(statement) {
233
+ const requests = `${statement.requests} ${statement.requests === 1 ? 'request' : 'requests'}`;
234
+ return `${statement.monthLabel}: ${requests}, ${formatSiteCareMinutes(statement.minutesLogged)} of work, ` + `${statement.includedMinutesUsed} of ${statement.poolMinutes} included minutes used` + (statement.extraHoursBilled > 0 ? `, ${describeHours(statement.extraHoursBilled)} of extra time billed` : '') + '.';
235
+ }
236
+ /**
237
+ * The extra-time invoice line's description: the request, its minutes, the
238
+ * hours billed and why, and the month's statement — what the owner reads on
239
+ * the invoice, and what the reconciliation compares Stripe's copy against.
240
+ */ export function siteCareExtraTimeDescription(input) {
241
+ const title = input.title.length > 80 ? `${input.title.slice(0, 79)}…` : input.title;
242
+ const approved = input.approvedHours && input.approvedHours > Math.ceil(input.overMinutes / 60) ? ` (the ${describeHours(input.approvedHours)} you approved)` : '';
243
+ const line = `Site Care extra time — “${title}”: ${formatSiteCareMinutes(input.minutesLogged)} of work, ` + `${formatSiteCareMinutes(input.overMinutes)} past the included hour, billed as ` + `${describeHours(input.billedHours)}${approved} at $${SITE_CARE_EXTRA_HOUR_USD}/hour. ` + siteCareMonthSentence(input.statement);
244
+ return line.length > SITE_CARE_LINE_DESCRIPTION_MAX ? `${line.slice(0, SITE_CARE_LINE_DESCRIPTION_MAX - 1)}…` : line;
245
+ }
246
+ /** A charge's standing in the owner's words. */ export function siteCareChargeWords(status) {
247
+ switch(status){
248
+ case 'creating':
249
+ case 'pending':
250
+ return 'On your next invoice';
251
+ case 'failed':
252
+ return 'Not billed';
253
+ case 'voided':
254
+ return 'Removed, not billed';
255
+ case 'credited':
256
+ return 'Credited back';
257
+ case 'refunded':
258
+ return 'Refunded';
259
+ default:
260
+ return 'Included';
261
+ }
262
+ }
263
+ /** Whether a site activity row is a publish a request can be linked to. */ export function isSiteCarePublishAction(action) {
264
+ return typeof action === 'string' && /^published\b/i.test(action.trim());
265
+ }
266
+ /** A publish link as a sentence: "Published “Home” on Oct 19, 2026, 3:04 PM". */ export function siteCarePublishSentence(publish, formatDate) {
267
+ if (!publish) return null;
268
+ const what = publish.targetName ? ` “${publish.targetName}”` : '';
269
+ return `${publish.action}${what} on ${formatDate(publish.atMs)}`;
270
+ }
271
+ /** A date as the owner's emails and statement write it, in Site Care's zone. */ export function formatSiteCareDate(ms) {
272
+ return new Date(ms).toLocaleString('en-US', {
273
+ timeZone: SITE_CARE_TIME_ZONE,
274
+ month: 'short',
275
+ day: 'numeric',
276
+ year: 'numeric',
277
+ hour: 'numeric',
278
+ minute: '2-digit',
279
+ timeZoneName: 'short'
280
+ });
281
+ }
282
+
283
+ //# sourceMappingURL=site-care.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/site-care.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 SITE_CARE_EXTRA_HOUR_USD,\n SITE_CARE_INCLUDED_BLOCK_MINUTES,\n SITE_CARE_INCLUDED_MINUTES,\n} from './plan-entitlements'\nimport { describeHours, type OwnerApprovalRecord } from './owner-approval-record'\n\n/**\n * SITE CARE REQUESTS: THE ARITHMETIC (AGL-3959).\n *\n * A Site Care owner asks for a change by email or from the console, and the\n * request is a support ticket tagged `siteCare`. Staff open it (a timer\n * starts), make the change, and close it with one number: the minutes. This\n * module is everything about that which needs no Firestore and no Stripe —\n * the month a request counts in, how its minutes are counted against the\n * included hour, what the close allows, what extra time bills, and the\n * statement the month adds up to. The routes, the staff panel, the owner's\n * Billing card and the reconciliation all read it, so they cannot disagree.\n *\n * ## The rules, from the Decision Log of 2026-10-19\n *\n * - The included hour is `SITE_CARE_INCLUDED_MINUTES` a month, no rollover,\n * plus any goodwill minutes staff add to that month. Each request counts\n * against it in `SITE_CARE_INCLUDED_BLOCK_MINUTES` blocks, rounded UP.\n * - A request's share of the hour is fixed when it closes, against what the\n * month's earlier requests left. Correcting one request's minutes later\n * recomputes that request alone, against the same remainder, so a fix\n * never moves a charge on a request nobody touched.\n * - Past the hour, extra time bills at `SITE_CARE_EXTRA_HOUR_USD` per\n * STARTED hour — whole hours, rounded up — and never less than the hours\n * the owner approved. With no approval on file, or more time than was\n * approved, the close is blocked: ask the owner first, or absorb it (no\n * charge, the minutes still count).\n */\n\n/** The `kind` a Site Care support ticket carries. */\nexport const SITE_CARE_TICKET_KIND = 'siteCare'\n\n/** Site Care's promise: done within two business days. */\nexport const SITE_CARE_RESPONSE_BUSINESS_DAYS = 2\n\n/** The zone a Site Care month is drawn in: where the work is done. */\nexport const SITE_CARE_TIME_ZONE = 'America/Chicago'\n\n/** The close's minutes are rounded up to this step. */\nexport const SITE_CARE_CLOSE_STEP_MINUTES = 5\n\n/** The most minutes one request may log. */\nexport const SITE_CARE_MAX_REQUEST_MINUTES = 60 * 40\n\n/**\n * The tax code the extra-time item is billed under: the code every Aglyn\n * product carries (the Decision Log of 2026-10-18), read off the Site Care\n * product, which is checked to carry it before anything is charged.\n */\nexport const SITE_CARE_EXTRA_TIME_TAX_CODE = 'txcd_10103001'\n\n/** The local part of the inbound address: `care@<inbound mail domain>`. */\nexport const SITE_CARE_INBOUND_LOCAL_PART = 'care'\n\n/** The longest title and message a request keeps. */\nexport const SITE_CARE_SUBJECT_MAX = 150\nexport const SITE_CARE_BODY_MAX = 5000\n\n/** Stripe's limit on an invoice item's description is generous; ours is not. */\nexport const SITE_CARE_LINE_DESCRIPTION_MAX = 480\n\n/** Where a request came from. */\nexport type SiteCareSource = 'email' | 'console'\n\n/**\n * Where an extra-time charge stands, as the platform recorded it.\n *\n * `creating` is written BEFORE the Stripe call and `pending` after it, so a\n * call that succeeded with no row behind it shows as `creating` with no item\n * id — which the reconciliation reports. Whether a `pending` item has been\n * invoiced yet is read from Stripe when it matters, never assumed.\n */\nexport type SiteCareChargeStatus =\n | 'creating'\n | 'pending'\n | 'failed'\n | 'voided'\n | 'credited'\n | 'refunded'\n\n/** Charges that still bill, or are about to. */\nexport const SITE_CARE_LIVE_CHARGE_STATUSES: readonly SiteCareChargeStatus[] = [\n 'creating',\n 'pending',\n]\n\nexport interface SiteCareCharge {\n status: SiteCareChargeStatus\n /** Whole hours billed. */\n hours: number\n /** `hours × rate`, in cents, before tax. */\n amountCents: number\n /** The invoice line description, exactly as it was sent. */\n description: string\n /** The key the create was sent under, so a retry makes nothing twice. */\n idempotencyKey: string\n invoiceItemId: string | null\n /** The invoice the item landed on, once read. */\n invoiceId: string | null\n creditNoteId: string | null\n refundId: string | null\n /** What Stripe said, when a call failed. */\n error: string | null\n updatedAtMs: number\n}\n\n/** The site publish that shipped the change, from the site's activity log. */\nexport interface SiteCarePublishLink {\n hostId: string\n activityId: string\n action: string\n targetName: string | null\n targetId: string | null\n atMs: number\n}\n\n/** The `siteCare` map on a Site Care ticket. */\nexport interface SiteCareRequest {\n source: SiteCareSource\n hostId: string | null\n requesterEmail: string | null\n state: 'open' | 'closed'\n /** The month the request counts in: opened, until it closes; then closed. */\n month: string\n timerStartedAtMs: number | null\n timerStartedByUid: string | null\n closedAtMs: number | null\n /** The one number staff entered at close, or the latest correction of it. */\n minutesLogged: number | null\n /** The timer's own minutes at close, unrounded, for the record. */\n timerMinutes: number | null\n countedMinutes: number\n includedMinutesUsed: number\n overMinutes: number\n billedHours: number\n absorbed: boolean\n approval: OwnerApprovalRecord | null\n charge: SiteCareCharge | null\n /** How many charges this request has ever started; keys each one. */\n chargeSeq: number\n publish: SiteCarePublishLink | null\n}\n\n/** Whether a value is a `YYYY-MM` month key. */\nexport function isSiteCareMonthKey(value: unknown): value is string {\n return typeof value === 'string' && /^\\d{4}-(0[1-9]|1[0-2])$/.test(value)\n}\n\n/** The month an instant falls in, in Site Care's zone: `2026-10`. */\nexport function siteCareMonthKey(ms: number): string {\n const parts = new Intl.DateTimeFormat('en-US', {\n timeZone: SITE_CARE_TIME_ZONE,\n year: 'numeric',\n month: '2-digit',\n }).formatToParts(new Date(ms))\n const year = parts.find((part) => part.type === 'year')?.value ?? '1970'\n const month = parts.find((part) => part.type === 'month')?.value ?? '01'\n return `${year}-${month}`\n}\n\n/** The month before a month key. */\nexport function previousSiteCareMonthKey(key: string): string {\n const [year, month] = key.split('-').map(Number)\n return month === 1 ? `${year - 1}-12` : `${year}-${String(month - 1).padStart(2, '0')}`\n}\n\n/** \"October 2026\". */\nexport function siteCareMonthLabel(key: string): string {\n if (!isSiteCareMonthKey(key)) return key\n const [year, month] = key.split('-').map(Number)\n return new Date(Date.UTC(year, month - 1, 15)).toLocaleString('en-US', {\n month: 'long',\n year: 'numeric',\n timeZone: 'UTC',\n })\n}\n\n/** The document id of an org's month: `{orgId}_{YYYY-MM}`, and the ticket field. */\nexport function siteCareOrgMonth(orgId: string, month: string): string {\n return `${orgId}_${month}`\n}\n\n/** \"45 min\", \"1 h\", \"1 h 15 min\". */\nexport function formatSiteCareMinutes(minutes: number): string {\n const total = Math.max(0, Math.round(Number(minutes) || 0))\n if (total < 60) return `${total} min`\n const hours = Math.floor(total / 60)\n const rest = total % 60\n return rest ? `${hours} h ${rest} min` : `${hours} h`\n}\n\n/** \"$79\", \"$158.00\" → always whole-dollar style when whole. */\nexport function formatSiteCareUsd(cents: number): string {\n const dollars = (Math.round(Number(cents) || 0) / 100)\n return Number.isInteger(dollars) ? `$${dollars}` : `$${dollars.toFixed(2)}`\n}\n\n/**\n * The close's number from the timer: elapsed minutes rounded UP to the next\n * five, and never less than five — a request somebody opened and closed was\n * still a request worked.\n */\nexport function closeMinutesFromTimer(startedAtMs: number | null, nowMs: number): number {\n if (!startedAtMs || nowMs <= startedAtMs) return SITE_CARE_CLOSE_STEP_MINUTES\n const elapsed = (nowMs - startedAtMs) / 60_000\n const rounded = Math.ceil(elapsed / SITE_CARE_CLOSE_STEP_MINUTES) * SITE_CARE_CLOSE_STEP_MINUTES\n return Math.min(SITE_CARE_MAX_REQUEST_MINUTES, Math.max(SITE_CARE_CLOSE_STEP_MINUTES, rounded))\n}\n\n/** Staff's number read back, or null when it is not a usable count of minutes. */\nexport function readSiteCareMinutes(value: unknown): number | null {\n const minutes = Number(value)\n if (!Number.isInteger(minutes) || minutes < 1 || minutes > SITE_CARE_MAX_REQUEST_MINUTES) {\n return null\n }\n return minutes\n}\n\n/** Minutes as they count against the included hour: whole blocks, rounded up. */\nexport function countedSiteCareMinutes(minutes: number): number {\n const value = Math.max(0, Number(minutes) || 0)\n return Math.ceil(value / SITE_CARE_INCLUDED_BLOCK_MINUTES) * SITE_CARE_INCLUDED_BLOCK_MINUTES\n}\n\n/** The month's included minutes: the hour plus goodwill. */\nexport function siteCareIncludedPool(goodwillMinutes: number): number {\n return SITE_CARE_INCLUDED_MINUTES + Math.max(0, Math.floor(Number(goodwillMinutes) || 0))\n}\n\nexport interface SiteCareAllocation {\n countedMinutes: number\n includedMinutesUsed: number\n overMinutes: number\n /** Whole hours the over-time needs, rounded up; 0 when within the hour. */\n requiredHours: number\n /** Included minutes the month had left before this request. */\n remainingBefore: number\n}\n\n/**\n * One request's minutes against what the month's OTHER closed requests left\n * of the included pool.\n */\nexport function allocateSiteCareMinutes(input: {\n minutes: number\n otherIncludedUsed: number\n goodwillMinutes: number\n}): SiteCareAllocation {\n const countedMinutes = countedSiteCareMinutes(input.minutes)\n const remainingBefore = Math.max(\n 0,\n siteCareIncludedPool(input.goodwillMinutes) - Math.max(0, input.otherIncludedUsed),\n )\n const includedMinutesUsed = Math.min(countedMinutes, remainingBefore)\n const overMinutes = countedMinutes - includedMinutesUsed\n return {\n countedMinutes,\n includedMinutesUsed,\n overMinutes,\n requiredHours: overMinutes > 0 ? Math.ceil(overMinutes / 60) : 0,\n remainingBefore,\n }\n}\n\nexport type SiteCareCloseVerdict =\n | { ok: true; billedHours: number; absorbed: boolean }\n | {\n ok: false\n /** No approval on file, or one for fewer hours than the work took. */\n reason: 'no-approval' | 'approval-exceeded'\n requiredHours: number\n approvedHours: number | null\n }\n\n/**\n * What a close (or a correction) may do with a request's extra time.\n *\n * Within the hour nothing bills. Past it, `absorb` closes with no charge;\n * otherwise an approval must cover the whole hours needed, and the bill is\n * the larger of those and the approved hours — the approved amount is the\n * floor. Nothing bills without the owner's recorded yes.\n */\nexport function siteCareCloseVerdict(input: {\n requiredHours: number\n approval: Pick<OwnerApprovalRecord, 'hours'> | null\n absorb: boolean\n}): SiteCareCloseVerdict {\n if (input.requiredHours <= 0) return { ok: true, billedHours: 0, absorbed: false }\n if (input.absorb) return { ok: true, billedHours: 0, absorbed: true }\n if (!input.approval) {\n return {\n ok: false,\n reason: 'no-approval',\n requiredHours: input.requiredHours,\n approvedHours: null,\n }\n }\n if (input.approval.hours < input.requiredHours) {\n return {\n ok: false,\n reason: 'approval-exceeded',\n requiredHours: input.requiredHours,\n approvedHours: input.approval.hours,\n }\n }\n return {\n ok: true,\n billedHours: Math.max(input.requiredHours, input.approval.hours),\n absorbed: false,\n }\n}\n\n/** Extra time in cents, before tax. */\nexport function siteCareExtraTimeCents(hours: number): number {\n return Math.max(0, Math.floor(hours)) * SITE_CARE_EXTRA_HOUR_USD * 100\n}\n\n/** One request as the month's statement counts it. */\nexport interface SiteCareStatementRequest {\n requestId: string\n title: string\n state: 'open' | 'closed'\n closedAtMs: number | null\n minutesLogged: number | null\n countedMinutes: number\n includedMinutesUsed: number\n overMinutes: number\n billedHours: number\n absorbed: boolean\n chargeStatus: SiteCareChargeStatus | null\n chargeCents: number\n}\n\nexport interface SiteCareMonthStatement {\n month: string\n monthLabel: string\n includedMinutes: number\n goodwillMinutes: number\n poolMinutes: number\n requests: number\n openRequests: number\n minutesLogged: number\n countedMinutes: number\n includedMinutesUsed: number\n minutesLeft: number\n /** Hours on charges that still bill. */\n extraHoursBilled: number\n extraBilledCents: number\n /** Cents voided, credited or refunded. */\n extraReversedCents: number\n}\n\n/** A request's facts as the statement reads them, from the stored map. */\nexport function siteCareStatementRequest(\n requestId: string,\n title: string,\n request: Partial<SiteCareRequest> | null | undefined,\n): SiteCareStatementRequest {\n const charge = request?.charge ?? null\n return {\n requestId,\n title,\n state: request?.state === 'closed' ? 'closed' : 'open',\n closedAtMs: request?.closedAtMs ?? null,\n minutesLogged: request?.minutesLogged ?? null,\n countedMinutes: Number(request?.countedMinutes) || 0,\n includedMinutesUsed: Number(request?.includedMinutesUsed) || 0,\n overMinutes: Number(request?.overMinutes) || 0,\n billedHours: Number(request?.billedHours) || 0,\n absorbed: request?.absorbed === true,\n chargeStatus: charge?.status ?? null,\n chargeCents: Number(charge?.amountCents) || 0,\n }\n}\n\n/** The month, summed. */\nexport function siteCareMonthStatement(input: {\n month: string\n requests: readonly SiteCareStatementRequest[]\n goodwillMinutes: number\n}): SiteCareMonthStatement {\n const closed = input.requests.filter((request) => request.state === 'closed')\n const goodwillMinutes = Math.max(0, Math.floor(Number(input.goodwillMinutes) || 0))\n const poolMinutes = siteCareIncludedPool(goodwillMinutes)\n const includedMinutesUsed = closed.reduce((sum, r) => sum + r.includedMinutesUsed, 0)\n const live = closed.filter(\n (r) => r.chargeStatus !== null && SITE_CARE_LIVE_CHARGE_STATUSES.includes(r.chargeStatus),\n )\n const reversed = closed.filter(\n (r) => r.chargeStatus === 'voided' || r.chargeStatus === 'credited' || r.chargeStatus === 'refunded',\n )\n return {\n month: input.month,\n monthLabel: siteCareMonthLabel(input.month),\n includedMinutes: SITE_CARE_INCLUDED_MINUTES,\n goodwillMinutes,\n poolMinutes,\n requests: input.requests.length,\n openRequests: input.requests.length - closed.length,\n minutesLogged: closed.reduce((sum, r) => sum + (r.minutesLogged ?? 0), 0),\n countedMinutes: closed.reduce((sum, r) => sum + r.countedMinutes, 0),\n includedMinutesUsed,\n minutesLeft: Math.max(0, poolMinutes - includedMinutesUsed),\n extraHoursBilled: live.reduce((sum, r) => sum + r.billedHours, 0),\n extraBilledCents: live.reduce((sum, r) => sum + r.chargeCents, 0),\n extraReversedCents: reversed.reduce((sum, r) => sum + r.chargeCents, 0),\n }\n}\n\n/** The month's totals in one line, as the statement and the invoice line say them. */\nexport function siteCareMonthSentence(statement: SiteCareMonthStatement): string {\n const requests = `${statement.requests} ${statement.requests === 1 ? 'request' : 'requests'}`\n return (\n `${statement.monthLabel}: ${requests}, ${formatSiteCareMinutes(statement.minutesLogged)} of work, ` +\n `${statement.includedMinutesUsed} of ${statement.poolMinutes} included minutes used` +\n (statement.extraHoursBilled > 0\n ? `, ${describeHours(statement.extraHoursBilled)} of extra time billed`\n : '') +\n '.'\n )\n}\n\n/**\n * The extra-time invoice line's description: the request, its minutes, the\n * hours billed and why, and the month's statement — what the owner reads on\n * the invoice, and what the reconciliation compares Stripe's copy against.\n */\nexport function siteCareExtraTimeDescription(input: {\n title: string\n minutesLogged: number\n overMinutes: number\n billedHours: number\n approvedHours: number | null\n statement: SiteCareMonthStatement\n}): string {\n const title = input.title.length > 80 ? `${input.title.slice(0, 79)}…` : input.title\n const approved =\n input.approvedHours && input.approvedHours > Math.ceil(input.overMinutes / 60)\n ? ` (the ${describeHours(input.approvedHours)} you approved)`\n : ''\n const line =\n `Site Care extra time — “${title}”: ${formatSiteCareMinutes(input.minutesLogged)} of work, ` +\n `${formatSiteCareMinutes(input.overMinutes)} past the included hour, billed as ` +\n `${describeHours(input.billedHours)}${approved} at $${SITE_CARE_EXTRA_HOUR_USD}/hour. ` +\n siteCareMonthSentence(input.statement)\n return line.length > SITE_CARE_LINE_DESCRIPTION_MAX\n ? `${line.slice(0, SITE_CARE_LINE_DESCRIPTION_MAX - 1)}…`\n : line\n}\n\n/** A charge's standing in the owner's words. */\nexport function siteCareChargeWords(status: SiteCareChargeStatus | null): string {\n switch (status) {\n case 'creating':\n case 'pending':\n return 'On your next invoice'\n case 'failed':\n return 'Not billed'\n case 'voided':\n return 'Removed, not billed'\n case 'credited':\n return 'Credited back'\n case 'refunded':\n return 'Refunded'\n default:\n return 'Included'\n }\n}\n\n/** Whether a site activity row is a publish a request can be linked to. */\nexport function isSiteCarePublishAction(action: unknown): boolean {\n return typeof action === 'string' && /^published\\b/i.test(action.trim())\n}\n\n/** A publish link as a sentence: \"Published “Home” on Oct 19, 2026, 3:04 PM\". */\nexport function siteCarePublishSentence(\n publish: SiteCarePublishLink | null,\n formatDate: (ms: number) => string,\n): string | null {\n if (!publish) return null\n const what = publish.targetName ? ` “${publish.targetName}”` : ''\n return `${publish.action}${what} on ${formatDate(publish.atMs)}`\n}\n\n/** A date as the owner's emails and statement write it, in Site Care's zone. */\nexport function formatSiteCareDate(ms: number): string {\n return new Date(ms).toLocaleString('en-US', {\n timeZone: SITE_CARE_TIME_ZONE,\n month: 'short',\n day: 'numeric',\n year: 'numeric',\n hour: 'numeric',\n minute: '2-digit',\n timeZoneName: 'short',\n })\n}\n"],"names":["SITE_CARE_EXTRA_HOUR_USD","SITE_CARE_INCLUDED_BLOCK_MINUTES","SITE_CARE_INCLUDED_MINUTES","describeHours","SITE_CARE_TICKET_KIND","SITE_CARE_RESPONSE_BUSINESS_DAYS","SITE_CARE_TIME_ZONE","SITE_CARE_CLOSE_STEP_MINUTES","SITE_CARE_MAX_REQUEST_MINUTES","SITE_CARE_EXTRA_TIME_TAX_CODE","SITE_CARE_INBOUND_LOCAL_PART","SITE_CARE_SUBJECT_MAX","SITE_CARE_BODY_MAX","SITE_CARE_LINE_DESCRIPTION_MAX","SITE_CARE_LIVE_CHARGE_STATUSES","isSiteCareMonthKey","value","test","siteCareMonthKey","ms","parts","Intl","DateTimeFormat","timeZone","year","month","formatToParts","Date","find","part","type","previousSiteCareMonthKey","key","split","map","Number","String","padStart","siteCareMonthLabel","UTC","toLocaleString","siteCareOrgMonth","orgId","formatSiteCareMinutes","minutes","total","Math","max","round","hours","floor","rest","formatSiteCareUsd","cents","dollars","isInteger","toFixed","closeMinutesFromTimer","startedAtMs","nowMs","elapsed","rounded","ceil","min","readSiteCareMinutes","countedSiteCareMinutes","siteCareIncludedPool","goodwillMinutes","allocateSiteCareMinutes","input","countedMinutes","remainingBefore","otherIncludedUsed","includedMinutesUsed","overMinutes","requiredHours","siteCareCloseVerdict","ok","billedHours","absorbed","absorb","approval","reason","approvedHours","siteCareExtraTimeCents","siteCareStatementRequest","requestId","title","request","charge","state","closedAtMs","minutesLogged","chargeStatus","status","chargeCents","amountCents","siteCareMonthStatement","closed","requests","filter","poolMinutes","reduce","sum","r","live","includes","reversed","monthLabel","includedMinutes","length","openRequests","minutesLeft","extraHoursBilled","extraBilledCents","extraReversedCents","siteCareMonthSentence","statement","siteCareExtraTimeDescription","slice","approved","line","siteCareChargeWords","isSiteCarePublishAction","action","trim","siteCarePublishSentence","publish","formatDate","what","targetName","atMs","formatSiteCareDate","day","hour","minute","timeZoneName"],"mappings":"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"}