@aglyn/aglyn 1.0.0-beta.270 → 1.0.0-beta.272
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +11 -11
- package/src/lib/app-utils/account-preferences.d.ts +50 -0
- package/src/lib/app-utils/account-preferences.js +96 -0
- package/src/lib/app-utils/account-preferences.js.map +1 -1
- package/src/lib/app-utils/activity-labels.js +2 -0
- package/src/lib/app-utils/activity-labels.js.map +1 -1
- package/src/lib/app-utils/analytics-events.d.ts +16 -2
- package/src/lib/app-utils/analytics-events.js +2 -0
- package/src/lib/app-utils/analytics-events.js.map +1 -1
- package/src/lib/app-utils/append-only-history.d.ts +94 -0
- package/src/lib/app-utils/append-only-history.js +73 -0
- package/src/lib/app-utils/append-only-history.js.map +1 -0
- package/src/lib/app-utils/bundle-intent.d.ts +186 -0
- package/src/lib/app-utils/bundle-intent.js +386 -0
- package/src/lib/app-utils/bundle-intent.js.map +1 -0
- package/src/lib/app-utils/client-build.d.ts +207 -0
- package/src/lib/app-utils/client-build.js +237 -0
- package/src/lib/app-utils/client-build.js.map +1 -0
- package/src/lib/app-utils/console-routes.d.ts +4 -0
- package/src/lib/app-utils/console-routes.js +2 -0
- package/src/lib/app-utils/console-routes.js.map +1 -1
- package/src/lib/app-utils/docs-help-sections.generated.js +13 -6
- package/src/lib/app-utils/docs-help-sections.generated.js.map +1 -1
- package/src/lib/app-utils/docs-help.generated.d.ts +11 -5
- package/src/lib/app-utils/docs-help.generated.js +31 -2
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.js +105 -35
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/margin-utilization.js +6 -4
- package/src/lib/app-utils/margin-utilization.js.map +1 -1
- package/src/lib/app-utils/media-filter.d.ts +26 -0
- package/src/lib/app-utils/media-filter.js +32 -0
- package/src/lib/app-utils/media-filter.js.map +1 -1
- package/src/lib/app-utils/onboarding-deep-link.d.ts +36 -2
- package/src/lib/app-utils/onboarding-deep-link.js +48 -5
- package/src/lib/app-utils/onboarding-deep-link.js.map +1 -1
- package/src/lib/app-utils/operator-alerts.js +21 -0
- package/src/lib/app-utils/operator-alerts.js.map +1 -1
- package/src/lib/app-utils/owner-approval-record.d.ts +102 -0
- package/src/lib/app-utils/owner-approval-record.js +148 -0
- package/src/lib/app-utils/owner-approval-record.js.map +1 -0
- package/src/lib/app-utils/plan-card-meters.d.ts +96 -0
- package/src/lib/app-utils/plan-card-meters.js +197 -0
- package/src/lib/app-utils/plan-card-meters.js.map +1 -0
- package/src/lib/app-utils/plan-entitlements.d.ts +183 -67
- package/src/lib/app-utils/plan-entitlements.js +213 -58
- package/src/lib/app-utils/plan-entitlements.js.map +1 -1
- package/src/lib/app-utils/release-flags.d.ts +1 -1
- package/src/lib/app-utils/release-flags.js +22 -0
- package/src/lib/app-utils/release-flags.js.map +1 -1
- package/src/lib/app-utils/server.d.ts +4 -0
- package/src/lib/app-utils/server.js +7 -0
- package/src/lib/app-utils/server.js.map +1 -1
- package/src/lib/app-utils/site-care-project.d.ts +227 -0
- package/src/lib/app-utils/site-care-project.js +423 -0
- package/src/lib/app-utils/site-care-project.js.map +1 -0
- package/src/lib/app-utils/site-care.d.ts +291 -0
- package/src/lib/app-utils/site-care.js +310 -0
- package/src/lib/app-utils/site-care.js.map +1 -0
- package/src/lib/app-utils/upgrade-proposal.d.ts +29 -4
- package/src/lib/app-utils/upgrade-proposal.js +60 -9
- package/src/lib/app-utils/upgrade-proposal.js.map +1 -1
- package/src/lib/foundation/definitions/org-billing.types.d.ts +40 -10
- package/src/lib/foundation/definitions/org-billing.types.js.map +1 -1
- package/src/lib/foundation/definitions/organization.types.d.ts +3 -1
- package/src/lib/foundation/definitions/organization.types.js.map +1 -1
- package/src/lib/foundation/definitions/platform.types.d.ts +20 -0
- package/src/lib/foundation/definitions/platform.types.js.map +1 -1
- package/src/lib/foundation/index.d.ts +1 -1
- package/src/lib/foundation/index.js.map +1 -1
- package/src/lib/plugin-manager/feature-plugins.d.ts +48 -0
- package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
- package/src/lib/plugin-manager/first-party-plugins.generated.js +16 -0
- package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
- package/src/lib/plugin-manager/plugin-ai-capabilities.d.ts +19 -0
- package/src/lib/plugin-manager/plugin-ai-capabilities.js.map +1 -1
- package/src/lib/plugin-manager/plugin-domain-search.d.ts +81 -0
- package/src/lib/plugin-manager/plugin-domain-search.js +63 -0
- package/src/lib/plugin-manager/plugin-domain-search.js.map +1 -0
- package/src/lib/plugin-manager/plugin-entitlements.d.ts +23 -5
- package/src/lib/plugin-manager/plugin-entitlements.js +10 -0
- package/src/lib/plugin-manager/plugin-entitlements.js.map +1 -1
- package/src/lib/plugin-manager/plugin-media-ingest.d.ts +18 -1
- package/src/lib/plugin-manager/plugin-media-ingest.js.map +1 -1
- package/src/lib/plugin-manager/plugin-site-resources.d.ts +74 -0
- package/src/lib/plugin-manager/plugin-site-resources.js +135 -0
- package/src/lib/plugin-manager/plugin-site-resources.js.map +1 -0
- package/src/lib/plugin-manager/purchase-gate-preclearance.d.ts +61 -0
- package/src/lib/plugin-manager/purchase-gate-preclearance.js +102 -0
- package/src/lib/plugin-manager/purchase-gate-preclearance.js.map +1 -0
|
@@ -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 { operatorIdentity } from './operator-identity'\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/**\n * WHERE A CHANGE IS EMAILED (AGL-3997): the support address. Site Care has no\n * address of its own. An owner writes to the support address they already\n * know, and a message to it from a member of a workspace with Site Care files\n * itself as a request; every other message is ordinary support mail. The\n * support mailbox also delivers each message to `support@<inbound mail\n * domain>`, where the mail provider announces it, and every Site Care email\n * replies to `support+<requestId>@<inbound mail domain>`, so an answer joins\n * its own request.\n */\nexport const SITE_CARE_INBOUND_LOCAL_PART = 'support'\n\n/**\n * The operator's support address, from the operator identity: what an owner\n * is told to email a change to, and what the intake reads as one. `null` on\n * a deployment that publishes no support address, where Request a change on\n * a site's dashboard is the only way in.\n */\nexport function siteCareSupportAddress(): string | null {\n return operatorIdentity().supportEmail\n}\n\n/**\n * The Reply-To of a Site Care email: `support+<requestId>@<domain>` for an\n * email about one request, so the answer joins it; the support address for\n * one about no single request (a statement, a project), so the answer reaches\n * the support inbox like any other message to it.\n */\nexport function siteCareReplyAddress(\n requestId: string | null,\n domain: string,\n supportEmail: string | null = siteCareSupportAddress(),\n): string {\n if (requestId) return `${SITE_CARE_INBOUND_LOCAL_PART}+${requestId}@${domain}`\n return supportEmail ?? `${SITE_CARE_INBOUND_LOCAL_PART}@${domain}`\n}\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","operatorIdentity","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","siteCareSupportAddress","supportEmail","siteCareReplyAddress","requestId","domain","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","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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"}
|
|
@@ -44,6 +44,20 @@ export declare function normalizeUpgradeProposalNote(note: unknown): string | un
|
|
|
44
44
|
* button opens a checkout for an unknown tier is worse than no card.
|
|
45
45
|
*/
|
|
46
46
|
export declare function readUpgradeProposal(org: Partial<AglynOrgBilling> | null | undefined): OrgUpgradeProposal | null;
|
|
47
|
+
/**
|
|
48
|
+
* The bundle half of a proposal (AGL-3956) as it is stored: add-ons through
|
|
49
|
+
* the shared parser and fitted to the plan, an interval only when one was
|
|
50
|
+
* chosen, a coupon only when it reads as a code, and the Aglyn AI tier
|
|
51
|
+
* (AGL-3965) only with the AI add-on — an `ai:<tier>` entry, or the stored
|
|
52
|
+
* `aiTier`. Whatever cannot be honored is left out rather than stored, so
|
|
53
|
+
* Billing is never asked to preselect something the checkout would refuse.
|
|
54
|
+
*/
|
|
55
|
+
export declare function normalizeUpgradeProposalBundle(plan: OrgPlan, input: {
|
|
56
|
+
addons?: unknown;
|
|
57
|
+
interval?: unknown;
|
|
58
|
+
coupon?: unknown;
|
|
59
|
+
aiTier?: unknown;
|
|
60
|
+
} | null | undefined): Pick<OrgUpgradeProposal, 'addons' | 'interval' | 'coupon' | 'aiTier'>;
|
|
47
61
|
/**
|
|
48
62
|
* The proposal the workspace should be shown: the stored one, while no
|
|
49
63
|
* subscription is live.
|
|
@@ -55,12 +69,23 @@ export declare function readUpgradeProposal(org: Partial<AglynOrgBilling> | null
|
|
|
55
69
|
* clears the field then, and this hides it in the meantime.
|
|
56
70
|
*/
|
|
57
71
|
export declare function standingUpgradeProposal(org: Partial<AglynOrgBilling> | null | undefined): OrgUpgradeProposal | null;
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
72
|
+
type ProposalBundle = Pick<OrgUpgradeProposal, 'plan'> & Partial<Pick<OrgUpgradeProposal, 'addons' | 'interval' | 'coupon' | 'aiTier'>>;
|
|
73
|
+
/**
|
|
74
|
+
* The intent a proposal carries into Billing: the plan, and the add-ons,
|
|
75
|
+
* cadence and coupon staff proposed with it (AGL-3956) — the same fields the
|
|
76
|
+
* bundle checkout link carries (AGL-3955), so the proposal card and the link
|
|
77
|
+
* land on the same preselected page.
|
|
78
|
+
*/
|
|
79
|
+
export declare function upgradeProposalIntent(proposal: ProposalBundle): OnboardingPlanIntent;
|
|
80
|
+
/** Where the proposal's button goes: Billing, with the bundle preselected. */
|
|
81
|
+
export declare function upgradeProposalDestination(orgSlug: string, proposal: ProposalBundle): string;
|
|
82
|
+
/** "Starter with Site Care, billed yearly, code BOOTH25". */
|
|
83
|
+
export declare function upgradeProposalSummary(proposal: ProposalBundle, options?: {
|
|
84
|
+
brand?: string;
|
|
85
|
+
}): string;
|
|
62
86
|
/** "Acme's Aglyn team proposed the Starter plan". */
|
|
63
87
|
export declare function upgradeProposalHeadline(proposal: Pick<OrgUpgradeProposal, 'plan'>, options: {
|
|
64
88
|
orgName?: string | null;
|
|
65
89
|
productName: string;
|
|
66
90
|
}): string;
|
|
91
|
+
export {};
|
|
@@ -23,9 +23,10 @@
|
|
|
23
23
|
* `orgs/{orgId}.upgradeProposal`; this module decides whether it still
|
|
24
24
|
* stands, what it may name, and where the button under it goes.
|
|
25
25
|
*/ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
26
|
+
import { describeBundle, fitBundleAddonsToPlan, hasBundleAddons, normalizeBundleCoupon, parseBundleAddons } from "./bundle-intent.js";
|
|
26
27
|
import { onboardingDestination } from "./onboarding-deep-link.js";
|
|
27
28
|
import { isLiveSubscriptionStatus } from "./org-billing-doc.js";
|
|
28
|
-
import { PLAN_LABELS, SELF_SERVE_PLANS } from "./plan-entitlements.js";
|
|
29
|
+
import { isAiTierId, PLAN_LABELS, SELF_SERVE_PLANS } from "./plan-entitlements.js";
|
|
29
30
|
/** The longest staff note a proposal carries. */ export const UPGRADE_PROPOSAL_NOTE_MAX = 500;
|
|
30
31
|
/**
|
|
31
32
|
* The plans staff may propose: every tier a customer can buy themselves,
|
|
@@ -51,12 +52,36 @@ export function isUpgradeProposalPlan(plan) {
|
|
|
51
52
|
return null;
|
|
52
53
|
}
|
|
53
54
|
const note = normalizeUpgradeProposalNote(raw.note);
|
|
55
|
+
const bundle = normalizeUpgradeProposalBundle(raw.plan, raw);
|
|
54
56
|
return _extends({
|
|
55
57
|
plan: raw.plan,
|
|
56
58
|
proposedBy: typeof raw.proposedBy === 'string' ? raw.proposedBy : '',
|
|
57
59
|
proposedAt: Number.isFinite(raw.proposedAt) ? Number(raw.proposedAt) : 0
|
|
58
60
|
}, note ? {
|
|
59
61
|
note
|
|
62
|
+
} : {}, bundle);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The bundle half of a proposal (AGL-3956) as it is stored: add-ons through
|
|
66
|
+
* the shared parser and fitted to the plan, an interval only when one was
|
|
67
|
+
* chosen, a coupon only when it reads as a code, and the Aglyn AI tier
|
|
68
|
+
* (AGL-3965) only with the AI add-on — an `ai:<tier>` entry, or the stored
|
|
69
|
+
* `aiTier`. Whatever cannot be honored is left out rather than stored, so
|
|
70
|
+
* Billing is never asked to preselect something the checkout would refuse.
|
|
71
|
+
*/ export function normalizeUpgradeProposalBundle(plan, input) {
|
|
72
|
+
const parsed = parseBundleAddons(input == null ? void 0 : input.addons);
|
|
73
|
+
const addons = fitBundleAddonsToPlan(parsed.addons, plan).addons;
|
|
74
|
+
const interval = (input == null ? void 0 : input.interval) === 'year' || (input == null ? void 0 : input.interval) === 'month' ? input.interval : undefined;
|
|
75
|
+
const coupon = normalizeBundleCoupon(input == null ? void 0 : input.coupon);
|
|
76
|
+
const aiTier = addons.aiAddon ? isAiTierId(input == null ? void 0 : input.aiTier) ? input.aiTier : parsed.aiTier : undefined;
|
|
77
|
+
return _extends({}, hasBundleAddons(addons) ? {
|
|
78
|
+
addons
|
|
79
|
+
} : {}, interval ? {
|
|
80
|
+
interval
|
|
81
|
+
} : {}, coupon ? {
|
|
82
|
+
coupon
|
|
83
|
+
} : {}, aiTier ? {
|
|
84
|
+
aiTier
|
|
60
85
|
} : {});
|
|
61
86
|
}
|
|
62
87
|
/**
|
|
@@ -72,19 +97,45 @@ export function isUpgradeProposalPlan(plan) {
|
|
|
72
97
|
if (isLiveSubscriptionStatus(org == null ? void 0 : org.billingStatus)) return null;
|
|
73
98
|
return readUpgradeProposal(org);
|
|
74
99
|
}
|
|
75
|
-
/**
|
|
76
|
-
|
|
100
|
+
/**
|
|
101
|
+
* The intent a proposal carries into Billing: the plan, and the add-ons,
|
|
102
|
+
* cadence and coupon staff proposed with it (AGL-3956) — the same fields the
|
|
103
|
+
* bundle checkout link carries (AGL-3955), so the proposal card and the link
|
|
104
|
+
* land on the same preselected page.
|
|
105
|
+
*/ export function upgradeProposalIntent(proposal) {
|
|
106
|
+
const addons = fitBundleAddonsToPlan(proposal.addons, proposal.plan).addons;
|
|
107
|
+
const coupon = normalizeBundleCoupon(proposal.coupon);
|
|
108
|
+
const interval = proposal.interval === 'year' || proposal.interval === 'month' ? proposal.interval : null;
|
|
109
|
+
return _extends({
|
|
77
110
|
plan: proposal.plan,
|
|
78
|
-
interval: 'month',
|
|
79
|
-
//
|
|
80
|
-
// its toggle says rather than being told "monthly".
|
|
81
|
-
intervalStated:
|
|
111
|
+
interval: interval != null ? interval : 'month',
|
|
112
|
+
// A proposal with no cadence names a plan, not a cadence, so the page
|
|
113
|
+
// keeps whatever its toggle says rather than being told "monthly".
|
|
114
|
+
intervalStated: interval !== null,
|
|
82
115
|
contactSales: false
|
|
83
|
-
}
|
|
116
|
+
}, addons.aiAddon ? {
|
|
117
|
+
ai: true
|
|
118
|
+
} : {}, addons.aiAddon && isAiTierId(proposal.aiTier) ? {
|
|
119
|
+
aiTier: proposal.aiTier
|
|
120
|
+
} : {}, hasBundleAddons(addons) ? {
|
|
121
|
+
addons
|
|
122
|
+
} : {}, coupon ? {
|
|
123
|
+
coupon
|
|
124
|
+
} : {});
|
|
84
125
|
}
|
|
85
|
-
/** Where the proposal's button goes: Billing, with the
|
|
126
|
+
/** Where the proposal's button goes: Billing, with the bundle preselected. */ export function upgradeProposalDestination(orgSlug, proposal) {
|
|
86
127
|
return onboardingDestination(orgSlug, upgradeProposalIntent(proposal));
|
|
87
128
|
}
|
|
129
|
+
/** "Starter with Site Care, billed yearly, code BOOTH25". */ export function upgradeProposalSummary(proposal, options = {}) {
|
|
130
|
+
var _proposal_addons, _proposal_interval, _proposal_coupon, _proposal_aiTier;
|
|
131
|
+
return describeBundle({
|
|
132
|
+
plan: proposal.plan,
|
|
133
|
+
addons: (_proposal_addons = proposal.addons) != null ? _proposal_addons : null,
|
|
134
|
+
interval: (_proposal_interval = proposal.interval) != null ? _proposal_interval : null,
|
|
135
|
+
coupon: (_proposal_coupon = proposal.coupon) != null ? _proposal_coupon : null,
|
|
136
|
+
aiTier: (_proposal_aiTier = proposal.aiTier) != null ? _proposal_aiTier : null
|
|
137
|
+
}, options);
|
|
138
|
+
}
|
|
88
139
|
/** "Acme's Aglyn team proposed the Starter plan". */ export function upgradeProposalHeadline(proposal, options) {
|
|
89
140
|
var _options_orgName;
|
|
90
141
|
const workspace = (_options_orgName = options.orgName) == null ? void 0 : _options_orgName.trim();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/upgrade-proposal.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\n/**\n * The platform team's request that a workspace move to a paid plan\n * (AGL-3466), read in one place.\n *\n * A workspace staff build for a client is handed over first and sold second:\n * the client joins, looks around on whatever the workspace already has, and\n * only then is asked to upgrade. Staff record that ask as\n * `orgs/{orgId}.upgradeProposal`; this module decides whether it still\n * stands, what it may name, and where the button under it goes.\n */\n\nimport type {\n AglynOrgBilling,\n OrgPlan,\n OrgUpgradeProposal,\n} from '../foundation/definitions/org-billing.types'\nimport {\n onboardingDestination,\n type OnboardingPlanIntent,\n} from './onboarding-deep-link'\nimport { isLiveSubscriptionStatus } from './org-billing-doc'\nimport { PLAN_LABELS, SELF_SERVE_PLANS } from './plan-entitlements'\n\n/** The longest staff note a proposal carries. */\nexport const UPGRADE_PROPOSAL_NOTE_MAX = 500\n\n/**\n * The plans staff may propose: every tier a customer can buy themselves,\n * less Free, which is not a purchase. Enterprise is quoted per deal, so it\n * is never proposed through a checkout link.\n */\nexport const UPGRADE_PROPOSAL_PLANS: readonly OrgPlan[] = SELF_SERVE_PLANS.filter(\n (plan) => plan !== 'free',\n)\n\nexport function isUpgradeProposalPlan(plan: unknown): plan is OrgPlan {\n return (\n typeof plan === 'string' &&\n (UPGRADE_PROPOSAL_PLANS as readonly string[]).includes(plan)\n )\n}\n\n/** A staff note as it is stored: trimmed, bounded, and absent when blank. */\nexport function normalizeUpgradeProposalNote(note: unknown): string | undefined {\n if (typeof note !== 'string') return undefined\n const trimmed = note.trim().slice(0, UPGRADE_PROPOSAL_NOTE_MAX)\n return trimmed || undefined\n}\n\n/**\n * The stored proposal, narrowed, or null when there is none worth showing.\n *\n * A proposal naming a plan this build cannot sell reads as none: a card whose\n * button opens a checkout for an unknown tier is worse than no card.\n */\nexport function readUpgradeProposal(\n org: Partial<AglynOrgBilling> | null | undefined,\n): OrgUpgradeProposal | null {\n const raw = org?.upgradeProposal as Partial<OrgUpgradeProposal> | null | undefined\n if (!raw || typeof raw !== 'object' || !isUpgradeProposalPlan(raw.plan)) {\n return null\n }\n const note = normalizeUpgradeProposalNote(raw.note)\n return {\n plan: raw.plan,\n proposedBy: typeof raw.proposedBy === 'string' ? raw.proposedBy : '',\n proposedAt: Number.isFinite(raw.proposedAt) ? Number(raw.proposedAt) : 0,\n ...(note ? { note } : {}),\n }\n}\n\n/**\n * The proposal the workspace should be shown: the stored one, while no\n * subscription is live.\n *\n * `billingStatus` is the org document's mirror of the subscription status,\n * readable by every member, so this answers from the document the console\n * already holds. A live subscription means the workspace has bought a plan,\n * which settles the question the proposal asked; `writeOrgBilling`'s caller\n * clears the field then, and this hides it in the meantime.\n */\nexport function standingUpgradeProposal(\n org: Partial<AglynOrgBilling> | null | undefined,\n): OrgUpgradeProposal | null {\n if (isLiveSubscriptionStatus(org?.billingStatus)) return null\n return readUpgradeProposal(org)\n}\n\n/** The plan intent a proposal carries into Billing: the plan, no interval. */\nexport function upgradeProposalIntent(\n proposal: Pick<OrgUpgradeProposal, 'plan'>,\n): OnboardingPlanIntent {\n return {\n plan: proposal.plan,\n interval: 'month',\n // The proposal names a plan, not a cadence, so the page keeps whatever\n // its toggle says rather than being told \"monthly\".\n intervalStated: false,\n contactSales: false,\n }\n}\n\n/** Where the proposal's button goes: Billing, with the plan preselected. */\nexport function upgradeProposalDestination(\n orgSlug: string,\n proposal: Pick<OrgUpgradeProposal, 'plan'>,\n): string {\n return onboardingDestination(orgSlug, upgradeProposalIntent(proposal))\n}\n\n/** \"Acme's Aglyn team proposed the Starter plan\". */\nexport function upgradeProposalHeadline(\n proposal: Pick<OrgUpgradeProposal, 'plan'>,\n options: { orgName?: string | null; productName: string },\n): string {\n const workspace = options.orgName?.trim()\n const team = workspace\n ? `${workspace}'s ${options.productName} team`\n : `Your ${options.productName} team`\n return `${team} proposed the ${PLAN_LABELS[proposal.plan]} plan`\n}\n"],"names":["onboardingDestination","isLiveSubscriptionStatus","PLAN_LABELS","SELF_SERVE_PLANS","UPGRADE_PROPOSAL_NOTE_MAX","UPGRADE_PROPOSAL_PLANS","filter","plan","isUpgradeProposalPlan","includes","normalizeUpgradeProposalNote","note","undefined","trimmed","trim","slice","readUpgradeProposal","org","raw","upgradeProposal","proposedBy","proposedAt","Number","isFinite","standingUpgradeProposal","billingStatus","upgradeProposalIntent","proposal","interval","intervalStated","contactSales","upgradeProposalDestination","orgSlug","upgradeProposalHeadline","options","workspace","orgName","team","productName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;CASC;AAOD,SACEA,qBAAqB,QAEhB,4BAAwB;AAC/B,SAASC,wBAAwB,QAAQ,uBAAmB;AAC5D,SAASC,WAAW,EAAEC,gBAAgB,QAAQ,yBAAqB;AAEnE,+CAA+C,GAC/C,OAAO,MAAMC,4BAA4B,IAAG;AAE5C;;;;CAIC,GACD,OAAO,MAAMC,yBAA6CF,iBAAiBG,MAAM,CAC/E,CAACC,OAASA,SAAS,QACpB;AAED,OAAO,SAASC,sBAAsBD,IAAa;IACjD,OACE,OAAOA,SAAS,YAChB,AAACF,uBAA6CI,QAAQ,CAACF;AAE3D;AAEA,2EAA2E,GAC3E,OAAO,SAASG,6BAA6BC,IAAa;IACxD,IAAI,OAAOA,SAAS,UAAU,OAAOC;IACrC,MAAMC,UAAUF,KAAKG,IAAI,GAAGC,KAAK,CAAC,GAAGX;IACrC,OAAOS,WAAWD;AACpB;AAEA;;;;;CAKC,GACD,OAAO,SAASI,oBACdC,GAAgD;IAEhD,MAAMC,MAAMD,uBAAAA,IAAKE,eAAe;IAChC,IAAI,CAACD,OAAO,OAAOA,QAAQ,YAAY,CAACV,sBAAsBU,IAAIX,IAAI,GAAG;QACvE,OAAO;IACT;IACA,MAAMI,OAAOD,6BAA6BQ,IAAIP,IAAI;IAClD,OAAO;QACLJ,MAAMW,IAAIX,IAAI;QACda,YAAY,OAAOF,IAAIE,UAAU,KAAK,WAAWF,IAAIE,UAAU,GAAG;QAClEC,YAAYC,OAAOC,QAAQ,CAACL,IAAIG,UAAU,IAAIC,OAAOJ,IAAIG,UAAU,IAAI;OACnEV,OAAO;QAAEA;IAAK,IAAI,CAAC;AAE3B;AAEA;;;;;;;;;CASC,GACD,OAAO,SAASa,wBACdP,GAAgD;IAEhD,IAAIhB,yBAAyBgB,uBAAAA,IAAKQ,aAAa,GAAG,OAAO;IACzD,OAAOT,oBAAoBC;AAC7B;AAEA,4EAA4E,GAC5E,OAAO,SAASS,sBACdC,QAA0C;IAE1C,OAAO;QACLpB,MAAMoB,SAASpB,IAAI;QACnBqB,UAAU;QACV,uEAAuE;QACvE,oDAAoD;QACpDC,gBAAgB;QAChBC,cAAc;IAChB;AACF;AAEA,0EAA0E,GAC1E,OAAO,SAASC,2BACdC,OAAe,EACfL,QAA0C;IAE1C,OAAO3B,sBAAsBgC,SAASN,sBAAsBC;AAC9D;AAEA,mDAAmD,GACnD,OAAO,SAASM,wBACdN,QAA0C,EAC1CO,OAAyD;QAEvCA;IAAlB,MAAMC,aAAYD,mBAAAA,QAAQE,OAAO,qBAAfF,iBAAiBpB,IAAI;IACvC,MAAMuB,OAAOF,YACT,GAAGA,UAAU,GAAG,EAAED,QAAQI,WAAW,CAAC,KAAK,CAAC,GAC5C,CAAC,KAAK,EAAEJ,QAAQI,WAAW,CAAC,KAAK,CAAC;IACtC,OAAO,GAAGD,KAAK,cAAc,EAAEnC,WAAW,CAACyB,SAASpB,IAAI,CAAC,CAAC,KAAK,CAAC;AAClE"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/upgrade-proposal.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\n/**\n * The platform team's request that a workspace move to a paid plan\n * (AGL-3466), read in one place.\n *\n * A workspace staff build for a client is handed over first and sold second:\n * the client joins, looks around on whatever the workspace already has, and\n * only then is asked to upgrade. Staff record that ask as\n * `orgs/{orgId}.upgradeProposal`; this module decides whether it still\n * stands, what it may name, and where the button under it goes.\n */\n\nimport type {\n AglynOrgBilling,\n OrgPlan,\n OrgUpgradeProposal,\n} from '../foundation/definitions/org-billing.types'\nimport {\n describeBundle,\n fitBundleAddonsToPlan,\n hasBundleAddons,\n normalizeBundleCoupon,\n parseBundleAddons,\n type BundleAddons,\n} from './bundle-intent'\nimport {\n onboardingDestination,\n type OnboardingPlanIntent,\n} from './onboarding-deep-link'\nimport { isLiveSubscriptionStatus } from './org-billing-doc'\nimport { isAiTierId, PLAN_LABELS, SELF_SERVE_PLANS } from './plan-entitlements'\n\n/** The longest staff note a proposal carries. */\nexport const UPGRADE_PROPOSAL_NOTE_MAX = 500\n\n/**\n * The plans staff may propose: every tier a customer can buy themselves,\n * less Free, which is not a purchase. Enterprise is quoted per deal, so it\n * is never proposed through a checkout link.\n */\nexport const UPGRADE_PROPOSAL_PLANS: readonly OrgPlan[] = SELF_SERVE_PLANS.filter(\n (plan) => plan !== 'free',\n)\n\nexport function isUpgradeProposalPlan(plan: unknown): plan is OrgPlan {\n return (\n typeof plan === 'string' &&\n (UPGRADE_PROPOSAL_PLANS as readonly string[]).includes(plan)\n )\n}\n\n/** A staff note as it is stored: trimmed, bounded, and absent when blank. */\nexport function normalizeUpgradeProposalNote(note: unknown): string | undefined {\n if (typeof note !== 'string') return undefined\n const trimmed = note.trim().slice(0, UPGRADE_PROPOSAL_NOTE_MAX)\n return trimmed || undefined\n}\n\n/**\n * The stored proposal, narrowed, or null when there is none worth showing.\n *\n * A proposal naming a plan this build cannot sell reads as none: a card whose\n * button opens a checkout for an unknown tier is worse than no card.\n */\nexport function readUpgradeProposal(\n org: Partial<AglynOrgBilling> | null | undefined,\n): OrgUpgradeProposal | null {\n const raw = org?.upgradeProposal as Partial<OrgUpgradeProposal> | null | undefined\n if (!raw || typeof raw !== 'object' || !isUpgradeProposalPlan(raw.plan)) {\n return null\n }\n const note = normalizeUpgradeProposalNote(raw.note)\n const bundle = normalizeUpgradeProposalBundle(raw.plan, raw)\n return {\n plan: raw.plan,\n proposedBy: typeof raw.proposedBy === 'string' ? raw.proposedBy : '',\n proposedAt: Number.isFinite(raw.proposedAt) ? Number(raw.proposedAt) : 0,\n ...(note ? { note } : {}),\n ...bundle,\n }\n}\n\n/**\n * The bundle half of a proposal (AGL-3956) as it is stored: add-ons through\n * the shared parser and fitted to the plan, an interval only when one was\n * chosen, a coupon only when it reads as a code, and the Aglyn AI tier\n * (AGL-3965) only with the AI add-on — an `ai:<tier>` entry, or the stored\n * `aiTier`. Whatever cannot be honored is left out rather than stored, so\n * Billing is never asked to preselect something the checkout would refuse.\n */\nexport function normalizeUpgradeProposalBundle(\n plan: OrgPlan,\n input:\n | { addons?: unknown; interval?: unknown; coupon?: unknown; aiTier?: unknown }\n | null\n | undefined,\n): Pick<OrgUpgradeProposal, 'addons' | 'interval' | 'coupon' | 'aiTier'> {\n const parsed = parseBundleAddons(input?.addons)\n const addons: BundleAddons = fitBundleAddonsToPlan(parsed.addons, plan).addons\n const interval =\n input?.interval === 'year' || input?.interval === 'month' ? input.interval : undefined\n const coupon = normalizeBundleCoupon(input?.coupon)\n const aiTier = addons.aiAddon\n ? isAiTierId(input?.aiTier)\n ? input.aiTier\n : parsed.aiTier\n : undefined\n return {\n ...(hasBundleAddons(addons) ? { addons } : {}),\n ...(interval ? { interval } : {}),\n ...(coupon ? { coupon } : {}),\n ...(aiTier ? { aiTier } : {}),\n }\n}\n\n/**\n * The proposal the workspace should be shown: the stored one, while no\n * subscription is live.\n *\n * `billingStatus` is the org document's mirror of the subscription status,\n * readable by every member, so this answers from the document the console\n * already holds. A live subscription means the workspace has bought a plan,\n * which settles the question the proposal asked; `writeOrgBilling`'s caller\n * clears the field then, and this hides it in the meantime.\n */\nexport function standingUpgradeProposal(\n org: Partial<AglynOrgBilling> | null | undefined,\n): OrgUpgradeProposal | null {\n if (isLiveSubscriptionStatus(org?.billingStatus)) return null\n return readUpgradeProposal(org)\n}\n\ntype ProposalBundle = Pick<OrgUpgradeProposal, 'plan'> &\n Partial<Pick<OrgUpgradeProposal, 'addons' | 'interval' | 'coupon' | 'aiTier'>>\n\n/**\n * The intent a proposal carries into Billing: the plan, and the add-ons,\n * cadence and coupon staff proposed with it (AGL-3956) — the same fields the\n * bundle checkout link carries (AGL-3955), so the proposal card and the link\n * land on the same preselected page.\n */\nexport function upgradeProposalIntent(proposal: ProposalBundle): OnboardingPlanIntent {\n const addons = fitBundleAddonsToPlan(proposal.addons, proposal.plan).addons\n const coupon = normalizeBundleCoupon(proposal.coupon)\n const interval = proposal.interval === 'year' || proposal.interval === 'month'\n ? proposal.interval\n : null\n return {\n plan: proposal.plan,\n interval: interval ?? 'month',\n // A proposal with no cadence names a plan, not a cadence, so the page\n // keeps whatever its toggle says rather than being told \"monthly\".\n intervalStated: interval !== null,\n contactSales: false,\n ...(addons.aiAddon ? { ai: true as const } : {}),\n ...(addons.aiAddon && isAiTierId(proposal.aiTier) ? { aiTier: proposal.aiTier } : {}),\n ...(hasBundleAddons(addons) ? { addons } : {}),\n ...(coupon ? { coupon } : {}),\n }\n}\n\n/** Where the proposal's button goes: Billing, with the bundle preselected. */\nexport function upgradeProposalDestination(\n orgSlug: string,\n proposal: ProposalBundle,\n): string {\n return onboardingDestination(orgSlug, upgradeProposalIntent(proposal))\n}\n\n/** \"Starter with Site Care, billed yearly, code BOOTH25\". */\nexport function upgradeProposalSummary(\n proposal: ProposalBundle,\n options: { brand?: string } = {},\n): string {\n return describeBundle(\n {\n plan: proposal.plan,\n addons: proposal.addons ?? null,\n interval: proposal.interval ?? null,\n coupon: proposal.coupon ?? null,\n aiTier: proposal.aiTier ?? null,\n },\n options,\n )\n}\n\n/** \"Acme's Aglyn team proposed the Starter plan\". */\nexport function upgradeProposalHeadline(\n proposal: Pick<OrgUpgradeProposal, 'plan'>,\n options: { orgName?: string | null; productName: string },\n): string {\n const workspace = options.orgName?.trim()\n const team = workspace\n ? `${workspace}'s ${options.productName} team`\n : `Your ${options.productName} team`\n return `${team} proposed the ${PLAN_LABELS[proposal.plan]} plan`\n}\n"],"names":["describeBundle","fitBundleAddonsToPlan","hasBundleAddons","normalizeBundleCoupon","parseBundleAddons","onboardingDestination","isLiveSubscriptionStatus","isAiTierId","PLAN_LABELS","SELF_SERVE_PLANS","UPGRADE_PROPOSAL_NOTE_MAX","UPGRADE_PROPOSAL_PLANS","filter","plan","isUpgradeProposalPlan","includes","normalizeUpgradeProposalNote","note","undefined","trimmed","trim","slice","readUpgradeProposal","org","raw","upgradeProposal","bundle","normalizeUpgradeProposalBundle","proposedBy","proposedAt","Number","isFinite","input","parsed","addons","interval","coupon","aiTier","aiAddon","standingUpgradeProposal","billingStatus","upgradeProposalIntent","proposal","intervalStated","contactSales","ai","upgradeProposalDestination","orgSlug","upgradeProposalSummary","options","upgradeProposalHeadline","workspace","orgName","team","productName"],"mappings":"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"}
|
|
@@ -135,11 +135,10 @@ export interface CoreOrgFeatureFlags {
|
|
|
135
135
|
* generate-section rung every tier from Pro up carries. This is the
|
|
136
136
|
* expensive rung: a generated surface carries the node tree, the catalog
|
|
137
137
|
* and the theme in and iterates, so it draws credits by the hundreds where
|
|
138
|
-
* a question draws tens.
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
* agreement.
|
|
138
|
+
* a question draws tens. Since AGL-3965 every plan carries it: the plan's
|
|
139
|
+
* included `assistCreditsPerMonth` pays for generation and assist alike
|
|
140
|
+
* (one pool), an Aglyn AI tier adds credits on top, and Free's taste keeps
|
|
141
|
+
* its own wall. Enterprise carries it in the agreement.
|
|
143
142
|
*/
|
|
144
143
|
aiGenerative?: boolean;
|
|
145
144
|
/** No-code workflow builder (AGL-101). */
|
|
@@ -661,6 +660,11 @@ export interface OrgPlanComp {
|
|
|
661
660
|
/** When it was granted — the server's clock. */
|
|
662
661
|
grantedAt: ITimestamp | null;
|
|
663
662
|
}
|
|
663
|
+
/**
|
|
664
|
+
* The Aglyn AI tiers (AGL-3965), smallest first. Sold on their own to any
|
|
665
|
+
* paid plan; the figures are `AI_TIERS` in `plan-entitlements.ts`.
|
|
666
|
+
*/
|
|
667
|
+
export type AiTierId = 'spark' | 'boost' | 'plus' | 'max' | 'ultra' | 'studio';
|
|
664
668
|
/**
|
|
665
669
|
* Paid addon quantities (AGL-112/524) purchased on top of the plan's
|
|
666
670
|
* included allowances, billed as items on the org's Stripe subscription.
|
|
@@ -716,11 +720,10 @@ export interface OrgSeatAddons {
|
|
|
716
720
|
/**
|
|
717
721
|
* Aglyn AI org-wide toggle, 0/1 (AGL-2896). One purchase covers every
|
|
718
722
|
* host in the org, the way Event Calendar does: `resolveOrgEntitlements`
|
|
719
|
-
* reads any quantity >= 1 as one
|
|
720
|
-
* `
|
|
721
|
-
*
|
|
722
|
-
* plan
|
|
723
|
-
* does not sell it.
|
|
723
|
+
* reads any quantity >= 1 as one and adds the purchased tier's credits
|
|
724
|
+
* (`AI_TIERS[org.aiTier].credits`, AGL-3965) to `assistCreditsPerMonth`.
|
|
725
|
+
* WHICH tier is `AglynOrgBilling.aiTier`; the price is the tier's, on any
|
|
726
|
+
* paid plan (`planSellsAiTiers`).
|
|
724
727
|
*/
|
|
725
728
|
aiAddon?: number;
|
|
726
729
|
}
|
|
@@ -1142,6 +1145,18 @@ export interface AglynOrgBilling extends AglynDocument {
|
|
|
1142
1145
|
enabledPlugins?: string[];
|
|
1143
1146
|
/** Purchased addon seats (AGL-112); billed monthly per seat. */
|
|
1144
1147
|
seatAddons?: OrgSeatAddons;
|
|
1148
|
+
/**
|
|
1149
|
+
* Which Aglyn AI tier `seatAddons.aiAddon` is (AGL-3965): Spark through
|
|
1150
|
+
* Studio, read off the subscription's AI item by the webhook and the
|
|
1151
|
+
* add-ons route, and written beside `seatAddons` by the same writes. An
|
|
1152
|
+
* ENTITLEMENT INPUT like `seatAddons`, so Admin-SDK-only.
|
|
1153
|
+
*
|
|
1154
|
+
* Absent on an org whose add-on was bought before tiers existed: it then
|
|
1155
|
+
* reads as the tier with that plan's old band and price
|
|
1156
|
+
* (`LEGACY_AI_ADDON_TIER_BY_PLAN`), so no bill and no credit figure moved.
|
|
1157
|
+
* `null` once written means no tier is held.
|
|
1158
|
+
*/
|
|
1159
|
+
aiTier?: AiTierId | null;
|
|
1145
1160
|
/**
|
|
1146
1161
|
* POS register seats assigned out of the org pool (AGL-1775). An
|
|
1147
1162
|
* ENTITLEMENT INPUT: it raises a per-site cap, so it is Admin-SDK-only.
|
|
@@ -1259,6 +1274,21 @@ export interface OrgUpgradeProposal {
|
|
|
1259
1274
|
proposedAt: number;
|
|
1260
1275
|
/** An optional line from staff, shown with the proposal. */
|
|
1261
1276
|
note?: string;
|
|
1277
|
+
/**
|
|
1278
|
+
* The add-ons proposed with the plan (AGL-3956), by `org.seatAddons` kind,
|
|
1279
|
+
* fitted to what the plan sells. Billing preselects them beside the plan.
|
|
1280
|
+
*/
|
|
1281
|
+
addons?: Partial<Record<keyof Required<OrgSeatAddons>, number>>;
|
|
1282
|
+
/** The cadence proposed, when staff chose one; absent leaves Billing's toggle alone. */
|
|
1283
|
+
interval?: 'month' | 'year';
|
|
1284
|
+
/** A promotion code minted on the staff Coupons page, applied at checkout. */
|
|
1285
|
+
coupon?: string;
|
|
1286
|
+
/**
|
|
1287
|
+
* The Aglyn AI tier proposed with the AI add-on (AGL-3965), written
|
|
1288
|
+
* `ai:<tier>` in the add-on list. Absent, checkout sells the plan's
|
|
1289
|
+
* default tier.
|
|
1290
|
+
*/
|
|
1291
|
+
aiTier?: AiTierId;
|
|
1262
1292
|
}
|
|
1263
1293
|
/**
|
|
1264
1294
|
* What the CRM lets an organization decide for every site at once
|