Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reconcile match transactionss #522

Merged
merged 6 commits into from
Jul 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/server/src/interfaces/Account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@ export interface IAccountTransaction {
referenceId: number;

referenceNumber?: string;

transactionNumber?: string;
transactionType?: string;

note?: string;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,9 @@ export interface ICashflowAccountTransaction {

date: Date;
formattedDate: string;

status: string;
formattedStatus: string;

uncategorizedTransactionId: number;
}
2 changes: 2 additions & 0 deletions packages/server/src/interfaces/Ledger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ export interface ILedgerEntry {
date: Date | string;

transactionType: string;
transactionSubType: string;

transactionId: number;

transactionNumber?: string;
Expand Down
6 changes: 5 additions & 1 deletion packages/server/src/loaders/eventEmitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ import { ValidateMatchingOnPaymentMadeDelete } from '@/services/Banking/Matching
import { ValidateMatchingOnCashflowDelete } from '@/services/Banking/Matching/events/ValidateMatchingOnCashflowDelete';
import { RecognizeSyncedBankTranasctions } from '@/services/Banking/Plaid/subscribers/RecognizeSyncedBankTransactions';
import { UnlinkBankRuleOnDeleteBankRule } from '@/services/Banking/Rules/events/UnlinkBankRuleOnDeleteBankRule';
import { DecrementUncategorizedTransactionOnMatching } from '@/services/Banking/Matching/events/DecrementUncategorizedTransactionsOnMatch';
import { DecrementUncategorizedTransactionOnExclude } from '@/services/Banking/Exclude/events/DecrementUncategorizedTransactionOnExclude';

export default () => {
return new EventPublisher();
Expand Down Expand Up @@ -258,6 +260,8 @@ export const susbcribers = () => {
// Bank Rules
TriggerRecognizedTransactions,
UnlinkBankRuleOnDeleteBankRule,
DecrementUncategorizedTransactionOnMatching,
DecrementUncategorizedTransactionOnExclude,

// Validate matching
ValidateMatchingOnCashflowDelete,
Expand All @@ -266,7 +270,7 @@ export const susbcribers = () => {
ValidateMatchingOnPaymentReceivedDelete,
ValidateMatchingOnPaymentMadeDelete,

// Plaid
// Plaid
RecognizeSyncedBankTranasctions,
];
};
30 changes: 29 additions & 1 deletion packages/server/src/models/CashflowTransaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import {
getCashflowAccountTransactionsTypes,
getCashflowTransactionType,
} from '@/services/Cashflow/utils';
import AccountTransaction from './AccountTransaction';
import { CASHFLOW_DIRECTION } from '@/services/Cashflow/constants';
import { getTransactionTypeLabel } from '@/utils/transactions-types';

export default class CashflowTransaction extends TenantModel {
transactionType: string;
amount: number;
Expand Down Expand Up @@ -95,6 +95,34 @@ export default class CashflowTransaction extends TenantModel {
return !!this.uncategorizedTransaction;
}

/**
* Model modifiers.
*/
static get modifiers() {
return {
/**
* Filter the published transactions.
*/
published(query) {
query.whereNot('published_at', null);
},

/**
* Filter the not categorized transactions.
*/
notCategorized(query) {
query.whereNull('cashflowTransactions.uncategorizedTransactionId');
},

/**
* Filter the categorized transactions.
*/
categorized(query) {
query.whereNotNull('cashflowTransactions.uncategorizedTransactionId');
},
};
}

/**
* Relationship mapping.
*/
Expand Down
30 changes: 28 additions & 2 deletions packages/server/src/models/UncategorizedCashflowTransaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,34 @@ export default class UncategorizedCashflowTransaction extends mixin(
* Filters the excluded transactions.
*/
excluded(query) {
query.whereNotNull('excluded_at')
}
query.whereNotNull('excluded_at');
},

/**
* Filter out the recognized transactions.
* @param query
*/
recognized(query) {
query.whereNotNull('recognizedTransactionId');
},

/**
* Filter out the not recognized transactions.
* @param query
*/
notRecognized(query) {
query.whereNull('recognizedTransactionId');
},

categorized(query) {
query.whereNotNull('categorizeRefType');
query.whereNotNull('categorizeRefId');
},

notCategorized(query) {
query.whereNull('categorizeRefType');
query.whereNull('categorizeRefId');
},
};
}

Expand Down
2 changes: 2 additions & 0 deletions packages/server/src/services/Accounting/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export const transformLedgerEntryToTransaction = (
referenceId: entry.transactionId,

transactionNumber: entry.transactionNumber,
transactionType: entry.transactionSubType,

referenceNumber: entry.referenceNumber,

note: entry.note,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Server } from 'socket.io';
import { Inject, Service } from 'typedi';
import { initialize } from 'objection';
import HasTenancyService from '@/services/Tenancy/TenancyService';

@Service()
export class GetBankAccountSummary {
Expand All @@ -14,22 +14,43 @@ export class GetBankAccountSummary {
* @returns
*/
public async getBankAccountSummary(tenantId: number, bankAccountId: number) {
const knex = this.tenancy.knex(tenantId);
const {
Account,
UncategorizedCashflowTransaction,
RecognizedBankTransaction,
MatchedBankTransaction,
} = this.tenancy.models(tenantId);

await initialize(knex, [
UncategorizedCashflowTransaction,
RecognizedBankTransaction,
MatchedBankTransaction,
]);
const bankAccount = await Account.query()
.findById(bankAccountId)
.throwIfNotFound();

// Retrieves the uncategorized transactions count of the given bank account.
const uncategorizedTranasctionsCount =
await UncategorizedCashflowTransaction.query()
.where('accountId', bankAccountId)
.count('id as total')
.first();
await UncategorizedCashflowTransaction.query().onBuild((q) => {
// Include just the given account.
q.where('accountId', bankAccountId);

// Only the not excluded.
q.modify('notExcluded');

// Only the not categorized.
q.modify('notCategorized');

// Only the not matched bank transactions.
q.withGraphJoined('matchedBankTransactions');
q.whereNull('matchedBankTransactions.id');

// Count the results.
q.count('uncategorized_cashflow_transactions.id as total');
q.first();
});

// Retrieves the recognized transactions count of the given bank account.
const recognizedTransactionsCount = await RecognizedBankTransaction.query()
Expand All @@ -43,8 +64,8 @@ export class GetBankAccountSummary {
.first();

const totalUncategorizedTransactions =
uncategorizedTranasctionsCount?.total;
const totalRecognizedTransactions = recognizedTransactionsCount?.total;
uncategorizedTranasctionsCount?.total || 0;
const totalRecognizedTransactions = recognizedTransactionsCount?.total || 0;

return {
name: bankAccount.name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ import HasTenancyService from '@/services/Tenancy/TenancyService';
import UnitOfWork from '@/services/UnitOfWork';
import { Inject, Service } from 'typedi';
import { validateTransactionNotCategorized } from './utils';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
import {
IBankTransactionUnexcludedEventPayload,
IBankTransactionUnexcludingEventPayload,
} from './_types';

@Service()
export class ExcludeBankTransaction {
Expand All @@ -11,6 +17,9 @@ export class ExcludeBankTransaction {
@Inject()
private uow: UnitOfWork;

@Inject()
private eventPublisher: EventPublisher;

/**
* Marks the given bank transaction as excluded.
* @param {number} tenantId
Expand All @@ -31,11 +40,23 @@ export class ExcludeBankTransaction {
validateTransactionNotCategorized(oldUncategorizedTransaction);

return this.uow.withTransaction(tenantId, async (trx) => {
await this.eventPublisher.emitAsync(events.bankTransactions.onExcluding, {
tenantId,
uncategorizedTransactionId,
trx,
} as IBankTransactionUnexcludingEventPayload);

await UncategorizedCashflowTransaction.query(trx)
.findById(uncategorizedTransactionId)
.patch({
excludedAt: new Date(),
});

await this.eventPublisher.emitAsync(events.bankTransactions.onExcluded, {
tenantId,
uncategorizedTransactionId,
trx,
} as IBankTransactionUnexcludedEventPayload);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ import HasTenancyService from '@/services/Tenancy/TenancyService';
import UnitOfWork from '@/services/UnitOfWork';
import { Inject, Service } from 'typedi';
import { validateTransactionNotCategorized } from './utils';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
import {
IBankTransactionExcludedEventPayload,
IBankTransactionExcludingEventPayload,
} from './_types';

@Service()
export class UnexcludeBankTransaction {
Expand All @@ -11,6 +17,9 @@ export class UnexcludeBankTransaction {
@Inject()
private uow: UnitOfWork;

@Inject()
private eventPublisher: EventPublisher;

/**
* Marks the given bank transaction as excluded.
* @param {number} tenantId
Expand All @@ -20,7 +29,7 @@ export class UnexcludeBankTransaction {
public async unexcludeBankTransaction(
tenantId: number,
uncategorizedTransactionId: number
) {
): Promise<void> {
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);

const oldUncategorizedTransaction =
Expand All @@ -31,11 +40,27 @@ export class UnexcludeBankTransaction {
validateTransactionNotCategorized(oldUncategorizedTransaction);

return this.uow.withTransaction(tenantId, async (trx) => {
await this.eventPublisher.emitAsync(
events.bankTransactions.onUnexcluding,
{
tenantId,
uncategorizedTransactionId,
} as IBankTransactionExcludingEventPayload
);

await UncategorizedCashflowTransaction.query(trx)
.findById(uncategorizedTransactionId)
.patch({
excludedAt: null,
});

await this.eventPublisher.emitAsync(
events.bankTransactions.onUnexcluded,
{
tenantId,
uncategorizedTransactionId,
} as IBankTransactionExcludedEventPayload
);
});
}
}
26 changes: 25 additions & 1 deletion packages/server/src/services/Banking/Exclude/_types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,30 @@
import { Knex } from "knex";

export interface ExcludedBankTransactionsQuery {
page?: number;
pageSize?: number;
accountId?: number;
}
}

export interface IBankTransactionUnexcludingEventPayload {
tenantId: number;
uncategorizedTransactionId: number;
trx?: Knex.Transaction
}

export interface IBankTransactionUnexcludedEventPayload {
tenantId: number;
uncategorizedTransactionId: number;
trx?: Knex.Transaction
}

export interface IBankTransactionExcludingEventPayload {
tenantId: number;
uncategorizedTransactionId: number;
trx?: Knex.Transaction
}
export interface IBankTransactionExcludedEventPayload {
tenantId: number;
uncategorizedTransactionId: number;
trx?: Knex.Transaction
}
Loading
Loading