Skip to content

Commit

Permalink
Merge pull request #2616 from cozy/feat/add-csv-data-export-service
Browse files Browse the repository at this point in the history
The `export` service fetches all transactions with their associated
relationships and transforms them into CSV lines following our own
schema.
It also transforms accounts without transactions into CSV lines
following the same schema.
The result is uploaded as a CSV file into the user's root directory.

To do so Banks now requires a permission to create and update
`io.cozy.files` documents.

The CSV is generated by the `@fast-csv` library which offers an easy
to use interface with a streamable transformation (although we don't
stream the content to cozy-stack yet as we end up with `EPIPE` errors
when cozy-stack responds with a 409 status response).

The exported accounts without transactions can be found at the top of
the file so they can easily be removed by the user if they want to
process their transactions in a spreadsheet editor.

The service can be launched programmatically with a 10m debounce
period.

(This is the final version of the commits that were mistakenly merged 
with #2618)

```
### ✨ Features

* Allow exporting transactions and accounts as a CSV file stored in the Cozy via a service.
```
  • Loading branch information
taratatach authored Mar 7, 2023
2 parents dbe6a86 + 6e8f94d commit 196b9cb
Show file tree
Hide file tree
Showing 4 changed files with 252 additions and 39 deletions.
121 changes: 114 additions & 7 deletions src/ducks/export/services.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import { Q } from 'cozy-client'
import { format } from '@fast-csv/format'
import formatDate from 'date-fns/format'

import { TRANSACTION_DOCTYPE } from 'doctypes'
import categories from 'ducks/categories/tree'
import { ACCOUNT_DOCTYPE, TRANSACTION_DOCTYPE } from 'doctypes'
import { getCategoryId, getApplicationDate } from 'ducks/transactions/helpers'
import { getCategoryName } from 'ducks/categories/categoriesMap'
import { DATE_FORMAT } from './constants'

const dateStr = date =>
Expand All @@ -25,6 +26,27 @@ export const fetchTransactionsToExport = async client => {
return client.hydrateDocuments(TRANSACTION_DOCTYPE, transactions)
}

/**
* Fetch all accounts and keep only those not associated with the given
* transactions.
*
* @param {CozyClient} client A CozyClient instance
* @param {object} options
* @param {Array<BankTransaction>} options.transactions Transactions used to filter accounts
*
* @return {Promise<Array<BankAccount>>} Promise that resolves with an array of io.cozy.bank.operations documents
*/
export const fetchAccountsToExport = async (client, { transactions }) => {
const accounts = await client.queryAll(Q(ACCOUNT_DOCTYPE).limitBy(1000))

return accounts.filter(
account =>
!transactions.some(
transaction => transaction.account?.data?._id === account._id
)
)
}

/**
* Create a Transform Stream to transform input data into CSV lines.
*
Expand All @@ -44,17 +66,29 @@ export const createFormatStream = () => {
'Amount',
'Currency',
'Type',
'Expected?',
'Expected debit date',
'Reimbursement status',
'Bank name',
'Account name',
'Custom account name',
'Account number',
'Account originalNumber',
'Account type',
'Account balance',
'Account coming balance',
'Account IBAN',
'Account vendorDeleted',
'Recurrent?',
'Recurrence name',
'Recurrence status',
'Recurrence frequency',
'Tag 1',
'Tag 2',
'Tag 3',
'Tag 4',
'Tag 5'
'Tag 5',
'Unique ID'
]
})
}
Expand All @@ -71,30 +105,48 @@ export const transactionsToCSV = function* (transactions) {
const account = transaction.account?.data
const recurrence = transaction.recurrence?.data
const tags = transaction.tags?.data
const categoryId = getCategoryId(transaction)

const data = [
dateStr(transaction.date),
dateStr(transaction.realisationDate),
dateStr(transaction.applicationDate),
dateStr(getApplicationDate(transaction)),
transaction.label,
transaction.originalBankLabel,
categories[transaction.cozyCategoryId],
getCategoryName(categoryId),
transaction.amount,
transaction.currency,
transaction.type,
transaction.isComing ? 'yes' : 'no',
transaction.valueDate,
transaction.reimbursementStatus
]

// Transaction's bank account information
data.push(
account?.institutionLabel,
account?.label,
account?.shortLabel,
account?.number,
account?.type
account?.originalNumber,
account?.type,
account?.balance,
account?.comingBalance,
account?.iban,
account?.vendorDeleted
)

// Transaction's recurrence information
data.push(recurrence?.manualLabel || recurrence?.automaticLabel)
data.push(
recurrence != null
? 'yes'
: transaction.relationships?.recurrence?.data?._id === 'not-recurrent'
? 'no'
: undefined,
recurrence?.manualLabel || recurrence?.automaticLabel,
recurrence?.status,
recurrence?.stats?.deltas?.median
)

// Transaction's tags information
for (let i = 0; i < 5; i++) {
Expand All @@ -105,6 +157,61 @@ export const transactionsToCSV = function* (transactions) {
}
}

// Unique identifier
data.push(transaction.vendorId || transaction.linxoId)

yield data
}
}

/**
* Generator that transforms the given accounts into our own CSV format and yields the result line by line.
*
* @param {Array<BankAccount>} accounts The list of accounts to transform
*
* @return {IterableIterator<Array<string|undefined>>}
*/
export const accountsWitoutTransactionsToCSV = function* (accounts) {
for (const account of accounts) {
// Transaction information
const data = [
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined
]

// Account information
data.push(
account.institutionLabel,
account.label,
account.shortLabel,
account.number,
account.originalNumber,
account.type,
account.balance,
account.comingBalance,
account.iban,
account.vendorDeleted
)

// Recurrence information
data.push(undefined, undefined, undefined, undefined)

// Tags information
data.push(undefined, undefined, undefined, undefined, undefined)

// Unique identifier
data.push(account.vendorId || account.linxoId)

yield data
}
}
Loading

0 comments on commit 196b9cb

Please sign in to comment.