cata-centavo 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/LICENSE +21 -0
- package/README.md +159 -0
- package/dist/bin/cata-centavo.js +226 -0
- package/dist/bin/cata-centavo.js.map +1 -0
- package/dist/cli/dispatch.js +67 -0
- package/dist/cli/dispatch.js.map +1 -0
- package/dist/cli/doctor.js +178 -0
- package/dist/cli/doctor.js.map +1 -0
- package/dist/cli/init.js +125 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/progress.js +85 -0
- package/dist/cli/progress.js.map +1 -0
- package/dist/cli/sync.js +26 -0
- package/dist/cli/sync.js.map +1 -0
- package/dist/config.js +119 -0
- package/dist/config.js.map +1 -0
- package/dist/core/account.js +12 -0
- package/dist/core/account.js.map +1 -0
- package/dist/core/accounts.js +91 -0
- package/dist/core/accounts.js.map +1 -0
- package/dist/core/aggregate.js +83 -0
- package/dist/core/aggregate.js.map +1 -0
- package/dist/core/balance.js +68 -0
- package/dist/core/balance.js.map +1 -0
- package/dist/core/bill-rows.js +132 -0
- package/dist/core/bill-rows.js.map +1 -0
- package/dist/core/bill.js +103 -0
- package/dist/core/bill.js.map +1 -0
- package/dist/core/category-source.js +26 -0
- package/dist/core/category-source.js.map +1 -0
- package/dist/core/category.js +72 -0
- package/dist/core/category.js.map +1 -0
- package/dist/core/consent.js +19 -0
- package/dist/core/consent.js.map +1 -0
- package/dist/core/contracts.js +9 -0
- package/dist/core/contracts.js.map +1 -0
- package/dist/core/counterparty.js +69 -0
- package/dist/core/counterparty.js.map +1 -0
- package/dist/core/date.js +25 -0
- package/dist/core/date.js.map +1 -0
- package/dist/core/description.js +58 -0
- package/dist/core/description.js.map +1 -0
- package/dist/core/diagnose.js +32 -0
- package/dist/core/diagnose.js.map +1 -0
- package/dist/core/mcc.js +124 -0
- package/dist/core/mcc.js.map +1 -0
- package/dist/core/refresh.js +142 -0
- package/dist/core/refresh.js.map +1 -0
- package/dist/core/self-transfer.js +22 -0
- package/dist/core/self-transfer.js.map +1 -0
- package/dist/core/taxonomy-tree.js +148 -0
- package/dist/core/taxonomy-tree.js.map +1 -0
- package/dist/core/taxonomy.js +70 -0
- package/dist/core/taxonomy.js.map +1 -0
- package/dist/core/transaction.js +2 -0
- package/dist/core/transaction.js.map +1 -0
- package/dist/core/transactions.js +46 -0
- package/dist/core/transactions.js.map +1 -0
- package/dist/logging.js +93 -0
- package/dist/logging.js.map +1 -0
- package/dist/mcp/cursor.js +64 -0
- package/dist/mcp/cursor.js.map +1 -0
- package/dist/mcp/format.js +38 -0
- package/dist/mcp/format.js.map +1 -0
- package/dist/mcp/server.js +50 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/source.js +2 -0
- package/dist/mcp/source.js.map +1 -0
- package/dist/mcp/tools/accounts.js +144 -0
- package/dist/mcp/tools/accounts.js.map +1 -0
- package/dist/mcp/tools/balance.js +96 -0
- package/dist/mcp/tools/balance.js.map +1 -0
- package/dist/mcp/tools/bill-summary-format.js +161 -0
- package/dist/mcp/tools/bill-summary-format.js.map +1 -0
- package/dist/mcp/tools/bill-summary.js +105 -0
- package/dist/mcp/tools/bill-summary.js.map +1 -0
- package/dist/mcp/tools/bills.js +144 -0
- package/dist/mcp/tools/bills.js.map +1 -0
- package/dist/mcp/tools/closing-days.js +90 -0
- package/dist/mcp/tools/closing-days.js.map +1 -0
- package/dist/mcp/tools/list-transactions.js +122 -0
- package/dist/mcp/tools/list-transactions.js.map +1 -0
- package/dist/mcp/tools/result.js +12 -0
- package/dist/mcp/tools/result.js.map +1 -0
- package/dist/mcp/tools/set-category.js +74 -0
- package/dist/mcp/tools/set-category.js.map +1 -0
- package/dist/mcp/tools/sources.js +73 -0
- package/dist/mcp/tools/sources.js.map +1 -0
- package/dist/mcp/tools/transaction-details.js +146 -0
- package/dist/mcp/tools/transaction-details.js.map +1 -0
- package/dist/mcp/tools/transaction-input.js +51 -0
- package/dist/mcp/tools/transaction-input.js.map +1 -0
- package/dist/mcp/tools/transactions.js +186 -0
- package/dist/mcp/tools/transactions.js.map +1 -0
- package/dist/pluggy/client.js +123 -0
- package/dist/pluggy/client.js.map +1 -0
- package/dist/pluggy/errors.js +152 -0
- package/dist/pluggy/errors.js.map +1 -0
- package/dist/pluggy/mapper.js +155 -0
- package/dist/pluggy/mapper.js.map +1 -0
- package/dist/pluggy/money.js +53 -0
- package/dist/pluggy/money.js.map +1 -0
- package/dist/pluggy/transaction-mapper.js +124 -0
- package/dist/pluggy/transaction-mapper.js.map +1 -0
- package/dist/pluggy/transport.js +170 -0
- package/dist/pluggy/transport.js.map +1 -0
- package/dist/pluggy/wire.js +207 -0
- package/dist/pluggy/wire.js.map +1 -0
- package/dist/storage/categories.js +101 -0
- package/dist/storage/categories.js.map +1 -0
- package/dist/storage/category-sql.js +37 -0
- package/dist/storage/category-sql.js.map +1 -0
- package/dist/storage/closing-days.js +27 -0
- package/dist/storage/closing-days.js.map +1 -0
- package/dist/storage/db.js +162 -0
- package/dist/storage/db.js.map +1 -0
- package/dist/storage/diagnostics.js +57 -0
- package/dist/storage/diagnostics.js.map +1 -0
- package/dist/storage/harvest.js +87 -0
- package/dist/storage/harvest.js.map +1 -0
- package/dist/storage/migrations.js +125 -0
- package/dist/storage/migrations.js.map +1 -0
- package/dist/storage/transaction-row.js +74 -0
- package/dist/storage/transaction-row.js.map +1 -0
- package/dist/storage/transactions.js +213 -0
- package/dist/storage/transactions.js.map +1 -0
- package/package.json +70 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { classify, parse, readJson } from "./errors.js";
|
|
2
|
+
import { AUTH_RESPONSE } from "./wire.js";
|
|
3
|
+
/**
|
|
4
|
+
* How early we stop trusting the key. Pluggy's `apiKey` lives two hours, and
|
|
5
|
+
* expiring at its full lifetime means a token with milliseconds left passes the
|
|
6
|
+
* check and the request 401s anyway — which is exactly what `pluggy-sdk` does
|
|
7
|
+
* (`payload.exp <= now` in its `isJwtExpired`). ADR §15 Phase 0.
|
|
8
|
+
*/
|
|
9
|
+
export const KEY_MARGIN_MS = 10 * 60 * 1000;
|
|
10
|
+
/** Used only when the key is not a JWT we can read an `exp` out of. */
|
|
11
|
+
const KEY_FALLBACK_LIFETIME_MS = 2 * 60 * 60 * 1000;
|
|
12
|
+
/**
|
|
13
|
+
* Disputed: the project spec says 360/min per IP, the prior Go implementation
|
|
14
|
+
* hardcodes 360 per *hour*, and ADR Phase 0.5 step 4 exists to settle which.
|
|
15
|
+
* Until it does, this errs high, because guessing too high costs a 429 and a
|
|
16
|
+
* retry while guessing too low makes the Phase 1 fan-out crawl and look broken.
|
|
17
|
+
*/
|
|
18
|
+
export const RATE_LIMIT = { requests: 360, windowMs: 60_000 };
|
|
19
|
+
/** How many times a 429 is worth waiting out before it becomes the caller's problem. */
|
|
20
|
+
export const RATE_LIMIT_RETRIES = 2;
|
|
21
|
+
/** What Pluggy's `Retry-After` says when it says nothing: their docs always send 60. */
|
|
22
|
+
const RETRY_AFTER_FALLBACK_MS = 60_000;
|
|
23
|
+
const DEFAULT_BASE_URL = "https://api.pluggy.ai";
|
|
24
|
+
/**
|
|
25
|
+
* Construction performs no I/O. A bad credential has to be reportable by
|
|
26
|
+
* `init`, and `init` cannot report a condition that already killed the process
|
|
27
|
+
* (§16.2).
|
|
28
|
+
*/
|
|
29
|
+
export function createTransport(options) {
|
|
30
|
+
const send = sender(options);
|
|
31
|
+
const key = keyResolver(options, send);
|
|
32
|
+
const sleep = options.sleep ?? delay;
|
|
33
|
+
async function authorized(method, path, body) {
|
|
34
|
+
let response = await send(method, path, { apiKey: await key(), body });
|
|
35
|
+
if (response.status === 401) {
|
|
36
|
+
response = await send(method, path, { apiKey: await key(true), body });
|
|
37
|
+
}
|
|
38
|
+
for (let retry = 0; retry < RATE_LIMIT_RETRIES && response.status === 429; retry += 1) {
|
|
39
|
+
options.log.warn({ method, path, attempt: retry + 1, maxRetries: RATE_LIMIT_RETRIES }, "rate limit response; retrying");
|
|
40
|
+
await sleep(retryAfterMs(response));
|
|
41
|
+
response = await send(method, path, { apiKey: await key(), body });
|
|
42
|
+
}
|
|
43
|
+
return response;
|
|
44
|
+
}
|
|
45
|
+
return { key: () => key(), authorized };
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The single send function. Everything reaching Pluggy passes through here, so
|
|
49
|
+
* the rate limiter cannot be forgotten by a new endpoint — the bug §16.2 found
|
|
50
|
+
* wired to two of nine call sites.
|
|
51
|
+
*/
|
|
52
|
+
function sender(options) {
|
|
53
|
+
const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
|
|
54
|
+
const limiter = options.limiter ?? slidingWindowLimiter(options.clock);
|
|
55
|
+
return async (method, path, extras) => {
|
|
56
|
+
await limiter.acquire();
|
|
57
|
+
const headers = { "content-type": "application/json" };
|
|
58
|
+
if (extras.apiKey !== undefined) {
|
|
59
|
+
headers["X-API-KEY"] = extras.apiKey;
|
|
60
|
+
}
|
|
61
|
+
const init = { method, headers };
|
|
62
|
+
if (extras.body !== undefined) {
|
|
63
|
+
init.body = JSON.stringify(extras.body);
|
|
64
|
+
}
|
|
65
|
+
return options.fetch(`${baseUrl}${path}`, init);
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The key, resolved lazily per request and cached, guarded by a single-flight so
|
|
70
|
+
* the account fan-out of §14.1 cannot issue N concurrent `POST /auth`.
|
|
71
|
+
*/
|
|
72
|
+
function keyResolver(options, send) {
|
|
73
|
+
let key = null;
|
|
74
|
+
let refreshing = null;
|
|
75
|
+
async function authenticate() {
|
|
76
|
+
const response = await send("POST", "/auth", { body: options.credentials });
|
|
77
|
+
if (!response.ok) {
|
|
78
|
+
throw classify(response.status, "authenticating");
|
|
79
|
+
}
|
|
80
|
+
return parse(AUTH_RESPONSE, await readJson(response), "the /auth response").apiKey;
|
|
81
|
+
}
|
|
82
|
+
return async (force = false) => {
|
|
83
|
+
const now = options.clock.now().getTime();
|
|
84
|
+
if (!force && key !== null && now < key.expiresAt) {
|
|
85
|
+
return key.value;
|
|
86
|
+
}
|
|
87
|
+
refreshing ??= authenticate()
|
|
88
|
+
.then((value) => {
|
|
89
|
+
key = { value, expiresAt: expiryOf(value, now) };
|
|
90
|
+
return value;
|
|
91
|
+
})
|
|
92
|
+
.finally(() => {
|
|
93
|
+
refreshing = null;
|
|
94
|
+
});
|
|
95
|
+
return refreshing;
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function retryAfterMs(response) {
|
|
99
|
+
const header = response.headers.get("retry-after");
|
|
100
|
+
let seconds;
|
|
101
|
+
if (header === null) {
|
|
102
|
+
seconds = Number.NaN;
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
seconds = Number(header);
|
|
106
|
+
}
|
|
107
|
+
if (Number.isFinite(seconds) && seconds > 0) {
|
|
108
|
+
return seconds * 1000;
|
|
109
|
+
}
|
|
110
|
+
return RETRY_AFTER_FALLBACK_MS;
|
|
111
|
+
}
|
|
112
|
+
function expiryOf(token, now) {
|
|
113
|
+
return (jwtExpiry(token) ?? now + KEY_FALLBACK_LIFETIME_MS) - KEY_MARGIN_MS;
|
|
114
|
+
}
|
|
115
|
+
/** Reads `exp` out of a JWT without a library. Returns null for anything else. */
|
|
116
|
+
function jwtExpiry(token) {
|
|
117
|
+
const exp = jwtPayload(token)?.["exp"];
|
|
118
|
+
if (typeof exp === "number") {
|
|
119
|
+
return exp * 1000;
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
function jwtPayload(token) {
|
|
124
|
+
const encoded = token.split(".")[1];
|
|
125
|
+
if (encoded === undefined) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
const decoded = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
|
|
130
|
+
if (typeof decoded === "object" && decoded !== null) {
|
|
131
|
+
return decoded;
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const delay = (milliseconds) => new Promise((resolve) => {
|
|
140
|
+
setTimeout(resolve, milliseconds);
|
|
141
|
+
});
|
|
142
|
+
/**
|
|
143
|
+
* Waits rather than rejecting, because a caller that is merely early should not
|
|
144
|
+
* have to know about the limit.
|
|
145
|
+
*/
|
|
146
|
+
export function slidingWindowLimiter(clock, limit = RATE_LIMIT.requests, windowMs = RATE_LIMIT.windowMs, sleep = delay) {
|
|
147
|
+
let hits = [];
|
|
148
|
+
return {
|
|
149
|
+
acquire: async () => {
|
|
150
|
+
for (;;) {
|
|
151
|
+
const now = clock.now().getTime();
|
|
152
|
+
hits = hits.filter((at) => now - at < windowMs);
|
|
153
|
+
if (hits.length < limit) {
|
|
154
|
+
hits = [...hits, now];
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const oldest = hits[0];
|
|
158
|
+
let waitMs;
|
|
159
|
+
if (oldest === undefined) {
|
|
160
|
+
waitMs = windowMs;
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
waitMs = oldest + windowMs - now;
|
|
164
|
+
}
|
|
165
|
+
await sleep(waitMs);
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
//# sourceMappingURL=transport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport.js","sourceRoot":"","sources":["../../src/pluggy/transport.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* The raw bodies we read from Pluggy, described rather than assumed.
|
|
4
|
+
*
|
|
5
|
+
* We do not reuse the SDK's entity types here, and the reason is specific: they
|
|
6
|
+
* declare fields like `lastUpdatedAt` as `Date`, which is only true inside the
|
|
7
|
+
* SDK, where a reviver installed in `got` turns every ISO-8601 string into a
|
|
8
|
+
* `Date` (`transforms.js`). Parsing the body ourselves, the value is a string
|
|
9
|
+
* and the type would be lying. Inferring from a schema makes that impossible.
|
|
10
|
+
*
|
|
11
|
+
* This is also ADR Phase 0.5 step 5 as code: trust nothing but the raw body.
|
|
12
|
+
* The prior implementation could never observe that free-tier `category` comes
|
|
13
|
+
* back null, because its own serializer deleted the evidence first (§16.2).
|
|
14
|
+
*
|
|
15
|
+
* Unknown keys are dropped, not rejected. Pluggy adding a field is not our
|
|
16
|
+
* problem; Pluggy removing one we read is, and that is what these catch.
|
|
17
|
+
*/
|
|
18
|
+
export const AUTH_RESPONSE = z.object({
|
|
19
|
+
apiKey: z.string().min(1),
|
|
20
|
+
});
|
|
21
|
+
/**
|
|
22
|
+
* Why `statusDetail` may fall back to nothing rather than failing the parse: it
|
|
23
|
+
* is a bag keyed by product name, its shape is documented only by example, and
|
|
24
|
+
* what we read out of it is advisory — which product could not be updated this
|
|
25
|
+
* time. Losing a whole completed sync to an unexpected key in an advisory field
|
|
26
|
+
* would be the worse trade. Nothing about the money passes through here.
|
|
27
|
+
*/
|
|
28
|
+
const STATUS_DETAIL = z
|
|
29
|
+
.record(z.string(), z
|
|
30
|
+
.object({ warnings: z.array(z.object({ message: z.string() })).nullish() })
|
|
31
|
+
.nullish())
|
|
32
|
+
.nullish()
|
|
33
|
+
.catch(null);
|
|
34
|
+
export const ITEM = z.object({
|
|
35
|
+
id: z.string().min(1),
|
|
36
|
+
connector: z.object({ name: z.string() }),
|
|
37
|
+
status: z.string(),
|
|
38
|
+
executionStatus: z.string().nullish(),
|
|
39
|
+
lastUpdatedAt: z.string().nullable(),
|
|
40
|
+
/** Present when the institution is waiting on a human. */
|
|
41
|
+
parameter: z.object({ name: z.string(), label: z.string() }).nullish(),
|
|
42
|
+
statusDetail: STATUS_DETAIL,
|
|
43
|
+
consecutiveFailedLoginAttempts: z.number().nullish(),
|
|
44
|
+
});
|
|
45
|
+
/**
|
|
46
|
+
* `GET /consents?itemId=` — the paged envelope of the consent behind one item.
|
|
47
|
+
*
|
|
48
|
+
* The consent's own `itemId` deliberately does not enter this schema.
|
|
49
|
+
* `docs/research/2026-07-26-phase-0-5-recon.md` §"`GET /consents?itemId=`
|
|
50
|
+
* returns a consent belonging to a different item" found it carries the UUID
|
|
51
|
+
* of the inner MeuPluggy item — a different one on each of the three
|
|
52
|
+
* connections observed, none matching a configured id. Reading it only
|
|
53
|
+
* creates the temptation to join a consent back to our connection by that
|
|
54
|
+
* field, which would silently produce zero matches. The only reliable
|
|
55
|
+
* association is "this is what the endpoint returned when asked about that
|
|
56
|
+
* item".
|
|
57
|
+
*/
|
|
58
|
+
export const CONSENT = z.object({
|
|
59
|
+
id: z.string().min(1),
|
|
60
|
+
expiresAt: z.string().nullish(),
|
|
61
|
+
revokedAt: z.string().nullish(),
|
|
62
|
+
products: z.array(z.string()).nullish(),
|
|
63
|
+
});
|
|
64
|
+
export const CONSENT_PAGE = z.object({
|
|
65
|
+
results: z.array(CONSENT),
|
|
66
|
+
total: z.number(),
|
|
67
|
+
totalPages: z.number(),
|
|
68
|
+
page: z.number(),
|
|
69
|
+
});
|
|
70
|
+
export const ACCOUNT = z.object({
|
|
71
|
+
id: z.string().min(1),
|
|
72
|
+
itemId: z.string().min(1),
|
|
73
|
+
type: z.string(),
|
|
74
|
+
subtype: z.string().nullish(),
|
|
75
|
+
name: z.string(),
|
|
76
|
+
marketingName: z.string().nullish(),
|
|
77
|
+
balance: z.number(),
|
|
78
|
+
currencyCode: z.string(),
|
|
79
|
+
creditData: z
|
|
80
|
+
.object({
|
|
81
|
+
brand: z.string().nullish(),
|
|
82
|
+
balanceCloseDate: z.string().nullish(),
|
|
83
|
+
balanceDueDate: z.string().nullish(),
|
|
84
|
+
availableCreditLimit: z.number().nullish(),
|
|
85
|
+
creditLimit: z.number().nullish(),
|
|
86
|
+
disaggregatedCreditLimits: z
|
|
87
|
+
.array(z.object({
|
|
88
|
+
creditLineLimitType: z.string().nullish(),
|
|
89
|
+
customizedLimitAmount: z.number().nullish(),
|
|
90
|
+
}))
|
|
91
|
+
.nullish(),
|
|
92
|
+
})
|
|
93
|
+
.nullish(),
|
|
94
|
+
});
|
|
95
|
+
/** Pluggy's offset envelope for `/accounts`. */
|
|
96
|
+
export const ACCOUNT_PAGE = z.object({
|
|
97
|
+
total: z.number(),
|
|
98
|
+
totalPages: z.number(),
|
|
99
|
+
page: z.number(),
|
|
100
|
+
results: z.array(ACCOUNT),
|
|
101
|
+
});
|
|
102
|
+
const BILL_AMOUNT = z.object({
|
|
103
|
+
amount: z.number(),
|
|
104
|
+
});
|
|
105
|
+
/** The raw `GET /bills?accountId=` body for one credit-card statement. */
|
|
106
|
+
export const BILL = z.object({
|
|
107
|
+
id: z.string(),
|
|
108
|
+
accountId: z.string().optional(),
|
|
109
|
+
billClosingDate: z.string().nullish(),
|
|
110
|
+
dueDate: z.string(),
|
|
111
|
+
totalAmount: z.number(),
|
|
112
|
+
totalAmountCurrencyCode: z.string().nullish(),
|
|
113
|
+
minimumPaymentAmount: z.number().nullish(),
|
|
114
|
+
allowsInstallments: z.boolean().nullish(),
|
|
115
|
+
financeCharges: z.array(BILL_AMOUNT),
|
|
116
|
+
payments: z.array(BILL_AMOUNT),
|
|
117
|
+
});
|
|
118
|
+
/** Pluggy's offset envelope for `/bills`. */
|
|
119
|
+
export const BILL_PAGE = z.object({
|
|
120
|
+
total: z.number(),
|
|
121
|
+
totalPages: z.number(),
|
|
122
|
+
page: z.number(),
|
|
123
|
+
results: z.array(BILL),
|
|
124
|
+
});
|
|
125
|
+
/** Nested fields Pluggy omits entirely when a card row has no such detail. */
|
|
126
|
+
export const CREDIT_CARD_METADATA = z.object({
|
|
127
|
+
billId: z.string().optional(),
|
|
128
|
+
installmentNumber: z.number().optional(),
|
|
129
|
+
totalInstallments: z.number().optional(),
|
|
130
|
+
cardNumber: z.string().optional(),
|
|
131
|
+
payeeMCC: z.number().optional(),
|
|
132
|
+
purchaseDate: z.string().optional(),
|
|
133
|
+
feeType: z.string().optional(),
|
|
134
|
+
billForecastDate: z.string().optional(),
|
|
135
|
+
});
|
|
136
|
+
export const PAYMENT_DATA = z.object({
|
|
137
|
+
receiver: z
|
|
138
|
+
.object({
|
|
139
|
+
name: z.string().nullish(),
|
|
140
|
+
documentNumber: z.object({ value: z.string().optional(), type: z.string().optional() }).optional(),
|
|
141
|
+
})
|
|
142
|
+
.nullish(),
|
|
143
|
+
paymentMethod: z.string().nullish(),
|
|
144
|
+
});
|
|
145
|
+
export const TRANSACTION = z.object({
|
|
146
|
+
id: z.string(),
|
|
147
|
+
accountId: z.string(),
|
|
148
|
+
date: z.string(),
|
|
149
|
+
description: z.string(),
|
|
150
|
+
descriptionRaw: z.string().nullish(),
|
|
151
|
+
amount: z.number(),
|
|
152
|
+
amountInAccountCurrency: z.number().nullish(),
|
|
153
|
+
currencyCode: z.string().nullish(),
|
|
154
|
+
category: z.string().nullish(),
|
|
155
|
+
categoryId: z.string().nullish(),
|
|
156
|
+
status: z.string().nullish(),
|
|
157
|
+
creditCardMetadata: CREDIT_CARD_METADATA.nullish(),
|
|
158
|
+
paymentData: PAYMENT_DATA.nullish(),
|
|
159
|
+
});
|
|
160
|
+
/** The cursor-paginated envelope for `/v2/transactions`. */
|
|
161
|
+
export const TRANSACTION_PAGE = z.object({
|
|
162
|
+
results: z.array(TRANSACTION),
|
|
163
|
+
next: z.string().nullable(),
|
|
164
|
+
});
|
|
165
|
+
export const CATEGORY = z.object({
|
|
166
|
+
id: z.string(),
|
|
167
|
+
description: z.string(),
|
|
168
|
+
descriptionTranslated: z.string().nullish(),
|
|
169
|
+
parentId: z.string().nullish(),
|
|
170
|
+
parentDescription: z.string().nullish(),
|
|
171
|
+
});
|
|
172
|
+
export const CATEGORY_PAGE = z.object({
|
|
173
|
+
results: z.array(CATEGORY),
|
|
174
|
+
total: z.number(),
|
|
175
|
+
totalPages: z.number(),
|
|
176
|
+
page: z.number(),
|
|
177
|
+
});
|
|
178
|
+
/** Reduces the one supported category page into the pure core taxonomy shape. */
|
|
179
|
+
export function parseCategoryPage(page) {
|
|
180
|
+
if (page.totalPages !== 1) {
|
|
181
|
+
throw new Error(`Categories totalPages is ${page.totalPages}; expected one complete page`);
|
|
182
|
+
}
|
|
183
|
+
return page.results.map((category) => ({
|
|
184
|
+
id: category.id,
|
|
185
|
+
parentId: category.parentId ?? null,
|
|
186
|
+
parentDescription: category.parentDescription ?? null,
|
|
187
|
+
}));
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Pluggy's error envelope. Read on every non-2xx, because the status alone is not
|
|
191
|
+
* a diagnosis: one 400 means a malformed request, another means a consent the
|
|
192
|
+
* user revoked, and only `codeDescription` and `message` separate them. Throwing
|
|
193
|
+
* "Pluggy returned 400" and dropping the rest is §16.2's scar, where the evidence
|
|
194
|
+
* was deleted before a human could see it.
|
|
195
|
+
*
|
|
196
|
+
* Every field is optional and `data` falls back rather than failing, because an
|
|
197
|
+
* error we cannot fully parse is still an error worth reporting.
|
|
198
|
+
*/
|
|
199
|
+
export const API_ERROR = z.object({
|
|
200
|
+
codeDescription: z.string().nullish(),
|
|
201
|
+
message: z.string().nullish(),
|
|
202
|
+
data: z
|
|
203
|
+
.object({ canRetryAfterDate: z.string().nullish() })
|
|
204
|
+
.nullish()
|
|
205
|
+
.catch(null),
|
|
206
|
+
});
|
|
207
|
+
//# sourceMappingURL=wire.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wire.js","sourceRoot":"","sources":["../../src/pluggy/wire.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { isDocument } from "../core/counterparty.js";
|
|
2
|
+
const systemClock = { now: () => new Date() };
|
|
3
|
+
const OVERRIDE_UPSERT = `
|
|
4
|
+
INSERT INTO userdata.category_overrides (transaction_id, category, created_at)
|
|
5
|
+
VALUES (?, ?, ?)
|
|
6
|
+
ON CONFLICT(transaction_id) DO UPDATE SET
|
|
7
|
+
category = excluded.category,
|
|
8
|
+
created_at = excluded.created_at
|
|
9
|
+
`;
|
|
10
|
+
const COUNTERPARTY_UPSERT = `
|
|
11
|
+
INSERT INTO userdata.counterparty_categories (document, category, origin, samples, agreeing, created_at)
|
|
12
|
+
VALUES (?, ?, 'manual', NULL, NULL, ?)
|
|
13
|
+
ON CONFLICT(document) DO UPDATE SET
|
|
14
|
+
category = excluded.category,
|
|
15
|
+
origin = 'manual',
|
|
16
|
+
samples = NULL,
|
|
17
|
+
agreeing = NULL,
|
|
18
|
+
created_at = excluded.created_at
|
|
19
|
+
`;
|
|
20
|
+
/**
|
|
21
|
+
* How many cached rows this counterparty now answers for.
|
|
22
|
+
*
|
|
23
|
+
* Rows carrying an override are excluded, because an override outranks the
|
|
24
|
+
* counterparty branch and those rows do not move. Counting them would overstate
|
|
25
|
+
* the blast radius in the one number a user reads to decide whether a write was
|
|
26
|
+
* broader than they meant (ADR §12.6).
|
|
27
|
+
*/
|
|
28
|
+
const AFFECTED_COUNT = `
|
|
29
|
+
SELECT COUNT(*) AS affected FROM transactions t
|
|
30
|
+
WHERE t.document = ?
|
|
31
|
+
AND NOT EXISTS (SELECT 1 FROM userdata.category_overrides o WHERE o.transaction_id = t.id)
|
|
32
|
+
`;
|
|
33
|
+
export function createCategoryWriter(db, clock = systemClock) {
|
|
34
|
+
const context = {
|
|
35
|
+
db,
|
|
36
|
+
clock,
|
|
37
|
+
override: db.prepare(OVERRIDE_UPSERT),
|
|
38
|
+
counterparty: db.prepare(COUNTERPARTY_UPSERT),
|
|
39
|
+
affected: db.prepare(AFFECTED_COUNT),
|
|
40
|
+
};
|
|
41
|
+
return {
|
|
42
|
+
setCategory: (ids, category) => setCategory(context, ids, category),
|
|
43
|
+
setCounterpartyCategory: (document, category) => setCounterpartyCategory(context, document, category),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The ids we hold, so unknown ones can be reported rather than raised. An agent
|
|
48
|
+
* working from a stale listing should get its known rows written and the rest
|
|
49
|
+
* named back (PRD item 4).
|
|
50
|
+
*/
|
|
51
|
+
function knownIds(db, ids) {
|
|
52
|
+
const sql = `SELECT id FROM transactions WHERE id IN (${placeholders(ids.length)})`;
|
|
53
|
+
const rows = db.prepare(sql).all(...ids);
|
|
54
|
+
return new Set(rows.map((row) => String(row.id)));
|
|
55
|
+
}
|
|
56
|
+
function setCategory(context, ids, category) {
|
|
57
|
+
if (ids.length === 0) {
|
|
58
|
+
return { updated: 0, unknownIds: [] };
|
|
59
|
+
}
|
|
60
|
+
const known = knownIds(context.db, ids);
|
|
61
|
+
const unknownIds = ids.filter((id) => !known.has(id));
|
|
62
|
+
if (known.size === 0) {
|
|
63
|
+
return { updated: 0, unknownIds };
|
|
64
|
+
}
|
|
65
|
+
const now = context.clock.now().toISOString();
|
|
66
|
+
inTransaction(context.db, () => {
|
|
67
|
+
for (const id of known) {
|
|
68
|
+
context.override.run(id, category, now);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
return { updated: known.size, unknownIds };
|
|
72
|
+
}
|
|
73
|
+
function setCounterpartyCategory(context, document, category) {
|
|
74
|
+
const digits = document.replace(/\D/gu, "");
|
|
75
|
+
if (!isDocument(digits)) {
|
|
76
|
+
throw new Error("document must be an 11-digit CPF or 14-digit CNPJ");
|
|
77
|
+
}
|
|
78
|
+
const now = context.clock.now().toISOString();
|
|
79
|
+
let affected = 0;
|
|
80
|
+
inTransaction(context.db, () => {
|
|
81
|
+
context.counterparty.run(digits, category, now);
|
|
82
|
+
const row = context.affected.get(digits);
|
|
83
|
+
affected = Number(row?.affected ?? 0);
|
|
84
|
+
});
|
|
85
|
+
return { affected };
|
|
86
|
+
}
|
|
87
|
+
function inTransaction(db, work) {
|
|
88
|
+
db.exec("BEGIN");
|
|
89
|
+
try {
|
|
90
|
+
work();
|
|
91
|
+
db.exec("COMMIT");
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
db.exec("ROLLBACK");
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function placeholders(count) {
|
|
99
|
+
return Array.from({ length: count }, () => "?").join(", ");
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=categories.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"categories.js","sourceRoot":"","sources":["../../src/storage/categories.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { BRANCHES } from "../core/category-source.js";
|
|
2
|
+
/**
|
|
3
|
+
* The six branches as six columns, plus the durable leaf.
|
|
4
|
+
*
|
|
5
|
+
* `c_leaf` is the finest category we still know for the row: what Pluggy sends
|
|
6
|
+
* today, and what the snapshot remembers once it stops sending anything. It is
|
|
7
|
+
* not a seventh branch — it is not rolled up and never competes for the derived
|
|
8
|
+
* category. It exists because the self-transfer exclusion in `core/aggregate.ts`
|
|
9
|
+
* distinguishes a credit card payment from an ordinary transfer, and only the
|
|
10
|
+
* leaf carries that distinction. Reading `transactions.category_id` directly
|
|
11
|
+
* there would let every card bill payment re-enter `spent` the day the plan
|
|
12
|
+
* drops to free.
|
|
13
|
+
*
|
|
14
|
+
* Resolution happens in `core/category-source.ts`, driven by the same array
|
|
15
|
+
* that builds this.
|
|
16
|
+
*
|
|
17
|
+
* There is deliberately no `CREATE VIEW`. A view living in the droppable file
|
|
18
|
+
* and referencing `userdata.` would couple `cache.db`'s schema to a schema name
|
|
19
|
+
* that has to be attached already — including during the migration run at
|
|
20
|
+
* startup, before any attach has happened.
|
|
21
|
+
*/
|
|
22
|
+
export const DERIVED_COLUMNS = `
|
|
23
|
+
(SELECT o.category FROM userdata.category_overrides o
|
|
24
|
+
WHERE o.transaction_id = t.id) AS c_override,
|
|
25
|
+
(SELECT c.category FROM userdata.counterparty_categories c
|
|
26
|
+
WHERE t.document IS NOT NULL AND c.document = t.document AND c.origin = 'manual') AS c_counterparty,
|
|
27
|
+
t.top_category_id AS c_pluggy,
|
|
28
|
+
(SELECT s.top_category_id FROM userdata.category_snapshot s
|
|
29
|
+
WHERE s.transaction_id = t.id) AS c_snapshot,
|
|
30
|
+
(SELECT c.category FROM userdata.counterparty_categories c
|
|
31
|
+
WHERE t.document IS NOT NULL AND c.document = t.document AND c.origin = 'learned') AS c_learned,
|
|
32
|
+
(SELECT m.category FROM mcc_categories m WHERE m.mcc = t.mcc) AS c_mcc,
|
|
33
|
+
COALESCE(t.category_id, (SELECT s.category_id FROM userdata.category_snapshot s
|
|
34
|
+
WHERE s.transaction_id = t.id)) AS c_leaf
|
|
35
|
+
`;
|
|
36
|
+
export const DERIVED_CATEGORY = `COALESCE(${BRANCHES.map((branch) => `c_${branch}`).join(", ")})`;
|
|
37
|
+
//# sourceMappingURL=category-sql.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"category-sql.js","sourceRoot":"","sources":["../../src/storage/category-sql.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,4BAA4B,CAAC;AAEtD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG;;;;;;;;;;;;;CAa9B,CAAC;AAEF,MAAM,CAAC,MAAM,gBAAgB,GAAG,YAAY,QAAQ,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,KAAK,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const CLOSING_DAY_UPSERT = `
|
|
2
|
+
INSERT INTO userdata.card_closing_day (account_id, day, created_at, updated_at)
|
|
3
|
+
VALUES (?, ?, ?, ?)
|
|
4
|
+
ON CONFLICT(account_id) DO UPDATE SET
|
|
5
|
+
day = excluded.day,
|
|
6
|
+
updated_at = excluded.updated_at
|
|
7
|
+
`;
|
|
8
|
+
const systemClock = { now: () => new Date() };
|
|
9
|
+
/** Builds the local store for user-provided card billing-cycle closing days. */
|
|
10
|
+
export function createClosingDayStore(db, clock = systemClock) {
|
|
11
|
+
const upsert = db.prepare(CLOSING_DAY_UPSERT);
|
|
12
|
+
const list = db.prepare("SELECT account_id, day FROM userdata.card_closing_day ORDER BY account_id");
|
|
13
|
+
const remove = db.prepare("DELETE FROM userdata.card_closing_day WHERE account_id = ?");
|
|
14
|
+
return {
|
|
15
|
+
list: () => listClosingDays(list),
|
|
16
|
+
set: (accountId, day) => {
|
|
17
|
+
const now = clock.now().toISOString();
|
|
18
|
+
upsert.run(accountId, day, now, now);
|
|
19
|
+
},
|
|
20
|
+
delete: (accountId) => Number(remove.run(accountId).changes),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function listClosingDays(statement) {
|
|
24
|
+
const rows = statement.all();
|
|
25
|
+
return rows.map((row) => ({ accountId: String(row["account_id"]), day: Number(row["day"]) }));
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=closing-days.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"closing-days.js","sourceRoot":"","sources":["../../src/storage/closing-days.ts"],"names":[],"mappings":"AAIA,MAAM,kBAAkB,GAAG;;;;;;CAM1B,CAAC;AAEF,MAAM,WAAW,GAAU,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,IAAI,IAAI,EAAE,EAAE,CAAC;AAErD,gFAAgF;AAChF,MAAM,UAAU,qBAAqB,CAAC,EAAgB,EAAE,QAAe,WAAW;IAChF,MAAM,MAAM,GAAG,EAAE,CAAC,OAAO,CAAC,kBAAkB,CAAC,CAAC;IAC9C,MAAM,IAAI,GAAG,EAAE,CAAC,OAAO,CAAC,2EAA2E,CAAC,CAAC;IACrG,MAAM,MAAM,GAAG,EAAE,CAAC,OAAO,CAAC,4DAA4D,CAAC,CAAC;IAExF,OAAO;QACL,IAAI,EAAE,GAAG,EAAE,CAAC,eAAe,CAAC,IAAI,CAAC;QACjC,GAAG,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE,EAAE;YACtB,MAAM,GAAG,GAAG,KAAK,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,CAAC;YACtC,MAAM,CAAC,GAAG,CAAC,SAAS,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;QACvC,CAAC;QACD,MAAM,EAAE,CAAC,SAAS,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,OAAO,CAAC;KAC7D,CAAC;AACJ,CAAC;AAED,SAAS,eAAe,CAAC,SAA8C;IACrE,MAAM,IAAI,GAAG,SAAS,CAAC,GAAG,EAA+B,CAAC;IAC1D,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AAChG,CAAC"}
|