mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 05:10:31 +00:00
refactor: financial statements to nestjs
This commit is contained in:
@@ -80,6 +80,7 @@
|
||||
"rxjs": "^7.8.1",
|
||||
"serialize-interceptor": "^1.1.7",
|
||||
"strategy": "^1.1.1",
|
||||
"stripe": "^16.10.0",
|
||||
"uniqid": "^5.2.0",
|
||||
"uuid": "^10.0.0",
|
||||
"xlsx": "^0.18.5",
|
||||
|
||||
@@ -7,6 +7,7 @@ import lemonsqueezy from './lemonsqueezy';
|
||||
import s3 from './s3';
|
||||
import openExchange from './open-exchange';
|
||||
import posthog from './posthog';
|
||||
import stripePayment from './stripe-payment';
|
||||
|
||||
export const config = [
|
||||
systemDatabase,
|
||||
@@ -18,4 +19,5 @@ export const config = [
|
||||
s3,
|
||||
openExchange,
|
||||
posthog,
|
||||
stripePayment,
|
||||
];
|
||||
|
||||
@@ -67,6 +67,7 @@ import { PostHogModule } from '../EventsTracker/postHog.module';
|
||||
import { EventTrackerModule } from '../EventsTracker/EventTracker.module';
|
||||
import { MailModule } from '../Mail/Mail.module';
|
||||
import { FinancialStatementsModule } from '../FinancialStatements/FinancialStatements.module';
|
||||
import { StripePaymentModule } from '../StripePayment/StripePayment.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -158,6 +159,7 @@ import { FinancialStatementsModule } from '../FinancialStatements/FinancialState
|
||||
PostHogModule,
|
||||
EventTrackerModule,
|
||||
FinancialStatementsModule,
|
||||
StripePaymentModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [
|
||||
|
||||
@@ -8,9 +8,11 @@ import { TransactionsByVendorModule } from './modules/TransactionsByVendor/Trans
|
||||
import { TransactionsByCustomerModule } from './modules/TransactionsByCustomer/TransactionsByCustomer.module';
|
||||
import { TransactionsByReferenceModule } from './modules/TransactionsByReference/TransactionByReference.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 { InventoryValuationSheetModule } from './modules/InventoryValuationSheet/InventoryValuationSheet.module';
|
||||
import { SalesTaxLiabilityModule } from './modules/SalesTaxLiabilitySummary/SalesTaxLiability.module';
|
||||
import { JournalSheetModule } from './modules/JournalSheet/JournalSheet.module';
|
||||
|
||||
@Module({
|
||||
providers: [],
|
||||
@@ -24,9 +26,11 @@ import { InventoryValuationSheetModule } from './modules/InventoryValuationSheet
|
||||
TransactionsByCustomerModule,
|
||||
TransactionsByReferenceModule,
|
||||
ARAgingSummaryModule,
|
||||
// APAgingSummaryModule,
|
||||
APAgingSummaryModule,
|
||||
InventoryItemDetailsModule,
|
||||
InventoryValuationSheetModule,
|
||||
SalesTaxLiabilityModule,
|
||||
JournalSheetModule,
|
||||
],
|
||||
})
|
||||
export class FinancialStatementsModule {}
|
||||
|
||||
@@ -7,18 +7,22 @@ import { APAgingSummaryPdfInjectable } from './APAgingSummaryPdfInjectable';
|
||||
import { APAgingSummaryRepository } from './APAgingSummaryRepository';
|
||||
import { APAgingSummaryApplication } from './APAgingSummaryApplication';
|
||||
import { APAgingSummaryController } from './APAgingSummary.controller';
|
||||
import { APAgingSummaryMeta } from './APAgingSummaryMeta';
|
||||
import { FinancialSheetCommonModule } from '../../common/FinancialSheetCommon.module';
|
||||
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
|
||||
|
||||
@Module({
|
||||
imports: [AgingSummaryModule],
|
||||
imports: [AgingSummaryModule, FinancialSheetCommonModule],
|
||||
providers: [
|
||||
APAgingSummaryService,
|
||||
APAgingSummaryMeta,
|
||||
APAgingSummaryTableInjectable,
|
||||
APAgingSummaryExportInjectable,
|
||||
APAgingSummaryPdfInjectable,
|
||||
APAgingSummaryRepository,
|
||||
APAgingSummaryApplication
|
||||
APAgingSummaryApplication,
|
||||
TenancyContext,
|
||||
],
|
||||
controllers: [APAgingSummaryController],
|
||||
})
|
||||
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 { Vendor } from '@/modules/Vendors/models/Vendor';
|
||||
import { IAPAgingSummaryQuery } from './APAgingSummary.types';
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
|
||||
import { groupBy } from 'ramda';
|
||||
import { IAPAgingSummaryQuery } from './APAgingSummary.types';
|
||||
import { ModelObject } from 'objection';
|
||||
|
||||
export class APAgingSummaryRepository {
|
||||
@Inject(Vendor.name)
|
||||
@@ -32,19 +32,19 @@ export class APAgingSummaryRepository {
|
||||
* Due bills by vendor id.
|
||||
* @param {Record<string, Bill[]>} dueBillsByVendorId
|
||||
*/
|
||||
dueBillsByVendorId: Record<number, Bill[]>;
|
||||
dueBillsByVendorId: Record<string, Bill[]>;
|
||||
|
||||
/**
|
||||
* Overdue bills.
|
||||
* @param {Bill[]} overdueBills
|
||||
* @param {Bill[]} overdueBills - overdue bills.
|
||||
*/
|
||||
overdueBills: Bill[];
|
||||
overdueBills: ModelObject<Bill>[];
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { I18nService } from 'nestjs-i18n';
|
||||
import {
|
||||
IAPAgingSummaryQuery,
|
||||
IAPAgingSummaryTable,
|
||||
@@ -8,18 +9,21 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
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.
|
||||
* @param {IAPAgingSummaryQuery} query -
|
||||
* @param {IAPAgingSummaryQuery} query -
|
||||
* @returns {Promise<IAPAgingSummaryTable>}
|
||||
*/
|
||||
public async table(
|
||||
query: IAPAgingSummaryQuery,
|
||||
): Promise<IAPAgingSummaryTable> {
|
||||
const report = await this.APAgingSummarySheet.APAgingSummary(query);
|
||||
const table = new APAgingSummaryTable(report.data, query, {});
|
||||
const table = new APAgingSummaryTable(report.data, query, this.i18nService);
|
||||
|
||||
return {
|
||||
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 {
|
||||
public readonly filter: ITransactionsByContactsFilter;
|
||||
public readonly i18n: I18nService
|
||||
public readonly i18n: I18nService;
|
||||
public readonly repository: TransactionsByContactRepository;
|
||||
|
||||
/**
|
||||
@@ -22,7 +22,7 @@ export class TransactionsByContact extends FinancialSheet {
|
||||
* @return {Omit<ITransactionsByContactsTransaction, 'runningBalance'>}
|
||||
*/
|
||||
protected contactTransactionMapper(
|
||||
entry: ILedgerEntry
|
||||
entry: ILedgerEntry,
|
||||
): Omit<ITransactionsByContactsTransaction, 'runningBalance'> {
|
||||
const account = this.repository.accountsGraph.getNodeData(entry.accountId);
|
||||
const currencyCode = this.baseCurrency;
|
||||
@@ -37,6 +37,7 @@ export class TransactionsByContact extends FinancialSheet {
|
||||
// @ts-ignore
|
||||
// transactionType: this.i18n.t(entry.referenceTypeFormatted),
|
||||
transactionType: '',
|
||||
// @ts-ignore
|
||||
date: entry.date,
|
||||
createdAt: entry.createdAt,
|
||||
};
|
||||
@@ -51,7 +52,7 @@ export class TransactionsByContact extends FinancialSheet {
|
||||
protected contactTransactionRunningBalance(
|
||||
openingBalance: number,
|
||||
accountNormal: 'credit' | 'debit',
|
||||
transactions: Omit<ITransactionsByContactsTransaction, 'runningBalance'>[]
|
||||
transactions: Omit<ITransactionsByContactsTransaction, 'runningBalance'>[],
|
||||
): any {
|
||||
let _openingBalance = openingBalance;
|
||||
|
||||
@@ -69,10 +70,10 @@ export class TransactionsByContact extends FinancialSheet {
|
||||
|
||||
const runningBalance = this.getTotalAmountMeta(
|
||||
_openingBalance,
|
||||
transaction.currencyCode
|
||||
transaction.currencyCode,
|
||||
);
|
||||
return { ...transaction, runningBalance };
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -85,7 +86,7 @@ export class TransactionsByContact extends FinancialSheet {
|
||||
protected getContactClosingBalance(
|
||||
customerTransactions: ITransactionsByContactsTransaction[],
|
||||
contactNormal: 'credit' | 'debit',
|
||||
openingBalance: number
|
||||
openingBalance: number,
|
||||
): number {
|
||||
const closingBalance = openingBalance;
|
||||
|
||||
@@ -124,7 +125,7 @@ export class TransactionsByContact extends FinancialSheet {
|
||||
*/
|
||||
protected getContactAmount(
|
||||
amount: number,
|
||||
currencyCode: string
|
||||
currencyCode: string,
|
||||
): ITransactionsByContactsAmount {
|
||||
return {
|
||||
amount,
|
||||
@@ -153,7 +154,7 @@ export class TransactionsByContact extends FinancialSheet {
|
||||
* @returns {boolean}
|
||||
*/
|
||||
private filterContactByNoneTransaction = (
|
||||
transactionsByContact: ITransactionsByContactsContact
|
||||
transactionsByContact: ITransactionsByContactsContact,
|
||||
): boolean => {
|
||||
return transactionsByContact.transactions.length > 0;
|
||||
};
|
||||
@@ -164,7 +165,7 @@ export class TransactionsByContact extends FinancialSheet {
|
||||
* @returns {boolean}
|
||||
*/
|
||||
private filterContactNoneZero = (
|
||||
transactionsByContact: ITransactionsByContactsContact
|
||||
transactionsByContact: ITransactionsByContactsContact,
|
||||
): boolean => {
|
||||
return transactionsByContact.closingBalance.amount !== 0;
|
||||
};
|
||||
@@ -190,7 +191,7 @@ export class TransactionsByContact extends FinancialSheet {
|
||||
* @returns {ICustomerBalanceSummaryCustomer[]}
|
||||
*/
|
||||
protected contactsFilter = (
|
||||
nodes: ITransactionsByContactsContact[]
|
||||
nodes: ITransactionsByContactsContact[],
|
||||
): ITransactionsByContactsContact[] => {
|
||||
return nodes.filter(this.contactNodeFilter);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Knex } from 'knex';
|
||||
import * as moment from 'moment';
|
||||
|
||||
export interface ILedger {
|
||||
entries: ILedgerEntry[];
|
||||
@@ -41,7 +42,7 @@ export interface ILedgerEntry {
|
||||
accountId?: number;
|
||||
accountNormal: string;
|
||||
contactId?: number;
|
||||
date: Date | string;
|
||||
date: moment.MomentInput;
|
||||
|
||||
transactionType: string;
|
||||
transactionSubType?: string;
|
||||
|
||||
@@ -5,7 +5,7 @@ export const transformLedgerEntryToTransaction = (
|
||||
entry: ILedgerEntry
|
||||
): Partial<AccountTransaction> => {
|
||||
return {
|
||||
date: entry.date,
|
||||
date: moment(entry.date).toDate(),
|
||||
|
||||
credit: entry.credit,
|
||||
debit: entry.debit,
|
||||
|
||||
@@ -59,7 +59,7 @@ import { MailNotificationModule } from '../MailNotification/MailNotification.mod
|
||||
MailModule,
|
||||
MailNotificationModule,
|
||||
InventoryCostModule,
|
||||
DynamicListModule
|
||||
DynamicListModule,
|
||||
],
|
||||
controllers: [SaleInvoicesController],
|
||||
providers: [
|
||||
@@ -94,7 +94,8 @@ import { MailNotificationModule } from '../MailNotification/MailNotification.mod
|
||||
SendSaleInvoiceMail,
|
||||
GetSaleInvoicesService,
|
||||
GetSaleInvoiceMailState,
|
||||
SendSaleInvoiceMailCommon
|
||||
SendSaleInvoiceMailCommon,
|
||||
],
|
||||
exports: [GetSaleInvoice],
|
||||
})
|
||||
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