@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.
- package/README.md +2 -1
- package/dist/bot/access-control.d.ts.map +1 -1
- package/dist/bot/access-control.js +17 -1
- package/dist/bot/access-control.js.map +1 -1
- package/dist/bot/callbacks.d.ts +89 -0
- package/dist/bot/callbacks.d.ts.map +1 -0
- package/dist/bot/callbacks.js +128 -0
- package/dist/bot/callbacks.js.map +1 -0
- package/dist/bot/ctx.d.ts +219 -0
- package/dist/bot/ctx.d.ts.map +1 -0
- package/dist/bot/ctx.js +52 -0
- package/dist/bot/ctx.js.map +1 -0
- package/dist/bot/index.d.ts +4 -0
- package/dist/bot/index.d.ts.map +1 -1
- package/dist/bot/index.js +4 -0
- package/dist/bot/index.js.map +1 -1
- package/dist/bot/kit.d.ts +1 -1
- package/dist/bot/kit.d.ts.map +1 -1
- package/dist/bot/kit.js +4 -2
- package/dist/bot/kit.js.map +1 -1
- package/dist/bot/menu.d.ts +23 -1
- package/dist/bot/menu.d.ts.map +1 -1
- package/dist/bot/menu.js +30 -1
- package/dist/bot/menu.js.map +1 -1
- package/dist/bot/payments/callbacks.d.ts +28 -0
- package/dist/bot/payments/callbacks.d.ts.map +1 -0
- package/dist/bot/payments/callbacks.js +102 -0
- package/dist/bot/payments/callbacks.js.map +1 -0
- package/dist/bot/payments/commands.d.ts +26 -0
- package/dist/bot/payments/commands.d.ts.map +1 -0
- package/dist/bot/payments/commands.js +41 -0
- package/dist/bot/payments/commands.js.map +1 -0
- package/dist/bot/payments/config.d.ts +30 -0
- package/dist/bot/payments/config.d.ts.map +1 -0
- package/dist/bot/payments/config.js +166 -0
- package/dist/bot/payments/config.js.map +1 -0
- package/dist/bot/payments/derive.d.ts +75 -0
- package/dist/bot/payments/derive.d.ts.map +1 -0
- package/dist/bot/payments/derive.js +151 -0
- package/dist/bot/payments/derive.js.map +1 -0
- package/dist/bot/payments/handlers.d.ts +113 -0
- package/dist/bot/payments/handlers.d.ts.map +1 -0
- package/dist/bot/payments/handlers.js +284 -0
- package/dist/bot/payments/handlers.js.map +1 -0
- package/dist/bot/payments/index.d.ts +17 -0
- package/dist/bot/payments/index.d.ts.map +1 -0
- package/dist/bot/payments/index.js +14 -0
- package/dist/bot/payments/index.js.map +1 -0
- package/dist/bot/payments/invoice.d.ts +101 -0
- package/dist/bot/payments/invoice.d.ts.map +1 -0
- package/dist/bot/payments/invoice.js +213 -0
- package/dist/bot/payments/invoice.js.map +1 -0
- package/dist/bot/payments/menu-item.d.ts +41 -0
- package/dist/bot/payments/menu-item.d.ts.map +1 -0
- package/dist/bot/payments/menu-item.js +259 -0
- package/dist/bot/payments/menu-item.js.map +1 -0
- package/dist/bot/payments/payload.d.ts +42 -0
- package/dist/bot/payments/payload.d.ts.map +1 -0
- package/dist/bot/payments/payload.js +69 -0
- package/dist/bot/payments/payload.js.map +1 -0
- package/dist/bot/payments/payouts.d.ts +88 -0
- package/dist/bot/payments/payouts.d.ts.map +1 -0
- package/dist/bot/payments/payouts.js +133 -0
- package/dist/bot/payments/payouts.js.map +1 -0
- package/dist/bot/payments/plugin.d.ts +271 -0
- package/dist/bot/payments/plugin.d.ts.map +1 -0
- package/dist/bot/payments/plugin.js +141 -0
- package/dist/bot/payments/plugin.js.map +1 -0
- package/dist/bot/payments/refund.d.ts +124 -0
- package/dist/bot/payments/refund.d.ts.map +1 -0
- package/dist/bot/payments/refund.js +308 -0
- package/dist/bot/payments/refund.js.map +1 -0
- package/dist/bot/payments/schemas.d.ts +58 -0
- package/dist/bot/payments/schemas.d.ts.map +1 -0
- package/dist/bot/payments/schemas.js +55 -0
- package/dist/bot/payments/schemas.js.map +1 -0
- package/dist/bot/payments/state.d.ts +90 -0
- package/dist/bot/payments/state.d.ts.map +1 -0
- package/dist/bot/payments/state.js +149 -0
- package/dist/bot/payments/state.js.map +1 -0
- package/dist/bot/payments/stores.d.ts +44 -0
- package/dist/bot/payments/stores.d.ts.map +1 -0
- package/dist/bot/payments/stores.js +39 -0
- package/dist/bot/payments/stores.js.map +1 -0
- package/dist/bot/payments/types.d.ts +299 -0
- package/dist/bot/payments/types.d.ts.map +1 -0
- package/dist/bot/payments/types.js +39 -0
- package/dist/bot/payments/types.js.map +1 -0
- package/dist/bot/payments/waiver.d.ts +81 -0
- package/dist/bot/payments/waiver.d.ts.map +1 -0
- package/dist/bot/payments/waiver.js +145 -0
- package/dist/bot/payments/waiver.js.map +1 -0
- package/dist/bot/storage.d.ts +154 -0
- package/dist/bot/storage.d.ts.map +1 -0
- package/dist/bot/storage.js +222 -0
- package/dist/bot/storage.js.map +1 -0
- 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"}
|