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