@opencard-dev/core 0.1.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 (53) hide show
  1. package/dist/db.d.ts +145 -0
  2. package/dist/db.d.ts.map +1 -0
  3. package/dist/db.js +373 -0
  4. package/dist/db.js.map +1 -0
  5. package/dist/errors.d.ts +72 -0
  6. package/dist/errors.d.ts.map +1 -0
  7. package/dist/errors.js +124 -0
  8. package/dist/errors.js.map +1 -0
  9. package/dist/index.d.ts +34 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +67 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/logger.d.ts +53 -0
  14. package/dist/logger.d.ts.map +1 -0
  15. package/dist/logger.js +109 -0
  16. package/dist/logger.js.map +1 -0
  17. package/dist/rules-engine.d.ts +159 -0
  18. package/dist/rules-engine.d.ts.map +1 -0
  19. package/dist/rules-engine.js +375 -0
  20. package/dist/rules-engine.js.map +1 -0
  21. package/dist/rules-store.d.ts +187 -0
  22. package/dist/rules-store.d.ts.map +1 -0
  23. package/dist/rules-store.js +291 -0
  24. package/dist/rules-store.js.map +1 -0
  25. package/dist/rules-validation.d.ts +54 -0
  26. package/dist/rules-validation.d.ts.map +1 -0
  27. package/dist/rules-validation.js +110 -0
  28. package/dist/rules-validation.js.map +1 -0
  29. package/dist/stripe-client.d.ts +154 -0
  30. package/dist/stripe-client.d.ts.map +1 -0
  31. package/dist/stripe-client.js +444 -0
  32. package/dist/stripe-client.js.map +1 -0
  33. package/dist/test-utils.d.ts +55 -0
  34. package/dist/test-utils.d.ts.map +1 -0
  35. package/dist/test-utils.js +91 -0
  36. package/dist/test-utils.js.map +1 -0
  37. package/dist/tracker.d.ts +130 -0
  38. package/dist/tracker.d.ts.map +1 -0
  39. package/dist/tracker.js +196 -0
  40. package/dist/tracker.js.map +1 -0
  41. package/dist/transaction-reconciler.d.ts +30 -0
  42. package/dist/transaction-reconciler.d.ts.map +1 -0
  43. package/dist/transaction-reconciler.js +131 -0
  44. package/dist/transaction-reconciler.js.map +1 -0
  45. package/dist/types.d.ts +194 -0
  46. package/dist/types.d.ts.map +1 -0
  47. package/dist/types.js +7 -0
  48. package/dist/types.js.map +1 -0
  49. package/dist/webhooks.d.ts +121 -0
  50. package/dist/webhooks.d.ts.map +1 -0
  51. package/dist/webhooks.js +307 -0
  52. package/dist/webhooks.js.map +1 -0
  53. package/package.json +46 -0
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Transaction Tracker
3
+ * ===================
4
+ * Keeps running totals of spending per card so the rules engine can enforce
5
+ * daily and monthly budget limits without needing a database.
6
+ *
7
+ * How it works:
8
+ * - All data lives in memory (a plain JS Map).
9
+ * - Totals are keyed by cardId.
10
+ * - Daily totals reset automatically when a new UTC calendar day starts.
11
+ * - Monthly totals reset on the first UTC day of each calendar month.
12
+ * - Amounts are always in cents (same unit Stripe uses).
13
+ *
14
+ * Limitations:
15
+ * - Data is lost on process restart. Phase 2 will add persistent storage.
16
+ * - In a multi-process / horizontally-scaled deployment, each process has
17
+ * its own tracker. For Phase 1 (single webhook server) this is fine.
18
+ */
19
+ /**
20
+ * All the spend data we track for a single card.
21
+ */
22
+ export interface CardSpend {
23
+ /** Running total of approved spending today (cents, UTC day). */
24
+ dailySpend: number;
25
+ /** Running total of approved spending this calendar month (cents, UTC month). */
26
+ monthlySpend: number;
27
+ /** How many transactions we've recorded for this card (all time, resets on restart). */
28
+ transactionCount: number;
29
+ /**
30
+ * The UTC date string (YYYY-MM-DD) when dailySpend was last reset.
31
+ * We compare this to today's date to know when to auto-reset.
32
+ */
33
+ lastDailyReset: string;
34
+ /**
35
+ * The UTC month string (YYYY-MM) when monthlySpend was last reset.
36
+ * e.g. "2026-03"
37
+ */
38
+ lastMonthlyReset: string;
39
+ }
40
+ /**
41
+ * TransactionTracker — the main class you interact with.
42
+ *
43
+ * Typical usage:
44
+ * const tracker = new TransactionTracker();
45
+ * tracker.addTransaction('card_abc', 2500); // $25.00
46
+ * const daily = tracker.getDailySpend('card_abc'); // 2500
47
+ */
48
+ export declare class TransactionTracker {
49
+ /**
50
+ * In-memory store: cardId → CardSpend.
51
+ * We use a Map because lookups are O(1) and it's easy to iterate.
52
+ */
53
+ private store;
54
+ /**
55
+ * Retrieves the spend record for a card, creating a new one if it doesn't
56
+ * exist yet. Also handles auto-reset of stale daily/monthly totals.
57
+ */
58
+ private getOrCreate;
59
+ /**
60
+ * Records a transaction for a card and adds its amount to daily and
61
+ * monthly running totals.
62
+ *
63
+ * Call this AFTER an authorization is approved — not before.
64
+ *
65
+ * @param cardId - Stripe card ID (e.g. "ic_abc123")
66
+ * @param amountCents - Transaction amount in cents (e.g. 2500 = $25.00)
67
+ */
68
+ addTransaction(cardId: string, amountCents: number): void;
69
+ /**
70
+ * Returns how many cents this card has spent today (UTC calendar day).
71
+ * Returns 0 if the card has never been seen or if the day has rolled over.
72
+ *
73
+ * @param cardId - Stripe card ID
74
+ */
75
+ getDailySpend(cardId: string): number;
76
+ /**
77
+ * Returns how many cents this card has spent this calendar month (UTC).
78
+ * Returns 0 if no transactions recorded yet.
79
+ *
80
+ * @param cardId - Stripe card ID
81
+ */
82
+ getMonthlySpend(cardId: string): number;
83
+ /**
84
+ * Returns the full spend record for a card, or null if we've never seen it.
85
+ * Useful for debugging and tests.
86
+ *
87
+ * @param cardId - Stripe card ID
88
+ */
89
+ getSpend(cardId: string): CardSpend | null;
90
+ /**
91
+ * Checks whether adding a hypothetical transaction would blow the daily limit.
92
+ *
93
+ * @param cardId - Stripe card ID
94
+ * @param amountCents - Amount to check (not yet recorded)
95
+ * @param limitCents - The daily limit in cents
96
+ * @returns true if the transaction would stay within the limit
97
+ */
98
+ wouldExceedDailyLimit(cardId: string, amountCents: number, limitCents: number): boolean;
99
+ /**
100
+ * Checks whether adding a hypothetical transaction would blow the monthly limit.
101
+ *
102
+ * @param cardId - Stripe card ID
103
+ * @param amountCents - Amount to check (not yet recorded)
104
+ * @param limitCents - The monthly limit in cents
105
+ * @returns true if the transaction would exceed the limit
106
+ */
107
+ wouldExceedMonthlyLimit(cardId: string, amountCents: number, limitCents: number): boolean;
108
+ /**
109
+ * Manually resets the daily and monthly spend for a card to zero.
110
+ * Primarily useful in tests — in production, resets happen automatically.
111
+ *
112
+ * @param cardId - Stripe card ID
113
+ */
114
+ reset(cardId: string): void;
115
+ /**
116
+ * Clears ALL spend data for ALL cards.
117
+ * Useful for test teardown. Don't call this in production.
118
+ */
119
+ resetAll(): void;
120
+ }
121
+ /**
122
+ * Module-level singleton tracker.
123
+ *
124
+ * The webhook server imports and uses this shared instance so that all
125
+ * authorization requests see the same running totals. If you're writing
126
+ * tests, either import this instance and call `.resetAll()` in beforeEach,
127
+ * or instantiate your own `new TransactionTracker()` for isolation.
128
+ */
129
+ export declare const tracker: TransactionTracker;
130
+ //# sourceMappingURL=tracker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tracker.d.ts","sourceRoot":"","sources":["../src/tracker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAIH;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,iEAAiE;IACjE,UAAU,EAAE,MAAM,CAAC;IAEnB,iFAAiF;IACjF,YAAY,EAAE,MAAM,CAAC;IAErB,wFAAwF;IACxF,gBAAgB,EAAE,MAAM,CAAC;IAEzB;;;OAGG;IACH,cAAc,EAAE,MAAM,CAAC;IAEvB;;;OAGG;IACH,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAsCD;;;;;;;GAOG;AACH,qBAAa,kBAAkB;IAC7B;;;OAGG;IACH,OAAO,CAAC,KAAK,CAAqC;IAIlD;;;OAGG;IACH,OAAO,CAAC,WAAW;IA8BnB;;;;;;;;OAQG;IACH,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI;IAczD;;;;;OAKG;IACH,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM;IAIrC;;;;;OAKG;IACH,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM;IAIvC;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI;IAM1C;;;;;;;OAOG;IACH,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO;IAIvF;;;;;;;OAOG;IACH,uBAAuB,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO;IAIzF;;;;;OAKG;IACH,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAI3B;;;OAGG;IACH,QAAQ,IAAI,IAAI;CAGjB;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,OAAO,oBAA2B,CAAC"}
@@ -0,0 +1,196 @@
1
+ "use strict";
2
+ /**
3
+ * Transaction Tracker
4
+ * ===================
5
+ * Keeps running totals of spending per card so the rules engine can enforce
6
+ * daily and monthly budget limits without needing a database.
7
+ *
8
+ * How it works:
9
+ * - All data lives in memory (a plain JS Map).
10
+ * - Totals are keyed by cardId.
11
+ * - Daily totals reset automatically when a new UTC calendar day starts.
12
+ * - Monthly totals reset on the first UTC day of each calendar month.
13
+ * - Amounts are always in cents (same unit Stripe uses).
14
+ *
15
+ * Limitations:
16
+ * - Data is lost on process restart. Phase 2 will add persistent storage.
17
+ * - In a multi-process / horizontally-scaled deployment, each process has
18
+ * its own tracker. For Phase 1 (single webhook server) this is fine.
19
+ */
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.tracker = exports.TransactionTracker = void 0;
22
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
23
+ /**
24
+ * Returns today's UTC date as a string like "2026-03-19".
25
+ * We use UTC everywhere so resets happen at a predictable time regardless
26
+ * of where the server is running.
27
+ */
28
+ function utcDateString() {
29
+ const now = new Date();
30
+ return now.toISOString().slice(0, 10); // "YYYY-MM-DD"
31
+ }
32
+ /**
33
+ * Returns the current UTC month as a string like "2026-03".
34
+ */
35
+ function utcMonthString() {
36
+ const now = new Date();
37
+ return now.toISOString().slice(0, 7); // "YYYY-MM"
38
+ }
39
+ /**
40
+ * Returns a fresh CardSpend record with all counters zeroed out.
41
+ * Used both for new cards and after resets.
42
+ */
43
+ function freshSpend() {
44
+ return {
45
+ dailySpend: 0,
46
+ monthlySpend: 0,
47
+ transactionCount: 0,
48
+ lastDailyReset: utcDateString(),
49
+ lastMonthlyReset: utcMonthString(),
50
+ };
51
+ }
52
+ // ─── TransactionTracker ───────────────────────────────────────────────────────
53
+ /**
54
+ * TransactionTracker — the main class you interact with.
55
+ *
56
+ * Typical usage:
57
+ * const tracker = new TransactionTracker();
58
+ * tracker.addTransaction('card_abc', 2500); // $25.00
59
+ * const daily = tracker.getDailySpend('card_abc'); // 2500
60
+ */
61
+ class TransactionTracker {
62
+ /**
63
+ * In-memory store: cardId → CardSpend.
64
+ * We use a Map because lookups are O(1) and it's easy to iterate.
65
+ */
66
+ store = new Map();
67
+ // ─── Private helpers ────────────────────────────────────────────────────
68
+ /**
69
+ * Retrieves the spend record for a card, creating a new one if it doesn't
70
+ * exist yet. Also handles auto-reset of stale daily/monthly totals.
71
+ */
72
+ getOrCreate(cardId) {
73
+ // Create a fresh record if this card hasn't been seen before.
74
+ if (!this.store.has(cardId)) {
75
+ this.store.set(cardId, freshSpend());
76
+ }
77
+ const spend = this.store.get(cardId);
78
+ // ── Daily reset ──────────────────────────────────────────────────────
79
+ // If the stored "last reset date" is earlier than today, it's a new day
80
+ // and we should zero out the daily total.
81
+ const today = utcDateString();
82
+ if (spend.lastDailyReset !== today) {
83
+ spend.dailySpend = 0;
84
+ spend.lastDailyReset = today;
85
+ }
86
+ // ── Monthly reset ────────────────────────────────────────────────────
87
+ // Same logic, but for the calendar month.
88
+ const thisMonth = utcMonthString();
89
+ if (spend.lastMonthlyReset !== thisMonth) {
90
+ spend.monthlySpend = 0;
91
+ spend.lastMonthlyReset = thisMonth;
92
+ }
93
+ return spend;
94
+ }
95
+ // ─── Public API ─────────────────────────────────────────────────────────
96
+ /**
97
+ * Records a transaction for a card and adds its amount to daily and
98
+ * monthly running totals.
99
+ *
100
+ * Call this AFTER an authorization is approved — not before.
101
+ *
102
+ * @param cardId - Stripe card ID (e.g. "ic_abc123")
103
+ * @param amountCents - Transaction amount in cents (e.g. 2500 = $25.00)
104
+ */
105
+ addTransaction(cardId, amountCents) {
106
+ if (amountCents < 0) {
107
+ // Negative amounts would corrupt our totals. Stripe refunds/reversals
108
+ // should be handled separately (Phase 2). For now, just ignore them.
109
+ console.warn(`[Tracker] Ignoring negative amount for card ${cardId}: ${amountCents}`);
110
+ return;
111
+ }
112
+ const spend = this.getOrCreate(cardId);
113
+ spend.dailySpend += amountCents;
114
+ spend.monthlySpend += amountCents;
115
+ spend.transactionCount += 1;
116
+ }
117
+ /**
118
+ * Returns how many cents this card has spent today (UTC calendar day).
119
+ * Returns 0 if the card has never been seen or if the day has rolled over.
120
+ *
121
+ * @param cardId - Stripe card ID
122
+ */
123
+ getDailySpend(cardId) {
124
+ return this.getOrCreate(cardId).dailySpend;
125
+ }
126
+ /**
127
+ * Returns how many cents this card has spent this calendar month (UTC).
128
+ * Returns 0 if no transactions recorded yet.
129
+ *
130
+ * @param cardId - Stripe card ID
131
+ */
132
+ getMonthlySpend(cardId) {
133
+ return this.getOrCreate(cardId).monthlySpend;
134
+ }
135
+ /**
136
+ * Returns the full spend record for a card, or null if we've never seen it.
137
+ * Useful for debugging and tests.
138
+ *
139
+ * @param cardId - Stripe card ID
140
+ */
141
+ getSpend(cardId) {
142
+ if (!this.store.has(cardId))
143
+ return null;
144
+ // Run getOrCreate so resets are applied before we return the record.
145
+ return this.getOrCreate(cardId);
146
+ }
147
+ /**
148
+ * Checks whether adding a hypothetical transaction would blow the daily limit.
149
+ *
150
+ * @param cardId - Stripe card ID
151
+ * @param amountCents - Amount to check (not yet recorded)
152
+ * @param limitCents - The daily limit in cents
153
+ * @returns true if the transaction would stay within the limit
154
+ */
155
+ wouldExceedDailyLimit(cardId, amountCents, limitCents) {
156
+ return this.getDailySpend(cardId) + amountCents > limitCents;
157
+ }
158
+ /**
159
+ * Checks whether adding a hypothetical transaction would blow the monthly limit.
160
+ *
161
+ * @param cardId - Stripe card ID
162
+ * @param amountCents - Amount to check (not yet recorded)
163
+ * @param limitCents - The monthly limit in cents
164
+ * @returns true if the transaction would exceed the limit
165
+ */
166
+ wouldExceedMonthlyLimit(cardId, amountCents, limitCents) {
167
+ return this.getMonthlySpend(cardId) + amountCents > limitCents;
168
+ }
169
+ /**
170
+ * Manually resets the daily and monthly spend for a card to zero.
171
+ * Primarily useful in tests — in production, resets happen automatically.
172
+ *
173
+ * @param cardId - Stripe card ID
174
+ */
175
+ reset(cardId) {
176
+ this.store.set(cardId, freshSpend());
177
+ }
178
+ /**
179
+ * Clears ALL spend data for ALL cards.
180
+ * Useful for test teardown. Don't call this in production.
181
+ */
182
+ resetAll() {
183
+ this.store.clear();
184
+ }
185
+ }
186
+ exports.TransactionTracker = TransactionTracker;
187
+ /**
188
+ * Module-level singleton tracker.
189
+ *
190
+ * The webhook server imports and uses this shared instance so that all
191
+ * authorization requests see the same running totals. If you're writing
192
+ * tests, either import this instance and call `.resetAll()` in beforeEach,
193
+ * or instantiate your own `new TransactionTracker()` for isolation.
194
+ */
195
+ exports.tracker = new TransactionTracker();
196
+ //# sourceMappingURL=tracker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tracker.js","sourceRoot":"","sources":["../src/tracker.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Transaction Reconciliation - Phase 1
3
+ * Handles Stripe issuing events
4
+ */
5
+ import Stripe from 'stripe';
6
+ import { StoredAuthorization, StoredTransaction, OpenCardDatabase } from './db';
7
+ export interface ReconciliationResult {
8
+ created: number;
9
+ updated: number;
10
+ orphaned: number;
11
+ errors: string[];
12
+ }
13
+ export declare class TransactionReconciler {
14
+ private database;
15
+ constructor(database?: OpenCardDatabase);
16
+ /**
17
+ * Handle an authorization request event.
18
+ * Persists the authorization with 'pending' status and the given decision reason.
19
+ *
20
+ * @param auth - Stripe authorization object
21
+ * @param decisionReason - The actual reason string from the rules engine (optional)
22
+ */
23
+ handleAuthorizationRequest(auth: Stripe.Issuing.Authorization, decisionReason?: string): StoredAuthorization;
24
+ handleAuthorizationCreated(auth: Stripe.Issuing.Authorization): StoredAuthorization | null;
25
+ handleTransactionCreated(txn: Stripe.Issuing.Transaction): StoredTransaction;
26
+ handleTransactionUpdated(txn: Stripe.Issuing.Transaction): StoredTransaction | null;
27
+ findOrphanedAuthorizations(cardId?: string, hoursOld?: number): StoredAuthorization[];
28
+ reconcile(): Promise<ReconciliationResult>;
29
+ }
30
+ //# sourceMappingURL=transaction-reconciler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transaction-reconciler.d.ts","sourceRoot":"","sources":["../src/transaction-reconciler.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,MAAM,MAAM,QAAQ,CAAC;AAC5B,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,gBAAgB,EAAgB,MAAM,MAAM,CAAC;AAK9F,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,qBAAa,qBAAqB;IAChC,OAAO,CAAC,QAAQ,CAAmB;gBAEvB,QAAQ,CAAC,EAAE,gBAAgB;IAIvC;;;;;;OAMG;IACH,0BAA0B,CACxB,IAAI,EAAE,MAAM,CAAC,OAAO,CAAC,aAAa,EAClC,cAAc,CAAC,EAAE,MAAM,GACtB,mBAAmB;IAkBtB,0BAA0B,CAAC,IAAI,EAAE,MAAM,CAAC,OAAO,CAAC,aAAa,GAAG,mBAAmB,GAAG,IAAI;IAiC1F,wBAAwB,CAAC,GAAG,EAAE,MAAM,CAAC,OAAO,CAAC,WAAW,GAAG,iBAAiB;IAuB5E,wBAAwB,CAAC,GAAG,EAAE,MAAM,CAAC,OAAO,CAAC,WAAW,GAAG,iBAAiB,GAAG,IAAI;IAwBnF,0BAA0B,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,QAAQ,SAAI,GAAG,mBAAmB,EAAE;IAe1E,SAAS,IAAI,OAAO,CAAC,oBAAoB,CAAC;CAqBjD"}
@@ -0,0 +1,131 @@
1
+ "use strict";
2
+ /**
3
+ * Transaction Reconciliation - Phase 1
4
+ * Handles Stripe issuing events
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.TransactionReconciler = void 0;
8
+ const db_1 = require("./db");
9
+ const logger_1 = require("./logger");
10
+ const logger = (0, logger_1.getLogger)('reconciler');
11
+ class TransactionReconciler {
12
+ database;
13
+ constructor(database) {
14
+ this.database = database || (0, db_1.initDatabase)();
15
+ }
16
+ /**
17
+ * Handle an authorization request event.
18
+ * Persists the authorization with 'pending' status and the given decision reason.
19
+ *
20
+ * @param auth - Stripe authorization object
21
+ * @param decisionReason - The actual reason string from the rules engine (optional)
22
+ */
23
+ handleAuthorizationRequest(auth, decisionReason) {
24
+ const cardId = typeof auth.card === 'string' ? auth.card : auth.card.id;
25
+ const result = this.database.createAuthorization({
26
+ stripe_id: auth.id,
27
+ card_id: cardId,
28
+ amount: auth.amount,
29
+ currency: auth.currency,
30
+ merchant_name: auth.merchant_data?.name || null,
31
+ merchant_category: auth.merchant_data?.category || null,
32
+ status: 'pending',
33
+ decision_reason: decisionReason || null,
34
+ });
35
+ logger.info('Created pending authorization', { stripe_id: auth.id, decision_reason: decisionReason });
36
+ return result;
37
+ }
38
+ handleAuthorizationCreated(auth) {
39
+ const existing = this.database.getAuthorizationByStripeId(auth.id);
40
+ if (!existing) {
41
+ logger.warn('Authorization.created for unknown auth', { stripe_id: auth.id });
42
+ return null;
43
+ }
44
+ // Use auth.approved (boolean) — it's on the Stripe type and maps cleanly to status.
45
+ // auth.status covers the lifecycle states: 'closed' | 'expired' | 'pending' | 'reversed'
46
+ // For .created events, approved/declined are the relevant states; we derive from auth.approved.
47
+ const newStatus = auth.approved ? 'approved' : 'declined';
48
+ let decisionReason = null;
49
+ if (auth.approved) {
50
+ decisionReason = 'approved_by_stripe';
51
+ }
52
+ else {
53
+ decisionReason = 'declined_by_stripe';
54
+ }
55
+ // If the existing record already has a specific decision reason (set during .request),
56
+ // don't overwrite it — the .request reason is richer and more valuable.
57
+ const finalReason = existing.decision_reason || decisionReason;
58
+ const updated = this.database.updateAuthorization(existing.id, {
59
+ status: newStatus,
60
+ decision_reason: finalReason,
61
+ });
62
+ logger.info('Updated authorization status', { stripe_id: auth.id, status: newStatus });
63
+ return updated;
64
+ }
65
+ handleTransactionCreated(txn) {
66
+ const cardId = typeof txn.card === 'string' ? txn.card : txn.card.id;
67
+ // Stripe's Issuing.Transaction does not have a 'status' field.
68
+ // Transactions are always captured by the time we receive issuing_transaction.created.
69
+ // Refunds/reversals come via issuing_transaction.updated with a different type check.
70
+ const status = 'captured';
71
+ const result = this.database.createTransaction({
72
+ stripe_id: txn.id,
73
+ card_id: cardId,
74
+ amount: txn.amount,
75
+ currency: txn.currency,
76
+ merchant_name: txn.merchant_data?.name || null,
77
+ merchant_category: txn.merchant_data?.category || null,
78
+ status,
79
+ stripe_created_at: new Date(txn.created * 1000).toISOString(),
80
+ });
81
+ logger.info('Created transaction', { stripe_id: txn.id, amount: txn.amount });
82
+ return result;
83
+ }
84
+ handleTransactionUpdated(txn) {
85
+ const existing = this.database.getTransaction(txn.id);
86
+ if (!existing) {
87
+ logger.warn('Transaction.updated for unknown txn', { stripe_id: txn.id });
88
+ return null;
89
+ }
90
+ // Transactions don't have a .status field on Stripe's type.
91
+ // For transaction.updated events, check the transaction type to determine
92
+ // if it's a refund/reversal; otherwise assume captured.
93
+ const isRefund = txn.type === 'refund';
94
+ const newStatus = isRefund ? 'reversed' : 'captured';
95
+ const db_inst = this.database.getDb();
96
+ const stmt = db_inst.prepare('UPDATE transactions SET amount = ?, status = ? WHERE stripe_id = ?');
97
+ stmt.run(txn.amount, newStatus, txn.id);
98
+ const result = this.database.getTransaction(txn.id);
99
+ logger.info('Updated transaction', { stripe_id: txn.id, status: newStatus });
100
+ return result || null;
101
+ }
102
+ findOrphanedAuthorizations(cardId, hoursOld = 1) {
103
+ const orphaned = [];
104
+ if (cardId) {
105
+ const pending = this.database.getPendingAuthorizationsOlderThan(cardId, hoursOld);
106
+ orphaned.push(...pending);
107
+ }
108
+ if (orphaned.length > 0) {
109
+ logger.warn('Found orphaned authorizations', { count: orphaned.length });
110
+ }
111
+ return orphaned;
112
+ }
113
+ async reconcile() {
114
+ const result = {
115
+ created: 0,
116
+ updated: 0,
117
+ orphaned: 0,
118
+ errors: [],
119
+ };
120
+ try {
121
+ const orphaned = this.findOrphanedAuthorizations(undefined, 1);
122
+ result.orphaned = orphaned.length;
123
+ }
124
+ catch (error) {
125
+ result.errors.push(`Error checking for orphaned authorizations: ${error instanceof Error ? error.message : String(error)}`);
126
+ }
127
+ return result;
128
+ }
129
+ }
130
+ exports.TransactionReconciler = TransactionReconciler;
131
+ //# sourceMappingURL=transaction-reconciler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transaction-reconciler.js","sourceRoot":"","sources":["../src/transaction-reconciler.ts"],"names":[],"mappings":"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"}