/** * Type surface for `bot/payments`. Two layers: * * 1. **Config shapes** the consumer authors (`BotPaymentsConfig`). * 2. **Derived types** the consumer reads through `ctx.payments` * (`TierKey`, `AtLeastKey`, `ProductKey`) — narrowed from their * config so e.g. `atLeast('vip.5')` is a TS error when only 2 * rungs are declared. * * Internal types (the ledger records, the catalog) live below and aren't * exported through the package entrypoint. * * See `CLAUDE.md` in this folder for the design rationale. */ import type { Polyglot } from "../../say/index.js"; import type { LangSession } from "../lang.js"; /** * The only Telegram-supported subscription period as of 2026 is exactly * 2,592,000 seconds (30 days). Modeled as a literal so misconfigurations * fail at compile time. See `sendInvoice` / * `createInvoiceLink` docs at https://core.telegram.org/bots/api. */ export type SubscriptionPeriod = "30d"; /** Internal value Telegram expects on the wire (seconds). */ export declare const SUBSCRIPTION_PERIOD_SECONDS: Readonly>; /** * One rung of the VIP ladder. Position in the array is the rank (1-based * id at the call site: `vip.1`, `vip.2`, …). Insert a rung at position 2 * → existing subscribers on rung 2+ shift by one, plus a trivial storage * migration; no string-id churn. * * `name` is display-only Polyglot — renaming a rung is a config edit; the * call sites (`atLeast('vip.2')`) are positional and untouched. * * `grants.credits` is applied **on every renewal** of this rung (not just * the initial purchase). That's the canonical SaaS shape: "VIP gets 1000 * messages per month." */ export type VipRung = { readonly xtr: number; readonly period: SubscriptionPeriod; readonly name: Polyglot; readonly grants?: { readonly credits?: number; }; }; /** * A consumable credit pack the user buys one-shot to top up their * `session.pay.credits` balance. Like VIP rungs, packs are positionally * identified (`credits.1`, `credits.2`, …) so the call site doesn't carry * a name. Adding/removing packs shifts ids; storage is the charge log * (which records the exact productKey at purchase time), so historical * charges stay valid even if you remove a pack later. */ export type CreditsPack = { readonly xtr: number; readonly grants: { readonly credits: number; }; readonly name?: Polyglot; }; /** * Credits axis config. `unit` is the user-facing word for one credit * (the menu says e.g. "1234 mensajes"). `packs` is the catalogue of * top-up bundles surfaced in the menu and via `invoice('credits.N')`. */ export type CreditsConfig = { readonly unit: Polyglot; readonly packs: ReadonlyArray>; }; /** * Orthogonal one-shot unlock (a "perk"). Named via the user's own key in * the config (`perks: { voice_mode: { … } }`), surfaced as * `perks.voice_mode` everywhere. */ export type Perk = { readonly xtr: number; readonly name: Polyglot; }; export type PerksConfig = Readonly>>; export type LegalConfig = { readonly sellerName: string; readonly nif: string; /** * Optional terms-of-service URL. When omitted, the consent prompt's * 📖 Terms button is hidden — there's no Telegram-side standard ToS * to default to (unlike `privacyUrl`). */ readonly termsUrl?: string; /** * Optional privacy policy URL. When omitted, defaults to Telegram's * [Standard Bot Privacy Policy](https://telegram.org/privacy-tpa), * which covers what this library's plugins retain (session record, * waiver consent, charge log) under "data necessary to function". * Override only when your bot retains data beyond what the standard * covers. */ readonly privacyUrl?: string; }; /** * Art. 103(m) TRLGDCU waiver text. Versioned so a wording change forces * re-consent; the version string is stored alongside each charge for audit. * * The Polyglot covers every locale the bot supports — the plugin * validates this at construction. */ export type WaiverConfig = { readonly version: string; readonly text: Polyglot; }; /** * What the bot author passes to `botPayments({...})`. The generic `L` * carries the bot's Polyglot language union forward into every nested * `Polyglot`, so TS catches a missing locale in any sub-config. */ export type BotPaymentsConfig = { readonly paysupport: string; /** Where in THIS bot's UI the user manages charges (the `/paysupport` * bullet line). Menu command names are app-specific; the default * names the library's own `/settings → 💎 VIP → 📜 History` path. */ readonly paysupportHint?: Polyglot; readonly legal: LegalConfig; readonly waiver: WaiverConfig; readonly vip?: ReadonlyArray>; readonly credits?: CreditsConfig; readonly perks?: PerksConfig; }; type _Range1To = Acc["length"] extends N ? Acc["length"] : Acc["length"] | _Range1To; /** * Positional 1-based ids for a tuple. `[a,b,c]` → `1 | 2 | 3`. * * Empty tuple → `never` (no ids exist). */ export type Indices1> = V["length"] extends 0 ? never : V["length"] extends number ? _Range1To : never; /** * Tier ids derivable from a `vip` config array. Bare `'vip'` is also * accepted as "any rung" by `atLeast` (see `AtLeastKey`). */ export type VipPositionalKeys | undefined> = V extends ReadonlyArray ? `vip.${Indices1}` : never; export type CreditsPackKeys | undefined> = C extends { packs: infer P; } ? P extends ReadonlyArray ? `credits.${Indices1

}` : never : never; export type PerkKeys

| undefined> = P extends PerksConfig ? `perks.${keyof P & string}` : never; /** * Every product the user can purchase. Discriminated by prefix. Note * `perks.X` uses the **author-supplied key** verbatim (e.g. * `perks.voice_mode`) while `vip.N` / `credits.N` are positional. */ export type ProductKey> = VipPositionalKeys | CreditsPackKeys | PerkKeys; /** * What `ctx.payments.tier()` can return. `'free'` is the bottom rung; * `vip.N` follows the ladder. */ export type TierKey> = "free" | VipPositionalKeys; /** * What `ctx.payments.atLeast(...)` accepts. Same as `TierKey` minus * `'free'` (atLeast('free') is trivially always true and unhelpful as a * gate) plus the bare `'vip'` synonym for "any rung in the namespace". */ export type AtLeastKey> = "vip" | VipPositionalKeys; export type VipRungResolved = { readonly id: `vip.${number}`; readonly rank: number; readonly xtr: number; readonly periodSeconds: number; readonly name: Polyglot; readonly creditsGranted: number; }; export type CreditsPackResolved = { readonly id: `credits.${number}`; readonly xtr: number; readonly creditsGranted: number; readonly name?: Polyglot; }; export type PerkResolved = { readonly id: `perks.${string}`; readonly key: string; readonly xtr: number; readonly name: Polyglot; }; export type ProductCatalog = { readonly vip: ReadonlyArray; readonly creditsUnit: Polyglot | undefined; readonly creditsPacks: ReadonlyArray; readonly perks: ReadonlyArray; /** O(1) lookup by ProductKey. */ readonly byKey: ReadonlyMap; }; export type WaiverRecord = { readonly at: number; readonly version: string; readonly locale: string; }; export type VipState = { /** 1-based rank — index into the resolved vip ladder + 1. */ readonly rung: number; /** Original `telegram_payment_charge_id` of the active subscription. */ readonly chargeId: string; /** Telegram's `subscription_expiration_date` — unix seconds. */ readonly expiresAt: number; /** True after the user toggled cancel-renewal; access keeps until `expiresAt`. */ readonly canceled: boolean; }; export type PerkState = { readonly chargeId: string; readonly at: number; }; export type PaymentsSession = { waiver?: WaiverRecord; credits?: number; vip?: VipState; perks?: Record; }; /** * Structural narrow of the shared session record the payment handlers * read: just the `pay` slice plus the recipient's locale. Single-sourced * here so derive/callbacks/commands/plugin agree on the shape. */ export type SessionLike = { pay?: PaymentsSession; } & LangSession; export type PaysupportState = "none" | "opened" | "refunded"; export type ChargeRecord = { readonly chargeId: string; readonly userId: number; readonly productKey: string; readonly xtr: number; readonly receivedAt: number; /** Decoded invoice payload echoed back from Telegram. */ readonly payload: string; /** * Waiver snapshot at purchase time. Captured even if the user * already consented earlier — defensive forensics. */ readonly waiverSnapshot: WaiverRecord; /** * Telegram's `subscription_expiration_date` (unix seconds) if this * charge was a subscription purchase. Used to compute tier expiry. */ readonly subscriptionExpiresAt?: number; /** * Credits this charge grants at fulfillment / renewal. Denormalized * onto the record so derivation works even if the catalog config * later changes (rare, but the charge log must remain authoritative * regardless of source-code edits). Zero for products that don't * grant credits. */ readonly creditsGranted: number; /** * 1-based vip rung this charge subscribed to, denormalized for the * same reason as `creditsGranted`. Only set for `vip.*` charges. */ readonly vipRung?: number; /** * Perk key this charge unlocked, denormalized. Only set for * `perks.*` charges. */ readonly perkKey?: string; paysupportState: PaysupportState; refundedAt?: number | null; /** Set after `record()` claims this charge into a payout batch. */ payoutBatchId?: string | null; }; export type PayoutRecord = { readonly batchId: string; readonly fromMs: number; readonly toMs: number; readonly tonAmount: number; readonly eurAtReceipt: number; readonly recordedAt: number; /** Optional Spanish factura number issued for this payout. */ facturaNumber?: string; }; /** * Snapshot returned by `derive.ts` — pure function of the charge log * for a given user. The plugin writes this into `session.pay.*` so * runtime checks are O(1), but the log is always the truth. */ export type DerivedPaymentsState = { readonly credits: number; readonly vip: VipState | undefined; readonly perks: Record; }; /** * What `payments.onFulfilled(productKey, handler)` receives. Sync * signature on purpose — the plugin always answers the * `successful_payment` event itself; the handler does its own async work * fire-and-forget. */ export type FulfillmentEvent = { readonly productKey: string; readonly userId: number; readonly chargeId: string; readonly xtr: number; readonly receivedAt: number; }; /** * Emitted (fire-and-forget, like `onFulfilled`) when an admin approves * a refund and Telegram's `refundStarPayment` succeeds. The mirror of * `FulfillmentEvent`, so a revenue ledger can reverse the row it wrote. */ export type RefundEvent = { readonly productKey: string; readonly userId: number; readonly chargeId: string; readonly xtr: number; readonly refundedAt: number; }; /** * Thrown by `credits.consume(n)` when the balance is below `n`. * `tryConsume(n)` returns `false` instead. */ export declare class InsufficientCredits extends Error { readonly requested: number; readonly available: number; constructor(requested: number, available: number); } export {}; //# sourceMappingURL=types.d.ts.map