| import { createHash } from "node:crypto"; |
| import type { AccountType } from "@engine/utils/account"; |
| import { getLogoURL } from "@engine/utils/logo"; |
| import { capitalCase } from "change-case"; |
| import type { |
| Account, |
| ConnectionStatus, |
| GetAccountBalanceResponse, |
| Transaction, |
| } from "../types"; |
| import type { |
| GetAccountDetailsResponse, |
| GetBalancesResponse, |
| GetExchangeCodeResponse, |
| GetSessionResponse, |
| GetTransaction, |
| Institution, |
| TransformInstitution, |
| } from "./types"; |
|
|
| export function hashInstitutionId(name: string, country?: string): string { |
| const input = `${name}-${country}`; |
| return createHash("md5").update(input).digest("hex").slice(0, 12); |
| } |
|
|
| export const transformInstitution = ( |
| institution: Institution, |
| ): TransformInstitution => ({ |
| id: hashInstitutionId(institution.name, institution.country), |
| name: institution.name, |
| logo: getLogoURL(institution.name, "png"), |
| provider: "enablebanking", |
| }); |
|
|
| function getAccountName(account: GetAccountDetailsResponse) { |
| if (account.product) { |
| return capitalCase(account.product); |
| } |
|
|
| if (account.name) { |
| return capitalCase(account.name); |
| } |
|
|
| if (account.details) { |
| return capitalCase(account.details); |
| } |
|
|
| return "Account"; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| function getAccountType(cashAccountType?: string): AccountType { |
| switch (cashAccountType) { |
| case "CARD": |
| return "credit"; |
| case "LOAN": |
| return "loan"; |
| default: |
| return "depository"; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const getAvailableBalance = ( |
| balance: GetAccountDetailsResponse["balance"], |
| ): number | null => { |
| |
| |
| if (balance?.balance_type?.toLowerCase().includes("available")) { |
| return +balance.balance_amount.amount; |
| } |
| return null; |
| }; |
|
|
| export const transformAccount = ( |
| account: GetAccountDetailsResponse, |
| ): Account => { |
| const accountType = getAccountType(account.cash_account_type); |
| const rawAmount = +account.balance.balance_amount.amount; |
|
|
| |
| |
| const amount = |
| accountType === "credit" && rawAmount < 0 ? Math.abs(rawAmount) : rawAmount; |
|
|
| return { |
| id: account.uid, |
| name: getAccountName(account), |
| currency: account.currency, |
| type: accountType, |
| institution: { |
| id: hashInstitutionId( |
| account.institution.name, |
| account.institution.country, |
| ), |
| name: account.institution.name, |
| logo: getLogoURL(account.institution.name, "png"), |
| provider: "enablebanking", |
| }, |
| balance: { |
| amount, |
| currency: account.currency, |
| }, |
| enrollment_id: null, |
| resource_id: account.identification_hash, |
| expires_at: account.valid_until, |
| iban: account.account_id?.iban || null, |
| subtype: account.cash_account_type?.toLowerCase() || null, |
| bic: account.account_servicer?.bic_fi || null, |
| |
| routing_number: null, |
| wire_routing_number: null, |
| account_number: null, |
| sort_code: null, |
| |
| available_balance: getAvailableBalance(account.balance), |
| credit_limit: account.credit_limit?.amount |
| ? +account.credit_limit.amount |
| : null, |
| }; |
| }; |
|
|
| export const transformSessionData = (session: GetExchangeCodeResponse) => { |
| return { |
| session_id: session.session_id, |
| expires_at: session.access.valid_until, |
| access: session.access, |
| accounts: session.accounts.map((account) => ({ |
| account_reference: account.identification_hash, |
| account_id: account.uid, |
| })), |
| }; |
| }; |
|
|
| type TransformBalanceParams = { |
| balance: GetBalancesResponse["balances"][0]; |
| creditLimit?: { currency: string; amount: string } | null; |
| accountType?: string; |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| export const transformBalance = ({ |
| balance, |
| creditLimit, |
| accountType, |
| }: TransformBalanceParams): GetAccountBalanceResponse => { |
| const rawAmount = +balance.balance_amount.amount; |
|
|
| |
| const amount = |
| accountType === "credit" && rawAmount < 0 ? Math.abs(rawAmount) : rawAmount; |
|
|
| |
| |
| |
| const availableBalance = balance.balance_type |
| ?.toLowerCase() |
| .includes("available") |
| ? accountType === "credit" && rawAmount < 0 |
| ? Math.abs(rawAmount) |
| : rawAmount |
| : null; |
|
|
| return { |
| amount, |
| currency: balance.balance_amount.currency, |
| available_balance: availableBalance, |
| credit_limit: creditLimit?.amount ? +creditLimit.amount : null, |
| }; |
| }; |
|
|
| export const transformConnectionStatus = ( |
| session: GetSessionResponse, |
| ): ConnectionStatus => ({ |
| status: session.status === "AUTHORIZED" ? "connected" : "disconnected", |
| }); |
|
|
| export const transformTransactionName = ( |
| transaction: GetTransaction, |
| ): string => { |
| |
| if ( |
| transaction.remittance_information?.length && |
| transaction.remittance_information[0] !== "" |
| ) { |
| return transaction.remittance_information[0]; |
| } |
|
|
| |
| if ( |
| transaction.credit_debit_indicator === "CRDT" && |
| transaction.debtor?.name |
| ) { |
| return transaction.debtor.name; |
| } |
| if ( |
| transaction.credit_debit_indicator === "DBIT" && |
| transaction.creditor?.name |
| ) { |
| return transaction.creditor.name; |
| } |
|
|
| |
| if (transaction.bank_transaction_code?.description) { |
| return transaction.bank_transaction_code.description; |
| } |
|
|
| |
| if (transaction.reference_number) { |
| return transaction.reference_number; |
| } |
|
|
| |
| return "No information"; |
| }; |
|
|
| type TransformTransactionCategory = { |
| transaction: GetTransaction; |
| accountType: AccountType; |
| }; |
|
|
| export const transformTransactionCategory = ({ |
| transaction, |
| accountType, |
| }: TransformTransactionCategory) => { |
| const amount = +transaction.transaction_amount.amount; |
| const isCredit = transaction.credit_debit_indicator === "CRDT"; |
|
|
| |
| if (isCredit && amount > 0) { |
| |
| if (accountType === "credit") { |
| |
| const description = transaction.bank_transaction_code?.description; |
| if (description === "Transfer" || description === "Payment") { |
| return "credit-card-payment"; |
| } |
| |
| return null; |
| } |
| return "income"; |
| } |
|
|
| return null; |
| }; |
|
|
| export const transformTransactionMethod = (transaction: GetTransaction) => { |
| if (transaction.credit_debit_indicator === "CRDT") { |
| return "payment"; |
| } |
|
|
| |
| if (transaction.bank_transaction_code?.description === "Transfer") { |
| return "transfer"; |
| } |
|
|
| return "other"; |
| }; |
|
|
| type TransactionDescription = { |
| transaction: GetTransaction; |
| name: string; |
| }; |
|
|
| const transformDescription = ({ |
| transaction, |
| name, |
| }: TransactionDescription) => { |
| if ( |
| transaction?.remittance_information?.length && |
| transaction.remittance_information.some( |
| (info) => info && info.trim() !== "", |
| ) |
| ) { |
| const text = transaction.remittance_information |
| .filter((info) => info && info.trim() !== "") |
| .join(" "); |
| const description = capitalCase(text); |
|
|
| |
| |
| if (description !== name) { |
| return description; |
| } |
| } |
|
|
| return null; |
| }; |
|
|
| const formatAmount = (transaction: GetTransaction): number => { |
| const amount = +transaction.transaction_amount.amount; |
| return transaction.credit_debit_indicator === "CRDT" ? amount : -amount; |
| }; |
|
|
| const transformCounterpartyName = (transaction: GetTransaction) => { |
| const { credit_debit_indicator, debtor, creditor } = transaction; |
|
|
| if (credit_debit_indicator === "CRDT" && debtor?.name) { |
| return capitalCase(debtor.name); |
| } |
|
|
| if (credit_debit_indicator === "DBIT" && creditor?.name) { |
| return capitalCase(creditor.name); |
| } |
|
|
| return null; |
| }; |
|
|
| type TransformTransactionPayload = { |
| transaction: GetTransaction; |
| accountType: AccountType; |
| }; |
|
|
| |
| |
| |
| |
| |
| function generateTransactionId(transaction: GetTransaction): string { |
| if (transaction.entry_reference) { |
| return transaction.entry_reference; |
| } |
|
|
| |
| if (transaction.transaction_id) { |
| return transaction.transaction_id; |
| } |
|
|
| |
| |
| |
| |
| |
| const input = [ |
| transaction.booking_date, |
| transaction.value_date, |
| transaction.transaction_amount.amount, |
| transaction.transaction_amount.currency, |
| transaction.credit_debit_indicator, |
| transaction.reference_number, |
| transaction.remittance_information?.join("|"), |
| transaction.balance_after_transaction?.amount, |
| ] |
| .map((v) => v ?? "") |
| .join("-"); |
|
|
| return createHash("md5").update(input).digest("hex"); |
| } |
|
|
| export const transformTransaction = ({ |
| transaction, |
| accountType, |
| }: TransformTransactionPayload): Transaction => { |
| const name = capitalCase(transformTransactionName(transaction)); |
| const description = transformDescription({ transaction, name }); |
|
|
| return { |
| id: generateTransactionId(transaction), |
| amount: formatAmount(transaction), |
| currency: transaction.transaction_amount.currency, |
| date: transaction.booking_date, |
| status: "posted", |
| balance: transaction.balance_after_transaction |
| ? +transaction.balance_after_transaction.amount |
| : null, |
| category: transformTransactionCategory({ transaction, accountType }), |
| counterparty_name: transformCounterpartyName(transaction), |
| merchant_name: null, |
| method: transformTransactionMethod(transaction), |
| name, |
| description, |
| currency_rate: null, |
| currency_source: null, |
| }; |
| }; |
|
|