Skip to content
Snippets Groups Projects
index.js 8.75 MiB
Newer Older
  • Learn to ignore specific revisions
  •   return (array && array.length)
        ? baseExtremum(array, baseIteratee(iteratee, 2), baseGt)
        : undefined;
    
    Romain CREY's avatar
    Romain CREY committed
    }
    
    
    module.exports = maxBy;
    
    Romain CREY's avatar
    Romain CREY committed
    
    
    /***/ }),
    
    Romain CREY's avatar
    Romain CREY committed
    /***/ (function(module, exports, __webpack_require__) {
    
    
    var isSymbol = __webpack_require__(1506);
    
    Romain CREY's avatar
    Romain CREY committed
    
    
    /**
     * The base implementation of methods like `_.max` and `_.min` which accepts a
     * `comparator` to determine the extremum value.
     *
     * @private
     * @param {Array} array The array to iterate over.
     * @param {Function} iteratee The iteratee invoked per iteration.
     * @param {Function} comparator The comparator used to compare values.
     * @returns {*} Returns the extremum value.
     */
    function baseExtremum(array, iteratee, comparator) {
      var index = -1,
          length = array.length;
    
    Romain CREY's avatar
    Romain CREY committed
    
    
      while (++index < length) {
        var value = array[index],
            current = iteratee(value);
    
    Romain CREY's avatar
    Romain CREY committed
    
    
        if (current != null && (computed === undefined
              ? (current === current && !isSymbol(current))
              : comparator(current, computed)
            )) {
          var computed = current,
              result = value;
        }
      }
      return result;
    }
    
    Romain CREY's avatar
    Romain CREY committed
    
    
    module.exports = baseExtremum;
    
    /***/ }),
    /* 1622 */
    /***/ (function(module, exports) {
    
    Romain CREY's avatar
    Romain CREY committed
    
    
    /**
     * The base implementation of `_.gt` which doesn't coerce arguments.
     *
     * @private
     * @param {*} value The value to compare.
     * @param {*} other The other value to compare.
     * @returns {boolean} Returns `true` if `value` is greater than `other`,
     *  else `false`.
     */
    function baseGt(value, other) {
      return value > other;
    }
    
    Romain CREY's avatar
    Romain CREY committed
    
    
    module.exports = baseGt;
    
    /***/ }),
    /* 1623 */
    /***/ (function(module, exports, __webpack_require__) {
    
    Romain CREY's avatar
    Romain CREY committed
    
    
    var parse = __webpack_require__(1624)
    
    Romain CREY's avatar
    Romain CREY committed
    
    
    /**
     * @category Day Helpers
     * @summary Add the specified number of days to the given date.
     *
     * @description
     * Add the specified number of days to the given date.
     *
     * @param {Date|String|Number} date - the date to be changed
     * @param {Number} amount - the amount of days to be added
     * @returns {Date} the new date with the days added
     *
     * @example
     * // Add 10 days to 1 September 2014:
     * var result = addDays(new Date(2014, 8, 1), 10)
     * //=> Thu Sep 11 2014 00:00:00
     */
    function addDays (dirtyDate, dirtyAmount) {
      var date = parse(dirtyDate)
      var amount = Number(dirtyAmount)
      date.setDate(date.getDate() + amount)
      return date
    }
    
    module.exports = addDays
    
    Romain CREY's avatar
    Romain CREY committed
    
    
    /***/ }),
    
    Romain CREY's avatar
    Romain CREY committed
    /***/ (function(module, exports, __webpack_require__) {
    
    
    var getTimezoneOffsetInMilliseconds = __webpack_require__(1625)
    var isDate = __webpack_require__(1626)
    
    Romain CREY's avatar
    Romain CREY committed
    
    
    var MILLISECONDS_IN_HOUR = 3600000
    var MILLISECONDS_IN_MINUTE = 60000
    var DEFAULT_ADDITIONAL_DIGITS = 2
    
    Romain CREY's avatar
    Romain CREY committed
    
    
    var parseTokenDateTimeDelimeter = /[T ]/
    var parseTokenPlainTime = /:/
    
    Romain CREY's avatar
    Romain CREY committed
    
    
    // year tokens
    var parseTokenYY = /^(\d{2})$/
    var parseTokensYYY = [
      /^([+-]\d{2})$/, // 0 additional digits
      /^([+-]\d{3})$/, // 1 additional digit
      /^([+-]\d{4})$/ // 2 additional digits
    ]
    
    Romain CREY's avatar
    Romain CREY committed
    
    
    var parseTokenYYYY = /^(\d{4})/
    var parseTokensYYYYY = [
      /^([+-]\d{4})/, // 0 additional digits
      /^([+-]\d{5})/, // 1 additional digit
      /^([+-]\d{6})/ // 2 additional digits
    ]
    
    Romain CREY's avatar
    Romain CREY committed
    
    
    // date tokens
    var parseTokenMM = /^-(\d{2})$/
    var parseTokenDDD = /^-?(\d{3})$/
    var parseTokenMMDD = /^-?(\d{2})-?(\d{2})$/
    var parseTokenWww = /^-?W(\d{2})$/
    var parseTokenWwwD = /^-?W(\d{2})-?(\d{1})$/
    
    Romain CREY's avatar
    Romain CREY committed
    
    
    // time tokens
    var parseTokenHH = /^(\d{2}([.,]\d*)?)$/
    var parseTokenHHMM = /^(\d{2}):?(\d{2}([.,]\d*)?)$/
    var parseTokenHHMMSS = /^(\d{2}):?(\d{2}):?(\d{2}([.,]\d*)?)$/
    
    Romain CREY's avatar
    Romain CREY committed
    
    
    // timezone tokens
    var parseTokenTimezone = /([Z+-].*)$/
    var parseTokenTimezoneZ = /^(Z)$/
    var parseTokenTimezoneHH = /^([+-])(\d{2})$/
    var parseTokenTimezoneHHMM = /^([+-])(\d{2}):?(\d{2})$/
    
    Romain CREY's avatar
    Romain CREY committed
    
    
    /**
     * @category Common Helpers
     * @summary Convert the given argument to an instance of Date.
     *
     * @description
     * Convert the given argument to an instance of Date.
     *
     * If the argument is an instance of Date, the function returns its clone.
     *
     * If the argument is a number, it is treated as a timestamp.
     *
     * If an argument is a string, the function tries to parse it.
     * Function accepts complete ISO 8601 formats as well as partial implementations.
     * ISO 8601: http://en.wikipedia.org/wiki/ISO_8601
     *
     * If all above fails, the function passes the given argument to Date constructor.
     *
     * @param {Date|String|Number} argument - the value to convert
     * @param {Object} [options] - the object with options
     * @param {0 | 1 | 2} [options.additionalDigits=2] - the additional number of digits in the extended year format
     * @returns {Date} the parsed date in the local time zone
     *
     * @example
     * // Convert string '2014-02-11T11:30:30' to date:
     * var result = parse('2014-02-11T11:30:30')
     * //=> Tue Feb 11 2014 11:30:30
     *
     * @example
     * // Parse string '+02014101',
     * // if the additional number of digits in the extended year format is 1:
     * var result = parse('+02014101', {additionalDigits: 1})
     * //=> Fri Apr 11 2014 00:00:00
     */
    function parse (argument, dirtyOptions) {
      if (isDate(argument)) {
        // Prevent the date to lose the milliseconds when passed to new Date() in IE10
        return new Date(argument.getTime())
      } else if (typeof argument !== 'string') {
        return new Date(argument)
      }
    
    Romain CREY's avatar
    Romain CREY committed
    
    
      var options = dirtyOptions || {}
      var additionalDigits = options.additionalDigits
      if (additionalDigits == null) {
        additionalDigits = DEFAULT_ADDITIONAL_DIGITS
      } else {
        additionalDigits = Number(additionalDigits)
      }
    
    Romain CREY's avatar
    Romain CREY committed
    
    
      var dateStrings = splitDateString(argument)
    
    Romain CREY's avatar
    Romain CREY committed
    
    
      var parseYearResult = parseYear(dateStrings.date, additionalDigits)
      var year = parseYearResult.year
      var restDateString = parseYearResult.restDateString
    
    Romain CREY's avatar
    Romain CREY committed
    
    
      var date = parseDate(restDateString, year)
    
    Romain CREY's avatar
    Romain CREY committed
    
    
      if (date) {
        var timestamp = date.getTime()
        var time = 0
        var offset
    
    Romain CREY's avatar
    Romain CREY committed
    
    
        if (dateStrings.time) {
          time = parseTime(dateStrings.time)
        }
    
    Romain CREY's avatar
    Romain CREY committed
    
    
        if (dateStrings.timezone) {
          offset = parseTimezone(dateStrings.timezone) * MILLISECONDS_IN_MINUTE
        } else {
          var fullTime = timestamp + time
          var fullTimeDate = new Date(fullTime)
    
    Romain CREY's avatar
    Romain CREY committed
    
    
          offset = getTimezoneOffsetInMilliseconds(fullTimeDate)
    
    Romain CREY's avatar
    Romain CREY committed
    
    
          // Adjust time when it's coming from DST
          var fullTimeDateNextDay = new Date(fullTime)
          fullTimeDateNextDay.setDate(fullTimeDate.getDate() + 1)
          var offsetDiff =
            getTimezoneOffsetInMilliseconds(fullTimeDateNextDay) -
            getTimezoneOffsetInMilliseconds(fullTimeDate)
          if (offsetDiff > 0) {
            offset += offsetDiff
          }
        }
    
    Romain CREY's avatar
    Romain CREY committed
    
    
        return new Date(timestamp + time + offset)
      } else {
        return new Date(argument)
      }
    }
    
    Romain CREY's avatar
    Romain CREY committed
    
    
    function splitDateString (dateString) {
      var dateStrings = {}
      var array = dateString.split(parseTokenDateTimeDelimeter)
      var timeString
    
    Romain CREY's avatar
    Romain CREY committed
    
    
      if (parseTokenPlainTime.test(array[0])) {
        dateStrings.date = null
        timeString = array[0]
      } else {
        dateStrings.date = array[0]
        timeString = array[1]
      }
    
    Romain CREY's avatar
    Romain CREY committed
    
    
      if (timeString) {
        var token = parseTokenTimezone.exec(timeString)
        if (token) {
          dateStrings.time = timeString.replace(token[1], '')
          dateStrings.timezone = token[1]
        } else {
          dateStrings.time = timeString
        }
      }
    
    Romain CREY's avatar
    Romain CREY committed
    
    
    Romain CREY's avatar
    Romain CREY committed
    
    
    function parseYear (dateString, additionalDigits) {
      var parseTokenYYY = parseTokensYYY[additionalDigits]
      var parseTokenYYYYY = parseTokensYYYYY[additionalDigits]
    
    Romain CREY's avatar
    Romain CREY committed
    
    
    Romain CREY's avatar
    Romain CREY committed
    
    
      // YYYY or ±YYYYY
      token = parseTokenYYYY.exec(dateString) || parseTokenYYYYY.exec(dateString)
      if (token) {
        var yearString = token[1]
        return {
          year: parseInt(yearString, 10),
          restDateString: dateString.slice(yearString.length)
        }
      }
    
    Romain CREY's avatar
    Romain CREY committed
    
    
      // YY or ±YYY
      token = parseTokenYY.exec(dateString) || parseTokenYYY.exec(dateString)
      if (token) {
        var centuryString = token[1]
        return {
          year: parseInt(centuryString, 10) * 100,
          restDateString: dateString.slice(centuryString.length)
        }
      }
    
    Romain CREY's avatar
    Romain CREY committed
    
    
      // Invalid ISO-formatted year
      return {
        year: null
      }
    }
    
    Romain CREY's avatar
    Romain CREY committed
    
    
    function parseDate (dateString, year) {
      // Invalid ISO-formatted year
      if (year === null) {
        return null
      }
    
    Romain CREY's avatar
    Romain CREY committed
    
    
      var token
      var date
      var month
      var week
    
    Romain CREY's avatar
    Romain CREY committed
    
    
      // YYYY
      if (dateString.length === 0) {
        date = new Date(0)
        date.setUTCFullYear(year)
        return date
    
    Romain CREY's avatar
    Romain CREY committed
      }
    
    
      // YYYY-MM
      token = parseTokenMM.exec(dateString)
      if (token) {
        date = new Date(0)
        month = parseInt(token[1], 10) - 1
        date.setUTCFullYear(year, month)
        return date
    
    Romain CREY's avatar
    Romain CREY committed
      }
    
    
      // YYYY-DDD or YYYYDDD
      token = parseTokenDDD.exec(dateString)
      if (token) {
        date = new Date(0)
        var dayOfYear = parseInt(token[1], 10)
        date.setUTCFullYear(year, 0, dayOfYear)
        return date
    
    Romain CREY's avatar
    Romain CREY committed
      }
    
    
      // YYYY-MM-DD or YYYYMMDD
      token = parseTokenMMDD.exec(dateString)
      if (token) {
        date = new Date(0)
        month = parseInt(token[1], 10) - 1
        var day = parseInt(token[2], 10)
        date.setUTCFullYear(year, month, day)
        return date
    
    Romain CREY's avatar
    Romain CREY committed
      }
    
    
      // YYYY-Www or YYYYWww
      token = parseTokenWww.exec(dateString)
      if (token) {
        week = parseInt(token[1], 10) - 1
        return dayOfISOYear(year, week)
    
    Romain CREY's avatar
    Romain CREY committed
      }
    
    
      // YYYY-Www-D or YYYYWwwD
      token = parseTokenWwwD.exec(dateString)
      if (token) {
        week = parseInt(token[1], 10) - 1
        var dayOfWeek = parseInt(token[2], 10) - 1
        return dayOfISOYear(year, week, dayOfWeek)
      }
    
    Romain CREY's avatar
    Romain CREY committed
    
    
      // Invalid ISO-formatted date
      return null
    }
    
    Romain CREY's avatar
    Romain CREY committed
    
    
    function parseTime (timeString) {
      var token
      var hours
      var minutes
    
      // hh
      token = parseTokenHH.exec(timeString)
      if (token) {
        hours = parseFloat(token[1].replace(',', '.'))
        return (hours % 24) * MILLISECONDS_IN_HOUR
    
    Romain CREY's avatar
    Romain CREY committed
      }
    
    
      // hh:mm or hhmm
      token = parseTokenHHMM.exec(timeString)
      if (token) {
        hours = parseInt(token[1], 10)
        minutes = parseFloat(token[2].replace(',', '.'))
        return (hours % 24) * MILLISECONDS_IN_HOUR +
          minutes * MILLISECONDS_IN_MINUTE
    
    Romain CREY's avatar
    Romain CREY committed
      }
    
    
      // hh:mm:ss or hhmmss
      token = parseTokenHHMMSS.exec(timeString)
      if (token) {
        hours = parseInt(token[1], 10)
        minutes = parseInt(token[2], 10)
        var seconds = parseFloat(token[3].replace(',', '.'))
        return (hours % 24) * MILLISECONDS_IN_HOUR +
          minutes * MILLISECONDS_IN_MINUTE +
          seconds * 1000
    
    Romain CREY's avatar
    Romain CREY committed
      }
    
    
      // Invalid ISO-formatted time
      return null
    }
    
    function parseTimezone (timezoneString) {
      var token
      var absoluteOffset
    
      // Z
      token = parseTokenTimezoneZ.exec(timezoneString)
      if (token) {
        return 0
    
    Romain CREY's avatar
    Romain CREY committed
      }
    
    
      // ±hh
      token = parseTokenTimezoneHH.exec(timezoneString)
      if (token) {
        absoluteOffset = parseInt(token[2], 10) * 60
        return (token[1] === '+') ? -absoluteOffset : absoluteOffset
    
    Romain CREY's avatar
    Romain CREY committed
      }
    
    
      // ±hh:mm or ±hhmm
      token = parseTokenTimezoneHHMM.exec(timezoneString)
      if (token) {
        absoluteOffset = parseInt(token[2], 10) * 60 + parseInt(token[3], 10)
        return (token[1] === '+') ? -absoluteOffset : absoluteOffset
      }
    
    Romain CREY's avatar
    Romain CREY committed
    
    
    Romain CREY's avatar
    Romain CREY committed
    
    
    function dayOfISOYear (isoYear, week, day) {
      week = week || 0
      day = day || 0
      var date = new Date(0)
      date.setUTCFullYear(isoYear, 0, 4)
      var fourthOfJanuaryDay = date.getUTCDay() || 7
      var diff = week * 7 + day + 1 - fourthOfJanuaryDay
      date.setUTCDate(date.getUTCDate() + diff)
      return date
    }
    
    module.exports = parse
    
    /***/ }),
    /* 1625 */
    /***/ (function(module, exports) {
    
    Romain CREY's avatar
    Romain CREY committed
    
    
    var MILLISECONDS_IN_MINUTE = 60000
    
    Romain CREY's avatar
    Romain CREY committed
    
    /**
    
     * Google Chrome as of 67.0.3396.87 introduced timezones with offset that includes seconds.
     * They usually appear for dates that denote time before the timezones were introduced
     * (e.g. for 'Europe/Prague' timezone the offset is GMT+00:57:44 before 1 October 1891
     * and GMT+01:00:00 after that date)
    
    Romain CREY's avatar
    Romain CREY committed
     *
    
     * Date#getTimezoneOffset returns the offset in minutes and would return 57 for the example above,
     * which would lead to incorrect calculations.
     *
     * This function returns the timezone offset in milliseconds that takes seconds in account.
    
    Romain CREY's avatar
    Romain CREY committed
     */
    
    module.exports = function getTimezoneOffsetInMilliseconds (dirtyDate) {
      var date = new Date(dirtyDate.getTime())
      var baseTimezoneOffset = date.getTimezoneOffset()
      date.setSeconds(0, 0)
      var millisecondsPartOfTimezoneOffset = date.getTime() % MILLISECONDS_IN_MINUTE
    
      return baseTimezoneOffset * MILLISECONDS_IN_MINUTE + millisecondsPartOfTimezoneOffset
    }
    
    
    /***/ }),
    /* 1626 */
    /***/ (function(module, exports) {
    
    Romain CREY's avatar
    Romain CREY committed
    
    /**
    
     * @category Common Helpers
     * @summary Is the given argument an instance of Date?
    
    Romain CREY's avatar
    Romain CREY committed
     *
    
     * @description
     * Is the given argument an instance of Date?
    
    Romain CREY's avatar
    Romain CREY committed
     *
    
     * @param {*} argument - the argument to check
     * @returns {Boolean} the given argument is an instance of Date
    
    Romain CREY's avatar
    Romain CREY committed
     *
     * @example
    
     * // Is 'mayonnaise' a Date?
     * var result = isDate('mayonnaise')
     * //=> false
     */
    function isDate (argument) {
      return argument instanceof Date
    }
    
    module.exports = isDate
    
    
    /***/ }),
    /* 1627 */
    /***/ (function(module, exports, __webpack_require__) {
    
    var parse = __webpack_require__(1624)
    
    /**
     * @category Common Helpers
     * @summary Is the first date after the second one?
    
    Romain CREY's avatar
    Romain CREY committed
     *
    
     * @description
     * Is the first date after the second one?
    
    Romain CREY's avatar
    Romain CREY committed
     *
    
     * @param {Date|String|Number} date - the date that should be after the other one to return true
     * @param {Date|String|Number} dateToCompare - the date to compare with
     * @returns {Boolean} the first date is after the second date
    
    Romain CREY's avatar
    Romain CREY committed
     *
    
     * @example
     * // Is 10 July 1989 after 11 February 1987?
     * var result = isAfter(new Date(1989, 6, 10), new Date(1987, 1, 11))
     * //=> true
    
    Romain CREY's avatar
    Romain CREY committed
     */
    
    function isAfter (dirtyDate, dirtyDateToCompare) {
      var date = parse(dirtyDate)
      var dateToCompare = parse(dirtyDateToCompare)
      return date.getTime() > dateToCompare.getTime()
    }
    
    Romain CREY's avatar
    Romain CREY committed
    
    
    module.exports = isAfter
    
    
    /***/ }),
    /* 1628 */
    /***/ (function(module, exports, __webpack_require__) {
    
    const groupBy = __webpack_require__(1579)
    const sortBy = __webpack_require__(1583)
    const { eitherIncludes } = __webpack_require__(1614)
    
    const getDateTransaction = op => op.date.substr(0, 10)
    
    /**
     * Groups `iterables` via `grouper` and returns an iterator
     * that yields [groupKey, groups]
     */
    const zipGroup = function*(iterables, grouper) {
      const grouped = iterables.map(items => groupBy(items, grouper))
      for (const key of Object.keys(grouped[0]).sort()) {
        const groups = grouped.map(keyedGroups => keyedGroups[key] || [])
        yield [key, groups]
    
    Romain CREY's avatar
    Romain CREY committed
      }
    
    }
    
    const squash = (str, char) => {
      const rx = new RegExp(String.raw`${char}{2,}`, 'gi')
      return str && str.replace(rx, char)
    }
    
    const redactedNumber = /\b[0-9X]+\b/gi
    const dateRx = /\b\d{2}\/\d{2}\/\d{4}\b/g
    
    const cleanLabel = label => label && label.replace(redactedNumber, '')
    const withoutDate = str => str && str.replace(dateRx, '')
    const compacted = str => str && str.replace(/\s/g, '').replace(/-/g, '')
    
    const scoreLabel = (newTr, existingTr) => {
      if (
        squash(existingTr.originalBankLabel, ' ') ===
        squash(newTr.originalBankLabel, ' ')
      ) {
        return [200, 'originalBankLabel']
      } else if (
        compacted(existingTr.originalBankLabel) ===
        compacted(newTr.originalBankLabel)
      ) {
        return [120, 'originalBankLabelCompacted']
      } else if (
        withoutDate(existingTr.originalBankLabel) ===
        withoutDate(newTr.originalBankLabel)
      ) {
        // For some transfers, the date in the originalBankLabel is different between
        // BudgetInsight and Linxo
        return [150, 'originalBankLabelWithoutDate']
      } else if (existingTr.label === newTr.label) {
        return [100, 'label']
      } else if (
        eitherIncludes(existingTr.label.toLowerCase(), newTr.label.toLowerCase())
      ) {
        return [70, 'eitherIncludes']
      } else if (
        eitherIncludes(
          cleanLabel(existingTr.label.toLowerCase()),
          cleanLabel(newTr.label.toLowerCase())
        )
      ) {
        return [50, 'fuzzy-eitherIncludes']
      } else {
        // Nothing matches, we penalize so that the score is below 0
        return [-1000, 'label-penalty']
    
    Romain CREY's avatar
    Romain CREY committed
      }
    
    Romain CREY's avatar
    Romain CREY committed
    
    
    const DAY = 1000 * 60 * 60 * 24
    
    Romain CREY's avatar
    Romain CREY committed
    
    
    const getDeltaDate = (newTr, existingTr) => {
      const nDate1 = new Date(newTr.date.substr(0, 10))
      const eDate1 = new Date(existingTr.date.substr(0, 10))
      const delta = Math.abs(eDate1 - nDate1)
      if (newTr.realisationDate) {
        const nDate2 = new Date(newTr.realisationDate.substr(0, 10))
        const delta2 = Math.abs(eDate1 - nDate2)
        return Math.min(delta, delta2)
      } else {
        return delta
      }
    }
    
    Romain CREY's avatar
    Romain CREY committed
    
    
    const scoreMatching = (newTr, existingTr, options = {}) => {
      const methods = []
      const res = {
        op: existingTr,
        methods
      }
    
    Romain CREY's avatar
    Romain CREY committed
    
    
      if (options.maxDateDelta) {
        const delta = getDeltaDate(newTr, existingTr)
        if (delta > options.maxDateDelta) {
          // Early exit, transactions are two far off time-wise
          res.points = -1000
          return res
    
    Romain CREY's avatar
    Romain CREY committed
        } else {
    
          methods.push('approx-date')
    
    Romain CREY's avatar
    Romain CREY committed
        }
    
    Romain CREY's avatar
    Romain CREY committed
    
    
      const [labelPoints, labelMethod] = scoreLabel(newTr, existingTr)
      methods.push(labelMethod)
      const amountDiff = Math.abs(existingTr.amount - newTr.amount)
      const amountPoints = amountDiff === 0 ? methods.push('amount') && 100 : -1000
    
      const points = amountPoints + labelPoints
      res.points = points
      return res
    
    Romain CREY's avatar
    Romain CREY committed
    }
    
    
    const matchTransaction = (newTr, existingTrs, options = {}) => {
      const exactVendorId = existingTrs.find(
        existingTr =>
          existingTr.vendorId &&
          newTr.vendorId &&
          existingTr.vendorId === newTr.vendorId
      )
      if (exactVendorId) {
        return { match: exactVendorId, method: 'vendorId' }
    
    Romain CREY's avatar
    Romain CREY committed
      }
    
    
      // Now we try to do it based on originalBankLabel, label and amount.
      // We score candidates according to their degree of matching
      // with the current transaction.
      // Candidates with score below 0 will be discarded.
      const withPoints = existingTrs.map(existingTr =>
        scoreMatching(newTr, existingTr, options)
      )
    
      const candidates = sortBy(withPoints, x => -x.points).filter(
        x => x.points > 0
      )
      return candidates.length > 0
        ? {
            match: candidates[0].op,
            method: candidates[0].methods.join('-')
          }
        : {
            candidates
          }
    
    Romain CREY's avatar
    Romain CREY committed
    }
    
    
    /**
     * Logic to match a transaction and removing it from the transactions to
     * match. `matchingFn` is the function used for matching.
     */
    const matchTransactionToGroup = function*(newTrs, existingTrs, options = {}) {
      const toMatch = Array.isArray(existingTrs) ? [...existingTrs] : []
      for (let newTr of newTrs) {
        const res = {
          transaction: newTr
        }
    
    Romain CREY's avatar
    Romain CREY committed
    
    
        const result =
          toMatch.length > 0 ? matchTransaction(newTr, toMatch, options) : null
        if (result) {
          Object.assign(res, result)
          const matchIdx = toMatch.indexOf(result.match)
          if (matchIdx > -1) {
            toMatch.splice(matchIdx, 1)
          }
        }
        yield res
    
    Romain CREY's avatar
    Romain CREY committed
      }
    
    Romain CREY's avatar
    Romain CREY committed
    
    
    /**
     * Several logics to match transactions.
     *
     * First group transactions per day and match transactions in
     * intra-day mode.
     * Then relax the date constraint 1 day per 1 day to reach
     * a maximum of 5 days of differences
     */
    const matchTransactions = function*(newTrs, existingTrs) {
      const unmatchedNew = new Set(newTrs)
      const unmatchedExisting = new Set(existingTrs)
      // eslint-disable-next-line no-unused-vars
      for (let [date, [newGroup, existingGroup]] of zipGroup(
        [newTrs, existingTrs],
        getDateTransaction
      )) {
        for (let result of matchTransactionToGroup(newGroup, existingGroup)) {
          if (result.match) {
            unmatchedExisting.delete(result.match)
            unmatchedNew.delete(result.transaction)
            yield result
          }
        }
    
    Romain CREY's avatar
    Romain CREY committed
      }
    
    
      const deltas = [3, 4, 5]
      for (let delta of deltas) {
        for (let result of matchTransactionToGroup(
          Array.from(unmatchedNew),
          Array.from(unmatchedExisting),
          {
            maxDateDelta: delta * DAY
          }
        )) {
          if (result.method) {
            result.method += `-delta${delta}`
          }
          if (result.match) {
            unmatchedExisting.delete(result.match)
            unmatchedNew.delete(result.transaction)
          }
          if (result.match || delta === deltas[deltas.length - 1]) {
            yield result
          }
    
    Romain CREY's avatar
    Romain CREY committed
        }
    
    Romain CREY's avatar
    Romain CREY committed
    }
    
    
    module.exports = {
      matchTransactions,
      scoreMatching
    
    Romain CREY's avatar
    Romain CREY committed
    }
    
    
    Romain CREY's avatar
    Romain CREY committed
    /***/ }),
    
    Romain CREY's avatar
    Romain CREY committed
    /***/ (function(module, exports, __webpack_require__) {
    
    
    const Document = __webpack_require__(1393)
    const sumBy = __webpack_require__(1630)
    
    Romain CREY's avatar
    Romain CREY committed
    
    
    class BankAccountStats extends Document {
      static checkCurrencies(accountsStats) {
        const currency = accountsStats[0].currency
    
    Romain CREY's avatar
    Romain CREY committed
    
    
        for (const accountStats of accountsStats) {
          if (accountStats.currency !== currency) {
            return false
          }
        }
    
    Romain CREY's avatar
    Romain CREY committed
    
    
    Romain CREY's avatar
    Romain CREY committed
    
    
      static sum(accountsStats) {
        if (accountsStats.length === 0) {
          throw new Error('You must give at least one stats object')
        }
    
    Romain CREY's avatar
    Romain CREY committed
    
    
        if (!this.checkCurrencies(accountsStats)) {
          throw new Error('Currency of all stats object must be the same.')
        }
    
        const properties = [
          'income',
          'additionalIncome',
          'mortgage',
          'loans',
          'fixedCharges'
        ]
    
        const summedStats = properties.reduce((sums, property) => {
          sums[property] = sumBy(
            accountsStats,
            accountStats => accountStats[property] || 0
          )
    
    Romain CREY's avatar
    Romain CREY committed
    
    
    Romain CREY's avatar
    Romain CREY committed
    
    
        summedStats.currency = accountsStats[0].currency
    
    Romain CREY's avatar
    Romain CREY committed
    
    
    Romain CREY's avatar
    Romain CREY committed
    
    
    BankAccountStats.doctype = 'io.cozy.bank.accounts.stats'
    BankAccountStats.idAttributes = ['_id']
    BankAccountStats.version = 1
    BankAccountStats.checkedAttributes = null
    
    Romain CREY's avatar
    Romain CREY committed
    
    
    module.exports = BankAccountStats
    
    /***/ }),
    /* 1630 */
    /***/ (function(module, exports, __webpack_require__) {
    
    var baseIteratee = __webpack_require__(1545),
        baseSum = __webpack_require__(1631);
    
    Romain CREY's avatar
    Romain CREY committed
    
    /**
    
     * This method is like `_.sum` except that it accepts `iteratee` which is
     * invoked for each element in `array` to generate the value to be summed.
     * The iteratee is invoked with one argument: (value).
    
    Romain CREY's avatar
    Romain CREY committed
     *
    
     * @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 {number} Returns the sum.
     * @example
     *
     * var objects = [{ 'n': 4 }, { 'n': 2 }, { 'n': 8 }, { 'n': 6 }];
     *
     * _.sumBy(objects, function(o) { return o.n; });
     * // => 20
     *
     * // The `_.property` iteratee shorthand.
     * _.sumBy(objects, 'n');
     * // => 20
    
    Romain CREY's avatar
    Romain CREY committed
     */
    
    function sumBy(array, iteratee) {
      return (array && array.length)
        ? baseSum(array, baseIteratee(iteratee, 2))
        : 0;
    }
    
    Romain CREY's avatar
    Romain CREY committed
    
    
    module.exports = sumBy;
    
    /***/ }),
    /* 1631 */
    /***/ (function(module, exports) {
    
    Romain CREY's avatar
    Romain CREY committed
    
    
    /**
     * The base implementation of `_.sum` and `_.sumBy` without support for
     * iteratee shorthands.
     *
     * @private
     * @param {Array} array The array to iterate over.
     * @param {Function} iteratee The function invoked per iteration.
     * @returns {number} Returns the sum.
     */
    function baseSum(array, iteratee) {
      var result,
          index = -1,
          length = array.length;
    
    Romain CREY's avatar
    Romain CREY committed
    
    
      while (++index < length) {
        var current = iteratee(array[index]);
        if (current !== undefined) {
          result = result === undefined ? current : (result + current);
        }
    
    Romain CREY's avatar
    Romain CREY committed
      }
    
    Romain CREY's avatar
    Romain CREY committed
    }
    
    
    module.exports = baseSum;
    
    /***/ }),
    /* 1632 */
    /***/ (function(module, exports, __webpack_require__) {
    
    Romain CREY's avatar
    Romain CREY committed
    
    
    const trimEnd = __webpack_require__(1633)
    const Document = __webpack_require__(1393)
    
    Romain CREY's avatar
    Romain CREY committed
    
    
    const FILENAME_WITH_EXTENSION_REGEX = /(.+)(\..*)$/
    
    Romain CREY's avatar
    Romain CREY committed
    
    
    /**
     * Class representing the file model.
     * @extends Document
     */
    class CozyFile extends Document {
      /**
       * async getFullpath - Gets a file's path
       *
       * @param  {string} dirID  The id of the parent directory
       * @param  {string} name   The file's name
       * @return {string}        The full path of the file in the cozy
       **/
      static async getFullpath(dirId, name) {
        if (!dirId) {
          throw new Error('You must provide a dirId')
    
    Romain CREY's avatar
    Romain CREY committed
        }
    
    
        const parentDir = await this.get(dirId)
        const parentDirectoryPath = trimEnd(parentDir.path, '/')
        return `${parentDirectoryPath}/${name}`
    
    Romain CREY's avatar
    Romain CREY committed
      }
    
    
      /**
       * Move file to destination.
       *
       * @param   {string} fileId               - The file's id (required)
       * @param   {object} destination
       * @param   {string} destination.folderId - The destination folder's id (required)
       * @param   {string} destination.path     - The file's path after the move (optional, used to optimize performance in case of conflict)
       * @param   {string} force                - Whether we should overwrite the destination in case of conflict (defaults to false)
       * @returns {Promise}                     - A promise that returns the move action response and the deleted file id (if any) if resolved or an Error if rejected
       *
       */
      static async move(fileId, destination, force = false) {
        const { folderId, path } = destination
        const filesCollection = this.cozyClient.collection('io.cozy.files')
        try {
          const resp = await filesCollection.updateFileMetadata(fileId, {
            dir_id: folderId
          })
    
    Romain CREY's avatar
    Romain CREY committed
    
    
          return {
            moved: resp.data,
            deleted: null
          }
        } catch (e) {
          if (e.status === 409 && force) {
            let destinationPath
            if (path) {
              destinationPath = path
            } else {
              const movedFile = await this.get(fileId)
              const filename = movedFile.name
              destinationPath = await this.getFullpath(folderId, filename)
            }
            const conflictResp = await filesCollection.statByPath(destinationPath)
            await filesCollection.destroy(conflictResp.data)
            const resp = await filesCollection.updateFileMetadata(fileId, {
              dir_id: folderId
            })
    
    Romain CREY's avatar
    Romain CREY committed
    
    
            return {
              moved: resp.data,
              deleted: conflictResp.data.id
            }
          } else {
            throw e
          }
        }
    
    Romain CREY's avatar
    Romain CREY committed
      }
    
      /**
       * Method to split both the filename and the extension
       *
       * @param {Object} file An io.cozy.files
       * @return {Object}  return an object with {filename: , extension: }
       */
      static splitFilename(file) {
        if (!file.name) throw new Error('file should have a name property ')
    
    Romain CREY's avatar
    Romain CREY committed
    
    
        if (file.type === 'file') {
          const match = file.name.match(FILENAME_WITH_EXTENSION_REGEX)
          if (match) {
            return { filename: match[1], extension: match[2] }
          }
        }
        return { filename: file.name, extension: '' }
    
    Romain CREY's avatar
    Romain CREY committed
      }
    
      /**
       *
       * Method to upload a file even if a file with the same name already exists.
       *
       * @param {String} path Fullpath for the file ex: path/to/
       * @param {Object} file HTML Object file
       * @param {Object} metadata An object containing the wanted metadata to attach
       */
      static async overrideFileForPath(pathArg, file, metadata) {
        let path = pathArg
        if (!path.endsWith('/')) path = path + '/'
    
    Romain CREY's avatar
    Romain CREY committed
    
    
        const filesCollection = this.cozyClient.collection('io.cozy.files')
    
    Romain CREY's avatar
    Romain CREY committed
        try {
    
          const existingFile = await filesCollection.statByPath(path + file.name)
    
    Romain CREY's avatar
    Romain CREY committed
    
    
          const { id: fileId, dir_id: dirId } = existingFile.data
          const resp = await filesCollection.updateFile(file, {
            dirId,
            fileId,
            metadata
          })
          return resp
        } catch (error) {
          if (/Not Found/.test(error)) {
            const dirId = await filesCollection.ensureDirectoryExists(path)
            const createdFile = await filesCollection.createFile(file, {
              dirId,
              metadata
            })
            return createdFile