@adriangalilea/utils 0.17.1 → 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 +9 -9
  2. package/dist/bot/access-control.d.ts.map +1 -1
  3. package/dist/bot/access-control.js +62 -45
  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 +93 -41
  18. package/dist/bot/kit.d.ts.map +1 -1
  19. package/dist/bot/kit.js +153 -30
  20. package/dist/bot/kit.js.map +1 -1
  21. package/dist/bot/menu.d.ts +31 -11
  22. package/dist/bot/menu.d.ts.map +1 -1
  23. package/dist/bot/menu.js +34 -9
  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,58 @@
1
+ /**
2
+ * Runtime schemas for the persisted ledger records. Wired into
3
+ * `stores.ts` as the optional validator on `botRecord<T>` — every read
4
+ * from storage gets `.parse()`'d, so a corrupted Redis record throws
5
+ * a clear `SourcedError({ source: 'storage', operation: 'validate' })`
6
+ * instead of NaN-ing downstream.
7
+ *
8
+ * Why zod? It's the canonical TS validator with the right API surface
9
+ * (`{ parse(data): T }`). The library's `RecordValidator<T>` type is
10
+ * structural, so swapping to valibot / ArkType / a hand-rolled guard
11
+ * just works — but zod is what we ship the default with.
12
+ *
13
+ * Schemas mirror the TS types in `types.ts` 1:1. Keep them in sync; the
14
+ * type system enforces that via the `z.infer<>` round-trip — if a
15
+ * schema drifts from the type, `satisfies ZodType<X>` on each export
16
+ * blows up. We use `satisfies` (not `as`) so the TS error points at the
17
+ * mismatch, not the assertion.
18
+ */
19
+ import { z } from "zod";
20
+ /**
21
+ * `ChargeRecord` schema. Mutable fields stay loose-typed (zod doesn't
22
+ * enforce readonly the way TS does) but every field is structurally
23
+ * present.
24
+ */
25
+ export declare const chargeRecordSchema: z.ZodObject<{
26
+ chargeId: z.ZodString;
27
+ userId: z.ZodNumber;
28
+ productKey: z.ZodString;
29
+ xtr: z.ZodNumber;
30
+ receivedAt: z.ZodNumber;
31
+ payload: z.ZodString;
32
+ waiverSnapshot: z.ZodObject<{
33
+ at: z.ZodNumber;
34
+ version: z.ZodString;
35
+ locale: z.ZodString;
36
+ }, z.core.$strip>;
37
+ subscriptionExpiresAt: z.ZodOptional<z.ZodNumber>;
38
+ creditsGranted: z.ZodNumber;
39
+ vipRung: z.ZodOptional<z.ZodNumber>;
40
+ perkKey: z.ZodOptional<z.ZodString>;
41
+ paysupportState: z.ZodEnum<{
42
+ none: "none";
43
+ opened: "opened";
44
+ refunded: "refunded";
45
+ }>;
46
+ refundedAt: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
47
+ payoutBatchId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
48
+ }, z.core.$strip>;
49
+ export declare const payoutRecordSchema: z.ZodObject<{
50
+ batchId: z.ZodString;
51
+ fromMs: z.ZodNumber;
52
+ toMs: z.ZodNumber;
53
+ tonAmount: z.ZodNumber;
54
+ eurAtReceipt: z.ZodNumber;
55
+ recordedAt: z.ZodNumber;
56
+ facturaNumber: z.ZodOptional<z.ZodString>;
57
+ }, z.core.$strip>;
58
+ //# sourceMappingURL=schemas.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schemas.d.ts","sourceRoot":"","sources":["../../../src/bot/payments/schemas.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAUxB;;;;GAIG;AACH,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;iBAeK,CAAC;AAErC,eAAO,MAAM,kBAAkB;;;;;;;;iBAQK,CAAC"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Runtime schemas for the persisted ledger records. Wired into
3
+ * `stores.ts` as the optional validator on `botRecord<T>` — every read
4
+ * from storage gets `.parse()`'d, so a corrupted Redis record throws
5
+ * a clear `SourcedError({ source: 'storage', operation: 'validate' })`
6
+ * instead of NaN-ing downstream.
7
+ *
8
+ * Why zod? It's the canonical TS validator with the right API surface
9
+ * (`{ parse(data): T }`). The library's `RecordValidator<T>` type is
10
+ * structural, so swapping to valibot / ArkType / a hand-rolled guard
11
+ * just works — but zod is what we ship the default with.
12
+ *
13
+ * Schemas mirror the TS types in `types.ts` 1:1. Keep them in sync; the
14
+ * type system enforces that via the `z.infer<>` round-trip — if a
15
+ * schema drifts from the type, `satisfies ZodType<X>` on each export
16
+ * blows up. We use `satisfies` (not `as`) so the TS error points at the
17
+ * mismatch, not the assertion.
18
+ */
19
+ import { z } from "zod";
20
+ const waiverRecordSchema = z.object({
21
+ at: z.number(),
22
+ version: z.string(),
23
+ locale: z.string(),
24
+ });
25
+ /**
26
+ * `ChargeRecord` schema. Mutable fields stay loose-typed (zod doesn't
27
+ * enforce readonly the way TS does) but every field is structurally
28
+ * present.
29
+ */
30
+ export const chargeRecordSchema = z.object({
31
+ chargeId: z.string(),
32
+ userId: z.number(),
33
+ productKey: z.string(),
34
+ xtr: z.number(),
35
+ receivedAt: z.number(),
36
+ payload: z.string(),
37
+ waiverSnapshot: waiverRecordSchema,
38
+ subscriptionExpiresAt: z.number().optional(),
39
+ creditsGranted: z.number(),
40
+ vipRung: z.number().optional(),
41
+ perkKey: z.string().optional(),
42
+ paysupportState: z.enum(["none", "opened", "refunded"]),
43
+ refundedAt: z.number().nullable().optional(),
44
+ payoutBatchId: z.string().nullable().optional(),
45
+ });
46
+ export const payoutRecordSchema = z.object({
47
+ batchId: z.string(),
48
+ fromMs: z.number(),
49
+ toMs: z.number(),
50
+ tonAmount: z.number(),
51
+ eurAtReceipt: z.number(),
52
+ recordedAt: z.number(),
53
+ facturaNumber: z.string().optional(),
54
+ });
55
+ //# sourceMappingURL=schemas.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schemas.js","sourceRoot":"","sources":["../../../src/bot/payments/schemas.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC;IACnC,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE;IACd,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE;IACnB,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE;CAClB,CAAmC,CAAC;AAErC;;;;GAIG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC1C,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE;IACpB,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE;IAClB,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE;IACtB,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE;IACf,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE;IACtB,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE;IACnB,cAAc,EAAE,kBAAkB;IAClC,qBAAqB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC5C,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE;IAC1B,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC9B,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC9B,eAAe,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,QAAQ,EAAE,UAAU,CAAC,CAAC;IACvD,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;IAC5C,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;CAC/C,CAAmC,CAAC;AAErC,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC1C,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE;IACnB,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE;IAClB,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;IAChB,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;IACrB,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE;IACxB,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE;IACtB,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CACpC,CAAmC,CAAC"}
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Pure state computation over the charge log.
3
+ *
4
+ * This file is `state.ts` (not `derive.ts`) because gramio's `derive`
5
+ * concept is reserved for ctx decoration (see `derive.ts` alongside,
6
+ * which builds `ctx.payments`). The reducers here are bot-id and ctx
7
+ * agnostic — they take `ChargeRecord[]` and a session-shaped object
8
+ * and mutate the session in place.
9
+ *
10
+ * The charge log (`pay:charge:{chargeId}`) is the single source of truth
11
+ * (CLAUDE.md §"Storage layout"). Session state is a cache for O(1)
12
+ * runtime checks. This module owns the function that rebuilds the cache
13
+ * from the log.
14
+ *
15
+ * Two surfaces:
16
+ *
17
+ * `deriveState(charges)` — pure, reduces a chronological list of
18
+ * NON-refunded charges into the cache
19
+ * shape. Used by reconcile and refund
20
+ * (after marking the refunded charge).
21
+ *
22
+ * `applyCharge(session, …)` — incremental, mutates one session record
23
+ * for a single new charge. Used by
24
+ * `handlers.ts` on `successful_payment`.
25
+ *
26
+ * `revertCreditsForCharge(session, charge)` — targeted decrement
27
+ * used by refund (vip/perks are full
28
+ * re-derive territory; credits is not,
29
+ * because consumption isn't in the log).
30
+ *
31
+ * Credits balance is **not fully derivable** from the log alone — the
32
+ * log records grants, not consumption. `deriveState` therefore returns
33
+ * `totalCreditsGranted` (sum) and callers decide whether to use it as
34
+ * the balance (reconcile of fresh state) or to ignore (incremental).
35
+ */
36
+ import type { ChargeRecord, DerivedPaymentsState, PaymentsSession } from "./types.js";
37
+ /**
38
+ * Result of `deriveState`. `totalCreditsGranted` is informational —
39
+ * subtract consumption (which isn't logged) for the real balance.
40
+ */
41
+ export type DeriveResult = DerivedPaymentsState & {
42
+ readonly totalCreditsGranted: number;
43
+ };
44
+ /**
45
+ * Pure reducer over a chronological list of charges. Caller is
46
+ * responsible for filtering out refunded ones if they want
47
+ * "current state after refunds" semantics.
48
+ */
49
+ export declare const deriveState: (charges: ReadonlyArray<ChargeRecord>) => DeriveResult;
50
+ /**
51
+ * Apply a single new charge to a session in place. Idempotent across
52
+ * `chargeId` because the caller (handlers.ts) guards via the
53
+ * `pay:idempotency:{chargeId}` sentinel before this fires; we don't
54
+ * re-check here.
55
+ *
56
+ * - vip: replaces `session.pay.vip` (renewals update expiresAt;
57
+ * tier upgrades replace rung)
58
+ * - credits: increments `session.pay.credits` by the granted amount
59
+ * (covers both pack purchases AND vip renewal grants)
60
+ * - perks: set-if-absent on `session.pay.perks[key]`
61
+ */
62
+ export declare const applyCharge: (session: {
63
+ pay?: PaymentsSession;
64
+ }, charge: ChargeRecord) => void;
65
+ /**
66
+ * Subtract a refunded charge's `creditsGranted` from the session
67
+ * balance, clamping at zero. The "clamp at zero" semantics are
68
+ * deliberate: a user may have already consumed credits when the refund
69
+ * arrives, and going negative would let them buy back into a debt — we
70
+ * refuse that and accept the asymmetry (the refunded Stars are already
71
+ * leaving the bot's balance via `refundStarPayment` either way).
72
+ */
73
+ export declare const revertCreditsForCharge: (session: {
74
+ pay?: PaymentsSession;
75
+ }, charge: ChargeRecord) => void;
76
+ /**
77
+ * Rebuild vip + perks (the fully-derivable parts of session.pay.*)
78
+ * from a fresh chronological view of the charge log, **after** the
79
+ * caller has marked any newly-refunded charges. Credits balance is
80
+ * NOT touched — caller handles that explicitly via
81
+ * `revertCreditsForCharge` for the targeted decrement semantics.
82
+ *
83
+ * Used by:
84
+ * - refund.ts after `refundStarPayment` succeeds
85
+ * - reconcile (admin command) for full state recovery
86
+ */
87
+ export declare const rebuildVipAndPerks: (session: {
88
+ pay?: PaymentsSession;
89
+ }, charges: ReadonlyArray<ChargeRecord>) => void;
90
+ //# sourceMappingURL=state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../../src/bot/payments/state.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,OAAO,KAAK,EACX,YAAY,EACZ,oBAAoB,EACpB,eAAe,EAGf,MAAM,YAAY,CAAC;AAEpB;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG,oBAAoB,GAAG;IACjD,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC;CACrC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,WAAW,GACvB,SAAS,aAAa,CAAC,YAAY,CAAC,KAClC,YAwCF,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,WAAW,GACvB,SAAS;IAAE,GAAG,CAAC,EAAE,eAAe,CAAA;CAAE,EAClC,QAAQ,YAAY,KAClB,IA0BF,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,GAClC,SAAS;IAAE,GAAG,CAAC,EAAE,eAAe,CAAA;CAAE,EAClC,QAAQ,YAAY,KAClB,IAMF,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,kBAAkB,GAC9B,SAAS;IAAE,GAAG,CAAC,EAAE,eAAe,CAAA;CAAE,EAClC,SAAS,aAAa,CAAC,YAAY,CAAC,KAClC,IAMF,CAAC"}
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Pure state computation over the charge log.
3
+ *
4
+ * This file is `state.ts` (not `derive.ts`) because gramio's `derive`
5
+ * concept is reserved for ctx decoration (see `derive.ts` alongside,
6
+ * which builds `ctx.payments`). The reducers here are bot-id and ctx
7
+ * agnostic — they take `ChargeRecord[]` and a session-shaped object
8
+ * and mutate the session in place.
9
+ *
10
+ * The charge log (`pay:charge:{chargeId}`) is the single source of truth
11
+ * (CLAUDE.md §"Storage layout"). Session state is a cache for O(1)
12
+ * runtime checks. This module owns the function that rebuilds the cache
13
+ * from the log.
14
+ *
15
+ * Two surfaces:
16
+ *
17
+ * `deriveState(charges)` — pure, reduces a chronological list of
18
+ * NON-refunded charges into the cache
19
+ * shape. Used by reconcile and refund
20
+ * (after marking the refunded charge).
21
+ *
22
+ * `applyCharge(session, …)` — incremental, mutates one session record
23
+ * for a single new charge. Used by
24
+ * `handlers.ts` on `successful_payment`.
25
+ *
26
+ * `revertCreditsForCharge(session, charge)` — targeted decrement
27
+ * used by refund (vip/perks are full
28
+ * re-derive territory; credits is not,
29
+ * because consumption isn't in the log).
30
+ *
31
+ * Credits balance is **not fully derivable** from the log alone — the
32
+ * log records grants, not consumption. `deriveState` therefore returns
33
+ * `totalCreditsGranted` (sum) and callers decide whether to use it as
34
+ * the balance (reconcile of fresh state) or to ignore (incremental).
35
+ */
36
+ /**
37
+ * Pure reducer over a chronological list of charges. Caller is
38
+ * responsible for filtering out refunded ones if they want
39
+ * "current state after refunds" semantics.
40
+ */
41
+ export const deriveState = (charges) => {
42
+ let vip;
43
+ const perks = {};
44
+ let totalCreditsGranted = 0;
45
+ // Charges are oldest-first by convention. Vip latest-wins by
46
+ // receivedAt; perks first-wins (an idempotent unlock — buying twice
47
+ // shouldn't change the unlock date).
48
+ for (const c of charges) {
49
+ if (c.paysupportState === "refunded")
50
+ continue;
51
+ totalCreditsGranted += c.creditsGranted;
52
+ if (c.vipRung !== undefined && c.subscriptionExpiresAt !== undefined) {
53
+ // Replace the active subscription only when this charge is newer
54
+ // or this is the first vip charge encountered.
55
+ if (!vip || c.receivedAt >= (vip.expiresAt ?? 0) || c.receivedAt > 0) {
56
+ vip = {
57
+ rung: c.vipRung,
58
+ chargeId: c.chargeId,
59
+ expiresAt: c.subscriptionExpiresAt,
60
+ canceled: false,
61
+ };
62
+ }
63
+ }
64
+ if (c.perkKey && !perks[c.perkKey]) {
65
+ perks[c.perkKey] = {
66
+ chargeId: c.chargeId,
67
+ at: c.receivedAt,
68
+ };
69
+ }
70
+ }
71
+ return {
72
+ credits: totalCreditsGranted,
73
+ vip,
74
+ perks,
75
+ totalCreditsGranted,
76
+ };
77
+ };
78
+ /**
79
+ * Apply a single new charge to a session in place. Idempotent across
80
+ * `chargeId` because the caller (handlers.ts) guards via the
81
+ * `pay:idempotency:{chargeId}` sentinel before this fires; we don't
82
+ * re-check here.
83
+ *
84
+ * - vip: replaces `session.pay.vip` (renewals update expiresAt;
85
+ * tier upgrades replace rung)
86
+ * - credits: increments `session.pay.credits` by the granted amount
87
+ * (covers both pack purchases AND vip renewal grants)
88
+ * - perks: set-if-absent on `session.pay.perks[key]`
89
+ */
90
+ export const applyCharge = (session, charge) => {
91
+ session.pay ??= {};
92
+ const pay = session.pay;
93
+ if (charge.creditsGranted > 0) {
94
+ pay.credits = (pay.credits ?? 0) + charge.creditsGranted;
95
+ }
96
+ if (charge.vipRung !== undefined &&
97
+ charge.subscriptionExpiresAt !== undefined) {
98
+ pay.vip = {
99
+ rung: charge.vipRung,
100
+ chargeId: charge.chargeId,
101
+ expiresAt: charge.subscriptionExpiresAt,
102
+ canceled: false,
103
+ };
104
+ }
105
+ if (charge.perkKey) {
106
+ pay.perks ??= {};
107
+ if (!pay.perks[charge.perkKey]) {
108
+ pay.perks[charge.perkKey] = {
109
+ chargeId: charge.chargeId,
110
+ at: charge.receivedAt,
111
+ };
112
+ }
113
+ }
114
+ };
115
+ /**
116
+ * Subtract a refunded charge's `creditsGranted` from the session
117
+ * balance, clamping at zero. The "clamp at zero" semantics are
118
+ * deliberate: a user may have already consumed credits when the refund
119
+ * arrives, and going negative would let them buy back into a debt — we
120
+ * refuse that and accept the asymmetry (the refunded Stars are already
121
+ * leaving the bot's balance via `refundStarPayment` either way).
122
+ */
123
+ export const revertCreditsForCharge = (session, charge) => {
124
+ if (!charge.creditsGranted)
125
+ return;
126
+ session.pay ??= {};
127
+ const pay = session.pay;
128
+ const current = pay.credits ?? 0;
129
+ pay.credits = Math.max(0, current - charge.creditsGranted);
130
+ };
131
+ /**
132
+ * Rebuild vip + perks (the fully-derivable parts of session.pay.*)
133
+ * from a fresh chronological view of the charge log, **after** the
134
+ * caller has marked any newly-refunded charges. Credits balance is
135
+ * NOT touched — caller handles that explicitly via
136
+ * `revertCreditsForCharge` for the targeted decrement semantics.
137
+ *
138
+ * Used by:
139
+ * - refund.ts after `refundStarPayment` succeeds
140
+ * - reconcile (admin command) for full state recovery
141
+ */
142
+ export const rebuildVipAndPerks = (session, charges) => {
143
+ const derived = deriveState(charges);
144
+ session.pay ??= {};
145
+ const pay = session.pay;
146
+ pay.vip = derived.vip;
147
+ pay.perks = derived.perks;
148
+ };
149
+ //# sourceMappingURL=state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.js","sourceRoot":"","sources":["../../../src/bot/payments/state.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * The plugin's storage layout, in one place.
3
+ *
4
+ * Replaces the scattered `chargeKey` / `userChargesIndexKey` /
5
+ * `idempotencyKey` / `payoutKey` / `payoutsIndexKey` helpers that used
6
+ * to live in `handlers.ts`, `refund.ts`, `payouts.ts`. Every storage
7
+ * operation in the plugin goes through one of these handles.
8
+ *
9
+ * Built once at plugin construction (in `plugin.ts`) and threaded
10
+ * through every concern that needs it.
11
+ */
12
+ import type { Storage } from "@gramio/storage";
13
+ import { botIndex, botRecord, botSentinel } from "../storage.js";
14
+ import type { ChargeRecord, PayoutRecord } from "./types.js";
15
+ export type PaymentsStores = {
16
+ /** `pay:charge:<chargeId>` — authoritative per-charge record. */
17
+ readonly charges: ReturnType<typeof botRecord<ChargeRecord>>;
18
+ /** `pay:idem:<chargeId>` — fulfillment idempotency sentinel. */
19
+ readonly idempotency: ReturnType<typeof botSentinel>;
20
+ /**
21
+ * `pay:user:<userId>:charges` — newest-first cap of chargeIds for
22
+ * `/refunds` listing + menu history (v2). Factory because the key
23
+ * is per-user.
24
+ */
25
+ readonly userCharges: (userId: number) => ReturnType<typeof botIndex>;
26
+ /** `pay:payout:<batchId>` — Fragment payout record. */
27
+ readonly payouts: ReturnType<typeof botRecord<PayoutRecord>>;
28
+ /** `pay:payouts:index` — newest-first batchIds for export. */
29
+ readonly payoutsIndex: ReturnType<typeof botIndex>;
30
+ };
31
+ /**
32
+ * Construct the plugin's storage handles. Called once at plugin
33
+ * construction; the result is passed into every concern that reads or
34
+ * writes plugin state.
35
+ *
36
+ * Schemas are wired in — every `.get()` on `charges` / `payouts` runs
37
+ * the corresponding zod schema's `.parse()` and throws a clear
38
+ * `SourcedError({ source: 'storage', operation: 'validate' })` on a
39
+ * corrupted record (instead of NaN-ing downstream when the consumer
40
+ * reads a now-missing field). `idempotency` is a presence-only sentinel
41
+ * so no schema applies; `*Index` stores `string[]` so likewise.
42
+ */
43
+ export declare const buildStores: (storage: Storage) => PaymentsStores;
44
+ //# sourceMappingURL=stores.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stores.d.ts","sourceRoot":"","sources":["../../../src/bot/payments/stores.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAE/C,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAEjE,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAO7D,MAAM,MAAM,cAAc,GAAG;IAC5B,iEAAiE;IACjE,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAC,OAAO,SAAS,CAAC,YAAY,CAAC,CAAC,CAAC;IAC7D,gEAAgE;IAChE,QAAQ,CAAC,WAAW,EAAE,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;IACrD;;;;OAIG;IACH,QAAQ,CAAC,WAAW,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;IACtE,uDAAuD;IACvD,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAC,OAAO,SAAS,CAAC,YAAY,CAAC,CAAC,CAAC;IAC7D,8DAA8D;IAC9D,QAAQ,CAAC,YAAY,EAAE,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;CACnD,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,WAAW,GAAI,SAAS,OAAO,KAAG,cAS7C,CAAC"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The plugin's storage layout, in one place.
3
+ *
4
+ * Replaces the scattered `chargeKey` / `userChargesIndexKey` /
5
+ * `idempotencyKey` / `payoutKey` / `payoutsIndexKey` helpers that used
6
+ * to live in `handlers.ts`, `refund.ts`, `payouts.ts`. Every storage
7
+ * operation in the plugin goes through one of these handles.
8
+ *
9
+ * Built once at plugin construction (in `plugin.ts`) and threaded
10
+ * through every concern that needs it.
11
+ */
12
+ import { botIndex, botRecord, botSentinel } from "../storage.js";
13
+ import { chargeRecordSchema, payoutRecordSchema } from "./schemas.js";
14
+ /** Cap on the per-user `pay:user:<id>:charges` index. Older charges
15
+ * drop off the list but remain in `pay:charge:<id>` for the gestor
16
+ * export (which scans charges directly, not the index). */
17
+ const USER_CHARGES_CAP = 100;
18
+ /**
19
+ * Construct the plugin's storage handles. Called once at plugin
20
+ * construction; the result is passed into every concern that reads or
21
+ * writes plugin state.
22
+ *
23
+ * Schemas are wired in — every `.get()` on `charges` / `payouts` runs
24
+ * the corresponding zod schema's `.parse()` and throws a clear
25
+ * `SourcedError({ source: 'storage', operation: 'validate' })` on a
26
+ * corrupted record (instead of NaN-ing downstream when the consumer
27
+ * reads a now-missing field). `idempotency` is a presence-only sentinel
28
+ * so no schema applies; `*Index` stores `string[]` so likewise.
29
+ */
30
+ export const buildStores = (storage) => ({
31
+ charges: botRecord(storage, "pay:charge", chargeRecordSchema),
32
+ idempotency: botSentinel(storage, "pay:idem"),
33
+ userCharges: (userId) => botIndex(storage, `pay:user:${userId}:charges`, {
34
+ capacity: USER_CHARGES_CAP,
35
+ }),
36
+ payouts: botRecord(storage, "pay:payout", payoutRecordSchema),
37
+ payoutsIndex: botIndex(storage, "pay:payouts:index"),
38
+ });
39
+ //# sourceMappingURL=stores.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stores.js","sourceRoot":"","sources":["../../../src/bot/payments/stores.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACjE,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAGtE;;4DAE4D;AAC5D,MAAM,gBAAgB,GAAG,GAAG,CAAC;AAmB7B;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,OAAgB,EAAkB,EAAE,CAAC,CAAC;IACjE,OAAO,EAAE,SAAS,CAAe,OAAO,EAAE,YAAY,EAAE,kBAAkB,CAAC;IAC3E,WAAW,EAAE,WAAW,CAAC,OAAO,EAAE,UAAU,CAAC;IAC7C,WAAW,EAAE,CAAC,MAAc,EAAE,EAAE,CAC/B,QAAQ,CAAC,OAAO,EAAE,YAAY,MAAM,UAAU,EAAE;QAC/C,QAAQ,EAAE,gBAAgB;KAC1B,CAAC;IACH,OAAO,EAAE,SAAS,CAAe,OAAO,EAAE,YAAY,EAAE,kBAAkB,CAAC;IAC3E,YAAY,EAAE,QAAQ,CAAC,OAAO,EAAE,mBAAmB,CAAC;CACpD,CAAC,CAAC"}