@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,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
|