Add support for adding raw transactions
This commit is contained in:
@@ -93,7 +93,21 @@ export class Actual {
|
|||||||
return actualTransaction;
|
return actualTransaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
async submit(transactions: Transaction[]): Promise<ActualImportResult> {
|
async import(transactions: Transaction[]): Promise<ActualImportResult> {
|
||||||
|
return this.#submit(transactions, t => api.importTransactions(t.account, [t]));
|
||||||
|
}
|
||||||
|
|
||||||
|
async add(transactions: Transaction[], learnCategories: boolean, runTransfers: boolean): Promise<ActualImportResult> {
|
||||||
|
return this.#submit(transactions, async t => {
|
||||||
|
|
||||||
|
const result = await api.addTransactions(t.account, [t], { learnCategories, runTransfers });
|
||||||
|
return result === "ok"
|
||||||
|
? { added: [t], updated: [], errors: [] }
|
||||||
|
: { added: [], updated: [], errors: [{ message: `Cannot add transaction: ${t}` }] };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async #submit(transactions: Transaction[], add: (t: ActualTransaction) => Promise<ActualImportResult>): Promise<ActualImportResult> {
|
||||||
try {
|
try {
|
||||||
await mkdir(this.#config.data, { recursive: true });
|
await mkdir(this.#config.data, { recursive: true });
|
||||||
} catch(e) {}
|
} catch(e) {}
|
||||||
@@ -119,7 +133,7 @@ export class Actual {
|
|||||||
}
|
}
|
||||||
|
|
||||||
else for (const transaction of toImport) {
|
else for (const transaction of toImport) {
|
||||||
const result = await api.importTransactions(transaction.account, [transaction]);
|
const result = await add(transaction);
|
||||||
|
|
||||||
output.added.push(...result.added);
|
output.added.push(...result.added);
|
||||||
output.updated.push(...result.updated);
|
output.updated.push(...result.updated);
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import { program } from "commander";
|
import { program } from "commander";
|
||||||
import { ImportOptions } from "@/types/cli";
|
import { AddOptions, ImportOptions } from "@/types/cli";
|
||||||
import { loadConfig } from "./config";
|
import { loadConfig } from "./config";
|
||||||
import { importTransactions } from "@/runner";
|
import { addTransactions, importTransactions } from "@/runner";
|
||||||
import { serve } from "@/server";
|
import { serve } from "@/server";
|
||||||
|
|
||||||
export function run(...args: string[]) {
|
export function run(...args: string[]) {
|
||||||
@@ -10,6 +10,17 @@ export function run(...args: string[]) {
|
|||||||
.name("actual-importer")
|
.name("actual-importer")
|
||||||
.version("0.0.1")
|
.version("0.0.1")
|
||||||
|
|
||||||
|
program
|
||||||
|
.command("add <csv-file>")
|
||||||
|
.requiredOption("-c, --config <file>", "sets the path to the YAML file with configuration")
|
||||||
|
.option("-d, --dry-run", "simulates the import by printing out what will be imported")
|
||||||
|
.option("-p, --profile <name>", "sets the desired profile to invoke")
|
||||||
|
.option("-s, --server <name>", "sets the desired server to upload transactions to")
|
||||||
|
.option("-t, --transfers", "whether transfer transactions should be resolved")
|
||||||
|
.option("-l, --learn", "whether new rules for category assignment should be created")
|
||||||
|
.option("-x, --set <arg>", "overrides the config option for this specific run (arg: <key>=<name>, i.e. profiles.myprofile.parser=pl.ing", (v: string, prev: string[]) => prev.concat([v]), [])
|
||||||
|
.action(doAdd)
|
||||||
|
|
||||||
program
|
program
|
||||||
.command("import <csv-file>")
|
.command("import <csv-file>")
|
||||||
.requiredOption("-c, --config <file>", "sets the path to the YAML file with configuration")
|
.requiredOption("-c, --config <file>", "sets the path to the YAML file with configuration")
|
||||||
@@ -28,6 +39,23 @@ export function run(...args: string[]) {
|
|||||||
program.parse(args);
|
program.parse(args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function doAdd(file: string, options: AddOptions) {
|
||||||
|
const config = parseConfig(options.config, options.set);
|
||||||
|
|
||||||
|
if (Object.keys(config?.profiles ?? {}).length === 0) {
|
||||||
|
throw new Error(`No profiles defined in the ${options.config}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(config?.servers ?? {}).length === 0) {
|
||||||
|
throw new Error(`No servers defined in the ${options.config}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const profile = options.profile ?? config.defaultProfile ?? Object.keys(config.profiles)[0];
|
||||||
|
const server = options.server ?? config.defaultServer ?? Object.keys(config.servers)[0];
|
||||||
|
|
||||||
|
const result = await addTransactions(fs.createReadStream(file), profile, server, config, options.dryRun, options.learn, options.transfers);
|
||||||
|
}
|
||||||
|
|
||||||
async function doImport(file: string, options: ImportOptions) {
|
async function doImport(file: string, options: ImportOptions) {
|
||||||
const config = parseConfig(options.config, options.set);
|
const config = parseConfig(options.config, options.set);
|
||||||
|
|
||||||
@@ -45,7 +73,6 @@ async function doImport(file: string, options: ImportOptions) {
|
|||||||
const result = await importTransactions(fs.createReadStream(file), profile, server, config, options.dryRun);
|
const result = await importTransactions(fs.createReadStream(file), profile, server, config, options.dryRun);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function doServe(port: string, options: any) {
|
function doServe(port: string, options: any) {
|
||||||
const config = parseConfig(options.config, options.set);
|
const config = parseConfig(options.config, options.set);
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ export type ImportResult = {
|
|||||||
skipped: string[][];
|
skipped: string[][];
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function importTransactions(stream: Readable, profile: string, server: string, config: Config, dryRun?: boolean): Promise<ImportResult> {
|
const submitTransactions = (load: (server: Actual, t: Transaction[]) => Promise<ActualImportResult>) => async (stream: Readable, profile: string, server: string, config: Config, dryRun?: boolean): Promise<ImportResult> => new Promise((resolve, reject) => {
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const profileConfig = config.profiles[profile];
|
const profileConfig = config.profiles[profile];
|
||||||
|
|
||||||
if (!profileConfig) {
|
if (!profileConfig) {
|
||||||
@@ -41,7 +40,7 @@ export async function importTransactions(stream: Readable, profile: string, serv
|
|||||||
const handleClose = async () => {
|
const handleClose = async () => {
|
||||||
try {
|
try {
|
||||||
const transactions = await parser.reconcile();
|
const transactions = await parser.reconcile();
|
||||||
const result = await actualServer.submit(transactions);
|
const result = await load(actualServer, transactions);
|
||||||
|
|
||||||
resolve({
|
resolve({
|
||||||
transactions,
|
transactions,
|
||||||
@@ -61,4 +60,8 @@ export async function importTransactions(stream: Readable, profile: string, serv
|
|||||||
.on('data', handleRow)
|
.on('data', handleRow)
|
||||||
.on('close', handleClose);
|
.on('close', handleClose);
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
export const importTransactions = submitTransactions((s, t) => s.import(t));
|
||||||
|
|
||||||
|
export const addTransactions = async (stream: Readable, profile: string, server: string, config: Config, dryRun?: boolean, learnCategories?: boolean, runTransfers?: boolean): Promise<ImportResult> =>
|
||||||
|
submitTransactions((s, t) => s.add(t, !!learnCategories, !!runTransfers))(stream, profile, server, config, dryRun);
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ImportResult, importTransactions } from "@/runner";
|
import { addTransactions, ImportResult, importTransactions } from "@/runner";
|
||||||
import { Config } from "@/types/config";
|
import { Config } from "@/types/config";
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import multer from "multer";
|
import multer from "multer";
|
||||||
@@ -30,6 +30,21 @@ export function serve(config: Config, port: number) {
|
|||||||
${Object.keys(config.servers).map(s => `<option${s === config.defaultServer ? " selected": ""}>${s}</option>`)}
|
${Object.keys(config.servers).map(s => `<option${s === config.defaultServer ? " selected": ""}>${s}</option>`)}
|
||||||
</select>
|
</select>
|
||||||
</p>
|
</p>
|
||||||
|
<p>
|
||||||
|
<label for="mode">Import mode</label>
|
||||||
|
<select id="mode" name="mode">
|
||||||
|
<option value="add" selected>Add</option>
|
||||||
|
<option value="import">Import</option>
|
||||||
|
</select>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<label for="learn">Learn categories (only for <u>add</u> mode)</label>
|
||||||
|
<input type="checkbox" id="learn" name="learn" />
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<label for="transfers">Run transfers (only for <u>add</u> mode)</label>
|
||||||
|
<input type="checkbox" id="transfers" name="transfers" checked />
|
||||||
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<button type="submit">Import</button>
|
<button type="submit">Import</button>
|
||||||
</p>
|
</p>
|
||||||
@@ -44,13 +59,15 @@ export function serve(config: Config, port: number) {
|
|||||||
throw new Error("No file to upload");
|
throw new Error("No file to upload");
|
||||||
}
|
}
|
||||||
|
|
||||||
const { profile, server } = req.body;
|
const { profile, server, learn, transfers, mode } = req.body;
|
||||||
|
|
||||||
const stream = new Readable();
|
const stream = new Readable();
|
||||||
stream.push(req.file.buffer);
|
stream.push(req.file.buffer);
|
||||||
stream.push(null);
|
stream.push(null);
|
||||||
|
|
||||||
const result = await importTransactions(stream, profile, server, config);
|
const result = mode === "add"
|
||||||
|
? await addTransactions(stream, profile, server, config, false, readCheckbox(learn), readCheckbox(transfers))
|
||||||
|
: await importTransactions(stream, profile, server, config);
|
||||||
res.send(formatResult(result));
|
res.send(formatResult(result));
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -59,6 +76,10 @@ export function serve(config: Config, port: number) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readCheckbox(value: string|undefined): boolean {
|
||||||
|
return ["on", "true"].includes(value?.toLowerCase() ?? "");
|
||||||
|
}
|
||||||
|
|
||||||
function formatResult(result: ImportResult): string {
|
function formatResult(result: ImportResult): string {
|
||||||
return `
|
return `
|
||||||
<html>
|
<html>
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
export type AddOptions = ImportOptions & {
|
||||||
|
learn?: boolean;
|
||||||
|
transfers?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export type ImportOptions = {
|
export type ImportOptions = {
|
||||||
config: string;
|
config: string;
|
||||||
dryRun?: boolean;
|
dryRun?: boolean;
|
||||||
|
|||||||
Reference in New Issue
Block a user