@plan-fi/imports 0.6.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/AGENTS.md +54 -0
- package/CHANGELOG.md +215 -0
- package/LICENSE +21 -0
- package/README.md +314 -0
- package/bin/planfi-import.mjs +549 -0
- package/docs/ADAPTER_GUIDE.md +244 -0
- package/fixtures/csv-sandbox.mjs +112 -0
- package/fixtures/fdx-sandbox.mjs +67 -0
- package/fixtures/finicity-sandbox.mjs +62 -0
- package/fixtures/mx-sandbox.mjs +37 -0
- package/fixtures/ofx-sandbox.mjs +196 -0
- package/fixtures/plaid-sandbox.mjs +56 -0
- package/package.json +69 -0
- package/planfi-import.d.ts +270 -0
- package/src/adapters/_template.mjs +112 -0
- package/src/adapters/csv.mjs +763 -0
- package/src/adapters/fdx.mjs +303 -0
- package/src/adapters/finicity.mjs +243 -0
- package/src/adapters/mx.mjs +159 -0
- package/src/adapters/ofx.mjs +324 -0
- package/src/adapters/plaid.mjs +140 -0
- package/src/canonical.ts +185 -0
- package/src/classify.mjs +72 -0
- package/src/contributions.mjs +65 -0
- package/src/index.mjs +42 -0
- package/src/to-planfi.mjs +340 -0
- package/src/util.mjs +65 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// _template.mjs — COPY-ME adapter skeleton. NOT REGISTERED in ADAPTERS on
|
|
2
|
+
// purpose (the leading underscore is the convention for "not a real source").
|
|
3
|
+
//
|
|
4
|
+
// How to use (the full step-by-step recipe lives in docs/ADAPTER_GUIDE.md):
|
|
5
|
+
// 1. Copy this file to src/adapters/<source>.mjs and rename `templateAdapter`
|
|
6
|
+
// → `<source>Adapter`, `source: '_template'` → your adapter id.
|
|
7
|
+
// 2. Fill in the TODOs below — translate ONLY your provider's vocabulary
|
|
8
|
+
// into the Canonical Financial Profile. NO Planfi domain logic here;
|
|
9
|
+
// that all lives in to-planfi.mjs, shared by every adapter.
|
|
10
|
+
// 3. Register it (index.mjs exports + ADAPTERS, planfi-import.d.ts, the CLI
|
|
11
|
+
// source list in bin/planfi-import.mjs), add fixtures/<source>-sandbox.mjs
|
|
12
|
+
// exporting `<source>Raw`, register that in
|
|
13
|
+
// test/helpers/fixture-registry.mjs, and add a fuzz generator.
|
|
14
|
+
// 4. Run `node --test` — test/adapter-contract.test.mjs runs every
|
|
15
|
+
// registered adapter through the identical battery.
|
|
16
|
+
//
|
|
17
|
+
// AS SHIPPED this skeleton returns an empty-but-STRUCTURALLY-VALID CFP: it
|
|
18
|
+
// passes the structural validator (validateCFP) but would FAIL the contract
|
|
19
|
+
// harness's fixture-content floor (a registered adapter's fixture must
|
|
20
|
+
// produce accounts and exercise at least one warning path) — that failure is
|
|
21
|
+
// the to-do list. A guide-consistency test in adapter-contract.test.mjs
|
|
22
|
+
// asserts exactly this behavior, so the template can't silently rot.
|
|
23
|
+
//
|
|
24
|
+
// Invariants (AGENTS.md is the authoritative list):
|
|
25
|
+
// - NEVER fabricate values. Missing cost basis stays undefined + a
|
|
26
|
+
// NO_COST_BASIS info warning; a guessed classification is a
|
|
27
|
+
// CLASSIFICATION_GUESSED warning; a value only the user can know becomes
|
|
28
|
+
// a needsInput ask (emitted by the shared mapper, not by you).
|
|
29
|
+
// - Warning codes come from the append-only catalog in src/canonical.ts
|
|
30
|
+
// (WarningCode). Build them with warning() from util.mjs — never ad-hoc.
|
|
31
|
+
// - Zero runtime dependencies. Only node built-ins and sibling modules.
|
|
32
|
+
//
|
|
33
|
+
// @typedef {import('../canonical').CanonicalFinancialProfile} CFP
|
|
34
|
+
// @typedef {import('../canonical').SourceAdapter} SourceAdapter
|
|
35
|
+
|
|
36
|
+
import { classify, classifyAsset } from '../classify.mjs';
|
|
37
|
+
import { contributionsByAccount } from '../contributions.mjs';
|
|
38
|
+
import { arr, num, pct, groupBy, monthsBetween, defaultAsOf, warning } from '../util.mjs';
|
|
39
|
+
|
|
40
|
+
// TODO: map YOUR provider's account-type vocabulary → the generic
|
|
41
|
+
// [type, subtype?] pair that classify() consumes. See the classification
|
|
42
|
+
// cheat sheet in docs/ADAPTER_GUIDE.md for the words classify() understands.
|
|
43
|
+
// Example:
|
|
44
|
+
// const MY_TYPE = {
|
|
45
|
+
// CHECKING: ['depository', 'checking'],
|
|
46
|
+
// '401K': ['investment', '401k'],
|
|
47
|
+
// MORTGAGE: ['loan', 'mortgage'],
|
|
48
|
+
// CREDITCARD: ['credit', 'credit card'],
|
|
49
|
+
// };
|
|
50
|
+
|
|
51
|
+
/** @implements {SourceAdapter} */
|
|
52
|
+
export const templateAdapter = {
|
|
53
|
+
source: '_template', // TODO: your adapter id (lowercase, matches the file name)
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Translate one raw provider payload → a Canonical Financial Profile.
|
|
57
|
+
* MUST be a total function: any input (null, junk, truncated data) returns
|
|
58
|
+
* a valid CFP — never throw. MUST be deterministic: same input, same output
|
|
59
|
+
* (the only allowed nondeterminism is defaultAsOf() when raw.asOf is absent).
|
|
60
|
+
*
|
|
61
|
+
* @param {object} raw - { accounts, holdings?, transactions?, owner, asOf }
|
|
62
|
+
* (document YOUR provider's exact shape here: which
|
|
63
|
+
* API endpoints the caller merges into each key)
|
|
64
|
+
* @returns {CFP}
|
|
65
|
+
*/
|
|
66
|
+
normalize(raw) {
|
|
67
|
+
// Hostile-input floor: null/undefined/primitive payloads normalize to an
|
|
68
|
+
// empty profile instead of throwing (property access on null throws!).
|
|
69
|
+
raw = raw && typeof raw === 'object' ? raw : {};
|
|
70
|
+
|
|
71
|
+
const warnings = []; // structured ImportWarning[] — use warning(code, sev, msg, accountId?)
|
|
72
|
+
const unmapped = []; // raw entities you could NOT map — push them here, never drop silently
|
|
73
|
+
|
|
74
|
+
// TODO 1: normalize transactions FIRST if your provider has them, and run
|
|
75
|
+
// contributionsByAccount() to infer per-account monthly savings:
|
|
76
|
+
// - only money flowing INTO investment accounts counts
|
|
77
|
+
// - dividends/interest/reinvest are GROWTH → exclude
|
|
78
|
+
// - an unlabeled credit is counted but flagged once: COARSE_INFERENCE
|
|
79
|
+
// (Copy the pattern from src/adapters/fdx.mjs or finicity.mjs.)
|
|
80
|
+
const contribByAccount = contributionsByAccount([]);
|
|
81
|
+
|
|
82
|
+
// TODO 2: map every provider account → a CanonicalAccount:
|
|
83
|
+
// const accounts = arr(raw.accounts).map((a) => { ... });
|
|
84
|
+
// - id: String(provider id) — stable, required
|
|
85
|
+
// - class/taxTreatment: classify(genType, subtype); confidence 'low'
|
|
86
|
+
// (or an unrecognized provider type) → push CLASSIFICATION_GUESSED
|
|
87
|
+
// - balance: finite number; for loan/credit take Math.abs(x) — liability
|
|
88
|
+
// balances are positive outstanding principal in the CFP
|
|
89
|
+
// - holdings (investment only): ticker/name/quantity/value/costBasis
|
|
90
|
+
// (costBasis missing → undefined + NO_COST_BASIS info warning) and
|
|
91
|
+
// assetType via classifyAsset()
|
|
92
|
+
// - liability (loan/credit only): rate as a FRACTION via pct(),
|
|
93
|
+
// minPayment, originationPrincipal, monthsRemaining via
|
|
94
|
+
// monthsBetween(raw.asOf, maturityDate), assetName for mortgages
|
|
95
|
+
// - ownerIndex: which earner owns it (0-based; default 0)
|
|
96
|
+
// - estMonthlyContribution: from contribByAccount[id] when present
|
|
97
|
+
const accounts = [];
|
|
98
|
+
void classify; void classifyAsset; void num; void pct; void groupBy;
|
|
99
|
+
void monthsBetween; void warning; void contribByAccount; // (drop these once the TODOs are filled in)
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
source: '_template', // keep in sync with this.source
|
|
103
|
+
// Prefer the caller's snapshot time; default is NOW (never the 1970 epoch).
|
|
104
|
+
asOf: raw.asOf || defaultAsOf(),
|
|
105
|
+
// Owner context (ages, goals, salary) passes through untouched — the
|
|
106
|
+
// shared mapper turns what's missing into structured needsInput asks.
|
|
107
|
+
owner: { ...(raw.owner ?? {}) },
|
|
108
|
+
accounts,
|
|
109
|
+
meta: { warnings, unmapped },
|
|
110
|
+
};
|
|
111
|
+
},
|
|
112
|
+
};
|