feat(server): sync Plaid transactions to uncategorized transactions

This commit is contained in:
Ahmed Bouhuolia
2024-03-04 13:42:17 +02:00
parent 9db03350e0
commit f23e8d98f6
10 changed files with 113 additions and 24 deletions

View File

@@ -23,7 +23,7 @@ export default class NewCashflowTransactionController extends BaseController {
const router = Router(); const router = Router();
router.get( router.get(
'/transactions/uncategorized', '/transactions/:id/uncategorized',
this.asyncMiddleware(this.getUncategorizedCashflowTransactions), this.asyncMiddleware(this.getUncategorizedCashflowTransactions),
this.catchServiceErrors this.catchServiceErrors
); );
@@ -237,10 +237,12 @@ export default class NewCashflowTransactionController extends BaseController {
next: NextFunction next: NextFunction
) => { ) => {
const { tenantId } = req; const { tenantId } = req;
const { id: accountId } = req.params;
try { try {
const data = await this.cashflowApplication.getUncategorizedTransactions( const data = await this.cashflowApplication.getUncategorizedTransactions(
tenantId tenantId,
accountId
); );
return res.status(200).send(data); return res.status(200).send(data);

View File

@@ -16,6 +16,7 @@ exports.up = function (knex) {
table.string('categorize_ref_type'); table.string('categorize_ref_type');
table.integer('categorize_ref_id').unsigned(); table.integer('categorize_ref_id').unsigned();
table.boolean('categorized').defaultTo(false); table.boolean('categorized').defaultTo(false);
table.string('plaid_transaction_id');
table.timestamps(); table.timestamps();
} }
); );

View File

@@ -257,3 +257,14 @@ export interface IUncategorizedCashflowTransaction {
categorizeRefId: number; categorizeRefId: number;
categorized: boolean; categorized: boolean;
} }
export interface CreateUncategorizedTransactionDTO {
date: Date | string;
accountId: number;
amount: number;
currencyCode: string;
description?: string;
referenceNo?: string | null;
plaidTransactionId?: string | null;
}

View File

@@ -38,6 +38,7 @@ export interface PlaidTransaction {
iso_currency_code: string; iso_currency_code: string;
transaction_id: string; transaction_id: string;
transaction_type: string; transaction_type: string;
payment_meta: { reference_number: string | null };
} }
export interface PlaidFetchedTransactionsUpdates { export interface PlaidFetchedTransactionsUpdates {

View File

@@ -11,6 +11,7 @@ import {
import NewCashflowTransactionService from '@/services/Cashflow/NewCashflowTransactionService'; import NewCashflowTransactionService from '@/services/Cashflow/NewCashflowTransactionService';
import { DeleteCashflowTransaction } from '@/services/Cashflow/DeleteCashflowTransactionService'; import { DeleteCashflowTransaction } from '@/services/Cashflow/DeleteCashflowTransactionService';
import HasTenancyService from '@/services/Tenancy/TenancyService'; import HasTenancyService from '@/services/Tenancy/TenancyService';
import { CashflowApplication } from '@/services/Cashflow/CashflowApplication';
const CONCURRENCY_ASYNC = 10; const CONCURRENCY_ASYNC = 10;
@@ -22,6 +23,9 @@ export class PlaidSyncDb {
@Inject() @Inject()
private createAccountService: CreateAccount; private createAccountService: CreateAccount;
@Inject()
private cashflowApp: CashflowApplication;
@Inject() @Inject()
private createCashflowTransactionService: NewCashflowTransactionService; private createCashflowTransactionService: NewCashflowTransactionService;
@@ -75,15 +79,16 @@ export class PlaidSyncDb {
cashflowAccount.id, cashflowAccount.id,
openingEquityBalance.id openingEquityBalance.id
); );
const accountsCashflowDTO = R.map(transformTransaction)(plaidTranasctions); const uncategorizedTransDTOs =
R.map(transformTransaction)(plaidTranasctions);
// Creating account transaction queue. // Creating account transaction queue.
await bluebird.map( await bluebird.map(
accountsCashflowDTO, uncategorizedTransDTOs,
(cashflowDTO) => (uncategoriedDTO) =>
this.createCashflowTransactionService.newCashflowTransaction( this.cashflowApp.createUncategorizedTransaction(
tenantId, tenantId,
cashflowDTO uncategoriedDTO
), ),
{ concurrency: CONCURRENCY_ASYNC } { concurrency: CONCURRENCY_ASYNC }
); );

View File

@@ -1,7 +1,9 @@
import * as R from 'ramda'; import * as R from 'ramda';
import { import {
CreateUncategorizedTransactionDTO,
IAccountCreateDTO, IAccountCreateDTO,
ICashflowNewCommandDTO, ICashflowNewCommandDTO,
IUncategorizedCashflowTransaction,
PlaidAccount, PlaidAccount,
PlaidTransaction, PlaidTransaction,
} from '@/interfaces'; } from '@/interfaces';
@@ -32,30 +34,22 @@ export const transformPlaidAccountToCreateAccount = (
* @param {number} cashflowAccountId - Cashflow account ID. * @param {number} cashflowAccountId - Cashflow account ID.
* @param {number} creditAccountId - Credit account ID. * @param {number} creditAccountId - Credit account ID.
* @param {PlaidTransaction} plaidTranasction - Plaid transaction. * @param {PlaidTransaction} plaidTranasction - Plaid transaction.
* @returns {ICashflowNewCommandDTO} * @returns {CreateUncategorizedTransactionDTO}
*/ */
export const transformPlaidTrxsToCashflowCreate = R.curry( export const transformPlaidTrxsToCashflowCreate = R.curry(
( (
cashflowAccountId: number, cashflowAccountId: number,
creditAccountId: number, creditAccountId: number,
plaidTranasction: PlaidTransaction plaidTranasction: PlaidTransaction
): ICashflowNewCommandDTO => { ): CreateUncategorizedTransactionDTO => {
return { return {
date: plaidTranasction.date, date: plaidTranasction.date,
transactionType: 'OwnerContribution',
description: plaidTranasction.name, description: plaidTranasction.name,
amount: plaidTranasction.amount, amount: plaidTranasction.amount,
exchangeRate: 1,
currencyCode: plaidTranasction.iso_currency_code, currencyCode: plaidTranasction.iso_currency_code,
creditAccountId, accountId: cashflowAccountId,
cashflowAccountId, referenceNo: plaidTranasction.payment_meta?.reference_number,
// transactionNumber: string;
// referenceNo: string;
plaidTransactionId: plaidTranasction.transaction_id, plaidTransactionId: plaidTranasction.transaction_id,
publish: true,
}; };
} }
); );

View File

@@ -4,10 +4,13 @@ import { UncategorizeCashflowTransaction } from './UncategorizeCashflowTransacti
import { CategorizeCashflowTransaction } from './CategorizeCashflowTransaction'; import { CategorizeCashflowTransaction } from './CategorizeCashflowTransaction';
import { import {
CategorizeTransactionAsExpenseDTO, CategorizeTransactionAsExpenseDTO,
CreateUncategorizedTransactionDTO,
ICategorizeCashflowTransactioDTO, ICategorizeCashflowTransactioDTO,
IUncategorizedCashflowTransaction,
} from '@/interfaces'; } from '@/interfaces';
import { CategorizeTransactionAsExpense } from './CategorizeTransactionAsExpense'; import { CategorizeTransactionAsExpense } from './CategorizeTransactionAsExpense';
import { GetUncategorizedTransactions } from './GetUncategorizedTransactions'; import { GetUncategorizedTransactions } from './GetUncategorizedTransactions';
import { CreateUncategorizedTransaction } from './CreateUncategorizedTransaction';
@Service() @Service()
export class CashflowApplication { export class CashflowApplication {
@@ -26,6 +29,9 @@ export class CashflowApplication {
@Inject() @Inject()
private getUncategorizedTransactionsService: GetUncategorizedTransactions; private getUncategorizedTransactionsService: GetUncategorizedTransactions;
@Inject()
private createUncategorizedTransactionService: CreateUncategorizedTransaction;
/** /**
* Deletes the given cashflow transaction. * Deletes the given cashflow transaction.
* @param {number} tenantId * @param {number} tenantId
@@ -39,6 +45,22 @@ export class CashflowApplication {
); );
} }
/**
* Creates a new uncategorized cash transaction.
* @param {number} tenantId
* @param {CreateUncategorizedTransactionDTO} createUncategorizedTransactionDTO
* @returns {IUncategorizedCashflowTransaction}
*/
public createUncategorizedTransaction(
tenantId: number,
createUncategorizedTransactionDTO: CreateUncategorizedTransactionDTO
) {
return this.createUncategorizedTransactionService.create(
tenantId,
createUncategorizedTransactionDTO
);
}
/** /**
* Uncategorize the given cashflow transaction. * Uncategorize the given cashflow transaction.
* @param {number} tenantId * @param {number} tenantId
@@ -98,7 +120,10 @@ export class CashflowApplication {
* @param {number} tenantId * @param {number} tenantId
* @returns {} * @returns {}
*/ */
public getUncategorizedTransactions(tenantId: number) { public getUncategorizedTransactions(tenantId: number, accountId: number) {
return this.getUncategorizedTransactionsService.getTransactions(tenantId); return this.getUncategorizedTransactionsService.getTransactions(
tenantId,
accountId
);
} }
} }

View File

@@ -0,0 +1,35 @@
import { Inject, Service } from 'typedi';
import HasTenancyService from '../Tenancy/TenancyService';
import UnitOfWork from '../UnitOfWork';
import { Knex } from 'knex';
import { CreateUncategorizedTransactionDTO } from '@/interfaces';
@Service()
export class CreateUncategorizedTransaction {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private uow: UnitOfWork;
/**
* Creates an uncategorized cashflow transaction.
* @param {number} tenantId
* @param {CreateUncategorizedTransactionDTO} createDTO
*/
public create(
tenantId: number,
createDTO: CreateUncategorizedTransactionDTO
) {
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
const transaction = await UncategorizedCashflowTransaction.query(
trx
).insertAndFetch({
...createDTO,
});
return transaction;
});
}
}

View File

@@ -13,13 +13,15 @@ export class GetUncategorizedTransactions {
/** /**
* Retrieves the uncategorized cashflow transactions. * Retrieves the uncategorized cashflow transactions.
* @param {number} tenantId * @param {number} tenantId - Tenant id.
* @param {number} accountId - Account Id.
*/ */
public async getTransactions(tenantId: number) { public async getTransactions(tenantId: number, accountId: number) {
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId); const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
const { results, pagination } = const { results, pagination } =
await UncategorizedCashflowTransaction.query() await UncategorizedCashflowTransaction.query()
.where('accountId', accountId)
.where('categorized', false) .where('categorized', false)
.withGraphFetched('account') .withGraphFetched('account')
.pagination(0, 10); .pagination(0, 10);

View File

@@ -7,9 +7,22 @@ export class UncategorizedTransactionTransformer extends Transformer {
* @returns {string[]} * @returns {string[]}
*/ */
public includeAttributes = (): string[] => { public includeAttributes = (): string[] => {
return ['formattetDepositAmount', 'formattedWithdrawalAmount']; return [
'formattedDate',
'formattetDepositAmount',
'formattedWithdrawalAmount',
];
}; };
/**
* Formattes the transaction date.
* @param transaction
* @returns {string}
*/
public formattedDate(transaction) {
return this.formatDate(transaction.date);
}
/** /**
* Formatted deposit amount. * Formatted deposit amount.
* @param transaction * @param transaction