platform-types 1.10.700 → 1.10.701

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.
@@ -24,3 +24,72 @@
24
24
  * enabled:false until they do.
25
25
  */
26
26
  Object.defineProperty(exports, "__esModule", { value: true });
27
+ exports.FEATURE_KEYS = exports.PLAN_TIERS = void 0;
28
+ /*
29
+ * Runtime tuples for the unions above.
30
+ *
31
+ * A union alone cannot be iterated, and any consumer that needs to enumerate
32
+ * every tier or key — zeroing a breakdown, fanning a query across tiers,
33
+ * rendering a pricing table's rows — otherwise hand-rolls its own copy that
34
+ * compiles fine while silently omitting a member. A missing member is not a
35
+ * visible break: it is a bucket that reads zero forever, or a row that never
36
+ * renders.
37
+ *
38
+ * ⚠️ `satisfies readonly PlanTier[]` alone does NOT catch that — it checks each
39
+ * element IS a tier, never that every tier is present. Hence `exhaustiveTuple`,
40
+ * whose second type parameter fails to resolve unless the tuple covers the
41
+ * union, making an omission a compile error at the tuple itself. Module-private
42
+ * on purpose: it is a type-level trick, not part of the published surface.
43
+ */
44
+ const exhaustiveTuple = () => (tuple) => tuple;
45
+ /** Every plan tier, in rank order (lowest first). */
46
+ exports.PLAN_TIERS = exhaustiveTuple()(['basico', 'emprendedor', 'profesional', 'avanzado']);
47
+ /**
48
+ * Every gated feature key.
49
+ *
50
+ * ⚠️ Adding a member here is not the whole change, and `typecheck` is what
51
+ * stops it shipping half-done. In the api, a new key needs a grant in **every
52
+ * tier** of `PLAN_FEATURES` (`stacks/services/planFeatureMatrix.ts`): that
53
+ * constant is typed total over every `PlanTier` × `FeatureKey` cell, so a tier
54
+ * missing the new key's grant, or a grant of the wrong shape for the key's
55
+ * declared `type`, fails `typecheck` at the `PLAN_FEATURES` declaration
56
+ * itself. It also widens the api's per-store override key schema, so a new key
57
+ * immediately becomes a valid override target.
58
+ *
59
+ * ⚠️ **A new member also obliges an api PLATFORM-STACK deploy.** Entitlements
60
+ * resolve inside the Lambda authorizer, built in `PlatformStack`. Deploying only
61
+ * the handler stacks leaves a STALE authorizer running the previous matrix: the
62
+ * key is absent from the resolved map, where a boolean gate refuses but a
63
+ * metered / lifetime cap PASSES THROUGH — a new cap silently unbounded.
64
+ * Checklist: `docs/PLANS_AND_ENTITLEMENTS.md` in the api repo.
65
+ */
66
+ exports.FEATURE_KEYS = [
67
+ 'afipInvoicing',
68
+ 'suppliers',
69
+ 'advancedReports',
70
+ 'cashManagement',
71
+ 'importExport',
72
+ 'domesticPayments',
73
+ 'stripePayments',
74
+ 'whatsappCommerce',
75
+ 'aiFeatures',
76
+ 'mobileApp',
77
+ 'customBranding',
78
+ 'customDomain',
79
+ 'prioritySupport',
80
+ 'advancedPricing',
81
+ 'marketplaceChannels',
82
+ 'storefront',
83
+ 'loyalty',
84
+ 'marketing',
85
+ 'mandatory2fa',
86
+ 'maxUsers',
87
+ 'maxProducts',
88
+ 'maxCustomers',
89
+ 'maxWarehouses',
90
+ 'priceListsMax',
91
+ 'webhooks',
92
+ 'maxOrdersMonth',
93
+ 'maxInvoicesMonth',
94
+ 'whatsappMessages',
95
+ ];
@@ -62,9 +62,10 @@ declare global {
62
62
  source?: 'plan' | 'override' | 'trial';
63
63
  }
64
64
  /**
65
- * All gated features. Add new keys here when a new feature becomes gateable;
66
- * every existing plan in `FEATURE_MATRIX` must then declare the new key
67
- * (TypeScript enforces this via `Record<FeatureKey, Entitlement>`).
65
+ * All gated features. Add new keys here when a new feature becomes gateable,
66
+ * and to the `FEATURE_KEYS` tuple below (`UncoveredFeatureKeys` fails
67
+ * `typecheck` naming the key until you do); the api's `PLAN_FEATURES` matrix
68
+ * must then grant it on every tier (typed total over `PlanTier` × `FeatureKey`).
68
69
  *
69
70
  * ⚠️ **`Record<FeatureKey, …>` is NOT the only construction a new member
70
71
  * breaks, and searching for it alone will tell you a consumer is unaffected
@@ -148,28 +149,84 @@ declare global {
148
149
  * booleans.
149
150
  */
150
151
  type PlanFeatureStatus = 'live' | 'planned' | 'future' | 'service';
152
+ /** Singular/plural es-AR noun for a numeric/metered feature's count (e.g. `{ one: 'usuario', other: 'usuarios' }`). */
153
+ type PlanFeatureUnit = {
154
+ one: string;
155
+ other: string;
156
+ };
157
+ /**
158
+ * The window a limit counts over: `month` — a metered counter that resets
159
+ * monthly (e.g. orders); `active` — a numeric cap on rows that exist right
160
+ * now (e.g. products, users, depósitos — disabling one frees a slot).
161
+ */
162
+ type PlanFeaturePeriod = 'month' | 'active';
151
163
  /**
152
- * A single feature row in a Plan's `features[]` array. Matches the
153
- * BE wire format from `GET /subscription/plans` exactly — boolean
154
- * features carry `enabled`, numeric/metered carry `limit` (-1 =
155
- * unlimited).
164
+ * Section a feature row renders under on a pricing table. Each key's label
165
+ * and sort position travel as a {@link PlanFeatureGroup} in
166
+ * `PlanCatalogue.groups`, so a consumer never hard-codes either.
156
167
  */
157
- interface PlanFeature {
168
+ type PlanFeatureGroupKey = 'limits' | 'billing' | 'channels' | 'operations' | 'growth' | 'enterprise';
169
+ interface PlanFeatureGroup {
170
+ key: PlanFeatureGroupKey;
171
+ /** User-facing Spanish heading. */
172
+ label: string;
173
+ /** Sort position among groups (ascending). */
174
+ order: number;
175
+ }
176
+ /** Fields every feature row carries, whatever its `type`. */
177
+ interface PlanFeatureBase {
158
178
  key: FeatureKey;
159
- type: EntitlementType;
160
179
  status: PlanFeatureStatus;
180
+ /** Short user-facing Spanish name (the row's title on a pricing table). */
181
+ label: string;
161
182
  /** User-facing Spanish description. */
162
183
  description: string;
163
- /** Set on `boolean` features; `null` on numeric/metered. */
164
- enabled: boolean | null;
165
- /** Set on `numeric`/`metered` features; `null` on boolean. -1 = unlimited. */
166
- limit: number | null;
184
+ group: PlanFeatureGroupKey;
185
+ /** Sort position within `group` (ascending). `features[]` arrives already sorted by group order, then this. */
186
+ order: number;
187
+ /** `null` on boolean keys. */
188
+ unit: PlanFeatureUnit | null;
189
+ /** `null` on boolean keys. */
190
+ period: PlanFeaturePeriod | null;
191
+ /**
192
+ * The grant rendered server-side as an es-AR noun phrase, ready to print:
193
+ * `Ilimitado` for `-1`, `1 usuario`, `Hasta 2.000 pedidos por mes`,
194
+ * `Incluido` / `No incluido` for a boolean. Display copy only — compare
195
+ * grants through `enabled` / `limit`, never by parsing this.
196
+ */
197
+ valueText: string;
167
198
  }
168
199
  /**
169
- * A sellable plan in the catalog. Aligned with the BE wire format from
170
- * `GET /subscription/plans`. Source of truth lives in
171
- * DynamoDB (`PLAN#{tier}` partition), administered via `POST /sa/plans`
172
- * + `PATCH /sa/plans/{tier}`.
200
+ * A single feature row in a Plan's `features[]` array, discriminated on
201
+ * `type`: a boolean row carries `enabled` and no `limit` key; a
202
+ * numeric/metered row carries `limit` and no `enabled` key. `limit: -1` =
203
+ * unlimited.
204
+ */
205
+ type PlanFeature = (PlanFeatureBase & {
206
+ type: 'boolean';
207
+ enabled: boolean;
208
+ }) | (PlanFeatureBase & {
209
+ type: 'numeric' | 'metered';
210
+ limit: number;
211
+ });
212
+ /**
213
+ * How a billing cycle of a plan can be bought:
214
+ *
215
+ * - `self` — self-serve checkout: the cycle has a charged price and the
216
+ * plan has a `currency`.
217
+ * - `sales` — the plan charges some cycle, but not this one self-serve
218
+ * (no price on it, or no `currency`); route to "Contactar ventas".
219
+ * - `none` — neither cycle is charged (the free tier); nothing to buy.
220
+ */
221
+ type PlanCheckoutMode = 'self' | 'sales' | 'none';
222
+ /**
223
+ * A sellable plan in the catalog, exactly as `GET /subscription/plans` and
224
+ * `GET /public/plans` emit it — both build it with the same catalogue
225
+ * builder, so the two endpoints return identical plans. The template fields
226
+ * (copy, prices, flags) live in DynamoDB (`PLAN#{tier}` / `SK: PLAN`),
227
+ * administered via `POST /platform/billing/plans` +
228
+ * `PATCH /platform/billing/plans/{tier}`; `features` are code, rendered at
229
+ * response time.
173
230
  *
174
231
  * Prices are integers in the `currency` smallest unit (centavos for ARS,
175
232
  * cents for USD). `null` = "Contactar ventas" (AVANZADO annual at launch
@@ -228,20 +285,51 @@ declare global {
228
285
  */
229
286
  color: string | null;
230
287
  /**
231
- * Per-feature configuration. Every `FeatureKey` appears exactly once.
232
- * Boolean features carry `enabled`; numeric/metered carry `limit`.
288
+ * Per-feature configuration. Every `FeatureKey` appears exactly once,
289
+ * whatever its `status` — a `planned` / `future` key is sent too, so
290
+ * filter on `status` to decide what to advertise. Sorted by group order,
291
+ * then `order`. Boolean features carry `enabled`; numeric/metered carry
292
+ * `limit`.
233
293
  */
234
294
  features: PlanFeature[];
235
295
  /**
236
296
  * Marketing-curated short bullets for the pricing card. Display order =
237
297
  * array order; up to 6 entries, 80 chars each. Always present — empty array
238
298
  * when unset (FE synthesizes a list from `features[]`). Lets marketing tweak
239
- * copy via `/sa/plans` without a redeploy; replaces the FE-only `BULLET_LIST_BY_TIER`.
299
+ * copy via `PATCH /platform/billing/plans/{tier}` without a redeploy;
300
+ * replaces the FE-only `BULLET_LIST_BY_TIER`.
240
301
  */
241
302
  bullets: string[];
303
+ /**
304
+ * Keys worth calling out on this tier's card: highlight-eligible keys with
305
+ * status `live` or `service` whose grant improves on the next lower
306
+ * ACTIVE tier (by `displayOrder`; an `isActive: false` tier is skipped,
307
+ * as pricing pages hide it). The lowest active tier lists its own
308
+ * eligible keys. A `planned` / `future` key never appears. Each is a
309
+ * `features[]` key.
310
+ */
311
+ highlights: FeatureKey[];
312
+ /** How each billing cycle can be bought — see {@link PlanCheckoutMode}. */
313
+ checkout: {
314
+ monthly: PlanCheckoutMode;
315
+ annual: PlanCheckoutMode;
316
+ };
317
+ /** Whether a subscription on this tier can be cancelled: `true` exactly when at least one cycle is charged. */
318
+ cancellable: boolean;
319
+ }
320
+ /**
321
+ * `data` of `GET /public/plans` and `GET /subscription/plans` — both return
322
+ * the same `{ plans, groups }`: the plans plus the feature groups their rows
323
+ * render under.
324
+ */
325
+ interface PlanCatalogue {
326
+ plans: Plan[];
327
+ /** Every group a `features[].group` can name, with its label and sort position. */
328
+ groups: PlanFeatureGroup[];
242
329
  }
243
330
  /**
244
- * One audit row per SUPER_ADMIN-driven plan mutation, returned by
331
+ * One audit row per MANAGER-driven plan mutation (`POST` / `PATCH
332
+ * /platform/billing/plans`, `asManager`), returned by
245
333
  * `GET /platform/billing/plans/{tier}/audit`. The store-subscription audit
246
334
  * shares this storage shape but is read as `SubscriptionAuditEntry`.
247
335
  * `before`/`after` carry only the fields that changed (diff slice).
@@ -648,3 +736,28 @@ declare global {
648
736
  }
649
737
  export type BillingProviderName = SubscriptionProvider;
650
738
  export type InvoiceSummary = globalThis.InvoiceSummary;
739
+ /** Every plan tier, in rank order (lowest first). */
740
+ export declare const PLAN_TIERS: readonly ["basico", "emprendedor", "profesional", "avanzado"];
741
+ /**
742
+ * Every gated feature key.
743
+ *
744
+ * ⚠️ Adding a member here is not the whole change, and `typecheck` is what
745
+ * stops it shipping half-done. In the api, a new key needs a grant in **every
746
+ * tier** of `PLAN_FEATURES` (`stacks/services/planFeatureMatrix.ts`): that
747
+ * constant is typed total over every `PlanTier` × `FeatureKey` cell, so a tier
748
+ * missing the new key's grant, or a grant of the wrong shape for the key's
749
+ * declared `type`, fails `typecheck` at the `PLAN_FEATURES` declaration
750
+ * itself. It also widens the api's per-store override key schema, so a new key
751
+ * immediately becomes a valid override target.
752
+ *
753
+ * ⚠️ **A new member also obliges an api PLATFORM-STACK deploy.** Entitlements
754
+ * resolve inside the Lambda authorizer, built in `PlatformStack`. Deploying only
755
+ * the handler stacks leaves a STALE authorizer running the previous matrix: the
756
+ * key is absent from the resolved map, where a boolean gate refuses but a
757
+ * metered / lifetime cap PASSES THROUGH — a new cap silently unbounded.
758
+ * Checklist: `docs/PLANS_AND_ENTITLEMENTS.md` in the api repo.
759
+ */
760
+ export declare const FEATURE_KEYS: readonly ["afipInvoicing", "suppliers", "advancedReports", "cashManagement", "importExport", "domesticPayments", "stripePayments", "whatsappCommerce", "aiFeatures", "mobileApp", "customBranding", "customDomain", "prioritySupport", "advancedPricing", "marketplaceChannels", "storefront", "loyalty", "marketing", "mandatory2fa", "maxUsers", "maxProducts", "maxCustomers", "maxWarehouses", "priceListsMax", "webhooks", "maxOrdersMonth", "maxInvoicesMonth", "whatsappMessages"];
761
+ type AssertNoUncoveredFeatureKey<T extends never> = T;
762
+ export type UncoveredFeatureKeys = AssertNoUncoveredFeatureKey<Exclude<FeatureKey, (typeof FEATURE_KEYS)[number]>>;
763
+ export {};
@@ -22,4 +22,71 @@
22
22
  * declared here even when their epics haven't shipped — the matrix can set
23
23
  * enabled:false until they do.
24
24
  */
25
- export {};
25
+ /*
26
+ * Runtime tuples for the unions above.
27
+ *
28
+ * A union alone cannot be iterated, and any consumer that needs to enumerate
29
+ * every tier or key — zeroing a breakdown, fanning a query across tiers,
30
+ * rendering a pricing table's rows — otherwise hand-rolls its own copy that
31
+ * compiles fine while silently omitting a member. A missing member is not a
32
+ * visible break: it is a bucket that reads zero forever, or a row that never
33
+ * renders.
34
+ *
35
+ * ⚠️ `satisfies readonly PlanTier[]` alone does NOT catch that — it checks each
36
+ * element IS a tier, never that every tier is present. Hence `exhaustiveTuple`,
37
+ * whose second type parameter fails to resolve unless the tuple covers the
38
+ * union, making an omission a compile error at the tuple itself. Module-private
39
+ * on purpose: it is a type-level trick, not part of the published surface.
40
+ */
41
+ const exhaustiveTuple = () => (tuple) => tuple;
42
+ /** Every plan tier, in rank order (lowest first). */
43
+ export const PLAN_TIERS = exhaustiveTuple()(['basico', 'emprendedor', 'profesional', 'avanzado']);
44
+ /**
45
+ * Every gated feature key.
46
+ *
47
+ * ⚠️ Adding a member here is not the whole change, and `typecheck` is what
48
+ * stops it shipping half-done. In the api, a new key needs a grant in **every
49
+ * tier** of `PLAN_FEATURES` (`stacks/services/planFeatureMatrix.ts`): that
50
+ * constant is typed total over every `PlanTier` × `FeatureKey` cell, so a tier
51
+ * missing the new key's grant, or a grant of the wrong shape for the key's
52
+ * declared `type`, fails `typecheck` at the `PLAN_FEATURES` declaration
53
+ * itself. It also widens the api's per-store override key schema, so a new key
54
+ * immediately becomes a valid override target.
55
+ *
56
+ * ⚠️ **A new member also obliges an api PLATFORM-STACK deploy.** Entitlements
57
+ * resolve inside the Lambda authorizer, built in `PlatformStack`. Deploying only
58
+ * the handler stacks leaves a STALE authorizer running the previous matrix: the
59
+ * key is absent from the resolved map, where a boolean gate refuses but a
60
+ * metered / lifetime cap PASSES THROUGH — a new cap silently unbounded.
61
+ * Checklist: `docs/PLANS_AND_ENTITLEMENTS.md` in the api repo.
62
+ */
63
+ export const FEATURE_KEYS = [
64
+ 'afipInvoicing',
65
+ 'suppliers',
66
+ 'advancedReports',
67
+ 'cashManagement',
68
+ 'importExport',
69
+ 'domesticPayments',
70
+ 'stripePayments',
71
+ 'whatsappCommerce',
72
+ 'aiFeatures',
73
+ 'mobileApp',
74
+ 'customBranding',
75
+ 'customDomain',
76
+ 'prioritySupport',
77
+ 'advancedPricing',
78
+ 'marketplaceChannels',
79
+ 'storefront',
80
+ 'loyalty',
81
+ 'marketing',
82
+ 'mandatory2fa',
83
+ 'maxUsers',
84
+ 'maxProducts',
85
+ 'maxCustomers',
86
+ 'maxWarehouses',
87
+ 'priceListsMax',
88
+ 'webhooks',
89
+ 'maxOrdersMonth',
90
+ 'maxInvoicesMonth',
91
+ 'whatsappMessages',
92
+ ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "platform-types",
3
- "version": "1.10.700",
3
+ "version": "1.10.701",
4
4
  "description": "Shared wire and entity contracts for the platform.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",