@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,196 @@
|
|
|
1
|
+
// OFX 1.x SGML payload for the keyless adapter — the "Download → Quicken"
|
|
2
|
+
// file a bank actually produces: KEY:VALUE header block, tags whose leaves are
|
|
3
|
+
// NEVER closed, and all four message sets the adapter reads:
|
|
4
|
+
// BANKMSGSRSV1 — checking + savings ledger balances
|
|
5
|
+
// CREDITCARDMSGSRSV1 — a card whose BALAMT is NEGATIVE (the OFX convention
|
|
6
|
+
// for amount owed; the adapter normalizes to positive)
|
|
7
|
+
// INVSTMTMSGSRSV1 — one brokerage statement: stock + mutual-fund
|
|
8
|
+
// positions (SECID → SECLIST lookup), available cash,
|
|
9
|
+
// and an INVTRANLIST with monthly INVBANKTRAN deposits
|
|
10
|
+
// (contribution inference) plus an INCOME dividend
|
|
11
|
+
// record that must be EXCLUDED as growth
|
|
12
|
+
// SECLISTMSGSRSV1 — ticker/name lookup for the positions
|
|
13
|
+
// OFX carries no tax-treatment info, so the investment account must come out
|
|
14
|
+
// taxable at LOW confidence with a CLASSIFICATION_GUESSED warning.
|
|
15
|
+
|
|
16
|
+
const deposits = ['20260115', '20260215', '20260315', '20260415', '20260515', '20260615']
|
|
17
|
+
.map((d, i) => `
|
|
18
|
+
<INVBANKTRAN>
|
|
19
|
+
<STMTTRN>
|
|
20
|
+
<TRNTYPE>CREDIT
|
|
21
|
+
<DTPOSTED>${d}120000.000[-5:EST]
|
|
22
|
+
<TRNAMT>1500.00
|
|
23
|
+
<FITID>DEP${i + 1}
|
|
24
|
+
<NAME>CONTRIBUTION ACH TRANSFER IN
|
|
25
|
+
</STMTTRN>
|
|
26
|
+
<SUBACCTFUND>CASH
|
|
27
|
+
</INVBANKTRAN>`).join('');
|
|
28
|
+
|
|
29
|
+
export const ofxRaw = {
|
|
30
|
+
asOf: '2026-07-02T00:00:00.000Z',
|
|
31
|
+
owner: {
|
|
32
|
+
desiredAnnualSpend: 72000,
|
|
33
|
+
filingState: 'TX',
|
|
34
|
+
earners: [{ name: 'Sam', age: 45, retirementAge: 63, annualSalary: 140000 }],
|
|
35
|
+
},
|
|
36
|
+
content: `OFXHEADER:100
|
|
37
|
+
DATA:OFXSGML
|
|
38
|
+
VERSION:102
|
|
39
|
+
SECURITY:NONE
|
|
40
|
+
ENCODING:USASCII
|
|
41
|
+
CHARSET:1252
|
|
42
|
+
COMPRESSION:NONE
|
|
43
|
+
OLDFILEUID:NONE
|
|
44
|
+
NEWFILEUID:NONE
|
|
45
|
+
|
|
46
|
+
<OFX>
|
|
47
|
+
<SIGNONMSGSRSV1>
|
|
48
|
+
<SONRS>
|
|
49
|
+
<STATUS>
|
|
50
|
+
<CODE>0
|
|
51
|
+
<SEVERITY>INFO
|
|
52
|
+
</STATUS>
|
|
53
|
+
<DTSERVER>20260702093000.000[-5:EST]
|
|
54
|
+
<LANGUAGE>ENG
|
|
55
|
+
</SONRS>
|
|
56
|
+
</SIGNONMSGSRSV1>
|
|
57
|
+
<BANKMSGSRSV1>
|
|
58
|
+
<STMTTRNRS>
|
|
59
|
+
<TRNUID>1001
|
|
60
|
+
<STMTRS>
|
|
61
|
+
<CURDEF>USD
|
|
62
|
+
<BANKACCTFROM>
|
|
63
|
+
<BANKID>021000021
|
|
64
|
+
<ACCTID>9917341234
|
|
65
|
+
<ACCTTYPE>CHECKING
|
|
66
|
+
</BANKACCTFROM>
|
|
67
|
+
<LEDGERBAL>
|
|
68
|
+
<BALAMT>11250.75
|
|
69
|
+
<DTASOF>20260702
|
|
70
|
+
</LEDGERBAL>
|
|
71
|
+
</STMTRS>
|
|
72
|
+
</STMTTRNRS>
|
|
73
|
+
<STMTTRNRS>
|
|
74
|
+
<TRNUID>1002
|
|
75
|
+
<STMTRS>
|
|
76
|
+
<CURDEF>USD
|
|
77
|
+
<BANKACCTFROM>
|
|
78
|
+
<BANKID>021000021
|
|
79
|
+
<ACCTID>9917349999
|
|
80
|
+
<ACCTTYPE>SAVINGS
|
|
81
|
+
</BANKACCTFROM>
|
|
82
|
+
<LEDGERBAL>
|
|
83
|
+
<BALAMT>27500.00
|
|
84
|
+
<DTASOF>20260702
|
|
85
|
+
</LEDGERBAL>
|
|
86
|
+
</STMTRS>
|
|
87
|
+
</STMTTRNRS>
|
|
88
|
+
</BANKMSGSRSV1>
|
|
89
|
+
<CREDITCARDMSGSRSV1>
|
|
90
|
+
<CCSTMTTRNRS>
|
|
91
|
+
<TRNUID>2001
|
|
92
|
+
<CCSTMTRS>
|
|
93
|
+
<CURDEF>USD
|
|
94
|
+
<CCACCTFROM>
|
|
95
|
+
<ACCTID>5412000012348888
|
|
96
|
+
</CCACCTFROM>
|
|
97
|
+
<LEDGERBAL>
|
|
98
|
+
<BALAMT>-2350.60
|
|
99
|
+
<DTASOF>20260702
|
|
100
|
+
</LEDGERBAL>
|
|
101
|
+
</CCSTMTRS>
|
|
102
|
+
</CCSTMTTRNRS>
|
|
103
|
+
</CREDITCARDMSGSRSV1>
|
|
104
|
+
<INVSTMTMSGSRSV1>
|
|
105
|
+
<INVSTMTTRNRS>
|
|
106
|
+
<TRNUID>3001
|
|
107
|
+
<INVSTMTRS>
|
|
108
|
+
<DTASOF>20260702120000.000[-5:EST]
|
|
109
|
+
<CURDEF>USD
|
|
110
|
+
<INVACCTFROM>
|
|
111
|
+
<BROKERID>fidelity.com
|
|
112
|
+
<ACCTID>X22334455
|
|
113
|
+
</INVACCTFROM>
|
|
114
|
+
<INVTRANLIST>
|
|
115
|
+
<DTSTART>20260101
|
|
116
|
+
<DTEND>20260702${deposits}
|
|
117
|
+
<INCOME>
|
|
118
|
+
<INVTRAN>
|
|
119
|
+
<FITID>DIV1
|
|
120
|
+
<DTTRADE>20260320
|
|
121
|
+
</INVTRAN>
|
|
122
|
+
<SECID>
|
|
123
|
+
<UNIQUEID>922908769
|
|
124
|
+
<UNIQUEIDTYPE>CUSIP
|
|
125
|
+
</SECID>
|
|
126
|
+
<INCOMETYPE>DIV
|
|
127
|
+
<TOTAL>640.00
|
|
128
|
+
<SUBACCTSEC>CASH
|
|
129
|
+
<SUBACCTFUND>CASH
|
|
130
|
+
</INCOME>
|
|
131
|
+
</INVTRANLIST>
|
|
132
|
+
<INVPOSLIST>
|
|
133
|
+
<POSSTOCK>
|
|
134
|
+
<INVPOS>
|
|
135
|
+
<SECID>
|
|
136
|
+
<UNIQUEID>922908769
|
|
137
|
+
<UNIQUEIDTYPE>CUSIP
|
|
138
|
+
</SECID>
|
|
139
|
+
<HELDINACCT>CASH
|
|
140
|
+
<POSTYPE>LONG
|
|
141
|
+
<UNITS>310
|
|
142
|
+
<UNITPRICE>305.12
|
|
143
|
+
<MKTVAL>94587.20
|
|
144
|
+
<DTPRICEASOF>20260702
|
|
145
|
+
</INVPOS>
|
|
146
|
+
</POSSTOCK>
|
|
147
|
+
<POSMF>
|
|
148
|
+
<INVPOS>
|
|
149
|
+
<SECID>
|
|
150
|
+
<UNIQUEID>315911750
|
|
151
|
+
<UNIQUEIDTYPE>CUSIP
|
|
152
|
+
</SECID>
|
|
153
|
+
<HELDINACCT>CASH
|
|
154
|
+
<POSTYPE>LONG
|
|
155
|
+
<UNITS>620
|
|
156
|
+
<UNITPRICE>211.76
|
|
157
|
+
<MKTVAL>131291.20
|
|
158
|
+
<DTPRICEASOF>20260702
|
|
159
|
+
</INVPOS>
|
|
160
|
+
</POSMF>
|
|
161
|
+
</INVPOSLIST>
|
|
162
|
+
<INVBAL>
|
|
163
|
+
<AVAILCASH>4200.55
|
|
164
|
+
<MARGINBALANCE>0
|
|
165
|
+
<SHORTBALANCE>0
|
|
166
|
+
</INVBAL>
|
|
167
|
+
</INVSTMTRS>
|
|
168
|
+
</INVSTMTTRNRS>
|
|
169
|
+
</INVSTMTMSGSRSV1>
|
|
170
|
+
<SECLISTMSGSRSV1>
|
|
171
|
+
<SECLIST>
|
|
172
|
+
<STOCKINFO>
|
|
173
|
+
<SECINFO>
|
|
174
|
+
<SECID>
|
|
175
|
+
<UNIQUEID>922908769
|
|
176
|
+
<UNIQUEIDTYPE>CUSIP
|
|
177
|
+
</SECID>
|
|
178
|
+
<SECNAME>VANGUARD TOTAL STOCK MARKET ETF
|
|
179
|
+
<TICKER>VTI
|
|
180
|
+
</SECINFO>
|
|
181
|
+
</STOCKINFO>
|
|
182
|
+
<MFINFO>
|
|
183
|
+
<SECINFO>
|
|
184
|
+
<SECID>
|
|
185
|
+
<UNIQUEID>315911750
|
|
186
|
+
<UNIQUEIDTYPE>CUSIP
|
|
187
|
+
</SECID>
|
|
188
|
+
<SECNAME>FIDELITY 500 INDEX FUND
|
|
189
|
+
<TICKER>FXAIX
|
|
190
|
+
</SECINFO>
|
|
191
|
+
</MFINFO>
|
|
192
|
+
</SECLIST>
|
|
193
|
+
</SECLISTMSGSRSV1>
|
|
194
|
+
</OFX>
|
|
195
|
+
`,
|
|
196
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Plaid-sandbox-shaped payload (merged /accounts + /investments/holdings +
|
|
2
|
+
// /liabilities), trimmed to the fields the adapter reads. Mirrors real Plaid
|
|
3
|
+
// response shapes. Intentionally includes a holding with NO cost basis and a
|
|
4
|
+
// low-confidence subtype to exercise the warning paths.
|
|
5
|
+
|
|
6
|
+
export const plaidRaw = {
|
|
7
|
+
asOf: '2026-07-02T00:00:00.000Z',
|
|
8
|
+
// Two earners → joint household. Accounts carry owner_index (0/1).
|
|
9
|
+
owner: {
|
|
10
|
+
desiredAnnualSpend: 90000, filingState: 'CA',
|
|
11
|
+
earners: [
|
|
12
|
+
{ name: 'Alex', age: 41, retirementAge: 62, annualSalary: 185000 },
|
|
13
|
+
{ name: 'Sam', age: 39, retirementAge: 62, annualSalary: 120000 },
|
|
14
|
+
],
|
|
15
|
+
},
|
|
16
|
+
accounts: [
|
|
17
|
+
{ account_id: 'chk1', name: 'Checking', type: 'depository', subtype: 'checking', balances: { current: 18400, iso_currency_code: 'USD' } },
|
|
18
|
+
{ account_id: 'sav1', name: 'Savings', type: 'depository', subtype: 'savings', balances: { current: 52000 } },
|
|
19
|
+
{ account_id: 'brk1', name: 'Brokerage', type: 'investment', subtype: 'brokerage', balances: { current: 240000 }, owner_index: 0 },
|
|
20
|
+
{ account_id: 'k401', name: 'Fidelity 401(k)', type: 'investment', subtype: '401k', balances: { current: 315000 }, owner_index: 0 },
|
|
21
|
+
{ account_id: 'roth1', name: 'Roth IRA', type: 'investment', subtype: 'roth', balances: { current: 88000 }, owner_index: 1 },
|
|
22
|
+
{ account_id: 'hsa1', name: 'HSA', type: 'investment', subtype: 'hsa', balances: { current: 22000 } },
|
|
23
|
+
{ account_id: 'edu1', name: "Kid's 529", type: 'investment', subtype: '529', balances: { current: 41000 } },
|
|
24
|
+
{ account_id: 'mtg1', name: 'Home mortgage', type: 'loan', subtype: 'mortgage', balances: { current: 512000 } },
|
|
25
|
+
{ account_id: 'std1', name: 'Student loan', type: 'loan', subtype: 'student', balances: { current: 28000 } },
|
|
26
|
+
{ account_id: 'cc1', name: 'Sapphire card', type: 'credit', subtype: 'credit card', balances: { current: 4200 } },
|
|
27
|
+
{ account_id: 'weird1', name: 'Mystery acct', type: 'investment', subtype: 'annuity', balances: { current: 15000 } },
|
|
28
|
+
],
|
|
29
|
+
securities: [
|
|
30
|
+
{ security_id: 's_vti', ticker_symbol: 'VTI', name: 'Vanguard Total Market ETF', type: 'etf' },
|
|
31
|
+
{ security_id: 's_aapl', ticker_symbol: 'AAPL', name: 'Apple Inc', type: 'equity' },
|
|
32
|
+
{ security_id: 's_btc', ticker_symbol: 'BTC', name: 'Bitcoin', type: 'cryptocurrency' },
|
|
33
|
+
{ security_id: 's_tgt', ticker_symbol: null, name: 'Target Retirement 2045', type: 'mutual fund' },
|
|
34
|
+
],
|
|
35
|
+
holdings: [
|
|
36
|
+
{ account_id: 'brk1', security_id: 's_vti', quantity: 800, institution_value: 200000, cost_basis: 150000 },
|
|
37
|
+
{ account_id: 'brk1', security_id: 's_btc', quantity: 0.5, institution_value: 40000, cost_basis: null },
|
|
38
|
+
{ account_id: 'k401', security_id: 's_tgt', quantity: 3200, institution_value: 315000, cost_basis: 260000 },
|
|
39
|
+
{ account_id: 'roth1', security_id: 's_aapl', quantity: 400, institution_value: 88000, cost_basis: 41000 },
|
|
40
|
+
],
|
|
41
|
+
liabilities: {
|
|
42
|
+
mortgage: [{ account_id: 'mtg1', interest_rate: { percentage: 6.25 }, next_monthly_payment: 3150, origination_principal_amount: 560000, maturity_date: '2052-06-01', property_address: { city: 'Palo Alto', street: '1 Main St' } }],
|
|
43
|
+
student: [{ account_id: 'std1', interest_rate_percentage: 5.5, minimum_payment_amount: 310 }],
|
|
44
|
+
credit: [{ account_id: 'cc1', aprs: [{ apr_percentage: 21.9 }], minimum_payment_amount: 95 }],
|
|
45
|
+
},
|
|
46
|
+
// /investments/transactions — monthly contributions Jan–Jun 2026 (drives the
|
|
47
|
+
// inferred savings rate). brk1 $2k/mo, k401 $1.5k/mo (Alex), roth1 $500/mo (Sam).
|
|
48
|
+
investmentTransactions: [
|
|
49
|
+
...monthly('brk1', 2000), ...monthly('k401', 1500), ...monthly('roth1', 500),
|
|
50
|
+
],
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
function monthly(account_id, amount) {
|
|
54
|
+
return ['2026-01-15', '2026-02-15', '2026-03-15', '2026-04-15', '2026-05-15', '2026-06-15']
|
|
55
|
+
.map((date) => ({ account_id, type: 'cash', subtype: 'contribution', amount: -amount, date }));
|
|
56
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@plan-fi/imports",
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "Import customer financial data from aggregators (Plaid, MX, Finicity, FDX) or CSV/OFX files into Planfi plans via a canonical model.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"types": "./planfi-import.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"planfi-import": "./bin/planfi-import.mjs"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./planfi-import.d.ts",
|
|
14
|
+
"default": "./src/index.mjs"
|
|
15
|
+
},
|
|
16
|
+
"./canonical": "./src/canonical.ts",
|
|
17
|
+
"./fixtures/*": "./fixtures/*"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"src",
|
|
21
|
+
"bin",
|
|
22
|
+
"fixtures",
|
|
23
|
+
"docs",
|
|
24
|
+
"planfi-import.d.ts",
|
|
25
|
+
"README.md",
|
|
26
|
+
"CHANGELOG.md",
|
|
27
|
+
"AGENTS.md",
|
|
28
|
+
"LICENSE"
|
|
29
|
+
],
|
|
30
|
+
"scripts": {
|
|
31
|
+
"test": "node --test",
|
|
32
|
+
"demo": "node -e \"import('./src/index.mjs').then(async({importToPlan})=>{const{plaidRaw}=await import('./fixtures/plaid-sandbox.mjs');console.log(JSON.stringify(importToPlan('plaid',plaidRaw),null,2))})\""
|
|
33
|
+
},
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=18"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"tsx": "^4.19.0",
|
|
39
|
+
"zod": "^4.3.5"
|
|
40
|
+
},
|
|
41
|
+
"keywords": [
|
|
42
|
+
"planfi",
|
|
43
|
+
"plaid",
|
|
44
|
+
"mx",
|
|
45
|
+
"finicity",
|
|
46
|
+
"fdx",
|
|
47
|
+
"financial-data-exchange",
|
|
48
|
+
"open-banking",
|
|
49
|
+
"mastercard-open-banking",
|
|
50
|
+
"akoya",
|
|
51
|
+
"csv",
|
|
52
|
+
"ofx",
|
|
53
|
+
"aggregator",
|
|
54
|
+
"financial-data",
|
|
55
|
+
"import",
|
|
56
|
+
"adapter"
|
|
57
|
+
],
|
|
58
|
+
"repository": {
|
|
59
|
+
"type": "git",
|
|
60
|
+
"url": "git+https://github.com/holdequity/planfi-import.git"
|
|
61
|
+
},
|
|
62
|
+
"publishConfig": {
|
|
63
|
+
"access": "public"
|
|
64
|
+
},
|
|
65
|
+
"homepage": "https://github.com/holdequity/planfi-import#readme",
|
|
66
|
+
"bugs": {
|
|
67
|
+
"url": "https://github.com/holdequity/planfi-import/issues"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* planfi-import.d.ts — hand-written type declarations for the package entry
|
|
3
|
+
* point (src/index.mjs). Self-contained on purpose: the runtime is zero-dep
|
|
4
|
+
* ESM JavaScript, and consumers should get full types without TypeScript
|
|
5
|
+
* having to compile the shipped `src/canonical.ts` (which remains the
|
|
6
|
+
* annotated source of truth these declarations mirror — keep them in sync).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// ── Canonical Financial Profile (mirrors src/canonical.ts) ──────────────────
|
|
10
|
+
|
|
11
|
+
/** Broad account family, provider-independent. */
|
|
12
|
+
export type AccountClass = 'depository' | 'investment' | 'loan' | 'credit' | 'property';
|
|
13
|
+
|
|
14
|
+
/** Tax treatment of an investment/holding bucket. `na` = not applicable (debt, cash). */
|
|
15
|
+
export type TaxTreatment = 'taxable' | 'traditional' | 'roth' | 'hsa' | '529' | 'na';
|
|
16
|
+
|
|
17
|
+
export type AssetType = 'equity' | 'etf' | 'mutual_fund' | 'bond' | 'cash' | 'crypto' | 'other';
|
|
18
|
+
|
|
19
|
+
/** One security position inside an investment account. */
|
|
20
|
+
export interface CanonicalHolding {
|
|
21
|
+
ticker?: string;
|
|
22
|
+
name?: string;
|
|
23
|
+
quantity?: number;
|
|
24
|
+
/** Market value of the position at `asOf`. */
|
|
25
|
+
value?: number;
|
|
26
|
+
/** Total cost basis, when the institution reported it. */
|
|
27
|
+
costBasis?: number;
|
|
28
|
+
assetType: AssetType;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Loan/credit detail attached to a `loan` or `credit` account. */
|
|
32
|
+
export interface LiabilityDetail {
|
|
33
|
+
/** APR as a fraction, e.g. 0.0625 for 6.25%. */
|
|
34
|
+
rate?: number;
|
|
35
|
+
minPayment?: number;
|
|
36
|
+
monthsRemaining?: number;
|
|
37
|
+
originationPrincipal?: number;
|
|
38
|
+
/** The asset securing the debt (property/vehicle), when known. */
|
|
39
|
+
assetName?: string;
|
|
40
|
+
assetValue?: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface CanonicalAccount {
|
|
44
|
+
/** Stable provider account id — the key for dedup/reconcile across refreshes. */
|
|
45
|
+
id: string;
|
|
46
|
+
institution?: string;
|
|
47
|
+
name?: string;
|
|
48
|
+
class: AccountClass;
|
|
49
|
+
/** Provider subtype normalized to lowercase, e.g. '401k', 'roth ira', 'mortgage'. */
|
|
50
|
+
subtype?: string;
|
|
51
|
+
taxTreatment?: TaxTreatment;
|
|
52
|
+
/** Asset value, or outstanding principal for a liability. */
|
|
53
|
+
balance: number;
|
|
54
|
+
currency?: string;
|
|
55
|
+
holdings?: CanonicalHolding[];
|
|
56
|
+
liability?: LiabilityDetail;
|
|
57
|
+
/** Which earner (0-based) owns this account, for joint households. */
|
|
58
|
+
ownerIndex?: number;
|
|
59
|
+
/** Inferred monthly contribution into this account (from transactions). */
|
|
60
|
+
estMonthlyContribution?: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Planning context aggregators usually CAN'T supply (age, goals, salary). */
|
|
64
|
+
export interface OwnerContext {
|
|
65
|
+
age?: number;
|
|
66
|
+
retirementAge?: number;
|
|
67
|
+
annualSalary?: number;
|
|
68
|
+
desiredAnnualSpend?: number;
|
|
69
|
+
/** Two-letter US state for tax settings. */
|
|
70
|
+
filingState?: string;
|
|
71
|
+
/** Per-earner overrides for joint households. */
|
|
72
|
+
earners?: Array<Partial<OwnerContext> & { name?: string }>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── Structured results (v0.2.0) ─────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Stable machine-readable warning codes. Append-only: a released code never
|
|
79
|
+
* changes meaning. Message text may improve between versions; codes will not.
|
|
80
|
+
*/
|
|
81
|
+
export type WarningCode =
|
|
82
|
+
| 'CLASSIFICATION_GUESSED'
|
|
83
|
+
| 'NO_COST_BASIS'
|
|
84
|
+
| 'COARSE_INFERENCE'
|
|
85
|
+
| 'CONTRIBUTION_CLAMPED'
|
|
86
|
+
| 'CONTRIBUTION_IMPLAUSIBLE'
|
|
87
|
+
| 'HSA_FOLDED_INTO_PORTFOLIO'
|
|
88
|
+
| 'HSA_COVERAGE_ASSUMED'
|
|
89
|
+
| 'IRA_SPLIT_ASSUMED'
|
|
90
|
+
| 'HOME_VALUE_ESTIMATED'
|
|
91
|
+
| 'MORTGAGE_SKIPPED'
|
|
92
|
+
| 'NEGATIVE_BALANCE_CLAMPED'
|
|
93
|
+
| 'DEBT_RATE_MISSING'
|
|
94
|
+
| 'CSV_UNMAPPED_COLUMNS'
|
|
95
|
+
| 'CSV_TRANSACTIONS_ONLY'
|
|
96
|
+
| 'IMPORT_EMPTY';
|
|
97
|
+
|
|
98
|
+
export interface ImportWarning {
|
|
99
|
+
code: WarningCode;
|
|
100
|
+
/** 'info' = lossless modeling note; 'warn' = a value may be wrong — verify. */
|
|
101
|
+
severity: 'info' | 'warn';
|
|
102
|
+
/** Human-readable explanation, safe to show to an end user. */
|
|
103
|
+
message: string;
|
|
104
|
+
/** Provider account id the warning refers to, when account-scoped. */
|
|
105
|
+
accountId?: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Fields an aggregator cannot supply — collect them from the user. */
|
|
109
|
+
export type NeedsInputField =
|
|
110
|
+
| 'age'
|
|
111
|
+
| 'retirement_age'
|
|
112
|
+
| 'annual_salary'
|
|
113
|
+
| 'desired_annual_spend'
|
|
114
|
+
| 'home_value'
|
|
115
|
+
| 'debt_rate';
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* One structured ask. De-duplicated on (field, accountId, earnerIndex) and
|
|
119
|
+
* emitted in deterministic order.
|
|
120
|
+
*/
|
|
121
|
+
export interface NeedsInput {
|
|
122
|
+
field: NeedsInputField;
|
|
123
|
+
/** Provider account id, for account-scoped asks (home_value, debt_rate). */
|
|
124
|
+
accountId?: string;
|
|
125
|
+
accountName?: string;
|
|
126
|
+
/** 0-based earner index, for demographic asks in multi-earner households. */
|
|
127
|
+
earnerIndex?: number;
|
|
128
|
+
/** Short human label, ready for a form. */
|
|
129
|
+
label: string;
|
|
130
|
+
/** One sentence: why the import couldn't supply this. */
|
|
131
|
+
why: string;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface CanonicalFinancialProfile {
|
|
135
|
+
/** Adapter source id: 'plaid' | 'mx' | 'finicity' | 'fdx' | ... */
|
|
136
|
+
source: string;
|
|
137
|
+
/** ISO timestamp of the underlying snapshot. */
|
|
138
|
+
asOf: string;
|
|
139
|
+
owner: OwnerContext;
|
|
140
|
+
accounts: CanonicalAccount[];
|
|
141
|
+
meta: {
|
|
142
|
+
/** Structured notes: guessed classifications, dropped/partial data. */
|
|
143
|
+
warnings: ImportWarning[];
|
|
144
|
+
/** Raw provider entities that couldn't be mapped — never silently dropped. */
|
|
145
|
+
unmapped: unknown[];
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** The contract implemented once per provider. */
|
|
150
|
+
export interface SourceAdapter<Raw = unknown> {
|
|
151
|
+
readonly source: string;
|
|
152
|
+
normalize(raw: Raw): CanonicalFinancialProfile;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ── Emitted plan (generate_financial_plan wire body) ────────────────────────
|
|
156
|
+
// The body is validated server-side by the engine's Zod schema; typing it
|
|
157
|
+
// loosely here keeps this package decoupled from engine releases.
|
|
158
|
+
export interface PlanfiPlan {
|
|
159
|
+
name: string;
|
|
160
|
+
earners: Array<{
|
|
161
|
+
name: string;
|
|
162
|
+
age?: number;
|
|
163
|
+
retirement_age?: number;
|
|
164
|
+
annual_salary?: number;
|
|
165
|
+
retirement_accounts?: {
|
|
166
|
+
k401?: { employee_annual: number };
|
|
167
|
+
ira?: { type: 'traditional' | 'roth' | 'both'; annual: number };
|
|
168
|
+
hsa?: { coverage: 'self' | 'family'; annual: number };
|
|
169
|
+
};
|
|
170
|
+
}>;
|
|
171
|
+
stocks: { current_value: number; monthly_contribution: number; annual_return: number };
|
|
172
|
+
cash: { current_value: number; monthly_contribution: number; annual_return: number };
|
|
173
|
+
account_balances: { taxable: number; traditional: number; roth: number };
|
|
174
|
+
real_estate?: Array<{
|
|
175
|
+
name: string;
|
|
176
|
+
current_value: number;
|
|
177
|
+
annual_appreciation: number;
|
|
178
|
+
mortgage?: { balance: number; rate: number; years_remaining: number };
|
|
179
|
+
}>;
|
|
180
|
+
debts?: Array<{ name: string; balance: number; rate: number; min_payment: number; asset_name?: string; asset_value?: number }>;
|
|
181
|
+
speculative?: Array<{ name: string; current_value: number; annual_growth_rate: number }>;
|
|
182
|
+
education_account?: { enabled: boolean; initialBalance: number; monthlyContribution: number };
|
|
183
|
+
tax_settings: { state: string };
|
|
184
|
+
desired_annual_spend?: number;
|
|
185
|
+
[key: string]: unknown;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export interface ImportResult {
|
|
189
|
+
/** POST this to /v1/tools/generate_financial_plan. */
|
|
190
|
+
plan: PlanfiPlan;
|
|
191
|
+
warnings: ImportWarning[];
|
|
192
|
+
needsInput: NeedsInput[];
|
|
193
|
+
/** The full canonical profile (ticker/shares/cost-basis preserved). */
|
|
194
|
+
cfp: CanonicalFinancialProfile;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export interface ToPlanfiOptions {
|
|
198
|
+
/** Two-letter US state used when the owner context has none. Default 'CA'. */
|
|
199
|
+
defaultState?: string;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ── Functions + adapters (mirrors src/index.mjs exports) ────────────────────
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* One-call import: raw provider payload → { plan, warnings, needsInput, cfp }.
|
|
206
|
+
* @throws if `source` is not a registered adapter id.
|
|
207
|
+
*/
|
|
208
|
+
export function importToPlan(
|
|
209
|
+
source: 'plaid' | 'mx' | 'finicity' | 'fdx' | 'csv' | 'ofx' | (string & {}),
|
|
210
|
+
raw: object,
|
|
211
|
+
opts?: ToPlanfiOptions,
|
|
212
|
+
): ImportResult;
|
|
213
|
+
|
|
214
|
+
/** The shared mapper: Canonical Financial Profile → wire body + diagnostics. */
|
|
215
|
+
export function toPlanfiPlan(
|
|
216
|
+
cfp: CanonicalFinancialProfile,
|
|
217
|
+
opts?: ToPlanfiOptions,
|
|
218
|
+
): { plan: PlanfiPlan; warnings: ImportWarning[]; needsInput: NeedsInput[] };
|
|
219
|
+
|
|
220
|
+
/** Map a provider (type, subtype) to the canonical class + tax treatment. */
|
|
221
|
+
export function classify(
|
|
222
|
+
type: string,
|
|
223
|
+
subtype?: string,
|
|
224
|
+
): { accountClass: AccountClass; taxTreatment: TaxTreatment; confidence: 'high' | 'medium' | 'low' };
|
|
225
|
+
|
|
226
|
+
/** Map a provider security type ('etf', 'Mutual Fund', …) → canonical AssetType. */
|
|
227
|
+
export function classifyAsset(securityType?: string): AssetType;
|
|
228
|
+
|
|
229
|
+
/** Infer a monthly contribution rate from investment transactions. */
|
|
230
|
+
export function inferMonthlyContribution(
|
|
231
|
+
txns: Array<{ account_id?: string; type?: string; subtype?: string; amount?: number; date?: string }>,
|
|
232
|
+
opts?: { windowMonths?: number },
|
|
233
|
+
): number;
|
|
234
|
+
|
|
235
|
+
/** Group transactions by account_id → inferred monthly contribution each. */
|
|
236
|
+
export function contributionsByAccount(
|
|
237
|
+
txns: Array<{ account_id?: string; type?: string; subtype?: string; amount?: number; date?: string }>,
|
|
238
|
+
opts?: { windowMonths?: number },
|
|
239
|
+
): Record<string, number>;
|
|
240
|
+
|
|
241
|
+
export declare const plaidAdapter: SourceAdapter<object>;
|
|
242
|
+
export declare const mxAdapter: SourceAdapter<object>;
|
|
243
|
+
export declare const finicityAdapter: SourceAdapter<object>;
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* FDX (Financial Data Exchange — the US open-banking standard; Akoya speaks
|
|
247
|
+
* it natively) → CFP. Raw shape: { accounts, holdings?, transactions?, owner?,
|
|
248
|
+
* asOf? } where accounts are FDX Account entities, wrapped
|
|
249
|
+
* ({ depositAccount: {…} } / { investmentAccount: {…} } / { loanAccount: {…} }
|
|
250
|
+
* / { locAccount: {…} }) or already flattened.
|
|
251
|
+
*/
|
|
252
|
+
export declare const fdxAdapter: SourceAdapter<object>;
|
|
253
|
+
|
|
254
|
+
/** One CSV file handed to the csv adapter (the keyless path). */
|
|
255
|
+
export interface CsvFile {
|
|
256
|
+
/** Used in warnings and as the fallback account name (Schwab positions). */
|
|
257
|
+
name?: string;
|
|
258
|
+
/** Force the mapping; omitted → the header fingerprint decides. */
|
|
259
|
+
kind?: 'accounts' | 'holdings' | 'transactions';
|
|
260
|
+
content: string;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** CSV exports → CFP. Raw shape: { files: CsvFile[], owner?, asOf? }. */
|
|
264
|
+
export declare const csvAdapter: SourceAdapter<{ files: CsvFile[]; owner?: OwnerContext; asOf?: string }>;
|
|
265
|
+
|
|
266
|
+
/** OFX 1.x (SGML) / 2.x (XML) → CFP. Raw shape: { content: string, owner?, asOf? }. */
|
|
267
|
+
export declare const ofxAdapter: SourceAdapter<{ content: string; owner?: OwnerContext; asOf?: string }>;
|
|
268
|
+
|
|
269
|
+
/** Registry of source adapters by id. */
|
|
270
|
+
export declare const ADAPTERS: Record<string, SourceAdapter<object>>;
|