mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-20 06:40:31 +00:00
refactor: financial statements to nestjs
This commit is contained in:
@@ -80,6 +80,7 @@
|
|||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"serialize-interceptor": "^1.1.7",
|
"serialize-interceptor": "^1.1.7",
|
||||||
"strategy": "^1.1.1",
|
"strategy": "^1.1.1",
|
||||||
|
"stripe": "^16.10.0",
|
||||||
"uniqid": "^5.2.0",
|
"uniqid": "^5.2.0",
|
||||||
"uuid": "^10.0.0",
|
"uuid": "^10.0.0",
|
||||||
"xlsx": "^0.18.5",
|
"xlsx": "^0.18.5",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import lemonsqueezy from './lemonsqueezy';
|
|||||||
import s3 from './s3';
|
import s3 from './s3';
|
||||||
import openExchange from './open-exchange';
|
import openExchange from './open-exchange';
|
||||||
import posthog from './posthog';
|
import posthog from './posthog';
|
||||||
|
import stripePayment from './stripe-payment';
|
||||||
|
|
||||||
export const config = [
|
export const config = [
|
||||||
systemDatabase,
|
systemDatabase,
|
||||||
@@ -18,4 +19,5 @@ export const config = [
|
|||||||
s3,
|
s3,
|
||||||
openExchange,
|
openExchange,
|
||||||
posthog,
|
posthog,
|
||||||
|
stripePayment,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ import { PostHogModule } from '../EventsTracker/postHog.module';
|
|||||||
import { EventTrackerModule } from '../EventsTracker/EventTracker.module';
|
import { EventTrackerModule } from '../EventsTracker/EventTracker.module';
|
||||||
import { MailModule } from '../Mail/Mail.module';
|
import { MailModule } from '../Mail/Mail.module';
|
||||||
import { FinancialStatementsModule } from '../FinancialStatements/FinancialStatements.module';
|
import { FinancialStatementsModule } from '../FinancialStatements/FinancialStatements.module';
|
||||||
|
import { StripePaymentModule } from '../StripePayment/StripePayment.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -158,6 +159,7 @@ import { FinancialStatementsModule } from '../FinancialStatements/FinancialState
|
|||||||
PostHogModule,
|
PostHogModule,
|
||||||
EventTrackerModule,
|
EventTrackerModule,
|
||||||
FinancialStatementsModule,
|
FinancialStatementsModule,
|
||||||
|
StripePaymentModule,
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [
|
providers: [
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ import { TransactionsByVendorModule } from './modules/TransactionsByVendor/Trans
|
|||||||
import { TransactionsByCustomerModule } from './modules/TransactionsByCustomer/TransactionsByCustomer.module';
|
import { TransactionsByCustomerModule } from './modules/TransactionsByCustomer/TransactionsByCustomer.module';
|
||||||
import { TransactionsByReferenceModule } from './modules/TransactionsByReference/TransactionByReference.module';
|
import { TransactionsByReferenceModule } from './modules/TransactionsByReference/TransactionByReference.module';
|
||||||
import { ARAgingSummaryModule } from './modules/ARAgingSummary/ARAgingSummary.module';
|
import { ARAgingSummaryModule } from './modules/ARAgingSummary/ARAgingSummary.module';
|
||||||
// import { APAgingSummaryModule } from './modules/APAgingSummary/APAgingSummary.module';
|
import { APAgingSummaryModule } from './modules/APAgingSummary/APAgingSummary.module';
|
||||||
import { InventoryItemDetailsModule } from './modules/InventoryItemDetails/InventoryItemDetails.module';
|
import { InventoryItemDetailsModule } from './modules/InventoryItemDetails/InventoryItemDetails.module';
|
||||||
import { InventoryValuationSheetModule } from './modules/InventoryValuationSheet/InventoryValuationSheet.module';
|
import { InventoryValuationSheetModule } from './modules/InventoryValuationSheet/InventoryValuationSheet.module';
|
||||||
|
import { SalesTaxLiabilityModule } from './modules/SalesTaxLiabilitySummary/SalesTaxLiability.module';
|
||||||
|
import { JournalSheetModule } from './modules/JournalSheet/JournalSheet.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [],
|
providers: [],
|
||||||
@@ -24,9 +26,11 @@ import { InventoryValuationSheetModule } from './modules/InventoryValuationSheet
|
|||||||
TransactionsByCustomerModule,
|
TransactionsByCustomerModule,
|
||||||
TransactionsByReferenceModule,
|
TransactionsByReferenceModule,
|
||||||
ARAgingSummaryModule,
|
ARAgingSummaryModule,
|
||||||
// APAgingSummaryModule,
|
APAgingSummaryModule,
|
||||||
InventoryItemDetailsModule,
|
InventoryItemDetailsModule,
|
||||||
InventoryValuationSheetModule,
|
InventoryValuationSheetModule,
|
||||||
|
SalesTaxLiabilityModule,
|
||||||
|
JournalSheetModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class FinancialStatementsModule {}
|
export class FinancialStatementsModule {}
|
||||||
|
|||||||
@@ -7,18 +7,22 @@ import { APAgingSummaryPdfInjectable } from './APAgingSummaryPdfInjectable';
|
|||||||
import { APAgingSummaryRepository } from './APAgingSummaryRepository';
|
import { APAgingSummaryRepository } from './APAgingSummaryRepository';
|
||||||
import { APAgingSummaryApplication } from './APAgingSummaryApplication';
|
import { APAgingSummaryApplication } from './APAgingSummaryApplication';
|
||||||
import { APAgingSummaryController } from './APAgingSummary.controller';
|
import { APAgingSummaryController } from './APAgingSummary.controller';
|
||||||
|
import { APAgingSummaryMeta } from './APAgingSummaryMeta';
|
||||||
|
import { FinancialSheetCommonModule } from '../../common/FinancialSheetCommon.module';
|
||||||
|
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [AgingSummaryModule],
|
imports: [AgingSummaryModule, FinancialSheetCommonModule],
|
||||||
providers: [
|
providers: [
|
||||||
APAgingSummaryService,
|
APAgingSummaryService,
|
||||||
|
APAgingSummaryMeta,
|
||||||
APAgingSummaryTableInjectable,
|
APAgingSummaryTableInjectable,
|
||||||
APAgingSummaryExportInjectable,
|
APAgingSummaryExportInjectable,
|
||||||
APAgingSummaryPdfInjectable,
|
APAgingSummaryPdfInjectable,
|
||||||
APAgingSummaryRepository,
|
APAgingSummaryRepository,
|
||||||
APAgingSummaryApplication
|
APAgingSummaryApplication,
|
||||||
|
TenancyContext,
|
||||||
],
|
],
|
||||||
controllers: [APAgingSummaryController],
|
controllers: [APAgingSummaryController],
|
||||||
})
|
})
|
||||||
export class APAgingSummaryModule {}
|
export class APAgingSummaryModule {}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { isEmpty } from 'lodash';
|
import { Inject } from '@nestjs/common';
|
||||||
|
import { isEmpty, groupBy } from 'lodash';
|
||||||
import { Bill } from '@/modules/Bills/models/Bill';
|
import { Bill } from '@/modules/Bills/models/Bill';
|
||||||
import { Vendor } from '@/modules/Vendors/models/Vendor';
|
import { Vendor } from '@/modules/Vendors/models/Vendor';
|
||||||
import { IAPAgingSummaryQuery } from './APAgingSummary.types';
|
|
||||||
import { Inject } from '@nestjs/common';
|
|
||||||
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
|
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
|
||||||
import { groupBy } from 'ramda';
|
import { IAPAgingSummaryQuery } from './APAgingSummary.types';
|
||||||
|
import { ModelObject } from 'objection';
|
||||||
|
|
||||||
export class APAgingSummaryRepository {
|
export class APAgingSummaryRepository {
|
||||||
@Inject(Vendor.name)
|
@Inject(Vendor.name)
|
||||||
@@ -32,19 +32,19 @@ export class APAgingSummaryRepository {
|
|||||||
* Due bills by vendor id.
|
* Due bills by vendor id.
|
||||||
* @param {Record<string, Bill[]>} dueBillsByVendorId
|
* @param {Record<string, Bill[]>} dueBillsByVendorId
|
||||||
*/
|
*/
|
||||||
dueBillsByVendorId: Record<number, Bill[]>;
|
dueBillsByVendorId: Record<string, Bill[]>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Overdue bills.
|
* Overdue bills.
|
||||||
* @param {Bill[]} overdueBills
|
* @param {Bill[]} overdueBills - overdue bills.
|
||||||
*/
|
*/
|
||||||
overdueBills: Bill[];
|
overdueBills: ModelObject<Bill>[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Overdue bills by vendor id.
|
* Overdue bills by vendor id.
|
||||||
* @param {Record<string, Bill[]>} overdueBillsByVendorId
|
* @param {Record<string, Bill[]>} overdueBillsByVendorId - Overdue bills by vendor id.
|
||||||
*/
|
*/
|
||||||
overdueBillsByVendorId: Record<number, Bill[]>;
|
overdueBillsByVendorId: ModelObject<Bill>[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Vendors.
|
* Vendors.
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { I18nService } from 'nestjs-i18n';
|
||||||
import {
|
import {
|
||||||
IAPAgingSummaryQuery,
|
IAPAgingSummaryQuery,
|
||||||
IAPAgingSummaryTable,
|
IAPAgingSummaryTable,
|
||||||
@@ -8,18 +9,21 @@ import { Injectable } from '@nestjs/common';
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class APAgingSummaryTableInjectable {
|
export class APAgingSummaryTableInjectable {
|
||||||
constructor(private readonly APAgingSummarySheet: APAgingSummaryService) {}
|
constructor(
|
||||||
|
private readonly APAgingSummarySheet: APAgingSummaryService,
|
||||||
|
private readonly i18nService: I18nService,
|
||||||
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves A/P aging summary in table format.
|
* Retrieves A/P aging summary in table format.
|
||||||
* @param {IAPAgingSummaryQuery} query -
|
* @param {IAPAgingSummaryQuery} query -
|
||||||
* @returns {Promise<IAPAgingSummaryTable>}
|
* @returns {Promise<IAPAgingSummaryTable>}
|
||||||
*/
|
*/
|
||||||
public async table(
|
public async table(
|
||||||
query: IAPAgingSummaryQuery,
|
query: IAPAgingSummaryQuery,
|
||||||
): Promise<IAPAgingSummaryTable> {
|
): Promise<IAPAgingSummaryTable> {
|
||||||
const report = await this.APAgingSummarySheet.APAgingSummary(query);
|
const report = await this.APAgingSummarySheet.APAgingSummary(query);
|
||||||
const table = new APAgingSummaryTable(report.data, query, {});
|
const table = new APAgingSummaryTable(report.data, query, this.i18nService);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
table: {
|
table: {
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { Controller, Get, Headers, Query, Res } from '@nestjs/common';
|
||||||
|
import { IJournalReportQuery } from './JournalSheet.types';
|
||||||
|
import { Response } from 'express';
|
||||||
|
import { AcceptType } from '@/constants/accept-type';
|
||||||
|
import { JournalSheetApplication } from './JournalSheetApplication';
|
||||||
|
|
||||||
|
@Controller('/reports/journal')
|
||||||
|
export class JournalSheetController {
|
||||||
|
constructor(private readonly journalSheetApp: JournalSheetApplication) {}
|
||||||
|
|
||||||
|
@Get('/')
|
||||||
|
async journalSheet(
|
||||||
|
@Query() query: IJournalReportQuery,
|
||||||
|
@Res() res: Response,
|
||||||
|
@Headers('accept') acceptHeader: string,
|
||||||
|
) {
|
||||||
|
// Retrieves the json table format.
|
||||||
|
if (acceptHeader.includes(AcceptType.ApplicationJsonTable)) {
|
||||||
|
const table = await this.journalSheetApp.table(query);
|
||||||
|
return res.status(200).send(table);
|
||||||
|
|
||||||
|
// Retrieves the csv format.
|
||||||
|
} else if (acceptHeader.includes(AcceptType.ApplicationCsv)) {
|
||||||
|
const buffer = await this.journalSheetApp.csv(query);
|
||||||
|
|
||||||
|
res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
|
||||||
|
res.setHeader('Content-Type', 'text/csv');
|
||||||
|
|
||||||
|
return res.send(buffer);
|
||||||
|
// Retrieves the xlsx format.
|
||||||
|
} else if (acceptHeader.includes(AcceptType.ApplicationXlsx)) {
|
||||||
|
const buffer = await this.journalSheetApp.xlsx(query);
|
||||||
|
|
||||||
|
res.setHeader('Content-Disposition', 'attachment; filename=output.xlsx');
|
||||||
|
res.setHeader(
|
||||||
|
'Content-Type',
|
||||||
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
);
|
||||||
|
return res.send(buffer);
|
||||||
|
// Retrieves the json format.
|
||||||
|
} else if (acceptHeader.includes(AcceptType.ApplicationPdf)) {
|
||||||
|
const pdfContent = await this.journalSheetApp.pdf(query);
|
||||||
|
|
||||||
|
res.set({
|
||||||
|
'Content-Type': 'application/pdf',
|
||||||
|
'Content-Length': pdfContent.length,
|
||||||
|
});
|
||||||
|
res.send(pdfContent);
|
||||||
|
} else {
|
||||||
|
const sheet = await this.journalSheetApp.sheet(query);
|
||||||
|
|
||||||
|
return res.status(200).send(sheet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { JournalSheetController } from './JournalSheet.controller';
|
||||||
|
import { JournalSheetApplication } from './JournalSheetApplication';
|
||||||
|
import { JournalSheetPdfInjectable } from './JournalSheetPdfInjectable';
|
||||||
|
import { JournalSheetExportInjectable } from './JournalSheetExport';
|
||||||
|
import { JournalSheetService } from './JournalSheetService';
|
||||||
|
import { JournalSheetTableInjectable } from './JournalSheetTableInjectable';
|
||||||
|
import { JournalSheetRepository } from './JournalSheetRepository';
|
||||||
|
import { JournalSheetMeta } from './JournalSheetMeta';
|
||||||
|
import { FinancialSheetCommonModule } from '../../common/FinancialSheetCommon.module';
|
||||||
|
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
|
||||||
|
import { AccountsModule } from '@/modules/Accounts/Accounts.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [FinancialSheetCommonModule, AccountsModule],
|
||||||
|
controllers: [JournalSheetController],
|
||||||
|
providers: [
|
||||||
|
JournalSheetApplication,
|
||||||
|
JournalSheetTableInjectable,
|
||||||
|
JournalSheetService,
|
||||||
|
JournalSheetExportInjectable,
|
||||||
|
JournalSheetPdfInjectable,
|
||||||
|
JournalSheetRepository,
|
||||||
|
JournalSheetMeta,
|
||||||
|
TenancyContext,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class JournalSheetModule {}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import { I18nService } from 'nestjs-i18n';
|
||||||
|
import { sumBy, chain, get, head } from 'lodash';
|
||||||
|
import moment from 'moment';
|
||||||
|
import {
|
||||||
|
IJournalReportEntriesGroup,
|
||||||
|
IJournalReportQuery,
|
||||||
|
IJournalSheetEntry,
|
||||||
|
IJournalTableData,
|
||||||
|
} from './JournalSheet.types';
|
||||||
|
import { FinancialSheet } from '../../common/FinancialSheet';
|
||||||
|
import { JournalSheetRepository } from './JournalSheetRepository';
|
||||||
|
import { Ledger } from '@/modules/Ledger/Ledger';
|
||||||
|
import { ILedgerEntry } from '@/modules/Ledger/types/Ledger.types';
|
||||||
|
|
||||||
|
export class JournalSheet extends FinancialSheet {
|
||||||
|
readonly ledger: Ledger;
|
||||||
|
readonly query: IJournalReportQuery;
|
||||||
|
readonly repository: JournalSheetRepository;
|
||||||
|
readonly i18n: I18nService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor method.
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
query: IJournalReportQuery,
|
||||||
|
repository: JournalSheetRepository,
|
||||||
|
i18n: I18nService,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.query = query;
|
||||||
|
this.repository = repository;
|
||||||
|
this.numberFormat = {
|
||||||
|
...this.numberFormat,
|
||||||
|
...this.query.numberFormat,
|
||||||
|
};
|
||||||
|
this.i18n = i18n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entry mapper.
|
||||||
|
* @param {ILedgerEntry} entry
|
||||||
|
*/
|
||||||
|
entryMapper(entry: ILedgerEntry): IJournalSheetEntry {
|
||||||
|
const account = this.repository.accountsGraph.getNodeData(entry.accountId);
|
||||||
|
const contact = this.repository.contactsById.get(entry.contactId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
entryId: entry.id,
|
||||||
|
|
||||||
|
index: entry.index,
|
||||||
|
note: entry.note,
|
||||||
|
|
||||||
|
contactName: get(contact, 'displayName'),
|
||||||
|
contactType: get(contact, 'contactService'),
|
||||||
|
|
||||||
|
accountName: account.name,
|
||||||
|
accountCode: account.code,
|
||||||
|
transactionNumber: entry.transactionNumber,
|
||||||
|
|
||||||
|
currencyCode: this.baseCurrency,
|
||||||
|
formattedCredit: this.formatNumber(entry.credit),
|
||||||
|
formattedDebit: this.formatNumber(entry.debit),
|
||||||
|
|
||||||
|
credit: entry.credit,
|
||||||
|
debit: entry.debit,
|
||||||
|
|
||||||
|
createdAt: entry.createdAt,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* maps the journal entries.
|
||||||
|
* @param {IJournalEntry[]} entries -
|
||||||
|
*/
|
||||||
|
entriesMapper(entries: ILedgerEntry[]): Array<IJournalSheetEntry> {
|
||||||
|
return entries.map(this.entryMapper.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mapping journal entries groups.
|
||||||
|
* @param {ILedgerEntry[]} entriesGroup -
|
||||||
|
* @param {ILedgerEntry} key -
|
||||||
|
* @return {IJournalReportEntriesGroup}
|
||||||
|
*/
|
||||||
|
entriesGroupsMapper(
|
||||||
|
entriesGroup: ILedgerEntry[],
|
||||||
|
groupEntry: ILedgerEntry,
|
||||||
|
): IJournalReportEntriesGroup {
|
||||||
|
const totalCredit = sumBy(entriesGroup, 'credit');
|
||||||
|
const totalDebit = sumBy(entriesGroup, 'debit');
|
||||||
|
|
||||||
|
return {
|
||||||
|
date: moment(groupEntry.date).toDate(),
|
||||||
|
dateFormatted: moment(groupEntry.date).format('YYYY MMM DD'),
|
||||||
|
|
||||||
|
transactionType: groupEntry.transactionType,
|
||||||
|
referenceId: groupEntry.transactionId,
|
||||||
|
referenceTypeFormatted: this.i18n.t(groupEntry.transactionType),
|
||||||
|
|
||||||
|
entries: this.entriesMapper(entriesGroup),
|
||||||
|
|
||||||
|
currencyCode: this.baseCurrency,
|
||||||
|
|
||||||
|
credit: totalCredit,
|
||||||
|
debit: totalDebit,
|
||||||
|
|
||||||
|
formattedCredit: this.formatTotalNumber(totalCredit),
|
||||||
|
formattedDebit: this.formatTotalNumber(totalDebit),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mapping the journal entries to entries groups.
|
||||||
|
* @param {IJournalEntry[]} entries
|
||||||
|
* @return {IJournalReportEntriesGroup[]}
|
||||||
|
*/
|
||||||
|
entriesWalker(entries: ILedgerEntry[]): IJournalReportEntriesGroup[] {
|
||||||
|
return chain(entries)
|
||||||
|
.groupBy((entry) => `${entry.referenceId}-${entry.referenceType}`)
|
||||||
|
.map((entriesGroup: ILedgerEntry[], key: string) => {
|
||||||
|
const headEntry = head(entriesGroup);
|
||||||
|
return this.entriesGroupsMapper(entriesGroup, headEntry);
|
||||||
|
})
|
||||||
|
.value();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve journal report.
|
||||||
|
* @return {IJournalReport}
|
||||||
|
*/
|
||||||
|
reportData(): IJournalTableData {
|
||||||
|
return this.entriesWalker(this.ledger.entries);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import { IFinancialSheetCommonMeta } from '../../types/Report.types';
|
||||||
|
import { IFinancialTable } from '../../types/Table.types';
|
||||||
|
|
||||||
|
export interface IJournalReportQuery {
|
||||||
|
fromDate: Date | string;
|
||||||
|
toDate: Date | string;
|
||||||
|
numberFormat: {
|
||||||
|
noCents: boolean;
|
||||||
|
divideOn1000: boolean;
|
||||||
|
};
|
||||||
|
transactionType: string;
|
||||||
|
transactionId: string;
|
||||||
|
|
||||||
|
accountsIds: number | number[];
|
||||||
|
fromRange: number;
|
||||||
|
toRange: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IJournalSheetEntry {
|
||||||
|
entryId: number;
|
||||||
|
index: number;
|
||||||
|
|
||||||
|
credit: number;
|
||||||
|
debit: number;
|
||||||
|
|
||||||
|
formattedDebit: string;
|
||||||
|
formattedCredit: string;
|
||||||
|
|
||||||
|
contactType: string;
|
||||||
|
contactName: string;
|
||||||
|
|
||||||
|
currencyCode: string;
|
||||||
|
|
||||||
|
accountName: string;
|
||||||
|
accountCode: string;
|
||||||
|
transactionNumber: string;
|
||||||
|
|
||||||
|
note: string;
|
||||||
|
createdAt: Date | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IJournalReportEntriesGroup {
|
||||||
|
date: Date;
|
||||||
|
dateFormatted: string;
|
||||||
|
|
||||||
|
entries: IJournalSheetEntry[];
|
||||||
|
currencyCode: string;
|
||||||
|
|
||||||
|
credit: number;
|
||||||
|
debit: number;
|
||||||
|
|
||||||
|
formattedCredit: string;
|
||||||
|
formattedDebit: string;
|
||||||
|
|
||||||
|
transactionType: string;
|
||||||
|
|
||||||
|
referenceId: number;
|
||||||
|
referenceTypeFormatted: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IJournalReport {
|
||||||
|
entries: IJournalReportEntriesGroup[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IJournalSheetMeta extends IFinancialSheetCommonMeta {
|
||||||
|
formattedDateRange: string;
|
||||||
|
formattedFromDate: string;
|
||||||
|
formattedToDate: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IJournalTable extends IFinancialTable {
|
||||||
|
query: IJournalReportQuery;
|
||||||
|
meta: IJournalSheetMeta;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IJournalTableData = IJournalReportEntriesGroup[];
|
||||||
|
|
||||||
|
export interface IJournalSheet {
|
||||||
|
data: IJournalTableData;
|
||||||
|
query: IJournalReportQuery;
|
||||||
|
meta: IJournalSheetMeta;
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { JournalSheetService } from './JournalSheetService';
|
||||||
|
import { JournalSheetTableInjectable } from './JournalSheetTableInjectable';
|
||||||
|
import { JournalSheetExportInjectable } from './JournalSheetExport';
|
||||||
|
import { JournalSheetPdfInjectable } from './JournalSheetPdfInjectable';
|
||||||
|
import { IJournalReportQuery, IJournalTable } from './JournalSheet.types';
|
||||||
|
|
||||||
|
export class JournalSheetApplication {
|
||||||
|
constructor(
|
||||||
|
private readonly journalSheetTable: JournalSheetTableInjectable,
|
||||||
|
private readonly journalSheet: JournalSheetService,
|
||||||
|
private readonly journalExport: JournalSheetExportInjectable,
|
||||||
|
private readonly journalPdf: JournalSheetPdfInjectable,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the journal sheet.
|
||||||
|
* @param {IJournalReportQuery} query
|
||||||
|
* @returns {}
|
||||||
|
*/
|
||||||
|
public sheet(query: IJournalReportQuery) {
|
||||||
|
return this.journalSheet.journalSheet(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the journal sheet in table format.
|
||||||
|
* @param {IJournalReportQuery} query
|
||||||
|
* @returns {Promise<IJournalTable>}
|
||||||
|
*/
|
||||||
|
public table(query: IJournalReportQuery): Promise<IJournalTable> {
|
||||||
|
return this.journalSheetTable.table(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the journal sheet in xlsx format.
|
||||||
|
* @param {IJournalReportQuery} query
|
||||||
|
* @returns {Promise<Buffer>}
|
||||||
|
*/
|
||||||
|
public xlsx(query: IJournalReportQuery) {
|
||||||
|
return this.journalExport.xlsx(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the journal sheet in csv format.
|
||||||
|
* @param {IJournalReportQuery} query
|
||||||
|
* @returns {Promise<string>}
|
||||||
|
*/
|
||||||
|
public csv(query: IJournalReportQuery) {
|
||||||
|
return this.journalExport.csv(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the journal sheet in pdf format.
|
||||||
|
* @param {IJournalReportQuery} query
|
||||||
|
* @returns {Promise<Buffer>}
|
||||||
|
*/
|
||||||
|
public pdf(query: IJournalReportQuery) {
|
||||||
|
return this.journalPdf.pdf(query);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { IJournalReportQuery } from './JournalSheet.types';
|
||||||
|
import { JournalSheetTableInjectable } from './JournalSheetTableInjectable';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { TableSheet } from '../../common/TableSheet';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class JournalSheetExportInjectable {
|
||||||
|
constructor(
|
||||||
|
private readonly journalSheetTable: JournalSheetTableInjectable,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the trial balance sheet in XLSX format.
|
||||||
|
* @param {IJournalReportQuery} query - Journal report query.
|
||||||
|
* @returns {Promise<Buffer>}
|
||||||
|
*/
|
||||||
|
public async xlsx(query: IJournalReportQuery) {
|
||||||
|
const table = await this.journalSheetTable.table(query);
|
||||||
|
|
||||||
|
const tableSheet = new TableSheet(table.table);
|
||||||
|
const tableCsv = tableSheet.convertToXLSX();
|
||||||
|
|
||||||
|
return tableSheet.convertToBuffer(tableCsv, 'xlsx');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the trial balance sheet in CSV format.
|
||||||
|
* @param {IJournalReportQuery} query - Journal report query.
|
||||||
|
* @returns {Promise<Buffer>}
|
||||||
|
*/
|
||||||
|
public async csv(query: IJournalReportQuery): Promise<string> {
|
||||||
|
const table = await this.journalSheetTable.table(query);
|
||||||
|
|
||||||
|
const tableSheet = new TableSheet(table.table);
|
||||||
|
const tableCsv = tableSheet.convertToCSV();
|
||||||
|
|
||||||
|
return tableCsv;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import moment from 'moment';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { FinancialSheetMeta } from '../../common/FinancialSheetMeta';
|
||||||
|
import { IJournalReportQuery, IJournalSheetMeta } from './JournalSheet.types';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class JournalSheetMeta {
|
||||||
|
constructor(private readonly financialSheetMeta: FinancialSheetMeta) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the journal sheet meta.
|
||||||
|
* @param {IJournalReportQuery} query -
|
||||||
|
* @returns {Promise<IJournalSheetMeta>}
|
||||||
|
*/
|
||||||
|
public async meta(
|
||||||
|
query: IJournalReportQuery,
|
||||||
|
): Promise<IJournalSheetMeta> {
|
||||||
|
const common = await this.financialSheetMeta.meta();
|
||||||
|
|
||||||
|
const formattedToDate = moment(query.toDate).format('YYYY/MM/DD');
|
||||||
|
const formattedFromDate = moment(query.fromDate).format('YYYY/MM/DD');
|
||||||
|
const formattedDateRange = `From ${formattedFromDate} | To ${formattedToDate}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...common,
|
||||||
|
formattedDateRange,
|
||||||
|
formattedFromDate,
|
||||||
|
formattedToDate,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { JournalSheetTableInjectable } from './JournalSheetTableInjectable';
|
||||||
|
import { HtmlTableCustomCss } from './constant';
|
||||||
|
import { TableSheetPdf } from '../../common/TableSheetPdf';
|
||||||
|
import { IJournalReportQuery } from './JournalSheet.types';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class JournalSheetPdfInjectable {
|
||||||
|
constructor(
|
||||||
|
private readonly journalSheetTable: JournalSheetTableInjectable,
|
||||||
|
private readonly tableSheetPdf: TableSheetPdf,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the given journal sheet table to pdf.
|
||||||
|
* @param {number} tenantId - Tenant ID.
|
||||||
|
* @param {IBalanceSheetQuery} query - Balance sheet query.
|
||||||
|
* @returns {Promise<Buffer>}
|
||||||
|
*/
|
||||||
|
public async pdf(query: IJournalReportQuery): Promise<Buffer> {
|
||||||
|
const table = await this.journalSheetTable.table(query);
|
||||||
|
|
||||||
|
return this.tableSheetPdf.convertToPdf(
|
||||||
|
table.table,
|
||||||
|
table.meta.sheetName,
|
||||||
|
table.meta.formattedDateRange,
|
||||||
|
HtmlTableCustomCss,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
import { AccountTransaction } from '@/modules/Accounts/models/AccountTransaction.model';
|
||||||
|
import { AccountRepository } from '@/modules/Accounts/repositories/Account.repository';
|
||||||
|
import { Contact } from '@/modules/Contacts/models/Contact';
|
||||||
|
import { Ledger } from '@/modules/Ledger/Ledger';
|
||||||
|
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
|
||||||
|
import { transformToMap } from '@/utils/transform-to-key';
|
||||||
|
import { Inject } from '@nestjs/common';
|
||||||
|
import { ModelObject } from 'objection';
|
||||||
|
|
||||||
|
export class JournalSheetRepository {
|
||||||
|
@Inject(TenancyContext)
|
||||||
|
private tenancyContext: TenancyContext;
|
||||||
|
|
||||||
|
@Inject(AccountRepository)
|
||||||
|
private accountRepository: AccountRepository;
|
||||||
|
|
||||||
|
@Inject(Contact.name)
|
||||||
|
private contactModel: typeof Contact;
|
||||||
|
|
||||||
|
@Inject(AccountTransaction.name)
|
||||||
|
private accountTransaction: typeof AccountTransaction;
|
||||||
|
|
||||||
|
@Inject(AccountTransaction.name)
|
||||||
|
private accountTransactions: Array<ModelObject<AccountTransaction>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public filter: any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public accountsGraph: any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public contacts: Array<ModelObject<Contact>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contacts by id map.
|
||||||
|
*/
|
||||||
|
public contactsById: Map<number, ModelObject<Contact>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public ledger: Ledger;
|
||||||
|
|
||||||
|
public baseCurrency: string;
|
||||||
|
|
||||||
|
setFilter(filter: any) {
|
||||||
|
this.filter = filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the journal sheet data.
|
||||||
|
*/
|
||||||
|
async load() {
|
||||||
|
await this.initBaseCurrency();
|
||||||
|
await this.initAccountsGraph();
|
||||||
|
await this.initAccountTransactions();
|
||||||
|
await this.initContacts();
|
||||||
|
await this.initLedger();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize base currency.
|
||||||
|
*/
|
||||||
|
async initBaseCurrency () {
|
||||||
|
const metadata = await this.tenancyContext.getTenantMetadata();
|
||||||
|
|
||||||
|
this.baseCurrency = metadata.baseCurrency;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize accounts graph.
|
||||||
|
*/
|
||||||
|
async initAccountsGraph() {
|
||||||
|
// Retrieve all accounts on the storage.
|
||||||
|
const accountsGraph = await this.accountRepository.getDependencyGraph();
|
||||||
|
this.accountsGraph = accountsGraph;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize account transactions.
|
||||||
|
*/
|
||||||
|
async initAccountTransactions() {
|
||||||
|
// Retrieve all journal transactions based on the given query.
|
||||||
|
const transactions = await this.accountTransaction
|
||||||
|
.query()
|
||||||
|
.onBuild((query) => {
|
||||||
|
if (this.filter.fromRange || this.filter.toRange) {
|
||||||
|
query.modify(
|
||||||
|
'filterAmountRange',
|
||||||
|
this.filter.fromRange,
|
||||||
|
this.filter.toRange,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
query.modify(
|
||||||
|
'filterDateRange',
|
||||||
|
this.filter.fromDate,
|
||||||
|
this.filter.toDate,
|
||||||
|
);
|
||||||
|
query.orderBy(['date', 'createdAt', 'indexGroup', 'index']);
|
||||||
|
|
||||||
|
if (this.filter.transactionType) {
|
||||||
|
query.where('reference_type', this.filter.transactionType);
|
||||||
|
}
|
||||||
|
if (this.filter.transactionType && this.filter.transactionId) {
|
||||||
|
query.where('reference_id', this.filter.transactionId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.accountTransactions = transactions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize contacts.
|
||||||
|
*/
|
||||||
|
async initContacts() {
|
||||||
|
const contacts = await this.contactModel.query();
|
||||||
|
|
||||||
|
this.contacts = contacts;
|
||||||
|
this.contactsById = transformToMap(contacts, 'id');
|
||||||
|
}
|
||||||
|
|
||||||
|
async initLedger(){
|
||||||
|
this.ledger = Ledger.fromTransactions(this.accountTransactions);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import { I18nService } from 'nestjs-i18n';
|
||||||
|
import { JournalSheet } from './JournalSheet';
|
||||||
|
import { JournalSheetMeta } from './JournalSheetMeta';
|
||||||
|
import { getJournalSheetDefaultQuery } from './constant';
|
||||||
|
import { IJournalReportQuery, IJournalSheet } from './JournalSheet.types';
|
||||||
|
import { events } from '@/common/events/events';
|
||||||
|
import { JournalSheetRepository } from './JournalSheetRepository';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class JournalSheetService {
|
||||||
|
constructor(
|
||||||
|
private readonly journalSheetMeta: JournalSheetMeta,
|
||||||
|
private readonly journalRepository: JournalSheetRepository,
|
||||||
|
private readonly eventPublisher: EventEmitter2,
|
||||||
|
private readonly i18n: I18nService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Journal sheet.
|
||||||
|
* @param {IJournalReportQuery} query - Journal sheet query.
|
||||||
|
* @returns {Promise<IJournalSheet>}
|
||||||
|
*/
|
||||||
|
async journalSheet(query: IJournalReportQuery): Promise<IJournalSheet> {
|
||||||
|
const filter = {
|
||||||
|
...getJournalSheetDefaultQuery(),
|
||||||
|
...query,
|
||||||
|
};
|
||||||
|
this.journalRepository.setFilter(query);
|
||||||
|
await this.journalRepository.load();
|
||||||
|
|
||||||
|
// Journal report instance.
|
||||||
|
const journalSheetInstance = new JournalSheet(
|
||||||
|
filter,
|
||||||
|
this.journalRepository,
|
||||||
|
this.i18n,
|
||||||
|
);
|
||||||
|
// Retrieve journal report columns.
|
||||||
|
const journalSheetData = journalSheetInstance.reportData();
|
||||||
|
|
||||||
|
// Retrieve the journal sheet meta.
|
||||||
|
const meta = await this.journalSheetMeta.meta(filter);
|
||||||
|
|
||||||
|
// Triggers `onJournalViewed` event.
|
||||||
|
await this.eventPublisher.emitAsync(events.reports.onJournalViewed, {
|
||||||
|
query,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: journalSheetData,
|
||||||
|
query: filter,
|
||||||
|
meta,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,240 @@
|
|||||||
|
import * as R from 'ramda';
|
||||||
|
import { first } from 'lodash';
|
||||||
|
import { I18nService } from 'nestjs-i18n';
|
||||||
|
import {
|
||||||
|
IJournalReportEntriesGroup,
|
||||||
|
IJournalReportQuery,
|
||||||
|
IJournalSheetEntry,
|
||||||
|
IJournalTableData,
|
||||||
|
} from './JournalSheet.types';
|
||||||
|
import { ROW_TYPE } from './types';
|
||||||
|
import { FinancialTable } from '../../common/FinancialTable';
|
||||||
|
import { FinancialSheetStructure } from '../../common/FinancialSheetStructure';
|
||||||
|
import { FinancialSheet } from '../../common/FinancialSheet';
|
||||||
|
import {
|
||||||
|
IColumnMapperMeta,
|
||||||
|
ITableColumn,
|
||||||
|
ITableColumnAccessor,
|
||||||
|
ITableRow,
|
||||||
|
} from '../../types/Table.types';
|
||||||
|
import { tableRowMapper } from '../../utils/Table.utils';
|
||||||
|
import { ILedgerEntry } from '@/modules/Ledger/types/Ledger.types';
|
||||||
|
|
||||||
|
export class JournalSheetTable extends R.pipe(
|
||||||
|
FinancialTable,
|
||||||
|
FinancialSheetStructure,
|
||||||
|
)(FinancialSheet) {
|
||||||
|
data: IJournalTableData;
|
||||||
|
query: IJournalReportQuery;
|
||||||
|
i18n: any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor method.
|
||||||
|
* @param {IJournalTableData} data -
|
||||||
|
* @param {IJournalReportQuery} query -
|
||||||
|
* @param {I18nService} i18n - I18n service.
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
data: IJournalTableData,
|
||||||
|
query: IJournalReportQuery,
|
||||||
|
i18n: I18nService,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.data = data;
|
||||||
|
this.query = query;
|
||||||
|
this.i18n = i18n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the common table accessors.
|
||||||
|
* @returns {ITableColumnAccessor[]}
|
||||||
|
*/
|
||||||
|
private groupColumnsAccessors = (): ITableColumnAccessor[] => {
|
||||||
|
return [
|
||||||
|
{ key: 'date', accessor: 'dateFormatted' },
|
||||||
|
{ key: 'transaction_type', accessor: 'referenceTypeFormatted' },
|
||||||
|
{ key: 'transaction_number', accessor: 'entry.transactionNumber' },
|
||||||
|
{ key: 'description', accessor: 'entry.note' },
|
||||||
|
{ key: 'account_code', accessor: 'entry.accountCode' },
|
||||||
|
{ key: 'account_name', accessor: 'entry.accountName' },
|
||||||
|
{ key: 'debit', accessor: 'entry.formattedDebit' },
|
||||||
|
{ key: 'credit', accessor: 'entry.formattedCredit' },
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the group entry accessors.
|
||||||
|
* @returns {ITableColumnAccessor[]}
|
||||||
|
*/
|
||||||
|
private entryColumnsAccessors = (): ITableColumnAccessor[] => {
|
||||||
|
return [
|
||||||
|
{ key: 'date', accessor: '_empty_' },
|
||||||
|
{ key: 'transaction_type', accessor: '_empty_' },
|
||||||
|
{ key: 'transaction_number', accessor: 'transactionNumber' },
|
||||||
|
{ key: 'description', accessor: 'note' },
|
||||||
|
{ key: 'account_code', accessor: 'accountCode' },
|
||||||
|
{ key: 'account_name', accessor: 'accountName' },
|
||||||
|
{ key: 'debit', accessor: 'formattedDebit' },
|
||||||
|
{ key: 'credit', accessor: 'formattedCredit' },
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the total entry column accessors.
|
||||||
|
* @returns {ITableColumnAccessor[]}
|
||||||
|
*/
|
||||||
|
private totalEntryColumnAccessors = (): ITableColumnAccessor[] => {
|
||||||
|
return [
|
||||||
|
{ key: 'date', accessor: '_empty_' },
|
||||||
|
{ key: 'transaction_type', accessor: '_empty_' },
|
||||||
|
{ key: 'transaction_number', accessor: '_empty_' },
|
||||||
|
{ key: 'description', accessor: '_empty_' },
|
||||||
|
{ key: 'account_code', accessor: '_empty_' },
|
||||||
|
{ key: 'account_name', accessor: '_empty_' },
|
||||||
|
{ key: 'debit', accessor: 'formattedDebit' },
|
||||||
|
{ key: 'credit', accessor: 'formattedCredit' },
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the total entry column accessors.
|
||||||
|
* @returns {IColumnMapperMeta[]}
|
||||||
|
*/
|
||||||
|
private blankEnrtyColumnAccessors = (): IColumnMapperMeta[] => {
|
||||||
|
return [
|
||||||
|
{ key: 'date', value: '' },
|
||||||
|
{ key: 'transaction_type', value: '' },
|
||||||
|
{ key: 'transaction_number', value: '' },
|
||||||
|
{ key: 'description', value: '' },
|
||||||
|
{ key: 'account_code', value: '' },
|
||||||
|
{ key: 'account_name', value: '' },
|
||||||
|
{ key: 'debit', value: '' },
|
||||||
|
{ key: 'credit', value: '' },
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the common columns.
|
||||||
|
* @returns {ITableColumn[]}
|
||||||
|
*/
|
||||||
|
private commonColumns(): ITableColumn[] {
|
||||||
|
return [
|
||||||
|
{ key: 'date', label: 'Date' },
|
||||||
|
{ key: 'transaction_type', label: 'Transaction Type' },
|
||||||
|
{ key: 'transaction_number', label: 'Num.' },
|
||||||
|
{ key: 'description', label: 'Description' },
|
||||||
|
{ key: 'account_code', label: 'Acc. Code' },
|
||||||
|
{ key: 'account_name', label: 'Account' },
|
||||||
|
{ key: 'debit', label: 'Debit' },
|
||||||
|
{ key: 'credit', label: 'Credit' },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps the group and first entry to table row.
|
||||||
|
* @param {IJournalReportEntriesGroup} group
|
||||||
|
* @returns {ITableRow}
|
||||||
|
*/
|
||||||
|
private firstEntryGroupMapper = (
|
||||||
|
group: IJournalReportEntriesGroup,
|
||||||
|
): ITableRow => {
|
||||||
|
const meta = {
|
||||||
|
rowTypes: [ROW_TYPE.ENTRY],
|
||||||
|
};
|
||||||
|
const computedGroup = { ...group, entry: first(group.entries) };
|
||||||
|
const columns = this.groupColumnsAccessors();
|
||||||
|
|
||||||
|
return tableRowMapper(computedGroup, columns, meta);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps the given group entry to table rows.
|
||||||
|
* @param {IJournalEntry} entry
|
||||||
|
* @returns {ITableRow}
|
||||||
|
*/
|
||||||
|
private entryMapper = (entry: IJournalSheetEntry): ITableRow => {
|
||||||
|
const columns = this.entryColumnsAccessors();
|
||||||
|
const meta = {
|
||||||
|
rowTypes: [ROW_TYPE.ENTRY],
|
||||||
|
};
|
||||||
|
return tableRowMapper(entry, columns, meta);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps the given group entries to table rows.
|
||||||
|
* @param {IJournalReportEntriesGroup} group
|
||||||
|
* @returns {ITableRow[]}
|
||||||
|
*/
|
||||||
|
private entriesMapper = (group: IJournalReportEntriesGroup): ITableRow[] => {
|
||||||
|
const entries = R.remove(0, 1, group.entries);
|
||||||
|
|
||||||
|
return R.map(this.entryMapper, entries);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps the given group entry to total table row.
|
||||||
|
* @param {IJournalReportEntriesGroup} group
|
||||||
|
* @returns {ITableRow}
|
||||||
|
*/
|
||||||
|
public totalEntryMapper = (group: IJournalReportEntriesGroup): ITableRow => {
|
||||||
|
const total = this.totalEntryColumnAccessors();
|
||||||
|
const meta = {
|
||||||
|
rowTypes: [ROW_TYPE.TOTAL],
|
||||||
|
};
|
||||||
|
return tableRowMapper(group, total, meta);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the blank entry row.
|
||||||
|
* @returns {ITableRow}
|
||||||
|
*/
|
||||||
|
private blankEntryMapper = (): ITableRow => {
|
||||||
|
const columns = this.blankEnrtyColumnAccessors();
|
||||||
|
const meta = {};
|
||||||
|
return tableRowMapper({} as ILedgerEntry, columns, meta);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps the entry group to table rows.
|
||||||
|
* @param {IJournalReportEntriesGroup} group -
|
||||||
|
* @returns {ITableRow}
|
||||||
|
*/
|
||||||
|
private groupMapper = (group: IJournalReportEntriesGroup): ITableRow[] => {
|
||||||
|
const firstRow = this.firstEntryGroupMapper(group);
|
||||||
|
const lastRows = this.entriesMapper(group);
|
||||||
|
const totalRow = this.totalEntryMapper(group);
|
||||||
|
const blankRow = this.blankEntryMapper();
|
||||||
|
|
||||||
|
return [firstRow, ...lastRows, totalRow, blankRow];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps the given group entries to table rows.
|
||||||
|
* @param {IJournalReportEntriesGroup[]} entries -
|
||||||
|
* @returns {ITableRow[]}
|
||||||
|
*/
|
||||||
|
private groupsMapper = (
|
||||||
|
entries: IJournalReportEntriesGroup[],
|
||||||
|
): ITableRow[] => {
|
||||||
|
return R.compose(R.flatten, R.map(this.groupMapper))(entries);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the table data rows.
|
||||||
|
* @returns {ITableRow[]}
|
||||||
|
*/
|
||||||
|
public tableData(): ITableRow[] {
|
||||||
|
return R.compose(this.groupsMapper)(this.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the table columns.
|
||||||
|
* @returns {ITableColumn[]}
|
||||||
|
*/
|
||||||
|
public tableColumns(): ITableColumn[] {
|
||||||
|
const columns = this.commonColumns();
|
||||||
|
|
||||||
|
return R.compose(this.tableColumnsCellIndexing)(columns);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { JournalSheetService } from './JournalSheetService';
|
||||||
|
import { IJournalReportQuery, IJournalTable } from './JournalSheet.types';
|
||||||
|
import { JournalSheetTable } from './JournalSheetTable';
|
||||||
|
import { I18nService } from 'nestjs-i18n';
|
||||||
|
|
||||||
|
export class JournalSheetTableInjectable {
|
||||||
|
constructor(
|
||||||
|
private readonly journalSheetService: JournalSheetService,
|
||||||
|
private readonly i18nService: I18nService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the journal sheet in table format.
|
||||||
|
* @param {IJournalReportQuery} query - Journal report query.
|
||||||
|
* @returns {Promise<IJournalTable>}
|
||||||
|
*/
|
||||||
|
public async table(query: IJournalReportQuery): Promise<IJournalTable> {
|
||||||
|
const journal = await this.journalSheetService.journalSheet(query);
|
||||||
|
const table = new JournalSheetTable(
|
||||||
|
journal.data,
|
||||||
|
journal.query,
|
||||||
|
this.i18nService,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
table: {
|
||||||
|
columns: table.tableColumns(),
|
||||||
|
rows: table.tableData(),
|
||||||
|
},
|
||||||
|
query: journal.query,
|
||||||
|
meta: journal.meta,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
export const HtmlTableCustomCss = `
|
||||||
|
table tr.row-type--total td{
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
table tr td:not(:first-child) {
|
||||||
|
border-left: 1px solid #ececec;
|
||||||
|
}
|
||||||
|
table tr:last-child td {
|
||||||
|
border-bottom: 1px solid #ececec;
|
||||||
|
}
|
||||||
|
table .cell--credit,
|
||||||
|
table .cell--debit,
|
||||||
|
table .column--credit,
|
||||||
|
table .column--debit{
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const getJournalSheetDefaultQuery = () => ({
|
||||||
|
fromDate: moment().startOf('month').format('YYYY-MM-DD'),
|
||||||
|
toDate: moment().format('YYYY-MM-DD'),
|
||||||
|
fromRange: null,
|
||||||
|
toRange: null,
|
||||||
|
accountsIds: [],
|
||||||
|
numberFormat: {
|
||||||
|
noCents: false,
|
||||||
|
divideOn1000: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
|
||||||
|
export enum ROW_TYPE {
|
||||||
|
ENTRY = 'ENTRY',
|
||||||
|
TOTAL = 'TOTAL'
|
||||||
|
};
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { SalesTaxLiabiltiySummaryPdf } from './SalesTaxLiabiltiySummaryPdf';
|
||||||
|
import { SalesTaxLiabilitySummaryTableInjectable } from './SalesTaxLiabilitySummaryTableInjectable';
|
||||||
|
import { SalesTaxLiabilitySummaryExportInjectable } from './SalesTaxLiabilitySummaryExportInjectable';
|
||||||
|
import { SalesTaxLiabilitySummaryService } from './SalesTaxLiabilitySummaryService';
|
||||||
|
import { SalesTaxLiabilitySummaryApplication } from './SalesTaxLiabilitySummaryApplication';
|
||||||
|
import { SalesTaxLiabilitySummaryController } from './SalesTaxLiabilitySummary.controller';
|
||||||
|
import { FinancialSheetCommonModule } from '../../common/FinancialSheetCommon.module';
|
||||||
|
import { SalesTaxLiabilitySummaryRepository } from './SalesTaxLiabilitySummaryRepository';
|
||||||
|
import { SalesTaxLiabilitySummaryMeta } from './SalesTaxLiabilitySummaryMeta';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [FinancialSheetCommonModule],
|
||||||
|
providers: [
|
||||||
|
SalesTaxLiabiltiySummaryPdf,
|
||||||
|
SalesTaxLiabilitySummaryTableInjectable,
|
||||||
|
SalesTaxLiabilitySummaryExportInjectable,
|
||||||
|
SalesTaxLiabilitySummaryService,
|
||||||
|
SalesTaxLiabilitySummaryRepository,
|
||||||
|
SalesTaxLiabilitySummaryMeta,
|
||||||
|
SalesTaxLiabilitySummaryApplication,
|
||||||
|
],
|
||||||
|
controllers: [SalesTaxLiabilitySummaryController],
|
||||||
|
})
|
||||||
|
export class SalesTaxLiabilityModule {}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { IFinancialSheetCommonMeta } from '../../types/Report.types';
|
||||||
|
import { IFinancialTable } from '../../types/Table.types';
|
||||||
|
|
||||||
|
export interface SalesTaxLiabilitySummaryQuery {
|
||||||
|
fromDate: Date;
|
||||||
|
toDate: Date;
|
||||||
|
basis: 'cash' | 'accrual';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SalesTaxLiabilitySummaryAmount {
|
||||||
|
amount: number;
|
||||||
|
formattedAmount: string;
|
||||||
|
currencyCode: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SalesTaxLiabilitySummaryTotal {
|
||||||
|
taxableAmount: SalesTaxLiabilitySummaryAmount;
|
||||||
|
taxAmount: SalesTaxLiabilitySummaryAmount;
|
||||||
|
collectedTaxAmount: SalesTaxLiabilitySummaryAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SalesTaxLiabilitySummaryRate {
|
||||||
|
id: number;
|
||||||
|
taxName: string;
|
||||||
|
taxableAmount: SalesTaxLiabilitySummaryAmount;
|
||||||
|
taxAmount: SalesTaxLiabilitySummaryAmount;
|
||||||
|
taxPercentage: any;
|
||||||
|
collectedTaxAmount: SalesTaxLiabilitySummaryAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SalesTaxLiabilitySummaryTableRowType {
|
||||||
|
TaxRate = 'TaxRate',
|
||||||
|
Total = 'Total',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SalesTaxLiabilitySummaryReportData {
|
||||||
|
taxRates: SalesTaxLiabilitySummaryRate[];
|
||||||
|
total: SalesTaxLiabilitySummaryTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SalesTaxLiabilitySummaryPayableById = Record<
|
||||||
|
string,
|
||||||
|
{ taxRateId: number; credit: number; debit: number }
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type SalesTaxLiabilitySummarySalesById = Record<
|
||||||
|
string,
|
||||||
|
{ taxRateId: number; credit: number; debit: number }
|
||||||
|
>;
|
||||||
|
|
||||||
|
export interface SalesTaxLiabilitySummaryMeta
|
||||||
|
extends IFinancialSheetCommonMeta {
|
||||||
|
formattedFromDate: string;
|
||||||
|
formattedToDate: string;
|
||||||
|
formattedDateRange: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISalesTaxLiabilitySummaryTable extends IFinancialTable {
|
||||||
|
query: SalesTaxLiabilitySummaryQuery;
|
||||||
|
meta: SalesTaxLiabilitySummaryMeta;
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { Controller, Get, Headers, Query, Res } from '@nestjs/common';
|
||||||
|
import { SalesTaxLiabilitySummaryQuery } from './SalesTaxLiability.types';
|
||||||
|
import { AcceptType } from '@/constants/accept-type';
|
||||||
|
import { SalesTaxLiabilitySummaryApplication } from './SalesTaxLiabilitySummaryApplication';
|
||||||
|
import { Response } from 'express';
|
||||||
|
import { PublicRoute } from '@/modules/Auth/Jwt.guard';
|
||||||
|
|
||||||
|
@Controller('/reports/sales-tax-liability-summary')
|
||||||
|
@PublicRoute()
|
||||||
|
export class SalesTaxLiabilitySummaryController {
|
||||||
|
constructor(
|
||||||
|
private readonly salesTaxLiabilitySummaryApp: SalesTaxLiabilitySummaryApplication,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
public async getSalesTaxLiabilitySummary(
|
||||||
|
@Query() query: SalesTaxLiabilitySummaryQuery,
|
||||||
|
@Res() res: Response,
|
||||||
|
@Headers('accept') acceptHeader: string,
|
||||||
|
) {
|
||||||
|
// Retrieves the json table format.
|
||||||
|
if (acceptHeader.includes(AcceptType.ApplicationJsonTable)) {
|
||||||
|
const table = await this.salesTaxLiabilitySummaryApp.table(query);
|
||||||
|
return res.status(200).send(table);
|
||||||
|
// Retrieves the xlsx format.
|
||||||
|
} else if (acceptHeader.includes(AcceptType.ApplicationXlsx)) {
|
||||||
|
const buffer = await this.salesTaxLiabilitySummaryApp.xlsx(query);
|
||||||
|
res.setHeader('Content-Disposition', 'attachment; filename=output.xlsx');
|
||||||
|
res.setHeader(
|
||||||
|
'Content-Type',
|
||||||
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
);
|
||||||
|
return res.send(buffer);
|
||||||
|
// Retrieves the csv format.
|
||||||
|
} else if (acceptHeader.includes(AcceptType.ApplicationCsv)) {
|
||||||
|
const buffer = await this.salesTaxLiabilitySummaryApp.csv(query);
|
||||||
|
res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
|
||||||
|
res.setHeader('Content-Type', 'text/csv');
|
||||||
|
|
||||||
|
return res.send(buffer);
|
||||||
|
// Retrieves the json format.
|
||||||
|
} else if (acceptHeader.includes(AcceptType.ApplicationPdf)) {
|
||||||
|
const pdfContent = await this.salesTaxLiabilitySummaryApp.pdf(query);
|
||||||
|
res.set({
|
||||||
|
'Content-Type': 'application/pdf',
|
||||||
|
'Content-Length': pdfContent.length,
|
||||||
|
});
|
||||||
|
return res.status(200).send(pdfContent);
|
||||||
|
} else {
|
||||||
|
const sheet = await this.salesTaxLiabilitySummaryApp.sheet(query);
|
||||||
|
return res.status(200).send(sheet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
import * as R from 'ramda';
|
||||||
|
import { isEmpty, sumBy } from 'lodash';
|
||||||
|
import {
|
||||||
|
SalesTaxLiabilitySummaryQuery,
|
||||||
|
SalesTaxLiabilitySummaryRate,
|
||||||
|
SalesTaxLiabilitySummaryReportData,
|
||||||
|
SalesTaxLiabilitySummaryTotal,
|
||||||
|
} from './SalesTaxLiability.types';
|
||||||
|
import { FinancialSheet } from '../../common/FinancialSheet';
|
||||||
|
import { ModelObject } from 'objection';
|
||||||
|
import { TaxRateModel } from '@/modules/TaxRates/models/TaxRate.model';
|
||||||
|
import { SalesTaxLiabilitySummaryRepository } from './SalesTaxLiabilitySummaryRepository';
|
||||||
|
|
||||||
|
export class SalesTaxLiabilitySummary extends FinancialSheet {
|
||||||
|
private query: SalesTaxLiabilitySummaryQuery;
|
||||||
|
private repository: SalesTaxLiabilitySummaryRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sales tax liability summary constructor.
|
||||||
|
* @param {SalesTaxLiabilitySummaryQuery} query
|
||||||
|
* @param {ITaxRate[]} taxRates
|
||||||
|
* @param {SalesTaxLiabilitySummaryPayableById} payableTaxesById
|
||||||
|
* @param {SalesTaxLiabilitySummarySalesById} salesTaxesById
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
query: SalesTaxLiabilitySummaryQuery,
|
||||||
|
repository: SalesTaxLiabilitySummaryRepository,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.query = query;
|
||||||
|
this.repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the tax rate liability node.
|
||||||
|
* @param {ITaxRate} taxRate
|
||||||
|
* @returns {SalesTaxLiabilitySummaryRate}
|
||||||
|
*/
|
||||||
|
private taxRateLiability = (
|
||||||
|
taxRate: ModelObject<TaxRateModel>,
|
||||||
|
): SalesTaxLiabilitySummaryRate => {
|
||||||
|
const payableTax = this.repository.taxesPayableByTaxRateId[taxRate.id];
|
||||||
|
const salesTax = this.repository.accountTransactionsByTaxRateId[taxRate.id];
|
||||||
|
|
||||||
|
const payableTaxAmount = payableTax
|
||||||
|
? payableTax.credit - payableTax.debit
|
||||||
|
: 0;
|
||||||
|
const salesTaxAmount = salesTax ? salesTax.credit - salesTax.debit : 0;
|
||||||
|
|
||||||
|
// Calculates the tax percentage.
|
||||||
|
const taxPercentage = R.compose(
|
||||||
|
R.unless(R.equals(0), R.divide(R.__, salesTaxAmount)),
|
||||||
|
)(payableTaxAmount);
|
||||||
|
|
||||||
|
// Calculates the payable tax amount.
|
||||||
|
const collectedTaxAmount = payableTax ? payableTax.debit : 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: taxRate.id,
|
||||||
|
taxName: `${taxRate.name} (${taxRate.rate}%)`,
|
||||||
|
taxableAmount: this.getAmountMeta(salesTaxAmount),
|
||||||
|
taxAmount: this.getAmountMeta(payableTaxAmount),
|
||||||
|
taxPercentage: this.getPercentageTotalAmountMeta(taxPercentage),
|
||||||
|
collectedTaxAmount: this.getAmountMeta(collectedTaxAmount),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters the non-transactions tax rates.
|
||||||
|
* @param {SalesTaxLiabilitySummaryRate[]} nodes
|
||||||
|
* @returns {SalesTaxLiabilitySummaryRate[]}
|
||||||
|
*/
|
||||||
|
private filterNonTransactionsTaxRates = (
|
||||||
|
nodes: SalesTaxLiabilitySummaryRate[],
|
||||||
|
): SalesTaxLiabilitySummaryRate[] => {
|
||||||
|
return nodes.filter((node) => {
|
||||||
|
const salesTrxs = this.repository.accountTransactionsByTaxRateId[node.id];
|
||||||
|
const payableTrxs = this.repository.taxesPayableByTaxRateId[node.id];
|
||||||
|
|
||||||
|
return !isEmpty(salesTrxs) || !isEmpty(payableTrxs);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the tax rates liability nodes.
|
||||||
|
* @returns {SalesTaxLiabilitySummaryRate[]}
|
||||||
|
*/
|
||||||
|
private taxRatesLiability = (): SalesTaxLiabilitySummaryRate[] => {
|
||||||
|
return R.compose(
|
||||||
|
this.filterNonTransactionsTaxRates,
|
||||||
|
R.map(this.taxRateLiability),
|
||||||
|
)(this.repository.taxRates);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the tax rates total node.
|
||||||
|
* @param {SalesTaxLiabilitySummaryRate[]} nodes
|
||||||
|
* @returns {SalesTaxLiabilitySummaryTotal}
|
||||||
|
*/
|
||||||
|
private taxRatesTotal = (
|
||||||
|
nodes: SalesTaxLiabilitySummaryRate[],
|
||||||
|
): SalesTaxLiabilitySummaryTotal => {
|
||||||
|
const taxableAmount = sumBy(nodes, 'taxableAmount.amount');
|
||||||
|
const taxAmount = sumBy(nodes, 'taxAmount.amount');
|
||||||
|
const collectedTaxAmount = sumBy(nodes, 'collectedTaxAmount.amount');
|
||||||
|
|
||||||
|
return {
|
||||||
|
taxableAmount: this.getTotalAmountMeta(taxableAmount),
|
||||||
|
taxAmount: this.getTotalAmountMeta(taxAmount),
|
||||||
|
collectedTaxAmount: this.getTotalAmountMeta(collectedTaxAmount),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the report data.
|
||||||
|
* @returns {SalesTaxLiabilitySummaryReportData}
|
||||||
|
*/
|
||||||
|
public reportData = (): SalesTaxLiabilitySummaryReportData => {
|
||||||
|
const taxRates = this.taxRatesLiability();
|
||||||
|
const total = this.taxRatesTotal(taxRates);
|
||||||
|
|
||||||
|
return { taxRates, total };
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
|
||||||
|
import { SalesTaxLiabilitySummaryTableInjectable } from './SalesTaxLiabilitySummaryTableInjectable';
|
||||||
|
import { SalesTaxLiabilitySummaryExportInjectable } from './SalesTaxLiabilitySummaryExportInjectable';
|
||||||
|
import { SalesTaxLiabilitySummaryService } from './SalesTaxLiabilitySummaryService';
|
||||||
|
import { SalesTaxLiabiltiySummaryPdf } from './SalesTaxLiabiltiySummaryPdf';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { SalesTaxLiabilitySummaryQuery } from './SalesTaxLiability.types';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SalesTaxLiabilitySummaryApplication {
|
||||||
|
constructor(
|
||||||
|
private readonly salesTaxLiabilitySheet: SalesTaxLiabilitySummaryService,
|
||||||
|
private readonly salesTaxLiabilityExport: SalesTaxLiabilitySummaryExportInjectable,
|
||||||
|
private readonly salesTaxLiabilityTable: SalesTaxLiabilitySummaryTableInjectable,
|
||||||
|
private readonly salesTaxLiabiltiyPdf: SalesTaxLiabiltiySummaryPdf,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the sales tax liability summary in json format.
|
||||||
|
* @param {SalesTaxLiabilitySummaryQuery} query -
|
||||||
|
* @returns {Promise<Buffer>}
|
||||||
|
*/
|
||||||
|
public sheet(query: SalesTaxLiabilitySummaryQuery) {
|
||||||
|
return this.salesTaxLiabilitySheet.salesTaxLiability(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the sales tax liability summary in table format.
|
||||||
|
* @param {SalesTaxLiabilitySummaryQuery} query
|
||||||
|
* @return {Promise<Buffer>}
|
||||||
|
*/
|
||||||
|
public table(query: SalesTaxLiabilitySummaryQuery) {
|
||||||
|
return this.salesTaxLiabilityTable.table(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the sales tax liability summary in XLSX format.
|
||||||
|
* @param {SalesTaxLiabilitySummaryQuery} query
|
||||||
|
* @returns {Promise<Buffer>}
|
||||||
|
*/
|
||||||
|
public xlsx(query: SalesTaxLiabilitySummaryQuery): Promise<Buffer> {
|
||||||
|
return this.salesTaxLiabilityExport.xlsx(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the sales tax liability summary in CSV format.
|
||||||
|
* @param {SalesTaxLiabilitySummaryQuery} query
|
||||||
|
* @returns {Promise<string>}
|
||||||
|
*/
|
||||||
|
public csv(query: SalesTaxLiabilitySummaryQuery): Promise<string> {
|
||||||
|
return this.salesTaxLiabilityExport.csv(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the sales tax liability summary in PDF format.
|
||||||
|
* @param {SalesTaxLiabilitySummaryQuery} query
|
||||||
|
* @returns {Promise<Buffer>}
|
||||||
|
*/
|
||||||
|
public pdf(query: SalesTaxLiabilitySummaryQuery): Promise<Buffer> {
|
||||||
|
return this.salesTaxLiabiltiyPdf.pdf(query);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { TableSheet } from '../../common/TableSheet';
|
||||||
|
import { SalesTaxLiabilitySummaryQuery } from './SalesTaxLiability.types';
|
||||||
|
import { SalesTaxLiabilitySummaryTableInjectable } from './SalesTaxLiabilitySummaryTableInjectable';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SalesTaxLiabilitySummaryExportInjectable {
|
||||||
|
constructor(
|
||||||
|
private readonly salesTaxLiabilityTable: SalesTaxLiabilitySummaryTableInjectable,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the cashflow sheet in XLSX format.
|
||||||
|
* @param {SalesTaxLiabilitySummaryQuery} query
|
||||||
|
* @returns {Promise<Buffer>}
|
||||||
|
*/
|
||||||
|
public async xlsx(query: SalesTaxLiabilitySummaryQuery): Promise<Buffer> {
|
||||||
|
const table = await this.salesTaxLiabilityTable.table(query);
|
||||||
|
|
||||||
|
const tableSheet = new TableSheet(table.table);
|
||||||
|
const tableCsv = tableSheet.convertToXLSX();
|
||||||
|
|
||||||
|
return tableSheet.convertToBuffer(tableCsv, 'xlsx');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the cashflow sheet in CSV format.
|
||||||
|
* @param {SalesTaxLiabilitySummaryQuery} query
|
||||||
|
* @returns {Promise<string>}
|
||||||
|
*/
|
||||||
|
public async csv(query: SalesTaxLiabilitySummaryQuery): Promise<string> {
|
||||||
|
const table = await this.salesTaxLiabilityTable.table(query);
|
||||||
|
|
||||||
|
const tableSheet = new TableSheet(table.table);
|
||||||
|
const tableCsv = tableSheet.convertToCSV();
|
||||||
|
|
||||||
|
return tableCsv;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import * as moment from 'moment';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { FinancialSheetMeta } from '../../common/FinancialSheetMeta';
|
||||||
|
import { SalesTaxLiabilitySummaryQuery } from './SalesTaxLiability.types';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SalesTaxLiabilitySummaryMeta {
|
||||||
|
constructor(private readonly financialSheetMeta: FinancialSheetMeta) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the report meta.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {SalesTaxLiabilitySummaryQuery} filter
|
||||||
|
*/
|
||||||
|
public async meta(query: SalesTaxLiabilitySummaryQuery) {
|
||||||
|
const commonMeta = await this.financialSheetMeta.meta();
|
||||||
|
const formattedToDate = moment(query.toDate).format('YYYY/MM/DD');
|
||||||
|
const formattedFromDate = moment(query.fromDate).format('YYYY/MM/DD');
|
||||||
|
const formattedDateRange = `From ${formattedFromDate} | To ${formattedToDate}`;
|
||||||
|
|
||||||
|
const sheetName = 'Sales Tax Liability Summary';
|
||||||
|
|
||||||
|
return {
|
||||||
|
...commonMeta,
|
||||||
|
sheetName,
|
||||||
|
formattedFromDate,
|
||||||
|
formattedToDate,
|
||||||
|
formattedDateRange,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
import { ACCOUNT_TYPE } from '@/constants/accounts';
|
||||||
|
import {
|
||||||
|
SalesTaxLiabilitySummaryPayableById,
|
||||||
|
SalesTaxLiabilitySummarySalesById,
|
||||||
|
} from './SalesTaxLiability.types';
|
||||||
|
import { Inject, Injectable, Scope } from '@nestjs/common';
|
||||||
|
import { keyBy } from 'lodash';
|
||||||
|
import { TaxRateModel } from '@/modules/TaxRates/models/TaxRate.model';
|
||||||
|
import { AccountTransaction } from '@/modules/Accounts/models/AccountTransaction.model';
|
||||||
|
import { Account } from '@/modules/Accounts/models/Account.model';
|
||||||
|
import { ModelObject } from 'objection';
|
||||||
|
|
||||||
|
@Injectable({ scope: Scope.TRANSIENT })
|
||||||
|
export class SalesTaxLiabilitySummaryRepository {
|
||||||
|
@Inject(TaxRateModel.name)
|
||||||
|
private readonly taxRateModel: typeof TaxRateModel;
|
||||||
|
|
||||||
|
@Inject(AccountTransaction.name)
|
||||||
|
private readonly accountTransactionModel: typeof AccountTransaction;
|
||||||
|
|
||||||
|
@Inject(Account.name)
|
||||||
|
private readonly accountModel: typeof Account;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {SalesTaxLiabilitySummarySalesById}
|
||||||
|
*/
|
||||||
|
accountTransactionsByTaxRateId: SalesTaxLiabilitySummarySalesById;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {SalesTaxLiabilitySummaryPayableById}
|
||||||
|
*/
|
||||||
|
taxesPayableByTaxRateId: SalesTaxLiabilitySummaryPayableById;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Array<ModelObject<TaxRateModel>>}
|
||||||
|
*/
|
||||||
|
taxRates: Array<ModelObject<TaxRateModel>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load data.
|
||||||
|
*/
|
||||||
|
async load() {
|
||||||
|
await this.initTaxRates();
|
||||||
|
await this.initTaxesPayableByTaxRateId();
|
||||||
|
await this.initAccountTransactionsByTaxRateId();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize tax rates.
|
||||||
|
*/
|
||||||
|
async initTaxRates() {
|
||||||
|
const taxRates = await this.getTaxRates();
|
||||||
|
this.taxRates = taxRates;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize account transactions by tax rate id.
|
||||||
|
*/
|
||||||
|
async initAccountTransactionsByTaxRateId() {
|
||||||
|
const transactionsByTaxRateId = await this.taxesSalesSumGroupedByRateId();
|
||||||
|
|
||||||
|
this.accountTransactionsByTaxRateId = transactionsByTaxRateId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize taxes payable by tax rate id.
|
||||||
|
*/
|
||||||
|
async initTaxesPayableByTaxRateId() {
|
||||||
|
const payableTaxes = await this.getTaxesPayableSumGroupedByRateId();
|
||||||
|
|
||||||
|
this.taxesPayableByTaxRateId = payableTaxes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve tax rates.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @returns {Promise<TaxRate[]>}
|
||||||
|
*/
|
||||||
|
public getTaxRates = () => {
|
||||||
|
return this.taxRateModel.query().orderBy('name', 'desc');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve taxes payable sum grouped by tax rate id.
|
||||||
|
* @returns {Promise<SalesTaxLiabilitySummaryPayableById>}
|
||||||
|
*/
|
||||||
|
public async getTaxesPayableSumGroupedByRateId(): Promise<SalesTaxLiabilitySummaryPayableById> {
|
||||||
|
// Retrieves tax payable accounts.
|
||||||
|
const taxPayableAccounts = await this.accountModel
|
||||||
|
.query()
|
||||||
|
.whereIn('accountType', [ACCOUNT_TYPE.TAX_PAYABLE]);
|
||||||
|
|
||||||
|
const payableAccountsIds = taxPayableAccounts.map((account) => account.id);
|
||||||
|
|
||||||
|
const groupedTaxesById = await this.accountTransactionModel
|
||||||
|
.query()
|
||||||
|
.whereIn('account_id', payableAccountsIds)
|
||||||
|
.whereNot('tax_rate_id', null)
|
||||||
|
.groupBy('tax_rate_id')
|
||||||
|
.select(['tax_rate_id'])
|
||||||
|
.sum('credit as credit')
|
||||||
|
.sum('debit as debit');
|
||||||
|
|
||||||
|
return keyBy(groupedTaxesById, 'taxRateId');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve taxes sales sum grouped by tax rate id.
|
||||||
|
* @returns {Promise<SalesTaxLiabilitySummarySalesById>}
|
||||||
|
*/
|
||||||
|
public taxesSalesSumGroupedByRateId =
|
||||||
|
async (): Promise<SalesTaxLiabilitySummarySalesById> => {
|
||||||
|
const incomeAccounts = await this.accountModel
|
||||||
|
.query()
|
||||||
|
.whereIn('accountType', [
|
||||||
|
ACCOUNT_TYPE.INCOME,
|
||||||
|
ACCOUNT_TYPE.OTHER_INCOME,
|
||||||
|
]);
|
||||||
|
const incomeAccountsIds = incomeAccounts.map((account) => account.id);
|
||||||
|
|
||||||
|
const groupedTaxesById = await this.accountTransactionModel
|
||||||
|
.query()
|
||||||
|
.whereIn('account_id', incomeAccountsIds)
|
||||||
|
.whereNot('tax_rate_id', null)
|
||||||
|
.groupBy('tax_rate_id')
|
||||||
|
.select(['tax_rate_id'])
|
||||||
|
.sum('credit as credit')
|
||||||
|
.sum('debit as debit');
|
||||||
|
|
||||||
|
return keyBy(groupedTaxesById, 'taxRateId');
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { SalesTaxLiabilitySummaryRepository } from './SalesTaxLiabilitySummaryRepository';
|
||||||
|
import { SalesTaxLiabilitySummary } from './SalesTaxLiabilitySummary';
|
||||||
|
import { SalesTaxLiabilitySummaryMeta } from './SalesTaxLiabilitySummaryMeta';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { SalesTaxLiabilitySummaryQuery } from './SalesTaxLiability.types';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SalesTaxLiabilitySummaryService {
|
||||||
|
constructor(
|
||||||
|
private readonly repository: SalesTaxLiabilitySummaryRepository,
|
||||||
|
private readonly salesTaxLiabilityMeta: SalesTaxLiabilitySummaryMeta,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve sales tax liability summary.
|
||||||
|
* @param {SalesTaxLiabilitySummaryQuery} query
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
public async salesTaxLiability(query: SalesTaxLiabilitySummaryQuery) {
|
||||||
|
await this.repository.load();
|
||||||
|
|
||||||
|
const taxLiabilitySummary = new SalesTaxLiabilitySummary(
|
||||||
|
query,
|
||||||
|
this.repository,
|
||||||
|
);
|
||||||
|
const meta = await this.salesTaxLiabilityMeta.meta(query);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: taxLiabilitySummary.reportData(),
|
||||||
|
query,
|
||||||
|
meta,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
import * as R from 'ramda';
|
||||||
|
import {
|
||||||
|
SalesTaxLiabilitySummaryQuery,
|
||||||
|
SalesTaxLiabilitySummaryRate,
|
||||||
|
SalesTaxLiabilitySummaryReportData,
|
||||||
|
SalesTaxLiabilitySummaryTotal,
|
||||||
|
} from './SalesTaxLiability.types';
|
||||||
|
import { AgingReport } from '../AgingSummary/AgingReport';
|
||||||
|
import { IROW_TYPE } from './_constants';
|
||||||
|
import { FinancialTable } from '../../common/FinancialTable';
|
||||||
|
import { FinancialSheetStructure } from '../../common/FinancialSheetStructure';
|
||||||
|
import { ITableRow } from '../../types/Table.types';
|
||||||
|
import { ITableColumn } from '../../types/Table.types';
|
||||||
|
import { tableRowMapper } from '../../utils/Table.utils';
|
||||||
|
|
||||||
|
export class SalesTaxLiabilitySummaryTable extends R.pipe(
|
||||||
|
FinancialTable,
|
||||||
|
FinancialSheetStructure,
|
||||||
|
)(AgingReport) {
|
||||||
|
private data: SalesTaxLiabilitySummaryReportData;
|
||||||
|
private query: SalesTaxLiabilitySummaryQuery;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sales tax liability summary table constructor.
|
||||||
|
* @param {SalesTaxLiabilitySummaryReportData} data
|
||||||
|
* @param {SalesTaxLiabilitySummaryQuery} query
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
data: SalesTaxLiabilitySummaryReportData,
|
||||||
|
query: SalesTaxLiabilitySummaryQuery,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.data = data;
|
||||||
|
this.query = query;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the tax rate row accessors.
|
||||||
|
* @returns {ITableColumnAccessor[]}
|
||||||
|
*/
|
||||||
|
private get taxRateRowAccessor() {
|
||||||
|
return [
|
||||||
|
{ key: 'taxName', accessor: 'taxName' },
|
||||||
|
{ key: 'taxPercentage', accessor: 'taxPercentage.formattedAmount' },
|
||||||
|
{ key: 'taxableAmount', accessor: 'taxableAmount.formattedAmount' },
|
||||||
|
{ key: 'collectedTax', accessor: 'collectedTaxAmount.formattedAmount' },
|
||||||
|
{ key: 'taxAmount', accessor: 'taxAmount.formattedAmount' },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the tax rate total row accessors.
|
||||||
|
* @returns {ITableColumnAccessor[]}
|
||||||
|
*/
|
||||||
|
private get taxRateTotalRowAccessors() {
|
||||||
|
return [
|
||||||
|
{ key: 'taxName', value: 'Total' },
|
||||||
|
{ key: 'taxPercentage', value: '' },
|
||||||
|
{ key: 'taxableAmount', accessor: 'taxableAmount.formattedAmount' },
|
||||||
|
{ key: 'collectedTax', accessor: 'collectedTaxAmount.formattedAmount' },
|
||||||
|
{ key: 'taxAmount', accessor: 'taxAmount.formattedAmount' },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps the tax rate node to table row.
|
||||||
|
* @param {SalesTaxLiabilitySummaryRate} node
|
||||||
|
* @returns {ITableRow}
|
||||||
|
*/
|
||||||
|
private taxRateTableRowMapper = (
|
||||||
|
node: SalesTaxLiabilitySummaryRate,
|
||||||
|
): ITableRow => {
|
||||||
|
const columns = this.taxRateRowAccessor;
|
||||||
|
const meta = {
|
||||||
|
rowTypes: [IROW_TYPE.TaxRate],
|
||||||
|
id: node.id,
|
||||||
|
};
|
||||||
|
return tableRowMapper(node, columns, meta);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps the tax rates nodes to table rows.
|
||||||
|
* @param {SalesTaxLiabilitySummaryRate[]} nodes
|
||||||
|
* @returns {ITableRow[]}
|
||||||
|
*/
|
||||||
|
private taxRatesTableRowsMapper = (
|
||||||
|
nodes: SalesTaxLiabilitySummaryRate[],
|
||||||
|
): ITableRow[] => {
|
||||||
|
return nodes.map(this.taxRateTableRowMapper);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps the tax rate total node to table row.
|
||||||
|
* @param {SalesTaxLiabilitySummaryTotal} node
|
||||||
|
* @returns {ITableRow}
|
||||||
|
*/
|
||||||
|
private taxRateTotalRowMapper = (node: SalesTaxLiabilitySummaryTotal) => {
|
||||||
|
const columns = this.taxRateTotalRowAccessors;
|
||||||
|
const meta = {
|
||||||
|
rowTypes: [IROW_TYPE.Total],
|
||||||
|
};
|
||||||
|
return tableRowMapper(node, columns, meta);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the tax rate total row.
|
||||||
|
* @returns {ITableRow}
|
||||||
|
*/
|
||||||
|
private get taxRateTotalRow(): ITableRow {
|
||||||
|
return this.taxRateTotalRowMapper(this.data.total);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the tax rates rows.
|
||||||
|
* @returns {ITableRow[]}
|
||||||
|
*/
|
||||||
|
private get taxRatesRows(): ITableRow[] {
|
||||||
|
return this.taxRatesTableRowsMapper(this.data.taxRates);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the table rows.
|
||||||
|
* @returns {ITableRow[]}
|
||||||
|
*/
|
||||||
|
public tableRows(): ITableRow[] {
|
||||||
|
return R.compose(
|
||||||
|
R.unless(R.isEmpty, R.append(this.taxRateTotalRow)),
|
||||||
|
R.concat(this.taxRatesRows),
|
||||||
|
)([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the table columns.
|
||||||
|
* @returns {ITableColumn[]}
|
||||||
|
*/
|
||||||
|
public tableColumns(): ITableColumn[] {
|
||||||
|
return R.compose(this.tableColumnsCellIndexing)([
|
||||||
|
{
|
||||||
|
label: 'Tax Name',
|
||||||
|
key: 'taxName',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Tax Percentage',
|
||||||
|
key: 'taxPercentage',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Taxable Amount',
|
||||||
|
key: 'taxableAmount',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Collected Tax',
|
||||||
|
key: 'collectedTax',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Tax Amount',
|
||||||
|
key: 'taxRate',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import {
|
||||||
|
ISalesTaxLiabilitySummaryTable,
|
||||||
|
SalesTaxLiabilitySummaryQuery,
|
||||||
|
} from './SalesTaxLiability.types';
|
||||||
|
import { SalesTaxLiabilitySummaryTable } from './SalesTaxLiabilitySummaryTable';
|
||||||
|
import { SalesTaxLiabilitySummaryService } from './SalesTaxLiabilitySummaryService';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SalesTaxLiabilitySummaryTableInjectable {
|
||||||
|
constructor(
|
||||||
|
private readonly salesTaxLiability: SalesTaxLiabilitySummaryService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve sales tax liability summary table.
|
||||||
|
* @param {SalesTaxLiabilitySummaryQuery} query
|
||||||
|
* @returns {Promise<ISalesTaxLiabilitySummaryTable>}
|
||||||
|
*/
|
||||||
|
public async table(
|
||||||
|
query: SalesTaxLiabilitySummaryQuery,
|
||||||
|
): Promise<ISalesTaxLiabilitySummaryTable> {
|
||||||
|
const report = await this.salesTaxLiability.salesTaxLiability(query);
|
||||||
|
// Creates the sales tax liability summary table.
|
||||||
|
const table = new SalesTaxLiabilitySummaryTable(report.data, query);
|
||||||
|
|
||||||
|
return {
|
||||||
|
table: {
|
||||||
|
rows: table.tableRows(),
|
||||||
|
columns: table.tableColumns(),
|
||||||
|
},
|
||||||
|
query: report.query,
|
||||||
|
meta: report.meta,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { SalesTaxLiabilitySummaryTableInjectable } from './SalesTaxLiabilitySummaryTableInjectable';
|
||||||
|
import { TableSheetPdf } from '../../common/TableSheetPdf';
|
||||||
|
import { SalesTaxLiabilitySummaryQuery } from './SalesTaxLiability.types';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SalesTaxLiabiltiySummaryPdf {
|
||||||
|
constructor(
|
||||||
|
private readonly salesTaxLiabiltiySummaryTable: SalesTaxLiabilitySummaryTableInjectable,
|
||||||
|
private readonly tableSheetPdf: TableSheetPdf,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the given sales tax liability summary table to pdf.
|
||||||
|
* @param {ISalesByItemsReportQuery} query - Balance sheet query.
|
||||||
|
* @returns {Promise<Buffer>}
|
||||||
|
*/
|
||||||
|
public async pdf(query: SalesTaxLiabilitySummaryQuery): Promise<Buffer> {
|
||||||
|
const table = await this.salesTaxLiabiltiySummaryTable.table(
|
||||||
|
query,
|
||||||
|
);
|
||||||
|
return this.tableSheetPdf.convertToPdf(
|
||||||
|
table.table,
|
||||||
|
table.meta.sheetName,
|
||||||
|
table.meta.formattedDateRange,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export enum IROW_TYPE {
|
||||||
|
TaxRate = 'TaxRate',
|
||||||
|
Total = 'Total',
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@ import { TransactionsByContactRepository } from './TransactionsByContactReposito
|
|||||||
|
|
||||||
export class TransactionsByContact extends FinancialSheet {
|
export class TransactionsByContact extends FinancialSheet {
|
||||||
public readonly filter: ITransactionsByContactsFilter;
|
public readonly filter: ITransactionsByContactsFilter;
|
||||||
public readonly i18n: I18nService
|
public readonly i18n: I18nService;
|
||||||
public readonly repository: TransactionsByContactRepository;
|
public readonly repository: TransactionsByContactRepository;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -22,7 +22,7 @@ export class TransactionsByContact extends FinancialSheet {
|
|||||||
* @return {Omit<ITransactionsByContactsTransaction, 'runningBalance'>}
|
* @return {Omit<ITransactionsByContactsTransaction, 'runningBalance'>}
|
||||||
*/
|
*/
|
||||||
protected contactTransactionMapper(
|
protected contactTransactionMapper(
|
||||||
entry: ILedgerEntry
|
entry: ILedgerEntry,
|
||||||
): Omit<ITransactionsByContactsTransaction, 'runningBalance'> {
|
): Omit<ITransactionsByContactsTransaction, 'runningBalance'> {
|
||||||
const account = this.repository.accountsGraph.getNodeData(entry.accountId);
|
const account = this.repository.accountsGraph.getNodeData(entry.accountId);
|
||||||
const currencyCode = this.baseCurrency;
|
const currencyCode = this.baseCurrency;
|
||||||
@@ -37,6 +37,7 @@ export class TransactionsByContact extends FinancialSheet {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
// transactionType: this.i18n.t(entry.referenceTypeFormatted),
|
// transactionType: this.i18n.t(entry.referenceTypeFormatted),
|
||||||
transactionType: '',
|
transactionType: '',
|
||||||
|
// @ts-ignore
|
||||||
date: entry.date,
|
date: entry.date,
|
||||||
createdAt: entry.createdAt,
|
createdAt: entry.createdAt,
|
||||||
};
|
};
|
||||||
@@ -51,7 +52,7 @@ export class TransactionsByContact extends FinancialSheet {
|
|||||||
protected contactTransactionRunningBalance(
|
protected contactTransactionRunningBalance(
|
||||||
openingBalance: number,
|
openingBalance: number,
|
||||||
accountNormal: 'credit' | 'debit',
|
accountNormal: 'credit' | 'debit',
|
||||||
transactions: Omit<ITransactionsByContactsTransaction, 'runningBalance'>[]
|
transactions: Omit<ITransactionsByContactsTransaction, 'runningBalance'>[],
|
||||||
): any {
|
): any {
|
||||||
let _openingBalance = openingBalance;
|
let _openingBalance = openingBalance;
|
||||||
|
|
||||||
@@ -69,10 +70,10 @@ export class TransactionsByContact extends FinancialSheet {
|
|||||||
|
|
||||||
const runningBalance = this.getTotalAmountMeta(
|
const runningBalance = this.getTotalAmountMeta(
|
||||||
_openingBalance,
|
_openingBalance,
|
||||||
transaction.currencyCode
|
transaction.currencyCode,
|
||||||
);
|
);
|
||||||
return { ...transaction, runningBalance };
|
return { ...transaction, runningBalance };
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +86,7 @@ export class TransactionsByContact extends FinancialSheet {
|
|||||||
protected getContactClosingBalance(
|
protected getContactClosingBalance(
|
||||||
customerTransactions: ITransactionsByContactsTransaction[],
|
customerTransactions: ITransactionsByContactsTransaction[],
|
||||||
contactNormal: 'credit' | 'debit',
|
contactNormal: 'credit' | 'debit',
|
||||||
openingBalance: number
|
openingBalance: number,
|
||||||
): number {
|
): number {
|
||||||
const closingBalance = openingBalance;
|
const closingBalance = openingBalance;
|
||||||
|
|
||||||
@@ -124,7 +125,7 @@ export class TransactionsByContact extends FinancialSheet {
|
|||||||
*/
|
*/
|
||||||
protected getContactAmount(
|
protected getContactAmount(
|
||||||
amount: number,
|
amount: number,
|
||||||
currencyCode: string
|
currencyCode: string,
|
||||||
): ITransactionsByContactsAmount {
|
): ITransactionsByContactsAmount {
|
||||||
return {
|
return {
|
||||||
amount,
|
amount,
|
||||||
@@ -153,7 +154,7 @@ export class TransactionsByContact extends FinancialSheet {
|
|||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
private filterContactByNoneTransaction = (
|
private filterContactByNoneTransaction = (
|
||||||
transactionsByContact: ITransactionsByContactsContact
|
transactionsByContact: ITransactionsByContactsContact,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
return transactionsByContact.transactions.length > 0;
|
return transactionsByContact.transactions.length > 0;
|
||||||
};
|
};
|
||||||
@@ -164,7 +165,7 @@ export class TransactionsByContact extends FinancialSheet {
|
|||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
private filterContactNoneZero = (
|
private filterContactNoneZero = (
|
||||||
transactionsByContact: ITransactionsByContactsContact
|
transactionsByContact: ITransactionsByContactsContact,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
return transactionsByContact.closingBalance.amount !== 0;
|
return transactionsByContact.closingBalance.amount !== 0;
|
||||||
};
|
};
|
||||||
@@ -190,7 +191,7 @@ export class TransactionsByContact extends FinancialSheet {
|
|||||||
* @returns {ICustomerBalanceSummaryCustomer[]}
|
* @returns {ICustomerBalanceSummaryCustomer[]}
|
||||||
*/
|
*/
|
||||||
protected contactsFilter = (
|
protected contactsFilter = (
|
||||||
nodes: ITransactionsByContactsContact[]
|
nodes: ITransactionsByContactsContact[],
|
||||||
): ITransactionsByContactsContact[] => {
|
): ITransactionsByContactsContact[] => {
|
||||||
return nodes.filter(this.contactNodeFilter);
|
return nodes.filter(this.contactNodeFilter);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Knex } from 'knex';
|
import { Knex } from 'knex';
|
||||||
|
import * as moment from 'moment';
|
||||||
|
|
||||||
export interface ILedger {
|
export interface ILedger {
|
||||||
entries: ILedgerEntry[];
|
entries: ILedgerEntry[];
|
||||||
@@ -41,7 +42,7 @@ export interface ILedgerEntry {
|
|||||||
accountId?: number;
|
accountId?: number;
|
||||||
accountNormal: string;
|
accountNormal: string;
|
||||||
contactId?: number;
|
contactId?: number;
|
||||||
date: Date | string;
|
date: moment.MomentInput;
|
||||||
|
|
||||||
transactionType: string;
|
transactionType: string;
|
||||||
transactionSubType?: string;
|
transactionSubType?: string;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export const transformLedgerEntryToTransaction = (
|
|||||||
entry: ILedgerEntry
|
entry: ILedgerEntry
|
||||||
): Partial<AccountTransaction> => {
|
): Partial<AccountTransaction> => {
|
||||||
return {
|
return {
|
||||||
date: entry.date,
|
date: moment(entry.date).toDate(),
|
||||||
|
|
||||||
credit: entry.credit,
|
credit: entry.credit,
|
||||||
debit: entry.debit,
|
debit: entry.debit,
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ import { MailNotificationModule } from '../MailNotification/MailNotification.mod
|
|||||||
MailModule,
|
MailModule,
|
||||||
MailNotificationModule,
|
MailNotificationModule,
|
||||||
InventoryCostModule,
|
InventoryCostModule,
|
||||||
DynamicListModule
|
DynamicListModule,
|
||||||
],
|
],
|
||||||
controllers: [SaleInvoicesController],
|
controllers: [SaleInvoicesController],
|
||||||
providers: [
|
providers: [
|
||||||
@@ -94,7 +94,8 @@ import { MailNotificationModule } from '../MailNotification/MailNotification.mod
|
|||||||
SendSaleInvoiceMail,
|
SendSaleInvoiceMail,
|
||||||
GetSaleInvoicesService,
|
GetSaleInvoicesService,
|
||||||
GetSaleInvoiceMailState,
|
GetSaleInvoiceMailState,
|
||||||
SendSaleInvoiceMailCommon
|
SendSaleInvoiceMailCommon,
|
||||||
],
|
],
|
||||||
|
exports: [GetSaleInvoice],
|
||||||
})
|
})
|
||||||
export class SaleInvoicesModule {}
|
export class SaleInvoicesModule {}
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { Knex } from 'knex';
|
||||||
|
import { GetSaleInvoice } from '../SaleInvoices/queries/GetSaleInvoice.service';
|
||||||
|
import { CreatePaymentReceivedService } from '../PaymentReceived/commands/CreatePaymentReceived.serivce';
|
||||||
|
import { UnitOfWork } from '../Tenancy/TenancyDB/UnitOfWork.service';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { AccountRepository } from '../Accounts/repositories/Account.repository';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CreatePaymentReceiveStripePayment {
|
||||||
|
constructor(
|
||||||
|
private readonly getSaleInvoiceService: GetSaleInvoice,
|
||||||
|
private readonly createPaymentReceivedService: CreatePaymentReceivedService,
|
||||||
|
private readonly uow: UnitOfWork,
|
||||||
|
private readonly accountRepository: AccountRepository,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a payment received transaction associated to the given invoice.
|
||||||
|
* @param {number} saleInvoiceId - Sale invoice id.
|
||||||
|
* @param {number} paidAmount - Paid amount.
|
||||||
|
*/
|
||||||
|
async createPaymentReceived(saleInvoiceId: number, paidAmount: number) {
|
||||||
|
// Create a payment received transaction under UOW envirement.
|
||||||
|
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
|
||||||
|
// Finds or creates a new stripe payment clearing account (current asset).
|
||||||
|
const stripeClearingAccount =
|
||||||
|
await this.accountRepository.findOrCreateStripeClearing({}, trx);
|
||||||
|
|
||||||
|
// Retrieves the given invoice to create payment transaction associated to it.
|
||||||
|
const invoice =
|
||||||
|
await this.getSaleInvoiceService.getSaleInvoice(saleInvoiceId);
|
||||||
|
|
||||||
|
const paymentReceivedDTO = {
|
||||||
|
customerId: invoice.customerId,
|
||||||
|
paymentDate: new Date(),
|
||||||
|
amount: paidAmount,
|
||||||
|
exchangeRate: 1,
|
||||||
|
referenceNo: '',
|
||||||
|
statement: '',
|
||||||
|
depositAccountId: stripeClearingAccount.id,
|
||||||
|
entries: [{ invoiceId: saleInvoiceId, paymentAmount: paidAmount }],
|
||||||
|
};
|
||||||
|
// Create a payment received transaction associated to the given invoice.
|
||||||
|
await this.createPaymentReceivedService.createPaymentReceived(
|
||||||
|
paymentReceivedDTO,
|
||||||
|
trx,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { StripePaymentService } from './StripePaymentService';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CreateStripeAccountLinkService {
|
||||||
|
constructor(private readonly stripePaymentService: StripePaymentService) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new Stripe account id.
|
||||||
|
* @param {string} stripeAccountId - Stripe account id.
|
||||||
|
*/
|
||||||
|
public createAccountLink(stripeAccountId: string) {
|
||||||
|
return this.stripePaymentService.createAccountLink(stripeAccountId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { CreateStripeAccountDTO } from './types';
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import { StripePaymentService } from './StripePaymentService';
|
||||||
|
import { events } from '@/common/events/events';
|
||||||
|
import { PaymentIntegration } from './models/PaymentIntegration.model';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CreateStripeAccountService {
|
||||||
|
constructor(
|
||||||
|
private readonly stripePaymentService: StripePaymentService,
|
||||||
|
private readonly eventPublisher: EventEmitter2,
|
||||||
|
|
||||||
|
@Inject(PaymentIntegration.name)
|
||||||
|
private readonly paymentIntegrationModel: typeof PaymentIntegration,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new Stripe account.
|
||||||
|
* @param {CreateStripeAccountDTO} stripeAccountDTO
|
||||||
|
* @returns {Promise<string>}
|
||||||
|
*/
|
||||||
|
async createStripeAccount(
|
||||||
|
stripeAccountDTO?: CreateStripeAccountDTO,
|
||||||
|
): Promise<string> {
|
||||||
|
const stripeAccount = await this.stripePaymentService.createAccount();
|
||||||
|
const stripeAccountId = stripeAccount.id;
|
||||||
|
|
||||||
|
const parsedStripeAccountDTO = {
|
||||||
|
name: 'Stripe',
|
||||||
|
...stripeAccountDTO,
|
||||||
|
};
|
||||||
|
// Stores the details of the Stripe account.
|
||||||
|
await this.paymentIntegrationModel.query().insert({
|
||||||
|
name: parsedStripeAccountDTO.name,
|
||||||
|
accountId: stripeAccountId,
|
||||||
|
active: false, // Active will turn true after onboarding.
|
||||||
|
service: 'Stripe',
|
||||||
|
});
|
||||||
|
// Triggers `onStripeIntegrationAccountCreated` event.
|
||||||
|
await this.eventPublisher.emitAsync(
|
||||||
|
events.stripeIntegration.onAccountCreated,
|
||||||
|
{
|
||||||
|
stripeAccountDTO,
|
||||||
|
stripeAccountId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return stripeAccountId;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { StripePaymentService } from './StripePaymentService';
|
||||||
|
import { Knex } from 'knex';
|
||||||
|
import { StripeOAuthCodeGrantedEventPayload } from './types';
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { UnitOfWork } from '../Tenancy/TenancyDB/UnitOfWork.service';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import { PaymentIntegration } from './models/PaymentIntegration.model';
|
||||||
|
import { events } from '@/common/events/events';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ExchangeStripeOAuthTokenService {
|
||||||
|
constructor(
|
||||||
|
private readonly stripePaymentService: StripePaymentService,
|
||||||
|
private readonly eventPublisher: EventEmitter2,
|
||||||
|
private readonly uow: UnitOfWork,
|
||||||
|
|
||||||
|
@Inject(PaymentIntegration.name)
|
||||||
|
private readonly paymentIntegrationModel: typeof PaymentIntegration,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exchange stripe oauth authorization code to access token and user id.
|
||||||
|
* @param {string} authorizationCode
|
||||||
|
*/
|
||||||
|
public async excahngeStripeOAuthToken(authorizationCode: string) {
|
||||||
|
const stripe = this.stripePaymentService.stripe;
|
||||||
|
|
||||||
|
const response = await stripe.oauth.token({
|
||||||
|
grant_type: 'authorization_code',
|
||||||
|
code: authorizationCode,
|
||||||
|
});
|
||||||
|
// const accessToken = response.access_token;
|
||||||
|
// const refreshToken = response.refresh_token;
|
||||||
|
const stripeUserId = response.stripe_user_id;
|
||||||
|
|
||||||
|
// Retrieves details of the Stripe account.
|
||||||
|
const account = await stripe.accounts.retrieve(stripeUserId, {
|
||||||
|
expand: ['business_profile'],
|
||||||
|
});
|
||||||
|
const companyName = account.business_profile?.name || 'Unknown name';
|
||||||
|
const paymentEnabled = account.charges_enabled;
|
||||||
|
const payoutEnabled = account.payouts_enabled;
|
||||||
|
|
||||||
|
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
|
||||||
|
// Stores the details of the Stripe account.
|
||||||
|
const paymentIntegration = await this.paymentIntegrationModel
|
||||||
|
.query(trx)
|
||||||
|
.insert({
|
||||||
|
name: companyName,
|
||||||
|
service: 'Stripe',
|
||||||
|
accountId: stripeUserId,
|
||||||
|
paymentEnabled,
|
||||||
|
payoutEnabled,
|
||||||
|
});
|
||||||
|
// Triggers `onStripeOAuthCodeGranted` event.
|
||||||
|
await this.eventPublisher.emitAsync(
|
||||||
|
events.stripeIntegration.onOAuthCodeGranted,
|
||||||
|
{
|
||||||
|
paymentIntegrationId: paymentIntegration.id,
|
||||||
|
trx,
|
||||||
|
} as StripeOAuthCodeGrantedEventPayload,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GetStripeAuthorizationLinkService {
|
||||||
|
constructor(private readonly config: ConfigService) {}
|
||||||
|
|
||||||
|
public getStripeAuthLink() {
|
||||||
|
const clientId = this.config.get('stripePayment.clientId');
|
||||||
|
const redirectUrl = this.config.get('stripePayment.redirectTo');
|
||||||
|
|
||||||
|
const authorizationUri = `https://connect.stripe.com/oauth/v2/authorize?response_type=code&client_id=${clientId}&scope=read_write&redirect_uri=${redirectUrl}`;
|
||||||
|
|
||||||
|
return authorizationUri;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { Body, Controller, Get, Injectable, Post } from '@nestjs/common';
|
||||||
|
import { StripePaymentApplication } from './StripePaymentApplication';
|
||||||
|
|
||||||
|
@Controller('/stripe')
|
||||||
|
export class StripeIntegrationController {
|
||||||
|
constructor(private readonly stripePaymentApp: StripePaymentApplication) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves Stripe OAuth2 connect link.
|
||||||
|
* @returns {Promise<Response|void>}
|
||||||
|
*/
|
||||||
|
@Get('/link')
|
||||||
|
public async getStripeConnectLink() {
|
||||||
|
const authorizationUri = this.stripePaymentApp.getStripeConnectLink();
|
||||||
|
|
||||||
|
return { url: authorizationUri };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exchanges the given Stripe authorization code to Stripe user id and access token.
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
@Post('/callback')
|
||||||
|
public async exchangeOAuth(@Body('code') code: string) {
|
||||||
|
await this.stripePaymentApp.exchangeStripeOAuthToken(code);
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new Stripe account.
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
public async createAccount() {
|
||||||
|
const accountId = await this.stripePaymentApp.createStripeAccount();
|
||||||
|
return {
|
||||||
|
accountId,
|
||||||
|
message: 'The Stripe account has been created successfully.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new Stripe account session.
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
@Post('/account_link')
|
||||||
|
public async createAccountLink(
|
||||||
|
@Body('stripeAccountId') stripeAccountId: string,
|
||||||
|
) {
|
||||||
|
const clientSecret =
|
||||||
|
await this.stripePaymentApp.createAccountLink(stripeAccountId);
|
||||||
|
|
||||||
|
return { clientSecret };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { CreateStripeAccountLinkService } from './CreateStripeAccountLink';
|
||||||
|
import { CreateStripeAccountService } from './CreateStripeAccountService';
|
||||||
|
import { StripePaymentApplication } from './StripePaymentApplication';
|
||||||
|
import { ExchangeStripeOAuthTokenService } from './ExchangeStripeOauthToken';
|
||||||
|
import { PaymentIntegration } from './models/PaymentIntegration.model';
|
||||||
|
import { SeedStripeAccountsOnOAuthGrantedSubscriber } from './subscribers/SeedStripeAccounts';
|
||||||
|
import { StripeWebhooksSubscriber } from './subscribers/StripeWebhooksSubscriber';
|
||||||
|
import { StripeIntegrationController } from './StripePayment.controller';
|
||||||
|
import { StripePaymentService } from './StripePaymentService';
|
||||||
|
import { InjectSystemModel } from '../System/SystemModels/SystemModels.module';
|
||||||
|
import { GetStripeAuthorizationLinkService } from './GetStripeAuthorizationLink';
|
||||||
|
import { AccountsModule } from '../Accounts/Accounts.module';
|
||||||
|
import { CreatePaymentReceiveStripePayment } from './CreatePaymentReceivedStripePayment';
|
||||||
|
import { SaleInvoicesModule } from '../SaleInvoices/SaleInvoices.module';
|
||||||
|
import { PaymentsReceivedModule } from '../PaymentReceived/PaymentsReceived.module';
|
||||||
|
import { TenancyContext } from '../Tenancy/TenancyContext.service';
|
||||||
|
|
||||||
|
const models = [InjectSystemModel(PaymentIntegration)];
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [AccountsModule, SaleInvoicesModule, PaymentsReceivedModule],
|
||||||
|
providers: [
|
||||||
|
...models,
|
||||||
|
StripePaymentService,
|
||||||
|
GetStripeAuthorizationLinkService,
|
||||||
|
CreateStripeAccountLinkService,
|
||||||
|
CreateStripeAccountService,
|
||||||
|
StripePaymentApplication,
|
||||||
|
ExchangeStripeOAuthTokenService,
|
||||||
|
CreatePaymentReceiveStripePayment,
|
||||||
|
SeedStripeAccountsOnOAuthGrantedSubscriber,
|
||||||
|
StripeWebhooksSubscriber,
|
||||||
|
TenancyContext,
|
||||||
|
],
|
||||||
|
controllers: [StripeIntegrationController],
|
||||||
|
})
|
||||||
|
export class StripePaymentModule {}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
export interface StripePaymentLinkCreatedEventPayload {
|
||||||
|
paymentLinkId: string;
|
||||||
|
saleInvoiceId: number;
|
||||||
|
stripeIntegrationId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StripeCheckoutSessionCompletedEventPayload {
|
||||||
|
event: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StripeInvoiceCheckoutSessionPOJO {
|
||||||
|
sessionId: string;
|
||||||
|
publishableKey: string;
|
||||||
|
redirectTo: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StripeWebhookEventPayload {
|
||||||
|
event: any;
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { CreateStripeAccountService } from './CreateStripeAccountService';
|
||||||
|
import { CreateStripeAccountLinkService } from './CreateStripeAccountLink';
|
||||||
|
import { CreateStripeAccountDTO } from './types';
|
||||||
|
import { ExchangeStripeOAuthTokenService } from './ExchangeStripeOauthToken';
|
||||||
|
import { GetStripeAuthorizationLinkService } from './GetStripeAuthorizationLink';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class StripePaymentApplication {
|
||||||
|
constructor(
|
||||||
|
private readonly createStripeAccountService: CreateStripeAccountService,
|
||||||
|
private readonly createStripeAccountLinkService: CreateStripeAccountLinkService,
|
||||||
|
private readonly exchangeStripeOAuthTokenService: ExchangeStripeOAuthTokenService,
|
||||||
|
private readonly getStripeConnectLinkService: GetStripeAuthorizationLinkService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new Stripe account.
|
||||||
|
* @param {number} createStripeAccountDTO
|
||||||
|
*/
|
||||||
|
public createStripeAccount(
|
||||||
|
createStripeAccountDTO: CreateStripeAccountDTO = {},
|
||||||
|
) {
|
||||||
|
return this.createStripeAccountService.createStripeAccount(
|
||||||
|
createStripeAccountDTO,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new Stripe account link of the given Stripe account.
|
||||||
|
* @param {string} stripeAccountId
|
||||||
|
* @returns {}
|
||||||
|
*/
|
||||||
|
public createAccountLink(stripeAccountId: string) {
|
||||||
|
return this.createStripeAccountLinkService.createAccountLink(
|
||||||
|
stripeAccountId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves Stripe OAuth2 connect link.
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
public getStripeConnectLink() {
|
||||||
|
return this.getStripeConnectLinkService.getStripeAuthLink();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exchanges the given Stripe authorization code to Stripe user id and access token.
|
||||||
|
* @param {string} authorizationCode
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
public exchangeStripeOAuthToken(authorizationCode: string) {
|
||||||
|
return this.exchangeStripeOAuthTokenService.excahngeStripeOAuthToken(
|
||||||
|
authorizationCode,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { Injectable, Scope } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import stripe from 'stripe';
|
||||||
|
|
||||||
|
const origin = 'https://cfdf-102-164-97-88.ngrok-free.app';
|
||||||
|
|
||||||
|
@Injectable({ scope: Scope.DEFAULT })
|
||||||
|
export class StripePaymentService {
|
||||||
|
public stripe: stripe;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor method.
|
||||||
|
* @param {ConfigService} config - ConfigService instance
|
||||||
|
*/
|
||||||
|
constructor(private readonly config: ConfigService) {
|
||||||
|
const secretKey = this.config.get('stripePayment.secretKey');
|
||||||
|
this.stripe = new stripe(secretKey, {
|
||||||
|
apiVersion: '2024-06-20',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new Stripe account session.
|
||||||
|
* @param {number} accountId
|
||||||
|
* @returns {Promise<string>}
|
||||||
|
*/
|
||||||
|
public async createAccountSession(accountId: string): Promise<string> {
|
||||||
|
try {
|
||||||
|
const accountSession = await this.stripe.accountSessions.create({
|
||||||
|
account: accountId,
|
||||||
|
components: {
|
||||||
|
account_onboarding: { enabled: true },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return accountSession.client_secret;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(
|
||||||
|
'An error occurred when calling the Stripe API to create an account session',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new Stripe account link.
|
||||||
|
* @param {number} accountId - Account id.
|
||||||
|
* @returns {Promise<stripe.Response<stripe.AccountLink>}
|
||||||
|
*/
|
||||||
|
public async createAccountLink(accountId: string) {
|
||||||
|
try {
|
||||||
|
const accountLink = await this.stripe.accountLinks.create({
|
||||||
|
account: accountId,
|
||||||
|
return_url: `${origin}/return/${accountId}`,
|
||||||
|
refresh_url: `${origin}/refresh/${accountId}`,
|
||||||
|
type: 'account_onboarding',
|
||||||
|
});
|
||||||
|
return accountLink;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(
|
||||||
|
'An error occurred when calling the Stripe API to create an account link:',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* C
|
||||||
|
* @returns {Promise<stripe.Response<stripe.Account>>}
|
||||||
|
*/
|
||||||
|
public async createAccount() {
|
||||||
|
try {
|
||||||
|
const account = await this.stripe.accounts.create({
|
||||||
|
type: 'standard',
|
||||||
|
});
|
||||||
|
return account;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(
|
||||||
|
'An error occurred when calling the Stripe API to create an account',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { BaseModel } from '@/models/Model';
|
||||||
|
import { Model } from 'objection';
|
||||||
|
|
||||||
|
export class PaymentIntegration extends BaseModel {
|
||||||
|
paymentEnabled!: boolean;
|
||||||
|
payoutEnabled!: boolean;
|
||||||
|
service?: string;
|
||||||
|
name?: string;
|
||||||
|
slug?: string;
|
||||||
|
accountId?: string;
|
||||||
|
options?: Record<string, any>;
|
||||||
|
active?: boolean;
|
||||||
|
|
||||||
|
static get tableName() {
|
||||||
|
return 'payment_integrations';
|
||||||
|
}
|
||||||
|
|
||||||
|
static get idColumn() {
|
||||||
|
return 'id';
|
||||||
|
}
|
||||||
|
|
||||||
|
static get virtualAttributes() {
|
||||||
|
return ['fullEnabled'];
|
||||||
|
}
|
||||||
|
|
||||||
|
static get jsonAttributes() {
|
||||||
|
return ['options'];
|
||||||
|
}
|
||||||
|
|
||||||
|
get fullEnabled() {
|
||||||
|
return this.paymentEnabled && this.payoutEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get modifiers() {
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* Query to filter enabled payment and payout.
|
||||||
|
*/
|
||||||
|
fullEnabled(query) {
|
||||||
|
query.where('paymentEnabled', true).andWhere('payoutEnabled', true);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static get jsonSchema() {
|
||||||
|
return {
|
||||||
|
type: 'object',
|
||||||
|
required: ['name', 'service'],
|
||||||
|
properties: {
|
||||||
|
id: { type: 'integer' },
|
||||||
|
service: { type: 'string' },
|
||||||
|
paymentEnabled: { type: 'boolean' },
|
||||||
|
payoutEnabled: { type: 'boolean' },
|
||||||
|
accountId: { type: 'string' },
|
||||||
|
options: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
bankAccountId: { type: 'number' },
|
||||||
|
clearingAccountId: { type: 'number' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
createdAt: { type: 'string', format: 'date-time' },
|
||||||
|
updatedAt: { type: 'string', format: 'date-time' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { OnEvent } from '@nestjs/event-emitter';
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { events } from '@/common/events/events';
|
||||||
|
import { AccountRepository } from '@/modules/Accounts/repositories/Account.repository';
|
||||||
|
import { PaymentIntegration } from '../models/PaymentIntegration.model';
|
||||||
|
import { StripeOAuthCodeGrantedEventPayload } from '../types';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SeedStripeAccountsOnOAuthGrantedSubscriber {
|
||||||
|
constructor(
|
||||||
|
private readonly accountRepository: AccountRepository,
|
||||||
|
|
||||||
|
@Inject(PaymentIntegration.name)
|
||||||
|
private readonly paymentIntegrationModel: typeof PaymentIntegration,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seeds the default integration settings once oauth authorization code granted.
|
||||||
|
* @param {StripeCheckoutSessionCompletedEventPayload} payload -
|
||||||
|
*/
|
||||||
|
@OnEvent(events.stripeIntegration.onOAuthCodeGranted)
|
||||||
|
async handleSeedStripeAccount({
|
||||||
|
paymentIntegrationId,
|
||||||
|
trx,
|
||||||
|
}: StripeOAuthCodeGrantedEventPayload) {
|
||||||
|
const clearingAccount =
|
||||||
|
await this.accountRepository.findOrCreateStripeClearing({}, trx);
|
||||||
|
|
||||||
|
const bankAccount = await this.accountRepository.findBySlug('bank-account');
|
||||||
|
|
||||||
|
// Patch the Stripe integration default settings.
|
||||||
|
await this.paymentIntegrationModel
|
||||||
|
.query(trx)
|
||||||
|
.findById(paymentIntegrationId)
|
||||||
|
.patch({
|
||||||
|
options: {
|
||||||
|
// @ts-ignore
|
||||||
|
bankAccountId: bankAccount.id,
|
||||||
|
clearingAccountId: clearingAccount.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { CreatePaymentReceiveStripePayment } from '../CreatePaymentReceivedStripePayment';
|
||||||
|
import {
|
||||||
|
StripeCheckoutSessionCompletedEventPayload,
|
||||||
|
StripeWebhookEventPayload,
|
||||||
|
} from '../StripePayment.types';
|
||||||
|
// import { initalizeTenantServices } from '@/api/middleware/TenantDependencyInjection';
|
||||||
|
// import { initializeTenantSettings } from '@/api/middleware/SettingsMiddleware';
|
||||||
|
import { OnEvent } from '@nestjs/event-emitter';
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { events } from '@/common/events/events';
|
||||||
|
import { PaymentIntegration } from '../models/PaymentIntegration.model';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class StripeWebhooksSubscriber {
|
||||||
|
constructor(
|
||||||
|
private readonly createPaymentReceiveStripePayment: CreatePaymentReceiveStripePayment,
|
||||||
|
|
||||||
|
@Inject(PaymentIntegration.name)
|
||||||
|
private readonly paymentIntegrationModel: typeof PaymentIntegration,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the checkout session completed webhook event.
|
||||||
|
* @param {StripeCheckoutSessionCompletedEventPayload} payload -
|
||||||
|
*/
|
||||||
|
@OnEvent(events.stripeWebhooks.onCheckoutSessionCompleted)
|
||||||
|
async handleCheckoutSessionCompleted({
|
||||||
|
event,
|
||||||
|
}: StripeCheckoutSessionCompletedEventPayload) {
|
||||||
|
const { metadata } = event.data.object;
|
||||||
|
const tenantId = parseInt(metadata.tenantId, 10);
|
||||||
|
const saleInvoiceId = parseInt(metadata.saleInvoiceId, 10);
|
||||||
|
|
||||||
|
// await initalizeTenantServices(tenantId);
|
||||||
|
// await initializeTenantSettings(tenantId);
|
||||||
|
|
||||||
|
// Get the amount from the event
|
||||||
|
const amount = event.data.object.amount_total;
|
||||||
|
|
||||||
|
// Convert from Stripe amount (cents) to normal amount (dollars)
|
||||||
|
const amountInDollars = amount / 100;
|
||||||
|
|
||||||
|
// Creates a new payment received transaction.
|
||||||
|
await this.createPaymentReceiveStripePayment.createPaymentReceived(
|
||||||
|
saleInvoiceId,
|
||||||
|
amountInDollars,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the account updated.
|
||||||
|
* @param {StripeWebhookEventPayload}
|
||||||
|
*/
|
||||||
|
@OnEvent(events.stripeWebhooks.onAccountUpdated)
|
||||||
|
async handleAccountUpdated({ event }: StripeWebhookEventPayload) {
|
||||||
|
const { metadata } = event.data.object;
|
||||||
|
const account = event.data.object;
|
||||||
|
const tenantId = parseInt(metadata.tenantId, 10);
|
||||||
|
|
||||||
|
if (!metadata?.paymentIntegrationId || !metadata.tenantId) return;
|
||||||
|
|
||||||
|
// Find the tenant or throw not found error.
|
||||||
|
// await Tenant.query().findById(tenantId).throwIfNotFound();
|
||||||
|
|
||||||
|
// Check if the account capabilities are active
|
||||||
|
if (account.capabilities.card_payments === 'active') {
|
||||||
|
// Marks the payment method integration as active.
|
||||||
|
await this.paymentIntegrationModel
|
||||||
|
.query()
|
||||||
|
.findById(metadata?.paymentIntegrationId)
|
||||||
|
.patch({
|
||||||
|
active: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
packages/server-nest/src/modules/StripePayment/types.ts
Normal file
9
packages/server-nest/src/modules/StripePayment/types.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { Knex } from 'knex';
|
||||||
|
|
||||||
|
export interface CreateStripeAccountDTO {
|
||||||
|
name?: string;
|
||||||
|
}
|
||||||
|
export interface StripeOAuthCodeGrantedEventPayload {
|
||||||
|
paymentIntegrationId: number;
|
||||||
|
trx?: Knex.Transaction;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user