@revstackhq/core 0.0.0-dev-20260227103607 → 0.0.0-dev-20260228062053

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 (3) hide show
  1. package/dist/index.d.ts +372 -118
  2. package/dist/index.js +179 -52
  3. package/package.json +4 -1
package/dist/index.d.ts CHANGED
@@ -1,9 +1,5 @@
1
- /**
2
- * @file types.ts
3
- * @description Core type definitions for Revstack's Billing Engine.
4
- * These types map directly to the PostgreSQL database schema and
5
- * serve as the contract for "Billing as Code".
6
- */
1
+ import { z } from 'zod';
2
+
7
3
  /**
8
4
  * The type of a feature/entitlement.
9
5
  * - 'boolean': On/Off flag (e.g., SSO Access).
@@ -19,7 +15,7 @@ type UnitType = "count" | "bytes" | "seconds" | "tokens" | "requests" | "custom"
19
15
  /**
20
16
  * How often a feature's usage counter resets.
21
17
  */
22
- type ResetPeriod = "monthly" | "yearly" | "never";
18
+ type ResetPeriod = "daily" | "weekly" | "monthly" | "yearly" | "never";
23
19
  /**
24
20
  * Billing interval for a plan's price.
25
21
  */
@@ -28,7 +24,7 @@ type BillingInterval = "monthly" | "quarterly" | "yearly" | "one_time";
28
24
  * The commercial classification of a plan.
29
25
  * - 'free': No payment required (e.g., Default Guest Plan, Starter).
30
26
  * - 'paid': Requires active payment method.
31
- * - 'custom': Enterprise / negotiated pricing.
27
+ * - 'custom': Enterprise / B2B negotiated contracts where there is no strict price array and the checkout should redirect to a "Contact Sales" flow.
32
28
  */
33
29
  type PlanType = "paid" | "free" | "custom";
34
30
  /**
@@ -131,6 +127,8 @@ interface PriceDef {
131
127
  /** The quantity of units the overage_amount applies to. */
132
128
  overage_unit: number;
133
129
  }>;
130
+ /** Slugs of addons that can be attached to this specific price/interval. */
131
+ available_addons?: string[];
134
132
  }
135
133
  /**
136
134
  * Full plan definition. Maps to the `plans` table in the database.
@@ -157,8 +155,6 @@ interface PlanDef {
157
155
  prices?: PriceDef[];
158
156
  /** Feature entitlements included in this plan. */
159
157
  features: Record<string, PlanFeatureValue>;
160
- /** Slugs of addons that can be attached to this plan. */
161
- available_addons?: string[];
162
158
  }
163
159
  /**
164
160
  * Input type for `definePlan()`.
@@ -168,7 +164,6 @@ interface PlanDef {
168
164
  type PlanDefInput = Omit<PlanDef, "slug" | "status" | "features"> & {
169
165
  status?: PlanStatus;
170
166
  features: Record<string, PlanFeatureValue>;
171
- available_addons?: string[];
172
167
  };
173
168
  /**
174
169
  * An add-on is a product purchased on top of a subscription.
@@ -183,8 +178,12 @@ interface AddonDef {
183
178
  description?: string;
184
179
  /** Billing type. */
185
180
  type: "recurring" | "one_time";
186
- /** Add-on pricing configurations (1:N). */
187
- prices?: PriceDef[];
181
+ /** Price amount in the smallest currency unit (e.g., cents). */
182
+ amount: number;
183
+ /** ISO 4217 currency code. */
184
+ currency: string;
185
+ /** Billing interval (Required if type === 'recurring'). */
186
+ billing_interval?: Exclude<BillingInterval, "one_time">;
188
187
  /** Feature entitlements this add-on modifies or grants. */
189
188
  features: Record<string, AddonFeatureValue>;
190
189
  }
@@ -195,19 +194,11 @@ interface AddonDef {
195
194
  type AddonDefInput = Omit<AddonDef, "slug">;
196
195
  type DiscountType = "percent" | "amount";
197
196
  type DiscountDuration = "once" | "forever" | "repeating";
198
- interface DiscountDef {
197
+ interface DiscountBase {
199
198
  /** The code the user enters at checkout (e.g., 'BLACKFRIDAY_24'). */
200
199
  code: string;
201
200
  /** Friendly name for invoices. */
202
201
  name?: string;
203
- /** 'percent' (0–100) or 'amount' (smallest currency unit). */
204
- type: DiscountType;
205
- /** The discount value. */
206
- value: number;
207
- /** How long the discount lasts. */
208
- duration: DiscountDuration;
209
- /** If duration is 'repeating', how many months. */
210
- duration_in_months?: number;
211
202
  /** Restrict to specific plan slugs. Empty = all. */
212
203
  applies_to_plans?: string[];
213
204
  /** Maximum number of redemptions globally. */
@@ -215,6 +206,21 @@ interface DiscountDef {
215
206
  /** Expiration date (ISO 8601). */
216
207
  expires_at?: string;
217
208
  }
209
+ type DiscountValueDef = {
210
+ type: "percent";
211
+ value: number;
212
+ } | {
213
+ type: "amount";
214
+ value: number;
215
+ };
216
+ type DiscountDurationDef = {
217
+ /** How long the discount lasts. */ duration: "once" | "forever";
218
+ duration_in_months?: never;
219
+ } | {
220
+ /** How long the discount lasts. */ duration: "repeating";
221
+ /** How many months. */ duration_in_months: number;
222
+ };
223
+ type DiscountDef = DiscountBase & DiscountValueDef & DiscountDurationDef;
218
224
  /**
219
225
  * The output of the Entitlement Engine.
220
226
  * Answers: "Can the user do this?"
@@ -249,49 +255,6 @@ interface RevstackConfig {
249
255
  coupons?: DiscountDef[];
250
256
  }
251
257
 
252
- /**
253
- * @file define.ts
254
- * @description Identity helpers for "Billing as Code" config authoring.
255
- *
256
- * These functions return their input unchanged — their sole purpose is to
257
- * provide autocompletion, type narrowing, and compile-time validation
258
- * when developers write their `revstack.config.ts`.
259
- *
260
- * @example
261
- * ```typescript
262
- * import { defineConfig, defineFeature, definePlan } from "@revstackhq/core";
263
- *
264
- * const features = {
265
- * seats: defineFeature({ name: "Seats", type: "static", unit_type: "count" }),
266
- * sso: defineFeature({ name: "SSO", type: "boolean", unit_type: "count" }),
267
- * };
268
- *
269
- * export default defineConfig({
270
- * features,
271
- * plans: {
272
- * default: definePlan<typeof features>({
273
- * name: "Default",
274
- * is_default: true,
275
- * is_public: false,
276
- * type: "free",
277
- * features: {},
278
- * }),
279
- * pro: definePlan<typeof features>({
280
- * name: "Pro",
281
- * is_default: false,
282
- * is_public: true,
283
- * type: "paid",
284
- * prices: [{ amount: 2900, currency: "USD", billing_interval: "monthly" }],
285
- * features: {
286
- * seats: { value_limit: 5, is_hard_limit: true },
287
- * sso: { value_bool: true },
288
- * },
289
- * }),
290
- * },
291
- * });
292
- * ```
293
- */
294
-
295
258
  /**
296
259
  * Define a feature with full type inference.
297
260
  * Identity function — returns the input as-is.
@@ -332,17 +295,8 @@ declare function definePlan<F extends Record<string, FeatureDefInput> = Record<s
332
295
  *
333
296
  * @typeParam F - Feature dictionary type for key restriction.
334
297
  */
335
- declare function defineAddon<F extends Record<string, FeatureDefInput> = Record<string, FeatureDefInput>>(config: Omit<AddonDefInput, "features" | "prices"> & {
298
+ declare function defineAddon<F extends Record<string, FeatureDefInput> = Record<string, FeatureDefInput>>(config: Omit<AddonDefInput, "features"> & {
336
299
  features: F extends Record<string, FeatureDefInput> ? Partial<Record<keyof F, AddonFeatureValue>> : Record<string, AddonFeatureValue>;
337
- prices?: Array<Omit<PriceDef, "overage_configuration"> & {
338
- overage_configuration?: F extends Record<string, FeatureDefInput> ? Partial<Record<keyof F, {
339
- overage_amount: number;
340
- overage_unit: number;
341
- }>> : Record<string, {
342
- overage_amount: number;
343
- overage_unit: number;
344
- }>;
345
- }>;
346
300
  }): AddonDefInput;
347
301
  /**
348
302
  * Define a discount/coupon with full type inference.
@@ -355,28 +309,6 @@ declare function defineDiscount<T extends DiscountDef>(config: T): T;
355
309
  */
356
310
  declare function defineConfig<T extends RevstackConfig>(config: T): T;
357
311
 
358
- /**
359
- * @file engine.ts
360
- * @description The Entitlement Engine — the logic core of Revstack.
361
- *
362
- * Determines if a user can access a feature based on their active Plan,
363
- * purchased Add-ons, and subscription payment status. All decisions are
364
- * pure, stateless computations with no side effects.
365
- *
366
- * @example
367
- * ```typescript
368
- * import { EntitlementEngine } from "@revstackhq/core";
369
- *
370
- * const engine = new EntitlementEngine(plan, addons, "active");
371
- *
372
- * // Single check
373
- * const result = engine.check("seats", 4);
374
- *
375
- * // Batch check
376
- * const results = engine.checkBatch({ seats: 4, ai_tokens: 12000 });
377
- * ```
378
- */
379
-
380
312
  /**
381
313
  * The Entitlement Engine — evaluates feature access for a single customer.
382
314
  *
@@ -420,24 +352,6 @@ declare class EntitlementEngine {
420
352
  checkBatch(usages: Record<string, number>): Record<string, CheckResult>;
421
353
  }
422
354
 
423
- /**
424
- * @file validator.ts
425
- * @description Runtime validation for Revstack billing configurations.
426
- *
427
- * Validates the business logic invariants of a `RevstackConfig` object
428
- * before it is synced to the backend. Catches misconfigurations early
429
- * that TypeScript's type system cannot enforce (e.g., referencing
430
- * undefined features, negative prices, duplicate slugs).
431
- *
432
- * @example
433
- * ```typescript
434
- * import { validateConfig, defineConfig } from "@revstackhq/core";
435
- *
436
- * const config = defineConfig({ features: {}, plans: {} });
437
- * validateConfig(config); // throws RevstackValidationError if invalid
438
- * ```
439
- */
440
-
441
355
  /**
442
356
  * Thrown when `validateConfig()` detects one or more invalid business
443
357
  * logic rules in a billing configuration.
@@ -457,12 +371,352 @@ declare class RevstackValidationError extends Error {
457
371
  * Checks the following invariants:
458
372
  * 1. **Default plan** — Exactly one plan has `is_default: true`.
459
373
  * 2. **Feature references** — Plans/addons only reference features defined in `config.features`.
460
- * 3. **Non-negative pricing** — All price amounts and limits are ≥ 0.
461
- * 4. **Discount bounds** — Percentage discounts have values in [0, 100].
462
374
  *
463
375
  * @param config - The billing configuration to validate.
464
376
  * @throws {RevstackValidationError} If any violations are found.
465
377
  */
466
378
  declare function validateConfig(config: RevstackConfig): void;
467
379
 
468
- export { type AddonDef, type AddonDefInput, type AddonFeatureValue, type BillingInterval, type CheckResult, type DiscountDef, type DiscountDuration, type DiscountType, EntitlementEngine, type FeatureDef, type FeatureDefInput, type FeatureType, type PlanDef, type PlanDefInput, type PlanFeatureValue, type PlanStatus, type PlanType, type PriceDef, type ResetPeriod, type RevstackConfig, RevstackValidationError, type SubscriptionStatus, type UnitType, defineAddon, defineConfig, defineDiscount, defineFeature, definePlan, validateConfig };
380
+ declare const FeatureTypeSchema: z.ZodEnum<{
381
+ boolean: "boolean";
382
+ static: "static";
383
+ metered: "metered";
384
+ }>;
385
+ declare const UnitTypeSchema: z.ZodEnum<{
386
+ count: "count";
387
+ bytes: "bytes";
388
+ seconds: "seconds";
389
+ tokens: "tokens";
390
+ requests: "requests";
391
+ custom: "custom";
392
+ }>;
393
+ declare const ResetPeriodSchema: z.ZodEnum<{
394
+ daily: "daily";
395
+ weekly: "weekly";
396
+ monthly: "monthly";
397
+ yearly: "yearly";
398
+ never: "never";
399
+ }>;
400
+ declare const BillingIntervalSchema: z.ZodEnum<{
401
+ monthly: "monthly";
402
+ yearly: "yearly";
403
+ quarterly: "quarterly";
404
+ one_time: "one_time";
405
+ }>;
406
+ declare const PlanTypeSchema: z.ZodEnum<{
407
+ custom: "custom";
408
+ paid: "paid";
409
+ free: "free";
410
+ }>;
411
+ declare const PlanStatusSchema: z.ZodEnum<{
412
+ draft: "draft";
413
+ active: "active";
414
+ archived: "archived";
415
+ }>;
416
+ declare const SubscriptionStatusSchema: z.ZodEnum<{
417
+ active: "active";
418
+ trialing: "trialing";
419
+ past_due: "past_due";
420
+ canceled: "canceled";
421
+ paused: "paused";
422
+ }>;
423
+ declare const FeatureDefInputSchema: z.ZodObject<{
424
+ name: z.ZodString;
425
+ description: z.ZodOptional<z.ZodString>;
426
+ type: z.ZodEnum<{
427
+ boolean: "boolean";
428
+ static: "static";
429
+ metered: "metered";
430
+ }>;
431
+ unit_type: z.ZodEnum<{
432
+ count: "count";
433
+ bytes: "bytes";
434
+ seconds: "seconds";
435
+ tokens: "tokens";
436
+ requests: "requests";
437
+ custom: "custom";
438
+ }>;
439
+ }, z.core.$strip>;
440
+ declare const PlanFeatureValueSchema: z.ZodObject<{
441
+ value_limit: z.ZodOptional<z.ZodNumber>;
442
+ value_bool: z.ZodOptional<z.ZodBoolean>;
443
+ value_text: z.ZodOptional<z.ZodString>;
444
+ is_hard_limit: z.ZodOptional<z.ZodBoolean>;
445
+ reset_period: z.ZodOptional<z.ZodEnum<{
446
+ daily: "daily";
447
+ weekly: "weekly";
448
+ monthly: "monthly";
449
+ yearly: "yearly";
450
+ never: "never";
451
+ }>>;
452
+ }, z.core.$strip>;
453
+ declare const AddonFeatureValueSchema: z.ZodObject<{
454
+ value_limit: z.ZodOptional<z.ZodNumber>;
455
+ type: z.ZodOptional<z.ZodEnum<{
456
+ increment: "increment";
457
+ set: "set";
458
+ }>>;
459
+ has_access: z.ZodOptional<z.ZodBoolean>;
460
+ is_hard_limit: z.ZodOptional<z.ZodBoolean>;
461
+ }, z.core.$strip>;
462
+ declare const OverageConfigurationSchema: z.ZodRecord<z.ZodString, z.ZodObject<{
463
+ overage_amount: z.ZodNumber;
464
+ overage_unit: z.ZodNumber;
465
+ }, z.core.$strip>>;
466
+ declare const PriceDefSchema: z.ZodObject<{
467
+ amount: z.ZodNumber;
468
+ currency: z.ZodString;
469
+ billing_interval: z.ZodEnum<{
470
+ monthly: "monthly";
471
+ yearly: "yearly";
472
+ quarterly: "quarterly";
473
+ one_time: "one_time";
474
+ }>;
475
+ trial_period_days: z.ZodOptional<z.ZodNumber>;
476
+ is_active: z.ZodOptional<z.ZodBoolean>;
477
+ overage_configuration: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
478
+ overage_amount: z.ZodNumber;
479
+ overage_unit: z.ZodNumber;
480
+ }, z.core.$strip>>>;
481
+ available_addons: z.ZodOptional<z.ZodArray<z.ZodString>>;
482
+ }, z.core.$strip>;
483
+ declare const PlanDefInputSchema: z.ZodObject<{
484
+ name: z.ZodString;
485
+ description: z.ZodOptional<z.ZodString>;
486
+ is_default: z.ZodBoolean;
487
+ is_public: z.ZodBoolean;
488
+ type: z.ZodEnum<{
489
+ custom: "custom";
490
+ paid: "paid";
491
+ free: "free";
492
+ }>;
493
+ status: z.ZodDefault<z.ZodOptional<z.ZodEnum<{
494
+ draft: "draft";
495
+ active: "active";
496
+ archived: "archived";
497
+ }>>>;
498
+ prices: z.ZodOptional<z.ZodArray<z.ZodObject<{
499
+ amount: z.ZodNumber;
500
+ currency: z.ZodString;
501
+ billing_interval: z.ZodEnum<{
502
+ monthly: "monthly";
503
+ yearly: "yearly";
504
+ quarterly: "quarterly";
505
+ one_time: "one_time";
506
+ }>;
507
+ trial_period_days: z.ZodOptional<z.ZodNumber>;
508
+ is_active: z.ZodOptional<z.ZodBoolean>;
509
+ overage_configuration: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
510
+ overage_amount: z.ZodNumber;
511
+ overage_unit: z.ZodNumber;
512
+ }, z.core.$strip>>>;
513
+ available_addons: z.ZodOptional<z.ZodArray<z.ZodString>>;
514
+ }, z.core.$strip>>>;
515
+ features: z.ZodRecord<z.ZodString, z.ZodObject<{
516
+ value_limit: z.ZodOptional<z.ZodNumber>;
517
+ value_bool: z.ZodOptional<z.ZodBoolean>;
518
+ value_text: z.ZodOptional<z.ZodString>;
519
+ is_hard_limit: z.ZodOptional<z.ZodBoolean>;
520
+ reset_period: z.ZodOptional<z.ZodEnum<{
521
+ daily: "daily";
522
+ weekly: "weekly";
523
+ monthly: "monthly";
524
+ yearly: "yearly";
525
+ never: "never";
526
+ }>>;
527
+ }, z.core.$strip>>;
528
+ }, z.core.$strip>;
529
+ declare const AddonDefInputSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
530
+ name: z.ZodString;
531
+ description: z.ZodOptional<z.ZodString>;
532
+ amount: z.ZodNumber;
533
+ currency: z.ZodString;
534
+ features: z.ZodRecord<z.ZodString, z.ZodObject<{
535
+ value_limit: z.ZodOptional<z.ZodNumber>;
536
+ type: z.ZodOptional<z.ZodEnum<{
537
+ increment: "increment";
538
+ set: "set";
539
+ }>>;
540
+ has_access: z.ZodOptional<z.ZodBoolean>;
541
+ is_hard_limit: z.ZodOptional<z.ZodBoolean>;
542
+ }, z.core.$strip>>;
543
+ type: z.ZodLiteral<"recurring">;
544
+ billing_interval: z.ZodEnum<{
545
+ monthly: "monthly";
546
+ yearly: "yearly";
547
+ quarterly: "quarterly";
548
+ }>;
549
+ }, z.core.$strip>, z.ZodObject<{
550
+ name: z.ZodString;
551
+ description: z.ZodOptional<z.ZodString>;
552
+ amount: z.ZodNumber;
553
+ currency: z.ZodString;
554
+ features: z.ZodRecord<z.ZodString, z.ZodObject<{
555
+ value_limit: z.ZodOptional<z.ZodNumber>;
556
+ type: z.ZodOptional<z.ZodEnum<{
557
+ increment: "increment";
558
+ set: "set";
559
+ }>>;
560
+ has_access: z.ZodOptional<z.ZodBoolean>;
561
+ is_hard_limit: z.ZodOptional<z.ZodBoolean>;
562
+ }, z.core.$strip>>;
563
+ type: z.ZodLiteral<"one_time">;
564
+ billing_interval: z.ZodOptional<z.ZodAny>;
565
+ }, z.core.$strip>], "type">;
566
+ declare const DiscountTypeSchema: z.ZodEnum<{
567
+ amount: "amount";
568
+ percent: "percent";
569
+ }>;
570
+ declare const DiscountDurationSchema: z.ZodEnum<{
571
+ once: "once";
572
+ forever: "forever";
573
+ repeating: "repeating";
574
+ }>;
575
+ declare const DiscountDefSchema: z.ZodIntersection<z.ZodIntersection<z.ZodObject<{
576
+ code: z.ZodString;
577
+ name: z.ZodOptional<z.ZodString>;
578
+ applies_to_plans: z.ZodOptional<z.ZodArray<z.ZodString>>;
579
+ max_redemptions: z.ZodOptional<z.ZodNumber>;
580
+ expires_at: z.ZodOptional<z.ZodString>;
581
+ }, z.core.$strip>, z.ZodDiscriminatedUnion<[z.ZodObject<{
582
+ type: z.ZodLiteral<"percent">;
583
+ value: z.ZodNumber;
584
+ }, z.core.$strip>, z.ZodObject<{
585
+ type: z.ZodLiteral<"amount">;
586
+ value: z.ZodNumber;
587
+ }, z.core.$strip>], "type">>, z.ZodDiscriminatedUnion<[z.ZodObject<{
588
+ duration: z.ZodLiteral<"once">;
589
+ duration_in_months: z.ZodOptional<z.ZodUndefined>;
590
+ }, z.core.$strip>, z.ZodObject<{
591
+ duration: z.ZodLiteral<"forever">;
592
+ duration_in_months: z.ZodOptional<z.ZodUndefined>;
593
+ }, z.core.$strip>, z.ZodObject<{
594
+ duration: z.ZodLiteral<"repeating">;
595
+ duration_in_months: z.ZodNumber;
596
+ }, z.core.$strip>], "duration">>;
597
+ declare const RevstackConfigSchema: z.ZodObject<{
598
+ features: z.ZodRecord<z.ZodString, z.ZodObject<{
599
+ name: z.ZodString;
600
+ description: z.ZodOptional<z.ZodString>;
601
+ type: z.ZodEnum<{
602
+ boolean: "boolean";
603
+ static: "static";
604
+ metered: "metered";
605
+ }>;
606
+ unit_type: z.ZodEnum<{
607
+ count: "count";
608
+ bytes: "bytes";
609
+ seconds: "seconds";
610
+ tokens: "tokens";
611
+ requests: "requests";
612
+ custom: "custom";
613
+ }>;
614
+ }, z.core.$strip>>;
615
+ plans: z.ZodRecord<z.ZodString, z.ZodObject<{
616
+ name: z.ZodString;
617
+ description: z.ZodOptional<z.ZodString>;
618
+ is_default: z.ZodBoolean;
619
+ is_public: z.ZodBoolean;
620
+ type: z.ZodEnum<{
621
+ custom: "custom";
622
+ paid: "paid";
623
+ free: "free";
624
+ }>;
625
+ status: z.ZodDefault<z.ZodOptional<z.ZodEnum<{
626
+ draft: "draft";
627
+ active: "active";
628
+ archived: "archived";
629
+ }>>>;
630
+ prices: z.ZodOptional<z.ZodArray<z.ZodObject<{
631
+ amount: z.ZodNumber;
632
+ currency: z.ZodString;
633
+ billing_interval: z.ZodEnum<{
634
+ monthly: "monthly";
635
+ yearly: "yearly";
636
+ quarterly: "quarterly";
637
+ one_time: "one_time";
638
+ }>;
639
+ trial_period_days: z.ZodOptional<z.ZodNumber>;
640
+ is_active: z.ZodOptional<z.ZodBoolean>;
641
+ overage_configuration: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
642
+ overage_amount: z.ZodNumber;
643
+ overage_unit: z.ZodNumber;
644
+ }, z.core.$strip>>>;
645
+ available_addons: z.ZodOptional<z.ZodArray<z.ZodString>>;
646
+ }, z.core.$strip>>>;
647
+ features: z.ZodRecord<z.ZodString, z.ZodObject<{
648
+ value_limit: z.ZodOptional<z.ZodNumber>;
649
+ value_bool: z.ZodOptional<z.ZodBoolean>;
650
+ value_text: z.ZodOptional<z.ZodString>;
651
+ is_hard_limit: z.ZodOptional<z.ZodBoolean>;
652
+ reset_period: z.ZodOptional<z.ZodEnum<{
653
+ daily: "daily";
654
+ weekly: "weekly";
655
+ monthly: "monthly";
656
+ yearly: "yearly";
657
+ never: "never";
658
+ }>>;
659
+ }, z.core.$strip>>;
660
+ }, z.core.$strip>>;
661
+ addons: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodDiscriminatedUnion<[z.ZodObject<{
662
+ name: z.ZodString;
663
+ description: z.ZodOptional<z.ZodString>;
664
+ amount: z.ZodNumber;
665
+ currency: z.ZodString;
666
+ features: z.ZodRecord<z.ZodString, z.ZodObject<{
667
+ value_limit: z.ZodOptional<z.ZodNumber>;
668
+ type: z.ZodOptional<z.ZodEnum<{
669
+ increment: "increment";
670
+ set: "set";
671
+ }>>;
672
+ has_access: z.ZodOptional<z.ZodBoolean>;
673
+ is_hard_limit: z.ZodOptional<z.ZodBoolean>;
674
+ }, z.core.$strip>>;
675
+ type: z.ZodLiteral<"recurring">;
676
+ billing_interval: z.ZodEnum<{
677
+ monthly: "monthly";
678
+ yearly: "yearly";
679
+ quarterly: "quarterly";
680
+ }>;
681
+ }, z.core.$strip>, z.ZodObject<{
682
+ name: z.ZodString;
683
+ description: z.ZodOptional<z.ZodString>;
684
+ amount: z.ZodNumber;
685
+ currency: z.ZodString;
686
+ features: z.ZodRecord<z.ZodString, z.ZodObject<{
687
+ value_limit: z.ZodOptional<z.ZodNumber>;
688
+ type: z.ZodOptional<z.ZodEnum<{
689
+ increment: "increment";
690
+ set: "set";
691
+ }>>;
692
+ has_access: z.ZodOptional<z.ZodBoolean>;
693
+ is_hard_limit: z.ZodOptional<z.ZodBoolean>;
694
+ }, z.core.$strip>>;
695
+ type: z.ZodLiteral<"one_time">;
696
+ billing_interval: z.ZodOptional<z.ZodAny>;
697
+ }, z.core.$strip>], "type">>>;
698
+ coupons: z.ZodOptional<z.ZodArray<z.ZodIntersection<z.ZodIntersection<z.ZodObject<{
699
+ code: z.ZodString;
700
+ name: z.ZodOptional<z.ZodString>;
701
+ applies_to_plans: z.ZodOptional<z.ZodArray<z.ZodString>>;
702
+ max_redemptions: z.ZodOptional<z.ZodNumber>;
703
+ expires_at: z.ZodOptional<z.ZodString>;
704
+ }, z.core.$strip>, z.ZodDiscriminatedUnion<[z.ZodObject<{
705
+ type: z.ZodLiteral<"percent">;
706
+ value: z.ZodNumber;
707
+ }, z.core.$strip>, z.ZodObject<{
708
+ type: z.ZodLiteral<"amount">;
709
+ value: z.ZodNumber;
710
+ }, z.core.$strip>], "type">>, z.ZodDiscriminatedUnion<[z.ZodObject<{
711
+ duration: z.ZodLiteral<"once">;
712
+ duration_in_months: z.ZodOptional<z.ZodUndefined>;
713
+ }, z.core.$strip>, z.ZodObject<{
714
+ duration: z.ZodLiteral<"forever">;
715
+ duration_in_months: z.ZodOptional<z.ZodUndefined>;
716
+ }, z.core.$strip>, z.ZodObject<{
717
+ duration: z.ZodLiteral<"repeating">;
718
+ duration_in_months: z.ZodNumber;
719
+ }, z.core.$strip>], "duration">>>>;
720
+ }, z.core.$strip>;
721
+
722
+ export { type AddonDef, type AddonDefInput, AddonDefInputSchema, type AddonFeatureValue, AddonFeatureValueSchema, type BillingInterval, BillingIntervalSchema, type CheckResult, type DiscountBase, type DiscountDef, DiscountDefSchema, type DiscountDuration, type DiscountDurationDef, DiscountDurationSchema, type DiscountType, DiscountTypeSchema, type DiscountValueDef, EntitlementEngine, type FeatureDef, type FeatureDefInput, FeatureDefInputSchema, type FeatureType, FeatureTypeSchema, OverageConfigurationSchema, type PlanDef, type PlanDefInput, PlanDefInputSchema, type PlanFeatureValue, PlanFeatureValueSchema, type PlanStatus, PlanStatusSchema, type PlanType, PlanTypeSchema, type PriceDef, PriceDefSchema, type ResetPeriod, ResetPeriodSchema, type RevstackConfig, RevstackConfigSchema, RevstackValidationError, type SubscriptionStatus, SubscriptionStatusSchema, type UnitType, UnitTypeSchema, defineAddon, defineConfig, defineDiscount, defineFeature, definePlan, validateConfig };
package/dist/index.js CHANGED
@@ -171,50 +171,43 @@ function validateFeatureReferences(productType, productSlug, features, knownFeat
171
171
  }
172
172
  }
173
173
  }
174
- function validatePricing(productType, slug, prices, configFeatures, errors) {
174
+ function validatePriceAddonIntervals(planSlug, priceIndex, price, configAddons, errors) {
175
+ if (!price.available_addons) return;
176
+ for (const addonSlug of price.available_addons) {
177
+ const addon = configAddons?.[addonSlug];
178
+ if (!addon) {
179
+ errors.push(
180
+ `Plan "${planSlug}" price references undefined addon "${addonSlug}".`
181
+ );
182
+ continue;
183
+ }
184
+ if (addon.type === "recurring" && addon.billing_interval !== price.billing_interval) {
185
+ errors.push(
186
+ `Interval Mismatch: Plan '${planSlug}' price is '${price.billing_interval}', but Addon '${addonSlug}' is '${addon.billing_interval}'. Recurring addons must match the price's billing interval.`
187
+ );
188
+ }
189
+ }
190
+ }
191
+ function validatePlanPricing(slug, prices, config, errors) {
192
+ const configFeatures = config.features;
175
193
  if (prices) {
176
- for (const price of prices) {
177
- if (price.amount < 0) {
178
- errors.push(
179
- `${productType} "${slug}" has a negative price amount (${price.amount}).`
180
- );
181
- }
194
+ prices.forEach((price, index) => {
182
195
  if (price.overage_configuration) {
183
- for (const [featureSlug, overage] of Object.entries(
184
- price.overage_configuration
185
- )) {
196
+ for (const featureSlug of Object.keys(price.overage_configuration)) {
186
197
  const feature = configFeatures[featureSlug];
187
198
  if (!feature) {
188
199
  errors.push(
189
- `${productType} "${slug}" overage_configuration references undefined feature "${featureSlug}".`
200
+ `Plan "${slug}" overage_configuration references undefined feature "${featureSlug}".`
190
201
  );
191
202
  } else if (feature.type !== "metered") {
192
203
  errors.push(
193
- `${productType} "${slug}" configures overage for feature "${featureSlug}", which is not of type 'metered'.`
194
- );
195
- }
196
- if (overage.overage_amount < 0) {
197
- errors.push(
198
- `${productType} "${slug}" overage_amount for feature "${featureSlug}" must be >= 0.`
199
- );
200
- }
201
- if (overage.overage_unit <= 0) {
202
- errors.push(
203
- `${productType} "${slug}" overage_unit for feature "${featureSlug}" must be > 0.`
204
+ `Plan "${slug}" configures overage for feature "${featureSlug}", which is not of type 'metered'.`
204
205
  );
205
206
  }
206
207
  }
207
208
  }
208
- }
209
- }
210
- }
211
- function validateFeatureLimits(productType, slug, features, errors) {
212
- for (const [featureSlug, value] of Object.entries(features)) {
213
- if (value.value_limit !== void 0 && value.value_limit < 0) {
214
- errors.push(
215
- `${productType} "${slug}" \u2192 feature "${featureSlug}" has a negative value_limit (${value.value_limit}).`
216
- );
217
- }
209
+ validatePriceAddonIntervals(slug, index, price, config.addons, errors);
210
+ });
218
211
  }
219
212
  }
220
213
  function validateDefaultPlan(config, errors) {
@@ -232,21 +225,6 @@ function validateDefaultPlan(config, errors) {
232
225
  );
233
226
  }
234
227
  }
235
- function validateDiscounts(config, errors) {
236
- if (!config.coupons) return;
237
- for (const coupon of config.coupons) {
238
- if (coupon.type === "percent" && (coupon.value < 0 || coupon.value > 100)) {
239
- errors.push(
240
- `Discount "${coupon.code}" has an invalid percentage value (${coupon.value}). Must be 0\u2013100.`
241
- );
242
- }
243
- if (coupon.type === "amount" && coupon.value < 0) {
244
- errors.push(
245
- `Discount "${coupon.code}" has a negative amount value (${coupon.value}).`
246
- );
247
- }
248
- }
249
- }
250
228
  function validateConfig(config) {
251
229
  const errors = [];
252
230
  const knownFeatureSlugs = new Set(Object.keys(config.features));
@@ -259,8 +237,7 @@ function validateConfig(config) {
259
237
  knownFeatureSlugs,
260
238
  errors
261
239
  );
262
- validatePricing("Plan", slug, plan.prices, config.features, errors);
263
- validateFeatureLimits("Plan", slug, plan.features, errors);
240
+ validatePlanPricing(slug, plan.prices, config, errors);
264
241
  }
265
242
  if (config.addons) {
266
243
  for (const [slug, addon] of Object.entries(config.addons)) {
@@ -271,18 +248,168 @@ function validateConfig(config) {
271
248
  knownFeatureSlugs,
272
249
  errors
273
250
  );
274
- validatePricing("Addon", slug, addon.prices, config.features, errors);
275
- validateFeatureLimits("Addon", slug, addon.features, errors);
276
251
  }
277
252
  }
278
- validateDiscounts(config, errors);
279
253
  if (errors.length > 0) {
280
254
  throw new RevstackValidationError(errors);
281
255
  }
282
256
  }
257
+
258
+ // src/schema.ts
259
+ import { z } from "zod";
260
+ var FeatureTypeSchema = z.enum(["boolean", "static", "metered"]);
261
+ var UnitTypeSchema = z.enum([
262
+ "count",
263
+ "bytes",
264
+ "seconds",
265
+ "tokens",
266
+ "requests",
267
+ "custom"
268
+ ]);
269
+ var ResetPeriodSchema = z.enum([
270
+ "daily",
271
+ "weekly",
272
+ "monthly",
273
+ "yearly",
274
+ "never"
275
+ ]);
276
+ var BillingIntervalSchema = z.enum([
277
+ "monthly",
278
+ "quarterly",
279
+ "yearly",
280
+ "one_time"
281
+ ]);
282
+ var PlanTypeSchema = z.enum(["paid", "free", "custom"]);
283
+ var PlanStatusSchema = z.enum(["draft", "active", "archived"]);
284
+ var SubscriptionStatusSchema = z.enum([
285
+ "active",
286
+ "trialing",
287
+ "past_due",
288
+ "canceled",
289
+ "paused"
290
+ ]);
291
+ var FeatureDefInputSchema = z.object({
292
+ name: z.string(),
293
+ description: z.string().optional(),
294
+ type: FeatureTypeSchema,
295
+ unit_type: UnitTypeSchema
296
+ });
297
+ var PlanFeatureValueSchema = z.object({
298
+ value_limit: z.number().min(0).optional(),
299
+ value_bool: z.boolean().optional(),
300
+ value_text: z.string().optional(),
301
+ is_hard_limit: z.boolean().optional(),
302
+ reset_period: ResetPeriodSchema.optional()
303
+ });
304
+ var AddonFeatureValueSchema = z.object({
305
+ value_limit: z.number().min(0).optional(),
306
+ type: z.enum(["increment", "set"]).optional(),
307
+ has_access: z.boolean().optional(),
308
+ is_hard_limit: z.boolean().optional()
309
+ });
310
+ var OverageConfigurationSchema = z.record(
311
+ z.string(),
312
+ z.object({
313
+ overage_amount: z.number().min(0),
314
+ overage_unit: z.number().min(1)
315
+ })
316
+ );
317
+ var PriceDefSchema = z.object({
318
+ amount: z.number().min(0),
319
+ currency: z.string(),
320
+ billing_interval: BillingIntervalSchema,
321
+ trial_period_days: z.number().min(0).optional(),
322
+ is_active: z.boolean().optional(),
323
+ overage_configuration: OverageConfigurationSchema.optional(),
324
+ available_addons: z.array(z.string()).optional()
325
+ });
326
+ var PlanDefInputSchema = z.object({
327
+ name: z.string(),
328
+ description: z.string().optional(),
329
+ is_default: z.boolean(),
330
+ is_public: z.boolean(),
331
+ type: PlanTypeSchema,
332
+ status: PlanStatusSchema.optional().default("active"),
333
+ prices: z.array(PriceDefSchema).optional(),
334
+ features: z.record(z.string(), PlanFeatureValueSchema)
335
+ });
336
+ var BaseAddonDefInput = z.object({
337
+ name: z.string(),
338
+ description: z.string().optional(),
339
+ amount: z.number().min(0),
340
+ currency: z.string(),
341
+ features: z.record(z.string(), AddonFeatureValueSchema)
342
+ });
343
+ var RecurringAddonSchema = BaseAddonDefInput.extend({
344
+ type: z.literal("recurring"),
345
+ billing_interval: z.enum(["monthly", "quarterly", "yearly"])
346
+ });
347
+ var OneTimeAddonSchema = BaseAddonDefInput.extend({
348
+ type: z.literal("one_time"),
349
+ // omitted/ignored for one_time
350
+ billing_interval: z.any().optional()
351
+ });
352
+ var AddonDefInputSchema = z.discriminatedUnion("type", [
353
+ RecurringAddonSchema,
354
+ OneTimeAddonSchema
355
+ ]);
356
+ var DiscountTypeSchema = z.enum(["percent", "amount"]);
357
+ var DiscountDurationSchema = z.enum(["once", "forever", "repeating"]);
358
+ var BaseDiscountDef = z.object({
359
+ code: z.string(),
360
+ name: z.string().optional(),
361
+ applies_to_plans: z.array(z.string()).optional(),
362
+ max_redemptions: z.number().min(1).optional(),
363
+ expires_at: z.string().datetime().optional()
364
+ });
365
+ var DiscountValueSchema = z.discriminatedUnion("type", [
366
+ z.object({ type: z.literal("percent"), value: z.number().min(0).max(100) }),
367
+ z.object({ type: z.literal("amount"), value: z.number().min(0) })
368
+ ]);
369
+ var DiscountDurationLogic = z.discriminatedUnion("duration", [
370
+ z.object({
371
+ duration: z.literal("once"),
372
+ duration_in_months: z.undefined().optional()
373
+ }),
374
+ z.object({
375
+ duration: z.literal("forever"),
376
+ duration_in_months: z.undefined().optional()
377
+ }),
378
+ z.object({
379
+ duration: z.literal("repeating"),
380
+ duration_in_months: z.number().min(1)
381
+ })
382
+ ]);
383
+ var DiscountDefSchema = BaseDiscountDef.and(DiscountValueSchema).and(
384
+ DiscountDurationLogic
385
+ );
386
+ var RevstackConfigSchema = z.object({
387
+ features: z.record(z.string(), FeatureDefInputSchema),
388
+ plans: z.record(z.string(), PlanDefInputSchema),
389
+ addons: z.record(z.string(), AddonDefInputSchema).optional(),
390
+ coupons: z.array(DiscountDefSchema).optional()
391
+ });
283
392
  export {
393
+ AddonDefInputSchema,
394
+ AddonFeatureValueSchema,
395
+ BillingIntervalSchema,
396
+ DiscountDefSchema,
397
+ DiscountDurationSchema,
398
+ DiscountTypeSchema,
284
399
  EntitlementEngine,
400
+ FeatureDefInputSchema,
401
+ FeatureTypeSchema,
402
+ OverageConfigurationSchema,
403
+ PlanDefInputSchema,
404
+ PlanFeatureValueSchema,
405
+ PlanStatusSchema,
406
+ PlanTypeSchema,
407
+ PriceDefSchema,
408
+ ResetPeriodSchema,
409
+ RevstackConfigSchema,
285
410
  RevstackValidationError,
411
+ SubscriptionStatusSchema,
412
+ UnitTypeSchema,
286
413
  defineAddon,
287
414
  defineConfig,
288
415
  defineDiscount,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@revstackhq/core",
3
- "version": "0.0.0-dev-20260227103607",
3
+ "version": "0.0.0-dev-20260228062053",
4
4
  "private": false,
5
5
  "license": "FSL-1.1-MIT",
6
6
  "type": "module",
@@ -31,6 +31,9 @@
31
31
  "publishConfig": {
32
32
  "access": "public"
33
33
  },
34
+ "dependencies": {
35
+ "zod": "^4.3.6"
36
+ },
34
37
  "scripts": {
35
38
  "build": "tsup",
36
39
  "dev": "tsup --watch",