Newer
Older
206001
206002
206003
206004
206005
206006
206007
206008
206009
206010
206011
206012
206013
206014
206015
206016
206017
206018
206019
* @returns {boolean} Returns `true` if `value` is an array-like object,
* else `false`.
* @example
*
* _.isArrayLikeObject([1, 2, 3]);
* // => true
*
* _.isArrayLikeObject(document.body.children);
* // => true
*
* _.isArrayLikeObject('abc');
* // => false
*
* _.isArrayLikeObject(_.noop);
* // => false
*/
function isArrayLikeObject(value) {
return isObjectLike(value) && isArrayLike(value);
}
module.exports = isArrayLikeObject;
/***/ }),
/* 1610 */
/***/ (function(module, exports) {
/**
* Gets the value at `key`, unless `key` is "__proto__" or "constructor".
*
* @private
* @param {Object} object The object to query.
* @param {string} key The key of the property to get.
* @returns {*} Returns the property value.
*/
function safeGet(object, key) {
if (key === 'constructor' && typeof object[key] === 'function') {
return;
}
if (key == '__proto__') {
return;
}
return object[key];
}
module.exports = safeGet;
/***/ (function(module, exports, __webpack_require__) {
var copyObject = __webpack_require__(1446),
keysIn = __webpack_require__(1468);
206058
206059
206060
206061
206062
206063
206064
206065
206066
206067
206068
206069
206070
206071
206072
206073
206074
206075
206076
206077
206078
206079
206080
206081
206082
206083
206084
/**
* Converts `value` to a plain object flattening inherited enumerable string
* keyed properties of `value` to own properties of the plain object.
*
* @static
* @memberOf _
* @since 3.0.0
* @category Lang
* @param {*} value The value to convert.
* @returns {Object} Returns the converted plain object.
* @example
*
* function Foo() {
* this.b = 2;
* }
*
* Foo.prototype.c = 3;
*
* _.assign({ 'a': 1 }, new Foo);
* // => { 'a': 1, 'b': 2 }
*
* _.assign({ 'a': 1 }, _.toPlainObject(new Foo));
* // => { 'a': 1, 'b': 2, 'c': 3 }
*/
function toPlainObject(value) {
return copyObject(value, keysIn(value));
}
module.exports = toPlainObject;
/***/ }),
/* 1612 */
/***/ (function(module, exports, __webpack_require__) {
var baseRest = __webpack_require__(1588),
isIterateeCall = __webpack_require__(1589);
/**
* Creates a function like `_.assign`.
*
* @private
* @param {Function} assigner The function to assign values.
* @returns {Function} Returns the new assigner function.
*/
function createAssigner(assigner) {
return baseRest(function(object, sources) {
var index = -1,
length = sources.length,
customizer = length > 1 ? sources[length - 1] : undefined,
guard = length > 2 ? sources[2] : undefined;
customizer = (assigner.length > 3 && typeof customizer == 'function')
? (length--, customizer)
: undefined;
if (guard && isIterateeCall(sources[0], sources[1], guard)) {
customizer = length < 3 ? undefined : customizer;
length = 1;
}
object = Object(object);
while (++index < length) {
var source = sources[index];
if (source) {
assigner(object, source, index, customizer);
}
return object;
});
}
module.exports = createAssigner;
/***/ }),
/* 1613 */
/***/ (function(module, exports, __webpack_require__) {
const sortBy = __webpack_require__(1583)
const { eitherIncludes } = __webpack_require__(1614)
const { getSlugFromInstitutionLabel } = __webpack_require__(1615)
const findExactMatch = (attr, account, existingAccounts) => {
const sameAttr = existingAccounts.filter(
existingAccount => existingAccount[attr] === account[attr]
)
if (sameAttr.length === 1) {
return { match: sameAttr[0], method: attr + '-exact' }
} else if (sameAttr.length > 1) {
return { matches: sameAttr, method: attr + '-exact' }
} else {
return null
}
}
const untrimmedAccountNumber = /^(?:[A-Za-z]+)?-?([0-9]+)-?(?:[A-Za-z]+)?$/
const redactedCreditCard = /xxxx xxxx xxxx (\d{4})/
const normalizeAccountNumber = (numberArg, ibanArg) => {
const iban = ibanArg && ibanArg.replace(/\s/g, '')
const number =
numberArg && !numberArg.match(redactedCreditCard)
? numberArg.replace(/\s/g, '')
: numberArg
let match
if (iban && iban.length == 27) {
return iban.substr(14, 11)
}
if (!number) {
return number
}
206171
206172
206173
206174
206175
206176
206177
206178
206179
206180
206181
206182
206183
206184
206185
206186
206187
206188
206189
206190
206191
206192
206193
if (number.length == 23) {
// Must be an IBAN without the COUNTRY code
// See support demand #9102 with BI
// We extract the account number from the IBAN
// COUNTRY (4) BANK (5) COUNTER (5) NUMBER (11) KEY (2)
// FRXX 16275 10501 00300060030 00
return number.substr(10, 11)
} else if (number.length == 16) {
// Linxo sends Bank account number that contains
// the counter number
return number.substr(5, 11)
} else if (
number.length > 11 &&
(match = number.match(untrimmedAccountNumber))
) {
// Some account numbers from BI are in the form
// CC-00300060030 (CC for Compte Courant) or
// LEO-00300060030
return match[1]
} else {
return number
}
}
/**
* If either of the account numbers has length 11 and one is contained
* in the other, it's a match
*/
const approxNumberMatch = (account, existingAccount) => {
return (
existingAccount.number &&
account.number &&
(existingAccount.number.length === 11 || account.number.length === 11) &&
eitherIncludes(existingAccount.number, account.number) &&
Math.min(existingAccount.number.length, account.number.length) >= 4
)
}
206209
206210
206211
206212
206213
206214
206215
206216
206217
206218
206219
206220
206221
206222
206223
206224
206225
206226
const creditCardMatch = (account, existingAccount) => {
if (account.type !== 'CreditCard' && existingAccount.type !== 'CreditCard') {
return false
}
let ccAccount, lastDigits
for (let acc of [account, existingAccount]) {
const match = acc && acc.number && acc.number.match(redactedCreditCard)
if (match) {
ccAccount = acc
lastDigits = match[1]
}
}
const other = ccAccount === account ? existingAccount : account
if (other && other.number && other.number.slice(-4) === lastDigits) {
return true
}
return false
}
const slugMatch = (account, existingAccount) => {
const possibleSlug = getSlugFromInstitutionLabel(account.institutionLabel)
const possibleSlugExisting = getSlugFromInstitutionLabel(
existingAccount.institutionLabel
)
return (
!possibleSlug ||
!possibleSlugExisting ||
possibleSlug === possibleSlugExisting
)
}
const currencyMatch = (account, existingAccount) => {
if (!account.currency) {
return false
}
return (
(existingAccount.rawNumber &&
existingAccount.rawNumber.includes(account.currency)) ||
(existingAccount.label &&
existingAccount.label.includes(account.currency)) ||
(existingAccount.originalBankLabel &&
existingAccount.originalBankLabel.includes(account.currency))
)
}
const sameTypeMatch = (account, existingAccount) => {
return account.type === existingAccount.type
}
const rules = [
{ rule: slugMatch, bonus: 0, malus: -1000 },
{ rule: approxNumberMatch, bonus: 50, malus: -50, name: 'approx-number' },
{ rule: sameTypeMatch, bonus: 50, malus: 0, name: 'same-type' },
{ rule: creditCardMatch, bonus: 150, malus: 0, name: 'credit-card-number' },
{ rule: currencyMatch, bonus: 50, malus: 0, name: 'currency' }
]
const score = (account, existingAccount) => {
const methods = []
const res = {
account: existingAccount,
methods
}
let points = 0
for (let { rule, bonus, malus, name } of rules) {
const ok = rule(account, existingAccount)
if (ok && bonus) {
points += bonus
}
if (!ok && malus) {
points += malus
}
if (name && ok) {
methods.push(name)
}
}
res.points = points
return res
}
const normalizeAccount = account => {
const normalizedAccountNumber = normalizeAccountNumber(
account.number,
account.iban
)
return {
...account,
rawNumber: account.number,
number: normalizedAccountNumber
}
}
const exactMatchAttributes = ['iban', 'number']
const eqNotUndefined = (attr1, attr2) => {
return attr1 && attr1 === attr2
}
const findMatch = (account, existingAccounts) => {
// Start with exact attribute matches
for (const exactAttribute of exactMatchAttributes) {
if (account[exactAttribute]) {
const result = findExactMatch(exactAttribute, account, existingAccounts)
if (result && result.match) {
return result
const matchOriginalNumber = existingAccounts.find(
otherAccount =>
eqNotUndefined(account.originalNumber, otherAccount.number) ||
eqNotUndefined(account.number, otherAccount.originalNumber)
)
if (matchOriginalNumber) {
return {
match: matchOriginalNumber,
method: 'originalNumber-exact'
}
}
const matchRawNumberCurrencyType = existingAccounts.find(
otherAccount =>
(eqNotUndefined(account.rawNumber, otherAccount.number) ||
eqNotUndefined(account.number, otherAccount.rawNumber)) &&
otherAccount.type == account.type &&
otherAccount.currency == account.currency
)
if (matchRawNumberCurrencyType) {
return {
match: matchRawNumberCurrencyType,
method: 'rawNumber-exact-currency-type'
}
}
// Now we get more fuzzy and score accounts
const scored = sortBy(
existingAccounts.map(existingAccount => score(account, existingAccount)),
x => -x.points
)
const candidates = scored.filter(x => x.points > 0)
if (candidates.length > 0) {
return {
match: candidates[0].account,
method: candidates[0].methods.join('-')
}
206360
206361
206362
206363
206364
206365
206366
206367
206368
206369
206370
206371
206372
206373
206374
206375
206376
206377
206378
206379
206380
206381
206382
206383
206384
206385
206386
/**
* Matches existing accounts with accounts fetched on a vendor
*
* @typedef {MatchResult}
* @property {io.cozy.account} account - Account from fetched accounts
* @property {io.cozy.account} match - Existing account that was matched. Null if no match was found.
* @property {string} method - How the two accounts were matched
*
* @param {io.cozy.account} fetchedAccounts - Account that have been fetched
* on the vendor and that will be matched with existing accounts
* @param {io.cozy.accounts} existingAccounts - Will be match against (those
* io.cozy.accounts already have an _id)
* @return {Array<MatchResult>} - Match results (as many results as fetchedAccounts.length)
*/
const matchAccounts = (fetchedAccountsArg, existingAccounts) => {
const fetchedAccounts = fetchedAccountsArg.map(normalizeAccount)
const toMatch = [...existingAccounts].map(normalizeAccount)
const results = []
for (let fetchedAccount of fetchedAccounts) {
const matchResult = findMatch(fetchedAccount, toMatch)
if (matchResult) {
const i = toMatch.indexOf(matchResult.match)
toMatch.splice(i, 1)
results.push({ account: fetchedAccount, ...matchResult })
} else {
results.push({ account: fetchedAccount })
}
module.exports = {
matchAccounts,
normalizeAccountNumber,
score,
creditCardMatch,
approxNumberMatch
}
/* 1614 */
/***/ (function(module, exports) {
const eitherIncludes = (str1, str2) => {
return Boolean(str1 && str2 && (str1.includes(str2) || str2.includes(str1)))
}
module.exports = {
eitherIncludes
}
/***/ (function(module, exports, __webpack_require__) {
const log = __webpack_require__(2).namespace('slug-account')
const labelSlugs = __webpack_require__(1616)
const institutionLabelsCompiled = Object.entries(labelSlugs).map(
([ilabelRx, slug]) => {
if (ilabelRx[0] === '/' && ilabelRx[ilabelRx.length - 1] === '/') {
return [new RegExp(ilabelRx.substr(1, ilabelRx.length - 2), 'i'), slug]
} else {
return [ilabelRx, slug]
}
}
)
206430
206431
206432
206433
206434
206435
206436
206437
206438
206439
206440
206441
206442
206443
206444
206445
206446
const getSlugFromInstitutionLabel = institutionLabel => {
if (!institutionLabel) {
log('warn', 'No institution label, cannot compute slug')
return
}
for (const [rx, slug] of institutionLabelsCompiled) {
if (rx instanceof RegExp) {
const match = institutionLabel.match(rx)
if (match) {
return slug
}
} else if (rx.toLowerCase() === institutionLabel.toLowerCase()) {
return slug
}
}
log('warn', `Could not compute slug for ${institutionLabel}`)
}
module.exports = {
getSlugFromInstitutionLabel
}
/***/ }),
/* 1616 */
/***/ (function(module, exports) {
206457
206458
206459
206460
206461
206462
206463
206464
206465
206466
206467
206468
206469
206470
206471
206472
206473
206474
206475
206476
206477
206478
206479
206480
206481
206482
206483
206484
206485
206486
206487
206488
206489
module.exports = {
'AXA Banque': 'axabanque102',
'/Banque Populaire.*/': 'banquepopulaire',
BforBank: 'bforbank97',
'BNP Paribas': 'bnpparibas82',
BNPP: 'bnpparibas82',
'/Boursorama.*/': 'boursorama83',
casden: 'casden173',
'/Hello bank!.*/': 'hellobank145',
Bred: 'bred',
CA: 'caatlantica3',
'Carrefour Banque': 'carrefour159',
"/Caisse d'Épargne.*/": 'caissedepargne1',
'Compte Nickel': 'comptenickel168',
'/^CIC.*/': 'cic63',
'Crédit Agricole': 'caatlantica3',
'Crédit Coopératif': 'creditcooperatif148',
'/Crédit du Nord.*/': 'cdngroup88',
'/Crédit Maritime.*/': 'creditmaritime',
'/Crédit Mutuel.*/': 'cic45',
'/Linxea/': 'linxea',
Fortuneo: 'fortuneo84',
'Hello bank!': 'hellobank145',
'HSBC France': 'hsbc119',
HSBC: 'hsbc119',
'/^ING.*/': 'ingdirect95',
'/La Banque Postale.*/': 'labanquepostale44',
'/LCL.*/': 'lcl-linxo',
Milleis: 'barclays136',
Monabanq: 'monabanq96',
'Société Générale': 'societegenerale',
'Société marseillaise de crédit': 'cdngroup109'
}
/***/ }),
/* 1617 */
/***/ (function(module, exports, __webpack_require__) {
const fromPairs = __webpack_require__(1570)
const log = __webpack_require__(2).namespace('BankingReconciliator')
class BankingReconciliator {
constructor(options) {
this.options = options
async saveAccounts(fetchedAccounts, options) {
const { BankAccount } = this.options
const stackAccounts = await BankAccount.fetchAll()
// Reconciliate
const reconciliatedAccounts = BankAccount.reconciliate(
fetchedAccounts,
stackAccounts
)
log('info', 'Saving accounts...')
const savedAccounts = await BankAccount.bulkSave(reconciliatedAccounts, {
handleDuplicates: 'remove'
})
if (options.onAccountsSaved) {
options.onAccountsSaved(savedAccounts)
}
return { savedAccounts, reconciliatedAccounts }
/**
* @typedef ReconciliatorResponse
* @attribute {Array<BankAccount>} accounts
* @attribute {Array<BankTransactions>} transactions
*/
/**
* @typedef ReconciliatorSaveOptions
* @attribute {Function} logProgress
*/
/**
* Save new accounts and transactions
*
* @param {Array<BankAccount>} fetchedAccounts
* @param {Array<BankTransactions>} fetchedTransactions
* @param {ReconciliatorSaveOptions} options
* @returns {ReconciliatorResponse}
*
*/
async save(fetchedAccounts, fetchedTransactions, options = {}) {
const { BankAccount, BankTransaction } = this.options
const { reconciliatedAccounts, savedAccounts } = await this.saveAccounts(
fetchedAccounts,
options
)
// Bank accounts saved in Cozy, we can now link transactions to accounts
// via their cozy id
const vendorIdToCozyId = fromPairs(
savedAccounts.map(acc => [acc[BankAccount.vendorIdAttr], acc._id])
)
log('info', 'Linking transactions to accounts...')
log('info', JSON.stringify(vendorIdToCozyId))
fetchedTransactions.forEach(tr => {
tr.account = vendorIdToCozyId[tr[BankTransaction.vendorAccountIdAttr]]
if (tr.account === undefined) {
log(
'warn',
`Transaction without account, vendorAccountIdAttr: ${BankTransaction.vendorAccountIdAttr}`
)
log('warn', 'transaction: ' + JSON.stringify(tr))
throw new Error('Transaction without account.')
}
})
const reconciliatedAccountIds = new Set(
reconciliatedAccounts.filter(acc => acc._id).map(acc => acc._id)
)
// Pass to transaction reconciliation only transactions that belong
// to one of the reconciliated accounts
const stackTransactions = (await BankTransaction.fetchAll()).filter(
transaction => reconciliatedAccountIds.has(transaction.account)
)
const transactions = BankTransaction.reconciliate(
fetchedTransactions,
stackTransactions,
options
)
206590
206591
206592
206593
206594
206595
206596
206597
206598
206599
206600
206601
206602
206603
206604
206605
log('info', 'Saving transactions...')
let i = 1
const logProgressFn = doc => {
log('debug', `[bulkSave] ${i++} Saving ${doc.date} ${doc.label}`)
}
const savedTransactions = await BankTransaction.bulkSave(transactions, {
concurrency: 30,
logProgress:
options.logProgress !== undefined ? options.logProgress : logProgressFn,
handleDuplicates: 'remove'
})
return {
accounts: savedAccounts,
transactions: savedTransactions
}
}
module.exports = BankingReconciliator
/* 1618 */
/***/ (function(module, exports, __webpack_require__) {
const keyBy = __webpack_require__(1619)
const groupBy = __webpack_require__(1579)
const maxBy = __webpack_require__(1620)
const addDays = __webpack_require__(1623)
const isAfter = __webpack_require__(1627)
const Document = __webpack_require__(1393)
const log = __webpack_require__(1601)
const BankAccount = __webpack_require__(1604)
const { matchTransactions } = __webpack_require__(1628)
const maxValue = (iterable, fn) => {
const res = maxBy(iterable, fn)
return res ? fn(res) : null
}
const getDate = transaction => {
const date = transaction.realisationDate || transaction.date
return date.slice(0, 10)
}
/**
* Get the date of the latest transaction in an array.
* Transactions in the future are ignored.
*
* @param {array} stackTransactions
* @returns {string} The date of the latest transaction (YYYY-MM-DD)
*/
const getSplitDate = stackTransactions => {
const now = new Date()
const notFutureTransactions = stackTransactions.filter(transaction => {
const date = getDate(transaction)
return !isAfter(date, now)
})
return maxValue(notFutureTransactions, getDate)
}
const ensureISOString = date => {
if (date instanceof Date) {
return date.toISOString()
} else {
return date
}
}
class Transaction extends Document {
static getDate(transaction) {
return transaction
}
206665
206666
206667
206668
206669
206670
206671
206672
206673
206674
206675
206676
206677
206678
206679
206680
206681
isAfter(minDate) {
if (!minDate) {
return true
} else {
const day = ensureISOString(this.date).slice(0, 10)
if (day !== 'NaN') {
return day > minDate
} else {
log(
'warn',
'transaction date could not be parsed. transaction: ' +
JSON.stringify(this)
)
return false
}
}
}
isBeforeOrSame(maxDate) {
if (!maxDate) {
return true
} else {
const day = ensureISOString(this.date).slice(0, 10)
if (day !== 'NaN') {
return day <= maxDate
} else {
log(
'warn',
'transaction date could not be parsed. transaction: ' +
JSON.stringify(this)
)
return false
/**
* Get the descriptive (and almost uniq) identifier of a transaction
* @param {object} transaction - The transaction (containing at least amount, originalBankLabel and date)
* @returns {object}
*/
getIdentifier() {
return `${this.amount}-${this.originalBankLabel}-${this.date}`
}
206710
206711
206712
206713
206714
206715
206716
206717
206718
206719
206720
206721
206722
206723
206724
206725
206726
206727
206728
206729
206730
206731
/**
* Get transactions that should be present in the stack but are not.
* Transactions that are older that 1 week before the oldest existing
* transaction are ignored.
*
* @param {array} newTransactions
* @param {array} stackTransactions
* @returns {array}
*/
static getMissedTransactions(
newTransactions,
stackTransactions,
options = {}
) {
const oldestDate = maxValue(stackTransactions, getDate)
const frontierDate = addDays(oldestDate, -7)
const recentNewTransactions = newTransactions.filter(tr =>
isAfter(getDate(tr), frontierDate)
)
const matchingResults = Array.from(
matchTransactions(recentNewTransactions, stackTransactions)
)
const missedTransactions = matchingResults
.filter(result => !result.match)
.map(result => result.transaction)
206737
206738
206739
206740
206741
206742
206743
206744
206745
206746
206747
206748
206749
206750
206751
206752
206753
206754
206755
const trackEvent = options.trackEvent
if (typeof trackEvent === 'function') {
try {
const nbMissed = missedTransactions.length
const nbExisting = stackTransactions.length
trackEvent({
e_a: 'ReconciliateMissing',
e_n: 'MissedTransactionPct',
e_v: parseFloat((nbMissed / nbExisting).toFixed(2), 10)
})
trackEvent({
e_a: 'ReconciliateMissing',
e_n: 'MissedTransactionAbs',
e_v: nbMissed
})
} catch (e) {
log('warn', `Could not send MissedTransaction event: ${e.message}`)
}
}
206757
206758
206759
206760
206761
206762
206763
206764
206765
206766
206767
206768
206769
206770
206771
206772
206773
206774
206775
206776
206777
return missedTransactions
}
static reconciliate(remoteTransactions, localTransactions, options = {}) {
const findByVendorId = transaction =>
localTransactions.find(t => t.vendorId === transaction.vendorId)
const groups = groupBy(remoteTransactions, transaction =>
findByVendorId(transaction) ? 'updatedTransactions' : 'newTransactions'
)
let newTransactions = groups.newTransactions || []
const updatedTransactions = groups.updatedTransactions || []
const splitDate = getSplitDate(localTransactions)
if (splitDate) {
if (typeof options.trackEvent === 'function') {
options.trackEvent({
e_a: 'ReconciliateSplitDate'
})
const isAfterSplit = x => Transaction.prototype.isAfter.call(x, splitDate)
const isBeforeSplit = x =>
Transaction.prototype.isBeforeOrSame.call(x, splitDate)
const transactionsAfterSplit = newTransactions.filter(isAfterSplit)
if (transactionsAfterSplit.length > 0) {
log(
'info',
`Found ${transactionsAfterSplit.length} transactions after ${splitDate}`
)
} else {
log('info', `No transaction after ${splitDate}`)
const transactionsBeforeSplit = newTransactions.filter(isBeforeSplit)
log(
'info',
`Found ${transactionsBeforeSplit.length} transactions before ${splitDate}`
)
const missedTransactions = Transaction.getMissedTransactions(
transactionsBeforeSplit,
localTransactions,
options
)
if (missedTransactions.length > 0) {
log(
'info',
`Found ${missedTransactions.length} missed transactions before ${splitDate}`
)
} else {
log('info', `No missed transactions before ${splitDate}`)
newTransactions = [...transactionsAfterSplit, ...missedTransactions]
} else {
log('info', "Can't find a split date, saving all new transactions")
log(
'info',
`Transaction reconciliation: new ${newTransactions.length}, updated ${updatedTransactions.length}, split date ${splitDate} `
)
return [].concat(newTransactions).concat(updatedTransactions)
}
static async getMostRecentForAccounts(accountIds) {
try {
log('debug', 'Transaction.getLast')
const index = await Document.getIndex(this.doctype, ['date', 'account'])
const options = {
selector: {
date: { $gte: null },
account: {
$in: accountIds
}
},
sort: [{ date: 'desc' }]
}
const transactions = await Document.query(index, options)
log('info', 'last transactions length: ' + transactions.length)
return transactions
} catch (e) {
log('error', e)
static async deleteOrphans() {
log('info', 'Deleting up orphan operations')
const accounts = keyBy(await BankAccount.fetchAll(), '_id')
const operations = await this.fetchAll()
const orphanOperations = operations.filter(x => !accounts[x.account])
log('info', `Total number of operations: ${operations.length}`)
log('info', `Total number of orphan operations: ${orphanOperations.length}`)
log('info', `Deleting ${orphanOperations.length} orphan operations...`)
if (orphanOperations.length > 0) {
return this.deleteAll(orphanOperations)
getVendorAccountId() {
return this[this.constructor.vendorAccountIdAttr]
}
static getCategoryId(transaction, options) {
const opts = {
localModelOverride: false,
localModelUsageThreshold: this.LOCAL_MODEL_USAGE_THRESHOLD,
globalModelUsageThreshold: this.GLOBAL_MODEL_USAGE_THRESHOLD,
...options
}
if (transaction.manualCategoryId) {
return transaction.manualCategoryId
}
if (
opts.localModelOverride &&
transaction.localCategoryId &&
transaction.localCategoryProba &&
transaction.localCategoryProba > opts.localModelUsageThreshold
) {
return transaction.localCategoryId
}
if (
transaction.cozyCategoryId &&
transaction.cozyCategoryProba &&
transaction.cozyCategoryProba > opts.globalModelUsageThreshold
) {
return transaction.cozyCategoryId
}
// If the cozy categorization models have not been applied, we return null
// so the transaction is considered as « categorization in progress ».
// Otherwize we just use the automatic categorization from the vendor
if (!transaction.localCategoryId && !transaction.cozyCategoryId) {
return null
}
return transaction.automaticCategoryId
}
206909
206910
206911
206912
206913
206914
206915
206916
206917
206918
206919
206920
206921
206922
206923
206924
Transaction.doctype = 'io.cozy.bank.operations'
Transaction.version = 1
Transaction.vendorAccountIdAttr = 'vendorAccountId'
Transaction.vendorIdAttr = 'vendorId'
Transaction.idAttributes = ['vendorId']
Transaction.checkedAttributes = [
'label',
'originalBankLabel',
'automaticCategoryId',
'account'
]
Transaction.LOCAL_MODEL_USAGE_THRESHOLD = 0.8
Transaction.GLOBAL_MODEL_USAGE_THRESHOLD = 0.15
Transaction.getSplitDate = getSplitDate
module.exports = Transaction
/***/ (function(module, exports, __webpack_require__) {
var baseAssignValue = __webpack_require__(1443),
createAggregator = __webpack_require__(1580);
206934
206935
206936
206937
206938
206939
206940
206941
206942
206943
206944
206945
206946
206947
206948
206949
206950
206951
206952
206953
206954
206955
206956
206957
206958
206959
206960
206961
206962
206963
206964
/**
* Creates an object composed of keys generated from the results of running
* each element of `collection` thru `iteratee`. The corresponding value of
* each key is the last element responsible for generating the key. The
* iteratee is invoked with one argument: (value).
*
* @static
* @memberOf _
* @since 4.0.0
* @category Collection
* @param {Array|Object} collection The collection to iterate over.
* @param {Function} [iteratee=_.identity] The iteratee to transform keys.
* @returns {Object} Returns the composed aggregate object.
* @example
*
* var array = [
* { 'dir': 'left', 'code': 97 },
* { 'dir': 'right', 'code': 100 }
* ];
*
* _.keyBy(array, function(o) {
* return String.fromCharCode(o.code);
* });
* // => { 'a': { 'dir': 'left', 'code': 97 }, 'd': { 'dir': 'right', 'code': 100 } }
*
* _.keyBy(array, 'dir');
* // => { 'left': { 'dir': 'left', 'code': 97 }, 'right': { 'dir': 'right', 'code': 100 } }
*/
var keyBy = createAggregator(function(result, value, key) {
baseAssignValue(result, key, value);
});
/***/ }),
/* 1620 */
/***/ (function(module, exports, __webpack_require__) {
var baseExtremum = __webpack_require__(1621),
baseGt = __webpack_require__(1622),
baseIteratee = __webpack_require__(1545);
206977
206978
206979
206980
206981
206982
206983
206984
206985
206986
206987
206988
206989
206990
206991
206992
206993
206994
206995
206996
206997
206998
206999
207000
/**
* This method is like `_.max` except that it accepts `iteratee` which is
* invoked for each element in `array` to generate the criterion by which
* the value is ranked. The iteratee is invoked with one argument: (value).
*
* @static
* @memberOf _
* @since 4.0.0
* @category Math
* @param {Array} array The array to iterate over.
* @param {Function} [iteratee=_.identity] The iteratee invoked per element.
* @returns {*} Returns the maximum value.
* @example
*
* var objects = [{ 'n': 1 }, { 'n': 2 }];
*
* _.maxBy(objects, function(o) { return o.n; });
* // => { 'n': 2 }
*
* // The `_.property` iteratee shorthand.
* _.maxBy(objects, 'n');
* // => { 'n': 2 }
*/
function maxBy(array, iteratee) {