mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 13:20:31 +00:00
Compare commits
35 Commits
reconcile-
...
advanced-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0e227ff28 | ||
|
|
b590d2cb03 | ||
|
|
daf1cd38c0 | ||
|
|
3e2997d745 | ||
|
|
f3af3843dd | ||
|
|
b68d180785 | ||
|
|
341d47cc7b | ||
|
|
5c3a371e8a | ||
|
|
1141991e44 | ||
|
|
8cd3a6c48d | ||
|
|
fe214b1b2d | ||
|
|
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.
|
||||
|
||||
## [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
|
||||
|
||||
* 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(),
|
||||
|
||||
// Assign
|
||||
body('assign_category')
|
||||
.isString()
|
||||
.isIn([
|
||||
'interest_income',
|
||||
'other_income',
|
||||
'deposit',
|
||||
'expense',
|
||||
'owner_drawings',
|
||||
]),
|
||||
body('assign_category').isString(),
|
||||
body('assign_account_id').isInt({ min: 0 }),
|
||||
body('assign_payee').isString().optional({ nullable: true }),
|
||||
body('assign_memo').isString().optional({ nullable: true }),
|
||||
|
||||
@@ -111,6 +111,7 @@ export default class BillsPayments extends BaseController {
|
||||
check('vendor_id').exists().isNumeric().toInt(),
|
||||
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
|
||||
|
||||
check('amount').exists().isNumeric().toFloat(),
|
||||
check('payment_account_id').exists().isNumeric().toInt(),
|
||||
check('payment_number').optional({ nullable: true }).trim().escape(),
|
||||
check('payment_date').exists(),
|
||||
@@ -118,13 +119,15 @@ export default class BillsPayments extends BaseController {
|
||||
check('reference').optional().trim().escape(),
|
||||
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.*.bill_id').exists().isNumeric().toInt(),
|
||||
check('entries.*.payment_amount').exists().isNumeric().toFloat(),
|
||||
|
||||
check('attachments').isArray().optional(),
|
||||
check('attachments.*.key').exists().isString(),
|
||||
|
||||
check('prepard_expenses_account_id').optional().isNumeric().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -151,6 +151,8 @@ export default class PaymentReceivesController extends BaseController {
|
||||
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
|
||||
|
||||
check('payment_date').exists(),
|
||||
check('amount').exists().isNumeric().toFloat(),
|
||||
|
||||
check('reference_no').optional(),
|
||||
check('deposit_account_id').exists().isNumeric().toInt(),
|
||||
check('payment_receive_no').optional({ nullable: true }).trim().escape(),
|
||||
@@ -158,7 +160,7 @@ export default class PaymentReceivesController extends BaseController {
|
||||
|
||||
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.*.index').optional().isNumeric().toInt(),
|
||||
@@ -167,6 +169,11 @@ export default class PaymentReceivesController extends BaseController {
|
||||
|
||||
check('attachments').isArray().optional(),
|
||||
check('attachments.*.key').exists().isString(),
|
||||
|
||||
check('unearned_revenue_account_id')
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
.toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
export const CashflowTransactionTypes = {
|
||||
OtherIncome: 'Other income',
|
||||
OtherExpense: 'Other expense',
|
||||
OwnerDrawing: 'Owner drawing',
|
||||
OwnerContribution: 'Owner contribution',
|
||||
TransferToAccount: 'Transfer to account',
|
||||
TransferFromAccount: 'Transfer from account',
|
||||
};
|
||||
|
||||
export const TransactionTypes = {
|
||||
SaleInvoice: 'Sale invoice',
|
||||
SaleReceipt: 'Sale receipt',
|
||||
PaymentReceive: 'Payment receive',
|
||||
PaymentReceive: 'Payment received',
|
||||
Bill: 'Bill',
|
||||
BillPayment: 'Payment made',
|
||||
VendorOpeningBalance: 'Vendor opening balance',
|
||||
@@ -17,12 +26,10 @@ export const TransactionTypes = {
|
||||
OtherExpense: 'Other expense',
|
||||
OwnerDrawing: 'Owner drawing',
|
||||
InvoiceWriteOff: 'Invoice write-off',
|
||||
|
||||
CreditNote: 'transaction_type.credit_note',
|
||||
VendorCredit: 'transaction_type.vendor_credit',
|
||||
|
||||
RefundCreditNote: 'transaction_type.refund_credit_note',
|
||||
RefundVendorCredit: 'transaction_type.refund_vendor_credit',
|
||||
|
||||
LandedCost: 'transaction_type.landed_cost',
|
||||
CashflowTransaction: CashflowTransactionTypes,
|
||||
};
|
||||
|
||||
@@ -5,7 +5,8 @@ exports.up = function (knex) {
|
||||
.integer('uncategorized_transaction_id')
|
||||
.unsigned()
|
||||
.references('id')
|
||||
.inTable('uncategorized_cashflow_transactions');
|
||||
.inTable('uncategorized_cashflow_transactions')
|
||||
.withKeyName('recognizedBankTransactionsUncategorizedTransIdForeign');
|
||||
table
|
||||
.integer('bank_rule_id')
|
||||
.unsigned()
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
exports.up = function (knex) {
|
||||
return knex.schema.table('uncategorized_cashflow_transactions', (table) => {
|
||||
table.integer('recognized_transaction_id').unsigned();
|
||||
table
|
||||
.integer('recognized_transaction_id')
|
||||
.unsigned()
|
||||
.references('id')
|
||||
.inTable('recognized_bank_transactions')
|
||||
.withKeyName('uncategorizedCashflowTransRecognizedTranIdForeign');
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
exports.up = function (knex) {
|
||||
return knex.schema.createTable('matched_bank_transactions', (table) => {
|
||||
table.increments('id');
|
||||
table.integer('uncategorized_transaction_id').unsigned();
|
||||
table
|
||||
.integer('uncategorized_transaction_id')
|
||||
.unsigned()
|
||||
.references('id')
|
||||
.inTable('uncategorized_cashflow_transactions');
|
||||
table.string('reference_type');
|
||||
table.integer('reference_id').unsigned();
|
||||
table.decimal('amount');
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
exports.up = function (knex) {
|
||||
return knex('accounts_transactions')
|
||||
.whereIn('referenceType', [
|
||||
'OtherIncome',
|
||||
'OtherExpense',
|
||||
'OwnerDrawing',
|
||||
'OwnerContribution',
|
||||
'TransferToAccount',
|
||||
'TransferFromAccount',
|
||||
])
|
||||
.update({
|
||||
transactionType: knex.raw(`
|
||||
CASE
|
||||
WHEN REFERENCE_TYPE = 'OtherIncome' THEN 'OtherIncome'
|
||||
WHEN REFERENCE_TYPE = 'OtherExpense' THEN 'OtherExpense'
|
||||
WHEN REFERENCE_TYPE = 'OwnerDrawing' THEN 'OwnerDrawing'
|
||||
WHEN REFERENCE_TYPE = 'OwnerContribution' THEN 'OwnerContribution'
|
||||
WHEN REFERENCE_TYPE = 'TransferToAccount' THEN 'TransferToAccount'
|
||||
WHEN REFERENCE_TYPE = 'TransferFromAccount' THEN 'TransferFromAccount'
|
||||
END
|
||||
`),
|
||||
referenceType: knex.raw(`
|
||||
CASE
|
||||
WHEN REFERENCE_TYPE IN ('OtherIncome', 'OtherExpense', 'OwnerDrawing', 'OwnerContribution', 'TransferToAccount', 'TransferFromAccount') THEN 'CashflowTransaction'
|
||||
ELSE REFERENCE_TYPE
|
||||
END
|
||||
`),
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function (knex) {
|
||||
return knex('accounts_transactions')
|
||||
.whereIn('transactionType', [
|
||||
'OtherIncome',
|
||||
'OtherExpense',
|
||||
'OwnerDrawing',
|
||||
'OwnerContribution',
|
||||
'TransferToAccount',
|
||||
'TransferFromAccount',
|
||||
])
|
||||
.update({
|
||||
referenceType: knex.raw(`
|
||||
CASE
|
||||
WHEN TRANSACTION_TYPE = 'OtherIncome' THEN 'OtherIncome'
|
||||
WHEN TRANSACTION_TYPE = 'OtherExpense' THEN 'OtherExpense'
|
||||
WHEN TRANSACTION_TYPE = 'OwnerDrawing' THEN 'OwnerDrawing'
|
||||
WHEN TRANSACTION_TYPE = 'OwnerContribution' THEN 'OwnerContribution'
|
||||
WHEN TRANSACTION_TYPE = 'TransferToAccount' THEN 'TransferToAccount'
|
||||
WHEN TRANSACTION_TYPE = 'TransferFromAccount' THEN 'TransferFromAccount'
|
||||
ELSE REFERENCE_TYPE
|
||||
END
|
||||
`),
|
||||
transactionType: knex.raw(`
|
||||
CASE
|
||||
WHEN TRANSACTION_TYPE IN ('OtherIncome', 'OtherExpense', 'OwnerDrawing', 'OwnerContribution', 'TransferToAccount', 'TransferFromAccount') THEN NULL
|
||||
ELSE TRANSACTION_TYPE
|
||||
END
|
||||
`),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
exports.up = function (knex) {
|
||||
return knex.schema.table('payment_receives', (table) => {
|
||||
table.decimal('applied_amount', 13, 3).defaultTo(0);
|
||||
table
|
||||
.integer('unearned_revenue_account_id')
|
||||
.unsigned()
|
||||
.references('id')
|
||||
.inTable('accounts');
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function (knex) {
|
||||
return knex.schema.table('payment_receives', (table) => {
|
||||
table.dropColumn('applied_amount');
|
||||
table.dropColumn('unearned_revenue_account_id');
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
exports.up = function (knex) {
|
||||
return knex.schema.table('bills_payments', (table) => {
|
||||
table.decimal('applied_amount', 13, 3).defaultTo(0);
|
||||
table
|
||||
.integer('prepard_expenses_account_id')
|
||||
.unsigned()
|
||||
.references('id')
|
||||
.inTable('accounts');
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function (knex) {
|
||||
return knex.schema.table('bills_payments', (table) => {
|
||||
table.dropColumn('applied_amount');
|
||||
table.dropColumn('prepard_expenses_account_id');
|
||||
});
|
||||
};
|
||||
@@ -12,8 +12,7 @@ export default class SeedAccounts extends TenantSeeder {
|
||||
description: this.i18n.__(account.description),
|
||||
currencyCode: this.tenant.metadata.baseCurrency,
|
||||
seededAt: new Date(),
|
||||
})
|
||||
);
|
||||
}));
|
||||
return knex('accounts').then(async () => {
|
||||
// Inserts seed entries.
|
||||
return knex('accounts').insert(data);
|
||||
|
||||
@@ -9,6 +9,28 @@ export const TaxPayableAccount = {
|
||||
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 [
|
||||
{
|
||||
name: 'Bank Account',
|
||||
@@ -323,4 +345,6 @@ export default [
|
||||
index: 1,
|
||||
predefined: 0,
|
||||
},
|
||||
UnearnedRevenueAccount,
|
||||
PrepardExpenses,
|
||||
];
|
||||
|
||||
@@ -66,7 +66,9 @@ export interface IAccountTransaction {
|
||||
referenceId: number;
|
||||
|
||||
referenceNumber?: string;
|
||||
|
||||
transactionNumber?: string;
|
||||
transactionType?: string;
|
||||
|
||||
note?: string;
|
||||
|
||||
|
||||
@@ -166,3 +166,10 @@ export interface IBillOpenedPayload {
|
||||
oldBill: IBill;
|
||||
tenantId: number;
|
||||
}
|
||||
|
||||
|
||||
export interface IBillPrepardExpensesAppliedEventPayload {
|
||||
tenantId: number;
|
||||
billId: number;
|
||||
trx?: Knex.Transaction;
|
||||
}
|
||||
|
||||
@@ -29,6 +29,9 @@ export interface IBillPayment {
|
||||
|
||||
localAmount?: number;
|
||||
branchId?: number;
|
||||
|
||||
prepardExpensesAccountId?: number;
|
||||
isPrepardExpense: boolean;
|
||||
}
|
||||
|
||||
export interface IBillPaymentEntryDTO {
|
||||
@@ -38,6 +41,7 @@ export interface IBillPaymentEntryDTO {
|
||||
|
||||
export interface IBillPaymentDTO {
|
||||
vendorId: number;
|
||||
amount: number;
|
||||
paymentAccountId: number;
|
||||
paymentNumber?: string;
|
||||
paymentDate: Date;
|
||||
@@ -47,6 +51,7 @@ export interface IBillPaymentDTO {
|
||||
entries: IBillPaymentEntryDTO[];
|
||||
branchId?: number;
|
||||
attachments?: AttachmentLinkDTO[];
|
||||
prepardExpensesAccountId?: number;
|
||||
}
|
||||
|
||||
export interface IBillReceivePageEntry {
|
||||
@@ -119,3 +124,11 @@ export enum IPaymentMadeAction {
|
||||
Delete = 'Delete',
|
||||
View = 'View',
|
||||
}
|
||||
|
||||
export interface IPaymentPrepardExpensesAppliedEventPayload {
|
||||
tenantId: number;
|
||||
billPaymentId: number;
|
||||
billId: number;
|
||||
appliedAmount: number;
|
||||
trx?: Knex.Transaction;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Knex } from 'knex';
|
||||
import {
|
||||
IFinancialSheetCommonMeta,
|
||||
INumberFormatQuery,
|
||||
@@ -257,7 +258,6 @@ export interface IUncategorizedCashflowTransaction {
|
||||
categorized: boolean;
|
||||
}
|
||||
|
||||
|
||||
export interface CreateUncategorizedTransactionDTO {
|
||||
date: Date | string;
|
||||
accountId: number;
|
||||
@@ -269,3 +269,16 @@ export interface CreateUncategorizedTransactionDTO {
|
||||
plaidTransactionId?: string | null;
|
||||
batch?: string;
|
||||
}
|
||||
|
||||
export interface IUncategorizedTransactionCreatingEventPayload {
|
||||
tenantId: number;
|
||||
createUncategorizedTransactionDTO: CreateUncategorizedTransactionDTO;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface IUncategorizedTransactionCreatedEventPayload {
|
||||
tenantId: number;
|
||||
uncategorizedTransaction: any;
|
||||
createUncategorizedTransactionDTO: CreateUncategorizedTransactionDTO;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
@@ -130,8 +130,9 @@ export interface ICommandCashflowDeletedPayload {
|
||||
|
||||
export interface ICashflowTransactionCategorizedPayload {
|
||||
tenantId: number;
|
||||
cashflowTransactionId: number;
|
||||
uncategorizedTransaction: any;
|
||||
cashflowTransaction: ICashflowTransaction;
|
||||
categorizeDTO: any;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
export interface ICashflowTransactionUncategorizingPayload {
|
||||
|
||||
@@ -29,4 +29,9 @@ export interface ICashflowAccountTransaction {
|
||||
|
||||
date: Date;
|
||||
formattedDate: string;
|
||||
|
||||
status: string;
|
||||
formattedStatus: string;
|
||||
|
||||
uncategorizedTransactionId: number;
|
||||
}
|
||||
|
||||
8
packages/server/src/interfaces/Import.ts
Normal file
8
packages/server/src/interfaces/Import.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { ImportFilePreviewPOJO } from "@/services/Import/interfaces";
|
||||
|
||||
|
||||
export interface IImportFileCommitedEventPayload {
|
||||
tenantId: number;
|
||||
importId: number;
|
||||
meta: ImportFilePreviewPOJO;
|
||||
}
|
||||
@@ -40,6 +40,8 @@ export interface ILedgerEntry {
|
||||
date: Date | string;
|
||||
|
||||
transactionType: string;
|
||||
transactionSubType?: string;
|
||||
|
||||
transactionId: number;
|
||||
|
||||
transactionNumber?: string;
|
||||
|
||||
@@ -25,8 +25,13 @@ export interface IPaymentReceive {
|
||||
updatedAt: Date;
|
||||
localAmount?: number;
|
||||
branchId?: number;
|
||||
unearnedRevenueAccountId?: number;
|
||||
}
|
||||
export interface IPaymentReceiveCreateDTO {
|
||||
|
||||
interface IPaymentReceivedCommonDTO {
|
||||
unearnedRevenueAccountId?: number;
|
||||
}
|
||||
export interface IPaymentReceiveCreateDTO extends IPaymentReceivedCommonDTO {
|
||||
customerId: number;
|
||||
paymentDate: Date;
|
||||
amount: number;
|
||||
@@ -41,7 +46,7 @@ export interface IPaymentReceiveCreateDTO {
|
||||
attachments?: AttachmentLinkDTO[];
|
||||
}
|
||||
|
||||
export interface IPaymentReceiveEditDTO {
|
||||
export interface IPaymentReceiveEditDTO extends IPaymentReceivedCommonDTO {
|
||||
customerId: number;
|
||||
paymentDate: Date;
|
||||
amount: number;
|
||||
@@ -184,3 +189,11 @@ export interface PaymentReceiveMailPresendEvent {
|
||||
paymentReceiveId: number;
|
||||
messageOptions: PaymentReceiveMailOptsDTO;
|
||||
}
|
||||
|
||||
export interface PaymentReceiveUnearnedRevenueAppliedEventPayload {
|
||||
tenantId: number;
|
||||
paymentReceiveId: number;
|
||||
saleInvoiceId: number;
|
||||
appliedAmount: number;
|
||||
trx?: Knex.Transaction;
|
||||
}
|
||||
|
||||
@@ -216,3 +216,9 @@ export interface ISaleInvoiceMailSent {
|
||||
saleInvoiceId: number;
|
||||
messageOptions: SendInvoiceMailDTO;
|
||||
}
|
||||
|
||||
export interface SaleInvoiceAppliedUnearnedRevenueOnCreatedEventPayload {
|
||||
tenantId: number;
|
||||
saleInvoiceId: number;
|
||||
trx?: Knex.Transaction;
|
||||
}
|
||||
|
||||
@@ -110,6 +110,11 @@ import { ValidateMatchingOnPaymentMadeDelete } from '@/services/Banking/Matching
|
||||
import { ValidateMatchingOnCashflowDelete } from '@/services/Banking/Matching/events/ValidateMatchingOnCashflowDelete';
|
||||
import { RecognizeSyncedBankTranasctions } from '@/services/Banking/Plaid/subscribers/RecognizeSyncedBankTransactions';
|
||||
import { UnlinkBankRuleOnDeleteBankRule } from '@/services/Banking/Rules/events/UnlinkBankRuleOnDeleteBankRule';
|
||||
import { DecrementUncategorizedTransactionOnMatching } from '@/services/Banking/Matching/events/DecrementUncategorizedTransactionsOnMatch';
|
||||
import { DecrementUncategorizedTransactionOnExclude } from '@/services/Banking/Exclude/events/DecrementUncategorizedTransactionOnExclude';
|
||||
import { DecrementUncategorizedTransactionOnCategorize } from '@/services/Cashflow/subscribers/DecrementUncategorizedTransactionOnCategorize';
|
||||
import { AutoApplyUnearnedRevenueOnInvoiceCreated } from '@/services/Sales/PaymentReceives/events/AutoApplyUnearnedRevenueOnInvoiceCreated';
|
||||
import { AutoApplyPrepardExpensesOnBillCreated } from '@/services/Purchases/Bills/events/AutoApplyPrepardExpensesOnBillCreated';
|
||||
|
||||
export default () => {
|
||||
return new EventPublisher();
|
||||
@@ -258,6 +263,9 @@ export const susbcribers = () => {
|
||||
// Bank Rules
|
||||
TriggerRecognizedTransactions,
|
||||
UnlinkBankRuleOnDeleteBankRule,
|
||||
DecrementUncategorizedTransactionOnMatching,
|
||||
DecrementUncategorizedTransactionOnExclude,
|
||||
DecrementUncategorizedTransactionOnCategorize,
|
||||
|
||||
// Validate matching
|
||||
ValidateMatchingOnCashflowDelete,
|
||||
@@ -266,7 +274,7 @@ export const susbcribers = () => {
|
||||
ValidateMatchingOnPaymentReceivedDelete,
|
||||
ValidateMatchingOnPaymentMadeDelete,
|
||||
|
||||
// Plaid
|
||||
// Plaid
|
||||
RecognizeSyncedBankTranasctions,
|
||||
];
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ export default class AccountTransaction extends TenantModel {
|
||||
debit: number;
|
||||
exchangeRate: number;
|
||||
taxRate: number;
|
||||
transactionType: string;
|
||||
|
||||
/**
|
||||
* Table name
|
||||
@@ -53,7 +54,7 @@ export default class AccountTransaction extends TenantModel {
|
||||
* @return {string}
|
||||
*/
|
||||
get referenceTypeFormatted() {
|
||||
return getTransactionTypeLabel(this.referenceType);
|
||||
return getTransactionTypeLabel(this.referenceType, this.transactionType);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -525,9 +525,9 @@ export default class Bill extends mixin(TenantModel, [
|
||||
return notFoundBillsIds;
|
||||
}
|
||||
|
||||
static changePaymentAmount(billId, amount) {
|
||||
static changePaymentAmount(billId, amount, trx) {
|
||||
const changeMethod = amount > 0 ? 'increment' : 'decrement';
|
||||
return this.query()
|
||||
return this.query(trx)
|
||||
.where('id', billId)
|
||||
[changeMethod]('payment_amount', Math.abs(amount));
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ export default class BillPayment extends mixin(TenantModel, [
|
||||
CustomViewBaseModel,
|
||||
ModelSearchable,
|
||||
]) {
|
||||
prepardExpensesAccountId: number;
|
||||
|
||||
/**
|
||||
* Table name
|
||||
*/
|
||||
@@ -47,6 +49,14 @@ export default class BillPayment extends mixin(TenantModel, [
|
||||
return BillPaymentSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detarmines whether the payment is prepard expense.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
get isPrepardExpense() {
|
||||
return !!this.prepardExpensesAccountId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship mapping.
|
||||
*/
|
||||
|
||||
@@ -5,9 +5,9 @@ import {
|
||||
getCashflowAccountTransactionsTypes,
|
||||
getCashflowTransactionType,
|
||||
} from '@/services/Cashflow/utils';
|
||||
import AccountTransaction from './AccountTransaction';
|
||||
import { CASHFLOW_DIRECTION } from '@/services/Cashflow/constants';
|
||||
import { getTransactionTypeLabel } from '@/utils/transactions-types';
|
||||
import { getCashflowTransactionFormattedType } from '@/utils/transactions-types';
|
||||
|
||||
export default class CashflowTransaction extends TenantModel {
|
||||
transactionType: string;
|
||||
amount: number;
|
||||
@@ -64,7 +64,7 @@ export default class CashflowTransaction extends TenantModel {
|
||||
* @returns {string}
|
||||
*/
|
||||
get transactionTypeFormatted() {
|
||||
return getTransactionTypeLabel(this.transactionType);
|
||||
return getCashflowTransactionFormattedType(this.transactionType);
|
||||
}
|
||||
|
||||
get typeMeta() {
|
||||
@@ -95,6 +95,34 @@ export default class CashflowTransaction extends TenantModel {
|
||||
return !!this.uncategorizedTransaction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Model modifiers.
|
||||
*/
|
||||
static get modifiers() {
|
||||
return {
|
||||
/**
|
||||
* Filter the published transactions.
|
||||
*/
|
||||
published(query) {
|
||||
query.whereNot('published_at', null);
|
||||
},
|
||||
|
||||
/**
|
||||
* Filter the not categorized transactions.
|
||||
*/
|
||||
notCategorized(query) {
|
||||
query.whereNull('cashflowTransactions.uncategorizedTransactionId');
|
||||
},
|
||||
|
||||
/**
|
||||
* Filter the categorized transactions.
|
||||
*/
|
||||
categorized(query) {
|
||||
query.whereNotNull('cashflowTransactions.uncategorizedTransactionId');
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship mapping.
|
||||
*/
|
||||
@@ -131,8 +159,7 @@ export default class CashflowTransaction extends TenantModel {
|
||||
to: 'accounts_transactions.referenceId',
|
||||
},
|
||||
filter(builder) {
|
||||
const referenceTypes = getCashflowAccountTransactionsTypes();
|
||||
builder.whereIn('reference_type', referenceTypes);
|
||||
builder.where('reference_type', 'CashflowTransaction');
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -105,8 +105,34 @@ export default class UncategorizedCashflowTransaction extends mixin(
|
||||
* Filters the excluded transactions.
|
||||
*/
|
||||
excluded(query) {
|
||||
query.whereNotNull('excluded_at')
|
||||
}
|
||||
query.whereNotNull('excluded_at');
|
||||
},
|
||||
|
||||
/**
|
||||
* Filter out the recognized transactions.
|
||||
* @param query
|
||||
*/
|
||||
recognized(query) {
|
||||
query.whereNotNull('recognizedTransactionId');
|
||||
},
|
||||
|
||||
/**
|
||||
* Filter out the not recognized transactions.
|
||||
* @param query
|
||||
*/
|
||||
notRecognized(query) {
|
||||
query.whereNull('recognizedTransactionId');
|
||||
},
|
||||
|
||||
categorized(query) {
|
||||
query.whereNotNull('categorizeRefType');
|
||||
query.whereNotNull('categorizeRefId');
|
||||
},
|
||||
|
||||
notCategorized(query) {
|
||||
query.whereNull('categorizeRefType');
|
||||
query.whereNull('categorizeRefId');
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -158,56 +184,4 @@ export default class UncategorizedCashflowTransaction extends mixin(
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the count of uncategorized transactions for the associated account
|
||||
* based on the specified operation.
|
||||
* @param {QueryContext} queryContext - The query context for the transaction.
|
||||
* @param {boolean} increment - Indicates whether to increment or decrement the count.
|
||||
*/
|
||||
private async updateUncategorizedTransactionCount(
|
||||
queryContext: QueryContext,
|
||||
increment: boolean,
|
||||
amount: number = 1
|
||||
) {
|
||||
const operation = increment ? 'increment' : 'decrement';
|
||||
|
||||
await Account.query(queryContext.transaction)
|
||||
.findById(this.accountId)
|
||||
[operation]('uncategorized_transactions', amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs after insert.
|
||||
* @param {QueryContext} queryContext
|
||||
*/
|
||||
public async $afterInsert(queryContext) {
|
||||
await super.$afterInsert(queryContext);
|
||||
await this.updateUncategorizedTransactionCount(queryContext, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs after update.
|
||||
* @param {ModelOptions} opt
|
||||
* @param {QueryContext} queryContext
|
||||
*/
|
||||
public async $afterUpdate(
|
||||
opt: ModelOptions,
|
||||
queryContext: QueryContext
|
||||
): Promise<any> {
|
||||
await super.$afterUpdate(opt, queryContext);
|
||||
|
||||
if (this.id && this.categorized) {
|
||||
await this.updateUncategorizedTransactionCount(queryContext, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs after delete.
|
||||
* @param {QueryContext} queryContext
|
||||
*/
|
||||
public async $afterDelete(queryContext: QueryContext) {
|
||||
await super.$afterDelete(queryContext);
|
||||
await this.updateUncategorizedTransactionCount(queryContext, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,12 @@ import { Account } from 'models';
|
||||
import TenantRepository from '@/repositories/TenantRepository';
|
||||
import { IAccount } from '@/interfaces';
|
||||
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 {
|
||||
/**
|
||||
@@ -179,4 +184,67 @@ export default class AccountRepository extends TenantRepository {
|
||||
}
|
||||
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 {
|
||||
repositoryName: string;
|
||||
|
||||
tenantId: number;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {number} tenantId
|
||||
* @param {number} tenantId
|
||||
*/
|
||||
constructor(knex, cache, i18n) {
|
||||
super(knex, cache, i18n);
|
||||
}
|
||||
}
|
||||
|
||||
setTenantId(tenantId: number) {
|
||||
this.tenantId = tenantId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ export const transformLedgerEntryToTransaction = (
|
||||
referenceId: entry.transactionId,
|
||||
|
||||
transactionNumber: entry.transactionNumber,
|
||||
transactionType: entry.transactionSubType,
|
||||
|
||||
referenceNumber: entry.referenceNumber,
|
||||
|
||||
note: entry.note,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { Server } from 'socket.io';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { initialize } from 'objection';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
|
||||
@Service()
|
||||
export class GetBankAccountSummary {
|
||||
@@ -14,22 +14,43 @@ export class GetBankAccountSummary {
|
||||
* @returns
|
||||
*/
|
||||
public async getBankAccountSummary(tenantId: number, bankAccountId: number) {
|
||||
const knex = this.tenancy.knex(tenantId);
|
||||
const {
|
||||
Account,
|
||||
UncategorizedCashflowTransaction,
|
||||
RecognizedBankTransaction,
|
||||
MatchedBankTransaction,
|
||||
} = this.tenancy.models(tenantId);
|
||||
|
||||
await initialize(knex, [
|
||||
UncategorizedCashflowTransaction,
|
||||
RecognizedBankTransaction,
|
||||
MatchedBankTransaction,
|
||||
]);
|
||||
const bankAccount = await Account.query()
|
||||
.findById(bankAccountId)
|
||||
.throwIfNotFound();
|
||||
|
||||
// Retrieves the uncategorized transactions count of the given bank account.
|
||||
const uncategorizedTranasctionsCount =
|
||||
await UncategorizedCashflowTransaction.query()
|
||||
.where('accountId', bankAccountId)
|
||||
.count('id as total')
|
||||
.first();
|
||||
await UncategorizedCashflowTransaction.query().onBuild((q) => {
|
||||
// Include just the given account.
|
||||
q.where('accountId', bankAccountId);
|
||||
|
||||
// Only the not excluded.
|
||||
q.modify('notExcluded');
|
||||
|
||||
// Only the not categorized.
|
||||
q.modify('notCategorized');
|
||||
|
||||
// Only the not matched bank transactions.
|
||||
q.withGraphJoined('matchedBankTransactions');
|
||||
q.whereNull('matchedBankTransactions.id');
|
||||
|
||||
// Count the results.
|
||||
q.count('uncategorized_cashflow_transactions.id as total');
|
||||
q.first();
|
||||
});
|
||||
|
||||
// Retrieves the recognized transactions count of the given bank account.
|
||||
const recognizedTransactionsCount = await RecognizedBankTransaction.query()
|
||||
@@ -43,8 +64,8 @@ export class GetBankAccountSummary {
|
||||
.first();
|
||||
|
||||
const totalUncategorizedTransactions =
|
||||
uncategorizedTranasctionsCount?.total;
|
||||
const totalRecognizedTransactions = recognizedTransactionsCount?.total;
|
||||
uncategorizedTranasctionsCount?.total || 0;
|
||||
const totalRecognizedTransactions = recognizedTransactionsCount?.total || 0;
|
||||
|
||||
return {
|
||||
name: bankAccount.name,
|
||||
|
||||
@@ -2,6 +2,12 @@ import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import UnitOfWork from '@/services/UnitOfWork';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { validateTransactionNotCategorized } from './utils';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import events from '@/subscribers/events';
|
||||
import {
|
||||
IBankTransactionUnexcludedEventPayload,
|
||||
IBankTransactionUnexcludingEventPayload,
|
||||
} from './_types';
|
||||
|
||||
@Service()
|
||||
export class ExcludeBankTransaction {
|
||||
@@ -11,6 +17,9 @@ export class ExcludeBankTransaction {
|
||||
@Inject()
|
||||
private uow: UnitOfWork;
|
||||
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
/**
|
||||
* Marks the given bank transaction as excluded.
|
||||
* @param {number} tenantId
|
||||
@@ -31,11 +40,23 @@ export class ExcludeBankTransaction {
|
||||
validateTransactionNotCategorized(oldUncategorizedTransaction);
|
||||
|
||||
return this.uow.withTransaction(tenantId, async (trx) => {
|
||||
await this.eventPublisher.emitAsync(events.bankTransactions.onExcluding, {
|
||||
tenantId,
|
||||
uncategorizedTransactionId,
|
||||
trx,
|
||||
} as IBankTransactionUnexcludingEventPayload);
|
||||
|
||||
await UncategorizedCashflowTransaction.query(trx)
|
||||
.findById(uncategorizedTransactionId)
|
||||
.patch({
|
||||
excludedAt: new Date(),
|
||||
});
|
||||
|
||||
await this.eventPublisher.emitAsync(events.bankTransactions.onExcluded, {
|
||||
tenantId,
|
||||
uncategorizedTransactionId,
|
||||
trx,
|
||||
} as IBankTransactionUnexcludedEventPayload);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,12 @@ import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import UnitOfWork from '@/services/UnitOfWork';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { validateTransactionNotCategorized } from './utils';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import events from '@/subscribers/events';
|
||||
import {
|
||||
IBankTransactionExcludedEventPayload,
|
||||
IBankTransactionExcludingEventPayload,
|
||||
} from './_types';
|
||||
|
||||
@Service()
|
||||
export class UnexcludeBankTransaction {
|
||||
@@ -11,6 +17,9 @@ export class UnexcludeBankTransaction {
|
||||
@Inject()
|
||||
private uow: UnitOfWork;
|
||||
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
/**
|
||||
* Marks the given bank transaction as excluded.
|
||||
* @param {number} tenantId
|
||||
@@ -20,7 +29,7 @@ export class UnexcludeBankTransaction {
|
||||
public async unexcludeBankTransaction(
|
||||
tenantId: number,
|
||||
uncategorizedTransactionId: number
|
||||
) {
|
||||
): Promise<void> {
|
||||
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
|
||||
|
||||
const oldUncategorizedTransaction =
|
||||
@@ -31,11 +40,27 @@ export class UnexcludeBankTransaction {
|
||||
validateTransactionNotCategorized(oldUncategorizedTransaction);
|
||||
|
||||
return this.uow.withTransaction(tenantId, async (trx) => {
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.bankTransactions.onUnexcluding,
|
||||
{
|
||||
tenantId,
|
||||
uncategorizedTransactionId,
|
||||
} as IBankTransactionExcludingEventPayload
|
||||
);
|
||||
|
||||
await UncategorizedCashflowTransaction.query(trx)
|
||||
.findById(uncategorizedTransactionId)
|
||||
.patch({
|
||||
excludedAt: null,
|
||||
});
|
||||
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.bankTransactions.onUnexcluded,
|
||||
{
|
||||
tenantId,
|
||||
uncategorizedTransactionId,
|
||||
} as IBankTransactionExcludedEventPayload
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,30 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
export interface ExcludedBankTransactionsQuery {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
accountId?: number;
|
||||
}
|
||||
}
|
||||
|
||||
export interface IBankTransactionUnexcludingEventPayload {
|
||||
tenantId: number;
|
||||
uncategorizedTransactionId: number;
|
||||
trx?: Knex.Transaction
|
||||
}
|
||||
|
||||
export interface IBankTransactionUnexcludedEventPayload {
|
||||
tenantId: number;
|
||||
uncategorizedTransactionId: number;
|
||||
trx?: Knex.Transaction
|
||||
}
|
||||
|
||||
export interface IBankTransactionExcludingEventPayload {
|
||||
tenantId: number;
|
||||
uncategorizedTransactionId: number;
|
||||
trx?: Knex.Transaction
|
||||
}
|
||||
export interface IBankTransactionExcludedEventPayload {
|
||||
tenantId: number;
|
||||
uncategorizedTransactionId: number;
|
||||
trx?: Knex.Transaction
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import events from '@/subscribers/events';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import {
|
||||
IBankTransactionExcludedEventPayload,
|
||||
IBankTransactionUnexcludedEventPayload,
|
||||
} from '../_types';
|
||||
|
||||
@Service()
|
||||
export class DecrementUncategorizedTransactionOnExclude {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
/**
|
||||
* Constructor method.
|
||||
*/
|
||||
public attach(bus) {
|
||||
bus.subscribe(
|
||||
events.bankTransactions.onExcluded,
|
||||
this.decrementUnCategorizedTransactionsOnExclude.bind(this)
|
||||
);
|
||||
bus.subscribe(
|
||||
events.bankTransactions.onUnexcluded,
|
||||
this.incrementUnCategorizedTransactionsOnUnexclude.bind(this)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the cashflow transaction whether matched with bank transaction on deleting.
|
||||
* @param {IManualJournalDeletingPayload}
|
||||
*/
|
||||
public async decrementUnCategorizedTransactionsOnExclude({
|
||||
tenantId,
|
||||
uncategorizedTransactionId,
|
||||
trx,
|
||||
}: IBankTransactionExcludedEventPayload) {
|
||||
const { UncategorizedCashflowTransaction, Account } =
|
||||
this.tenancy.models(tenantId);
|
||||
|
||||
const transaction = await UncategorizedCashflowTransaction.query(
|
||||
trx
|
||||
).findById(uncategorizedTransactionId);
|
||||
|
||||
await Account.query(trx)
|
||||
.findById(transaction.accountId)
|
||||
.decrement('uncategorizedTransactions', 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the cashflow transaction whether matched with bank transaction on deleting.
|
||||
* @param {IManualJournalDeletingPayload}
|
||||
*/
|
||||
public async incrementUnCategorizedTransactionsOnUnexclude({
|
||||
tenantId,
|
||||
uncategorizedTransactionId,
|
||||
trx,
|
||||
}: IBankTransactionUnexcludedEventPayload) {
|
||||
const { UncategorizedCashflowTransaction, Account } =
|
||||
this.tenancy.models(tenantId);
|
||||
|
||||
const transaction = await UncategorizedCashflowTransaction.query().findById(
|
||||
uncategorizedTransactionId
|
||||
);
|
||||
//
|
||||
await Account.query(trx)
|
||||
.findById(transaction.accountId)
|
||||
.increment('uncategorizedTransactions', 1);
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,9 @@ export class GetMatchedTransactionBillsTransformer extends Transformer {
|
||||
'transactionNo',
|
||||
'transactionType',
|
||||
'transsactionTypeFormatted',
|
||||
'transactionNormal',
|
||||
'referenceId',
|
||||
'referenceType',
|
||||
];
|
||||
};
|
||||
|
||||
@@ -100,4 +103,29 @@ export class GetMatchedTransactionBillsTransformer extends Transformer {
|
||||
protected transsactionTypeFormatted() {
|
||||
return 'Bill';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the bill transaction normal (debit or credit).
|
||||
* @returns {string}
|
||||
*/
|
||||
protected transactionNormal() {
|
||||
return 'credit';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the match transaction reference id.
|
||||
* @param bill
|
||||
* @returns {number}
|
||||
*/
|
||||
protected referenceId(bill) {
|
||||
return bill.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the match transaction referenece type.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected referenceType() {
|
||||
return 'Bill';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
import { Transformer } from '@/lib/Transformer/Transformer';
|
||||
|
||||
export class GetMatchedTransactionCashflowTransformer extends Transformer {
|
||||
/**
|
||||
* Include these attributes to sale credit note object.
|
||||
* @returns {Array}
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return [
|
||||
'referenceNo',
|
||||
'amount',
|
||||
'amountFormatted',
|
||||
'transactionNo',
|
||||
'date',
|
||||
'dateFormatted',
|
||||
'transactionId',
|
||||
'transactionNo',
|
||||
'transactionType',
|
||||
'transsactionTypeFormatted',
|
||||
'transactionNormal',
|
||||
'referenceId',
|
||||
'referenceType',
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Exclude all attributes.
|
||||
* @returns {Array<string>}
|
||||
*/
|
||||
public excludeAttributes = (): string[] => {
|
||||
return ['*'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the invoice reference number.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected referenceNo(invoice) {
|
||||
return invoice.referenceNo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the transaction amount.
|
||||
* @param transaction
|
||||
* @returns {number}
|
||||
*/
|
||||
protected amount(transaction) {
|
||||
return transaction.amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the transaction formatted amount.
|
||||
* @param transaction
|
||||
* @returns {string}
|
||||
*/
|
||||
protected amountFormatted(transaction) {
|
||||
return this.formatNumber(transaction.amount, {
|
||||
currencyCode: transaction.currencyCode,
|
||||
money: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the date of the invoice.
|
||||
* @param invoice
|
||||
* @returns {Date}
|
||||
*/
|
||||
protected date(transaction) {
|
||||
return transaction.date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the date of the invoice.
|
||||
* @param invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected dateFormatted(transaction) {
|
||||
return this.formatDate(transaction.date);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the transaction ID of the invoice.
|
||||
* @param invoice
|
||||
* @returns {number}
|
||||
*/
|
||||
protected transactionId(transaction) {
|
||||
return transaction.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the invoice transaction number.
|
||||
* @param invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected transactionNo(transaction) {
|
||||
return transaction.transactionNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the invoice transaction type.
|
||||
* @param invoice
|
||||
* @returns {String}
|
||||
*/
|
||||
protected transactionType(transaction) {
|
||||
return transaction.transactionType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the invoice formatted transaction type.
|
||||
* @param invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected transsactionTypeFormatted(transaction) {
|
||||
return transaction.transactionTypeFormatted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the cashflow transaction normal (credit or debit).
|
||||
* @param transaction
|
||||
* @returns {string}
|
||||
*/
|
||||
protected transactionNormal(transaction) {
|
||||
return transaction.isCashCredit ? 'credit' : 'debit';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the cashflow transaction reference id.
|
||||
* @param transaction
|
||||
* @returns {number}
|
||||
*/
|
||||
protected referenceId(transaction) {
|
||||
return transaction.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the cashflow transaction reference type.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected referenceType() {
|
||||
return 'CashflowTransaction';
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,9 @@ export class GetMatchedTransactionExpensesTransformer extends Transformer {
|
||||
'transactionNo',
|
||||
'transactionType',
|
||||
'transsactionTypeFormatted',
|
||||
'transactionNormal',
|
||||
'referenceType',
|
||||
'referenceId',
|
||||
];
|
||||
};
|
||||
|
||||
@@ -111,4 +114,29 @@ export class GetMatchedTransactionExpensesTransformer extends Transformer {
|
||||
protected transsactionTypeFormatted() {
|
||||
return 'Expense';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the expense transaction normal (credit or debit).
|
||||
* @returns {string}
|
||||
*/
|
||||
protected transactionNormal() {
|
||||
return 'credit';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the transaction reference type.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected referenceType() {
|
||||
return 'Expense';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the transaction reference id.
|
||||
* @param transaction
|
||||
* @returns {number}
|
||||
*/
|
||||
protected referenceId(transaction) {
|
||||
return transaction.id;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,9 @@ export class GetMatchedTransactionInvoicesTransformer extends Transformer {
|
||||
'transactionNo',
|
||||
'transactionType',
|
||||
'transsactionTypeFormatted',
|
||||
'transactionNormal',
|
||||
'referenceType',
|
||||
'referenceId'
|
||||
];
|
||||
};
|
||||
|
||||
@@ -49,7 +52,7 @@ export class GetMatchedTransactionInvoicesTransformer extends Transformer {
|
||||
* @param invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formatAmount(invoice) {
|
||||
protected amountFormatted(invoice) {
|
||||
return this.formatNumber(invoice.dueAmount, {
|
||||
currencyCode: invoice.currencyCode,
|
||||
money: true,
|
||||
@@ -79,7 +82,7 @@ export class GetMatchedTransactionInvoicesTransformer extends Transformer {
|
||||
* @param invoice
|
||||
* @returns {number}
|
||||
*/
|
||||
protected getTransactionId(invoice) {
|
||||
protected transactionId(invoice) {
|
||||
return invoice.id;
|
||||
}
|
||||
/**
|
||||
@@ -108,4 +111,28 @@ export class GetMatchedTransactionInvoicesTransformer extends Transformer {
|
||||
protected transsactionTypeFormatted(invoice) {
|
||||
return 'Sale invoice';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the transaction normal of invoice (credit or debit).
|
||||
* @returns {string}
|
||||
*/
|
||||
protected transactionNormal() {
|
||||
return 'debit';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the transaction reference type.
|
||||
* @returns {string}
|
||||
*/ protected referenceType() {
|
||||
return 'SaleInvoice';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the transaction reference id.
|
||||
* @param transaction
|
||||
* @returns {number}
|
||||
*/
|
||||
protected referenceId(transaction) {
|
||||
return transaction.id;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { sumBy } from 'lodash';
|
||||
import { Transformer } from '@/lib/Transformer/Transformer';
|
||||
import { AccountNormal } from '@/interfaces';
|
||||
|
||||
export class GetMatchedTransactionManualJournalsTransformer extends Transformer {
|
||||
/**
|
||||
@@ -17,6 +19,9 @@ export class GetMatchedTransactionManualJournalsTransformer extends Transformer
|
||||
'transactionNo',
|
||||
'transactionType',
|
||||
'transsactionTypeFormatted',
|
||||
'transactionNormal',
|
||||
'referenceType',
|
||||
'referenceId',
|
||||
];
|
||||
};
|
||||
|
||||
@@ -37,13 +42,20 @@ export class GetMatchedTransactionManualJournalsTransformer extends Transformer
|
||||
return manualJournal.referenceNo;
|
||||
}
|
||||
|
||||
protected total(manualJournal) {
|
||||
const credit = sumBy(manualJournal?.entries, 'credit');
|
||||
const debit = sumBy(manualJournal?.entries, 'debit');
|
||||
|
||||
return debit - credit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the manual journal amount.
|
||||
* @param manualJournal
|
||||
* @returns {number}
|
||||
*/
|
||||
protected amount(manualJournal) {
|
||||
return manualJournal.amount;
|
||||
return Math.abs(this.total(manualJournal));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -107,5 +119,31 @@ export class GetMatchedTransactionManualJournalsTransformer extends Transformer
|
||||
protected transsactionTypeFormatted() {
|
||||
return 'Manual Journal';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the manual journal transaction normal (credit or debit).
|
||||
* @returns {string}
|
||||
*/
|
||||
protected transactionNormal(transaction) {
|
||||
const amount = this.total(transaction);
|
||||
|
||||
return amount >= 0 ? AccountNormal.DEBIT : AccountNormal.CREDIT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the manual journal reference type.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected referenceType() {
|
||||
return 'ManualJournal';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the manual journal reference id.
|
||||
* @param transaction
|
||||
* @returns {number}
|
||||
*/
|
||||
protected referenceId(transaction) {
|
||||
return transaction.id;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import { GetMatchedTransactionsByBills } from './GetMatchedTransactionsByBills';
|
||||
import { GetMatchedTransactionsByManualJournals } from './GetMatchedTransactionsByManualJournals';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { sortClosestMatchTransactions } from './_utils';
|
||||
import { GetMatchedTransactionsByCashflow } from './GetMatchedTransactionsByCashflow';
|
||||
import { GetMatchedTransactionsByInvoices } from './GetMatchedTransactionsByInvoices';
|
||||
|
||||
@Service()
|
||||
export class GetMatchedTransactions {
|
||||
@@ -15,7 +17,7 @@ export class GetMatchedTransactions {
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private getMatchedInvoicesService: GetMatchedTransactionsByExpenses;
|
||||
private getMatchedInvoicesService: GetMatchedTransactionsByInvoices;
|
||||
|
||||
@Inject()
|
||||
private getMatchedBillsService: GetMatchedTransactionsByBills;
|
||||
@@ -26,6 +28,9 @@ export class GetMatchedTransactions {
|
||||
@Inject()
|
||||
private getMatchedExpensesService: GetMatchedTransactionsByExpenses;
|
||||
|
||||
@Inject()
|
||||
private getMatchedCashflowService: GetMatchedTransactionsByCashflow;
|
||||
|
||||
/**
|
||||
* Registered matched transactions types.
|
||||
*/
|
||||
@@ -35,6 +40,7 @@ export class GetMatchedTransactions {
|
||||
{ type: 'Bill', service: this.getMatchedBillsService },
|
||||
{ type: 'Expense', service: this.getMatchedExpensesService },
|
||||
{ type: 'ManualJournal', service: this.getMatchedManualJournalService },
|
||||
{ type: 'Cashflow', service: this.getMatchedCashflowService },
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { initialize } from 'objection';
|
||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||
import { GetMatchedTransactionBillsTransformer } from './GetMatchedTransactionBillsTransformer';
|
||||
import { GetMatchedTransactionsFilter, MatchedTransactionPOJO } from './types';
|
||||
@@ -22,10 +23,25 @@ export class GetMatchedTransactionsByBills extends GetMatchedTransactionsByType
|
||||
tenantId: number,
|
||||
filter: GetMatchedTransactionsFilter
|
||||
) {
|
||||
const { Bill } = this.tenancy.models(tenantId);
|
||||
const { Bill, MatchedBankTransaction } = this.tenancy.models(tenantId);
|
||||
const knex = this.tenancy.knex(tenantId);
|
||||
|
||||
// Initialize the models metadata.
|
||||
await initialize(knex, [Bill, MatchedBankTransaction]);
|
||||
|
||||
// Retrieves the bill matches.
|
||||
const bills = await Bill.query().onBuild((q) => {
|
||||
q.whereNotExists(Bill.relatedQuery('matchedBankTransaction'));
|
||||
q.withGraphJoined('matchedBankTransaction');
|
||||
q.whereNull('matchedBankTransaction.id');
|
||||
q.modify('published');
|
||||
|
||||
if (filter.fromDate) {
|
||||
q.where('billDate', '>=', filter.fromDate);
|
||||
}
|
||||
if (filter.toDate) {
|
||||
q.where('billDate', '<=', filter.toDate);
|
||||
}
|
||||
q.orderBy('billDate', 'DESC');
|
||||
});
|
||||
|
||||
return this.transformer.transform(
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { initialize } from 'objection';
|
||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||
import { GetMatchedTransactionsByType } from './GetMatchedTransactionsByType';
|
||||
import { GetMatchedTransactionCashflowTransformer } from './GetMatchedTransactionCashflowTransformer';
|
||||
import { GetMatchedTransactionsFilter } from './types';
|
||||
|
||||
@Service()
|
||||
export class GetMatchedTransactionsByCashflow extends GetMatchedTransactionsByType {
|
||||
@Inject()
|
||||
private transformer: TransformerInjectable;
|
||||
|
||||
/**
|
||||
* Retrieve the matched transactions of cash flow.
|
||||
* @param {number} tenantId
|
||||
* @param {GetMatchedTransactionsFilter} filter
|
||||
* @returns
|
||||
*/
|
||||
async getMatchedTransactions(
|
||||
tenantId: number,
|
||||
filter: Omit<GetMatchedTransactionsFilter, 'transactionType'>
|
||||
) {
|
||||
const { CashflowTransaction, MatchedBankTransaction } =
|
||||
this.tenancy.models(tenantId);
|
||||
const knex = this.tenancy.knex(tenantId);
|
||||
|
||||
// Initialize the ORM models metadata.
|
||||
await initialize(knex, [CashflowTransaction, MatchedBankTransaction]);
|
||||
|
||||
const transactions = await CashflowTransaction.query().onBuild((q) => {
|
||||
// Not matched to bank transaction.
|
||||
q.withGraphJoined('matchedBankTransaction');
|
||||
q.whereNull('matchedBankTransaction.id');
|
||||
|
||||
// Not categorized.
|
||||
q.modify('notCategorized');
|
||||
|
||||
// Published.
|
||||
q.modify('published');
|
||||
|
||||
if (filter.fromDate) {
|
||||
q.where('date', '>=', filter.fromDate);
|
||||
}
|
||||
if (filter.toDate) {
|
||||
q.where('date', '<=', filter.toDate);
|
||||
}
|
||||
q.orderBy('date', 'DESC');
|
||||
});
|
||||
|
||||
return this.transformer.transform(
|
||||
tenantId,
|
||||
transactions,
|
||||
new GetMatchedTransactionCashflowTransformer()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the matched transaction of cash flow.
|
||||
* @param {number} tenantId
|
||||
* @param {number} transactionId
|
||||
* @returns
|
||||
*/
|
||||
async getMatchedTransaction(tenantId: number, transactionId: number) {
|
||||
const { CashflowTransaction, MatchedBankTransaction } =
|
||||
this.tenancy.models(tenantId);
|
||||
const knex = this.tenancy.knex(tenantId);
|
||||
|
||||
// Initialize the ORM models metadata.
|
||||
await initialize(knex, [CashflowTransaction, MatchedBankTransaction]);
|
||||
|
||||
const transactions = await CashflowTransaction.query()
|
||||
.findById(transactionId)
|
||||
.withGraphJoined('matchedBankTransaction')
|
||||
.whereNull('matchedBankTransaction.id')
|
||||
.modify('notCategorized')
|
||||
.modify('published')
|
||||
.throwIfNotFound();
|
||||
|
||||
return this.transformer.transform(
|
||||
tenantId,
|
||||
transactions,
|
||||
new GetMatchedTransactionCashflowTransformer()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { initialize } from 'objection';
|
||||
import { GetMatchedTransactionsFilter, MatchedTransactionPOJO } from './types';
|
||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
@@ -23,22 +24,34 @@ export class GetMatchedTransactionsByExpenses extends GetMatchedTransactionsByTy
|
||||
tenantId: number,
|
||||
filter: GetMatchedTransactionsFilter
|
||||
) {
|
||||
const { Expense } = this.tenancy.models(tenantId);
|
||||
const { Expense, MatchedBankTransaction } = this.tenancy.models(tenantId);
|
||||
const knex = this.tenancy.knex(tenantId);
|
||||
|
||||
// Initialize the models metadata.
|
||||
await initialize(knex, [Expense, MatchedBankTransaction]);
|
||||
|
||||
// Retrieve the expense matches.
|
||||
const expenses = await Expense.query().onBuild((query) => {
|
||||
query.whereNotExists(Expense.relatedQuery('matchedBankTransaction'));
|
||||
// Filter out the not matched to bank transactions.
|
||||
query.withGraphJoined('matchedBankTransaction');
|
||||
query.whereNull('matchedBankTransaction.id');
|
||||
|
||||
// Filter the published onyl
|
||||
query.modify('filterByPublished');
|
||||
|
||||
if (filter.fromDate) {
|
||||
query.where('payment_date', '>=', filter.fromDate);
|
||||
query.where('paymentDate', '>=', filter.fromDate);
|
||||
}
|
||||
if (filter.toDate) {
|
||||
query.where('payment_date', '<=', filter.toDate);
|
||||
query.where('paymentDate', '<=', filter.toDate);
|
||||
}
|
||||
if (filter.minAmount) {
|
||||
query.where('total_amount', '>=', filter.minAmount);
|
||||
query.where('totalAmount', '>=', filter.minAmount);
|
||||
}
|
||||
if (filter.maxAmount) {
|
||||
query.where('total_amount', '<=', filter.maxAmount);
|
||||
query.where('totalAmount', '<=', filter.maxAmount);
|
||||
}
|
||||
query.orderBy('paymentDate', 'DESC');
|
||||
});
|
||||
return this.transformer.transform(
|
||||
tenantId,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { initialize } from 'objection';
|
||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||
import { GetMatchedTransactionInvoicesTransformer } from './GetMatchedTransactionInvoicesTransformer';
|
||||
import {
|
||||
@@ -6,7 +8,6 @@ import {
|
||||
MatchedTransactionsPOJO,
|
||||
} from './types';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { GetMatchedTransactionsByType } from './GetMatchedTransactionsByType';
|
||||
|
||||
@Service()
|
||||
@@ -27,10 +28,27 @@ export class GetMatchedTransactionsByInvoices extends GetMatchedTransactionsByTy
|
||||
tenantId: number,
|
||||
filter: GetMatchedTransactionsFilter
|
||||
): Promise<MatchedTransactionsPOJO> {
|
||||
const { SaleInvoice } = this.tenancy.models(tenantId);
|
||||
const { SaleInvoice, MatchedBankTransaction } =
|
||||
this.tenancy.models(tenantId);
|
||||
const knex = this.tenancy.knex(tenantId);
|
||||
|
||||
// Initialize the models metadata.
|
||||
await initialize(knex, [SaleInvoice, MatchedBankTransaction]);
|
||||
|
||||
// Retrieve the invoices that not matched, unpaid.
|
||||
const invoices = await SaleInvoice.query().onBuild((q) => {
|
||||
q.whereNotExists(SaleInvoice.relatedQuery('matchedBankTransaction'));
|
||||
q.withGraphJoined('matchedBankTransaction');
|
||||
q.whereNull('matchedBankTransaction.id');
|
||||
q.modify('unpaid');
|
||||
q.modify('published');
|
||||
|
||||
if (filter.fromDate) {
|
||||
q.where('invoiceDate', '>=', filter.fromDate);
|
||||
}
|
||||
if (filter.toDate) {
|
||||
q.where('invoiceDate', '<=', filter.toDate);
|
||||
}
|
||||
q.orderBy('invoiceDate', 'DESC');
|
||||
});
|
||||
|
||||
return this.transformer.transform(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { initialize } from 'objection';
|
||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||
import { GetMatchedTransactionManualJournalsTransformer } from './GetMatchedTransactionManualJournalsTransformer';
|
||||
import { GetMatchedTransactionsByType } from './GetMatchedTransactionsByType';
|
||||
@@ -19,12 +20,26 @@ export class GetMatchedTransactionsByManualJournals extends GetMatchedTransactio
|
||||
tenantId: number,
|
||||
filter: Omit<GetMatchedTransactionsFilter, 'transactionType'>
|
||||
) {
|
||||
const { ManualJournal } = this.tenancy.models(tenantId);
|
||||
const { ManualJournal, ManualJournalEntry, MatchedBankTransaction } =
|
||||
this.tenancy.models(tenantId);
|
||||
const knex = this.tenancy.knex(tenantId);
|
||||
|
||||
await initialize(knex, [
|
||||
ManualJournal,
|
||||
ManualJournalEntry,
|
||||
MatchedBankTransaction,
|
||||
]);
|
||||
const accountId = 1000;
|
||||
|
||||
const manualJournals = await ManualJournal.query().onBuild((query) => {
|
||||
query.whereNotExists(
|
||||
ManualJournal.relatedQuery('matchedBankTransaction')
|
||||
);
|
||||
query.withGraphJoined('matchedBankTransaction');
|
||||
query.whereNull('matchedBankTransaction.id');
|
||||
|
||||
query.withGraphJoined('entries');
|
||||
query.where('entries.accountId', accountId);
|
||||
|
||||
query.modify('filterByPublished');
|
||||
|
||||
if (filter.fromDate) {
|
||||
query.where('date', '>=', filter.fromDate);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isEmpty, sumBy } from 'lodash';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { Knex } from 'knex';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { PromisePool } from '@supercharge/promise-pool';
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from './types';
|
||||
import { MatchTransactionsTypes } from './MatchTransactionsTypes';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import { sumMatchTranasctions } from './_utils';
|
||||
|
||||
@Service()
|
||||
export class MatchBankTransactions {
|
||||
@@ -90,9 +91,8 @@ export class MatchBankTransactions {
|
||||
throw new ServiceError(error);
|
||||
}
|
||||
// Calculate the total given matching transactions.
|
||||
const totalMatchedTranasctions = sumBy(
|
||||
validatationResult.results,
|
||||
'amount'
|
||||
const totalMatchedTranasctions = sumMatchTranasctions(
|
||||
validatationResult.results
|
||||
);
|
||||
// Validates the total given matching transcations whether is not equal
|
||||
// uncategorized transaction amount.
|
||||
|
||||
@@ -4,6 +4,8 @@ import { GetMatchedTransactionsByBills } from './GetMatchedTransactionsByBills';
|
||||
import { GetMatchedTransactionsByManualJournals } from './GetMatchedTransactionsByManualJournals';
|
||||
import { MatchTransactionsTypesRegistry } from './MatchTransactionsTypesRegistry';
|
||||
import { GetMatchedTransactionsByInvoices } from './GetMatchedTransactionsByInvoices';
|
||||
import { GetMatchedTransactionCashflowTransformer } from './GetMatchedTransactionCashflowTransformer';
|
||||
import { GetMatchedTransactionsByCashflow } from './GetMatchedTransactionsByCashflow';
|
||||
|
||||
@Service()
|
||||
export class MatchTransactionsTypes {
|
||||
@@ -25,6 +27,10 @@ export class MatchTransactionsTypes {
|
||||
type: 'ManualJournal',
|
||||
service: GetMatchedTransactionsByManualJournals,
|
||||
},
|
||||
{
|
||||
type: 'CashflowTransaction',
|
||||
service: GetMatchedTransactionsByCashflow,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ export class UnmatchMatchedBankTransaction {
|
||||
return this.uow.withTransaction(tenantId, async (trx) => {
|
||||
await this.eventPublisher.emitAsync(events.bankMatch.onUnmatching, {
|
||||
tenantId,
|
||||
uncategorizedTransactionId,
|
||||
trx,
|
||||
} as IBankTransactionUnmatchingEventPayload);
|
||||
|
||||
@@ -40,6 +41,7 @@ export class UnmatchMatchedBankTransaction {
|
||||
|
||||
await this.eventPublisher.emitAsync(events.bankMatch.onUnmatched, {
|
||||
tenantId,
|
||||
uncategorizedTransactionId,
|
||||
trx,
|
||||
} as IBankTransactionUnmatchingEventPayload);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Knex } from 'knex';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { ERRORS } from './types';
|
||||
|
||||
@Service()
|
||||
@@ -18,12 +19,13 @@ export class ValidateTransactionMatched {
|
||||
public async validateTransactionNoMatchLinking(
|
||||
tenantId: number,
|
||||
referenceType: string,
|
||||
referenceId: number
|
||||
referenceId: number,
|
||||
trx?: Knex.Transaction
|
||||
) {
|
||||
const { MatchedBankTransaction } = this.tenancy.models(tenantId);
|
||||
|
||||
const foundMatchedTransaction =
|
||||
await MatchedBankTransaction.query().findOne({
|
||||
await MatchedBankTransaction.query(trx).findOne({
|
||||
referenceType,
|
||||
referenceId,
|
||||
});
|
||||
|
||||
@@ -20,3 +20,12 @@ export const sortClosestMatchTransactions = (
|
||||
),
|
||||
])(matches);
|
||||
};
|
||||
|
||||
export const sumMatchTranasctions = (transactions: Array<any>) => {
|
||||
return transactions.reduce(
|
||||
(total, item) =>
|
||||
total +
|
||||
(item.transactionNormal === 'debit' ? 1 : -1) * parseFloat(item.amount),
|
||||
0
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import events from '@/subscribers/events';
|
||||
import {
|
||||
IBankTransactionMatchedEventPayload,
|
||||
IBankTransactionUnmatchedEventPayload,
|
||||
} from '../types';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
|
||||
@Service()
|
||||
export class DecrementUncategorizedTransactionOnMatching {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
/**
|
||||
* Constructor method.
|
||||
*/
|
||||
public attach(bus) {
|
||||
bus.subscribe(
|
||||
events.bankMatch.onMatched,
|
||||
this.decrementUnCategorizedTransactionsOnMatching.bind(this)
|
||||
);
|
||||
bus.subscribe(
|
||||
events.bankMatch.onUnmatched,
|
||||
this.incrementUnCategorizedTransactionsOnUnmatching.bind(this)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the cashflow transaction whether matched with bank transaction on deleting.
|
||||
* @param {IManualJournalDeletingPayload}
|
||||
*/
|
||||
public async decrementUnCategorizedTransactionsOnMatching({
|
||||
tenantId,
|
||||
uncategorizedTransactionId,
|
||||
trx,
|
||||
}: IBankTransactionMatchedEventPayload) {
|
||||
const { UncategorizedCashflowTransaction, Account } =
|
||||
this.tenancy.models(tenantId);
|
||||
|
||||
const transaction = await UncategorizedCashflowTransaction.query().findById(
|
||||
uncategorizedTransactionId
|
||||
);
|
||||
await Account.query(trx)
|
||||
.findById(transaction.accountId)
|
||||
.decrement('uncategorizedTransactions', 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the cashflow transaction whether matched with bank transaction on deleting.
|
||||
* @param {IManualJournalDeletingPayload}
|
||||
*/
|
||||
public async incrementUnCategorizedTransactionsOnUnmatching({
|
||||
tenantId,
|
||||
uncategorizedTransactionId,
|
||||
trx,
|
||||
}: IBankTransactionUnmatchedEventPayload) {
|
||||
const { UncategorizedCashflowTransaction, Account } =
|
||||
this.tenancy.models(tenantId);
|
||||
|
||||
const transaction = await UncategorizedCashflowTransaction.query().findById(
|
||||
uncategorizedTransactionId
|
||||
);
|
||||
await Account.query(trx)
|
||||
.findById(transaction.accountId)
|
||||
.increment('uncategorizedTransactions', 1);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { IManualJournalDeletingPayload } from '@/interfaces';
|
||||
import { ICommandCashflowDeletingPayload, IManualJournalDeletingPayload } from '@/interfaces';
|
||||
import events from '@/subscribers/events';
|
||||
import { ValidateTransactionMatched } from '../ValidateTransactionsMatched';
|
||||
|
||||
@@ -24,13 +24,14 @@ export class ValidateMatchingOnCashflowDelete {
|
||||
*/
|
||||
public async validateMatchingOnCashflowDeleting({
|
||||
tenantId,
|
||||
oldManualJournal,
|
||||
oldCashflowTransaction,
|
||||
trx,
|
||||
}: IManualJournalDeletingPayload) {
|
||||
}: ICommandCashflowDeletingPayload) {
|
||||
await this.validateNoMatchingLinkedService.validateTransactionNoMatchLinking(
|
||||
tenantId,
|
||||
'ManualJournal',
|
||||
oldManualJournal.id
|
||||
'CashflowTransaction',
|
||||
oldCashflowTransaction.id,
|
||||
trx
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,8 @@ export class ValidateMatchingOnExpenseDelete {
|
||||
await this.validateNoMatchingLinkedService.validateTransactionNoMatchLinking(
|
||||
tenantId,
|
||||
'Expense',
|
||||
oldExpense.id
|
||||
oldExpense.id,
|
||||
trx
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,8 @@ export class ValidateMatchingOnManualJournalDelete {
|
||||
await this.validateNoMatchingLinkedService.validateTransactionNoMatchLinking(
|
||||
tenantId,
|
||||
'ManualJournal',
|
||||
oldManualJournal.id
|
||||
oldManualJournal.id,
|
||||
trx
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,8 @@ export class ValidateMatchingOnPaymentMadeDelete {
|
||||
await this.validateNoMatchingLinkedService.validateTransactionNoMatchLinking(
|
||||
tenantId,
|
||||
'PaymentMade',
|
||||
oldBillPayment.id
|
||||
oldBillPayment.id,
|
||||
trx
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,8 @@ export class ValidateMatchingOnPaymentReceivedDelete {
|
||||
await this.validateNoMatchingLinkedService.validateTransactionNoMatchLinking(
|
||||
tenantId,
|
||||
'PaymentReceive',
|
||||
oldPaymentReceive.id
|
||||
oldPaymentReceive.id,
|
||||
trx
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,10 +16,14 @@ export interface IBankTransactionMatchedEventPayload {
|
||||
|
||||
export interface IBankTransactionUnmatchingEventPayload {
|
||||
tenantId: number;
|
||||
uncategorizedTransactionId: number;
|
||||
trx?: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface IBankTransactionUnmatchedEventPayload {
|
||||
tenantId: number;
|
||||
uncategorizedTransactionId: number;
|
||||
trx?: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface IMatchTransactionDTO {
|
||||
|
||||
@@ -17,7 +17,7 @@ import { DeleteCashflowTransaction } from '@/services/Cashflow/DeleteCashflowTra
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { CashflowApplication } from '@/services/Cashflow/CashflowApplication';
|
||||
import { Knex } from 'knex';
|
||||
import { uniqid } from 'uniqid';
|
||||
import uniqid from 'uniqid';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import events from '@/subscribers/events';
|
||||
|
||||
@@ -148,7 +148,6 @@ export class PlaidSyncDb {
|
||||
*/
|
||||
public async syncAccountsTransactions(
|
||||
tenantId: number,
|
||||
batchNo: string,
|
||||
plaidAccountsTransactions: PlaidTransaction[],
|
||||
trx?: Knex.Transaction
|
||||
): Promise<void> {
|
||||
@@ -161,7 +160,6 @@ export class PlaidSyncDb {
|
||||
return this.syncAccountTranactions(
|
||||
tenantId,
|
||||
plaidAccountId,
|
||||
batchNo,
|
||||
plaidTransactions,
|
||||
trx
|
||||
);
|
||||
|
||||
@@ -73,8 +73,6 @@ export class PlaidUpdateTransactions {
|
||||
added.concat(modified),
|
||||
trx
|
||||
);
|
||||
// Sync removed transactions.
|
||||
await this.plaidSync.syncRemoveTransactions(tenantId, removed, trx);
|
||||
// Sync transactions cursor.
|
||||
await this.plaidSync.syncTransactionsCursor(
|
||||
tenantId,
|
||||
|
||||
@@ -37,7 +37,6 @@ export const transformPlaidAccountToCreateAccount = R.curry(
|
||||
export const transformPlaidTrxsToCashflowCreate = R.curry(
|
||||
(
|
||||
cashflowAccountId: number,
|
||||
creditAccountId: number,
|
||||
plaidTranasction: PlaidTransaction
|
||||
): CreateUncategorizedTransactionDTO => {
|
||||
return {
|
||||
|
||||
@@ -18,11 +18,11 @@ export class RegonizeTransactionsJob {
|
||||
* Triggers sending invoice mail.
|
||||
*/
|
||||
private handler = async (job, done: Function) => {
|
||||
const { tenantId } = job.attrs.data;
|
||||
const { tenantId, batch } = job.attrs.data;
|
||||
const regonizeTransactions = Container.get(RecognizeTranasctionsService);
|
||||
|
||||
try {
|
||||
await regonizeTransactions.recognizeTransactions(tenantId);
|
||||
await regonizeTransactions.recognizeTransactions(tenantId, batch);
|
||||
done();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
|
||||
@@ -5,6 +5,8 @@ import {
|
||||
IBankRuleEventDeletedPayload,
|
||||
IBankRuleEventEditedPayload,
|
||||
} from '../../Rules/types';
|
||||
import { IImportFileCommitedEventPayload } from '@/interfaces/Import';
|
||||
import { Import } from '@/system/models';
|
||||
|
||||
@Service()
|
||||
export class TriggerRecognizedTransactions {
|
||||
@@ -27,6 +29,10 @@ export class TriggerRecognizedTransactions {
|
||||
events.bankRules.onDeleted,
|
||||
this.recognizedTransactionsOnRuleDeleted.bind(this)
|
||||
);
|
||||
bus.subscribe(
|
||||
events.import.onImportCommitted,
|
||||
this.triggerRecognizeTransactionsOnImportCommitted.bind(this)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,4 +79,20 @@ export class TriggerRecognizedTransactions {
|
||||
const payload = { tenantId };
|
||||
await this.agenda.now('recognize-uncategorized-transactions-job', payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers the recognize bank transactions once the imported file commit.
|
||||
* @param {IImportFileCommitedEventPayload} payload -
|
||||
*/
|
||||
private async triggerRecognizeTransactionsOnImportCommitted({
|
||||
tenantId,
|
||||
importId,
|
||||
meta,
|
||||
}: IImportFileCommitedEventPayload) {
|
||||
const importFile = await Import.query().findOne({ importId });
|
||||
const batch = importFile.paramsParsed.batch;
|
||||
const payload = { tenantId, batch };
|
||||
|
||||
await this.agenda.now('recognize-uncategorized-transactions-job', payload);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { upperFirst, camelCase } from 'lodash';
|
||||
import { Transformer } from '@/lib/Transformer/Transformer';
|
||||
import { getTransactionTypeLabel } from '@/utils/transactions-types';
|
||||
import { getCashflowTransactionFormattedType } from '@/utils/transactions-types';
|
||||
|
||||
export class GetBankRulesTransformer extends Transformer {
|
||||
/**
|
||||
@@ -29,8 +28,7 @@ export class GetBankRulesTransformer extends Transformer {
|
||||
* @returns {string}
|
||||
*/
|
||||
protected assignCategoryFormatted(bankRule: any) {
|
||||
const assignCategory = upperFirst(camelCase(bankRule.assignCategory));
|
||||
return getTransactionTypeLabel(assignCategory);
|
||||
return getCashflowTransactionFormattedType(bankRule.assignCategory);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -34,11 +34,12 @@ export default class CashflowTransactionJournalEntries {
|
||||
currencyCode: transaction.currencyCode,
|
||||
exchangeRate: transaction.exchangeRate,
|
||||
|
||||
transactionType: transformCashflowTransactionType(
|
||||
transaction.transactionType
|
||||
),
|
||||
transactionType: 'CashflowTransaction',
|
||||
transactionId: transaction.id,
|
||||
transactionNumber: transaction.transactionNumber,
|
||||
transactionSubType: transformCashflowTransactionType(
|
||||
transaction.transactionType
|
||||
),
|
||||
referenceNumber: transaction.referenceNo,
|
||||
|
||||
note: transaction.description,
|
||||
@@ -161,12 +162,10 @@ export default class CashflowTransactionJournalEntries {
|
||||
cashflowTransactionId: number,
|
||||
trx?: Knex.Transaction
|
||||
): Promise<void> => {
|
||||
const transactionTypes = getCashflowAccountTransactionsTypes();
|
||||
|
||||
await this.ledgerStorage.deleteByReference(
|
||||
tenantId,
|
||||
cashflowTransactionId,
|
||||
transactionTypes,
|
||||
'CashflowTransaction',
|
||||
trx
|
||||
);
|
||||
};
|
||||
|
||||
@@ -84,20 +84,23 @@ export class CategorizeCashflowTransaction {
|
||||
cashflowTransactionDTO
|
||||
);
|
||||
// Updates the uncategorized transaction as categorized.
|
||||
await UncategorizedCashflowTransaction.query(trx).patchAndFetchById(
|
||||
uncategorizedTransactionId,
|
||||
{
|
||||
categorized: true,
|
||||
categorizeRefType: 'CashflowTransaction',
|
||||
categorizeRefId: cashflowTransaction.id,
|
||||
}
|
||||
);
|
||||
const uncategorizedTransaction =
|
||||
await UncategorizedCashflowTransaction.query(trx).patchAndFetchById(
|
||||
uncategorizedTransactionId,
|
||||
{
|
||||
categorized: true,
|
||||
categorizeRefType: 'CashflowTransaction',
|
||||
categorizeRefId: cashflowTransaction.id,
|
||||
}
|
||||
);
|
||||
// Triggers `onCashflowTransactionCategorized` event.
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.cashflow.onTransactionCategorized,
|
||||
{
|
||||
tenantId,
|
||||
// cashflowTransaction,
|
||||
cashflowTransaction,
|
||||
uncategorizedTransaction,
|
||||
categorizeDTO,
|
||||
trx,
|
||||
} as ICashflowTransactionCategorizedPayload
|
||||
);
|
||||
|
||||
@@ -2,7 +2,13 @@ import { Knex } from 'knex';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
import UnitOfWork from '../UnitOfWork';
|
||||
import { CreateUncategorizedTransactionDTO } from '@/interfaces';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import events from '@/subscribers/events';
|
||||
import {
|
||||
CreateUncategorizedTransactionDTO,
|
||||
IUncategorizedTransactionCreatedEventPayload,
|
||||
IUncategorizedTransactionCreatingEventPayload,
|
||||
} from '@/interfaces';
|
||||
|
||||
@Service()
|
||||
export class CreateUncategorizedTransaction {
|
||||
@@ -12,6 +18,9 @@ export class CreateUncategorizedTransaction {
|
||||
@Inject()
|
||||
private uow: UnitOfWork;
|
||||
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
/**
|
||||
* Creates an uncategorized cashflow transaction.
|
||||
* @param {number} tenantId
|
||||
@@ -19,7 +28,7 @@ export class CreateUncategorizedTransaction {
|
||||
*/
|
||||
public create(
|
||||
tenantId: number,
|
||||
createDTO: CreateUncategorizedTransactionDTO,
|
||||
createUncategorizedTransactionDTO: CreateUncategorizedTransactionDTO,
|
||||
trx?: Knex.Transaction
|
||||
) {
|
||||
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
|
||||
@@ -27,12 +36,30 @@ export class CreateUncategorizedTransaction {
|
||||
return this.uow.withTransaction(
|
||||
tenantId,
|
||||
async (trx: Knex.Transaction) => {
|
||||
const transaction = await UncategorizedCashflowTransaction.query(
|
||||
trx
|
||||
).insertAndFetch({
|
||||
...createDTO,
|
||||
});
|
||||
return transaction;
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.cashflow.onTransactionUncategorizedCreating,
|
||||
{
|
||||
tenantId,
|
||||
createUncategorizedTransactionDTO,
|
||||
trx,
|
||||
} as IUncategorizedTransactionCreatingEventPayload
|
||||
);
|
||||
|
||||
const uncategorizedTransaction =
|
||||
await UncategorizedCashflowTransaction.query(trx).insertAndFetch({
|
||||
...createUncategorizedTransactionDTO,
|
||||
});
|
||||
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.cashflow.onTransactionUncategorizedCreated,
|
||||
{
|
||||
tenantId,
|
||||
uncategorizedTransaction,
|
||||
createUncategorizedTransactionDTO,
|
||||
trx,
|
||||
} as IUncategorizedTransactionCreatedEventPayload
|
||||
);
|
||||
return uncategorizedTransaction;
|
||||
},
|
||||
trx
|
||||
);
|
||||
|
||||
@@ -101,6 +101,7 @@ export default class NewCashflowTransactionService {
|
||||
...fromDTO,
|
||||
transactionNumber,
|
||||
currencyCode: cashflowAccount.currencyCode,
|
||||
exchangeRate: fromDTO?.exchangeRate || 1,
|
||||
transactionType: transformCashflowTransactionType(
|
||||
fromDTO.transactionType
|
||||
),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { Knex } from 'knex';
|
||||
import * as yup from 'yup';
|
||||
import uniqid from 'uniqid';
|
||||
import { Importable } from '../Import/Importable';
|
||||
import { CreateUncategorizedTransaction } from './CreateUncategorizedTransaction';
|
||||
import { CreateUncategorizedTransactionDTO } from '@/interfaces';
|
||||
@@ -15,6 +16,7 @@ export class UncategorizedTransactionsImportable extends Importable {
|
||||
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Passing the sheet DTO to create uncategorized transaction.
|
||||
* @param {number} tenantId
|
||||
@@ -43,6 +45,7 @@ export class UncategorizedTransactionsImportable extends Importable {
|
||||
return {
|
||||
...createDTO,
|
||||
accountId: context.import.paramsParsed.accountId,
|
||||
batch: context.import.paramsParsed.batch,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -54,6 +57,9 @@ export class UncategorizedTransactionsImportable extends Importable {
|
||||
return BankTransactionsSampleData;
|
||||
}
|
||||
|
||||
// ------------------
|
||||
// # Params
|
||||
// ------------------
|
||||
/**
|
||||
* Params validation schema.
|
||||
* @returns {ValidationSchema[]}
|
||||
@@ -79,4 +85,17 @@ export class UncategorizedTransactionsImportable extends Importable {
|
||||
await Account.query().findById(params.accountId).throwIfNotFound({});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformes the import params before storing them.
|
||||
* @param {Record<string, any>} parmas
|
||||
*/
|
||||
public transformParams(parmas: Record<string, any>) {
|
||||
const batch = uniqid();
|
||||
|
||||
return {
|
||||
...parmas,
|
||||
batch,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import events from '@/subscribers/events';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import {
|
||||
ICashflowTransactionCategorizedPayload,
|
||||
ICashflowTransactionUncategorizedPayload,
|
||||
} from '@/interfaces';
|
||||
|
||||
@Service()
|
||||
export class DecrementUncategorizedTransactionOnCategorize {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
/**
|
||||
* Constructor method.
|
||||
*/
|
||||
public attach(bus) {
|
||||
bus.subscribe(
|
||||
events.cashflow.onTransactionCategorized,
|
||||
this.decrementUnCategorizedTransactionsOnCategorized.bind(this)
|
||||
);
|
||||
bus.subscribe(
|
||||
events.cashflow.onTransactionUncategorized,
|
||||
this.incrementUnCategorizedTransactionsOnUncategorized.bind(this)
|
||||
);
|
||||
bus.subscribe(
|
||||
events.cashflow.onTransactionUncategorizedCreated,
|
||||
this.incrementUncategoirzedTransactionsOnCreated.bind(this)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrement the uncategoirzed transactions on the account once categorizing.
|
||||
* @param {ICashflowTransactionCategorizedPayload}
|
||||
*/
|
||||
public async decrementUnCategorizedTransactionsOnCategorized({
|
||||
tenantId,
|
||||
uncategorizedTransaction,
|
||||
}: ICashflowTransactionCategorizedPayload) {
|
||||
const { Account } = this.tenancy.models(tenantId);
|
||||
|
||||
await Account.query()
|
||||
.findById(uncategorizedTransaction.accountId)
|
||||
.decrement('uncategorizedTransactions', 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment the uncategorized transaction on the given account on uncategorizing.
|
||||
* @param {IManualJournalDeletingPayload}
|
||||
*/
|
||||
public async incrementUnCategorizedTransactionsOnUncategorized({
|
||||
tenantId,
|
||||
uncategorizedTransaction,
|
||||
}: ICashflowTransactionUncategorizedPayload) {
|
||||
const { Account } = this.tenancy.models(tenantId);
|
||||
|
||||
await Account.query()
|
||||
.findById(uncategorizedTransaction.accountId)
|
||||
.increment('uncategorizedTransactions', 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Increments uncategorized transactions count once creating a new transaction.
|
||||
* @param {ICommandCashflowCreatedPayload} payload -
|
||||
*/
|
||||
public async incrementUncategoirzedTransactionsOnCreated({
|
||||
tenantId,
|
||||
uncategorizedTransaction,
|
||||
trx,
|
||||
}: any) {
|
||||
const { Account } = this.tenancy.models(tenantId);
|
||||
|
||||
if (!uncategorizedTransaction.accountId) return;
|
||||
|
||||
await Account.query(trx)
|
||||
.findById(uncategorizedTransaction.accountId)
|
||||
.increment('uncategorizedTransactions', 1);
|
||||
}
|
||||
}
|
||||
@@ -38,4 +38,24 @@ export default class ContactTransfromer extends Transformer {
|
||||
? this.formatDate(contact.openingBalanceAt)
|
||||
: '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the unused credit balance.
|
||||
* @param {IContact} contact
|
||||
* @returns {number}
|
||||
*/
|
||||
protected unusedCredit = (contact: IContact): number => {
|
||||
return contact.balance > 0 ? 0 : Math.abs(contact.balance);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the formatted unused credit balance.
|
||||
* @param {IContact} contact
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedUnusedCredit = (contact: IContact): string => {
|
||||
const unusedCredit = this.unusedCredit(contact);
|
||||
|
||||
return formatNumber(unusedCredit, { currencyCode: contact.currencyCode });
|
||||
};
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ export default class CustomerTransfromer extends ContactTransfromer {
|
||||
'formattedOpeningBalanceAt',
|
||||
'customerType',
|
||||
'formattedCustomerType',
|
||||
'unusedCredit',
|
||||
'formattedUnusedCredit',
|
||||
];
|
||||
};
|
||||
|
||||
|
||||
@@ -45,9 +45,9 @@ export class CustomersApplication {
|
||||
|
||||
/**
|
||||
* Creates a new customer.
|
||||
* @param {number} tenantId
|
||||
* @param {ICustomerNewDTO} customerDTO
|
||||
* @param {ISystemUser} authorizedUser
|
||||
* @param {number} tenantId
|
||||
* @param {ICustomerNewDTO} customerDTO
|
||||
* @param {ISystemUser} authorizedUser
|
||||
* @returns {Promise<ICustomer>}
|
||||
*/
|
||||
public createCustomer = (tenantId: number, customerDTO: ICustomerNewDTO) => {
|
||||
@@ -56,9 +56,9 @@ export class CustomersApplication {
|
||||
|
||||
/**
|
||||
* Edits details of the given customer.
|
||||
* @param {number} tenantId
|
||||
* @param {number} customerId
|
||||
* @param {ICustomerEditDTO} customerDTO
|
||||
* @param {number} tenantId
|
||||
* @param {number} customerId
|
||||
* @param {ICustomerEditDTO} customerDTO
|
||||
* @return {Promise<ICustomer>}
|
||||
*/
|
||||
public editCustomer = (
|
||||
@@ -75,9 +75,9 @@ export class CustomersApplication {
|
||||
|
||||
/**
|
||||
* Deletes the given customer and associated transactions.
|
||||
* @param {number} tenantId
|
||||
* @param {number} customerId
|
||||
* @param {ISystemUser} authorizedUser
|
||||
* @param {number} tenantId
|
||||
* @param {number} customerId
|
||||
* @param {ISystemUser} authorizedUser
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public deleteCustomer = (
|
||||
@@ -94,9 +94,9 @@ export class CustomersApplication {
|
||||
|
||||
/**
|
||||
* Changes the opening balance of the given customer.
|
||||
* @param {number} tenantId
|
||||
* @param {number} customerId
|
||||
* @param {Date|string} openingBalanceEditDTO
|
||||
* @param {number} tenantId
|
||||
* @param {number} customerId
|
||||
* @param {Date|string} openingBalanceEditDTO
|
||||
* @returns {Promise<ICustomer>}
|
||||
*/
|
||||
public editOpeningBalance = (
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Service } from 'typedi';
|
||||
import ContactTransfromer from '../ContactTransformer';
|
||||
|
||||
export default class VendorTransfromer extends ContactTransfromer {
|
||||
@@ -10,7 +9,9 @@ export default class VendorTransfromer extends ContactTransfromer {
|
||||
return [
|
||||
'formattedBalance',
|
||||
'formattedOpeningBalance',
|
||||
'formattedOpeningBalanceAt'
|
||||
'formattedOpeningBalanceAt',
|
||||
'unusedCredit',
|
||||
'formattedUnusedCredit',
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ export const DEFAULT_VIEWS = [
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
export const ERRORS = {
|
||||
OPENING_BALANCE_DATE_REQUIRED: 'OPENING_BALANCE_DATE_REQUIRED',
|
||||
CONTACT_ALREADY_INACTIVE: 'CONTACT_ALREADY_INACTIVE',
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import R from 'ramda';
|
||||
import moment from 'moment';
|
||||
import { first, isEmpty } from 'lodash';
|
||||
import {
|
||||
ICashflowAccountTransaction,
|
||||
ICashflowAccountTransactionsQuery,
|
||||
INumberFormatQuery,
|
||||
} from '@/interfaces';
|
||||
import FinancialSheet from '../FinancialSheet';
|
||||
import { runningAmount } from 'utils';
|
||||
import { CashflowAccountTransactionsRepo } from './CashflowAccountTransactionsRepo';
|
||||
import { BankTransactionStatus } from './constants';
|
||||
import { formatBankTransactionsStatus } from './utils';
|
||||
|
||||
export default class CashflowAccountTransactionReport extends FinancialSheet {
|
||||
private transactions: any;
|
||||
private openingBalance: number;
|
||||
export class CashflowAccountTransactionReport extends FinancialSheet {
|
||||
private runningBalance: any;
|
||||
private numberFormat: INumberFormatQuery;
|
||||
private baseCurrency: string;
|
||||
private query: ICashflowAccountTransactionsQuery;
|
||||
private repo: CashflowAccountTransactionsRepo;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
@@ -23,19 +23,61 @@ export default class CashflowAccountTransactionReport extends FinancialSheet {
|
||||
* @param {ICashflowAccountTransactionsQuery} query -
|
||||
*/
|
||||
constructor(
|
||||
transactions,
|
||||
openingBalance: number,
|
||||
repo: CashflowAccountTransactionsRepo,
|
||||
query: ICashflowAccountTransactionsQuery
|
||||
) {
|
||||
super();
|
||||
|
||||
this.transactions = transactions;
|
||||
this.openingBalance = openingBalance;
|
||||
|
||||
this.runningBalance = runningAmount(this.openingBalance);
|
||||
this.repo = repo;
|
||||
this.query = query;
|
||||
this.numberFormat = query.numberFormat;
|
||||
this.baseCurrency = 'USD';
|
||||
this.runningBalance = runningAmount(this.repo.openingBalance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the transaction status.
|
||||
* @param {} transaction
|
||||
* @returns {BankTransactionStatus}
|
||||
*/
|
||||
private getTransactionStatus(transaction: any): BankTransactionStatus {
|
||||
const categorizedTrans = this.repo.uncategorizedTransactionsMapByRef.get(
|
||||
`${transaction.referenceType}-${transaction.referenceId}`
|
||||
);
|
||||
const matchedTrans = this.repo.matchedBankTransactionsMapByRef.get(
|
||||
`${transaction.referenceType}-${transaction.referenceId}`
|
||||
);
|
||||
if (!isEmpty(categorizedTrans)) {
|
||||
return BankTransactionStatus.Categorized;
|
||||
} else if (!isEmpty(matchedTrans)) {
|
||||
return BankTransactionStatus.Matched;
|
||||
} else {
|
||||
return BankTransactionStatus.Manual;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the uncategoized transaction id from the given transaction.
|
||||
* @param transaction
|
||||
* @returns {number|null}
|
||||
*/
|
||||
private getUncategorizedTransId(transaction: any): number {
|
||||
// The given transaction would be categorized, matched or not, so we'd take a look at
|
||||
// the categorized transaction first to get the id if not exist, then should look at the matched
|
||||
// transaction if not exist too, so the given transaction has no uncategorized transaction id.
|
||||
const categorizedTrans = this.repo.uncategorizedTransactionsMapByRef.get(
|
||||
`${transaction.referenceType}-${transaction.referenceId}`
|
||||
);
|
||||
const matchedTrans = this.repo.matchedBankTransactionsMapByRef.get(
|
||||
`${transaction.referenceType}-${transaction.referenceId}`
|
||||
);
|
||||
// Relation between the transaction and matching always been one-to-one.
|
||||
const firstCategorizedTrans = first(categorizedTrans);
|
||||
const firstMatchedTrans = first(matchedTrans);
|
||||
|
||||
return (
|
||||
firstCategorizedTrans?.id ||
|
||||
firstMatchedTrans?.uncategorizedTransactionId ||
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -44,6 +86,10 @@ export default class CashflowAccountTransactionReport extends FinancialSheet {
|
||||
* @returns {ICashflowAccountTransaction}
|
||||
*/
|
||||
private transactionNode = (transaction: any): ICashflowAccountTransaction => {
|
||||
const status = this.getTransactionStatus(transaction);
|
||||
const uncategorizedTransactionId =
|
||||
this.getUncategorizedTransId(transaction);
|
||||
|
||||
return {
|
||||
date: transaction.date,
|
||||
formattedDate: moment(transaction.date).format('YYYY-MM-DD'),
|
||||
@@ -67,6 +113,9 @@ export default class CashflowAccountTransactionReport extends FinancialSheet {
|
||||
|
||||
balance: 0,
|
||||
formattedBalance: '',
|
||||
status,
|
||||
formattedStatus: formatBankTransactionsStatus(status),
|
||||
uncategorizedTransactionId,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -146,6 +195,6 @@ export default class CashflowAccountTransactionReport extends FinancialSheet {
|
||||
* @returns {ICashflowAccountTransaction[]}
|
||||
*/
|
||||
public reportData(): ICashflowAccountTransaction[] {
|
||||
return this.transactionsNode(this.transactions);
|
||||
return this.transactionsNode(this.repo.transactions);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,59 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { ICashflowAccountTransactionsQuery, IPaginationMeta } from '@/interfaces';
|
||||
import * as R from 'ramda';
|
||||
import { ICashflowAccountTransactionsQuery } from '@/interfaces';
|
||||
import {
|
||||
groupMatchedBankTransactions,
|
||||
groupUncategorizedTransactions,
|
||||
} from './utils';
|
||||
|
||||
@Service()
|
||||
export default class CashflowAccountTransactionsRepo {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
export class CashflowAccountTransactionsRepo {
|
||||
private models: any;
|
||||
public query: ICashflowAccountTransactionsQuery;
|
||||
public transactions: any;
|
||||
public uncategorizedTransactions: any;
|
||||
public uncategorizedTransactionsMapByRef: Map<string, any>;
|
||||
public matchedBankTransactions: any;
|
||||
public matchedBankTransactionsMapByRef: Map<string, any>;
|
||||
public pagination: any;
|
||||
public openingBalance: any;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {any} models
|
||||
* @param {ICashflowAccountTransactionsQuery} query
|
||||
*/
|
||||
constructor(models: any, query: ICashflowAccountTransactionsQuery) {
|
||||
this.models = models;
|
||||
this.query = query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Async initalize the resources.
|
||||
*/
|
||||
async asyncInit() {
|
||||
await this.initCashflowAccountTransactions();
|
||||
await this.initCashflowAccountOpeningBalance();
|
||||
await this.initCategorizedTransactions();
|
||||
await this.initMatchedTransactions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the cashflow account transactions.
|
||||
* @param {number} tenantId -
|
||||
* @param {ICashflowAccountTransactionsQuery} query -
|
||||
*/
|
||||
async getCashflowAccountTransactions(
|
||||
tenantId: number,
|
||||
query: ICashflowAccountTransactionsQuery
|
||||
) {
|
||||
const { AccountTransaction } = this.tenancy.models(tenantId);
|
||||
async initCashflowAccountTransactions() {
|
||||
const { AccountTransaction } = this.models;
|
||||
|
||||
return AccountTransaction.query()
|
||||
.where('account_id', query.accountId)
|
||||
const { results, pagination } = await AccountTransaction.query()
|
||||
.where('account_id', this.query.accountId)
|
||||
.orderBy([
|
||||
{ column: 'date', order: 'desc' },
|
||||
{ column: 'created_at', order: 'desc' },
|
||||
])
|
||||
.pagination(query.page - 1, query.pageSize);
|
||||
.pagination(this.query.page - 1, this.query.pageSize);
|
||||
|
||||
this.transactions = results;
|
||||
this.pagination = pagination;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -34,22 +63,18 @@ export default class CashflowAccountTransactionsRepo {
|
||||
* @param {IPaginationMeta} pagination
|
||||
* @return {Promise<number>}
|
||||
*/
|
||||
async getCashflowAccountOpeningBalance(
|
||||
tenantId: number,
|
||||
accountId: number,
|
||||
pagination: IPaginationMeta
|
||||
): Promise<number> {
|
||||
const { AccountTransaction } = this.tenancy.models(tenantId);
|
||||
async initCashflowAccountOpeningBalance(): Promise<void> {
|
||||
const { AccountTransaction } = this.models;
|
||||
|
||||
// Retrieve the opening balance of credit and debit balances.
|
||||
const openingBalancesSubquery = AccountTransaction.query()
|
||||
.where('account_id', accountId)
|
||||
.where('account_id', this.query.accountId)
|
||||
.orderBy([
|
||||
{ column: 'date', order: 'desc' },
|
||||
{ column: 'created_at', order: 'desc' },
|
||||
])
|
||||
.limit(pagination.total)
|
||||
.offset(pagination.pageSize * (pagination.page - 1));
|
||||
.limit(this.pagination.total)
|
||||
.offset(this.pagination.pageSize * (this.pagination.page - 1));
|
||||
|
||||
// Sumation of credit and debit balance.
|
||||
const openingBalances = await AccountTransaction.query()
|
||||
@@ -60,6 +85,43 @@ export default class CashflowAccountTransactionsRepo {
|
||||
|
||||
const openingBalance = openingBalances.debit - openingBalances.credit;
|
||||
|
||||
return openingBalance;
|
||||
this.openingBalance = openingBalance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the uncategorized transactions of the bank account.
|
||||
*/
|
||||
async initCategorizedTransactions() {
|
||||
const { UncategorizedCashflowTransaction } = this.models;
|
||||
const refs = this.transactions.map((t) => [t.referenceType, t.referenceId]);
|
||||
|
||||
const uncategorizedTransactions =
|
||||
await UncategorizedCashflowTransaction.query().whereIn(
|
||||
['categorizeRefType', 'categorizeRefId'],
|
||||
refs
|
||||
);
|
||||
|
||||
this.uncategorizedTransactions = uncategorizedTransactions;
|
||||
this.uncategorizedTransactionsMapByRef = groupUncategorizedTransactions(
|
||||
uncategorizedTransactions
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the matched bank transactions of the bank account.
|
||||
*/
|
||||
async initMatchedTransactions(): Promise<void> {
|
||||
const { MatchedBankTransaction } = this.models;
|
||||
const refs = this.transactions.map((t) => [t.referenceType, t.referenceId]);
|
||||
|
||||
const matchedBankTransactions =
|
||||
await MatchedBankTransaction.query().whereIn(
|
||||
['referenceType', 'referenceId'],
|
||||
refs
|
||||
);
|
||||
this.matchedBankTransactions = matchedBankTransactions;
|
||||
this.matchedBankTransactionsMapByRef = groupMatchedBankTransactions(
|
||||
matchedBankTransactions
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,16 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { includes } from 'lodash';
|
||||
import * as qim from 'qim';
|
||||
import { ICashflowAccountTransactionsQuery, IAccount } from '@/interfaces';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import FinancialSheet from '../FinancialSheet';
|
||||
import CashflowAccountTransactionsRepo from './CashflowAccountTransactionsRepo';
|
||||
import CashflowAccountTransactionsReport from './CashflowAccountTransactions';
|
||||
import { ACCOUNT_TYPE } from '@/data/AccountTypes';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import { ERRORS } from './constants';
|
||||
import { CashflowAccountTransactionReport } from './CashflowAccountTransactions';
|
||||
import I18nService from '@/services/I18n/I18nService';
|
||||
import { CashflowAccountTransactionsRepo } from './CashflowAccountTransactionsRepo';
|
||||
|
||||
@Service()
|
||||
export default class CashflowAccountTransactionsService extends FinancialSheet {
|
||||
@Inject()
|
||||
tenancy: TenancyService;
|
||||
|
||||
@Inject()
|
||||
cashflowTransactionsRepo: CashflowAccountTransactionsRepo;
|
||||
|
||||
@Inject()
|
||||
i18nService: I18nService;
|
||||
private tenancy: TenancyService;
|
||||
|
||||
/**
|
||||
* Defaults balance sheet filter query.
|
||||
@@ -50,59 +40,24 @@ export default class CashflowAccountTransactionsService extends FinancialSheet {
|
||||
tenantId: number,
|
||||
query: ICashflowAccountTransactionsQuery
|
||||
) {
|
||||
const { Account } = this.tenancy.models(tenantId);
|
||||
const models = this.tenancy.models(tenantId);
|
||||
const parsedQuery = { ...this.defaultQuery, ...query };
|
||||
|
||||
// Retrieve the given account or throw not found service error.
|
||||
const account = await Account.query().findById(parsedQuery.accountId);
|
||||
|
||||
// Validates the cashflow account type.
|
||||
this.validateCashflowAccountType(account);
|
||||
|
||||
// Retrieve the cashflow account transactions.
|
||||
const { results: transactions, pagination } =
|
||||
await this.cashflowTransactionsRepo.getCashflowAccountTransactions(
|
||||
tenantId,
|
||||
parsedQuery
|
||||
);
|
||||
// Retrieve the cashflow account opening balance.
|
||||
const openingBalance =
|
||||
await this.cashflowTransactionsRepo.getCashflowAccountOpeningBalance(
|
||||
tenantId,
|
||||
parsedQuery.accountId,
|
||||
pagination
|
||||
);
|
||||
// Retrieve the computed report.
|
||||
const report = new CashflowAccountTransactionsReport(
|
||||
transactions,
|
||||
openingBalance,
|
||||
// Initalize the bank transactions report repository.
|
||||
const cashflowTransactionsRepo = new CashflowAccountTransactionsRepo(
|
||||
models,
|
||||
parsedQuery
|
||||
);
|
||||
const reportTranasctions = report.reportData();
|
||||
await cashflowTransactionsRepo.asyncInit();
|
||||
|
||||
return {
|
||||
transactions: this.i18nService.i18nApply(
|
||||
[[qim.$each, 'formattedTransactionType']],
|
||||
reportTranasctions,
|
||||
tenantId
|
||||
),
|
||||
pagination,
|
||||
};
|
||||
}
|
||||
// Retrieve the computed report.
|
||||
const report = new CashflowAccountTransactionReport(
|
||||
cashflowTransactionsRepo,
|
||||
parsedQuery
|
||||
);
|
||||
const transactions = report.reportData();
|
||||
const pagination = cashflowTransactionsRepo.pagination;
|
||||
|
||||
/**
|
||||
* Validates the cashflow account type.
|
||||
* @param {IAccount} account -
|
||||
*/
|
||||
private validateCashflowAccountType(account: IAccount) {
|
||||
const cashflowTypes = [
|
||||
ACCOUNT_TYPE.CASH,
|
||||
ACCOUNT_TYPE.CREDIT_CARD,
|
||||
ACCOUNT_TYPE.BANK,
|
||||
];
|
||||
|
||||
if (!includes(cashflowTypes, account.accountType)) {
|
||||
throw new ServiceError(ERRORS.ACCOUNT_ID_HAS_INVALID_TYPE);
|
||||
}
|
||||
return { transactions, pagination };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
export const ERRORS = {
|
||||
ACCOUNT_ID_HAS_INVALID_TYPE: 'ACCOUNT_ID_HAS_INVALID_TYPE',
|
||||
};
|
||||
|
||||
export enum BankTransactionStatus {
|
||||
Categorized = 'categorized',
|
||||
Matched = 'matched',
|
||||
Manual = 'manual',
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import * as R from 'ramda';
|
||||
|
||||
export const groupUncategorizedTransactions = (
|
||||
uncategorizedTransactions: any
|
||||
): Map<string, any> => {
|
||||
return new Map(
|
||||
R.toPairs(
|
||||
R.groupBy(
|
||||
(transaction) =>
|
||||
`${transaction.categorizeRefType}-${transaction.categorizeRefId}`,
|
||||
uncategorizedTransactions
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export const groupMatchedBankTransactions = (
|
||||
uncategorizedTransactions: any
|
||||
): Map<string, any> => {
|
||||
return new Map(
|
||||
R.toPairs(
|
||||
R.groupBy(
|
||||
(transaction) =>
|
||||
`${transaction.referenceType}-${transaction.referenceId}`,
|
||||
uncategorizedTransactions
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export const formatBankTransactionsStatus = (status) => {
|
||||
switch (status) {
|
||||
case 'categorized':
|
||||
return 'Categorized';
|
||||
case 'matched':
|
||||
return 'Matched';
|
||||
case 'manual':
|
||||
return 'Manual';
|
||||
}
|
||||
};
|
||||
@@ -15,14 +15,10 @@ import { ServiceError } from '@/exceptions';
|
||||
import { getUniqueImportableValue, trimObject } from './_utils';
|
||||
import { ImportableResources } from './ImportableResources';
|
||||
import ResourceService from '../Resource/ResourceService';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
import { Import } from '@/system/models';
|
||||
|
||||
@Service()
|
||||
export class ImportFileCommon {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private importFileValidator: ImportFileDataValidator;
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
import { ImportFilePreviewPOJO } from './interfaces';
|
||||
import { ImportFileProcess } from './ImportFileProcess';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import events from '@/subscribers/events';
|
||||
import { IImportFileCommitedEventPayload } from '@/interfaces/Import';
|
||||
|
||||
@Service()
|
||||
export class ImportFileProcessCommit {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private importFile: ImportFileProcess;
|
||||
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
/**
|
||||
* Commits the imported file.
|
||||
* @param {number} tenantId
|
||||
* @param {number} importId
|
||||
* @returns {Promise<ImportFilePreviewPOJO>}
|
||||
*/
|
||||
public async commit(
|
||||
tenantId: number,
|
||||
importId: number
|
||||
): Promise<ImportFilePreviewPOJO> {
|
||||
const knex = this.tenancy.knex(tenantId);
|
||||
const trx = await knex.transaction({ isolationLevel: 'read uncommitted' });
|
||||
|
||||
const meta = await this.importFile.import(tenantId, importId, trx);
|
||||
|
||||
// Commit the successed transaction.
|
||||
await trx.commit();
|
||||
|
||||
// Triggers `onImportFileCommitted` event.
|
||||
await this.eventPublisher.emitAsync(events.import.onImportCommitted, {
|
||||
meta,
|
||||
importId,
|
||||
tenantId,
|
||||
} as IImportFileCommitedEventPayload);
|
||||
|
||||
return meta;
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { ImportFileProcess } from './ImportFileProcess';
|
||||
import { ImportFilePreview } from './ImportFilePreview';
|
||||
import { ImportSampleService } from './ImportSample';
|
||||
import { ImportFileMeta } from './ImportFileMeta';
|
||||
import { ImportFileProcessCommit } from './ImportFileProcessCommit';
|
||||
|
||||
@Inject()
|
||||
export class ImportResourceApplication {
|
||||
@@ -27,6 +28,9 @@ export class ImportResourceApplication {
|
||||
@Inject()
|
||||
private importMetaService: ImportFileMeta;
|
||||
|
||||
@Inject()
|
||||
private importProcessCommit: ImportFileProcessCommit;
|
||||
|
||||
/**
|
||||
* Reads the imported file and stores the import file meta under unqiue id.
|
||||
* @param {number} tenantId -
|
||||
@@ -74,12 +78,12 @@ export class ImportResourceApplication {
|
||||
* @returns {Promise<ImportFilePreviewPOJO>}
|
||||
*/
|
||||
public async process(tenantId: number, importId: number) {
|
||||
return this.importProcessService.import(tenantId, importId);
|
||||
return this.importProcessCommit.commit(tenantId, importId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the import meta of the given import id.
|
||||
* @param {number} tenantId -
|
||||
* @param {number} tenantId -
|
||||
* @param {string} importId - Import id.
|
||||
* @returns {}
|
||||
*/
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import moment from 'moment';
|
||||
import { sumBy } from 'lodash';
|
||||
import { sumBy, chain } from 'lodash';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { Knex } from 'knex';
|
||||
import { AccountNormal, IBillPayment, ILedgerEntry } from '@/interfaces';
|
||||
import {
|
||||
AccountNormal,
|
||||
IBillPayment,
|
||||
IBillPaymentEntry,
|
||||
ILedger,
|
||||
ILedgerEntry,
|
||||
} from '@/interfaces';
|
||||
import Ledger from '@/services/Accounting/Ledger';
|
||||
import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
@@ -21,6 +27,7 @@ export class BillPaymentGLEntries {
|
||||
* @param {number} tenantId
|
||||
* @param {number} billPaymentId
|
||||
* @param {Knex.Transaction} trx
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public writePaymentGLEntries = async (
|
||||
tenantId: number,
|
||||
@@ -65,6 +72,7 @@ export class BillPaymentGLEntries {
|
||||
* @param {number} tenantId
|
||||
* @param {number} billPaymentId
|
||||
* @param {Knex.Transaction} trx
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public rewritePaymentGLEntries = async (
|
||||
tenantId: number,
|
||||
@@ -102,7 +110,7 @@ export class BillPaymentGLEntries {
|
||||
* @param {IBillPayment} billPayment
|
||||
* @returns {}
|
||||
*/
|
||||
private getPaymentCommonEntry = (billPayment: IBillPayment) => {
|
||||
private getPaymentCommonEntry = (billPayment: IBillPayment): ILedgerEntry => {
|
||||
const formattedDate = moment(billPayment.paymentDate).format('YYYY-MM-DD');
|
||||
|
||||
return {
|
||||
@@ -127,7 +135,7 @@ export class BillPaymentGLEntries {
|
||||
|
||||
/**
|
||||
* Calculates the payment total exchange gain/loss.
|
||||
* @param {IBillPayment} paymentReceive - Payment receive with entries.
|
||||
* @param {IBillPayment} paymentReceive - Payment receive with entries.
|
||||
* @returns {number}
|
||||
*/
|
||||
private getPaymentExGainOrLoss = (billPayment: IBillPayment): number => {
|
||||
@@ -141,10 +149,10 @@ export class BillPaymentGLEntries {
|
||||
|
||||
/**
|
||||
* Retrieves the payment exchange gain/loss entries.
|
||||
* @param {IBillPayment} billPayment -
|
||||
* @param {number} APAccountId -
|
||||
* @param {number} gainLossAccountId -
|
||||
* @param {string} baseCurrency -
|
||||
* @param {IBillPayment} billPayment -
|
||||
* @param {number} APAccountId -
|
||||
* @param {number} gainLossAccountId -
|
||||
* @param {string} baseCurrency -
|
||||
* @returns {ILedgerEntry[]}
|
||||
*/
|
||||
private getPaymentExGainOrLossEntries = (
|
||||
@@ -186,7 +194,7 @@ export class BillPaymentGLEntries {
|
||||
|
||||
/**
|
||||
* Retrieves the payment deposit GL entry.
|
||||
* @param {IBillPayment} billPayment
|
||||
* @param {IBillPayment} billPayment
|
||||
* @returns {ILedgerEntry}
|
||||
*/
|
||||
private getPaymentGLEntry = (billPayment: IBillPayment): ILedgerEntry => {
|
||||
@@ -198,6 +206,7 @@ export class BillPaymentGLEntries {
|
||||
accountId: billPayment.paymentAccountId,
|
||||
accountNormal: AccountNormal.DEBIT,
|
||||
index: 2,
|
||||
indexGroup: 10,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -226,8 +235,8 @@ export class BillPaymentGLEntries {
|
||||
|
||||
/**
|
||||
* Retrieves the payment GL entries.
|
||||
* @param {IBillPayment} billPayment
|
||||
* @param {number} APAccountId
|
||||
* @param {IBillPayment} billPayment
|
||||
* @param {number} APAccountId
|
||||
* @returns {ILedgerEntry[]}
|
||||
*/
|
||||
private getPaymentGLEntries = (
|
||||
@@ -254,10 +263,53 @@ export class BillPaymentGLEntries {
|
||||
return [paymentEntry, payableEntry, ...exGainLossEntries];
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* BEFORE APPLYING TO PAYMENT TO BILLS.
|
||||
* -----------------------------------------
|
||||
* - Cash/Bank - Credit.
|
||||
* - Prepard Expenses - Debit
|
||||
*
|
||||
* AFTER APPLYING BILLS TO PAYMENT.
|
||||
* -----------------------------------------
|
||||
* - Prepard Expenses - Credit
|
||||
* - A/P - Debit
|
||||
*
|
||||
* @param {number} APAccountId - A/P account id.
|
||||
* @param {IBillPayment} billPayment
|
||||
*/
|
||||
private getPrepardExpenseGLEntries = (
|
||||
APAccountId: number,
|
||||
billPayment: IBillPayment
|
||||
) => {
|
||||
const prepardExpenseEntry = this.getPrepardExpenseEntry(billPayment);
|
||||
const withdrawalEntry = this.getPaymentGLEntry(billPayment);
|
||||
|
||||
const paymentLinesEntries = chain(billPayment.entries)
|
||||
.map((billPaymentEntry) => {
|
||||
const APEntry = this.getAccountPayablePaymentLineEntry(
|
||||
APAccountId,
|
||||
billPayment,
|
||||
billPaymentEntry
|
||||
);
|
||||
const creditPrepardExpenseEntry = this.getCreditPrepardExpenseEntry(
|
||||
billPayment,
|
||||
billPaymentEntry
|
||||
);
|
||||
return [creditPrepardExpenseEntry, APEntry];
|
||||
})
|
||||
.flatten()
|
||||
.value();
|
||||
const prepardExpenseEntries = [prepardExpenseEntry, withdrawalEntry];
|
||||
const combinedEntries = [...prepardExpenseEntries, ...paymentLinesEntries];
|
||||
|
||||
return combinedEntries;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the bill payment ledger.
|
||||
* @param {IBillPayment} billPayment
|
||||
* @param {number} APAccountId
|
||||
* @param {IBillPayment} billPayment
|
||||
* @param {number} APAccountId
|
||||
* @returns {Ledger}
|
||||
*/
|
||||
private getBillPaymentLedger = (
|
||||
@@ -266,12 +318,79 @@ export class BillPaymentGLEntries {
|
||||
gainLossAccountId: number,
|
||||
baseCurrency: string
|
||||
): Ledger => {
|
||||
const entries = this.getPaymentGLEntries(
|
||||
billPayment,
|
||||
APAccountId,
|
||||
gainLossAccountId,
|
||||
baseCurrency
|
||||
);
|
||||
const entries = billPayment.isPrepardExpense
|
||||
? this.getPrepardExpenseGLEntries(APAccountId, billPayment)
|
||||
: this.getPaymentGLEntries(
|
||||
billPayment,
|
||||
APAccountId,
|
||||
gainLossAccountId,
|
||||
baseCurrency
|
||||
);
|
||||
return new Ledger(entries);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the prepard expense GL entry.
|
||||
* @param {IBillPayment} billPayment
|
||||
* @returns {ILedgerEntry}
|
||||
*/
|
||||
private getPrepardExpenseEntry = (
|
||||
billPayment: IBillPayment
|
||||
): ILedgerEntry => {
|
||||
const commonJournal = this.getPaymentCommonEntry(billPayment);
|
||||
|
||||
return {
|
||||
...commonJournal,
|
||||
debit: billPayment.localAmount,
|
||||
accountId: billPayment.prepardExpensesAccountId,
|
||||
accountNormal: AccountNormal.DEBIT,
|
||||
indexGroup: 10,
|
||||
index: 1,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the GL entries of credit prepard expense for the give payment line.
|
||||
* @param {IBillPayment} billPayment
|
||||
* @param {IBillPaymentEntry} billPaymentEntry
|
||||
* @returns {ILedgerEntry}
|
||||
*/
|
||||
private getCreditPrepardExpenseEntry = (
|
||||
billPayment: IBillPayment,
|
||||
billPaymentEntry: IBillPaymentEntry
|
||||
) => {
|
||||
const commonJournal = this.getPaymentCommonEntry(billPayment);
|
||||
|
||||
return {
|
||||
...commonJournal,
|
||||
credit: billPaymentEntry.paymentAmount,
|
||||
accountId: billPayment.prepardExpensesAccountId,
|
||||
accountNormal: AccountNormal.DEBIT,
|
||||
index: 2,
|
||||
indexGroup: 20,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the A/P debit of the payment line.
|
||||
* @param {number} APAccountId
|
||||
* @param {IBillPayment} billPayment
|
||||
* @param {IBillPaymentEntry} billPaymentEntry
|
||||
* @returns {ILedgerEntry}
|
||||
*/
|
||||
private getAccountPayablePaymentLineEntry = (
|
||||
APAccountId: number,
|
||||
billPayment: IBillPayment,
|
||||
billPaymentEntry: IBillPaymentEntry
|
||||
): ILedgerEntry => {
|
||||
const commonJournal = this.getPaymentCommonEntry(billPayment);
|
||||
|
||||
return {
|
||||
...commonJournal,
|
||||
debit: billPaymentEntry.paymentAmount,
|
||||
accountId: APAccountId,
|
||||
index: 1,
|
||||
indexGroup: 20,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,6 +17,10 @@ export class PaymentWriteGLEntriesSubscriber {
|
||||
*/
|
||||
public attach(bus) {
|
||||
bus.subscribe(events.billPayment.onCreated, this.handleWriteJournalEntries);
|
||||
bus.subscribe(
|
||||
events.billPayment.onPrepardExpensesApplied,
|
||||
this.handleWritePrepardExpenseGLEntries
|
||||
);
|
||||
bus.subscribe(
|
||||
events.billPayment.onEdited,
|
||||
this.handleRewriteJournalEntriesOncePaymentEdited
|
||||
@@ -28,7 +32,8 @@ export class PaymentWriteGLEntriesSubscriber {
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle bill payment writing journal entries once created.
|
||||
* Handles bill payment writing journal entries once created.
|
||||
* @param {IBillPaymentEventCreatedPayload} payload -
|
||||
*/
|
||||
private handleWriteJournalEntries = async ({
|
||||
tenantId,
|
||||
@@ -44,6 +49,22 @@ export class PaymentWriteGLEntriesSubscriber {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles rewrite prepard expense GL entries once the bill payment applying to bills.
|
||||
* @param {IBillPaymentEventCreatedPayload} payload -
|
||||
*/
|
||||
private handleWritePrepardExpenseGLEntries = async ({
|
||||
tenantId,
|
||||
billPaymentId,
|
||||
trx,
|
||||
}: IBillPaymentEventCreatedPayload) => {
|
||||
await this.billPaymentGLEntries.rewritePaymentGLEntries(
|
||||
tenantId,
|
||||
billPaymentId,
|
||||
trx
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle bill payment re-writing journal entries once the payment transaction be edited.
|
||||
*/
|
||||
|
||||
@@ -4,12 +4,16 @@ import { omit, sumBy } from 'lodash';
|
||||
import { IBillPayment, IBillPaymentDTO, IVendor } from '@/interfaces';
|
||||
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
|
||||
import { formatDateFields } from '@/utils';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
|
||||
@Service()
|
||||
export class CommandBillPaymentDTOTransformer {
|
||||
@Inject()
|
||||
private branchDTOTransform: BranchTransactionDTOTransform;
|
||||
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Transforms create/edit DTO to model.
|
||||
* @param {number} tenantId
|
||||
@@ -23,14 +27,27 @@ export class CommandBillPaymentDTOTransformer {
|
||||
vendor: IVendor,
|
||||
oldBillPayment?: IBillPayment
|
||||
): Promise<IBillPayment> {
|
||||
const { accountRepository } = this.tenancy.repositories(tenantId);
|
||||
const appliedAmount = sumBy(billPaymentDTO.entries, 'paymentAmount');
|
||||
|
||||
const hasPrepardExpenses = appliedAmount < billPaymentDTO.amount;
|
||||
const prepardExpensesAccount = hasPrepardExpenses
|
||||
? await accountRepository.findOrCreatePrepardExpenses()
|
||||
: null;
|
||||
const prepardExpensesAccountId =
|
||||
hasPrepardExpenses && prepardExpensesAccount
|
||||
? billPaymentDTO.prepardExpensesAccountId ?? prepardExpensesAccount?.id
|
||||
: billPaymentDTO.prepardExpensesAccountId;
|
||||
|
||||
const initialDTO = {
|
||||
...formatDateFields(omit(billPaymentDTO, ['attachments']), [
|
||||
'paymentDate',
|
||||
]),
|
||||
amount: sumBy(billPaymentDTO.entries, 'paymentAmount'),
|
||||
appliedAmount,
|
||||
currencyCode: vendor.currencyCode,
|
||||
exchangeRate: billPaymentDTO.exchangeRate || 1,
|
||||
entries: billPaymentDTO.entries,
|
||||
prepardExpensesAccountId,
|
||||
};
|
||||
return R.compose(
|
||||
this.branchDTOTransform.transformDTO<IBillPayment>(tenantId)
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import { Knex } from 'knex';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import PromisePool, { ProcessHandler } from '@supercharge/promise-pool';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import {
|
||||
IBillPayment,
|
||||
IBillPrepardExpensesAppliedEventPayload,
|
||||
IPaymentPrepardExpensesAppliedEventPayload,
|
||||
} from '@/interfaces';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import events from '@/subscribers/events';
|
||||
|
||||
@Service()
|
||||
export class AutoApplyPrepardExpenses {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
/**
|
||||
* Auto apply prepard expenses to the given bill.
|
||||
* @param {number} tenantId
|
||||
* @param {number} billId
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async autoApplyPrepardExpensesToBill(
|
||||
tenantId: number,
|
||||
billId: number,
|
||||
trx?: Knex.Transaction
|
||||
): Promise<void> {
|
||||
const { BillPayment, Bill } = this.tenancy.models(tenantId);
|
||||
|
||||
const bill = await Bill.query(trx).findById(billId).throwIfNotFound();
|
||||
|
||||
const unappliedPayments = await BillPayment.query(trx)
|
||||
.where('vendorId', bill.vendorId)
|
||||
.whereRaw('amount - applied_amount > 0')
|
||||
.whereNotNull('prepardExpensesAccountId');
|
||||
|
||||
let unappliedAmount = bill.total;
|
||||
let appliedTotalAmount = 0; // Total applied amount after applying.
|
||||
|
||||
const precessHandler: ProcessHandler<IBillPayment, void> = async (
|
||||
unappliedPayment: IBillPayment,
|
||||
index: number,
|
||||
pool
|
||||
) => {
|
||||
const appliedAmount = Math.min(unappliedAmount, unappliedPayment.amount);
|
||||
unappliedAmount = unappliedAmount - appliedAmount;
|
||||
appliedTotalAmount += appliedAmount;
|
||||
|
||||
// Stop applying once the unapplied amount reach zero or less.
|
||||
if (appliedAmount <= 0) {
|
||||
pool.stop();
|
||||
return;
|
||||
}
|
||||
await this.applyBillToPaymentMade(
|
||||
tenantId,
|
||||
unappliedPayment.id,
|
||||
bill.id,
|
||||
appliedAmount,
|
||||
trx
|
||||
);
|
||||
};
|
||||
await PromisePool.withConcurrency(1)
|
||||
.for(unappliedPayments)
|
||||
.process(precessHandler);
|
||||
|
||||
// Increase the paid amount of the purchase invoice.
|
||||
await Bill.changePaymentAmount(billId, appliedTotalAmount, trx);
|
||||
|
||||
// Triggers `onBillPrepardExpensesApplied` event.
|
||||
await this.eventPublisher.emitAsync(events.bill.onPrepardExpensesApplied, {
|
||||
tenantId,
|
||||
billId,
|
||||
trx,
|
||||
} as IBillPrepardExpensesAppliedEventPayload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the given bill to payment made transaction.
|
||||
* @param {number} tenantId
|
||||
* @param {number} billPaymentId
|
||||
* @param {number} billId
|
||||
* @param {number} appliedAmount
|
||||
* @param {Knex.Transaction} trx
|
||||
*/
|
||||
public applyBillToPaymentMade = async (
|
||||
tenantId: number,
|
||||
billPaymentId: number,
|
||||
billId: number,
|
||||
appliedAmount: number,
|
||||
trx?: Knex.Transaction
|
||||
) => {
|
||||
const { BillPaymentEntry, BillPayment } = this.tenancy.models(tenantId);
|
||||
|
||||
await BillPaymentEntry.query(trx).insert({
|
||||
billPaymentId,
|
||||
billId,
|
||||
paymentAmount: appliedAmount,
|
||||
});
|
||||
await BillPayment.query(trx).increment('appliedAmount', appliedAmount);
|
||||
|
||||
// Triggers `onBillPaymentPrepardExpensesApplied` event.
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.billPayment.onPrepardExpensesApplied,
|
||||
{
|
||||
tenantId,
|
||||
billPaymentId,
|
||||
billId,
|
||||
appliedAmount,
|
||||
trx,
|
||||
} as IPaymentPrepardExpensesAppliedEventPayload
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { AutoApplyPrepardExpenses } from '../AutoApplyPrepardExpenses';
|
||||
import events from '@/subscribers/events';
|
||||
import { IBillCreatedPayload } from '@/interfaces';
|
||||
|
||||
@Service()
|
||||
export class AutoApplyPrepardExpensesOnBillCreated {
|
||||
@Inject()
|
||||
private autoApplyPrepardExpenses: AutoApplyPrepardExpenses;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
*/
|
||||
public attach(bus) {
|
||||
bus.subscribe(
|
||||
events.bill.onCreated,
|
||||
this.handleAutoApplyPrepardExpensesOnBillCreated.bind(this)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the auto apply prepard expenses on bill created.
|
||||
* @param {IBillCreatedPayload} payload -
|
||||
*/
|
||||
private async handleAutoApplyPrepardExpensesOnBillCreated({
|
||||
tenantId,
|
||||
billId,
|
||||
trx,
|
||||
}: IBillCreatedPayload) {
|
||||
await this.autoApplyPrepardExpenses.autoApplyPrepardExpensesToBill(
|
||||
tenantId,
|
||||
billId,
|
||||
trx
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { Knex } from 'knex';
|
||||
import PromisePool, { ProcessHandler } from '@supercharge/promise-pool';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import events from '@/subscribers/events';
|
||||
import {
|
||||
IPaymentReceive,
|
||||
PaymentReceiveUnearnedRevenueAppliedEventPayload,
|
||||
SaleInvoiceAppliedUnearnedRevenueOnCreatedEventPayload,
|
||||
} from '@/interfaces';
|
||||
|
||||
@Service()
|
||||
export class AutoApplyUnearnedRevenue {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
/**
|
||||
* Auto apply invoice to advanced payment received transactions.
|
||||
* @param {number} tenantId
|
||||
* @param {number} invoiceId
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public async autoApplyUnearnedRevenueToInvoice(
|
||||
tenantId: number,
|
||||
saleInvoiceId: number,
|
||||
trx?: Knex.Transaction
|
||||
): Promise<void> {
|
||||
const { PaymentReceive, SaleInvoice } = this.tenancy.models(tenantId);
|
||||
|
||||
const invoice = await SaleInvoice.query(trx)
|
||||
.findById(saleInvoiceId)
|
||||
.throwIfNotFound();
|
||||
|
||||
const unappliedPayments = await PaymentReceive.query(trx)
|
||||
.where('customerId', invoice.customerId)
|
||||
.whereRaw('amount - applied_amount > 0')
|
||||
.whereNotNull('unearnedRevenueAccountId');
|
||||
|
||||
let unappliedAmount = invoice.total;
|
||||
let appliedTotalAmount = 0; // Total applied amount after applying.
|
||||
|
||||
const processHandler: ProcessHandler<
|
||||
IPaymentReceive,
|
||||
Promise<void>
|
||||
> = async (unappliedPayment: IPaymentReceive, index: number, pool) => {
|
||||
const appliedAmount = Math.min(unappliedAmount, unappliedPayment.amount);
|
||||
unappliedAmount = unappliedAmount - appliedAmount;
|
||||
appliedTotalAmount += appliedAmount;
|
||||
|
||||
// Stop applying once the unapplied amount reache zero or less.
|
||||
if (appliedAmount <= 0) {
|
||||
pool.stop();
|
||||
return;
|
||||
}
|
||||
await this.applyInvoiceToPaymentReceived(
|
||||
tenantId,
|
||||
unappliedPayment.id,
|
||||
invoice.id,
|
||||
appliedAmount,
|
||||
trx
|
||||
);
|
||||
};
|
||||
await PromisePool.withConcurrency(1)
|
||||
.for(unappliedPayments)
|
||||
.process(processHandler);
|
||||
|
||||
// Increase the paid amount of the sale invoice.
|
||||
await SaleInvoice.changePaymentAmount(
|
||||
saleInvoiceId,
|
||||
appliedTotalAmount,
|
||||
trx
|
||||
);
|
||||
// Triggers event `onSaleInvoiceUnearnedRevenue`.
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.saleInvoice.onUnearnedRevenueApplied,
|
||||
{
|
||||
tenantId,
|
||||
saleInvoiceId,
|
||||
trx,
|
||||
} as SaleInvoiceAppliedUnearnedRevenueOnCreatedEventPayload
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the given invoice to payment received transaction.
|
||||
* @param {number} tenantId
|
||||
* @param {number} paymentReceivedId
|
||||
* @param {number} invoiceId
|
||||
* @param {number} appliedAmount
|
||||
* @param {Knex.Transaction} trx
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public applyInvoiceToPaymentReceived = async (
|
||||
tenantId: number,
|
||||
paymentReceiveId: number,
|
||||
invoiceId: number,
|
||||
appliedAmount: number,
|
||||
trx?: Knex.Transaction
|
||||
): Promise<void> => {
|
||||
const { PaymentReceiveEntry, PaymentReceive } =
|
||||
this.tenancy.models(tenantId);
|
||||
|
||||
await PaymentReceiveEntry.query(trx).insert({
|
||||
paymentReceiveId,
|
||||
invoiceId,
|
||||
paymentAmount: appliedAmount,
|
||||
});
|
||||
await PaymentReceive.query(trx).increment('appliedAmount', appliedAmount);
|
||||
|
||||
// Triggers the event `onPaymentReceivedUnearnedRevenue`.
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.paymentReceive.onUnearnedRevenueApplied,
|
||||
{
|
||||
tenantId,
|
||||
paymentReceiveId,
|
||||
saleInvoiceId: invoiceId,
|
||||
appliedAmount,
|
||||
trx,
|
||||
} as PaymentReceiveUnearnedRevenueAppliedEventPayload
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import { PaymentReceiveValidators } from './PaymentReceiveValidators';
|
||||
import { PaymentReceiveIncrement } from './PaymentReceiveIncrement';
|
||||
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
|
||||
import { formatDateFields } from '@/utils';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
|
||||
@Service()
|
||||
export class PaymentReceiveDTOTransformer {
|
||||
@@ -23,6 +24,9 @@ export class PaymentReceiveDTOTransformer {
|
||||
@Inject()
|
||||
private branchDTOTransform: BranchTransactionDTOTransform;
|
||||
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Transformes the create payment receive DTO to model object.
|
||||
* @param {number} tenantId
|
||||
@@ -36,7 +40,8 @@ export class PaymentReceiveDTOTransformer {
|
||||
paymentReceiveDTO: IPaymentReceiveCreateDTO | IPaymentReceiveEditDTO,
|
||||
oldPaymentReceive?: IPaymentReceive
|
||||
): Promise<IPaymentReceive> {
|
||||
const paymentAmount = sumBy(paymentReceiveDTO.entries, 'paymentAmount');
|
||||
const { accountRepository } = this.tenancy.repositories(tenantId);
|
||||
const appliedAmount = sumBy(paymentReceiveDTO.entries, 'paymentAmount');
|
||||
|
||||
// Retreive the next invoice number.
|
||||
const autoNextNumber =
|
||||
@@ -50,17 +55,29 @@ export class PaymentReceiveDTOTransformer {
|
||||
|
||||
this.validators.validatePaymentNoRequire(paymentReceiveNo);
|
||||
|
||||
const hasUnearnedPayment = appliedAmount < paymentReceiveDTO.amount;
|
||||
const unearnedRevenueAccount = hasUnearnedPayment
|
||||
? await accountRepository.findOrCreateUnearnedRevenue()
|
||||
: null;
|
||||
|
||||
const unearnedRevenueAccountId =
|
||||
hasUnearnedPayment && unearnedRevenueAccount
|
||||
? paymentReceiveDTO.unearnedRevenueAccountId ??
|
||||
unearnedRevenueAccount?.id
|
||||
: paymentReceiveDTO.unearnedRevenueAccountId;
|
||||
|
||||
const initialDTO = {
|
||||
...formatDateFields(omit(paymentReceiveDTO, ['entries', 'attachments']), [
|
||||
'paymentDate',
|
||||
]),
|
||||
amount: paymentAmount,
|
||||
appliedAmount,
|
||||
currencyCode: customer.currencyCode,
|
||||
...(paymentReceiveNo ? { paymentReceiveNo } : {}),
|
||||
exchangeRate: paymentReceiveDTO.exchangeRate || 1,
|
||||
entries: paymentReceiveDTO.entries.map((entry) => ({
|
||||
...entry,
|
||||
})),
|
||||
unearnedRevenueAccountId,
|
||||
};
|
||||
return R.compose(
|
||||
this.branchDTOTransform.transformDTO<IPaymentReceive>(tenantId)
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { sumBy } from 'lodash';
|
||||
import { Knex } from 'knex';
|
||||
import Ledger from '@/services/Accounting/Ledger';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import {
|
||||
IPaymentReceive,
|
||||
ILedgerEntry,
|
||||
AccountNormal,
|
||||
IPaymentReceiveGLCommonEntry,
|
||||
} from '@/interfaces';
|
||||
import { IPaymentReceive, ILedgerEntry, AccountNormal } from '@/interfaces';
|
||||
import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
|
||||
import { TenantMetadata } from '@/system/models';
|
||||
import { PaymentReceivedGLCommon } from './PaymentReceivedGLCommon';
|
||||
|
||||
@Service()
|
||||
export class PaymentReceiveGLEntries {
|
||||
export class PaymentReceiveGLEntries extends PaymentReceivedGLCommon {
|
||||
@Inject()
|
||||
private tenancy: TenancyService;
|
||||
|
||||
@@ -22,9 +17,9 @@ export class PaymentReceiveGLEntries {
|
||||
|
||||
/**
|
||||
* Writes payment GL entries to the storage.
|
||||
* @param {number} tenantId
|
||||
* @param {number} paymentReceiveId
|
||||
* @param {Knex.Transaction} trx
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {number} paymentReceiveId - Payment received id.
|
||||
* @param {Knex.Transaction} trx - Knex transaction.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public writePaymentGLEntries = async (
|
||||
@@ -34,14 +29,19 @@ export class PaymentReceiveGLEntries {
|
||||
): Promise<void> => {
|
||||
const { PaymentReceive } = this.tenancy.models(tenantId);
|
||||
|
||||
// Retrieves the given tenant metadata.
|
||||
const tenantMeta = await TenantMetadata.query().findOne({ tenantId });
|
||||
|
||||
// Retrieves the payment receive with associated entries.
|
||||
const paymentReceive = await PaymentReceive.query(trx)
|
||||
.findById(paymentReceiveId)
|
||||
.withGraphFetched('entries.invoice');
|
||||
|
||||
// Cannot continue if the received payment is unearned revenue type,
|
||||
// that type of transactions have different type of GL entries.
|
||||
if (paymentReceive.unearnedRevenueAccountId) {
|
||||
return;
|
||||
}
|
||||
// Retrieves the given tenant metadata.
|
||||
const tenantMeta = await TenantMetadata.query().findOne({ tenantId });
|
||||
|
||||
// Retrives the payment receive ledger.
|
||||
const ledger = await this.getPaymentReceiveGLedger(
|
||||
tenantId,
|
||||
@@ -53,25 +53,6 @@ export class PaymentReceiveGLEntries {
|
||||
await this.ledgerStorage.commit(tenantId, ledger, trx);
|
||||
};
|
||||
|
||||
/**
|
||||
* Reverts the given payment receive GL entries.
|
||||
* @param {number} tenantId
|
||||
* @param {number} paymentReceiveId
|
||||
* @param {Knex.Transaction} trx
|
||||
*/
|
||||
public revertPaymentGLEntries = async (
|
||||
tenantId: number,
|
||||
paymentReceiveId: number,
|
||||
trx?: Knex.Transaction
|
||||
) => {
|
||||
await this.ledgerStorage.deleteByReference(
|
||||
tenantId,
|
||||
paymentReceiveId,
|
||||
'PaymentReceive',
|
||||
trx
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Rewrites the given payment receive GL entries.
|
||||
* @param {number} tenantId
|
||||
@@ -92,10 +73,10 @@ export class PaymentReceiveGLEntries {
|
||||
|
||||
/**
|
||||
* Retrieves the payment receive general ledger.
|
||||
* @param {number} tenantId -
|
||||
* @param {IPaymentReceive} paymentReceive -
|
||||
* @param {string} baseCurrencyCode -
|
||||
* @param {Knex.Transaction} trx -
|
||||
* @param {number} tenantId -
|
||||
* @param {IPaymentReceive} paymentReceive -
|
||||
* @param {string} baseCurrencyCode -
|
||||
* @param {Knex.Transaction} trx -
|
||||
* @returns {Ledger}
|
||||
*/
|
||||
public getPaymentReceiveGLedger = async (
|
||||
@@ -126,100 +107,9 @@ export class PaymentReceiveGLEntries {
|
||||
return new Ledger(ledgerEntries);
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates the payment total exchange gain/loss.
|
||||
* @param {IBillPayment} paymentReceive - Payment receive with entries.
|
||||
* @returns {number}
|
||||
*/
|
||||
private getPaymentExGainOrLoss = (
|
||||
paymentReceive: IPaymentReceive
|
||||
): number => {
|
||||
return sumBy(paymentReceive.entries, (entry) => {
|
||||
const paymentLocalAmount =
|
||||
entry.paymentAmount * paymentReceive.exchangeRate;
|
||||
const invoicePayment = entry.paymentAmount * entry.invoice.exchangeRate;
|
||||
|
||||
return paymentLocalAmount - invoicePayment;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the common entry of payment receive.
|
||||
* @param {IPaymentReceive} paymentReceive
|
||||
* @returns {}
|
||||
*/
|
||||
private getPaymentReceiveCommonEntry = (
|
||||
paymentReceive: IPaymentReceive
|
||||
): IPaymentReceiveGLCommonEntry => {
|
||||
return {
|
||||
debit: 0,
|
||||
credit: 0,
|
||||
|
||||
currencyCode: paymentReceive.currencyCode,
|
||||
exchangeRate: paymentReceive.exchangeRate,
|
||||
|
||||
transactionId: paymentReceive.id,
|
||||
transactionType: 'PaymentReceive',
|
||||
|
||||
transactionNumber: paymentReceive.paymentReceiveNo,
|
||||
referenceNumber: paymentReceive.referenceNo,
|
||||
|
||||
date: paymentReceive.paymentDate,
|
||||
userId: paymentReceive.userId,
|
||||
createdAt: paymentReceive.createdAt,
|
||||
|
||||
branchId: paymentReceive.branchId,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the payment exchange gain/loss entry.
|
||||
* @param {IPaymentReceive} paymentReceive -
|
||||
* @param {number} ARAccountId -
|
||||
* @param {number} exchangeGainOrLossAccountId -
|
||||
* @param {string} baseCurrencyCode -
|
||||
* @returns {ILedgerEntry[]}
|
||||
*/
|
||||
private getPaymentExchangeGainLossEntry = (
|
||||
paymentReceive: IPaymentReceive,
|
||||
ARAccountId: number,
|
||||
exchangeGainOrLossAccountId: number,
|
||||
baseCurrencyCode: string
|
||||
): ILedgerEntry[] => {
|
||||
const commonJournal = this.getPaymentReceiveCommonEntry(paymentReceive);
|
||||
const gainOrLoss = this.getPaymentExGainOrLoss(paymentReceive);
|
||||
const absGainOrLoss = Math.abs(gainOrLoss);
|
||||
|
||||
return gainOrLoss
|
||||
? [
|
||||
{
|
||||
...commonJournal,
|
||||
currencyCode: baseCurrencyCode,
|
||||
exchangeRate: 1,
|
||||
debit: gainOrLoss > 0 ? absGainOrLoss : 0,
|
||||
credit: gainOrLoss < 0 ? absGainOrLoss : 0,
|
||||
accountId: ARAccountId,
|
||||
contactId: paymentReceive.customerId,
|
||||
index: 3,
|
||||
accountNormal: AccountNormal.CREDIT,
|
||||
},
|
||||
{
|
||||
...commonJournal,
|
||||
currencyCode: baseCurrencyCode,
|
||||
exchangeRate: 1,
|
||||
credit: gainOrLoss > 0 ? absGainOrLoss : 0,
|
||||
debit: gainOrLoss < 0 ? absGainOrLoss : 0,
|
||||
accountId: exchangeGainOrLossAccountId,
|
||||
index: 3,
|
||||
accountNormal: AccountNormal.DEBIT,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the payment deposit GL entry.
|
||||
* @param {IPaymentReceive} paymentReceive
|
||||
* @param {IPaymentReceive} paymentReceive
|
||||
* @returns {ILedgerEntry}
|
||||
*/
|
||||
private getPaymentDepositGLEntry = (
|
||||
@@ -238,8 +128,8 @@ export class PaymentReceiveGLEntries {
|
||||
|
||||
/**
|
||||
* Retrieves the payment receivable entry.
|
||||
* @param {IPaymentReceive} paymentReceive
|
||||
* @param {number} ARAccountId
|
||||
* @param {IPaymentReceive} paymentReceive
|
||||
* @param {number} ARAccountId
|
||||
* @returns {ILedgerEntry}
|
||||
*/
|
||||
private getPaymentReceivableEntry = (
|
||||
@@ -262,15 +152,15 @@ export class PaymentReceiveGLEntries {
|
||||
* Records payment receive journal transactions.
|
||||
*
|
||||
* Invoice payment journals.
|
||||
* --------
|
||||
* - Account receivable -> Debit
|
||||
* - Payment account [current asset] -> Credit
|
||||
* ------------
|
||||
* - Account Receivable -> Debit
|
||||
* - Payment Account [current asset] -> Credit
|
||||
*
|
||||
* @param {number} tenantId
|
||||
* @param {IPaymentReceive} paymentRecieve - Payment receive model.
|
||||
* @param {number} ARAccountId - A/R account id.
|
||||
* @param {number} exGainOrLossAccountId - Exchange gain/loss account id.
|
||||
* @param {string} baseCurrency - Base currency code.
|
||||
* @param {number} tenantId
|
||||
* @param {IPaymentReceive} paymentRecieve - Payment receive model.
|
||||
* @param {number} ARAccountId - A/R account id.
|
||||
* @param {number} exGainOrLossAccountId - Exchange gain/loss account id.
|
||||
* @param {string} baseCurrency - Base currency code.
|
||||
* @returns {Promise<ILedgerEntry>}
|
||||
*/
|
||||
public getPaymentReceiveGLEntries = (
|
||||
|
||||
@@ -107,7 +107,6 @@ export class PaymentReceiveValidators {
|
||||
const invoicesIds = paymentReceiveEntries.map(
|
||||
(e: IPaymentReceiveEntryDTO) => e.invoiceId
|
||||
);
|
||||
|
||||
const storedInvoices = await SaleInvoice.query().whereIn('id', invoicesIds);
|
||||
|
||||
const storedInvoicesMap = new Map(
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
import { Knex } from 'knex';
|
||||
import { sumBy } from 'lodash';
|
||||
import {
|
||||
AccountNormal,
|
||||
ILedgerEntry,
|
||||
IPaymentReceive,
|
||||
IPaymentReceiveGLCommonEntry,
|
||||
} from '@/interfaces';
|
||||
import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
|
||||
|
||||
export class PaymentReceivedGLCommon {
|
||||
private ledgerStorage: LedgerStorageService;
|
||||
|
||||
/**
|
||||
* Retrieves the common entry of payment receive.
|
||||
* @param {IPaymentReceive} paymentReceive
|
||||
* @returns {IPaymentReceiveGLCommonEntry}
|
||||
*/
|
||||
protected getPaymentReceiveCommonEntry = (
|
||||
paymentReceive: IPaymentReceive
|
||||
): IPaymentReceiveGLCommonEntry => {
|
||||
return {
|
||||
debit: 0,
|
||||
credit: 0,
|
||||
|
||||
currencyCode: paymentReceive.currencyCode,
|
||||
exchangeRate: paymentReceive.exchangeRate,
|
||||
|
||||
transactionId: paymentReceive.id,
|
||||
transactionType: 'PaymentReceive',
|
||||
|
||||
transactionNumber: paymentReceive.paymentReceiveNo,
|
||||
referenceNumber: paymentReceive.referenceNo,
|
||||
|
||||
date: paymentReceive.paymentDate,
|
||||
userId: paymentReceive.userId,
|
||||
createdAt: paymentReceive.createdAt,
|
||||
|
||||
branchId: paymentReceive.branchId,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the payment exchange gain/loss entry.
|
||||
* @param {IPaymentReceive} paymentReceive -
|
||||
* @param {number} ARAccountId -
|
||||
* @param {number} exchangeGainOrLossAccountId -
|
||||
* @param {string} baseCurrencyCode -
|
||||
* @returns {ILedgerEntry[]}
|
||||
*/
|
||||
protected getPaymentExchangeGainLossEntry = (
|
||||
paymentReceive: IPaymentReceive,
|
||||
ARAccountId: number,
|
||||
exchangeGainOrLossAccountId: number,
|
||||
baseCurrencyCode: string
|
||||
): ILedgerEntry[] => {
|
||||
const commonJournal = this.getPaymentReceiveCommonEntry(paymentReceive);
|
||||
const gainOrLoss = this.getPaymentExGainOrLoss(paymentReceive);
|
||||
const absGainOrLoss = Math.abs(gainOrLoss);
|
||||
|
||||
return gainOrLoss
|
||||
? [
|
||||
{
|
||||
...commonJournal,
|
||||
currencyCode: baseCurrencyCode,
|
||||
exchangeRate: 1,
|
||||
debit: gainOrLoss > 0 ? absGainOrLoss : 0,
|
||||
credit: gainOrLoss < 0 ? absGainOrLoss : 0,
|
||||
accountId: ARAccountId,
|
||||
contactId: paymentReceive.customerId,
|
||||
index: 3,
|
||||
accountNormal: AccountNormal.CREDIT,
|
||||
},
|
||||
{
|
||||
...commonJournal,
|
||||
currencyCode: baseCurrencyCode,
|
||||
exchangeRate: 1,
|
||||
credit: gainOrLoss > 0 ? absGainOrLoss : 0,
|
||||
debit: gainOrLoss < 0 ? absGainOrLoss : 0,
|
||||
accountId: exchangeGainOrLossAccountId,
|
||||
index: 3,
|
||||
accountNormal: AccountNormal.DEBIT,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates the payment total exchange gain/loss.
|
||||
* @param {IBillPayment} paymentReceive - Payment receive with entries.
|
||||
* @returns {number}
|
||||
*/
|
||||
private getPaymentExGainOrLoss = (
|
||||
paymentReceive: IPaymentReceive
|
||||
): number => {
|
||||
return sumBy(paymentReceive.entries, (entry) => {
|
||||
const paymentLocalAmount =
|
||||
entry.paymentAmount * paymentReceive.exchangeRate;
|
||||
const invoicePayment = entry.paymentAmount * entry.invoice.exchangeRate;
|
||||
|
||||
return paymentLocalAmount - invoicePayment;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Reverts the given payment receive GL entries.
|
||||
* @param {number} tenantId
|
||||
* @param {number} paymentReceiveId
|
||||
* @param {Knex.Transaction} trx
|
||||
*/
|
||||
public revertPaymentGLEntries = async (
|
||||
tenantId: number,
|
||||
paymentReceiveId: number,
|
||||
trx?: Knex.Transaction
|
||||
) => {
|
||||
await this.ledgerStorage.deleteByReference(
|
||||
tenantId,
|
||||
paymentReceiveId,
|
||||
'PaymentReceive',
|
||||
trx
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
import * as R from 'ramda';
|
||||
import { Knex } from 'knex';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { flatten } from 'lodash';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { TenantMetadata } from '@/system/models';
|
||||
import { PaymentReceivedGLCommon } from './PaymentReceivedGLCommon';
|
||||
import {
|
||||
AccountNormal,
|
||||
ILedgerEntry,
|
||||
IPaymentReceive,
|
||||
IPaymentReceiveEntry,
|
||||
} from '@/interfaces';
|
||||
import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
|
||||
import Ledger from '@/services/Accounting/Ledger';
|
||||
|
||||
@Service()
|
||||
export class PaymentReceivedUnearnedGLEntries extends PaymentReceivedGLCommon {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private ledgerStorage: LedgerStorageService;
|
||||
|
||||
/**
|
||||
* Writes payment GL entries to the storage.
|
||||
* @param {number} tenantId
|
||||
* @param {number} paymentReceiveId
|
||||
* @param {Knex.Transaction} trx
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public writePaymentGLEntries = async (
|
||||
tenantId: number,
|
||||
paymentReceiveId: number,
|
||||
trx?: Knex.Transaction
|
||||
): Promise<void> => {
|
||||
const { PaymentReceive } = this.tenancy.models(tenantId);
|
||||
|
||||
// Retrieves the payment receive with associated entries.
|
||||
const paymentReceive = await PaymentReceive.query(trx)
|
||||
.findById(paymentReceiveId)
|
||||
.withGraphFetched('entries.invoice');
|
||||
|
||||
// Stop early if
|
||||
if (!paymentReceive.unearnedRevenueAccountId) {
|
||||
return;
|
||||
}
|
||||
// Retrieves the given tenant metadata.
|
||||
const tenantMeta = await TenantMetadata.query().findOne({ tenantId });
|
||||
|
||||
const ledger = await this.getPaymentReceiveGLedger(
|
||||
tenantId,
|
||||
paymentReceive
|
||||
);
|
||||
// Commit the ledger entries to the storage.
|
||||
await this.ledgerStorage.commit(tenantId, ledger, trx);
|
||||
};
|
||||
|
||||
/**
|
||||
* Rewrites the given payment receive GL entries.
|
||||
* @param {number} tenantId
|
||||
* @param {number} paymentReceiveId
|
||||
* @param {Knex.Transaction} trx
|
||||
*/
|
||||
public rewritePaymentGLEntries = async (
|
||||
tenantId: number,
|
||||
paymentReceiveId: number,
|
||||
trx?: Knex.Transaction
|
||||
) => {
|
||||
// Reverts the payment GL entries.
|
||||
await this.revertPaymentGLEntries(tenantId, paymentReceiveId, trx);
|
||||
|
||||
// Writes the payment GL entries.
|
||||
await this.writePaymentGLEntries(tenantId, paymentReceiveId, trx);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the payment received GL entries.
|
||||
* @param {number} tenantId
|
||||
* @param {IPaymentReceive} paymentReceive
|
||||
* @returns {Promise<Ledger>}
|
||||
*/
|
||||
private getPaymentReceiveGLedger = async (
|
||||
tenantId: number,
|
||||
paymentReceive: IPaymentReceive
|
||||
) => {
|
||||
const { accountRepository } = this.tenancy.repositories(tenantId);
|
||||
|
||||
// Retrieve the A/R account of the given currency.
|
||||
const receivableAccount =
|
||||
await accountRepository.findOrCreateAccountReceivable(
|
||||
paymentReceive.currencyCode
|
||||
);
|
||||
// Retrieve the payment GL entries.
|
||||
const entries = this.getPaymentGLEntries(
|
||||
receivableAccount.id,
|
||||
paymentReceive
|
||||
);
|
||||
const unearnedRevenueEntries =
|
||||
this.getUnearnedRevenueEntries(paymentReceive);
|
||||
|
||||
const combinedEntries = [...unearnedRevenueEntries, ...entries];
|
||||
|
||||
return new Ledger(combinedEntries);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the payment received GL entries.
|
||||
* @param {number} ARAccountId - A/R account id.
|
||||
* @param {IPaymentReceive} paymentReceive -
|
||||
* @returns {Array<ILedgerEntry>}
|
||||
*/
|
||||
private getPaymentGLEntries = R.curry(
|
||||
(
|
||||
ARAccountId: number,
|
||||
paymentReceive: IPaymentReceive
|
||||
): Array<ILedgerEntry> => {
|
||||
const getPaymentEntryGLEntries = this.getPaymentEntryGLEntries(
|
||||
ARAccountId,
|
||||
paymentReceive
|
||||
);
|
||||
const entriesGroup = paymentReceive.entries.map((paymentEntry) => {
|
||||
return getPaymentEntryGLEntries(paymentEntry);
|
||||
});
|
||||
return flatten(entriesGroup);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Retrieve the payment entry GL entries.
|
||||
* @param {IPaymentReceiveEntry} paymentReceivedEntry -
|
||||
* @param {IPaymentReceive} paymentReceive -
|
||||
* @returns {Array<ILedgerEntry>}
|
||||
*/
|
||||
private getPaymentEntryGLEntries = R.curry(
|
||||
(
|
||||
ARAccountId: number,
|
||||
paymentReceive: IPaymentReceive,
|
||||
paymentReceivedEntry: IPaymentReceiveEntry
|
||||
): Array<ILedgerEntry> => {
|
||||
const unearnedRevenueEntry = this.getDebitUnearnedRevenueGLEntry(
|
||||
paymentReceivedEntry.paymentAmount,
|
||||
paymentReceive
|
||||
);
|
||||
const AREntry = this.getPaymentReceivableEntry(
|
||||
paymentReceivedEntry.paymentAmount,
|
||||
paymentReceive,
|
||||
ARAccountId
|
||||
);
|
||||
return [unearnedRevenueEntry, AREntry];
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Retrieves the payment deposit GL entry.
|
||||
* @param {IPaymentReceive} paymentReceive
|
||||
* @returns {ILedgerEntry}
|
||||
*/
|
||||
private getDebitUnearnedRevenueGLEntry = (
|
||||
amount: number,
|
||||
paymentReceive: IPaymentReceive
|
||||
): ILedgerEntry => {
|
||||
const commonJournal = this.getPaymentReceiveCommonEntry(paymentReceive);
|
||||
|
||||
return {
|
||||
...commonJournal,
|
||||
debit: amount,
|
||||
accountId: paymentReceive.unearnedRevenueAccountId,
|
||||
accountNormal: AccountNormal.CREDIT,
|
||||
index: 2,
|
||||
indexGroup: 20,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the payment receivable entry.
|
||||
* @param {IPaymentReceive} paymentReceive
|
||||
* @param {number} ARAccountId
|
||||
* @returns {ILedgerEntry}
|
||||
*/
|
||||
private getPaymentReceivableEntry = (
|
||||
amount: number,
|
||||
paymentReceive: IPaymentReceive,
|
||||
ARAccountId: number
|
||||
): ILedgerEntry => {
|
||||
const commonJournal = this.getPaymentReceiveCommonEntry(paymentReceive);
|
||||
|
||||
return {
|
||||
...commonJournal,
|
||||
credit: amount,
|
||||
contactId: paymentReceive.customerId,
|
||||
accountId: ARAccountId,
|
||||
accountNormal: AccountNormal.DEBIT,
|
||||
index: 1,
|
||||
indexGroup: 20,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the unearned revenue entries.
|
||||
* @param {IPaymentReceive} paymentReceived -
|
||||
* @returns {Array<ILedgerEntry>}
|
||||
*/
|
||||
private getUnearnedRevenueEntries = (
|
||||
paymentReceive: IPaymentReceive
|
||||
): Array<ILedgerEntry> => {
|
||||
const depositEntry = this.getDepositPaymentGLEntry(paymentReceive);
|
||||
const unearnedEntry = this.getUnearnedRevenueEntry(paymentReceive);
|
||||
|
||||
return [depositEntry, unearnedEntry];
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the payment deposit entry.
|
||||
* @param {IPaymentReceive} paymentReceived -
|
||||
* @returns {ILedgerEntry}
|
||||
*/
|
||||
private getDepositPaymentGLEntry = (
|
||||
paymentReceive: IPaymentReceive
|
||||
): ILedgerEntry => {
|
||||
const commonJournal = this.getPaymentReceiveCommonEntry(paymentReceive);
|
||||
|
||||
return {
|
||||
...commonJournal,
|
||||
debit: paymentReceive.amount,
|
||||
accountId: paymentReceive.depositAccountId,
|
||||
accountNormal: AccountNormal.DEBIT,
|
||||
indexGroup: 10,
|
||||
index: 1,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the unearned revenue entry.
|
||||
* @param {IPaymentReceive} paymentReceived -
|
||||
* @returns {ILedgerEntry}
|
||||
*/
|
||||
private getUnearnedRevenueEntry = (
|
||||
paymentReceived: IPaymentReceive
|
||||
): ILedgerEntry => {
|
||||
const commonJournal = this.getPaymentReceiveCommonEntry(paymentReceived);
|
||||
|
||||
return {
|
||||
...commonJournal,
|
||||
credit: paymentReceived.amount,
|
||||
accountId: paymentReceived.unearnedRevenueAccountId,
|
||||
accountNormal: AccountNormal.CREDIT,
|
||||
indexGroup: 10,
|
||||
index: 1,
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import events from '@/subscribers/events';
|
||||
import { AutoApplyUnearnedRevenue } from '../AutoApplyUnearnedRevenue';
|
||||
|
||||
@Service()
|
||||
export class AutoApplyUnearnedRevenueOnInvoiceCreated {
|
||||
@Inject()
|
||||
private autoApplyUnearnedRevenue: AutoApplyUnearnedRevenue;
|
||||
/**
|
||||
* Constructor method.
|
||||
*/
|
||||
public attach(bus) {
|
||||
bus.subscribe(
|
||||
events.saleInvoice.onCreated,
|
||||
this.handleAutoApplyUnearnedRevenueOnInvoiceCreated.bind(this)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the auto apply unearned revenue on invoice creating.
|
||||
* @param
|
||||
*/
|
||||
private async handleAutoApplyUnearnedRevenueOnInvoiceCreated({
|
||||
tenantId,
|
||||
saleInvoice,
|
||||
trx,
|
||||
}) {
|
||||
await this.autoApplyUnearnedRevenue.autoApplyUnearnedRevenueToInvoice(
|
||||
tenantId,
|
||||
saleInvoice.id,
|
||||
trx
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import { getPrice } from '@lemonsqueezy/lemonsqueezy.js';
|
||||
import config from '@/config';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import {
|
||||
@@ -10,7 +9,6 @@ import {
|
||||
} from './utils';
|
||||
import { Plan } from '@/system/models';
|
||||
import { Subscription } from './Subscription';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
@Service()
|
||||
export class LemonSqueezyWebhooks {
|
||||
@@ -18,7 +16,7 @@ export class LemonSqueezyWebhooks {
|
||||
private subscriptionService: Subscription;
|
||||
|
||||
/**
|
||||
* handle the LemonSqueezy webhooks.
|
||||
* Handles the Lemon Squeezy webhooks.
|
||||
* @param {string} rawBody
|
||||
* @param {string} signature
|
||||
* @returns {Promise<void>}
|
||||
@@ -74,7 +72,7 @@ export class LemonSqueezyWebhooks {
|
||||
const variantId = attributes.variant_id as string;
|
||||
|
||||
// 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) {
|
||||
throw new Error(`Plan with variantId ${variantId} not found.`);
|
||||
@@ -82,26 +80,9 @@ export class LemonSqueezyWebhooks {
|
||||
// Update the subscription in the database.
|
||||
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.
|
||||
if (webhookEvent === 'subscription_created') {
|
||||
await this.subscriptionService.newSubscribtion(
|
||||
tenantId,
|
||||
'early-adaptor'
|
||||
);
|
||||
await this.subscriptionService.newSubscribtion(tenantId, plan.slug);
|
||||
}
|
||||
}
|
||||
} else if (webhookEvent.startsWith('order_')) {
|
||||
|
||||
@@ -77,7 +77,12 @@ export default class HasTenancyService {
|
||||
const knex = this.knex(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;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -3,15 +3,20 @@ import {
|
||||
IPaymentReceiveCreatedPayload,
|
||||
IPaymentReceiveDeletedPayload,
|
||||
IPaymentReceiveEditedPayload,
|
||||
PaymentReceiveUnearnedRevenueAppliedEventPayload,
|
||||
} from '@/interfaces';
|
||||
import events from '@/subscribers/events';
|
||||
import { PaymentReceiveGLEntries } from '@/services/Sales/PaymentReceives/PaymentReceiveGLEntries';
|
||||
import { PaymentReceivedUnearnedGLEntries } from '@/services/Sales/PaymentReceives/PaymentReceivedUnearnedGLEntries';
|
||||
|
||||
@Service()
|
||||
export default class PaymentReceivesWriteGLEntriesSubscriber {
|
||||
@Inject()
|
||||
private paymentReceiveGLEntries: PaymentReceiveGLEntries;
|
||||
|
||||
@Inject()
|
||||
private paymentReceivedUnearnedGLEntries: PaymentReceivedUnearnedGLEntries;
|
||||
|
||||
/**
|
||||
* Attaches events with handlers.
|
||||
*/
|
||||
@@ -32,6 +37,7 @@ export default class PaymentReceivesWriteGLEntriesSubscriber {
|
||||
|
||||
/**
|
||||
* Handle journal entries writing once the payment receive created.
|
||||
* @param {IPaymentReceiveCreatedPayload} payload -
|
||||
*/
|
||||
private handleWriteJournalEntriesOnceCreated = async ({
|
||||
tenantId,
|
||||
@@ -43,14 +49,21 @@ export default class PaymentReceivesWriteGLEntriesSubscriber {
|
||||
paymentReceiveId,
|
||||
trx
|
||||
);
|
||||
await this.paymentReceivedUnearnedGLEntries.writePaymentGLEntries(
|
||||
tenantId,
|
||||
paymentReceiveId,
|
||||
trx
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle journal entries writing once the payment receive edited.
|
||||
* @param {IPaymentReceiveEditedPayload} payload -
|
||||
*/
|
||||
private handleOverwriteJournalEntriesOnceEdited = async ({
|
||||
tenantId,
|
||||
paymentReceive,
|
||||
paymentReceiveId,
|
||||
trx,
|
||||
}: IPaymentReceiveEditedPayload) => {
|
||||
await this.paymentReceiveGLEntries.rewritePaymentGLEntries(
|
||||
@@ -58,10 +71,16 @@ export default class PaymentReceivesWriteGLEntriesSubscriber {
|
||||
paymentReceive.id,
|
||||
trx
|
||||
);
|
||||
await this.paymentReceivedUnearnedGLEntries.rewritePaymentGLEntries(
|
||||
tenantId,
|
||||
paymentReceiveId,
|
||||
trx
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles revert journal entries once deleted.
|
||||
* @param {IPaymentReceiveDeletedPayload} payload -
|
||||
*/
|
||||
private handleRevertJournalEntriesOnceDeleted = async ({
|
||||
tenantId,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user