feat: wip categorized transactions

This commit is contained in:
Ahmed Bouhuolia
2024-03-04 21:01:36 +02:00
parent f23e8d98f6
commit 68f2f4ee84
12 changed files with 115 additions and 30 deletions

View File

@@ -7,6 +7,7 @@ exports.up = function (knex) {
table.decimal('amount'); table.decimal('amount');
table.string('currency_code'); table.string('currency_code');
table.string('reference_no').index(); table.string('reference_no').index();
table.string('payee');
table table
.integer('account_id') .integer('account_id')
.unsigned() .unsigned()

View File

@@ -0,0 +1,7 @@
exports.up = function (knex) {
return knex.schema.table('accounts', (table) => {
table.integer('uncategorized_transactions').defaultTo(0);
});
};
exports.down = function (knex) {};

View File

@@ -264,6 +264,7 @@ export interface CreateUncategorizedTransactionDTO {
accountId: number; accountId: number;
amount: number; amount: number;
currencyCode: string; currencyCode: string;
payee?: string;
description?: string; description?: string;
referenceNo?: string | null; referenceNo?: string | null;
plaidTransactionId?: string | null; plaidTransactionId?: string | null;

View File

@@ -38,7 +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 }; payment_meta: { reference_number: string | null; payee: string | null };
} }
export interface PlaidFetchedTransactionsUpdates { export interface PlaidFetchedTransactionsUpdates {

View File

@@ -196,6 +196,7 @@ export default class Account extends mixin(TenantModel, [
const Expense = require('models/Expense'); const Expense = require('models/Expense');
const ExpenseEntry = require('models/ExpenseCategory'); const ExpenseEntry = require('models/ExpenseCategory');
const ItemEntry = require('models/ItemEntry'); const ItemEntry = require('models/ItemEntry');
const UncategorizedTransaction = require('models/UncategorizedCashflowTransaction');
return { return {
/** /**
@@ -305,6 +306,21 @@ export default class Account extends mixin(TenantModel, [
to: 'items_entries.sellAccountId', to: 'items_entries.sellAccountId',
}, },
}, },
/**
* Associated uncategorized transactions.
*/
uncategorizedTransactions: {
relation: Model.HasManyRelation,
modelClass: UncategorizedTransaction.default,
join: {
from: 'accounts.id',
to: 'uncategorized_cashflow_transactions.accountId',
},
filter: (query) => {
query.filter('categorized', false);
},
},
}; };
} }

View File

@@ -1,6 +1,7 @@
/* eslint-disable global-require */ /* eslint-disable global-require */
import TenantModel from 'models/TenantModel'; import TenantModel from 'models/TenantModel';
import { Model } from 'objection'; import { Model } from 'objection';
import Account from './Account';
export default class UncategorizedCashflowTransaction extends TenantModel { export default class UncategorizedCashflowTransaction extends TenantModel {
amount: number; amount: number;
@@ -80,4 +81,29 @@ export default class UncategorizedCashflowTransaction extends TenantModel {
}, },
}; };
} }
/**
*
* @param queryContext
*/
public async $afterInsert(queryContext) {
await super.$afterInsert(queryContext);
// Increments the uncategorized transactions count of the associated account.
await Account.query(queryContext.transaction)
.findById(this.accountId)
.increment('uncategorized_transactions', 1);
}
/**
*
* @param queryContext
*/
public async $afterDelete(queryContext) {
await super.$afterDelete(queryContext);
await Account.query()
.findById(this.accountId)
.decrement('uncategorized_transactions', 1);
}
} }

View File

@@ -90,7 +90,7 @@ export class PlaidSyncDb {
tenantId, tenantId,
uncategoriedDTO uncategoriedDTO
), ),
{ concurrency: CONCURRENCY_ASYNC } { concurrency: 1 }
); );
} }

View File

@@ -44,8 +44,9 @@ export const transformPlaidTrxsToCashflowCreate = R.curry(
): CreateUncategorizedTransactionDTO => { ): CreateUncategorizedTransactionDTO => {
return { return {
date: plaidTranasction.date, date: plaidTranasction.date,
description: plaidTranasction.name,
amount: plaidTranasction.amount, amount: plaidTranasction.amount,
description: plaidTranasction.name,
payee: plaidTranasction.payment_meta?.payee,
currencyCode: plaidTranasction.iso_currency_code, currencyCode: plaidTranasction.iso_currency_code,
accountId: cashflowAccountId, accountId: cashflowAccountId,
referenceNo: plaidTranasction.payment_meta?.reference_number, referenceNo: plaidTranasction.payment_meta?.reference_number,

View File

@@ -1,6 +1,6 @@
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import HasTenancyService from '../Tenancy/TenancyService'; import HasTenancyService from '../Tenancy/TenancyService';
import UnitOfWork from '../UnitOfWork'; import UnitOfWork, { IsolationLevel } from '../UnitOfWork';
import { Knex } from 'knex'; import { Knex } from 'knex';
import { CreateUncategorizedTransactionDTO } from '@/interfaces'; import { CreateUncategorizedTransactionDTO } from '@/interfaces';
@@ -21,15 +21,20 @@ export class CreateUncategorizedTransaction {
tenantId: number, tenantId: number,
createDTO: CreateUncategorizedTransactionDTO createDTO: CreateUncategorizedTransactionDTO
) { ) {
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId); const { UncategorizedCashflowTransaction, Account } =
this.tenancy.models(tenantId);
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { return this.uow.withTransaction(
tenantId,
async (trx: Knex.Transaction) => {
const transaction = await UncategorizedCashflowTransaction.query( const transaction = await UncategorizedCashflowTransaction.query(
trx trx
).insertAndFetch({ ).insertAndFetch({
...createDTO, ...createDTO,
}); });
return transaction; return transaction;
}); },
);
} }
} }

View File

@@ -29,10 +29,13 @@ export class UncategorizedTransactionTransformer extends Transformer {
* @returns {string} * @returns {string}
*/ */
protected formattetDepositAmount(transaction) { protected formattetDepositAmount(transaction) {
if (transaction.isDepositTransaction) {
return formatNumber(transaction.deposit, { return formatNumber(transaction.deposit, {
currencyCode: transaction.currencyCode, currencyCode: transaction.currencyCode,
}); });
} }
return '';
}
/** /**
* Formatted withdrawal amount. * Formatted withdrawal amount.
@@ -40,8 +43,11 @@ export class UncategorizedTransactionTransformer extends Transformer {
* @returns {string} * @returns {string}
*/ */
protected formattedWithdrawalAmount(transaction) { protected formattedWithdrawalAmount(transaction) {
if (transaction.isWithdrawalTransaction) {
return formatNumber(transaction.withdrawal, { return formatNumber(transaction.withdrawal, {
currencyCode: transaction.currencyCode, currencyCode: transaction.currencyCode,
}); });
} }
return '';
}
} }

View File

@@ -84,6 +84,18 @@ function AccountBankBalanceItem() {
); );
} }
function AccountNumberItem() {
const { currentAccount } = useAccountTransactionsContext();
if (!currentAccount.account_mask) return null;
return (
<AccountBalanceItemWrap>
Account Number: xxx{currentAccount.account_mask}
</AccountBalanceItemWrap>
);
}
function AccountTransactionsDetailsBarSkeleton() { function AccountTransactionsDetailsBarSkeleton() {
return ( return (
<React.Fragment> <React.Fragment>
@@ -101,6 +113,7 @@ function AccountTransactionsDetailsContent() {
return ( return (
<React.Fragment> <React.Fragment>
<AccountSwitchItem /> <AccountSwitchItem />
<AccountNumberItem />
<AccountBalanceItem /> <AccountBalanceItem />
<AccountBankBalanceItem /> <AccountBankBalanceItem />
</React.Fragment> </React.Fragment>

View File

@@ -8,12 +8,17 @@ const AccountContentTabs = styled(ContentTabs)`
`; `;
export function AccountTransactionsFilterTabs() { export function AccountTransactionsFilterTabs() {
const { filterTab, setFilterTab } = useAccountTransactionsContext(); const { filterTab, setFilterTab, currentAccount } =
useAccountTransactionsContext();
const handleChange = (value) => { const handleChange = (value) => {
setFilterTab(value); setFilterTab(value);
}; };
const hasUncategorizedTransx = Boolean(
currentAccount.uncategorized_transactions,
);
return ( return (
<AccountContentTabs value={filterTab} onChange={handleChange}> <AccountContentTabs value={filterTab} onChange={handleChange}>
<ContentTabs.Tab <ContentTabs.Tab
@@ -21,16 +26,20 @@ export function AccountTransactionsFilterTabs() {
title={'Dashboard'} title={'Dashboard'}
description={'Account Summary'} description={'Account Summary'}
/> />
{hasUncategorizedTransx && (
<ContentTabs.Tab <ContentTabs.Tab
id={'uncategorized'} id={'uncategorized'}
title={ title={
<> <>
<span style={{ color: '#ff0000' }}>20</span> Uncategorized <span style={{ color: '#ff0000' }}>
Transactions {currentAccount.uncategorized_transactions}
</span>{' '}
Uncategorized Transactions
</> </>
} }
description={'For Bank Statement'} description={'For Bank Statement'}
/> />
)}
<ContentTabs.Tab <ContentTabs.Tab
id="all" id="all"
title={'All Transactions'} title={'All Transactions'}