Skip to content
GitLab
Menu
Projects
Groups
Snippets
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
kresusapp
kresus
Commits
e6275633
Commit
e6275633
authored
Feb 21, 2022
by
Benjamin Bouvier
Browse files
Sync balances on every transactions or accounts sync.
Fixes
#1155
.
parent
49de73a7
Pipeline
#491256
passed with stage
in 2 minutes and 14 seconds
Changes
3
Pipelines
1
Hide whitespace changes
Inline
Side-by-side
client/models.ts
View file @
e6275633
...
...
@@ -276,7 +276,6 @@ export class Account {
// Make sure to keep this in sync with the above ctor.
newAccount
.
operationIds
=
previousAccount
.
operationIds
;
newAccount
.
balance
=
previousAccount
.
balance
;
newAccount
.
outstandingSum
=
previousAccount
.
outstandingSum
;
return
newAccount
;
...
...
server/lib/accounts-manager.ts
View file @
e6275633
...
...
@@ -48,6 +48,7 @@ async function mergeAccounts(userId: number, known: Account, provided: Partial<A
iban
:
provided
.
iban
,
currency
:
provided
.
currency
,
type
:
provided
.
type
,
balance
:
provided
.
balance
??
known
.
balance
,
};
await
Account
.
update
(
userId
,
known
.
id
,
newProps
);
}
...
...
@@ -62,16 +63,17 @@ export type UserActionOrValue<T> = UserActionResponse | Value<T>;
// Given a partial account returned from a provider, normalizes its fields so
// as to make it useful for Kresus.
function
normalizeAccount
(
access
:
Access
,
source
:
ProviderAccount
):
Partial
<
Account
>
{
const
balance
=
source
.
hasOwnProperty
(
'
balance
'
)
?
Number
.
parseFloat
(
source
.
balance
||
'
0
'
)
||
0
:
null
;
const
account
:
Partial
<
Account
>
=
{
vendorAccountId
:
source
.
vendorAccountId
,
vendorId
:
access
.
vendorId
,
accessId
:
access
.
id
,
iban
:
source
.
iban
??
null
,
label
:
source
.
label
,
initialBalance
:
Number
.
parseFloat
(
source
.
balance
||
'
0
'
)
||
0
,
balance
:
source
.
hasOwnProperty
(
'
balance
'
)
?
Number
.
parseFloat
(
source
.
balance
||
'
0
'
)
||
0
:
null
,
initialBalance
:
balance
||
0
,
balance
,
lastCheckDate
:
new
Date
(),
importDate
:
new
Date
(),
};
...
...
@@ -306,9 +308,9 @@ function normalizeTransaction(
tr
.
importDate
=
startOfPoll
;
const
opera
tionType
=
transactionTypeIdToName
(
providerTr
.
type
);
if
(
opera
tionType
!==
null
)
{
tr
.
type
=
opera
tionType
;
const
transac
tionType
=
transactionTypeIdToName
(
providerTr
.
type
);
if
(
transac
tionType
!==
null
)
{
tr
.
type
=
transac
tionType
;
}
else
{
log
.
warn
(
'
unknown source Transaction type:
'
,
...
...
@@ -383,6 +385,42 @@ class AccountManager {
this
.
resyncAccountBalance
=
this
.
q
.
wrap
(
this
.
resyncAccountBalance
.
bind
(
this
));
}
async
getActualAccountBalances
(
userId
:
number
,
access
:
Access
,
oldAccounts
:
Account
[],
userActionFields
:
Record
<
string
,
string
>
|
null
):
Promise
<
{
accountId
:
number
;
balance
:
number
;
}[]
>
{
const
result
=
await
pollAccounts
(
GLOBAL_CONTEXT
,
userId
,
access
,
{
updateProvider
:
false
,
isInteractive
:
false
,
userActionFields
,
});
if
(
result
.
kind
===
'
user_action
'
)
{
return
[];
}
const
polledAccounts
=
result
.
value
;
const
diff
=
diffAccounts
(
oldAccounts
,
polledAccounts
);
const
results
=
[];
for
(
const
[
existing
,
polled
]
of
diff
.
perfectMatches
)
{
if
(
polled
.
balance
??
false
)
{
const
balance
=
polled
.
balance
??
0
;
// typescript doesn't understand the ?? operator
results
.
push
({
accountId
:
existing
.
id
,
balance
});
}
}
// Don't do anything with accounts that aren't perfect matches.
return
results
;
}
// Polls accounts from the providers and syncs them with those already
// known in the database.
async
syncAccounts
(
...
...
@@ -401,8 +439,11 @@ class AccountManager {
const
diff
=
diffAccounts
(
oldAccounts
,
accounts
);
for
(
const
[
known
]
of
diff
.
perfectMatches
)
{
for
(
const
[
known
,
polled
]
of
diff
.
perfectMatches
)
{
log
.
info
(
`Account
${
known
.
id
}
already known and in Kresus's database`
);
if
(
polled
.
balance
??
false
)
{
await
Account
.
update
(
userId
,
known
.
id
,
{
balance
:
polled
.
balance
});
}
}
const
accountInfoMap
:
Map
<
number
,
AccountInfo
>
=
new
Map
();
...
...
@@ -486,9 +527,9 @@ merging as per request`);
if
(
result
.
kind
===
'
user_action
'
)
{
return
result
;
}
let
opera
tions
=
result
.
value
;
let
transac
tions
=
result
.
value
;
log
.
info
(
'
Comparing with database to ignore already known
opera
tions…
'
);
log
.
info
(
'
Comparing with database to ignore already known
transac
tions…
'
);
let
toCreate
:
Partial
<
Transaction
>
[]
=
[];
let
toUpdate
:
{
known
:
Transaction
;
update
:
Partial
<
Transaction
>
}[]
=
[];
for
(
const
accountInfo
of
accountInfoMap
.
values
())
{
...
...
@@ -499,14 +540,14 @@ merging as per request`);
// `otherTransactions`.
const
providerTransactions
:
Partial
<
Transaction
>
[]
=
[];
const
otherTransactions
:
Partial
<
Transaction
>
[]
=
[];
for
(
const
op
of
opera
tions
)
{
for
(
const
op
of
transac
tions
)
{
if
(
op
.
accountId
===
account
.
id
)
{
providerTransactions
.
push
(
op
);
}
else
{
otherTransactions
.
push
(
op
);
}
}
opera
tions
=
otherTransactions
;
transac
tions
=
otherTransactions
;
if
(
!
providerTransactions
.
length
)
{
continue
;
...
...
@@ -552,7 +593,7 @@ merging as per request`);
toUpdate
=
toUpdate
.
concat
(
newToUpdate
);
toCreate
=
toCreate
.
concat
(
providerOrphans
,
newToCreate
);
// Resync balance only if we are sure that the
opera
tion is a new one.
// Resync balance only if we are sure that the
transac
tion is a new one.
// TODO can probably remove accountInfo/accountInfoMap when we sync
// the balance correctly.
const
accountImportDate
=
new
Date
(
account
.
importDate
);
...
...
@@ -563,7 +604,7 @@ merging as per request`);
.
reduce
((
sum
:
number
,
op
:
Partial
<
Transaction
>
)
=>
{
assert
(
typeof
op
.
amount
!==
'
undefined
'
,
'
opera
tion must have an amount at least
'
'
transac
tion must have an amount at least
'
);
return
sum
+
op
.
amount
;
},
0
);
...
...
@@ -581,8 +622,8 @@ merging as per request`);
if
(
toCreate
.
length
)
{
log
.
info
(
`
${
toCreate
.
length
}
new transactions found!`
);
log
.
info
(
'
Creating new transactions…
'
);
for
(
const
opera
tionToCreate
of
toCreate
)
{
const
created
=
await
Transaction
.
create
(
userId
,
opera
tionToCreate
);
for
(
const
transac
tionToCreate
of
toCreate
)
{
const
created
=
await
Transaction
.
create
(
userId
,
transac
tionToCreate
);
createdTransactions
.
push
(
created
);
}
log
.
info
(
'
Done.
'
);
...
...
@@ -598,12 +639,29 @@ merging as per request`);
log
.
info
(
'
Done.
'
);
}
let
balanceFixups
:
null
|
{
accountId
:
number
;
balance
:
number
}[]
=
null
;
if
(
pAccountInfoMap
===
null
)
{
// When pAccountInfoMap is null, we're only polling
// transactions; otherwise we've polled the accounts too and
// could perform a better balance merge.
log
.
info
(
'
Adjusting account balances...
'
);
balanceFixups
=
await
this
.
getActualAccountBalances
(
userId
,
access
,
allAccounts
.
slice
(),
userActionFields
);
}
const
accounts
:
Account
[]
=
[];
for
(
const
{
account
,
balanceOffset
}
of
accountInfoMap
.
values
())
{
const
accountUpdate
:
{
lastCheckDate
:
Date
;
initialBalance
?:
number
;
}
=
{
lastCheckDate
:
startOfPoll
};
balance
?:
number
;
}
=
{
lastCheckDate
:
startOfPoll
,
};
if
(
balanceOffset
!==
0
)
{
log
.
info
(
`Account
${
account
.
label
}
initial balance is going
...
...
@@ -611,16 +669,23 @@ to be resynced, by an offset of ${balanceOffset}.`);
accountUpdate
.
initialBalance
=
account
.
initialBalance
-
balanceOffset
;
}
if
(
balanceFixups
!==
null
)
{
const
found
=
balanceFixups
.
find
(
entry
=>
entry
.
accountId
===
account
.
id
);
if
(
found
)
{
accountUpdate
.
balance
=
found
.
balance
;
}
}
const
updated
=
await
Account
.
update
(
userId
,
account
.
id
,
accountUpdate
);
accounts
.
push
(
updated
);
}
if
(
toCreate
.
length
>
0
)
{
log
.
info
(
'
Checking alerts for accounts balance...
'
);
await
alertManager
.
checkAlertsForAccounts
(
userId
,
access
);
log
.
info
(
'
Checking alerts based on accounts balances...
'
);
await
alertManager
.
checkAlertsForAccounts
(
userId
,
access
);
log
.
info
(
'
Checking alerts for transactions amount...
'
);
await
alertManager
.
checkAlertsForOperations
(
userId
,
access
,
createdTransactions
);
if
(
createdTransactions
.
length
>
0
)
{
log
.
info
(
'
Checking alerts based on transactions amounts...
'
);
await
alertManager
.
checkAlertsForTransactions
(
userId
,
access
,
createdTransactions
);
}
await
Access
.
update
(
userId
,
access
.
id
,
{
fetchStatus
:
FETCH_STATUS_SUCCESS
});
...
...
@@ -653,25 +718,33 @@ to be resynced, by an offset of ${balanceOffset}.`);
// Ensure the account number is actually a string.
const
vendorAccountId
=
account
.
vendorAccountId
.
toString
();
const
f
oun
d
=
result
.
value
.
find
(
const
polledAcc
oun
t
=
result
.
value
.
find
(
(
acc
:
Partial
<
Account
>
)
=>
acc
.
vendorAccountId
===
vendorAccountId
);
if
(
typeof
f
oun
d
===
'
undefined
'
)
{
if
(
typeof
polledAcc
oun
t
===
'
undefined
'
)
{
// This case can happen if it's a known orphan.
throw
new
KError
(
'
account not found
'
,
404
);
}
const
kresusBalance
=
await
account
.
computeBalance
(
account
.
initialBalance
);
const
balanceDelta
=
(
found
.
initialBalance
??
0
)
-
kresusBalance
;
const
accountUpdate
:
Partial
<
Account
>
=
{};
const
knownComputedBalance
=
await
account
.
computeBalance
(
account
.
initialBalance
);
const
computedBalanceDelta
=
(
polledAccount
.
initialBalance
??
0
)
-
knownComputedBalance
;
if
(
Math
.
abs
(
computedBalanceDelta
)
>
0.001
)
{
log
.
info
(
`Updating initial balance for account
${
account
.
vendorAccountId
}
`
);
accountUpdate
.
initialBalance
=
account
.
initialBalance
+
computedBalanceDelta
;
}
const
currentBalanceDelta
=
(
polledAccount
.
balance
??
0
)
-
(
account
.
balance
as
number
);
if
(
Math
.
abs
(
currentBalanceDelta
)
>
0.001
)
{
log
.
info
(
`Updating real balance for
${
account
.
vendorAccountId
}
`
);
accountUpdate
.
balance
=
polledAccount
.
balance
;
}
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
,
balance
:
found
.
balance
,
});
// Only do the update if there's anything to update.
if
(
Object
.
keys
(
accountUpdate
).
length
>
0
)
{
const
updatedAccount
=
await
Account
.
update
(
userId
,
account
.
id
,
accountUpdate
);
return
{
kind
:
'
value
'
,
value
:
updatedAccount
,
...
...
server/lib/alert-manager.ts
View file @
e6275633
...
...
@@ -54,7 +54,7 @@ ${$t(i18n, 'server.email.signature')}
}
}
async
checkAlertsFor
Opera
tions
(
async
checkAlertsFor
Transac
tions
(
userId
:
number
,
access
:
Access
,
operations
:
Transaction
[]
...
...
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment