@aglyn/aglyn 1.0.0-beta.219 → 1.0.0-beta.221
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 +12 -12
- package/src/lib/app-utils/actions.d.ts +1 -59
- package/src/lib/app-utils/actions.js +7 -72
- package/src/lib/app-utils/actions.js.map +1 -1
- package/src/lib/app-utils/consent-groups.d.ts +1 -1
- package/src/lib/app-utils/consent-groups.js.map +1 -1
- package/src/lib/app-utils/console-routes.d.ts +0 -36
- package/src/lib/app-utils/console-routes.js +2 -37
- package/src/lib/app-utils/console-routes.js.map +1 -1
- package/src/lib/app-utils/contacts.d.ts +1 -1
- package/src/lib/app-utils/contacts.js.map +1 -1
- package/src/lib/app-utils/container-membership.d.ts +93 -0
- package/src/lib/app-utils/container-membership.js +165 -0
- package/src/lib/app-utils/container-membership.js.map +1 -0
- package/src/lib/app-utils/crm-task-reminders.d.ts +23 -6
- package/src/lib/app-utils/crm-task-reminders.js +32 -11
- package/src/lib/app-utils/crm-task-reminders.js.map +1 -1
- package/src/lib/app-utils/crm.d.ts +24 -4
- package/src/lib/app-utils/crm.js +23 -0
- package/src/lib/app-utils/crm.js.map +1 -1
- package/src/lib/app-utils/csv-upload.d.ts +1 -1
- package/src/lib/app-utils/csv-upload.js +1 -1
- package/src/lib/app-utils/csv-upload.js.map +1 -1
- package/src/lib/app-utils/{dataset-csv.d.ts → csv.d.ts} +10 -43
- package/src/lib/app-utils/{dataset-csv.js → csv.js} +10 -81
- package/src/lib/app-utils/csv.js.map +1 -0
- package/src/lib/app-utils/datasets.d.ts +0 -19
- package/src/lib/app-utils/datasets.js +0 -13
- package/src/lib/app-utils/datasets.js.map +1 -1
- package/src/lib/app-utils/docs-help.generated.d.ts +4 -4
- package/src/lib/app-utils/docs-help.generated.js +5 -0
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.js +71 -36
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/dynamic-list-rule.js +3 -3
- package/src/lib/app-utils/dynamic-list-rule.js.map +1 -1
- package/src/lib/app-utils/email-text.d.ts +80 -0
- package/src/lib/app-utils/email-text.js +189 -0
- package/src/lib/app-utils/email-text.js.map +1 -0
- package/src/lib/app-utils/{list-assignment-policy.d.ts → enrollment-basis.d.ts} +4 -0
- package/src/lib/app-utils/{list-assignment-policy.js → enrollment-basis.js} +5 -1
- package/src/lib/app-utils/enrollment-basis.js.map +1 -0
- package/src/lib/app-utils/expand-repeatables.js +4 -4
- package/src/lib/app-utils/expand-repeatables.js.map +1 -1
- package/src/lib/app-utils/forms.js +2 -2
- package/src/lib/app-utils/forms.js.map +1 -1
- package/src/lib/app-utils/health-report.d.ts +2 -3
- package/src/lib/app-utils/health-report.js +30 -6
- package/src/lib/app-utils/health-report.js.map +1 -1
- package/src/lib/app-utils/inbound-mail-domain.d.ts +27 -0
- package/src/lib/app-utils/inbound-mail-domain.js +31 -0
- package/src/lib/app-utils/inbound-mail-domain.js.map +1 -0
- package/src/lib/app-utils/leaving-notice.d.ts +96 -0
- package/src/lib/app-utils/leaving-notice.js +157 -0
- package/src/lib/app-utils/leaving-notice.js.map +1 -0
- package/src/lib/app-utils/lockdown.d.ts +11 -0
- package/src/lib/app-utils/lockdown.js +33 -3
- package/src/lib/app-utils/lockdown.js.map +1 -1
- package/src/lib/app-utils/member-email-aliases.d.ts +25 -0
- package/src/lib/app-utils/member-email-aliases.js +29 -0
- package/src/lib/app-utils/member-email-aliases.js.map +1 -1
- package/src/lib/app-utils/notifications.d.ts +69 -0
- package/src/lib/app-utils/notifications.js +119 -0
- package/src/lib/app-utils/notifications.js.map +1 -1
- package/src/lib/app-utils/operator-alerts.d.ts +15 -1
- package/src/lib/app-utils/operator-alerts.js +64 -10
- package/src/lib/app-utils/operator-alerts.js.map +1 -1
- package/src/lib/app-utils/plan-entitlements.d.ts +164 -178
- package/src/lib/app-utils/plan-entitlements.js +200 -224
- package/src/lib/app-utils/plan-entitlements.js.map +1 -1
- package/src/lib/app-utils/platform-marketing-consent.d.ts +56 -2
- package/src/lib/app-utils/platform-marketing-consent.js +6 -5
- package/src/lib/app-utils/platform-marketing-consent.js.map +1 -1
- package/src/lib/app-utils/plugin-starter-templates.generated.d.ts +12 -0
- package/src/lib/app-utils/plugin-starter-templates.generated.js +711 -0
- package/src/lib/app-utils/plugin-starter-templates.generated.js.map +1 -0
- package/src/lib/app-utils/repeat-query.d.ts +56 -0
- package/src/lib/app-utils/{dataset-query.js → repeat-query.js} +16 -16
- package/src/lib/app-utils/repeat-query.js.map +1 -0
- package/src/lib/app-utils/screen-link-context-value.d.ts +7 -0
- package/src/lib/app-utils/screen-link-context-value.js.map +1 -1
- package/src/lib/app-utils/screen-link-context.js +14 -2
- package/src/lib/app-utils/screen-link-context.js.map +1 -1
- package/src/lib/app-utils/screen-route.d.ts +3 -0
- package/src/lib/app-utils/screen-route.js +6 -1
- package/src/lib/app-utils/screen-route.js.map +1 -1
- package/src/lib/app-utils/server.d.ts +7 -9
- package/src/lib/app-utils/server.js +18 -26
- package/src/lib/app-utils/server.js.map +1 -1
- package/src/lib/app-utils/starter-template-nodes.d.ts +137 -0
- package/src/lib/app-utils/starter-template-nodes.js +129 -0
- package/src/lib/app-utils/starter-template-nodes.js.map +1 -0
- package/src/lib/app-utils/starter-templates.d.ts +11 -34
- package/src/lib/app-utils/starter-templates.js +19 -230
- package/src/lib/app-utils/starter-templates.js.map +1 -1
- package/src/lib/app-utils/{email-topics.d.ts → subscription-topics.d.ts} +64 -48
- package/src/lib/app-utils/{email-topics.js → subscription-topics.js} +39 -109
- package/src/lib/app-utils/subscription-topics.js.map +1 -0
- package/src/lib/app-utils/usage-budget.d.ts +73 -188
- package/src/lib/app-utils/usage-budget.js +58 -194
- package/src/lib/app-utils/usage-budget.js.map +1 -1
- package/src/lib/app-utils/utm-attribution.d.ts +1 -1
- package/src/lib/app-utils/utm-attribution.js +1 -1
- package/src/lib/app-utils/utm-attribution.js.map +1 -1
- package/src/lib/app-utils/{campaign-touch.d.ts → utm-touch.d.ts} +21 -13
- package/src/lib/app-utils/{campaign-touch.js → utm-touch.js} +30 -22
- package/src/lib/app-utils/utm-touch.js.map +1 -0
- package/src/lib/app-utils/variables.d.ts +9 -9
- package/src/lib/app-utils/variables.js.map +1 -1
- package/src/lib/app-utils/visitor-record-ceiling.d.ts +32 -0
- package/src/lib/app-utils/visitor-record-ceiling.js +39 -0
- package/src/lib/app-utils/visitor-record-ceiling.js.map +1 -1
- package/src/lib/foundation/definitions/org-billing.types.d.ts +2 -2
- package/src/lib/foundation/definitions/org-billing.types.js.map +1 -1
- package/src/lib/plugin-manager/feature-plugins.d.ts +80 -79
- package/src/lib/plugin-manager/feature-plugins.js +22 -35
- package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
- package/src/lib/plugin-manager/first-party-plugins.generated.d.ts +18 -1
- package/src/lib/plugin-manager/first-party-plugins.generated.js +62 -2
- package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
- package/src/lib/plugin-manager/index.d.ts +0 -1
- package/src/lib/plugin-manager/index.js +0 -1
- package/src/lib/plugin-manager/index.js.map +1 -1
- package/src/lib/plugin-manager/plugin-contact-capture.d.ts +9 -4
- package/src/lib/plugin-manager/plugin-contact-capture.js.map +1 -1
- package/src/lib/plugin-manager/plugin-containers.d.ts +68 -0
- package/src/lib/plugin-manager/plugin-containers.generated.d.ts +10 -0
- package/src/lib/plugin-manager/plugin-containers.generated.js +20 -0
- package/src/lib/plugin-manager/plugin-containers.generated.js.map +1 -0
- package/src/lib/plugin-manager/plugin-containers.js +27 -0
- package/src/lib/plugin-manager/plugin-containers.js.map +1 -0
- package/src/lib/plugin-manager/plugin-entitlements.d.ts +8 -0
- package/src/lib/plugin-manager/plugin-entitlements.js.map +1 -1
- package/src/lib/plugin-manager/plugin-membership-detach.js +1 -1
- package/src/lib/plugin-manager/plugin-membership-detach.js.map +1 -1
- package/src/lib/plugin-manager/plugin-record-email-state.d.ts +69 -0
- package/src/lib/plugin-manager/plugin-record-email-state.js +32 -0
- package/src/lib/plugin-manager/plugin-record-email-state.js.map +1 -1
- package/src/lib/plugin-manager/plugin-record-lists.d.ts +115 -0
- package/src/lib/plugin-manager/plugin-record-lists.js +75 -0
- package/src/lib/plugin-manager/plugin-record-lists.js.map +1 -0
- package/src/lib/plugin-manager/plugin-record-timeline.d.ts +42 -8
- package/src/lib/plugin-manager/plugin-record-timeline.js.map +1 -1
- package/src/lib/plugin-manager/plugin-server-steps.d.ts +170 -0
- package/src/lib/plugin-manager/plugin-server-steps.js +116 -0
- package/src/lib/plugin-manager/plugin-server-steps.js.map +1 -0
- package/src/lib/plugin-manager/plugin-site-beacons.d.ts +71 -0
- package/src/lib/plugin-manager/plugin-site-beacons.js +54 -0
- package/src/lib/plugin-manager/plugin-site-beacons.js.map +1 -0
- package/src/lib/plugin-manager/plugin-subscription-topics.generated.d.ts +11 -0
- package/src/lib/plugin-manager/plugin-subscription-topics.generated.js +41 -0
- package/src/lib/plugin-manager/plugin-subscription-topics.generated.js.map +1 -0
- package/src/lib/plugin-manager/plugin-usage-axes.d.ts +212 -0
- package/src/lib/plugin-manager/plugin-usage-axes.js +82 -3
- package/src/lib/plugin-manager/plugin-usage-axes.js.map +1 -1
- package/src/lib/plugin-manager/plugin-usage-meters.d.ts +108 -0
- package/src/lib/plugin-manager/plugin-usage-meters.js +105 -0
- package/src/lib/plugin-manager/plugin-usage-meters.js.map +1 -0
- package/src/lib/plugin-manager/usage-alert-contributors.d.ts +76 -6
- package/src/lib/plugin-manager/usage-alert-contributors.js +23 -10
- package/src/lib/plugin-manager/usage-alert-contributors.js.map +1 -1
- package/src/lib/app-utils/assist-credits.d.ts +0 -482
- package/src/lib/app-utils/assist-credits.js +0 -510
- package/src/lib/app-utils/assist-credits.js.map +0 -1
- package/src/lib/app-utils/campaign-membership.d.ts +0 -121
- package/src/lib/app-utils/campaign-membership.js +0 -189
- package/src/lib/app-utils/campaign-membership.js.map +0 -1
- package/src/lib/app-utils/campaign-touch.js.map +0 -1
- package/src/lib/app-utils/console-record-links.d.ts +0 -84
- package/src/lib/app-utils/console-record-links.js +0 -76
- package/src/lib/app-utils/console-record-links.js.map +0 -1
- package/src/lib/app-utils/crm-booking.d.ts +0 -75
- package/src/lib/app-utils/crm-booking.js +0 -133
- package/src/lib/app-utils/crm-booking.js.map +0 -1
- package/src/lib/app-utils/crm-inbound.d.ts +0 -245
- package/src/lib/app-utils/crm-inbound.js +0 -407
- package/src/lib/app-utils/crm-inbound.js.map +0 -1
- package/src/lib/app-utils/crm-reports.d.ts +0 -479
- package/src/lib/app-utils/crm-reports.js +0 -680
- package/src/lib/app-utils/crm-reports.js.map +0 -1
- package/src/lib/app-utils/dataset-csv.js.map +0 -1
- package/src/lib/app-utils/dataset-query.d.ts +0 -57
- package/src/lib/app-utils/dataset-query.js.map +0 -1
- package/src/lib/app-utils/email-topics.js.map +0 -1
- package/src/lib/app-utils/list-assignment-policy.js.map +0 -1
- package/src/lib/plugin-manager/plugin-metered-lines.d.ts +0 -97
- package/src/lib/plugin-manager/plugin-metered-lines.js +0 -123
- package/src/lib/plugin-manager/plugin-metered-lines.js.map +0 -1
|
@@ -1,510 +0,0 @@
|
|
|
1
|
-
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
-
/**
|
|
3
|
-
* @license
|
|
4
|
-
* Copyright 2026 Aglyn LLC
|
|
5
|
-
*
|
|
6
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
-
* you may not use this file except in compliance with the License.
|
|
8
|
-
* You may obtain a copy of the License at
|
|
9
|
-
*
|
|
10
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
-
*
|
|
12
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
-
* See the License for the specific language governing permissions and
|
|
16
|
-
* limitations under the License.
|
|
17
|
-
*/ import { isUncappedPlanComp, resolveOrgEntitlements, resolvePlanPricing } from "./plan-entitlements.js";
|
|
18
|
-
import { planQuotaOf } from "../plugin-manager/plugin-plan-entitlements.js";
|
|
19
|
-
/**
|
|
20
|
-
* Aglyn Assist credits: the unit assist is sold, metered and refused in.
|
|
21
|
-
*
|
|
22
|
-
* ## Why cost, and not messages
|
|
23
|
-
*
|
|
24
|
-
* Assist is measured per exchange as `estCostUsd` — the exchange priced at
|
|
25
|
-
* the serving model's billed rates, computed where the tokens were counted. A
|
|
26
|
-
* message allowance would throw that measurement away and price every action
|
|
27
|
-
* the same. It cannot: a question is a few thousand tokens, while generating
|
|
28
|
-
* a screen carries the node tree, the component catalog and the theme tokens
|
|
29
|
-
* in, emits structured markup out, and iterates. The two differ by up to two
|
|
30
|
-
* orders of magnitude, so one workspace's ten screen builds can outspend
|
|
31
|
-
* another's thousand questions while both read as "within allowance".
|
|
32
|
-
*
|
|
33
|
-
* Metering in cost is the only unit under which those two workspaces draw
|
|
34
|
-
* down their bands differently, which is the entire point.
|
|
35
|
-
*
|
|
36
|
-
* ## Why a credit rather than the dollar figure
|
|
37
|
-
*
|
|
38
|
-
* `estCostUsd` is a dollar figure off our own cost model, not a price.
|
|
39
|
-
* Publishing it would put our model choice, our per-model rates and our
|
|
40
|
-
* margin on a billing page, and it would move under customers every time a
|
|
41
|
-
* model is swapped. A credit is a fixed quantity of that spend with a stable
|
|
42
|
-
* public meaning, so the band on the plan card stays the same number when
|
|
43
|
-
* the model behind it changes.
|
|
44
|
-
*
|
|
45
|
-
* Everything customer-facing counts credits. This module is the ONLY place
|
|
46
|
-
* the two units meet.
|
|
47
|
-
*
|
|
48
|
-
* ## `estCostUsd` is the BILLED figure, not the bill we pay (AGL-3015)
|
|
49
|
-
*
|
|
50
|
-
* It is every exchange priced at the catalog's billed rates, which a model
|
|
51
|
-
* may carry above what its provider charges. Credits are drawn from it, so
|
|
52
|
-
* it is the right figure for a band, a wall, a cap and an invoice line. It
|
|
53
|
-
* is the WRONG figure for a margin or a cost meter, which read the rollup's
|
|
54
|
-
* provider figure through `assistProviderCostUsd` instead.
|
|
55
|
-
*/ /**
|
|
56
|
-
* What one credit is BILLED at, in USD.
|
|
57
|
-
*
|
|
58
|
-
* A tenth of a cent, chosen for two properties rather than for roundness:
|
|
59
|
-
*
|
|
60
|
-
* - **1,000 credits cost exactly $1.00**, so the per-1,000 retail rates on
|
|
61
|
-
* `PLAN_PRICING.extraAssistCreditsUsdPer1k` read directly as multiples of
|
|
62
|
-
* cost. A rate of $2.00 per 1,000 is cost x2 with no arithmetic in between,
|
|
63
|
-
* which is what makes the 50% margin floor checkable by eye.
|
|
64
|
-
* - **A single question is tens of credits, a generated screen is hundreds.**
|
|
65
|
-
* At Sonnet list rates a short grounded answer runs around $0.013, so ~13
|
|
66
|
-
* credits; a screen build runs into the hundreds. Both are whole numbers
|
|
67
|
-
* with room to differ, which a cent-sized credit would have flattened to
|
|
68
|
-
* "1 vs 20" and a dollar-sized one to "0 vs 0".
|
|
69
|
-
*
|
|
70
|
-
* ⚠️ This is a COST-MODEL constant, not a price and not a rate we publish.
|
|
71
|
-
* It never appears on a customer surface; `assistCreditsFromUsd` is how a
|
|
72
|
-
* measured dollar figure becomes something a customer may be shown.
|
|
73
|
-
*
|
|
74
|
-
* ⚠️ AND IT IS NOT WHAT A CREDIT COSTS US (AGL-3015). The dollar figure this
|
|
75
|
-
* divides is priced at the model catalog's BILLED rates, and a model may be
|
|
76
|
-
* billed above what the provider charges. A credit is therefore a tenth of a
|
|
77
|
-
* cent of BILLED spend, and at most a tenth of a cent of provider spend. Read
|
|
78
|
-
* against a retail rate it yields the floor of the line margin, never the
|
|
79
|
-
* middle of it — which is the safe direction, and the direction every margin
|
|
80
|
-
* assertion in this repo depends on. What a period actually cost is measured,
|
|
81
|
-
* not derived: `assistProviderCostUsd` reads it off the rollup.
|
|
82
|
-
*/ export const ASSIST_CREDIT_COST_USD = 0.001;
|
|
83
|
-
/**
|
|
84
|
-
* The field a monthly assist rollup, a signal row and a kind bucket keep
|
|
85
|
-
* PROVIDER spend under, beside the `estCostUsd` a customer's credits are
|
|
86
|
-
* drawn from (AGL-3015).
|
|
87
|
-
*
|
|
88
|
-
* Named here rather than in the meter that writes it because the readers are
|
|
89
|
-
* spread across the plugin, the billing routes and the staff surfaces, and a
|
|
90
|
-
* field name spelled out at six call sites is a field name that will be
|
|
91
|
-
* spelled differently at the seventh.
|
|
92
|
-
*/ export const ASSIST_PROVIDER_COST_FIELD = 'providerCostUsd';
|
|
93
|
-
/**
|
|
94
|
-
* What a metered period ACTUALLY COST US, from the two dollar figures a
|
|
95
|
-
* rollup carries.
|
|
96
|
-
*
|
|
97
|
-
* `billedUsd` is `estCostUsd`: every exchange priced at its model's billed
|
|
98
|
-
* rates, and the figure credits are drawn from. `providerUsd` is
|
|
99
|
-
* `ASSIST_PROVIDER_COST_FIELD`: the same exchanges priced at what the
|
|
100
|
-
* provider charged. The second is the answer wherever it exists.
|
|
101
|
-
*
|
|
102
|
-
* ## Why a rollup may not carry one, and which way that errs
|
|
103
|
-
*
|
|
104
|
-
* The provider figure is written from the commit that split the two rates
|
|
105
|
-
* onward. A period closed before it answers with the BILLED figure, which is
|
|
106
|
-
* at or above what we paid — so a margin taken on history reads low and a
|
|
107
|
-
* cost reads high, the direction a cost figure is allowed to be wrong in.
|
|
108
|
-
* The one period that under-reads is the month in flight when the split
|
|
109
|
-
* shipped, whose provider figure covers only the exchanges after it; that
|
|
110
|
-
* month's error is bounded by the markup on a few days of a meter whose
|
|
111
|
-
* largest measured organization is a fraction of a cent.
|
|
112
|
-
*/ export function assistProviderCostUsd(billedUsd, providerUsd) {
|
|
113
|
-
const provider = Number(providerUsd);
|
|
114
|
-
if (Number.isFinite(provider) && provider > 0) return provider;
|
|
115
|
-
const billed = Number(billedUsd);
|
|
116
|
-
return Number.isFinite(billed) && billed > 0 ? billed : 0;
|
|
117
|
-
}
|
|
118
|
-
/**
|
|
119
|
-
* The minimum line margin every retail assist rate must clear.
|
|
120
|
-
*
|
|
121
|
-
* The same floor now applied to every retail rate in this codebase, and the
|
|
122
|
-
* reason the assist ladder stops descending at cost x2 on Agency instead of
|
|
123
|
-
* taking one more step the way the contacts ladder once did.
|
|
124
|
-
*/ export const ASSIST_CREDIT_MIN_MARGIN_PCT = 0.5;
|
|
125
|
-
/**
|
|
126
|
-
* Credits -> our cost in USD. Non-finite or negative input is 0 credits, so a
|
|
127
|
-
* corrupt band cannot become a budget of `NaN` that every comparison passes.
|
|
128
|
-
*/ export function assistUsdFromCredits(credits) {
|
|
129
|
-
const value = Number(credits);
|
|
130
|
-
if (!Number.isFinite(value) || value <= 0) return 0;
|
|
131
|
-
return Math.round(value * ASSIST_CREDIT_COST_USD * 1000000) / 1000000;
|
|
132
|
-
}
|
|
133
|
-
/**
|
|
134
|
-
* Measured USD -> credits, rounded UP.
|
|
135
|
-
*
|
|
136
|
-
* Up, because this converts spend the platform has ALREADY made into the
|
|
137
|
-
* figure a customer is shown against their band. Rounding down would let a
|
|
138
|
-
* long tail of sub-credit exchanges cost real money and draw nothing, which
|
|
139
|
-
* is the one direction a meter must not err in.
|
|
140
|
-
*/ export function assistCreditsFromUsd(usd) {
|
|
141
|
-
const value = Number(usd);
|
|
142
|
-
if (!Number.isFinite(value) || value <= 0) return 0;
|
|
143
|
-
return Math.ceil(value / ASSIST_CREDIT_COST_USD);
|
|
144
|
-
}
|
|
145
|
-
/**
|
|
146
|
-
* The org's monthly assist band in credits, or `null` when its plan sells no
|
|
147
|
-
* assist band at all.
|
|
148
|
-
*
|
|
149
|
-
* ## `null` is not zero, and the difference is the whole safety property
|
|
150
|
-
*
|
|
151
|
-
* A band of 0 resolves to `null` — "this org sells no band" — and never to
|
|
152
|
-
* a budget of `$0`. An org with no band still reaches the console
|
|
153
|
-
* assistant's docs-grounded rung, which is bounded by the free daily message
|
|
154
|
-
* cap and by the operator's spend backstop. Resolving zero as a budget would
|
|
155
|
-
* refuse that rung outright: a whole assistant turned off by a pricing field
|
|
156
|
-
* that was never about it.
|
|
157
|
-
*
|
|
158
|
-
* Since AGL-3203 no PLAN ROW bands at zero. Free carries the AGL-2925 taste
|
|
159
|
-
* of `FREE_AI_TASTE_CREDITS_PER_MONTH` with no rate beside it, so it
|
|
160
|
-
* resolves to a budget here and `assistBandRefuses` makes that budget a
|
|
161
|
-
* wall; Starter carries 750 WITH a rate, so its budget is a line it meters
|
|
162
|
-
* past rather than a wall. Both draw their docs-grounded chat on the same
|
|
163
|
-
* band, which is fine — each is sized so a month of it costs well under a
|
|
164
|
-
* dollar.
|
|
165
|
-
*
|
|
166
|
-
* So the zero case is now reached only by a per-org override that writes
|
|
167
|
-
* one, and the reading above is what keeps that org's assistant running.
|
|
168
|
-
*
|
|
169
|
-
* The Aglyn AI add-on (AGL-2896) needs no knowledge here either way: the
|
|
170
|
-
* resolver has already added `AI_ADDON_CREDITS_PER_MONTH[plan]` to the
|
|
171
|
-
* band, because the add-on widens the one pool rather than opening a
|
|
172
|
-
* second.
|
|
173
|
-
*
|
|
174
|
-
* The same reading is what makes the guard survive a stubbed entitlements
|
|
175
|
-
* module. A test double that answers 0 for every quota produces `null` here,
|
|
176
|
-
* which falls through to the operator backstop, rather than a budget of zero
|
|
177
|
-
* that refuses everything while the assertions read green. A clamp that goes
|
|
178
|
-
* green having refused every request has proved nothing, and 0 is exactly the
|
|
179
|
-
* value a stub returns.
|
|
180
|
-
*
|
|
181
|
-
* ## Non-finite is also `null`, deliberately
|
|
182
|
-
*
|
|
183
|
-
* `Infinity` must never resolve to a budget. It is unrepresentable off-process
|
|
184
|
-
* — `JSON.stringify(Infinity)` is `null` and reads back as 0 — so a band that
|
|
185
|
-
* reached here as `Infinity` is a band that would read as zero somewhere
|
|
186
|
-
* else. Falling through to the backstop is bounded and consistent; honouring
|
|
187
|
-
* it would be an unbounded budget on one process and a refusal on the next.
|
|
188
|
-
*
|
|
189
|
-
* An UNCAPPED staff comp (AGL-3049) is where that `Infinity` comes from on
|
|
190
|
-
* purpose, and `null` is right for it here too: there is no band to measure
|
|
191
|
-
* against, be over, price or alert on. What `null` must NOT bring with it is
|
|
192
|
-
* the backstop, whose repo default would cap the one workspace staff said
|
|
193
|
-
* has no cap — so `assistBandRefuses` answers false for it, and the
|
|
194
|
-
* reservation's ceiling (`assistMonthlyCeilingUsd` in the AI plugin) drops
|
|
195
|
-
* the default and keeps only an operator's explicit figure.
|
|
196
|
-
*/ export function resolveAssistCreditBudget(org) {
|
|
197
|
-
const credits = planQuotaOf(resolveOrgEntitlements(org), 'assistCreditsPerMonth');
|
|
198
|
-
if (!Number.isFinite(credits) || credits <= 0) return null;
|
|
199
|
-
return Math.floor(credits);
|
|
200
|
-
}
|
|
201
|
-
/** The org's monthly assist band in USD of provider spend, or `null`. */ export function resolveAssistBudgetUsd(org) {
|
|
202
|
-
const credits = resolveAssistCreditBudget(org);
|
|
203
|
-
return credits === null ? null : assistUsdFromCredits(credits);
|
|
204
|
-
}
|
|
205
|
-
/**
|
|
206
|
-
* Credits spent past the band. A `null` band yields 0 — an org with no band
|
|
207
|
-
* has nothing to be over, and reporting the whole month's spend as overage
|
|
208
|
-
* there would invoice the free tier for its docs answers.
|
|
209
|
-
*/ export function assistCreditOverage(usedCredits, bandCredits) {
|
|
210
|
-
const used = Number(usedCredits);
|
|
211
|
-
if (!Number.isFinite(used) || used <= 0) return 0;
|
|
212
|
-
if (bandCredits === null) return 0;
|
|
213
|
-
const band = Number(bandCredits);
|
|
214
|
-
if (!Number.isFinite(band) || band <= 0) return 0;
|
|
215
|
-
return Math.max(0, used - band);
|
|
216
|
-
}
|
|
217
|
-
/**
|
|
218
|
-
* The per-1,000 rate the org's assist credits are sold at past its band, or
|
|
219
|
-
* `null` when nothing is sold past it — the ONE reading of the rate
|
|
220
|
-
* (AGL-2896). `priceAssistCreditOverage`, `assistBandRefuses`,
|
|
221
|
-
* `assistRefusedByHardCap` and `assistHardCapRefusalText` all go through
|
|
222
|
-
* it, so the gate, the invoice and the refusal sentence cannot quote three
|
|
223
|
-
* different rates for one org.
|
|
224
|
-
*
|
|
225
|
-
* The plan's `extraAssistCreditsUsdPer1k` IS the answer — the whole answer,
|
|
226
|
-
* since AGL-3203. Starter used to be the one exception: it banded at 0, so
|
|
227
|
-
* the rate could not sit on its plan row without advertising a fee on a
|
|
228
|
-
* quantity the plan never sold, and the add-on's band was priced from a
|
|
229
|
-
* constant read here instead. Starter now includes 750 credits
|
|
230
|
-
* unconditionally and carries $3.00 on its own row, so the exception and its
|
|
231
|
-
* constant are gone and there is exactly one place the rate is written.
|
|
232
|
-
*
|
|
233
|
-
* Free and Enterprise are null with or without the add-on: Free sells no
|
|
234
|
-
* add-on and its taste band is a wall by decision (AGL-2925), and
|
|
235
|
-
* Enterprise's usage is in the contract.
|
|
236
|
-
*
|
|
237
|
-
* A STAFF COMP is null on every plan and with every add-on (AGL-3034),
|
|
238
|
-
* because `resolvePlanPricing` — never `PLAN_PRICING` indexed directly —
|
|
239
|
-
* sells nothing on a comp. That is what makes a comp's band a wall at the
|
|
240
|
-
* gate and prices its overage to zero at the charge: the AI overage path
|
|
241
|
-
* (AGL-3011) claims only what `assistMonthOverage` prices, so a comp never
|
|
242
|
-
* becomes a Stripe invoice. The function is a single read for that reason;
|
|
243
|
-
* a second source for the rate is how a comp gets quoted one.
|
|
244
|
-
*/ export function resolveAssistOverageRateUsdPer1k(org) {
|
|
245
|
-
return resolvePlanPricing(org).extraAssistCreditsUsdPer1k;
|
|
246
|
-
}
|
|
247
|
-
/**
|
|
248
|
-
* Prices assist credits past a plan's band.
|
|
249
|
-
*
|
|
250
|
-
* ## No `allowed`, deliberately — and for the opposite reason to email
|
|
251
|
-
*
|
|
252
|
-
* `priceEmailSendOverage` omits an `allowed` field because email has two
|
|
253
|
-
* gates and a transactional sender consulting the wrong one drops a password
|
|
254
|
-
* reset. Assist omits it because it has exactly ONE gate and this is not it:
|
|
255
|
-
* whether the band refuses is `assistBandRefuses`, applied inside
|
|
256
|
-
* `reserveAssistMessage` before a token is spent. A second answer to "may
|
|
257
|
-
* this proceed", computed after the fact from a rounded credit figure, would
|
|
258
|
-
* be a second gate to drift from the first.
|
|
259
|
-
*
|
|
260
|
-
* ## Takes the overage, does not recompute it
|
|
261
|
-
*
|
|
262
|
-
* `assistCreditOverage` derives the excess once, from the measured meter and
|
|
263
|
-
* the resolved band. Re-deriving it here would be a second overage model.
|
|
264
|
-
*
|
|
265
|
-
* A null rate yields zero structurally rather than by a check, which is what
|
|
266
|
-
* keeps Free and Starter at zero on this axis.
|
|
267
|
-
*/ export function priceAssistCreditOverage(org, overageCredits) {
|
|
268
|
-
const overageRateUsd = resolveAssistOverageRateUsdPer1k(org);
|
|
269
|
-
const credits = Number(overageCredits);
|
|
270
|
-
const over = Number.isFinite(credits) && credits > 0 ? credits : 0;
|
|
271
|
-
return {
|
|
272
|
-
overageCredits: over,
|
|
273
|
-
overageMonthlyUsd: overageRateUsd === null ? 0 : Math.round(over / 1000 * overageRateUsd * 100) / 100,
|
|
274
|
-
overageRateUsd
|
|
275
|
-
};
|
|
276
|
-
}
|
|
277
|
-
/**
|
|
278
|
-
* The line margin a per-1,000-credit retail rate earns AT LEAST, as a
|
|
279
|
-
* fraction.
|
|
280
|
-
*
|
|
281
|
-
* One expression, so the ladder and its floor cannot be checked against two
|
|
282
|
-
* different definitions of margin. A null or non-positive rate has no margin
|
|
283
|
-
* to report and answers `null` rather than 1 or 0 — "this plan sells no
|
|
284
|
-
* overage" is not "this plan sells overage at 100% margin".
|
|
285
|
-
*
|
|
286
|
-
* A LOWER BOUND, not the realized figure (AGL-3015). The cost side is
|
|
287
|
-
* `ASSIST_CREDIT_COST_USD`, which is what a credit BILLS at; a model billed
|
|
288
|
-
* above its provider rate costs us less than that per credit, so the real
|
|
289
|
-
* line margin is this or better and never worse. The floor stays checkable
|
|
290
|
-
* by eye — a $2.00 rate is still cost x2 — and a retail rate that clears the
|
|
291
|
-
* floor here clears it on every model mix. What a period realized is the
|
|
292
|
-
* staff margin card's business, and it reads measured provider spend.
|
|
293
|
-
*/ export function assistCreditRateMarginPct(rateUsdPer1k) {
|
|
294
|
-
if (rateUsdPer1k === null) return null;
|
|
295
|
-
const rate = Number(rateUsdPer1k);
|
|
296
|
-
if (!Number.isFinite(rate) || rate <= 0) return null;
|
|
297
|
-
const costPer1k = ASSIST_CREDIT_COST_USD * 1000;
|
|
298
|
-
return (rate - costPer1k) / rate;
|
|
299
|
-
}
|
|
300
|
-
/**
|
|
301
|
-
* Projects a measured spend figure and a ceiling into the credit view.
|
|
302
|
-
*
|
|
303
|
-
* Both inputs are nullable because the reservation's are: `costUsd` is `null`
|
|
304
|
-
* when nothing consulted the monthly document, and a ceiling is `null` when
|
|
305
|
-
* none applied. Neither collapses to 0 here — "nobody looked" and "spent
|
|
306
|
-
* nothing" have to stay distinguishable at the surface, exactly as they do on
|
|
307
|
-
* the reservation.
|
|
308
|
-
*/ export function publicAssistCredits(costUsd, ceilingUsd) {
|
|
309
|
-
const used = costUsd === null ? 0 : assistCreditsFromUsd(costUsd);
|
|
310
|
-
if (ceilingUsd === null) return {
|
|
311
|
-
used,
|
|
312
|
-
limit: null,
|
|
313
|
-
remaining: null
|
|
314
|
-
};
|
|
315
|
-
const limit = assistCreditsFromUsd(ceilingUsd);
|
|
316
|
-
return {
|
|
317
|
-
used,
|
|
318
|
-
limit,
|
|
319
|
-
remaining: Math.max(0, limit - used)
|
|
320
|
-
};
|
|
321
|
-
}
|
|
322
|
-
/**
|
|
323
|
-
* The console's name for the org's hard-cap switch (AGL-2653), quoted by the
|
|
324
|
-
* refusal so the words on the 402 and the words beside the switch are one
|
|
325
|
-
* string. Change it here and both move.
|
|
326
|
-
*/ export const ASSIST_HARD_CAP_CONTROL_LABEL = 'Stop AI assist at the included band';
|
|
327
|
-
/** Where the switch lives, in the words the refusal uses. */ export const ASSIST_HARD_CAP_CONTROL_LOCATION = 'Billing → Usage';
|
|
328
|
-
/**
|
|
329
|
-
* Whether the org asked to be stopped at its assist band (AGL-2653).
|
|
330
|
-
*
|
|
331
|
-
* Strictly `=== true`: the map is absent on every org that has not touched
|
|
332
|
-
* the switch, and absent is the selling default. A truthy string or a `1`
|
|
333
|
-
* written by hand must not switch an assistant off at the band, so nothing
|
|
334
|
-
* short of the boolean counts.
|
|
335
|
-
*/ export function resolveAssistHardCap(org) {
|
|
336
|
-
var _org_assistOverage;
|
|
337
|
-
return (org == null ? void 0 : (_org_assistOverage = org.assistOverage) == null ? void 0 : _org_assistOverage.hardCap) === true;
|
|
338
|
-
}
|
|
339
|
-
/**
|
|
340
|
-
* Is the org's assist band a WALL, or a line past which credits are sold?
|
|
341
|
-
*
|
|
342
|
-
* Two things make it a wall, and only two:
|
|
343
|
-
*
|
|
344
|
-
* - **The org asked.** `assistOverage.hardCap` is the customer's own ceiling,
|
|
345
|
-
* the `storageOverage.capUsd` of this meter.
|
|
346
|
-
* - **There is nothing to sell past it at.** An org whose
|
|
347
|
-
* `resolveAssistOverageRateUsdPer1k` is `null` has no rate to bill, so
|
|
348
|
-
* credits past its band would be provider spend with no invoice line — the
|
|
349
|
-
* silent free overage `plan-entitlements.spec.ts` forbids. Enterprise is
|
|
350
|
-
* one case, its band being contractual; FREE is the other, its taste band
|
|
351
|
-
* a wall by the AGL-2925 decision so that nothing about a Free workspace
|
|
352
|
-
* can produce a charge.
|
|
353
|
-
*
|
|
354
|
-
* Everything else sells past the band by default, which is the 2026-09-07
|
|
355
|
-
* decision this function encodes — and since AGL-3203 that includes Starter,
|
|
356
|
-
* which carries 750 credits and a $3.00 rate like every paid tier above it.
|
|
357
|
-
*
|
|
358
|
-
* An org with NO band at all answers the same as Enterprise, harmlessly:
|
|
359
|
-
* `resolveAssistBudgetUsd` is `null` for it and the reservation never
|
|
360
|
-
* measures against a band. No plan ROW is in that state any more — Free and
|
|
361
|
-
* Starter both were, at different times — so it is reached by a per-org
|
|
362
|
-
* override that writes a zero.
|
|
363
|
-
*
|
|
364
|
-
* An UNCAPPED staff comp (AGL-3049) is neither: it has no band to be a wall,
|
|
365
|
-
* and nothing is sold past a band it does not have. It answers false, ahead
|
|
366
|
-
* of the switch — a workspace staff uncapped is not stopped by a band, and
|
|
367
|
-
* the reservation reads that answer as "no wall, no backstop default".
|
|
368
|
-
*/ export function assistBandRefuses(org) {
|
|
369
|
-
if (isUncappedPlanComp(org)) return false;
|
|
370
|
-
if (resolveAssistHardCap(org)) return true;
|
|
371
|
-
return resolveAssistOverageRateUsdPer1k(org) === null;
|
|
372
|
-
}
|
|
373
|
-
/**
|
|
374
|
-
* The sentence a Free workspace is told when one of the taste's own
|
|
375
|
-
* precautions refused it, or `null` when the refusal was something else.
|
|
376
|
-
*
|
|
377
|
-
* One string per rung, for both doors and the gate ladder, so a customer
|
|
378
|
-
* reading the console panel and one reading the besigner get the same
|
|
379
|
-
* words. Every sentence is customer-safe: it names what to do (upgrade,
|
|
380
|
-
* wait) and never a figure from our cost model, a counter document, or
|
|
381
|
-
* another workspace.
|
|
382
|
-
*/ export function assistFreeTasteRefusalText(refusedBy) {
|
|
383
|
-
switch(refusedBy){
|
|
384
|
-
case 'account':
|
|
385
|
-
return 'Your free AI credits for this month are used across your ' + 'workspaces — upgrade any workspace to keep going.';
|
|
386
|
-
case 'requests':
|
|
387
|
-
return 'Free workspaces get a limited number of AI requests a day — ' + 'try again tomorrow, or upgrade to keep going.';
|
|
388
|
-
case 'refusals':
|
|
389
|
-
return 'The AI declined several requests today, so free AI generation is ' + 'paused until tomorrow. Try a different kind of brief then.';
|
|
390
|
-
case 'platform':
|
|
391
|
-
return 'Free AI generation is paused for the rest of today — try again ' + 'tomorrow. Paid workspaces are not affected.';
|
|
392
|
-
default:
|
|
393
|
-
return null;
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
/**
|
|
397
|
-
* Whether a refusal at the band was the org's OWN doing — the switch is on
|
|
398
|
-
* AND turning it off would have let the exchange through. The refusal that
|
|
399
|
-
* names the switch has to be the one the switch caused: on a plan with no
|
|
400
|
-
* rate the band refuses whatever the switch says, and telling that customer
|
|
401
|
-
* to turn it off would send them to a control that does nothing.
|
|
402
|
-
*/ export function assistRefusedByHardCap(org, refusedBy) {
|
|
403
|
-
return refusedBy === 'band' && resolveAssistHardCap(org) && resolveAssistOverageRateUsdPer1k(org) !== null;
|
|
404
|
-
}
|
|
405
|
-
/**
|
|
406
|
-
* The sentence a workspace is told when its own hard cap refused it. ONE
|
|
407
|
-
* string for both assist entrypoints, so the console panel and the besigner
|
|
408
|
-
* cannot describe the same control in two ways. It names the switch by its
|
|
409
|
-
* label, says where it lives, and quotes the plan's rate so the reader knows
|
|
410
|
-
* what turning it off costs.
|
|
411
|
-
*/ export function assistHardCapRefusalText(org) {
|
|
412
|
-
const rate = resolveAssistOverageRateUsdPer1k(org);
|
|
413
|
-
const rateClause = rate === null ? '' : ` at $${rate.toFixed(2)} per 1,000 credits`;
|
|
414
|
-
return `This workspace used its assistant credits for the month, and ` + `"${ASSIST_HARD_CAP_CONTROL_LABEL}" is on. Turn it off under ` + `${ASSIST_HARD_CAP_CONTROL_LOCATION} to keep going${rateClause}.`;
|
|
415
|
-
}
|
|
416
|
-
/**
|
|
417
|
-
* A month's assist overage from its measured spend — the ONE derivation the
|
|
418
|
-
* invoice and every meter read (AGL-2653).
|
|
419
|
-
*
|
|
420
|
-
* Takes `estCostUsd` as the rollup reads it and composes the three steps
|
|
421
|
-
* that already existed — credits from spend, overage past the band, price at
|
|
422
|
-
* the plan's rate — so `report-usage` cannot assemble them in a different
|
|
423
|
-
* order from a console readout and arrive at a different figure. The
|
|
424
|
-
* rounding is the other overage terms' rounding: `priceAssistCreditOverage`
|
|
425
|
-
* rounds to the cent, and the rollup turns dollars into cents with the same
|
|
426
|
-
* `Math.round(usd * 100)` it applies to contacts, API and dataset storage.
|
|
427
|
-
*
|
|
428
|
-
* A non-finite or negative spend is zero credits, for the reason
|
|
429
|
-
* `assistCreditsFromUsd` gives; a plan with no band or no rate prices zero
|
|
430
|
-
* structurally, so Free, Starter and Enterprise bill nothing here without a
|
|
431
|
-
* check that could be forgotten.
|
|
432
|
-
*/ export function assistMonthOverage(org, estCostUsd) {
|
|
433
|
-
const usedCredits = assistCreditsFromUsd(estCostUsd);
|
|
434
|
-
const bandCredits = resolveAssistCreditBudget(org);
|
|
435
|
-
return _extends({
|
|
436
|
-
usedCredits,
|
|
437
|
-
bandCredits
|
|
438
|
-
}, priceAssistCreditOverage(org, assistCreditOverage(usedCredits, bandCredits)));
|
|
439
|
-
}
|
|
440
|
-
/** The console's name for the org's overage ceiling (AGL-2898). */ export const ASSIST_OVERAGE_CAP_CONTROL_LABEL = 'Stop AI when this month’s overage reaches';
|
|
441
|
-
/**
|
|
442
|
-
* Bounds on the ceiling a self-serve org may set. The floor is a dollar
|
|
443
|
-
* because a ceiling of cents would refuse on the first exchange past the
|
|
444
|
-
* band while reading as a limit; the roof is the storage cap's, so no
|
|
445
|
-
* self-serve org writes itself an enterprise-sized commitment.
|
|
446
|
-
*/ export const ASSIST_OVERAGE_CAP_MIN_USD = 1;
|
|
447
|
-
export const ASSIST_OVERAGE_CAP_MAX_USD = 100000;
|
|
448
|
-
/**
|
|
449
|
-
* The org's monthly ceiling on AI overage dollars, or `null` for none.
|
|
450
|
-
*
|
|
451
|
-
* Strictly a finite positive number. `null`, absent, a string, `NaN`,
|
|
452
|
-
* `Infinity` and zero all read as no ceiling — the failure mode of the
|
|
453
|
-
* alternative is a wall of `NaN` that every comparison passes, or a ceiling
|
|
454
|
-
* of zero that refuses the first exchange past the band while the card
|
|
455
|
-
* says nothing is set.
|
|
456
|
-
*/ export function resolveAssistOverageCapUsd(org) {
|
|
457
|
-
var _org_assistOverage;
|
|
458
|
-
const raw = org == null ? void 0 : (_org_assistOverage = org.assistOverage) == null ? void 0 : _org_assistOverage.capUsd;
|
|
459
|
-
if (typeof raw !== 'number' || !Number.isFinite(raw) || raw <= 0) return null;
|
|
460
|
-
return raw;
|
|
461
|
-
}
|
|
462
|
-
/**
|
|
463
|
-
* Has this month's overage reached the org's own ceiling (AGL-2898)?
|
|
464
|
-
*
|
|
465
|
-
* Composed from `assistMonthOverage`, the ONE derivation the invoice reads,
|
|
466
|
-
* so the figure the ceiling stops at is the figure the invoice would have
|
|
467
|
-
* billed. That composition is also what confines the ceiling to plans that
|
|
468
|
-
* sell past their band: a plan with no rate prices its overage to zero, and
|
|
469
|
-
* zero never reaches a positive ceiling — Free and Enterprise cannot get here
|
|
470
|
-
* without a check that could be forgotten. `>=` rather than `>`, because an
|
|
471
|
-
* org that asked to stop AT a figure is stopped when the figure is met.
|
|
472
|
-
*/ export function assistOverageCapReached(org, estCostUsd) {
|
|
473
|
-
const cap = resolveAssistOverageCapUsd(org);
|
|
474
|
-
if (cap === null) return false;
|
|
475
|
-
return assistMonthOverage(org, estCostUsd).overageMonthlyUsd >= cap;
|
|
476
|
-
}
|
|
477
|
-
/**
|
|
478
|
-
* Whether a refusal was the org's own ceiling. The reservation answers
|
|
479
|
-
* `'cap'` only when `assistOverageCapReached` did, and that helper only
|
|
480
|
-
* reaches a ceiling on a plan with a rate, so the two conditions restated
|
|
481
|
-
* here are the same guarantee `assistRefusedByHardCap` gives the switch: the
|
|
482
|
-
* control the sentence names is the control that refused.
|
|
483
|
-
*/ export function assistRefusedByOverageCap(org, refusedBy) {
|
|
484
|
-
return refusedBy === 'cap' && resolveAssistOverageCapUsd(org) !== null && resolveAssistOverageRateUsdPer1k(org) !== null;
|
|
485
|
-
}
|
|
486
|
-
/**
|
|
487
|
-
* The sentence a workspace is told when its own overage ceiling refused it.
|
|
488
|
-
* ONE string for both assist entrypoints, as `assistHardCapRefusalText` is
|
|
489
|
-
* for the switch. It names the control by its label, quotes the figure the
|
|
490
|
-
* org chose, and says where to raise or clear it.
|
|
491
|
-
*/ export function assistOverageCapRefusalText(org) {
|
|
492
|
-
const cap = resolveAssistOverageCapUsd(org);
|
|
493
|
-
const figure = cap === null ? '' : ` $${cap.toFixed(2)}`;
|
|
494
|
-
return `This month’s AI assist overage reached${figure}, the figure you set as ` + `"${ASSIST_OVERAGE_CAP_CONTROL_LABEL}". Raise or remove it under ` + `${ASSIST_HARD_CAP_CONTROL_LOCATION} to keep going.`;
|
|
495
|
-
}
|
|
496
|
-
/**
|
|
497
|
-
* The refusal sentence for whichever of the org's OWN controls refused, or
|
|
498
|
-
* `null` when the refusal was nobody's control — the message cap, the
|
|
499
|
-
* operator's backstop, or a plan that sells no overage. Both doors answer a
|
|
500
|
-
* 402 with this text when it is a string and their usual 429 otherwise, so
|
|
501
|
-
* the status and the sentence cannot disagree about whose decision it was.
|
|
502
|
-
*/ export function assistOwnControlRefusalText(org, refusedBy) {
|
|
503
|
-
if (assistRefusedByHardCap(org, refusedBy)) return assistHardCapRefusalText(org);
|
|
504
|
-
if (assistRefusedByOverageCap(org, refusedBy)) {
|
|
505
|
-
return assistOverageCapRefusalText(org);
|
|
506
|
-
}
|
|
507
|
-
return null;
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
//# sourceMappingURL=assist-credits.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/assist-credits.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 isUncappedPlanComp,\n resolveOrgEntitlements,\n resolvePlanPricing,\n} from './plan-entitlements'\nimport { planQuotaOf } from '../plugin-manager/plugin-plan-entitlements'\nimport type { AglynOrgBilling } from '../foundation/definitions/org-billing.types'\n\n/**\n * Aglyn Assist credits: the unit assist is sold, metered and refused in.\n *\n * ## Why cost, and not messages\n *\n * Assist is measured per exchange as `estCostUsd` — the exchange priced at\n * the serving model's billed rates, computed where the tokens were counted. A\n * message allowance would throw that measurement away and price every action\n * the same. It cannot: a question is a few thousand tokens, while generating\n * a screen carries the node tree, the component catalog and the theme tokens\n * in, emits structured markup out, and iterates. The two differ by up to two\n * orders of magnitude, so one workspace's ten screen builds can outspend\n * another's thousand questions while both read as \"within allowance\".\n *\n * Metering in cost is the only unit under which those two workspaces draw\n * down their bands differently, which is the entire point.\n *\n * ## Why a credit rather than the dollar figure\n *\n * `estCostUsd` is a dollar figure off our own cost model, not a price.\n * Publishing it would put our model choice, our per-model rates and our\n * margin on a billing page, and it would move under customers every time a\n * model is swapped. A credit is a fixed quantity of that spend with a stable\n * public meaning, so the band on the plan card stays the same number when\n * the model behind it changes.\n *\n * Everything customer-facing counts credits. This module is the ONLY place\n * the two units meet.\n *\n * ## `estCostUsd` is the BILLED figure, not the bill we pay (AGL-3015)\n *\n * It is every exchange priced at the catalog's billed rates, which a model\n * may carry above what its provider charges. Credits are drawn from it, so\n * it is the right figure for a band, a wall, a cap and an invoice line. It\n * is the WRONG figure for a margin or a cost meter, which read the rollup's\n * provider figure through `assistProviderCostUsd` instead.\n */\n\n/**\n * What one credit is BILLED at, in USD.\n *\n * A tenth of a cent, chosen for two properties rather than for roundness:\n *\n * - **1,000 credits cost exactly $1.00**, so the per-1,000 retail rates on\n * `PLAN_PRICING.extraAssistCreditsUsdPer1k` read directly as multiples of\n * cost. A rate of $2.00 per 1,000 is cost x2 with no arithmetic in between,\n * which is what makes the 50% margin floor checkable by eye.\n * - **A single question is tens of credits, a generated screen is hundreds.**\n * At Sonnet list rates a short grounded answer runs around $0.013, so ~13\n * credits; a screen build runs into the hundreds. Both are whole numbers\n * with room to differ, which a cent-sized credit would have flattened to\n * \"1 vs 20\" and a dollar-sized one to \"0 vs 0\".\n *\n * ⚠️ This is a COST-MODEL constant, not a price and not a rate we publish.\n * It never appears on a customer surface; `assistCreditsFromUsd` is how a\n * measured dollar figure becomes something a customer may be shown.\n *\n * ⚠️ AND IT IS NOT WHAT A CREDIT COSTS US (AGL-3015). The dollar figure this\n * divides is priced at the model catalog's BILLED rates, and a model may be\n * billed above what the provider charges. A credit is therefore a tenth of a\n * cent of BILLED spend, and at most a tenth of a cent of provider spend. Read\n * against a retail rate it yields the floor of the line margin, never the\n * middle of it — which is the safe direction, and the direction every margin\n * assertion in this repo depends on. What a period actually cost is measured,\n * not derived: `assistProviderCostUsd` reads it off the rollup.\n */\nexport const ASSIST_CREDIT_COST_USD = 0.001\n\n/**\n * The field a monthly assist rollup, a signal row and a kind bucket keep\n * PROVIDER spend under, beside the `estCostUsd` a customer's credits are\n * drawn from (AGL-3015).\n *\n * Named here rather than in the meter that writes it because the readers are\n * spread across the plugin, the billing routes and the staff surfaces, and a\n * field name spelled out at six call sites is a field name that will be\n * spelled differently at the seventh.\n */\nexport const ASSIST_PROVIDER_COST_FIELD = 'providerCostUsd'\n\n/**\n * What a metered period ACTUALLY COST US, from the two dollar figures a\n * rollup carries.\n *\n * `billedUsd` is `estCostUsd`: every exchange priced at its model's billed\n * rates, and the figure credits are drawn from. `providerUsd` is\n * `ASSIST_PROVIDER_COST_FIELD`: the same exchanges priced at what the\n * provider charged. The second is the answer wherever it exists.\n *\n * ## Why a rollup may not carry one, and which way that errs\n *\n * The provider figure is written from the commit that split the two rates\n * onward. A period closed before it answers with the BILLED figure, which is\n * at or above what we paid — so a margin taken on history reads low and a\n * cost reads high, the direction a cost figure is allowed to be wrong in.\n * The one period that under-reads is the month in flight when the split\n * shipped, whose provider figure covers only the exchanges after it; that\n * month's error is bounded by the markup on a few days of a meter whose\n * largest measured organization is a fraction of a cent.\n */\nexport function assistProviderCostUsd(\n billedUsd: unknown,\n providerUsd: unknown,\n): number {\n const provider = Number(providerUsd)\n if (Number.isFinite(provider) && provider > 0) return provider\n const billed = Number(billedUsd)\n return Number.isFinite(billed) && billed > 0 ? billed : 0\n}\n\n/**\n * The minimum line margin every retail assist rate must clear.\n *\n * The same floor now applied to every retail rate in this codebase, and the\n * reason the assist ladder stops descending at cost x2 on Agency instead of\n * taking one more step the way the contacts ladder once did.\n */\nexport const ASSIST_CREDIT_MIN_MARGIN_PCT = 0.5\n\n/**\n * Credits -> our cost in USD. Non-finite or negative input is 0 credits, so a\n * corrupt band cannot become a budget of `NaN` that every comparison passes.\n */\nexport function assistUsdFromCredits(credits: number): number {\n const value = Number(credits)\n if (!Number.isFinite(value) || value <= 0) return 0\n return Math.round(value * ASSIST_CREDIT_COST_USD * 1_000_000) / 1_000_000\n}\n\n/**\n * Measured USD -> credits, rounded UP.\n *\n * Up, because this converts spend the platform has ALREADY made into the\n * figure a customer is shown against their band. Rounding down would let a\n * long tail of sub-credit exchanges cost real money and draw nothing, which\n * is the one direction a meter must not err in.\n */\nexport function assistCreditsFromUsd(usd: number): number {\n const value = Number(usd)\n if (!Number.isFinite(value) || value <= 0) return 0\n return Math.ceil(value / ASSIST_CREDIT_COST_USD)\n}\n\n/**\n * The org's monthly assist band in credits, or `null` when its plan sells no\n * assist band at all.\n *\n * ## `null` is not zero, and the difference is the whole safety property\n *\n * A band of 0 resolves to `null` — \"this org sells no band\" — and never to\n * a budget of `$0`. An org with no band still reaches the console\n * assistant's docs-grounded rung, which is bounded by the free daily message\n * cap and by the operator's spend backstop. Resolving zero as a budget would\n * refuse that rung outright: a whole assistant turned off by a pricing field\n * that was never about it.\n *\n * Since AGL-3203 no PLAN ROW bands at zero. Free carries the AGL-2925 taste\n * of `FREE_AI_TASTE_CREDITS_PER_MONTH` with no rate beside it, so it\n * resolves to a budget here and `assistBandRefuses` makes that budget a\n * wall; Starter carries 750 WITH a rate, so its budget is a line it meters\n * past rather than a wall. Both draw their docs-grounded chat on the same\n * band, which is fine — each is sized so a month of it costs well under a\n * dollar.\n *\n * So the zero case is now reached only by a per-org override that writes\n * one, and the reading above is what keeps that org's assistant running.\n *\n * The Aglyn AI add-on (AGL-2896) needs no knowledge here either way: the\n * resolver has already added `AI_ADDON_CREDITS_PER_MONTH[plan]` to the\n * band, because the add-on widens the one pool rather than opening a\n * second.\n *\n * The same reading is what makes the guard survive a stubbed entitlements\n * module. A test double that answers 0 for every quota produces `null` here,\n * which falls through to the operator backstop, rather than a budget of zero\n * that refuses everything while the assertions read green. A clamp that goes\n * green having refused every request has proved nothing, and 0 is exactly the\n * value a stub returns.\n *\n * ## Non-finite is also `null`, deliberately\n *\n * `Infinity` must never resolve to a budget. It is unrepresentable off-process\n * — `JSON.stringify(Infinity)` is `null` and reads back as 0 — so a band that\n * reached here as `Infinity` is a band that would read as zero somewhere\n * else. Falling through to the backstop is bounded and consistent; honouring\n * it would be an unbounded budget on one process and a refusal on the next.\n *\n * An UNCAPPED staff comp (AGL-3049) is where that `Infinity` comes from on\n * purpose, and `null` is right for it here too: there is no band to measure\n * against, be over, price or alert on. What `null` must NOT bring with it is\n * the backstop, whose repo default would cap the one workspace staff said\n * has no cap — so `assistBandRefuses` answers false for it, and the\n * reservation's ceiling (`assistMonthlyCeilingUsd` in the AI plugin) drops\n * the default and keeps only an operator's explicit figure.\n */\nexport function resolveAssistCreditBudget(\n org: Partial<AglynOrgBilling> | null | undefined,\n): number | null {\n const credits = planQuotaOf(resolveOrgEntitlements(org), 'assistCreditsPerMonth')\n if (!Number.isFinite(credits) || credits <= 0) return null\n return Math.floor(credits)\n}\n\n/** The org's monthly assist band in USD of provider spend, or `null`. */\nexport function resolveAssistBudgetUsd(\n org: Partial<AglynOrgBilling> | null | undefined,\n): number | null {\n const credits = resolveAssistCreditBudget(org)\n return credits === null ? null : assistUsdFromCredits(credits)\n}\n\n/**\n * Credits spent past the band. A `null` band yields 0 — an org with no band\n * has nothing to be over, and reporting the whole month's spend as overage\n * there would invoice the free tier for its docs answers.\n */\nexport function assistCreditOverage(\n usedCredits: number,\n bandCredits: number | null,\n): number {\n const used = Number(usedCredits)\n if (!Number.isFinite(used) || used <= 0) return 0\n if (bandCredits === null) return 0\n const band = Number(bandCredits)\n if (!Number.isFinite(band) || band <= 0) return 0\n return Math.max(0, used - band)\n}\n\n/**\n * The per-1,000 rate the org's assist credits are sold at past its band, or\n * `null` when nothing is sold past it — the ONE reading of the rate\n * (AGL-2896). `priceAssistCreditOverage`, `assistBandRefuses`,\n * `assistRefusedByHardCap` and `assistHardCapRefusalText` all go through\n * it, so the gate, the invoice and the refusal sentence cannot quote three\n * different rates for one org.\n *\n * The plan's `extraAssistCreditsUsdPer1k` IS the answer — the whole answer,\n * since AGL-3203. Starter used to be the one exception: it banded at 0, so\n * the rate could not sit on its plan row without advertising a fee on a\n * quantity the plan never sold, and the add-on's band was priced from a\n * constant read here instead. Starter now includes 750 credits\n * unconditionally and carries $3.00 on its own row, so the exception and its\n * constant are gone and there is exactly one place the rate is written.\n *\n * Free and Enterprise are null with or without the add-on: Free sells no\n * add-on and its taste band is a wall by decision (AGL-2925), and\n * Enterprise's usage is in the contract.\n *\n * A STAFF COMP is null on every plan and with every add-on (AGL-3034),\n * because `resolvePlanPricing` — never `PLAN_PRICING` indexed directly —\n * sells nothing on a comp. That is what makes a comp's band a wall at the\n * gate and prices its overage to zero at the charge: the AI overage path\n * (AGL-3011) claims only what `assistMonthOverage` prices, so a comp never\n * becomes a Stripe invoice. The function is a single read for that reason;\n * a second source for the rate is how a comp gets quoted one.\n */\nexport function resolveAssistOverageRateUsdPer1k(\n org: Partial<AglynOrgBilling> | null | undefined,\n): number | null {\n return resolvePlanPricing(org).extraAssistCreditsUsdPer1k\n}\n\nexport interface AssistCreditOveragePrice {\n /** Credits past the plan's band, as handed in. */\n overageCredits: number\n /** Estimated charge at the plan's per-1,000 rate. */\n overageMonthlyUsd: number\n /** Per-1,000 rate; null when the plan prices no assist overage. */\n overageRateUsd: number | null\n}\n\n/**\n * Prices assist credits past a plan's band.\n *\n * ## No `allowed`, deliberately — and for the opposite reason to email\n *\n * `priceEmailSendOverage` omits an `allowed` field because email has two\n * gates and a transactional sender consulting the wrong one drops a password\n * reset. Assist omits it because it has exactly ONE gate and this is not it:\n * whether the band refuses is `assistBandRefuses`, applied inside\n * `reserveAssistMessage` before a token is spent. A second answer to \"may\n * this proceed\", computed after the fact from a rounded credit figure, would\n * be a second gate to drift from the first.\n *\n * ## Takes the overage, does not recompute it\n *\n * `assistCreditOverage` derives the excess once, from the measured meter and\n * the resolved band. Re-deriving it here would be a second overage model.\n *\n * A null rate yields zero structurally rather than by a check, which is what\n * keeps Free and Starter at zero on this axis.\n */\nexport function priceAssistCreditOverage(\n org: Partial<AglynOrgBilling> | null | undefined,\n overageCredits: number,\n): AssistCreditOveragePrice {\n const overageRateUsd = resolveAssistOverageRateUsdPer1k(org)\n const credits = Number(overageCredits)\n const over = Number.isFinite(credits) && credits > 0 ? credits : 0\n return {\n overageCredits: over,\n overageMonthlyUsd:\n overageRateUsd === null\n ? 0\n : Math.round((over / 1000) * overageRateUsd * 100) / 100,\n overageRateUsd,\n }\n}\n\n/**\n * The line margin a per-1,000-credit retail rate earns AT LEAST, as a\n * fraction.\n *\n * One expression, so the ladder and its floor cannot be checked against two\n * different definitions of margin. A null or non-positive rate has no margin\n * to report and answers `null` rather than 1 or 0 — \"this plan sells no\n * overage\" is not \"this plan sells overage at 100% margin\".\n *\n * A LOWER BOUND, not the realized figure (AGL-3015). The cost side is\n * `ASSIST_CREDIT_COST_USD`, which is what a credit BILLS at; a model billed\n * above its provider rate costs us less than that per credit, so the real\n * line margin is this or better and never worse. The floor stays checkable\n * by eye — a $2.00 rate is still cost x2 — and a retail rate that clears the\n * floor here clears it on every model mix. What a period realized is the\n * staff margin card's business, and it reads measured provider spend.\n */\nexport function assistCreditRateMarginPct(\n rateUsdPer1k: number | null,\n): number | null {\n if (rateUsdPer1k === null) return null\n const rate = Number(rateUsdPer1k)\n if (!Number.isFinite(rate) || rate <= 0) return null\n const costPer1k = ASSIST_CREDIT_COST_USD * 1000\n return (rate - costPer1k) / rate\n}\n\n/**\n * What a refused or in-progress workspace may be TOLD about its assist band.\n *\n * The reservation carries `costUsd` and `costLimitUsd` because that is what\n * it measured and what it compared against, and both are our provider bill.\n * Shipping the reservation itself to a browser publishes them. This is the\n * projection that crosses that boundary: credits only, no dollars, no model,\n * no rates.\n */\nexport interface PublicAssistCredits {\n /** Credits drawn this month, rounded up. */\n used: number\n /** The band, or `null` when no band applied to this request. */\n limit: number | null\n /** Credits left in the band, or `null` when there is no band. */\n remaining: number | null\n}\n\n/**\n * Projects a measured spend figure and a ceiling into the credit view.\n *\n * Both inputs are nullable because the reservation's are: `costUsd` is `null`\n * when nothing consulted the monthly document, and a ceiling is `null` when\n * none applied. Neither collapses to 0 here — \"nobody looked\" and \"spent\n * nothing\" have to stay distinguishable at the surface, exactly as they do on\n * the reservation.\n */\nexport function publicAssistCredits(\n costUsd: number | null,\n ceilingUsd: number | null,\n): PublicAssistCredits {\n const used = costUsd === null ? 0 : assistCreditsFromUsd(costUsd)\n if (ceilingUsd === null) return { used, limit: null, remaining: null }\n const limit = assistCreditsFromUsd(ceilingUsd)\n return { used, limit, remaining: Math.max(0, limit - used) }\n}\n\n/**\n * The console's name for the org's hard-cap switch (AGL-2653), quoted by the\n * refusal so the words on the 402 and the words beside the switch are one\n * string. Change it here and both move.\n */\nexport const ASSIST_HARD_CAP_CONTROL_LABEL = 'Stop AI assist at the included band'\n\n/** Where the switch lives, in the words the refusal uses. */\nexport const ASSIST_HARD_CAP_CONTROL_LOCATION = 'Billing → Usage'\n\n/**\n * Whether the org asked to be stopped at its assist band (AGL-2653).\n *\n * Strictly `=== true`: the map is absent on every org that has not touched\n * the switch, and absent is the selling default. A truthy string or a `1`\n * written by hand must not switch an assistant off at the band, so nothing\n * short of the boolean counts.\n */\nexport function resolveAssistHardCap(\n org: Partial<AglynOrgBilling> | null | undefined,\n): boolean {\n return org?.assistOverage?.hardCap === true\n}\n\n/**\n * Is the org's assist band a WALL, or a line past which credits are sold?\n *\n * Two things make it a wall, and only two:\n *\n * - **The org asked.** `assistOverage.hardCap` is the customer's own ceiling,\n * the `storageOverage.capUsd` of this meter.\n * - **There is nothing to sell past it at.** An org whose\n * `resolveAssistOverageRateUsdPer1k` is `null` has no rate to bill, so\n * credits past its band would be provider spend with no invoice line — the\n * silent free overage `plan-entitlements.spec.ts` forbids. Enterprise is\n * one case, its band being contractual; FREE is the other, its taste band\n * a wall by the AGL-2925 decision so that nothing about a Free workspace\n * can produce a charge.\n *\n * Everything else sells past the band by default, which is the 2026-09-07\n * decision this function encodes — and since AGL-3203 that includes Starter,\n * which carries 750 credits and a $3.00 rate like every paid tier above it.\n *\n * An org with NO band at all answers the same as Enterprise, harmlessly:\n * `resolveAssistBudgetUsd` is `null` for it and the reservation never\n * measures against a band. No plan ROW is in that state any more — Free and\n * Starter both were, at different times — so it is reached by a per-org\n * override that writes a zero.\n *\n * An UNCAPPED staff comp (AGL-3049) is neither: it has no band to be a wall,\n * and nothing is sold past a band it does not have. It answers false, ahead\n * of the switch — a workspace staff uncapped is not stopped by a band, and\n * the reservation reads that answer as \"no wall, no backstop default\".\n */\nexport function assistBandRefuses(\n org: Partial<AglynOrgBilling> | null | undefined,\n): boolean {\n if (isUncappedPlanComp(org)) return false\n if (resolveAssistHardCap(org)) return true\n return resolveAssistOverageRateUsdPer1k(org) === null\n}\n\n/**\n * Which ceiling refused a reservation, or `null` when it was admitted.\n *\n * - `messages`: the message cap (daily on the free rung, monthly entitled).\n * - `budget`: the operator's spend backstop.\n * - `band`: the plan's own band, as a wall — the switch, or a plan with\n * no rate to sell past it at.\n * - `cap`: the org's own dollar ceiling on overage (AGL-2898).\n *\n * The four that exist only on the Free taste (AGL-2925), each a precaution\n * against the one band that has no invoice behind it:\n *\n * - `account`: the ACCOUNT's 300 credits are spent across every free\n * workspace it owns — the org band alone would multiply by\n * the workspaces one person may hold.\n * - `requests`: the account's daily free request cap.\n * - `refusals`: the account's free generation is paused for the day after\n * too many `refusal` stops — a brief the model declined is a\n * brief that should not be retried thirty times.\n * - `platform`: the platform-wide daily ceiling on free spend; nobody's\n * fault, and \"try again tomorrow\" is the whole answer.\n *\n * Declared here, beside the helpers that read it, so the reservation, its\n * public projection and both doors name one union.\n */\nexport type AssistRefusedBy =\n | 'messages'\n | 'budget'\n | 'band'\n | 'cap'\n | 'account'\n | 'requests'\n | 'refusals'\n | 'platform'\n | null\n\n/**\n * The sentence a Free workspace is told when one of the taste's own\n * precautions refused it, or `null` when the refusal was something else.\n *\n * One string per rung, for both doors and the gate ladder, so a customer\n * reading the console panel and one reading the besigner get the same\n * words. Every sentence is customer-safe: it names what to do (upgrade,\n * wait) and never a figure from our cost model, a counter document, or\n * another workspace.\n */\nexport function assistFreeTasteRefusalText(\n refusedBy: AssistRefusedBy,\n): string | null {\n switch (refusedBy) {\n case 'account':\n return (\n 'Your free AI credits for this month are used across your ' +\n 'workspaces — upgrade any workspace to keep going.'\n )\n case 'requests':\n return (\n 'Free workspaces get a limited number of AI requests a day — ' +\n 'try again tomorrow, or upgrade to keep going.'\n )\n case 'refusals':\n return (\n 'The AI declined several requests today, so free AI generation is ' +\n 'paused until tomorrow. Try a different kind of brief then.'\n )\n case 'platform':\n return (\n 'Free AI generation is paused for the rest of today — try again ' +\n 'tomorrow. Paid workspaces are not affected.'\n )\n default:\n return null\n }\n}\n\n/**\n * Whether a refusal at the band was the org's OWN doing — the switch is on\n * AND turning it off would have let the exchange through. The refusal that\n * names the switch has to be the one the switch caused: on a plan with no\n * rate the band refuses whatever the switch says, and telling that customer\n * to turn it off would send them to a control that does nothing.\n */\nexport function assistRefusedByHardCap(\n org: Partial<AglynOrgBilling> | null | undefined,\n refusedBy: AssistRefusedBy,\n): boolean {\n return (\n refusedBy === 'band' &&\n resolveAssistHardCap(org) &&\n resolveAssistOverageRateUsdPer1k(org) !== null\n )\n}\n\n/**\n * The sentence a workspace is told when its own hard cap refused it. ONE\n * string for both assist entrypoints, so the console panel and the besigner\n * cannot describe the same control in two ways. It names the switch by its\n * label, says where it lives, and quotes the plan's rate so the reader knows\n * what turning it off costs.\n */\nexport function assistHardCapRefusalText(\n org: Partial<AglynOrgBilling> | null | undefined,\n): string {\n const rate = resolveAssistOverageRateUsdPer1k(org)\n const rateClause =\n rate === null ? '' : ` at $${rate.toFixed(2)} per 1,000 credits`\n return (\n `This workspace used its assistant credits for the month, and ` +\n `\"${ASSIST_HARD_CAP_CONTROL_LABEL}\" is on. Turn it off under ` +\n `${ASSIST_HARD_CAP_CONTROL_LOCATION} to keep going${rateClause}.`\n )\n}\n\nexport interface AssistMonthOverage extends AssistCreditOveragePrice {\n /** Credits drawn this month, rounded up from the measured spend. */\n usedCredits: number\n /** The band those credits were measured against, or `null` for no band. */\n bandCredits: number | null\n}\n\n/**\n * A month's assist overage from its measured spend — the ONE derivation the\n * invoice and every meter read (AGL-2653).\n *\n * Takes `estCostUsd` as the rollup reads it and composes the three steps\n * that already existed — credits from spend, overage past the band, price at\n * the plan's rate — so `report-usage` cannot assemble them in a different\n * order from a console readout and arrive at a different figure. The\n * rounding is the other overage terms' rounding: `priceAssistCreditOverage`\n * rounds to the cent, and the rollup turns dollars into cents with the same\n * `Math.round(usd * 100)` it applies to contacts, API and dataset storage.\n *\n * A non-finite or negative spend is zero credits, for the reason\n * `assistCreditsFromUsd` gives; a plan with no band or no rate prices zero\n * structurally, so Free, Starter and Enterprise bill nothing here without a\n * check that could be forgotten.\n */\nexport function assistMonthOverage(\n org: Partial<AglynOrgBilling> | null | undefined,\n estCostUsd: number,\n): AssistMonthOverage {\n const usedCredits = assistCreditsFromUsd(estCostUsd)\n const bandCredits = resolveAssistCreditBudget(org)\n return {\n usedCredits,\n bandCredits,\n ...priceAssistCreditOverage(\n org,\n assistCreditOverage(usedCredits, bandCredits),\n ),\n }\n}\n\n/** The console's name for the org's overage ceiling (AGL-2898). */\nexport const ASSIST_OVERAGE_CAP_CONTROL_LABEL =\n 'Stop AI when this month’s overage reaches'\n\n/**\n * Bounds on the ceiling a self-serve org may set. The floor is a dollar\n * because a ceiling of cents would refuse on the first exchange past the\n * band while reading as a limit; the roof is the storage cap's, so no\n * self-serve org writes itself an enterprise-sized commitment.\n */\nexport const ASSIST_OVERAGE_CAP_MIN_USD = 1\nexport const ASSIST_OVERAGE_CAP_MAX_USD = 100_000\n\n/**\n * The org's monthly ceiling on AI overage dollars, or `null` for none.\n *\n * Strictly a finite positive number. `null`, absent, a string, `NaN`,\n * `Infinity` and zero all read as no ceiling — the failure mode of the\n * alternative is a wall of `NaN` that every comparison passes, or a ceiling\n * of zero that refuses the first exchange past the band while the card\n * says nothing is set.\n */\nexport function resolveAssistOverageCapUsd(\n org: Partial<AglynOrgBilling> | null | undefined,\n): number | null {\n const raw = org?.assistOverage?.capUsd\n if (typeof raw !== 'number' || !Number.isFinite(raw) || raw <= 0) return null\n return raw\n}\n\n/**\n * Has this month's overage reached the org's own ceiling (AGL-2898)?\n *\n * Composed from `assistMonthOverage`, the ONE derivation the invoice reads,\n * so the figure the ceiling stops at is the figure the invoice would have\n * billed. That composition is also what confines the ceiling to plans that\n * sell past their band: a plan with no rate prices its overage to zero, and\n * zero never reaches a positive ceiling — Free and Enterprise cannot get here\n * without a check that could be forgotten. `>=` rather than `>`, because an\n * org that asked to stop AT a figure is stopped when the figure is met.\n */\nexport function assistOverageCapReached(\n org: Partial<AglynOrgBilling> | null | undefined,\n estCostUsd: number,\n): boolean {\n const cap = resolveAssistOverageCapUsd(org)\n if (cap === null) return false\n return assistMonthOverage(org, estCostUsd).overageMonthlyUsd >= cap\n}\n\n/**\n * Whether a refusal was the org's own ceiling. The reservation answers\n * `'cap'` only when `assistOverageCapReached` did, and that helper only\n * reaches a ceiling on a plan with a rate, so the two conditions restated\n * here are the same guarantee `assistRefusedByHardCap` gives the switch: the\n * control the sentence names is the control that refused.\n */\nexport function assistRefusedByOverageCap(\n org: Partial<AglynOrgBilling> | null | undefined,\n refusedBy: AssistRefusedBy,\n): boolean {\n return (\n refusedBy === 'cap' &&\n resolveAssistOverageCapUsd(org) !== null &&\n resolveAssistOverageRateUsdPer1k(org) !== null\n )\n}\n\n/**\n * The sentence a workspace is told when its own overage ceiling refused it.\n * ONE string for both assist entrypoints, as `assistHardCapRefusalText` is\n * for the switch. It names the control by its label, quotes the figure the\n * org chose, and says where to raise or clear it.\n */\nexport function assistOverageCapRefusalText(\n org: Partial<AglynOrgBilling> | null | undefined,\n): string {\n const cap = resolveAssistOverageCapUsd(org)\n const figure = cap === null ? '' : ` $${cap.toFixed(2)}`\n return (\n `This month’s AI assist overage reached${figure}, the figure you set as ` +\n `\"${ASSIST_OVERAGE_CAP_CONTROL_LABEL}\". Raise or remove it under ` +\n `${ASSIST_HARD_CAP_CONTROL_LOCATION} to keep going.`\n )\n}\n\n/**\n * The refusal sentence for whichever of the org's OWN controls refused, or\n * `null` when the refusal was nobody's control — the message cap, the\n * operator's backstop, or a plan that sells no overage. Both doors answer a\n * 402 with this text when it is a string and their usual 429 otherwise, so\n * the status and the sentence cannot disagree about whose decision it was.\n */\nexport function assistOwnControlRefusalText(\n org: Partial<AglynOrgBilling> | null | undefined,\n refusedBy: AssistRefusedBy,\n): string | null {\n if (assistRefusedByHardCap(org, refusedBy)) return assistHardCapRefusalText(org)\n if (assistRefusedByOverageCap(org, refusedBy)) {\n return assistOverageCapRefusalText(org)\n }\n return null\n}\n"],"names":["isUncappedPlanComp","resolveOrgEntitlements","resolvePlanPricing","planQuotaOf","ASSIST_CREDIT_COST_USD","ASSIST_PROVIDER_COST_FIELD","assistProviderCostUsd","billedUsd","providerUsd","provider","Number","isFinite","billed","ASSIST_CREDIT_MIN_MARGIN_PCT","assistUsdFromCredits","credits","value","Math","round","assistCreditsFromUsd","usd","ceil","resolveAssistCreditBudget","org","floor","resolveAssistBudgetUsd","assistCreditOverage","usedCredits","bandCredits","used","band","max","resolveAssistOverageRateUsdPer1k","extraAssistCreditsUsdPer1k","priceAssistCreditOverage","overageCredits","overageRateUsd","over","overageMonthlyUsd","assistCreditRateMarginPct","rateUsdPer1k","rate","costPer1k","publicAssistCredits","costUsd","ceilingUsd","limit","remaining","ASSIST_HARD_CAP_CONTROL_LABEL","ASSIST_HARD_CAP_CONTROL_LOCATION","resolveAssistHardCap","assistOverage","hardCap","assistBandRefuses","assistFreeTasteRefusalText","refusedBy","assistRefusedByHardCap","assistHardCapRefusalText","rateClause","toFixed","assistMonthOverage","estCostUsd","ASSIST_OVERAGE_CAP_CONTROL_LABEL","ASSIST_OVERAGE_CAP_MIN_USD","ASSIST_OVERAGE_CAP_MAX_USD","resolveAssistOverageCapUsd","raw","capUsd","assistOverageCapReached","cap","assistRefusedByOverageCap","assistOverageCapRefusalText","figure","assistOwnControlRefusalText"],"mappings":"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"}
|