Commit f6b7f824 authored by Benjamin Bouvier's avatar Benjamin Bouvier
Browse files

Fixes #413: Remember amounts of transactions older than the import date of the

account to resync balance;
parent 265864d9
......@@ -89,7 +89,7 @@ export default class NewBankForm extends React.Component {
this.domPassword().select();
break;
case Errors.INVALID_PARAMETERS:
alert($t('client.sync.invalid_parameters', { content: err.content }));
alert($t('client.sync.invalid_parameters', { content: err.content || '?' }));
break;
case Errors.EXPIRED_PASSWORD:
alert($t('client.sync.expired_password'));
......
......@@ -115,10 +115,14 @@ async function mergeAccounts(old, kid) {
await old.updateAttributes(newAccount);
}
function sumOpAmounts(acc, op) {
return acc + +op.amount;
}
export default class AccountManager {
constructor() {
this.newAccounts = [];
this.newAccountsMap = new Map;
}
async retrieveAndAddAccountsByAccess(access) {
......@@ -126,6 +130,12 @@ export default class AccountManager {
}
async retrieveAccountsByAccess(access, shouldAddNewAccounts) {
if (this.newAccountsMap.size) {
log.warn(`At the start of retrieveAccountsByAccess, newAccountsMap
should be empty.`);
this.newAccountsMap.clear();
}
if (!access.hasPassword()) {
log.warn("Skipping accounts fetching -- password isn't present");
let errcode = getErrorCode('NO_PASSWORD');
......@@ -142,7 +152,8 @@ export default class AccountManager {
iban: accountWeboob.iban,
title: accountWeboob.label,
initialAmount: accountWeboob.balance,
lastChecked: new Date()
lastChecked: new Date(),
importDate: new Date()
};
if (currency.isKnown(accountWeboob.currency)) {
account.currency = accountWeboob.currency;
......@@ -168,10 +179,31 @@ export default class AccountManager {
}
if (shouldAddNewAccounts) {
log.info('New account found.');
log.info('New account found, saving it as per request.');
let newAccountInfo = {
account: null,
balanceOffset: 0
};
// Consider all the operations that could have been inserted
// before the fix in #405.
let existingOperations = await Operation.byAccount(account);
if (existingOperations.length) {
let offset = existingOperations.reduce(sumOpAmounts, 0);
newAccountInfo.balanceOffset += offset;
}
// Save the account in DB and in the new accounts map.
let newAccount = await Account.create(account);
this.newAccounts.push(newAccount);
newAccountInfo.account = newAccount;
this.newAccountsMap.set(newAccount.accountNumber, newAccountInfo);
continue;
}
log.info('Unknown account found, not saving as per request.');
}
}
......@@ -188,6 +220,25 @@ export default class AccountManager {
let now = moment().format('YYYY-MM-DDTHH:mm:ss.000Z');
let allAccounts = await Account.byAccess(access);
let accountMap = new Map;
for (let account of allAccounts) {
if (this.newAccountsMap.has(account.accountNumber)) {
let oldEntry = this.newAccountsMap.get(account.accountNumber);
accountMap.set(account.accountNumber, oldEntry);
continue;
}
accountMap.set(account.accountNumber, {
account,
balanceOffset: 0
});
}
// Eagerly clear state.
this.newAccountsMap.clear();
// Normalize source information
for (let sourceOp of sourceOps) {
......@@ -210,16 +261,10 @@ export default class AccountManager {
operations.push(operation);
}
let allAccounts = await Account.byAccess(access);
let accountSet = new Set;
for (let account of allAccounts) {
accountSet.add(account.accountNumber);
}
let newOperations = [];
for (let operation of operations) {
// Ignore operations coming from unknown accounts.
if (!accountSet.has(operation.bankAccount))
if (!accountMap.has(operation.bankAccount))
continue;
// Ignore operations already known in database.
......@@ -227,7 +272,16 @@ export default class AccountManager {
if (similarOperations && similarOperations.length)
continue;
// It is definitely a new operation.
newOperations.push(operation);
// Remember amounts of transactions older than the import, to
// resync balance.
let accountInfo = accountMap.get(operation.bankAccount);
let opDate = new Date(operation.date);
if (+opDate <= +accountInfo.account.importDate) {
accountInfo.balanceOffset += +operation.amount;
}
}
// Create the new operations
......@@ -235,36 +289,29 @@ export default class AccountManager {
if (numNewOperations) {
log.info(`${newOperations.length} new operations found!`);
}
for (let operationToCreate of newOperations) {
await Operation.create(operationToCreate);
}
// Carry over all the triggers on new operations.
if (this.newAccounts && this.newAccounts.length) {
log.info('Updating initial amount of newly imported accounts...');
}
let reducer = (sum, op) => sum + op.amount;
for (let account of this.newAccounts) {
// Consider all the operations we've just inserted and the
// operations that could have been inserted before the fix in #405.
let relatedOperations = Operation.byAccount(account);
if (!relatedOperations.length)
continue;
let offset = relatedOperations.reduce(reducer, 0);
account.initialAmount -= offset;
await account.save();
// Update account balances.
for (let { account, balanceOffset } of accountMap.values()) {
if (balanceOffset) {
log.info(`Account ${account.title} initial balance is going to be resynced, by an
offset of ${balanceOffset}.`);
account.initialAmount -= balanceOffset;
await account.save();
}
}
// Carry over all the triggers on new operations.
log.info("Updating 'last checked' for linked accounts...");
for (let account of allAccounts) {
await account.updateAttributes({ lastChecked: new Date() });
}
log.info('Informing user new operations have been imported...');
// Don't show the notification after importing a new account.
if (numNewOperations > 0 && this.newAccounts.length === 0) {
if (numNewOperations > 0) {
/* eslint-disable camelcase */
let count = { smart_count: numNewOperations };
......@@ -285,8 +332,5 @@ export default class AccountManager {
access.fetchStatus = 'OK';
await access.save();
log.info('Post process: done.');
// reset state
this.newAccounts = [];
}
}
......@@ -4,7 +4,6 @@ import moment from 'moment' ;
import { makeLogger, KError } from '../../helpers';
import errors from '../../shared/errors.json';
let log = makeLogger('sources/mock');
const TIME_TO_GENERATE_OPERATIONS_MS = 500;
......@@ -184,28 +183,28 @@ let selectRandomAccount = uuid => {
let generate = uuid => {
let operations = [];
let count = 5;
let i = count;
let i = 5;
while (i--) {
operations.push(generateOne(selectRandomAccount(uuid)));
}
while (rand(0, 100) > 70 && count < 8) {
while (rand(0, 100) > 70 && i < 3) {
operations.push(generateOne(selectRandomAccount(uuid)));
count++;
i++;
}
// Generate exact same operations imported at the same time
// These operations shall not be considered as duplicates.
if (rand(0, 100) > 85 && operations.length) {
log.info('Generate a similar but non-duplicate operation.');
operations.push(operations[0]);
count++;
}
// Generate always the same operation, so that it is considered
// as a duplicate.
if (rand(0, 100) > 70) {
log.info('Generate a possibly duplicate operation.');
let duplicateOperation = {
title: 'This is a duplicate operation',
amount: '13.37',
......@@ -219,12 +218,33 @@ let generate = uuid => {
date = date.add(1, 'days');
}
duplicateOperation.date = date.format('YYYY-MM-DDT00:00:00.000[Z]');
log.info('Generated a duplicate operation');
operations.push(duplicateOperation);
count++;
}
log.info(`generated ${count} fake operations`);
// Sometimes generate a very old operation, probably older than the oldest
// one.
if (rand(0, 100) > 90) {
log.info('Generate a very old transaction to trigger balance resync.');
let op = {
title: 'Ye Olde Transaction',
raw: 'Ye Olde Transaction - for #413 testing',
amount: '42.12',
account: hashAccount(uuid).main,
date: new Date('01/01/2000')
};
operations.push(op);
}
log.info(`Generated ${operations.length} fake operations:`);
let accountMap = new Map;
for (let op of operations) {
let prev = accountMap.has(op.account) ? accountMap.get(op.account) : [0, 0];
accountMap.set(op.account, [prev[0] + 1, prev[1] + +op.amount]);
}
for (let [account, [num, amount]] of accountMap) {
log.info(`- ${num} new operations (${amount}) for account ${account}.`);
}
return operations;
};
......
......@@ -17,7 +17,8 @@ let Account = americano.getModel('bankaccount', {
iban: String,
initialAmount: Number,
currency: String,
lastChecked: Date
lastChecked: Date,
importDate: Date
});
Account = promisifyModel(Account);
......
......@@ -43,6 +43,10 @@ async function updateCustomFields(access, changeFn) {
}
}
function reduceOperationsDate(oldest, operation) {
return Math.min(oldest, +new Date(operation.dateImport));
}
let migrations = [
async function m1() {
......@@ -213,6 +217,29 @@ website format.`);
await b.destroy();
log.info('\tdone!');
}
},
async function m6() {
log.info('Ensure "importDate" field is present in accounts.');
let accounts = await Account.all();
for (let a of accounts) {
if (typeof a.importDate !== 'undefined')
continue;
log.info(`\t${a.accountNumber} has no importDate.`);
let ops = await Operation.byAccount(a);
let dateNumber = Date.now();
if (ops.length) {
dateNumber = ops.reduce(reduceOperationsDate, Date.now());
}
a.importDate = new Date(dateNumber);
await a.save();
log.info(`\tImport date for ${a.title} (${a.accountNumber}): ${a.importDate}`);
}
}
];
......
Supports Markdown
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