@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,284 @@
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 { panic, SourcedError } from "../../offensive.js";
24
+ import { say } from "../../say/index.js";
25
+ import { createLogger } from "../../universal/log.js";
26
+ import { decodePayload } from "./payload.js";
27
+ import { applyCharge } from "./state.js";
28
+ const log = createLogger("bot/payments");
29
+ const FALLBACK_LANG = "en";
30
+ const ctxLang = (ctx) => ctx.session?.language ?? FALLBACK_LANG;
31
+ // ─── pre_checkout_query handler ────────────────────────────────────
32
+ /**
33
+ * Validate the in-flight purchase synchronously and ack within 10 s.
34
+ *
35
+ * - Currency must be `XTR` (we only sell digital goods; fiat would
36
+ * have a `provider_token` and not reach this plugin).
37
+ * - Payload must decode + match `ctx.from.id` (anti-tamper).
38
+ * - Product must exist in the catalog.
39
+ * - `total_amount` must match the catalog's declared xtr — defense
40
+ * against a stale invoice or a config edit between invoice send
41
+ * and tap.
42
+ *
43
+ * On any failure: ack `ok: false` with a brief localized reason. The
44
+ * user sees Telegram's "Payment failed" sheet with that reason.
45
+ */
46
+ export const buildPreCheckoutHandler = (catalog) => async (ctx) => {
47
+ log.event(`pre_checkout_query received: user=${ctx.from.id} currency=${ctx.currency} amount=${ctx.totalAmount} payload="${ctx.invoicePayload}" id=${ctx.id}`);
48
+ const reject = async (reasonEn, reasonEs) => {
49
+ log.warn(`pre_checkout rejecting: ${reasonEn} (id=${ctx.id})`);
50
+ try {
51
+ await ctx.bot.api.answerPreCheckoutQuery({
52
+ pre_checkout_query_id: ctx.id,
53
+ ok: false,
54
+ // We don't have ctx.session on pre_checkout (it's a different
55
+ // event scope per gramio), so we can't read user lang. Default
56
+ // to a bilingual blurb that's intelligible to both en/es users.
57
+ error_message: `${reasonEn} · ${reasonEs}`,
58
+ });
59
+ }
60
+ catch (e) {
61
+ log.error(`answerPreCheckoutQuery(false) failed: ${e}`);
62
+ }
63
+ };
64
+ if (ctx.currency !== "XTR") {
65
+ await reject("Unsupported currency.", "Moneda no soportada.");
66
+ return;
67
+ }
68
+ const decoded = decodePayload(ctx.invoicePayload);
69
+ if (!decoded) {
70
+ await reject("Malformed invoice.", "Factura inválida.");
71
+ return;
72
+ }
73
+ if (decoded.userId !== ctx.from.id) {
74
+ await reject("User mismatch.", "Usuario no coincide.");
75
+ return;
76
+ }
77
+ const product = catalog.byKey.get(decoded.productKey);
78
+ if (!product) {
79
+ await reject("Unknown product.", "Producto desconocido.");
80
+ return;
81
+ }
82
+ if (product.xtr !== ctx.totalAmount) {
83
+ await reject(`Price changed (expected ${product.xtr}, got ${ctx.totalAmount}).`, "Precio cambiado.");
84
+ return;
85
+ }
86
+ log.success(`pre_checkout approving: product=${decoded.productKey} user=${ctx.from.id} amount=${ctx.totalAmount}`);
87
+ try {
88
+ await ctx.bot.api.answerPreCheckoutQuery({
89
+ pre_checkout_query_id: ctx.id,
90
+ ok: true,
91
+ });
92
+ }
93
+ catch (e) {
94
+ log.error(`answerPreCheckoutQuery(true) failed: ${e}`);
95
+ }
96
+ };
97
+ // ─── successful_payment handler ────────────────────────────────────
98
+ const buildChargeRecord = (product, userId, payment, waiverSnapshot) => {
99
+ const base = {
100
+ chargeId: payment.telegram_payment_charge_id,
101
+ userId,
102
+ productKey: product.id,
103
+ xtr: payment.total_amount,
104
+ receivedAt: Date.now(),
105
+ payload: payment.invoice_payload,
106
+ waiverSnapshot,
107
+ paysupportState: "none",
108
+ refundedAt: null,
109
+ payoutBatchId: null,
110
+ creditsGranted: 0,
111
+ };
112
+ if (product.id.startsWith("vip.")) {
113
+ const v = product;
114
+ return {
115
+ ...base,
116
+ vipRung: v.rank,
117
+ subscriptionExpiresAt: payment.subscription_expiration_date,
118
+ creditsGranted: v.creditsGranted,
119
+ };
120
+ }
121
+ if (product.id.startsWith("credits.")) {
122
+ const c = product;
123
+ return { ...base, creditsGranted: c.creditsGranted };
124
+ }
125
+ // perk
126
+ const p = product;
127
+ return { ...base, perkKey: p.key };
128
+ };
129
+ /**
130
+ * Process a `successful_payment` event. Idempotent on chargeId.
131
+ *
132
+ * On first delivery:
133
+ * 1. Decode payload, look up product
134
+ * 2. Persist `ChargeRecord` + idempotency sentinel + user index
135
+ * 3. Mutate `ctx.session.pay.*` via `applyCharge`
136
+ * 4. Auto-cancel any LOWER vip rung (tier upgrade case)
137
+ * 5. Send user confirmation + emit onFulfilled hooks
138
+ *
139
+ * On duplicate delivery: returns silently.
140
+ */
141
+ export const buildSuccessfulPaymentHandler = (opts) => async (ctx) => {
142
+ const payment = ctx.eventPayment?.payload;
143
+ if (!payment) {
144
+ log.warn("successful_payment handler fired but ctx.eventPayment.payload is undefined — gramio context shape changed?");
145
+ return;
146
+ }
147
+ if (payment.currency !== "XTR")
148
+ return; // fiat — not ours
149
+ log.event(`successful_payment received: chargeId=${payment.telegram_payment_charge_id} amount=${payment.total_amount} payload="${payment.invoice_payload}" subExpires=${payment.subscription_expiration_date ?? "n/a"}`);
150
+ const decoded = decodePayload(payment.invoice_payload);
151
+ if (!decoded) {
152
+ log.error(`successful_payment un-decodable payload — fulfillment skipped: chargeId=${payment.telegram_payment_charge_id} payload="${payment.invoice_payload}"`);
153
+ return;
154
+ }
155
+ const userId = ctx.from?.id ?? decoded.userId;
156
+ // Idempotency: claim chargeId via the sentinel store. Returns
157
+ // `true` on first claim, `false` if already claimed (duplicate
158
+ // delivery — Telegram retries `successful_payment` in some
159
+ // failure modes per the sendInvoice doc). NOT atomic under
160
+ // concurrent load (storage doesn't expose setNX); the window is
161
+ // bounded by Telegram's retry delay and acceptable for v1.
162
+ const claimed = await opts.stores.idempotency.claim(ctx, payment.telegram_payment_charge_id);
163
+ if (!claimed) {
164
+ log.info(`successful_payment idempotent no-op: chargeId=${payment.telegram_payment_charge_id} already fulfilled`);
165
+ return;
166
+ }
167
+ const product = opts.catalog.byKey.get(decoded.productKey);
168
+ if (!product) {
169
+ log.error(`successful_payment for unknown productKey "${decoded.productKey}" — config drift? chargeId=${payment.telegram_payment_charge_id}`);
170
+ return;
171
+ }
172
+ const waiverSnapshot = ctx.session.pay?.waiver;
173
+ if (!waiverSnapshot) {
174
+ // Should never happen: every invoice flow gates on waiver
175
+ // freshness. If we reach this point, our own gating logic has
176
+ // a hole. Scream so it gets found, but DON'T drop the user's
177
+ // money — finish fulfillment with a synthetic "missing
178
+ // snapshot" entry that's auditable. The `panic` is
179
+ // intentionally after the persistence path on purpose to keep
180
+ // the user whole; the next event will crash.
181
+ log.error(`CRITICAL: successful_payment without prior waiver — gate bypassed? userId=${userId} chargeId=${payment.telegram_payment_charge_id}`);
182
+ }
183
+ const charge = buildChargeRecord(product, userId, payment, waiverSnapshot ?? {
184
+ at: Date.now(),
185
+ version: opts.cfg.waiver.version,
186
+ locale: ctxLang(ctx),
187
+ });
188
+ // 1. Write the charge log + per-user index.
189
+ // (Idempotency sentinel was already claimed above.)
190
+ await opts.stores.charges.set(ctx, charge.chargeId, charge);
191
+ await opts.stores.userCharges(userId).prepend(ctx, charge.chargeId);
192
+ log.info(`charge persisted: chargeId=${charge.chargeId} product=${charge.productKey} user=${userId} xtr=${charge.xtr} creditsGranted=${charge.creditsGranted}`);
193
+ // 2. Apply to session (cache). Capture the previous vip chargeId
194
+ // FIRST so we can cancel its renewal below if this was an upgrade.
195
+ const previousVipChargeId = ctx.session.pay?.vip?.chargeId;
196
+ const previousVipRung = ctx.session.pay?.vip?.rung;
197
+ applyCharge(ctx.session, charge);
198
+ // 3. Tier-upgrade auto-cancel: when the user buys a HIGHER rung
199
+ // than their currently-active subscription, cancel the lower one's
200
+ // auto-renewal so they aren't double-billed at the next cycle. The
201
+ // lower rung keeps access until its period end — Telegram handles
202
+ // that silently because the higher rung's rank-based `atLeast`
203
+ // check already satisfies any vip.N check from now on.
204
+ if (charge.vipRung !== undefined &&
205
+ previousVipChargeId &&
206
+ previousVipChargeId !== charge.chargeId &&
207
+ previousVipRung !== undefined &&
208
+ previousVipRung < charge.vipRung) {
209
+ try {
210
+ await ctx.bot.api.editUserStarSubscription({
211
+ user_id: userId,
212
+ telegram_payment_charge_id: previousVipChargeId,
213
+ is_canceled: true,
214
+ });
215
+ log.info(`auto-canceled previous vip rung ${previousVipRung} on upgrade to ${charge.vipRung} (user=${userId})`);
216
+ }
217
+ catch (e) {
218
+ // Already canceled, expired, etc. Log; don't block fulfillment.
219
+ log.warn(`auto-cancel previous vip subscription failed (likely already canceled/expired): user=${userId} previousChargeId=${previousVipChargeId} err=${e instanceof Error ? e.message : String(e)}`);
220
+ }
221
+ }
222
+ // 4. User-facing confirmation
223
+ const lang = ctxLang(ctx);
224
+ await ctx.send(buildSuccessMessage(product, opts.cfg, lang));
225
+ // 5. Fire onFulfilled hooks (fire-and-forget)
226
+ const event = {
227
+ productKey: product.id,
228
+ userId,
229
+ chargeId: charge.chargeId,
230
+ xtr: charge.xtr,
231
+ receivedAt: charge.receivedAt,
232
+ };
233
+ const fireBucket = (key) => {
234
+ const arr = opts.onFulfilled.get(key);
235
+ if (!arr)
236
+ return;
237
+ for (const fn of arr) {
238
+ try {
239
+ fn(event, ctx);
240
+ }
241
+ catch (e) {
242
+ log.error(`onFulfilled handler for "${key}" threw: ${e}`);
243
+ }
244
+ }
245
+ };
246
+ fireBucket(product.id);
247
+ fireBucket("*");
248
+ log.success(`fulfillment complete: product=${product.id} user=${userId} chargeId=${charge.chargeId}`);
249
+ // 6. Sanity-fail AFTER everything has been persisted + the user
250
+ // has their confirmation — defensive, see CRITICAL log above.
251
+ if (!waiverSnapshot) {
252
+ panic("bot/payments: successful_payment fulfilled without a stored waiver " +
253
+ "snapshot. Gate logic has a hole. User was made whole; investigate.");
254
+ }
255
+ };
256
+ const buildSuccessMessage = (product, cfg, lang) => {
257
+ if (product.id.startsWith("vip.")) {
258
+ const v = product;
259
+ const name = v.name[lang] ??
260
+ v.name[FALLBACK_LANG] ??
261
+ "VIP";
262
+ return say({
263
+ en: `✅ Welcome to ${name}! Active for 30 days.`,
264
+ es: `✅ ¡Bienvenido a ${name}! Activo durante 30 días.`,
265
+ }, lang);
266
+ }
267
+ if (product.id.startsWith("credits.")) {
268
+ const c = product;
269
+ const unit = cfg.credits?.unit?.[lang] ??
270
+ "credits";
271
+ return say({
272
+ en: `✅ +${c.creditsGranted} ${unit} added to your balance.`,
273
+ es: `✅ +${c.creditsGranted} ${unit} añadidos a tu saldo.`,
274
+ }, lang);
275
+ }
276
+ const p = product;
277
+ const name = p.name[lang] ??
278
+ p.name[FALLBACK_LANG] ??
279
+ p.key;
280
+ return say({ en: `✅ Unlocked: ${name}.`, es: `✅ Desbloqueado: ${name}.` }, lang);
281
+ };
282
+ // ─── re-export SourcedError for catch sites ───────────────────────
283
+ export { SourcedError };
284
+ //# sourceMappingURL=handlers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handlers.js","sourceRoot":"","sources":["../../../src/bot/payments/handlers.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * `bot/payments` — public exports.
3
+ *
4
+ * The factory `botPayments({...})` is the only thing most consumers
5
+ * need; everything else is exported for advanced wiring, tests, and
6
+ * type narrowing at consumer side.
7
+ *
8
+ * See `CLAUDE.md` in this folder for the design rationale + compliance
9
+ * memo (Spanish autónomo, Art. 103(m), Telegram Stars seller-of-record
10
+ * analysis, …).
11
+ */
12
+ export type { ExportInput, ExportResult, PayoutsApi, RecordPayoutInput, } from "./payouts.js";
13
+ export type { BotPaymentsOptions, BotPaymentsResult, PaymentsCtx, } from "./plugin.js";
14
+ export { botPayments } from "./plugin.js";
15
+ export type { AtLeastKey, BotPaymentsConfig, ChargeRecord, CreditsConfig, CreditsPack, DerivedPaymentsState, FulfillmentEvent, LegalConfig, PaymentsSession, PayoutRecord, PaysupportState, Perk, PerkState, PerksConfig, ProductKey, SubscriptionPeriod, TierKey, VipRung, VipState, WaiverConfig, WaiverRecord, } from "./types.js";
16
+ export { InsufficientCredits } from "./types.js";
17
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/bot/payments/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,YAAY,EACX,WAAW,EACX,YAAY,EACZ,UAAU,EACV,iBAAiB,GACjB,MAAM,cAAc,CAAC;AACtB,YAAY,EACX,kBAAkB,EAClB,iBAAiB,EACjB,WAAW,GACX,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,YAAY,EACX,UAAU,EACV,iBAAiB,EACjB,YAAY,EACZ,aAAa,EACb,WAAW,EACX,oBAAoB,EACpB,gBAAgB,EAChB,WAAW,EACX,eAAe,EACf,YAAY,EACZ,eAAe,EACf,IAAI,EACJ,SAAS,EACT,WAAW,EACX,UAAU,EACV,kBAAkB,EAClB,OAAO,EACP,OAAO,EACP,QAAQ,EACR,YAAY,EACZ,YAAY,GACZ,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * `bot/payments` — public exports.
3
+ *
4
+ * The factory `botPayments({...})` is the only thing most consumers
5
+ * need; everything else is exported for advanced wiring, tests, and
6
+ * type narrowing at consumer side.
7
+ *
8
+ * See `CLAUDE.md` in this folder for the design rationale + compliance
9
+ * memo (Spanish autónomo, Art. 103(m), Telegram Stars seller-of-record
10
+ * analysis, …).
11
+ */
12
+ export { botPayments } from "./plugin.js";
13
+ export { InsufficientCredits } from "./types.js";
14
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/bot/payments/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAaH,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAwB1C,OAAO,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC"}
@@ -0,0 +1,101 @@
1
+ /**
2
+ * `sendInvoice` wrappers — one per product kind, plus the high-level
3
+ * `presentInvoice` entry that the `ctx.payments.invoice(...)` surface
4
+ * routes to.
5
+ *
6
+ * Stars-only by design (see CLAUDE.md §1, §2). Every invoice goes out
7
+ * with `currency: 'XTR'` and `provider_token: ""` — Telegram rejects
8
+ * provider_token for XTR. The `prices` array must contain exactly one
9
+ * `LabeledPrice` for XTR per the Bot API docs.
10
+ *
11
+ * The function calls `bot.api.sendInvoice` directly (not `ctx.sendInvoice`)
12
+ * so the same path works from both message and callback_query contexts
13
+ * — `message_thread_id` is plumbed explicitly so threaded replies land
14
+ * in the right thread regardless of which mixin would otherwise inject it.
15
+ *
16
+ * https://core.telegram.org/bots/api#sendinvoice
17
+ * https://core.telegram.org/bots/payments-stars
18
+ */
19
+ import type { BotPaymentsConfig, CreditsPackResolved, PaymentsSession, PerkResolved, ProductCatalog, VipRungResolved } from "./types.js";
20
+ /**
21
+ * Structural shape we need from the calling ctx. Matches both
22
+ * `MessageContext` and `CallbackQueryContext` after gramio's mixins —
23
+ * we pull `chat.id` / `threadId` from either layout via short-circuits.
24
+ */
25
+ export type InvoiceCtx = {
26
+ bot: {
27
+ api: {
28
+ sendInvoice: (params: SendInvoiceParams) => Promise<unknown>;
29
+ };
30
+ };
31
+ from?: {
32
+ id: number;
33
+ };
34
+ chat?: {
35
+ id: number;
36
+ };
37
+ threadId?: number;
38
+ message?: {
39
+ threadId?: number;
40
+ chat?: {
41
+ id: number;
42
+ };
43
+ };
44
+ session?: {
45
+ pay?: PaymentsSession;
46
+ language?: string;
47
+ };
48
+ };
49
+ /** Subset of the Telegram `sendInvoice` schema this module needs. */
50
+ type SendInvoiceParams = {
51
+ chat_id: number;
52
+ message_thread_id?: number;
53
+ title: string;
54
+ description: string;
55
+ payload: string;
56
+ provider_token: string;
57
+ currency: "XTR";
58
+ prices: Array<{
59
+ label: string;
60
+ amount: number;
61
+ }>;
62
+ subscription_period?: number;
63
+ start_parameter?: string;
64
+ };
65
+ /**
66
+ * Issue a Telegram Stars invoice for `product`. Caller is expected to
67
+ * have already cleared waiver consent — `presentInvoice` (the public
68
+ * entry) handles that gate. This function does the wire call only.
69
+ *
70
+ * Wraps Telegram-side failures in a typed `SourcedError({ source:
71
+ * 'telegram', operation: 'sendInvoice' })` so the catch boundary in
72
+ * `plugin.ts` can render a localized fallback message.
73
+ */
74
+ export declare const sendInvoiceForProduct: (ctx: InvoiceCtx, cfg: BotPaymentsConfig<string>, product: VipRungResolved | CreditsPackResolved | PerkResolved, userId: number) => Promise<void>;
75
+ /**
76
+ * What the entry returned to mean. `'invoice_sent'` = the user got a
77
+ * Telegram payment sheet; `'waiver_prompt_sent'` = the user got the
78
+ * consent prompt and must tap before the invoice goes out;
79
+ * `'unknown_product'` = the key didn't resolve and we already told the
80
+ * user nothing happened.
81
+ */
82
+ export type PresentInvoiceResult = "invoice_sent" | "waiver_prompt_sent" | "unknown_product";
83
+ /**
84
+ * High-level entry for `ctx.payments.invoice(productKey)`.
85
+ *
86
+ * Validates the productKey against the catalog, gates on waiver
87
+ * freshness, and either:
88
+ *
89
+ * - sends the consent prompt (one-tap "✅ Consiento" button carrying
90
+ * the productKey, so the user resumes the purchase on tap), OR
91
+ * - calls `sendInvoiceForProduct` directly when consent is already
92
+ * fresh.
93
+ *
94
+ * Returns a discriminated tag so callers can branch on what the user
95
+ * saw — useful for `require()`-style helpers.
96
+ */
97
+ export declare const presentInvoice: (ctx: InvoiceCtx & {
98
+ send: (text: string, params?: object) => Promise<unknown>;
99
+ }, cfg: BotPaymentsConfig<string>, catalog: ProductCatalog, productKey: string) => Promise<PresentInvoiceResult>;
100
+ export {};
101
+ //# sourceMappingURL=invoice.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"invoice.d.ts","sourceRoot":"","sources":["../../../src/bot/payments/invoice.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAMH,OAAO,KAAK,EACX,iBAAiB,EACjB,mBAAmB,EACnB,eAAe,EACf,YAAY,EACZ,cAAc,EACd,eAAe,EACf,MAAM,YAAY,CAAC;AAWpB;;;;GAIG;AACH,MAAM,MAAM,UAAU,GAAG;IACxB,GAAG,EAAE;QACJ,GAAG,EAAE;YACJ,WAAW,EAAE,CAAC,MAAM,EAAE,iBAAiB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;SAC7D,CAAC;KACF,CAAC;IACF,IAAI,CAAC,EAAE;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC;IACtB,IAAI,CAAC,EAAE;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE;YAAE,EAAE,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,CAAC;IACvD,OAAO,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,eAAe,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACvD,CAAC;AAEF,qEAAqE;AACrE,KAAK,iBAAiB,GAAG;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,KAAK,CAAC;IAChB,MAAM,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACjD,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,eAAe,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AA8HF;;;;;;;;GAQG;AACH,eAAO,MAAM,qBAAqB,GACjC,KAAK,UAAU,EACf,KAAK,iBAAiB,CAAC,MAAM,CAAC,EAC9B,SAAS,eAAe,GAAG,mBAAmB,GAAG,YAAY,EAC7D,QAAQ,MAAM,KACZ,OAAO,CAAC,IAAI,CAsDd,CAAC;AAIF;;;;;;GAMG;AACH,MAAM,MAAM,oBAAoB,GAC7B,cAAc,GACd,oBAAoB,GACpB,iBAAiB,CAAC;AAErB;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,cAAc,GAC1B,KAAK,UAAU,GAAG;IACjB,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CAC1D,EACD,KAAK,iBAAiB,CAAC,MAAM,CAAC,EAC9B,SAAS,cAAc,EACvB,YAAY,MAAM,KAChB,OAAO,CAAC,oBAAoB,CAuC9B,CAAC"}
@@ -0,0 +1,213 @@
1
+ /**
2
+ * `sendInvoice` wrappers — one per product kind, plus the high-level
3
+ * `presentInvoice` entry that the `ctx.payments.invoice(...)` surface
4
+ * routes to.
5
+ *
6
+ * Stars-only by design (see CLAUDE.md §1, §2). Every invoice goes out
7
+ * with `currency: 'XTR'` and `provider_token: ""` — Telegram rejects
8
+ * provider_token for XTR. The `prices` array must contain exactly one
9
+ * `LabeledPrice` for XTR per the Bot API docs.
10
+ *
11
+ * The function calls `bot.api.sendInvoice` directly (not `ctx.sendInvoice`)
12
+ * so the same path works from both message and callback_query contexts
13
+ * — `message_thread_id` is plumbed explicitly so threaded replies land
14
+ * in the right thread regardless of which mixin would otherwise inject it.
15
+ *
16
+ * https://core.telegram.org/bots/api#sendinvoice
17
+ * https://core.telegram.org/bots/payments-stars
18
+ */
19
+ import { panic, SourcedError } from "../../offensive.js";
20
+ import { say } from "../../say/index.js";
21
+ import { createLogger } from "../../universal/log.js";
22
+ import { encodePayload } from "./payload.js";
23
+ import { renderConsentBody, waiverKeyboard } from "./waiver.js";
24
+ const log = createLogger("bot/payments");
25
+ // Telegram caps from the Bot API (sendInvoice §):
26
+ const TITLE_MAX = 32;
27
+ const DESCRIPTION_MAX = 255;
28
+ const FALLBACK_LANG = "en";
29
+ // ─── small helpers ─────────────────────────────────────────────────
30
+ const ctxLang = (ctx) => ctx.session?.language ?? FALLBACK_LANG;
31
+ const ctxChatId = (ctx) => {
32
+ const id = ctx.chat?.id ?? ctx.message?.chat?.id;
33
+ if (id === undefined) {
34
+ panic("bot/payments/invoice: ctx is missing chat.id (neither top-level nor message.chat)");
35
+ }
36
+ return id;
37
+ };
38
+ const ctxThreadId = (ctx) => ctx.threadId ?? ctx.message?.threadId;
39
+ /**
40
+ * Resolve a Polyglot to a single string with a fallback chain. Trims
41
+ * + caps at `maxLen`.
42
+ */
43
+ const resolveLabel = (value, lang, maxLen) => {
44
+ if (!value)
45
+ return undefined;
46
+ const v = value[lang] ?? value[FALLBACK_LANG] ?? Object.values(value)[0];
47
+ if (typeof v !== "string")
48
+ return undefined;
49
+ const trimmed = v.trim();
50
+ return trimmed.length > maxLen ? trimmed.slice(0, maxLen) : trimmed;
51
+ };
52
+ // ─── title / description per product kind ──────────────────────────
53
+ const titleFor = (product, cfg, lang) => {
54
+ if (product.id.startsWith("vip.")) {
55
+ return (resolveLabel(product.name, lang, TITLE_MAX) ??
56
+ say({ en: "VIP", es: "VIP" }, lang));
57
+ }
58
+ if (product.id.startsWith("credits.")) {
59
+ const p = product;
60
+ const explicit = resolveLabel(p.name, lang, TITLE_MAX);
61
+ if (explicit)
62
+ return explicit;
63
+ const unit = resolveLabel(cfg.credits?.unit, lang, 32) ??
64
+ "credits";
65
+ // "100 credits" / "100 mensajes" — fits in 32 chars for reasonable values.
66
+ const base = `${p.creditsGranted} ${unit}`;
67
+ return base.length > TITLE_MAX ? base.slice(0, TITLE_MAX) : base;
68
+ }
69
+ // perk
70
+ return (resolveLabel(product.name, lang, TITLE_MAX) ??
71
+ product.key);
72
+ };
73
+ const descriptionFor = (product, cfg, lang) => {
74
+ if (product.id.startsWith("vip.")) {
75
+ const v = product;
76
+ const name = resolveLabel(v.name, lang, 24) ?? say({ en: "VIP", es: "VIP" }, lang);
77
+ const baseEn = `30 days of ${name} access`;
78
+ const baseEs = `30 días de acceso ${name}`;
79
+ const base = say({ en: baseEn, es: baseEs }, lang);
80
+ if (v.creditsGranted > 0) {
81
+ const unit = resolveLabel(cfg.credits?.unit, lang, 24) ??
82
+ "credits";
83
+ const tailEn = `. Includes ${v.creditsGranted} ${unit} per month.`;
84
+ const tailEs = `. Incluye ${v.creditsGranted} ${unit} al mes.`;
85
+ const tail = say({ en: tailEn, es: tailEs }, lang);
86
+ const full = base + tail;
87
+ return full.length > DESCRIPTION_MAX
88
+ ? full.slice(0, DESCRIPTION_MAX)
89
+ : full;
90
+ }
91
+ return base.length > DESCRIPTION_MAX
92
+ ? base.slice(0, DESCRIPTION_MAX)
93
+ : base;
94
+ }
95
+ if (product.id.startsWith("credits.")) {
96
+ const p = product;
97
+ const unit = resolveLabel(cfg.credits?.unit, lang, 24) ??
98
+ "credits";
99
+ const base = say({
100
+ en: `Top up your balance by ${p.creditsGranted} ${unit}.`,
101
+ es: `Recarga tu saldo con ${p.creditsGranted} ${unit}.`,
102
+ }, lang);
103
+ return base.length > DESCRIPTION_MAX
104
+ ? base.slice(0, DESCRIPTION_MAX)
105
+ : base;
106
+ }
107
+ // perk
108
+ const p = product;
109
+ const name = resolveLabel(p.name, lang, 24) ?? p.key;
110
+ const base = say({
111
+ en: `Unlock ${name} — one-time.`,
112
+ es: `Desbloquear ${name} — único pago.`,
113
+ }, lang);
114
+ return base.length > DESCRIPTION_MAX ? base.slice(0, DESCRIPTION_MAX) : base;
115
+ };
116
+ // ─── core send ─────────────────────────────────────────────────────
117
+ /**
118
+ * Issue a Telegram Stars invoice for `product`. Caller is expected to
119
+ * have already cleared waiver consent — `presentInvoice` (the public
120
+ * entry) handles that gate. This function does the wire call only.
121
+ *
122
+ * Wraps Telegram-side failures in a typed `SourcedError({ source:
123
+ * 'telegram', operation: 'sendInvoice' })` so the catch boundary in
124
+ * `plugin.ts` can render a localized fallback message.
125
+ */
126
+ export const sendInvoiceForProduct = async (ctx, cfg, product, userId) => {
127
+ const lang = ctxLang(ctx);
128
+ const chatId = ctxChatId(ctx);
129
+ const threadId = ctxThreadId(ctx);
130
+ const title = titleFor(product, cfg, lang);
131
+ const description = descriptionFor(product, cfg, lang);
132
+ const payload = encodePayload(product.id, userId);
133
+ const priceLabel = resolveLabel(product.name, lang, 32) ??
134
+ title;
135
+ const params = {
136
+ chat_id: chatId,
137
+ ...(threadId !== undefined && { message_thread_id: threadId }),
138
+ title,
139
+ description,
140
+ payload,
141
+ provider_token: "", // XTR — must be empty
142
+ currency: "XTR",
143
+ prices: [{ label: priceLabel, amount: product.xtr }],
144
+ };
145
+ if (product.id.startsWith("vip.")) {
146
+ params.subscription_period = product.periodSeconds;
147
+ }
148
+ log.event(`sendInvoice → telegram: product=${product.id} user=${userId} xtr=${product.xtr} title="${title}" subscription_period=${params.subscription_period ?? "none"}`);
149
+ try {
150
+ await ctx.bot.api.sendInvoice(params);
151
+ log.success(`sendInvoice accepted by telegram: product=${product.id} user=${userId}`);
152
+ }
153
+ catch (e) {
154
+ const msg = e instanceof Error ? e.message : String(e);
155
+ log.error(`sendInvoice rejected by telegram: ${msg}`);
156
+ // Hint on the specific subscription-export setup gotcha — there's
157
+ // no documented fix path in gramio/telegram docs, so surface ours.
158
+ if (msg.includes("SUBSCRIPTION_EXPORT_MISSING")) {
159
+ log.warn("hint: open BotFather → /mybots → your bot → Bot Settings → Star Subscriptions and link a Fragment payout method before sending subscription invoices");
160
+ }
161
+ throw new SourcedError({
162
+ source: "telegram",
163
+ operation: "sendInvoice",
164
+ message: msg,
165
+ cause: e,
166
+ context: { productKey: product.id, userId, chatId, threadId },
167
+ });
168
+ }
169
+ };
170
+ /**
171
+ * High-level entry for `ctx.payments.invoice(productKey)`.
172
+ *
173
+ * Validates the productKey against the catalog, gates on waiver
174
+ * freshness, and either:
175
+ *
176
+ * - sends the consent prompt (one-tap "✅ Consiento" button carrying
177
+ * the productKey, so the user resumes the purchase on tap), OR
178
+ * - calls `sendInvoiceForProduct` directly when consent is already
179
+ * fresh.
180
+ *
181
+ * Returns a discriminated tag so callers can branch on what the user
182
+ * saw — useful for `require()`-style helpers.
183
+ */
184
+ export const presentInvoice = async (ctx, cfg, catalog, productKey) => {
185
+ const product = catalog.byKey.get(productKey);
186
+ if (!product) {
187
+ log.warn(`presentInvoice unknown productKey "${productKey}"`);
188
+ const lang = ctxLang(ctx);
189
+ await ctx.send(say({
190
+ en: `❌ Unknown product "${productKey}".`,
191
+ es: `❌ Producto desconocido "${productKey}".`,
192
+ }, lang));
193
+ return "unknown_product";
194
+ }
195
+ const userId = ctx.from?.id;
196
+ if (!userId) {
197
+ panic("bot/payments/invoice: presentInvoice called with no ctx.from.id");
198
+ }
199
+ const stored = ctx.session?.pay?.waiver;
200
+ const fresh = stored && stored.version === cfg.waiver.version;
201
+ if (!fresh) {
202
+ log.info(`presentInvoice: waiver ${stored ? `stale (${stored.version} ≠ ${cfg.waiver.version})` : "missing"} — sending consent prompt for ${product.id} user=${userId}`);
203
+ const lang = ctxLang(ctx);
204
+ await ctx.send(renderConsentBody(cfg, lang), {
205
+ reply_markup: waiverKeyboard(cfg, product.id, lang),
206
+ });
207
+ return "waiver_prompt_sent";
208
+ }
209
+ log.info(`presentInvoice: waiver fresh — going straight to sendInvoice for ${product.id} user=${userId}`);
210
+ await sendInvoiceForProduct(ctx, cfg, product, userId);
211
+ return "invoice_sent";
212
+ };
213
+ //# sourceMappingURL=invoice.js.map