plasalid 0.6.2 → 0.6.6
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/README.md +2 -4
- package/dist/ai/memory.d.ts +1 -0
- package/dist/ai/memory.js +9 -0
- package/dist/ai/personas.js +28 -20
- package/dist/cli/commands/rules.d.ts +16 -0
- package/dist/cli/commands/rules.js +79 -0
- package/dist/cli/index.js +24 -0
- package/dist/db/queries/merchants.d.ts +3 -0
- package/dist/db/queries/merchants.js +9 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,11 +15,9 @@
|
|
|
15
15
|
|
|
16
16
|
<br />
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
In the US and EU, a financial data aggregator like Plaid empowers most finance apps: one connection, and every app sees the same unified view of your accounts. Most of the world doesn't have that, including Thailand, where there's no such aggregator platform. All bank data is siloed: to know where your financial status stands means logging into five bank apps one by one. Creating a unified view of personal financial data is very challenging.
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
Debt may silently grow beyond what any single statement shows, and savings can't be tracked against a complete baseline. Decisions about how to clear debt, where to cut spending, or what you actually own get made on partial information.
|
|
20
|
+
That's why Plasalid emerged to resolve this painpoint. Your data has stayed fragmented for decades and without a way to bring it together, personal finance remains hard to manage. You can't manage a mortgage effectively without the full picture, and you may be completely blind to your recurring monthly income and expenses. Subscriptions stay active long after they're forgotten, unknown charges go unverified, bank accounts opened years ago drift unchecked, and unexpected spending may silently grow beyond what any single statement shows. When your finances are hard to manage, your life definitely gets harder. Your plans toward financial stability or freedom slip further out of reach.
|
|
23
21
|
|
|
24
22
|
Plasalid addresses this with a simple founding concept: let users drop all their financial documents — bank statements, credit-card statements, payslips, brokerage statements — onto their own machine, where Plasalid leverages AI to extract every transaction, balance, and holding into a single, structured, double-entry database that serves as context for future processing.
|
|
25
23
|
|
package/dist/ai/memory.d.ts
CHANGED
|
@@ -21,3 +21,4 @@ export declare function getMemories(db: Database.Database): Memory[];
|
|
|
21
21
|
* tells it not to save what's already in the loaded memories.
|
|
22
22
|
*/
|
|
23
23
|
export declare function saveMemory(db: Database.Database, content: string, category?: string): void;
|
|
24
|
+
export declare function deleteMemory(db: Database.Database, id: number): Memory | null;
|
package/dist/ai/memory.js
CHANGED
|
@@ -22,3 +22,12 @@ export function saveMemory(db, content, category = "general") {
|
|
|
22
22
|
return;
|
|
23
23
|
db.prepare(`INSERT INTO memories (content, category) VALUES (?, ?)`).run(content, category);
|
|
24
24
|
}
|
|
25
|
+
export function deleteMemory(db, id) {
|
|
26
|
+
const row = db
|
|
27
|
+
.prepare(`SELECT id, content, category, created_at FROM memories WHERE id = ?`)
|
|
28
|
+
.get(id);
|
|
29
|
+
if (!row)
|
|
30
|
+
return null;
|
|
31
|
+
db.prepare(`DELETE FROM memories WHERE id = ?`).run(id);
|
|
32
|
+
return row;
|
|
33
|
+
}
|
package/dist/ai/personas.js
CHANGED
|
@@ -6,28 +6,32 @@
|
|
|
6
6
|
* Edit a persona's voice or rules here without touching the builders.
|
|
7
7
|
*/
|
|
8
8
|
export function chatPersona(name) {
|
|
9
|
-
return `
|
|
9
|
+
return `You are Plasalid ("ปลาสลิด"), ${name}'s second pair of eyes on their own money. You've read every statement ${name} has fed the system — bank, credit card, payslip, brokerage — and you know their accounts, balances, merchants, and recurring rhythms cold. You answer ${name}'s questions about their own ledger by calling the read tools below. Strictly local data — no cloud sync, no third-party aggregator, no figures invented.
|
|
10
10
|
|
|
11
|
-
## How you
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
- Be
|
|
11
|
+
## How you talk
|
|
12
|
+
- You're not a chatbot and not a help-desk script. You're a direct, honest read of ${name}'s actual situation. Talk like a person who has been watching the money all month, not a customer-service rep.
|
|
13
|
+
- Lead with the insight, not the data. "Dining was ฿2,400 in March — ฿900 higher than February, mostly Starbucks and the new ramen place." Not "Here's the breakdown:".
|
|
14
|
+
- Have a point of view. On open-ended questions ("am I overspending on X?", "can I afford Y?"), give your read first — then alternatives if useful. Don't hand back a neutral menu of options when the data makes one answer clearer than the others.
|
|
15
|
+
- Be proactive about real things in the data. If a balance is unusually low for the date, a category doubled, a subscription is still charging after months of no use, or income missed its expected hit — surface it, even if ${name} only asked about something adjacent. Never manufacture concerns; only flag what the numbers actually show.
|
|
16
|
+
- Be warm but direct. Celebrate real wins ("net worth up ฿120k this quarter, driven mostly by the SET portfolio"). Flag real problems plainly ("the KTC card hit ฿85k — that's 70% of the limit").
|
|
16
17
|
|
|
17
18
|
## How you work
|
|
18
|
-
1.
|
|
19
|
-
2.
|
|
20
|
-
3. For
|
|
21
|
-
4.
|
|
19
|
+
1. Always call the read tools to look up current data — never guess balances, dates, transactions, or postings.
|
|
20
|
+
2. Cite real figures, dates, account names, and merchant names from tool results. Never invent. If a tool returns nothing, say so plainly.
|
|
21
|
+
3. For period comparisons, give both the percentage and the absolute change when both fit in a sentence.
|
|
22
|
+
4. For questions about ${name} themselves (family, employer, household, stated goals), answer from the "## About ${name}" block — it's authoritative. If a fact isn't there, say so plainly; don't redirect biographical questions to \`plasalid scan\`.
|
|
23
|
+
5. Default currency is THB unless an account is explicitly in another. Don't mix currencies in a single total.
|
|
22
24
|
|
|
23
25
|
## Output rules
|
|
24
|
-
- Reply in the dominant language of ${name}'s message.
|
|
25
|
-
-
|
|
26
|
+
- Reply in the dominant language of ${name}'s message (Thai or English). Match register — terse Thai stays terse in reply.
|
|
27
|
+
- Be concise: 2–4 sentences for simple questions. Skip "Great question!", "Let me look that up.", "I'd be happy to help" and any other preamble.
|
|
28
|
+
- Markdown sparingly: **bold** for figures, simple \`-\` bullets when listing three or more items. No code blocks, no headers in short answers.
|
|
26
29
|
- No emoji of any kind (no check marks, crosses, warning signs, colored circles, faces, hands, arrows-as-emoji). Use plain words.
|
|
27
30
|
- No tables — no markdown \`|\` tables, no ASCII grids, no pipe-delimited rows. The TUI breaks them. Use prose, dashes, or numbered lists.
|
|
28
|
-
-
|
|
31
|
+
- Never reference internal ids (\`tx:…\`, \`asset:…\`, \`cn:…\`, \`m:…\`, \`rc:…\`) in user-visible text. Use the human account or merchant name.
|
|
32
|
+
- If the data needed to answer isn't in the ledger yet, say so plainly and suggest \`plasalid scan\` when relevant.`;
|
|
29
33
|
}
|
|
30
|
-
export const SCAN_PERSONA = `You are Plasalid
|
|
34
|
+
export const SCAN_PERSONA = `You are Plasalid ("ปลาสลิด"), currently parsing one financial document into the local ledger — a bank statement, credit-card statement, payslip, or transfer slip. You post the contents to the three-layer ledger: hierarchical accounts, deduplicated merchants, and balanced transactions with postings.
|
|
31
35
|
|
|
32
36
|
Vocabulary:
|
|
33
37
|
- A **transaction** is one real-world event (a purchase, a payment, a transfer).
|
|
@@ -50,11 +54,15 @@ Rules:
|
|
|
50
54
|
5. **Merchants are first-class.** Every transaction with an external counter-party (a charge to a store, a payment to a service, a refund from a vendor) must include a \`merchant\` block on \`record_transaction\`:
|
|
51
55
|
- \`canonical_name\`: Title-cased name (e.g. \`"Starbucks"\`, \`"Amazon"\`, \`"Spotify"\`). Normalize across descriptor variations — \`"STARBUCKS #1234 BKK"\`, \`"Starbucks #5678 BANGKOK"\`, \`"SBUX TH"\` all share \`"Starbucks"\`.
|
|
52
56
|
- \`alias\`: the exact raw statement descriptor. Plasalid normalizes and dedups it.
|
|
53
|
-
- \`default_account_id\`:
|
|
57
|
+
- \`default_account_id\`: **do not** set this on first sight, even when you're confident. The merchant's stored default is a user-taught rule, not an LLM hunch — it's only written when the resolver applies a user answer (via \`set_merchant_default_account\`) or when the user states a rule directly in record mode. Leave \`default_account_id\` unset (omit the field) on every fresh merchant block. You may still post the current row to your best-guess expense account; just don't teach the merchant that mapping system-wide.
|
|
54
58
|
Also set \`raw_descriptor\` on the transaction to the exact statement line for downstream lookups.
|
|
55
59
|
For transfers between own accounts and pure balance movements, omit the merchant block.
|
|
56
60
|
6. **Pre-resolved merchants.** If the prompt context shows a merchant already known for the descriptor, use the supplied \`merchant_id\` and \`default_account_id\` on \`record_transaction\` instead of proposing a fresh merchant block. You may override the default expense account when the row's context says otherwise (e.g. a Starbucks gift-card top-up is not Dining).
|
|
57
|
-
7. **Suspense fallback.** If you cannot categorize
|
|
61
|
+
7. **Suspense fallback (expense and income).** If you cannot categorize a posting with reasonable confidence:
|
|
62
|
+
- For an expense (debit on an expense account): post the expense side to \`expense:uncategorized\` (auto-created), and call \`note_unknown\` with \`kind="uncategorized_expense"\` and the just-posted \`transaction_id\`.
|
|
63
|
+
- For an income (credit on an income account where the subtype — salary, bonus, freelance, interest, dividend, refund — isn't obvious): post the credit to \`income:uncategorized\` (auto-created) and call \`note_unknown\` with \`kind="uncategorized"\` and the \`transaction_id\`. Do not pick \`income:other\` or any subtype as a guess.
|
|
64
|
+
|
|
65
|
+
Do **not** invent a category in either direction. The resolver batches these into one cleanup pass and (only then) learns the merchant's default from the user's fix.
|
|
58
66
|
8. Dates: convert Buddhist Era → Gregorian by subtracting 543 from the year. Store as YYYY-MM-DD.
|
|
59
67
|
9. Default currency is THB. Tag every posting with its ISO 4217 currency code on the \`record_transaction\` call; only deviate from THB when the row explicitly shows another currency (foreign-card purchases, FX transfers, multi-currency wallets).
|
|
60
68
|
10. Account numbers: store only the last 4 digits (mask the rest with bullets, e.g. \`••1234\`). Never persist the full account number.
|
|
@@ -80,7 +88,7 @@ How to phrase note_unknown:
|
|
|
80
88
|
- Provide \`options\` when the resolution is a small finite choice (e.g. which category to use, debit vs credit). When you do, always include "Skip — leave as is" as one of them.
|
|
81
89
|
|
|
82
90
|
Output formatting: use plain ASCII numbers (\`1.\`, \`2.\`, \`3.\`) for any lists. Never use Unicode circled digits (①②③). Never use emoji of any kind (no check marks, crosses, warning signs, colored circles, faces, hands, etc.) — use plain words.`;
|
|
83
|
-
export const RECORD_PERSONA = `You are Plasalid
|
|
91
|
+
export const RECORD_PERSONA = `You are Plasalid ("ปลาสลิด"), currently turning one short user utterance into the right ledger entries. The user typed something they want logged — a purchase, a transfer, a balance, a new account, or some combination. Turn that utterance into the right calls against the local three-layer ledger (hierarchical accounts, merchants, transactions+postings) and then stop.
|
|
84
92
|
|
|
85
93
|
Mission flow:
|
|
86
94
|
1. Classify the utterance into one of: NEW TRANSACTION (an event happened), BALANCE UPDATE (the user is stating a current balance, not an event), NEW ACCOUNT (the user is seeding an account that doesn't exist yet), MULTI-STEP (e.g. "pay all credit card debt from X" needs one transaction per card).
|
|
@@ -135,7 +143,7 @@ Output rules:
|
|
|
135
143
|
- No tables, no markdown grids, no emoji of any kind. Plain ASCII.
|
|
136
144
|
- Never reference internal ids in your reply text. Use human names. (Tool call arguments are fine to use ids.)
|
|
137
145
|
- If you genuinely cannot proceed (non-interactive mode and clarify is required), reply explaining what's missing.`;
|
|
138
|
-
export const RESOLVE_PERSONA = `You are Plasalid'
|
|
146
|
+
export const RESOLVE_PERSONA = `You are Plasalid ("ปลาสลิด"), currently working through every open unknown the scanner couldn't resolve. The user message hands you EVERY open unknown at once. Your goal is to close every one of them with as few user prompts as possible — automate the obvious cases first; ask only when judgment is genuinely required.
|
|
139
147
|
|
|
140
148
|
Inputs you receive:
|
|
141
149
|
- One line per open unknown in the user message: id, kind, transaction/account/file ids, prompt, options.
|
|
@@ -144,7 +152,7 @@ Inputs you receive:
|
|
|
144
152
|
|
|
145
153
|
The workflow is five steps. Do them in order. Do not skip step 1.
|
|
146
154
|
|
|
147
|
-
**Step 1 — Survey
|
|
155
|
+
**Step 1 — Survey.** Read the entire unknown list. Build a mental map: which kinds appear, which unknowns share a merchant / descriptor / account pair, which rows a loaded memory rule covers, which kinds you can resolve via heuristic alone. The goal is to know the whole shape before mutating anything.
|
|
148
156
|
|
|
149
157
|
**Step 2 — Apply memory-driven silent resolutions.** For every unknown a loaded memory rule covers (merchant→category, known recurrence identity, "these two accounts are separate", account-purpose fact), apply the implied mutation, then call \`close_unknown\` with the implied answer. Group sibling unknowns under one \`close_unknown\` call via \`related_unknown_ids\` — one call per memory rule, not one per row.
|
|
150
158
|
|
|
@@ -152,7 +160,7 @@ The workflow is five steps. Do them in order. Do not skip step 1.
|
|
|
152
160
|
- kind=\`duplicate\` — if the two transactions share the same merchant on the same date in the same file, default "Keep both" silently. (The inspector already drops these at source, but if one leaks through, suppress it here.)
|
|
153
161
|
- kind=\`correlation\` — if both sides are already linked to a recurrence, default "Keep separate" silently (recurring transfers aren't duplicates).
|
|
154
162
|
- kind=\`recurrence_candidate\` — if a memory rule names the recurrence (e.g. "Monthly ฿199 on KTC Card → Spotify subscription"), call \`record_recurrence\` with the candidate's transaction_ids and the implied frequency, then \`close_unknown\`.
|
|
155
|
-
- kind=\`uncategorized\` / \`uncategorized_expense\` — if the transaction's merchant already has a default_account_id set, apply that category via \`update_posting\` and \`close_unknown\`.
|
|
163
|
+
- kind=\`uncategorized\` / \`uncategorized_expense\` — if the transaction's merchant already has a \`default_account_id\` set, apply that category via \`update_posting\` and \`close_unknown\`. The scanner is forbidden from writing \`default_account_id\` on first sight, so any stored default is a past user answer and is authoritative — re-asking would just annoy the user.
|
|
156
164
|
- kind=\`similar_accounts\` — if the two names differ only in casing/whitespace, that's a high-confidence merge; still group with a single \`ask_user\` (don't auto-merge without confirmation, but ask only once).
|
|
157
165
|
|
|
158
166
|
In each case, call \`close_unknown\` with the implied answer and \`related_unknown_ids\` if any siblings share that answer.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type Database from "libsql";
|
|
2
|
+
export interface ForgetMatch {
|
|
3
|
+
displayId: string;
|
|
4
|
+
text: string;
|
|
5
|
+
}
|
|
6
|
+
export type ForgetOutcome = {
|
|
7
|
+
ok: true;
|
|
8
|
+
matched: ForgetMatch[];
|
|
9
|
+
} | {
|
|
10
|
+
ok: false;
|
|
11
|
+
error: string;
|
|
12
|
+
};
|
|
13
|
+
export declare function renderRules(db: Database.Database): string;
|
|
14
|
+
export declare function forgetRules(db: Database.Database, pattern: string): ForgetOutcome;
|
|
15
|
+
export declare function showRules(): void;
|
|
16
|
+
export declare function forgetRule(pattern: string): void;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { getDb } from "../../db/connection.js";
|
|
3
|
+
import { getMemories, deleteMemory } from "../../ai/memory.js";
|
|
4
|
+
import { listMerchants, clearMerchantDefaultAccount, } from "../../db/queries/merchants.js";
|
|
5
|
+
function collectRules(db) {
|
|
6
|
+
const out = [];
|
|
7
|
+
for (const m of getMemories(db)) {
|
|
8
|
+
out.push({
|
|
9
|
+
displayId: `mem:${m.id}`,
|
|
10
|
+
text: m.content,
|
|
11
|
+
forget: (db) => {
|
|
12
|
+
deleteMemory(db, m.id);
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
const merchants = listMerchants(db, { withDefaultOnly: true });
|
|
17
|
+
merchants.forEach((m, i) => {
|
|
18
|
+
out.push({
|
|
19
|
+
displayId: `mch:${i + 1}`,
|
|
20
|
+
text: `${m.canonical_name} → ${m.default_account_id}`,
|
|
21
|
+
forget: (db) => {
|
|
22
|
+
clearMerchantDefaultAccount(db, m.id);
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
export function renderRules(db) {
|
|
29
|
+
const rules = collectRules(db);
|
|
30
|
+
if (rules.length === 0) {
|
|
31
|
+
return ("No rules yet.\n\n" +
|
|
32
|
+
chalk.dim("Rules accumulate as you resolve unknowns. Run `plasalid resolve` after a scan."));
|
|
33
|
+
}
|
|
34
|
+
const width = Math.max(...rules.map((r) => r.displayId.length));
|
|
35
|
+
const lines = [chalk.bold(`Rules (${rules.length}):`)];
|
|
36
|
+
for (const r of rules) {
|
|
37
|
+
lines.push(` ${chalk.cyan(r.displayId.padEnd(width))} ${r.text}`);
|
|
38
|
+
}
|
|
39
|
+
lines.push("");
|
|
40
|
+
lines.push(chalk.dim("To remove: plasalid forget <regex>"));
|
|
41
|
+
return lines.join("\n");
|
|
42
|
+
}
|
|
43
|
+
export function forgetRules(db, pattern) {
|
|
44
|
+
let re;
|
|
45
|
+
try {
|
|
46
|
+
re = new RegExp(`^${pattern}$`);
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
return { ok: false, error: `Invalid regex /${pattern}/: ${err.message}` };
|
|
50
|
+
}
|
|
51
|
+
const snapshot = collectRules(db);
|
|
52
|
+
const hits = snapshot.filter((r) => re.test(r.displayId));
|
|
53
|
+
if (!hits.length) {
|
|
54
|
+
return {
|
|
55
|
+
ok: false,
|
|
56
|
+
error: `No rule matches /${pattern}/. Run \`plasalid rules\` to list ids.`,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const matched = hits.map((r) => {
|
|
60
|
+
r.forget(db);
|
|
61
|
+
return { displayId: r.displayId, text: r.text };
|
|
62
|
+
});
|
|
63
|
+
return { ok: true, matched };
|
|
64
|
+
}
|
|
65
|
+
export function showRules() {
|
|
66
|
+
console.log(renderRules(getDb()));
|
|
67
|
+
}
|
|
68
|
+
export function forgetRule(pattern) {
|
|
69
|
+
const outcome = forgetRules(getDb(), pattern);
|
|
70
|
+
if (!outcome.ok) {
|
|
71
|
+
console.error(chalk.red(outcome.error));
|
|
72
|
+
process.exitCode = 1;
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const width = Math.max(...outcome.matched.map((m) => m.displayId.length));
|
|
76
|
+
for (const m of outcome.matched) {
|
|
77
|
+
console.log(`Forgot ${chalk.cyan(m.displayId.padEnd(width))} ${m.text}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
package/dist/cli/index.js
CHANGED
|
@@ -131,6 +131,22 @@ program
|
|
|
131
131
|
kind: opts.kind,
|
|
132
132
|
});
|
|
133
133
|
});
|
|
134
|
+
program
|
|
135
|
+
.command("rules")
|
|
136
|
+
.description("List rules the system has learned")
|
|
137
|
+
.action(async () => {
|
|
138
|
+
ensureConfigured();
|
|
139
|
+
const { showRules } = await import("./commands/rules.js");
|
|
140
|
+
showRules();
|
|
141
|
+
});
|
|
142
|
+
program
|
|
143
|
+
.command("forget <regex>")
|
|
144
|
+
.description("Delete every learned rule whose id matches <regex> (anchored). Run `plasalid rules` to list ids.")
|
|
145
|
+
.action(async (regex) => {
|
|
146
|
+
ensureConfigured();
|
|
147
|
+
const { forgetRule } = await import("./commands/rules.js");
|
|
148
|
+
forgetRule(regex);
|
|
149
|
+
});
|
|
134
150
|
program
|
|
135
151
|
.command("revert <regex>")
|
|
136
152
|
.description("Delete scanned files matching <regex> and all their transactions")
|
|
@@ -167,6 +183,14 @@ program.configureHelp({
|
|
|
167
183
|
name: "resolve",
|
|
168
184
|
desc: "Walk open unknowns one at a time and apply your decision",
|
|
169
185
|
},
|
|
186
|
+
{
|
|
187
|
+
name: "rules",
|
|
188
|
+
desc: "List rules the system has learned",
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
name: "forget",
|
|
192
|
+
desc: "Delete learned rules whose ids match <regex> (anchored)",
|
|
193
|
+
},
|
|
170
194
|
{
|
|
171
195
|
name: "revert",
|
|
172
196
|
desc: "Delete scanned files matching <regex> and their transactions",
|
|
@@ -40,3 +40,6 @@ export declare function setMerchantDefaultAccount(db: Database.Database, merchan
|
|
|
40
40
|
before: string | null;
|
|
41
41
|
after: string;
|
|
42
42
|
};
|
|
43
|
+
export declare function clearMerchantDefaultAccount(db: Database.Database, merchantId: string): {
|
|
44
|
+
before: string | null;
|
|
45
|
+
} | null;
|
|
@@ -118,3 +118,12 @@ export function setMerchantDefaultAccount(db, merchantId, accountId) {
|
|
|
118
118
|
.run(accountId, merchantId);
|
|
119
119
|
return { before: before.default_account_id, after: accountId };
|
|
120
120
|
}
|
|
121
|
+
export function clearMerchantDefaultAccount(db, merchantId) {
|
|
122
|
+
const row = db
|
|
123
|
+
.prepare(`SELECT default_account_id FROM merchants WHERE id = ?`)
|
|
124
|
+
.get(merchantId);
|
|
125
|
+
if (!row)
|
|
126
|
+
return null;
|
|
127
|
+
db.prepare(`UPDATE merchants SET default_account_id = NULL WHERE id = ?`).run(merchantId);
|
|
128
|
+
return { before: row.default_account_id };
|
|
129
|
+
}
|