@adriangalilea/utils 0.18.0 → 0.19.0

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 (97) hide show
  1. package/README.md +2 -1
  2. package/dist/bot/access-control.d.ts.map +1 -1
  3. package/dist/bot/access-control.js +17 -1
  4. package/dist/bot/access-control.js.map +1 -1
  5. package/dist/bot/callbacks.d.ts +89 -0
  6. package/dist/bot/callbacks.d.ts.map +1 -0
  7. package/dist/bot/callbacks.js +128 -0
  8. package/dist/bot/callbacks.js.map +1 -0
  9. package/dist/bot/ctx.d.ts +219 -0
  10. package/dist/bot/ctx.d.ts.map +1 -0
  11. package/dist/bot/ctx.js +52 -0
  12. package/dist/bot/ctx.js.map +1 -0
  13. package/dist/bot/index.d.ts +4 -0
  14. package/dist/bot/index.d.ts.map +1 -1
  15. package/dist/bot/index.js +4 -0
  16. package/dist/bot/index.js.map +1 -1
  17. package/dist/bot/kit.d.ts +1 -1
  18. package/dist/bot/kit.d.ts.map +1 -1
  19. package/dist/bot/kit.js +4 -2
  20. package/dist/bot/kit.js.map +1 -1
  21. package/dist/bot/menu.d.ts +23 -1
  22. package/dist/bot/menu.d.ts.map +1 -1
  23. package/dist/bot/menu.js +30 -1
  24. package/dist/bot/menu.js.map +1 -1
  25. package/dist/bot/payments/callbacks.d.ts +28 -0
  26. package/dist/bot/payments/callbacks.d.ts.map +1 -0
  27. package/dist/bot/payments/callbacks.js +102 -0
  28. package/dist/bot/payments/callbacks.js.map +1 -0
  29. package/dist/bot/payments/commands.d.ts +26 -0
  30. package/dist/bot/payments/commands.d.ts.map +1 -0
  31. package/dist/bot/payments/commands.js +41 -0
  32. package/dist/bot/payments/commands.js.map +1 -0
  33. package/dist/bot/payments/config.d.ts +30 -0
  34. package/dist/bot/payments/config.d.ts.map +1 -0
  35. package/dist/bot/payments/config.js +166 -0
  36. package/dist/bot/payments/config.js.map +1 -0
  37. package/dist/bot/payments/derive.d.ts +75 -0
  38. package/dist/bot/payments/derive.d.ts.map +1 -0
  39. package/dist/bot/payments/derive.js +151 -0
  40. package/dist/bot/payments/derive.js.map +1 -0
  41. package/dist/bot/payments/handlers.d.ts +113 -0
  42. package/dist/bot/payments/handlers.d.ts.map +1 -0
  43. package/dist/bot/payments/handlers.js +284 -0
  44. package/dist/bot/payments/handlers.js.map +1 -0
  45. package/dist/bot/payments/index.d.ts +17 -0
  46. package/dist/bot/payments/index.d.ts.map +1 -0
  47. package/dist/bot/payments/index.js +14 -0
  48. package/dist/bot/payments/index.js.map +1 -0
  49. package/dist/bot/payments/invoice.d.ts +101 -0
  50. package/dist/bot/payments/invoice.d.ts.map +1 -0
  51. package/dist/bot/payments/invoice.js +213 -0
  52. package/dist/bot/payments/invoice.js.map +1 -0
  53. package/dist/bot/payments/menu-item.d.ts +41 -0
  54. package/dist/bot/payments/menu-item.d.ts.map +1 -0
  55. package/dist/bot/payments/menu-item.js +259 -0
  56. package/dist/bot/payments/menu-item.js.map +1 -0
  57. package/dist/bot/payments/payload.d.ts +42 -0
  58. package/dist/bot/payments/payload.d.ts.map +1 -0
  59. package/dist/bot/payments/payload.js +69 -0
  60. package/dist/bot/payments/payload.js.map +1 -0
  61. package/dist/bot/payments/payouts.d.ts +88 -0
  62. package/dist/bot/payments/payouts.d.ts.map +1 -0
  63. package/dist/bot/payments/payouts.js +133 -0
  64. package/dist/bot/payments/payouts.js.map +1 -0
  65. package/dist/bot/payments/plugin.d.ts +271 -0
  66. package/dist/bot/payments/plugin.d.ts.map +1 -0
  67. package/dist/bot/payments/plugin.js +141 -0
  68. package/dist/bot/payments/plugin.js.map +1 -0
  69. package/dist/bot/payments/refund.d.ts +124 -0
  70. package/dist/bot/payments/refund.d.ts.map +1 -0
  71. package/dist/bot/payments/refund.js +308 -0
  72. package/dist/bot/payments/refund.js.map +1 -0
  73. package/dist/bot/payments/schemas.d.ts +58 -0
  74. package/dist/bot/payments/schemas.d.ts.map +1 -0
  75. package/dist/bot/payments/schemas.js +55 -0
  76. package/dist/bot/payments/schemas.js.map +1 -0
  77. package/dist/bot/payments/state.d.ts +90 -0
  78. package/dist/bot/payments/state.d.ts.map +1 -0
  79. package/dist/bot/payments/state.js +149 -0
  80. package/dist/bot/payments/state.js.map +1 -0
  81. package/dist/bot/payments/stores.d.ts +44 -0
  82. package/dist/bot/payments/stores.d.ts.map +1 -0
  83. package/dist/bot/payments/stores.js +39 -0
  84. package/dist/bot/payments/stores.js.map +1 -0
  85. package/dist/bot/payments/types.d.ts +299 -0
  86. package/dist/bot/payments/types.d.ts.map +1 -0
  87. package/dist/bot/payments/types.js +39 -0
  88. package/dist/bot/payments/types.js.map +1 -0
  89. package/dist/bot/payments/waiver.d.ts +81 -0
  90. package/dist/bot/payments/waiver.d.ts.map +1 -0
  91. package/dist/bot/payments/waiver.js +145 -0
  92. package/dist/bot/payments/waiver.js.map +1 -0
  93. package/dist/bot/storage.d.ts +154 -0
  94. package/dist/bot/storage.d.ts.map +1 -0
  95. package/dist/bot/storage.js +222 -0
  96. package/dist/bot/storage.js.map +1 -0
  97. package/package.json +24 -3
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Construction-time validation + catalog build.
3
+ *
4
+ * `validateConfig(cfg)` is the gate. It throws `Panic` on every shape
5
+ * problem the bot author can introduce — a misconfigured payments
6
+ * plugin should scream at startup, not silently misbehave at runtime
7
+ * (see `src/offensive.ts` and CLAUDE.md §"Failure modes").
8
+ *
9
+ * `buildCatalog(cfg)` produces the normalized `ProductCatalog` the rest
10
+ * of the plugin consults (positional ids resolved, polyglots passed
11
+ * through, O(1) lookup by ProductKey).
12
+ *
13
+ * Both are pure — no gramio, no storage, no side effects.
14
+ */
15
+ import { panic } from "../../offensive.js";
16
+ import { SUBSCRIPTION_PERIOD_SECONDS, } from "./types.js";
17
+ // ─── validation ────────────────────────────────────────────────────
18
+ const requireNonEmptyString = (value, where) => {
19
+ if (typeof value !== "string" || value.length === 0) {
20
+ panic(`bot/payments: ${where} must be a non-empty string (got ${value})`);
21
+ }
22
+ return value;
23
+ };
24
+ const requirePositiveInt = (value, where) => {
25
+ if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
26
+ panic(`bot/payments: ${where} must be a positive integer (got ${value})`);
27
+ }
28
+ return value;
29
+ };
30
+ const polyglotKeys = (p) => Object.keys(p).filter((k) => typeof p[k] === "string");
31
+ const requirePolyglotCovers = (p, locales, where) => {
32
+ if (!p || typeof p !== "object") {
33
+ panic(`bot/payments: ${where} must be a Polyglot object`);
34
+ }
35
+ if (locales.size === 0)
36
+ return; // no required-locales declared — accept anything non-empty
37
+ for (const loc of locales) {
38
+ const v = p[loc];
39
+ if (typeof v !== "string" || v.length === 0) {
40
+ panic(`bot/payments: ${where} is missing a non-empty entry for locale "${loc}"`);
41
+ }
42
+ }
43
+ };
44
+ /**
45
+ * Validates the config and returns the **locales set** that the plugin
46
+ * will use for cross-checking other Polyglots. Derived from
47
+ * `waiver.text` (the single mandatory Polyglot) — every other Polyglot
48
+ * in the config must cover the same locales.
49
+ *
50
+ * Throws `Panic` on any structural problem.
51
+ */
52
+ export const validateConfig = (cfg) => {
53
+ if (!cfg || typeof cfg !== "object") {
54
+ panic("bot/payments: config must be an object");
55
+ }
56
+ requireNonEmptyString(cfg.paysupport, "paysupport");
57
+ requireNonEmptyString(cfg.legal?.sellerName, "legal.sellerName");
58
+ requireNonEmptyString(cfg.legal?.nif, "legal.nif");
59
+ // termsUrl and privacyUrl are OPTIONAL. privacyUrl gets a sensible
60
+ // Telegram-side default at use time (waiver.ts). termsUrl, when
61
+ // omitted, hides the 📖 Terms button from the consent prompt — no
62
+ // canonical Telegram ToS to fall back to.
63
+ if (cfg.legal?.termsUrl !== undefined) {
64
+ requireNonEmptyString(cfg.legal.termsUrl, "legal.termsUrl");
65
+ }
66
+ if (cfg.legal?.privacyUrl !== undefined) {
67
+ requireNonEmptyString(cfg.legal.privacyUrl, "legal.privacyUrl");
68
+ }
69
+ requireNonEmptyString(cfg.waiver?.version, "waiver.version");
70
+ const waiverKeys = polyglotKeys(cfg.waiver?.text ?? {});
71
+ if (waiverKeys.length === 0) {
72
+ panic("bot/payments: waiver.text must declare at least one locale with non-empty content");
73
+ }
74
+ const locales = new Set(waiverKeys);
75
+ const hasVip = Array.isArray(cfg.vip) && cfg.vip.length > 0;
76
+ const hasCredits = !!cfg.credits &&
77
+ Array.isArray(cfg.credits.packs) &&
78
+ cfg.credits.packs.length > 0;
79
+ const hasPerks = !!cfg.perks && Object.keys(cfg.perks).length > 0;
80
+ if (!hasVip && !hasCredits && !hasPerks) {
81
+ panic("bot/payments: declare at least one of `vip`, `credits.packs`, or `perks` — " +
82
+ "a payments plugin with no products has nothing to sell");
83
+ }
84
+ if (hasVip) {
85
+ cfg.vip?.forEach((rung, i) => {
86
+ const at = `vip[${i}]`;
87
+ requirePositiveInt(rung.xtr, `${at}.xtr`);
88
+ if (rung.period !== "30d") {
89
+ panic(`bot/payments: ${at}.period must be "30d" (Telegram supports no other ` +
90
+ `subscription period as of 2026) — got "${rung.period}"`);
91
+ }
92
+ requirePolyglotCovers(rung.name, locales, `${at}.name`);
93
+ if (rung.grants?.credits !== undefined) {
94
+ requirePositiveInt(rung.grants.credits, `${at}.grants.credits`);
95
+ }
96
+ });
97
+ }
98
+ if (hasCredits) {
99
+ const c = cfg.credits;
100
+ if (!c)
101
+ panic("bot/payments: credits config disappeared mid-validation");
102
+ requirePolyglotCovers(c.unit, locales, "credits.unit");
103
+ c.packs.forEach((pack, i) => {
104
+ const at = `credits.packs[${i}]`;
105
+ requirePositiveInt(pack.xtr, `${at}.xtr`);
106
+ requirePositiveInt(pack.grants?.credits, `${at}.grants.credits`);
107
+ if (pack.name) {
108
+ requirePolyglotCovers(pack.name, locales, `${at}.name`);
109
+ }
110
+ });
111
+ }
112
+ if (hasPerks && cfg.perks) {
113
+ for (const [key, perk] of Object.entries(cfg.perks)) {
114
+ const at = `perks.${key}`;
115
+ if (!/^[a-z][a-z0-9_]*$/i.test(key)) {
116
+ panic(`bot/payments: perk key "${key}" must match /^[a-z][a-z0-9_]*$/i (used in ` +
117
+ `payload encoding, must be safe)`);
118
+ }
119
+ requirePositiveInt(perk.xtr, `${at}.xtr`);
120
+ requirePolyglotCovers(perk.name, locales, `${at}.name`);
121
+ }
122
+ }
123
+ return locales;
124
+ };
125
+ // ─── catalog build ─────────────────────────────────────────────────
126
+ /**
127
+ * Materialize the validated config into the runtime catalog. Assumes
128
+ * `validateConfig(cfg)` has already been called and passed.
129
+ */
130
+ export const buildCatalog = (cfg) => {
131
+ const vip = (cfg.vip ?? []).map((rung, i) => ({
132
+ id: `vip.${i + 1}`,
133
+ rank: i + 1,
134
+ xtr: rung.xtr,
135
+ periodSeconds: SUBSCRIPTION_PERIOD_SECONDS[rung.period],
136
+ name: rung.name,
137
+ creditsGranted: rung.grants?.credits ?? 0,
138
+ }));
139
+ const creditsPacks = (cfg.credits?.packs ?? []).map((pack, i) => ({
140
+ id: `credits.${i + 1}`,
141
+ xtr: pack.xtr,
142
+ creditsGranted: pack.grants.credits,
143
+ name: pack.name,
144
+ }));
145
+ const perks = Object.entries(cfg.perks ?? {}).map(([key, perk]) => ({
146
+ id: `perks.${key}`,
147
+ key,
148
+ xtr: perk.xtr,
149
+ name: perk.name,
150
+ }));
151
+ const byKey = new Map();
152
+ for (const r of vip)
153
+ byKey.set(r.id, r);
154
+ for (const p of creditsPacks)
155
+ byKey.set(p.id, p);
156
+ for (const p of perks)
157
+ byKey.set(p.id, p);
158
+ return {
159
+ vip,
160
+ creditsUnit: cfg.credits?.unit,
161
+ creditsPacks,
162
+ perks,
163
+ byKey,
164
+ };
165
+ };
166
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../../src/bot/payments/config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,75 @@
1
+ /**
2
+ * `ctx.payments` derive — the per-event ctx decoration.
3
+ *
4
+ * Extracted from `plugin.ts` to keep that file pure wiring per the
5
+ * `bot/CLAUDE.md` plugin file convention. Everything that reads or
6
+ * mutates session state inside an event handler goes through one of
7
+ * the methods this module attaches:
8
+ *
9
+ * - `ctx.payments.atLeast(id)` — tier gate (lazy expiry)
10
+ * - `ctx.payments.tier()` — current rung (`'free' | 'vip.N'`)
11
+ * - `ctx.payments.tier.level()` — integer rank
12
+ * - `ctx.payments.tier.label()` — resolved Polyglot label
13
+ * - `ctx.payments.credits.*` — balance / consume / tryConsume
14
+ * - `ctx.payments.has(perkId)` — perk ownership boolean
15
+ * - `ctx.payments.require(...)` — gate + localized upgrade prompt
16
+ * - `ctx.payments.invoice(key)` — purchase entry (waiver-gated)
17
+ *
18
+ * Returned shape is a function so `plugin.ts` can drop it directly
19
+ * into `.derive(["message", "callback_query"], buildPaymentsDerive({...}))`.
20
+ */
21
+ import { type BotPaymentsConfig, type ProductCatalog } from "./types.js";
22
+ type TierKey = "free" | `vip.${number}`;
23
+ /**
24
+ * Callable `tier` namespace. Matches `PaymentsCtx['tier']` from `plugin.ts`
25
+ * — exported there as the public surface. Repeated minimally here so
26
+ * derive.ts isn't import-cycle dependent on plugin.ts.
27
+ */
28
+ export type TierFn = (() => TierKey) & {
29
+ level: () => number;
30
+ label: () => string | undefined;
31
+ };
32
+ export type CreditsApi = {
33
+ balance: () => number;
34
+ consume: (n: number) => void;
35
+ tryConsume: (n: number) => boolean;
36
+ };
37
+ export type RequireOpts = {
38
+ feature?: {
39
+ en: string;
40
+ es: string;
41
+ } | string;
42
+ };
43
+ /**
44
+ * What `buildPaymentsDerive` attaches under `ctx.payments`. Plugin.ts
45
+ * casts this to the typed `PaymentsCtx<Cfg>` (with positional ladder
46
+ * narrowing) before exposing it to bot authors — TypeScript can't
47
+ * preserve the generic narrowing through a function-shaped derive
48
+ * factory cleanly, so the strict type lives at the public surface.
49
+ */
50
+ export type PaymentsDerived = {
51
+ atLeast: (id: string) => boolean;
52
+ tier: TierFn;
53
+ has: (perkId: string) => boolean;
54
+ credits: CreditsApi;
55
+ require: (id: string, opts?: RequireOpts) => Promise<boolean>;
56
+ invoice: (productKey: string) => Promise<"invoice_sent" | "waiver_prompt_sent" | "unknown_product">;
57
+ };
58
+ export type BuildPaymentsDeriveArgs = {
59
+ /** Resolved product catalogue (rungs, packs, perks). */
60
+ catalog: ProductCatalog;
61
+ /**
62
+ * Full config — currently only consulted inside `invoice()` for the
63
+ * waiver wording + product display strings via `presentInvoice`.
64
+ */
65
+ cfg: BotPaymentsConfig<string>;
66
+ };
67
+ /**
68
+ * Returns the function you pass to gramio's `.derive([events], fn)`.
69
+ * The returned function takes ctx, returns `{ payments }`.
70
+ */
71
+ export declare const buildPaymentsDerive: ({ cfg, catalog, }: BuildPaymentsDeriveArgs) => (ctx: unknown) => {
72
+ payments: PaymentsDerived;
73
+ };
74
+ export {};
75
+ //# sourceMappingURL=derive.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"derive.d.ts","sourceRoot":"","sources":["../../../src/bot/payments/derive.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAMH,OAAO,EACN,KAAK,iBAAiB,EAGtB,KAAK,cAAc,EACnB,MAAM,YAAY,CAAC;AAMpB,KAAK,OAAO,GAAG,MAAM,GAAG,OAAO,MAAM,EAAE,CAAC;AAExC;;;;GAIG;AACH,MAAM,MAAM,MAAM,GAAG,CAAC,MAAM,OAAO,CAAC,GAAG;IACtC,KAAK,EAAE,MAAM,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;CAChC,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACxB,OAAO,EAAE,MAAM,MAAM,CAAC;IACtB,OAAO,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7B,UAAU,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC;CACnC,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACzB,OAAO,CAAC,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,GAAG,MAAM,CAAC;CAC9C,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,MAAM,eAAe,GAAG;IAC7B,OAAO,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC;IACjC,OAAO,EAAE,UAAU,CAAC;IACpB,OAAO,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC9D,OAAO,EAAE,CACR,UAAU,EAAE,MAAM,KACd,OAAO,CAAC,cAAc,GAAG,oBAAoB,GAAG,iBAAiB,CAAC,CAAC;CACxE,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACrC,wDAAwD;IACxD,OAAO,EAAE,cAAc,CAAC;IACxB;;;OAGG;IACH,GAAG,EAAE,iBAAiB,CAAC,MAAM,CAAC,CAAC;CAC/B,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,mBAAmB,GAAI,mBAGjC,uBAAuB,MACjB,KAAK,OAAO,KAAG;IAAE,QAAQ,EAAE,eAAe,CAAA;CA+IlD,CAAC"}
@@ -0,0 +1,151 @@
1
+ /**
2
+ * `ctx.payments` derive — the per-event ctx decoration.
3
+ *
4
+ * Extracted from `plugin.ts` to keep that file pure wiring per the
5
+ * `bot/CLAUDE.md` plugin file convention. Everything that reads or
6
+ * mutates session state inside an event handler goes through one of
7
+ * the methods this module attaches:
8
+ *
9
+ * - `ctx.payments.atLeast(id)` — tier gate (lazy expiry)
10
+ * - `ctx.payments.tier()` — current rung (`'free' | 'vip.N'`)
11
+ * - `ctx.payments.tier.level()` — integer rank
12
+ * - `ctx.payments.tier.label()` — resolved Polyglot label
13
+ * - `ctx.payments.credits.*` — balance / consume / tryConsume
14
+ * - `ctx.payments.has(perkId)` — perk ownership boolean
15
+ * - `ctx.payments.require(...)` — gate + localized upgrade prompt
16
+ * - `ctx.payments.invoice(key)` — purchase entry (waiver-gated)
17
+ *
18
+ * Returned shape is a function so `plugin.ts` can drop it directly
19
+ * into `.derive(["message", "callback_query"], buildPaymentsDerive({...}))`.
20
+ */
21
+ import { say } from "../../say/index.js";
22
+ import { narrow } from "../ctx.js";
23
+ import { menuNavCb } from "../menu.js";
24
+ import { presentInvoice } from "./invoice.js";
25
+ import { InsufficientCredits, } from "./types.js";
26
+ const FALLBACK_LANG = "en";
27
+ /**
28
+ * Returns the function you pass to gramio's `.derive([events], fn)`.
29
+ * The returned function takes ctx, returns `{ payments }`.
30
+ */
31
+ export const buildPaymentsDerive = ({ cfg, catalog, }) => {
32
+ return (ctx) => {
33
+ // Single narrow at the top — every subsequent reference uses
34
+ // `c.<field>` and reads cleanly. The structural type is the
35
+ // intersection of message + callback_query event ctxs so this
36
+ // derive works for both event scopes (gramio's runtime dedup
37
+ // guarantees one execution per update).
38
+ const c = narrow(ctx);
39
+ // Lazy expiry — vip is only "active" while its expiresAt is in
40
+ // the future (CLAUDE.md §"sweep" deferred to v2). Pure read; no
41
+ // cleanup of stale `session.pay.vip` records.
42
+ const isVipActive = () => {
43
+ const v = c.session.pay?.vip;
44
+ if (!v)
45
+ return false;
46
+ return v.expiresAt * 1000 > Date.now();
47
+ };
48
+ const effectiveRung = () => {
49
+ if (!isVipActive())
50
+ return 0;
51
+ return c.session.pay?.vip?.rung ?? 0;
52
+ };
53
+ const atLeast = (id) => {
54
+ if (id === "free")
55
+ return true;
56
+ const rung = effectiveRung();
57
+ if (id === "vip")
58
+ return rung > 0;
59
+ if (id.startsWith("vip.")) {
60
+ const want = Number.parseInt(id.slice(4), 10);
61
+ return rung >= want;
62
+ }
63
+ return false;
64
+ };
65
+ const tierFn = (() => {
66
+ const rung = effectiveRung();
67
+ if (rung === 0)
68
+ return "free";
69
+ return `vip.${rung}`;
70
+ });
71
+ tierFn.level = () => effectiveRung();
72
+ tierFn.label = () => {
73
+ const rung = effectiveRung();
74
+ if (rung === 0)
75
+ return undefined;
76
+ const r = catalog.vip[rung - 1];
77
+ if (!r)
78
+ return undefined;
79
+ const lang = c.session.language ?? FALLBACK_LANG;
80
+ return (r.name[lang] ??
81
+ r.name[FALLBACK_LANG]);
82
+ };
83
+ const credits = {
84
+ balance: () => c.session.pay?.credits ?? 0,
85
+ consume: (n) => {
86
+ const current = c.session.pay?.credits ?? 0;
87
+ if (current < n)
88
+ throw new InsufficientCredits(n, current);
89
+ c.session.pay ??= {};
90
+ c.session.pay.credits = current - n;
91
+ },
92
+ tryConsume: (n) => {
93
+ const current = c.session.pay?.credits ?? 0;
94
+ if (current < n)
95
+ return false;
96
+ c.session.pay ??= {};
97
+ c.session.pay.credits = current - n;
98
+ return true;
99
+ },
100
+ };
101
+ const has = (perkId) => !!c.session.pay?.perks?.[perkId];
102
+ const requireFn = async (id, opts) => {
103
+ if (atLeast(id))
104
+ return true;
105
+ const lang = c.session.language ?? FALLBACK_LANG;
106
+ const featureName = opts?.feature === undefined
107
+ ? undefined
108
+ : typeof opts.feature === "string"
109
+ ? opts.feature
110
+ : (opts.feature[lang] ?? opts.feature.en);
111
+ const body = featureName !== undefined
112
+ ? say({
113
+ en: `💎 "${featureName}" needs VIP. Tap below to upgrade.`,
114
+ es: `💎 "${featureName}" necesita VIP. Toca abajo para mejorar.`,
115
+ }, lang)
116
+ : say({
117
+ en: "💎 This needs VIP. Tap below to upgrade.",
118
+ es: "💎 Esto necesita VIP. Toca abajo para mejorar.",
119
+ }, lang);
120
+ // Deep-link into our menuItem (id='pay') via botMenu's
121
+ // shared menuNavCb schema. v1 opens VIP root; deep-linking to
122
+ // a specific rung is a v2 TODO (see CLAUDE.md).
123
+ const button = say({ en: "💎 Open VIP plans", es: "💎 Ver planes VIP" }, lang);
124
+ await c.send(body, {
125
+ reply_markup: {
126
+ inline_keyboard: [
127
+ [
128
+ {
129
+ text: button,
130
+ callback_data: menuNavCb.pack({ path: "pay" }),
131
+ },
132
+ ],
133
+ ],
134
+ },
135
+ });
136
+ return false;
137
+ };
138
+ const invoiceFn = (productKey) => presentInvoice(narrow(c), cfg, catalog, productKey);
139
+ return {
140
+ payments: {
141
+ atLeast,
142
+ tier: tierFn,
143
+ has,
144
+ credits,
145
+ require: requireFn,
146
+ invoice: invoiceFn,
147
+ },
148
+ };
149
+ };
150
+ };
151
+ //# sourceMappingURL=derive.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"derive.js","sourceRoot":"","sources":["../../../src/bot/payments/derive.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Telegram event handlers: `pre_checkout_query`, `successful_payment`,
3
+ * and the auto-installed `/paysupport` command.
4
+ *
5
+ * Two hot paths the handlers MUST get right:
6
+ *
7
+ * - `pre_checkout_query` has a **10 s hard deadline** at Telegram's
8
+ * side. We answer synchronously after a single payload decode +
9
+ * catalog lookup + xtr match check. No storage round-trips.
10
+ *
11
+ * - `successful_payment` is **idempotent by `telegram_payment_charge_id`**.
12
+ * We claim the chargeId via a sentinel key (`pay:idempotency:{id}`)
13
+ * using set-if-absent semantics; a duplicate delivery returns a
14
+ * no-op. Telegram retries this event in some failure modes (see
15
+ * `sendInvoice` docs § Tips), so this guard is load-bearing.
16
+ *
17
+ * `/paysupport` is mandated by the Bot Developer ToS §6.5 — every bot
18
+ * accepting payments must surface a way for users to request refunds.
19
+ * We register the command and route users to `/settings → 💎 VIP`
20
+ * where the refund flow lives (mirrors how `bot/menu` keeps every
21
+ * user-facing flow under one slash command).
22
+ */
23
+ import { SourcedError } from "../../offensive.js";
24
+ import type { BotPaymentCtx, BotPreCheckoutCtx } from "../ctx.js";
25
+ import type { PaymentsStores } from "./stores.js";
26
+ import type { BotPaymentsConfig, FulfillmentEvent, PaymentsSession, ProductCatalog } from "./types.js";
27
+ /**
28
+ * Strict bot.api shape for this plugin. Composes with the canonical
29
+ * `BotMessageCtx` / `BotPreCheckoutCtx` / `BotPaymentCtx` via their
30
+ * `Api` generic, so a typo on a method name or wrong param shape is a
31
+ * compile error here.
32
+ */
33
+ type PaymentBotApi = {
34
+ answerPreCheckoutQuery: (params: {
35
+ pre_checkout_query_id: string;
36
+ ok: boolean;
37
+ error_message?: string;
38
+ }) => Promise<unknown>;
39
+ editUserStarSubscription: (params: {
40
+ user_id: number;
41
+ telegram_payment_charge_id: string;
42
+ is_canceled: boolean;
43
+ }) => Promise<unknown>;
44
+ sendMessage: (params: {
45
+ chat_id: number;
46
+ message_thread_id?: number;
47
+ text: string;
48
+ reply_markup?: unknown;
49
+ }) => Promise<unknown>;
50
+ };
51
+ type SessionLike = {
52
+ pay?: PaymentsSession;
53
+ language?: string;
54
+ };
55
+ /**
56
+ * Pre-checkout event ctx (Telegram's 10s deadline). Strict bot.api;
57
+ * everything else is structural. No session on this event scope.
58
+ */
59
+ type PreCheckoutCtx = BotPreCheckoutCtx<PaymentBotApi>;
60
+ /**
61
+ * Successful-payment event ctx. Reads the raw `.payload` (snake_case
62
+ * Telegram fields) because every downstream consumer already speaks
63
+ * snake_case, and going through gramio's wrapper getters would mean
64
+ * a second translation layer. See `bot/ctx.ts` § BotPaymentCtx for
65
+ * the historical bug that produced this convention (mixing wrapper
66
+ * camelCase with snake_case yields silent `undefined`).
67
+ */
68
+ type MessageCtx = BotPaymentCtx<SessionLike, PaymentBotApi>;
69
+ /**
70
+ * Validate the in-flight purchase synchronously and ack within 10 s.
71
+ *
72
+ * - Currency must be `XTR` (we only sell digital goods; fiat would
73
+ * have a `provider_token` and not reach this plugin).
74
+ * - Payload must decode + match `ctx.from.id` (anti-tamper).
75
+ * - Product must exist in the catalog.
76
+ * - `total_amount` must match the catalog's declared xtr — defense
77
+ * against a stale invoice or a config edit between invoice send
78
+ * and tap.
79
+ *
80
+ * On any failure: ack `ok: false` with a brief localized reason. The
81
+ * user sees Telegram's "Payment failed" sheet with that reason.
82
+ */
83
+ export declare const buildPreCheckoutHandler: (catalog: ProductCatalog) => (ctx: PreCheckoutCtx) => Promise<void>;
84
+ export type SuccessfulPaymentHandlerOptions = {
85
+ stores: PaymentsStores;
86
+ cfg: BotPaymentsConfig<string>;
87
+ catalog: ProductCatalog;
88
+ /**
89
+ * Map of productKey → callbacks fired AFTER fulfillment succeeds.
90
+ * Key `"*"` is the catch-all, run on every fulfillment in addition
91
+ * to the productKey-specific list.
92
+ *
93
+ * Sync return signature; the plugin always answers Telegram itself.
94
+ * Handlers are fire-and-forget — async work inside is OK but the
95
+ * plugin doesn't await them.
96
+ */
97
+ onFulfilled: ReadonlyMap<string, ReadonlyArray<(event: FulfillmentEvent, ctx: MessageCtx) => void>>;
98
+ };
99
+ /**
100
+ * Process a `successful_payment` event. Idempotent on chargeId.
101
+ *
102
+ * On first delivery:
103
+ * 1. Decode payload, look up product
104
+ * 2. Persist `ChargeRecord` + idempotency sentinel + user index
105
+ * 3. Mutate `ctx.session.pay.*` via `applyCharge`
106
+ * 4. Auto-cancel any LOWER vip rung (tier upgrade case)
107
+ * 5. Send user confirmation + emit onFulfilled hooks
108
+ *
109
+ * On duplicate delivery: returns silently.
110
+ */
111
+ export declare const buildSuccessfulPaymentHandler: (opts: SuccessfulPaymentHandlerOptions) => (ctx: MessageCtx) => Promise<void>;
112
+ export { SourcedError };
113
+ //# sourceMappingURL=handlers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../../../src/bot/payments/handlers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAS,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAGzD,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAGlE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,KAAK,EACX,iBAAiB,EAGjB,gBAAgB,EAChB,eAAe,EAEf,cAAc,EAEd,MAAM,YAAY,CAAC;AAQpB;;;;;GAKG;AACH,KAAK,aAAa,GAAG;IACpB,sBAAsB,EAAE,CAAC,MAAM,EAAE;QAChC,qBAAqB,EAAE,MAAM,CAAC;QAC9B,EAAE,EAAE,OAAO,CAAC;QACZ,aAAa,CAAC,EAAE,MAAM,CAAC;KACvB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACvB,wBAAwB,EAAE,CAAC,MAAM,EAAE;QAClC,OAAO,EAAE,MAAM,CAAC;QAChB,0BAA0B,EAAE,MAAM,CAAC;QACnC,WAAW,EAAE,OAAO,CAAC;KACrB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACvB,WAAW,EAAE,CAAC,MAAM,EAAE;QACrB,OAAO,EAAE,MAAM,CAAC;QAChB,iBAAiB,CAAC,EAAE,MAAM,CAAC;QAC3B,IAAI,EAAE,MAAM,CAAC;QACb,YAAY,CAAC,EAAE,OAAO,CAAC;KACvB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CACvB,CAAC;AAEF,KAAK,WAAW,GAAG;IAAE,GAAG,CAAC,EAAE,eAAe,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAEhE;;;GAGG;AACH,KAAK,cAAc,GAAG,iBAAiB,CAAC,aAAa,CAAC,CAAC;AAEvD;;;;;;;GAOG;AACH,KAAK,UAAU,GAAG,aAAa,CAAC,WAAW,EAAE,aAAa,CAAC,CAAC;AAU5D;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,uBAAuB,GAClC,SAAS,cAAc,MACjB,KAAK,cAAc,KAAG,OAAO,CAAC,IAAI,CA+DxC,CAAC;AAyCH,MAAM,MAAM,+BAA+B,GAAG;IAC7C,MAAM,EAAE,cAAc,CAAC;IACvB,GAAG,EAAE,iBAAiB,CAAC,MAAM,CAAC,CAAC;IAC/B,OAAO,EAAE,cAAc,CAAC;IACxB;;;;;;;;OAQG;IACH,WAAW,EAAE,WAAW,CACvB,MAAM,EACN,aAAa,CAAC,CAAC,KAAK,EAAE,gBAAgB,EAAE,GAAG,EAAE,UAAU,KAAK,IAAI,CAAC,CACjE,CAAC;CACF,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,6BAA6B,GACxC,MAAM,+BAA+B,MAC/B,KAAK,UAAU,KAAG,OAAO,CAAC,IAAI,CA2JpC,CAAC;AA+CH,OAAO,EAAE,YAAY,EAAE,CAAC"}