Compare commits

..

8 commits
v0.0.5 ... main

Author SHA1 Message Date
Campbell Alden
3e6f1f4780 Fix helpers importing 2026-03-26 00:05:22 +09:00
Campbell Alden
470be2880c Set up a test for the failing parsing 2026-03-26 00:00:13 +09:00
Campbell Alden
4e4760b55a Enable direnv 2026-03-25 18:58:37 +09:00
Campbell Alden
0858f525db Add more specific information when logging and report it over the API 2026-03-22 00:29:15 +09:00
Campbell Alden
896163470d Accept arbitrarily formatted numbers as the amount input 2026-03-22 00:23:02 +09:00
Campbell Alden
2bb7902c3b Remove commit and correct handling of transaction amount 2026-03-20 17:29:54 +09:00
Campbell Alden
48ceec334e try removing the loadBudget call entirely 2026-03-20 17:22:54 +09:00
Campbell Alden
c61ec98ce2 Go back to trying to write values like a client, but do so more defensively 2026-03-20 17:11:02 +09:00
6 changed files with 4382 additions and 30 deletions

9
.envrc Normal file
View file

@ -0,0 +1,9 @@
#!/usr/bin/env bash
# the shebang is ignored, but nice for editors
if type -P lorri &>/dev/null; then
eval "$(lorri direnv)"
else
echo 'while direnv evaluated .envrc, could not find the command "lorri" [https://github.com/nix-community/lorri]'
use nix
fi

39
helpers.cjs Normal file
View file

@ -0,0 +1,39 @@
function parse(data) {
if (typeof data !== 'object' || data === null) {
throw new Error(`Bad Data, expected an object, got: ${data === null ? null : typeof data}`);
}
if ((typeof data.title) !== 'string') {
throw new Error(`Bad Title: ${data.title}`);
}
if (!['string', 'number'].includes(typeof data.amount)) {
throw new Error(`Bad amount: ${data.amount}`);
}
if (typeof data.amount === 'string') {
const value = Number(data.amount.replaceAll(/[^\d]/g, ''));
data.amount = Number.isNaN(value) ? 0 : value;
}
return data;
}
function makeTransaction(data, account) {
return {
account,
date: new Date().toLocaleDateString('sv-SE'), // "YYYY-MM-DD"
payee_name: data.title,
// Actual considers this an integer representing a decimal value so the last two digits of the integer
// are placed after the decimal point (unclear why but okay...)
// It has to be negative or actual will think it's a deposit
amount: data.amount * -100,
cleared: true,
};
}
module.exports = {
makeTransaction,
parse
};

49
helpers.spec.js Normal file
View file

@ -0,0 +1,49 @@
const { describe, expect, test } = require('@jest/globals');
const { parse } = require('./helpers');
describe(parse.name, () => {
const TEST_DATA = {
title: 'test',
amount: 1000,
};
[
['number title', { ...TEST_DATA, title: 11 }],
['boolean title', { ...TEST_DATA, title: false }],
['object title', { ...TEST_DATA, title: {} }],
['null title', { ...TEST_DATA, title: null }],
['boolean amount', { ...TEST_DATA, amount: true }],
['object amount', { ...TEST_DATA, amount: {} }],
].forEach(([tag, badData]) => {
test(`it rejects ${tag} as bad data`, () => {
expect(() => parse(badData)).toThrow();
});
});
test('it accepts a regular value', () => {
let result;
expect(() => {
result = parse(TEST_DATA);
}).not.toThrow();
expect(result).toEqual(TEST_DATA);
});
test('it parses yen', () => {
let result;
expect(() => {
result = parse({ title: 'test', amount: '¥3,746'});
}).not.toThrow();
expect(result).toEqual({
amount: 3746,
title: 'test',
});
});
test('it strips out non-number values from strings', () => {
let result;
expect(() => {
result = parse({ title: 'test', amount: '¥3,7~4j6.1t1'});
}).not.toThrow();
expect(result).toEqual({
amount: 374611,
title: 'test',
});
});
});

4259
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,10 +1,17 @@
{ {
"type": "module",
"name": "reporter.js", "name": "reporter.js",
"version": "0.0.1", "version": "0.0.1",
"scripts": {
"test": "jest"
},
"dependencies": { "dependencies": {
"@actual-app/api": "^26.3.0", "@actual-app/api": "^26.3.0",
"async-lock": "^1.4.1", "async-lock": "^1.4.1",
"cors": "^2.8.6", "cors": "^2.8.6",
"express": "^5.2.1" "express": "^5.2.1"
},
"devDependencies": {
"jest": "^30.3.0"
} }
} }

View file

@ -1,6 +1,9 @@
// Expects a JSON config file to be passed in the env variable CONFIG_FILE. // Expects a JSON config file to be passed in the env variable CONFIG_FILE.
// The file should contain: // The file should contain:
// "dataDir": path // "dataDir": path
// "serverURL": where to point the requests
// "password": The password for logging in
// "budgetId": The ID of the budget containing the account to write transactions into.
// "accountId": UUID for the account to add transactions to. // "accountId": UUID for the account to add transactions to.
import express from 'express' import express from 'express'
@ -8,31 +11,22 @@ import AsyncLock from 'async-lock';
import cors from 'cors' import cors from 'cors'
import api from '@actual-app/api'; import api from '@actual-app/api';
import fs from 'fs'; import fs from 'fs';
import { makeTransaction, parse } from './helpers.cjs';
function parse(data) { async function addTransaction(config, transaction) {
if (typeof data !== 'object' || data === null) { try {
throw new Error('Bad Data'); await api.init({
dataDir: config.dataDir,
serverURL: config.serverURL,
password: config.password,
});
await api.downloadBudget(config.budgetId);
await api.sync();
await api.addTransactions(config.accountId, [transaction]);
} finally {
await api.shutdown();
} }
if (typeof data.title !== 'string') {
throw new Error('Bad Title');
}
if (typeof data.amount !== 'number') {
throw new Error('Bad amount');
}
return data;
}
function makeTransaction(data, account) {
return {
account,
date: new Date().toLocaleDateString('sv-SE'), // "YYYY-MM-DD"
payee_name: data.title,
amount: data.amount,
cleared: true,
};
} }
async function init() { async function init() {
@ -48,21 +42,16 @@ async function init() {
await lock.acquire('transaction', async () => { await lock.acquire('transaction', async () => {
console.debug('Executing Transaction'); console.debug('Executing Transaction');
try { try {
await api.init({
dataDir: config.dataDir,
});
const transaction = makeTransaction(parse(req.body), config.accountId); const transaction = makeTransaction(parse(req.body), config.accountId);
console.log(transaction); console.log(transaction);
await api.addTransactions(config.accountId, [transaction]); await addTransaction(config, transaction)
console.log(`Successfully logged "${transaction.payee_name} - ¥${transaction.amount}"`); console.log(`Successfully logged "${transaction.payee_name} - ¥${transaction.amount}"`);
return res.json({ result: 'success' }); return res.json({ result: 'success' });
} catch (e) { } catch (e) {
console.log('Transaction failed...'); console.log('Transaction failed...');
console.log(e); console.log(e);
console.log(e.message); console.log(e.message);
return res.json({ result: 'failure' }); return res.json({ result: 'failure', error: e.message });
} finally {
await api.shutdown();
} }
}); });
}); });