mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-21 15:20:34 +00:00
Compare commits
31 Commits
reconcile-
...
v0.18.9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53f37f4f48 | ||
|
|
0a7b522b87 | ||
|
|
9e6500ac79 | ||
|
|
b93cb546f4 | ||
|
|
6d17f9cbeb | ||
|
|
fe214b1b2d | ||
|
|
6b6b73b77c | ||
|
|
107a6f793b | ||
|
|
67d155759e | ||
|
|
7e2e87256f | ||
|
|
df7790d7c1 | ||
|
|
72128a72c4 | ||
|
|
eb3f23554f | ||
|
|
69ddf43b3e | ||
|
|
249eadaeaa | ||
|
|
59168bc691 | ||
|
|
81b26c6f13 | ||
|
|
da435d85d9 | ||
|
|
533006b90e | ||
|
|
d096e49d45 | ||
|
|
73acdb6240 | ||
|
|
38d4122d11 | ||
|
|
24a77c81b3 | ||
|
|
7f41b4280e | ||
|
|
aa89653967 | ||
|
|
b80bc95fa5 | ||
|
|
9a5befbee7 | ||
|
|
b7487f19d3 | ||
|
|
cd9039fe16 | ||
|
|
87f60f7461 | ||
|
|
202179ec0b |
@@ -2,6 +2,14 @@
|
|||||||
|
|
||||||
All notable changes to Bigcapital server-side will be in this file.
|
All notable changes to Bigcapital server-side will be in this file.
|
||||||
|
|
||||||
|
## [v0.18.0] - 10-08-2024
|
||||||
|
|
||||||
|
* feat: Bank rules for automated categorization by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/511
|
||||||
|
* feat: Categorize & match bank transaction by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/511
|
||||||
|
* feat: Reconcile match transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/522
|
||||||
|
* fix: Issues in matching transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/523
|
||||||
|
* fix: Cashflow transactions types by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/524
|
||||||
|
|
||||||
## [v0.17.5] - 17-06-2024
|
## [v0.17.5] - 17-06-2024
|
||||||
|
|
||||||
* fix: Balance sheet and P/L nested accounts by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/501
|
* fix: Balance sheet and P/L nested accounts by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/501
|
||||||
|
|||||||
@@ -38,15 +38,7 @@ export class BankingRulesController extends BaseController {
|
|||||||
body('conditions.*.value').exists(),
|
body('conditions.*.value').exists(),
|
||||||
|
|
||||||
// Assign
|
// Assign
|
||||||
body('assign_category')
|
body('assign_category').isString(),
|
||||||
.isString()
|
|
||||||
.isIn([
|
|
||||||
'interest_income',
|
|
||||||
'other_income',
|
|
||||||
'deposit',
|
|
||||||
'expense',
|
|
||||||
'owner_drawings',
|
|
||||||
]),
|
|
||||||
body('assign_account_id').isInt({ min: 0 }),
|
body('assign_account_id').isInt({ min: 0 }),
|
||||||
body('assign_payee').isString().optional({ nullable: true }),
|
body('assign_payee').isString().optional({ nullable: true }),
|
||||||
body('assign_memo').isString().optional({ nullable: true }),
|
body('assign_memo').isString().optional({ nullable: true }),
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ export default class BillsPayments extends BaseController {
|
|||||||
check('vendor_id').exists().isNumeric().toInt(),
|
check('vendor_id').exists().isNumeric().toInt(),
|
||||||
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
|
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
|
||||||
|
|
||||||
|
check('amount').exists().isNumeric().toFloat(),
|
||||||
check('payment_account_id').exists().isNumeric().toInt(),
|
check('payment_account_id').exists().isNumeric().toInt(),
|
||||||
check('payment_number').optional({ nullable: true }).trim().escape(),
|
check('payment_number').optional({ nullable: true }).trim().escape(),
|
||||||
check('payment_date').exists(),
|
check('payment_date').exists(),
|
||||||
@@ -118,7 +119,7 @@ export default class BillsPayments extends BaseController {
|
|||||||
check('reference').optional().trim().escape(),
|
check('reference').optional().trim().escape(),
|
||||||
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
|
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||||
|
|
||||||
check('entries').exists().isArray({ min: 1 }),
|
check('entries').exists().isArray(),
|
||||||
check('entries.*.index').optional().isNumeric().toInt(),
|
check('entries.*.index').optional().isNumeric().toInt(),
|
||||||
check('entries.*.bill_id').exists().isNumeric().toInt(),
|
check('entries.*.bill_id').exists().isNumeric().toInt(),
|
||||||
check('entries.*.payment_amount').exists().isNumeric().toFloat(),
|
check('entries.*.payment_amount').exists().isNumeric().toFloat(),
|
||||||
|
|||||||
@@ -150,6 +150,7 @@ export default class PaymentReceivesController extends BaseController {
|
|||||||
check('customer_id').exists().isNumeric().toInt(),
|
check('customer_id').exists().isNumeric().toInt(),
|
||||||
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
|
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
|
||||||
|
|
||||||
|
check('amount').exists().isNumeric().toFloat(),
|
||||||
check('payment_date').exists(),
|
check('payment_date').exists(),
|
||||||
check('reference_no').optional(),
|
check('reference_no').optional(),
|
||||||
check('deposit_account_id').exists().isNumeric().toInt(),
|
check('deposit_account_id').exists().isNumeric().toInt(),
|
||||||
@@ -158,8 +159,7 @@ export default class PaymentReceivesController extends BaseController {
|
|||||||
|
|
||||||
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
|
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||||
|
|
||||||
check('entries').isArray({ min: 1 }),
|
check('entries').isArray({}),
|
||||||
|
|
||||||
check('entries.*.id').optional({ nullable: true }).isNumeric().toInt(),
|
check('entries.*.id').optional({ nullable: true }).isNumeric().toInt(),
|
||||||
check('entries.*.index').optional().isNumeric().toInt(),
|
check('entries.*.index').optional().isNumeric().toInt(),
|
||||||
check('entries.*.invoice_id').exists().isNumeric().toInt(),
|
check('entries.*.invoice_id').exists().isNumeric().toInt(),
|
||||||
|
|||||||
@@ -237,4 +237,8 @@ module.exports = {
|
|||||||
endpoint: process.env.S3_ENDPOINT,
|
endpoint: process.env.S3_ENDPOINT,
|
||||||
bucket: process.env.S3_BUCKET || 'bigcapital-documents',
|
bucket: process.env.S3_BUCKET || 'bigcapital-documents',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
loops: {
|
||||||
|
apiKey: process.env.LOOPS_API_KEY,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 = {
|
export const TransactionTypes = {
|
||||||
SaleInvoice: 'Sale invoice',
|
SaleInvoice: 'Sale invoice',
|
||||||
SaleReceipt: 'Sale receipt',
|
SaleReceipt: 'Sale receipt',
|
||||||
PaymentReceive: 'Payment receive',
|
PaymentReceive: 'Payment received',
|
||||||
Bill: 'Bill',
|
Bill: 'Bill',
|
||||||
BillPayment: 'Payment made',
|
BillPayment: 'Payment made',
|
||||||
VendorOpeningBalance: 'Vendor opening balance',
|
VendorOpeningBalance: 'Vendor opening balance',
|
||||||
@@ -17,12 +26,10 @@ export const TransactionTypes = {
|
|||||||
OtherExpense: 'Other expense',
|
OtherExpense: 'Other expense',
|
||||||
OwnerDrawing: 'Owner drawing',
|
OwnerDrawing: 'Owner drawing',
|
||||||
InvoiceWriteOff: 'Invoice write-off',
|
InvoiceWriteOff: 'Invoice write-off',
|
||||||
|
|
||||||
CreditNote: 'transaction_type.credit_note',
|
CreditNote: 'transaction_type.credit_note',
|
||||||
VendorCredit: 'transaction_type.vendor_credit',
|
VendorCredit: 'transaction_type.vendor_credit',
|
||||||
|
|
||||||
RefundCreditNote: 'transaction_type.refund_credit_note',
|
RefundCreditNote: 'transaction_type.refund_credit_note',
|
||||||
RefundVendorCredit: 'transaction_type.refund_vendor_credit',
|
RefundVendorCredit: 'transaction_type.refund_vendor_credit',
|
||||||
|
|
||||||
LandedCost: 'transaction_type.landed_cost',
|
LandedCost: 'transaction_type.landed_cost',
|
||||||
|
CashflowTransaction: CashflowTransactionTypes,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ exports.up = function (knex) {
|
|||||||
.integer('uncategorized_transaction_id')
|
.integer('uncategorized_transaction_id')
|
||||||
.unsigned()
|
.unsigned()
|
||||||
.references('id')
|
.references('id')
|
||||||
.inTable('uncategorized_cashflow_transactions');
|
.inTable('uncategorized_cashflow_transactions')
|
||||||
|
.withKeyName('recognizedBankTransactionsUncategorizedTransIdForeign');
|
||||||
table
|
table
|
||||||
.integer('bank_rule_id')
|
.integer('bank_rule_id')
|
||||||
.unsigned()
|
.unsigned()
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
exports.up = function (knex) {
|
exports.up = function (knex) {
|
||||||
return knex.schema.table('uncategorized_cashflow_transactions', (table) => {
|
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) {
|
exports.up = function (knex) {
|
||||||
return knex.schema.createTable('matched_bank_transactions', (table) => {
|
return knex.schema.createTable('matched_bank_transactions', (table) => {
|
||||||
table.increments('id');
|
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.string('reference_type');
|
||||||
table.integer('reference_id').unsigned();
|
table.integer('reference_id').unsigned();
|
||||||
table.decimal('amount');
|
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
|
||||||
|
`),
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -12,8 +12,7 @@ export default class SeedAccounts extends TenantSeeder {
|
|||||||
description: this.i18n.__(account.description),
|
description: this.i18n.__(account.description),
|
||||||
currencyCode: this.tenant.metadata.baseCurrency,
|
currencyCode: this.tenant.metadata.baseCurrency,
|
||||||
seededAt: new Date(),
|
seededAt: new Date(),
|
||||||
})
|
}));
|
||||||
);
|
|
||||||
return knex('accounts').then(async () => {
|
return knex('accounts').then(async () => {
|
||||||
// Inserts seed entries.
|
// Inserts seed entries.
|
||||||
return knex('accounts').insert(data);
|
return knex('accounts').insert(data);
|
||||||
|
|||||||
@@ -9,6 +9,28 @@ export const TaxPayableAccount = {
|
|||||||
predefined: 1,
|
predefined: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const UnearnedRevenueAccount = {
|
||||||
|
name: 'Unearned Revenue',
|
||||||
|
slug: 'unearned-revenue',
|
||||||
|
account_type: 'other-current-liability',
|
||||||
|
parent_account_id: null,
|
||||||
|
code: '50005',
|
||||||
|
active: true,
|
||||||
|
index: 1,
|
||||||
|
predefined: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PrepardExpenses = {
|
||||||
|
name: 'Prepaid Expenses',
|
||||||
|
slug: 'prepaid-expenses',
|
||||||
|
account_type: 'other-current-asset',
|
||||||
|
parent_account_id: null,
|
||||||
|
code: '100010',
|
||||||
|
active: true,
|
||||||
|
index: 1,
|
||||||
|
predefined: true,
|
||||||
|
};
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
{
|
{
|
||||||
name: 'Bank Account',
|
name: 'Bank Account',
|
||||||
@@ -323,4 +345,6 @@ export default [
|
|||||||
index: 1,
|
index: 1,
|
||||||
predefined: 0,
|
predefined: 0,
|
||||||
},
|
},
|
||||||
|
UnearnedRevenueAccount,
|
||||||
|
PrepardExpenses,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -66,7 +66,9 @@ export interface IAccountTransaction {
|
|||||||
referenceId: number;
|
referenceId: number;
|
||||||
|
|
||||||
referenceNumber?: string;
|
referenceNumber?: string;
|
||||||
|
|
||||||
transactionNumber?: string;
|
transactionNumber?: string;
|
||||||
|
transactionType?: string;
|
||||||
|
|
||||||
note?: string;
|
note?: string;
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Knex } from 'knex';
|
||||||
import {
|
import {
|
||||||
IFinancialSheetCommonMeta,
|
IFinancialSheetCommonMeta,
|
||||||
INumberFormatQuery,
|
INumberFormatQuery,
|
||||||
@@ -257,7 +258,6 @@ export interface IUncategorizedCashflowTransaction {
|
|||||||
categorized: boolean;
|
categorized: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface CreateUncategorizedTransactionDTO {
|
export interface CreateUncategorizedTransactionDTO {
|
||||||
date: Date | string;
|
date: Date | string;
|
||||||
accountId: number;
|
accountId: number;
|
||||||
@@ -269,3 +269,16 @@ export interface CreateUncategorizedTransactionDTO {
|
|||||||
plaidTransactionId?: string | null;
|
plaidTransactionId?: string | null;
|
||||||
batch?: string;
|
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 {
|
export interface ICashflowTransactionCategorizedPayload {
|
||||||
tenantId: number;
|
tenantId: number;
|
||||||
cashflowTransactionId: number;
|
uncategorizedTransaction: any;
|
||||||
cashflowTransaction: ICashflowTransaction;
|
cashflowTransaction: ICashflowTransaction;
|
||||||
|
categorizeDTO: any;
|
||||||
trx: Knex.Transaction;
|
trx: Knex.Transaction;
|
||||||
}
|
}
|
||||||
export interface ICashflowTransactionUncategorizingPayload {
|
export interface ICashflowTransactionUncategorizingPayload {
|
||||||
|
|||||||
@@ -29,4 +29,9 @@ export interface ICashflowAccountTransaction {
|
|||||||
|
|
||||||
date: Date;
|
date: Date;
|
||||||
formattedDate: string;
|
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;
|
date: Date | string;
|
||||||
|
|
||||||
transactionType: string;
|
transactionType: string;
|
||||||
|
transactionSubType?: string;
|
||||||
|
|
||||||
transactionId: number;
|
transactionId: number;
|
||||||
|
|
||||||
transactionNumber?: string;
|
transactionNumber?: string;
|
||||||
|
|||||||
@@ -110,6 +110,10 @@ import { ValidateMatchingOnPaymentMadeDelete } from '@/services/Banking/Matching
|
|||||||
import { ValidateMatchingOnCashflowDelete } from '@/services/Banking/Matching/events/ValidateMatchingOnCashflowDelete';
|
import { ValidateMatchingOnCashflowDelete } from '@/services/Banking/Matching/events/ValidateMatchingOnCashflowDelete';
|
||||||
import { RecognizeSyncedBankTranasctions } from '@/services/Banking/Plaid/subscribers/RecognizeSyncedBankTransactions';
|
import { RecognizeSyncedBankTranasctions } from '@/services/Banking/Plaid/subscribers/RecognizeSyncedBankTransactions';
|
||||||
import { UnlinkBankRuleOnDeleteBankRule } from '@/services/Banking/Rules/events/UnlinkBankRuleOnDeleteBankRule';
|
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';
|
||||||
|
import { LoopsEventsSubscriber } from '@/services/Loops/LoopsEventsSubscriber';
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
return new EventPublisher();
|
return new EventPublisher();
|
||||||
@@ -258,6 +262,9 @@ export const susbcribers = () => {
|
|||||||
// Bank Rules
|
// Bank Rules
|
||||||
TriggerRecognizedTransactions,
|
TriggerRecognizedTransactions,
|
||||||
UnlinkBankRuleOnDeleteBankRule,
|
UnlinkBankRuleOnDeleteBankRule,
|
||||||
|
DecrementUncategorizedTransactionOnMatching,
|
||||||
|
DecrementUncategorizedTransactionOnExclude,
|
||||||
|
DecrementUncategorizedTransactionOnCategorize,
|
||||||
|
|
||||||
// Validate matching
|
// Validate matching
|
||||||
ValidateMatchingOnCashflowDelete,
|
ValidateMatchingOnCashflowDelete,
|
||||||
@@ -266,7 +273,10 @@ export const susbcribers = () => {
|
|||||||
ValidateMatchingOnPaymentReceivedDelete,
|
ValidateMatchingOnPaymentReceivedDelete,
|
||||||
ValidateMatchingOnPaymentMadeDelete,
|
ValidateMatchingOnPaymentMadeDelete,
|
||||||
|
|
||||||
// Plaid
|
// Plaid
|
||||||
RecognizeSyncedBankTranasctions,
|
RecognizeSyncedBankTranasctions,
|
||||||
|
|
||||||
|
// Loops
|
||||||
|
LoopsEventsSubscriber
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export default class AccountTransaction extends TenantModel {
|
|||||||
debit: number;
|
debit: number;
|
||||||
exchangeRate: number;
|
exchangeRate: number;
|
||||||
taxRate: number;
|
taxRate: number;
|
||||||
|
transactionType: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Table name
|
* Table name
|
||||||
@@ -53,7 +54,7 @@ export default class AccountTransaction extends TenantModel {
|
|||||||
* @return {string}
|
* @return {string}
|
||||||
*/
|
*/
|
||||||
get referenceTypeFormatted() {
|
get referenceTypeFormatted() {
|
||||||
return getTransactionTypeLabel(this.referenceType);
|
return getTransactionTypeLabel(this.referenceType, this.transactionType);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -525,9 +525,9 @@ export default class Bill extends mixin(TenantModel, [
|
|||||||
return notFoundBillsIds;
|
return notFoundBillsIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
static changePaymentAmount(billId, amount) {
|
static changePaymentAmount(billId, amount, trx) {
|
||||||
const changeMethod = amount > 0 ? 'increment' : 'decrement';
|
const changeMethod = amount > 0 ? 'increment' : 'decrement';
|
||||||
return this.query()
|
return this.query(trx)
|
||||||
.where('id', billId)
|
.where('id', billId)
|
||||||
[changeMethod]('payment_amount', Math.abs(amount));
|
[changeMethod]('payment_amount', Math.abs(amount));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import {
|
|||||||
getCashflowAccountTransactionsTypes,
|
getCashflowAccountTransactionsTypes,
|
||||||
getCashflowTransactionType,
|
getCashflowTransactionType,
|
||||||
} from '@/services/Cashflow/utils';
|
} from '@/services/Cashflow/utils';
|
||||||
import AccountTransaction from './AccountTransaction';
|
|
||||||
import { CASHFLOW_DIRECTION } from '@/services/Cashflow/constants';
|
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 {
|
export default class CashflowTransaction extends TenantModel {
|
||||||
transactionType: string;
|
transactionType: string;
|
||||||
amount: number;
|
amount: number;
|
||||||
@@ -64,7 +64,7 @@ export default class CashflowTransaction extends TenantModel {
|
|||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
get transactionTypeFormatted() {
|
get transactionTypeFormatted() {
|
||||||
return getTransactionTypeLabel(this.transactionType);
|
return getCashflowTransactionFormattedType(this.transactionType);
|
||||||
}
|
}
|
||||||
|
|
||||||
get typeMeta() {
|
get typeMeta() {
|
||||||
@@ -95,6 +95,34 @@ export default class CashflowTransaction extends TenantModel {
|
|||||||
return !!this.uncategorizedTransaction;
|
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.
|
* Relationship mapping.
|
||||||
*/
|
*/
|
||||||
@@ -131,8 +159,7 @@ export default class CashflowTransaction extends TenantModel {
|
|||||||
to: 'accounts_transactions.referenceId',
|
to: 'accounts_transactions.referenceId',
|
||||||
},
|
},
|
||||||
filter(builder) {
|
filter(builder) {
|
||||||
const referenceTypes = getCashflowAccountTransactionsTypes();
|
builder.where('reference_type', 'CashflowTransaction');
|
||||||
builder.whereIn('reference_type', referenceTypes);
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -105,8 +105,34 @@ export default class UncategorizedCashflowTransaction extends mixin(
|
|||||||
* Filters the excluded transactions.
|
* Filters the excluded transactions.
|
||||||
*/
|
*/
|
||||||
excluded(query) {
|
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,12 @@ import { Account } from 'models';
|
|||||||
import TenantRepository from '@/repositories/TenantRepository';
|
import TenantRepository from '@/repositories/TenantRepository';
|
||||||
import { IAccount } from '@/interfaces';
|
import { IAccount } from '@/interfaces';
|
||||||
import { Knex } from 'knex';
|
import { Knex } from 'knex';
|
||||||
import { TaxPayableAccount } from '@/database/seeds/data/accounts';
|
import {
|
||||||
|
PrepardExpenses,
|
||||||
|
TaxPayableAccount,
|
||||||
|
UnearnedRevenueAccount,
|
||||||
|
} from '@/database/seeds/data/accounts';
|
||||||
|
import { TenantMetadata } from '@/system/models';
|
||||||
|
|
||||||
export default class AccountRepository extends TenantRepository {
|
export default class AccountRepository extends TenantRepository {
|
||||||
/**
|
/**
|
||||||
@@ -179,4 +184,67 @@ export default class AccountRepository extends TenantRepository {
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds or creates the unearned revenue.
|
||||||
|
* @param {Record<string, string>} extraAttrs
|
||||||
|
* @param {Knex.Transaction} trx
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
public async findOrCreateUnearnedRevenue(
|
||||||
|
extraAttrs: Record<string, string> = {},
|
||||||
|
trx?: Knex.Transaction
|
||||||
|
) {
|
||||||
|
// Retrieves the given tenant metadata.
|
||||||
|
const tenantMeta = await TenantMetadata.query().findOne({
|
||||||
|
tenantId: this.tenantId,
|
||||||
|
});
|
||||||
|
const _extraAttrs = {
|
||||||
|
currencyCode: tenantMeta.baseCurrency,
|
||||||
|
...extraAttrs,
|
||||||
|
};
|
||||||
|
let result = await this.model
|
||||||
|
.query(trx)
|
||||||
|
.findOne({ slug: UnearnedRevenueAccount.slug, ..._extraAttrs });
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
result = await this.model.query(trx).insertAndFetch({
|
||||||
|
...UnearnedRevenueAccount,
|
||||||
|
..._extraAttrs,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds or creates the prepard expenses account.
|
||||||
|
* @param {Record<string, string>} extraAttrs
|
||||||
|
* @param {Knex.Transaction} trx
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
public async findOrCreatePrepardExpenses(
|
||||||
|
extraAttrs: Record<string, string> = {},
|
||||||
|
trx?: Knex.Transaction
|
||||||
|
) {
|
||||||
|
// Retrieves the given tenant metadata.
|
||||||
|
const tenantMeta = await TenantMetadata.query().findOne({
|
||||||
|
tenantId: this.tenantId,
|
||||||
|
});
|
||||||
|
const _extraAttrs = {
|
||||||
|
currencyCode: tenantMeta.baseCurrency,
|
||||||
|
...extraAttrs,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = await this.model
|
||||||
|
.query(trx)
|
||||||
|
.findOne({ slug: PrepardExpenses.slug, ..._extraAttrs });
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
result = await this.model.query(trx).insertAndFetch({
|
||||||
|
...PrepardExpenses,
|
||||||
|
..._extraAttrs,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,17 @@ import CachableRepository from './CachableRepository';
|
|||||||
|
|
||||||
export default class TenantRepository extends CachableRepository {
|
export default class TenantRepository extends CachableRepository {
|
||||||
repositoryName: string;
|
repositoryName: string;
|
||||||
|
tenantId: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor method.
|
* Constructor method.
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
*/
|
*/
|
||||||
constructor(knex, cache, i18n) {
|
constructor(knex, cache, i18n) {
|
||||||
super(knex, cache, i18n);
|
super(knex, cache, i18n);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
setTenantId(tenantId: number) {
|
||||||
|
this.tenantId = tenantId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ export const transformLedgerEntryToTransaction = (
|
|||||||
referenceId: entry.transactionId,
|
referenceId: entry.transactionId,
|
||||||
|
|
||||||
transactionNumber: entry.transactionNumber,
|
transactionNumber: entry.transactionNumber,
|
||||||
|
transactionType: entry.transactionSubType,
|
||||||
|
|
||||||
referenceNumber: entry.referenceNumber,
|
referenceNumber: entry.referenceNumber,
|
||||||
|
|
||||||
note: entry.note,
|
note: entry.note,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
|
||||||
import { Server } from 'socket.io';
|
|
||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
|
import { initialize } from 'objection';
|
||||||
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class GetBankAccountSummary {
|
export class GetBankAccountSummary {
|
||||||
@@ -14,22 +14,43 @@ export class GetBankAccountSummary {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
public async getBankAccountSummary(tenantId: number, bankAccountId: number) {
|
public async getBankAccountSummary(tenantId: number, bankAccountId: number) {
|
||||||
|
const knex = this.tenancy.knex(tenantId);
|
||||||
const {
|
const {
|
||||||
Account,
|
Account,
|
||||||
UncategorizedCashflowTransaction,
|
UncategorizedCashflowTransaction,
|
||||||
RecognizedBankTransaction,
|
RecognizedBankTransaction,
|
||||||
|
MatchedBankTransaction,
|
||||||
} = this.tenancy.models(tenantId);
|
} = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
await initialize(knex, [
|
||||||
|
UncategorizedCashflowTransaction,
|
||||||
|
RecognizedBankTransaction,
|
||||||
|
MatchedBankTransaction,
|
||||||
|
]);
|
||||||
const bankAccount = await Account.query()
|
const bankAccount = await Account.query()
|
||||||
.findById(bankAccountId)
|
.findById(bankAccountId)
|
||||||
.throwIfNotFound();
|
.throwIfNotFound();
|
||||||
|
|
||||||
// Retrieves the uncategorized transactions count of the given bank account.
|
// Retrieves the uncategorized transactions count of the given bank account.
|
||||||
const uncategorizedTranasctionsCount =
|
const uncategorizedTranasctionsCount =
|
||||||
await UncategorizedCashflowTransaction.query()
|
await UncategorizedCashflowTransaction.query().onBuild((q) => {
|
||||||
.where('accountId', bankAccountId)
|
// Include just the given account.
|
||||||
.count('id as total')
|
q.where('accountId', bankAccountId);
|
||||||
.first();
|
|
||||||
|
// 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.
|
// Retrieves the recognized transactions count of the given bank account.
|
||||||
const recognizedTransactionsCount = await RecognizedBankTransaction.query()
|
const recognizedTransactionsCount = await RecognizedBankTransaction.query()
|
||||||
@@ -43,8 +64,8 @@ export class GetBankAccountSummary {
|
|||||||
.first();
|
.first();
|
||||||
|
|
||||||
const totalUncategorizedTransactions =
|
const totalUncategorizedTransactions =
|
||||||
uncategorizedTranasctionsCount?.total;
|
uncategorizedTranasctionsCount?.total || 0;
|
||||||
const totalRecognizedTransactions = recognizedTransactionsCount?.total;
|
const totalRecognizedTransactions = recognizedTransactionsCount?.total || 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: bankAccount.name,
|
name: bankAccount.name,
|
||||||
|
|||||||
@@ -2,6 +2,12 @@ import HasTenancyService from '@/services/Tenancy/TenancyService';
|
|||||||
import UnitOfWork from '@/services/UnitOfWork';
|
import UnitOfWork from '@/services/UnitOfWork';
|
||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import { validateTransactionNotCategorized } from './utils';
|
import { validateTransactionNotCategorized } from './utils';
|
||||||
|
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||||
|
import events from '@/subscribers/events';
|
||||||
|
import {
|
||||||
|
IBankTransactionUnexcludedEventPayload,
|
||||||
|
IBankTransactionUnexcludingEventPayload,
|
||||||
|
} from './_types';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class ExcludeBankTransaction {
|
export class ExcludeBankTransaction {
|
||||||
@@ -11,6 +17,9 @@ export class ExcludeBankTransaction {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private uow: UnitOfWork;
|
private uow: UnitOfWork;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private eventPublisher: EventPublisher;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Marks the given bank transaction as excluded.
|
* Marks the given bank transaction as excluded.
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
@@ -31,11 +40,23 @@ export class ExcludeBankTransaction {
|
|||||||
validateTransactionNotCategorized(oldUncategorizedTransaction);
|
validateTransactionNotCategorized(oldUncategorizedTransaction);
|
||||||
|
|
||||||
return this.uow.withTransaction(tenantId, async (trx) => {
|
return this.uow.withTransaction(tenantId, async (trx) => {
|
||||||
|
await this.eventPublisher.emitAsync(events.bankTransactions.onExcluding, {
|
||||||
|
tenantId,
|
||||||
|
uncategorizedTransactionId,
|
||||||
|
trx,
|
||||||
|
} as IBankTransactionUnexcludingEventPayload);
|
||||||
|
|
||||||
await UncategorizedCashflowTransaction.query(trx)
|
await UncategorizedCashflowTransaction.query(trx)
|
||||||
.findById(uncategorizedTransactionId)
|
.findById(uncategorizedTransactionId)
|
||||||
.patch({
|
.patch({
|
||||||
excludedAt: new Date(),
|
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 UnitOfWork from '@/services/UnitOfWork';
|
||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import { validateTransactionNotCategorized } from './utils';
|
import { validateTransactionNotCategorized } from './utils';
|
||||||
|
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||||
|
import events from '@/subscribers/events';
|
||||||
|
import {
|
||||||
|
IBankTransactionExcludedEventPayload,
|
||||||
|
IBankTransactionExcludingEventPayload,
|
||||||
|
} from './_types';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class UnexcludeBankTransaction {
|
export class UnexcludeBankTransaction {
|
||||||
@@ -11,6 +17,9 @@ export class UnexcludeBankTransaction {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private uow: UnitOfWork;
|
private uow: UnitOfWork;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private eventPublisher: EventPublisher;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Marks the given bank transaction as excluded.
|
* Marks the given bank transaction as excluded.
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
@@ -20,7 +29,7 @@ export class UnexcludeBankTransaction {
|
|||||||
public async unexcludeBankTransaction(
|
public async unexcludeBankTransaction(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
uncategorizedTransactionId: number
|
uncategorizedTransactionId: number
|
||||||
) {
|
): Promise<void> {
|
||||||
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
|
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
const oldUncategorizedTransaction =
|
const oldUncategorizedTransaction =
|
||||||
@@ -31,11 +40,27 @@ export class UnexcludeBankTransaction {
|
|||||||
validateTransactionNotCategorized(oldUncategorizedTransaction);
|
validateTransactionNotCategorized(oldUncategorizedTransaction);
|
||||||
|
|
||||||
return this.uow.withTransaction(tenantId, async (trx) => {
|
return this.uow.withTransaction(tenantId, async (trx) => {
|
||||||
|
await this.eventPublisher.emitAsync(
|
||||||
|
events.bankTransactions.onUnexcluding,
|
||||||
|
{
|
||||||
|
tenantId,
|
||||||
|
uncategorizedTransactionId,
|
||||||
|
} as IBankTransactionExcludingEventPayload
|
||||||
|
);
|
||||||
|
|
||||||
await UncategorizedCashflowTransaction.query(trx)
|
await UncategorizedCashflowTransaction.query(trx)
|
||||||
.findById(uncategorizedTransactionId)
|
.findById(uncategorizedTransactionId)
|
||||||
.patch({
|
.patch({
|
||||||
excludedAt: null,
|
excludedAt: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await this.eventPublisher.emitAsync(
|
||||||
|
events.bankTransactions.onUnexcluded,
|
||||||
|
{
|
||||||
|
tenantId,
|
||||||
|
uncategorizedTransactionId,
|
||||||
|
} as IBankTransactionExcludedEventPayload
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,30 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
export interface ExcludedBankTransactionsQuery {
|
export interface ExcludedBankTransactionsQuery {
|
||||||
page?: number;
|
page?: number;
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
accountId?: 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',
|
'transactionNo',
|
||||||
'transactionType',
|
'transactionType',
|
||||||
'transsactionTypeFormatted',
|
'transsactionTypeFormatted',
|
||||||
|
'transactionNormal',
|
||||||
|
'referenceId',
|
||||||
|
'referenceType',
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -100,4 +103,29 @@ export class GetMatchedTransactionBillsTransformer extends Transformer {
|
|||||||
protected transsactionTypeFormatted() {
|
protected transsactionTypeFormatted() {
|
||||||
return 'Bill';
|
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',
|
'transactionNo',
|
||||||
'transactionType',
|
'transactionType',
|
||||||
'transsactionTypeFormatted',
|
'transsactionTypeFormatted',
|
||||||
|
'transactionNormal',
|
||||||
|
'referenceType',
|
||||||
|
'referenceId',
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -111,4 +114,29 @@ export class GetMatchedTransactionExpensesTransformer extends Transformer {
|
|||||||
protected transsactionTypeFormatted() {
|
protected transsactionTypeFormatted() {
|
||||||
return 'Expense';
|
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',
|
'transactionNo',
|
||||||
'transactionType',
|
'transactionType',
|
||||||
'transsactionTypeFormatted',
|
'transsactionTypeFormatted',
|
||||||
|
'transactionNormal',
|
||||||
|
'referenceType',
|
||||||
|
'referenceId'
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -49,7 +52,7 @@ export class GetMatchedTransactionInvoicesTransformer extends Transformer {
|
|||||||
* @param invoice
|
* @param invoice
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
protected formatAmount(invoice) {
|
protected amountFormatted(invoice) {
|
||||||
return this.formatNumber(invoice.dueAmount, {
|
return this.formatNumber(invoice.dueAmount, {
|
||||||
currencyCode: invoice.currencyCode,
|
currencyCode: invoice.currencyCode,
|
||||||
money: true,
|
money: true,
|
||||||
@@ -79,7 +82,7 @@ export class GetMatchedTransactionInvoicesTransformer extends Transformer {
|
|||||||
* @param invoice
|
* @param invoice
|
||||||
* @returns {number}
|
* @returns {number}
|
||||||
*/
|
*/
|
||||||
protected getTransactionId(invoice) {
|
protected transactionId(invoice) {
|
||||||
return invoice.id;
|
return invoice.id;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
@@ -108,4 +111,28 @@ export class GetMatchedTransactionInvoicesTransformer extends Transformer {
|
|||||||
protected transsactionTypeFormatted(invoice) {
|
protected transsactionTypeFormatted(invoice) {
|
||||||
return 'Sale 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 { Transformer } from '@/lib/Transformer/Transformer';
|
||||||
|
import { AccountNormal } from '@/interfaces';
|
||||||
|
|
||||||
export class GetMatchedTransactionManualJournalsTransformer extends Transformer {
|
export class GetMatchedTransactionManualJournalsTransformer extends Transformer {
|
||||||
/**
|
/**
|
||||||
@@ -17,6 +19,9 @@ export class GetMatchedTransactionManualJournalsTransformer extends Transformer
|
|||||||
'transactionNo',
|
'transactionNo',
|
||||||
'transactionType',
|
'transactionType',
|
||||||
'transsactionTypeFormatted',
|
'transsactionTypeFormatted',
|
||||||
|
'transactionNormal',
|
||||||
|
'referenceType',
|
||||||
|
'referenceId',
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -37,13 +42,20 @@ export class GetMatchedTransactionManualJournalsTransformer extends Transformer
|
|||||||
return manualJournal.referenceNo;
|
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.
|
* Retrieves the manual journal amount.
|
||||||
* @param manualJournal
|
* @param manualJournal
|
||||||
* @returns {number}
|
* @returns {number}
|
||||||
*/
|
*/
|
||||||
protected amount(manualJournal) {
|
protected amount(manualJournal) {
|
||||||
return manualJournal.amount;
|
return Math.abs(this.total(manualJournal));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -107,5 +119,31 @@ export class GetMatchedTransactionManualJournalsTransformer extends Transformer
|
|||||||
protected transsactionTypeFormatted() {
|
protected transsactionTypeFormatted() {
|
||||||
return 'Manual Journal';
|
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 { GetMatchedTransactionsByManualJournals } from './GetMatchedTransactionsByManualJournals';
|
||||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
import { sortClosestMatchTransactions } from './_utils';
|
import { sortClosestMatchTransactions } from './_utils';
|
||||||
|
import { GetMatchedTransactionsByCashflow } from './GetMatchedTransactionsByCashflow';
|
||||||
|
import { GetMatchedTransactionsByInvoices } from './GetMatchedTransactionsByInvoices';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class GetMatchedTransactions {
|
export class GetMatchedTransactions {
|
||||||
@@ -15,7 +17,7 @@ export class GetMatchedTransactions {
|
|||||||
private tenancy: HasTenancyService;
|
private tenancy: HasTenancyService;
|
||||||
|
|
||||||
@Inject()
|
@Inject()
|
||||||
private getMatchedInvoicesService: GetMatchedTransactionsByExpenses;
|
private getMatchedInvoicesService: GetMatchedTransactionsByInvoices;
|
||||||
|
|
||||||
@Inject()
|
@Inject()
|
||||||
private getMatchedBillsService: GetMatchedTransactionsByBills;
|
private getMatchedBillsService: GetMatchedTransactionsByBills;
|
||||||
@@ -26,6 +28,9 @@ export class GetMatchedTransactions {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private getMatchedExpensesService: GetMatchedTransactionsByExpenses;
|
private getMatchedExpensesService: GetMatchedTransactionsByExpenses;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private getMatchedCashflowService: GetMatchedTransactionsByCashflow;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registered matched transactions types.
|
* Registered matched transactions types.
|
||||||
*/
|
*/
|
||||||
@@ -35,6 +40,7 @@ export class GetMatchedTransactions {
|
|||||||
{ type: 'Bill', service: this.getMatchedBillsService },
|
{ type: 'Bill', service: this.getMatchedBillsService },
|
||||||
{ type: 'Expense', service: this.getMatchedExpensesService },
|
{ type: 'Expense', service: this.getMatchedExpensesService },
|
||||||
{ type: 'ManualJournal', service: this.getMatchedManualJournalService },
|
{ type: 'ManualJournal', service: this.getMatchedManualJournalService },
|
||||||
|
{ type: 'Cashflow', service: this.getMatchedCashflowService },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
|
import { initialize } from 'objection';
|
||||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||||
import { GetMatchedTransactionBillsTransformer } from './GetMatchedTransactionBillsTransformer';
|
import { GetMatchedTransactionBillsTransformer } from './GetMatchedTransactionBillsTransformer';
|
||||||
import { GetMatchedTransactionsFilter, MatchedTransactionPOJO } from './types';
|
import { GetMatchedTransactionsFilter, MatchedTransactionPOJO } from './types';
|
||||||
@@ -22,10 +23,25 @@ export class GetMatchedTransactionsByBills extends GetMatchedTransactionsByType
|
|||||||
tenantId: number,
|
tenantId: number,
|
||||||
filter: GetMatchedTransactionsFilter
|
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) => {
|
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(
|
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 { Inject, Service } from 'typedi';
|
||||||
|
import { initialize } from 'objection';
|
||||||
import { GetMatchedTransactionsFilter, MatchedTransactionPOJO } from './types';
|
import { GetMatchedTransactionsFilter, MatchedTransactionPOJO } from './types';
|
||||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
@@ -23,22 +24,34 @@ export class GetMatchedTransactionsByExpenses extends GetMatchedTransactionsByTy
|
|||||||
tenantId: number,
|
tenantId: number,
|
||||||
filter: GetMatchedTransactionsFilter
|
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) => {
|
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) {
|
if (filter.fromDate) {
|
||||||
query.where('payment_date', '>=', filter.fromDate);
|
query.where('paymentDate', '>=', filter.fromDate);
|
||||||
}
|
}
|
||||||
if (filter.toDate) {
|
if (filter.toDate) {
|
||||||
query.where('payment_date', '<=', filter.toDate);
|
query.where('paymentDate', '<=', filter.toDate);
|
||||||
}
|
}
|
||||||
if (filter.minAmount) {
|
if (filter.minAmount) {
|
||||||
query.where('total_amount', '>=', filter.minAmount);
|
query.where('totalAmount', '>=', filter.minAmount);
|
||||||
}
|
}
|
||||||
if (filter.maxAmount) {
|
if (filter.maxAmount) {
|
||||||
query.where('total_amount', '<=', filter.maxAmount);
|
query.where('totalAmount', '<=', filter.maxAmount);
|
||||||
}
|
}
|
||||||
|
query.orderBy('paymentDate', 'DESC');
|
||||||
});
|
});
|
||||||
return this.transformer.transform(
|
return this.transformer.transform(
|
||||||
tenantId,
|
tenantId,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import { initialize } from 'objection';
|
||||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||||
import { GetMatchedTransactionInvoicesTransformer } from './GetMatchedTransactionInvoicesTransformer';
|
import { GetMatchedTransactionInvoicesTransformer } from './GetMatchedTransactionInvoicesTransformer';
|
||||||
import {
|
import {
|
||||||
@@ -6,7 +8,6 @@ import {
|
|||||||
MatchedTransactionsPOJO,
|
MatchedTransactionsPOJO,
|
||||||
} from './types';
|
} from './types';
|
||||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
import { Inject, Service } from 'typedi';
|
|
||||||
import { GetMatchedTransactionsByType } from './GetMatchedTransactionsByType';
|
import { GetMatchedTransactionsByType } from './GetMatchedTransactionsByType';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
@@ -27,10 +28,27 @@ export class GetMatchedTransactionsByInvoices extends GetMatchedTransactionsByTy
|
|||||||
tenantId: number,
|
tenantId: number,
|
||||||
filter: GetMatchedTransactionsFilter
|
filter: GetMatchedTransactionsFilter
|
||||||
): Promise<MatchedTransactionsPOJO> {
|
): 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) => {
|
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(
|
return this.transformer.transform(
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
|
import { initialize } from 'objection';
|
||||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||||
import { GetMatchedTransactionManualJournalsTransformer } from './GetMatchedTransactionManualJournalsTransformer';
|
import { GetMatchedTransactionManualJournalsTransformer } from './GetMatchedTransactionManualJournalsTransformer';
|
||||||
import { GetMatchedTransactionsByType } from './GetMatchedTransactionsByType';
|
import { GetMatchedTransactionsByType } from './GetMatchedTransactionsByType';
|
||||||
@@ -19,12 +20,26 @@ export class GetMatchedTransactionsByManualJournals extends GetMatchedTransactio
|
|||||||
tenantId: number,
|
tenantId: number,
|
||||||
filter: Omit<GetMatchedTransactionsFilter, 'transactionType'>
|
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) => {
|
const manualJournals = await ManualJournal.query().onBuild((query) => {
|
||||||
query.whereNotExists(
|
query.withGraphJoined('matchedBankTransaction');
|
||||||
ManualJournal.relatedQuery('matchedBankTransaction')
|
query.whereNull('matchedBankTransaction.id');
|
||||||
);
|
|
||||||
|
query.withGraphJoined('entries');
|
||||||
|
query.where('entries.accountId', accountId);
|
||||||
|
|
||||||
|
query.modify('filterByPublished');
|
||||||
|
|
||||||
if (filter.fromDate) {
|
if (filter.fromDate) {
|
||||||
query.where('date', '>=', 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 { Knex } from 'knex';
|
||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import { PromisePool } from '@supercharge/promise-pool';
|
import { PromisePool } from '@supercharge/promise-pool';
|
||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
} from './types';
|
} from './types';
|
||||||
import { MatchTransactionsTypes } from './MatchTransactionsTypes';
|
import { MatchTransactionsTypes } from './MatchTransactionsTypes';
|
||||||
import { ServiceError } from '@/exceptions';
|
import { ServiceError } from '@/exceptions';
|
||||||
|
import { sumMatchTranasctions } from './_utils';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class MatchBankTransactions {
|
export class MatchBankTransactions {
|
||||||
@@ -90,9 +91,8 @@ export class MatchBankTransactions {
|
|||||||
throw new ServiceError(error);
|
throw new ServiceError(error);
|
||||||
}
|
}
|
||||||
// Calculate the total given matching transactions.
|
// Calculate the total given matching transactions.
|
||||||
const totalMatchedTranasctions = sumBy(
|
const totalMatchedTranasctions = sumMatchTranasctions(
|
||||||
validatationResult.results,
|
validatationResult.results
|
||||||
'amount'
|
|
||||||
);
|
);
|
||||||
// Validates the total given matching transcations whether is not equal
|
// Validates the total given matching transcations whether is not equal
|
||||||
// uncategorized transaction amount.
|
// uncategorized transaction amount.
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { GetMatchedTransactionsByBills } from './GetMatchedTransactionsByBills';
|
|||||||
import { GetMatchedTransactionsByManualJournals } from './GetMatchedTransactionsByManualJournals';
|
import { GetMatchedTransactionsByManualJournals } from './GetMatchedTransactionsByManualJournals';
|
||||||
import { MatchTransactionsTypesRegistry } from './MatchTransactionsTypesRegistry';
|
import { MatchTransactionsTypesRegistry } from './MatchTransactionsTypesRegistry';
|
||||||
import { GetMatchedTransactionsByInvoices } from './GetMatchedTransactionsByInvoices';
|
import { GetMatchedTransactionsByInvoices } from './GetMatchedTransactionsByInvoices';
|
||||||
|
import { GetMatchedTransactionCashflowTransformer } from './GetMatchedTransactionCashflowTransformer';
|
||||||
|
import { GetMatchedTransactionsByCashflow } from './GetMatchedTransactionsByCashflow';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class MatchTransactionsTypes {
|
export class MatchTransactionsTypes {
|
||||||
@@ -25,6 +27,10 @@ export class MatchTransactionsTypes {
|
|||||||
type: 'ManualJournal',
|
type: 'ManualJournal',
|
||||||
service: GetMatchedTransactionsByManualJournals,
|
service: GetMatchedTransactionsByManualJournals,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'CashflowTransaction',
|
||||||
|
service: GetMatchedTransactionsByCashflow,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export class UnmatchMatchedBankTransaction {
|
|||||||
return this.uow.withTransaction(tenantId, async (trx) => {
|
return this.uow.withTransaction(tenantId, async (trx) => {
|
||||||
await this.eventPublisher.emitAsync(events.bankMatch.onUnmatching, {
|
await this.eventPublisher.emitAsync(events.bankMatch.onUnmatching, {
|
||||||
tenantId,
|
tenantId,
|
||||||
|
uncategorizedTransactionId,
|
||||||
trx,
|
trx,
|
||||||
} as IBankTransactionUnmatchingEventPayload);
|
} as IBankTransactionUnmatchingEventPayload);
|
||||||
|
|
||||||
@@ -40,6 +41,7 @@ export class UnmatchMatchedBankTransaction {
|
|||||||
|
|
||||||
await this.eventPublisher.emitAsync(events.bankMatch.onUnmatched, {
|
await this.eventPublisher.emitAsync(events.bankMatch.onUnmatched, {
|
||||||
tenantId,
|
tenantId,
|
||||||
|
uncategorizedTransactionId,
|
||||||
trx,
|
trx,
|
||||||
} as IBankTransactionUnmatchingEventPayload);
|
} as IBankTransactionUnmatchingEventPayload);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import { Knex } from 'knex';
|
||||||
|
import { Inject, Service } from 'typedi';
|
||||||
import { ServiceError } from '@/exceptions';
|
import { ServiceError } from '@/exceptions';
|
||||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
import { Inject, Service } from 'typedi';
|
|
||||||
import { ERRORS } from './types';
|
import { ERRORS } from './types';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
@@ -18,12 +19,13 @@ export class ValidateTransactionMatched {
|
|||||||
public async validateTransactionNoMatchLinking(
|
public async validateTransactionNoMatchLinking(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
referenceType: string,
|
referenceType: string,
|
||||||
referenceId: number
|
referenceId: number,
|
||||||
|
trx?: Knex.Transaction
|
||||||
) {
|
) {
|
||||||
const { MatchedBankTransaction } = this.tenancy.models(tenantId);
|
const { MatchedBankTransaction } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
const foundMatchedTransaction =
|
const foundMatchedTransaction =
|
||||||
await MatchedBankTransaction.query().findOne({
|
await MatchedBankTransaction.query(trx).findOne({
|
||||||
referenceType,
|
referenceType,
|
||||||
referenceId,
|
referenceId,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,3 +20,12 @@ export const sortClosestMatchTransactions = (
|
|||||||
),
|
),
|
||||||
])(matches);
|
])(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 { Inject, Service } from 'typedi';
|
||||||
import { IManualJournalDeletingPayload } from '@/interfaces';
|
import { ICommandCashflowDeletingPayload, IManualJournalDeletingPayload } from '@/interfaces';
|
||||||
import events from '@/subscribers/events';
|
import events from '@/subscribers/events';
|
||||||
import { ValidateTransactionMatched } from '../ValidateTransactionsMatched';
|
import { ValidateTransactionMatched } from '../ValidateTransactionsMatched';
|
||||||
|
|
||||||
@@ -24,13 +24,14 @@ export class ValidateMatchingOnCashflowDelete {
|
|||||||
*/
|
*/
|
||||||
public async validateMatchingOnCashflowDeleting({
|
public async validateMatchingOnCashflowDeleting({
|
||||||
tenantId,
|
tenantId,
|
||||||
oldManualJournal,
|
oldCashflowTransaction,
|
||||||
trx,
|
trx,
|
||||||
}: IManualJournalDeletingPayload) {
|
}: ICommandCashflowDeletingPayload) {
|
||||||
await this.validateNoMatchingLinkedService.validateTransactionNoMatchLinking(
|
await this.validateNoMatchingLinkedService.validateTransactionNoMatchLinking(
|
||||||
tenantId,
|
tenantId,
|
||||||
'ManualJournal',
|
'CashflowTransaction',
|
||||||
oldManualJournal.id
|
oldCashflowTransaction.id,
|
||||||
|
trx
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ export class ValidateMatchingOnExpenseDelete {
|
|||||||
await this.validateNoMatchingLinkedService.validateTransactionNoMatchLinking(
|
await this.validateNoMatchingLinkedService.validateTransactionNoMatchLinking(
|
||||||
tenantId,
|
tenantId,
|
||||||
'Expense',
|
'Expense',
|
||||||
oldExpense.id
|
oldExpense.id,
|
||||||
|
trx
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ export class ValidateMatchingOnManualJournalDelete {
|
|||||||
await this.validateNoMatchingLinkedService.validateTransactionNoMatchLinking(
|
await this.validateNoMatchingLinkedService.validateTransactionNoMatchLinking(
|
||||||
tenantId,
|
tenantId,
|
||||||
'ManualJournal',
|
'ManualJournal',
|
||||||
oldManualJournal.id
|
oldManualJournal.id,
|
||||||
|
trx
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ export class ValidateMatchingOnPaymentMadeDelete {
|
|||||||
await this.validateNoMatchingLinkedService.validateTransactionNoMatchLinking(
|
await this.validateNoMatchingLinkedService.validateTransactionNoMatchLinking(
|
||||||
tenantId,
|
tenantId,
|
||||||
'PaymentMade',
|
'PaymentMade',
|
||||||
oldBillPayment.id
|
oldBillPayment.id,
|
||||||
|
trx
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ export class ValidateMatchingOnPaymentReceivedDelete {
|
|||||||
await this.validateNoMatchingLinkedService.validateTransactionNoMatchLinking(
|
await this.validateNoMatchingLinkedService.validateTransactionNoMatchLinking(
|
||||||
tenantId,
|
tenantId,
|
||||||
'PaymentReceive',
|
'PaymentReceive',
|
||||||
oldPaymentReceive.id
|
oldPaymentReceive.id,
|
||||||
|
trx
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,10 +16,14 @@ export interface IBankTransactionMatchedEventPayload {
|
|||||||
|
|
||||||
export interface IBankTransactionUnmatchingEventPayload {
|
export interface IBankTransactionUnmatchingEventPayload {
|
||||||
tenantId: number;
|
tenantId: number;
|
||||||
|
uncategorizedTransactionId: number;
|
||||||
|
trx?: Knex.Transaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IBankTransactionUnmatchedEventPayload {
|
export interface IBankTransactionUnmatchedEventPayload {
|
||||||
tenantId: number;
|
tenantId: number;
|
||||||
|
uncategorizedTransactionId: number;
|
||||||
|
trx?: Knex.Transaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IMatchTransactionDTO {
|
export interface IMatchTransactionDTO {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { DeleteCashflowTransaction } from '@/services/Cashflow/DeleteCashflowTra
|
|||||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
import { CashflowApplication } from '@/services/Cashflow/CashflowApplication';
|
import { CashflowApplication } from '@/services/Cashflow/CashflowApplication';
|
||||||
import { Knex } from 'knex';
|
import { Knex } from 'knex';
|
||||||
import { uniqid } from 'uniqid';
|
import uniqid from 'uniqid';
|
||||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||||
import events from '@/subscribers/events';
|
import events from '@/subscribers/events';
|
||||||
|
|
||||||
@@ -148,7 +148,6 @@ export class PlaidSyncDb {
|
|||||||
*/
|
*/
|
||||||
public async syncAccountsTransactions(
|
public async syncAccountsTransactions(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
batchNo: string,
|
|
||||||
plaidAccountsTransactions: PlaidTransaction[],
|
plaidAccountsTransactions: PlaidTransaction[],
|
||||||
trx?: Knex.Transaction
|
trx?: Knex.Transaction
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
@@ -161,7 +160,6 @@ export class PlaidSyncDb {
|
|||||||
return this.syncAccountTranactions(
|
return this.syncAccountTranactions(
|
||||||
tenantId,
|
tenantId,
|
||||||
plaidAccountId,
|
plaidAccountId,
|
||||||
batchNo,
|
|
||||||
plaidTransactions,
|
plaidTransactions,
|
||||||
trx
|
trx
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -73,8 +73,6 @@ export class PlaidUpdateTransactions {
|
|||||||
added.concat(modified),
|
added.concat(modified),
|
||||||
trx
|
trx
|
||||||
);
|
);
|
||||||
// Sync removed transactions.
|
|
||||||
await this.plaidSync.syncRemoveTransactions(tenantId, removed, trx);
|
|
||||||
// Sync transactions cursor.
|
// Sync transactions cursor.
|
||||||
await this.plaidSync.syncTransactionsCursor(
|
await this.plaidSync.syncTransactionsCursor(
|
||||||
tenantId,
|
tenantId,
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ export const transformPlaidAccountToCreateAccount = R.curry(
|
|||||||
export const transformPlaidTrxsToCashflowCreate = R.curry(
|
export const transformPlaidTrxsToCashflowCreate = R.curry(
|
||||||
(
|
(
|
||||||
cashflowAccountId: number,
|
cashflowAccountId: number,
|
||||||
creditAccountId: number,
|
|
||||||
plaidTranasction: PlaidTransaction
|
plaidTranasction: PlaidTransaction
|
||||||
): CreateUncategorizedTransactionDTO => {
|
): CreateUncategorizedTransactionDTO => {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -18,11 +18,11 @@ export class RegonizeTransactionsJob {
|
|||||||
* Triggers sending invoice mail.
|
* Triggers sending invoice mail.
|
||||||
*/
|
*/
|
||||||
private handler = async (job, done: Function) => {
|
private handler = async (job, done: Function) => {
|
||||||
const { tenantId } = job.attrs.data;
|
const { tenantId, batch } = job.attrs.data;
|
||||||
const regonizeTransactions = Container.get(RecognizeTranasctionsService);
|
const regonizeTransactions = Container.get(RecognizeTranasctionsService);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await regonizeTransactions.recognizeTransactions(tenantId);
|
await regonizeTransactions.recognizeTransactions(tenantId, batch);
|
||||||
done();
|
done();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import {
|
|||||||
IBankRuleEventDeletedPayload,
|
IBankRuleEventDeletedPayload,
|
||||||
IBankRuleEventEditedPayload,
|
IBankRuleEventEditedPayload,
|
||||||
} from '../../Rules/types';
|
} from '../../Rules/types';
|
||||||
|
import { IImportFileCommitedEventPayload } from '@/interfaces/Import';
|
||||||
|
import { Import } from '@/system/models';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class TriggerRecognizedTransactions {
|
export class TriggerRecognizedTransactions {
|
||||||
@@ -27,6 +29,10 @@ export class TriggerRecognizedTransactions {
|
|||||||
events.bankRules.onDeleted,
|
events.bankRules.onDeleted,
|
||||||
this.recognizedTransactionsOnRuleDeleted.bind(this)
|
this.recognizedTransactionsOnRuleDeleted.bind(this)
|
||||||
);
|
);
|
||||||
|
bus.subscribe(
|
||||||
|
events.import.onImportCommitted,
|
||||||
|
this.triggerRecognizeTransactionsOnImportCommitted.bind(this)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -73,4 +79,20 @@ export class TriggerRecognizedTransactions {
|
|||||||
const payload = { tenantId };
|
const payload = { tenantId };
|
||||||
await this.agenda.now('recognize-uncategorized-transactions-job', payload);
|
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 { Transformer } from '@/lib/Transformer/Transformer';
|
||||||
import { getTransactionTypeLabel } from '@/utils/transactions-types';
|
import { getCashflowTransactionFormattedType } from '@/utils/transactions-types';
|
||||||
|
|
||||||
export class GetBankRulesTransformer extends Transformer {
|
export class GetBankRulesTransformer extends Transformer {
|
||||||
/**
|
/**
|
||||||
@@ -29,8 +28,7 @@ export class GetBankRulesTransformer extends Transformer {
|
|||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
protected assignCategoryFormatted(bankRule: any) {
|
protected assignCategoryFormatted(bankRule: any) {
|
||||||
const assignCategory = upperFirst(camelCase(bankRule.assignCategory));
|
return getCashflowTransactionFormattedType(bankRule.assignCategory);
|
||||||
return getTransactionTypeLabel(assignCategory);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -34,11 +34,12 @@ export default class CashflowTransactionJournalEntries {
|
|||||||
currencyCode: transaction.currencyCode,
|
currencyCode: transaction.currencyCode,
|
||||||
exchangeRate: transaction.exchangeRate,
|
exchangeRate: transaction.exchangeRate,
|
||||||
|
|
||||||
transactionType: transformCashflowTransactionType(
|
transactionType: 'CashflowTransaction',
|
||||||
transaction.transactionType
|
|
||||||
),
|
|
||||||
transactionId: transaction.id,
|
transactionId: transaction.id,
|
||||||
transactionNumber: transaction.transactionNumber,
|
transactionNumber: transaction.transactionNumber,
|
||||||
|
transactionSubType: transformCashflowTransactionType(
|
||||||
|
transaction.transactionType
|
||||||
|
),
|
||||||
referenceNumber: transaction.referenceNo,
|
referenceNumber: transaction.referenceNo,
|
||||||
|
|
||||||
note: transaction.description,
|
note: transaction.description,
|
||||||
@@ -161,12 +162,10 @@ export default class CashflowTransactionJournalEntries {
|
|||||||
cashflowTransactionId: number,
|
cashflowTransactionId: number,
|
||||||
trx?: Knex.Transaction
|
trx?: Knex.Transaction
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const transactionTypes = getCashflowAccountTransactionsTypes();
|
|
||||||
|
|
||||||
await this.ledgerStorage.deleteByReference(
|
await this.ledgerStorage.deleteByReference(
|
||||||
tenantId,
|
tenantId,
|
||||||
cashflowTransactionId,
|
cashflowTransactionId,
|
||||||
transactionTypes,
|
'CashflowTransaction',
|
||||||
trx
|
trx
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -84,20 +84,23 @@ export class CategorizeCashflowTransaction {
|
|||||||
cashflowTransactionDTO
|
cashflowTransactionDTO
|
||||||
);
|
);
|
||||||
// Updates the uncategorized transaction as categorized.
|
// Updates the uncategorized transaction as categorized.
|
||||||
await UncategorizedCashflowTransaction.query(trx).patchAndFetchById(
|
const uncategorizedTransaction =
|
||||||
uncategorizedTransactionId,
|
await UncategorizedCashflowTransaction.query(trx).patchAndFetchById(
|
||||||
{
|
uncategorizedTransactionId,
|
||||||
categorized: true,
|
{
|
||||||
categorizeRefType: 'CashflowTransaction',
|
categorized: true,
|
||||||
categorizeRefId: cashflowTransaction.id,
|
categorizeRefType: 'CashflowTransaction',
|
||||||
}
|
categorizeRefId: cashflowTransaction.id,
|
||||||
);
|
}
|
||||||
|
);
|
||||||
// Triggers `onCashflowTransactionCategorized` event.
|
// Triggers `onCashflowTransactionCategorized` event.
|
||||||
await this.eventPublisher.emitAsync(
|
await this.eventPublisher.emitAsync(
|
||||||
events.cashflow.onTransactionCategorized,
|
events.cashflow.onTransactionCategorized,
|
||||||
{
|
{
|
||||||
tenantId,
|
tenantId,
|
||||||
// cashflowTransaction,
|
cashflowTransaction,
|
||||||
|
uncategorizedTransaction,
|
||||||
|
categorizeDTO,
|
||||||
trx,
|
trx,
|
||||||
} as ICashflowTransactionCategorizedPayload
|
} as ICashflowTransactionCategorizedPayload
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,7 +2,13 @@ import { Knex } from 'knex';
|
|||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import HasTenancyService from '../Tenancy/TenancyService';
|
import HasTenancyService from '../Tenancy/TenancyService';
|
||||||
import UnitOfWork from '../UnitOfWork';
|
import UnitOfWork from '../UnitOfWork';
|
||||||
import { CreateUncategorizedTransactionDTO } from '@/interfaces';
|
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||||
|
import events from '@/subscribers/events';
|
||||||
|
import {
|
||||||
|
CreateUncategorizedTransactionDTO,
|
||||||
|
IUncategorizedTransactionCreatedEventPayload,
|
||||||
|
IUncategorizedTransactionCreatingEventPayload,
|
||||||
|
} from '@/interfaces';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class CreateUncategorizedTransaction {
|
export class CreateUncategorizedTransaction {
|
||||||
@@ -12,6 +18,9 @@ export class CreateUncategorizedTransaction {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private uow: UnitOfWork;
|
private uow: UnitOfWork;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private eventPublisher: EventPublisher;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an uncategorized cashflow transaction.
|
* Creates an uncategorized cashflow transaction.
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
@@ -19,7 +28,7 @@ export class CreateUncategorizedTransaction {
|
|||||||
*/
|
*/
|
||||||
public create(
|
public create(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
createDTO: CreateUncategorizedTransactionDTO,
|
createUncategorizedTransactionDTO: CreateUncategorizedTransactionDTO,
|
||||||
trx?: Knex.Transaction
|
trx?: Knex.Transaction
|
||||||
) {
|
) {
|
||||||
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
|
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
|
||||||
@@ -27,12 +36,30 @@ export class CreateUncategorizedTransaction {
|
|||||||
return this.uow.withTransaction(
|
return this.uow.withTransaction(
|
||||||
tenantId,
|
tenantId,
|
||||||
async (trx: Knex.Transaction) => {
|
async (trx: Knex.Transaction) => {
|
||||||
const transaction = await UncategorizedCashflowTransaction.query(
|
await this.eventPublisher.emitAsync(
|
||||||
trx
|
events.cashflow.onTransactionUncategorizedCreating,
|
||||||
).insertAndFetch({
|
{
|
||||||
...createDTO,
|
tenantId,
|
||||||
});
|
createUncategorizedTransactionDTO,
|
||||||
return transaction;
|
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
|
trx
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ export default class NewCashflowTransactionService {
|
|||||||
...fromDTO,
|
...fromDTO,
|
||||||
transactionNumber,
|
transactionNumber,
|
||||||
currencyCode: cashflowAccount.currencyCode,
|
currencyCode: cashflowAccount.currencyCode,
|
||||||
|
exchangeRate: fromDTO?.exchangeRate || 1,
|
||||||
transactionType: transformCashflowTransactionType(
|
transactionType: transformCashflowTransactionType(
|
||||||
fromDTO.transactionType
|
fromDTO.transactionType
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import { Knex } from 'knex';
|
import { Knex } from 'knex';
|
||||||
import * as yup from 'yup';
|
import * as yup from 'yup';
|
||||||
|
import uniqid from 'uniqid';
|
||||||
import { Importable } from '../Import/Importable';
|
import { Importable } from '../Import/Importable';
|
||||||
import { CreateUncategorizedTransaction } from './CreateUncategorizedTransaction';
|
import { CreateUncategorizedTransaction } from './CreateUncategorizedTransaction';
|
||||||
import { CreateUncategorizedTransactionDTO } from '@/interfaces';
|
import { CreateUncategorizedTransactionDTO } from '@/interfaces';
|
||||||
@@ -15,6 +16,7 @@ export class UncategorizedTransactionsImportable extends Importable {
|
|||||||
|
|
||||||
@Inject()
|
@Inject()
|
||||||
private tenancy: HasTenancyService;
|
private tenancy: HasTenancyService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Passing the sheet DTO to create uncategorized transaction.
|
* Passing the sheet DTO to create uncategorized transaction.
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
@@ -43,6 +45,7 @@ export class UncategorizedTransactionsImportable extends Importable {
|
|||||||
return {
|
return {
|
||||||
...createDTO,
|
...createDTO,
|
||||||
accountId: context.import.paramsParsed.accountId,
|
accountId: context.import.paramsParsed.accountId,
|
||||||
|
batch: context.import.paramsParsed.batch,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,6 +57,9 @@ export class UncategorizedTransactionsImportable extends Importable {
|
|||||||
return BankTransactionsSampleData;
|
return BankTransactionsSampleData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ------------------
|
||||||
|
// # Params
|
||||||
|
// ------------------
|
||||||
/**
|
/**
|
||||||
* Params validation schema.
|
* Params validation schema.
|
||||||
* @returns {ValidationSchema[]}
|
* @returns {ValidationSchema[]}
|
||||||
@@ -79,4 +85,17 @@ export class UncategorizedTransactionsImportable extends Importable {
|
|||||||
await Account.query().findById(params.accountId).throwIfNotFound({});
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -45,9 +45,9 @@ export class CustomersApplication {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new customer.
|
* Creates a new customer.
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
* @param {ICustomerNewDTO} customerDTO
|
* @param {ICustomerNewDTO} customerDTO
|
||||||
* @param {ISystemUser} authorizedUser
|
* @param {ISystemUser} authorizedUser
|
||||||
* @returns {Promise<ICustomer>}
|
* @returns {Promise<ICustomer>}
|
||||||
*/
|
*/
|
||||||
public createCustomer = (tenantId: number, customerDTO: ICustomerNewDTO) => {
|
public createCustomer = (tenantId: number, customerDTO: ICustomerNewDTO) => {
|
||||||
@@ -56,9 +56,9 @@ export class CustomersApplication {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Edits details of the given customer.
|
* Edits details of the given customer.
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
* @param {number} customerId
|
* @param {number} customerId
|
||||||
* @param {ICustomerEditDTO} customerDTO
|
* @param {ICustomerEditDTO} customerDTO
|
||||||
* @return {Promise<ICustomer>}
|
* @return {Promise<ICustomer>}
|
||||||
*/
|
*/
|
||||||
public editCustomer = (
|
public editCustomer = (
|
||||||
@@ -75,9 +75,9 @@ export class CustomersApplication {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes the given customer and associated transactions.
|
* Deletes the given customer and associated transactions.
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
* @param {number} customerId
|
* @param {number} customerId
|
||||||
* @param {ISystemUser} authorizedUser
|
* @param {ISystemUser} authorizedUser
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
public deleteCustomer = (
|
public deleteCustomer = (
|
||||||
@@ -94,9 +94,9 @@ export class CustomersApplication {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Changes the opening balance of the given customer.
|
* Changes the opening balance of the given customer.
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
* @param {number} customerId
|
* @param {number} customerId
|
||||||
* @param {Date|string} openingBalanceEditDTO
|
* @param {Date|string} openingBalanceEditDTO
|
||||||
* @returns {Promise<ICustomer>}
|
* @returns {Promise<ICustomer>}
|
||||||
*/
|
*/
|
||||||
public editOpeningBalance = (
|
public editOpeningBalance = (
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
import R from 'ramda';
|
import R from 'ramda';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
import { first, isEmpty } from 'lodash';
|
||||||
import {
|
import {
|
||||||
ICashflowAccountTransaction,
|
ICashflowAccountTransaction,
|
||||||
ICashflowAccountTransactionsQuery,
|
ICashflowAccountTransactionsQuery,
|
||||||
INumberFormatQuery,
|
|
||||||
} from '@/interfaces';
|
} from '@/interfaces';
|
||||||
import FinancialSheet from '../FinancialSheet';
|
import FinancialSheet from '../FinancialSheet';
|
||||||
import { runningAmount } from 'utils';
|
import { runningAmount } from 'utils';
|
||||||
|
import { CashflowAccountTransactionsRepo } from './CashflowAccountTransactionsRepo';
|
||||||
|
import { BankTransactionStatus } from './constants';
|
||||||
|
import { formatBankTransactionsStatus } from './utils';
|
||||||
|
|
||||||
export default class CashflowAccountTransactionReport extends FinancialSheet {
|
export class CashflowAccountTransactionReport extends FinancialSheet {
|
||||||
private transactions: any;
|
|
||||||
private openingBalance: number;
|
|
||||||
private runningBalance: any;
|
private runningBalance: any;
|
||||||
private numberFormat: INumberFormatQuery;
|
|
||||||
private baseCurrency: string;
|
|
||||||
private query: ICashflowAccountTransactionsQuery;
|
private query: ICashflowAccountTransactionsQuery;
|
||||||
|
private repo: CashflowAccountTransactionsRepo;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor method.
|
* Constructor method.
|
||||||
@@ -23,19 +23,61 @@ export default class CashflowAccountTransactionReport extends FinancialSheet {
|
|||||||
* @param {ICashflowAccountTransactionsQuery} query -
|
* @param {ICashflowAccountTransactionsQuery} query -
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
transactions,
|
repo: CashflowAccountTransactionsRepo,
|
||||||
openingBalance: number,
|
|
||||||
query: ICashflowAccountTransactionsQuery
|
query: ICashflowAccountTransactionsQuery
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.transactions = transactions;
|
this.repo = repo;
|
||||||
this.openingBalance = openingBalance;
|
|
||||||
|
|
||||||
this.runningBalance = runningAmount(this.openingBalance);
|
|
||||||
this.query = query;
|
this.query = query;
|
||||||
this.numberFormat = query.numberFormat;
|
this.runningBalance = runningAmount(this.repo.openingBalance);
|
||||||
this.baseCurrency = 'USD';
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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}
|
* @returns {ICashflowAccountTransaction}
|
||||||
*/
|
*/
|
||||||
private transactionNode = (transaction: any): ICashflowAccountTransaction => {
|
private transactionNode = (transaction: any): ICashflowAccountTransaction => {
|
||||||
|
const status = this.getTransactionStatus(transaction);
|
||||||
|
const uncategorizedTransactionId =
|
||||||
|
this.getUncategorizedTransId(transaction);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
date: transaction.date,
|
date: transaction.date,
|
||||||
formattedDate: moment(transaction.date).format('YYYY-MM-DD'),
|
formattedDate: moment(transaction.date).format('YYYY-MM-DD'),
|
||||||
@@ -67,6 +113,9 @@ export default class CashflowAccountTransactionReport extends FinancialSheet {
|
|||||||
|
|
||||||
balance: 0,
|
balance: 0,
|
||||||
formattedBalance: '',
|
formattedBalance: '',
|
||||||
|
status,
|
||||||
|
formattedStatus: formatBankTransactionsStatus(status),
|
||||||
|
uncategorizedTransactionId,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -146,6 +195,6 @@ export default class CashflowAccountTransactionReport extends FinancialSheet {
|
|||||||
* @returns {ICashflowAccountTransaction[]}
|
* @returns {ICashflowAccountTransaction[]}
|
||||||
*/
|
*/
|
||||||
public reportData(): 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 * as R from 'ramda';
|
||||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
import { ICashflowAccountTransactionsQuery } from '@/interfaces';
|
||||||
import { ICashflowAccountTransactionsQuery, IPaginationMeta } from '@/interfaces';
|
import {
|
||||||
|
groupMatchedBankTransactions,
|
||||||
|
groupUncategorizedTransactions,
|
||||||
|
} from './utils';
|
||||||
|
|
||||||
@Service()
|
export class CashflowAccountTransactionsRepo {
|
||||||
export default class CashflowAccountTransactionsRepo {
|
private models: any;
|
||||||
@Inject()
|
public query: ICashflowAccountTransactionsQuery;
|
||||||
private tenancy: HasTenancyService;
|
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.
|
* Retrieve the cashflow account transactions.
|
||||||
* @param {number} tenantId -
|
* @param {number} tenantId -
|
||||||
* @param {ICashflowAccountTransactionsQuery} query -
|
* @param {ICashflowAccountTransactionsQuery} query -
|
||||||
*/
|
*/
|
||||||
async getCashflowAccountTransactions(
|
async initCashflowAccountTransactions() {
|
||||||
tenantId: number,
|
const { AccountTransaction } = this.models;
|
||||||
query: ICashflowAccountTransactionsQuery
|
|
||||||
) {
|
|
||||||
const { AccountTransaction } = this.tenancy.models(tenantId);
|
|
||||||
|
|
||||||
return AccountTransaction.query()
|
const { results, pagination } = await AccountTransaction.query()
|
||||||
.where('account_id', query.accountId)
|
.where('account_id', this.query.accountId)
|
||||||
.orderBy([
|
.orderBy([
|
||||||
{ column: 'date', order: 'desc' },
|
{ column: 'date', order: 'desc' },
|
||||||
{ column: 'created_at', 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
|
* @param {IPaginationMeta} pagination
|
||||||
* @return {Promise<number>}
|
* @return {Promise<number>}
|
||||||
*/
|
*/
|
||||||
async getCashflowAccountOpeningBalance(
|
async initCashflowAccountOpeningBalance(): Promise<void> {
|
||||||
tenantId: number,
|
const { AccountTransaction } = this.models;
|
||||||
accountId: number,
|
|
||||||
pagination: IPaginationMeta
|
|
||||||
): Promise<number> {
|
|
||||||
const { AccountTransaction } = this.tenancy.models(tenantId);
|
|
||||||
|
|
||||||
// Retrieve the opening balance of credit and debit balances.
|
// Retrieve the opening balance of credit and debit balances.
|
||||||
const openingBalancesSubquery = AccountTransaction.query()
|
const openingBalancesSubquery = AccountTransaction.query()
|
||||||
.where('account_id', accountId)
|
.where('account_id', this.query.accountId)
|
||||||
.orderBy([
|
.orderBy([
|
||||||
{ column: 'date', order: 'desc' },
|
{ column: 'date', order: 'desc' },
|
||||||
{ column: 'created_at', order: 'desc' },
|
{ column: 'created_at', order: 'desc' },
|
||||||
])
|
])
|
||||||
.limit(pagination.total)
|
.limit(this.pagination.total)
|
||||||
.offset(pagination.pageSize * (pagination.page - 1));
|
.offset(this.pagination.pageSize * (this.pagination.page - 1));
|
||||||
|
|
||||||
// Sumation of credit and debit balance.
|
// Sumation of credit and debit balance.
|
||||||
const openingBalances = await AccountTransaction.query()
|
const openingBalances = await AccountTransaction.query()
|
||||||
@@ -60,6 +85,43 @@ export default class CashflowAccountTransactionsRepo {
|
|||||||
|
|
||||||
const openingBalance = openingBalances.debit - openingBalances.credit;
|
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 { Service, Inject } from 'typedi';
|
||||||
import { includes } from 'lodash';
|
|
||||||
import * as qim from 'qim';
|
import * as qim from 'qim';
|
||||||
import { ICashflowAccountTransactionsQuery, IAccount } from '@/interfaces';
|
import { ICashflowAccountTransactionsQuery, IAccount } from '@/interfaces';
|
||||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||||
import FinancialSheet from '../FinancialSheet';
|
import FinancialSheet from '../FinancialSheet';
|
||||||
import CashflowAccountTransactionsRepo from './CashflowAccountTransactionsRepo';
|
import { CashflowAccountTransactionReport } from './CashflowAccountTransactions';
|
||||||
import CashflowAccountTransactionsReport from './CashflowAccountTransactions';
|
|
||||||
import { ACCOUNT_TYPE } from '@/data/AccountTypes';
|
|
||||||
import { ServiceError } from '@/exceptions';
|
|
||||||
import { ERRORS } from './constants';
|
|
||||||
import I18nService from '@/services/I18n/I18nService';
|
import I18nService from '@/services/I18n/I18nService';
|
||||||
|
import { CashflowAccountTransactionsRepo } from './CashflowAccountTransactionsRepo';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class CashflowAccountTransactionsService extends FinancialSheet {
|
export default class CashflowAccountTransactionsService extends FinancialSheet {
|
||||||
@Inject()
|
@Inject()
|
||||||
tenancy: TenancyService;
|
private tenancy: TenancyService;
|
||||||
|
|
||||||
@Inject()
|
|
||||||
cashflowTransactionsRepo: CashflowAccountTransactionsRepo;
|
|
||||||
|
|
||||||
@Inject()
|
|
||||||
i18nService: I18nService;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defaults balance sheet filter query.
|
* Defaults balance sheet filter query.
|
||||||
@@ -50,59 +40,24 @@ export default class CashflowAccountTransactionsService extends FinancialSheet {
|
|||||||
tenantId: number,
|
tenantId: number,
|
||||||
query: ICashflowAccountTransactionsQuery
|
query: ICashflowAccountTransactionsQuery
|
||||||
) {
|
) {
|
||||||
const { Account } = this.tenancy.models(tenantId);
|
const models = this.tenancy.models(tenantId);
|
||||||
const parsedQuery = { ...this.defaultQuery, ...query };
|
const parsedQuery = { ...this.defaultQuery, ...query };
|
||||||
|
|
||||||
// Retrieve the given account or throw not found service error.
|
// Initalize the bank transactions report repository.
|
||||||
const account = await Account.query().findById(parsedQuery.accountId);
|
const cashflowTransactionsRepo = new CashflowAccountTransactionsRepo(
|
||||||
|
models,
|
||||||
// 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,
|
|
||||||
parsedQuery
|
parsedQuery
|
||||||
);
|
);
|
||||||
const reportTranasctions = report.reportData();
|
await cashflowTransactionsRepo.asyncInit();
|
||||||
|
|
||||||
return {
|
// Retrieve the computed report.
|
||||||
transactions: this.i18nService.i18nApply(
|
const report = new CashflowAccountTransactionReport(
|
||||||
[[qim.$each, 'formattedTransactionType']],
|
cashflowTransactionsRepo,
|
||||||
reportTranasctions,
|
parsedQuery
|
||||||
tenantId
|
);
|
||||||
),
|
const transactions = report.reportData();
|
||||||
pagination,
|
const pagination = cashflowTransactionsRepo.pagination;
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
return { transactions, 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
export const ERRORS = {
|
export const ERRORS = {
|
||||||
ACCOUNT_ID_HAS_INVALID_TYPE: 'ACCOUNT_ID_HAS_INVALID_TYPE',
|
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 { getUniqueImportableValue, trimObject } from './_utils';
|
||||||
import { ImportableResources } from './ImportableResources';
|
import { ImportableResources } from './ImportableResources';
|
||||||
import ResourceService from '../Resource/ResourceService';
|
import ResourceService from '../Resource/ResourceService';
|
||||||
import HasTenancyService from '../Tenancy/TenancyService';
|
|
||||||
import { Import } from '@/system/models';
|
import { Import } from '@/system/models';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class ImportFileCommon {
|
export class ImportFileCommon {
|
||||||
@Inject()
|
|
||||||
private tenancy: HasTenancyService;
|
|
||||||
|
|
||||||
@Inject()
|
@Inject()
|
||||||
private importFileValidator: ImportFileDataValidator;
|
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 { ImportFilePreview } from './ImportFilePreview';
|
||||||
import { ImportSampleService } from './ImportSample';
|
import { ImportSampleService } from './ImportSample';
|
||||||
import { ImportFileMeta } from './ImportFileMeta';
|
import { ImportFileMeta } from './ImportFileMeta';
|
||||||
|
import { ImportFileProcessCommit } from './ImportFileProcessCommit';
|
||||||
|
|
||||||
@Inject()
|
@Inject()
|
||||||
export class ImportResourceApplication {
|
export class ImportResourceApplication {
|
||||||
@@ -27,6 +28,9 @@ export class ImportResourceApplication {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private importMetaService: ImportFileMeta;
|
private importMetaService: ImportFileMeta;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private importProcessCommit: ImportFileProcessCommit;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads the imported file and stores the import file meta under unqiue id.
|
* Reads the imported file and stores the import file meta under unqiue id.
|
||||||
* @param {number} tenantId -
|
* @param {number} tenantId -
|
||||||
@@ -74,12 +78,12 @@ export class ImportResourceApplication {
|
|||||||
* @returns {Promise<ImportFilePreviewPOJO>}
|
* @returns {Promise<ImportFilePreviewPOJO>}
|
||||||
*/
|
*/
|
||||||
public async process(tenantId: number, importId: number) {
|
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.
|
* Retrieves the import meta of the given import id.
|
||||||
* @param {number} tenantId -
|
* @param {number} tenantId -
|
||||||
* @param {string} importId - Import id.
|
* @param {string} importId - Import id.
|
||||||
* @returns {}
|
* @returns {}
|
||||||
*/
|
*/
|
||||||
|
|||||||
51
packages/server/src/services/Loops/LoopsEventsSubscriber.ts
Normal file
51
packages/server/src/services/Loops/LoopsEventsSubscriber.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import config from '@/config';
|
||||||
|
import { IAuthSignUpVerifiedEventPayload } from '@/interfaces';
|
||||||
|
import events from '@/subscribers/events';
|
||||||
|
import { SystemUser } from '@/system/models';
|
||||||
|
|
||||||
|
export class LoopsEventsSubscriber {
|
||||||
|
/**
|
||||||
|
* Constructor method.
|
||||||
|
*/
|
||||||
|
public attach(bus) {
|
||||||
|
bus.subscribe(
|
||||||
|
events.auth.signUpConfirmed,
|
||||||
|
this.triggerEventOnSignupVerified.bind(this)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Once the user verified sends the event to the Loops.
|
||||||
|
* @param {IAuthSignUpVerifiedEventPayload} param0
|
||||||
|
*/
|
||||||
|
public async triggerEventOnSignupVerified({
|
||||||
|
email,
|
||||||
|
userId,
|
||||||
|
}: IAuthSignUpVerifiedEventPayload) {
|
||||||
|
// Can't continue since the Loops the api key is not configured.
|
||||||
|
if (!config.loops.apiKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const user = await SystemUser.query().findById(userId);
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
method: 'POST',
|
||||||
|
url: 'https://app.loops.so/api/v1/events/send',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${config.loops.apiKey}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
email,
|
||||||
|
userId,
|
||||||
|
firstName: user.firstName,
|
||||||
|
lastName: user.lastName,
|
||||||
|
eventName: 'USER_VERIFIED',
|
||||||
|
eventProperties: {},
|
||||||
|
mailingLists: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await axios(options);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import { omit, sumBy } from 'lodash';
|
|||||||
import { IBillPayment, IBillPaymentDTO, IVendor } from '@/interfaces';
|
import { IBillPayment, IBillPaymentDTO, IVendor } from '@/interfaces';
|
||||||
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
|
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
|
||||||
import { formatDateFields } from '@/utils';
|
import { formatDateFields } from '@/utils';
|
||||||
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class CommandBillPaymentDTOTransformer {
|
export class CommandBillPaymentDTOTransformer {
|
||||||
@@ -23,11 +24,14 @@ export class CommandBillPaymentDTOTransformer {
|
|||||||
vendor: IVendor,
|
vendor: IVendor,
|
||||||
oldBillPayment?: IBillPayment
|
oldBillPayment?: IBillPayment
|
||||||
): Promise<IBillPayment> {
|
): Promise<IBillPayment> {
|
||||||
|
const amount =
|
||||||
|
billPaymentDTO.amount ?? sumBy(billPaymentDTO.entries, 'paymentAmount');
|
||||||
|
|
||||||
const initialDTO = {
|
const initialDTO = {
|
||||||
...formatDateFields(omit(billPaymentDTO, ['attachments']), [
|
...formatDateFields(omit(billPaymentDTO, ['attachments']), [
|
||||||
'paymentDate',
|
'paymentDate',
|
||||||
]),
|
]),
|
||||||
amount: sumBy(billPaymentDTO.entries, 'paymentAmount'),
|
amount,
|
||||||
currencyCode: vendor.currencyCode,
|
currencyCode: vendor.currencyCode,
|
||||||
exchangeRate: billPaymentDTO.exchangeRate || 1,
|
exchangeRate: billPaymentDTO.exchangeRate || 1,
|
||||||
entries: billPaymentDTO.entries,
|
entries: billPaymentDTO.entries,
|
||||||
|
|||||||
@@ -36,7 +36,9 @@ export class PaymentReceiveDTOTransformer {
|
|||||||
paymentReceiveDTO: IPaymentReceiveCreateDTO | IPaymentReceiveEditDTO,
|
paymentReceiveDTO: IPaymentReceiveCreateDTO | IPaymentReceiveEditDTO,
|
||||||
oldPaymentReceive?: IPaymentReceive
|
oldPaymentReceive?: IPaymentReceive
|
||||||
): Promise<IPaymentReceive> {
|
): Promise<IPaymentReceive> {
|
||||||
const paymentAmount = sumBy(paymentReceiveDTO.entries, 'paymentAmount');
|
const amount =
|
||||||
|
paymentReceiveDTO.amount ??
|
||||||
|
sumBy(paymentReceiveDTO.entries, 'paymentAmount');
|
||||||
|
|
||||||
// Retreive the next invoice number.
|
// Retreive the next invoice number.
|
||||||
const autoNextNumber =
|
const autoNextNumber =
|
||||||
@@ -54,7 +56,7 @@ export class PaymentReceiveDTOTransformer {
|
|||||||
...formatDateFields(omit(paymentReceiveDTO, ['entries', 'attachments']), [
|
...formatDateFields(omit(paymentReceiveDTO, ['entries', 'attachments']), [
|
||||||
'paymentDate',
|
'paymentDate',
|
||||||
]),
|
]),
|
||||||
amount: paymentAmount,
|
amount,
|
||||||
currencyCode: customer.currencyCode,
|
currencyCode: customer.currencyCode,
|
||||||
...(paymentReceiveNo ? { paymentReceiveNo } : {}),
|
...(paymentReceiveNo ? { paymentReceiveNo } : {}),
|
||||||
exchangeRate: paymentReceiveDTO.exchangeRate || 1,
|
exchangeRate: paymentReceiveDTO.exchangeRate || 1,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { getPrice } from '@lemonsqueezy/lemonsqueezy.js';
|
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import {
|
import {
|
||||||
@@ -10,7 +9,6 @@ import {
|
|||||||
} from './utils';
|
} from './utils';
|
||||||
import { Plan } from '@/system/models';
|
import { Plan } from '@/system/models';
|
||||||
import { Subscription } from './Subscription';
|
import { Subscription } from './Subscription';
|
||||||
import { isEmpty } from 'lodash';
|
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class LemonSqueezyWebhooks {
|
export class LemonSqueezyWebhooks {
|
||||||
@@ -18,7 +16,7 @@ export class LemonSqueezyWebhooks {
|
|||||||
private subscriptionService: Subscription;
|
private subscriptionService: Subscription;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* handle the LemonSqueezy webhooks.
|
* Handles the Lemon Squeezy webhooks.
|
||||||
* @param {string} rawBody
|
* @param {string} rawBody
|
||||||
* @param {string} signature
|
* @param {string} signature
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
@@ -74,7 +72,7 @@ export class LemonSqueezyWebhooks {
|
|||||||
const variantId = attributes.variant_id as string;
|
const variantId = attributes.variant_id as string;
|
||||||
|
|
||||||
// We assume that the Plan table is up to date.
|
// We assume that the Plan table is up to date.
|
||||||
const plan = await Plan.query().findOne('slug', 'early-adaptor');
|
const plan = await Plan.query().findOne('lemonVariantId', variantId);
|
||||||
|
|
||||||
if (!plan) {
|
if (!plan) {
|
||||||
throw new Error(`Plan with variantId ${variantId} not found.`);
|
throw new Error(`Plan with variantId ${variantId} not found.`);
|
||||||
@@ -82,26 +80,9 @@ export class LemonSqueezyWebhooks {
|
|||||||
// Update the subscription in the database.
|
// Update the subscription in the database.
|
||||||
const priceId = attributes.first_subscription_item.price_id;
|
const priceId = attributes.first_subscription_item.price_id;
|
||||||
|
|
||||||
// Get the price data from Lemon Squeezy.
|
|
||||||
const priceData = await getPrice(priceId);
|
|
||||||
|
|
||||||
if (priceData.error) {
|
|
||||||
throw new Error(
|
|
||||||
`Failed to get the price data for the subscription ${eventBody.data.id}.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const isUsageBased =
|
|
||||||
attributes.first_subscription_item.is_usage_based;
|
|
||||||
const price = isUsageBased
|
|
||||||
? priceData.data?.data.attributes.unit_price_decimal
|
|
||||||
: priceData.data?.data.attributes.unit_price;
|
|
||||||
|
|
||||||
// Create a new subscription of the tenant.
|
// Create a new subscription of the tenant.
|
||||||
if (webhookEvent === 'subscription_created') {
|
if (webhookEvent === 'subscription_created') {
|
||||||
await this.subscriptionService.newSubscribtion(
|
await this.subscriptionService.newSubscribtion(tenantId, plan.slug);
|
||||||
tenantId,
|
|
||||||
'early-adaptor'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (webhookEvent.startsWith('order_')) {
|
} else if (webhookEvent.startsWith('order_')) {
|
||||||
|
|||||||
@@ -77,7 +77,12 @@ export default class HasTenancyService {
|
|||||||
const knex = this.knex(tenantId);
|
const knex = this.knex(tenantId);
|
||||||
const i18n = this.i18n(tenantId);
|
const i18n = this.i18n(tenantId);
|
||||||
|
|
||||||
return tenantRepositoriesLoader(knex, cache, i18n);
|
const repositories = tenantRepositoriesLoader(knex, cache, i18n);
|
||||||
|
|
||||||
|
Object.values(repositories).forEach((repository) => {
|
||||||
|
repository.setTenantId(tenantId);
|
||||||
|
});
|
||||||
|
return repositories;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,13 @@ export default {
|
|||||||
baseCurrencyUpdated: 'onOrganizationBaseCurrencyUpdated',
|
baseCurrencyUpdated: 'onOrganizationBaseCurrencyUpdated',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User subscription events.
|
||||||
|
*/
|
||||||
|
subscription: {
|
||||||
|
onSubscribed: 'onOrganizationSubscribed',
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tenants managment service.
|
* Tenants managment service.
|
||||||
*/
|
*/
|
||||||
@@ -399,6 +406,9 @@ export default {
|
|||||||
onTransactionCategorizing: 'onTransactionCategorizing',
|
onTransactionCategorizing: 'onTransactionCategorizing',
|
||||||
onTransactionCategorized: 'onCashflowTransactionCategorized',
|
onTransactionCategorized: 'onCashflowTransactionCategorized',
|
||||||
|
|
||||||
|
onTransactionUncategorizedCreating: 'onTransactionUncategorizedCreating',
|
||||||
|
onTransactionUncategorizedCreated: 'onTransactionUncategorizedCreated',
|
||||||
|
|
||||||
onTransactionUncategorizing: 'onTransactionUncategorizing',
|
onTransactionUncategorizing: 'onTransactionUncategorizing',
|
||||||
onTransactionUncategorized: 'onTransactionUncategorized',
|
onTransactionUncategorized: 'onTransactionUncategorized',
|
||||||
|
|
||||||
@@ -639,4 +649,17 @@ export default {
|
|||||||
onUnmatching: 'onBankTransactionUnmathcing',
|
onUnmatching: 'onBankTransactionUnmathcing',
|
||||||
onUnmatched: 'onBankTransactionUnmathced',
|
onUnmatched: 'onBankTransactionUnmathced',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
bankTransactions: {
|
||||||
|
onExcluding: 'onBankTransactionExclude',
|
||||||
|
onExcluded: 'onBankTransactionExcluded',
|
||||||
|
|
||||||
|
onUnexcluding: 'onBankTransactionUnexcluding',
|
||||||
|
onUnexcluded: 'onBankTransactionUnexcluded',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Import files.
|
||||||
|
import: {
|
||||||
|
onImportCommitted: 'onImportFileCommitted',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
exports.up = function (knex) {
|
||||||
|
return knex.schema.table('subscription_plans', (table) => {
|
||||||
|
table.string('lemon_variant_id').nullable().index();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = (knex) => {
|
||||||
|
return knex.schema.table('subscription_plans', (table) => {
|
||||||
|
table.dropColumn('lemon_variant_id');
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
exports.up = function (knex) {
|
||||||
|
return knex('subscription_plans').insert([
|
||||||
|
// Capital Basic
|
||||||
|
{
|
||||||
|
name: 'Capital Basic (Monthly)',
|
||||||
|
slug: 'capital-basic-monthly',
|
||||||
|
price: 10,
|
||||||
|
active: true,
|
||||||
|
currency: 'USD',
|
||||||
|
invoice_period: 1,
|
||||||
|
invoice_interval: 'month',
|
||||||
|
lemon_variant_id: '446152',
|
||||||
|
// lemon_variant_id: '450016',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Capital Basic (Annually)',
|
||||||
|
slug: 'capital-basic-annually',
|
||||||
|
price: 90,
|
||||||
|
active: true,
|
||||||
|
currency: 'USD',
|
||||||
|
invoice_period: 1,
|
||||||
|
invoice_interval: 'year',
|
||||||
|
lemon_variant_id: '446153',
|
||||||
|
// lemon_variant_id: '450018',
|
||||||
|
},
|
||||||
|
|
||||||
|
// # Capital Essential
|
||||||
|
{
|
||||||
|
name: 'Capital Essential (Monthly)',
|
||||||
|
slug: 'capital-essential-monthly',
|
||||||
|
price: 20,
|
||||||
|
active: true,
|
||||||
|
currency: 'USD',
|
||||||
|
invoice_period: 1,
|
||||||
|
invoice_interval: 'month',
|
||||||
|
lemon_variant_id: '446155',
|
||||||
|
// lemon_variant_id: '450028',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Capital Essential (Annually)',
|
||||||
|
slug: 'capital-essential-annually',
|
||||||
|
price: 180,
|
||||||
|
active: true,
|
||||||
|
invoice_period: 1,
|
||||||
|
invoice_interval: 'year',
|
||||||
|
lemon_variant_id: '446156',
|
||||||
|
// lemon_variant_id: '450029',
|
||||||
|
},
|
||||||
|
|
||||||
|
// # Capital Plus
|
||||||
|
{
|
||||||
|
name: 'Capital Plus (Monthly)',
|
||||||
|
slug: 'capital-plus-monthly',
|
||||||
|
price: 25,
|
||||||
|
active: true,
|
||||||
|
invoice_period: 1,
|
||||||
|
invoice_interval: 'month',
|
||||||
|
lemon_variant_id: '446165',
|
||||||
|
// lemon_variant_id: '450031',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Capital Plus (Annually)',
|
||||||
|
slug: 'capital-plus-annually',
|
||||||
|
price: 228,
|
||||||
|
active: true,
|
||||||
|
invoice_period: 1,
|
||||||
|
invoice_interval: 'year',
|
||||||
|
lemon_variant_id: '446164',
|
||||||
|
// lemon_variant_id: '450032',
|
||||||
|
},
|
||||||
|
|
||||||
|
// # Capital Big
|
||||||
|
{
|
||||||
|
name: 'Capital Big (Monthly)',
|
||||||
|
slug: 'capital-big-monthly',
|
||||||
|
price: 40,
|
||||||
|
active: true,
|
||||||
|
invoice_period: 1,
|
||||||
|
invoice_interval: 'month',
|
||||||
|
lemon_variant_id: '446167',
|
||||||
|
// lemon_variant_id: '450024',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Capital Big (Annually)',
|
||||||
|
slug: 'capital-big-annually',
|
||||||
|
price: 360,
|
||||||
|
active: true,
|
||||||
|
invoice_period: 1,
|
||||||
|
invoice_interval: 'year',
|
||||||
|
lemon_variant_id: '446168',
|
||||||
|
// lemon_variant_id: '450025',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function (knex) {};
|
||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,9 +23,10 @@
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
.label {
|
.label {
|
||||||
font-size: 14px;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #2F343C;
|
color: #2F343C;
|
||||||
|
|
||||||
@@ -47,13 +48,31 @@
|
|||||||
}
|
}
|
||||||
.price {
|
.price {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #404854;
|
color: #252A31;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pricePer{
|
.pricePer{
|
||||||
color: #738091;
|
color: #738091;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featureItem{
|
||||||
|
flex: 1;
|
||||||
|
color: #1C2127;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featurePopover :global .bp4-popover-content{
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
.featurePopoverContent{
|
||||||
|
font-size: 12px
|
||||||
|
}
|
||||||
|
.featurePopoverLabel {
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.4px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,11 @@
|
|||||||
import { Button, ButtonProps, Intent } from '@blueprintjs/core';
|
import {
|
||||||
|
Button,
|
||||||
|
ButtonProps,
|
||||||
|
Intent,
|
||||||
|
Position,
|
||||||
|
Text,
|
||||||
|
Tooltip,
|
||||||
|
} from '@blueprintjs/core';
|
||||||
import clsx from 'classnames';
|
import clsx from 'classnames';
|
||||||
import { Box, Group, Stack } from '../Layout';
|
import { Box, Group, Stack } from '../Layout';
|
||||||
import styles from './PricingPlan.module.scss';
|
import styles from './PricingPlan.module.scss';
|
||||||
@@ -64,7 +71,7 @@ export interface PricingPriceProps {
|
|||||||
*/
|
*/
|
||||||
PricingPlan.Price = ({ price, subPrice }: PricingPriceProps) => {
|
PricingPlan.Price = ({ price, subPrice }: PricingPriceProps) => {
|
||||||
return (
|
return (
|
||||||
<Stack spacing={6} className={styles.priceRoot}>
|
<Stack spacing={4} className={styles.priceRoot}>
|
||||||
<h4 className={styles.price}>{price}</h4>
|
<h4 className={styles.price}>{price}</h4>
|
||||||
<span className={styles.pricePer}>{subPrice}</span>
|
<span className={styles.pricePer}>{subPrice}</span>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -101,7 +108,7 @@ export interface PricingFeaturesProps {
|
|||||||
*/
|
*/
|
||||||
PricingPlan.Features = ({ children }: PricingFeaturesProps) => {
|
PricingPlan.Features = ({ children }: PricingFeaturesProps) => {
|
||||||
return (
|
return (
|
||||||
<Stack spacing={10} className={styles.features}>
|
<Stack spacing={14} className={styles.features}>
|
||||||
{children}
|
{children}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
@@ -109,15 +116,41 @@ PricingPlan.Features = ({ children }: PricingFeaturesProps) => {
|
|||||||
|
|
||||||
export interface PricingFeatureLineProps {
|
export interface PricingFeatureLineProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
hintContent?: string;
|
||||||
|
hintLabel?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays a single feature line within a list of features.
|
* Displays a single feature line within a list of features.
|
||||||
* @param children - The content of the feature line.
|
* @param children - The content of the feature line.
|
||||||
*/
|
*/
|
||||||
PricingPlan.FeatureLine = ({ children }: PricingFeatureLineProps) => {
|
PricingPlan.FeatureLine = ({
|
||||||
return (
|
children,
|
||||||
<Group noWrap spacing={12}>
|
hintContent,
|
||||||
|
hintLabel,
|
||||||
|
}: PricingFeatureLineProps) => {
|
||||||
|
return hintContent ? (
|
||||||
|
<Tooltip
|
||||||
|
content={
|
||||||
|
<Stack spacing={5}>
|
||||||
|
{hintLabel && (
|
||||||
|
<Text className={styles.featurePopoverLabel}>{hintLabel}</Text>
|
||||||
|
)}
|
||||||
|
<Text className={styles.featurePopoverContent}>{hintContent}</Text>
|
||||||
|
</Stack>
|
||||||
|
}
|
||||||
|
position={Position.TOP_LEFT}
|
||||||
|
popoverClassName={styles.featurePopover}
|
||||||
|
modifiers={{ offset: { enabled: true, offset: '0,10' } }}
|
||||||
|
minimal
|
||||||
|
>
|
||||||
|
<Group noWrap spacing={8} style={{ cursor: 'help' }}>
|
||||||
|
<CheckCircled height={12} width={12} />
|
||||||
|
<Box className={styles.featureItem}>{children}</Box>
|
||||||
|
</Group>
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
<Group noWrap spacing={8}>
|
||||||
<CheckCircled height={12} width={12} />
|
<CheckCircled height={12} width={12} />
|
||||||
<Box className={styles.featureItem}>{children}</Box>
|
<Box className={styles.featureItem}>{children}</Box>
|
||||||
</Group>
|
</Group>
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ export const ACCOUNT_TYPE = {
|
|||||||
BANK: 'bank',
|
BANK: 'bank',
|
||||||
ACCOUNTS_RECEIVABLE: 'accounts-receivable',
|
ACCOUNTS_RECEIVABLE: 'accounts-receivable',
|
||||||
INVENTORY: 'inventory',
|
INVENTORY: 'inventory',
|
||||||
OTHER_CURRENT_ASSET: 'other-ACCOUNT_PARENT_TYPE.CURRENT_ASSET',
|
OTHER_CURRENT_ASSET: 'other-current-asset',
|
||||||
FIXED_ASSET: 'fixed-asset',
|
FIXED_ASSET: 'fixed-asset',
|
||||||
NON_CURRENT_ASSET: 'non-ACCOUNT_PARENT_TYPE.CURRENT_ASSET',
|
NON_CURRENT_ASSET: 'non-current-asset',
|
||||||
|
|
||||||
ACCOUNTS_PAYABLE: 'accounts-payable',
|
ACCOUNTS_PAYABLE: 'accounts-payable',
|
||||||
CREDIT_CARD: 'credit-card',
|
CREDIT_CARD: 'credit-card',
|
||||||
|
|||||||
@@ -39,3 +39,12 @@ export const TRANSACRIONS_TYPE = [
|
|||||||
'OtherExpense',
|
'OtherExpense',
|
||||||
'TransferToAccount',
|
'TransferToAccount',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const MoneyCategoryPerCreditAccountRootType = {
|
||||||
|
OwnerContribution: ['equity'],
|
||||||
|
OtherIncome: ['income'],
|
||||||
|
OwnerDrawing: ['equity'],
|
||||||
|
OtherExpense: ['expense'],
|
||||||
|
TransferToAccount: ['asset'],
|
||||||
|
TransferFromAccount: ['asset'],
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,10 +1,140 @@
|
|||||||
// @ts-nocheck
|
interface SubscriptionPlanFeature {
|
||||||
// Subscription plans.
|
text: string;
|
||||||
export const plans = [
|
hint?: string;
|
||||||
|
label?: string;
|
||||||
];
|
style?: Record<string, string>;
|
||||||
|
}
|
||||||
|
interface SubscriptionPlan {
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
description: string;
|
||||||
|
features: SubscriptionPlanFeature[];
|
||||||
|
featured?: boolean;
|
||||||
|
monthlyPrice: string;
|
||||||
|
monthlyPriceLabel: string;
|
||||||
|
annuallyPrice: string;
|
||||||
|
annuallyPriceLabel: string;
|
||||||
|
monthlyVariantId: string;
|
||||||
|
annuallyVariantId: string;
|
||||||
|
}
|
||||||
|
|
||||||
// Payment methods.
|
export const SubscriptionPlans = [
|
||||||
export const paymentMethods = [
|
{
|
||||||
|
name: 'Capital Basic',
|
||||||
];
|
slug: 'capital_basic',
|
||||||
|
description: 'Good for service businesses that just started.',
|
||||||
|
features: [
|
||||||
|
{
|
||||||
|
text: 'Unlimited Sale Invoices',
|
||||||
|
hintLabel: 'Unlimited Sale Invoices',
|
||||||
|
hint: 'Good for service businesses that just started for service businesses that just started',
|
||||||
|
},
|
||||||
|
{ text: 'Unlimated Sale Estimates' },
|
||||||
|
{ text: 'Track GST and VAT' },
|
||||||
|
{ text: 'Connect Banks for Automatic Importing' },
|
||||||
|
{ text: 'Chart of Accounts' },
|
||||||
|
{
|
||||||
|
text: 'Manual Journals',
|
||||||
|
hintLabel: 'Manual Journals',
|
||||||
|
hint: 'Write manual journals entries for financial transactions not automatically captured by the system to adjust financial statements.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Basic Financial Reports & Insights',
|
||||||
|
hint: 'Balance sheet, profit & loss statement, cashflow statement, general ledger, journal sheet, A/P aging summary, A/R aging summary',
|
||||||
|
},
|
||||||
|
{ text: 'Unlimited User Seats' },
|
||||||
|
],
|
||||||
|
monthlyPrice: '$10',
|
||||||
|
monthlyPriceLabel: 'Per month',
|
||||||
|
annuallyPrice: '$7.5',
|
||||||
|
annuallyPriceLabel: 'Per month',
|
||||||
|
monthlyVariantId: '446152',
|
||||||
|
// monthlyVariantId: '450016',
|
||||||
|
annuallyVariantId: '446153',
|
||||||
|
// annuallyVariantId: '450018',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Capital Essential',
|
||||||
|
slug: 'capital_plus',
|
||||||
|
description: 'Good for have inventory and want more financial reports.',
|
||||||
|
features: [
|
||||||
|
{ text: 'All Capital Basic features' },
|
||||||
|
{ text: 'Purchase Invoices' },
|
||||||
|
{
|
||||||
|
text: 'Multi Currency Transactions',
|
||||||
|
hintLabel: 'Multi Currency',
|
||||||
|
hint: 'Pay and get paid and do manual journals in any currency with real time exchange rates conversions.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Transactions Locking',
|
||||||
|
hintLabel: 'Transactions Locking',
|
||||||
|
hint: 'Transaction Locking freezes transactions to prevent any additions, modifications, or deletions of transactions recorded during the specified date.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Inventory Tracking',
|
||||||
|
hintLabel: 'Inventory Tracking',
|
||||||
|
hint: 'Track goods in the stock, cost of goods, and get notifications when quantity is low.',
|
||||||
|
},
|
||||||
|
{ text: 'Smart Financial Reports' },
|
||||||
|
{ text: 'Advanced Inventory Reports' },
|
||||||
|
],
|
||||||
|
monthlyPrice: '$20',
|
||||||
|
monthlyPriceLabel: 'Per month',
|
||||||
|
annuallyPrice: '$15',
|
||||||
|
annuallyPriceLabel: 'Per month',
|
||||||
|
// monthlyVariantId: '450028',
|
||||||
|
monthlyVariantId: '446155',
|
||||||
|
// annuallyVariantId: '450029',
|
||||||
|
annuallyVariantId: '446156',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Capital Plus',
|
||||||
|
slug: 'essentials',
|
||||||
|
description: 'Good for business want financial and access control.',
|
||||||
|
features: [
|
||||||
|
{ text: 'All Capital Essential features' },
|
||||||
|
{ text: 'Custom User Roles Access' },
|
||||||
|
{ text: 'Vendor Credits' },
|
||||||
|
{
|
||||||
|
text: 'Budgeting',
|
||||||
|
hint: 'Create multiple budgets and compare targets with actuals to understand how your business is performing.',
|
||||||
|
},
|
||||||
|
{ text: 'Analysis Cost Center' },
|
||||||
|
],
|
||||||
|
monthlyPrice: '$25',
|
||||||
|
monthlyPriceLabel: 'Per month',
|
||||||
|
annuallyPrice: '$19',
|
||||||
|
annuallyPriceLabel: 'Per month',
|
||||||
|
featured: true,
|
||||||
|
// monthlyVariantId: '450031',
|
||||||
|
monthlyVariantId: '446165',
|
||||||
|
// annuallyVariantId: '450032',
|
||||||
|
annuallyVariantId: '446164',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Capital Big',
|
||||||
|
slug: 'essentials',
|
||||||
|
description: 'Good for businesses have multiple branches.',
|
||||||
|
features: [
|
||||||
|
{ text: 'All Capital Plus features' },
|
||||||
|
{
|
||||||
|
text: 'Multiple Branches',
|
||||||
|
hintLabel: '',
|
||||||
|
hint: 'Track the organization transactions and accounts in multiple branches.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Multiple Warehouses',
|
||||||
|
hintLabel: 'Multiple Warehouses',
|
||||||
|
hint: 'Track the organization inventory in multiple warehouses and transfer goods between them.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
monthlyPrice: '$40',
|
||||||
|
monthlyPriceLabel: 'Per month',
|
||||||
|
annuallyPrice: '$30',
|
||||||
|
annuallyPriceLabel: 'Per month',
|
||||||
|
// monthlyVariantId: '450024',
|
||||||
|
monthlyVariantId: '446167',
|
||||||
|
// annuallyVariantId: '450025',
|
||||||
|
annuallyVariantId: '446168',
|
||||||
|
},
|
||||||
|
] as SubscriptionPlan[];
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { DashboardPageContent } from '@/components';
|
|||||||
import { transformTableStateToQuery, compose } from '@/utils';
|
import { transformTableStateToQuery, compose } from '@/utils';
|
||||||
|
|
||||||
import { ManualJournalsListProvider } from './ManualJournalsListProvider';
|
import { ManualJournalsListProvider } from './ManualJournalsListProvider';
|
||||||
import ManualJournalsViewTabs from './ManualJournalsViewTabs';
|
|
||||||
import ManualJournalsDataTable from './ManualJournalsDataTable';
|
import ManualJournalsDataTable from './ManualJournalsDataTable';
|
||||||
import ManualJournalsActionsBar from './ManualJournalActionsBar';
|
import ManualJournalsActionsBar from './ManualJournalActionsBar';
|
||||||
import withManualJournals from './withManualJournals';
|
import withManualJournals from './withManualJournals';
|
||||||
@@ -29,7 +28,6 @@ function ManualJournalsTable({
|
|||||||
<ManualJournalsActionsBar />
|
<ManualJournalsActionsBar />
|
||||||
|
|
||||||
<DashboardPageContent>
|
<DashboardPageContent>
|
||||||
<ManualJournalsViewTabs />
|
|
||||||
<ManualJournalsDataTable />
|
<ManualJournalsDataTable />
|
||||||
</DashboardPageContent>
|
</DashboardPageContent>
|
||||||
</ManualJournalsListProvider>
|
</ManualJournalsListProvider>
|
||||||
|
|||||||
@@ -2,15 +2,15 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
|
|
||||||
import '@/style/pages/Accounts/List.scss';
|
import '@/style/pages/Accounts/List.scss';
|
||||||
import { DashboardPageContent, DashboardContentTable } from '@/components';
|
|
||||||
|
|
||||||
|
import { DashboardPageContent, DashboardContentTable } from '@/components';
|
||||||
import { AccountsChartProvider } from './AccountsChartProvider';
|
import { AccountsChartProvider } from './AccountsChartProvider';
|
||||||
import AccountsViewsTabs from './AccountsViewsTabs';
|
|
||||||
import AccountsActionsBar from './AccountsActionsBar';
|
import AccountsActionsBar from './AccountsActionsBar';
|
||||||
import AccountsDataTable from './AccountsDataTable';
|
import AccountsDataTable from './AccountsDataTable';
|
||||||
|
|
||||||
import withAccounts from '@/containers/Accounts/withAccounts';
|
import withAccounts from '@/containers/Accounts/withAccounts';
|
||||||
import withAccountsTableActions from './withAccountsTableActions';
|
import withAccountsTableActions from './withAccountsTableActions';
|
||||||
|
|
||||||
import { transformAccountsStateToQuery } from './utils';
|
import { transformAccountsStateToQuery } from './utils';
|
||||||
import { compose } from '@/utils';
|
import { compose } from '@/utils';
|
||||||
|
|
||||||
@@ -41,8 +41,6 @@ function AccountsChart({
|
|||||||
<AccountsActionsBar />
|
<AccountsActionsBar />
|
||||||
|
|
||||||
<DashboardPageContent>
|
<DashboardPageContent>
|
||||||
<AccountsViewsTabs />
|
|
||||||
|
|
||||||
<DashboardContentTable>
|
<DashboardContentTable>
|
||||||
<AccountsDataTable />
|
<AccountsDataTable />
|
||||||
</DashboardContentTable>
|
</DashboardContentTable>
|
||||||
|
|||||||
@@ -69,6 +69,14 @@ function AccountDeleteTransactionAlert({
|
|||||||
'Cannot delete transaction converted from uncategorized transaction but you uncategorize it.',
|
'Cannot delete transaction converted from uncategorized transaction but you uncategorize it.',
|
||||||
intent: Intent.DANGER,
|
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
|
// @ts-nocheck
|
||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
import { Form, Formik, FormikHelpers, useFormikContext } from 'formik';
|
import { Form, Formik, FormikHelpers, useFormikContext } from 'formik';
|
||||||
import { Button, Classes, Intent, Radio, Tag } from '@blueprintjs/core';
|
import { Button, Classes, Intent, Radio, Tag } from '@blueprintjs/core';
|
||||||
import * as R from 'ramda';
|
import * as R from 'ramda';
|
||||||
@@ -16,11 +17,11 @@ import {
|
|||||||
} from '@/components';
|
} from '@/components';
|
||||||
import { useCreateBankRule, useEditBankRule } from '@/hooks/query/bank-rules';
|
import { useCreateBankRule, useEditBankRule } from '@/hooks/query/bank-rules';
|
||||||
import {
|
import {
|
||||||
AssignTransactionTypeOptions,
|
|
||||||
FieldCondition,
|
FieldCondition,
|
||||||
Fields,
|
Fields,
|
||||||
RuleFormValues,
|
RuleFormValues,
|
||||||
TransactionTypeOptions,
|
TransactionTypeOptions,
|
||||||
|
getAccountRootFromMoneyCategory,
|
||||||
initialValues,
|
initialValues,
|
||||||
} from './_utils';
|
} from './_utils';
|
||||||
import { useRuleFormDialogBoot } from './RuleFormBoot';
|
import { useRuleFormDialogBoot } from './RuleFormBoot';
|
||||||
@@ -31,6 +32,11 @@ import {
|
|||||||
} from '@/utils';
|
} from '@/utils';
|
||||||
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||||
import { DialogsName } from '@/constants/dialogs';
|
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({
|
function RuleFormContentFormRoot({
|
||||||
// #withDialogActions
|
// #withDialogActions
|
||||||
@@ -47,7 +53,6 @@ function RuleFormContentFormRoot({
|
|||||||
...initialValues,
|
...initialValues,
|
||||||
...transformToForm(transformToCamelCase(bankRule), initialValues),
|
...transformToForm(transformToCamelCase(bankRule), initialValues),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handles the form submitting.
|
// Handles the form submitting.
|
||||||
const handleSubmit = (
|
const handleSubmit = (
|
||||||
values: RuleFormValues,
|
values: RuleFormValues,
|
||||||
@@ -92,8 +97,9 @@ function RuleFormContentFormRoot({
|
|||||||
label={'Rule Name'}
|
label={'Rule Name'}
|
||||||
labelInfo={<Tag minimal>Required</Tag>}
|
labelInfo={<Tag minimal>Required</Tag>}
|
||||||
style={{ maxWidth: 300 }}
|
style={{ maxWidth: 300 }}
|
||||||
|
fastField
|
||||||
>
|
>
|
||||||
<FInputGroup name={'name'} />
|
<FInputGroup name={'name'} fastField />
|
||||||
</FFormGroup>
|
</FFormGroup>
|
||||||
|
|
||||||
<FFormGroup
|
<FFormGroup
|
||||||
@@ -101,29 +107,22 @@ function RuleFormContentFormRoot({
|
|||||||
label={'Apply the rule to account'}
|
label={'Apply the rule to account'}
|
||||||
labelInfo={<Tag minimal>Required</Tag>}
|
labelInfo={<Tag minimal>Required</Tag>}
|
||||||
style={{ maxWidth: 350 }}
|
style={{ maxWidth: 350 }}
|
||||||
|
fastField
|
||||||
>
|
>
|
||||||
<AccountsSelect
|
<AccountsSelect
|
||||||
name={'applyIfAccountId'}
|
name={'applyIfAccountId'}
|
||||||
items={accounts}
|
items={accounts}
|
||||||
filterByTypes={['cash', 'bank']}
|
filterByTypes={['cash', 'bank']}
|
||||||
|
fastField
|
||||||
/>
|
/>
|
||||||
</FFormGroup>
|
</FFormGroup>
|
||||||
|
|
||||||
<FFormGroup
|
<RuleApplyIfTransactionTypeField />
|
||||||
name={'applyIfTransactionType'}
|
|
||||||
label={'Apply to transactions are'}
|
|
||||||
style={{ maxWidth: 350 }}
|
|
||||||
>
|
|
||||||
<FSelect
|
|
||||||
name={'applyIfTransactionType'}
|
|
||||||
items={TransactionTypeOptions}
|
|
||||||
popoverProps={{ minimal: true, inline: false }}
|
|
||||||
/>
|
|
||||||
</FFormGroup>
|
|
||||||
|
|
||||||
<FFormGroup
|
<FFormGroup
|
||||||
name={'conditionsType'}
|
name={'conditionsType'}
|
||||||
label={'Categorize the transactions when'}
|
label={'Categorize the transactions when'}
|
||||||
|
fastField
|
||||||
>
|
>
|
||||||
<FRadioGroup name={'conditionsType'}>
|
<FRadioGroup name={'conditionsType'}>
|
||||||
<Radio value={'and'} label={'All the following criteria matches'} />
|
<Radio value={'and'} label={'All the following criteria matches'} />
|
||||||
@@ -139,34 +138,16 @@ function RuleFormContentFormRoot({
|
|||||||
Then Assign
|
Then Assign
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<FFormGroup
|
<RuleAssignCategoryField />
|
||||||
name={'assignCategory'}
|
<RuleAssignCategoryAccountField />
|
||||||
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>
|
|
||||||
|
|
||||||
<FFormGroup
|
<FFormGroup
|
||||||
name={'assignRef'}
|
name={'assignRef'}
|
||||||
label={'Reference'}
|
label={'Reference'}
|
||||||
style={{ maxWidth: 300 }}
|
style={{ maxWidth: 300 }}
|
||||||
|
fastField
|
||||||
>
|
>
|
||||||
<FInputGroup name={'assignRef'} />
|
<FInputGroup name={'assignRef'} fastField />
|
||||||
</FFormGroup>
|
</FFormGroup>
|
||||||
|
|
||||||
<RuleFormActions />
|
<RuleFormActions />
|
||||||
@@ -203,11 +184,13 @@ function RuleFormConditions() {
|
|||||||
name={`conditions[${index}].field`}
|
name={`conditions[${index}].field`}
|
||||||
label={'Field'}
|
label={'Field'}
|
||||||
style={{ marginBottom: 0, flex: '1 0' }}
|
style={{ marginBottom: 0, flex: '1 0' }}
|
||||||
|
fastField
|
||||||
>
|
>
|
||||||
<FSelect
|
<FSelect
|
||||||
name={`conditions[${index}].field`}
|
name={`conditions[${index}].field`}
|
||||||
items={Fields}
|
items={Fields}
|
||||||
popoverProps={{ minimal: true, inline: false }}
|
popoverProps={{ minimal: true, inline: false }}
|
||||||
|
fastField
|
||||||
/>
|
/>
|
||||||
</FFormGroup>
|
</FFormGroup>
|
||||||
|
|
||||||
@@ -215,11 +198,13 @@ function RuleFormConditions() {
|
|||||||
name={`conditions[${index}].comparator`}
|
name={`conditions[${index}].comparator`}
|
||||||
label={'Condition'}
|
label={'Condition'}
|
||||||
style={{ marginBottom: 0, flex: '1 0' }}
|
style={{ marginBottom: 0, flex: '1 0' }}
|
||||||
|
fastField
|
||||||
>
|
>
|
||||||
<FSelect
|
<FSelect
|
||||||
name={`conditions[${index}].comparator`}
|
name={`conditions[${index}].comparator`}
|
||||||
items={FieldCondition}
|
items={FieldCondition}
|
||||||
popoverProps={{ minimal: true, inline: false }}
|
popoverProps={{ minimal: true, inline: false }}
|
||||||
|
fastField
|
||||||
/>
|
/>
|
||||||
</FFormGroup>
|
</FFormGroup>
|
||||||
|
|
||||||
@@ -227,8 +212,9 @@ function RuleFormConditions() {
|
|||||||
name={`conditions[${index}].value`}
|
name={`conditions[${index}].value`}
|
||||||
label={'Value'}
|
label={'Value'}
|
||||||
style={{ marginBottom: 0, flex: '1 0 ', width: '40%' }}
|
style={{ marginBottom: 0, flex: '1 0 ', width: '40%' }}
|
||||||
|
fastField
|
||||||
>
|
>
|
||||||
<FInputGroup name={`conditions[${index}].value`} />
|
<FInputGroup name={`conditions[${index}].value`} fastField />
|
||||||
</FFormGroup>
|
</FFormGroup>
|
||||||
</Group>
|
</Group>
|
||||||
))}
|
))}
|
||||||
@@ -284,3 +270,104 @@ function RuleFormActionsRoot({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const RuleFormActions = R.compose(withDialogActions)(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 = {
|
export const initialValues = {
|
||||||
name: '',
|
name: '',
|
||||||
order: 0,
|
order: 0,
|
||||||
applyIfAccountId: '',
|
applyIfAccountId: '',
|
||||||
applyIfTransactionType: '',
|
applyIfTransactionType: 'deposit',
|
||||||
conditionsType: 'and',
|
conditionsType: 'and',
|
||||||
conditions: [
|
conditions: [
|
||||||
{
|
{
|
||||||
@@ -47,3 +50,9 @@ export const FieldCondition = [
|
|||||||
export const AssignTransactionTypeOptions = [
|
export const AssignTransactionTypeOptions = [
|
||||||
{ value: 'expense', text: 'Expense' },
|
{ 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
|
// @ts-nocheck
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
import { Intent } from '@blueprintjs/core';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DataTable,
|
DataTable,
|
||||||
@@ -9,6 +10,7 @@ import {
|
|||||||
TableSkeletonHeader,
|
TableSkeletonHeader,
|
||||||
TableVirtualizedListRows,
|
TableVirtualizedListRows,
|
||||||
FormattedMessage as T,
|
FormattedMessage as T,
|
||||||
|
AppToaster,
|
||||||
} from '@/components';
|
} from '@/components';
|
||||||
import { TABLES } from '@/constants/tables';
|
import { TABLES } from '@/constants/tables';
|
||||||
|
|
||||||
@@ -19,9 +21,11 @@ import withDrawerActions from '@/containers/Drawer/withDrawerActions';
|
|||||||
import { useMemorizedColumnsWidths } from '@/hooks';
|
import { useMemorizedColumnsWidths } from '@/hooks';
|
||||||
import { useAccountTransactionsColumns, ActionsMenu } from './components';
|
import { useAccountTransactionsColumns, ActionsMenu } from './components';
|
||||||
import { useAccountTransactionsAllContext } from './AccountTransactionsAllBoot';
|
import { useAccountTransactionsAllContext } from './AccountTransactionsAllBoot';
|
||||||
|
import { useUnmatchMatchedUncategorizedTransaction } from '@/hooks/query/bank-rules';
|
||||||
import { handleCashFlowTransactionType } from './utils';
|
import { handleCashFlowTransactionType } from './utils';
|
||||||
|
|
||||||
import { compose } from '@/utils';
|
import { compose } from '@/utils';
|
||||||
|
import { useUncategorizeTransaction } from '@/hooks/query';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Account transactions data table.
|
* Account transactions data table.
|
||||||
@@ -43,14 +47,14 @@ function AccountTransactionsDataTable({
|
|||||||
const { cashflowTransactions, isCashFlowTransactionsLoading } =
|
const { cashflowTransactions, isCashFlowTransactionsLoading } =
|
||||||
useAccountTransactionsAllContext();
|
useAccountTransactionsAllContext();
|
||||||
|
|
||||||
|
const { mutateAsync: uncategorizeTransaction } = useUncategorizeTransaction();
|
||||||
|
const { mutateAsync: unmatchTransaction } =
|
||||||
|
useUnmatchMatchedUncategorizedTransaction();
|
||||||
|
|
||||||
// Local storage memorizing columns widths.
|
// Local storage memorizing columns widths.
|
||||||
const [initialColumnsWidths, , handleColumnResizing] =
|
const [initialColumnsWidths, , handleColumnResizing] =
|
||||||
useMemorizedColumnsWidths(TABLES.CASHFLOW_Transactions);
|
useMemorizedColumnsWidths(TABLES.CASHFLOW_Transactions);
|
||||||
|
|
||||||
// handle delete transaction
|
|
||||||
const handleDeleteTransaction = ({ reference_id }) => {
|
|
||||||
openAlert('account-transaction-delete', { referenceId: reference_id });
|
|
||||||
};
|
|
||||||
// Handle view details action.
|
// Handle view details action.
|
||||||
const handleViewDetailCashflowTransaction = (referenceType) => {
|
const handleViewDetailCashflowTransaction = (referenceType) => {
|
||||||
handleCashFlowTransactionType(referenceType, openDrawer);
|
handleCashFlowTransactionType(referenceType, openDrawer);
|
||||||
@@ -60,6 +64,38 @@ function AccountTransactionsDataTable({
|
|||||||
const referenceType = cell.row.original;
|
const referenceType = cell.row.original;
|
||||||
handleCashFlowTransactionType(referenceType, openDrawer);
|
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 (
|
return (
|
||||||
<CashflowTransactionsTable
|
<CashflowTransactionsTable
|
||||||
@@ -87,7 +123,8 @@ function AccountTransactionsDataTable({
|
|||||||
className="table-constrant"
|
className="table-constrant"
|
||||||
payload={{
|
payload={{
|
||||||
onViewDetails: handleViewDetailCashflowTransaction,
|
onViewDetails: handleViewDetailCashflowTransaction,
|
||||||
onDelete: handleDeleteTransaction,
|
onUncategorize: handleUncategorizeTransaction,
|
||||||
|
onUnmatch: handleUnmatchTransaction,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,19 +12,17 @@ import {
|
|||||||
AppToaster,
|
AppToaster,
|
||||||
} from '@/components';
|
} from '@/components';
|
||||||
import { TABLES } from '@/constants/tables';
|
import { TABLES } from '@/constants/tables';
|
||||||
|
import { ActionsMenu } from './UncategorizedTransactions/components';
|
||||||
|
|
||||||
import withSettings from '@/containers/Settings/withSettings';
|
import withSettings from '@/containers/Settings/withSettings';
|
||||||
import { withBankingActions } from '../withBankingActions';
|
import { withBankingActions } from '../withBankingActions';
|
||||||
|
|
||||||
import { useMemorizedColumnsWidths } from '@/hooks';
|
import { useMemorizedColumnsWidths } from '@/hooks';
|
||||||
import {
|
import { useAccountUncategorizedTransactionsColumns } from './components';
|
||||||
ActionsMenu,
|
|
||||||
useAccountUncategorizedTransactionsColumns,
|
|
||||||
} from './components';
|
|
||||||
import { useAccountUncategorizedTransactionsContext } from './AllTransactionsUncategorizedBoot';
|
import { useAccountUncategorizedTransactionsContext } from './AllTransactionsUncategorizedBoot';
|
||||||
|
import { useExcludeUncategorizedTransaction } from '@/hooks/query/bank-rules';
|
||||||
|
|
||||||
import { compose } from '@/utils';
|
import { compose } from '@/utils';
|
||||||
import { useExcludeUncategorizedTransaction } from '@/hooks/query/bank-rules';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Account transactions data table.
|
* 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,
|
Intent,
|
||||||
Menu,
|
Menu,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
MenuDivider,
|
|
||||||
Tag,
|
Tag,
|
||||||
Popover,
|
|
||||||
PopoverInteractionKind,
|
PopoverInteractionKind,
|
||||||
Position,
|
Position,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from '@blueprintjs/core';
|
} from '@blueprintjs/core';
|
||||||
import {
|
import { Box, FormatDateCell, Icon, MaterialProgressBar } from '@/components';
|
||||||
Box,
|
|
||||||
Can,
|
|
||||||
FormatDateCell,
|
|
||||||
Icon,
|
|
||||||
MaterialProgressBar,
|
|
||||||
} from '@/components';
|
|
||||||
import { useAccountTransactionsContext } from './AccountTransactionsProvider';
|
import { useAccountTransactionsContext } from './AccountTransactionsProvider';
|
||||||
import { safeCallback } from '@/utils';
|
import { safeCallback } from '@/utils';
|
||||||
|
|
||||||
export function ActionsMenu({
|
export function ActionsMenu({
|
||||||
payload: { onCategorize, onExclude },
|
payload: { onUncategorize, onUnmatch },
|
||||||
row: { original },
|
row: { original },
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Menu>
|
<Menu>
|
||||||
<MenuItem
|
{original.status === 'categorized' && (
|
||||||
icon={<Icon icon="reader-18" />}
|
<MenuItem
|
||||||
text={'Categorize'}
|
icon={<Icon icon="reader-18" />}
|
||||||
onClick={safeCallback(onCategorize, original)}
|
text={'Uncategorize'}
|
||||||
/>
|
onClick={safeCallback(onUncategorize, original)}
|
||||||
<MenuDivider />
|
/>
|
||||||
<MenuItem
|
)}
|
||||||
text={'Exclude'}
|
{original.status === 'matched' && (
|
||||||
onClick={safeCallback(onExclude, original)}
|
<MenuItem
|
||||||
icon={<Icon icon="disable" iconSize={16} />}
|
text={'Unmatch'}
|
||||||
/>
|
icon={<Icon icon="unlink" iconSize={16} />}
|
||||||
|
onClick={safeCallback(onUnmatch, original)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Menu>
|
</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.
|
* Retrieve account transctions table columns.
|
||||||
*/
|
*/
|
||||||
@@ -70,7 +82,7 @@ export function useAccountTransactionsColumns() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'transaction_number',
|
id: 'transaction_number',
|
||||||
Header: intl.get('transaction_number'),
|
Header: 'Transaction #',
|
||||||
accessor: 'transaction_number',
|
accessor: 'transaction_number',
|
||||||
width: 160,
|
width: 160,
|
||||||
className: 'transaction_number',
|
className: 'transaction_number',
|
||||||
@@ -79,13 +91,18 @@ export function useAccountTransactionsColumns() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'reference_number',
|
id: 'reference_number',
|
||||||
Header: intl.get('reference_no'),
|
Header: 'Ref.#',
|
||||||
accessor: 'reference_number',
|
accessor: 'reference_number',
|
||||||
width: 160,
|
width: 160,
|
||||||
className: 'reference_number',
|
className: 'reference_number',
|
||||||
clickable: true,
|
clickable: true,
|
||||||
textOverview: true,
|
textOverview: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'status',
|
||||||
|
Header: 'Status',
|
||||||
|
accessor: allTransactionsStatusAccessor,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'deposit',
|
id: 'deposit',
|
||||||
Header: intl.get('cash_flow.label.deposit'),
|
Header: intl.get('cash_flow.label.deposit'),
|
||||||
@@ -116,16 +133,6 @@ export function useAccountTransactionsColumns() {
|
|||||||
align: 'right',
|
align: 'right',
|
||||||
clickable: true,
|
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',
|
id: 'reference_number',
|
||||||
Header: intl.get('reference_no'),
|
Header: 'Ref.#',
|
||||||
accessor: 'reference_number',
|
accessor: 'reference_no',
|
||||||
width: 50,
|
width: 50,
|
||||||
className: 'reference_number',
|
|
||||||
clickable: true,
|
clickable: true,
|
||||||
textOverview: true,
|
textOverview: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
FFormGroup,
|
FFormGroup,
|
||||||
FInputGroup,
|
FInputGroup,
|
||||||
FTextArea,
|
FTextArea,
|
||||||
|
Icon,
|
||||||
} from '@/components';
|
} from '@/components';
|
||||||
import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot';
|
import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot';
|
||||||
import { CategorizeTransactionBranchField } from '../CategorizeTransactionBranchField';
|
import { CategorizeTransactionBranchField } from '../CategorizeTransactionBranchField';
|
||||||
@@ -21,7 +22,7 @@ export default function CategorizeTransactionOtherIncome() {
|
|||||||
popoverProps={{ position: Position.BOTTOM, minimal: true }}
|
popoverProps={{ position: Position.BOTTOM, minimal: true }}
|
||||||
formatDate={(date) => date.toLocaleDateString()}
|
formatDate={(date) => date.toLocaleDateString()}
|
||||||
parseDate={(str) => new Date(str)}
|
parseDate={(str) => new Date(str)}
|
||||||
inputProps={{ fill: true }}
|
inputProps={{ fill: true, leftElement: <Icon icon={'date-range'} /> }}
|
||||||
/>
|
/>
|
||||||
</FFormGroup>
|
</FFormGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
FFormGroup,
|
FFormGroup,
|
||||||
FInputGroup,
|
FInputGroup,
|
||||||
FTextArea,
|
FTextArea,
|
||||||
|
Icon,
|
||||||
} from '@/components';
|
} from '@/components';
|
||||||
import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot';
|
import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot';
|
||||||
import { CategorizeTransactionBranchField } from '../CategorizeTransactionBranchField';
|
import { CategorizeTransactionBranchField } from '../CategorizeTransactionBranchField';
|
||||||
@@ -21,7 +22,7 @@ export default function CategorizeTransactionOwnerContribution() {
|
|||||||
popoverProps={{ position: Position.BOTTOM, minimal: true }}
|
popoverProps={{ position: Position.BOTTOM, minimal: true }}
|
||||||
formatDate={(date) => date.toLocaleDateString()}
|
formatDate={(date) => date.toLocaleDateString()}
|
||||||
parseDate={(str) => new Date(str)}
|
parseDate={(str) => new Date(str)}
|
||||||
inputProps={{ fill: true }}
|
inputProps={{ fill: true, leftElement: <Icon icon={'date-range'} /> }}
|
||||||
/>
|
/>
|
||||||
</FFormGroup>
|
</FFormGroup>
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user