@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.
- package/dist/db.d.ts +145 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +373 -0
- package/dist/db.js.map +1 -0
- package/dist/errors.d.ts +72 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +124 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +67 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +53 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +109 -0
- package/dist/logger.js.map +1 -0
- package/dist/rules-engine.d.ts +159 -0
- package/dist/rules-engine.d.ts.map +1 -0
- package/dist/rules-engine.js +375 -0
- package/dist/rules-engine.js.map +1 -0
- package/dist/rules-store.d.ts +187 -0
- package/dist/rules-store.d.ts.map +1 -0
- package/dist/rules-store.js +291 -0
- package/dist/rules-store.js.map +1 -0
- package/dist/rules-validation.d.ts +54 -0
- package/dist/rules-validation.d.ts.map +1 -0
- package/dist/rules-validation.js +110 -0
- package/dist/rules-validation.js.map +1 -0
- package/dist/stripe-client.d.ts +154 -0
- package/dist/stripe-client.d.ts.map +1 -0
- package/dist/stripe-client.js +444 -0
- package/dist/stripe-client.js.map +1 -0
- package/dist/test-utils.d.ts +55 -0
- package/dist/test-utils.d.ts.map +1 -0
- package/dist/test-utils.js +91 -0
- package/dist/test-utils.js.map +1 -0
- package/dist/tracker.d.ts +130 -0
- package/dist/tracker.d.ts.map +1 -0
- package/dist/tracker.js +196 -0
- package/dist/tracker.js.map +1 -0
- package/dist/transaction-reconciler.d.ts +30 -0
- package/dist/transaction-reconciler.d.ts.map +1 -0
- package/dist/transaction-reconciler.js +131 -0
- package/dist/transaction-reconciler.js.map +1 -0
- package/dist/types.d.ts +194 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -0
- package/dist/webhooks.d.ts +121 -0
- package/dist/webhooks.d.ts.map +1 -0
- package/dist/webhooks.js +307 -0
- package/dist/webhooks.js.map +1 -0
- 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"}
|
package/dist/tracker.js
ADDED
|
@@ -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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"}
|