Commit d3c8b075 authored by Nicolas Frandeboeuf's avatar Nicolas Frandeboeuf
Browse files

Use bank's real balance instead of initialBalance + sum of transactions

parent e3c7a3eb
Pipeline #443040 failed with stage
in 1 minute and 32 seconds
......@@ -29,7 +29,7 @@ export class DriverAccount extends Driver {
account.lastCheckDate,
account.balance,
account.outstandingSum,
account.initialBalance,
account.balance,
account
);
}
......
......@@ -237,6 +237,7 @@ export class Account {
assertHas(arg, 'label');
assertHas(arg, 'vendorAccountId');
assertHas(arg, 'initialBalance');
assertHas(arg, 'balance');
assertHas(arg, 'lastCheckDate');
assertHas(arg, 'id');
......@@ -245,6 +246,7 @@ export class Account {
this.label = arg.label;
this.vendorAccountId = arg.vendorAccountId;
this.initialBalance = arg.initialBalance;
this.balance = arg.balance;
this.lastCheckDate = new Date(arg.lastCheckDate);
this.id = arg.id;
......@@ -262,7 +264,6 @@ export class Account {
// These fields will be updated when the operations are attached to the account.
// Make sure to update `updateFrom` if you add any fields here.
this.operationIds = [];
this.balance = this.initialBalance;
this.outstandingSum = 0;
}
......@@ -275,8 +276,7 @@ export class Account {
// Make sure to keep this in sync with the above ctor.
newAccount.operationIds = previousAccount.operationIds;
newAccount.balance =
previousAccount.balance - previousAccount.initialBalance + newAccount.initialBalance;
newAccount.balance = previousAccount.balance;
newAccount.outstandingSum = previousAccount.outstandingSum;
return newAccount;
......
......@@ -708,8 +708,7 @@ function reduceResyncBalance(state: BankState, action: Action<ResyncBalanceParam
assertDefined(initialBalance);
return mutateState(state, mut => {
const account = accountById(mut.state, accountId);
const balance = account.balance - account.initialBalance + initialBalance;
mergeInObject(mut.state.accountMap, accountId, { initialBalance, balance });
mergeInObject(mut.state.accountMap, accountId, { initialBalance });
});
}
......@@ -1167,7 +1166,6 @@ function makeCompareOperationByIds(state: BankState) {
function addOperations(mut: MutableState, operations: Partial<Operation>[]): void {
const accountsToSort = new Set<Account>();
const today = new Date();
for (const op of operations) {
const operation = new Operation(op);
......@@ -1176,9 +1174,9 @@ function addOperations(mut: MutableState, operations: Partial<Operation>[]): voi
account.operationIds.push(operation.id);
if (shouldIncludeInBalance(operation, today, account.type)) {
account.balance += operation.amount;
} else if (shouldIncludeInOutstandingSum(operation)) {
// Do not include it in the balance, this should be computed by the server.
if (shouldIncludeInOutstandingSum(operation)) {
account.outstandingSum += operation.amount;
}
......@@ -1384,12 +1382,11 @@ function removeOperation(mut: MutableState, operationId: number): void {
const op = operationById(mut.state, operationId);
const account = accountById(mut.state, op.accountId);
let { balance, outstandingSum } = account;
const today = new Date();
let { outstandingSum } = account;
// Do not remove it from the balance, this should be computed by the server.
if (shouldIncludeInBalance(op, today, account.type)) {
balance -= op.amount;
} else if (shouldIncludeInOutstandingSum(op)) {
if (shouldIncludeInOutstandingSum(op)) {
outstandingSum -= op.amount;
}
......
......@@ -137,6 +137,14 @@ async function getAllData(userId: number, options: GetAllDataOptions = {}): Prom
ret.operations = await Transaction.all(userId);
ret.settings = await Setting.all(userId);
// If there is no balance for an account, compute one based on the initial amount and the
// transactions.
ret.accounts.forEach(async acc => {
if (acc.balance === null) {
acc.balance = await acc.computeBalance();
}
});
if (isExport) {
ret.budgets = await Budget.all(userId);
......
......@@ -152,6 +152,9 @@ export async function create(req: IdentifiedRequest<Transaction>, res: express.R
operation.isUserDefinedType = true;
}
const op = await Transaction.create(userId, operation);
// This probably updated the account's balance, we should sent it back.
res.status(201).json(op);
} catch (err) {
asyncErr(res, err, 'when creating operation for a bank account');
......@@ -164,6 +167,9 @@ export async function destroy(req: PreloadedRequest<Transaction>, res: express.R
const { id: userId } = req.user;
const op = req.preloaded.operation;
await Transaction.destroy(userId, op.id);
// This probably updated the account's balance, we should sent it back.
res.status(204).end();
} catch (err) {
asyncErr(res, err, 'when deleting operation');
......
......@@ -133,6 +133,9 @@ async function retrieveAllAccountsByAccess(
iban: sourceAccount.iban,
label: sourceAccount.label,
initialBalance: Number.parseFloat(sourceAccount.balance) || 0,
balance: sourceAccount.hasOwnProperty('balance')
? Number.parseFloat(sourceAccount.balance) || 0
: null,
lastCheckDate: new Date(),
importDate: new Date(),
};
......@@ -631,16 +634,25 @@ to be resynced, by an offset of ${balanceOffset}.`);
if (Math.abs(balanceDelta) > 0.001) {
log.info(`Updating balance for account ${account.vendorAccountId}`);
const initialBalance = account.initialBalance + balanceDelta;
const updatedAccount = await Account.update(userId, account.id, { initialBalance });
const updatedAccount = await Account.update(userId, account.id, {
initialBalance,
balance: realBalance,
});
return {
kind: 'value',
value: updatedAccount,
};
}
} else {
// This case can happen if it's a known orphan.
throw new KError('account not found', 404);
const updatedAccount = await Account.update(userId, account.id, {
balance: realBalance,
});
return {
kind: 'value',
value: updatedAccount,
};
}
// This case can happen if it's a known orphan.
throw new KError('account not found', 404);
return { kind: 'value', value: account };
}
......
......@@ -142,7 +142,7 @@ ${$t(i18n, 'server.email.signature')}
continue;
}
const balance = await account.computeBalance();
const balance = await account.getBalance();
for (const alert of alerts) {
if (!alert.testBalance(balance)) {
continue;
......
......@@ -219,7 +219,7 @@ class ReportManager {
const formatCurrency = await account.getCurrencyFormatter();
const lastCheckDate = formatDate(i18n.localeId).toShortString(account.lastCheckDate);
const balance = await account.computeBalance();
const balance = await account.getBalance();
content += `\t* ${accountsNameMap.get(account.id)} : `;
content += `${formatCurrency(balance)} (`;
content += $t(i18n, 'server.email.report.last_sync');
......
......@@ -109,6 +109,10 @@ export default class Account {
@Column('boolean', { default: false })
excludeFromBalance = false;
// Balance on the account, updated at each account update.
@Column('numeric', { nullable: true, default: null, transformer: new ForceNumericColumn() })
balance: number | null = null;
// Methods.
computeBalance = async (): Promise<number> => {
......@@ -120,6 +124,12 @@ export default class Account {
return Math.round(s * 100) / 100;
};
getBalance = async (): Promise<number> => {
if (this.balance !== null) return this.balance;
return await this.computeBalance();
};
computeOutstandingSum = async (): Promise<number> => {
const ops = await Transaction.byAccount(this.userId, this);
const s = ops
......
import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';
// Add a new column "session" that contains a serialized JSON object containing
// the current cookie/session store for a bank backend.
export class AddBalanceInAccount1631037503295 implements MigrationInterface {
async up(q: QueryRunner): Promise<void> {
await q.addColumn(
'account',
new TableColumn({
name: 'balance',
type: 'numeric',
isNullable: true,
default: null,
})
);
}
async down(q: QueryRunner): Promise<void> {
await q.dropColumn('account', 'balance');
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment