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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/package.json +11 -11
  2. package/src/lib/app-utils/activity-labels.js +2 -0
  3. package/src/lib/app-utils/activity-labels.js.map +1 -1
  4. package/src/lib/app-utils/analytics-events.d.ts +13 -0
  5. package/src/lib/app-utils/analytics-events.js +2 -0
  6. package/src/lib/app-utils/analytics-events.js.map +1 -1
  7. package/src/lib/app-utils/append-only-history.d.ts +94 -0
  8. package/src/lib/app-utils/append-only-history.js +73 -0
  9. package/src/lib/app-utils/append-only-history.js.map +1 -0
  10. package/src/lib/app-utils/bundle-intent.d.ts +186 -0
  11. package/src/lib/app-utils/bundle-intent.js +386 -0
  12. package/src/lib/app-utils/bundle-intent.js.map +1 -0
  13. package/src/lib/app-utils/client-build.d.ts +204 -0
  14. package/src/lib/app-utils/client-build.js +237 -0
  15. package/src/lib/app-utils/client-build.js.map +1 -0
  16. package/src/lib/app-utils/console-routes.d.ts +4 -0
  17. package/src/lib/app-utils/console-routes.js +2 -0
  18. package/src/lib/app-utils/console-routes.js.map +1 -1
  19. package/src/lib/app-utils/docs-help-sections.generated.js +7 -5
  20. package/src/lib/app-utils/docs-help-sections.generated.js.map +1 -1
  21. package/src/lib/app-utils/docs-help.generated.d.ts +5 -5
  22. package/src/lib/app-utils/docs-help.generated.js +9 -2
  23. package/src/lib/app-utils/docs-help.generated.js.map +1 -1
  24. package/src/lib/app-utils/docs-index.generated.js +101 -31
  25. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  26. package/src/lib/app-utils/margin-utilization.js +6 -4
  27. package/src/lib/app-utils/margin-utilization.js.map +1 -1
  28. package/src/lib/app-utils/media-filter.d.ts +26 -0
  29. package/src/lib/app-utils/media-filter.js +32 -0
  30. package/src/lib/app-utils/media-filter.js.map +1 -1
  31. package/src/lib/app-utils/onboarding-deep-link.d.ts +36 -2
  32. package/src/lib/app-utils/onboarding-deep-link.js +48 -5
  33. package/src/lib/app-utils/onboarding-deep-link.js.map +1 -1
  34. package/src/lib/app-utils/operator-alerts.js +21 -0
  35. package/src/lib/app-utils/operator-alerts.js.map +1 -1
  36. package/src/lib/app-utils/owner-approval-record.d.ts +102 -0
  37. package/src/lib/app-utils/owner-approval-record.js +148 -0
  38. package/src/lib/app-utils/owner-approval-record.js.map +1 -0
  39. package/src/lib/app-utils/plan-entitlements.d.ts +183 -67
  40. package/src/lib/app-utils/plan-entitlements.js +213 -58
  41. package/src/lib/app-utils/plan-entitlements.js.map +1 -1
  42. package/src/lib/app-utils/release-flags.d.ts +1 -1
  43. package/src/lib/app-utils/release-flags.js +10 -0
  44. package/src/lib/app-utils/release-flags.js.map +1 -1
  45. package/src/lib/app-utils/server.d.ts +4 -0
  46. package/src/lib/app-utils/server.js +7 -0
  47. package/src/lib/app-utils/server.js.map +1 -1
  48. package/src/lib/app-utils/site-care-project.d.ts +227 -0
  49. package/src/lib/app-utils/site-care-project.js +423 -0
  50. package/src/lib/app-utils/site-care-project.js.map +1 -0
  51. package/src/lib/app-utils/site-care.d.ts +268 -0
  52. package/src/lib/app-utils/site-care.js +283 -0
  53. package/src/lib/app-utils/site-care.js.map +1 -0
  54. package/src/lib/app-utils/upgrade-proposal.d.ts +29 -4
  55. package/src/lib/app-utils/upgrade-proposal.js +60 -9
  56. package/src/lib/app-utils/upgrade-proposal.js.map +1 -1
  57. package/src/lib/foundation/definitions/org-billing.types.d.ts +40 -10
  58. package/src/lib/foundation/definitions/org-billing.types.js.map +1 -1
  59. package/src/lib/foundation/definitions/organization.types.d.ts +3 -1
  60. package/src/lib/foundation/definitions/organization.types.js.map +1 -1
  61. package/src/lib/foundation/index.d.ts +1 -1
  62. package/src/lib/foundation/index.js.map +1 -1
  63. package/src/lib/plugin-manager/feature-plugins.d.ts +48 -0
  64. package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
  65. package/src/lib/plugin-manager/plugin-ai-capabilities.d.ts +19 -0
  66. package/src/lib/plugin-manager/plugin-ai-capabilities.js.map +1 -1
  67. package/src/lib/plugin-manager/plugin-domain-search.d.ts +81 -0
  68. package/src/lib/plugin-manager/plugin-domain-search.js +63 -0
  69. package/src/lib/plugin-manager/plugin-domain-search.js.map +1 -0
  70. package/src/lib/plugin-manager/plugin-entitlements.d.ts +23 -5
  71. package/src/lib/plugin-manager/plugin-entitlements.js +10 -0
  72. package/src/lib/plugin-manager/plugin-entitlements.js.map +1 -1
  73. package/src/lib/plugin-manager/purchase-gate-preclearance.d.ts +61 -0
  74. package/src/lib/plugin-manager/purchase-gate-preclearance.js +102 -0
  75. package/src/lib/plugin-manager/purchase-gate-preclearance.js.map +1 -0
@@ -362,7 +362,10 @@ export const PLAN_ENTITLEMENTS = {
362
362
  removeBranding: true,
363
363
  scheduledPublishing: false,
364
364
  aiAssist: true,
365
- aiGenerative: false,
365
+ // One pool (AGL-3965): the plan's included credits pay for generation
366
+ // as well as assist on every paid plan, and an Aglyn AI tier adds
367
+ // credits on top rather than opening this door.
368
+ aiGenerative: true,
366
369
  workflows: true,
367
370
  dataStore: true,
368
371
  videoMedia: false,
@@ -481,7 +484,7 @@ export const PLAN_ENTITLEMENTS = {
481
484
  removeBranding: true,
482
485
  scheduledPublishing: false,
483
486
  aiAssist: true,
484
- aiGenerative: false,
487
+ aiGenerative: true,
485
488
  workflows: true,
486
489
  dataStore: true,
487
490
  videoMedia: true,
@@ -552,7 +555,7 @@ export const PLAN_ENTITLEMENTS = {
552
555
  removeBranding: true,
553
556
  scheduledPublishing: true,
554
557
  aiAssist: true,
555
- aiGenerative: false,
558
+ aiGenerative: true,
556
559
  workflows: true,
557
560
  dataStore: true,
558
561
  videoMedia: true,
@@ -626,7 +629,7 @@ export const PLAN_ENTITLEMENTS = {
626
629
  removeBranding: true,
627
630
  scheduledPublishing: true,
628
631
  aiAssist: true,
629
- aiGenerative: false,
632
+ aiGenerative: true,
630
633
  workflows: true,
631
634
  dataStore: true,
632
635
  videoMedia: true,
@@ -697,7 +700,7 @@ export const PLAN_ENTITLEMENTS = {
697
700
  removeBranding: true,
698
701
  scheduledPublishing: true,
699
702
  aiAssist: true,
700
- aiGenerative: false,
703
+ aiGenerative: true,
701
704
  workflows: true,
702
705
  dataStore: true,
703
706
  videoMedia: true,
@@ -772,7 +775,7 @@ export const PLAN_ENTITLEMENTS = {
772
775
  removeBranding: true,
773
776
  scheduledPublishing: true,
774
777
  aiAssist: true,
775
- aiGenerative: false,
778
+ aiGenerative: true,
776
779
  workflows: true,
777
780
  dataStore: true,
778
781
  videoMedia: true,
@@ -1016,52 +1019,149 @@ fillPluginPlanEntitlements(PLAN_ENTITLEMENTS);
1016
1019
  * deal) — `planSellsSiteCare` is that rule. `resolveOrgEntitlements` flips
1017
1020
  * the org-wide `features.siteCare` from any purchased quantity >= 1.
1018
1021
  */ export const SITE_CARE_ADDON_MONTHLY_USD = 49;
1022
+ /**
1023
+ * Site Care's included time (AGL-3959): about an hour of staff work a month,
1024
+ * no rollover. `SITE_CARE_INCLUDED_BLOCK_MINUTES` is the step each request is
1025
+ * counted against it in, always rounded up — a 10-minute change uses one
1026
+ * 15-minute block. The timer's actual minutes are still logged beside it.
1027
+ */ export const SITE_CARE_INCLUDED_MINUTES = 60;
1028
+ /** The block a request counts against the included hour in (AGL-3959). */ export const SITE_CARE_INCLUDED_BLOCK_MINUTES = 15;
1029
+ /**
1030
+ * Site Care extra time (AGL-3959): $79 per STARTED hour past the included
1031
+ * hour in a month. Billed in whole hours, rounded up — any part of an hour is
1032
+ * the full rate — and never less than the hours the owner approved. Only with
1033
+ * the owner's recorded yes, as an invoice item on the next subscription
1034
+ * invoice; never recurring. `site-care.ts` holds the arithmetic.
1035
+ */ export const SITE_CARE_EXTRA_HOUR_USD = 79;
1036
+ /**
1037
+ * Site Care projects (AGL-3970): work that adds a page, a form, a design or
1038
+ * a campaign is a flat quote, never hourly. The quote is the estimated hours
1039
+ * × `SITE_CARE_EXTRA_HOUR_USD`, rounded up to the whole hour, and never below
1040
+ * the published starting price of the kind of project it is. These three are
1041
+ * those starting prices, as `/pricing` publishes them.
1042
+ */ export const SITE_CARE_PROJECT_LANDING_PAGE_FROM_USD = 249;
1043
+ /** A redesign's published starting price (AGL-3970). */ export const SITE_CARE_PROJECT_REDESIGN_FROM_USD = 749;
1044
+ /** A logo or brand kit's published starting price (AGL-3970). */ export const SITE_CARE_PROJECT_BRAND_KIT_FROM_USD = 349;
1045
+ /**
1046
+ * A project priced ABOVE this is billed half up front and half at launch, on
1047
+ * two invoices from the same record; at or below it, in full up front
1048
+ * (AGL-3970). `site-care-project.ts` holds the arithmetic.
1049
+ */ export const SITE_CARE_PROJECT_SPLIT_ABOVE_USD = 500;
1019
1050
  /**
1020
1051
  * POS Pro register add-on (AGL-329): $89/mo per extra register/location
1021
1052
  * (Shopify POS Pro parity). Purchased add-ons land as a per-org
1022
1053
  * `posRegisters` entitlement override, which
1023
1054
  * `resolveOrgEntitlements` already applies over the plan default.
1024
1055
  */ export const POS_REGISTER_ADDON_MONTHLY_USD = 89;
1056
+ /** The tiers, smallest first — the order every picker lists them in. */ export const AI_TIER_IDS = [
1057
+ 'spark',
1058
+ 'boost',
1059
+ 'plus',
1060
+ 'max',
1061
+ 'ultra',
1062
+ 'studio'
1063
+ ];
1064
+ export const AI_TIERS = {
1065
+ spark: {
1066
+ label: 'Spark',
1067
+ monthlyUsd: 9,
1068
+ yearlyUsd: 108,
1069
+ credits: 4000
1070
+ },
1071
+ boost: {
1072
+ label: 'Boost',
1073
+ monthlyUsd: 19,
1074
+ yearlyUsd: 228,
1075
+ credits: 9000
1076
+ },
1077
+ plus: {
1078
+ label: 'Plus',
1079
+ monthlyUsd: 39,
1080
+ yearlyUsd: 468,
1081
+ credits: 19000
1082
+ },
1083
+ max: {
1084
+ label: 'Max',
1085
+ monthlyUsd: 69,
1086
+ yearlyUsd: 828,
1087
+ credits: 34000
1088
+ },
1089
+ ultra: {
1090
+ label: 'Ultra',
1091
+ monthlyUsd: 99,
1092
+ yearlyUsd: 1188,
1093
+ credits: 49000
1094
+ },
1095
+ studio: {
1096
+ label: 'Studio',
1097
+ monthlyUsd: 299,
1098
+ yearlyUsd: 3588,
1099
+ credits: 149000
1100
+ }
1101
+ };
1025
1102
  /**
1026
- * The Aglyn AI add-on's credit band (AGL-2896): the credits a month the
1027
- * add-on ADDS to the plan's `assistCreditsPerMonth` when
1028
- * `seatAddons.aiAddon` is set. One pool and one meter — the add-on widens
1029
- * the band the assist meter already draws on rather than opening a second
1030
- * one, so `resolveAssistCreditBudget`, the overage price and the hard-cap
1031
- * switch all see one number.
1032
- *
1033
- * ## Sized against the add-on's own price
1034
- *
1035
- * A credit is `ASSIST_CREDIT_COST_USD` of provider spend, so each band is a
1036
- * cost figure: 1,000 credits are $1.00. Every band here costs at most 50% of
1037
- * `PlanPricing.aiAddonMonthlyUsd` at full consumption — Starter $4 of $9,
1038
- * Pro $9 of $19, Business $19 of $39, Scale $34 of $69, Advanced $49 of
1039
- * $99, Agency $149 of $299 — which is the same line-margin floor
1040
- * (`ASSIST_CREDIT_MIN_MARGIN_PCT`) every retail assist rate clears.
1041
- * `tier-margin-floor.spec.ts` holds the whole tier non-negative with the
1042
- * add-on band at 100% and the add-on's revenue counted, and pins each band's
1043
- * share of its price.
1044
- *
1045
- * ## Free and Enterprise
1046
- *
1047
- * Free is 0: the plan sells no add-on (`aiAddonMonthlyUsd` is null), so a
1048
- * quantity written onto a Free org adds nothing. Enterprise is finite at
1049
- * twice Agency's, the rule every Enterprise fallback follows since the
1050
- * 2026-09-07 decision (AGL-2654) — never `UNLIMITED`, for the reason
1051
- * `ENTERPRISE_ASSIST_CREDITS_PER_MONTH` gives: an unbounded band reads back
1052
- * as zero off the wire. Enterprise carries `aiGenerative` in the agreement
1053
- * and its price is null too, so this figure is what a contract that names
1054
- * the add-on starts from before its own number is written.
1055
- */ export const AI_ADDON_CREDITS_PER_MONTH = {
1056
- free: 0,
1057
- starter: 4000,
1058
- pro: 9000,
1059
- business: 19000,
1060
- scale: 34000,
1061
- advanced: 49000,
1062
- agency: 149000,
1063
- enterprise: 298000
1103
+ * The tier an add-on bought before AGL-3965 is (AGL-3965): the one with the
1104
+ * same price and the same band the plan sold it at, so a workspace that
1105
+ * held "the Pro add-on" holds Boost — $19, 9,000 credits — and neither its
1106
+ * bill nor its credits move. Read when `aiTier` is absent and to classify a
1107
+ * per-plan `STRIPE_PRICE_{PLAN}_AI_ADDON` item still on a subscription.
1108
+ */ export const LEGACY_AI_ADDON_TIER_BY_PLAN = {
1109
+ starter: 'spark',
1110
+ pro: 'boost',
1111
+ business: 'plus',
1112
+ scale: 'max',
1113
+ advanced: 'ultra',
1114
+ agency: 'studio'
1064
1115
  };
1116
+ /**
1117
+ * The tier a purchase that names a plan and no tier means (AGL-3965): a
1118
+ * `?plan=pro&ai=1` deep link, a checkout posting `aiAddon: true` alone. The
1119
+ * tier with that plan's old price and band, so a link published before tiers
1120
+ * existed still sells what it quoted. Null where the plan sells no tier.
1121
+ */ export function defaultAiTierForPlan(plan) {
1122
+ var _LEGACY_AI_ADDON_TIER_BY_PLAN_plan;
1123
+ if (!planSellsAiTiers(plan)) return null;
1124
+ return (_LEGACY_AI_ADDON_TIER_BY_PLAN_plan = LEGACY_AI_ADDON_TIER_BY_PLAN[plan]) != null ? _LEGACY_AI_ADDON_TIER_BY_PLAN_plan : null;
1125
+ }
1126
+ /** Whether a value is one of the tier ids. */ export function isAiTierId(value) {
1127
+ return typeof value === 'string' && AI_TIER_IDS.includes(value);
1128
+ }
1129
+ /** `ai:<tier>` for a tier. */ export function aiTierAddonKey(tier) {
1130
+ return `ai:${tier}`;
1131
+ }
1132
+ /**
1133
+ * The tier an `ai:<tier>` key names, or null for anything else — another
1134
+ * add-on's key, a bare `ai`, an unknown tier, a non-string. Exact and
1135
+ * case-sensitive: a key is read off a link, and guessing is the expensive
1136
+ * direction to be wrong in (`parseOnboardingPlanIntent` reads `?ai=` the
1137
+ * same way).
1138
+ */ export function parseAiTierAddonKey(key) {
1139
+ if (typeof key !== 'string' || !key.startsWith('ai:')) return null;
1140
+ const tier = key.slice(3);
1141
+ return isAiTierId(tier) ? tier : null;
1142
+ }
1143
+ /**
1144
+ * Every tier as an `ai:<tier>` add-on, smallest first — what a picker that
1145
+ * offers "Aglyn AI" beside the other add-ons lists. Empty for a plan that
1146
+ * sells no tier (Free, Enterprise) when a plan is given.
1147
+ */ export function listAiTierAddons(plan) {
1148
+ if (plan !== undefined && !planSellsAiTiers(plan)) return [];
1149
+ return AI_TIER_IDS.map((tier)=>({
1150
+ key: aiTierAddonKey(tier),
1151
+ tier,
1152
+ label: AI_TIERS[tier].label,
1153
+ monthlyUsd: AI_TIERS[tier].monthlyUsd,
1154
+ yearlyUsd: AI_TIERS[tier].yearlyUsd,
1155
+ credits: AI_TIERS[tier].credits
1156
+ }));
1157
+ }
1158
+ /**
1159
+ * Whether a plan may buy an Aglyn AI tier (AGL-3965): every paid self-serve
1160
+ * plan. Not Free, which has no subscription to carry it, and not Enterprise,
1161
+ * whose AI is in the agreement.
1162
+ */ export function planSellsAiTiers(plan) {
1163
+ return planSellsSiteCare(plan);
1164
+ }
1065
1165
  /**
1066
1166
  * The generative add-on's name as a person sees it (AGL-2896): the brand,
1067
1167
  * then "AI". Read from the platform brand by default and from the org's
@@ -1108,7 +1208,6 @@ fillPluginPlanEntitlements(PLAN_ENTITLEMENTS);
1108
1208
  extraDataGbMonthlyUsd: null,
1109
1209
  extraApiRequestsUsdPer1k: null,
1110
1210
  extraAssistCreditsUsdPer1k: null,
1111
- aiAddonMonthlyUsd: null,
1112
1211
  extraContactsUsdPer1k: null,
1113
1212
  extraEmailSendsUsdPer1k: null,
1114
1213
  meteredInfraPassThrough: false
@@ -1129,7 +1228,6 @@ fillPluginPlanEntitlements(PLAN_ENTITLEMENTS);
1129
1228
  // the silent free overage, and this rate without the band is a fee
1130
1229
  // advertised on a quantity the plan never sold.
1131
1230
  extraAssistCreditsUsdPer1k: 3,
1132
- aiAddonMonthlyUsd: 9,
1133
1231
  extraContactsUsdPer1k: 1,
1134
1232
  // No email band to be "over" — see `emailSendsPerMonth` on the Starter
1135
1233
  // entitlements. `emailSendsOverage` returns 0 for any non-positive band,
@@ -1148,7 +1246,6 @@ fillPluginPlanEntitlements(PLAN_ENTITLEMENTS);
1148
1246
  extraDataGbMonthlyUsd: 0.36,
1149
1247
  extraApiRequestsUsdPer1k: null,
1150
1248
  extraAssistCreditsUsdPer1k: 3,
1151
- aiAddonMonthlyUsd: 19,
1152
1249
  extraContactsUsdPer1k: 0.75,
1153
1250
  extraEmailSendsUsdPer1k: 2.25,
1154
1251
  meteredInfraPassThrough: true
@@ -1163,7 +1260,6 @@ fillPluginPlanEntitlements(PLAN_ENTITLEMENTS);
1163
1260
  extraDataGbMonthlyUsd: 0.36,
1164
1261
  extraApiRequestsUsdPer1k: 0.5,
1165
1262
  extraAssistCreditsUsdPer1k: 2.75,
1166
- aiAddonMonthlyUsd: 39,
1167
1263
  extraContactsUsdPer1k: 0.5,
1168
1264
  extraEmailSendsUsdPer1k: 2,
1169
1265
  meteredInfraPassThrough: true
@@ -1178,7 +1274,6 @@ fillPluginPlanEntitlements(PLAN_ENTITLEMENTS);
1178
1274
  extraDataGbMonthlyUsd: 0.36,
1179
1275
  extraApiRequestsUsdPer1k: 0.35,
1180
1276
  extraAssistCreditsUsdPer1k: 2.5,
1181
- aiAddonMonthlyUsd: 69,
1182
1277
  extraContactsUsdPer1k: 0.4,
1183
1278
  extraEmailSendsUsdPer1k: 1.9,
1184
1279
  meteredInfraPassThrough: true
@@ -1196,7 +1291,6 @@ fillPluginPlanEntitlements(PLAN_ENTITLEMENTS);
1196
1291
  // or above $0.234 (AGL-3444).
1197
1292
  extraApiRequestsUsdPer1k: 0.25,
1198
1293
  extraAssistCreditsUsdPer1k: 2.25,
1199
- aiAddonMonthlyUsd: 99,
1200
1294
  // FLOORED at the 50% retail margin, not stepped down again. The ladder
1201
1295
  // above it descends $1.00 → $0.75 → $0.50 → $0.40, and one more step
1202
1296
  // would have reached $0.25 against a `perContactMonth` cost of $0.20 per
@@ -1230,7 +1324,6 @@ fillPluginPlanEntitlements(PLAN_ENTITLEMENTS);
1230
1324
  // FLOORED at the 50% retail margin — see Advanced's.
1231
1325
  extraApiRequestsUsdPer1k: 0.25,
1232
1326
  extraAssistCreditsUsdPer1k: 2,
1233
- aiAddonMonthlyUsd: 299,
1234
1327
  // $0.40, because the band it meters is now FINITE.
1235
1328
  //
1236
1329
  // This row has been wrong in both directions. It shipped $0.20 against an
@@ -1273,7 +1366,6 @@ fillPluginPlanEntitlements(PLAN_ENTITLEMENTS);
1273
1366
  extraDataGbMonthlyUsd: null,
1274
1367
  extraApiRequestsUsdPer1k: null,
1275
1368
  extraAssistCreditsUsdPer1k: null,
1276
- aiAddonMonthlyUsd: null,
1277
1369
  extraContactsUsdPer1k: null,
1278
1370
  extraEmailSendsUsdPer1k: null,
1279
1371
  meteredInfraPassThrough: false
@@ -1622,8 +1714,11 @@ function trimmedOrNull(value) {
1622
1714
  const addons = (_ref = org == null ? void 0 : org.seatAddons) != null ? _ref : {};
1623
1715
  const seats = (quantity, price)=>Math.max(0, quantity != null ? quantity : 0) * (price != null ? price : 0);
1624
1716
  return (annual ? pricing.basePriceAnnualMonthlyUsd : pricing.basePriceMonthlyUsd) + seats(addons.managers, pricing.extraSeatMonthlyUsd) + seats(addons.members, pricing.extraCollaboratorMonthlyUsd) + seats(addons.datasets, pricing.extraDatasetMonthlyUsd) + seats(addons.hosts, pricing.extraHostMonthlyUsd) + seats(addons.posRegisters, POS_REGISTER_ADDON_MONTHLY_USD) + seats(addons.eventCalendar, EVENT_CALENDAR_ADDON_MONTHLY_USD) + seats(siteCareUnits(addons), SITE_CARE_ADDON_MONTHLY_USD) + // A toggle, not a quantity: one purchase covers the org, so a stored
1625
- // quantity above one bills once.
1626
- seats(aiAddonUnits(addons), pricing.aiAddonMonthlyUsd);
1717
+ // quantity above one bills once, at its tier's price (AGL-3965).
1718
+ aiTierMonthlyUsd(aiTierOfPurchase(org));
1719
+ }
1720
+ /** A tier's monthly price, or 0 for no tier. */ function aiTierMonthlyUsd(tier) {
1721
+ return tier ? AI_TIERS[tier].monthlyUsd : 0;
1627
1722
  }
1628
1723
  /**
1629
1724
  * How many Aglyn AI add-ons a purchase map counts as: 0 or 1 (AGL-2896).
@@ -2449,6 +2544,65 @@ function resolvePlan(org) {
2449
2544
  */ export function hasAiAddon(org) {
2450
2545
  return aiAddonUnits(resolvePurchasedAddons(org)) === 1;
2451
2546
  }
2547
+ /**
2548
+ * Which Aglyn AI tier the org's purchase map holds (AGL-3965), whether or
2549
+ * not the subscription is still paying for it — `resolveAiTier` is the
2550
+ * reading that drops a dead one. `org.aiTier` when it names a tier;
2551
+ * otherwise, for an add-on bought before tiers existed, the tier with that
2552
+ * plan's old price and band (`LEGACY_AI_ADDON_TIER_BY_PLAN`). Null when no
2553
+ * add-on is held or the plan sells no tier (Free, Enterprise).
2554
+ */ export function aiTierOfPurchase(org) {
2555
+ var _LEGACY_AI_ADDON_TIER_BY_PLAN_;
2556
+ if (aiAddonUnits(org == null ? void 0 : org.seatAddons) !== 1) return null;
2557
+ if (!planSellsAiTiers(org == null ? void 0 : org.plan)) return null;
2558
+ if (isAiTierId(org == null ? void 0 : org.aiTier)) return org.aiTier;
2559
+ return (_LEGACY_AI_ADDON_TIER_BY_PLAN_ = LEGACY_AI_ADDON_TIER_BY_PLAN[org == null ? void 0 : org.plan]) != null ? _LEGACY_AI_ADDON_TIER_BY_PLAN_ : null;
2560
+ }
2561
+ /**
2562
+ * The Aglyn AI tier that currently applies (AGL-3965): bought, on a
2563
+ * subscription still paying for it, on a plan that sells tiers. The ONE
2564
+ * reading every surface that shows or counts a tier goes through.
2565
+ */ export function resolveAiTier(org) {
2566
+ return hasAiAddon(org) ? aiTierOfPurchase(org) : null;
2567
+ }
2568
+ /**
2569
+ * What `seatAddons.aiAddon` adds on an Enterprise org (AGL-2896, kept by
2570
+ * AGL-3965): twice Studio's band, the figure the per-plan table carried as
2571
+ * twice Agency's (the rule every Enterprise fallback follows since AGL-2654),
2572
+ * and still what a contract that names the add-on starts from before its own
2573
+ * number is written. Enterprise buys no tier — its AI is in the agreement —
2574
+ * so this is the contract figure kept, not a price; finite, never
2575
+ * `UNLIMITED`, for the reason `ENTERPRISE_ASSIST_CREDITS_PER_MONTH` gives.
2576
+ */ export const ENTERPRISE_AI_ADDON_CREDITS_PER_MONTH = AI_TIERS.studio.credits * 2;
2577
+ /**
2578
+ * The Aglyn AI add-on's per-unit quota as `resolveOrgEntitlements` folds it
2579
+ * (AGL-3965): the purchased tier's credits, on a plan that sells tiers;
2580
+ * Enterprise's contract figure (`ENTERPRISE_AI_ADDON_CREDITS_PER_MONTH`);
2581
+ * else 0 (Free). The AI plugin declares its seat add-on with this as
2582
+ * `quota.perUnit`.
2583
+ */ export function aiTierQuotaPerUnit(org, plan) {
2584
+ if (plan === 'enterprise') return ENTERPRISE_AI_ADDON_CREDITS_PER_MONTH;
2585
+ if (!planSellsAiTiers(plan)) return 0;
2586
+ const tier = aiTierOfPurchase(org);
2587
+ return tier ? AI_TIERS[tier].credits : 0;
2588
+ }
2589
+ /**
2590
+ * The credits a month the org's Aglyn AI add-on adds to its plan's included
2591
+ * band (AGL-3965) — 0 with none held. The SAME figure `resolveOrgEntitlements`
2592
+ * folds into `assistCreditsPerMonth` (`aiTierQuotaPerUnit`), so a surface
2593
+ * quoting "your tier adds" and the meter agree.
2594
+ */ export function aiTierCreditsPerMonth(org) {
2595
+ if (!hasAiAddon(org)) return 0;
2596
+ return aiTierQuotaPerUnit(org, resolveEffectivePlan(org));
2597
+ }
2598
+ /**
2599
+ * Whether this org can buy or change an Aglyn AI tier itself (AGL-3965): a
2600
+ * plan that sells tiers, and not one a staff comp grants — a comp has no
2601
+ * subscription to put the tier on (`compPricing`).
2602
+ */ export function orgCanBuyAiTier(org) {
2603
+ return !resolvePlanComp(org) && planSellsAiTiers(resolveEffectivePlan(org));
2604
+ }
2605
+ /** "Aglyn AI Boost": the product name, then the tier's. */ export const aiTierName = (tier, brand = PLATFORM_BRAND_NAME)=>`${aiAddonName(brand)} ${AI_TIERS[tier].label}`;
2452
2606
  /**
2453
2607
  * Whether the org's Site Care add-on currently applies (AGL-3954): bought,
2454
2608
  * and on a subscription that is still paying for it, so a dead subscription
@@ -2652,7 +2806,7 @@ function resolvePlan(org) {
2652
2806
  // `hostLimit` and purchased sites do. The per-unit figure is the PLAN's,
2653
2807
  // read off the effective plan rather than the stored one, so a dead
2654
2808
  // subscription that resolved to free adds free's figure.
2655
- const folded = foldPluginSeatAddons(resolved, purchased, resolvePlan(org));
2809
+ const folded = foldPluginSeatAddons(resolved, purchased, resolvePlan(org), org != null ? org : {});
2656
2810
  if (!extraHosts && !eventCalendar && !siteCare && folded === resolved) {
2657
2811
  return resolved;
2658
2812
  }
@@ -2693,7 +2847,7 @@ function resolvePlan(org) {
2693
2847
  * quota each names gains its per-plan figure per unit, and its features
2694
2848
  * switch on. Returns the input object untouched when nothing applies, so a
2695
2849
  * caller can tell "nothing folded" from "folded to the same numbers".
2696
- */ function foldPluginSeatAddons(resolved, purchased, plan) {
2850
+ */ function foldPluginSeatAddons(resolved, purchased, plan, org) {
2697
2851
  let out = resolved;
2698
2852
  for (const addon of listPluginSeatAddons()){
2699
2853
  var _addon_features;
@@ -2703,8 +2857,9 @@ function resolvePlan(org) {
2703
2857
  features: _extends({}, out.features)
2704
2858
  });
2705
2859
  if (addon.quota) {
2706
- var _addon_quota_perUnitByPlan_plan, _bag_addon_quota_key;
2707
- const perUnit = (_addon_quota_perUnitByPlan_plan = addon.quota.perUnitByPlan[plan]) != null ? _addon_quota_perUnitByPlan_plan : 0;
2860
+ var _ref, _bag_addon_quota_key;
2861
+ var _addon_quota_perUnitByPlan;
2862
+ const perUnit = addon.quota.perUnit ? addon.quota.perUnit(org, plan) : (_ref = (_addon_quota_perUnitByPlan = addon.quota.perUnitByPlan) == null ? void 0 : _addon_quota_perUnitByPlan[plan]) != null ? _ref : 0;
2708
2863
  const bag = next;
2709
2864
  const current = Number((_bag_addon_quota_key = bag[addon.quota.key]) != null ? _bag_addon_quota_key : 0);
2710
2865
  bag[addon.quota.key] = (Number.isFinite(current) ? current : 0) + perUnit * units;