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

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 +370 -109
  2. package/dist/index.js +172 -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
  }
@@ -249,49 +248,6 @@ interface RevstackConfig {
249
248
  coupons?: DiscountDef[];
250
249
  }
251
250
 
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
251
  /**
296
252
  * Define a feature with full type inference.
297
253
  * Identity function — returns the input as-is.
@@ -332,17 +288,8 @@ declare function definePlan<F extends Record<string, FeatureDefInput> = Record<s
332
288
  *
333
289
  * @typeParam F - Feature dictionary type for key restriction.
334
290
  */
335
- declare function defineAddon<F extends Record<string, FeatureDefInput> = Record<string, FeatureDefInput>>(config: Omit<AddonDefInput, "features" | "prices"> & {
291
+ declare function defineAddon<F extends Record<string, FeatureDefInput> = Record<string, FeatureDefInput>>(config: Omit<AddonDefInput, "features"> & {
336
292
  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
293
  }): AddonDefInput;
347
294
  /**
348
295
  * Define a discount/coupon with full type inference.
@@ -355,28 +302,6 @@ declare function defineDiscount<T extends DiscountDef>(config: T): T;
355
302
  */
356
303
  declare function defineConfig<T extends RevstackConfig>(config: T): T;
357
304
 
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
305
  /**
381
306
  * The Entitlement Engine — evaluates feature access for a single customer.
382
307
  *
@@ -420,24 +345,6 @@ declare class EntitlementEngine {
420
345
  checkBatch(usages: Record<string, number>): Record<string, CheckResult>;
421
346
  }
422
347
 
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
348
  /**
442
349
  * Thrown when `validateConfig()` detects one or more invalid business
443
350
  * logic rules in a billing configuration.
@@ -457,12 +364,366 @@ declare class RevstackValidationError extends Error {
457
364
  * Checks the following invariants:
458
365
  * 1. **Default plan** — Exactly one plan has `is_default: true`.
459
366
  * 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
367
  *
463
368
  * @param config - The billing configuration to validate.
464
369
  * @throws {RevstackValidationError} If any violations are found.
465
370
  */
466
371
  declare function validateConfig(config: RevstackConfig): void;
467
372
 
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 };
373
+ declare const FeatureTypeSchema: z.ZodEnum<{
374
+ boolean: "boolean";
375
+ static: "static";
376
+ metered: "metered";
377
+ }>;
378
+ declare const UnitTypeSchema: z.ZodEnum<{
379
+ count: "count";
380
+ bytes: "bytes";
381
+ seconds: "seconds";
382
+ tokens: "tokens";
383
+ requests: "requests";
384
+ custom: "custom";
385
+ }>;
386
+ declare const ResetPeriodSchema: z.ZodEnum<{
387
+ daily: "daily";
388
+ weekly: "weekly";
389
+ monthly: "monthly";
390
+ yearly: "yearly";
391
+ never: "never";
392
+ }>;
393
+ declare const BillingIntervalSchema: z.ZodEnum<{
394
+ monthly: "monthly";
395
+ yearly: "yearly";
396
+ quarterly: "quarterly";
397
+ one_time: "one_time";
398
+ }>;
399
+ declare const PlanTypeSchema: z.ZodEnum<{
400
+ custom: "custom";
401
+ paid: "paid";
402
+ free: "free";
403
+ }>;
404
+ declare const PlanStatusSchema: z.ZodEnum<{
405
+ draft: "draft";
406
+ active: "active";
407
+ archived: "archived";
408
+ }>;
409
+ declare const SubscriptionStatusSchema: z.ZodEnum<{
410
+ active: "active";
411
+ trialing: "trialing";
412
+ past_due: "past_due";
413
+ canceled: "canceled";
414
+ paused: "paused";
415
+ }>;
416
+ declare const FeatureDefInputSchema: z.ZodObject<{
417
+ name: z.ZodString;
418
+ description: z.ZodOptional<z.ZodString>;
419
+ type: z.ZodEnum<{
420
+ boolean: "boolean";
421
+ static: "static";
422
+ metered: "metered";
423
+ }>;
424
+ unit_type: z.ZodEnum<{
425
+ count: "count";
426
+ bytes: "bytes";
427
+ seconds: "seconds";
428
+ tokens: "tokens";
429
+ requests: "requests";
430
+ custom: "custom";
431
+ }>;
432
+ }, z.core.$strip>;
433
+ declare const PlanFeatureValueSchema: z.ZodObject<{
434
+ value_limit: z.ZodOptional<z.ZodNumber>;
435
+ value_bool: z.ZodOptional<z.ZodBoolean>;
436
+ value_text: z.ZodOptional<z.ZodString>;
437
+ is_hard_limit: z.ZodOptional<z.ZodBoolean>;
438
+ reset_period: z.ZodOptional<z.ZodEnum<{
439
+ daily: "daily";
440
+ weekly: "weekly";
441
+ monthly: "monthly";
442
+ yearly: "yearly";
443
+ never: "never";
444
+ }>>;
445
+ }, z.core.$strip>;
446
+ declare const AddonFeatureValueSchema: z.ZodObject<{
447
+ value_limit: z.ZodOptional<z.ZodNumber>;
448
+ type: z.ZodOptional<z.ZodEnum<{
449
+ increment: "increment";
450
+ set: "set";
451
+ }>>;
452
+ has_access: z.ZodOptional<z.ZodBoolean>;
453
+ is_hard_limit: z.ZodOptional<z.ZodBoolean>;
454
+ }, z.core.$strip>;
455
+ declare const OverageConfigurationSchema: z.ZodRecord<z.ZodString, z.ZodObject<{
456
+ overage_amount: z.ZodNumber;
457
+ overage_unit: z.ZodNumber;
458
+ }, z.core.$strip>>;
459
+ declare const PriceDefSchema: z.ZodObject<{
460
+ amount: z.ZodNumber;
461
+ currency: z.ZodString;
462
+ billing_interval: z.ZodEnum<{
463
+ monthly: "monthly";
464
+ yearly: "yearly";
465
+ quarterly: "quarterly";
466
+ one_time: "one_time";
467
+ }>;
468
+ trial_period_days: z.ZodOptional<z.ZodNumber>;
469
+ is_active: z.ZodOptional<z.ZodBoolean>;
470
+ overage_configuration: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
471
+ overage_amount: z.ZodNumber;
472
+ overage_unit: z.ZodNumber;
473
+ }, z.core.$strip>>>;
474
+ available_addons: z.ZodOptional<z.ZodArray<z.ZodString>>;
475
+ }, z.core.$strip>;
476
+ declare const PlanDefInputSchema: z.ZodObject<{
477
+ name: z.ZodString;
478
+ description: z.ZodOptional<z.ZodString>;
479
+ is_default: z.ZodBoolean;
480
+ is_public: z.ZodBoolean;
481
+ type: z.ZodEnum<{
482
+ custom: "custom";
483
+ paid: "paid";
484
+ free: "free";
485
+ }>;
486
+ status: z.ZodDefault<z.ZodOptional<z.ZodEnum<{
487
+ draft: "draft";
488
+ active: "active";
489
+ archived: "archived";
490
+ }>>>;
491
+ prices: z.ZodOptional<z.ZodArray<z.ZodObject<{
492
+ amount: z.ZodNumber;
493
+ currency: z.ZodString;
494
+ billing_interval: z.ZodEnum<{
495
+ monthly: "monthly";
496
+ yearly: "yearly";
497
+ quarterly: "quarterly";
498
+ one_time: "one_time";
499
+ }>;
500
+ trial_period_days: z.ZodOptional<z.ZodNumber>;
501
+ is_active: z.ZodOptional<z.ZodBoolean>;
502
+ overage_configuration: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
503
+ overage_amount: z.ZodNumber;
504
+ overage_unit: z.ZodNumber;
505
+ }, z.core.$strip>>>;
506
+ available_addons: z.ZodOptional<z.ZodArray<z.ZodString>>;
507
+ }, z.core.$strip>>>;
508
+ features: z.ZodRecord<z.ZodString, z.ZodObject<{
509
+ value_limit: z.ZodOptional<z.ZodNumber>;
510
+ value_bool: z.ZodOptional<z.ZodBoolean>;
511
+ value_text: z.ZodOptional<z.ZodString>;
512
+ is_hard_limit: z.ZodOptional<z.ZodBoolean>;
513
+ reset_period: z.ZodOptional<z.ZodEnum<{
514
+ daily: "daily";
515
+ weekly: "weekly";
516
+ monthly: "monthly";
517
+ yearly: "yearly";
518
+ never: "never";
519
+ }>>;
520
+ }, z.core.$strip>>;
521
+ }, z.core.$strip>;
522
+ declare const AddonDefInputSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
523
+ name: z.ZodString;
524
+ description: z.ZodOptional<z.ZodString>;
525
+ amount: z.ZodNumber;
526
+ currency: z.ZodString;
527
+ features: z.ZodRecord<z.ZodString, z.ZodObject<{
528
+ value_limit: z.ZodOptional<z.ZodNumber>;
529
+ type: z.ZodOptional<z.ZodEnum<{
530
+ increment: "increment";
531
+ set: "set";
532
+ }>>;
533
+ has_access: z.ZodOptional<z.ZodBoolean>;
534
+ is_hard_limit: z.ZodOptional<z.ZodBoolean>;
535
+ }, z.core.$strip>>;
536
+ type: z.ZodLiteral<"recurring">;
537
+ billing_interval: z.ZodEnum<{
538
+ monthly: "monthly";
539
+ yearly: "yearly";
540
+ quarterly: "quarterly";
541
+ }>;
542
+ }, z.core.$strip>, z.ZodObject<{
543
+ name: z.ZodString;
544
+ description: z.ZodOptional<z.ZodString>;
545
+ amount: z.ZodNumber;
546
+ currency: z.ZodString;
547
+ features: z.ZodRecord<z.ZodString, z.ZodObject<{
548
+ value_limit: z.ZodOptional<z.ZodNumber>;
549
+ type: z.ZodOptional<z.ZodEnum<{
550
+ increment: "increment";
551
+ set: "set";
552
+ }>>;
553
+ has_access: z.ZodOptional<z.ZodBoolean>;
554
+ is_hard_limit: z.ZodOptional<z.ZodBoolean>;
555
+ }, z.core.$strip>>;
556
+ type: z.ZodLiteral<"one_time">;
557
+ billing_interval: z.ZodOptional<z.ZodAny>;
558
+ }, z.core.$strip>], "type">;
559
+ declare const DiscountTypeSchema: z.ZodEnum<{
560
+ amount: "amount";
561
+ percent: "percent";
562
+ }>;
563
+ declare const DiscountDurationSchema: z.ZodEnum<{
564
+ once: "once";
565
+ forever: "forever";
566
+ repeating: "repeating";
567
+ }>;
568
+ declare const DiscountDefSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
569
+ code: z.ZodString;
570
+ name: z.ZodOptional<z.ZodString>;
571
+ duration: z.ZodEnum<{
572
+ once: "once";
573
+ forever: "forever";
574
+ repeating: "repeating";
575
+ }>;
576
+ duration_in_months: z.ZodOptional<z.ZodNumber>;
577
+ applies_to_plans: z.ZodOptional<z.ZodArray<z.ZodString>>;
578
+ max_redemptions: z.ZodOptional<z.ZodNumber>;
579
+ expires_at: z.ZodOptional<z.ZodString>;
580
+ type: z.ZodLiteral<"percent">;
581
+ value: z.ZodNumber;
582
+ }, z.core.$strip>, z.ZodObject<{
583
+ code: z.ZodString;
584
+ name: z.ZodOptional<z.ZodString>;
585
+ duration: z.ZodEnum<{
586
+ once: "once";
587
+ forever: "forever";
588
+ repeating: "repeating";
589
+ }>;
590
+ duration_in_months: z.ZodOptional<z.ZodNumber>;
591
+ applies_to_plans: z.ZodOptional<z.ZodArray<z.ZodString>>;
592
+ max_redemptions: z.ZodOptional<z.ZodNumber>;
593
+ expires_at: z.ZodOptional<z.ZodString>;
594
+ type: z.ZodLiteral<"amount">;
595
+ value: z.ZodNumber;
596
+ }, z.core.$strip>], "type">;
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.ZodDiscriminatedUnion<[z.ZodObject<{
699
+ code: z.ZodString;
700
+ name: z.ZodOptional<z.ZodString>;
701
+ duration: z.ZodEnum<{
702
+ once: "once";
703
+ forever: "forever";
704
+ repeating: "repeating";
705
+ }>;
706
+ duration_in_months: z.ZodOptional<z.ZodNumber>;
707
+ applies_to_plans: z.ZodOptional<z.ZodArray<z.ZodString>>;
708
+ max_redemptions: z.ZodOptional<z.ZodNumber>;
709
+ expires_at: z.ZodOptional<z.ZodString>;
710
+ type: z.ZodLiteral<"percent">;
711
+ value: z.ZodNumber;
712
+ }, z.core.$strip>, z.ZodObject<{
713
+ code: z.ZodString;
714
+ name: z.ZodOptional<z.ZodString>;
715
+ duration: z.ZodEnum<{
716
+ once: "once";
717
+ forever: "forever";
718
+ repeating: "repeating";
719
+ }>;
720
+ duration_in_months: z.ZodOptional<z.ZodNumber>;
721
+ applies_to_plans: z.ZodOptional<z.ZodArray<z.ZodString>>;
722
+ max_redemptions: z.ZodOptional<z.ZodNumber>;
723
+ expires_at: z.ZodOptional<z.ZodString>;
724
+ type: z.ZodLiteral<"amount">;
725
+ value: z.ZodNumber;
726
+ }, z.core.$strip>], "type">>>;
727
+ }, z.core.$strip>;
728
+
729
+ export { type AddonDef, type AddonDefInput, AddonDefInputSchema, type AddonFeatureValue, AddonFeatureValueSchema, type BillingInterval, BillingIntervalSchema, type CheckResult, type DiscountDef, DiscountDefSchema, type DiscountDuration, DiscountDurationSchema, type DiscountType, DiscountTypeSchema, 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,161 @@ 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
+ duration: DiscountDurationSchema,
362
+ duration_in_months: z.number().min(1).optional(),
363
+ applies_to_plans: z.array(z.string()).optional(),
364
+ max_redemptions: z.number().min(1).optional(),
365
+ expires_at: z.string().datetime().optional()
366
+ });
367
+ var PercentDiscountSchema = BaseDiscountDef.extend({
368
+ type: z.literal("percent"),
369
+ value: z.number().min(0).max(100)
370
+ });
371
+ var AmountDiscountSchema = BaseDiscountDef.extend({
372
+ type: z.literal("amount"),
373
+ value: z.number().min(0)
374
+ });
375
+ var DiscountDefSchema = z.discriminatedUnion("type", [
376
+ PercentDiscountSchema,
377
+ AmountDiscountSchema
378
+ ]);
379
+ var RevstackConfigSchema = z.object({
380
+ features: z.record(z.string(), FeatureDefInputSchema),
381
+ plans: z.record(z.string(), PlanDefInputSchema),
382
+ addons: z.record(z.string(), AddonDefInputSchema).optional(),
383
+ coupons: z.array(DiscountDefSchema).optional()
384
+ });
283
385
  export {
386
+ AddonDefInputSchema,
387
+ AddonFeatureValueSchema,
388
+ BillingIntervalSchema,
389
+ DiscountDefSchema,
390
+ DiscountDurationSchema,
391
+ DiscountTypeSchema,
284
392
  EntitlementEngine,
393
+ FeatureDefInputSchema,
394
+ FeatureTypeSchema,
395
+ OverageConfigurationSchema,
396
+ PlanDefInputSchema,
397
+ PlanFeatureValueSchema,
398
+ PlanStatusSchema,
399
+ PlanTypeSchema,
400
+ PriceDefSchema,
401
+ ResetPeriodSchema,
402
+ RevstackConfigSchema,
285
403
  RevstackValidationError,
404
+ SubscriptionStatusSchema,
405
+ UnitTypeSchema,
286
406
  defineAddon,
287
407
  defineConfig,
288
408
  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-20260228060138",
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",