mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 13:20:31 +00:00
Compare commits
17 Commits
reconcile-
...
v0.18.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
249eadaeaa | ||
|
|
59168bc691 | ||
|
|
81b26c6f13 | ||
|
|
da435d85d9 | ||
|
|
533006b90e | ||
|
|
d096e49d45 | ||
|
|
73acdb6240 | ||
|
|
38d4122d11 | ||
|
|
24a77c81b3 | ||
|
|
7f41b4280e | ||
|
|
aa89653967 | ||
|
|
b80bc95fa5 | ||
|
|
9a5befbee7 | ||
|
|
b7487f19d3 | ||
|
|
cd9039fe16 | ||
|
|
87f60f7461 | ||
|
|
202179ec0b |
@@ -38,15 +38,7 @@ export class BankingRulesController extends BaseController {
|
||||
body('conditions.*.value').exists(),
|
||||
|
||||
// Assign
|
||||
body('assign_category')
|
||||
.isString()
|
||||
.isIn([
|
||||
'interest_income',
|
||||
'other_income',
|
||||
'deposit',
|
||||
'expense',
|
||||
'owner_drawings',
|
||||
]),
|
||||
body('assign_category').isString(),
|
||||
body('assign_account_id').isInt({ min: 0 }),
|
||||
body('assign_payee').isString().optional({ nullable: true }),
|
||||
body('assign_memo').isString().optional({ nullable: true }),
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
export const CashflowTransactionTypes = {
|
||||
OtherIncome: 'Other income',
|
||||
OtherExpense: 'Other expense',
|
||||
OwnerDrawing: 'Owner drawing',
|
||||
OwnerContribution: 'Owner contribution',
|
||||
TransferToAccount: 'Transfer to account',
|
||||
TransferFromAccount: 'Transfer from account',
|
||||
};
|
||||
|
||||
export const TransactionTypes = {
|
||||
SaleInvoice: 'Sale invoice',
|
||||
SaleReceipt: 'Sale receipt',
|
||||
PaymentReceive: 'Payment receive',
|
||||
PaymentReceive: 'Payment received',
|
||||
Bill: 'Bill',
|
||||
BillPayment: 'Payment made',
|
||||
VendorOpeningBalance: 'Vendor opening balance',
|
||||
@@ -17,12 +26,10 @@ export const TransactionTypes = {
|
||||
OtherExpense: 'Other expense',
|
||||
OwnerDrawing: 'Owner drawing',
|
||||
InvoiceWriteOff: 'Invoice write-off',
|
||||
|
||||
CreditNote: 'transaction_type.credit_note',
|
||||
VendorCredit: 'transaction_type.vendor_credit',
|
||||
|
||||
RefundCreditNote: 'transaction_type.refund_credit_note',
|
||||
RefundVendorCredit: 'transaction_type.refund_vendor_credit',
|
||||
|
||||
LandedCost: 'transaction_type.landed_cost',
|
||||
CashflowTransaction: CashflowTransactionTypes,
|
||||
};
|
||||
|
||||
@@ -5,7 +5,8 @@ exports.up = function (knex) {
|
||||
.integer('uncategorized_transaction_id')
|
||||
.unsigned()
|
||||
.references('id')
|
||||
.inTable('uncategorized_cashflow_transactions');
|
||||
.inTable('uncategorized_cashflow_transactions')
|
||||
.withKeyName('recognizedBankTransactionsUncategorizedTransIdForeign');
|
||||
table
|
||||
.integer('bank_rule_id')
|
||||
.unsigned()
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
exports.up = function (knex) {
|
||||
return knex.schema.table('uncategorized_cashflow_transactions', (table) => {
|
||||
table.integer('recognized_transaction_id').unsigned();
|
||||
table
|
||||
.integer('recognized_transaction_id')
|
||||
.unsigned()
|
||||
.references('id')
|
||||
.inTable('recognized_bank_transactions')
|
||||
.withKeyName('uncategorizedCashflowTransRecognizedTranIdForeign');
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
exports.up = function (knex) {
|
||||
return knex.schema.createTable('matched_bank_transactions', (table) => {
|
||||
table.increments('id');
|
||||
table.integer('uncategorized_transaction_id').unsigned();
|
||||
table
|
||||
.integer('uncategorized_transaction_id')
|
||||
.unsigned()
|
||||
.references('id')
|
||||
.inTable('uncategorized_cashflow_transactions');
|
||||
table.string('reference_type');
|
||||
table.integer('reference_id').unsigned();
|
||||
table.decimal('amount');
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
exports.up = function (knex) {
|
||||
return knex('accounts_transactions')
|
||||
.whereIn('referenceType', [
|
||||
'OtherIncome',
|
||||
'OtherExpense',
|
||||
'OwnerDrawing',
|
||||
'OwnerContribution',
|
||||
'TransferToAccount',
|
||||
'TransferFromAccount',
|
||||
])
|
||||
.update({
|
||||
transactionType: knex.raw(`
|
||||
CASE
|
||||
WHEN REFERENCE_TYPE = 'OtherIncome' THEN 'OtherIncome'
|
||||
WHEN REFERENCE_TYPE = 'OtherExpense' THEN 'OtherExpense'
|
||||
WHEN REFERENCE_TYPE = 'OwnerDrawing' THEN 'OwnerDrawing'
|
||||
WHEN REFERENCE_TYPE = 'OwnerContribution' THEN 'OwnerContribution'
|
||||
WHEN REFERENCE_TYPE = 'TransferToAccount' THEN 'TransferToAccount'
|
||||
WHEN REFERENCE_TYPE = 'TransferFromAccount' THEN 'TransferFromAccount'
|
||||
END
|
||||
`),
|
||||
referenceType: knex.raw(`
|
||||
CASE
|
||||
WHEN REFERENCE_TYPE IN ('OtherIncome', 'OtherExpense', 'OwnerDrawing', 'OwnerContribution', 'TransferToAccount', 'TransferFromAccount') THEN 'CashflowTransaction'
|
||||
ELSE REFERENCE_TYPE
|
||||
END
|
||||
`),
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function (knex) {
|
||||
return knex('accounts_transactions')
|
||||
.whereIn('transactionType', [
|
||||
'OtherIncome',
|
||||
'OtherExpense',
|
||||
'OwnerDrawing',
|
||||
'OwnerContribution',
|
||||
'TransferToAccount',
|
||||
'TransferFromAccount',
|
||||
])
|
||||
.update({
|
||||
referenceType: knex.raw(`
|
||||
CASE
|
||||
WHEN TRANSACTION_TYPE = 'OtherIncome' THEN 'OtherIncome'
|
||||
WHEN TRANSACTION_TYPE = 'OtherExpense' THEN 'OtherExpense'
|
||||
WHEN TRANSACTION_TYPE = 'OwnerDrawing' THEN 'OwnerDrawing'
|
||||
WHEN TRANSACTION_TYPE = 'OwnerContribution' THEN 'OwnerContribution'
|
||||
WHEN TRANSACTION_TYPE = 'TransferToAccount' THEN 'TransferToAccount'
|
||||
WHEN TRANSACTION_TYPE = 'TransferFromAccount' THEN 'TransferFromAccount'
|
||||
ELSE REFERENCE_TYPE
|
||||
END
|
||||
`),
|
||||
transactionType: knex.raw(`
|
||||
CASE
|
||||
WHEN TRANSACTION_TYPE IN ('OtherIncome', 'OtherExpense', 'OwnerDrawing', 'OwnerContribution', 'TransferToAccount', 'TransferFromAccount') THEN NULL
|
||||
ELSE TRANSACTION_TYPE
|
||||
END
|
||||
`),
|
||||
});
|
||||
};
|
||||
@@ -66,7 +66,9 @@ export interface IAccountTransaction {
|
||||
referenceId: number;
|
||||
|
||||
referenceNumber?: string;
|
||||
|
||||
transactionNumber?: string;
|
||||
transactionType?: string;
|
||||
|
||||
note?: string;
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Knex } from 'knex';
|
||||
import {
|
||||
IFinancialSheetCommonMeta,
|
||||
INumberFormatQuery,
|
||||
@@ -257,7 +258,6 @@ export interface IUncategorizedCashflowTransaction {
|
||||
categorized: boolean;
|
||||
}
|
||||
|
||||
|
||||
export interface CreateUncategorizedTransactionDTO {
|
||||
date: Date | string;
|
||||
accountId: number;
|
||||
@@ -269,3 +269,16 @@ export interface CreateUncategorizedTransactionDTO {
|
||||
plaidTransactionId?: string | null;
|
||||
batch?: string;
|
||||
}
|
||||
|
||||
export interface IUncategorizedTransactionCreatingEventPayload {
|
||||
tenantId: number;
|
||||
createUncategorizedTransactionDTO: CreateUncategorizedTransactionDTO;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface IUncategorizedTransactionCreatedEventPayload {
|
||||
tenantId: number;
|
||||
uncategorizedTransaction: any;
|
||||
createUncategorizedTransactionDTO: CreateUncategorizedTransactionDTO;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
@@ -130,8 +130,9 @@ export interface ICommandCashflowDeletedPayload {
|
||||
|
||||
export interface ICashflowTransactionCategorizedPayload {
|
||||
tenantId: number;
|
||||
cashflowTransactionId: number;
|
||||
uncategorizedTransaction: any;
|
||||
cashflowTransaction: ICashflowTransaction;
|
||||
categorizeDTO: any;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
export interface ICashflowTransactionUncategorizingPayload {
|
||||
|
||||
@@ -29,4 +29,9 @@ export interface ICashflowAccountTransaction {
|
||||
|
||||
date: Date;
|
||||
formattedDate: string;
|
||||
|
||||
status: string;
|
||||
formattedStatus: string;
|
||||
|
||||
uncategorizedTransactionId: number;
|
||||
}
|
||||
|
||||
8
packages/server/src/interfaces/Import.ts
Normal file
8
packages/server/src/interfaces/Import.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { ImportFilePreviewPOJO } from "@/services/Import/interfaces";
|
||||
|
||||
|
||||
export interface IImportFileCommitedEventPayload {
|
||||
tenantId: number;
|
||||
importId: number;
|
||||
meta: ImportFilePreviewPOJO;
|
||||
}
|
||||
@@ -40,6 +40,8 @@ export interface ILedgerEntry {
|
||||
date: Date | string;
|
||||
|
||||
transactionType: string;
|
||||
transactionSubType: string;
|
||||
|
||||
transactionId: number;
|
||||
|
||||
transactionNumber?: string;
|
||||
|
||||
@@ -110,6 +110,9 @@ 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';
|
||||
import { DecrementUncategorizedTransactionOnCategorize } from '@/services/Cashflow/subscribers/DecrementUncategorizedTransactionOnCategorize';
|
||||
|
||||
export default () => {
|
||||
return new EventPublisher();
|
||||
@@ -258,6 +261,9 @@ export const susbcribers = () => {
|
||||
// Bank Rules
|
||||
TriggerRecognizedTransactions,
|
||||
UnlinkBankRuleOnDeleteBankRule,
|
||||
DecrementUncategorizedTransactionOnMatching,
|
||||
DecrementUncategorizedTransactionOnExclude,
|
||||
DecrementUncategorizedTransactionOnCategorize,
|
||||
|
||||
// Validate matching
|
||||
ValidateMatchingOnCashflowDelete,
|
||||
@@ -266,7 +272,7 @@ export const susbcribers = () => {
|
||||
ValidateMatchingOnPaymentReceivedDelete,
|
||||
ValidateMatchingOnPaymentMadeDelete,
|
||||
|
||||
// Plaid
|
||||
// Plaid
|
||||
RecognizeSyncedBankTranasctions,
|
||||
];
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ export default class AccountTransaction extends TenantModel {
|
||||
debit: number;
|
||||
exchangeRate: number;
|
||||
taxRate: number;
|
||||
transactionType: string;
|
||||
|
||||
/**
|
||||
* Table name
|
||||
@@ -53,7 +54,7 @@ export default class AccountTransaction extends TenantModel {
|
||||
* @return {string}
|
||||
*/
|
||||
get referenceTypeFormatted() {
|
||||
return getTransactionTypeLabel(this.referenceType);
|
||||
return getTransactionTypeLabel(this.referenceType, this.transactionType);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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';
|
||||
import { getCashflowTransactionFormattedType } from '@/utils/transactions-types';
|
||||
|
||||
export default class CashflowTransaction extends TenantModel {
|
||||
transactionType: string;
|
||||
amount: number;
|
||||
@@ -64,7 +64,7 @@ export default class CashflowTransaction extends TenantModel {
|
||||
* @returns {string}
|
||||
*/
|
||||
get transactionTypeFormatted() {
|
||||
return getTransactionTypeLabel(this.transactionType);
|
||||
return getCashflowTransactionFormattedType(this.transactionType);
|
||||
}
|
||||
|
||||
get typeMeta() {
|
||||
@@ -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.
|
||||
*/
|
||||
@@ -131,8 +159,7 @@ export default class CashflowTransaction extends TenantModel {
|
||||
to: 'accounts_transactions.referenceId',
|
||||
},
|
||||
filter(builder) {
|
||||
const referenceTypes = getCashflowAccountTransactionsTypes();
|
||||
builder.whereIn('reference_type', referenceTypes);
|
||||
builder.where('reference_type', 'CashflowTransaction');
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -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');
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -158,56 +184,4 @@ export default class UncategorizedCashflowTransaction extends mixin(
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the count of uncategorized transactions for the associated account
|
||||
* based on the specified operation.
|
||||
* @param {QueryContext} queryContext - The query context for the transaction.
|
||||
* @param {boolean} increment - Indicates whether to increment or decrement the count.
|
||||
*/
|
||||
private async updateUncategorizedTransactionCount(
|
||||
queryContext: QueryContext,
|
||||
increment: boolean,
|
||||
amount: number = 1
|
||||
) {
|
||||
const operation = increment ? 'increment' : 'decrement';
|
||||
|
||||
await Account.query(queryContext.transaction)
|
||||
.findById(this.accountId)
|
||||
[operation]('uncategorized_transactions', amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs after insert.
|
||||
* @param {QueryContext} queryContext
|
||||
*/
|
||||
public async $afterInsert(queryContext) {
|
||||
await super.$afterInsert(queryContext);
|
||||
await this.updateUncategorizedTransactionCount(queryContext, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs after update.
|
||||
* @param {ModelOptions} opt
|
||||
* @param {QueryContext} queryContext
|
||||
*/
|
||||
public async $afterUpdate(
|
||||
opt: ModelOptions,
|
||||
queryContext: QueryContext
|
||||
): Promise<any> {
|
||||
await super.$afterUpdate(opt, queryContext);
|
||||
|
||||
if (this.id && this.categorized) {
|
||||
await this.updateUncategorizedTransactionCount(queryContext, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs after delete.
|
||||
* @param {QueryContext} queryContext
|
||||
*/
|
||||
public async $afterDelete(queryContext: QueryContext) {
|
||||
await super.$afterDelete(queryContext);
|
||||
await this.updateUncategorizedTransactionCount(queryContext, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ export const transformLedgerEntryToTransaction = (
|
||||
referenceId: entry.transactionId,
|
||||
|
||||
transactionNumber: entry.transactionNumber,
|
||||
transactionType: entry.transactionSubType,
|
||||
|
||||
referenceNumber: entry.referenceNumber,
|
||||
|
||||
note: entry.note,
|
||||
|
||||
@@ -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 {
|
||||
@@ -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()
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
@@ -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
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
@@ -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
|
||||
@@ -20,7 +29,7 @@ export class UnexcludeBankTransaction {
|
||||
public async unexcludeBankTransaction(
|
||||
tenantId: number,
|
||||
uncategorizedTransactionId: number
|
||||
) {
|
||||
): Promise<void> {
|
||||
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
|
||||
|
||||
const oldUncategorizedTransaction =
|
||||
@@ -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
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import events from '@/subscribers/events';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import {
|
||||
IBankTransactionExcludedEventPayload,
|
||||
IBankTransactionUnexcludedEventPayload,
|
||||
} from '../_types';
|
||||
|
||||
@Service()
|
||||
export class DecrementUncategorizedTransactionOnExclude {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
/**
|
||||
* Constructor method.
|
||||
*/
|
||||
public attach(bus) {
|
||||
bus.subscribe(
|
||||
events.bankTransactions.onExcluded,
|
||||
this.decrementUnCategorizedTransactionsOnExclude.bind(this)
|
||||
);
|
||||
bus.subscribe(
|
||||
events.bankTransactions.onUnexcluded,
|
||||
this.incrementUnCategorizedTransactionsOnUnexclude.bind(this)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the cashflow transaction whether matched with bank transaction on deleting.
|
||||
* @param {IManualJournalDeletingPayload}
|
||||
*/
|
||||
public async decrementUnCategorizedTransactionsOnExclude({
|
||||
tenantId,
|
||||
uncategorizedTransactionId,
|
||||
trx,
|
||||
}: IBankTransactionExcludedEventPayload) {
|
||||
const { UncategorizedCashflowTransaction, Account } =
|
||||
this.tenancy.models(tenantId);
|
||||
|
||||
const transaction = await UncategorizedCashflowTransaction.query(
|
||||
trx
|
||||
).findById(uncategorizedTransactionId);
|
||||
|
||||
await Account.query(trx)
|
||||
.findById(transaction.accountId)
|
||||
.decrement('uncategorizedTransactions', 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the cashflow transaction whether matched with bank transaction on deleting.
|
||||
* @param {IManualJournalDeletingPayload}
|
||||
*/
|
||||
public async incrementUnCategorizedTransactionsOnUnexclude({
|
||||
tenantId,
|
||||
uncategorizedTransactionId,
|
||||
trx,
|
||||
}: IBankTransactionUnexcludedEventPayload) {
|
||||
const { UncategorizedCashflowTransaction, Account } =
|
||||
this.tenancy.models(tenantId);
|
||||
|
||||
const transaction = await UncategorizedCashflowTransaction.query().findById(
|
||||
uncategorizedTransactionId
|
||||
);
|
||||
//
|
||||
await Account.query(trx)
|
||||
.findById(transaction.accountId)
|
||||
.increment('uncategorizedTransactions', 1);
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,9 @@ export class GetMatchedTransactionBillsTransformer extends Transformer {
|
||||
'transactionNo',
|
||||
'transactionType',
|
||||
'transsactionTypeFormatted',
|
||||
'transactionNormal',
|
||||
'referenceId',
|
||||
'referenceType',
|
||||
];
|
||||
};
|
||||
|
||||
@@ -100,4 +103,29 @@ export class GetMatchedTransactionBillsTransformer extends Transformer {
|
||||
protected transsactionTypeFormatted() {
|
||||
return 'Bill';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the bill transaction normal (debit or credit).
|
||||
* @returns {string}
|
||||
*/
|
||||
protected transactionNormal() {
|
||||
return 'credit';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the match transaction reference id.
|
||||
* @param bill
|
||||
* @returns {number}
|
||||
*/
|
||||
protected referenceId(bill) {
|
||||
return bill.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the match transaction referenece type.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected referenceType() {
|
||||
return 'Bill';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
import { Transformer } from '@/lib/Transformer/Transformer';
|
||||
|
||||
export class GetMatchedTransactionCashflowTransformer extends Transformer {
|
||||
/**
|
||||
* Include these attributes to sale credit note object.
|
||||
* @returns {Array}
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return [
|
||||
'referenceNo',
|
||||
'amount',
|
||||
'amountFormatted',
|
||||
'transactionNo',
|
||||
'date',
|
||||
'dateFormatted',
|
||||
'transactionId',
|
||||
'transactionNo',
|
||||
'transactionType',
|
||||
'transsactionTypeFormatted',
|
||||
'transactionNormal',
|
||||
'referenceId',
|
||||
'referenceType',
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Exclude all attributes.
|
||||
* @returns {Array<string>}
|
||||
*/
|
||||
public excludeAttributes = (): string[] => {
|
||||
return ['*'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the invoice reference number.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected referenceNo(invoice) {
|
||||
return invoice.referenceNo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the transaction amount.
|
||||
* @param transaction
|
||||
* @returns {number}
|
||||
*/
|
||||
protected amount(transaction) {
|
||||
return transaction.amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the transaction formatted amount.
|
||||
* @param transaction
|
||||
* @returns {string}
|
||||
*/
|
||||
protected amountFormatted(transaction) {
|
||||
return this.formatNumber(transaction.amount, {
|
||||
currencyCode: transaction.currencyCode,
|
||||
money: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the date of the invoice.
|
||||
* @param invoice
|
||||
* @returns {Date}
|
||||
*/
|
||||
protected date(transaction) {
|
||||
return transaction.date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the date of the invoice.
|
||||
* @param invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected dateFormatted(transaction) {
|
||||
return this.formatDate(transaction.date);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the transaction ID of the invoice.
|
||||
* @param invoice
|
||||
* @returns {number}
|
||||
*/
|
||||
protected transactionId(transaction) {
|
||||
return transaction.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the invoice transaction number.
|
||||
* @param invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected transactionNo(transaction) {
|
||||
return transaction.transactionNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the invoice transaction type.
|
||||
* @param invoice
|
||||
* @returns {String}
|
||||
*/
|
||||
protected transactionType(transaction) {
|
||||
return transaction.transactionType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the invoice formatted transaction type.
|
||||
* @param invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected transsactionTypeFormatted(transaction) {
|
||||
return transaction.transactionTypeFormatted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the cashflow transaction normal (credit or debit).
|
||||
* @param transaction
|
||||
* @returns {string}
|
||||
*/
|
||||
protected transactionNormal(transaction) {
|
||||
return transaction.isCashCredit ? 'credit' : 'debit';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the cashflow transaction reference id.
|
||||
* @param transaction
|
||||
* @returns {number}
|
||||
*/
|
||||
protected referenceId(transaction) {
|
||||
return transaction.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the cashflow transaction reference type.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected referenceType() {
|
||||
return 'CashflowTransaction';
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,9 @@ export class GetMatchedTransactionExpensesTransformer extends Transformer {
|
||||
'transactionNo',
|
||||
'transactionType',
|
||||
'transsactionTypeFormatted',
|
||||
'transactionNormal',
|
||||
'referenceType',
|
||||
'referenceId',
|
||||
];
|
||||
};
|
||||
|
||||
@@ -111,4 +114,29 @@ export class GetMatchedTransactionExpensesTransformer extends Transformer {
|
||||
protected transsactionTypeFormatted() {
|
||||
return 'Expense';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the expense transaction normal (credit or debit).
|
||||
* @returns {string}
|
||||
*/
|
||||
protected transactionNormal() {
|
||||
return 'credit';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the transaction reference type.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected referenceType() {
|
||||
return 'Expense';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the transaction reference id.
|
||||
* @param transaction
|
||||
* @returns {number}
|
||||
*/
|
||||
protected referenceId(transaction) {
|
||||
return transaction.id;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,9 @@ export class GetMatchedTransactionInvoicesTransformer extends Transformer {
|
||||
'transactionNo',
|
||||
'transactionType',
|
||||
'transsactionTypeFormatted',
|
||||
'transactionNormal',
|
||||
'referenceType',
|
||||
'referenceId'
|
||||
];
|
||||
};
|
||||
|
||||
@@ -49,7 +52,7 @@ export class GetMatchedTransactionInvoicesTransformer extends Transformer {
|
||||
* @param invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formatAmount(invoice) {
|
||||
protected amountFormatted(invoice) {
|
||||
return this.formatNumber(invoice.dueAmount, {
|
||||
currencyCode: invoice.currencyCode,
|
||||
money: true,
|
||||
@@ -79,7 +82,7 @@ export class GetMatchedTransactionInvoicesTransformer extends Transformer {
|
||||
* @param invoice
|
||||
* @returns {number}
|
||||
*/
|
||||
protected getTransactionId(invoice) {
|
||||
protected transactionId(invoice) {
|
||||
return invoice.id;
|
||||
}
|
||||
/**
|
||||
@@ -108,4 +111,28 @@ export class GetMatchedTransactionInvoicesTransformer extends Transformer {
|
||||
protected transsactionTypeFormatted(invoice) {
|
||||
return 'Sale invoice';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the transaction normal of invoice (credit or debit).
|
||||
* @returns {string}
|
||||
*/
|
||||
protected transactionNormal() {
|
||||
return 'debit';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the transaction reference type.
|
||||
* @returns {string}
|
||||
*/ protected referenceType() {
|
||||
return 'SaleInvoice';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the transaction reference id.
|
||||
* @param transaction
|
||||
* @returns {number}
|
||||
*/
|
||||
protected referenceId(transaction) {
|
||||
return transaction.id;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { sumBy } from 'lodash';
|
||||
import { Transformer } from '@/lib/Transformer/Transformer';
|
||||
import { AccountNormal } from '@/interfaces';
|
||||
|
||||
export class GetMatchedTransactionManualJournalsTransformer extends Transformer {
|
||||
/**
|
||||
@@ -17,6 +19,9 @@ export class GetMatchedTransactionManualJournalsTransformer extends Transformer
|
||||
'transactionNo',
|
||||
'transactionType',
|
||||
'transsactionTypeFormatted',
|
||||
'transactionNormal',
|
||||
'referenceType',
|
||||
'referenceId',
|
||||
];
|
||||
};
|
||||
|
||||
@@ -37,13 +42,20 @@ export class GetMatchedTransactionManualJournalsTransformer extends Transformer
|
||||
return manualJournal.referenceNo;
|
||||
}
|
||||
|
||||
protected total(manualJournal) {
|
||||
const credit = sumBy(manualJournal?.entries, 'credit');
|
||||
const debit = sumBy(manualJournal?.entries, 'debit');
|
||||
|
||||
return debit - credit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the manual journal amount.
|
||||
* @param manualJournal
|
||||
* @returns {number}
|
||||
*/
|
||||
protected amount(manualJournal) {
|
||||
return manualJournal.amount;
|
||||
return Math.abs(this.total(manualJournal));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -107,5 +119,31 @@ export class GetMatchedTransactionManualJournalsTransformer extends Transformer
|
||||
protected transsactionTypeFormatted() {
|
||||
return 'Manual Journal';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the manual journal transaction normal (credit or debit).
|
||||
* @returns {string}
|
||||
*/
|
||||
protected transactionNormal(transaction) {
|
||||
const amount = this.total(transaction);
|
||||
|
||||
return amount >= 0 ? AccountNormal.DEBIT : AccountNormal.CREDIT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the manual journal reference type.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected referenceType() {
|
||||
return 'ManualJournal';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the manual journal reference id.
|
||||
* @param transaction
|
||||
* @returns {number}
|
||||
*/
|
||||
protected referenceId(transaction) {
|
||||
return transaction.id;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import { GetMatchedTransactionsByBills } from './GetMatchedTransactionsByBills';
|
||||
import { GetMatchedTransactionsByManualJournals } from './GetMatchedTransactionsByManualJournals';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { sortClosestMatchTransactions } from './_utils';
|
||||
import { GetMatchedTransactionsByCashflow } from './GetMatchedTransactionsByCashflow';
|
||||
import { GetMatchedTransactionsByInvoices } from './GetMatchedTransactionsByInvoices';
|
||||
|
||||
@Service()
|
||||
export class GetMatchedTransactions {
|
||||
@@ -15,7 +17,7 @@ export class GetMatchedTransactions {
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private getMatchedInvoicesService: GetMatchedTransactionsByExpenses;
|
||||
private getMatchedInvoicesService: GetMatchedTransactionsByInvoices;
|
||||
|
||||
@Inject()
|
||||
private getMatchedBillsService: GetMatchedTransactionsByBills;
|
||||
@@ -26,6 +28,9 @@ export class GetMatchedTransactions {
|
||||
@Inject()
|
||||
private getMatchedExpensesService: GetMatchedTransactionsByExpenses;
|
||||
|
||||
@Inject()
|
||||
private getMatchedCashflowService: GetMatchedTransactionsByCashflow;
|
||||
|
||||
/**
|
||||
* Registered matched transactions types.
|
||||
*/
|
||||
@@ -35,6 +40,7 @@ export class GetMatchedTransactions {
|
||||
{ type: 'Bill', service: this.getMatchedBillsService },
|
||||
{ type: 'Expense', service: this.getMatchedExpensesService },
|
||||
{ type: 'ManualJournal', service: this.getMatchedManualJournalService },
|
||||
{ type: 'Cashflow', service: this.getMatchedCashflowService },
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { initialize } from 'objection';
|
||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||
import { GetMatchedTransactionBillsTransformer } from './GetMatchedTransactionBillsTransformer';
|
||||
import { GetMatchedTransactionsFilter, MatchedTransactionPOJO } from './types';
|
||||
@@ -22,10 +23,25 @@ export class GetMatchedTransactionsByBills extends GetMatchedTransactionsByType
|
||||
tenantId: number,
|
||||
filter: GetMatchedTransactionsFilter
|
||||
) {
|
||||
const { Bill } = this.tenancy.models(tenantId);
|
||||
const { Bill, MatchedBankTransaction } = this.tenancy.models(tenantId);
|
||||
const knex = this.tenancy.knex(tenantId);
|
||||
|
||||
// Initialize the models metadata.
|
||||
await initialize(knex, [Bill, MatchedBankTransaction]);
|
||||
|
||||
// Retrieves the bill matches.
|
||||
const bills = await Bill.query().onBuild((q) => {
|
||||
q.whereNotExists(Bill.relatedQuery('matchedBankTransaction'));
|
||||
q.withGraphJoined('matchedBankTransaction');
|
||||
q.whereNull('matchedBankTransaction.id');
|
||||
q.modify('published');
|
||||
|
||||
if (filter.fromDate) {
|
||||
q.where('billDate', '>=', filter.fromDate);
|
||||
}
|
||||
if (filter.toDate) {
|
||||
q.where('billDate', '<=', filter.toDate);
|
||||
}
|
||||
q.orderBy('billDate', 'DESC');
|
||||
});
|
||||
|
||||
return this.transformer.transform(
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { initialize } from 'objection';
|
||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||
import { GetMatchedTransactionsByType } from './GetMatchedTransactionsByType';
|
||||
import { GetMatchedTransactionCashflowTransformer } from './GetMatchedTransactionCashflowTransformer';
|
||||
import { GetMatchedTransactionsFilter } from './types';
|
||||
|
||||
@Service()
|
||||
export class GetMatchedTransactionsByCashflow extends GetMatchedTransactionsByType {
|
||||
@Inject()
|
||||
private transformer: TransformerInjectable;
|
||||
|
||||
/**
|
||||
* Retrieve the matched transactions of cash flow.
|
||||
* @param {number} tenantId
|
||||
* @param {GetMatchedTransactionsFilter} filter
|
||||
* @returns
|
||||
*/
|
||||
async getMatchedTransactions(
|
||||
tenantId: number,
|
||||
filter: Omit<GetMatchedTransactionsFilter, 'transactionType'>
|
||||
) {
|
||||
const { CashflowTransaction, MatchedBankTransaction } =
|
||||
this.tenancy.models(tenantId);
|
||||
const knex = this.tenancy.knex(tenantId);
|
||||
|
||||
// Initialize the ORM models metadata.
|
||||
await initialize(knex, [CashflowTransaction, MatchedBankTransaction]);
|
||||
|
||||
const transactions = await CashflowTransaction.query().onBuild((q) => {
|
||||
// Not matched to bank transaction.
|
||||
q.withGraphJoined('matchedBankTransaction');
|
||||
q.whereNull('matchedBankTransaction.id');
|
||||
|
||||
// Not categorized.
|
||||
q.modify('notCategorized');
|
||||
|
||||
// Published.
|
||||
q.modify('published');
|
||||
|
||||
if (filter.fromDate) {
|
||||
q.where('date', '>=', filter.fromDate);
|
||||
}
|
||||
if (filter.toDate) {
|
||||
q.where('date', '<=', filter.toDate);
|
||||
}
|
||||
q.orderBy('date', 'DESC');
|
||||
});
|
||||
|
||||
return this.transformer.transform(
|
||||
tenantId,
|
||||
transactions,
|
||||
new GetMatchedTransactionCashflowTransformer()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the matched transaction of cash flow.
|
||||
* @param {number} tenantId
|
||||
* @param {number} transactionId
|
||||
* @returns
|
||||
*/
|
||||
async getMatchedTransaction(tenantId: number, transactionId: number) {
|
||||
const { CashflowTransaction, MatchedBankTransaction } =
|
||||
this.tenancy.models(tenantId);
|
||||
const knex = this.tenancy.knex(tenantId);
|
||||
|
||||
// Initialize the ORM models metadata.
|
||||
await initialize(knex, [CashflowTransaction, MatchedBankTransaction]);
|
||||
|
||||
const transactions = await CashflowTransaction.query()
|
||||
.findById(transactionId)
|
||||
.withGraphJoined('matchedBankTransaction')
|
||||
.whereNull('matchedBankTransaction.id')
|
||||
.modify('notCategorized')
|
||||
.modify('published')
|
||||
.throwIfNotFound();
|
||||
|
||||
return this.transformer.transform(
|
||||
tenantId,
|
||||
transactions,
|
||||
new GetMatchedTransactionCashflowTransformer()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { initialize } from 'objection';
|
||||
import { GetMatchedTransactionsFilter, MatchedTransactionPOJO } from './types';
|
||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
@@ -23,22 +24,34 @@ export class GetMatchedTransactionsByExpenses extends GetMatchedTransactionsByTy
|
||||
tenantId: number,
|
||||
filter: GetMatchedTransactionsFilter
|
||||
) {
|
||||
const { Expense } = this.tenancy.models(tenantId);
|
||||
const { Expense, MatchedBankTransaction } = this.tenancy.models(tenantId);
|
||||
const knex = this.tenancy.knex(tenantId);
|
||||
|
||||
// Initialize the models metadata.
|
||||
await initialize(knex, [Expense, MatchedBankTransaction]);
|
||||
|
||||
// Retrieve the expense matches.
|
||||
const expenses = await Expense.query().onBuild((query) => {
|
||||
query.whereNotExists(Expense.relatedQuery('matchedBankTransaction'));
|
||||
// Filter out the not matched to bank transactions.
|
||||
query.withGraphJoined('matchedBankTransaction');
|
||||
query.whereNull('matchedBankTransaction.id');
|
||||
|
||||
// Filter the published onyl
|
||||
query.modify('filterByPublished');
|
||||
|
||||
if (filter.fromDate) {
|
||||
query.where('payment_date', '>=', filter.fromDate);
|
||||
query.where('paymentDate', '>=', filter.fromDate);
|
||||
}
|
||||
if (filter.toDate) {
|
||||
query.where('payment_date', '<=', filter.toDate);
|
||||
query.where('paymentDate', '<=', filter.toDate);
|
||||
}
|
||||
if (filter.minAmount) {
|
||||
query.where('total_amount', '>=', filter.minAmount);
|
||||
query.where('totalAmount', '>=', filter.minAmount);
|
||||
}
|
||||
if (filter.maxAmount) {
|
||||
query.where('total_amount', '<=', filter.maxAmount);
|
||||
query.where('totalAmount', '<=', filter.maxAmount);
|
||||
}
|
||||
query.orderBy('paymentDate', 'DESC');
|
||||
});
|
||||
return this.transformer.transform(
|
||||
tenantId,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { initialize } from 'objection';
|
||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||
import { GetMatchedTransactionInvoicesTransformer } from './GetMatchedTransactionInvoicesTransformer';
|
||||
import {
|
||||
@@ -6,7 +8,6 @@ import {
|
||||
MatchedTransactionsPOJO,
|
||||
} from './types';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { GetMatchedTransactionsByType } from './GetMatchedTransactionsByType';
|
||||
|
||||
@Service()
|
||||
@@ -27,10 +28,27 @@ export class GetMatchedTransactionsByInvoices extends GetMatchedTransactionsByTy
|
||||
tenantId: number,
|
||||
filter: GetMatchedTransactionsFilter
|
||||
): Promise<MatchedTransactionsPOJO> {
|
||||
const { SaleInvoice } = this.tenancy.models(tenantId);
|
||||
const { SaleInvoice, MatchedBankTransaction } =
|
||||
this.tenancy.models(tenantId);
|
||||
const knex = this.tenancy.knex(tenantId);
|
||||
|
||||
// Initialize the models metadata.
|
||||
await initialize(knex, [SaleInvoice, MatchedBankTransaction]);
|
||||
|
||||
// Retrieve the invoices that not matched, unpaid.
|
||||
const invoices = await SaleInvoice.query().onBuild((q) => {
|
||||
q.whereNotExists(SaleInvoice.relatedQuery('matchedBankTransaction'));
|
||||
q.withGraphJoined('matchedBankTransaction');
|
||||
q.whereNull('matchedBankTransaction.id');
|
||||
q.modify('unpaid');
|
||||
q.modify('published');
|
||||
|
||||
if (filter.fromDate) {
|
||||
q.where('invoiceDate', '>=', filter.fromDate);
|
||||
}
|
||||
if (filter.toDate) {
|
||||
q.where('invoiceDate', '<=', filter.toDate);
|
||||
}
|
||||
q.orderBy('invoiceDate', 'DESC');
|
||||
});
|
||||
|
||||
return this.transformer.transform(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { initialize } from 'objection';
|
||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||
import { GetMatchedTransactionManualJournalsTransformer } from './GetMatchedTransactionManualJournalsTransformer';
|
||||
import { GetMatchedTransactionsByType } from './GetMatchedTransactionsByType';
|
||||
@@ -19,12 +20,26 @@ export class GetMatchedTransactionsByManualJournals extends GetMatchedTransactio
|
||||
tenantId: number,
|
||||
filter: Omit<GetMatchedTransactionsFilter, 'transactionType'>
|
||||
) {
|
||||
const { ManualJournal } = this.tenancy.models(tenantId);
|
||||
const { ManualJournal, ManualJournalEntry, MatchedBankTransaction } =
|
||||
this.tenancy.models(tenantId);
|
||||
const knex = this.tenancy.knex(tenantId);
|
||||
|
||||
await initialize(knex, [
|
||||
ManualJournal,
|
||||
ManualJournalEntry,
|
||||
MatchedBankTransaction,
|
||||
]);
|
||||
const accountId = 1000;
|
||||
|
||||
const manualJournals = await ManualJournal.query().onBuild((query) => {
|
||||
query.whereNotExists(
|
||||
ManualJournal.relatedQuery('matchedBankTransaction')
|
||||
);
|
||||
query.withGraphJoined('matchedBankTransaction');
|
||||
query.whereNull('matchedBankTransaction.id');
|
||||
|
||||
query.withGraphJoined('entries');
|
||||
query.where('entries.accountId', accountId);
|
||||
|
||||
query.modify('filterByPublished');
|
||||
|
||||
if (filter.fromDate) {
|
||||
query.where('date', '>=', filter.fromDate);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isEmpty, sumBy } from 'lodash';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { Knex } from 'knex';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { PromisePool } from '@supercharge/promise-pool';
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from './types';
|
||||
import { MatchTransactionsTypes } from './MatchTransactionsTypes';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import { sumMatchTranasctions } from './_utils';
|
||||
|
||||
@Service()
|
||||
export class MatchBankTransactions {
|
||||
@@ -90,9 +91,8 @@ export class MatchBankTransactions {
|
||||
throw new ServiceError(error);
|
||||
}
|
||||
// Calculate the total given matching transactions.
|
||||
const totalMatchedTranasctions = sumBy(
|
||||
validatationResult.results,
|
||||
'amount'
|
||||
const totalMatchedTranasctions = sumMatchTranasctions(
|
||||
validatationResult.results
|
||||
);
|
||||
// Validates the total given matching transcations whether is not equal
|
||||
// uncategorized transaction amount.
|
||||
|
||||
@@ -4,6 +4,8 @@ import { GetMatchedTransactionsByBills } from './GetMatchedTransactionsByBills';
|
||||
import { GetMatchedTransactionsByManualJournals } from './GetMatchedTransactionsByManualJournals';
|
||||
import { MatchTransactionsTypesRegistry } from './MatchTransactionsTypesRegistry';
|
||||
import { GetMatchedTransactionsByInvoices } from './GetMatchedTransactionsByInvoices';
|
||||
import { GetMatchedTransactionCashflowTransformer } from './GetMatchedTransactionCashflowTransformer';
|
||||
import { GetMatchedTransactionsByCashflow } from './GetMatchedTransactionsByCashflow';
|
||||
|
||||
@Service()
|
||||
export class MatchTransactionsTypes {
|
||||
@@ -25,6 +27,10 @@ export class MatchTransactionsTypes {
|
||||
type: 'ManualJournal',
|
||||
service: GetMatchedTransactionsByManualJournals,
|
||||
},
|
||||
{
|
||||
type: 'CashflowTransaction',
|
||||
service: GetMatchedTransactionsByCashflow,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ export class UnmatchMatchedBankTransaction {
|
||||
return this.uow.withTransaction(tenantId, async (trx) => {
|
||||
await this.eventPublisher.emitAsync(events.bankMatch.onUnmatching, {
|
||||
tenantId,
|
||||
uncategorizedTransactionId,
|
||||
trx,
|
||||
} as IBankTransactionUnmatchingEventPayload);
|
||||
|
||||
@@ -40,6 +41,7 @@ export class UnmatchMatchedBankTransaction {
|
||||
|
||||
await this.eventPublisher.emitAsync(events.bankMatch.onUnmatched, {
|
||||
tenantId,
|
||||
uncategorizedTransactionId,
|
||||
trx,
|
||||
} as IBankTransactionUnmatchingEventPayload);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Knex } from 'knex';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { ERRORS } from './types';
|
||||
|
||||
@Service()
|
||||
@@ -18,12 +19,13 @@ export class ValidateTransactionMatched {
|
||||
public async validateTransactionNoMatchLinking(
|
||||
tenantId: number,
|
||||
referenceType: string,
|
||||
referenceId: number
|
||||
referenceId: number,
|
||||
trx?: Knex.Transaction
|
||||
) {
|
||||
const { MatchedBankTransaction } = this.tenancy.models(tenantId);
|
||||
|
||||
const foundMatchedTransaction =
|
||||
await MatchedBankTransaction.query().findOne({
|
||||
await MatchedBankTransaction.query(trx).findOne({
|
||||
referenceType,
|
||||
referenceId,
|
||||
});
|
||||
|
||||
@@ -20,3 +20,12 @@ export const sortClosestMatchTransactions = (
|
||||
),
|
||||
])(matches);
|
||||
};
|
||||
|
||||
export const sumMatchTranasctions = (transactions: Array<any>) => {
|
||||
return transactions.reduce(
|
||||
(total, item) =>
|
||||
total +
|
||||
(item.transactionNormal === 'debit' ? 1 : -1) * parseFloat(item.amount),
|
||||
0
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import events from '@/subscribers/events';
|
||||
import {
|
||||
IBankTransactionMatchedEventPayload,
|
||||
IBankTransactionUnmatchedEventPayload,
|
||||
} from '../types';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
|
||||
@Service()
|
||||
export class DecrementUncategorizedTransactionOnMatching {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
/**
|
||||
* Constructor method.
|
||||
*/
|
||||
public attach(bus) {
|
||||
bus.subscribe(
|
||||
events.bankMatch.onMatched,
|
||||
this.decrementUnCategorizedTransactionsOnMatching.bind(this)
|
||||
);
|
||||
bus.subscribe(
|
||||
events.bankMatch.onUnmatched,
|
||||
this.incrementUnCategorizedTransactionsOnUnmatching.bind(this)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the cashflow transaction whether matched with bank transaction on deleting.
|
||||
* @param {IManualJournalDeletingPayload}
|
||||
*/
|
||||
public async decrementUnCategorizedTransactionsOnMatching({
|
||||
tenantId,
|
||||
uncategorizedTransactionId,
|
||||
trx,
|
||||
}: IBankTransactionMatchedEventPayload) {
|
||||
const { UncategorizedCashflowTransaction, Account } =
|
||||
this.tenancy.models(tenantId);
|
||||
|
||||
const transaction = await UncategorizedCashflowTransaction.query().findById(
|
||||
uncategorizedTransactionId
|
||||
);
|
||||
await Account.query(trx)
|
||||
.findById(transaction.accountId)
|
||||
.decrement('uncategorizedTransactions', 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the cashflow transaction whether matched with bank transaction on deleting.
|
||||
* @param {IManualJournalDeletingPayload}
|
||||
*/
|
||||
public async incrementUnCategorizedTransactionsOnUnmatching({
|
||||
tenantId,
|
||||
uncategorizedTransactionId,
|
||||
trx,
|
||||
}: IBankTransactionUnmatchedEventPayload) {
|
||||
const { UncategorizedCashflowTransaction, Account } =
|
||||
this.tenancy.models(tenantId);
|
||||
|
||||
const transaction = await UncategorizedCashflowTransaction.query().findById(
|
||||
uncategorizedTransactionId
|
||||
);
|
||||
await Account.query(trx)
|
||||
.findById(transaction.accountId)
|
||||
.increment('uncategorizedTransactions', 1);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { IManualJournalDeletingPayload } from '@/interfaces';
|
||||
import { ICommandCashflowDeletingPayload, IManualJournalDeletingPayload } from '@/interfaces';
|
||||
import events from '@/subscribers/events';
|
||||
import { ValidateTransactionMatched } from '../ValidateTransactionsMatched';
|
||||
|
||||
@@ -24,13 +24,14 @@ export class ValidateMatchingOnCashflowDelete {
|
||||
*/
|
||||
public async validateMatchingOnCashflowDeleting({
|
||||
tenantId,
|
||||
oldManualJournal,
|
||||
oldCashflowTransaction,
|
||||
trx,
|
||||
}: IManualJournalDeletingPayload) {
|
||||
}: ICommandCashflowDeletingPayload) {
|
||||
await this.validateNoMatchingLinkedService.validateTransactionNoMatchLinking(
|
||||
tenantId,
|
||||
'ManualJournal',
|
||||
oldManualJournal.id
|
||||
'CashflowTransaction',
|
||||
oldCashflowTransaction.id,
|
||||
trx
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,8 @@ export class ValidateMatchingOnExpenseDelete {
|
||||
await this.validateNoMatchingLinkedService.validateTransactionNoMatchLinking(
|
||||
tenantId,
|
||||
'Expense',
|
||||
oldExpense.id
|
||||
oldExpense.id,
|
||||
trx
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,8 @@ export class ValidateMatchingOnManualJournalDelete {
|
||||
await this.validateNoMatchingLinkedService.validateTransactionNoMatchLinking(
|
||||
tenantId,
|
||||
'ManualJournal',
|
||||
oldManualJournal.id
|
||||
oldManualJournal.id,
|
||||
trx
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,8 @@ export class ValidateMatchingOnPaymentMadeDelete {
|
||||
await this.validateNoMatchingLinkedService.validateTransactionNoMatchLinking(
|
||||
tenantId,
|
||||
'PaymentMade',
|
||||
oldBillPayment.id
|
||||
oldBillPayment.id,
|
||||
trx
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,8 @@ export class ValidateMatchingOnPaymentReceivedDelete {
|
||||
await this.validateNoMatchingLinkedService.validateTransactionNoMatchLinking(
|
||||
tenantId,
|
||||
'PaymentReceive',
|
||||
oldPaymentReceive.id
|
||||
oldPaymentReceive.id,
|
||||
trx
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,10 +16,14 @@ export interface IBankTransactionMatchedEventPayload {
|
||||
|
||||
export interface IBankTransactionUnmatchingEventPayload {
|
||||
tenantId: number;
|
||||
uncategorizedTransactionId: number;
|
||||
trx?: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface IBankTransactionUnmatchedEventPayload {
|
||||
tenantId: number;
|
||||
uncategorizedTransactionId: number;
|
||||
trx?: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface IMatchTransactionDTO {
|
||||
|
||||
@@ -17,7 +17,7 @@ import { DeleteCashflowTransaction } from '@/services/Cashflow/DeleteCashflowTra
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { CashflowApplication } from '@/services/Cashflow/CashflowApplication';
|
||||
import { Knex } from 'knex';
|
||||
import { uniqid } from 'uniqid';
|
||||
import uniqid from 'uniqid';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import events from '@/subscribers/events';
|
||||
|
||||
@@ -148,7 +148,6 @@ export class PlaidSyncDb {
|
||||
*/
|
||||
public async syncAccountsTransactions(
|
||||
tenantId: number,
|
||||
batchNo: string,
|
||||
plaidAccountsTransactions: PlaidTransaction[],
|
||||
trx?: Knex.Transaction
|
||||
): Promise<void> {
|
||||
@@ -161,7 +160,6 @@ export class PlaidSyncDb {
|
||||
return this.syncAccountTranactions(
|
||||
tenantId,
|
||||
plaidAccountId,
|
||||
batchNo,
|
||||
plaidTransactions,
|
||||
trx
|
||||
);
|
||||
|
||||
@@ -37,7 +37,6 @@ export const transformPlaidAccountToCreateAccount = R.curry(
|
||||
export const transformPlaidTrxsToCashflowCreate = R.curry(
|
||||
(
|
||||
cashflowAccountId: number,
|
||||
creditAccountId: number,
|
||||
plaidTranasction: PlaidTransaction
|
||||
): CreateUncategorizedTransactionDTO => {
|
||||
return {
|
||||
|
||||
@@ -18,11 +18,11 @@ export class RegonizeTransactionsJob {
|
||||
* Triggers sending invoice mail.
|
||||
*/
|
||||
private handler = async (job, done: Function) => {
|
||||
const { tenantId } = job.attrs.data;
|
||||
const { tenantId, batch } = job.attrs.data;
|
||||
const regonizeTransactions = Container.get(RecognizeTranasctionsService);
|
||||
|
||||
try {
|
||||
await regonizeTransactions.recognizeTransactions(tenantId);
|
||||
await regonizeTransactions.recognizeTransactions(tenantId, batch);
|
||||
done();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
|
||||
@@ -5,6 +5,8 @@ import {
|
||||
IBankRuleEventDeletedPayload,
|
||||
IBankRuleEventEditedPayload,
|
||||
} from '../../Rules/types';
|
||||
import { IImportFileCommitedEventPayload } from '@/interfaces/Import';
|
||||
import { Import } from '@/system/models';
|
||||
|
||||
@Service()
|
||||
export class TriggerRecognizedTransactions {
|
||||
@@ -27,6 +29,10 @@ export class TriggerRecognizedTransactions {
|
||||
events.bankRules.onDeleted,
|
||||
this.recognizedTransactionsOnRuleDeleted.bind(this)
|
||||
);
|
||||
bus.subscribe(
|
||||
events.import.onImportCommitted,
|
||||
this.triggerRecognizeTransactionsOnImportCommitted.bind(this)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,4 +79,20 @@ export class TriggerRecognizedTransactions {
|
||||
const payload = { tenantId };
|
||||
await this.agenda.now('recognize-uncategorized-transactions-job', payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers the recognize bank transactions once the imported file commit.
|
||||
* @param {IImportFileCommitedEventPayload} payload -
|
||||
*/
|
||||
private async triggerRecognizeTransactionsOnImportCommitted({
|
||||
tenantId,
|
||||
importId,
|
||||
meta,
|
||||
}: IImportFileCommitedEventPayload) {
|
||||
const importFile = await Import.query().findOne({ importId });
|
||||
const batch = importFile.paramsParsed.batch;
|
||||
const payload = { tenantId, batch };
|
||||
|
||||
await this.agenda.now('recognize-uncategorized-transactions-job', payload);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { upperFirst, camelCase } from 'lodash';
|
||||
import { Transformer } from '@/lib/Transformer/Transformer';
|
||||
import { getTransactionTypeLabel } from '@/utils/transactions-types';
|
||||
import { getCashflowTransactionFormattedType } from '@/utils/transactions-types';
|
||||
|
||||
export class GetBankRulesTransformer extends Transformer {
|
||||
/**
|
||||
@@ -29,8 +28,7 @@ export class GetBankRulesTransformer extends Transformer {
|
||||
* @returns {string}
|
||||
*/
|
||||
protected assignCategoryFormatted(bankRule: any) {
|
||||
const assignCategory = upperFirst(camelCase(bankRule.assignCategory));
|
||||
return getTransactionTypeLabel(assignCategory);
|
||||
return getCashflowTransactionFormattedType(bankRule.assignCategory);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -34,11 +34,12 @@ export default class CashflowTransactionJournalEntries {
|
||||
currencyCode: transaction.currencyCode,
|
||||
exchangeRate: transaction.exchangeRate,
|
||||
|
||||
transactionType: transformCashflowTransactionType(
|
||||
transaction.transactionType
|
||||
),
|
||||
transactionType: 'CashflowTransaction',
|
||||
transactionId: transaction.id,
|
||||
transactionNumber: transaction.transactionNumber,
|
||||
transactionSubType: transformCashflowTransactionType(
|
||||
transaction.transactionType
|
||||
),
|
||||
referenceNumber: transaction.referenceNo,
|
||||
|
||||
note: transaction.description,
|
||||
@@ -161,12 +162,10 @@ export default class CashflowTransactionJournalEntries {
|
||||
cashflowTransactionId: number,
|
||||
trx?: Knex.Transaction
|
||||
): Promise<void> => {
|
||||
const transactionTypes = getCashflowAccountTransactionsTypes();
|
||||
|
||||
await this.ledgerStorage.deleteByReference(
|
||||
tenantId,
|
||||
cashflowTransactionId,
|
||||
transactionTypes,
|
||||
'CashflowTransaction',
|
||||
trx
|
||||
);
|
||||
};
|
||||
|
||||
@@ -84,20 +84,23 @@ export class CategorizeCashflowTransaction {
|
||||
cashflowTransactionDTO
|
||||
);
|
||||
// Updates the uncategorized transaction as categorized.
|
||||
await UncategorizedCashflowTransaction.query(trx).patchAndFetchById(
|
||||
uncategorizedTransactionId,
|
||||
{
|
||||
categorized: true,
|
||||
categorizeRefType: 'CashflowTransaction',
|
||||
categorizeRefId: cashflowTransaction.id,
|
||||
}
|
||||
);
|
||||
const uncategorizedTransaction =
|
||||
await UncategorizedCashflowTransaction.query(trx).patchAndFetchById(
|
||||
uncategorizedTransactionId,
|
||||
{
|
||||
categorized: true,
|
||||
categorizeRefType: 'CashflowTransaction',
|
||||
categorizeRefId: cashflowTransaction.id,
|
||||
}
|
||||
);
|
||||
// Triggers `onCashflowTransactionCategorized` event.
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.cashflow.onTransactionCategorized,
|
||||
{
|
||||
tenantId,
|
||||
// cashflowTransaction,
|
||||
cashflowTransaction,
|
||||
uncategorizedTransaction,
|
||||
categorizeDTO,
|
||||
trx,
|
||||
} as ICashflowTransactionCategorizedPayload
|
||||
);
|
||||
|
||||
@@ -2,7 +2,13 @@ import { Knex } from 'knex';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
import UnitOfWork from '../UnitOfWork';
|
||||
import { CreateUncategorizedTransactionDTO } from '@/interfaces';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import events from '@/subscribers/events';
|
||||
import {
|
||||
CreateUncategorizedTransactionDTO,
|
||||
IUncategorizedTransactionCreatedEventPayload,
|
||||
IUncategorizedTransactionCreatingEventPayload,
|
||||
} from '@/interfaces';
|
||||
|
||||
@Service()
|
||||
export class CreateUncategorizedTransaction {
|
||||
@@ -12,6 +18,9 @@ export class CreateUncategorizedTransaction {
|
||||
@Inject()
|
||||
private uow: UnitOfWork;
|
||||
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
/**
|
||||
* Creates an uncategorized cashflow transaction.
|
||||
* @param {number} tenantId
|
||||
@@ -19,7 +28,7 @@ export class CreateUncategorizedTransaction {
|
||||
*/
|
||||
public create(
|
||||
tenantId: number,
|
||||
createDTO: CreateUncategorizedTransactionDTO,
|
||||
createUncategorizedTransactionDTO: CreateUncategorizedTransactionDTO,
|
||||
trx?: Knex.Transaction
|
||||
) {
|
||||
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
|
||||
@@ -27,12 +36,30 @@ export class CreateUncategorizedTransaction {
|
||||
return this.uow.withTransaction(
|
||||
tenantId,
|
||||
async (trx: Knex.Transaction) => {
|
||||
const transaction = await UncategorizedCashflowTransaction.query(
|
||||
trx
|
||||
).insertAndFetch({
|
||||
...createDTO,
|
||||
});
|
||||
return transaction;
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.cashflow.onTransactionUncategorizedCreated,
|
||||
{
|
||||
tenantId,
|
||||
createUncategorizedTransactionDTO,
|
||||
trx,
|
||||
} as IUncategorizedTransactionCreatingEventPayload
|
||||
);
|
||||
|
||||
const uncategorizedTransaction =
|
||||
await UncategorizedCashflowTransaction.query(trx).insertAndFetch({
|
||||
...createUncategorizedTransactionDTO,
|
||||
});
|
||||
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.cashflow.onTransactionUncategorizedCreated,
|
||||
{
|
||||
tenantId,
|
||||
uncategorizedTransaction,
|
||||
createUncategorizedTransactionDTO,
|
||||
trx,
|
||||
} as IUncategorizedTransactionCreatedEventPayload
|
||||
);
|
||||
return uncategorizedTransaction;
|
||||
},
|
||||
trx
|
||||
);
|
||||
|
||||
@@ -101,6 +101,7 @@ export default class NewCashflowTransactionService {
|
||||
...fromDTO,
|
||||
transactionNumber,
|
||||
currencyCode: cashflowAccount.currencyCode,
|
||||
exchangeRate: fromDTO?.exchangeRate || 1,
|
||||
transactionType: transformCashflowTransactionType(
|
||||
fromDTO.transactionType
|
||||
),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { Knex } from 'knex';
|
||||
import * as yup from 'yup';
|
||||
import uniqid from 'uniqid';
|
||||
import { Importable } from '../Import/Importable';
|
||||
import { CreateUncategorizedTransaction } from './CreateUncategorizedTransaction';
|
||||
import { CreateUncategorizedTransactionDTO } from '@/interfaces';
|
||||
@@ -15,6 +16,7 @@ export class UncategorizedTransactionsImportable extends Importable {
|
||||
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Passing the sheet DTO to create uncategorized transaction.
|
||||
* @param {number} tenantId
|
||||
@@ -43,6 +45,7 @@ export class UncategorizedTransactionsImportable extends Importable {
|
||||
return {
|
||||
...createDTO,
|
||||
accountId: context.import.paramsParsed.accountId,
|
||||
batch: context.import.paramsParsed.batch,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -54,6 +57,9 @@ export class UncategorizedTransactionsImportable extends Importable {
|
||||
return BankTransactionsSampleData;
|
||||
}
|
||||
|
||||
// ------------------
|
||||
// # Params
|
||||
// ------------------
|
||||
/**
|
||||
* Params validation schema.
|
||||
* @returns {ValidationSchema[]}
|
||||
@@ -79,4 +85,17 @@ export class UncategorizedTransactionsImportable extends Importable {
|
||||
await Account.query().findById(params.accountId).throwIfNotFound({});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformes the import params before storing them.
|
||||
* @param {Record<string, any>} parmas
|
||||
*/
|
||||
public transformParams(parmas: Record<string, any>) {
|
||||
const batch = uniqid();
|
||||
|
||||
return {
|
||||
...parmas,
|
||||
batch,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import events from '@/subscribers/events';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import {
|
||||
ICashflowTransactionCategorizedPayload,
|
||||
ICashflowTransactionUncategorizedPayload,
|
||||
} from '@/interfaces';
|
||||
|
||||
@Service()
|
||||
export class DecrementUncategorizedTransactionOnCategorize {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
/**
|
||||
* Constructor method.
|
||||
*/
|
||||
public attach(bus) {
|
||||
bus.subscribe(
|
||||
events.cashflow.onTransactionCategorized,
|
||||
this.decrementUnCategorizedTransactionsOnCategorized.bind(this)
|
||||
);
|
||||
bus.subscribe(
|
||||
events.cashflow.onTransactionUncategorized,
|
||||
this.incrementUnCategorizedTransactionsOnUncategorized.bind(this)
|
||||
);
|
||||
bus.subscribe(
|
||||
events.cashflow.onTransactionUncategorizedCreated,
|
||||
this.incrementUncategoirzedTransactionsOnCreated.bind(this)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrement the uncategoirzed transactions on the account once categorizing.
|
||||
* @param {ICashflowTransactionCategorizedPayload}
|
||||
*/
|
||||
public async decrementUnCategorizedTransactionsOnCategorized({
|
||||
tenantId,
|
||||
uncategorizedTransaction,
|
||||
}: ICashflowTransactionCategorizedPayload) {
|
||||
const { Account } = this.tenancy.models(tenantId);
|
||||
|
||||
await Account.query()
|
||||
.findById(uncategorizedTransaction.accountId)
|
||||
.decrement('uncategorizedTransactions', 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment the uncategorized transaction on the given account on uncategorizing.
|
||||
* @param {IManualJournalDeletingPayload}
|
||||
*/
|
||||
public async incrementUnCategorizedTransactionsOnUncategorized({
|
||||
tenantId,
|
||||
uncategorizedTransaction,
|
||||
}: ICashflowTransactionUncategorizedPayload) {
|
||||
const { Account } = this.tenancy.models(tenantId);
|
||||
|
||||
await Account.query()
|
||||
.findById(uncategorizedTransaction.accountId)
|
||||
.increment('uncategorizedTransactions', 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Increments uncategorized transactions count once creating a new transaction.
|
||||
* @param {ICommandCashflowCreatedPayload} payload -
|
||||
*/
|
||||
public async incrementUncategoirzedTransactionsOnCreated({
|
||||
tenantId,
|
||||
uncategorizedTransaction,
|
||||
trx,
|
||||
}: any) {
|
||||
const { Account } = this.tenancy.models(tenantId);
|
||||
|
||||
if (!uncategorizedTransaction.accountId) return;
|
||||
|
||||
await Account.query(trx)
|
||||
.findById(uncategorizedTransaction.accountId)
|
||||
.increment('uncategorizedTransactions', 1);
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,20 @@
|
||||
import R from 'ramda';
|
||||
import moment from 'moment';
|
||||
import { first, isEmpty } from 'lodash';
|
||||
import {
|
||||
ICashflowAccountTransaction,
|
||||
ICashflowAccountTransactionsQuery,
|
||||
INumberFormatQuery,
|
||||
} from '@/interfaces';
|
||||
import FinancialSheet from '../FinancialSheet';
|
||||
import { runningAmount } from 'utils';
|
||||
import { CashflowAccountTransactionsRepo } from './CashflowAccountTransactionsRepo';
|
||||
import { BankTransactionStatus } from './constants';
|
||||
import { formatBankTransactionsStatus } from './utils';
|
||||
|
||||
export default class CashflowAccountTransactionReport extends FinancialSheet {
|
||||
private transactions: any;
|
||||
private openingBalance: number;
|
||||
export class CashflowAccountTransactionReport extends FinancialSheet {
|
||||
private runningBalance: any;
|
||||
private numberFormat: INumberFormatQuery;
|
||||
private baseCurrency: string;
|
||||
private query: ICashflowAccountTransactionsQuery;
|
||||
private repo: CashflowAccountTransactionsRepo;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
@@ -23,19 +23,61 @@ export default class CashflowAccountTransactionReport extends FinancialSheet {
|
||||
* @param {ICashflowAccountTransactionsQuery} query -
|
||||
*/
|
||||
constructor(
|
||||
transactions,
|
||||
openingBalance: number,
|
||||
repo: CashflowAccountTransactionsRepo,
|
||||
query: ICashflowAccountTransactionsQuery
|
||||
) {
|
||||
super();
|
||||
|
||||
this.transactions = transactions;
|
||||
this.openingBalance = openingBalance;
|
||||
|
||||
this.runningBalance = runningAmount(this.openingBalance);
|
||||
this.repo = repo;
|
||||
this.query = query;
|
||||
this.numberFormat = query.numberFormat;
|
||||
this.baseCurrency = 'USD';
|
||||
this.runningBalance = runningAmount(this.repo.openingBalance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the transaction status.
|
||||
* @param {} transaction
|
||||
* @returns {BankTransactionStatus}
|
||||
*/
|
||||
private getTransactionStatus(transaction: any): BankTransactionStatus {
|
||||
const categorizedTrans = this.repo.uncategorizedTransactionsMapByRef.get(
|
||||
`${transaction.referenceType}-${transaction.referenceId}`
|
||||
);
|
||||
const matchedTrans = this.repo.matchedBankTransactionsMapByRef.get(
|
||||
`${transaction.referenceType}-${transaction.referenceId}`
|
||||
);
|
||||
if (!isEmpty(categorizedTrans)) {
|
||||
return BankTransactionStatus.Categorized;
|
||||
} else if (!isEmpty(matchedTrans)) {
|
||||
return BankTransactionStatus.Matched;
|
||||
} else {
|
||||
return BankTransactionStatus.Manual;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the uncategoized transaction id from the given transaction.
|
||||
* @param transaction
|
||||
* @returns {number|null}
|
||||
*/
|
||||
private getUncategorizedTransId(transaction: any): number {
|
||||
// The given transaction would be categorized, matched or not, so we'd take a look at
|
||||
// the categorized transaction first to get the id if not exist, then should look at the matched
|
||||
// transaction if not exist too, so the given transaction has no uncategorized transaction id.
|
||||
const categorizedTrans = this.repo.uncategorizedTransactionsMapByRef.get(
|
||||
`${transaction.referenceType}-${transaction.referenceId}`
|
||||
);
|
||||
const matchedTrans = this.repo.matchedBankTransactionsMapByRef.get(
|
||||
`${transaction.referenceType}-${transaction.referenceId}`
|
||||
);
|
||||
// Relation between the transaction and matching always been one-to-one.
|
||||
const firstCategorizedTrans = first(categorizedTrans);
|
||||
const firstMatchedTrans = first(matchedTrans);
|
||||
|
||||
return (
|
||||
firstCategorizedTrans?.id ||
|
||||
firstMatchedTrans?.uncategorizedTransactionId ||
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -44,6 +86,10 @@ export default class CashflowAccountTransactionReport extends FinancialSheet {
|
||||
* @returns {ICashflowAccountTransaction}
|
||||
*/
|
||||
private transactionNode = (transaction: any): ICashflowAccountTransaction => {
|
||||
const status = this.getTransactionStatus(transaction);
|
||||
const uncategorizedTransactionId =
|
||||
this.getUncategorizedTransId(transaction);
|
||||
|
||||
return {
|
||||
date: transaction.date,
|
||||
formattedDate: moment(transaction.date).format('YYYY-MM-DD'),
|
||||
@@ -67,6 +113,9 @@ export default class CashflowAccountTransactionReport extends FinancialSheet {
|
||||
|
||||
balance: 0,
|
||||
formattedBalance: '',
|
||||
status,
|
||||
formattedStatus: formatBankTransactionsStatus(status),
|
||||
uncategorizedTransactionId,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -146,6 +195,6 @@ export default class CashflowAccountTransactionReport extends FinancialSheet {
|
||||
* @returns {ICashflowAccountTransaction[]}
|
||||
*/
|
||||
public reportData(): ICashflowAccountTransaction[] {
|
||||
return this.transactionsNode(this.transactions);
|
||||
return this.transactionsNode(this.repo.transactions);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,59 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { ICashflowAccountTransactionsQuery, IPaginationMeta } from '@/interfaces';
|
||||
import * as R from 'ramda';
|
||||
import { ICashflowAccountTransactionsQuery } from '@/interfaces';
|
||||
import {
|
||||
groupMatchedBankTransactions,
|
||||
groupUncategorizedTransactions,
|
||||
} from './utils';
|
||||
|
||||
@Service()
|
||||
export default class CashflowAccountTransactionsRepo {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
export class CashflowAccountTransactionsRepo {
|
||||
private models: any;
|
||||
public query: ICashflowAccountTransactionsQuery;
|
||||
public transactions: any;
|
||||
public uncategorizedTransactions: any;
|
||||
public uncategorizedTransactionsMapByRef: Map<string, any>;
|
||||
public matchedBankTransactions: any;
|
||||
public matchedBankTransactionsMapByRef: Map<string, any>;
|
||||
public pagination: any;
|
||||
public openingBalance: any;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {any} models
|
||||
* @param {ICashflowAccountTransactionsQuery} query
|
||||
*/
|
||||
constructor(models: any, query: ICashflowAccountTransactionsQuery) {
|
||||
this.models = models;
|
||||
this.query = query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Async initalize the resources.
|
||||
*/
|
||||
async asyncInit() {
|
||||
await this.initCashflowAccountTransactions();
|
||||
await this.initCashflowAccountOpeningBalance();
|
||||
await this.initCategorizedTransactions();
|
||||
await this.initMatchedTransactions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the cashflow account transactions.
|
||||
* @param {number} tenantId -
|
||||
* @param {ICashflowAccountTransactionsQuery} query -
|
||||
*/
|
||||
async getCashflowAccountTransactions(
|
||||
tenantId: number,
|
||||
query: ICashflowAccountTransactionsQuery
|
||||
) {
|
||||
const { AccountTransaction } = this.tenancy.models(tenantId);
|
||||
async initCashflowAccountTransactions() {
|
||||
const { AccountTransaction } = this.models;
|
||||
|
||||
return AccountTransaction.query()
|
||||
.where('account_id', query.accountId)
|
||||
const { results, pagination } = await AccountTransaction.query()
|
||||
.where('account_id', this.query.accountId)
|
||||
.orderBy([
|
||||
{ column: 'date', order: 'desc' },
|
||||
{ column: 'created_at', order: 'desc' },
|
||||
])
|
||||
.pagination(query.page - 1, query.pageSize);
|
||||
.pagination(this.query.page - 1, this.query.pageSize);
|
||||
|
||||
this.transactions = results;
|
||||
this.pagination = pagination;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -34,22 +63,18 @@ export default class CashflowAccountTransactionsRepo {
|
||||
* @param {IPaginationMeta} pagination
|
||||
* @return {Promise<number>}
|
||||
*/
|
||||
async getCashflowAccountOpeningBalance(
|
||||
tenantId: number,
|
||||
accountId: number,
|
||||
pagination: IPaginationMeta
|
||||
): Promise<number> {
|
||||
const { AccountTransaction } = this.tenancy.models(tenantId);
|
||||
async initCashflowAccountOpeningBalance(): Promise<void> {
|
||||
const { AccountTransaction } = this.models;
|
||||
|
||||
// Retrieve the opening balance of credit and debit balances.
|
||||
const openingBalancesSubquery = AccountTransaction.query()
|
||||
.where('account_id', accountId)
|
||||
.where('account_id', this.query.accountId)
|
||||
.orderBy([
|
||||
{ column: 'date', order: 'desc' },
|
||||
{ column: 'created_at', order: 'desc' },
|
||||
])
|
||||
.limit(pagination.total)
|
||||
.offset(pagination.pageSize * (pagination.page - 1));
|
||||
.limit(this.pagination.total)
|
||||
.offset(this.pagination.pageSize * (this.pagination.page - 1));
|
||||
|
||||
// Sumation of credit and debit balance.
|
||||
const openingBalances = await AccountTransaction.query()
|
||||
@@ -60,6 +85,43 @@ export default class CashflowAccountTransactionsRepo {
|
||||
|
||||
const openingBalance = openingBalances.debit - openingBalances.credit;
|
||||
|
||||
return openingBalance;
|
||||
this.openingBalance = openingBalance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the uncategorized transactions of the bank account.
|
||||
*/
|
||||
async initCategorizedTransactions() {
|
||||
const { UncategorizedCashflowTransaction } = this.models;
|
||||
const refs = this.transactions.map((t) => [t.referenceType, t.referenceId]);
|
||||
|
||||
const uncategorizedTransactions =
|
||||
await UncategorizedCashflowTransaction.query().whereIn(
|
||||
['categorizeRefType', 'categorizeRefId'],
|
||||
refs
|
||||
);
|
||||
|
||||
this.uncategorizedTransactions = uncategorizedTransactions;
|
||||
this.uncategorizedTransactionsMapByRef = groupUncategorizedTransactions(
|
||||
uncategorizedTransactions
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the matched bank transactions of the bank account.
|
||||
*/
|
||||
async initMatchedTransactions(): Promise<void> {
|
||||
const { MatchedBankTransaction } = this.models;
|
||||
const refs = this.transactions.map((t) => [t.referenceType, t.referenceId]);
|
||||
|
||||
const matchedBankTransactions =
|
||||
await MatchedBankTransaction.query().whereIn(
|
||||
['referenceType', 'referenceId'],
|
||||
refs
|
||||
);
|
||||
this.matchedBankTransactions = matchedBankTransactions;
|
||||
this.matchedBankTransactionsMapByRef = groupMatchedBankTransactions(
|
||||
matchedBankTransactions
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,16 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { includes } from 'lodash';
|
||||
import * as qim from 'qim';
|
||||
import { ICashflowAccountTransactionsQuery, IAccount } from '@/interfaces';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import FinancialSheet from '../FinancialSheet';
|
||||
import CashflowAccountTransactionsRepo from './CashflowAccountTransactionsRepo';
|
||||
import CashflowAccountTransactionsReport from './CashflowAccountTransactions';
|
||||
import { ACCOUNT_TYPE } from '@/data/AccountTypes';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import { ERRORS } from './constants';
|
||||
import { CashflowAccountTransactionReport } from './CashflowAccountTransactions';
|
||||
import I18nService from '@/services/I18n/I18nService';
|
||||
import { CashflowAccountTransactionsRepo } from './CashflowAccountTransactionsRepo';
|
||||
|
||||
@Service()
|
||||
export default class CashflowAccountTransactionsService extends FinancialSheet {
|
||||
@Inject()
|
||||
tenancy: TenancyService;
|
||||
|
||||
@Inject()
|
||||
cashflowTransactionsRepo: CashflowAccountTransactionsRepo;
|
||||
|
||||
@Inject()
|
||||
i18nService: I18nService;
|
||||
private tenancy: TenancyService;
|
||||
|
||||
/**
|
||||
* Defaults balance sheet filter query.
|
||||
@@ -50,59 +40,24 @@ export default class CashflowAccountTransactionsService extends FinancialSheet {
|
||||
tenantId: number,
|
||||
query: ICashflowAccountTransactionsQuery
|
||||
) {
|
||||
const { Account } = this.tenancy.models(tenantId);
|
||||
const models = this.tenancy.models(tenantId);
|
||||
const parsedQuery = { ...this.defaultQuery, ...query };
|
||||
|
||||
// Retrieve the given account or throw not found service error.
|
||||
const account = await Account.query().findById(parsedQuery.accountId);
|
||||
|
||||
// Validates the cashflow account type.
|
||||
this.validateCashflowAccountType(account);
|
||||
|
||||
// Retrieve the cashflow account transactions.
|
||||
const { results: transactions, pagination } =
|
||||
await this.cashflowTransactionsRepo.getCashflowAccountTransactions(
|
||||
tenantId,
|
||||
parsedQuery
|
||||
);
|
||||
// Retrieve the cashflow account opening balance.
|
||||
const openingBalance =
|
||||
await this.cashflowTransactionsRepo.getCashflowAccountOpeningBalance(
|
||||
tenantId,
|
||||
parsedQuery.accountId,
|
||||
pagination
|
||||
);
|
||||
// Retrieve the computed report.
|
||||
const report = new CashflowAccountTransactionsReport(
|
||||
transactions,
|
||||
openingBalance,
|
||||
// Initalize the bank transactions report repository.
|
||||
const cashflowTransactionsRepo = new CashflowAccountTransactionsRepo(
|
||||
models,
|
||||
parsedQuery
|
||||
);
|
||||
const reportTranasctions = report.reportData();
|
||||
await cashflowTransactionsRepo.asyncInit();
|
||||
|
||||
return {
|
||||
transactions: this.i18nService.i18nApply(
|
||||
[[qim.$each, 'formattedTransactionType']],
|
||||
reportTranasctions,
|
||||
tenantId
|
||||
),
|
||||
pagination,
|
||||
};
|
||||
}
|
||||
// Retrieve the computed report.
|
||||
const report = new CashflowAccountTransactionReport(
|
||||
cashflowTransactionsRepo,
|
||||
parsedQuery
|
||||
);
|
||||
const transactions = report.reportData();
|
||||
const pagination = cashflowTransactionsRepo.pagination;
|
||||
|
||||
/**
|
||||
* Validates the cashflow account type.
|
||||
* @param {IAccount} account -
|
||||
*/
|
||||
private validateCashflowAccountType(account: IAccount) {
|
||||
const cashflowTypes = [
|
||||
ACCOUNT_TYPE.CASH,
|
||||
ACCOUNT_TYPE.CREDIT_CARD,
|
||||
ACCOUNT_TYPE.BANK,
|
||||
];
|
||||
|
||||
if (!includes(cashflowTypes, account.accountType)) {
|
||||
throw new ServiceError(ERRORS.ACCOUNT_ID_HAS_INVALID_TYPE);
|
||||
}
|
||||
return { transactions, pagination };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
export const ERRORS = {
|
||||
ACCOUNT_ID_HAS_INVALID_TYPE: 'ACCOUNT_ID_HAS_INVALID_TYPE',
|
||||
};
|
||||
|
||||
export enum BankTransactionStatus {
|
||||
Categorized = 'categorized',
|
||||
Matched = 'matched',
|
||||
Manual = 'manual',
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import * as R from 'ramda';
|
||||
|
||||
export const groupUncategorizedTransactions = (
|
||||
uncategorizedTransactions: any
|
||||
): Map<string, any> => {
|
||||
return new Map(
|
||||
R.toPairs(
|
||||
R.groupBy(
|
||||
(transaction) =>
|
||||
`${transaction.categorizeRefType}-${transaction.categorizeRefId}`,
|
||||
uncategorizedTransactions
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export const groupMatchedBankTransactions = (
|
||||
uncategorizedTransactions: any
|
||||
): Map<string, any> => {
|
||||
return new Map(
|
||||
R.toPairs(
|
||||
R.groupBy(
|
||||
(transaction) =>
|
||||
`${transaction.referenceType}-${transaction.referenceId}`,
|
||||
uncategorizedTransactions
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export const formatBankTransactionsStatus = (status) => {
|
||||
switch (status) {
|
||||
case 'categorized':
|
||||
return 'Categorized';
|
||||
case 'matched':
|
||||
return 'Matched';
|
||||
case 'manual':
|
||||
return 'Manual';
|
||||
}
|
||||
};
|
||||
@@ -15,14 +15,10 @@ import { ServiceError } from '@/exceptions';
|
||||
import { getUniqueImportableValue, trimObject } from './_utils';
|
||||
import { ImportableResources } from './ImportableResources';
|
||||
import ResourceService from '../Resource/ResourceService';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
import { Import } from '@/system/models';
|
||||
|
||||
@Service()
|
||||
export class ImportFileCommon {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private importFileValidator: ImportFileDataValidator;
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
import { ImportFilePreviewPOJO } from './interfaces';
|
||||
import { ImportFileProcess } from './ImportFileProcess';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import events from '@/subscribers/events';
|
||||
import { IImportFileCommitedEventPayload } from '@/interfaces/Import';
|
||||
|
||||
@Service()
|
||||
export class ImportFileProcessCommit {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private importFile: ImportFileProcess;
|
||||
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
/**
|
||||
* Commits the imported file.
|
||||
* @param {number} tenantId
|
||||
* @param {number} importId
|
||||
* @returns {Promise<ImportFilePreviewPOJO>}
|
||||
*/
|
||||
public async commit(
|
||||
tenantId: number,
|
||||
importId: number
|
||||
): Promise<ImportFilePreviewPOJO> {
|
||||
const knex = this.tenancy.knex(tenantId);
|
||||
const trx = await knex.transaction({ isolationLevel: 'read uncommitted' });
|
||||
|
||||
const meta = await this.importFile.import(tenantId, importId, trx);
|
||||
|
||||
// Commit the successed transaction.
|
||||
await trx.commit();
|
||||
|
||||
// Triggers `onImportFileCommitted` event.
|
||||
await this.eventPublisher.emitAsync(events.import.onImportCommitted, {
|
||||
meta,
|
||||
importId,
|
||||
tenantId,
|
||||
} as IImportFileCommitedEventPayload);
|
||||
|
||||
return meta;
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { ImportFileProcess } from './ImportFileProcess';
|
||||
import { ImportFilePreview } from './ImportFilePreview';
|
||||
import { ImportSampleService } from './ImportSample';
|
||||
import { ImportFileMeta } from './ImportFileMeta';
|
||||
import { ImportFileProcessCommit } from './ImportFileProcessCommit';
|
||||
|
||||
@Inject()
|
||||
export class ImportResourceApplication {
|
||||
@@ -27,6 +28,9 @@ export class ImportResourceApplication {
|
||||
@Inject()
|
||||
private importMetaService: ImportFileMeta;
|
||||
|
||||
@Inject()
|
||||
private importProcessCommit: ImportFileProcessCommit;
|
||||
|
||||
/**
|
||||
* Reads the imported file and stores the import file meta under unqiue id.
|
||||
* @param {number} tenantId -
|
||||
@@ -74,12 +78,12 @@ export class ImportResourceApplication {
|
||||
* @returns {Promise<ImportFilePreviewPOJO>}
|
||||
*/
|
||||
public async process(tenantId: number, importId: number) {
|
||||
return this.importProcessService.import(tenantId, importId);
|
||||
return this.importProcessCommit.commit(tenantId, importId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the import meta of the given import id.
|
||||
* @param {number} tenantId -
|
||||
* @param {number} tenantId -
|
||||
* @param {string} importId - Import id.
|
||||
* @returns {}
|
||||
*/
|
||||
|
||||
@@ -399,6 +399,9 @@ export default {
|
||||
onTransactionCategorizing: 'onTransactionCategorizing',
|
||||
onTransactionCategorized: 'onCashflowTransactionCategorized',
|
||||
|
||||
onTransactionUncategorizedCreating: 'onTransactionUncategorizedCreating',
|
||||
onTransactionUncategorizedCreated: 'onTransactionUncategorizedCreated',
|
||||
|
||||
onTransactionUncategorizing: 'onTransactionUncategorizing',
|
||||
onTransactionUncategorized: 'onTransactionUncategorized',
|
||||
|
||||
@@ -639,4 +642,17 @@ export default {
|
||||
onUnmatching: 'onBankTransactionUnmathcing',
|
||||
onUnmatched: 'onBankTransactionUnmathced',
|
||||
},
|
||||
|
||||
bankTransactions: {
|
||||
onExcluding: 'onBankTransactionExclude',
|
||||
onExcluded: 'onBankTransactionExcluded',
|
||||
|
||||
onUnexcluding: 'onBankTransactionUnexcluding',
|
||||
onUnexcluded: 'onBankTransactionUnexcluded',
|
||||
},
|
||||
|
||||
// Import files.
|
||||
import: {
|
||||
onImportCommitted: 'onImportFileCommitted',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,5 +1,36 @@
|
||||
import { TransactionTypes } from '@/data/TransactionTypes';
|
||||
import { isObject, upperFirst, camelCase } from 'lodash';
|
||||
import {
|
||||
TransactionTypes,
|
||||
CashflowTransactionTypes,
|
||||
} from '@/data/TransactionTypes';
|
||||
|
||||
export const getTransactionTypeLabel = (transactionType: string) => {
|
||||
return TransactionTypes[transactionType];
|
||||
/**
|
||||
* Retrieves the formatted type of account transaction.
|
||||
* @param {string} referenceType
|
||||
* @param {string} transactionType
|
||||
* @returns {string}
|
||||
*/
|
||||
export const getTransactionTypeLabel = (
|
||||
referenceType: string,
|
||||
transactionType?: string
|
||||
) => {
|
||||
const _referenceType = upperFirst(camelCase(referenceType));
|
||||
const _transactionType = upperFirst(camelCase(transactionType));
|
||||
|
||||
return isObject(TransactionTypes[_referenceType])
|
||||
? TransactionTypes[_referenceType][_transactionType]
|
||||
: TransactionTypes[_referenceType] || null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the formatted type of cashflow transaction.
|
||||
* @param {string} transactionType
|
||||
* @returns {string¿}
|
||||
*/
|
||||
export const getCashflowTransactionFormattedType = (
|
||||
transactionType: string
|
||||
) => {
|
||||
const _transactionType = upperFirst(camelCase(transactionType));
|
||||
|
||||
return CashflowTransactionTypes[_transactionType] || null;
|
||||
};
|
||||
|
||||
@@ -39,3 +39,12 @@ export const TRANSACRIONS_TYPE = [
|
||||
'OtherExpense',
|
||||
'TransferToAccount',
|
||||
];
|
||||
|
||||
export const MoneyCategoryPerCreditAccountRootType = {
|
||||
OwnerContribution: ['equity'],
|
||||
OtherIncome: ['income'],
|
||||
OwnerDrawing: ['equity'],
|
||||
OtherExpense: ['expense'],
|
||||
TransferToAccount: ['asset'],
|
||||
TransferFromAccount: ['asset'],
|
||||
};
|
||||
|
||||
@@ -69,6 +69,14 @@ function AccountDeleteTransactionAlert({
|
||||
'Cannot delete transaction converted from uncategorized transaction but you uncategorize it.',
|
||||
intent: Intent.DANGER,
|
||||
});
|
||||
} else if (
|
||||
errors.find((e) => e.type === 'CANNOT_DELETE_TRANSACTION_MATCHED')
|
||||
) {
|
||||
AppToaster.show({
|
||||
message:
|
||||
'Cannot delete a transaction matched to the bank transaction',
|
||||
intent: Intent.DANGER,
|
||||
});
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// @ts-nocheck
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { Form, Formik, FormikHelpers, useFormikContext } from 'formik';
|
||||
import { Button, Classes, Intent, Radio, Tag } from '@blueprintjs/core';
|
||||
import * as R from 'ramda';
|
||||
@@ -16,11 +17,11 @@ import {
|
||||
} from '@/components';
|
||||
import { useCreateBankRule, useEditBankRule } from '@/hooks/query/bank-rules';
|
||||
import {
|
||||
AssignTransactionTypeOptions,
|
||||
FieldCondition,
|
||||
Fields,
|
||||
RuleFormValues,
|
||||
TransactionTypeOptions,
|
||||
getAccountRootFromMoneyCategory,
|
||||
initialValues,
|
||||
} from './_utils';
|
||||
import { useRuleFormDialogBoot } from './RuleFormBoot';
|
||||
@@ -31,6 +32,11 @@ import {
|
||||
} from '@/utils';
|
||||
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||
import { DialogsName } from '@/constants/dialogs';
|
||||
import { getAddMoneyInOptions, getAddMoneyOutOptions } from '@/constants';
|
||||
|
||||
// Retrieves the add money in button options.
|
||||
const MoneyInOptions = getAddMoneyInOptions();
|
||||
const MoneyOutOptions = getAddMoneyOutOptions();
|
||||
|
||||
function RuleFormContentFormRoot({
|
||||
// #withDialogActions
|
||||
@@ -47,7 +53,6 @@ function RuleFormContentFormRoot({
|
||||
...initialValues,
|
||||
...transformToForm(transformToCamelCase(bankRule), initialValues),
|
||||
};
|
||||
|
||||
// Handles the form submitting.
|
||||
const handleSubmit = (
|
||||
values: RuleFormValues,
|
||||
@@ -92,8 +97,9 @@ function RuleFormContentFormRoot({
|
||||
label={'Rule Name'}
|
||||
labelInfo={<Tag minimal>Required</Tag>}
|
||||
style={{ maxWidth: 300 }}
|
||||
fastField
|
||||
>
|
||||
<FInputGroup name={'name'} />
|
||||
<FInputGroup name={'name'} fastField />
|
||||
</FFormGroup>
|
||||
|
||||
<FFormGroup
|
||||
@@ -101,29 +107,22 @@ function RuleFormContentFormRoot({
|
||||
label={'Apply the rule to account'}
|
||||
labelInfo={<Tag minimal>Required</Tag>}
|
||||
style={{ maxWidth: 350 }}
|
||||
fastField
|
||||
>
|
||||
<AccountsSelect
|
||||
name={'applyIfAccountId'}
|
||||
items={accounts}
|
||||
filterByTypes={['cash', 'bank']}
|
||||
fastField
|
||||
/>
|
||||
</FFormGroup>
|
||||
|
||||
<FFormGroup
|
||||
name={'applyIfTransactionType'}
|
||||
label={'Apply to transactions are'}
|
||||
style={{ maxWidth: 350 }}
|
||||
>
|
||||
<FSelect
|
||||
name={'applyIfTransactionType'}
|
||||
items={TransactionTypeOptions}
|
||||
popoverProps={{ minimal: true, inline: false }}
|
||||
/>
|
||||
</FFormGroup>
|
||||
<RuleApplyIfTransactionTypeField />
|
||||
|
||||
<FFormGroup
|
||||
name={'conditionsType'}
|
||||
label={'Categorize the transactions when'}
|
||||
fastField
|
||||
>
|
||||
<FRadioGroup name={'conditionsType'}>
|
||||
<Radio value={'and'} label={'All the following criteria matches'} />
|
||||
@@ -139,34 +138,16 @@ function RuleFormContentFormRoot({
|
||||
Then Assign
|
||||
</h3>
|
||||
|
||||
<FFormGroup
|
||||
name={'assignCategory'}
|
||||
label={'Transaction type'}
|
||||
labelInfo={<Tag minimal>Required</Tag>}
|
||||
style={{ maxWidth: 300 }}
|
||||
>
|
||||
<FSelect
|
||||
name={'assignCategory'}
|
||||
items={AssignTransactionTypeOptions}
|
||||
popoverProps={{ minimal: true, inline: false }}
|
||||
/>
|
||||
</FFormGroup>
|
||||
|
||||
<FFormGroup
|
||||
name={'assignAccountId'}
|
||||
label={'Account category'}
|
||||
labelInfo={<Tag minimal>Required</Tag>}
|
||||
style={{ maxWidth: 300 }}
|
||||
>
|
||||
<AccountsSelect name={'assignAccountId'} items={accounts} />
|
||||
</FFormGroup>
|
||||
<RuleAssignCategoryField />
|
||||
<RuleAssignCategoryAccountField />
|
||||
|
||||
<FFormGroup
|
||||
name={'assignRef'}
|
||||
label={'Reference'}
|
||||
style={{ maxWidth: 300 }}
|
||||
fastField
|
||||
>
|
||||
<FInputGroup name={'assignRef'} />
|
||||
<FInputGroup name={'assignRef'} fastField />
|
||||
</FFormGroup>
|
||||
|
||||
<RuleFormActions />
|
||||
@@ -203,11 +184,13 @@ function RuleFormConditions() {
|
||||
name={`conditions[${index}].field`}
|
||||
label={'Field'}
|
||||
style={{ marginBottom: 0, flex: '1 0' }}
|
||||
fastField
|
||||
>
|
||||
<FSelect
|
||||
name={`conditions[${index}].field`}
|
||||
items={Fields}
|
||||
popoverProps={{ minimal: true, inline: false }}
|
||||
fastField
|
||||
/>
|
||||
</FFormGroup>
|
||||
|
||||
@@ -215,11 +198,13 @@ function RuleFormConditions() {
|
||||
name={`conditions[${index}].comparator`}
|
||||
label={'Condition'}
|
||||
style={{ marginBottom: 0, flex: '1 0' }}
|
||||
fastField
|
||||
>
|
||||
<FSelect
|
||||
name={`conditions[${index}].comparator`}
|
||||
items={FieldCondition}
|
||||
popoverProps={{ minimal: true, inline: false }}
|
||||
fastField
|
||||
/>
|
||||
</FFormGroup>
|
||||
|
||||
@@ -227,8 +212,9 @@ function RuleFormConditions() {
|
||||
name={`conditions[${index}].value`}
|
||||
label={'Value'}
|
||||
style={{ marginBottom: 0, flex: '1 0 ', width: '40%' }}
|
||||
fastField
|
||||
>
|
||||
<FInputGroup name={`conditions[${index}].value`} />
|
||||
<FInputGroup name={`conditions[${index}].value`} fastField />
|
||||
</FFormGroup>
|
||||
</Group>
|
||||
))}
|
||||
@@ -284,3 +270,104 @@ function RuleFormActionsRoot({
|
||||
}
|
||||
|
||||
const RuleFormActions = R.compose(withDialogActions)(RuleFormActionsRoot);
|
||||
|
||||
function RuleApplyIfTransactionTypeField() {
|
||||
const { setFieldValue } = useFormikContext<RuleFormValues>();
|
||||
|
||||
const handleItemChange = useCallback(
|
||||
(item: any) => {
|
||||
setFieldValue('applyIfTransactionType', item.value);
|
||||
setFieldValue('assignCategory', '');
|
||||
setFieldValue('assignAccountId', '');
|
||||
},
|
||||
[setFieldValue],
|
||||
);
|
||||
|
||||
return (
|
||||
<FFormGroup
|
||||
name={'applyIfTransactionType'}
|
||||
label={'Apply to transactions are'}
|
||||
style={{ maxWidth: 350 }}
|
||||
fastField
|
||||
>
|
||||
<FSelect
|
||||
name={'applyIfTransactionType'}
|
||||
items={TransactionTypeOptions}
|
||||
popoverProps={{ minimal: true, inline: false }}
|
||||
onItemChange={handleItemChange}
|
||||
fastField
|
||||
/>
|
||||
</FFormGroup>
|
||||
);
|
||||
}
|
||||
|
||||
function RuleAssignCategoryField() {
|
||||
const { values, setFieldValue } = useFormikContext<RuleFormValues>();
|
||||
|
||||
// Retrieves the transaction types if it is deposit or withdrawal.
|
||||
const transactionTypes = useMemo(
|
||||
() =>
|
||||
values?.applyIfTransactionType === 'deposit'
|
||||
? MoneyInOptions
|
||||
: MoneyOutOptions,
|
||||
[values?.applyIfTransactionType],
|
||||
);
|
||||
|
||||
// Handles the select item change.
|
||||
const handleItemChange = useCallback(
|
||||
(item: any) => {
|
||||
setFieldValue('assignCategory', item.value);
|
||||
setFieldValue('assignAccountId', '');
|
||||
},
|
||||
[setFieldValue],
|
||||
);
|
||||
|
||||
return (
|
||||
<FFormGroup
|
||||
name={'assignCategory'}
|
||||
label={'Transaction type'}
|
||||
labelInfo={<Tag minimal>Required</Tag>}
|
||||
style={{ maxWidth: 300 }}
|
||||
fastField
|
||||
>
|
||||
<FSelect
|
||||
name={'assignCategory'}
|
||||
items={transactionTypes}
|
||||
popoverProps={{ minimal: true, inline: false }}
|
||||
valueAccessor={'value'}
|
||||
textAccessor={'name'}
|
||||
onItemChange={handleItemChange}
|
||||
fastField
|
||||
/>
|
||||
</FFormGroup>
|
||||
);
|
||||
}
|
||||
|
||||
function RuleAssignCategoryAccountField() {
|
||||
const { values } = useFormikContext<RuleFormValues>();
|
||||
const { accounts } = useRuleFormDialogBoot();
|
||||
|
||||
const accountRoot = useMemo(
|
||||
() => getAccountRootFromMoneyCategory(values.assignCategory),
|
||||
[values.assignCategory],
|
||||
);
|
||||
|
||||
return (
|
||||
<FFormGroup
|
||||
name={'assignAccountId'}
|
||||
label={'Account category'}
|
||||
labelInfo={<Tag minimal>Required</Tag>}
|
||||
style={{ maxWidth: 300 }}
|
||||
fastField
|
||||
shouldUpdateDeps={{ accountRoot }}
|
||||
>
|
||||
<AccountsSelect
|
||||
name={'assignAccountId'}
|
||||
items={accounts}
|
||||
filterByRootTypes={accountRoot}
|
||||
shouldUpdateDeps={{ accountRoot }}
|
||||
fastField
|
||||
/>
|
||||
</FFormGroup>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { camelCase, get, upperFirst } from 'lodash';
|
||||
import { MoneyCategoryPerCreditAccountRootType } from '@/constants/cashflowOptions';
|
||||
|
||||
export const initialValues = {
|
||||
name: '',
|
||||
order: 0,
|
||||
applyIfAccountId: '',
|
||||
applyIfTransactionType: '',
|
||||
applyIfTransactionType: 'deposit',
|
||||
conditionsType: 'and',
|
||||
conditions: [
|
||||
{
|
||||
@@ -47,3 +50,9 @@ export const FieldCondition = [
|
||||
export const AssignTransactionTypeOptions = [
|
||||
{ value: 'expense', text: 'Expense' },
|
||||
];
|
||||
|
||||
export const getAccountRootFromMoneyCategory = (category: string): string[] => {
|
||||
const _category = upperFirst(camelCase(category));
|
||||
|
||||
return get(MoneyCategoryPerCreditAccountRootType, _category) || [];
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
|
||||
import {
|
||||
DataTable,
|
||||
@@ -9,6 +10,7 @@ import {
|
||||
TableSkeletonHeader,
|
||||
TableVirtualizedListRows,
|
||||
FormattedMessage as T,
|
||||
AppToaster,
|
||||
} from '@/components';
|
||||
import { TABLES } from '@/constants/tables';
|
||||
|
||||
@@ -19,9 +21,11 @@ import withDrawerActions from '@/containers/Drawer/withDrawerActions';
|
||||
import { useMemorizedColumnsWidths } from '@/hooks';
|
||||
import { useAccountTransactionsColumns, ActionsMenu } from './components';
|
||||
import { useAccountTransactionsAllContext } from './AccountTransactionsAllBoot';
|
||||
import { useUnmatchMatchedUncategorizedTransaction } from '@/hooks/query/bank-rules';
|
||||
import { handleCashFlowTransactionType } from './utils';
|
||||
|
||||
import { compose } from '@/utils';
|
||||
import { useUncategorizeTransaction } from '@/hooks/query';
|
||||
|
||||
/**
|
||||
* Account transactions data table.
|
||||
@@ -43,14 +47,14 @@ function AccountTransactionsDataTable({
|
||||
const { cashflowTransactions, isCashFlowTransactionsLoading } =
|
||||
useAccountTransactionsAllContext();
|
||||
|
||||
const { mutateAsync: uncategorizeTransaction } = useUncategorizeTransaction();
|
||||
const { mutateAsync: unmatchTransaction } =
|
||||
useUnmatchMatchedUncategorizedTransaction();
|
||||
|
||||
// Local storage memorizing columns widths.
|
||||
const [initialColumnsWidths, , handleColumnResizing] =
|
||||
useMemorizedColumnsWidths(TABLES.CASHFLOW_Transactions);
|
||||
|
||||
// handle delete transaction
|
||||
const handleDeleteTransaction = ({ reference_id }) => {
|
||||
openAlert('account-transaction-delete', { referenceId: reference_id });
|
||||
};
|
||||
// Handle view details action.
|
||||
const handleViewDetailCashflowTransaction = (referenceType) => {
|
||||
handleCashFlowTransactionType(referenceType, openDrawer);
|
||||
@@ -60,6 +64,38 @@ function AccountTransactionsDataTable({
|
||||
const referenceType = cell.row.original;
|
||||
handleCashFlowTransactionType(referenceType, openDrawer);
|
||||
};
|
||||
// Handles the unmatching the matched transaction.
|
||||
const handleUnmatchTransaction = (transaction) => {
|
||||
unmatchTransaction({ id: transaction.uncategorized_transaction_id })
|
||||
.then(() => {
|
||||
AppToaster.show({
|
||||
message: 'The bank transaction has been unmatched.',
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
AppToaster.show({
|
||||
message: 'Something went wrong.',
|
||||
intent: Intent.DANGER,
|
||||
});
|
||||
});
|
||||
};
|
||||
// Handle uncategorize transaction.
|
||||
const handleUncategorizeTransaction = (transaction) => {
|
||||
uncategorizeTransaction(transaction.uncategorized_transaction_id)
|
||||
.then(() => {
|
||||
AppToaster.show({
|
||||
message: 'The bank transaction has been uncategorized.',
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
AppToaster.show({
|
||||
message: 'Something went wrong.',
|
||||
intent: Intent.DANGER,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<CashflowTransactionsTable
|
||||
@@ -87,7 +123,8 @@ function AccountTransactionsDataTable({
|
||||
className="table-constrant"
|
||||
payload={{
|
||||
onViewDetails: handleViewDetailCashflowTransaction,
|
||||
onDelete: handleDeleteTransaction,
|
||||
onUncategorize: handleUncategorizeTransaction,
|
||||
onUnmatch: handleUnmatchTransaction,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -12,19 +12,17 @@ import {
|
||||
AppToaster,
|
||||
} from '@/components';
|
||||
import { TABLES } from '@/constants/tables';
|
||||
import { ActionsMenu } from './UncategorizedTransactions/components';
|
||||
|
||||
import withSettings from '@/containers/Settings/withSettings';
|
||||
import { withBankingActions } from '../withBankingActions';
|
||||
|
||||
import { useMemorizedColumnsWidths } from '@/hooks';
|
||||
import {
|
||||
ActionsMenu,
|
||||
useAccountUncategorizedTransactionsColumns,
|
||||
} from './components';
|
||||
import { useAccountUncategorizedTransactionsColumns } from './components';
|
||||
import { useAccountUncategorizedTransactionsContext } from './AllTransactionsUncategorizedBoot';
|
||||
import { useExcludeUncategorizedTransaction } from '@/hooks/query/bank-rules';
|
||||
|
||||
import { compose } from '@/utils';
|
||||
import { useExcludeUncategorizedTransaction } from '@/hooks/query/bank-rules';
|
||||
|
||||
/**
|
||||
* Account transactions data table.
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// @ts-nocheck
|
||||
import { Menu, MenuItem, MenuDivider } from '@blueprintjs/core';
|
||||
import { Icon } from '@/components';
|
||||
import { safeCallback } from '@/utils';
|
||||
|
||||
export function ActionsMenu({
|
||||
payload: { onCategorize, onExclude },
|
||||
row: { original },
|
||||
}) {
|
||||
return (
|
||||
<Menu>
|
||||
<MenuItem
|
||||
icon={<Icon icon="reader-18" />}
|
||||
text={'Categorize'}
|
||||
onClick={safeCallback(onCategorize, original)}
|
||||
/>
|
||||
<MenuDivider />
|
||||
<MenuItem
|
||||
text={'Exclude'}
|
||||
onClick={safeCallback(onExclude, original)}
|
||||
icon={<Icon icon="disable" iconSize={16} />}
|
||||
/>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
@@ -5,44 +5,56 @@ import {
|
||||
Intent,
|
||||
Menu,
|
||||
MenuItem,
|
||||
MenuDivider,
|
||||
Tag,
|
||||
Popover,
|
||||
PopoverInteractionKind,
|
||||
Position,
|
||||
Tooltip,
|
||||
} from '@blueprintjs/core';
|
||||
import {
|
||||
Box,
|
||||
Can,
|
||||
FormatDateCell,
|
||||
Icon,
|
||||
MaterialProgressBar,
|
||||
} from '@/components';
|
||||
import { Box, FormatDateCell, Icon, MaterialProgressBar } from '@/components';
|
||||
import { useAccountTransactionsContext } from './AccountTransactionsProvider';
|
||||
import { safeCallback } from '@/utils';
|
||||
|
||||
export function ActionsMenu({
|
||||
payload: { onCategorize, onExclude },
|
||||
payload: { onUncategorize, onUnmatch },
|
||||
row: { original },
|
||||
}) {
|
||||
return (
|
||||
<Menu>
|
||||
<MenuItem
|
||||
icon={<Icon icon="reader-18" />}
|
||||
text={'Categorize'}
|
||||
onClick={safeCallback(onCategorize, original)}
|
||||
/>
|
||||
<MenuDivider />
|
||||
<MenuItem
|
||||
text={'Exclude'}
|
||||
onClick={safeCallback(onExclude, original)}
|
||||
icon={<Icon icon="disable" iconSize={16} />}
|
||||
/>
|
||||
{original.status === 'categorized' && (
|
||||
<MenuItem
|
||||
icon={<Icon icon="reader-18" />}
|
||||
text={'Uncategorize'}
|
||||
onClick={safeCallback(onUncategorize, original)}
|
||||
/>
|
||||
)}
|
||||
{original.status === 'matched' && (
|
||||
<MenuItem
|
||||
text={'Unmatch'}
|
||||
icon={<Icon icon="unlink" iconSize={16} />}
|
||||
onClick={safeCallback(onUnmatch, original)}
|
||||
/>
|
||||
)}
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
|
||||
const allTransactionsStatusAccessor = (transaction) => {
|
||||
return (
|
||||
<Tag
|
||||
intent={
|
||||
transaction.status === 'categorized'
|
||||
? Intent.SUCCESS
|
||||
: transaction.status === 'matched'
|
||||
? Intent.SUCCESS
|
||||
: Intent.NONE
|
||||
}
|
||||
minimal={transaction.status === 'manual'}
|
||||
>
|
||||
{transaction.formatted_status}
|
||||
</Tag>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve account transctions table columns.
|
||||
*/
|
||||
@@ -70,7 +82,7 @@ export function useAccountTransactionsColumns() {
|
||||
},
|
||||
{
|
||||
id: 'transaction_number',
|
||||
Header: intl.get('transaction_number'),
|
||||
Header: 'Transaction #',
|
||||
accessor: 'transaction_number',
|
||||
width: 160,
|
||||
className: 'transaction_number',
|
||||
@@ -79,13 +91,18 @@ export function useAccountTransactionsColumns() {
|
||||
},
|
||||
{
|
||||
id: 'reference_number',
|
||||
Header: intl.get('reference_no'),
|
||||
Header: 'Ref.#',
|
||||
accessor: 'reference_number',
|
||||
width: 160,
|
||||
className: 'reference_number',
|
||||
clickable: true,
|
||||
textOverview: true,
|
||||
},
|
||||
{
|
||||
id: 'status',
|
||||
Header: 'Status',
|
||||
accessor: allTransactionsStatusAccessor,
|
||||
},
|
||||
{
|
||||
id: 'deposit',
|
||||
Header: intl.get('cash_flow.label.deposit'),
|
||||
@@ -116,16 +133,6 @@ export function useAccountTransactionsColumns() {
|
||||
align: 'right',
|
||||
clickable: true,
|
||||
},
|
||||
{
|
||||
id: 'balance',
|
||||
Header: intl.get('balance'),
|
||||
accessor: 'formatted_balance',
|
||||
className: 'balance',
|
||||
width: 150,
|
||||
textOverview: true,
|
||||
clickable: true,
|
||||
align: 'right',
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
@@ -204,10 +211,9 @@ export function useAccountUncategorizedTransactionsColumns() {
|
||||
},
|
||||
{
|
||||
id: 'reference_number',
|
||||
Header: intl.get('reference_no'),
|
||||
accessor: 'reference_number',
|
||||
Header: 'Ref.#',
|
||||
accessor: 'reference_no',
|
||||
width: 50,
|
||||
className: 'reference_number',
|
||||
clickable: true,
|
||||
textOverview: true,
|
||||
},
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
FFormGroup,
|
||||
FInputGroup,
|
||||
FTextArea,
|
||||
Icon,
|
||||
} from '@/components';
|
||||
import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot';
|
||||
import { CategorizeTransactionBranchField } from '../CategorizeTransactionBranchField';
|
||||
@@ -21,7 +22,7 @@ export default function CategorizeTransactionOtherIncome() {
|
||||
popoverProps={{ position: Position.BOTTOM, minimal: true }}
|
||||
formatDate={(date) => date.toLocaleDateString()}
|
||||
parseDate={(str) => new Date(str)}
|
||||
inputProps={{ fill: true }}
|
||||
inputProps={{ fill: true, leftElement: <Icon icon={'date-range'} /> }}
|
||||
/>
|
||||
</FFormGroup>
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
FFormGroup,
|
||||
FInputGroup,
|
||||
FTextArea,
|
||||
Icon,
|
||||
} from '@/components';
|
||||
import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot';
|
||||
import { CategorizeTransactionBranchField } from '../CategorizeTransactionBranchField';
|
||||
@@ -21,7 +22,7 @@ export default function CategorizeTransactionOwnerContribution() {
|
||||
popoverProps={{ position: Position.BOTTOM, minimal: true }}
|
||||
formatDate={(date) => date.toLocaleDateString()}
|
||||
parseDate={(str) => new Date(str)}
|
||||
inputProps={{ fill: true }}
|
||||
inputProps={{ fill: true, leftElement: <Icon icon={'date-range'} /> }}
|
||||
/>
|
||||
</FFormGroup>
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
FFormGroup,
|
||||
FInputGroup,
|
||||
FTextArea,
|
||||
Icon,
|
||||
} from '@/components';
|
||||
import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot';
|
||||
import { CategorizeTransactionBranchField } from '../CategorizeTransactionBranchField';
|
||||
@@ -21,7 +22,7 @@ export default function CategorizeTransactionTransferFrom() {
|
||||
popoverProps={{ position: Position.BOTTOM, minimal: true }}
|
||||
formatDate={(date) => date.toLocaleDateString()}
|
||||
parseDate={(str) => new Date(str)}
|
||||
inputProps={{ fill: true }}
|
||||
inputProps={{ fill: true, leftElement: <Icon icon={'date-range'} /> }}
|
||||
/>
|
||||
</FFormGroup>
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
FFormGroup,
|
||||
FInputGroup,
|
||||
FTextArea,
|
||||
Icon,
|
||||
} from '@/components';
|
||||
import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot';
|
||||
import { CategorizeTransactionBranchField } from '../CategorizeTransactionBranchField';
|
||||
@@ -21,7 +22,7 @@ export default function CategorizeTransactionOtherExpense() {
|
||||
popoverProps={{ position: Position.BOTTOM, minimal: true }}
|
||||
formatDate={(date) => date.toLocaleDateString()}
|
||||
parseDate={(str) => new Date(str)}
|
||||
inputProps={{ fill: true }}
|
||||
inputProps={{ fill: true, leftElement: <Icon icon={'date-range'} /> }}
|
||||
/>
|
||||
</FFormGroup>
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
FFormGroup,
|
||||
FInputGroup,
|
||||
FTextArea,
|
||||
Icon,
|
||||
} from '@/components';
|
||||
import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot';
|
||||
import { CategorizeTransactionBranchField } from '../CategorizeTransactionBranchField';
|
||||
@@ -21,7 +22,7 @@ export default function CategorizeTransactionOwnerDrawings() {
|
||||
popoverProps={{ position: Position.BOTTOM, minimal: true }}
|
||||
formatDate={(date) => date.toLocaleDateString()}
|
||||
parseDate={(str) => new Date(str)}
|
||||
inputProps={{ fill: true }}
|
||||
inputProps={{ fill: true, leftElement: <Icon icon={'date-range'} /> }}
|
||||
/>
|
||||
</FFormGroup>
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
FFormGroup,
|
||||
FInputGroup,
|
||||
FTextArea,
|
||||
Icon,
|
||||
} from '@/components';
|
||||
import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot';
|
||||
import { CategorizeTransactionBranchField } from '../CategorizeTransactionBranchField';
|
||||
@@ -21,7 +22,7 @@ export default function CategorizeTransactionToAccount() {
|
||||
popoverProps={{ position: Position.BOTTOM, minimal: true }}
|
||||
formatDate={(date) => date.toLocaleDateString()}
|
||||
parseDate={(str) => new Date(str)}
|
||||
inputProps={{ fill: true }}
|
||||
inputProps={{ fill: true, leftElement: <Icon icon={'date-range'} /> }}
|
||||
/>
|
||||
</FFormGroup>
|
||||
|
||||
|
||||
@@ -8,16 +8,26 @@ import {
|
||||
} from '../withBankingActions';
|
||||
import { CategorizeTransactionTabsBoot } from './CategorizeTransactionTabsBoot';
|
||||
import { withBanking } from '../withBanking';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
interface CategorizeTransactionAsideProps extends WithBankingActionsProps {}
|
||||
|
||||
function CategorizeTransactionAsideRoot({
|
||||
// #withBankingActions
|
||||
closeMatchingTransactionAside,
|
||||
closeReconcileMatchingTransaction,
|
||||
|
||||
// #withBanking
|
||||
selectedUncategorizedTransactionId,
|
||||
}: CategorizeTransactionAsideProps) {
|
||||
//
|
||||
useEffect(
|
||||
() => () => {
|
||||
closeReconcileMatchingTransaction();
|
||||
},
|
||||
[closeReconcileMatchingTransaction],
|
||||
);
|
||||
|
||||
const handleClose = () => {
|
||||
closeMatchingTransactionAside();
|
||||
};
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
color: rgb(21, 82, 200),
|
||||
}
|
||||
}
|
||||
|
||||
&:hover:not(.active){
|
||||
border-color: #c0c0c0;
|
||||
}
|
||||
@@ -25,7 +24,7 @@
|
||||
margin: 0;
|
||||
}
|
||||
.checkbox:global(.bp4-control.bp4-checkbox) :global .bp4-control-indicator{
|
||||
border-color: #CBCBCB;
|
||||
box-shadow: 0 0 0 1px #CBCBCB;
|
||||
}
|
||||
.checkbox:global(.bp4-control.bp4-checkbox) :global .bp4-control-indicator{
|
||||
margin-right: 4px;
|
||||
@@ -34,9 +33,17 @@
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.checkbox:global(.bp4-control.bp4-checkbox) :global input:checked ~ .bp4-control-indicator{
|
||||
box-shadow: 0 0 0 1px #0069ff;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: #10161A;
|
||||
font-size: 15px;
|
||||
color: #252A33;
|
||||
font-size: 15px;
|
||||
}
|
||||
.label :global strong {
|
||||
font-weight: 500;
|
||||
font-variant-numeric:tabular-nums;
|
||||
}
|
||||
|
||||
.date {
|
||||
|
||||
@@ -9,7 +9,7 @@ export interface MatchTransactionCheckboxProps {
|
||||
active?: boolean;
|
||||
initialActive?: boolean;
|
||||
onChange?: (state: boolean) => void;
|
||||
label: string;
|
||||
label: string | React.ReactNode;
|
||||
date: string;
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ export function MatchTransactionCheckbox({
|
||||
position="apart"
|
||||
onClick={handleClick}
|
||||
>
|
||||
<Stack spacing={3}>
|
||||
<Stack spacing={2}>
|
||||
<span className={styles.label}>{label}</span>
|
||||
<Text className={styles.date}>Date: {date}</Text>
|
||||
</Stack>
|
||||
|
||||
@@ -4,7 +4,7 @@ export const MatchingReconcileFormSchema = Yup.object().shape({
|
||||
type: Yup.string().required().label('Type'),
|
||||
date: Yup.string().required().label('Date'),
|
||||
amount: Yup.string().required().label('Amount'),
|
||||
memo: Yup.string().required().label('Memo'),
|
||||
memo: Yup.string().required().min(3).label('Memo'),
|
||||
referenceNo: Yup.string().label('Refernece #'),
|
||||
category: Yup.string().required().label('Categogry'),
|
||||
});
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// @ts-nocheck
|
||||
import * as R from 'ramda';
|
||||
import { Button, Intent, Position, Tag } from '@blueprintjs/core';
|
||||
import { Form, Formik, FormikValues, useFormikContext } from 'formik';
|
||||
import { Form, Formik, FormikHelpers, useFormikContext } from 'formik';
|
||||
import moment from 'moment';
|
||||
import { round } from 'lodash';
|
||||
import {
|
||||
AccountsSelect,
|
||||
AppToaster,
|
||||
@@ -12,6 +14,7 @@ import {
|
||||
FInputGroup,
|
||||
FMoneyInputGroup,
|
||||
Group,
|
||||
Icon,
|
||||
} from '@/components';
|
||||
import { Aside } from '@/components/Aside/Aside';
|
||||
import { momentFormatter } from '@/utils';
|
||||
@@ -25,29 +28,43 @@ import {
|
||||
import { useCreateCashflowTransaction } from '@/hooks/query';
|
||||
import { useAccountTransactionsContext } from '../../AccountTransactions/AccountTransactionsProvider';
|
||||
import { MatchingReconcileFormSchema } from './MatchingReconcileTransactionForm.schema';
|
||||
import { initialValues } from './_utils';
|
||||
import { initialValues, transformToReq } from './_utils';
|
||||
import { withBanking } from '../../withBanking';
|
||||
|
||||
interface MatchingReconcileTransactionFormProps {
|
||||
onSubmitSuccess?: (values: any) => void;
|
||||
}
|
||||
|
||||
function MatchingReconcileTransactionFormRoot({
|
||||
closeReconcileMatchingTransaction,
|
||||
}) {
|
||||
reconcileMatchingTransactionPendingAmount,
|
||||
|
||||
// #props¿
|
||||
onSubmitSuccess,
|
||||
}: MatchingReconcileTransactionFormProps) {
|
||||
// Mutation create cashflow transaction.
|
||||
const { mutateAsync: createCashflowTransactionMutate } =
|
||||
useCreateCashflowTransaction();
|
||||
|
||||
const { accountId } = useAccountTransactionsContext();
|
||||
|
||||
// Handles the aside close.
|
||||
const handleAsideClose = () => {
|
||||
closeReconcileMatchingTransaction();
|
||||
};
|
||||
// Handle the form submitting.
|
||||
const handleSubmit = (
|
||||
values: MatchingReconcileTransactionValues,
|
||||
{ setSubmitting }: FormikValues<MatchingReconcileTransactionValues>,
|
||||
{
|
||||
setSubmitting,
|
||||
setErrors,
|
||||
}: FormikHelpers<MatchingReconcileTransactionValues>,
|
||||
) => {
|
||||
setSubmitting(true);
|
||||
const _values = transformToReq(values, accountId);
|
||||
|
||||
createCashflowTransactionMutate(_values)
|
||||
.then(() => {
|
||||
.then((res) => {
|
||||
setSubmitting(false);
|
||||
|
||||
AppToaster.show({
|
||||
@@ -55,17 +72,36 @@ function MatchingReconcileTransactionFormRoot({
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
closeReconcileMatchingTransaction();
|
||||
onSubmitSuccess &&
|
||||
onSubmitSuccess({ id: res.data.id, type: 'CashflowTransaction' });
|
||||
})
|
||||
.catch((error) => {
|
||||
setSubmitting(false);
|
||||
|
||||
AppToaster.show({
|
||||
message: 'Something went wrong.',
|
||||
intent: Intent.DANGER,
|
||||
});
|
||||
if (
|
||||
error.response.data?.errors?.find(
|
||||
(e) => e.type === 'BRANCH_ID_REQUIRED',
|
||||
)
|
||||
) {
|
||||
setErrors({
|
||||
branchId: 'The branch is required.',
|
||||
});
|
||||
} else {
|
||||
AppToaster.show({
|
||||
message: 'Something went wrong.',
|
||||
intent: Intent.DANGER,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const _initialValues = {
|
||||
...initialValues,
|
||||
amount: round(Math.abs(reconcileMatchingTransactionPendingAmount), 2) || 0,
|
||||
date: moment().format('YYYY-MM-DD'),
|
||||
type:
|
||||
reconcileMatchingTransactionPendingAmount > 0 ? 'deposit' : 'withdrawal',
|
||||
};
|
||||
|
||||
return (
|
||||
<Aside
|
||||
title={'Create Reconcile Transactions'}
|
||||
@@ -75,7 +111,7 @@ function MatchingReconcileTransactionFormRoot({
|
||||
<MatchingReconcileTransactionBoot>
|
||||
<Formik
|
||||
onSubmit={handleSubmit}
|
||||
initialValues={initialValues}
|
||||
initialValues={_initialValues}
|
||||
validationSchema={MatchingReconcileFormSchema}
|
||||
>
|
||||
<Form className={styles.form}>
|
||||
@@ -93,11 +129,136 @@ function MatchingReconcileTransactionFormRoot({
|
||||
);
|
||||
}
|
||||
|
||||
export const MatchingReconcileTransactionForm = R.compose(withBankingActions)(
|
||||
MatchingReconcileTransactionFormRoot,
|
||||
);
|
||||
export const MatchingReconcileTransactionForm = R.compose(
|
||||
withBankingActions,
|
||||
withBanking(({ reconcileMatchingTransactionPendingAmount }) => ({
|
||||
reconcileMatchingTransactionPendingAmount,
|
||||
})),
|
||||
)(MatchingReconcileTransactionFormRoot);
|
||||
|
||||
export function MatchingReconcileTransactionFooter() {
|
||||
function ReconcileMatchingType() {
|
||||
const { setFieldValue, values } =
|
||||
useFormikContext<MatchingReconcileFormValues>();
|
||||
|
||||
const handleChange = (value: string) => {
|
||||
setFieldValue('type', value);
|
||||
setFieldValue('category');
|
||||
};
|
||||
return (
|
||||
<ContentTabs
|
||||
value={values?.type || 'deposit'}
|
||||
onChange={handleChange}
|
||||
small
|
||||
>
|
||||
<ContentTabs.Tab id={'deposit'} title={'Deposit'} />
|
||||
<ContentTabs.Tab id={'withdrawal'} title={'Withdrawal'} />
|
||||
</ContentTabs>
|
||||
);
|
||||
}
|
||||
|
||||
function CreateReconcileTransactionContent() {
|
||||
const { branches } = useMatchingReconcileTransactionBoot();
|
||||
|
||||
return (
|
||||
<Box className={styles.content}>
|
||||
<ReconcileMatchingType />
|
||||
|
||||
<FFormGroup label={'Date'} name={'date'} fastField>
|
||||
<FDateInput
|
||||
{...momentFormatter('YYYY/MM/DD')}
|
||||
name={'date'}
|
||||
popoverProps={{
|
||||
minimal: false,
|
||||
position: Position.LEFT,
|
||||
modifiers: {
|
||||
preventOverflow: { enabled: true },
|
||||
},
|
||||
boundary: 'viewport',
|
||||
}}
|
||||
inputProps={{ fill: true, leftElement: <Icon icon={'date-range'} /> }}
|
||||
fill
|
||||
fastField
|
||||
/>
|
||||
</FFormGroup>
|
||||
|
||||
<FFormGroup
|
||||
label={'Amount'}
|
||||
name={'amount'}
|
||||
labelInfo={<Tag minimal>Required</Tag>}
|
||||
fastField
|
||||
>
|
||||
<FMoneyInputGroup name={'amount'} fastField />
|
||||
</FFormGroup>
|
||||
|
||||
<MatchingReconcileCategoryField />
|
||||
|
||||
<FFormGroup
|
||||
label={'Memo'}
|
||||
name={'memo'}
|
||||
labelInfo={<Tag minimal>Required</Tag>}
|
||||
fastField
|
||||
>
|
||||
<FInputGroup name={'memo'} fastField />
|
||||
</FFormGroup>
|
||||
|
||||
<FFormGroup label={'Reference No.'} name={'reference_no'} fastField>
|
||||
<FInputGroup name={'reference_no'} />
|
||||
</FFormGroup>
|
||||
|
||||
<FFormGroup
|
||||
name={'branchId'}
|
||||
label={'Branch'}
|
||||
labelInfo={<Tag minimal>Required</Tag>}
|
||||
fastField
|
||||
>
|
||||
<BranchSelect
|
||||
name={'branchId'}
|
||||
branches={branches}
|
||||
popoverProps={{
|
||||
minimal: false,
|
||||
position: Position.LEFT,
|
||||
modifiers: {
|
||||
preventOverflow: { enabled: true },
|
||||
},
|
||||
boundary: 'viewport',
|
||||
}}
|
||||
fastField
|
||||
/>
|
||||
</FFormGroup>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function MatchingReconcileCategoryField() {
|
||||
const { accounts } = useMatchingReconcileTransactionBoot();
|
||||
const { values } = useFormikContext();
|
||||
|
||||
return (
|
||||
<FFormGroup
|
||||
label={'Category'}
|
||||
name={'category'}
|
||||
labelInfo={<Tag minimal>Required</Tag>}
|
||||
fastField
|
||||
>
|
||||
<AccountsSelect
|
||||
name={'category'}
|
||||
items={accounts}
|
||||
popoverProps={{
|
||||
minimal: false,
|
||||
position: Position.LEFT,
|
||||
modifiers: {
|
||||
preventOverflow: { enabled: true },
|
||||
},
|
||||
boundary: 'viewport',
|
||||
}}
|
||||
filterByRootTypes={values.type === 'deposit' ? 'income' : 'expense'}
|
||||
fastField
|
||||
/>
|
||||
</FFormGroup>
|
||||
);
|
||||
}
|
||||
|
||||
function MatchingReconcileTransactionFooter() {
|
||||
const { isSubmitting } = useFormikContext();
|
||||
|
||||
return (
|
||||
@@ -115,85 +276,3 @@ export function MatchingReconcileTransactionFooter() {
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function ReconcileMatchingType() {
|
||||
const { setFieldValue, values } =
|
||||
useFormikContext<MatchingReconcileFormValues>();
|
||||
|
||||
const handleChange = (value: string) => {
|
||||
setFieldValue('type', value);
|
||||
};
|
||||
return (
|
||||
<ContentTabs
|
||||
value={values?.type || 'deposit'}
|
||||
onChange={handleChange}
|
||||
small
|
||||
>
|
||||
<ContentTabs.Tab id={'deposit'} title={'Deposit'} />
|
||||
<ContentTabs.Tab id={'withdrawal'} title={'Withdrawal'} />
|
||||
</ContentTabs>
|
||||
);
|
||||
}
|
||||
|
||||
export function CreateReconcileTransactionContent() {
|
||||
const { accounts, branches } = useMatchingReconcileTransactionBoot();
|
||||
|
||||
return (
|
||||
<Box className={styles.content}>
|
||||
<ReconcileMatchingType />
|
||||
|
||||
<FFormGroup label={'Date'} name={'date'}>
|
||||
<FDateInput
|
||||
{...momentFormatter('YYYY/MM/DD')}
|
||||
name={'date'}
|
||||
formatDate={(date) => date.toLocaleString()}
|
||||
popoverProps={{
|
||||
position: Position.LEFT,
|
||||
}}
|
||||
inputProps={{ fill: true }}
|
||||
fill
|
||||
/>
|
||||
</FFormGroup>
|
||||
|
||||
<FFormGroup
|
||||
label={'Amount'}
|
||||
name={'amount'}
|
||||
labelInfo={<Tag minimal>Required</Tag>}
|
||||
>
|
||||
<FMoneyInputGroup name={'amount'} />
|
||||
</FFormGroup>
|
||||
|
||||
<FFormGroup
|
||||
label={'Category'}
|
||||
name={'category'}
|
||||
labelInfo={<Tag minimal>Required</Tag>}
|
||||
>
|
||||
<AccountsSelect
|
||||
name={'category'}
|
||||
items={accounts}
|
||||
popoverProps={{ minimal: false, position: Position.LEFT }}
|
||||
/>
|
||||
</FFormGroup>
|
||||
|
||||
<FFormGroup
|
||||
label={'Memo'}
|
||||
name={'memo'}
|
||||
labelInfo={<Tag minimal>Required</Tag>}
|
||||
>
|
||||
<FInputGroup name={'memo'} />
|
||||
</FFormGroup>
|
||||
|
||||
<FFormGroup label={'Reference No.'} name={'reference_no'}>
|
||||
<FInputGroup name={'reference_no'} />
|
||||
</FFormGroup>
|
||||
|
||||
<FFormGroup name={'branchId'} label={'Branch'}>
|
||||
<BranchSelect
|
||||
name={'branchId'}
|
||||
branches={branches}
|
||||
popoverProps={{ minimal: true }}
|
||||
/>
|
||||
</FFormGroup>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// @ts-nocheck
|
||||
import { isEmpty } from 'lodash';
|
||||
import * as R from 'ramda';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { AnchorButton, Button, Intent, Tag, Text } from '@blueprintjs/core';
|
||||
import { FastField, FastFieldProps, Formik, useFormikContext } from 'formik';
|
||||
import { AppToaster, Box, FormatNumber, Group, Stack } from '@/components';
|
||||
@@ -39,9 +40,6 @@ const initialValues = {
|
||||
function MatchingBankTransactionRoot({
|
||||
// #withBankingActions
|
||||
closeMatchingTransactionAside,
|
||||
|
||||
// #withBanking
|
||||
openReconcileMatchingTransaction,
|
||||
}) {
|
||||
const { uncategorizedTransactionId } = useCategorizeTransactionTabsBoot();
|
||||
const { mutateAsync: matchTransaction } = useMatchUncategorizedTransaction();
|
||||
@@ -71,6 +69,18 @@ function MatchingBankTransactionRoot({
|
||||
closeMatchingTransactionAside();
|
||||
})
|
||||
.catch((err) => {
|
||||
if (
|
||||
err.response?.data.errors.find(
|
||||
(e) => e.type === 'TOTAL_MATCHING_TRANSACTIONS_INVALID',
|
||||
)
|
||||
) {
|
||||
AppToaster.show({
|
||||
message: `The total amount does not equal the uncategorized transaction.`,
|
||||
intent: Intent.DANGER,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
AppToaster.show({
|
||||
intent: Intent.DANGER,
|
||||
message: 'Something went wrong.',
|
||||
@@ -84,25 +94,86 @@ function MatchingBankTransactionRoot({
|
||||
uncategorizedTransactionId={uncategorizedTransactionId}
|
||||
>
|
||||
<Formik initialValues={initialValues} onSubmit={handleSubmit}>
|
||||
<>
|
||||
<MatchingBankTransactionContent />
|
||||
|
||||
{openReconcileMatchingTransaction && (
|
||||
<MatchingReconcileTransactionForm />
|
||||
)}
|
||||
{!openReconcileMatchingTransaction && <MatchTransactionFooter />}
|
||||
</>
|
||||
<MatchingBankTransactionFormContent />
|
||||
</Formik>
|
||||
</MatchingTransactionBoot>
|
||||
);
|
||||
}
|
||||
|
||||
export const MatchingBankTransaction = R.compose(
|
||||
export const MatchingBankTransaction = R.compose(withBankingActions)(
|
||||
MatchingBankTransactionRoot,
|
||||
);
|
||||
|
||||
/**
|
||||
* Matching bank transaction form content.
|
||||
* @returns {React.ReactNode}
|
||||
*/
|
||||
const MatchingBankTransactionFormContent = R.compose(
|
||||
withBankingActions,
|
||||
withBanking(({ openReconcileMatchingTransaction }) => ({
|
||||
openReconcileMatchingTransaction,
|
||||
})),
|
||||
)(MatchingBankTransactionRoot);
|
||||
)(
|
||||
({
|
||||
// #withBanking
|
||||
openReconcileMatchingTransaction,
|
||||
}) => {
|
||||
const {
|
||||
isMatchingTransactionsFetching,
|
||||
isMatchingTransactionsSuccess,
|
||||
matches,
|
||||
} = useMatchingTransactionBoot();
|
||||
const [pending, setPending] = useState<null | {
|
||||
refId: number;
|
||||
refType: string;
|
||||
}>(null);
|
||||
|
||||
const { setFieldValue } = useFormikContext();
|
||||
|
||||
// This effect is responsible for automatically marking a transaction as matched
|
||||
// when the matching process is successful and not currently fetching.
|
||||
useEffect(() => {
|
||||
if (
|
||||
pending &&
|
||||
isMatchingTransactionsSuccess &&
|
||||
!isMatchingTransactionsFetching
|
||||
) {
|
||||
const foundMatch = matches?.find(
|
||||
(m) =>
|
||||
m.referenceType === pending?.refType &&
|
||||
m.referenceId === pending?.refId,
|
||||
);
|
||||
if (foundMatch) {
|
||||
setFieldValue(`matched.${pending.refType}-${pending.refId}`, true);
|
||||
}
|
||||
setPending(null);
|
||||
}
|
||||
}, [
|
||||
isMatchingTransactionsFetching,
|
||||
isMatchingTransactionsSuccess,
|
||||
matches,
|
||||
pending,
|
||||
setFieldValue,
|
||||
]);
|
||||
|
||||
const handleReconcileFormSubmitSuccess = (payload) => {
|
||||
setPending({ refId: payload.id, refType: payload.type });
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<MatchingBankTransactionContent />
|
||||
|
||||
{openReconcileMatchingTransaction && (
|
||||
<MatchingReconcileTransactionForm
|
||||
onSubmitSuccess={handleReconcileFormSubmitSuccess}
|
||||
/>
|
||||
)}
|
||||
{!openReconcileMatchingTransaction && <MatchTransactionFooter />}
|
||||
</>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
function MatchingBankTransactionContent() {
|
||||
return (
|
||||
@@ -141,8 +212,8 @@ function PerfectMatchingTransactions() {
|
||||
key={index}
|
||||
label={`${match.transsactionTypeFormatted} for ${match.amountFormatted}`}
|
||||
date={match.dateFormatted}
|
||||
transactionId={match.transactionId}
|
||||
transactionType={match.transactionType}
|
||||
transactionId={match.referenceId}
|
||||
transactionType={match.referenceType}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
@@ -166,9 +237,6 @@ function PossibleMatchingTransactions() {
|
||||
<Box className={styles.matchBar}>
|
||||
<Stack spacing={2}>
|
||||
<h2 className={styles.matchBarTitle}>Possible Matches</h2>
|
||||
<Text style={{ fontSize: 12, color: '#5C7080' }}>
|
||||
Transactions up to 20 Aug 2019
|
||||
</Text>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
@@ -176,10 +244,15 @@ function PossibleMatchingTransactions() {
|
||||
{possibleMatches.map((match, index) => (
|
||||
<MatchTransactionField
|
||||
key={index}
|
||||
label={`${match.transsactionTypeFormatted} for ${match.amountFormatted}`}
|
||||
label={
|
||||
<>
|
||||
{`${match.transsactionTypeFormatted} for `}
|
||||
<strong>{match.amountFormatted}</strong>
|
||||
</>
|
||||
}
|
||||
date={match.dateFormatted}
|
||||
transactionId={match.transactionId}
|
||||
transactionType={match.transactionType}
|
||||
transactionId={match.referenceId}
|
||||
transactionType={match.referenceType}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
@@ -240,7 +313,7 @@ const MatchTransactionFooter = R.compose(withBankingActions)(
|
||||
submitForm();
|
||||
};
|
||||
const handleReconcileTransaction = () => {
|
||||
openReconcileMatchingTransaction();
|
||||
openReconcileMatchingTransaction(totalPending);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -258,7 +331,7 @@ const MatchTransactionFooter = R.compose(withBankingActions)(
|
||||
</AnchorButton>
|
||||
)}
|
||||
<Text
|
||||
style={{ fontSize: 14, marginLeft: 'auto', color: '#5F6B7C' }}
|
||||
style={{ fontSize: 14, marginLeft: 'auto', color: '#404854' }}
|
||||
tagName="span"
|
||||
>
|
||||
Pending <FormatNumber value={totalPending} currency={'USD'} />
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { defaultTo } from 'lodash';
|
||||
import React, { createContext } from 'react';
|
||||
import { defaultTo } from 'lodash';
|
||||
import * as R from 'ramda';
|
||||
import { useGetBankTransactionsMatches } from '@/hooks/query/bank-rules';
|
||||
|
||||
interface MatchingTransactionBootValues {
|
||||
isMatchingTransactionsLoading: boolean;
|
||||
isMatchingTransactionsFetching: boolean;
|
||||
isMatchingTransactionsSuccess: boolean;
|
||||
possibleMatches: Array<any>;
|
||||
perfectMatchesCount: number;
|
||||
perfectMatches: Array<any>;
|
||||
@@ -26,13 +29,24 @@ function MatchingTransactionBoot({
|
||||
const {
|
||||
data: matchingTransactions,
|
||||
isLoading: isMatchingTransactionsLoading,
|
||||
isFetching: isMatchingTransactionsFetching,
|
||||
isSuccess: isMatchingTransactionsSuccess,
|
||||
} = useGetBankTransactionsMatches(uncategorizedTransactionId);
|
||||
|
||||
const possibleMatches = defaultTo(matchingTransactions?.possibleMatches, []);
|
||||
const perfectMatchesCount = matchingTransactions?.perfectMatches?.length || 0;
|
||||
const perfectMatches = defaultTo(matchingTransactions?.perfectMatches, []);
|
||||
|
||||
const matches = R.concat(perfectMatches, possibleMatches);
|
||||
|
||||
const provider = {
|
||||
isMatchingTransactionsLoading,
|
||||
possibleMatches: defaultTo(matchingTransactions?.possibleMatches, []),
|
||||
perfectMatchesCount: matchingTransactions?.perfectMatches?.length || 0,
|
||||
perfectMatches: defaultTo(matchingTransactions?.perfectMatches, []),
|
||||
isMatchingTransactionsFetching,
|
||||
isMatchingTransactionsSuccess,
|
||||
possibleMatches,
|
||||
perfectMatchesCount,
|
||||
perfectMatches,
|
||||
matches,
|
||||
} as MatchingTransactionBootValues;
|
||||
|
||||
return <RuleFormBootContext.Provider value={provider} {...props} />;
|
||||
|
||||
@@ -24,12 +24,14 @@ export const useGetPendingAmountMatched = () => {
|
||||
return useMemo(() => {
|
||||
const matchedItems = [...perfectMatches, ...possibleMatches].filter(
|
||||
(match) => {
|
||||
const key = `${match.transactionType}-${match.transactionId}`;
|
||||
const key = `${match.referenceType}-${match.referenceId}`;
|
||||
return values.matched[key];
|
||||
},
|
||||
);
|
||||
const totalMatchedAmount = matchedItems.reduce(
|
||||
(total, item) => total + parseFloat(item.amount),
|
||||
(total, item) =>
|
||||
total +
|
||||
(item.transactionNormal === 'debit' ? 1 : -1) * parseFloat(item.amount),
|
||||
0,
|
||||
);
|
||||
const amount = uncategorizedTransaction.amount;
|
||||
|
||||
@@ -9,7 +9,10 @@ export const withBanking = (mapState) => {
|
||||
selectedUncategorizedTransactionId:
|
||||
state.plaid.uncategorizedTransactionIdForMatching,
|
||||
openReconcileMatchingTransaction:
|
||||
state.plaid.openReconcileMatchingTransaction,
|
||||
state.plaid.openReconcileMatchingTransaction.isOpen,
|
||||
|
||||
reconcileMatchingTransactionPendingAmount:
|
||||
state.plaid.openReconcileMatchingTransaction.pending,
|
||||
};
|
||||
return mapState ? mapState(mapped, state, props) : mapped;
|
||||
};
|
||||
|
||||
@@ -11,7 +11,7 @@ export interface WithBankingActionsProps {
|
||||
setUncategorizedTransactionIdForMatching: (
|
||||
uncategorizedTransactionId: number,
|
||||
) => void;
|
||||
openReconcileMatchingTransaction: () => void;
|
||||
openReconcileMatchingTransaction: (pendingAmount: number) => void;
|
||||
closeReconcileMatchingTransaction: () => void;
|
||||
}
|
||||
|
||||
@@ -24,8 +24,8 @@ const mapDipatchToProps = (dispatch: any): WithBankingActionsProps => ({
|
||||
dispatch(
|
||||
setUncategorizedTransactionIdForMatching(uncategorizedTransactionId),
|
||||
),
|
||||
openReconcileMatchingTransaction: () =>
|
||||
dispatch(openReconcileMatchingTransaction()),
|
||||
openReconcileMatchingTransaction: (pendingAmount: number) =>
|
||||
dispatch(openReconcileMatchingTransaction({ pending: pendingAmount })),
|
||||
closeReconcileMatchingTransaction: () =>
|
||||
dispatch(closeReconcileMatchingTransaction()),
|
||||
});
|
||||
|
||||
@@ -37,7 +37,7 @@ interface CreateBankRuleResponse {}
|
||||
/**
|
||||
* Creates a new bank rule.
|
||||
* @param {UseMutationOptions<CreateBankRuleValues, Error, CreateBankRuleValues>} options -
|
||||
* @returns {UseMutationResult<CreateBankRuleValues, Error, CreateBankRuleValues>}
|
||||
* @returns {UseMutationResult<CreateBankRuleValues, Error, CreateBankRuleValues>}TCHES
|
||||
*/
|
||||
export function useCreateBankRule(
|
||||
options?: UseMutationOptions<
|
||||
@@ -235,6 +235,12 @@ export function useExcludeUncategorizedTransaction(
|
||||
queryClient.invalidateQueries(
|
||||
t.CASHFLOW_ACCOUNT_UNCATEGORIZED_TRANSACTIONS_INFINITY,
|
||||
);
|
||||
// Invalidate accounts.
|
||||
queryClient.invalidateQueries(t.ACCOUNTS);
|
||||
queryClient.invalidateQueries(t.ACCOUNT);
|
||||
|
||||
// invalidate bank account summary.
|
||||
queryClient.invalidateQueries(QUERY_KEY.BANK_ACCOUNT_SUMMARY_META);
|
||||
},
|
||||
...options,
|
||||
},
|
||||
@@ -282,6 +288,12 @@ export function useUnexcludeUncategorizedTransaction(
|
||||
queryClient.invalidateQueries(
|
||||
t.CASHFLOW_ACCOUNT_UNCATEGORIZED_TRANSACTIONS_INFINITY,
|
||||
);
|
||||
// Invalidate accounts.
|
||||
queryClient.invalidateQueries(t.ACCOUNTS);
|
||||
queryClient.invalidateQueries(t.ACCOUNT);
|
||||
|
||||
// Invalidate bank account summary.
|
||||
queryClient.invalidateQueries(QUERY_KEY.BANK_ACCOUNT_SUMMARY_META);
|
||||
},
|
||||
...options,
|
||||
},
|
||||
@@ -322,6 +334,60 @@ export function useMatchUncategorizedTransaction(
|
||||
queryClient.invalidateQueries(
|
||||
t.CASHFLOW_ACCOUNT_UNCATEGORIZED_TRANSACTIONS_INFINITY,
|
||||
);
|
||||
queryClient.invalidateQueries(t.CASHFLOW_ACCOUNT_TRANSACTIONS_INFINITY);
|
||||
|
||||
// Invalidate accounts.
|
||||
queryClient.invalidateQueries(t.ACCOUNTS);
|
||||
queryClient.invalidateQueries(t.ACCOUNT);
|
||||
|
||||
// Invalidate bank account summary.
|
||||
queryClient.invalidateQueries(QUERY_KEY.BANK_ACCOUNT_SUMMARY_META);
|
||||
},
|
||||
...props,
|
||||
});
|
||||
}
|
||||
|
||||
interface UnmatchUncategorizedTransactionValues {
|
||||
id: number;
|
||||
}
|
||||
interface UnmatchUncategorizedTransactionRes {}
|
||||
|
||||
/**
|
||||
* Unmatch the given matched uncategorized transaction.
|
||||
* @param {UseMutationOptions<UnmatchUncategorizedTransactionRes, Error, UnmatchUncategorizedTransactionValues>} props
|
||||
* @returns {UseMutationResult<UnmatchUncategorizedTransactionRes, Error, UnmatchUncategorizedTransactionValues>}
|
||||
*/
|
||||
export function useUnmatchMatchedUncategorizedTransaction(
|
||||
props?: UseMutationOptions<
|
||||
UnmatchUncategorizedTransactionRes,
|
||||
Error,
|
||||
UnmatchUncategorizedTransactionValues
|
||||
>,
|
||||
): UseMutationResult<
|
||||
UnmatchUncategorizedTransactionRes,
|
||||
Error,
|
||||
UnmatchUncategorizedTransactionValues
|
||||
> {
|
||||
const queryClient = useQueryClient();
|
||||
const apiRequest = useApiRequest();
|
||||
|
||||
return useMutation<
|
||||
UnmatchUncategorizedTransactionRes,
|
||||
Error,
|
||||
UnmatchUncategorizedTransactionValues
|
||||
>(({ id }) => apiRequest.post(`/banking/matches/unmatch/${id}`), {
|
||||
onSuccess: (res, id) => {
|
||||
queryClient.invalidateQueries(
|
||||
t.CASHFLOW_ACCOUNT_UNCATEGORIZED_TRANSACTIONS_INFINITY,
|
||||
);
|
||||
queryClient.invalidateQueries(t.CASHFLOW_ACCOUNT_TRANSACTIONS_INFINITY);
|
||||
|
||||
// Invalidate accounts.
|
||||
queryClient.invalidateQueries(t.ACCOUNTS);
|
||||
queryClient.invalidateQueries(t.ACCOUNT);
|
||||
|
||||
// Invalidate bank account summary.
|
||||
queryClient.invalidateQueries(QUERY_KEY.BANK_ACCOUNT_SUMMARY_META);
|
||||
},
|
||||
...props,
|
||||
});
|
||||
|
||||
@@ -58,6 +58,8 @@ export function useCreateCashflowTransaction(props) {
|
||||
onSuccess: () => {
|
||||
// Invalidate queries.
|
||||
commonInvalidateQueries(queryClient);
|
||||
|
||||
queryClient.invalidateQueries('BANK_TRANSACTION_MATCHES');
|
||||
},
|
||||
...props,
|
||||
},
|
||||
@@ -251,6 +253,9 @@ export function useCategorizeTransaction(props) {
|
||||
queryClient.invalidateQueries(
|
||||
t.CASHFLOW_ACCOUNT_UNCATEGORIZED_TRANSACTIONS_INFINITY,
|
||||
);
|
||||
|
||||
// Invalidate bank account summary.
|
||||
queryClient.invalidateQueries('BANK_ACCOUNT_SUMMARY_META');
|
||||
},
|
||||
...props,
|
||||
},
|
||||
@@ -274,6 +279,9 @@ export function useUncategorizeTransaction(props) {
|
||||
queryClient.invalidateQueries(
|
||||
t.CASHFLOW_ACCOUNT_UNCATEGORIZED_TRANSACTIONS_INFINITY,
|
||||
);
|
||||
|
||||
// Invalidate bank account summary.
|
||||
queryClient.invalidateQueries('BANK_ACCOUNT_SUMMARY_META');
|
||||
},
|
||||
...props,
|
||||
},
|
||||
|
||||
@@ -629,4 +629,10 @@ export default {
|
||||
],
|
||||
viewBox: '0 0 16 16',
|
||||
},
|
||||
unlink: {
|
||||
path: [
|
||||
'M11.9975 0.00500107C14.2061 0.00500107 15.995 1.79388 15.995 4.0025C15.995 5.11181 15.5353 6.0912 14.8058 6.81075L14.8257 6.83074L13.8264 7.83011L13.8064 7.81012C13.2562 8.36798 12.5482 8.76807 11.7539 8.92548L10.8249 7.99643L12.4073 6.401L13.4066 5.40163L13.3966 5.39163C13.7564 5.03186 13.9963 4.54217 13.9963 3.99251C13.9963 2.8932 13.0968 1.99376 11.9975 1.99376C11.4479 1.99376 10.9582 2.23361 10.5984 2.59338L10.5884 2.58339L8.0001 5.17168L7.07559 4.24717C7.23518 3.45247 7.63943 2.74409 8.18989 2.19363L8.1699 2.17365L9.16928 1.17427L9.18926 1.19426C9.90882 0.464714 10.8982 0.00500107 11.9975 0.00500107ZM2.29289 2.29289C2.68341 1.90237 3.31657 1.90237 3.7071 2.29289L13.7071 12.2929C14.0976 12.6834 14.0976 13.3166 13.7071 13.7071C13.3166 14.0976 12.6834 14.0976 12.2929 13.7071L8.93565 10.3499C8.97565 10.562 8.99938 10.7781 8.99938 10.9981C8.99938 12.0974 8.53966 13.0868 7.81012 13.8064L7.83011 13.8263L6.83073 14.8257L6.81074 14.8057C6.09119 15.5353 5.10181 15.995 4.0025 15.995C1.79388 15.995 0.00499688 14.2061 0.00499688 11.9975C0.00499688 10.8982 0.464709 9.90879 1.19425 9.18924L1.17427 9.16925L2.17364 8.16988L2.19363 8.18986C2.91318 7.46032 3.90256 7.00061 5.00187 7.00061C5.2251 7.00061 5.44064 7.02369 5.65087 7.06509L2.29289 3.7071C1.90236 3.31658 1.90236 2.68341 2.29289 2.29289ZM8.00244 9.41666L8.707 10.1212L5.41162 13.4166L5.40162 13.4066C5.04185 13.7664 4.55216 14.0062 4.0025 14.0062C2.90319 14.0062 2.00375 13.1068 2.00375 12.0075C2.00375 11.4578 2.2436 10.9681 2.60337 10.6084L2.59338 10.5984L5.88876 7.30298L6.58333 7.99755L4.29231 10.2886C4.11243 10.4685 4.00249 10.7183 4.00249 10.9981C4.00249 11.5478 4.45221 11.9975 5.00187 11.9975C5.28169 11.9975 5.53154 11.8876 5.71143 11.7077L8.00244 9.41666ZM8.70466 5.87623L10.1238 7.29534L11.7077 5.71143C11.8876 5.53154 11.9975 5.2817 11.9975 5.00187C11.9975 4.45222 11.5478 4.0025 10.9981 4.0025C10.7183 4.0025 10.4685 4.11243 10.2886 4.29232L8.70466 5.87623Z',
|
||||
],
|
||||
viewBox: '0 0 16 16',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ interface StorePlaidState {
|
||||
plaidToken: string;
|
||||
openMatchingTransactionAside: boolean;
|
||||
uncategorizedTransactionIdForMatching: number | null;
|
||||
openReconcileMatchingTransaction: boolean;
|
||||
openReconcileMatchingTransaction: { isOpen: boolean; pending: number };
|
||||
}
|
||||
|
||||
export const PlaidSlice = createSlice({
|
||||
@@ -13,7 +13,10 @@ export const PlaidSlice = createSlice({
|
||||
plaidToken: '',
|
||||
openMatchingTransactionAside: false,
|
||||
uncategorizedTransactionIdForMatching: null,
|
||||
openReconcileMatchingTransaction: false,
|
||||
openReconcileMatchingTransaction: {
|
||||
isOpen: false,
|
||||
pending: 0,
|
||||
},
|
||||
} as StorePlaidState,
|
||||
reducers: {
|
||||
setPlaidId: (state: StorePlaidState, action: PayloadAction<string>) => {
|
||||
@@ -37,12 +40,17 @@ export const PlaidSlice = createSlice({
|
||||
state.uncategorizedTransactionIdForMatching = null;
|
||||
},
|
||||
|
||||
openReconcileMatchingTransaction: (state: StorePlaidState) => {
|
||||
state.openReconcileMatchingTransaction = true;
|
||||
openReconcileMatchingTransaction: (
|
||||
state: StorePlaidState,
|
||||
action: PayloadAction<{ pending: number }>,
|
||||
) => {
|
||||
state.openReconcileMatchingTransaction.isOpen = true;
|
||||
state.openReconcileMatchingTransaction.pending = action.payload.pending;
|
||||
},
|
||||
|
||||
closeReconcileMatchingTransaction: (state: StorePlaidState) => {
|
||||
state.openReconcileMatchingTransaction = false;
|
||||
state.openReconcileMatchingTransaction.isOpen = false;
|
||||
state.openReconcileMatchingTransaction.pending = 0;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -50,3 +50,9 @@ $form-check-input-indeterminate-bg-image: url("data:image/svg+xml,<svg xmlns='ht
|
||||
// z-indexs
|
||||
$zindex-dashboard-splash-screen: 39;
|
||||
$zindex-toast: 40;
|
||||
|
||||
// Controls
|
||||
$control-checked-background-color: #0069ff !default;
|
||||
$control-checked-background-color-hover: #0069ff !default;
|
||||
$control-checked-background-color-active: #0069ff !default;
|
||||
$control-box-shadow: inset 0 0 0 1px #666 !default;
|
||||
@@ -271,203 +271,6 @@ label.bp4-label {
|
||||
.select-list--tooltip-items .bp4-popover-target {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@mixin control-checked-colors($selector: ':checked') {
|
||||
input#{$selector}~.#{$ns}-control-indicator {
|
||||
box-shadow: none;
|
||||
background-color: $control-checked-background-color;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
&:hover input#{$selector}~.#{$ns}-control-indicator {
|
||||
box-shadow: none;
|
||||
background-color: $control-checked-background-color-hover;
|
||||
border-color: $control-checked-background-color-active;
|
||||
}
|
||||
|
||||
input:not(:disabled):active#{$selector}~.#{$ns}-control-indicator {
|
||||
box-shadow: none;
|
||||
background-color: $control-checked-background-color-active;
|
||||
border-color: $control-checked-background-color-active;
|
||||
}
|
||||
|
||||
input:disabled#{$selector}~.#{$ns}-control-indicator {
|
||||
box-shadow: none;
|
||||
background: rgba($control-checked-background-color, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
///@extend
|
||||
.#{$ns}-control {
|
||||
input:checked~.#{$ns}-control-indicator {
|
||||
box-shadow: none;
|
||||
background-color: transparent;
|
||||
background-image: none;
|
||||
}
|
||||
|
||||
&:hover input:checked~.#{$ns}-control-indicator {
|
||||
box-shadow: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
input:not(:disabled):active:checked~.#{$ns}-control-indicator {
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
input:disabled:checked~.#{$ns}-control-indicator {
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&.#{$ns}-disabled {
|
||||
cursor: not-allowed;
|
||||
color: $pt-text-color-disabled;
|
||||
}
|
||||
|
||||
&.#{$ns}-inline {
|
||||
display: inline-block;
|
||||
margin-right: $pt-grid-size * 2;
|
||||
}
|
||||
|
||||
.#{$ns}-control-indicator {
|
||||
box-shadow: 0 0 0 transparent;
|
||||
background-clip: padding-box;
|
||||
background-color: transparent;
|
||||
background-image: none;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
|
||||
&::before {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
&.bp4-large .#{$ns}-control-indicator {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
|
||||
&::before {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .#{$ns}-control-indicator {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
input:not(:disabled):active~.#{$ns}-control-indicator {
|
||||
box-shadow: 0 0 0 transparent;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/*
|
||||
Checkbox
|
||||
|
||||
Markup:
|
||||
<label class="#{$ns}-control #{$ns}-checkbox {{.modifier}}">
|
||||
<input type="checkbox" {{:modifier}} />
|
||||
<span class="#{$ns}-control-indicator"></span>
|
||||
Checkbox
|
||||
</label>
|
||||
|
||||
:checked - Checked
|
||||
:disabled - Disabled. Also add <code>.#{$ns}-disabled</code> to <code>.#{$ns}-control</code> to change text color (not shown below).
|
||||
:indeterminate - Indeterminate. Note that this style can only be achieved via JavaScript
|
||||
<code>input.indeterminate = true</code>.
|
||||
.#{$ns}-align-right - Right-aligned indicator
|
||||
.#{$ns}-large - Large
|
||||
|
||||
Styleguide checkbox
|
||||
*/
|
||||
&.#{$ns}-checkbox {
|
||||
&:hover input:indeterminate~.#{$ns}-control-indicator {
|
||||
// box-shadow: 0 0 0 transparent;
|
||||
}
|
||||
|
||||
@mixin indicator-inline-icon($icon) {
|
||||
&::before {
|
||||
// embed SVG icon image as backgroud-image above gradient.
|
||||
// the SVG image content is inlined into the CSS, so use this sparingly.
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@include control-checked-colors(':checked');
|
||||
|
||||
// make :indeterminate look like :checked _for Checkbox only_
|
||||
@include control-checked-colors(':indeterminate');
|
||||
|
||||
.#{$ns}-control-indicator {
|
||||
border: 1px solid #c6c6c6;
|
||||
border-radius: $pt-border-radius;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
input:checked~.#{$ns}-control-indicator {
|
||||
background-image: escape-svg($form-check-input-checked-bg-image);
|
||||
border-color: $form-check-input-checked-bg-color;
|
||||
background-color: $form-check-input-checked-bg-color;
|
||||
}
|
||||
|
||||
input:indeterminate~.#{$ns}-control-indicator {
|
||||
// background-image: escape-svg($form-check-input-indeterminate-bg-image);
|
||||
border-color: $form-check-input-checked-bg-color;
|
||||
background-color: $form-check-input-checked-bg-color;
|
||||
box-shadow: 0 0 0 0 transparent;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Radio
|
||||
|
||||
Markup:
|
||||
<label class="#{$ns}-control #{$ns}-radio {{.modifier}}">
|
||||
<input type="radio" name="docs-radio-regular" {{:modifier}} />
|
||||
<span class="#{$ns}-control-indicator"></span>
|
||||
Radio
|
||||
</label>
|
||||
|
||||
:checked - Selected
|
||||
:disabled - Disabled. Also add <code>.#{$ns}-disabled</code> to <code>.#{$ns}-control</code> to change text color (not shown below).
|
||||
.#{$ns}-align-right - Right-aligned indicator
|
||||
.#{$ns}-large - Large
|
||||
|
||||
Styleguide radio
|
||||
*/
|
||||
&.#{$ns}-radio {
|
||||
.#{$ns}-control-indicator {
|
||||
border: 2px solid #cecece;
|
||||
background-color: #fff;
|
||||
|
||||
&::before {
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
input:checked~.#{$ns}-control-indicator {
|
||||
border-color: $form-check-input-checked-bg-color;
|
||||
|
||||
&::before {
|
||||
background-image: radial-gradient($form-check-input-checked-bg-color 40%,
|
||||
transparent 40%);
|
||||
}
|
||||
}
|
||||
|
||||
input:checked:disabled~.#{$ns}-control-indicator::before {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
input:focus~.#{$ns}-control-indicator {
|
||||
-moz-outline-radius: $control-indicator-size;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bp4-menu-item::before,
|
||||
.bp4-menu-item>.bp4-icon {
|
||||
color: #4b5d6b;
|
||||
|
||||
Reference in New Issue
Block a user