import { t as CacheAdapter } from "./cache.adapter-z0MMDFoK.js"; import { a as Coupon, c as CreateCouponInput, i as AppliedCoupon, l as UpdateCouponInput, n as PlanQueryOptions, t as DatabaseAdapter } from "./database.adapter-oL35G_XI.js"; import { C as SubscriptionsOptions, O as UsageStatus, S as SubscriptionsLogger, T as UpdatePlanInput, _ as PlanPrice, d as Invoice, f as InvoiceLineItem, g as Plan, i as CreatePlanInput, m as InvoiceWithDetails, r as CreateInvoiceInput, s as FeatureRegistry, u as FeatureValues, v as SubscriberType, w as UpdateInvoiceInput, x as SubscriptionWithPlan, y as Subscription } from "./types-3EaBC58c.js"; import { l as WebhookEvent, s as PaymentGatewayAdapter, t as CancelOptions } from "./payment.adapter-BOT9T6jQ.js"; import { t as PdfRenderer } from "./pdf-renderer-B-BQphqe.js"; import { i as SubscriptionInvoiceData } from "./invoice-utils-5kEz_8RQ.js"; //#region src/services/invoices.service.d.ts interface InvoicesServiceOptions { /** * Prefix for generated sequential invoice numbers * @default 'INV-' */ invoiceNumberPrefix?: string; /** * Prefix for generated credit note numbers * @default 'CN-' */ creditNotePrefix?: string; } interface InvoiceTotals { subtotal: number; taxAmount: number; total: number; /** * The uniform tax rate across all taxed line items, or undefined when * line items use different rates (or no tax at all). */ taxRate?: number; } declare class InvoicesService { private readonly db; private readonly invoiceNumberPrefix; private readonly creditNotePrefix; constructor(db: DatabaseAdapter, options?: InvoicesServiceOptions); /** * Get invoice by ID */ get(id: string): Promise; /** * Get invoice by ID with full subscription and plan details */ getWithDetails(id: string): Promise | null>; /** * Get all invoices for a subscription */ listBySubscription(subscriptionId: string): Promise; /** * Get all invoices for a subscriber (by subscriber ID) */ listBySubscriber(subscriberId: string): Promise; /** * Create a new invoice * * When line items are provided, `subtotal`/`taxAmount`/`total` are computed * from them (see {@link computeTotals}); explicit values in `data` win. * When the database adapter implements `invoices.nextInvoiceNumber`, a * sequential `invoiceNumber` is assigned automatically. */ create(data: CreateInvoiceInput): Promise; /** * Update an invoice */ update(id: string, data: UpdateInvoiceInput): Promise; /** * Compute subtotal, tax, and total from line items. * * Tax is exclusive by default: `tax = amount * taxRate / 100` is added on * top of the line item amount. When a line item carries the * `taxInclusive` flag, its `amount` already contains the tax and the tax * portion is extracted instead (`tax = amount - amount / (1 + rate)`). */ computeTotals(lineItems: InvoiceLineItem[], discountAmount?: number): InvoiceTotals; /** * Create a credit note for an invoice: a negative-amount invoice linked * to the original via `creditNoteOfId`. * * The original invoice is left untouched (its status is NOT changed) so * the credit note acts as a standalone settlement document. Credit notes * get their own number sequence (`creditNotePrefix`, default `CN-`). * * @throws InvoiceVoidError when the invoice is void, is itself a credit * note, or already has a credit note */ createCreditNote(invoiceId: string): Promise; /** * Void an invoice with safety checks. * * Only `draft`, `open`, and `uncollectible` invoices can be voided. * Voiding a `paid` invoice is rejected (create a credit note instead); * voiding an already-void invoice is rejected as well. * * @throws InvoiceVoidError when the invoice cannot be voided */ voidInvoice(id: string): Promise; /** * Mark invoice as paid */ markPaid(id: string): Promise; /** * Mark invoice as void * * Prefer {@link voidInvoice}, which refuses to void paid or already-void * invoices. This method performs the raw status update without checks. */ markVoid(id: string): Promise; /** * Mark invoice as uncollectible */ markUncollectible(id: string): Promise; /** * Finalize a draft invoice (make it open for payment) */ finalize(id: string): Promise; } //#endregion //#region src/services/coupons.service.d.ts interface CouponsServiceOptions { /** * Default currency for `fixed` coupons when none is provided * @default 'USD' */ defaultCurrency?: string; } declare class CouponsService { private readonly db; private readonly defaultCurrency; constructor(db: DatabaseAdapter, options?: CouponsServiceOptions); /** * Create a new coupon */ create(data: CreateCouponInput): Promise; /** * Get a coupon by its redemption code (case-insensitive) */ getByCode(code: string): Promise; /** * Update a coupon */ update(id: string, data: UpdateCouponInput): Promise; /** * Deactivate a coupon (it can no longer be redeemed) */ deactivate(id: string): Promise; /** * Validate a coupon code without redeeming it. * * @throws CouponNotFoundError when the code does not exist * @throws CouponInvalidError when the coupon is inactive, expired, or exhausted */ validate(code: string): Promise; /** * Compute the discount a coupon gives against an amount (e.g. a plan price) * without redeeming it. Pure calculation — no persistence side effects. * * @returns The discount amount (>= 0, never exceeding `amount`) */ computeDiscount(coupon: Coupon, amount: number): number; /** * Validate and redeem a coupon against an amount (e.g. a plan price). * Increments the coupon's redemption counter. * * @returns The coupon, the discount amount, and the final total */ apply(code: string, amount: number): Promise; /** * Coupon persistence is optional on the database adapter. When the * adapter does not implement it, fail with a helpful setup error instead * of an obscure "cannot read property of undefined". */ private couponsOrThrow; } //#endregion //#region src/services/permissions.service.d.ts interface PermissionsServiceOptions { /** * Cache TTL in seconds * @default 300 */ cacheTtlSeconds?: number; /** * Grace period in days for expired subscriptions * @default 0 */ gracePeriodDays?: number; } declare class PermissionsService { private readonly db; private readonly features; private readonly cache; private readonly cacheTtl; private readonly gracePeriodDays; constructor(db: DatabaseAdapter, features: TFeatures, cache?: CacheAdapter, options?: PermissionsServiceOptions); /** * Check if subscriber has access to a boolean feature */ can(subscriberId: string, feature: K): Promise; /** * Get current usage vs limit for a feature */ remaining(subscriberId: string, feature: K): Promise; /** * Check if subscriber can use more of a limited feature * * Always returns true for metered features — they never block on overage. */ canUse(subscriberId: string, feature: K, count?: number): Promise; /** * Increment usage counter (throws if limit exceeded) * * Uses increment-then-verify to avoid TOCTOU race conditions: * atomically increment first, then check if we exceeded the limit. * If exceeded, roll back the increment. */ use(subscriberId: string, feature: K, count?: number): Promise; /** * Decrement usage counter (e.g., when deleting a product) */ release(subscriberId: string, feature: K, count?: number): Promise; /** * Set usage to a specific value */ setUsage(subscriberId: string, feature: K, count: number): Promise; /** * Reset all usage for a subscriber (e.g., at period start) */ resetUsage(subscriberId: string, feature?: keyof TFeatures): Promise; /** * Get all feature values for subscriber's current plan */ getFeatures(subscriberId: string): Promise>; /** * Get the limit value for a specific feature */ getLimit(subscriberId: string, feature: K): Promise; /** * Get the rate value for a specific feature (e.g., transaction fee percentage) */ getRate(subscriberId: string, feature: K): Promise; /** * Assert a boolean feature is enabled (throws if not) */ assertCan(subscriberId: string, feature: K, planName?: string): Promise; /** * Assert usage is within limits (throws if exceeded) */ assertCanUse(subscriberId: string, feature: K, count?: number): Promise; /** * Get all usage stats for a subscriber */ getAllUsage(subscriberId: string): Promise>; private validateSubscriptionActive; } //#endregion //#region src/services/plans.service.d.ts interface PlansServiceOptions { /** * Default currency for new plans * @default 'USD' */ defaultCurrency?: string; /** * Cache TTL in seconds * @default 300 */ cacheTtlSeconds?: number; } declare class PlansService { private readonly db; private readonly features; private readonly cache; private readonly defaultCurrency; private readonly cacheTtl; constructor(db: DatabaseAdapter, features: TFeatures, cache?: CacheAdapter, options?: PlansServiceOptions); /** * List all plans */ list(options?: { activeOnly?: boolean; }): Promise[]>; /** * List all plans with advanced filtering for admin dashboard. * Does NOT use cache - always returns fresh data. */ listForAdmin(options?: PlanQueryOptions): Promise<{ plans: Plan[]; total: number; }>; /** * Get a plan by ID */ get(id: string): Promise>; /** * Create a new plan */ create(data: CreatePlanInput): Promise>; /** * Update an existing plan */ update(id: string, data: UpdatePlanInput): Promise>; /** * Delete a plan */ delete(id: string): Promise; /** * Duplicate a plan with optional overrides */ duplicate(id: string, overrides?: Partial>): Promise>; /** * Get the price of a plan in a specific currency. * * Returns the plan's canonical `price` when `currency` matches * `plan.currency`; otherwise looks up the additional `prices` price * points. Returns null when the plan has no price in that currency. * Currency matching is case-insensitive. */ getPrice(plan: Plan, currency: string): PlanPrice | null; /** * Get the resolved value of a specific feature for a plan */ getFeatureValue(plan: Plan, feature: K): TFeatures[K]['type'] extends 'boolean' ? boolean : number; /** * Get all resolved feature values for a plan */ getResolvedFeatures(plan: Plan): FeatureValues; /** * Compare features between two plans */ compareFeatures(planA: Plan, planB: Plan): Record; private invalidateListCaches; } //#endregion //#region src/services/subscriptions.service.d.ts /** * Dunning (failed payment recovery) configuration */ interface DunningOptions { /** * Days after the first payment failure at which a retry is attempted. * Each entry represents one retry attempt; when all are exhausted the * configured `action` is applied. * @default [1, 3, 5, 7] */ retryScheduleDays?: number[]; /** * Action to apply to the subscription once the retry schedule is exhausted. * - 'pause': set status to 'paused' (access revoked, can be resumed later) * - 'cancel': cancel the subscription immediately * - 'none': only track attempts, leave the subscription in 'past_due' * @default 'none' */ action?: "pause" | "cancel" | "none"; } interface SubscriptionsServiceOptions { /** * Default subscriber type * @default 'tenant' */ subscriberType?: SubscriberType; /** * Default trial period in days * @default 0 */ trialDays?: number; /** * Grace period in days after expiration * @default 0 */ gracePeriodDays?: number; /** * Cache TTL in seconds * @default 300 */ cacheTtlSeconds?: number; /** * Dunning (failed payment recovery) configuration. * Used by `processDunning` to retry past-due subscriptions and apply a * terminal action when retries are exhausted. */ dunning?: DunningOptions; /** * Optional logger */ logger?: SubscriptionsLogger; } /** * Result of a plan change operation */ interface ChangePlanResult { /** Updated subscription with new plan */ subscription: SubscriptionWithPlan; /** Whether payment was charged */ charged: boolean; /** Payment ID if charged */ paymentId?: string; /** Amount actually charged in smallest currency unit (e.g. halalas/cents). Only set when charged or skipPayment with a known amount. */ chargeAmount?: number; /** Whether payment is pending 3DS verification */ paymentPending?: boolean; /** 3DS verification URL if payment is pending */ verificationUrl?: string; } /** * Per-subscription outcome of a `processDunning` run */ interface DunningProcessResult { /** Subscriber that was processed */ subscriberId: string; /** Subscription that was processed */ subscriptionId: string; /** Total retry attempts recorded after this run */ attempts: number; /** Whether the configured retry schedule has been exhausted */ exhausted: boolean; /** Whether the subscription recovered (a retry charge succeeded) */ recovered: boolean; /** Terminal action applied when exhausted ('none' when nothing was applied) */ actionApplied: "pause" | "cancel" | "none"; } /** * Per-subscription outcome of an `executePendingChanges` run */ interface PendingChangeResult { /** Subscriber whose plan change was applied */ subscriberId: string; /** Subscription that was updated */ subscriptionId: string; /** Plan the subscription was on before the change */ previousPlanId: string; /** Plan that is now in effect */ newPlanId: string; } interface RenewSubscriptionOptions { /** Do not charge the saved payment token (payment handled elsewhere). */ skipPayment?: boolean; /** * Treat the renewal as already paid by an external flow (e.g. a `payment.paid` * webhook fired after the gateway charged the customer). When set, the renewal * invoice is created with `paid` status instead of `open`. Implies the caller * is responsible for the actual charge, so it is typically used together with * `skipPayment`. */ paidExternally?: boolean; /** Gateway payment/invoice ID to record on the renewal invoice. */ gatewayInvoiceId?: string; } /** * Preview of a plan change - shows what user will pay */ interface PlanChangePreview { /** Current plan details */ currentPlan: { id: string; name: string; price: number; currency: string; }; /** New plan details */ newPlan: { id: string; name: string; price: number; currency: string; }; /** Whether this is an upgrade (new plan is more expensive) */ isUpgrade: boolean; /** Whether this is a downgrade (new plan is cheaper) */ isDowngrade: boolean; /** Price difference (positive = upgrade, negative = downgrade) */ priceDifference: number; /** Days remaining in current period */ daysRemaining: number; /** Total days in current period */ totalDays: number; /** Proration ratio (0-1) */ prorationRatio: number; /** Amount to charge now (in smallest currency unit, e.g., halalas) */ amountDue: number; /** Amount in regular units (e.g., SAR) */ amountDueFormatted: number; /** Currency code */ currency: string; /** When the new plan will take effect */ effectiveDate: Date; /** Message describing the change */ message: string; } declare class SubscriptionsService { private readonly db; private readonly cache; private readonly payment; private readonly subscriberType; private readonly trialDays; private readonly gracePeriodDays; private readonly cacheTtl; private readonly dunningRetryScheduleDays; private readonly dunningAction; private readonly logger; constructor(db: DatabaseAdapter, cache?: CacheAdapter, payment?: PaymentGatewayAdapter, options?: SubscriptionsServiceOptions); /** * Get subscription for a subscriber */ get(subscriberId: string): Promise | null>; /** * Create a new subscription */ create(subscriberId: string, planId: string, options?: { trialDays?: number; gatewayCustomerId?: string; metadata?: Record; /** * Mark the initial invoice as already paid (payment was collected by an * external flow, e.g. a verified token charged on the frontend). Defaults * to `false`, in which case the initial invoice is created as `open`. */ paidExternally?: boolean; /** Gateway payment/invoice ID to record on the initial invoice. */ gatewayInvoiceId?: string; }): Promise; /** * Change subscription plan * * For upgrades (new plan is more expensive): * - If payment adapter supports chargePayment and there's a saved token, charges immediately * - Proration can be enabled to charge only the difference for remaining period * - Plan changes immediately on successful payment * * For downgrades (new plan is cheaper): * - Plan change is scheduled for end of current period * - No immediate payment required * * @param subscriberId - The subscriber ID * @param newPlanId - The new plan to switch to * @param options - Change options * @returns Updated subscription */ changePlan(subscriberId: string, newPlanId: string, options?: { /** Apply change immediately (default: true for upgrades, false for downgrades) */ immediately?: boolean; /** Prorate the charge based on remaining period (default: true) */ prorate?: boolean; /** Custom token ID to charge (overrides saved token) */ tokenId?: string; /** Callback URL for 3DS verification */ callbackUrl?: string; /** Skip payment even for upgrades (use with caution) */ skipPayment?: boolean; /** Verified token ID from frontend 3DS - save for future renewals */ verifiedTokenId?: string; /** * Gateway payment/invoice ID for a payment already collected by an * external flow (e.g. frontend 3DS). Recorded on the upgrade invoice so * the service stays the single source of truth for invoicing. */ gatewayInvoiceId?: string; }): Promise>; /** * Preview a plan change - calculates what user will pay without making any changes. * Use this to show the user the cost before they confirm the plan change. * * @param subscriberId - The subscriber ID * @param newPlanId - The new plan to preview * @returns Preview with proration details */ previewChangePlan(subscriberId: string, newPlanId: string): Promise; /** * Cancel subscription */ cancel(subscriberId: string, options?: CancelOptions): Promise; /** * Pause subscription (if supported by payment gateway). * * Records `pausedAt` in metadata so `resume()` can extend the current * period by the time spent paused (paid time is not lost). */ pause(subscriberId: string): Promise; /** * Resume paused subscription. * * Restores the 'active' status and extends `currentPeriodEnd` by the time * the subscription spent paused (recorded as `pausedAt` by `pause()`), so * subscribers keep the paid time they missed. When no pause timestamp is * available (e.g. paused externally) and the period already expired, a * fresh period is started instead. */ resume(subscriberId: string): Promise; /** * Reactivate a subscription that was scheduled for cancellation. * This clears cancelAt/canceledAt without changing the subscription period. */ reactivate(subscriberId: string): Promise; /** * Start a trial subscription */ startTrial(subscriberId: string, planId: string, trialDays: number): Promise; /** * Extend trial period */ extendTrial(subscriberId: string, additionalDays: number): Promise; /** * Check if subscription is active (including trial and grace period) */ isActive(subscriberId: string): Promise; /** * Check if subscription is in trial */ isTrialing(subscriberId: string): Promise; /** * Get days remaining in current period */ daysRemaining(subscriberId: string): Promise; /** * Renew subscription for a new period */ renew(subscriberId: string, options?: RenewSubscriptionOptions): Promise; /** * Record a payment failure against the subscriber's subscription. * * Stores the failure reason/timestamp in metadata and invalidates the cache * so subsequent reads are consistent. Also initializes the dunning tracking * fields (`dunningStartedAt`, `dunningAttempts`) used by `processDunning` — * existing dunning state is preserved so repeated failures within the same * dunning cycle don't reset the retry clock. Returns the updated * subscription, or `null` when the subscriber has no subscription. */ recordPaymentFailure(subscriberId: string, message: string): Promise; /** * Process dunning for all past-due subscriptions. * * Intended to be called on a schedule (e.g. daily cron). For each * `past_due` subscription it: * 1. Computes how many retries from `dunning.retryScheduleDays` are due, * based on when the failure was first recorded (`dunningStartedAt`, * initialized by `recordPaymentFailure`). * 2. When a retry is due and the gateway supports direct charges, attempts * to charge the saved payment method; a successful charge renews the * subscription and clears the dunning state. * 3. Otherwise increments `metadata.dunningAttempts`. * 4. Once the schedule is exhausted, applies the configured * `dunning.action` ('pause' or 'cancel') via the regular pause()/cancel() * paths so caches stay consistent. * * @param now - Reference time (defaults to the current time; injectable for testing) * @returns One result per past-due subscription processed */ processDunning(now?: Date): Promise; /** * Apply pending plan changes (scheduled downgrades) that have reached their * effective date. * * Downgrades requested via `changePlan` are stored as * `metadata.pendingDowngradePlanId` and take effect at the end of the * current period. This entry point finds subscriptions whose period has * ended and applies the pending plan change without charging (the next * `renew` call bills the new plan). * * @param now - Reference time (defaults to the current time; injectable for testing) * @returns One result per plan change applied */ executePendingChanges(now?: Date): Promise; /** * Compute the amount to charge for an upgrade, in the smallest currency unit. * * Centralizes the proration math so the charge, the skip-payment recompute, * and any future caller stay in sync. Guards against a zero-length period * (which would otherwise produce NaN/Infinity from a divide-by-zero). */ private computeUpgradeChargeMinorUnits; /** * Apply the configured terminal dunning action to a subscriber whose retry * schedule is exhausted. Reuses the regular pause()/cancel() paths so * status, metadata, and caches stay consistent. Failures are logged and * reported as 'none' so one bad subscription doesn't abort the whole run. */ private applyDunningAction; /** * Attempt a single dunning retry charge against the subscriber's saved * payment method. On success the subscription is renewed (payment already * collected) and the dunning metadata is cleared. Returns whether the * subscription recovered. Degrades gracefully to `false` when the gateway * cannot charge directly or no payment method is stored. */ private attemptDunningCharge; private clearPendingSubscriptionMetadata; private getOrThrow; private isActiveStatus; private finalizeEndedSubscriptionIfNeeded; private isSubscriptionActive; private calculatePeriodEnd; } //#endregion //#region src/core/errors.d.ts /** * Base error class for all subscription-related errors */ declare class SubscriptionError extends Error { readonly code: string; readonly statusCode: number; constructor(message: string, code: string, statusCode?: number); } /** * Plan not found error */ declare class PlanNotFoundError extends SubscriptionError { constructor(planId: string); } /** * Subscription not found for subscriber */ declare class SubscriptionNotFoundError extends SubscriptionError { constructor(subscriberId: string); } /** * Feature not available on current plan */ declare class FeatureNotAllowedError extends SubscriptionError { readonly feature: string; constructor(feature: string, planName?: string); } /** * Usage limit exceeded for feature */ declare class UsageLimitExceededError extends SubscriptionError { readonly feature: string; readonly limit: number; readonly used: number; constructor(feature: string, limit: number, used: number); } /** * Subscription has expired */ declare class SubscriptionExpiredError extends SubscriptionError { constructor(); } /** * Subscription is inactive (canceled, paused, etc.) */ declare class SubscriptionInactiveError extends SubscriptionError { constructor(status: string); } /** * Payment gateway error */ declare class PaymentGatewayError extends SubscriptionError { readonly gatewayError?: unknown | undefined; constructor(message: string, gatewayError?: unknown | undefined); } /** * Invalid plan configuration */ declare class InvalidPlanError extends SubscriptionError { constructor(message: string); } /** * Duplicate subscription - subscriber already has an active subscription */ declare class DuplicateSubscriptionError extends SubscriptionError { constructor(subscriberId: string); } /** * Subscription is not scheduled for cancellation */ declare class SubscriptionNotCanceledError extends SubscriptionError { constructor(); } /** * Payment failed during subscription operation */ declare class PaymentFailedError extends SubscriptionError { readonly paymentId?: string | undefined; readonly errorCode?: string | undefined; readonly isRetryable?: boolean | undefined; readonly userAction?: string | undefined; constructor(message: string, paymentId?: string | undefined, errorCode?: string | undefined, isRetryable?: boolean | undefined, userAction?: string | undefined); } /** * Coupon not found by code or ID */ declare class CouponNotFoundError extends SubscriptionError { readonly couponCode: string; constructor(couponCode: string); } /** * Coupon cannot be redeemed (expired, exhausted, or inactive) */ declare class CouponInvalidError extends SubscriptionError { readonly couponCode: string; readonly reason: 'expired' | 'exhausted' | 'inactive'; constructor(couponCode: string, reason: 'expired' | 'exhausted' | 'inactive'); } /** * Invoice cannot be voided (or a credit note cannot be created for it) */ declare class InvoiceVoidError extends SubscriptionError { constructor(message: string); } //#endregion //#region src/core/features.d.ts /** * Define a type-safe feature registry for your application. * * @example * ```typescript * const features = defineFeatures({ * analytics: { type: 'boolean', default: false }, * maxProducts: { type: 'limit', default: 100 }, * transactionFeePercent: { type: 'rate', default: 2.5 }, * }); * ``` */ declare function defineFeatures(features: T): T; /** * Resolve feature values by merging plan features with defaults */ declare function resolveFeatures(registry: T, planFeatures: Partial> | null | undefined): FeatureValues; /** * Validate that plan features match the registry definitions */ declare function validatePlanFeatures(registry: T, features: Partial>): { valid: boolean; errors: string[]; }; //#endregion //#region src/core/events.d.ts /** * Well-known event names emitted by {@link withEvents}. * * The {@link SubscriptionEvent.type} field is a plain `string` so custom * adapters may emit their own event names; this union documents the names the * SDK itself produces. */ type SubscriptionEventType = 'subscription.created' | 'subscription.renewed' | 'subscription.canceled' | 'subscription.plan_changed' | 'subscription.payment_failed' | 'usage.limit_reached' | 'invoice.paid' | 'plan.created' | 'plan.updated' | 'plan.deleted'; /** * A single domain event emitted after a successful mutating operation. */ interface SubscriptionEvent { /** * Unique event id (UUID v4) */ id: string; /** * Event name (see {@link SubscriptionEventType} for well-known names) */ type: string; /** * When the event occurred */ occurredAt: Date; /** * Subscriber the event relates to, when applicable */ subscriberId?: string; /** * Event payload */ data: Record; } /** * Sink for subscription events. * * Implementations may deliver events synchronously or asynchronously; a * rejected promise or thrown error is treated as an emit failure (and, inside * {@link withEvents}, is swallowed so it never breaks the underlying * operation). */ interface EventsAdapter { emit(event: SubscriptionEvent): Promise | void; } /** * No-op events adapter that silently discards all events. */ declare const noopEventsAdapter: EventsAdapter; /** * Options for {@link withEvents} */ interface WithEventsOptions { /** * Called when emitting an event fails. Defaults to silently swallowing the * error so event delivery never breaks billing operations. */ onError?: (error: unknown, event: SubscriptionEvent) => void; /** * Payment provider name used by the wrapped `handleWebhookRequest`. * * When provided, `handleWebhookRequest` is re-implemented on top of the * wrapped `handleWebhook` (so Request-based webhooks also emit events), * mirroring the signature-header extraction of the original instance. * When omitted, the original `handleWebhookRequest` is passed through * unchanged and Request-based webhooks do NOT emit events. */ webhookProvider?: string; /** * Header carrying the webhook signature for the wrapped * `handleWebhookRequest`. Only used together with `webhookProvider`. * @default 'x-signature' */ webhookSignatureHeader?: string; } /** * Wrap a subscriptions instance so its key mutating operations emit events. * * The returned instance is a shallow copy: the original instance and its * services are left untouched. Events are emitted only after an operation * completes successfully — never when it throws — and event delivery failures * never break the operation itself. * * Emitted events: * - `plan.created` / `plan.updated` / `plan.deleted` — plans service * - `subscription.created` / `subscription.canceled` / `subscription.renewed` / * `subscription.plan_changed` / `subscription.payment_failed` — subscriptions service * - `usage.limit_reached` — when `permissions.use()` consumes the last unit of a limited feature * - `invoice.paid` — `invoices.markPaid()` and paid webhook events * * @example * ```typescript * const subs = withEvents(createSubscriptions(config), { * emit: (event) => queue.publish(event.type, event), * }); * ``` */ declare function withEvents(subs: Subscriptions, adapter: EventsAdapter, options?: WithEventsOptions): Subscriptions; //#endregion //#region src/adapters/outbox.adapter.d.ts /** * A persisted event waiting to be relayed. * * Identical in shape to {@link SubscriptionEvent}; stores may add their own * bookkeeping columns (status, attempts, ...) on top of it. */ type OutboxRecord = SubscriptionEvent; /** * Minimal persistence contract for the transactional outbox pattern. * * Implementations typically write to the same database (and ideally the same * transaction) as the billing operation that produced the event, so events are * never lost even if the process crashes before delivery. * * Only `insert` is required — an outbox without `claimPending`/`markSent` can * still record events for external relay (e.g. a CDC pipeline or a cron worker * querying the table directly). */ interface OutboxStore { /** * Persist an event for later relay. */ insert(record: OutboxRecord): Promise; /** * Claim up to `limit` pending events for relay. * * Implementations should mark claimed rows so concurrent relayers do not * pick up the same events (e.g. `FOR UPDATE SKIP LOCKED` in Postgres). */ claimPending?(limit?: number): Promise; /** * Mark a previously claimed event as successfully delivered. */ markSent?(id: string): Promise; } /** * Create an {@link EventsAdapter} that persists every event to an * {@link OutboxStore} for later relay instead of delivering it immediately. * * Combine with {@link withEvents} and {@link relayOutbox}: * * @example * ```typescript * const subs = withEvents(createSubscriptions(config), createOutboxEventsAdapter(store)); * * // Later, in a cron job / queue worker: * await relayOutbox(store, { emit: (e) => queue.publish(e.type, e) }); * ``` */ declare function createOutboxEventsAdapter(store: OutboxStore): EventsAdapter; /** * Options for {@link relayOutbox} */ interface RelayOutboxOptions { /** * Maximum number of events to claim and relay in this run * @default 100 */ limit?: number; /** * Called when relaying an individual event fails. The event is left * pending so a later relay run can retry it. Defaults to silently * skipping the failed event. */ onError?: (error: unknown, record: OutboxRecord) => void; } /** * Result of a {@link relayOutbox} run */ interface RelayOutboxResult { /** * Number of events claimed from the store */ claimed: number; /** * Number of events successfully delivered to the target */ sent: number; /** * Number of events that failed delivery (left pending for retry) */ failed: number; } /** * Claim pending events from an outbox store and re-emit them to another * {@link EventsAdapter}, marking each as sent on success. * * Events whose delivery fails are left pending (and never marked sent) so a * later relay run retries them. This provides at-least-once delivery: the * target adapter must tolerate duplicate events. * * @throws If the store does not implement `claimPending` */ declare function relayOutbox(store: OutboxStore, target: EventsAdapter, options?: RelayOutboxOptions): Promise; //#endregion //#region src/integrations/http.d.ts /** * Resolve subscriber display info for invoice rendering */ type SubscriberInfoResolver = (subscriberId: string) => Promise; /** * Options for the web-standard HTTP handler */ interface SubscriptionsHttpOptions { /** * Route prefix for subscription endpoints. * The handler locates this prefix anywhere in the request path, so the * handler can be mounted under any base path (e.g. `/api` in Next.js or a * Hono sub-router) as long as the prefix itself appears in the URL. * @default '/subscriptions' */ prefix?: string; /** * Resolve the subscriber ID for the incoming request. * Defaults to reading the `x-subscriber-id` header. */ getSubscriberId?: (request: Request) => string | null | undefined | Promise; /** * Header carrying the webhook signature for `POST /webhooks/:provider`. * Falls back to common gateway headers (`x-moyasar-signature`, * `stripe-signature`, `x-webhook-signature`) when absent. * @default 'x-signature' */ webhookSignatureHeader?: string; /** * Invoice configuration for `GET /invoices/:id` * * The route supports two output modes via the `format` query param: * - `format=html` (default) — rendered invoice HTML; add `print=1` to get a * print-ready page (toolbar + print CSS) and `autoPrint=1` to open the * browser print dialog on load (client-side "Save as PDF", zero server * cost). * - `format=pdf` — server-generated PDF via the configured `pdfRenderer`. */ invoice?: { /** * Path to a custom Handlebars template (Node.js filesystem only). * Prefer `templateSource` on non-Node runtimes. Defaults to the built-in * inlined template, which needs no filesystem at all. */ templatePath?: string; /** * Inline Handlebars template source. Runtime-agnostic alternative to * `templatePath`; takes precedence when both are set. */ templateSource?: string; /** * PDF renderer for `format=pdf` (e.g. `puppeteerPdfRenderer` on Node, * `cloudflarePdfRenderer` on Workers). Without it, `format=pdf` * returns 501 — use `format=html&print=1` for client-side printing. */ pdfRenderer?: PdfRenderer; /** * Platform information for the invoice header */ platform?: SubscriptionInvoiceData["platform"]; /** * Resolve subscriber display info for the invoice */ getSubscriberInfo?: SubscriberInfoResolver; /** * Locale for date/currency formatting * @default 'ar-EG' */ locale?: string; /** * Custom HTML renderer. When provided, the built-in Handlebars renderer * is never invoked. Takes precedence over `templatePath`/`templateSource`. */ renderHtml?: (data: SubscriptionInvoiceData) => Promise; }; } /** * Web-standard handler returned by {@link createSubscriptionsHttpHandler} */ interface SubscriptionsHttpHandler { fetch(request: Request): Promise; } /** * Create a framework-neutral HTTP handler exposing the subscription API over * web-standard Request/Response. Mount it from any framework (Hono, Next.js * route handlers, a plain `fetch` listener, etc.). * * Routes (under `options.prefix`, default `/subscriptions`): * - `GET /plans` — list active plans * - `GET /current` — current subscription for the subscriber * - `POST /subscribe` — `{ planId, trialDays?, verifiedTokenId?, paymentId? }` * - `POST /cancel` — `{ immediately?, reason? }` * - `POST /change-plan` — `{ planId, ... }` * - `GET /usage` — usage for all limit features * - `GET /invoices` — invoices for the subscriber * - `GET /invoices/:id` — invoice as HTML (`?print=1` / `?autoPrint=1` * for client-side printing) or PDF (`?format=pdf`, * requires `invoice.pdfRenderer`) * - `POST /webhooks/:provider` — payment gateway webhooks * * @example * ```typescript * const handler = createSubscriptionsHttpHandler(subs, { * getSubscriberId: (req) => req.headers.get('x-tenant-id'), * }); * * // Cloudflare Workers / plain fetch * export default { fetch: handler.fetch }; * ``` */ declare function createSubscriptionsHttpHandler(subs: Subscriptions, options?: SubscriptionsHttpOptions): SubscriptionsHttpHandler; //#endregion //#region src/index.d.ts /** * Webhook-related options accepted in addition to {@link SubscriptionsOptions}. * These configure how {@link Subscriptions.handleWebhookRequest} translates * web-standard webhook Requests into provider webhook handling. */ interface SubscriptionsWebhookOptions { /** * Header carrying the webhook signature. * Falls back to common gateway headers (`x-moyasar-signature`, * `stripe-signature`, `x-webhook-signature`) when the configured header * is absent. * @default 'x-signature' */ webhookSignatureHeader?: string; /** * Payment provider name to handle webhooks for * @default the configured payment adapter's provider */ webhookProvider?: string; } /** * Dunning (failed-payment retry) options accepted in addition to * {@link SubscriptionsOptions}. Driven externally via * `subscriptions.processDunning()` (e.g. from a cron / scheduled handler). */ interface SubscriptionsDunningOptions { /** * Dunning configuration for failed subscription payments. * @default { retryScheduleDays: [1, 3, 5, 7], action: 'none' } */ dunning?: DunningOptions; } /** * Configuration for creating a subscriptions instance */ interface SubscriptionsConfig { /** * Database adapter (required) */ database: DatabaseAdapter; /** * Feature registry defining available features */ features: TFeatures; /** * Cache adapter (optional, improves performance) */ cache?: CacheAdapter; /** * Payment gateway adapter (optional, for Stripe/Paddle integration) */ payment?: PaymentGatewayAdapter; /** * Additional options */ options?: SubscriptionsOptions & SubscriptionsWebhookOptions & SubscriptionsDunningOptions; } /** * Main subscriptions instance with all services */ interface Subscriptions { /** * Plans service for managing subscription plans */ plans: PlansService; /** * Subscriptions service for managing subscriber subscriptions */ subscriptions: SubscriptionsService; /** * Permissions service for feature gates and usage tracking */ permissions: PermissionsService; /** * Invoices service for invoice management */ invoices: InvoicesService; /** * Coupons service for discount codes */ coupons: CouponsService; /** * Check if subscriber has access to a boolean feature * Shorthand for `permissions.can()` */ can(subscriberId: string, feature: K): Promise; /** * Get current usage vs limit for a feature * Shorthand for `permissions.remaining()` */ remaining(subscriberId: string, feature: K): Promise; /** * Increment usage counter (throws if limit exceeded) * Shorthand for `permissions.use()` */ use(subscriberId: string, feature: K, count?: number): Promise; /** * Decrement usage counter * Shorthand for `permissions.release()` */ release(subscriberId: string, feature: K, count?: number): Promise; /** * Handle webhook event from payment gateway */ handleWebhook(provider: string, payload: string | Uint8Array, signature: string): Promise; /** * Handle an incoming webhook as a web-standard `Request` and produce a * web-standard `Response`. Reads the raw body, extracts the signature from * the configured header (falling back to common gateway headers), and * delegates to {@link Subscriptions.handleWebhook}. * * Returns 200 JSON on success, 400 on signature verification failure, * and 500 for any other error. */ handleWebhookRequest(request: Request): Promise; } /** * Create a subscriptions instance with all services configured * * @example * ```typescript * import { createSubscriptions, defineFeatures } from '@abshahin/subscriptions'; * import { prismaAdapter } from '@abshahin/subscriptions/adapters/prisma'; * * const features = defineFeatures({ * analytics: { type: 'boolean', default: false }, * maxProducts: { type: 'limit', default: 100 }, * }); * * const subs = createSubscriptions({ * database: prismaAdapter(db), * features, * }); * * // Usage * if (await subs.can(tenantId, 'analytics')) { * // Show analytics * } * ``` */ declare function createSubscriptions(config: SubscriptionsConfig): Subscriptions; //#endregion export { InvoiceVoidError as A, ChangePlanResult as B, resolveFeatures as C, DuplicateSubscriptionError as D, CouponNotFoundError as E, SubscriptionExpiredError as F, SubscriptionsService as G, DunningProcessResult as H, SubscriptionInactiveError as I, CouponsService as J, PlansService as K, SubscriptionNotCanceledError as L, PaymentGatewayError as M, PlanNotFoundError as N, FeatureNotAllowedError as O, SubscriptionError as P, SubscriptionNotFoundError as R, defineFeatures as S, CouponInvalidError as T, PendingChangeResult as U, DunningOptions as V, PlanChangePreview as W, InvoicesService as X, CouponsServiceOptions as Y, InvoicesServiceOptions as Z, SubscriptionEvent as _, createSubscriptions as a, noopEventsAdapter as b, SubscriptionsHttpOptions as c, OutboxStore as d, RelayOutboxOptions as f, EventsAdapter as g, relayOutbox as h, SubscriptionsWebhookOptions as i, PaymentFailedError as j, InvalidPlanError as k, createSubscriptionsHttpHandler as l, createOutboxEventsAdapter as m, SubscriptionsConfig as n, SubscriberInfoResolver as o, RelayOutboxResult as p, PermissionsService as q, SubscriptionsDunningOptions as r, SubscriptionsHttpHandler as s, Subscriptions as t, OutboxRecord as u, SubscriptionEventType as v, validatePlanFeatures as w, withEvents as x, WithEventsOptions as y, UsageLimitExceededError as z }; //# sourceMappingURL=index-gRqfQZ6n.d.ts.map