@chainlesschain/personal-data-hub 0.4.18 → 0.4.24
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/__tests__/adapters/bank-family.test.js +125 -0
- package/__tests__/adapters/biz-tianyancha.test.js +159 -0
- package/__tests__/adapters/car-mercedesme.test.js +74 -0
- package/__tests__/adapters/doc-camscanner.test.js +147 -0
- package/__tests__/adapters/finance-dcep.test.js +74 -0
- package/__tests__/adapters/fitness-joyrun.test.js +82 -0
- package/__tests__/adapters/gov-12123.test.js +103 -0
- package/__tests__/adapters/gov-ixiamen.test.js +150 -0
- package/__tests__/adapters/gov-tax.test.js +135 -0
- package/__tests__/adapters/health-meiyou.test.js +125 -0
- package/__tests__/adapters/music-qq.test.js +112 -0
- package/__tests__/adapters/reading-family.test.js +108 -0
- package/__tests__/adapters/social-dongchedi.test.js +165 -0
- package/__tests__/adapters/travel-didi-consumer.test.js +66 -0
- package/__tests__/adapters/video-xigua.test.js +106 -0
- package/__tests__/adapters/wework-pc.test.js +124 -0
- package/__tests__/audio-ximalaya-snapshot.test.js +279 -0
- package/__tests__/fitness-keep-snapshot.test.js +224 -0
- package/__tests__/shopping-eleme-snapshot.test.js +454 -0
- package/__tests__/shopping-vipshop-snapshot.test.js +425 -0
- package/__tests__/shopping-xianyu-snapshot.test.js +451 -0
- package/__tests__/social-douban-snapshot.test.js +351 -0
- package/lib/adapter-guide.js +31 -3
- package/lib/adapters/_bank-base.js +405 -0
- package/lib/adapters/_reading-base.js +315 -0
- package/lib/adapters/audio-ximalaya/index.js +414 -0
- package/lib/adapters/bank-bankcomm/index.js +27 -0
- package/lib/adapters/bank-boc/index.js +26 -0
- package/lib/adapters/bank-cmbc/index.js +26 -0
- package/lib/adapters/bank-icbc/index.js +27 -0
- package/lib/adapters/biz-tianyancha/index.js +348 -0
- package/lib/adapters/car-mercedesme/index.js +225 -0
- package/lib/adapters/doc-camscanner/index.js +102 -0
- package/lib/adapters/finance-dcep/index.js +302 -0
- package/lib/adapters/fitness-joyrun/index.js +295 -0
- package/lib/adapters/fitness-keep/index.js +343 -0
- package/lib/adapters/gov-12123/index.js +391 -0
- package/lib/adapters/gov-ixiamen/index.js +380 -0
- package/lib/adapters/gov-tax/index.js +451 -0
- package/lib/adapters/health-meiyou/index.js +393 -0
- package/lib/adapters/music-qq/index.js +372 -0
- package/lib/adapters/reading-fanqie/index.js +61 -0
- package/lib/adapters/reading-qimao/index.js +61 -0
- package/lib/adapters/shopping-eleme/index.js +441 -0
- package/lib/adapters/shopping-vipshop/index.js +429 -0
- package/lib/adapters/shopping-xianyu/index.js +454 -0
- package/lib/adapters/social-dongchedi/index.js +360 -0
- package/lib/adapters/social-douban/index.js +564 -0
- package/lib/adapters/travel-didi-consumer/index.js +148 -0
- package/lib/adapters/video-xigua/index.js +68 -0
- package/lib/adapters/wework-pc/index.js +31 -0
- package/lib/index.js +52 -0
- package/package.json +1 -1
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* _bank-base — shared infrastructure for personal-banking adapters (民生 /
|
|
3
|
+
* 中行 / 交行 / ...), §12.1 Phase 13+ "交易明细 + 信用卡".
|
|
4
|
+
*
|
|
5
|
+
* ⚠️ MAXIMALLY SENSITIVE (real-name banking, strong-auth + 可能人脸).
|
|
6
|
+
* BEST-EFFORT SCAFFOLDS: mobile-bank apps have NO documented public API and
|
|
7
|
+
* sit behind real-name SSO; the cookie-api endpoints supplied by each wrapper
|
|
8
|
+
* are FABRICATED placeholders (overridable, NOT field-verified — FAMILY-23
|
|
9
|
+
* playbook) and cannot authenticate without the bank's real login. **snapshot
|
|
10
|
+
* mode is the reliable path** (the app / a manual 交易明细 + 账单 export
|
|
11
|
+
* produces a JSON); the cookie path is a seam only, surfaces
|
|
12
|
+
* `auth.unverified=true`. Every bank adapter is gated sensitivity:"high" +
|
|
13
|
+
* legalGate:true — the registry REQUIRES explicit legal/consent confirmation
|
|
14
|
+
* before any collection runs.
|
|
15
|
+
*
|
|
16
|
+
* Two record kinds, uniform across banks:
|
|
17
|
+
* - "transaction" 交易明细: { txId, time, amount, direction(debit支出/credit收入),
|
|
18
|
+
* counterparty(对方户名), summary(摘要), balance, channel } → EVENT(PAYMENT).
|
|
19
|
+
* - "card" 信用卡账单: { billId, billMonth(YYYY-MM), statementAmount,
|
|
20
|
+
* minPayment, dueDate, status } → EVENT(OTHER).
|
|
21
|
+
*
|
|
22
|
+
* Snapshot schema (schemaVersion 1):
|
|
23
|
+
* {
|
|
24
|
+
* "schemaVersion": 1, "snapshottedAt": <ms>,
|
|
25
|
+
* "account": { "userId": "...", "name": "..." },
|
|
26
|
+
* "events": [
|
|
27
|
+
* { "kind": "transaction", "id": "tx-<id>", "txId": "...", "time": <s|ms>,
|
|
28
|
+
* "amount": 123.45, "direction": "debit", "counterparty": "...",
|
|
29
|
+
* "summary": "...", "balance": 9999.0, "channel": "手机银行" },
|
|
30
|
+
* { "kind": "card", "id": "card-<id>", "billId": "...", "billMonth": "2025-03",
|
|
31
|
+
* "statementAmount": 3210.0, "minPayment": 321.0, "dueDate": <s|ms>,
|
|
32
|
+
* "status": "已出账" }
|
|
33
|
+
* ]
|
|
34
|
+
* }
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
"use strict";
|
|
38
|
+
|
|
39
|
+
const fs = require("node:fs");
|
|
40
|
+
const { newId } = require("../ids");
|
|
41
|
+
const { ENTITY_TYPES, EVENT_SUBTYPES, CAPTURED_BY } = require("../constants");
|
|
42
|
+
|
|
43
|
+
const SNAPSHOT_SCHEMA_VERSION = 1;
|
|
44
|
+
const KIND_TRANSACTION = "transaction";
|
|
45
|
+
const KIND_CARD = "card";
|
|
46
|
+
const VALID_SNAPSHOT_KINDS = Object.freeze([KIND_TRANSACTION, KIND_CARD]);
|
|
47
|
+
const PAGE_SIZE = 30;
|
|
48
|
+
|
|
49
|
+
function parseTime(v) {
|
|
50
|
+
if (Number.isFinite(v)) return v > 1e12 ? v : v >= 1e9 ? v * 1000 : v;
|
|
51
|
+
if (typeof v === "string") {
|
|
52
|
+
if (/^\d+$/.test(v)) {
|
|
53
|
+
const n = parseInt(v, 10);
|
|
54
|
+
return n > 1e12 ? n : n >= 1e9 ? n * 1000 : n;
|
|
55
|
+
}
|
|
56
|
+
const t = Date.parse(v);
|
|
57
|
+
return Number.isFinite(t) ? t : null;
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function toAmount(v) {
|
|
63
|
+
if (Number.isFinite(v)) return v;
|
|
64
|
+
if (typeof v === "string") {
|
|
65
|
+
const n = parseFloat(v.replace(/[,,¥\s]/g, ""));
|
|
66
|
+
return Number.isFinite(n) ? n : null;
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Normalize a raw direction hint → "debit" (支出) | "credit" (收入).
|
|
72
|
+
function normDirection(raw) {
|
|
73
|
+
const d = raw.direction || raw.dcFlag || raw.dc_flag || raw.flag || raw.type;
|
|
74
|
+
const s = String(d == null ? "" : d).toLowerCase();
|
|
75
|
+
if (/credit|收入|入账|贷|^c$|^cr$|\+/.test(s)) return "credit";
|
|
76
|
+
if (/debit|支出|出账|借|^d$|^dr$|-/.test(s)) return "debit";
|
|
77
|
+
// fall back to amount sign
|
|
78
|
+
const amt = toAmount(raw.amount != null ? raw.amount : raw.tranAmount);
|
|
79
|
+
if (Number.isFinite(amt)) return amt < 0 ? "debit" : "credit";
|
|
80
|
+
return "debit";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function mapTransaction(raw) {
|
|
84
|
+
if (!raw || typeof raw !== "object") return null;
|
|
85
|
+
const id = raw.txId || raw.tx_id || raw.id || raw.serialNo || raw.tranSeq || raw.seq;
|
|
86
|
+
if (id == null) return null;
|
|
87
|
+
const amt = toAmount(raw.amount != null ? raw.amount : raw.tranAmount != null ? raw.tranAmount : raw.amt);
|
|
88
|
+
return {
|
|
89
|
+
txId: String(id),
|
|
90
|
+
timeMs: parseTime(raw.time || raw.tranTime || raw.tran_time || raw.date || raw.transactionTime),
|
|
91
|
+
amount: amt != null ? Math.abs(amt) : null,
|
|
92
|
+
direction: normDirection(raw),
|
|
93
|
+
counterparty: raw.counterparty || raw.counterParty || raw.payee || raw.oppName || raw.merchant || null,
|
|
94
|
+
summary: raw.summary || raw.abstract || raw.remark || raw.desc || raw.tranType || "交易",
|
|
95
|
+
balance: toAmount(raw.balance != null ? raw.balance : raw.bal),
|
|
96
|
+
channel: raw.channel || raw.chnl || null,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function mapCard(raw) {
|
|
101
|
+
if (!raw || typeof raw !== "object") return null;
|
|
102
|
+
const id = raw.billId || raw.bill_id || raw.id || raw.statementId || raw.billMonth || raw.bill_month;
|
|
103
|
+
if (id == null) return null;
|
|
104
|
+
return {
|
|
105
|
+
billId: String(id),
|
|
106
|
+
billMonth: raw.billMonth || raw.bill_month || raw.month || raw.period || null,
|
|
107
|
+
statementAmount: toAmount(raw.statementAmount != null ? raw.statementAmount : raw.amount != null ? raw.amount : raw.totalAmount),
|
|
108
|
+
minPayment: toAmount(raw.minPayment != null ? raw.minPayment : raw.minRepay),
|
|
109
|
+
dueMs: parseTime(raw.dueDate || raw.due_date || raw.repayDate),
|
|
110
|
+
status: raw.status || raw.statusName || raw.state || null,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function extractList(resp) {
|
|
115
|
+
if (!resp || typeof resp !== "object") return [];
|
|
116
|
+
if (Array.isArray(resp.list)) return resp.list;
|
|
117
|
+
if (Array.isArray(resp.data)) return resp.data;
|
|
118
|
+
const d = resp.data;
|
|
119
|
+
if (d && typeof d === "object") {
|
|
120
|
+
if (Array.isArray(d.list)) return d.list;
|
|
121
|
+
if (Array.isArray(d.records)) return d.records;
|
|
122
|
+
if (Array.isArray(d.result)) return d.result;
|
|
123
|
+
if (Array.isArray(d.details)) return d.details;
|
|
124
|
+
}
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function billMonthToMs(billMonth) {
|
|
129
|
+
if (billMonth == null) return null;
|
|
130
|
+
const m = String(billMonth).match(/^(\d{4})[-/]?(\d{1,2})/);
|
|
131
|
+
if (m) {
|
|
132
|
+
const t = Date.parse(`${m[1]}-${String(m[2]).padStart(2, "0")}-01T00:00:00Z`);
|
|
133
|
+
return Number.isFinite(t) ? t : null;
|
|
134
|
+
}
|
|
135
|
+
return parseTime(billMonth);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Build a bank adapter class.
|
|
140
|
+
* @param {object} cfg
|
|
141
|
+
* @param {string} cfg.NAME e.g. "bank-cmbc"
|
|
142
|
+
* @param {string} cfg.VERSION
|
|
143
|
+
* @param {string} cfg.platform e.g. "cmbc"
|
|
144
|
+
* @param {string} cfg.defaultTxUrl best-effort transaction-list endpoint
|
|
145
|
+
* @param {string} cfg.defaultCardUrl best-effort credit-card-bill endpoint
|
|
146
|
+
*/
|
|
147
|
+
function createBankAdapter(cfg) {
|
|
148
|
+
const { NAME, VERSION, platform, defaultTxUrl, defaultCardUrl } = cfg;
|
|
149
|
+
const { CookieAuth } = require("./shopping-base");
|
|
150
|
+
|
|
151
|
+
function stableOriginalId(kind, id) {
|
|
152
|
+
const safe =
|
|
153
|
+
(typeof id === "string" && id.length > 0 && id) ||
|
|
154
|
+
(typeof id === "number" && Number.isFinite(id) && String(id)) ||
|
|
155
|
+
`unknown-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
156
|
+
return `${platform}:${kind}:${safe}`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
class BankAdapter {
|
|
160
|
+
constructor(opts = {}) {
|
|
161
|
+
this.account = opts.account || null;
|
|
162
|
+
this._cookieAuth =
|
|
163
|
+
opts.account && opts.account.cookies
|
|
164
|
+
? new CookieAuth({ platform, cookies: opts.account.cookies })
|
|
165
|
+
: null;
|
|
166
|
+
this._fetchFn = typeof opts.fetchFn === "function" ? opts.fetchFn : defaultFetch;
|
|
167
|
+
this._signProvider = typeof opts.signProvider === "function" ? opts.signProvider : null;
|
|
168
|
+
this._urls = {
|
|
169
|
+
transaction: opts.transactionUrl || opts.listUrl || defaultTxUrl,
|
|
170
|
+
card: opts.cardUrl || defaultCardUrl,
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
this.name = NAME;
|
|
174
|
+
this.version = VERSION;
|
|
175
|
+
this.capabilities = ["sync:snapshot", "sync:cookie-api", `parse:${platform}-transaction`, `parse:${platform}-card`];
|
|
176
|
+
this.extractMode = "web-api";
|
|
177
|
+
this.rateLimits = { perMinute: 5, perDay: 60 };
|
|
178
|
+
this.dataDisclosure = {
|
|
179
|
+
fields: [
|
|
180
|
+
`${platform}:transaction (time / amount / direction / counterparty / balance)`,
|
|
181
|
+
`${platform}:card (billMonth / statementAmount / status)`,
|
|
182
|
+
],
|
|
183
|
+
sensitivity: "high",
|
|
184
|
+
legalGate: true,
|
|
185
|
+
defaultInclude: { transaction: true, card: true },
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
this._deps = { fs };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async authenticate(ctx = {}) {
|
|
192
|
+
if (ctx && typeof ctx.inputPath === "string" && ctx.inputPath.length > 0) {
|
|
193
|
+
try {
|
|
194
|
+
this._deps.fs.accessSync(ctx.inputPath, this._deps.fs.constants.R_OK);
|
|
195
|
+
} catch (err) {
|
|
196
|
+
return { ok: false, reason: "INPUT_PATH_UNREADABLE", message: `snapshot not readable at ${ctx.inputPath}: ${err.message}` };
|
|
197
|
+
}
|
|
198
|
+
return { ok: true, mode: "snapshot-file" };
|
|
199
|
+
}
|
|
200
|
+
if (this._cookieAuth) {
|
|
201
|
+
const ok = await this._cookieAuth.validate();
|
|
202
|
+
if (!ok) return { ok: false, reason: "INVALID_COOKIE", error: "cookies missing" };
|
|
203
|
+
return { ok: true, account: (this.account && this.account.userId) || null, mode: "cookie", unverified: true };
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
ok: false,
|
|
207
|
+
reason: "NO_INPUT",
|
|
208
|
+
message: `${NAME}.authenticate: needs opts.inputPath (snapshot mode) OR opts.account.cookies (cookie-api mode, best-effort/unverified)`,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async healthCheck() {
|
|
213
|
+
if (this._cookieAuth) {
|
|
214
|
+
const r = await this.authenticate();
|
|
215
|
+
return r.ok ? { ok: true, lastChecked: Date.now(), unverified: true } : { ok: false, reason: r.reason, error: r.error };
|
|
216
|
+
}
|
|
217
|
+
return { ok: true, lastChecked: Date.now() };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async *sync(opts = {}) {
|
|
221
|
+
if (typeof opts.inputPath === "string" && opts.inputPath.length > 0) {
|
|
222
|
+
yield* this._syncViaSnapshot(opts);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (this._cookieAuth) {
|
|
226
|
+
yield* this._syncViaCookie(opts);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
throw new Error(`${NAME}.sync: needs opts.inputPath (snapshot mode) OR opts.account.cookies (cookie-api mode)`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async *_syncViaSnapshot(opts) {
|
|
233
|
+
const raw = this._deps.fs.readFileSync(opts.inputPath, "utf-8");
|
|
234
|
+
const snapshot = JSON.parse(raw);
|
|
235
|
+
if (!snapshot || typeof snapshot !== "object" || snapshot.schemaVersion !== SNAPSHOT_SCHEMA_VERSION) {
|
|
236
|
+
throw new Error(`${NAME}.sync: snapshot schemaVersion mismatch (got ${snapshot && snapshot.schemaVersion}, expected ${SNAPSHOT_SCHEMA_VERSION})`);
|
|
237
|
+
}
|
|
238
|
+
const fallbackCapturedAt =
|
|
239
|
+
Number.isFinite(snapshot.snapshottedAt) && snapshot.snapshottedAt > 0 ? Math.floor(snapshot.snapshottedAt) : Date.now();
|
|
240
|
+
const account = snapshot.account && typeof snapshot.account === "object" ? snapshot.account : null;
|
|
241
|
+
const include = opts.include || {};
|
|
242
|
+
const limit = Number.isInteger(opts.limit) && opts.limit > 0 ? opts.limit : Infinity;
|
|
243
|
+
|
|
244
|
+
const events = Array.isArray(snapshot.events) ? snapshot.events : [];
|
|
245
|
+
let emitted = 0;
|
|
246
|
+
for (const ev of events) {
|
|
247
|
+
if (emitted >= limit) return;
|
|
248
|
+
if (!ev || typeof ev !== "object") continue;
|
|
249
|
+
if (!VALID_SNAPSHOT_KINDS.includes(ev.kind)) continue;
|
|
250
|
+
if (include[ev.kind] === false) continue;
|
|
251
|
+
|
|
252
|
+
const rec = ev.kind === KIND_TRANSACTION ? mapTransaction(ev) : mapCard(ev);
|
|
253
|
+
if (!rec) continue;
|
|
254
|
+
const recTime = ev.kind === KIND_TRANSACTION ? rec.timeMs : billMonthToMs(rec.billMonth) || rec.dueMs;
|
|
255
|
+
const capturedAt = parseTime(ev.capturedAt) || recTime || fallbackCapturedAt;
|
|
256
|
+
yield {
|
|
257
|
+
adapter: NAME,
|
|
258
|
+
kind: ev.kind,
|
|
259
|
+
originalId: stableOriginalId(ev.kind, ev.kind === KIND_TRANSACTION ? rec.txId : rec.billId),
|
|
260
|
+
capturedAt,
|
|
261
|
+
payload: { record: rec, kind: ev.kind, account },
|
|
262
|
+
};
|
|
263
|
+
emitted += 1;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async *_syncViaCookie(opts = {}) {
|
|
268
|
+
if (!(await this._cookieAuth.validate())) return;
|
|
269
|
+
const cookies = this._cookieAuth.toHeader();
|
|
270
|
+
const include = opts.include || {};
|
|
271
|
+
const limit = Number.isInteger(opts.limit) && opts.limit > 0 ? opts.limit : Infinity;
|
|
272
|
+
const maxPages = Number.isInteger(opts.maxPages) && opts.maxPages > 0 ? opts.maxPages : 12;
|
|
273
|
+
|
|
274
|
+
const plan = [
|
|
275
|
+
{ kind: KIND_TRANSACTION, url: this._urls.transaction, map: mapTransaction, idOf: (r) => r.txId, ts: (r) => r.timeMs },
|
|
276
|
+
{ kind: KIND_CARD, url: this._urls.card, map: mapCard, idOf: (r) => r.billId, ts: (r) => billMonthToMs(r.billMonth) || r.dueMs },
|
|
277
|
+
];
|
|
278
|
+
|
|
279
|
+
let emitted = 0;
|
|
280
|
+
for (const step of plan) {
|
|
281
|
+
if (include[step.kind] === false) continue;
|
|
282
|
+
let page = 1;
|
|
283
|
+
while (page <= maxPages) {
|
|
284
|
+
const query = { page, size: PAGE_SIZE };
|
|
285
|
+
let sign = null;
|
|
286
|
+
if (this._signProvider) sign = await this._signProvider({ url: step.url, query, cookies });
|
|
287
|
+
const resp = await this._fetchFn({ url: step.url, cookies, query, sign });
|
|
288
|
+
const items = extractList(resp);
|
|
289
|
+
if (!items.length) break;
|
|
290
|
+
for (const it of items) {
|
|
291
|
+
const rec = step.map(it);
|
|
292
|
+
if (!rec) continue;
|
|
293
|
+
if (emitted >= limit) return;
|
|
294
|
+
yield {
|
|
295
|
+
adapter: NAME,
|
|
296
|
+
kind: step.kind,
|
|
297
|
+
originalId: stableOriginalId(step.kind, step.idOf(rec)),
|
|
298
|
+
capturedAt: step.ts(rec) || Date.now(),
|
|
299
|
+
payload: { record: rec, kind: step.kind, cookie: true },
|
|
300
|
+
};
|
|
301
|
+
emitted += 1;
|
|
302
|
+
}
|
|
303
|
+
if (items.length < PAGE_SIZE) break;
|
|
304
|
+
page += 1;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
normalize(raw) {
|
|
310
|
+
if (!raw || !raw.payload || !raw.payload.record) {
|
|
311
|
+
throw new Error(`${NAME}.normalize: payload.record missing`);
|
|
312
|
+
}
|
|
313
|
+
const kind = raw.kind || raw.payload.kind;
|
|
314
|
+
const rec = raw.payload.record;
|
|
315
|
+
const ingestedAt = Date.now();
|
|
316
|
+
const source = {
|
|
317
|
+
adapter: NAME,
|
|
318
|
+
adapterVersion: VERSION,
|
|
319
|
+
originalId: raw.originalId,
|
|
320
|
+
capturedAt: raw.capturedAt || ingestedAt,
|
|
321
|
+
capturedBy: CAPTURED_BY.API,
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
if (kind === KIND_TRANSACTION) {
|
|
325
|
+
const occurredAt = rec.timeMs || raw.capturedAt || ingestedAt;
|
|
326
|
+
const arrow = rec.direction === "credit" ? "收入" : "支出";
|
|
327
|
+
return {
|
|
328
|
+
events: [
|
|
329
|
+
{
|
|
330
|
+
id: newId(),
|
|
331
|
+
type: ENTITY_TYPES.EVENT,
|
|
332
|
+
subtype: EVENT_SUBTYPES.PAYMENT,
|
|
333
|
+
occurredAt,
|
|
334
|
+
actor: "person-self",
|
|
335
|
+
content: { title: `${arrow}: ${rec.summary}`.slice(0, 80), text: rec.summary },
|
|
336
|
+
ingestedAt,
|
|
337
|
+
source,
|
|
338
|
+
extra: {
|
|
339
|
+
platform,
|
|
340
|
+
kind: KIND_TRANSACTION,
|
|
341
|
+
amount: rec.amount,
|
|
342
|
+
direction: rec.direction,
|
|
343
|
+
counterparty: rec.counterparty || null,
|
|
344
|
+
balance: rec.balance,
|
|
345
|
+
channel: rec.channel || null,
|
|
346
|
+
},
|
|
347
|
+
},
|
|
348
|
+
],
|
|
349
|
+
persons: [],
|
|
350
|
+
places: [],
|
|
351
|
+
items: [],
|
|
352
|
+
topics: [],
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
// card
|
|
356
|
+
const occurredAt = billMonthToMs(rec.billMonth) || rec.dueMs || raw.capturedAt || ingestedAt;
|
|
357
|
+
return {
|
|
358
|
+
events: [
|
|
359
|
+
{
|
|
360
|
+
id: newId(),
|
|
361
|
+
type: ENTITY_TYPES.EVENT,
|
|
362
|
+
subtype: EVENT_SUBTYPES.OTHER,
|
|
363
|
+
occurredAt,
|
|
364
|
+
actor: "person-self",
|
|
365
|
+
content: { title: `信用卡账单${rec.billMonth ? ` ${rec.billMonth}` : ""}`.slice(0, 80), text: "信用卡账单" },
|
|
366
|
+
ingestedAt,
|
|
367
|
+
source,
|
|
368
|
+
extra: {
|
|
369
|
+
platform,
|
|
370
|
+
kind: KIND_CARD,
|
|
371
|
+
billMonth: rec.billMonth || null,
|
|
372
|
+
statementAmount: rec.statementAmount,
|
|
373
|
+
minPayment: rec.minPayment,
|
|
374
|
+
dueMs: rec.dueMs || null,
|
|
375
|
+
status: rec.status || null,
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
],
|
|
379
|
+
persons: [],
|
|
380
|
+
places: [],
|
|
381
|
+
items: [],
|
|
382
|
+
topics: [],
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return BankAdapter;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async function defaultFetch(_opts) {
|
|
391
|
+
throw new Error("bank-base: no fetchFn configured for cookie-api mode");
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
module.exports = {
|
|
395
|
+
createBankAdapter,
|
|
396
|
+
mapTransaction,
|
|
397
|
+
mapCard,
|
|
398
|
+
extractList,
|
|
399
|
+
normDirection,
|
|
400
|
+
toAmount,
|
|
401
|
+
parseTime,
|
|
402
|
+
billMonthToMs,
|
|
403
|
+
SNAPSHOT_SCHEMA_VERSION,
|
|
404
|
+
VALID_SNAPSHOT_KINDS,
|
|
405
|
+
};
|