diff --git a/package.json b/package.json index e9343addc..69f5dd546 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "build:server": "lerna run build --scope \"@bigcapital/server\" --scope \"@bigcapital/utils\" --scope \"@bigcapital/pdf-templates\" --scope \"@bigcapital/email-components\"", "serve:server": "lerna run serve --scope \"@bigcapital/server\" --scope \"@bigcapital/utils\"", "server2:start": "lerna run start:dev --scope \"@bigcapital/server2\"", - "test:e2e": "playwright test", + "test:watch": "lerna run test:watch", + "test:e2e": "lerna run test:e2e", "prepare": "husky install" }, "devDependencies": { diff --git a/packages/server-nest/package.json b/packages/server-nest/package.json index 3bae968a5..f62650e03 100644 --- a/packages/server-nest/package.json +++ b/packages/server-nest/package.json @@ -20,6 +20,9 @@ "test:e2e": "jest --config ./test/jest-e2e.json --watchAll" }, "dependencies": { + "@bigcapital/utils": "*", + "@bigcapital/email-components": "*", + "@bigcapital/pdf-templates": "*", "@nestjs/bull": "^10.2.1", "@nestjs/bullmq": "^10.2.1", "@nestjs/cache-manager": "^2.2.2", @@ -36,6 +39,8 @@ "@types/ramda": "^0.30.2", "accounting": "^0.4.1", "async": "^3.2.0", + "axios": "^1.6.0", + "form-data": "^4.0.0", "bull": "^4.16.3", "bullmq": "^5.21.1", "cache-manager": "^6.1.1", @@ -53,6 +58,8 @@ "mysql2": "^3.11.3", "nestjs-cls": "^4.4.1", "nestjs-i18n": "^10.4.9", + "uuid": "^10.0.0", + "pug": "^3.0.2", "object-hash": "^2.0.3", "objection": "^3.1.5", "passport": "^0.7.0", diff --git a/packages/server-nest/src/libs/chromiumly/Chromiumly.ts b/packages/server-nest/src/libs/chromiumly/Chromiumly.ts new file mode 100644 index 000000000..276ca4538 --- /dev/null +++ b/packages/server-nest/src/libs/chromiumly/Chromiumly.ts @@ -0,0 +1,26 @@ +import { ChromiumRoute, LibreOfficeRoute, PdfEngineRoute } from './_types'; + +export class Chromiumly { + public static readonly GOTENBERG_ENDPOINT = process.env.GOTENBERG_URL || ''; + + public static readonly CHROMIUM_PATH = 'forms/chromium/convert'; + public static readonly PDF_ENGINES_PATH = 'forms/pdfengines'; + public static readonly LIBRE_OFFICE_PATH = 'forms/libreoffice'; + + public static readonly GOTENBERG_DOCS_ENDPOINT = + process.env.GOTENBERG_DOCS_URL || ''; + + public static readonly CHROMIUM_ROUTES = { + url: ChromiumRoute.URL, + html: ChromiumRoute.HTML, + markdown: ChromiumRoute.MARKDOWN, + }; + + public static readonly PDF_ENGINE_ROUTES = { + merge: PdfEngineRoute.MERGE, + }; + + public static readonly LIBRE_OFFICE_ROUTES = { + convert: LibreOfficeRoute.CONVERT, + }; +} diff --git a/packages/server-nest/src/libs/chromiumly/ConvertUtils.ts b/packages/server-nest/src/libs/chromiumly/ConvertUtils.ts new file mode 100644 index 000000000..38d27fd99 --- /dev/null +++ b/packages/server-nest/src/libs/chromiumly/ConvertUtils.ts @@ -0,0 +1,66 @@ +import FormData from 'form-data'; +import { GotenbergUtils } from './GotenbergUtils'; +import { PageProperties } from './_types'; + +export class ConverterUtils { + public static injectPageProperties( + data: FormData, + pageProperties: PageProperties + ): void { + if (pageProperties.size) { + GotenbergUtils.assert( + pageProperties.size.width >= 1.0 && pageProperties.size.height >= 1.5, + 'size is smaller than the minimum printing requirements (i.e. 1.0 x 1.5 in)' + ); + + data.append('paperWidth', pageProperties.size.width); + data.append('paperHeight', pageProperties.size.height); + } + if (pageProperties.margins) { + GotenbergUtils.assert( + pageProperties.margins.top >= 0 && + pageProperties.margins.bottom >= 0 && + pageProperties.margins.left >= 0 && + pageProperties.margins.left >= 0, + 'negative margins are not allowed' + ); + data.append('marginTop', pageProperties.margins.top); + data.append('marginBottom', pageProperties.margins.bottom); + data.append('marginLeft', pageProperties.margins.left); + data.append('marginRight', pageProperties.margins.right); + } + if (pageProperties.preferCssPageSize) { + data.append( + 'preferCssPageSize', + String(pageProperties.preferCssPageSize) + ); + } + if (pageProperties.printBackground) { + data.append('printBackground', String(pageProperties.printBackground)); + } + if (pageProperties.landscape) { + data.append('landscape', String(pageProperties.landscape)); + } + if (pageProperties.scale) { + GotenbergUtils.assert( + pageProperties.scale >= 0.1 && pageProperties.scale <= 2.0, + 'scale is outside of [0.1 - 2] range' + ); + data.append('scale', pageProperties.scale); + } + + if (pageProperties.nativePageRanges) { + GotenbergUtils.assert( + pageProperties.nativePageRanges.from > 0 && + pageProperties.nativePageRanges.to > 0 && + pageProperties.nativePageRanges.to >= + pageProperties.nativePageRanges.from, + 'page ranges syntax error' + ); + data.append( + 'nativePageRanges', + `${pageProperties.nativePageRanges.from}-${pageProperties.nativePageRanges.to}` + ); + } + } +} diff --git a/packages/server-nest/src/libs/chromiumly/Converter.ts b/packages/server-nest/src/libs/chromiumly/Converter.ts new file mode 100644 index 000000000..cf8f70f4a --- /dev/null +++ b/packages/server-nest/src/libs/chromiumly/Converter.ts @@ -0,0 +1,10 @@ +import { Chromiumly } from './Chromiumly'; +import { ChromiumRoute } from './_types'; + +export abstract class Converter { + readonly endpoint: string; + + constructor(route: ChromiumRoute) { + this.endpoint = `${Chromiumly.GOTENBERG_ENDPOINT}/${Chromiumly.CHROMIUM_PATH}/${Chromiumly.CHROMIUM_ROUTES[route]}`; + } +} diff --git a/packages/server-nest/src/libs/chromiumly/GotenbergUtils.ts b/packages/server-nest/src/libs/chromiumly/GotenbergUtils.ts new file mode 100644 index 000000000..63a609792 --- /dev/null +++ b/packages/server-nest/src/libs/chromiumly/GotenbergUtils.ts @@ -0,0 +1,24 @@ +import FormData from 'form-data'; +import Axios from 'axios'; + +export class GotenbergUtils { + public static assert(condition: boolean, message: string): asserts condition { + if (!condition) { + throw new Error(message); + } + } + + public static async fetch(endpoint: string, data: FormData): Promise { + try { + const response = await Axios.post(endpoint, data, { + headers: { + ...data.getHeaders(), + }, + responseType: 'arraybuffer', // This ensures you get a Buffer bac + }); + return response.data; + } catch (error) { + console.error(error); + } + } +} diff --git a/packages/server-nest/src/libs/chromiumly/HTMLConvert.ts b/packages/server-nest/src/libs/chromiumly/HTMLConvert.ts new file mode 100644 index 000000000..3eb109405 --- /dev/null +++ b/packages/server-nest/src/libs/chromiumly/HTMLConvert.ts @@ -0,0 +1,38 @@ +import { constants, createReadStream, PathLike, promises } from 'fs'; +import FormData from 'form-data'; +import { GotenbergUtils } from './GotenbergUtils'; +import { IConverter, PageProperties } from './_types'; +import { PdfFormat, ChromiumRoute } from './_types'; +import { ConverterUtils } from './ConvertUtils'; +import { Converter } from './Converter'; + +export class HtmlConverter extends Converter implements IConverter { + constructor() { + super(ChromiumRoute.HTML); + } + + async convert({ + html, + properties, + pdfFormat, + }: { + html: PathLike; + properties?: PageProperties; + pdfFormat?: PdfFormat; + }): Promise { + try { + await promises.access(html, constants.R_OK); + const data = new FormData(); + if (pdfFormat) { + data.append('pdfFormat', pdfFormat); + } + data.append('index.html', createReadStream(html)); + if (properties) { + ConverterUtils.injectPageProperties(data, properties); + } + return GotenbergUtils.fetch(this.endpoint, data); + } catch (error) { + throw error; + } + } +} diff --git a/packages/server-nest/src/libs/chromiumly/UrlConvert.ts b/packages/server-nest/src/libs/chromiumly/UrlConvert.ts new file mode 100644 index 000000000..d1a462124 --- /dev/null +++ b/packages/server-nest/src/libs/chromiumly/UrlConvert.ts @@ -0,0 +1,38 @@ +import FormData from 'form-data'; +import { IConverter, PageProperties, PdfFormat, ChromiumRoute } from './_types'; +import { ConverterUtils } from './ConvertUtils'; +import { Converter } from './Converter'; +import { GotenbergUtils } from './GotenbergUtils'; + +export class UrlConverter extends Converter implements IConverter { + constructor() { + super(ChromiumRoute.URL); + } + + async convert({ + url, + properties, + pdfFormat, + }: { + url: string; + properties?: PageProperties; + pdfFormat?: PdfFormat; + }): Promise { + try { + const _url = new URL(url); + const data = new FormData(); + + if (pdfFormat) { + data.append('pdfFormat', pdfFormat); + } + data.append('url', _url.href); + + if (properties) { + ConverterUtils.injectPageProperties(data, properties); + } + return GotenbergUtils.fetch(this.endpoint, data); + } catch (error) { + throw error; + } + } +} diff --git a/packages/server-nest/src/libs/chromiumly/_types.ts b/packages/server-nest/src/libs/chromiumly/_types.ts new file mode 100644 index 000000000..453f59ed8 --- /dev/null +++ b/packages/server-nest/src/libs/chromiumly/_types.ts @@ -0,0 +1,51 @@ +import { PathLike } from 'fs'; + +export type PageSize = { + width: number; // Paper width, in inches (default 8.5) + height: number; //Paper height, in inches (default 11) +}; + +export type PageMargins = { + top: number; // Top margin, in inches (default 0.39) + bottom: number; // Bottom margin, in inches (default 0.39) + left: number; // Left margin, in inches (default 0.39) + right: number; // Right margin, in inches (default 0.39) +}; + +export type PageProperties = { + size?: PageSize; + margins?: PageMargins; + preferCssPageSize?: boolean; // Define whether to prefer page size as defined by CSS (default false) + printBackground?: boolean; // Print the background graphics (default false) + landscape?: boolean; // Set the paper orientation to landscape (default false) + scale?: number; // The scale of the page rendering (default 1.0) + nativePageRanges?: { from: number; to: number }; // Page ranges to print +}; + +export interface IConverter { + convert({ + ...args + }: { + [x: string]: string | PathLike | PageProperties | PdfFormat; + }): Promise; +} + +export enum PdfFormat { + A_1a = 'PDF/A-1a', + A_2b = 'PDF/A-2b', + A_3b = 'PDF/A-3b', +} + +export enum ChromiumRoute { + URL = 'url', + HTML = 'html', + MARKDOWN = 'markdown', +} + +export enum PdfEngineRoute { + MERGE = 'merge', +} + +export enum LibreOfficeRoute { + CONVERT = 'convert', +} diff --git a/packages/server-nest/src/modules/App/App.module.ts b/packages/server-nest/src/modules/App/App.module.ts index f95b1b89b..06f34f83e 100644 --- a/packages/server-nest/src/modules/App/App.module.ts +++ b/packages/server-nest/src/modules/App/App.module.ts @@ -29,7 +29,6 @@ import { APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core'; import { JwtAuthGuard } from '../Auth/Jwt.guard'; import { UserIpInterceptor } from '@/interceptors/user-ip.interceptor'; import { TenancyGlobalMiddleware } from '../Tenancy/TenancyGlobal.middleware'; -import { TransformerInjectable } from '../Transformer/TransformerInjectable.service'; import { TransformerModule } from '../Transformer/Transformer.module'; import { AccountsModule } from '../Accounts/Accounts.module'; import { ExpensesModule } from '../Expenses/Expenses.module'; @@ -40,6 +39,11 @@ import { BranchesModule } from '../Branches/Branches.module'; import { WarehousesModule } from '../Warehouses/Warehouses.module'; import { SaleEstimatesModule } from '../SaleEstimates/SaleEstimates.module'; import { SerializeInterceptor } from '@/common/interceptors/serialize.interceptor'; +import { ChromiumlyTenancyModule } from '../ChromiumlyTenancy/ChromiumlyTenancy.module'; +import { CustomersModule } from '../Customers/Customers.module'; +import { VendorsModule } from '../Vendors/Vendors.module'; +import { BillsModule } from '../Bills/Bills.module'; +import { BillPaymentsModule } from '../BillPayments/BillPayments.module'; @Module({ imports: [ @@ -93,6 +97,8 @@ import { SerializeInterceptor } from '@/common/interceptors/serialize.intercepto }), TenancyDatabaseModule, TenancyModelsModule, + ChromiumlyTenancyModule, + TransformerModule, ItemsModule, ItemCategoryModule, AccountsModule, @@ -101,7 +107,11 @@ import { SerializeInterceptor } from '@/common/interceptors/serialize.intercepto PdfTemplatesModule, BranchesModule, WarehousesModule, + CustomersModule, + VendorsModule, SaleEstimatesModule, + BillsModule, + BillPaymentsModule, ], controllers: [AppController], providers: [ diff --git a/packages/server-nest/src/modules/BillLandedCosts/models/BillLandedCost.ts b/packages/server-nest/src/modules/BillLandedCosts/models/BillLandedCost.ts new file mode 100644 index 000000000..07bfc2f44 --- /dev/null +++ b/packages/server-nest/src/modules/BillLandedCosts/models/BillLandedCost.ts @@ -0,0 +1,103 @@ +import { Model } from 'objection'; +import { lowerCase } from 'lodash'; +// import TenantModel from 'models/TenantModel'; +import { BaseModel } from '@/models/Model'; + +export class BillLandedCost extends BaseModel { + amount: number; + fromTransactionId: number; + fromTransactionType: string; + fromTransactionEntryId: number; + allocationMethod: string; + costAccountId: number; + description: string; + billId: number; + exchangeRate: number; + + /** + * Table name + */ + static get tableName() { + return 'bill_located_costs'; + } + + /** + * Model timestamps. + */ + get timestamps() { + return ['createdAt', 'updatedAt']; + } + + /** + * Virtual attributes. + */ + static get virtualAttributes() { + return ['localAmount', 'allocationMethodFormatted']; + } + + /** + * Retrieves the cost local amount. + * @returns {number} + */ + get localAmount() { + return this.amount * this.exchangeRate; + } + + /** + * Allocation method formatted. + */ + get allocationMethodFormatted() { + const allocationMethod = lowerCase(this.allocationMethod); + + const keyLabelsPairs = { + value: 'allocation_method.value.label', + quantity: 'allocation_method.quantity.label', + }; + return keyLabelsPairs[allocationMethod] || ''; + } + + /** + * Relationship mapping. + */ + static get relationMappings() { + const BillLandedCostEntry = require('models/BillLandedCostEntry'); + const Bill = require('models/Bill'); + const ItemEntry = require('models/ItemEntry'); + const ExpenseCategory = require('models/ExpenseCategory'); + + return { + bill: { + relation: Model.BelongsToOneRelation, + modelClass: Bill.default, + join: { + from: 'bill_located_costs.billId', + to: 'bills.id', + }, + }, + allocateEntries: { + relation: Model.HasManyRelation, + modelClass: BillLandedCostEntry.default, + join: { + from: 'bill_located_costs.id', + to: 'bill_located_cost_entries.billLocatedCostId', + }, + }, + allocatedFromBillEntry: { + relation: Model.BelongsToOneRelation, + modelClass: ItemEntry.default, + join: { + from: 'bill_located_costs.fromTransactionEntryId', + to: 'items_entries.id', + }, + }, + allocatedFromExpenseEntry: { + relation: Model.BelongsToOneRelation, + modelClass: ExpenseCategory.default, + join: { + from: 'bill_located_costs.fromTransactionEntryId', + to: 'expense_transaction_categories.id', + }, + }, + }; + } +} diff --git a/packages/server-nest/src/modules/BillLandedCosts/models/BillLandedCostEntry.ts b/packages/server-nest/src/modules/BillLandedCosts/models/BillLandedCostEntry.ts new file mode 100644 index 000000000..049bef1c5 --- /dev/null +++ b/packages/server-nest/src/modules/BillLandedCosts/models/BillLandedCostEntry.ts @@ -0,0 +1,32 @@ +import { Model } from 'objection'; +import TenantModel from 'models/TenantModel'; + +export default class BillLandedCostEntry extends TenantModel { + /** + * Table name + */ + static get tableName() { + return 'bill_located_cost_entries'; + } + + /** + * Relationship mapping. + */ + static get relationMappings() { + const ItemEntry = require('models/ItemEntry'); + + return { + itemEntry: { + relation: Model.BelongsToOneRelation, + modelClass: ItemEntry.default, + join: { + from: 'bill_located_cost_entries.entryId', + to: 'items_entries.id', + }, + filter(builder) { + builder.where('reference_type', 'Bill'); + }, + }, + }; + } +} diff --git a/packages/server-nest/src/modules/BillPayments/BillPayments.module.ts b/packages/server-nest/src/modules/BillPayments/BillPayments.module.ts new file mode 100644 index 000000000..18cf55270 --- /dev/null +++ b/packages/server-nest/src/modules/BillPayments/BillPayments.module.ts @@ -0,0 +1,18 @@ +import { Module } from '@nestjs/common'; +import { BillPaymentsApplication } from './BillPaymentsApplication.service'; +import { CreateBillPaymentService } from './commands/CreateBillPayment.service'; +import { EditBillPayment } from './commands/EditBillPayment.service'; +import { GetBillPayment } from './queries/GetBillPayment.service'; +import { DeleteBillPayment } from './commands/DeleteBillPayment.service'; + +@Module({ + providers: [ + BillPaymentsApplication, + CreateBillPaymentService, + EditBillPayment, + GetBillPayment, + DeleteBillPayment, + ], + controllers: [], +}) +export class BillPaymentsModule {} diff --git a/packages/server-nest/src/modules/BillPayments/BillPaymentsApplication.service.ts b/packages/server-nest/src/modules/BillPayments/BillPaymentsApplication.service.ts new file mode 100644 index 000000000..2a21b9ce3 --- /dev/null +++ b/packages/server-nest/src/modules/BillPayments/BillPaymentsApplication.service.ts @@ -0,0 +1,83 @@ +import { Injectable } from '@nestjs/common'; +import { CreateBillPaymentService } from './commands/CreateBillPayment.service'; +import { DeleteBillPayment } from './commands/DeleteBillPayment.service'; +import { EditBillPayment } from './commands/EditBillPayment.service'; +import { GetBillPayments } from './GetBillPayments'; +import { GetBillPayment } from './queries/GetBillPayment.service'; +import { GetPaymentBills } from './queries/GetPaymentBills.service'; +import { IBillPaymentDTO } from './types/BillPayments.types'; + +/** + * Bill payments application. + * @service + */ +@Injectable() +export class BillPaymentsApplication { + constructor( + private createBillPaymentService: CreateBillPaymentService, + private deleteBillPaymentService: DeleteBillPayment, + private editBillPaymentService: EditBillPayment, + private getBillPaymentsService: GetBillPayments, + private getBillPaymentService: GetBillPayment, + private getPaymentBillsService: GetPaymentBills, + ) {} + + /** + * Creates a bill payment with associated GL entries. + * @param {IBillPaymentDTO} billPaymentDTO + * @returns {Promise} + */ + public createBillPayment(billPaymentDTO: IBillPaymentDTO) { + return this.createBillPaymentService.createBillPayment(billPaymentDTO); + } + + /** + * Delets the given bill payment with associated GL entries. + * @param {number} billPaymentId + */ + public deleteBillPayment(billPaymentId: number) { + return this.deleteBillPaymentService.deleteBillPayment(billPaymentId); + } + + /** + * Edits the given bill payment with associated GL entries. + * @param {number} billPaymentId - The given bill payment id. + * @param {IBillPaymentDTO} billPaymentDTO - The given bill payment DTO. + * @returns {Promise} + */ + public editBillPayment( + billPaymentId: number, + billPaymentDTO: IBillPaymentDTO, + ) { + return this.editBillPaymentService.editBillPayment( + billPaymentId, + billPaymentDTO, + ); + } + + /** + * Retrieves bill payments list. + * @param {number} tenantId + * @param filterDTO + * @returns + */ + // public getBillPayments(filterDTO: IBillPaymentsFilter) { + // return this.getBillPaymentsService.getBillPayments(filterDTO); + // } + + /** + * Retrieve specific bill payment. + * @param {number} billPyamentId - The given bill payment id. + */ + public getBillPayment(billPyamentId: number) { + return this.getBillPaymentService.getBillPayment(billPyamentId); + } + + /** + * Retrieve payment made associated bills. + * @param {number} billPaymentId - The given bill payment id. + */ + public getPaymentBills(billPaymentId: number) { + return this.getPaymentBillsService.getPaymentBills(billPaymentId); + } +} diff --git a/packages/server-nest/src/modules/BillPayments/GetBillPayments.ts b/packages/server-nest/src/modules/BillPayments/GetBillPayments.ts new file mode 100644 index 000000000..23a78dbcd --- /dev/null +++ b/packages/server-nest/src/modules/BillPayments/GetBillPayments.ts @@ -0,0 +1,75 @@ +import { Inject, Service } from 'typedi'; +import * as R from 'ramda'; +import { + IBillPayment, + IBillPaymentsFilter, + IPaginationMeta, + IFilterMeta, +} from '@/interfaces'; +import { BillPaymentTransformer } from './queries/BillPaymentTransformer'; +import DynamicListingService from '@/services/DynamicListing/DynamicListService'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; + +@Service() +export class GetBillPayments { + @Inject() + private tenancy: HasTenancyService; + + @Inject() + private dynamicListService: DynamicListingService; + + @Inject() + private transformer: TransformerInjectable; + + /** + * Retrieve bill payment paginted and filterable list. + * @param {number} tenantId + * @param {IBillPaymentsFilter} billPaymentsFilter + */ + public async getBillPayments( + tenantId: number, + filterDTO: IBillPaymentsFilter + ): Promise<{ + billPayments: IBillPayment[]; + pagination: IPaginationMeta; + filterMeta: IFilterMeta; + }> { + const { BillPayment } = this.tenancy.models(tenantId); + + // Parses filter DTO. + const filter = this.parseListFilterDTO(filterDTO); + + // Dynamic list service. + const dynamicList = await this.dynamicListService.dynamicList( + tenantId, + BillPayment, + filter + ); + const { results, pagination } = await BillPayment.query() + .onBuild((builder) => { + builder.withGraphFetched('vendor'); + builder.withGraphFetched('paymentAccount'); + + dynamicList.buildQuery()(builder); + filter?.filterQuery && filter?.filterQuery(builder); + }) + .pagination(filter.page - 1, filter.pageSize); + + // Transformes the bill payments models to POJO. + const billPayments = await this.transformer.transform( + tenantId, + results, + new BillPaymentTransformer() + ); + return { + billPayments, + pagination, + filterMeta: dynamicList.getResponseMeta(), + }; + } + + private parseListFilterDTO(filterDTO) { + return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO); + } +} diff --git a/packages/server-nest/src/modules/BillPayments/commands/BillPaymentBillSync.service.ts b/packages/server-nest/src/modules/BillPayments/commands/BillPaymentBillSync.service.ts new file mode 100644 index 000000000..49e990550 --- /dev/null +++ b/packages/server-nest/src/modules/BillPayments/commands/BillPaymentBillSync.service.ts @@ -0,0 +1,44 @@ +import { Injectable } from '@nestjs/common'; +import { Knex } from 'knex'; +import { Bill } from '../../Bills/models/Bill'; +import { IBillPaymentEntryDTO } from '../types/BillPayments.types'; + +@Injectable() +export class BillPaymentBillSync { + constructor(private readonly bill: typeof Bill) {} + + /** + * Saves bills payment amount changes different. + * @param {number} tenantId - + * @param {IBillPaymentEntryDTO[]} paymentMadeEntries - + * @param {IBillPaymentEntryDTO[]} oldPaymentMadeEntries - + */ + public async saveChangeBillsPaymentAmount( + paymentMadeEntries: IBillPaymentEntryDTO[], + oldPaymentMadeEntries?: IBillPaymentEntryDTO[], + trx?: Knex.Transaction, + ): Promise { + const opers: Promise[] = []; + + const diffEntries = entriesAmountDiff( + paymentMadeEntries, + oldPaymentMadeEntries, + 'paymentAmount', + 'billId', + ); + diffEntries.forEach( + (diffEntry: { paymentAmount: number; billId: number }) => { + if (diffEntry.paymentAmount === 0) { + return; + } + const oper = this.bill.changePaymentAmount( + diffEntry.billId, + diffEntry.paymentAmount, + trx, + ); + opers.push(oper); + }, + ); + await Promise.all(opers); + } +} diff --git a/packages/server-nest/src/modules/BillPayments/commands/BillPaymentExportable.ts b/packages/server-nest/src/modules/BillPayments/commands/BillPaymentExportable.ts new file mode 100644 index 000000000..44bf4b6cf --- /dev/null +++ b/packages/server-nest/src/modules/BillPayments/commands/BillPaymentExportable.ts @@ -0,0 +1,34 @@ +// import { Inject, Service } from 'typedi'; +// import { Exportable } from '@/services/Export/Exportable'; +// import { BillPaymentsApplication } from './BillPaymentsApplication'; +// import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants'; + +// @Service() +// export class BillPaymentExportable extends Exportable { +// @Inject() +// private billPaymentsApplication: BillPaymentsApplication; + +// /** +// * Retrieves the accounts data to exportable sheet. +// * @param {number} tenantId +// * @returns +// */ +// public exportable(tenantId: number, query: any) { +// const filterQuery = (builder) => { +// builder.withGraphFetched('entries.bill'); +// builder.withGraphFetched('branch'); +// }; +// const parsedQuery = { +// sortOrder: 'desc', +// columnSortBy: 'created_at', +// ...query, +// page: 1, +// pageSize: EXPORT_SIZE_LIMIT, +// filterQuery +// } as any; + +// return this.billPaymentsApplication +// .getBillPayments(tenantId, parsedQuery) +// .then((output) => output.billPayments); +// } +// } diff --git a/packages/server-nest/src/modules/BillPayments/commands/BillPaymentGLEntries.ts b/packages/server-nest/src/modules/BillPayments/commands/BillPaymentGLEntries.ts new file mode 100644 index 000000000..2ff515d7b --- /dev/null +++ b/packages/server-nest/src/modules/BillPayments/commands/BillPaymentGLEntries.ts @@ -0,0 +1,277 @@ +// import moment from 'moment'; +// import { sumBy } from 'lodash'; +// import { Service, Inject } from 'typedi'; +// import { Knex } from 'knex'; +// import { AccountNormal, IBillPayment, ILedgerEntry } from '@/interfaces'; +// import Ledger from '@/services/Accounting/Ledger'; +// import LedgerStorageService from '@/services/Accounting/LedgerStorageService'; +// import HasTenancyService from '@/services/Tenancy/TenancyService'; +// import { TenantMetadata } from '@/system/models'; + +// @Service() +// export class BillPaymentGLEntries { +// @Inject() +// private tenancy: HasTenancyService; + +// @Inject() +// private ledgerStorage: LedgerStorageService; + +// /** +// * Creates a bill payment GL entries. +// * @param {number} tenantId +// * @param {number} billPaymentId +// * @param {Knex.Transaction} trx +// */ +// public writePaymentGLEntries = async ( +// tenantId: number, +// billPaymentId: number, +// trx?: Knex.Transaction +// ): Promise => { +// const { accountRepository } = this.tenancy.repositories(tenantId); +// const { BillPayment, Account } = this.tenancy.models(tenantId); + +// // Retrieves the bill payment details with associated entries. +// const payment = await BillPayment.query(trx) +// .findById(billPaymentId) +// .withGraphFetched('entries.bill'); + +// // Retrieves the given tenant metadata. +// const tenantMeta = await TenantMetadata.query().findOne({ tenantId }); + +// // Finds or creates a new A/P account of the given currency. +// const APAccount = await accountRepository.findOrCreateAccountsPayable( +// payment.currencyCode, +// {}, +// trx +// ); +// // Exchange gain or loss account. +// const EXGainLossAccount = await Account.query(trx).modify( +// 'findBySlug', +// 'exchange-grain-loss' +// ); +// // Retrieves the bill payment ledger. +// const ledger = this.getBillPaymentLedger( +// payment, +// APAccount.id, +// EXGainLossAccount.id, +// tenantMeta.baseCurrency +// ); +// // Commits the ledger on the storage. +// await this.ledgerStorage.commit(tenantId, ledger, trx); +// }; + +// /** +// * Rewrites the bill payment GL entries. +// * @param {number} tenantId +// * @param {number} billPaymentId +// * @param {Knex.Transaction} trx +// */ +// public rewritePaymentGLEntries = async ( +// tenantId: number, +// billPaymentId: number, +// trx?: Knex.Transaction +// ): Promise => { +// // Revert payment GL entries. +// await this.revertPaymentGLEntries(tenantId, billPaymentId, trx); + +// // Write payment GL entries. +// await this.writePaymentGLEntries(tenantId, billPaymentId, trx); +// }; + +// /** +// * Reverts the bill payment GL entries. +// * @param {number} tenantId +// * @param {number} billPaymentId +// * @param {Knex.Transaction} trx +// */ +// public revertPaymentGLEntries = async ( +// tenantId: number, +// billPaymentId: number, +// trx?: Knex.Transaction +// ): Promise => { +// await this.ledgerStorage.deleteByReference( +// tenantId, +// billPaymentId, +// 'BillPayment', +// trx +// ); +// }; + +// /** +// * Retrieves the payment common entry. +// * @param {IBillPayment} billPayment +// * @returns {} +// */ +// private getPaymentCommonEntry = (billPayment: IBillPayment) => { +// const formattedDate = moment(billPayment.paymentDate).format('YYYY-MM-DD'); + +// return { +// debit: 0, +// credit: 0, + +// exchangeRate: billPayment.exchangeRate, +// currencyCode: billPayment.currencyCode, + +// transactionId: billPayment.id, +// transactionType: 'BillPayment', + +// transactionNumber: billPayment.paymentNumber, +// referenceNumber: billPayment.reference, + +// date: formattedDate, +// createdAt: billPayment.createdAt, + +// branchId: billPayment.branchId, +// }; +// }; + +// /** +// * Calculates the payment total exchange gain/loss. +// * @param {IBillPayment} paymentReceive - Payment receive with entries. +// * @returns {number} +// */ +// private getPaymentExGainOrLoss = (billPayment: IBillPayment): number => { +// return sumBy(billPayment.entries, (entry) => { +// const paymentLocalAmount = entry.paymentAmount * billPayment.exchangeRate; +// const invoicePayment = entry.paymentAmount * entry.bill.exchangeRate; + +// return invoicePayment - paymentLocalAmount; +// }); +// }; + +// /** +// * Retrieves the payment exchange gain/loss entries. +// * @param {IBillPayment} billPayment - +// * @param {number} APAccountId - +// * @param {number} gainLossAccountId - +// * @param {string} baseCurrency - +// * @returns {ILedgerEntry[]} +// */ +// private getPaymentExGainOrLossEntries = ( +// billPayment: IBillPayment, +// APAccountId: number, +// gainLossAccountId: number, +// baseCurrency: string +// ): ILedgerEntry[] => { +// const commonEntry = this.getPaymentCommonEntry(billPayment); +// const totalExGainOrLoss = this.getPaymentExGainOrLoss(billPayment); +// const absExGainOrLoss = Math.abs(totalExGainOrLoss); + +// return totalExGainOrLoss +// ? [ +// { +// ...commonEntry, +// currencyCode: baseCurrency, +// exchangeRate: 1, +// credit: totalExGainOrLoss > 0 ? absExGainOrLoss : 0, +// debit: totalExGainOrLoss < 0 ? absExGainOrLoss : 0, +// accountId: gainLossAccountId, +// index: 2, +// indexGroup: 20, +// accountNormal: AccountNormal.DEBIT, +// }, +// { +// ...commonEntry, +// currencyCode: baseCurrency, +// exchangeRate: 1, +// debit: totalExGainOrLoss > 0 ? absExGainOrLoss : 0, +// credit: totalExGainOrLoss < 0 ? absExGainOrLoss : 0, +// accountId: APAccountId, +// index: 3, +// accountNormal: AccountNormal.DEBIT, +// }, +// ] +// : []; +// }; + +// /** +// * Retrieves the payment deposit GL entry. +// * @param {IBillPayment} billPayment +// * @returns {ILedgerEntry} +// */ +// private getPaymentGLEntry = (billPayment: IBillPayment): ILedgerEntry => { +// const commonEntry = this.getPaymentCommonEntry(billPayment); + +// return { +// ...commonEntry, +// credit: billPayment.localAmount, +// accountId: billPayment.paymentAccountId, +// accountNormal: AccountNormal.DEBIT, +// index: 2, +// }; +// }; + +// /** +// * Retrieves the payment GL payable entry. +// * @param {IBillPayment} billPayment +// * @param {number} APAccountId +// * @returns {ILedgerEntry} +// */ +// private getPaymentGLPayableEntry = ( +// billPayment: IBillPayment, +// APAccountId: number +// ): ILedgerEntry => { +// const commonEntry = this.getPaymentCommonEntry(billPayment); + +// return { +// ...commonEntry, +// exchangeRate: billPayment.exchangeRate, +// debit: billPayment.localAmount, +// contactId: billPayment.vendorId, +// accountId: APAccountId, +// accountNormal: AccountNormal.CREDIT, +// index: 1, +// }; +// }; + +// /** +// * Retrieves the payment GL entries. +// * @param {IBillPayment} billPayment +// * @param {number} APAccountId +// * @returns {ILedgerEntry[]} +// */ +// private getPaymentGLEntries = ( +// billPayment: IBillPayment, +// APAccountId: number, +// gainLossAccountId: number, +// baseCurrency: string +// ): ILedgerEntry[] => { +// // Retrieves the payment deposit entry. +// const paymentEntry = this.getPaymentGLEntry(billPayment); + +// // Retrieves the payment debit A/R entry. +// const payableEntry = this.getPaymentGLPayableEntry( +// billPayment, +// APAccountId +// ); +// // Retrieves the exchange gain/loss entries. +// const exGainLossEntries = this.getPaymentExGainOrLossEntries( +// billPayment, +// APAccountId, +// gainLossAccountId, +// baseCurrency +// ); +// return [paymentEntry, payableEntry, ...exGainLossEntries]; +// }; + +// /** +// * Retrieves the bill payment ledger. +// * @param {IBillPayment} billPayment +// * @param {number} APAccountId +// * @returns {Ledger} +// */ +// private getBillPaymentLedger = ( +// billPayment: IBillPayment, +// APAccountId: number, +// gainLossAccountId: number, +// baseCurrency: string +// ): Ledger => { +// const entries = this.getPaymentGLEntries( +// billPayment, +// APAccountId, +// gainLossAccountId, +// baseCurrency +// ); +// return new Ledger(entries); +// }; +// } diff --git a/packages/server-nest/src/modules/BillPayments/commands/BillPaymentGLEntriesSubscriber.ts b/packages/server-nest/src/modules/BillPayments/commands/BillPaymentGLEntriesSubscriber.ts new file mode 100644 index 000000000..2b4f0b6f6 --- /dev/null +++ b/packages/server-nest/src/modules/BillPayments/commands/BillPaymentGLEntriesSubscriber.ts @@ -0,0 +1,76 @@ +// import { Inject, Service } from 'typedi'; +// import events from '@/subscribers/events'; +// import { +// IBillPaymentEventCreatedPayload, +// IBillPaymentEventDeletedPayload, +// IBillPaymentEventEditedPayload, +// } from '@/interfaces'; +// import { BillPaymentGLEntries } from './BillPaymentGLEntries'; + +// @Service() +// export class PaymentWriteGLEntriesSubscriber { +// @Inject() +// private billPaymentGLEntries: BillPaymentGLEntries; + +// /** +// * Attaches events with handles. +// */ +// public attach(bus) { +// bus.subscribe(events.billPayment.onCreated, this.handleWriteJournalEntries); +// bus.subscribe( +// events.billPayment.onEdited, +// this.handleRewriteJournalEntriesOncePaymentEdited +// ); +// bus.subscribe( +// events.billPayment.onDeleted, +// this.handleRevertJournalEntries +// ); +// } + +// /** +// * Handle bill payment writing journal entries once created. +// */ +// private handleWriteJournalEntries = async ({ +// tenantId, +// billPayment, +// trx, +// }: IBillPaymentEventCreatedPayload) => { +// // Records the journal transactions after bills payment +// // and change diff account balance. +// await this.billPaymentGLEntries.writePaymentGLEntries( +// tenantId, +// billPayment.id, +// trx +// ); +// }; + +// /** +// * Handle bill payment re-writing journal entries once the payment transaction be edited. +// */ +// private handleRewriteJournalEntriesOncePaymentEdited = async ({ +// tenantId, +// billPayment, +// trx, +// }: IBillPaymentEventEditedPayload) => { +// await this.billPaymentGLEntries.rewritePaymentGLEntries( +// tenantId, +// billPayment.id, +// trx +// ); +// }; + +// /** +// * Reverts journal entries once bill payment deleted. +// */ +// private handleRevertJournalEntries = async ({ +// tenantId, +// billPaymentId, +// trx, +// }: IBillPaymentEventDeletedPayload) => { +// await this.billPaymentGLEntries.revertPaymentGLEntries( +// tenantId, +// billPaymentId, +// trx +// ); +// }; +// } diff --git a/packages/server-nest/src/modules/BillPayments/commands/BillPaymentValidators.service.ts b/packages/server-nest/src/modules/BillPayments/commands/BillPaymentValidators.service.ts new file mode 100644 index 000000000..940cad4f5 --- /dev/null +++ b/packages/server-nest/src/modules/BillPayments/commands/BillPaymentValidators.service.ts @@ -0,0 +1,256 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { sumBy, difference } from 'lodash'; +import { + IBillPaymentDTO, + IBillPaymentEntryDTO, + IBillPayment, + IBillPaymentEntry, +} from '../types/BillPayments.types'; +import { ERRORS } from '../constants'; +import { Bill } from '../../Bills/models/Bill'; +import { BillPayment } from '../models/BillPayment'; +import { BillPaymentEntry } from '../models/BillPaymentEntry'; +import { ServiceError } from '../../Items/ServiceError'; +import { ACCOUNT_TYPE } from '@/constants/accounts'; +import { Account } from '../../Accounts/models/Account.model'; + +@Injectable() +export class BillPaymentValidators { + constructor( + @Inject(Bill.name) + private readonly billModel: typeof Bill, + + @Inject(BillPayment.name) + private readonly billPaymentModel: typeof BillPayment, + + @Inject(BillPaymentEntry.name) + private readonly billPaymentEntryModel: typeof BillPaymentEntry, + + @Inject(Account.name) + private readonly accountModel: typeof Account, + ) {} + + /** + * Validates the bill payment existance. + * @param {Request} req + * @param {Response} res + * @param {Function} next + */ + public async getPaymentMadeOrThrowError(paymentMadeId: number) { + const billPayment = await this.billPaymentModel + .query() + .withGraphFetched('entries') + .findById(paymentMadeId); + + if (!billPayment) { + throw new ServiceError(ERRORS.PAYMENT_MADE_NOT_FOUND); + } + return billPayment; + } + + /** + * Validates the payment account. + * @param {number} tenantId - + * @param {number} paymentAccountId + * @return {Promise} + */ + public async getPaymentAccountOrThrowError(paymentAccountId: number) { + const paymentAccount = await this.accountModel + .query() + .findById(paymentAccountId); + if (!paymentAccount) { + throw new ServiceError(ERRORS.PAYMENT_ACCOUNT_NOT_FOUND); + } + // Validate the payment account type. + if ( + !paymentAccount.isAccountType([ + ACCOUNT_TYPE.BANK, + ACCOUNT_TYPE.CASH, + ACCOUNT_TYPE.OTHER_CURRENT_ASSET, + ]) + ) { + throw new ServiceError(ERRORS.PAYMENT_ACCOUNT_NOT_CURRENT_ASSET_TYPE); + } + return paymentAccount; + } + + /** + * Validates the payment number uniqness. + * @param {number} tenantId - + * @param {string} paymentMadeNumber - + * @return {Promise} + */ + public async validatePaymentNumber( + paymentMadeNumber: string, + notPaymentMadeId?: number, + ) { + const foundBillPayment = await this.billPaymentModel + .query() + .onBuild((builder: any) => { + builder.findOne('payment_number', paymentMadeNumber); + + if (notPaymentMadeId) { + builder.whereNot('id', notPaymentMadeId); + } + }); + + if (foundBillPayment) { + throw new ServiceError(ERRORS.BILL_PAYMENT_NUMBER_NOT_UNQIUE); + } + return foundBillPayment; + } + + /** + * Validate whether the entries bills ids exist on the storage. + */ + public async validateBillsExistance( + billPaymentEntries: { billId: number }[], + vendorId: number, + ) { + const entriesBillsIds = billPaymentEntries.map((e: any) => e.billId); + + const storedBills = await this.billModel + .query() + .whereIn('id', entriesBillsIds) + .where('vendor_id', vendorId); + + const storedBillsIds = storedBills.map((t: Bill) => t.id); + const notFoundBillsIds = difference(entriesBillsIds, storedBillsIds); + + if (notFoundBillsIds.length > 0) { + throw new ServiceError(ERRORS.BILL_ENTRIES_IDS_NOT_FOUND); + } + // Validate the not opened bills. + const notOpenedBills = storedBills.filter((bill) => !bill.openedAt); + + if (notOpenedBills.length > 0) { + throw new ServiceError(ERRORS.BILLS_NOT_OPENED_YET, null, { + notOpenedBills, + }); + } + return storedBills; + } + + /** + * Validate wether the payment amount bigger than the payable amount. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + * @return {void} + */ + public async validateBillsDueAmount( + billPaymentEntries: IBillPaymentEntryDTO[], + oldPaymentEntries: IBillPaymentEntry[] = [], + ) { + const billsIds = billPaymentEntries.map( + (entry: IBillPaymentEntryDTO) => entry.billId, + ); + + const storedBills = await this.billModel.query().whereIn('id', billsIds); + const storedBillsMap = new Map( + storedBills.map((bill) => { + const oldEntries = oldPaymentEntries.filter( + (entry) => entry.billId === bill.id, + ); + const oldPaymentAmount = sumBy(oldEntries, 'paymentAmount') || 0; + + return [ + bill.id, + { ...bill, dueAmount: bill.dueAmount + oldPaymentAmount }, + ]; + }), + ); + interface invalidPaymentAmountError { + index: number; + due_amount: number; + } + const hasWrongPaymentAmount: invalidPaymentAmountError[] = []; + + billPaymentEntries.forEach((entry: IBillPaymentEntryDTO, index: number) => { + const entryBill = storedBillsMap.get(entry.billId); + const { dueAmount } = entryBill; + + if (dueAmount < entry.paymentAmount) { + hasWrongPaymentAmount.push({ index, due_amount: dueAmount }); + } + }); + if (hasWrongPaymentAmount.length > 0) { + throw new ServiceError(ERRORS.INVALID_BILL_PAYMENT_AMOUNT); + } + } + + /** + * Validate the payment receive entries IDs existance. + * @param {Request} req + * @param {Response} res + * @return {Response} + */ + public async validateEntriesIdsExistance( + billPaymentId: number, + billPaymentEntries: IBillPaymentEntry[], + ) { + const entriesIds = billPaymentEntries + .filter((entry: any) => entry.id) + .map((entry: any) => entry.id); + + const storedEntries = await this.billPaymentEntryModel + .query() + .where('bill_payment_id', billPaymentId); + + const storedEntriesIds = storedEntries.map((entry: any) => entry.id); + const notFoundEntriesIds = difference(entriesIds, storedEntriesIds); + + if (notFoundEntriesIds.length > 0) { + throw new ServiceError(ERRORS.BILL_PAYMENT_ENTRIES_NOT_FOUND); + } + } + + /** + * * Validate the payment vendor whether modified. + * @param {string} billPaymentNo + */ + public validateVendorNotModified( + billPaymentDTO: IBillPaymentDTO, + oldBillPayment: BillPayment, + ) { + if (billPaymentDTO.vendorId !== oldBillPayment.vendorId) { + throw new ServiceError(ERRORS.PAYMENT_NUMBER_SHOULD_NOT_MODIFY); + } + } + + /** + * Validates the payment account currency code. The deposit account curreny + * should be equals the customer currency code or the base currency. + * @param {string} paymentAccountCurrency + * @param {string} customerCurrency + * @param {string} baseCurrency + * @throws {ServiceError(ERRORS.WITHDRAWAL_ACCOUNT_CURRENCY_INVALID)} + */ + public validateWithdrawalAccountCurrency = ( + paymentAccountCurrency: string, + customerCurrency: string, + baseCurrency: string, + ) => { + if ( + paymentAccountCurrency !== customerCurrency && + paymentAccountCurrency !== baseCurrency + ) { + throw new ServiceError(ERRORS.WITHDRAWAL_ACCOUNT_CURRENCY_INVALID); + } + }; + + /** + * Validates the given vendor has no associated payments. + * @param {number} tenantId + * @param {number} vendorId + */ + public async validateVendorHasNoPayments(vendorId: number) { + const payments = await this.billPaymentModel + .query() + .where('vendor_id', vendorId); + + if (payments.length > 0) { + throw new ServiceError(ERRORS.VENDOR_HAS_PAYMENTS); + } + } +} diff --git a/packages/server-nest/src/modules/BillPayments/commands/BillPaymentsImportable.ts b/packages/server-nest/src/modules/BillPayments/commands/BillPaymentsImportable.ts new file mode 100644 index 000000000..70d8feb34 --- /dev/null +++ b/packages/server-nest/src/modules/BillPayments/commands/BillPaymentsImportable.ts @@ -0,0 +1,45 @@ +// import { Inject, Service } from 'typedi'; +// import { Knex } from 'knex'; +// import { IBillPaymentDTO } from '@/interfaces'; +// import { CreateBillPayment } from './CreateBillPayment'; +// import { Importable } from '@/services/Import/Importable'; +// import { BillsPaymentsSampleData } from './constants'; + +// @Service() +// export class BillPaymentsImportable extends Importable { +// @Inject() +// private createBillPaymentService: CreateBillPayment; + +// /** +// * Importing to account service. +// * @param {number} tenantId +// * @param {IAccountCreateDTO} createAccountDTO +// * @returns +// */ +// public importable( +// tenantId: number, +// billPaymentDTO: IBillPaymentDTO, +// trx?: Knex.Transaction +// ) { +// return this.createBillPaymentService.createBillPayment( +// tenantId, +// billPaymentDTO, +// trx +// ); +// } + +// /** +// * Concurrrency controlling of the importing process. +// * @returns {number} +// */ +// public get concurrency() { +// return 1; +// } + +// /** +// * Retrieves the sample data that used to download accounts sample sheet. +// */ +// public sampleData(): any[] { +// return BillsPaymentsSampleData; +// } +// } diff --git a/packages/server-nest/src/modules/BillPayments/commands/BillPaymentsPages.service.ts b/packages/server-nest/src/modules/BillPayments/commands/BillPaymentsPages.service.ts new file mode 100644 index 000000000..2bf45bc5a --- /dev/null +++ b/packages/server-nest/src/modules/BillPayments/commands/BillPaymentsPages.service.ts @@ -0,0 +1,106 @@ +import { Inject, Service } from 'typedi'; +import { omit } from 'lodash'; +import { ERRORS } from '../constants'; +import { Injectable } from '@nestjs/common'; +import { Bill } from '../../Bills/models/Bill'; +import { BillPayment } from '../models/BillPayment'; +import { IBillReceivePageEntry } from '../types/BillPayments.types'; +import { ServiceError } from '../../Items/ServiceError'; + +@Injectable() +export default class BillPaymentsPages { + /** + * @param {typeof Bill} billModel - Bill model. + * @param {typeof BillPayment} billPaymentModel - Bill payment model. + */ + constructor( + @Inject(Bill.name) + private readonly billModel: typeof Bill, + + @Inject(BillPayment.name) + private readonly billPaymentModel: typeof BillPayment, + ) {} + + /** + * Retrieve bill payment with associated metadata. + * @param {number} billPaymentId - The bill payment id. + * @return {object} + */ + public async getBillPaymentEditPage(billPaymentId: number): Promise<{ + billPayment: Omit; + entries: IBillReceivePageEntry[]; + }> { + const billPayment = await this.billPaymentModel + .query() + .findById(billPaymentId) + .withGraphFetched('entries.bill') + .withGraphFetched('attachments'); + + // Throw not found the bill payment. + if (!billPayment) { + throw new ServiceError(ERRORS.PAYMENT_MADE_NOT_FOUND); + } + const paymentEntries = billPayment.entries.map((entry) => ({ + ...this.mapBillToPageEntry(entry.bill), + dueAmount: entry.bill.dueAmount + entry.paymentAmount, + paymentAmount: entry.paymentAmount, + })); + + const resPayableBills = await Bill.query() + .modify('opened') + .modify('dueBills') + .where('vendor_id', billPayment.vendorId) + .whereNotIn( + 'id', + billPayment.entries.map((e) => e.billId), + ) + .orderBy('bill_date', 'ASC'); + + // Mapping the payable bills to entries. + const restPayableEntries = resPayableBills.map(this.mapBillToPageEntry); + const entries = [...paymentEntries, ...restPayableEntries]; + + return { + billPayment: omit(billPayment, ['entries']), + entries, + }; + } + + /** + * Retrieve the payable entries of the new page once vendor be selected. + * @param {number} tenantId + * @param {number} vendorId + */ + public async getNewPageEntries( + vendorId: number, + ): Promise { + // Retrieve all payable bills that associated to the payment made transaction. + const payableBills = await this.billModel + .query() + .modify('opened') + .modify('dueBills') + .where('vendor_id', vendorId) + .orderBy('bill_date', 'ASC'); + + return payableBills.map(this.mapBillToPageEntry); + } + + /** + * Retrive edit page invoices entries from the given sale invoices models. + * @param {Bill} bill - Bill. + * @return {IBillReceivePageEntry} + */ + private mapBillToPageEntry(bill: Bill): IBillReceivePageEntry { + return { + entryType: 'invoice', + billId: bill.id, + billNo: bill.billNumber, + amount: bill.amount, + dueAmount: bill.dueAmount, + totalPaymentAmount: bill.paymentAmount, + paymentAmount: bill.paymentAmount, + currencyCode: bill.currencyCode, + date: bill.billDate, + }; + } +} diff --git a/packages/server-nest/src/modules/BillPayments/commands/CommandBillPaymentDTOTransformer.service.ts b/packages/server-nest/src/modules/BillPayments/commands/CommandBillPaymentDTOTransformer.service.ts new file mode 100644 index 000000000..2c4dbf3c6 --- /dev/null +++ b/packages/server-nest/src/modules/BillPayments/commands/CommandBillPaymentDTOTransformer.service.ts @@ -0,0 +1,51 @@ +import { Injectable } from '@nestjs/common'; +import * as R from 'ramda'; +import { omit, sumBy } from 'lodash'; +import { formatDateFields } from '@/utils/format-date-fields'; +import { IBillPaymentDTO } from '../types/BillPayments.types'; +import { assocItemEntriesDefaultIndex } from '@/utils/associate-item-entries-index'; +import { BranchTransactionDTOTransformer } from '@/modules/Branches/integrations/BranchTransactionDTOTransform'; +import { Vendor } from '@/modules/Vendors/models/Vendor'; +import { BillPayment } from '../models/BillPayment'; + +@Injectable() +export class CommandBillPaymentDTOTransformer { + constructor( + private readonly branchDTOTransform: BranchTransactionDTOTransformer, + ) {} + + /** + * Transforms create/edit DTO to model. + * @param {number} tenantId + * @param {IBillPaymentDTO} billPaymentDTO - Bill payment. + * @param {IBillPayment} oldBillPayment - Old bill payment. + * @return {Promise} + */ + public async transformDTOToModel( + billPaymentDTO: IBillPaymentDTO, + vendor: Vendor, + oldBillPayment?: BillPayment, + ): Promise { + const amount = + billPaymentDTO.amount ?? sumBy(billPaymentDTO.entries, 'paymentAmount'); + + // Associate the default index to each item entry. + const entries = R.compose( + // Associate the default index to payment entries. + assocItemEntriesDefaultIndex, + )(billPaymentDTO.entries); + + const initialDTO = { + ...formatDateFields(omit(billPaymentDTO, ['attachments']), [ + 'paymentDate', + ]), + amount, + currencyCode: vendor.currencyCode, + exchangeRate: billPaymentDTO.exchangeRate || 1, + entries, + }; + return R.compose(this.branchDTOTransform.transformDTO)( + initialDTO, + ); + } +} diff --git a/packages/server-nest/src/modules/BillPayments/commands/CreateBillPayment.service.ts b/packages/server-nest/src/modules/BillPayments/commands/CreateBillPayment.service.ts new file mode 100644 index 000000000..eff867c11 --- /dev/null +++ b/packages/server-nest/src/modules/BillPayments/commands/CreateBillPayment.service.ts @@ -0,0 +1,122 @@ +import { Knex } from 'knex'; +import { + IBillPaymentDTO, + IBillPayment, + IBillPaymentEventCreatedPayload, + IBillPaymentCreatingPayload, +} from '../types/BillPayments.types'; +import { Inject, Injectable } from '@nestjs/common'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { UnitOfWork } from '../../Tenancy/TenancyDB/UnitOfWork.service'; +import { BillPaymentValidators } from './BillPaymentValidators.service'; +import { CommandBillPaymentDTOTransformer } from './CommandBillPaymentDTOTransformer.service'; +import { events } from '@/common/events/events'; +import { TenancyContext } from '../../Tenancy/TenancyContext.service'; +import { BillPayment } from '../models/BillPayment'; +import { Vendor } from '../../Vendors/models/Vendor'; + +@Injectable() +export class CreateBillPaymentService { + /** + * @param {UnitOfWork} uow - Unit of work service. + * @param {EventEmitter2} eventPublisher - Event emitter service. + * @param {BillPaymentValidators} validators - Bill payment validators service. + * @param {CommandBillPaymentDTOTransformer} commandTransformerDTO - Command bill payment DTO transformer service. + * @param {TenancyContext} tenancyContext - Tenancy context service. + * @param {typeof Vendor} vendorModel - Vendor model. + * @param {typeof BillPayment} billPaymentModel - Bill payment model. + */ + constructor( + private uow: UnitOfWork, + private eventPublisher: EventEmitter2, + private validators: BillPaymentValidators, + private commandTransformerDTO: CommandBillPaymentDTOTransformer, + private tenancyContext: TenancyContext, + + @Inject(Vendor.name) + private readonly vendorModel: typeof Vendor, + + @Inject(BillPayment.name) + private readonly billPaymentModel: typeof BillPayment, + ) {} + + /** + * Creates a new bill payment transcations and store it to the storage + * with associated bills entries and journal transactions. + * ------ + * Precedures:- + * ------ + * - Records the bill payment transaction. + * - Records the bill payment associated entries. + * - Increment the payment amount of the given vendor bills. + * - Decrement the vendor balance. + * - Records payment journal entries. + * ------ + * @param {number} tenantId - Tenant id. + * @param {BillPaymentDTO} billPayment - Bill payment object. + */ + public async createBillPayment( + billPaymentDTO: IBillPaymentDTO, + trx?: Knex.Transaction, + ): Promise { + const tenantMeta = await this.tenancyContext.getTenant(true); + + // Retrieves the payment vendor or throw not found error. + const vendor = await this.vendorModel + .query() + .findById(billPaymentDTO.vendorId) + .throwIfNotFound(); + + // Transform create DTO to model object. + const billPaymentObj = await this.commandTransformerDTO.transformDTOToModel( + billPaymentDTO, + vendor, + ); + // Validate the payment account existance and type. + const paymentAccount = await this.validators.getPaymentAccountOrThrowError( + billPaymentObj.paymentAccountId, + ); + // Validate the payment number uniquiness. + if (billPaymentObj.paymentNumber) { + await this.validators.validatePaymentNumber(billPaymentObj.paymentNumber); + } + // Validates the bills existance and associated to the given vendor. + await this.validators.validateBillsExistance( + billPaymentObj.entries, + billPaymentDTO.vendorId, + ); + // Validates the bills due payment amount. + await this.validators.validateBillsDueAmount(billPaymentObj.entries); + // Validates the withdrawal account currency code. + this.validators.validateWithdrawalAccountCurrency( + paymentAccount.currencyCode, + vendor.currencyCode, + tenantMeta.metadata.baseCurrency, + ); + // Writes bill payment transacation with associated transactions + // under unit-of-work envirement. + return this.uow.withTransaction(async (trx: Knex.Transaction) => { + // Triggers `onBillPaymentCreating` event. + await this.eventPublisher.emitAsync(events.billPayment.onCreating, { + billPaymentDTO, + trx, + } as IBillPaymentCreatingPayload); + + // Writes the bill payment graph to the storage. + const billPayment = await this.billPaymentModel + .query(trx) + .insertGraphAndFetch({ + ...billPaymentObj, + }); + // Triggers `onBillPaymentCreated` event. + await this.eventPublisher.emitAsync(events.billPayment.onCreated, { + billPayment, + billPaymentId: billPayment.id, + billPaymentDTO, + trx, + } as IBillPaymentEventCreatedPayload); + + return billPayment; + }, trx); + } +} diff --git a/packages/server-nest/src/modules/BillPayments/commands/DeleteBillPayment.service.ts b/packages/server-nest/src/modules/BillPayments/commands/DeleteBillPayment.service.ts new file mode 100644 index 000000000..8bc9575d2 --- /dev/null +++ b/packages/server-nest/src/modules/BillPayments/commands/DeleteBillPayment.service.ts @@ -0,0 +1,74 @@ +import { Knex } from 'knex'; +import { Inject, Injectable } from '@nestjs/common'; +import { + IBillPaymentDeletingPayload, + IBillPaymentEventDeletedPayload, +} from '../types/BillPayments.types'; +import { BillPayment } from '../models/BillPayment'; +import { BillPaymentEntry } from '../models/BillPaymentEntry'; +import { UnitOfWork } from '../../Tenancy/TenancyDB/UnitOfWork.service'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { events } from '@/common/events/events'; + +@Injectable() +export class DeleteBillPayment { + /** + * @param {EventPublisher} eventPublisher - Event publisher. + * @param {UnitOfWork} uow - Unit of work. + * @param {typeof BillPayment} billPaymentModel - Bill payment model. + * @param {typeof BillPaymentEntry} billPaymentEntryModel - Bill payment entry model. + */ + constructor( + private readonly eventEmitter: EventEmitter2, + private readonly uow: UnitOfWork, + + @Inject(BillPayment.name) + private readonly billPaymentModel: typeof BillPayment, + + @Inject(BillPaymentEntry.name) + private readonly billPaymentEntryModel: typeof BillPaymentEntry, + ) {} + + /** + * Deletes the bill payment and associated transactions. + * @param {Integer} billPaymentId - The given bill payment id. + * @return {Promise} + */ + public async deleteBillPayment(billPaymentId: number) { + // Retrieve the bill payment or throw not found service error. + const oldBillPayment = await this.billPaymentModel + .query() + .withGraphFetched('entries') + .findById(billPaymentId) + .throwIfNotFound(); + + // Deletes the bill transactions with associated transactions under + // unit-of-work environment. + return this.uow.withTransaction(async (trx: Knex.Transaction) => { + // Triggers `onBillPaymentDeleting` payload. + await this.eventEmitter.emitAsync(events.billPayment.onDeleting, { + oldBillPayment, + trx, + } as IBillPaymentDeletingPayload); + + // Deletes the bill payment associated entries. + await this.billPaymentEntryModel + .query(trx) + .where('bill_payment_id', billPaymentId) + .delete(); + + // Deletes the bill payment transaction. + await this.billPaymentModel + .query(trx) + .where('id', billPaymentId) + .delete(); + + // Triggers `onBillPaymentDeleted` event. + await this.eventEmitter.emitAsync(events.billPayment.onDeleted, { + billPaymentId, + oldBillPayment, + trx, + } as IBillPaymentEventDeletedPayload); + }); + } +} diff --git a/packages/server-nest/src/modules/BillPayments/commands/EditBillPayment.service.ts b/packages/server-nest/src/modules/BillPayments/commands/EditBillPayment.service.ts new file mode 100644 index 000000000..757b14320 --- /dev/null +++ b/packages/server-nest/src/modules/BillPayments/commands/EditBillPayment.service.ts @@ -0,0 +1,136 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { BillPaymentValidators } from './BillPaymentValidators.service'; +import { + IBillPaymentEditingPayload, + IBillPaymentEventEditedPayload, +} from '../types/BillPayments.types'; +import { Knex } from 'knex'; +import { BillPayment } from '../models/BillPayment'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service'; +import { CommandBillPaymentDTOTransformer } from './CommandBillPaymentDTOTransformer.service'; +import { Vendor } from '@/modules/Vendors/models/Vendor'; +import { events } from '@/common/events/events'; +import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service'; + +@Injectable() +export class EditBillPayment { + constructor( + private readonly validators: BillPaymentValidators, + private readonly eventPublisher: EventEmitter2, + private readonly uow: UnitOfWork, + private readonly transformer: CommandBillPaymentDTOTransformer, + private readonly tenancyContext: TenancyContext, + + @Inject(BillPayment.name) + private readonly billPaymentModel: typeof BillPayment, + + @Inject(Vendor.name) + private readonly vendorModel: typeof Vendor, + ) {} + + /** + * Edits the details of the given bill payment. + * + * Preceducres: + * ------ + * - Update the bill payment transaction. + * - Insert the new bill payment entries that have no ids. + * - Update the bill paymeny entries that have ids. + * - Delete the bill payment entries that not presented. + * - Re-insert the journal transactions and update the diff accounts balance. + * - Update the diff vendor balance. + * - Update the diff bill payment amount. + * ------ + * @param {number} tenantId - Tenant id + * @param {Integer} billPaymentId + * @param {BillPaymentDTO} billPayment + * @param {IBillPayment} oldBillPayment + */ + public async editBillPayment( + billPaymentId: number, + billPaymentDTO, + ): Promise { + const tenantMeta = await this.tenancyContext.getTenant(true); + + const oldBillPayment = await this.billPaymentModel + .query() + .findById(billPaymentId) + .withGraphFetched('entries') + .throwIfNotFound(); + + const vendor = await this.vendorModel + .query() + .findById(billPaymentDTO.vendorId) + .throwIfNotFound(); + + const billPaymentObj = await this.transformer.transformDTOToModel( + billPaymentDTO, + vendor, + oldBillPayment, + ); + // Validate vendor not modified. + this.validators.validateVendorNotModified(billPaymentDTO, oldBillPayment); + + // Validate the payment account existance and type. + const paymentAccount = await this.validators.getPaymentAccountOrThrowError( + billPaymentObj.paymentAccountId, + ); + // Validate the items entries IDs existance on the storage. + await this.validators.validateEntriesIdsExistance( + billPaymentId, + billPaymentObj.entries, + ); + // Validate the bills existance and associated to the given vendor. + await this.validators.validateBillsExistance( + billPaymentObj.entries, + billPaymentDTO.vendorId, + ); + // Validates the bills due payment amount. + await this.validators.validateBillsDueAmount( + billPaymentObj.entries, + oldBillPayment.entries, + ); + // Validate the payment number uniquiness. + if (billPaymentObj.paymentNumber) { + await this.validators.validatePaymentNumber( + billPaymentObj.paymentNumber, + billPaymentId, + ); + } + // Validates the withdrawal account currency code. + this.validators.validateWithdrawalAccountCurrency( + paymentAccount.currencyCode, + vendor.currencyCode, + tenantMeta.metadata.baseCurrency, + ); + // Edits the bill transactions with associated transactions + // under unit-of-work envirement. + return this.uow.withTransaction(async (trx: Knex.Transaction) => { + // Triggers `onBillPaymentEditing` event. + await this.eventPublisher.emitAsync(events.billPayment.onEditing, { + oldBillPayment, + billPaymentDTO, + trx, + } as IBillPaymentEditingPayload); + + // Edits the bill payment transaction graph on the storage. + const billPayment = await this.billPaymentModel + .query(trx) + .upsertGraphAndFetch({ + id: billPaymentId, + ...billPaymentObj, + }); + // Triggers `onBillPaymentEdited` event. + await this.eventPublisher.emitAsync(events.billPayment.onEdited, { + billPaymentId, + billPayment, + oldBillPayment, + billPaymentDTO, + trx, + } as IBillPaymentEventEditedPayload); + + return billPayment; + }); + } +} diff --git a/packages/server-nest/src/modules/BillPayments/constants.ts b/packages/server-nest/src/modules/BillPayments/constants.ts new file mode 100644 index 000000000..29c93b1f5 --- /dev/null +++ b/packages/server-nest/src/modules/BillPayments/constants.ts @@ -0,0 +1,50 @@ +export const ERRORS = { + BILL_VENDOR_NOT_FOUND: 'VENDOR_NOT_FOUND', + PAYMENT_MADE_NOT_FOUND: 'PAYMENT_MADE_NOT_FOUND', + BILL_PAYMENT_NUMBER_NOT_UNQIUE: 'BILL_PAYMENT_NUMBER_NOT_UNQIUE', + PAYMENT_ACCOUNT_NOT_FOUND: 'PAYMENT_ACCOUNT_NOT_FOUND', + PAYMENT_ACCOUNT_NOT_CURRENT_ASSET_TYPE: + 'PAYMENT_ACCOUNT_NOT_CURRENT_ASSET_TYPE', + BILL_ENTRIES_IDS_NOT_FOUND: 'BILL_ENTRIES_IDS_NOT_FOUND', + BILL_PAYMENT_ENTRIES_NOT_FOUND: 'BILL_PAYMENT_ENTRIES_NOT_FOUND', + INVALID_BILL_PAYMENT_AMOUNT: 'INVALID_BILL_PAYMENT_AMOUNT', + PAYMENT_NUMBER_SHOULD_NOT_MODIFY: 'PAYMENT_NUMBER_SHOULD_NOT_MODIFY', + BILLS_NOT_OPENED_YET: 'BILLS_NOT_OPENED_YET', + VENDOR_HAS_PAYMENTS: 'VENDOR_HAS_PAYMENTS', + WITHDRAWAL_ACCOUNT_CURRENCY_INVALID: 'WITHDRAWAL_ACCOUNT_CURRENCY_INVALID', +}; + +export const DEFAULT_VIEWS = []; + +export const BillsPaymentsSampleData = [ + { + 'Payment Date': '2024-03-01', + Vendor: 'Gabriel Kovacek', + 'Payment No.': 'P-10001', + 'Reference No.': 'REF-1', + 'Payment Account': 'Petty Cash', + Statement: 'Vel et dolorem architecto veniam.', + 'Bill No': 'B-120', + 'Payment Amount': 100, + }, + { + 'Payment Date': '2024-03-02', + Vendor: 'Gabriel Kovacek', + 'Payment No.': 'P-10002', + 'Reference No.': 'REF-2', + 'Payment Account': 'Petty Cash', + Statement: 'Id est molestias.', + 'Bill No': 'B-121', + 'Payment Amount': 100, + }, + { + 'Payment Date': '2024-03-03', + Vendor: 'Gabriel Kovacek', + 'Payment No.': 'P-10003', + 'Reference No.': 'REF-3', + 'Payment Account': 'Petty Cash', + Statement: 'Quam cupiditate at nihil dicta dignissimos non fugit illo.', + 'Bill No': 'B-122', + 'Payment Amount': 100, + }, +]; diff --git a/packages/server-nest/src/modules/BillPayments/models/BillPayment.ts b/packages/server-nest/src/modules/BillPayments/models/BillPayment.ts new file mode 100644 index 000000000..dab8d8f16 --- /dev/null +++ b/packages/server-nest/src/modules/BillPayments/models/BillPayment.ts @@ -0,0 +1,176 @@ +import { Model, mixin } from 'objection'; +// import TenantModel from 'models/TenantModel'; +// import ModelSetting from './ModelSetting'; +// import BillPaymentSettings from './BillPayment.Settings'; +// import CustomViewBaseModel from './CustomViewBaseModel'; +// import { DEFAULT_VIEWS } from '@/services/Sales/PaymentReceived/constants'; +// import ModelSearchable from './ModelSearchable'; +import { BaseModel } from '@/models/Model'; + +export class BillPayment extends BaseModel{ + vendorId: number; + amount: number; + currencyCode: string; + paymentAccountId: number; + paymentNumber: string; + paymentDate: string; + paymentMethod: string; + reference: string; + userId: number; + statement: string; + exchangeRate: number; + + createdAt?: Date; + updatedAt?: Date; + + /** + * Table name + */ + static get tableName() { + return 'bills_payments'; + } + + /** + * Timestamps columns. + */ + get timestamps() { + return ['createdAt', 'updatedAt']; + } + + /** + * Virtual attributes. + */ + static get virtualAttributes() { + return ['localAmount']; + } + + /** + * Payment amount in local currency. + * @returns {number} + */ + get localAmount() { + return this.amount * this.exchangeRate; + } + + /** + * Model settings. + */ + // static get meta() { + // return BillPaymentSettings; + // } + + /** + * Relationship mapping. + */ + // static get relationMappings() { + // const BillPaymentEntry = require('models/BillPaymentEntry'); + // const AccountTransaction = require('models/AccountTransaction'); + // const Vendor = require('models/Vendor'); + // const Account = require('models/Account'); + // const Branch = require('models/Branch'); + // const Document = require('models/Document'); + + // return { + // entries: { + // relation: Model.HasManyRelation, + // modelClass: BillPaymentEntry.default, + // join: { + // from: 'bills_payments.id', + // to: 'bills_payments_entries.billPaymentId', + // }, + // filter: (query) => { + // query.orderBy('index', 'ASC'); + // }, + // }, + + // vendor: { + // relation: Model.BelongsToOneRelation, + // modelClass: Vendor.default, + // join: { + // from: 'bills_payments.vendorId', + // to: 'contacts.id', + // }, + // filter(query) { + // query.where('contact_service', 'vendor'); + // }, + // }, + + // paymentAccount: { + // relation: Model.BelongsToOneRelation, + // modelClass: Account.default, + // join: { + // from: 'bills_payments.paymentAccountId', + // to: 'accounts.id', + // }, + // }, + + // transactions: { + // relation: Model.HasManyRelation, + // modelClass: AccountTransaction.default, + // join: { + // from: 'bills_payments.id', + // to: 'accounts_transactions.referenceId', + // }, + // filter(builder) { + // builder.where('reference_type', 'BillPayment'); + // }, + // }, + + // /** + // * Bill payment may belongs to branch. + // */ + // branch: { + // relation: Model.BelongsToOneRelation, + // modelClass: Branch.default, + // join: { + // from: 'bills_payments.branchId', + // to: 'branches.id', + // }, + // }, + + // /** + // * Bill payment may has many attached attachments. + // */ + // attachments: { + // relation: Model.ManyToManyRelation, + // modelClass: Document.default, + // join: { + // from: 'bills_payments.id', + // through: { + // from: 'document_links.modelId', + // to: 'document_links.documentId', + // }, + // to: 'documents.id', + // }, + // filter(query) { + // query.where('model_ref', 'BillPayment'); + // }, + // }, + // }; + // } + + /** + * Retrieve the default custom views, roles and columns. + */ + // static get defaultViews() { + // return DEFAULT_VIEWS; + // } + + /** + * Model search attributes. + */ + static get searchRoles() { + return [ + { fieldKey: 'payment_number', comparator: 'contains' }, + { condition: 'or', fieldKey: 'reference_no', comparator: 'contains' }, + { condition: 'or', fieldKey: 'amount', comparator: 'equals' }, + ]; + } + + /** + * Prevents mutate base currency since the model is not empty. + */ + static get preventMutateBaseCurrency() { + return true; + } +} diff --git a/packages/server-nest/src/modules/BillPayments/models/BillPaymentEntry.ts b/packages/server-nest/src/modules/BillPayments/models/BillPaymentEntry.ts new file mode 100644 index 000000000..f586990a5 --- /dev/null +++ b/packages/server-nest/src/modules/BillPayments/models/BillPaymentEntry.ts @@ -0,0 +1,51 @@ +import { Model } from 'objection'; +// import TenantModel from 'models/TenantModel'; +import { BaseModel } from '@/models/Model'; + +export class BillPaymentEntry extends BaseModel { + public billPaymentId: number; + public billId: number; + public paymentAmount: number; + public index: number; + + /** + * Table name + */ + static get tableName() { + return 'bills_payments_entries'; + } + + /** + * Timestamps columns. + */ + get timestamps() { + return []; + } + + /** + * Relationship mapping. + */ + static get relationMappings() { + const { Bill } = require('../../Bills/models/Bill'); + const { BillPayment } = require('../../BillPayments/models/BillPayment'); + + return { + payment: { + relation: Model.BelongsToOneRelation, + modelClass: BillPayment.default, + join: { + from: 'bills_payments_entries.billPaymentId', + to: 'bills_payments.id', + }, + }, + bill: { + relation: Model.BelongsToOneRelation, + modelClass: Bill.default, + join: { + from: 'bills_payments_entries.billId', + to: 'bills.id', + }, + }, + }; + } +} diff --git a/packages/server-nest/src/modules/BillPayments/queries/BillPaymentEntry.transformer.ts b/packages/server-nest/src/modules/BillPayments/queries/BillPaymentEntry.transformer.ts new file mode 100644 index 000000000..b72a1d982 --- /dev/null +++ b/packages/server-nest/src/modules/BillPayments/queries/BillPaymentEntry.transformer.ts @@ -0,0 +1,27 @@ +import { BillTransformer } from "../../Bills/queries/Bill.transformer"; +import { Transformer } from "../../Transformer/Transformer"; + +export class BillPaymentEntryTransformer extends Transformer{ + /** + * Include these attributes to bill payment object. + * @returns {Array} + */ + public includeAttributes = (): string[] => { + return ['paymentAmountFormatted', 'bill']; + }; + + /** + * Retreives the bill. + */ + protected bill = (entry) => { + return this.item(entry.bill, new BillTransformer()); + }; + + /** + * Retreives the payment amount formatted. + * @returns {string} + */ + protected paymentAmountFormatted(entry) { + return this.formatNumber(entry.paymentAmount, { money: false }); + } +} diff --git a/packages/server-nest/src/modules/BillPayments/queries/BillPaymentTransactionTransformer.ts b/packages/server-nest/src/modules/BillPayments/queries/BillPaymentTransactionTransformer.ts new file mode 100644 index 000000000..02b184d44 --- /dev/null +++ b/packages/server-nest/src/modules/BillPayments/queries/BillPaymentTransactionTransformer.ts @@ -0,0 +1,60 @@ +import { Transformer } from '@/modules/Transformer/Transformer'; + +export class BillPaymentTransactionTransformer extends Transformer { + /** + * Include these attributes to sale credit note object. + * @returns {Array} + */ + public includeAttributes = (): string[] => { + return ['formattedPaymentAmount', 'formattedPaymentDate']; + }; + + /** + * Retrieve formatted bill payment amount. + * @param {ICreditNote} credit + * @returns {string} + */ + protected formattedPaymentAmount = (entry): string => { + return this.formatNumber(entry.paymentAmount, { + currencyCode: entry.payment.currencyCode, + }); + }; + + /** + * Retrieve formatted bill payment date. + * @param entry + * @returns {string} + */ + protected formattedPaymentDate = (entry): string => { + return this.formatDate(entry.payment.paymentDate); + }; + + /** + * + * @param entry + * @returns + */ + public transform = (entry) => { + return { + billId: entry.billId, + billPaymentId: entry.billPaymentId, + + paymentDate: entry.payment.paymentDate, + formattedPaymentDate: entry.formattedPaymentDate, + + paymentAmount: entry.paymentAmount, + formattedPaymentAmount: entry.formattedPaymentAmount, + currencyCode: entry.payment.currencyCode, + + paymentNumber: entry.payment.paymentNumber, + paymentReferenceNo: entry.payment.reference, + + billNumber: entry.bill.billNumber, + billReferenceNo: entry.bill.referenceNo, + + paymentAccountId: entry.payment.paymentAccountId, + paymentAccountName: entry.payment.paymentAccount.name, + paymentAccountSlug: entry.payment.paymentAccount.slug, + }; + }; +} diff --git a/packages/server-nest/src/modules/BillPayments/queries/BillPaymentTransformer.ts b/packages/server-nest/src/modules/BillPayments/queries/BillPaymentTransformer.ts new file mode 100644 index 000000000..2558327c7 --- /dev/null +++ b/packages/server-nest/src/modules/BillPayments/queries/BillPaymentTransformer.ts @@ -0,0 +1,65 @@ +import { BillPaymentEntryTransformer } from './BillPaymentEntry.transformer'; +import { Transformer } from '@/modules/Transformer/Transformer'; +import { BillPayment } from '../models/BillPayment'; +import { AttachmentTransformer } from '@/modules/Attachments/Attachment.transformer'; + +export class BillPaymentTransformer extends Transformer { + /** + * Include these attributes to sale invoice object. + * @returns {Array} + */ + public includeAttributes = (): string[] => { + return [ + 'formattedPaymentDate', + 'formattedCreatedAt', + 'formattedAmount', + 'entries', + 'attachments', + ]; + }; + + /** + * Retrieve formatted invoice date. + * @param {IBill} invoice + * @returns {String} + */ + protected formattedPaymentDate = (billPayment: BillPayment): string => { + return this.formatDate(billPayment.paymentDate); + }; + + /** + * Retrieve formatted created at date. + * @param {IBillPayment} billPayment + * @returns {string} + */ + protected formattedCreatedAt = (billPayment: BillPayment): string => { + return this.formatDate(billPayment.createdAt); + }; + + /** + * Retrieve formatted bill amount. + * @param {IBill} invoice + * @returns {string} + */ + protected formattedAmount = (billPayment: BillPayment): string => { + return this.formatNumber(billPayment.amount, { + currencyCode: billPayment.currencyCode, + }); + }; + + /** + * Retreives the bill payment entries. + */ + protected entries = (billPayment: BillPayment) => { + return this.item(billPayment.entries, new BillPaymentEntryTransformer()); + }; + + /** + * Retrieves the bill attachments. + * @param {ISaleInvoice} invoice + * @returns + */ + protected attachments = (billPayment: BillPayment) => { + return this.item(billPayment.attachments, new AttachmentTransformer()); + }; +} diff --git a/packages/server-nest/src/modules/BillPayments/queries/GetBillPayment.service.ts b/packages/server-nest/src/modules/BillPayments/queries/GetBillPayment.service.ts new file mode 100644 index 000000000..060f846df --- /dev/null +++ b/packages/server-nest/src/modules/BillPayments/queries/GetBillPayment.service.ts @@ -0,0 +1,37 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { TransformerInjectable } from '../../Transformer/TransformerInjectable.service'; +import { BillPayment } from '../models/BillPayment'; +import { BillPaymentTransformer } from './BillPaymentTransformer'; + +@Injectable() +export class GetBillPayment { + constructor( + private readonly transformer: TransformerInjectable, + + @Inject(BillPayment.name) + private readonly billPaymentModel: typeof BillPayment, + ) {} + + /** + * Retrieves bill payment. + * @param {number} billPyamentId + * @return {Promise} + */ + public async getBillPayment(billPyamentId: number): Promise { + const billPayment = await this.billPaymentModel + .query() + .withGraphFetched('entries.bill') + .withGraphFetched('vendor') + .withGraphFetched('paymentAccount') + .withGraphFetched('transactions') + .withGraphFetched('branch') + .withGraphFetched('attachments') + .findById(billPyamentId) + .throwIfNotFound(); + + return this.transformer.transform( + billPayment, + new BillPaymentTransformer(), + ); + } +} diff --git a/packages/server-nest/src/modules/BillPayments/queries/GetPaymentBills.service.ts b/packages/server-nest/src/modules/BillPayments/queries/GetPaymentBills.service.ts new file mode 100644 index 000000000..edc36b476 --- /dev/null +++ b/packages/server-nest/src/modules/BillPayments/queries/GetPaymentBills.service.ts @@ -0,0 +1,35 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Bill } from '@/modules/Bills/models/Bill'; +import { BillPayment } from '../models/BillPayment'; + +@Injectable() +export class GetPaymentBills { + /** + * @param {typeof Bill} billModel + * @param {typeof BillPayment} billPaymentModel + */ + constructor( + @Inject(Bill.name) + private readonly billModel: typeof Bill, + + @Inject(BillPayment.name) + private readonly billPaymentModel: typeof BillPayment, + ) {} + + /** + * Retrieve payment made associated bills. + * @param {number} billPaymentId - Bill payment id. + */ + public async getPaymentBills(billPaymentId: number) { + const billPayment = await this.billPaymentModel + .query() + .findById(billPaymentId) + .throwIfNotFound(); + + const paymentBillsIds = billPayment.entries.map((entry) => entry.id); + + const bills = await this.billModel.query().whereIn('id', paymentBillsIds); + + return bills; + } +} diff --git a/packages/server-nest/src/modules/BillPayments/types/BillPayments.types.ts b/packages/server-nest/src/modules/BillPayments/types/BillPayments.types.ts new file mode 100644 index 000000000..6b706a647 --- /dev/null +++ b/packages/server-nest/src/modules/BillPayments/types/BillPayments.types.ts @@ -0,0 +1,121 @@ +import { Knex } from 'knex'; +import { IBill } from './Bill'; +import { AttachmentLinkDTO } from './Attachments'; +import { BillPayment } from '../models/BillPayment'; + +export interface IBillPaymentEntry { + id?: number; + billPaymentId: number; + billId: number; + paymentAmount: number; + + bill?: IBill; +} + +export interface IBillPayment { + id?: number; + vendorId: number; + amount: number; + currencyCode: string; + reference: string; + paymentAccountId: number; + paymentNumber: string; + paymentDate: Date; + exchangeRate: number | null; + userId: number; + entries: IBillPaymentEntry[]; + statement: string; + createdAt: Date; + updatedAt: Date; + + localAmount?: number; + branchId?: number; +} + +export interface IBillPaymentEntryDTO { + billId: number; + paymentAmount: number; +} + +export interface IBillPaymentDTO { + vendorId: number; + paymentAccountId: number; + paymentNumber?: string; + paymentDate: Date; + exchangeRate?: number; + statement: string; + reference: string; + entries: IBillPaymentEntryDTO[]; + branchId?: number; + attachments?: AttachmentLinkDTO[]; +} + +export interface IBillReceivePageEntry { + billId: number; + entryType: string; + billNo: string; + dueAmount: number; + amount: number; + totalPaymentAmount: number; + paymentAmount: number; + currencyCode: string; + date: Date | string; +} + +export interface IBillPaymentsService { + validateVendorHasNoPayments(tenantId: number, vendorId): Promise; +} + +export interface IBillPaymentEventCreatedPayload { + // tenantId: number; + billPayment: BillPayment; + billPaymentDTO: IBillPaymentDTO; + billPaymentId: number; + trx: Knex.Transaction; +} + +export interface IBillPaymentCreatingPayload { + // tenantId: number; + billPaymentDTO: IBillPaymentDTO; + trx: Knex.Transaction; +} + +export interface IBillPaymentEditingPayload { + // tenantId: number; + billPaymentDTO: IBillPaymentDTO; + oldBillPayment: BillPayment; + trx: Knex.Transaction; +} +export interface IBillPaymentEventEditedPayload { + // tenantId: number; + billPaymentId: number; + billPayment: BillPayment; + oldBillPayment: BillPayment; + billPaymentDTO: IBillPaymentDTO; + trx: Knex.Transaction; +} + +export interface IBillPaymentEventDeletedPayload { + // tenantId: number; + billPaymentId: number; + oldBillPayment: BillPayment; + trx: Knex.Transaction; +} + +export interface IBillPaymentDeletingPayload { + oldBillPayment: BillPayment; + trx: Knex.Transaction; +} + +export interface IBillPaymentPublishingPayload { + // tenantId: number; + oldBillPayment: BillPayment; + trx: Knex.Transaction; +} + +export enum IPaymentMadeAction { + Create = 'Create', + Edit = 'Edit', + Delete = 'Delete', + View = 'View', +} diff --git a/packages/server-nest/src/modules/Bills/Bills.application.ts b/packages/server-nest/src/modules/Bills/Bills.application.ts new file mode 100644 index 000000000..eb9693161 --- /dev/null +++ b/packages/server-nest/src/modules/Bills/Bills.application.ts @@ -0,0 +1,105 @@ + +import { CreateBill } from './commands/CreateBill.service'; +import { EditBillService } from './commands/EditBill.service'; +import { GetBill } from './queries/GetBill'; +// import { GetBills } from './queries/GetBills'; +import { DeleteBill } from './commands/DeleteBill.service'; +import { + IBillDTO, + IBillEditDTO, +} from './Bills.types'; +import { GetDueBills } from './queries/GetDueBills.service'; +import { OpenBillService } from './commands/OpenBill.service'; +import { GetBillPayments } from './queries/GetBillPayments'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class BillsApplication { + constructor( + private createBillService: CreateBill, + private editBillService: EditBillService, + private getBillService: GetBill, + // private getBillsService: GetBills, + private deleteBillService: DeleteBill, + private getDueBillsService: GetDueBills, + private openBillService: OpenBillService, + private getBillPaymentsService: GetBillPayments, + ) {} + + /** + * Creates a new bill with associated GL entries. + * @param {IBillDTO} billDTO + * @returns + */ + public createBill(billDTO: IBillDTO) { + return this.createBillService.createBill(billDTO); + } + + /** + * Edits the given bill with associated GL entries. + * @param {number} billId + * @param {IBillEditDTO} billDTO + * @returns + */ + public editBill(billId: number, billDTO: IBillEditDTO) { + return this.editBillService.editBill(billId, billDTO); + } + + /** + * Deletes the given bill with associated GL entries. + * @param {number} billId - Bill id. + * @returns {Promise} + */ + public deleteBill(billId: number) { + return this.deleteBillService.deleteBill(billId); + } + + /** + * Retrieve bills data table list. + * @param {number} tenantId - + * @param {IBillsFilter} billsFilter - + */ + // public getBills( + // filterDTO: IBillsFilter, + // ) { + // return this.getBillsService.getBills(filterDTO); + // } + + /** + * Retrieves the given bill details. + * @param {number} billId + * @returns + */ + public getBill(billId: number) { + return this.getBillService.getBill(billId); + } + + /** + * Open the given bill. + * @param {number} tenantId + * @param {number} billId + * @returns {Promise} + */ + public openBill(billId: number): Promise { + return this.openBillService.openBill(billId); + } + + /** + * Retrieves due bills of the given vendor. + * @param {number} tenantId + * @param {number} vendorId + * @returns + */ + public getDueBills(vendorId?: number) { + return this.getDueBillsService.getDueBills(vendorId); + } + + /** + * Retrieve the specific bill associated payment transactions. + * @param {number} tenantId + * @param {number} billId + */ + public getBillPayments(billId: number) { + return this.getBillPaymentsService.getBillPayments(billId); + } +} diff --git a/packages/server-nest/src/modules/Bills/Bills.constants.ts b/packages/server-nest/src/modules/Bills/Bills.constants.ts new file mode 100644 index 000000000..fe61c5857 --- /dev/null +++ b/packages/server-nest/src/modules/Bills/Bills.constants.ts @@ -0,0 +1,123 @@ +export const ERRORS = { + BILL_NOT_FOUND: 'BILL_NOT_FOUND', + BILL_VENDOR_NOT_FOUND: 'BILL_VENDOR_NOT_FOUND', + BILL_ITEMS_NOT_PURCHASABLE: 'BILL_ITEMS_NOT_PURCHASABLE', + BILL_NUMBER_EXISTS: 'BILL_NUMBER_EXISTS', + BILL_ITEMS_NOT_FOUND: 'BILL_ITEMS_NOT_FOUND', + BILL_ENTRIES_IDS_NOT_FOUND: 'BILL_ENTRIES_IDS_NOT_FOUND', + NOT_PURCHASE_ABLE_ITEMS: 'NOT_PURCHASE_ABLE_ITEMS', + BILL_ALREADY_OPEN: 'BILL_ALREADY_OPEN', + BILL_NO_IS_REQUIRED: 'BILL_NO_IS_REQUIRED', + BILL_HAS_ASSOCIATED_PAYMENT_ENTRIES: 'BILL_HAS_ASSOCIATED_PAYMENT_ENTRIES', + VENDOR_HAS_BILLS: 'VENDOR_HAS_BILLS', + BILL_HAS_ASSOCIATED_LANDED_COSTS: 'BILL_HAS_ASSOCIATED_LANDED_COSTS', + BILL_ENTRIES_ALLOCATED_COST_COULD_DELETED: + 'BILL_ENTRIES_ALLOCATED_COST_COULD_DELETED', + LOCATED_COST_ENTRIES_SHOULD_BIGGE_THAN_NEW_ENTRIES: + 'LOCATED_COST_ENTRIES_SHOULD_BIGGE_THAN_NEW_ENTRIES', + LANDED_COST_ENTRIES_SHOULD_BE_INVENTORY_ITEMS: + 'LANDED_COST_ENTRIES_SHOULD_BE_INVENTORY_ITEMS', + BILL_HAS_APPLIED_TO_VENDOR_CREDIT: 'BILL_HAS_APPLIED_TO_VENDOR_CREDIT', + BILL_AMOUNT_SMALLER_THAN_PAID_AMOUNT: 'BILL_AMOUNT_SMALLER_THAN_PAID_AMOUNT', +}; + +export const DEFAULT_VIEW_COLUMNS = []; + +export const DEFAULT_VIEWS = [ + { + name: 'Draft', + slug: 'draft', + rolesLogicExpression: '1', + roles: [ + { index: 1, fieldKey: 'status', comparator: 'equals', value: 'draft' }, + ], + columns: DEFAULT_VIEW_COLUMNS, + }, + { + name: 'Opened', + slug: 'opened', + rolesLogicExpression: '1', + roles: [ + { index: 1, fieldKey: 'status', comparator: 'equals', value: 'opened' }, + ], + columns: DEFAULT_VIEW_COLUMNS, + }, + { + name: 'Unpaid', + slug: 'unpaid', + rolesLogicExpression: '1', + roles: [ + { index: 1, fieldKey: 'status', comparator: 'equals', value: 'unpaid' }, + ], + columns: DEFAULT_VIEW_COLUMNS, + }, + { + name: 'Overdue', + slug: 'overdue', + rolesLogicExpression: '1', + roles: [ + { index: 1, fieldKey: 'status', comparator: 'equals', value: 'overdue' }, + ], + columns: DEFAULT_VIEW_COLUMNS, + }, + { + name: 'Partially paid', + slug: 'partially-paid', + rolesLogicExpression: '1', + roles: [ + { + index: 1, + fieldKey: 'status', + comparator: 'equals', + value: 'partially-paid', + }, + ], + columns: DEFAULT_VIEW_COLUMNS, + }, +]; + +export const BillsSampleData = [ + { + 'Bill No.': 'B-101', + 'Reference No.': 'REF0', + Date: '2024-01-01', + 'Due Date': '2024-03-01', + Vendor: 'Gabriel Kovacek', + 'Exchange Rate': 1, + Note: 'Vel in sit sint.', + Open: 'T', + Item: 'VonRueden, Ruecker and Hettinger', + Quantity: 100, + Rate: 100, + 'Line Description': 'Id a vel quis vel aut.', + }, + { + 'Bill No.': 'B-102', + 'Reference No.': 'REF0', + Date: '2024-01-01', + 'Due Date': '2024-03-01', + Vendor: 'Gabriel Kovacek', + 'Exchange Rate': 1, + Note: 'Quia ut dolorem qui sint velit.', + Open: 'T', + Item: 'Thompson - Reichert', + Quantity: 200, + Rate: 50, + 'Line Description': + 'Nesciunt in adipisci quia ab reiciendis nam sed saepe consequatur.', + }, + { + 'Bill No.': 'B-103', + 'Reference No.': 'REF0', + Date: '2024-01-01', + 'Due Date': '2024-03-01', + Vendor: 'Gabriel Kovacek', + 'Exchange Rate': 1, + Note: 'Dolore aut voluptatem minus pariatur alias pariatur.', + Open: 'T', + Item: 'VonRueden, Ruecker and Hettinger', + Quantity: 100, + Rate: 100, + 'Line Description': 'Quam eligendi provident.', + }, +]; diff --git a/packages/server-nest/src/modules/Bills/Bills.module.ts b/packages/server-nest/src/modules/Bills/Bills.module.ts new file mode 100644 index 000000000..1583d40ab --- /dev/null +++ b/packages/server-nest/src/modules/Bills/Bills.module.ts @@ -0,0 +1,17 @@ +import { Module } from '@nestjs/common'; +import { BillsApplication } from './Bills.application'; +import { CreateBill } from './commands/CreateBill.service'; +import { DeleteBill } from './commands/DeleteBill.service'; +import { GetBill } from './queries/GetBill'; +import { BillDTOTransformer } from './commands/BillDTOTransformer.service'; + +@Module({ + providers: [ + BillsApplication, + CreateBill, + GetBill, + DeleteBill, + BillDTOTransformer, + ], +}) +export class BillsModule {} diff --git a/packages/server-nest/src/modules/Bills/Bills.types.ts b/packages/server-nest/src/modules/Bills/Bills.types.ts new file mode 100644 index 000000000..522976bac --- /dev/null +++ b/packages/server-nest/src/modules/Bills/Bills.types.ts @@ -0,0 +1,112 @@ +import { Knex } from 'knex'; +import { IItemEntryDTO } from '../TransactionItemEntry/ItemEntry.types'; +import { AttachmentLinkDTO } from '../Attachments/Attachments.types'; +import { Bill } from './models/Bill'; + +export interface IBillDTO { + vendorId: number; + billNumber: string; + billDate: Date; + dueDate: Date; + referenceNo: string; + status: string; + note: string; + amount: number; + paymentAmount: number; + exchangeRate?: number; + open: boolean; + entries: IItemEntryDTO[]; + branchId?: number; + warehouseId?: number; + projectId?: number; + isInclusiveTax?: boolean; + attachments?: AttachmentLinkDTO[]; +} + +export interface IBillEditDTO { + vendorId: number; + billNumber: string; + billDate: Date; + dueDate: Date; + referenceNo: string; + status: string; + note: string; + amount: number; + paymentAmount: number; + open: boolean; + entries: IItemEntryDTO[]; + + branchId?: number; + warehouseId?: number; + projectId?: number; + attachments?: AttachmentLinkDTO[]; +} + +// export interface IBillsFilter extends IDynamicListFilterDTO { +// stringifiedFilterRoles?: string; +// page: number; +// pageSize: number; +// filterQuery?: (q: any) => void; +// } + +export interface IBillCreatedPayload { + // tenantId: number; + bill: Bill; + billDTO: IBillDTO; + billId: number; + trx?: Knex.Transaction; +} + +export interface IBillCreatingPayload { + // tenantId: number; + billDTO: IBillDTO; + trx: Knex.Transaction; +} + +export interface IBillEditingPayload { + // tenantId: number; + oldBill: Bill; + billDTO: IBillEditDTO; + trx: Knex.Transaction; +} +export interface IBillEditedPayload { + // tenantId: number; + // billId: number; + oldBill: Bill; + bill: Bill; + billDTO: IBillDTO; + trx?: Knex.Transaction; +} + +export interface IBIllEventDeletedPayload { + // tenantId: number; + billId: number; + oldBill: Bill; + trx: Knex.Transaction; +} + +export interface IBillEventDeletingPayload { + // tenantId: number; + oldBill: Bill; + trx: Knex.Transaction; +} +export enum BillAction { + Create = 'Create', + Edit = 'Edit', + Delete = 'Delete', + View = 'View', + NotifyBySms = 'NotifyBySms', +} + +export interface IBillOpeningPayload { + trx: Knex.Transaction; + // tenantId: number; + oldBill: Bill; +} + +export interface IBillOpenedPayload { + bill: Bill; + oldBill: Bill; + trx?: Knex.Transaction; + // tenantId: number; +} diff --git a/packages/server-nest/src/modules/Bills/commands/BillDTOTransformer.service.ts b/packages/server-nest/src/modules/Bills/commands/BillDTOTransformer.service.ts new file mode 100644 index 000000000..fb04a9b53 --- /dev/null +++ b/packages/server-nest/src/modules/Bills/commands/BillDTOTransformer.service.ts @@ -0,0 +1,140 @@ +import { omit, sumBy } from 'lodash'; +import moment from 'moment'; +import { Inject, Injectable } from '@nestjs/common'; +import * as R from 'ramda'; +import { formatDateFields } from '@/utils/format-date-fields'; +import composeAsync from 'async/compose'; +import { BranchTransactionDTOTransformer } from '@/modules/Branches/integrations/BranchTransactionDTOTransform'; +import { WarehouseTransactionDTOTransform } from '@/modules/Warehouses/Integrations/WarehouseTransactionDTOTransform'; +import { ItemEntry } from '@/modules/Items/models/ItemEntry'; +import { Item } from '@/modules/Items/models/Item'; +import { ItemEntriesTaxTransactions } from '@/modules/TaxRates/ItemEntriesTaxTransactions.service'; +import { IBillDTO } from '../Bills.types'; +import { Vendor } from '@/modules/Vendors/models/Vendor'; +import { Bill } from '../models/Bill'; +import { assocItemEntriesDefaultIndex } from '@/utils/associate-item-entries-index'; +import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service'; + +@Injectable() +export class BillDTOTransformer { + constructor( + private branchDTOTransform: BranchTransactionDTOTransformer, + private warehouseDTOTransform: WarehouseTransactionDTOTransform, + private taxDTOTransformer: ItemEntriesTaxTransactions, + private tenancyContext: TenancyContext, + + @Inject(ItemEntry) private itemEntryModel: typeof ItemEntry, + @Inject(Item) private itemModel: typeof Item, + ) {} + + /** + * Retrieve the bill entries total. + * @param {IItemEntry[]} entries + * @returns {number} + */ + private getBillEntriesTotal(entries: ItemEntry[]): number { + return sumBy(entries, (e) => this.itemEntryModel.calcAmount(e)); + } + + /** + * Retrieve the bill landed cost amount. + * @param {IBillDTO} billDTO + * @returns {number} + */ + private getBillLandedCostAmount(billDTO: IBillDTO): number { + const costEntries = billDTO.entries.filter((entry) => entry.landedCost); + + return this.getBillEntriesTotal(costEntries); + } + + /** + * Converts create bill DTO to model. + * @param {IBillDTO} billDTO + * @param {IBill} oldBill + * @returns {IBill} + */ + public async billDTOToModel( + billDTO: IBillDTO, + vendor: Vendor, + oldBill?: Bill, + ) { + const amount = sumBy(billDTO.entries, (e) => + this.itemEntryModel.calcAmount(e), + ); + // Retrieve the landed cost amount from landed cost entries. + const landedCostAmount = this.getBillLandedCostAmount(billDTO); + + // Retrieve the authorized user. + const authorizedUser = await this.tenancyContext.getSystemUser(); + + // Bill number from DTO or frprom auto-increment. + const billNumber = billDTO.billNumber || oldBill?.billNumber; + + const initialEntries = billDTO.entries.map((entry) => ({ + referenceType: 'Bill', + isInclusiveTax: billDTO.isInclusiveTax, + ...omit(entry, ['amount']), + })); + const asyncEntries = await composeAsync( + // Associate tax rate from tax id to entries. + this.taxDTOTransformer.assocTaxRateFromTaxIdToEntries, + // Associate tax rate id from tax code to entries. + this.taxDTOTransformer.assocTaxRateIdFromCodeToEntries, + // Sets the default cost account to the bill entries. + this.setBillEntriesDefaultAccounts(), + )(initialEntries); + + const entries = R.compose( + // Remove tax code from entries. + R.map(R.omit(['taxCode'])), + // Associate the default index to each item entry line. + assocItemEntriesDefaultIndex, + )(asyncEntries); + + const initialDTO = { + ...formatDateFields(omit(billDTO, ['open', 'entries', 'attachments']), [ + 'billDate', + 'dueDate', + ]), + amount, + landedCostAmount, + currencyCode: vendor.currencyCode, + exchangeRate: billDTO.exchangeRate || 1, + billNumber, + entries, + // Avoid rewrite the open date in edit mode when already opened. + ...(billDTO.open && + !oldBill?.openedAt && { + openedAt: moment().toMySqlDateTime(), + }), + userId: authorizedUser.id, + }; + return R.compose( + // Associates tax amount withheld to the model. + this.taxDTOTransformer.assocTaxAmountWithheldFromEntries, + this.branchDTOTransform.transformDTO, + this.warehouseDTOTransform.transformDTO, + )(initialDTO); + } + + /** + * Sets the default cost account to the bill entries. + */ + private setBillEntriesDefaultAccounts() { + return async (entries: ItemEntry[]) => { + const entriesItemsIds = entries.map((e) => e.itemId); + const items = await this.itemModel.query().whereIn('id', entriesItemsIds); + + return entries.map((entry) => { + const item = items.find((i) => i.id === entry.itemId); + + return { + ...entry, + ...(item.type !== 'inventory' && { + costAccountId: entry.costAccountId || item.costAccountId, + }), + }; + }); + }; + } +} diff --git a/packages/server-nest/src/modules/Bills/commands/BillGLEntries.ts b/packages/server-nest/src/modules/Bills/commands/BillGLEntries.ts new file mode 100644 index 000000000..2a20814ad --- /dev/null +++ b/packages/server-nest/src/modules/Bills/commands/BillGLEntries.ts @@ -0,0 +1,288 @@ +// import moment from 'moment'; +// import { sumBy } from 'lodash'; +// import { Knex } from 'knex'; +// import { Inject, Service } from 'typedi'; +// import * as R from 'ramda'; +// import { AccountNormal, IBill, IItemEntry, ILedgerEntry } from '@/interfaces'; +// import HasTenancyService from '@/services/Tenancy/TenancyService'; +// import Ledger from '@/services/Accounting/Ledger'; +// import LedgerStorageService from '@/services/Accounting/LedgerStorageService'; +// import ItemsEntriesService from '@/services/Items/ItemsEntriesService'; + +// @Service() +// export class BillGLEntries { +// @Inject() +// private tenancy: HasTenancyService; + +// @Inject() +// private ledgerStorage: LedgerStorageService; + +// @Inject() +// private itemsEntriesService: ItemsEntriesService; + +// /** +// * Creates bill GL entries. +// * @param {number} tenantId - +// * @param {number} billId - +// * @param {Knex.Transaction} trx - +// */ +// public writeBillGLEntries = async ( +// tenantId: number, +// billId: number, +// trx?: Knex.Transaction +// ) => { +// const { accountRepository } = this.tenancy.repositories(tenantId); +// const { Bill } = this.tenancy.models(tenantId); + +// // Retrieves bill with associated entries and landed costs. +// const bill = await Bill.query(trx) +// .findById(billId) +// .withGraphFetched('entries.item') +// .withGraphFetched('entries.allocatedCostEntries') +// .withGraphFetched('locatedLandedCosts.allocateEntries'); + +// // Finds or create a A/P account based on the given currency. +// const APAccount = await accountRepository.findOrCreateAccountsPayable( +// bill.currencyCode, +// {}, +// trx +// ); +// // Find or create tax payable account. +// const taxPayableAccount = await accountRepository.findOrCreateTaxPayable( +// {}, +// trx +// ); +// const billLedger = this.getBillLedger( +// bill, +// APAccount.id, +// taxPayableAccount.id +// ); +// // Commit the GL enties on the storage. +// await this.ledgerStorage.commit(tenantId, billLedger, trx); +// }; + +// /** +// * Reverts the given bill GL entries. +// * @param {number} tenantId +// * @param {number} billId +// * @param {Knex.Transaction} trx +// */ +// public revertBillGLEntries = async ( +// tenantId: number, +// billId: number, +// trx?: Knex.Transaction +// ) => { +// await this.ledgerStorage.deleteByReference(tenantId, billId, 'Bill', trx); +// }; + +// /** +// * Rewrites the given bill GL entries. +// * @param {number} tenantId +// * @param {number} billId +// * @param {Knex.Transaction} trx +// */ +// public rewriteBillGLEntries = async ( +// tenantId: number, +// billId: number, +// trx?: Knex.Transaction +// ) => { +// // Reverts the bill GL entries. +// await this.revertBillGLEntries(tenantId, billId, trx); + +// // Writes the bill GL entries. +// await this.writeBillGLEntries(tenantId, billId, trx); +// }; + +// /** +// * Retrieves the bill common entry. +// * @param {IBill} bill +// * @returns {ILedgerEntry} +// */ +// private getBillCommonEntry = (bill: IBill) => { +// return { +// debit: 0, +// credit: 0, +// currencyCode: bill.currencyCode, +// exchangeRate: bill.exchangeRate || 1, + +// transactionId: bill.id, +// transactionType: 'Bill', + +// date: moment(bill.billDate).format('YYYY-MM-DD'), +// userId: bill.userId, + +// referenceNumber: bill.referenceNo, +// transactionNumber: bill.billNumber, + +// branchId: bill.branchId, +// projectId: bill.projectId, + +// createdAt: bill.createdAt, +// }; +// }; + +// /** +// * Retrieves the bill item inventory/cost entry. +// * @param {IBill} bill - +// * @param {IItemEntry} entry - +// * @param {number} index - +// */ +// private getBillItemEntry = R.curry( +// (bill: IBill, entry: IItemEntry, index: number): ILedgerEntry => { +// const commonJournalMeta = this.getBillCommonEntry(bill); + +// const localAmount = bill.exchangeRate * entry.amountExludingTax; +// const landedCostAmount = sumBy(entry.allocatedCostEntries, 'cost'); + +// return { +// ...commonJournalMeta, +// debit: localAmount + landedCostAmount, +// accountId: +// ['inventory'].indexOf(entry.item.type) !== -1 +// ? entry.item.inventoryAccountId +// : entry.costAccountId, +// index: index + 1, +// indexGroup: 10, +// itemId: entry.itemId, +// itemQuantity: entry.quantity, +// accountNormal: AccountNormal.DEBIT, +// }; +// } +// ); + +// /** +// * Retrieves the bill landed cost entry. +// * @param {IBill} bill - +// * @param {} landedCost - +// * @param {number} index - +// */ +// private getBillLandedCostEntry = R.curry( +// (bill: IBill, landedCost, index: number): ILedgerEntry => { +// const commonJournalMeta = this.getBillCommonEntry(bill); + +// return { +// ...commonJournalMeta, +// credit: landedCost.amount, +// accountId: landedCost.costAccountId, +// accountNormal: AccountNormal.DEBIT, +// index: 1, +// indexGroup: 20, +// }; +// } +// ); + +// /** +// * Retrieves the bill payable entry. +// * @param {number} payableAccountId +// * @param {IBill} bill +// * @returns {ILedgerEntry} +// */ +// private getBillPayableEntry = ( +// payableAccountId: number, +// bill: IBill +// ): ILedgerEntry => { +// const commonJournalMeta = this.getBillCommonEntry(bill); + +// return { +// ...commonJournalMeta, +// credit: bill.totalLocal, +// accountId: payableAccountId, +// contactId: bill.vendorId, +// accountNormal: AccountNormal.CREDIT, +// index: 1, +// indexGroup: 5, +// }; +// }; + +// /** +// * Retrieves the bill tax GL entry. +// * @param {IBill} bill - +// * @param {number} taxPayableAccountId - +// * @param {IItemEntry} entry - +// * @param {number} index - +// * @returns {ILedgerEntry} +// */ +// private getBillTaxEntry = R.curry( +// ( +// bill: IBill, +// taxPayableAccountId: number, +// entry: IItemEntry, +// index: number +// ): ILedgerEntry => { +// const commonJournalMeta = this.getBillCommonEntry(bill); + +// return { +// ...commonJournalMeta, +// debit: entry.taxAmount, +// index, +// indexGroup: 30, +// accountId: taxPayableAccountId, +// accountNormal: AccountNormal.CREDIT, +// taxRateId: entry.taxRateId, +// taxRate: entry.taxRate, +// }; +// } +// ); + +// /** +// * Retrieves the bill tax GL entries. +// * @param {IBill} bill +// * @param {number} taxPayableAccountId +// * @returns {ILedgerEntry[]} +// */ +// private getBillTaxEntries = (bill: IBill, taxPayableAccountId: number) => { +// // Retrieves the non-zero tax entries. +// const nonZeroTaxEntries = this.itemsEntriesService.getNonZeroEntries( +// bill.entries +// ); +// const transformTaxEntry = this.getBillTaxEntry(bill, taxPayableAccountId); + +// return nonZeroTaxEntries.map(transformTaxEntry); +// }; + +// /** +// * Retrieves the given bill GL entries. +// * @param {IBill} bill +// * @param {number} payableAccountId +// * @returns {ILedgerEntry[]} +// */ +// private getBillGLEntries = ( +// bill: IBill, +// payableAccountId: number, +// taxPayableAccountId: number +// ): ILedgerEntry[] => { +// const payableEntry = this.getBillPayableEntry(payableAccountId, bill); + +// const itemEntryTransformer = this.getBillItemEntry(bill); +// const landedCostTransformer = this.getBillLandedCostEntry(bill); + +// const itemsEntries = bill.entries.map(itemEntryTransformer); +// const landedCostEntries = bill.locatedLandedCosts.map( +// landedCostTransformer +// ); +// const taxEntries = this.getBillTaxEntries(bill, taxPayableAccountId); + +// // Allocate cost entries journal entries. +// return [payableEntry, ...itemsEntries, ...landedCostEntries, ...taxEntries]; +// }; + +// /** +// * Retrieves the given bill ledger. +// * @param {IBill} bill +// * @param {number} payableAccountId +// * @returns {Ledger} +// */ +// private getBillLedger = ( +// bill: IBill, +// payableAccountId: number, +// taxPayableAccountId: number +// ) => { +// const entries = this.getBillGLEntries( +// bill, +// payableAccountId, +// taxPayableAccountId +// ); + +// return new Ledger(entries); +// }; +// } diff --git a/packages/server-nest/src/modules/Bills/commands/BillGLEntriesSubscriber.ts b/packages/server-nest/src/modules/Bills/commands/BillGLEntriesSubscriber.ts new file mode 100644 index 000000000..504684565 --- /dev/null +++ b/packages/server-nest/src/modules/Bills/commands/BillGLEntriesSubscriber.ts @@ -0,0 +1,75 @@ +// import { Inject, Service } from 'typedi'; +// import events from '@/subscribers/events'; +// import { +// IBillCreatedPayload, +// IBillEditedPayload, +// IBIllEventDeletedPayload, +// IBillOpenedPayload, +// } from '@/interfaces'; +// import { BillGLEntries } from './BillGLEntries'; + +// @Service() +// export class BillGLEntriesSubscriber { +// @Inject() +// private billGLEntries: BillGLEntries; + +// /** +// * Attaches events with handles. +// */ +// public attach(bus) { +// bus.subscribe( +// events.bill.onCreated, +// this.handlerWriteJournalEntriesOnCreate +// ); +// bus.subscribe( +// events.bill.onOpened, +// this.handlerWriteJournalEntriesOnCreate +// ); +// bus.subscribe( +// events.bill.onEdited, +// this.handleOverwriteJournalEntriesOnEdit +// ); +// bus.subscribe(events.bill.onDeleted, this.handlerDeleteJournalEntries); +// } + +// /** +// * Handles writing journal entries once bill created. +// * @param {IBillCreatedPayload} payload - +// */ +// private handlerWriteJournalEntriesOnCreate = async ({ +// tenantId, +// bill, +// trx, +// }: IBillCreatedPayload | IBillOpenedPayload) => { +// if (!bill.openedAt) return null; + +// await this.billGLEntries.writeBillGLEntries(tenantId, bill.id, trx); +// }; + +// /** +// * Handles the overwriting journal entries once bill edited. +// * @param {IBillEditedPayload} payload - +// */ +// private handleOverwriteJournalEntriesOnEdit = async ({ +// tenantId, +// billId, +// bill, +// trx, +// }: IBillEditedPayload) => { +// if (!bill.openedAt) return null; + +// await this.billGLEntries.rewriteBillGLEntries(tenantId, billId, trx); +// }; + +// /** +// * Handles revert journal entries on bill deleted. +// * @param {IBIllEventDeletedPayload} payload - +// */ +// private handlerDeleteJournalEntries = async ({ +// tenantId, +// billId, +// trx, +// }: IBIllEventDeletedPayload) => { +// await this.billGLEntries.revertBillGLEntries(tenantId, billId, trx); +// }; +// } diff --git a/packages/server-nest/src/modules/Bills/commands/BillInventoryTransactions.ts b/packages/server-nest/src/modules/Bills/commands/BillInventoryTransactions.ts new file mode 100644 index 000000000..e0d8df399 --- /dev/null +++ b/packages/server-nest/src/modules/Bills/commands/BillInventoryTransactions.ts @@ -0,0 +1,82 @@ +// import { Knex } from 'knex'; +// import { Inject, Service } from 'typedi'; +// import InventoryService from '@/services/Inventory/Inventory'; +// import ItemsEntriesService from '@/services/Items/ItemsEntriesService'; +// import HasTenancyService from '@/services/Tenancy/TenancyService'; + +// @Service() +// export class BillInventoryTransactions { +// @Inject() +// private tenancy: HasTenancyService; + +// @Inject() +// private itemsEntriesService: ItemsEntriesService; + +// @Inject() +// private inventoryService: InventoryService; + +// /** +// * Records the inventory transactions from the given bill input. +// * @param {Bill} bill - Bill model object. +// * @param {number} billId - Bill id. +// * @return {Promise} +// */ +// public async recordInventoryTransactions( +// tenantId: number, +// billId: number, +// override?: boolean, +// trx?: Knex.Transaction +// ): Promise { +// const { Bill } = this.tenancy.models(tenantId); + +// // Retireve bill with assocaited entries and allocated cost entries. +// const bill = await Bill.query(trx) +// .findById(billId) +// .withGraphFetched('entries.allocatedCostEntries'); + +// // Loads the inventory items entries of the given sale invoice. +// const inventoryEntries = +// await this.itemsEntriesService.filterInventoryEntries( +// tenantId, +// bill.entries +// ); +// const transaction = { +// transactionId: bill.id, +// transactionType: 'Bill', +// exchangeRate: bill.exchangeRate, + +// date: bill.billDate, +// direction: 'IN', +// entries: inventoryEntries, +// createdAt: bill.createdAt, + +// warehouseId: bill.warehouseId, +// }; +// await this.inventoryService.recordInventoryTransactionsFromItemsEntries( +// tenantId, +// transaction, +// override, +// trx +// ); +// } + +// /** +// * Reverts the inventory transactions of the given bill id. +// * @param {number} tenantId - Tenant id. +// * @param {number} billId - Bill id. +// * @return {Promise} +// */ +// public async revertInventoryTransactions( +// tenantId: number, +// billId: number, +// trx?: Knex.Transaction +// ) { +// // Deletes the inventory transactions by the given reference id and type. +// await this.inventoryService.deleteInventoryTransactions( +// tenantId, +// billId, +// 'Bill', +// trx +// ); +// } +// } diff --git a/packages/server-nest/src/modules/Bills/commands/BillPaymentsGLEntriesRewrite.ts b/packages/server-nest/src/modules/Bills/commands/BillPaymentsGLEntriesRewrite.ts new file mode 100644 index 000000000..b95b916a9 --- /dev/null +++ b/packages/server-nest/src/modules/Bills/commands/BillPaymentsGLEntriesRewrite.ts @@ -0,0 +1,76 @@ +// import { Knex } from 'knex'; +// import async from 'async'; +// import HasTenancyService from '@/services/Tenancy/TenancyService'; +// import { Inject, Service } from 'typedi'; +// import { BillPaymentGLEntries } from '../BillPayments/BillPaymentGLEntries'; + +// @Service() +// export class BillPaymentsGLEntriesRewrite { +// @Inject() +// public tenancy: HasTenancyService; + +// @Inject() +// public paymentGLEntries: BillPaymentGLEntries; + +// /** +// * Rewrites payments GL entries that associated to the given bill. +// * @param {number} tenantId +// * @param {number} billId +// * @param {Knex.Transaction} trx +// */ +// public rewriteBillPaymentsGLEntries = async ( +// tenantId: number, +// billId: number, +// trx?: Knex.Transaction +// ) => { +// const { BillPaymentEntry } = this.tenancy.models(tenantId); + +// const billPaymentEntries = await BillPaymentEntry.query().where( +// 'billId', +// billId +// ); +// const paymentsIds = billPaymentEntries.map((e) => e.billPaymentId); + +// await this.rewritePaymentsGLEntriesQueue(tenantId, paymentsIds, trx); +// }; + +// /** +// * Rewrites the payments GL entries under async queue. +// * @param {number} tenantId +// * @param {number[]} paymentsIds +// * @param {Knex.Transaction} trx +// */ +// public rewritePaymentsGLEntriesQueue = async ( +// tenantId: number, +// paymentsIds: number[], +// trx?: Knex.Transaction +// ) => { +// // Initiate a new queue for accounts balance mutation. +// const rewritePaymentGL = async.queue(this.rewritePaymentsGLEntriesTask, 10); + +// paymentsIds.forEach((paymentId: number) => { +// rewritePaymentGL.push({ paymentId, trx, tenantId }); +// }); +// // +// if (paymentsIds.length > 0) await rewritePaymentGL.drain(); +// }; + +// /** +// * Rewrites the payments GL entries task. +// * @param {number} tenantId - +// * @param {number} paymentId - +// * @param {Knex.Transaction} trx - +// * @returns {Promise} +// */ +// public rewritePaymentsGLEntriesTask = async ({ +// tenantId, +// paymentId, +// trx, +// }) => { +// await this.paymentGLEntries.rewritePaymentGLEntries( +// tenantId, +// paymentId, +// trx +// ); +// }; +// } diff --git a/packages/server-nest/src/modules/Bills/commands/BillPaymentsGLEntriesRewriteSubscriber.ts b/packages/server-nest/src/modules/Bills/commands/BillPaymentsGLEntriesRewriteSubscriber.ts new file mode 100644 index 000000000..22d5b869a --- /dev/null +++ b/packages/server-nest/src/modules/Bills/commands/BillPaymentsGLEntriesRewriteSubscriber.ts @@ -0,0 +1,36 @@ +// import { Inject, Service } from 'typedi'; +// import events from '@/subscribers/events'; +// import { IBillEditedPayload } from '@/interfaces'; +// import { BillPaymentsGLEntriesRewrite } from './BillPaymentsGLEntriesRewrite'; + +// @Service() +// export class BillPaymentsGLEntriesRewriteSubscriber { +// @Inject() +// private billPaymentGLEntriesRewrite: BillPaymentsGLEntriesRewrite; + +// /** +// * Attaches events with handles. +// */ +// public attach(bus) { +// bus.subscribe( +// events.bill.onEdited, +// this.handlerRewritePaymentsGLOnBillEdited +// ); +// } + +// /** +// * Handles writing journal entries once bill created. +// * @param {IBillCreatedPayload} payload - +// */ +// private handlerRewritePaymentsGLOnBillEdited = async ({ +// tenantId, +// billId, +// trx, +// }: IBillEditedPayload) => { +// await this.billPaymentGLEntriesRewrite.rewriteBillPaymentsGLEntries( +// tenantId, +// billId, +// trx +// ); +// }; +// } diff --git a/packages/server-nest/src/modules/Bills/commands/BillsExportable.ts b/packages/server-nest/src/modules/Bills/commands/BillsExportable.ts new file mode 100644 index 000000000..e3dcf983f --- /dev/null +++ b/packages/server-nest/src/modules/Bills/commands/BillsExportable.ts @@ -0,0 +1,37 @@ +// import { Inject, Service } from 'typedi'; +// import { Knex } from 'knex'; +// import { IBillsFilter } from '@/interfaces'; +// import { Exportable } from '@/services/Export/Exportable'; +// import { BillsApplication } from '../Bills.application'; +// import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants'; +// import Objection from 'objection'; + +// @Service() +// export class BillsExportable extends Exportable { +// @Inject() +// private billsApplication: BillsApplication; + +// /** +// * Retrieves the accounts data to exportable sheet. +// * @param {number} tenantId +// * @returns +// */ +// public exportable(tenantId: number, query: IBillsFilter) { +// const filterQuery = (query) => { +// query.withGraphFetched('branch'); +// query.withGraphFetched('warehouse'); +// }; +// const parsedQuery = { +// sortOrder: 'desc', +// columnSortBy: 'created_at', +// ...query, +// page: 1, +// pageSize: EXPORT_SIZE_LIMIT, +// filterQuery, +// } as IBillsFilter; + +// return this.billsApplication +// .getBills(tenantId, parsedQuery) +// .then((output) => output.bills); +// } +// } diff --git a/packages/server-nest/src/modules/Bills/commands/BillsImportable.ts b/packages/server-nest/src/modules/Bills/commands/BillsImportable.ts new file mode 100644 index 000000000..5eb138b50 --- /dev/null +++ b/packages/server-nest/src/modules/Bills/commands/BillsImportable.ts @@ -0,0 +1,46 @@ +// import { Inject, Service } from 'typedi'; +// import { Knex } from 'knex'; +// import { Importable } from '@/services/Import/Importable'; +// import { CreateBill } from './CreateBill.service'; +// import { IBillDTO } from '@/interfaces'; +// import { BillsSampleData } from '../Bills.constants'; + +// @Service() +// export class BillsImportable extends Importable { +// @Inject() +// private createBillService: CreateBill; + +// /** +// * Importing to account service. +// * @param {number} tenantId +// * @param {IAccountCreateDTO} createAccountDTO +// * @returns +// */ +// public importable( +// tenantId: number, +// createAccountDTO: IBillDTO, +// trx?: Knex.Transaction +// ) { +// return this.createBillService.createBill( +// tenantId, +// createAccountDTO, +// {}, +// trx +// ); +// } + +// /** +// * Concurrrency controlling of the importing process. +// * @returns {number} +// */ +// public get concurrency() { +// return 1; +// } + +// /** +// * Retrieves the sample data that used to download accounts sample sheet. +// */ +// public sampleData(): any[] { +// return BillsSampleData; +// } +// } diff --git a/packages/server-nest/src/modules/Bills/commands/BillsValidators.service.ts b/packages/server-nest/src/modules/Bills/commands/BillsValidators.service.ts new file mode 100644 index 000000000..50068bbef --- /dev/null +++ b/packages/server-nest/src/modules/Bills/commands/BillsValidators.service.ts @@ -0,0 +1,167 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { ERRORS } from '../Bills.constants'; +import { Bill } from '../models/Bill'; +import { ServiceError } from '@/modules/Items/ServiceError'; +import { IItemEntryDTO } from '@/modules/TransactionItemEntry/ItemEntry.types'; +import { Item } from '@/modules/Items/models/Item'; +import { BillPaymentEntry } from '@/modules/BillPayments/models/BillPaymentEntry'; +import { BillLandedCost } from '@/modules/BillLandedCosts/models/BillLandedCost'; + +@Injectable() +export class BillsValidators { + constructor( + @Inject(Bill.name) private billModel: typeof Bill, + + @Inject(BillPaymentEntry.name) + private billPaymentEntryModel: typeof BillPaymentEntry, + + @Inject(BillLandedCost.name) + private billLandedCostModel: typeof BillLandedCost, + + @Inject(VendorCreditAppliedBill.name) + private vendorCreditAppliedBillModel: typeof VendorCreditAppliedBill, + + @Inject(Item.name) private itemModel: typeof Item, + ) {} + + /** + * Validates the bill existance. + * @param {Bill | undefined | null} bill + */ + public validateBillExistance(bill: Bill | undefined | null) { + if (!bill) { + throw new ServiceError(ERRORS.BILL_NOT_FOUND); + } + } + + /** + * Validates the bill amount is bigger than paid amount. + * @param {number} billAmount + * @param {number} paidAmount + */ + public validateBillAmountBiggerPaidAmount( + billAmount: number, + paidAmount: number, + ) { + if (billAmount < paidAmount) { + throw new ServiceError(ERRORS.BILL_AMOUNT_SMALLER_THAN_PAID_AMOUNT); + } + } + + /** + * Validates the bill number existance. + */ + public async validateBillNumberExists( + billNumber: string, + notBillId?: number, + ) { + const foundBills = await this.billModel + .query() + .where('bill_number', billNumber) + .onBuild((builder) => { + if (notBillId) { + builder.whereNot('id', notBillId); + } + }); + + if (foundBills.length > 0) { + throw new ServiceError( + ERRORS.BILL_NUMBER_EXISTS, + 'The bill number is not unique.', + ); + } + } + + /** + * Validate the bill has no payment entries. + * @param {number} billId - Bill id. + */ + public async validateBillHasNoEntries(billId: number) { + // Retrieve the bill associate payment made entries. + const entries = await this.billPaymentEntryModel + .query() + .where('bill_id', billId); + + if (entries.length > 0) { + throw new ServiceError(ERRORS.BILL_HAS_ASSOCIATED_PAYMENT_ENTRIES); + } + return entries; + } + + /** + * Validate the bill number require. + * @param {string} billNo - + */ + public validateBillNoRequire(billNo: string) { + if (!billNo) { + throw new ServiceError(ERRORS.BILL_NO_IS_REQUIRED); + } + } + + /** + * Validate bill transaction has no associated allocated landed cost transactions. + * @param {number} billId + */ + public async validateBillHasNoLandedCost(billId: number) { + const billLandedCosts = await this.billLandedCostModel + .query() + .where('billId', billId); + + if (billLandedCosts.length > 0) { + throw new ServiceError(ERRORS.BILL_HAS_ASSOCIATED_LANDED_COSTS); + } + } + + /** + * Validate transaction entries that have landed cost type should not be + * inventory items. + * @param {IItemEntryDTO[]} newEntriesDTO - + */ + public async validateCostEntriesShouldBeInventoryItems( + newEntriesDTO: IItemEntryDTO[], + ) { + const entriesItemsIds = newEntriesDTO.map((e) => e.itemId); + const entriesItems = await this.itemModel + .query() + .whereIn('id', entriesItemsIds); + + const entriesItemsById = transformToMap(entriesItems, 'id'); + + // Filter the landed cost entries that not associated with inventory item. + const nonInventoryHasCost = newEntriesDTO.filter((entry) => { + const item = entriesItemsById.get(entry.itemId); + + return entry.landedCost && item.type !== 'inventory'; + }); + if (nonInventoryHasCost.length > 0) { + throw new ServiceError( + ERRORS.LANDED_COST_ENTRIES_SHOULD_BE_INVENTORY_ITEMS, + ); + } + } + + /** + * + * @param {number} billId + */ + public validateBillHasNoAppliedToCredit = async (billId: number) => { + const appliedTransactions = await this.vendorCreditAppliedBillModel + .query() + .where('billId', billId); + if (appliedTransactions.length > 0) { + throw new ServiceError(ERRORS.BILL_HAS_APPLIED_TO_VENDOR_CREDIT); + } + }; + + /** + * Validate the given vendor has no associated bills transactions. + * @param {number} vendorId - Vendor id. + */ + public async validateVendorHasNoBills(vendorId: number) { + const bills = await this.billModel.query().where('vendor_id', vendorId); + + if (bills.length > 0) { + throw new ServiceError(ERRORS.VENDOR_HAS_BILLS); + } + } +} diff --git a/packages/server-nest/src/modules/Bills/commands/CreateBill.service.ts b/packages/server-nest/src/modules/Bills/commands/CreateBill.service.ts new file mode 100644 index 000000000..0368f70e1 --- /dev/null +++ b/packages/server-nest/src/modules/Bills/commands/CreateBill.service.ts @@ -0,0 +1,100 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Knex } from 'knex'; +import { + IBillDTO, + IBillCreatedPayload, + IBillCreatingPayload, +} from '../Bills.types'; +import { BillDTOTransformer } from './BillDTOTransformer.service'; +import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { BillsValidators } from './BillsValidators.service'; +import { ItemsEntriesService } from '@/modules/Items/ItemsEntries.service'; +import { Bill } from '../models/Bill'; +import { Vendor } from '@/modules/Vendors/models/Vendor'; +import { events } from '@/common/events/events'; + +@Injectable() +export class CreateBill { + constructor( + private uow: UnitOfWork, + private eventPublisher: EventEmitter2, + private validators: BillsValidators, + private itemsEntriesService: ItemsEntriesService, + private transformerDTO: BillDTOTransformer, + + @Inject(Bill.name) + private billModel: typeof Bill, + + @Inject(Vendor.name) + private contactModel: typeof Vendor, + ) {} + + /** + * Creates a new bill and stored it to the storage. + * ---- + * Precedures. + * ---- + * - Insert bill transactions to the storage. + * - Insert bill entries to the storage. + * - Increment the given vendor id. + * - Record bill journal transactions on the given accounts. + * - Record bill items inventory transactions. + * ---- + * @param {IBillDTO} billDTO - + * @return {Promise} + */ + public async createBill( + billDTO: IBillDTO, + trx?: Knex.Transaction, + ): Promise { + // Retrieves the given bill vendor or throw not found error. + const vendor = await this.contactModel + .query() + .modify('vendor') + .findById(billDTO.vendorId) + .throwIfNotFound(); + + // Validate the bill number uniqiness on the storage. + await this.validators.validateBillNumberExists(billDTO.billNumber); + + // Validate items IDs existance. + await this.itemsEntriesService.validateItemsIdsExistance(billDTO.entries); + + // Validate non-purchasable items. + await this.itemsEntriesService.validateNonPurchasableEntriesItems( + billDTO.entries, + ); + // Validates the cost entries should be with inventory items. + await this.validators.validateCostEntriesShouldBeInventoryItems( + billDTO.entries, + ); + // Transform the bill DTO to model object. + const billObj = await this.transformerDTO.billDTOToModel( + billDTO, + vendor, + ); + + // Write new bill transaction with associated transactions under UOW env. + return this.uow.withTransaction(async (trx: Knex.Transaction) => { + // Triggers `onBillCreating` event. + await this.eventPublisher.emitAsync(events.bill.onCreating, { + trx, + billDTO, + } as IBillCreatingPayload); + + // Inserts the bill graph object to the storage. + const bill = await this.billModel.query(trx).upsertGraph(billObj); + + // Triggers `onBillCreated` event. + await this.eventPublisher.emitAsync(events.bill.onCreated, { + bill, + billId: bill.id, + billDTO, + trx, + } as IBillCreatedPayload); + + return bill; + }, trx); + } +} diff --git a/packages/server-nest/src/modules/Bills/commands/DeleteBill.service.ts b/packages/server-nest/src/modules/Bills/commands/DeleteBill.service.ts new file mode 100644 index 000000000..7e21be56a --- /dev/null +++ b/packages/server-nest/src/modules/Bills/commands/DeleteBill.service.ts @@ -0,0 +1,74 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Knex } from 'knex'; +import { events } from '@/common/events/events'; +import { + IBIllEventDeletedPayload, + IBillEventDeletingPayload, +} from '../Bills.types'; +import { BillsValidators } from './BillsValidators.service'; +import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { ItemEntry } from '@/modules/Items/models/ItemEntry'; +import { Bill } from '../models/Bill'; + +@Injectable() +export class DeleteBill { + constructor( + private readonly validators: BillsValidators, + private readonly uow: UnitOfWork, + private readonly eventPublisher: EventEmitter2, + @Inject(Bill.name) private readonly billModel: typeof Bill, + @Inject(ItemEntry.name) private readonly itemEntryModel: typeof ItemEntry, + ) {} + + /** + * Deletes the bill with associated entries. + * @param {number} billId + * @return {void} + */ + public async deleteBill(billId: number) { + // Retrieve the given bill or throw not found error. + const oldBill = await this.billModel + .query() + .findById(billId) + .withGraphFetched('entries'); + + // Validates the bill existence. + this.validators.validateBillExistance(oldBill); + + // Validate the given bill has no associated landed cost transactions. + await this.validators.validateBillHasNoLandedCost(billId); + + // Validate the purchase bill has no associated payments transactions. + await this.validators.validateBillHasNoEntries(billId); + + // Validate the given bill has no associated reconciled with vendor credits. + await this.validators.validateBillHasNoAppliedToCredit(billId); + + // Deletes bill transaction with associated transactions under + // unit-of-work environment. + return this.uow.withTransaction(async (trx: Knex.Transaction) => { + // Triggers `onBillDeleting` event. + await this.eventPublisher.emitAsync(events.bill.onDeleting, { + trx, + oldBill, + } as IBillEventDeletingPayload); + + // Delete all associated bill entries. + await ItemEntry.query(trx) + .where('reference_type', 'Bill') + .where('reference_id', billId) + .delete(); + + // Delete the bill transaction. + await Bill.query(trx).findById(billId).delete(); + + // Triggers `onBillDeleted` event. + await this.eventPublisher.emitAsync(events.bill.onDeleted, { + billId, + oldBill, + trx, + } as IBIllEventDeletedPayload); + }); + } +} diff --git a/packages/server-nest/src/modules/Bills/commands/EditBill.service.ts b/packages/server-nest/src/modules/Bills/commands/EditBill.service.ts new file mode 100644 index 000000000..74b22de0a --- /dev/null +++ b/packages/server-nest/src/modules/Bills/commands/EditBill.service.ts @@ -0,0 +1,134 @@ +import { + IBillEditDTO, + IBillEditedPayload, + IBillEditingPayload, +} from '../Bills.types'; +import { Inject, Injectable } from '@nestjs/common'; +import { BillsValidators } from './BillsValidators.service'; +import { ItemsEntriesService } from '@/modules/Items/ItemsEntries.service'; +import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { BillDTOTransformer } from './BillDTOTransformer.service'; +import { Bill } from '../models/Bill'; +import { events } from '@/common/events/events'; +import { Vendor } from '@/modules/Vendors/models/Vendor'; + + +@Injectable() +export class EditBillService { + constructor( + private validators: BillsValidators, + private itemsEntriesService: ItemsEntriesService, + private uow: UnitOfWork, + private eventPublisher: EventEmitter2, + private entriesService: ItemEntries, + private transformerDTO: BillDTOTransformer, + @Inject(Bill.name) private billModel: typeof Bill, + @Inject(Vendor.name) private contactModel: typeof Vendor, + ) {} + + /** + * Edits details of the given bill id with associated entries. + * + * Precedures: + * ------- + * - Update the bill transaction on the storage. + * - Update the bill entries on the storage and insert the not have id and delete + * once that not presented. + * - Increment the diff amount on the given vendor id. + * - Re-write the inventory transactions. + * - Re-write the bill journal transactions. + * ------ + * @param {Integer} billId - The given bill id. + * @param {IBillEditDTO} billDTO - The given new bill details. + * @return {Promise} + */ + public async editBill( + billId: number, + billDTO: IBillEditDTO, + ): Promise { + // Retrieve the given bill or throw not found error. + const oldBill = await this.billModel + .query() + .findById(billId) + .withGraphFetched('entries'); + + // Validate bill existance. + this.validators.validateBillExistance(oldBill); + + // Retrieve vendor details or throw not found service error. + const vendor = await this.contactModel + .query() + .findById(billDTO.vendorId) + .modify('vendor') + .throwIfNotFound(); + + // Validate bill number uniqiness on the storage. + if (billDTO.billNumber) { + await this.validators.validateBillNumberExists( + billDTO.billNumber, + billId + ); + } + // Validate the entries ids existance. + await this.itemsEntriesService.validateEntriesIdsExistance( + billId, + 'Bill', + billDTO.entries + ); + // Validate the items ids existance on the storage. + await this.itemsEntriesService.validateItemsIdsExistance( + billDTO.entries + ); + // Accept the purchasable items only. + await this.itemsEntriesService.validateNonPurchasableEntriesItems( + billDTO.entries + ); + + // Transforms the bill DTO to model object. + const billObj = await this.transformerDTO.billDTOToModel( + billDTO, + vendor, + oldBill + ); + // Validate bill total amount should be bigger than paid amount. + this.validators.validateBillAmountBiggerPaidAmount( + billObj.amount, + oldBill.paymentAmount + ); + // Validate landed cost entries that have allocated cost could not be deleted. + await this.entriesService.validateLandedCostEntriesNotDeleted( + oldBill.entries, + billObj.entries + ); + // Validate new landed cost entries should be bigger than new entries. + await this.entriesService.validateLocatedCostEntriesSmallerThanNewEntries( + oldBill.entries, + billObj.entries + ); + // Edits bill transactions and associated transactions under UOW envirement. + return this.uow.withTransaction(async (trx: Knex.Transaction) => { + // Triggers `onBillEditing` event. + await this.eventPublisher.emitAsync(events.bill.onEditing, { + oldBill, + billDTO, + trx, + } as IBillEditingPayload); + + // Update the bill transaction. + const bill = await this.billModel.query(trx).upsertGraphAndFetch({ + id: billId, + ...billObj, + }); + // Triggers event `onBillEdited`. + await this.eventPublisher.emitAsync(events.bill.onEdited, { + oldBill, + bill, + billDTO, + trx, + } as IBillEditedPayload); + + return bill; + }); + } +} diff --git a/packages/server-nest/src/modules/Bills/commands/OpenBill.service.ts b/packages/server-nest/src/modules/Bills/commands/OpenBill.service.ts new file mode 100644 index 000000000..6b9dd899d --- /dev/null +++ b/packages/server-nest/src/modules/Bills/commands/OpenBill.service.ts @@ -0,0 +1,65 @@ +import moment from 'moment'; +import { Inject, Injectable } from '@nestjs/common'; +import { ERRORS } from '../Bills.constants'; +import { BillsValidators } from './BillsValidators.service'; +import { IBillOpenedPayload, IBillOpeningPayload } from '../Bills.types'; +import { Bill } from '../models/Bill'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service'; +import { ServiceError } from '@/modules/Items/ServiceError'; +import { events } from '@/common/events/events'; + +@Injectable() +export class OpenBillService { + constructor( + private readonly uow: UnitOfWork, + private readonly validators: BillsValidators, + private readonly eventPublisher: EventEmitter2, + + @Inject(Bill.name) + private readonly billModel: typeof Bill, + ) {} + + /** + * Mark the bill as open. + * @param {number} billId - Bill id. + * @return {Promise} + */ + public async openBill(billId: number): Promise { + // Retrieve the given bill or throw not found error. + const oldBill = await this.billModel + .query() + .findById(billId) + .withGraphFetched('entries'); + + // Validates the bill existence. + this.validators.validateBillExistance(oldBill); + + if (oldBill.isOpen) { + throw new ServiceError(ERRORS.BILL_ALREADY_OPEN); + } + + return this.uow.withTransaction(async (trx) => { + // Triggers `onBillCreating` event. + await this.eventPublisher.emitAsync(events.bill.onOpening, { + oldBill, + trx, + } as IBillOpeningPayload); + + // Save the bill opened at on the storage. + const bill = await this.billModel + .query(trx) + .patchAndFetchById(billId, { + openedAt: moment().toMySqlDateTime(), + }) + .withGraphFetched('entries'); + + // Triggers `onBillCreating` event. + await this.eventPublisher.emitAsync(events.bill.onOpened, { + bill, + oldBill, + trx, + } as IBillOpenedPayload); + }); + } +} diff --git a/packages/server-nest/src/modules/Bills/models/Bill.ts b/packages/server-nest/src/modules/Bills/models/Bill.ts new file mode 100644 index 000000000..696c2dd3d --- /dev/null +++ b/packages/server-nest/src/modules/Bills/models/Bill.ts @@ -0,0 +1,585 @@ +import { Model, raw, mixin } from 'objection'; +import { castArray, difference } from 'lodash'; +import moment from 'moment'; +// import TenantModel from 'models/TenantModel'; +// import BillSettings from './Bill.Settings'; +// import ModelSetting from './ModelSetting'; +// import CustomViewBaseModel from './CustomViewBaseModel'; +// import { DEFAULT_VIEWS } from '@/services/Purchases/Bills/constants'; +// import ModelSearchable from './ModelSearchable'; +import { BaseModel } from '@/models/Model'; + +export class Bill extends BaseModel{ + public amount: number; + public paymentAmount: number; + public landedCostAmount: number; + public allocatedCostAmount: number; + public isInclusiveTax: boolean; + public taxAmountWithheld: number; + public exchangeRate: number; + public vendorId: number; + public billNumber: string; + public billDate: Date; + public dueDate: Date; + public referenceNo: string; + public status: string; + public note: string; + public currencyCode: string; + public creditedAmount: number; + public invLotNumber: string; + public invoicedAmount: number; + public openedAt: Date | string; + public userId: number; + + public createdAt: Date; + public updatedAt: Date | null; + + /** + * Timestamps columns. + */ + get timestamps() { + return ['createdAt', 'updatedAt']; + } + + /** + * Virtual attributes. + */ + static get virtualAttributes() { + return [ + 'balance', + 'dueAmount', + 'isOpen', + 'isPartiallyPaid', + 'isFullyPaid', + 'isPaid', + 'remainingDays', + 'overdueDays', + 'isOverdue', + 'unallocatedCostAmount', + 'localAmount', + 'localAllocatedCostAmount', + 'billableAmount', + 'amountLocal', + 'subtotal', + 'subtotalLocal', + 'subtotalExcludingTax', + 'taxAmountWithheldLocal', + 'total', + 'totalLocal', + ]; + } + + /** + * Invoice amount in base currency. + * @returns {number} + */ + get amountLocal() { + return this.amount * this.exchangeRate; + } + + /** + * Subtotal. (Tax inclusive) if the tax inclusive is enabled. + * @returns {number} + */ + get subtotal() { + return this.amount; + } + + /** + * Subtotal in base currency. (Tax inclusive) if the tax inclusive is enabled. + * @returns {number} + */ + get subtotalLocal() { + return this.amountLocal; + } + + /** + * Sale invoice amount excluding tax. + * @returns {number} + */ + get subtotalExcludingTax() { + return this.isInclusiveTax + ? this.subtotal - this.taxAmountWithheld + : this.subtotal; + } + + /** + * Tax amount withheld in base currency. + * @returns {number} + */ + get taxAmountWithheldLocal() { + return this.taxAmountWithheld * this.exchangeRate; + } + + /** + * Invoice total. (Tax included) + * @returns {number} + */ + get total() { + return this.isInclusiveTax + ? this.subtotal + : this.subtotal + this.taxAmountWithheld; + } + + /** + * Invoice total in local currency. (Tax included) + * @returns {number} + */ + get totalLocal() { + return this.total * this.exchangeRate; + } + + /** + * Table name + */ + static get tableName() { + return 'bills'; + } + + /** + * Model modifiers. + */ + static get modifiers() { + return { + /** + * Filters the bills in draft status. + */ + draft(query) { + query.where('opened_at', null); + }, + + /** + * Filters the opened bills. + */ + published(query) { + query.whereNot('openedAt', null); + }, + + /** + * Filters the opened bills. + */ + opened(query) { + query.whereNot('opened_at', null); + }, + /** + * Filters the unpaid bills. + */ + unpaid(query) { + query.where('payment_amount', 0); + }, + /** + * Filters the due bills. + */ + dueBills(query) { + query.where( + raw(`COALESCE(AMOUNT, 0) - + COALESCE(PAYMENT_AMOUNT, 0) - + COALESCE(CREDITED_AMOUNT, 0) > 0 + `) + ); + }, + /** + * Filters the overdue bills. + */ + overdue(query) { + query.where('due_date', '<', moment().format('YYYY-MM-DD')); + }, + /** + * Filters the not overdue invoices. + */ + notOverdue(query, asDate = moment().format('YYYY-MM-DD')) { + query.where('due_date', '>=', asDate); + }, + /** + * Filters the partially paid bills. + */ + partiallyPaid(query) { + query.whereNot('payment_amount', 0); + query.whereNot(raw('`PAYMENT_AMOUNT` = `AMOUNT`')); + }, + /** + * Filters the paid bills. + */ + paid(query) { + query.where(raw('`PAYMENT_AMOUNT` = `AMOUNT`')); + }, + /** + * Filters the bills from the given date. + */ + fromDate(query, fromDate) { + query.where('bill_date', '<=', fromDate); + }, + + /** + * Sort the bills by full-payment bills. + */ + sortByStatus(query, order) { + query.orderByRaw(`PAYMENT_AMOUNT = AMOUNT ${order}`); + }, + + /** + * Status filter. + */ + statusFilter(query, filterType) { + switch (filterType) { + case 'draft': + query.modify('draft'); + break; + case 'delivered': + query.modify('delivered'); + break; + case 'unpaid': + query.modify('unpaid'); + break; + case 'overdue': + default: + query.modify('overdue'); + break; + case 'partially-paid': + query.modify('partiallyPaid'); + break; + case 'paid': + query.modify('paid'); + break; + } + }, + + /** + * Filters by branches. + */ + filterByBranches(query, branchesIds) { + const formattedBranchesIds = castArray(branchesIds); + + query.whereIn('branchId', formattedBranchesIds); + }, + + dueBillsFromDate(query, asDate = moment().format('YYYY-MM-DD')) { + query.modify('dueBills'); + query.modify('notOverdue'); + query.modify('fromDate', asDate); + }, + + overdueBillsFromDate(query, asDate = moment().format('YYYY-MM-DD')) { + query.modify('dueBills'); + query.modify('overdue', asDate); + query.modify('fromDate', asDate); + }, + + /** + * + */ + billable(query) { + query.where(raw('AMOUNT > INVOICED_AMOUNT')); + }, + }; + } + + /** + * Invoice amount in organization base currency. + * @deprecated + * @returns {number} + */ + get localAmount() { + return this.amountLocal; + } + + /** + * Retrieves the local allocated cost amount. + * @returns {number} + */ + get localAllocatedCostAmount() { + return this.allocatedCostAmount * this.exchangeRate; + } + + /** + * Retrieves the local landed cost amount. + * @returns {number} + */ + get localLandedCostAmount() { + return this.landedCostAmount * this.exchangeRate; + } + + /** + * Retrieves the local unallocated cost amount. + * @returns {number} + */ + get localUnallocatedCostAmount() { + return this.unallocatedCostAmount * this.exchangeRate; + } + + /** + * Retrieve the balance of bill. + * @return {number} + */ + get balance() { + return this.paymentAmount + this.creditedAmount; + } + + /** + * Due amount of the given. + * @return {number} + */ + get dueAmount() { + return Math.max(this.total - this.balance, 0); + } + + /** + * Detarmine whether the bill is open. + * @return {boolean} + */ + get isOpen() { + return !!this.openedAt; + } + + /** + * Deetarmine whether the bill paid partially. + * @return {boolean} + */ + get isPartiallyPaid() { + return this.dueAmount !== this.total && this.dueAmount > 0; + } + + /** + * Deetarmine whether the bill paid fully. + * @return {boolean} + */ + get isFullyPaid() { + return this.dueAmount === 0; + } + + /** + * Detarmines whether the bill paid fully or partially. + * @return {boolean} + */ + get isPaid() { + return this.isPartiallyPaid || this.isFullyPaid; + } + + /** + * Retrieve the remaining days in number + * @return {number|null} + */ + get remainingDays() { + const currentMoment = moment(); + const dueDateMoment = moment(this.dueDate); + + return Math.max(dueDateMoment.diff(currentMoment, 'days'), 0); + } + + /** + * Retrieve the overdue days in number. + * @return {number|null} + */ + get overdueDays() { + const currentMoment = moment(); + const dueDateMoment = moment(this.dueDate); + + return Math.max(currentMoment.diff(dueDateMoment, 'days'), 0); + } + + /** + * Detarmines the due date is over. + * @return {boolean} + */ + get isOverdue() { + return this.overdueDays > 0; + } + + /** + * Retrieve the unallocated cost amount. + * @return {number} + */ + get unallocatedCostAmount() { + return Math.max(this.landedCostAmount - this.allocatedCostAmount, 0); + } + + /** + * Retrieves the calculated amount which have not been invoiced. + */ + get billableAmount() { + return Math.max(this.total - this.invoicedAmount, 0); + } + + /** + * Bill model settings. + */ + // static get meta() { + // return BillSettings; + // } + + /** + * Relationship mapping. + */ + static get relationMappings() { + const Vendor = require('models/Vendor'); + const ItemEntry = require('models/ItemEntry'); + const BillLandedCost = require('models/BillLandedCost'); + const Branch = require('models/Branch'); + const Warehouse = require('models/Warehouse'); + const TaxRateTransaction = require('models/TaxRateTransaction'); + const Document = require('models/Document'); + const { MatchedBankTransaction } = require('models/MatchedBankTransaction'); + + return { + vendor: { + relation: Model.BelongsToOneRelation, + modelClass: Vendor.default, + join: { + from: 'bills.vendorId', + to: 'contacts.id', + }, + filter(query) { + query.where('contact_service', 'vendor'); + }, + }, + + entries: { + relation: Model.HasManyRelation, + modelClass: ItemEntry.default, + join: { + from: 'bills.id', + to: 'items_entries.referenceId', + }, + filter(builder) { + builder.where('reference_type', 'Bill'); + builder.orderBy('index', 'ASC'); + }, + }, + + locatedLandedCosts: { + relation: Model.HasManyRelation, + modelClass: BillLandedCost.default, + join: { + from: 'bills.id', + to: 'bill_located_costs.billId', + }, + }, + + /** + * Bill may belongs to associated branch. + */ + branch: { + relation: Model.BelongsToOneRelation, + modelClass: Branch.default, + join: { + from: 'bills.branchId', + to: 'branches.id', + }, + }, + + /** + * Bill may has associated warehouse. + */ + warehouse: { + relation: Model.BelongsToOneRelation, + modelClass: Warehouse.default, + join: { + from: 'bills.warehouseId', + to: 'warehouses.id', + }, + }, + + /** + * Bill may has associated tax rate transactions. + */ + taxes: { + relation: Model.HasManyRelation, + modelClass: TaxRateTransaction.default, + join: { + from: 'bills.id', + to: 'tax_rate_transactions.referenceId', + }, + filter(builder) { + builder.where('reference_type', 'Bill'); + }, + }, + + /** + * Bill may has many attached attachments. + */ + attachments: { + relation: Model.ManyToManyRelation, + modelClass: Document.default, + join: { + from: 'bills.id', + through: { + from: 'document_links.modelId', + to: 'document_links.documentId', + }, + to: 'documents.id', + }, + filter(query) { + query.where('model_ref', 'Bill'); + }, + }, + + /** + * Bill may belongs to matched bank transaction. + */ + matchedBankTransaction: { + relation: Model.HasManyRelation, + modelClass: MatchedBankTransaction, + join: { + from: 'bills.id', + to: 'matched_bank_transactions.referenceId', + }, + filter(query) { + query.where('reference_type', 'Bill'); + }, + }, + }; + } + + /** + * Retrieve the not found bills ids as array that associated to the given vendor. + * @param {Array} billsIds + * @param {number} vendorId - + * @return {Array} + */ + static async getNotFoundBills(billsIds, vendorId) { + const storedBills = await this.query().onBuild((builder) => { + builder.whereIn('id', billsIds); + + if (vendorId) { + builder.where('vendor_id', vendorId); + } + }); + + const storedBillsIds = storedBills.map((t) => t.id); + + const notFoundBillsIds = difference(billsIds, storedBillsIds); + return notFoundBillsIds; + } + + static changePaymentAmount(billId, amount, trx) { + const changeMethod = amount > 0 ? 'increment' : 'decrement'; + return this.query(trx) + .where('id', billId) + [changeMethod]('payment_amount', Math.abs(amount)); + } + + /** + * Retrieve the default custom views, roles and columns. + */ + // static get defaultViews() { + // return DEFAULT_VIEWS; + // } + + /** + * Model search attributes. + */ + static get searchRoles() { + return [ + { fieldKey: 'bill_number', comparator: 'contains' }, + { condition: 'or', fieldKey: 'reference_no', comparator: 'contains' }, + { condition: 'or', fieldKey: 'amount', comparator: 'equals' }, + ]; + } + + /** + * Prevents mutate base currency since the model is not empty. + */ + static get preventMutateBaseCurrency() { + return true; + } +} diff --git a/packages/server-nest/src/modules/Bills/queries/Bill.transformer.ts b/packages/server-nest/src/modules/Bills/queries/Bill.transformer.ts new file mode 100644 index 000000000..9227d216f --- /dev/null +++ b/packages/server-nest/src/modules/Bills/queries/Bill.transformer.ts @@ -0,0 +1,217 @@ +import { Transformer } from '@/modules/Transformer/Transformer'; +import { Bill } from '../models/Bill'; +import { AttachmentTransformer } from '@/modules/Attachments/Attachment.transformer'; +import { ItemEntryTransformer } from '@/modules/TransactionItemEntry/ItemEntry.transformer'; +import { SaleInvoiceTaxEntryTransformer } from '@/modules/SaleInvoices/queries/SaleInvoiceTaxEntry.transformer'; + +export class BillTransformer extends Transformer { + /** + * Include these attributes to sale bill object. + * @returns {Array} + */ + public includeAttributes = (): string[] => { + return [ + 'formattedBillDate', + 'formattedDueDate', + 'formattedCreatedAt', + 'formattedAmount', + 'formattedPaymentAmount', + 'formattedBalance', + 'formattedDueAmount', + 'formattedExchangeRate', + 'subtotalFormatted', + 'subtotalLocalFormatted', + 'subtotalExcludingTaxFormatted', + 'taxAmountWithheldLocalFormatted', + 'totalFormatted', + 'totalLocalFormatted', + 'taxes', + 'entries', + 'attachments', + ]; + }; + + /** + * Excluded attributes. + * @returns {string[]} + */ + public excludeAttributes = (): string[] => { + return ['amount', 'amountLocal', 'localAmount']; + }; + + /** + * Retrieve formatted bill date. + * @param {IBill} bill + * @returns {String} + */ + protected formattedBillDate = (bill: Bill): string => { + return this.formatDate(bill.billDate); + }; + + /** + * Retrieve formatted bill date. + * @param {IBill} bill + * @returns {String} + */ + protected formattedDueDate = (bill: Bill): string => { + return this.formatDate(bill.dueDate); + }; + + /** + * Retrieve the formatted created at date. + * @param {IBill} bill + * @returns {string} + */ + protected formattedCreatedAt = (bill: Bill): string => { + return this.formatDate(bill.createdAt); + }; + + /** + * Retrieve formatted bill amount. + * @param {IBill} bill + * @returns {string} + */ + protected formattedAmount = (bill: Bill): string => { + return this.formatNumber(bill.amount, { currencyCode: bill.currencyCode }); + }; + + /** + * Retrieve formatted bill amount. + * @param {IBill} bill + * @returns {string} + */ + protected formattedPaymentAmount = (bill: Bill): string => { + return this.formatNumber(bill.paymentAmount, { + currencyCode: bill.currencyCode, + }); + }; + + /** + * Retrieve formatted bill amount. + * @param {IBill} bill + * @returns {string} + */ + protected formattedDueAmount = (bill: Bill): string => { + return this.formatNumber(bill.dueAmount, { + currencyCode: bill.currencyCode, + }); + }; + + /** + * Retrieve formatted bill balance. + * @param {IBill} bill + * @returns {string} + */ + protected formattedBalance = (bill: Bill): string => { + return this.formatNumber(bill.balance, { currencyCode: bill.currencyCode }); + }; + + /** + * Retrieve the formatted exchange rate. + * @param {IBill} bill + * @returns {string} + */ + protected formattedExchangeRate = (bill: Bill): string => { + return this.formatNumber(bill.exchangeRate, { + currencyCode: this.context.organization.baseCurrency, + }); + }; + + /** + * Retrieves the formatted subtotal. + * @param {IBill} bill + * @returns {string} + */ + protected subtotalFormatted = (bill: Bill): string => { + return this.formatNumber(bill.subtotal, { + currencyCode: bill.currencyCode, + }); + }; + + /** + * Retrieves the local subtotal formatted. + * @param {IBill} bill + * @returns {string} + */ + protected subtotalLocalFormatted = (bill: Bill): string => { + return this.formatNumber(bill.subtotalLocal, { + currencyCode: this.context.organization.baseCurrency, + }); + }; + + /** + * Retrieves the formatted subtotal tax excluded. + * @param {IBill} bill + * @returns {string} + */ + protected subtotalExcludingTaxFormatted = (bill: Bill): string => { + return this.formatNumber(bill.subtotalExcludingTax, { + currencyCode: bill.currencyCode, + }); + }; + + /** + * Retrieves the local formatted tax amount withheld + * @param {IBill} bill + * @returns {string} + */ + protected taxAmountWithheldLocalFormatted = (bill: Bill): string => { + return this.formatNumber(bill.taxAmountWithheldLocal, { + currencyCode: this.context.organization.baseCurrency, + }); + }; + + /** + * Retrieves the total formatted. + * @param {IBill} bill + * @returns {string} + */ + protected totalFormatted = (bill: Bill): string => { + return this.formatNumber(bill.total, { + currencyCode: bill.currencyCode, + }); + }; + + /** + * Retrieves the local total formatted. + * @param {IBill} bill + * @returns {string} + */ + protected totalLocalFormatted = (bill: Bill): string => { + return this.formatNumber(bill.totalLocal, { + currencyCode: this.context.organization.baseCurrency, + }); + }; + + /** + * Retrieve the taxes lines of bill. + * @param {Bill} bill + */ + // protected taxes = (bill: Bill) => { + // return this.item(bill.taxes, new SaleInvoiceTaxEntryTransformer(), { + // subtotal: bill.subtotal, + // isInclusiveTax: bill.isInclusiveTax, + // currencyCode: bill.currencyCode, + // }); + // }; + + /** + * Retrieves the entries of the bill. + * @param {Bill} credit + * @returns {} + */ + // protected entries = (bill: Bill) => { + // return this.item(bill.entries, new ItemEntryTransformer(), { + // currencyCode: bill.currencyCode, + // }); + // }; + + /** + * Retrieves the bill attachments. + * @param {ISaleInvoice} invoice + * @returns + */ + // protected attachments = (bill: Bill) => { + // return this.item(bill.attachments, new AttachmentTransformer()); + // }; +} diff --git a/packages/server-nest/src/modules/Bills/queries/GetBill.ts b/packages/server-nest/src/modules/Bills/queries/GetBill.ts new file mode 100644 index 000000000..0cc475d79 --- /dev/null +++ b/packages/server-nest/src/modules/Bills/queries/GetBill.ts @@ -0,0 +1,38 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { BillsValidators } from '../commands/BillsValidators.service'; +import { BillTransformer } from './Bill.transformer'; +import { Bill } from '../models/Bill'; +import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service'; + +@Injectable() +export class GetBill { + constructor( + @Inject(Bill.name) private billModel: typeof Bill, + private transformer: TransformerInjectable, + private validators: BillsValidators, + ) {} + + /** + * Retrieve the given bill details with associated items entries. + * @param {Integer} billId - Specific bill. + * @returns {Promise} + */ + public async getBill(billId: number): Promise { + const bill = await this.billModel + .query() + .findById(billId) + .withGraphFetched('vendor') + .withGraphFetched('entries.item') + .withGraphFetched('branch') + .withGraphFetched('taxes.taxRate') + .withGraphFetched('attachments'); + + // Validates the bill existence. + this.validators.validateBillExistance(bill); + + return this.transformer.transform( + bill, + new BillTransformer(), + ); + } +} diff --git a/packages/server-nest/src/modules/Bills/queries/GetBillPayments.ts b/packages/server-nest/src/modules/Bills/queries/GetBillPayments.ts new file mode 100644 index 000000000..ef13ec272 --- /dev/null +++ b/packages/server-nest/src/modules/Bills/queries/GetBillPayments.ts @@ -0,0 +1,32 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { BillPaymentEntry } from '@/modules/BillPayments/models/BillPaymentEntry'; +import { BillPaymentTransactionTransformer } from '@/modules/BillPayments/queries/BillPaymentTransactionTransformer'; +import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service'; + +@Injectable() +export class GetBillPayments { + constructor( + @Inject(BillPaymentEntry.name) + private billPaymentEntryModel: typeof BillPaymentEntry, + private transformer: TransformerInjectable, + ) {} + + /** + * Retrieve the specific bill associated payment transactions. + * @param {number} billId + * @returns {} + */ + public getBillPayments = async (billId: number) => { + const billsEntries = await this.billPaymentEntryModel + .query() + .where('billId', billId) + .withGraphJoined('payment.paymentAccount') + .withGraphJoined('bill') + .orderBy('payment:paymentDate', 'ASC'); + + return this.transformer.transform( + billsEntries, + new BillPaymentTransactionTransformer(), + ); + }; +} diff --git a/packages/server-nest/src/modules/Bills/queries/GetBills.ts b/packages/server-nest/src/modules/Bills/queries/GetBills.ts new file mode 100644 index 000000000..2fae14a85 --- /dev/null +++ b/packages/server-nest/src/modules/Bills/queries/GetBills.ts @@ -0,0 +1,72 @@ +// import { Injectable } from '@nestjs/common'; +// import * as R from 'ramda'; +// import { +// IBill, +// IBillsFilter, +// IFilterMeta, +// IPaginationMeta, +// } from '@/interfaces'; +// import { BillTransformer } from './Bill.transformer'; +// import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service'; +// // import { DynamicListingService } from '@/modules/DynamicListing/DynamicListService'; + +// @Injectable() +// export class GetBills { +// constructor( +// private transformer: TransformerInjectable, +// private dynamicListService: DynamicListingService, +// ) {} + +// /** +// * Retrieve bills data table list. +// * @param {number} tenantId - +// * @param {IBillsFilter} billsFilter - +// */ +// public async getBills( +// tenantId: number, +// filterDTO: IBillsFilter, +// ): Promise<{ +// bills: IBill; +// pagination: IPaginationMeta; +// filterMeta: IFilterMeta; +// }> { +// // Parses bills list filter DTO. +// const filter = this.parseListFilterDTO(filterDTO); + +// // Dynamic list service. +// const dynamicFilter = await this.dynamicListService.dynamicList( +// tenantId, +// Bill, +// filter, +// ); +// const { results, pagination } = await Bill.query() +// .onBuild((builder) => { +// builder.withGraphFetched('vendor'); +// builder.withGraphFetched('entries.item'); +// dynamicFilter.buildQuery()(builder); + +// // Filter query. +// filterDTO?.filterQuery && filterDTO?.filterQuery(builder); +// }) +// .pagination(filter.page - 1, filter.pageSize); + +// // Tranform the bills to POJO. +// const bills = await this.transformer.transform( +// results, +// new PurchaseInvoiceTransformer(), +// ); +// return { +// bills, +// pagination, +// filterMeta: dynamicFilter.getResponseMeta(), +// }; +// } + +// /** +// * Parses bills list filter DTO. +// * @param filterDTO - +// */ +// private parseListFilterDTO(filterDTO) { +// return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO); +// } +// } diff --git a/packages/server-nest/src/modules/Bills/queries/GetDueBills.service.ts b/packages/server-nest/src/modules/Bills/queries/GetDueBills.service.ts new file mode 100644 index 000000000..4a6825a5d --- /dev/null +++ b/packages/server-nest/src/modules/Bills/queries/GetDueBills.service.ts @@ -0,0 +1,23 @@ +import { Injectable } from '@nestjs/common'; +import { Bill } from '../models/Bill'; + +@Injectable() +export class GetDueBills { + constructor(private billModel: typeof Bill) {} + + /** + * Retrieve all due bills or for specific given vendor id. + * @param {number} vendorId - + */ + public async getDueBills(vendorId?: number): Promise { + const dueBills = await this.billModel.query().onBuild((query) => { + query.orderBy('bill_date', 'DESC'); + query.modify('dueBills'); + + if (vendorId) { + query.where('vendor_id', vendorId); + } + }); + return dueBills; + } +} diff --git a/packages/server-nest/src/modules/ChromiumlyTenancy/ChromiumlyHtmlConvert.service.ts b/packages/server-nest/src/modules/ChromiumlyTenancy/ChromiumlyHtmlConvert.service.ts new file mode 100644 index 000000000..b79575c06 --- /dev/null +++ b/packages/server-nest/src/modules/ChromiumlyTenancy/ChromiumlyHtmlConvert.service.ts @@ -0,0 +1,72 @@ +import { Inject, Injectable } from '@nestjs/common'; +import path from 'path'; +import { promises as fs } from 'fs'; +import { PageProperties, PdfFormat } from '@/libs/Chromiumly/_types'; +import { UrlConverter } from '@/libs/Chromiumly/UrlConvert'; +import { Chromiumly } from '@/libs/Chromiumly/Chromiumly'; +import { + PDF_FILE_EXPIRE_IN, + getPdfFilePath, + getPdfFilesStorageDir, +} from './utils'; +import { Document } from './models/Document'; + +@Injectable() +export class ChromiumlyHtmlConvert { + /** + * @param {typeof Document} documentModel - Document model. + */ + constructor(@Inject(Document.name) private documentModel: typeof Document) {} + + /** + * Write HTML content to temporary file. + * @param {number} tenantId - Tenant id. + * @param {string} content - HTML content. + * @returns {Promise<[string, () => Promise]>} + */ + async writeTempHtmlFile( + content: string, + ): Promise<[string, () => Promise]> { + const filename = `document-print-${Date.now()}.html`; + const filePath = getPdfFilePath(filename); + + await fs.writeFile(filePath, content); + await this.documentModel + .query() + .insert({ key: filename, mimeType: 'text/html' }); + + const cleanup = async () => { + await fs.unlink(filePath); + await Document.query().where('key', filename).delete(); + }; + return [filename, cleanup]; + } + + /** + * Converts the given HTML content to PDF. + * @param {string} html + * @param {PageProperties} properties + * @param {PdfFormat} pdfFormat + * @returns {Array} + */ + async convert( + html: string, + properties?: PageProperties, + pdfFormat?: PdfFormat, + ): Promise { + const [filename, cleanupTempFile] = await this.writeTempHtmlFile(html); + const fileDir = getPdfFilesStorageDir(filename); + + const url = path.join(Chromiumly.GOTENBERG_DOCS_ENDPOINT, fileDir); + const urlConverter = new UrlConverter(); + + const buffer = await urlConverter.convert({ + url, + properties, + pdfFormat, + }); + await cleanupTempFile(); + + return buffer; + } +} diff --git a/packages/server-nest/src/modules/ChromiumlyTenancy/ChromiumlyTenancy.module.ts b/packages/server-nest/src/modules/ChromiumlyTenancy/ChromiumlyTenancy.module.ts new file mode 100644 index 000000000..64e10cf3e --- /dev/null +++ b/packages/server-nest/src/modules/ChromiumlyTenancy/ChromiumlyTenancy.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { ChromiumlyHtmlConvert } from './ChromiumlyHtmlConvert.service'; +import { ChromiumlyTenancy } from './ChromiumlyTenancy.service'; + +@Module({ + providers: [ChromiumlyHtmlConvert, ChromiumlyTenancy], +}) +export class ChromiumlyTenancyModule {} diff --git a/packages/server-nest/src/modules/ChromiumlyTenancy/ChromiumlyTenancy.service.ts b/packages/server-nest/src/modules/ChromiumlyTenancy/ChromiumlyTenancy.service.ts new file mode 100644 index 000000000..b38aa9f09 --- /dev/null +++ b/packages/server-nest/src/modules/ChromiumlyTenancy/ChromiumlyTenancy.service.ts @@ -0,0 +1,27 @@ +import { PageProperties, PdfFormat } from '@/libs/Chromiumly/_types'; +import { ChromiumlyHtmlConvert } from './ChromiumlyHtmlConvert.service'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class ChromiumlyTenancy { + constructor(private htmlConvert: ChromiumlyHtmlConvert) {} + + /** + * Converts the given HTML content to PDF. + * @param {string} content + * @param {PageProperties} properties + * @param {PdfFormat} pdfFormat + * @returns {Promise} + */ + public convertHtmlContent( + content: string, + properties?: PageProperties, + pdfFormat?: PdfFormat + ) { + const parsedProperties = { + margins: { top: 0, bottom: 0, left: 0, right: 0 }, + ...properties, + } + return this.htmlConvert.convert(content, parsedProperties, pdfFormat); + } +} diff --git a/packages/server-nest/src/modules/ChromiumlyTenancy/models/Document.ts b/packages/server-nest/src/modules/ChromiumlyTenancy/models/Document.ts new file mode 100644 index 000000000..5e34e968a --- /dev/null +++ b/packages/server-nest/src/modules/ChromiumlyTenancy/models/Document.ts @@ -0,0 +1,26 @@ +import { mixin } from 'objection'; +// import TenantModel from 'models/TenantModel'; +// import ModelSetting from './ModelSetting'; +// import ModelSearchable from './ModelSearchable'; +import { BaseModel } from '@/models/Model'; + +export class Document extends BaseModel { + public key: string; + public mimeType: string; + public size?: number; + public originName?: string; + + /** + * Table name + */ + static get tableName() { + return 'documents'; + } + + /** + * Model timestamps. + */ + get timestamps() { + return ['createdAt', 'updatedAt']; + } +} diff --git a/packages/server-nest/src/modules/ChromiumlyTenancy/models/DocumentLink.ts b/packages/server-nest/src/modules/ChromiumlyTenancy/models/DocumentLink.ts new file mode 100644 index 000000000..937ae5aa4 --- /dev/null +++ b/packages/server-nest/src/modules/ChromiumlyTenancy/models/DocumentLink.ts @@ -0,0 +1,47 @@ +import { Model, mixin } from 'objection'; +// import TenantModel from 'models/TenantModel'; +// import ModelSetting from './ModelSetting'; +// import ModelSearchable from './ModelSearchable'; +import { BaseModel } from '@/models/Model'; + +export class DocumentLink extends BaseModel{ + public modelRef: string; + public modelId: string; + public documentId: number; + public expiresAt?: Date; + + /** + * Table name + */ + static get tableName() { + return 'document_links'; + } + + /** + * Model timestamps. + */ + get timestamps() { + return ['createdAt', 'updatedAt']; + } + + /** + * Relationship mapping. + */ + static get relationMappings() { + const Document = require('./Document'); + + return { + /** + * Sale invoice associated entries. + */ + document: { + relation: Model.HasOneRelation, + modelClass: Document.default, + join: { + from: 'document_links.documentId', + to: 'documents.id', + }, + }, + }; + } +} diff --git a/packages/server-nest/src/modules/ChromiumlyTenancy/utils.ts b/packages/server-nest/src/modules/ChromiumlyTenancy/utils.ts new file mode 100644 index 000000000..fd7bf7bce --- /dev/null +++ b/packages/server-nest/src/modules/ChromiumlyTenancy/utils.ts @@ -0,0 +1,14 @@ +import path from 'path'; + +export const PDF_FILE_SUB_DIR = '/pdf'; +export const PDF_FILE_EXPIRE_IN = 40; // ms + +export const getPdfFilesStorageDir = (filename: string) => { + return path.join(PDF_FILE_SUB_DIR, filename); +}; + +export const getPdfFilePath = (filename: string) => { + const storageDir = getPdfFilesStorageDir(filename); + + return path.join(global.__storage_dir, storageDir); +}; diff --git a/packages/server-nest/src/modules/Contacts/Contact.transformer.ts b/packages/server-nest/src/modules/Contacts/Contact.transformer.ts new file mode 100644 index 000000000..385f45488 --- /dev/null +++ b/packages/server-nest/src/modules/Contacts/Contact.transformer.ts @@ -0,0 +1,40 @@ +import { isNull } from 'lodash'; +import { Transformer } from '../Transformer/Transformer'; +import { Contact } from './models/Contact'; + +export class ContactTransfromer extends Transformer { + /** + * Retrieve formatted expense amount. + * @param {IExpense} expense + * @returns {string} + */ + protected formattedBalance = (contact: Contact): string => { + return this.formatNumber(contact.balance, { + currencyCode: contact.currencyCode, + }); + }; + + /** + * Retrieve formatted expense landed cost amount. + * @param {IExpense} expense + * @returns {string} + */ + protected formattedOpeningBalance = (contact: Contact): string => { + return !isNull(contact.openingBalance) + ? this.formatNumber(contact.openingBalance, { + currencyCode: contact.currencyCode, + }) + : ''; + }; + + /** + * Retriecve fromatted date. + * @param {IExpense} expense + * @returns {string} + */ + protected formattedOpeningBalanceAt = (contact: Contact): string => { + return !isNull(contact.openingBalanceAt) + ? this.formatDate(contact.openingBalanceAt) + : ''; + }; +} diff --git a/packages/server-nest/src/modules/Contacts/types/Contacts.types.ts b/packages/server-nest/src/modules/Contacts/types/Contacts.types.ts new file mode 100644 index 000000000..1d5fd1016 --- /dev/null +++ b/packages/server-nest/src/modules/Contacts/types/Contacts.types.ts @@ -0,0 +1,124 @@ +export enum ContactService { + Customer = 'customer', + Vendor = 'vendor', +} + +// ---------------------------------- +export interface IContactAddress { + billingAddress1: string; + billingAddress2: string; + billingAddressCity: string; + billingAddressCountry: string; + billingAddressEmail: string; + billingAddressZipcode: string; + billingAddressPhone: string; + billingAddressState: string; + + shippingAddress1: string; + shippingAddress2: string; + shippingAddressCity: string; + shippingAddressCountry: string; + shippingAddressEmail: string; + shippingAddressZipcode: string; + shippingAddressPhone: string; + shippingAddressState: string; +} +export interface IContactAddressDTO { + billingAddress1?: string; + billingAddress2?: string; + billingAddressCity?: string; + billingAddressCountry?: string; + billingAddressEmail?: string; + billingAddressZipcode?: string; + billingAddressPhone?: string; + billingAddressState?: string; + + shippingAddress1?: string; + shippingAddress2?: string; + shippingAddressCity?: string; + shippingAddressCountry?: string; + shippingAddressEmail?: string; + shippingAddressZipcode?: string; + shippingAddressPhone?: string; + shippingAddressState?: string; +} +export interface IContact extends IContactAddress { + id?: number; + contactService: 'customer' | 'vendor'; + contactType: string; + + balance: number; + currencyCode: string; + + openingBalance: number; + openingBalanceExchangeRate: number; + localOpeningBalance?: number; + openingBalanceAt: Date; + openingBalanceBranchId: number; + + salutation: string; + firstName: string; + lastName: string; + companyName: string; + displayName: string; + + email: string; + website: string; + workPhone: string; + personalPhone: string; + + note: string; + active: boolean; +} +export interface IContactNewDTO { + contactType?: string; + + currencyCode?: string; + + openingBalance?: number; + openingBalanceAt?: string; + + salutation?: string; + firstName?: string; + lastName?: string; + companyName?: string; + displayName: string; + + website?: string; + email?: string; + workPhone?: string; + personalPhone?: string; + + note?: string; + active: boolean; +} +export interface IContactEditDTO { + contactType?: string; + + salutation?: string; + firstName?: string; + lastName?: string; + companyName?: string; + displayName: string; + + website?: string; + email?: string; + workPhone?: string; + personalPhone?: string; + + note?: string; + active: boolean; +} + +// export interface IContactsAutoCompleteFilter { +// limit: number; +// keyword: string; +// filterRoles?: IFilterRole[]; +// columnSortBy: string; +// sortOrder: string; +// } + +export interface IContactAutoCompleteItem { + displayName: string; + contactService: string; +} diff --git a/packages/server-nest/src/modules/CreditNotes/models/CreditNote.ts b/packages/server-nest/src/modules/CreditNotes/models/CreditNote.ts new file mode 100644 index 000000000..db3d0daa7 --- /dev/null +++ b/packages/server-nest/src/modules/CreditNotes/models/CreditNote.ts @@ -0,0 +1,322 @@ +import { mixin, Model, raw } from 'objection'; +// import TenantModel from 'models/TenantModel'; +// import ModelSetting from './ModelSetting'; +// import CustomViewBaseModel from './CustomViewBaseModel'; +// import { DEFAULT_VIEWS } from '@/services/CreditNotes/constants'; +// import ModelSearchable from './ModelSearchable'; +// import CreditNoteMeta from './CreditNote.Meta'; +import { BaseModel } from '@/models/Model'; + +export class CreditNote extends BaseModel { + customerId: number; + creditNoteDate: Date; + creditNoteNumber: string; + referenceNo: string; + amount: number; + exchangeRate: number; + refundedAmount: number; + invoicesAmount: number; + currencyCode: string; + note: string; + termsConditions: string; + openedAt: Date; + userId: number; + + /** + * Table name + */ + static get tableName() { + return 'credit_notes'; + } + + /** + * Timestamps columns. + */ + get timestamps() { + return ['created_at', 'updated_at']; + } + + /** + * Virtual attributes. + */ + static get virtualAttributes() { + return [ + 'localAmount', + 'isDraft', + 'isPublished', + 'isOpen', + 'isClosed', + 'creditsRemaining', + 'creditsUsed', + ]; + } + + /** + * Credit note amount in local currency. + * @returns {number} + */ + get localAmount() { + return this.amount * this.exchangeRate; + } + + /** + * Detarmines whether the credit note is draft. + * @returns {boolean} + */ + get isDraft() { + return !this.openedAt; + } + + /** + * Detarmines whether vendor credit is published. + * @returns {boolean} + */ + get isPublished() { + return !!this.openedAt; + } + + /** + * Detarmines whether the credit note is open. + * @return {boolean} + */ + get isOpen() { + return !!this.openedAt && this.creditsRemaining > 0; + } + + /** + * Detarmines whether the credit note is closed. + * @return {boolean} + */ + get isClosed() { + return this.openedAt && this.creditsRemaining === 0; + } + + /** + * Retrieve the credits remaining. + */ + get creditsRemaining() { + return Math.max(this.amount - this.refundedAmount - this.invoicesAmount, 0); + } + + get creditsUsed() { + return this.refundedAmount + this.invoicesAmount; + } + + /** + * Model modifiers. + */ + static get modifiers() { + return { + /** + * Filters the credit notes in draft status. + */ + draft(query) { + query.where('opened_at', null); + }, + + /** + * Filters the. + */ + published(query) { + query.whereNot('opened_at', null); + }, + + /** + * Filters the open credit notes. + */ + open(query) { + query + .where( + raw(`COALESCE(REFUNDED_AMOUNT) + COALESCE(INVOICES_AMOUNT) < + COALESCE(AMOUNT)`) + ) + .modify('published'); + }, + + /** + * Filters the closed credit notes. + */ + closed(query) { + query + .where( + raw(`COALESCE(REFUNDED_AMOUNT) + COALESCE(INVOICES_AMOUNT) = + COALESCE(AMOUNT)`) + ) + .modify('published'); + }, + + /** + * Status filter. + */ + filterByStatus(query, filterType) { + switch (filterType) { + case 'draft': + query.modify('draft'); + break; + case 'published': + query.modify('published'); + break; + case 'open': + default: + query.modify('open'); + break; + case 'closed': + query.modify('closed'); + break; + } + }, + + /** + * + */ + sortByStatus(query, order) { + query.orderByRaw( + `COALESCE(REFUNDED_AMOUNT) + COALESCE(INVOICES_AMOUNT) = COALESCE(AMOUNT) ${order}` + ); + }, + }; + } + + /** + * Relationship mapping. + */ + static get relationMappings() { + const AccountTransaction = require('models/AccountTransaction'); + const ItemEntry = require('models/ItemEntry'); + const Customer = require('models/Customer'); + const Branch = require('models/Branch'); + const Document = require('models/Document'); + const Warehouse = require('models/Warehouse'); + + return { + /** + * Credit note associated entries. + */ + entries: { + relation: Model.HasManyRelation, + modelClass: ItemEntry.default, + join: { + from: 'credit_notes.id', + to: 'items_entries.referenceId', + }, + filter(builder) { + builder.where('reference_type', 'CreditNote'); + builder.orderBy('index', 'ASC'); + }, + }, + + /** + * Belongs to customer model. + */ + customer: { + relation: Model.BelongsToOneRelation, + modelClass: Customer.default, + join: { + from: 'credit_notes.customerId', + to: 'contacts.id', + }, + filter(query) { + query.where('contact_service', 'Customer'); + }, + }, + + /** + * Credit note associated GL entries. + */ + transactions: { + relation: Model.HasManyRelation, + modelClass: AccountTransaction.default, + join: { + from: 'credit_notes.id', + to: 'accounts_transactions.referenceId', + }, + filter(builder) { + builder.where('reference_type', 'CreditNote'); + }, + }, + + /** + * Credit note may belongs to branch. + */ + branch: { + relation: Model.BelongsToOneRelation, + modelClass: Branch.default, + join: { + from: 'credit_notes.branchId', + to: 'branches.id', + }, + }, + + /** + * Credit note may has associated warehouse. + */ + warehouse: { + relation: Model.BelongsToOneRelation, + modelClass: Warehouse.default, + join: { + from: 'credit_notes.warehouseId', + to: 'warehouses.id', + }, + }, + + /** + * Credit note may has many attached attachments. + */ + attachments: { + relation: Model.ManyToManyRelation, + modelClass: Document.default, + join: { + from: 'credit_notes.id', + through: { + from: 'document_links.modelId', + to: 'document_links.documentId', + }, + to: 'documents.id', + }, + filter(query) { + query.where('model_ref', 'CreditNote'); + }, + }, + }; + } + + /** + * Sale invoice meta. + */ + // static get meta() { + // return CreditNoteMeta; + // } + + // /** + // * Retrieve the default custom views, roles and columns. + // */ + // static get defaultViews() { + // return DEFAULT_VIEWS; + // } + + /** + * Model searchable. + */ + static get searchable() { + return true; + } + + /** + * Model search attributes. + */ + static get searchRoles() { + return [ + { fieldKey: 'credit_number', comparator: 'contains' }, + { condition: 'or', fieldKey: 'reference_no', comparator: 'contains' }, + { condition: 'or', fieldKey: 'amount', comparator: 'equals' }, + ]; + } + + /** + * Prevents mutate base currency since the model is not empty. + * @returns {boolean} + */ + static get preventMutateBaseCurrency() { + return true; + } +} diff --git a/packages/server-nest/src/modules/CreditNotes/models/CreditNoteAppliedInvoice.ts b/packages/server-nest/src/modules/CreditNotes/models/CreditNoteAppliedInvoice.ts new file mode 100644 index 000000000..f302bcedb --- /dev/null +++ b/packages/server-nest/src/modules/CreditNotes/models/CreditNoteAppliedInvoice.ts @@ -0,0 +1,50 @@ +import { mixin, Model } from 'objection'; +// import TenantModel from 'models/TenantModel'; +// import ModelSetting from './ModelSetting'; +// import CustomViewBaseModel from './CustomViewBaseModel'; +// import ModelSearchable from './ModelSearchable'; +import { BaseModel } from '@/models/Model'; + +export class CreditNoteAppliedInvoice extends BaseModel { + /** + * Table name + */ + static get tableName() { + return 'credit_note_applied_invoice'; + } + + /** + * Timestamps columns. + */ + get timestamps() { + return ['created_at', 'updated_at']; + } + + /** + * Relationship mapping. + */ + static get relationMappings() { + const SaleInvoice = require('models/SaleInvoice'); + const CreditNote = require('models/CreditNote'); + + return { + saleInvoice: { + relation: Model.BelongsToOneRelation, + modelClass: SaleInvoice.default, + join: { + from: 'credit_note_applied_invoice.invoiceId', + to: 'sales_invoices.id', + }, + }, + + creditNote: { + relation: Model.BelongsToOneRelation, + modelClass: CreditNote.default, + join: { + from: 'credit_note_applied_invoice.creditNoteId', + to: 'credit_notes.id', + }, + }, + }; + } +} diff --git a/packages/server-nest/src/modules/Customers/CustomerGLEntries.ts b/packages/server-nest/src/modules/Customers/CustomerGLEntries.ts new file mode 100644 index 000000000..dc900c5a8 --- /dev/null +++ b/packages/server-nest/src/modules/Customers/CustomerGLEntries.ts @@ -0,0 +1,117 @@ +// import { Service, Inject } from 'typedi'; +// import { AccountNormal, ICustomer, ILedgerEntry } from '@/interfaces'; +// import Ledger from '@/services/Accounting/Ledger'; + +// @Service() +// export class CustomerGLEntries { +// /** +// * Retrieves the customer opening balance common entry attributes. +// * @param {ICustomer} customer +// */ +// private getCustomerOpeningGLCommonEntry = (customer: ICustomer) => { +// return { +// exchangeRate: customer.openingBalanceExchangeRate, +// currencyCode: customer.currencyCode, + +// transactionType: 'CustomerOpeningBalance', +// transactionId: customer.id, + +// date: customer.openingBalanceAt, +// userId: customer.userId, +// contactId: customer.id, + +// credit: 0, +// debit: 0, + +// branchId: customer.openingBalanceBranchId, +// }; +// }; + +// /** +// * Retrieves the customer opening GL credit entry. +// * @param {number} ARAccountId +// * @param {ICustomer} customer +// * @returns {ILedgerEntry} +// */ +// private getCustomerOpeningGLCreditEntry = ( +// ARAccountId: number, +// customer: ICustomer +// ): ILedgerEntry => { +// const commonEntry = this.getCustomerOpeningGLCommonEntry(customer); + +// return { +// ...commonEntry, +// credit: 0, +// debit: customer.localOpeningBalance, +// accountId: ARAccountId, +// accountNormal: AccountNormal.DEBIT, +// index: 1, +// }; +// }; + +// /** +// * Retrieves the customer opening GL debit entry. +// * @param {number} incomeAccountId +// * @param {ICustomer} customer +// * @returns {ILedgerEntry} +// */ +// private getCustomerOpeningGLDebitEntry = ( +// incomeAccountId: number, +// customer: ICustomer +// ): ILedgerEntry => { +// const commonEntry = this.getCustomerOpeningGLCommonEntry(customer); + +// return { +// ...commonEntry, +// credit: customer.localOpeningBalance, +// debit: 0, +// accountId: incomeAccountId, +// accountNormal: AccountNormal.CREDIT, + +// index: 2, +// }; +// }; + +// /** +// * Retrieves the customer opening GL entries. +// * @param {number} ARAccountId +// * @param {number} incomeAccountId +// * @param {ICustomer} customer +// * @returns {ILedgerEntry[]} +// */ +// public getCustomerOpeningGLEntries = ( +// ARAccountId: number, +// incomeAccountId: number, +// customer: ICustomer +// ) => { +// const debitEntry = this.getCustomerOpeningGLDebitEntry( +// incomeAccountId, +// customer +// ); +// const creditEntry = this.getCustomerOpeningGLCreditEntry( +// ARAccountId, +// customer +// ); +// return [debitEntry, creditEntry]; +// }; + +// /** +// * Retrieves the customer opening balance ledger. +// * @param {number} ARAccountId +// * @param {number} incomeAccountId +// * @param {ICustomer} customer +// * @returns {ILedger} +// */ +// public getCustomerOpeningLedger = ( +// ARAccountId: number, +// incomeAccountId: number, +// customer: ICustomer +// ) => { +// const entries = this.getCustomerOpeningGLEntries( +// ARAccountId, +// incomeAccountId, +// customer +// ); +// return new Ledger(entries); +// }; +// } diff --git a/packages/server-nest/src/modules/Customers/CustomerGLEntriesStorage.ts b/packages/server-nest/src/modules/Customers/CustomerGLEntriesStorage.ts new file mode 100644 index 000000000..3d5a7ca15 --- /dev/null +++ b/packages/server-nest/src/modules/Customers/CustomerGLEntriesStorage.ts @@ -0,0 +1,90 @@ +// import { Knex } from 'knex'; +// import LedgerStorageService from '@/services/Accounting/LedgerStorageService'; +// import HasTenancyService from '@/services/Tenancy/TenancyService'; +// import { Service, Inject } from 'typedi'; +// import { CustomerGLEntries } from './CustomerGLEntries'; + +// @Service() +// export class CustomerGLEntriesStorage { +// @Inject() +// private tenancy: HasTenancyService; + +// @Inject() +// private ledegrRepository: LedgerStorageService; + +// @Inject() +// private customerGLEntries: CustomerGLEntries; + +// /** +// * Customer opening balance journals. +// * @param {number} tenantId +// * @param {number} customerId +// * @param {Knex.Transaction} trx +// */ +// public writeCustomerOpeningBalance = async ( +// tenantId: number, +// customerId: number, +// trx?: Knex.Transaction +// ) => { +// const { Customer } = this.tenancy.models(tenantId); +// const { accountRepository } = this.tenancy.repositories(tenantId); + +// const customer = await Customer.query(trx).findById(customerId); + +// // Finds the income account. +// const incomeAccount = await accountRepository.findOne({ +// slug: 'other-income', +// }); +// // Find or create the A/R account. +// const ARAccount = await accountRepository.findOrCreateAccountReceivable( +// customer.currencyCode, +// {}, +// trx +// ); +// // Retrieves the customer opening balance ledger. +// const ledger = this.customerGLEntries.getCustomerOpeningLedger( +// ARAccount.id, +// incomeAccount.id, +// customer +// ); +// // Commits the ledger entries to the storage. +// await this.ledegrRepository.commit(tenantId, ledger, trx); +// }; + +// /** +// * Reverts the customer opening balance GL entries. +// * @param {number} tenantId +// * @param {number} customerId +// * @param {Knex.Transaction} trx +// */ +// public revertCustomerOpeningBalance = async ( +// tenantId: number, +// customerId: number, +// trx?: Knex.Transaction +// ) => { +// await this.ledegrRepository.deleteByReference( +// tenantId, +// customerId, +// 'CustomerOpeningBalance', +// trx +// ); +// }; + +// /** +// * Writes the customer opening balance GL entries. +// * @param {number} tenantId +// * @param {number} customerId +// * @param {Knex.Transaction} trx +// */ +// public rewriteCustomerOpeningBalance = async ( +// tenantId: number, +// customerId: number, +// trx?: Knex.Transaction +// ) => { +// // Reverts the customer opening balance entries. +// await this.revertCustomerOpeningBalance(tenantId, customerId, trx); + +// // Write the customer opening balance entries. +// await this.writeCustomerOpeningBalance(tenantId, customerId, trx); +// }; +// } diff --git a/packages/server-nest/src/modules/Customers/Customers.controller.ts b/packages/server-nest/src/modules/Customers/Customers.controller.ts new file mode 100644 index 000000000..b26d9a206 --- /dev/null +++ b/packages/server-nest/src/modules/Customers/Customers.controller.ts @@ -0,0 +1,54 @@ +import { + Body, + Controller, + Delete, + Get, + Param, + Post, + Put, +} from '@nestjs/common'; +import { CustomersApplication } from './CustomersApplication.service'; +import { + ICustomerEditDTO, + ICustomerNewDTO, + ICustomerOpeningBalanceEditDTO, +} from './types/Customers.types'; + +@Controller('customers') +export class CustomersController { + constructor(private customersApplication: CustomersApplication) {} + + @Get(':id') + getCustomer(@Param('id') customerId: number) { + return this.customersApplication.getCustomer(customerId); + } + + @Post() + createCustomer(@Body() customerDTO: ICustomerNewDTO) { + return this.customersApplication.createCustomer(customerDTO); + } + + @Put(':id') + editCustomer( + @Param('id') customerId: number, + @Body() customerDTO: ICustomerEditDTO, + ) { + return this.customersApplication.editCustomer(customerId, customerDTO); + } + + @Delete(':id') + deleteCustomer(@Param('id') customerId: number) { + return this.customersApplication.deleteCustomer(customerId); + } + + @Put(':id/opening-balance') + editOpeningBalance( + @Param('id') customerId: number, + @Body() openingBalanceDTO: ICustomerOpeningBalanceEditDTO, + ) { + return this.customersApplication.editOpeningBalance( + customerId, + openingBalanceDTO, + ); + } +} diff --git a/packages/server-nest/src/modules/Customers/Customers.module.ts b/packages/server-nest/src/modules/Customers/Customers.module.ts new file mode 100644 index 000000000..0c22e425d --- /dev/null +++ b/packages/server-nest/src/modules/Customers/Customers.module.ts @@ -0,0 +1,34 @@ +import { Module } from '@nestjs/common'; +import { TenancyContext } from '../Tenancy/TenancyContext.service'; +import { TenancyDatabaseModule } from '../Tenancy/TenancyDB/TenancyDB.module'; +import { TransformerInjectable } from '../Transformer/TransformerInjectable.service'; +import { ActivateCustomer } from './commands/ActivateCustomer.service'; +import { CreateCustomer } from './commands/CreateCustomer.service'; +import { CustomerValidators } from './commands/CustomerValidators.service'; +import { EditCustomer } from './commands/EditCustomer.service'; +import { EditOpeningBalanceCustomer } from './commands/EditOpeningBalanceCustomer.service'; +import { GetCustomerService } from './commands/GetCustomer.service'; +import { CreateEditCustomerDTO } from './commands/CreateEditCustomerDTO.service'; +import { CustomersController } from './Customers.controller'; +import { CustomersApplication } from './CustomersApplication.service'; +import { DeleteCustomer } from './commands/DeleteCustomer.service'; + +@Module({ + imports: [TenancyDatabaseModule], + controllers: [CustomersController], + providers: [ + ActivateCustomer, + CreateCustomer, + CustomerValidators, + EditCustomer, + EditOpeningBalanceCustomer, + CustomerValidators, + CreateEditCustomerDTO, + GetCustomerService, + CustomersApplication, + DeleteCustomer, + TenancyContext, + TransformerInjectable, + ], +}) +export class CustomersModule {} diff --git a/packages/server-nest/src/modules/Customers/CustomersApplication.service.ts b/packages/server-nest/src/modules/Customers/CustomersApplication.service.ts new file mode 100644 index 000000000..3ae351df1 --- /dev/null +++ b/packages/server-nest/src/modules/Customers/CustomersApplication.service.ts @@ -0,0 +1,89 @@ +import { Injectable } from '@nestjs/common'; +import { GetCustomerService } from './commands/GetCustomer.service'; +import { CreateCustomer } from './commands/CreateCustomer.service'; +import { EditCustomer } from './commands/EditCustomer.service'; +import { DeleteCustomer } from './commands/DeleteCustomer.service'; +import { EditOpeningBalanceCustomer } from './commands/EditOpeningBalanceCustomer.service'; +import { + ICustomerEditDTO, + ICustomerNewDTO, + ICustomerOpeningBalanceEditDTO, + // ICustomersFilter, +} from './types/Customers.types'; + +@Injectable() +export class CustomersApplication { + constructor( + private getCustomerService: GetCustomerService, + private createCustomerService: CreateCustomer, + private editCustomerService: EditCustomer, + private deleteCustomerService: DeleteCustomer, + private editOpeningBalanceService: EditOpeningBalanceCustomer, + // private getCustomersService: GetCustomers, + ) {} + + /** + * Retrieves the given customer details. + * @param {number} tenantId + * @param {number} customerId + */ + public getCustomer = (customerId: number) => { + return this.getCustomerService.getCustomer(customerId); + }; + + /** + * Creates a new customer. + * @param {ICustomerNewDTO} customerDTO + * @returns {Promise} + */ + public createCustomer = (customerDTO: ICustomerNewDTO) => { + return this.createCustomerService.createCustomer(customerDTO); + }; + + /** + * Edits details of the given customer. + * @param {number} customerId - Customer id. + * @param {ICustomerEditDTO} customerDTO - Customer edit DTO. + * @return {Promise} + */ + public editCustomer = (customerId: number, customerDTO: ICustomerEditDTO) => { + return this.editCustomerService.editCustomer(customerId, customerDTO); + }; + + /** + * Deletes the given customer and associated transactions. + * @param {number} tenantId + * @param {number} customerId + * @param {ISystemUser} authorizedUser + * @returns {Promise} + */ + public deleteCustomer = (customerId: number) => { + return this.deleteCustomerService.deleteCustomer(customerId); + }; + + /** + * Changes the opening balance of the given customer. + * @param {number} tenantId + * @param {number} customerId + * @param {Date|string} openingBalanceEditDTO + * @returns {Promise} + */ + public editOpeningBalance = ( + customerId: number, + openingBalanceEditDTO: ICustomerOpeningBalanceEditDTO, + ) => { + return this.editOpeningBalanceService.changeOpeningBalance( + customerId, + openingBalanceEditDTO, + ); + }; + + /** + * Retrieve customers paginated list. + * @param {number} tenantId - Tenant id. + * @param {ICustomersFilter} filter - Cusotmers filter. + */ + // public getCustomers = (filterDTO: ICustomersFilter) => { + // return this.getCustomersService.getCustomersList(filterDTO); + // }; +} diff --git a/packages/server-nest/src/modules/Customers/CustomersExportable.ts b/packages/server-nest/src/modules/Customers/CustomersExportable.ts new file mode 100644 index 000000000..0384df0d1 --- /dev/null +++ b/packages/server-nest/src/modules/Customers/CustomersExportable.ts @@ -0,0 +1,30 @@ +// import { Inject, Service } from 'typedi'; +// import { IItemsFilter } from '@/interfaces'; +// import { CustomersApplication } from './CustomersApplication'; +// import { Exportable } from '@/services/Export/Exportable'; +// import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants'; + +// @Service() +// export class CustomersExportable extends Exportable { +// @Inject() +// private customersApplication: CustomersApplication; + +// /** +// * Retrieves the accounts data to exportable sheet. +// * @param {number} tenantId +// * @returns +// */ +// public exportable(tenantId: number, query: IItemsFilter) { +// const parsedQuery = { +// sortOrder: 'DESC', +// columnSortBy: 'created_at', +// ...query, +// page: 1, +// pageSize: EXPORT_SIZE_LIMIT, +// } as IItemsFilter; + +// return this.customersApplication +// .getCustomers(tenantId, parsedQuery) +// .then((output) => output.customers); +// } +// } diff --git a/packages/server-nest/src/modules/Customers/CustomersImportable.ts b/packages/server-nest/src/modules/Customers/CustomersImportable.ts new file mode 100644 index 000000000..b3a7eeb59 --- /dev/null +++ b/packages/server-nest/src/modules/Customers/CustomersImportable.ts @@ -0,0 +1,34 @@ +// import { Inject, Service } from 'typedi'; +// import { Importable } from '@/services/Import/Importable'; +// import { CreateCustomer } from './CRUD/CreateCustomer'; +// import { Knex } from 'knex'; +// import { ICustomer, ICustomerNewDTO } from '@/interfaces'; +// import { CustomersSampleData } from './_SampleData'; + +// @Service() +// export class CustomersImportable extends Importable { +// @Inject() +// private createCustomerService: CreateCustomer; + +// /** +// * Mapps the imported data to create a new customer service. +// * @param {number} tenantId +// * @param {ICustomerNewDTO} createDTO +// * @param {Knex.Transaction} trx +// * @returns {Promise} +// */ +// public async importable( +// tenantId: number, +// createDTO: ICustomerNewDTO, +// trx?: Knex.Transaction +// ): Promise { +// await this.createCustomerService.createCustomer(tenantId, createDTO, trx); +// } + +// /** +// * Retrieves the sample data of customers used to download sample sheet. +// */ +// public sampleData(): any[] { +// return CustomersSampleData; +// } +// } diff --git a/packages/server-nest/src/modules/Customers/_SampleData.ts b/packages/server-nest/src/modules/Customers/_SampleData.ts new file mode 100644 index 000000000..601d845d5 --- /dev/null +++ b/packages/server-nest/src/modules/Customers/_SampleData.ts @@ -0,0 +1,158 @@ + +export const CustomersSampleData = [ + { + "Customer Type": "Business", + "First Name": "Nicolette", + "Last Name": "Schamberger", + "Company Name": "Homenick - Hane", + "Display Name": "Rowland Rowe", + "Email": "cicero86@yahoo.com", + "Personal Phone Number": "811-603-2235", + "Work Phone Number": "906-993-5190", + "Website": "http://google.com", + "Opening Balance": 54302.23, + "Opening Balance At": "2022-02-02", + "Opening Balance Ex. Rate": 2, + "Currency": "LYD", + "Active": "F", + "Note": "Doloribus autem optio temporibus dolores mollitia sit.", + "Billing Address 1": "862 Jessika Well", + "Billing Address 2": "1091 Dorthy Mount", + "Billing Address City": "Deckowfort", + "Billing Address Country": "Ghana", + "Billing Address Phone": "825-011-5207", + "Billing Address Postcode": "38228", + "Billing Address State": "Oregon", + "Shipping Address 1": "37626 Thiel Villages", + "Shipping Address 2": "132 Batz Avenue", + "Shipping Address City": "Pagacburgh", + "Shipping Address Country": "Albania", + "Shipping Address Phone": "171-546-3701", + "Shipping Address Postcode": "13709", + "Shipping Address State": "Georgia" + }, + { + "Customer Type": "Business", + "First Name": "Hermann", + "Last Name": "Crooks", + "Company Name": "Veum - Schaefer", + "Display Name": "Harley Veum", + "Email": "immanuel56@hotmail.com", + "Personal Phone Number": "449-780-9999", + "Work Phone Number": "970-473-5785", + "Website": "http://google.com", + "Opening Balance": 54302.23, + "Opening Balance At": "2022-02-02", + "Opening Balance Ex. Rate": 2, + "Currency": "LYD", + "Active": "T", + "Note": "Doloribus dolore dolor dicta vitae in fugit nisi quibusdam.", + "Billing Address 1": "532 Simonis Spring", + "Billing Address 2": "3122 Nicolas Inlet", + "Billing Address City": "East Matteofort", + "Billing Address Country": "Holy See (Vatican City State)", + "Billing Address Phone": "366-084-8629", + "Billing Address Postcode": "41607", + "Billing Address State": "Montana", + "Shipping Address 1": "2889 Tremblay Plaza", + "Shipping Address 2": "71355 Kutch Isle", + "Shipping Address City": "D'Amorehaven", + "Shipping Address Country": "Monaco", + "Shipping Address Phone": "614-189-3328", + "Shipping Address Postcode": "09634-0435", + "Shipping Address State": "Nevada" + }, + { + "Customer Type": "Business", + "First Name": "Nellie", + "Last Name": "Gulgowski", + "Company Name": "Boyle, Heller and Jones", + "Display Name": "Randall Kohler", + "Email": "anibal_frami@yahoo.com", + "Personal Phone Number": "498-578-0740", + "Work Phone Number": "394-550-6827", + "Website": "http://google.com", + "Opening Balance": 54302.23, + "Opening Balance At": "2022-02-02", + "Opening Balance Ex. Rate": 2, + "Currency": "LYD", + "Active": "T", + "Note": "Vero quibusdam rem fugit aperiam est modi.", + "Billing Address 1": "214 Sauer Villages", + "Billing Address 2": "30687 Kacey Square", + "Billing Address City": "Jayceborough", + "Billing Address Country": "Benin", + "Billing Address Phone": "332-820-1127", + "Billing Address Postcode": "16425-3887", + "Billing Address State": "Mississippi", + "Shipping Address 1": "562 Diamond Loaf", + "Shipping Address 2": "9595 Satterfield Trafficway", + "Shipping Address City": "Alexandrinefort", + "Shipping Address Country": "Puerto Rico", + "Shipping Address Phone": "776-500-8456", + "Shipping Address Postcode": "30258", + "Shipping Address State": "South Dakota" + }, + { + "Customer Type": "Business", + "First Name": "Stone", + "Last Name": "Jerde", + "Company Name": "Cassin, Casper and Maggio", + "Display Name": "Clint McLaughlin", + "Email": "nathanael22@yahoo.com", + "Personal Phone Number": "562-790-6059", + "Work Phone Number": "686-838-0027", + "Website": "http://google.com", + "Opening Balance": 54302.23, + "Opening Balance At": "2022-02-02", + "Opening Balance Ex. Rate": 2, + "Currency": "LYD", + "Active": "F", + "Note": "Quis cumque molestias rerum.", + "Billing Address 1": "22590 Cathy Harbor", + "Billing Address 2": "24493 Brycen Brooks", + "Billing Address City": "Elnorashire", + "Billing Address Country": "Andorra", + "Billing Address Phone": "701-852-8005", + "Billing Address Postcode": "5680", + "Billing Address State": "Nevada", + "Shipping Address 1": "5355 Erdman Bridge", + "Shipping Address 2": "421 Jeanette Camp", + "Shipping Address City": "East Philip", + "Shipping Address Country": "Venezuela", + "Shipping Address Phone": "426-119-0858", + "Shipping Address Postcode": "34929-0501", + "Shipping Address State": "Tennessee" + }, + { + "Customer Type": "Individual", + "First Name": "Lempi", + "Last Name": "Kling", + "Company Name": "Schamberger, O'Connell and Bechtelar", + "Display Name": "Alexie Barton", + "Email": "eulah.kreiger@hotmail.com", + "Personal Phone Number": "745-756-1063", + "Work Phone Number": "965-150-1945", + "Website": "http://google.com", + "Opening Balance": 54302.23, + "Opening Balance At": "2022-02-02", + "Opening Balance Ex. Rate": 2, + "Currency": "LYD", + "Active": "F", + "Note": "Maxime laboriosam hic voluptate maiores est officia.", + "Billing Address 1": "0851 Jones Flat", + "Billing Address 2": "845 Bailee Drives", + "Billing Address City": "Kamrenport", + "Billing Address Country": "Niger", + "Billing Address Phone": "220-125-0608", + "Billing Address Postcode": "30311", + "Billing Address State": "Delaware", + "Shipping Address 1": "929 Ferry Row", + "Shipping Address 2": "020 Adam Plaza", + "Shipping Address City": "West Carmellaside", + "Shipping Address Country": "Ghana", + "Shipping Address Phone": "053-333-6679", + "Shipping Address Postcode": "79221-4681", + "Shipping Address State": "Illinois" + } +] diff --git a/packages/server-nest/src/modules/Customers/commands/ActivateCustomer.service.ts b/packages/server-nest/src/modules/Customers/commands/ActivateCustomer.service.ts new file mode 100644 index 000000000..86aa27de1 --- /dev/null +++ b/packages/server-nest/src/modules/Customers/commands/ActivateCustomer.service.ts @@ -0,0 +1,66 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { CustomerValidators } from './CustomerValidators.service'; +import { + ICustomerActivatedPayload, + ICustomerActivatingPayload, +} from '../types/Customers.types'; +import { Customer } from '@/modules/Customers/models/Customer'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service'; +import { events } from '@/common/events/events'; +import { Knex } from 'knex'; + +@Injectable() +export class ActivateCustomer { + /** + * @param {UnitOfWork} uow - Unit of work service. + * @param {EventEmitter2} eventPublisher - Event emitter service. + * @param {CustomerValidators} validators - Customer validators service. + * @param {typeof Customer} customerModel - Customer model. + */ + constructor( + private uow: UnitOfWork, + private eventPublisher: EventEmitter2, + private validators: CustomerValidators, + + @Inject(Customer.name) + private customerModel: typeof Customer, + ) {} + + /** + * Inactive the given contact. + * @param {number} customerId - Customer id. + * @returns {Promise} + */ + public async activateCustomer(customerId: number): Promise { + // Retrieves the customer or throw not found error. + const oldCustomer = await this.customerModel + .query() + .findById(customerId) + .throwIfNotFound(); + + this.validators.validateNotAlreadyPublished(oldCustomer); + + // Edits the given customer with associated transactions on unit-of-work environment. + return this.uow.withTransaction(async (trx: Knex.Transaction) => { + // Triggers `onCustomerActivating` event. + await this.eventPublisher.emitAsync(events.customers.onActivating, { + trx, + oldCustomer, + } as ICustomerActivatingPayload); + + // Update the given customer details. + const customer = await this.customerModel + .query(trx) + .findById(customerId) + .updateAndFetchById(customerId, { active: true }); + + // Triggers `onCustomerActivated` event. + await this.eventPublisher.emitAsync(events.customers.onActivated, { + trx, + oldCustomer, + customer, + } as ICustomerActivatedPayload); + }); + } +} diff --git a/packages/server-nest/src/modules/Customers/commands/CreateCustomer.service.ts b/packages/server-nest/src/modules/Customers/commands/CreateCustomer.service.ts new file mode 100644 index 000000000..a9d5a0a8c --- /dev/null +++ b/packages/server-nest/src/modules/Customers/commands/CreateCustomer.service.ts @@ -0,0 +1,65 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Knex } from 'knex'; +import { CreateEditCustomerDTO } from './CreateEditCustomerDTO.service'; +import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { Customer } from '../models/Customer'; +import { events } from '@/common/events/events'; +import { + ICustomerEventCreatedPayload, + ICustomerEventCreatingPayload, + ICustomerNewDTO, +} from '../types/Customers.types'; + +@Injectable() +export class CreateCustomer { + /** + * @param {UnitOfWork} uow - Unit of work service. + * @param {EventEmitter2} eventPublisher - Event emitter service. + * @param {CreateEditCustomerDTO} customerDTO - Customer DTO. + * @param {typeof Customer} customerModel - Customer model. + */ + constructor( + private readonly uow: UnitOfWork, + private readonly eventPublisher: EventEmitter2, + private readonly customerDTO: CreateEditCustomerDTO, + + @Inject(Customer.name) + private readonly customerModel: typeof Customer, + ) {} + + /** + * Creates a new customer. + * @param {ICustomerNewDTO} customerDTO + * @return {Promise} + */ + public async createCustomer( + customerDTO: ICustomerNewDTO, + trx?: Knex.Transaction, + ): Promise { + // Transformes the customer DTO to customer object. + const customerObj = await this.customerDTO.transformCreateDTO(customerDTO); + + // Creates a new customer under unit-of-work envirement. + return this.uow.withTransaction(async (trx: Knex.Transaction) => { + // Triggers `onCustomerCreating` event. + await this.eventPublisher.emitAsync(events.customers.onCreating, { + customerDTO, + trx, + } as ICustomerEventCreatingPayload); + + // Creates a new contact as customer. + const customer = await this.customerModel.query(trx).insertAndFetch({ + ...customerObj, + }); + // Triggers `onCustomerCreated` event. + await this.eventPublisher.emitAsync(events.customers.onCreated, { + customer, + customerId: customer.id, + trx, + } as ICustomerEventCreatedPayload); + + return customer; + }, trx); + } +} diff --git a/packages/server-nest/src/modules/Customers/commands/CreateEditCustomerDTO.service.ts b/packages/server-nest/src/modules/Customers/commands/CreateEditCustomerDTO.service.ts new file mode 100644 index 000000000..056256ab7 --- /dev/null +++ b/packages/server-nest/src/modules/Customers/commands/CreateEditCustomerDTO.service.ts @@ -0,0 +1,73 @@ +import moment from 'moment'; +import { defaultTo, omit, isEmpty } from 'lodash'; +import { Injectable } from '@nestjs/common'; +import { Customer } from '../models/Customer'; +import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service'; +import { ICustomerEditDTO, ICustomerNewDTO } from '../types/Customers.types'; +import { ContactService } from '@/modules/Contacts/types/Contacts.types'; + +@Injectable() +export class CreateEditCustomerDTO { + /** + * @param {TenancyContext} tenancyContext - Tenancy context service. + */ + constructor(private readonly tenancyContext: TenancyContext) {} + + /** + * Transformes the create/edit DTO. + * @param {ICustomerNewDTO | ICustomerEditDTO} customerDTO + * @returns + */ + private transformCommonDTO = ( + customerDTO: ICustomerNewDTO | ICustomerEditDTO, + ): Partial => { + return { + ...omit(customerDTO, ['customerType']), + contactType: customerDTO.customerType, + }; + }; + + /** + * Transformes the create DTO. + * @param {ICustomerNewDTO} customerDTO + * @returns {Promise>} + */ + public transformCreateDTO = async (customerDTO: ICustomerNewDTO) => { + const commonDTO = this.transformCommonDTO(customerDTO); + + // Retrieves the tenant metadata. + const tenantMeta = await this.tenancyContext.getTenant(true); + + return { + ...commonDTO, + currencyCode: + commonDTO.currencyCode || tenantMeta?.metadata?.baseCurrency, + active: defaultTo(customerDTO.active, true), + contactService: ContactService.Customer, + ...(!isEmpty(customerDTO.openingBalanceAt) + ? { + openingBalanceAt: moment( + customerDTO?.openingBalanceAt, + ).toMySqlDateTime(), + } + : {}), + openingBalanceExchangeRate: defaultTo( + customerDTO.openingBalanceExchangeRate, + 1, + ), + }; + }; + + /** + * Transformes the edit DTO. + * @param {ICustomerEditDTO} customerDTO + * @returns + */ + public transformEditDTO = (customerDTO: ICustomerEditDTO) => { + const commonDTO = this.transformCommonDTO(customerDTO); + + return { + ...commonDTO, + }; + }; +} diff --git a/packages/server-nest/src/modules/Customers/commands/CustomerValidators.service.ts b/packages/server-nest/src/modules/Customers/commands/CustomerValidators.service.ts new file mode 100644 index 000000000..32269ce82 --- /dev/null +++ b/packages/server-nest/src/modules/Customers/commands/CustomerValidators.service.ts @@ -0,0 +1,17 @@ +import { ERRORS } from '../constants'; +import { Injectable } from '@nestjs/common'; +import { Customer } from '../models/Customer'; +import { ServiceError } from '@/modules/Items/ServiceError'; + +@Injectable() +export class CustomerValidators { + /** + * Validates the given customer is not already published. + * @param {ICustomer} customer + */ + public validateNotAlreadyPublished = (customer: Customer) => { + if (customer.active) { + throw new ServiceError(ERRORS.CUSTOMER_ALREADY_ACTIVE); + } + }; +} diff --git a/packages/server-nest/src/modules/Customers/commands/DeleteCustomer.service.ts b/packages/server-nest/src/modules/Customers/commands/DeleteCustomer.service.ts new file mode 100644 index 000000000..eea015cab --- /dev/null +++ b/packages/server-nest/src/modules/Customers/commands/DeleteCustomer.service.ts @@ -0,0 +1,62 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Knex } from 'knex'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { + ICustomerDeletingPayload, + ICustomerEventDeletedPayload, +} from '../types/Customers.types'; +import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service'; +import { Customer } from '../models/Customer'; +import { events } from '@/common/events/events'; + +@Injectable() +export class DeleteCustomer { + /** + * @param {UnitOfWork} uow - Unit of work service. + * @param {EventEmitter2} eventPublisher - Event emitter service. + * @param {typeof Customer} contactModel - Customer model. + */ + constructor( + private uow: UnitOfWork, + private eventPublisher: EventEmitter2, + @Inject(Customer.name) private contactModel: typeof Customer, + ) {} + + /** + * Deletes the given customer from the storage. + * @param {number} customerId - Customer ID. + * @return {Promise} + */ + public async deleteCustomer( + customerId: number, + ): Promise { + // Retrieve the customer or throw not found service error. + const oldCustomer = await this.contactModel + .query() + .findById(customerId) + .modify('customer') + .throwIfNotFound(); + // .queryAndThrowIfHasRelations({ + // type: ERRORS.CUSTOMER_HAS_TRANSACTIONS, + // }); + + // Triggers `onCustomerDeleting` event. + await this.eventPublisher.emitAsync(events.customers.onDeleting, { + customerId, + oldCustomer, + } as ICustomerDeletingPayload); + + // Deletes the customer and associated entities under UOW transaction. + return this.uow.withTransaction(async (trx: Knex.Transaction) => { + // Delete the customer from the storage. + await this.contactModel.query(trx).findById(customerId).delete(); + + // Throws `onCustomerDeleted` event. + await this.eventPublisher.emitAsync(events.customers.onDeleted, { + customerId, + oldCustomer, + trx, + } as ICustomerEventDeletedPayload); + }); + } +} diff --git a/packages/server-nest/src/modules/Customers/commands/EditCustomer.service.ts b/packages/server-nest/src/modules/Customers/commands/EditCustomer.service.ts new file mode 100644 index 000000000..6d72e91b5 --- /dev/null +++ b/packages/server-nest/src/modules/Customers/commands/EditCustomer.service.ts @@ -0,0 +1,74 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Knex } from 'knex'; +import { + ICustomerEditDTO, + ICustomerEventEditedPayload, + ICustomerEventEditingPayload, +} from '../types/Customers.types'; +import { CreateEditCustomerDTO } from './CreateEditCustomerDTO.service'; +import { Customer } from '../models/Customer'; +import { events } from '@/common/events/events'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service'; + +@Injectable() +export class EditCustomer { + /** + * @param {UnitOfWork} uow - Unit of work service. + * @param {EventEmitter2} eventPublisher - Event emitter service. + * @param {CreateEditCustomerDTO} customerDTO - Customer DTO. + * @param {typeof Customer} contactModel - Customer model. + */ + constructor( + private uow: UnitOfWork, + private eventPublisher: EventEmitter2, + private customerDTO: CreateEditCustomerDTO, + @Inject(Customer.name) private contactModel: typeof Customer, + ) {} + + /** + * Edits details of the given customer. + * @param {number} customerId + * @param {ICustomerEditDTO} customerDTO + * @return {Promise} + */ + public async editCustomer( + customerId: number, + customerDTO: ICustomerEditDTO, + ): Promise { + // Retrieve the customer or throw not found error. + const oldCustomer = await this.contactModel + .query() + .findById(customerId) + .modify('customer') + .throwIfNotFound(); + + // Transforms the given customer DTO to object. + const customerObj = this.customerDTO.transformEditDTO(customerDTO); + + // Edits the given customer under unit-of-work environment. + return this.uow.withTransaction(async (trx: Knex.Transaction) => { + // Triggers `onCustomerEditing` event. + await this.eventPublisher.emitAsync(events.customers.onEditing, { + customerDTO, + customerId, + trx, + } as ICustomerEventEditingPayload); + + // Edits the customer details on the storage. + const customer = await this.contactModel + .query() + .updateAndFetchById(customerId, { + ...customerObj, + }); + // Triggers `onCustomerEdited` event. + await this.eventPublisher.emitAsync(events.customers.onEdited, { + customerId, + customer, + trx, + } as ICustomerEventEditedPayload); + + return customer; + }); + } +} diff --git a/packages/server-nest/src/modules/Customers/commands/EditOpeningBalanceCustomer.service.ts b/packages/server-nest/src/modules/Customers/commands/EditOpeningBalanceCustomer.service.ts new file mode 100644 index 000000000..871f1c6e9 --- /dev/null +++ b/packages/server-nest/src/modules/Customers/commands/EditOpeningBalanceCustomer.service.ts @@ -0,0 +1,71 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Knex } from 'knex'; +import { + ICustomerOpeningBalanceEditDTO, + ICustomerOpeningBalanceEditedPayload, + ICustomerOpeningBalanceEditingPayload, +} from '../types/Customers.types'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service'; +import { Customer } from '../models/Customer'; +import { events } from '@/common/events/events'; + +@Injectable() +export class EditOpeningBalanceCustomer { + /** + * @param {EventEmitter2} eventPublisher - Event emitter service. + * @param {UnitOfWork} uow - Unit of work service. + * @param {typeof Customer} customerModel - Customer model. + */ + constructor( + private eventPublisher: EventEmitter2, + private uow: UnitOfWork, + @Inject(Customer.name) private customerModel: typeof Customer, + ) {} + + /** + * Changes the opening balance of the given customer. + * @param {number} customerId - Customer ID. + * @param {ICustomerOpeningBalanceEditDTO} openingBalanceEditDTO + */ + public async changeOpeningBalance( + customerId: number, + openingBalanceEditDTO: ICustomerOpeningBalanceEditDTO, + ): Promise { + // Retrieves the old customer or throw not found error. + const oldCustomer = await this.customerModel + .query() + .findById(customerId) + .throwIfNotFound(); + + // Mutates the customer opening balance under unit-of-work. + return this.uow.withTransaction(async (trx: Knex.Transaction) => { + // Triggers `onCustomerOpeningBalanceChanging` event. + await this.eventPublisher.emitAsync( + events.customers.onOpeningBalanceChanging, + { + oldCustomer, + openingBalanceEditDTO, + trx, + } as ICustomerOpeningBalanceEditingPayload, + ); + // Mutates the customer on the storage. + const customer = await this.customerModel + .query() + .patchAndFetchById(customerId, { + ...openingBalanceEditDTO, + }); + // Triggers `onCustomerOpeingBalanceChanged` event. + await this.eventPublisher.emitAsync( + events.customers.onOpeningBalanceChanged, + { + customer, + oldCustomer, + openingBalanceEditDTO, + trx, + } as ICustomerOpeningBalanceEditedPayload, + ); + return customer; + }); + } +} diff --git a/packages/server-nest/src/modules/Customers/commands/GetCustomer.service.ts b/packages/server-nest/src/modules/Customers/commands/GetCustomer.service.ts new file mode 100644 index 000000000..7bca8c420 --- /dev/null +++ b/packages/server-nest/src/modules/Customers/commands/GetCustomer.service.ts @@ -0,0 +1,31 @@ + +import { Inject, Injectable } from '@nestjs/common'; +import { CustomerTransfromer } from '../queries/CustomerTransformer'; +import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service'; +import { Customer } from '../models/Customer'; + +@Injectable() +export class GetCustomerService { + constructor( + private transformer: TransformerInjectable, + @Inject(Customer.name) private customerModel: typeof Customer, + ) {} + + /** + * Retrieve the given customer details. + * @param {number} customerId + */ + public async getCustomer(customerId: number) { + // Retrieve the customer model or throw not found error. + const customer = await this.customerModel + .query() + .findById(customerId) + .throwIfNotFound(); + + // Retrieves the transformered customers. + return this.transformer.transform( + customer, + new CustomerTransfromer() + ); + } +} diff --git a/packages/server-nest/src/modules/Customers/commands/GetCustomers.ts b/packages/server-nest/src/modules/Customers/commands/GetCustomers.ts new file mode 100644 index 000000000..4380a3fa3 --- /dev/null +++ b/packages/server-nest/src/modules/Customers/commands/GetCustomers.ts @@ -0,0 +1,76 @@ +// import { Inject, Service } from 'typedi'; +// import * as R from 'ramda'; +// import { +// ICustomer, +// ICustomersFilter, +// IFilterMeta, +// IPaginationMeta, +// } from '@/interfaces'; +// import HasTenancyService from '@/services/Tenancy/TenancyService'; +// import DynamicListingService from '@/services/DynamicListing/DynamicListService'; +// import CustomerTransfromer from '../queries/CustomerTransformer'; +// import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; + +// @Service() +// export class GetCustomers { +// @Inject() +// private tenancy: HasTenancyService; + +// @Inject() +// private dynamicListService: DynamicListingService; + +// @Inject() +// private transformer: TransformerInjectable; + +// /** +// * Parses customers list filter DTO. +// * @param filterDTO - +// */ +// private parseCustomersListFilterDTO(filterDTO) { +// return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO); +// } + +// /** +// * Retrieve customers paginated list. +// * @param {number} tenantId - Tenant id. +// * @param {ICustomersFilter} filter - Cusotmers filter. +// */ +// public async getCustomersList( +// filterDTO: ICustomersFilter +// ): Promise<{ +// customers: ICustomer[]; +// pagination: IPaginationMeta; +// filterMeta: IFilterMeta; +// }> { +// const { Customer } = this.tenancy.models(tenantId); + +// // Parses customers list filter DTO. +// const filter = this.parseCustomersListFilterDTO(filterDTO); + +// // Dynamic list. +// const dynamicList = await this.dynamicListService.dynamicList( +// tenantId, +// Customer, +// filter +// ); +// // Customers. +// const { results, pagination } = await Customer.query() +// .onBuild((builder) => { +// dynamicList.buildQuery()(builder); +// builder.modify('inactiveMode', filter.inactiveMode); +// }) +// .pagination(filter.page - 1, filter.pageSize); + +// // Retrieves the transformed customers. +// const customers = await this.transformer.transform( +// tenantId, +// results, +// new CustomerTransfromer() +// ); +// return { +// customers, +// pagination, +// filterMeta: dynamicList.getResponseMeta(), +// }; +// } +// } diff --git a/packages/server-nest/src/modules/Customers/constants.ts b/packages/server-nest/src/modules/Customers/constants.ts new file mode 100644 index 000000000..d14770903 --- /dev/null +++ b/packages/server-nest/src/modules/Customers/constants.ts @@ -0,0 +1,27 @@ +export const DEFAULT_VIEW_COLUMNS = []; + +export const DEFAULT_VIEWS = [ + { + name: 'Overdue', + slug: 'overdue', + rolesLogicExpression: '1', + roles: [ + { index: 1, fieldKey: 'status', comparator: 'equals', value: 'overdue' }, + ], + columns: DEFAULT_VIEW_COLUMNS, + }, + { + name: 'Unpaid', + slug: 'unpaid', + rolesLogicExpression: '1', + roles: [ + { index: 1, fieldKey: 'status', comparator: 'equals', value: 'unpaid' }, + ], + columns: DEFAULT_VIEW_COLUMNS, + }, +]; + +export const ERRORS = { + CUSTOMER_HAS_TRANSACTIONS: 'CUSTOMER_HAS_TRANSACTIONS', + CUSTOMER_ALREADY_ACTIVE: 'CUSTOMER_ALREADY_ACTIVE', +}; diff --git a/packages/server-nest/src/modules/Customers/models/Customer.ts b/packages/server-nest/src/modules/Customers/models/Customer.ts index 6a6707e46..95791d224 100644 --- a/packages/server-nest/src/modules/Customers/models/Customer.ts +++ b/packages/server-nest/src/modules/Customers/models/Customer.ts @@ -28,8 +28,9 @@ export class Customer extends BaseModel{ currencyCode: string; openingBalance: number; - openingBalanceAt: Date; + openingBalanceAt: Date | string; openingBalanceExchangeRate: number; + openingBalanceBranchId?: number; salutation?: string; firstName?: string; diff --git a/packages/server-nest/src/modules/Customers/queries/CustomerTransformer.ts b/packages/server-nest/src/modules/Customers/queries/CustomerTransformer.ts new file mode 100644 index 000000000..aa5e1f132 --- /dev/null +++ b/packages/server-nest/src/modules/Customers/queries/CustomerTransformer.ts @@ -0,0 +1,38 @@ +import { ContactTransfromer } from "../../Contacts/Contact.transformer"; + +export class CustomerTransfromer extends ContactTransfromer { + /** + * Include these attributes to expense object. + * @returns {Array} + */ + public includeAttributes = (): string[] => { + return [ + 'formattedBalance', + 'formattedOpeningBalance', + 'formattedOpeningBalanceAt', + 'customerType', + 'formattedCustomerType', + ]; + }; + + /** + * Retrieve customer type. + * @returns {string} + */ + protected customerType = (customer): string => { + return customer.contactType; + }; + + /** + * Retrieve the formatted customer type. + * @param customer + * @returns {string} + */ + protected formattedCustomerType = (customer): string => { + const keywords = { + individual: 'customer.type.individual', + business: 'customer.type.business', + }; + return this.context.i18n.t(keywords[customer.contactType] || ''); + }; +} diff --git a/packages/server-nest/src/modules/Customers/subscribers/CustomerGLEntriesSubscriber.ts b/packages/server-nest/src/modules/Customers/subscribers/CustomerGLEntriesSubscriber.ts new file mode 100644 index 000000000..1e5911f80 --- /dev/null +++ b/packages/server-nest/src/modules/Customers/subscribers/CustomerGLEntriesSubscriber.ts @@ -0,0 +1,91 @@ +// import { Service, Inject } from 'typedi'; +// import { +// ICustomerEventCreatedPayload, +// ICustomerEventDeletedPayload, +// ICustomerOpeningBalanceEditedPayload, +// } from '@/interfaces'; +// import events from '@/subscribers/events'; +// import { CustomerGLEntriesStorage } from '../CustomerGLEntriesStorage'; + +// @Service() +// export class CustomerWriteGLOpeningBalanceSubscriber { +// @Inject() +// private customerGLEntries: CustomerGLEntriesStorage; + +// /** +// * Attaches events with handlers. +// */ +// public attach(bus) { +// bus.subscribe( +// events.customers.onCreated, +// this.handleWriteOpenBalanceEntries +// ); +// bus.subscribe( +// events.customers.onDeleted, +// this.handleRevertOpeningBalanceEntries +// ); +// bus.subscribe( +// events.customers.onOpeningBalanceChanged, +// this.handleRewriteOpeningEntriesOnChanged +// ); +// } + +// /** +// * Handles the writing opening balance journal entries once the customer created. +// * @param {ICustomerEventCreatedPayload} payload - +// */ +// private handleWriteOpenBalanceEntries = async ({ +// tenantId, +// customer, +// trx, +// }: ICustomerEventCreatedPayload) => { +// // Writes the customer opening balance journal entries. +// if (customer.openingBalance) { +// await this.customerGLEntries.writeCustomerOpeningBalance( +// tenantId, +// customer.id, +// trx +// ); +// } +// }; + +// /** +// * Handles the deleting opeing balance journal entrise once the customer deleted. +// * @param {ICustomerEventDeletedPayload} payload - +// */ +// private handleRevertOpeningBalanceEntries = async ({ +// tenantId, +// customerId, +// trx, +// }: ICustomerEventDeletedPayload) => { +// await this.customerGLEntries.revertCustomerOpeningBalance( +// tenantId, +// customerId, +// trx +// ); +// }; + +// /** +// * Handles the rewrite opening balance entries once opening balnace changed. +// * @param {ICustomerOpeningBalanceEditedPayload} payload - +// */ +// private handleRewriteOpeningEntriesOnChanged = async ({ +// tenantId, +// customer, +// trx, +// }: ICustomerOpeningBalanceEditedPayload) => { +// if (customer.openingBalance) { +// await this.customerGLEntries.rewriteCustomerOpeningBalance( +// tenantId, +// customer.id, +// trx +// ); +// } else { +// await this.customerGLEntries.revertCustomerOpeningBalance( +// tenantId, +// customer.id, +// trx +// ); +// } +// }; +// } diff --git a/packages/server-nest/src/modules/Customers/types/Customers.types.ts b/packages/server-nest/src/modules/Customers/types/Customers.types.ts new file mode 100644 index 000000000..d5673a5b1 --- /dev/null +++ b/packages/server-nest/src/modules/Customers/types/Customers.types.ts @@ -0,0 +1,147 @@ +import { Knex } from 'knex'; +import { Customer } from '../models/Customer'; +import { IContactAddressDTO } from '@/modules/Contacts/types/Contacts.types'; + +// Customer Interfaces. +// ---------------------------------- +export interface ICustomerNewDTO extends IContactAddressDTO { + customerType: string; + + currencyCode: string; + + openingBalance?: number; + openingBalanceAt?: string; + openingBalanceExchangeRate?: number; + openingBalanceBranchId?: number; + + salutation?: string; + firstName?: string; + lastName?: string; + companyName?: string; + displayName: string; + + website?: string; + email?: string; + workPhone?: string; + personalPhone?: string; + + note?: string; + active?: boolean; +} + +export interface ICustomerEditDTO extends IContactAddressDTO { + customerType: string; + + salutation?: string; + firstName?: string; + lastName?: string; + companyName?: string; + displayName: string; + + website?: string; + email?: string; + workPhone?: string; + personalPhone?: string; + + note?: string; + active?: boolean; +} + +// export interface ICustomersFilter extends IDynamicListFilter { +// stringifiedFilterRoles?: string; +// page?: number; +// pageSize?: number; +// } + +// Customer Events. +// ---------------------------------- +export interface ICustomerEventCreatedPayload { + // tenantId: number; + customerId: number; + // authorizedUser: ISystemUser; + customer: Customer; + trx: Knex.Transaction; +} +export interface ICustomerEventCreatingPayload { + // tenantId: number; + customerDTO: ICustomerNewDTO; + trx: Knex.Transaction; +} +export interface ICustomerEventEditedPayload { + // tenantId: number + customerId: number; + customer: Customer; + trx: Knex.Transaction; +} + +export interface ICustomerEventEditingPayload { + // tenantId: number; + customerDTO: ICustomerEditDTO; + customerId: number; + trx: Knex.Transaction; +} + +export interface ICustomerDeletingPayload { + // tenantId: number; + customerId: number; + oldCustomer: Customer; +} + +export interface ICustomerEventDeletedPayload { + // tenantId: number; + customerId: number; + oldCustomer: Customer; + trx: Knex.Transaction; +} +export interface ICustomerEventCreatingPayload { + // tenantId: number; + customerDTO: ICustomerNewDTO; + trx: Knex.Transaction; +} +export enum CustomerAction { + Create = 'Create', + Edit = 'Edit', + Delete = 'Delete', + View = 'View', +} + +export enum VendorAction { + Create = 'Create', + Edit = 'Edit', + Delete = 'Delete', + View = 'View', +} + +export interface ICustomerOpeningBalanceEditDTO { + openingBalance: number; + openingBalanceAt: Date | string; + openingBalanceExchangeRate: number; + openingBalanceBranchId?: number; +} + +export interface ICustomerOpeningBalanceEditingPayload { + oldCustomer: Customer; + openingBalanceEditDTO: ICustomerOpeningBalanceEditDTO; + trx?: Knex.Transaction; +} + +export interface ICustomerOpeningBalanceEditedPayload { + customer: Customer; + oldCustomer: Customer; + openingBalanceEditDTO: ICustomerOpeningBalanceEditDTO; + trx: Knex.Transaction; +} + + +export interface ICustomerActivatingPayload { + // tenantId: number; + trx: Knex.Transaction, + oldCustomer: Customer; +} + +export interface ICustomerActivatedPayload { + // tenantId: number; + trx?: Knex.Transaction; + oldCustomer: Customer; + customer: Customer; +} diff --git a/packages/server-nest/src/modules/Items/ItemsEntries.service.ts b/packages/server-nest/src/modules/Items/ItemsEntries.service.ts index 0e717356d..d70440039 100644 --- a/packages/server-nest/src/modules/Items/ItemsEntries.service.ts +++ b/packages/server-nest/src/modules/Items/ItemsEntries.service.ts @@ -213,23 +213,21 @@ export class ItemsEntriesService { /** * Sets the cost/sell accounts to the invoice entries. */ - public setItemsEntriesDefaultAccounts() { - return async (entries: ItemEntry[]) => { - const entriesItemsIds = entries.map((e) => e.itemId); - const items = await this.itemModel.query().whereIn('id', entriesItemsIds); + public async setItemsEntriesDefaultAccounts(entries: ItemEntry[]) { + const entriesItemsIds = entries.map((e) => e.itemId); + const items = await this.itemModel.query().whereIn('id', entriesItemsIds); - return entries.map((entry) => { - const item = items.find((i) => i.id === entry.itemId); + return entries.map((entry) => { + const item = items.find((i) => i.id === entry.itemId); - return { - ...entry, - sellAccountId: entry.sellAccountId || item.sellAccountId, - ...(item.type === 'inventory' && { - costAccountId: entry.costAccountId || item.costAccountId, - }), - }; - }); - }; + return { + ...entry, + sellAccountId: entry.sellAccountId || item.sellAccountId, + ...(item.type === 'inventory' && { + costAccountId: entry.costAccountId || item.costAccountId, + }), + }; + }); } /** diff --git a/packages/server-nest/src/modules/Items/models/ItemEntry.ts b/packages/server-nest/src/modules/Items/models/ItemEntry.ts index e33100dd0..0dd260618 100644 --- a/packages/server-nest/src/modules/Items/models/ItemEntry.ts +++ b/packages/server-nest/src/modules/Items/models/ItemEntry.ts @@ -9,6 +9,9 @@ export class ItemEntry extends BaseModel { public quantity: number; public rate: number; public isInclusiveTax: number; + public itemId: number; + public costAccountId: number; + public taxRateId: number; /** * Table name. diff --git a/packages/server-nest/src/modules/PaymentLinks/models/PaymentLink.ts b/packages/server-nest/src/modules/PaymentLinks/models/PaymentLink.ts new file mode 100644 index 000000000..08a23a992 --- /dev/null +++ b/packages/server-nest/src/modules/PaymentLinks/models/PaymentLink.ts @@ -0,0 +1,30 @@ +import { Model } from 'objection'; + +export class PaymentLink extends Model { + public tenantId!: number; + public resourceId!: number; + public resourceType!: string; + public linkId!: string; + public publicity!: string; + public expiryAt!: Date; + + // Timestamps + public createdAt!: Date; + public updatedAt!: Date; + + /** + * Table name. + * @returns {string} + */ + static get tableName() { + return 'payment_links'; + } + + /** + * Timestamps columns. + * @returns {string[]} + */ + static get timestamps() { + return ['createdAt', 'updatedAt']; + } +} diff --git a/packages/server-nest/src/modules/PaymentReceived/PaymentReceived.application.ts b/packages/server-nest/src/modules/PaymentReceived/PaymentReceived.application.ts new file mode 100644 index 000000000..2dda2dfa5 --- /dev/null +++ b/packages/server-nest/src/modules/PaymentReceived/PaymentReceived.application.ts @@ -0,0 +1,188 @@ +import { + IPaymentReceivedCreateDTO, + IPaymentReceivedEditDTO, + IPaymentReceivedSmsDetails, + // IPaymentsReceivedFilter, + // ISystemUser, + // PaymentReceiveMailOptsDTO, +} from './types/PaymentReceived.types'; +import { Injectable } from '@nestjs/common'; +import { CreatePaymentReceivedService } from './commands/CreatePaymentReceived.serivce'; +import { EditPaymentReceived } from './commands/EditPaymentReceived.service'; +import { DeletePaymentReceived } from './commands/DeletePaymentReceived.service'; +// import { GetPaymentReceives } from './queries/GetPaymentsReceived.service'; +import { GetPaymentReceived } from './queries/GetPaymentReceived.service'; +import { GetPaymentReceivedInvoices } from './queries/GetPaymentReceivedInvoices.service'; +// import { PaymentReceiveNotifyBySms } from './PaymentReceivedSmsNotify'; +import GetPaymentReceivedPdf from './queries/GetPaymentReceivedPdf.service'; +// import { SendPaymentReceiveMailNotification } from './PaymentReceivedMailNotification'; +import { GetPaymentReceivedState } from './queries/GetPaymentReceivedState.service'; + +@Injectable() +export class PaymentReceivesApplication { + constructor( + private createPaymentReceivedService: CreatePaymentReceivedService, + private editPaymentReceivedService: EditPaymentReceived, + private deletePaymentReceivedService: DeletePaymentReceived, + // private getPaymentsReceivedService: GetPaymentReceives, + private getPaymentReceivedService: GetPaymentReceived, + private getPaymentReceiveInvoicesService: GetPaymentReceivedInvoices, + // private paymentSmsNotify: PaymentReceiveNotifyBySms, + // private paymentMailNotify: SendPaymentReceiveMailNotification, + private getPaymentReceivePdfService: GetPaymentReceivedPdf, + private getPaymentReceivedStateService: GetPaymentReceivedState, + ) {} + + /** + * Creates a new payment receive. + * @param {IPaymentReceivedCreateDTO} paymentReceiveDTO + * @returns + */ + public createPaymentReceived(paymentReceiveDTO: IPaymentReceivedCreateDTO) { + return this.createPaymentReceivedService.createPaymentReceived( + paymentReceiveDTO, + ); + } + + /** + * Edit details the given payment receive with associated entries. + * @param {number} tenantId + * @param {number} paymentReceiveId + * @param {IPaymentReceivedEditDTO} paymentReceiveDTO + * @param {ISystemUser} authorizedUser + * @returns + */ + public editPaymentReceive( + paymentReceiveId: number, + paymentReceiveDTO: IPaymentReceivedEditDTO, + ) { + return this.editPaymentReceivedService.editPaymentReceive( + paymentReceiveId, + paymentReceiveDTO, + ); + } + + /** + * Deletes the given payment receive. + * @param {number} paymentReceiveId - Payment received id. + * @returns {Promise} + */ + public deletePaymentReceive(paymentReceiveId: number) { + return this.deletePaymentReceivedService.deletePaymentReceive( + paymentReceiveId, + ); + } + + /** + * Retrieve payment receives paginated and filterable. + * @param {number} tenantId + * @param {IPaymentsReceivedFilter} filterDTO + * @returns + */ + // public async getPaymentReceives( + // tenantId: number, + // filterDTO: IPaymentsReceivedFilter, + // ): Promise<{ + // paymentReceives: IPaymentReceived[]; + // pagination: IPaginationMeta; + // filterMeta: IFilterMeta; + // }> { + // return this.getPaymentsReceivedService.getPaymentReceives( + // tenantId, + // filterDTO, + // ); + // } + + /** + * Retrieves the given payment receive. + * @param {number} paymentReceiveId + * @returns {Promise} + */ + public async getPaymentReceive(paymentReceiveId: number) { + return this.getPaymentReceivedService.getPaymentReceive(paymentReceiveId); + } + + /** + * Retrieves associated sale invoices of the given payment receive. + * @param {number} paymentReceiveId + * @returns + */ + public getPaymentReceiveInvoices(paymentReceiveId: number) { + return this.getPaymentReceiveInvoicesService.getPaymentReceiveInvoices( + paymentReceiveId, + ); + } + + /** + * Notify customer via sms about payment receive details. + * @param {number} tenantId - Tenant id. + * @param {number} paymentReceiveid - Payment receive id. + */ + // public notifyPaymentBySms(tenantId: number, paymentReceiveid: number) { + // return this.paymentSmsNotify.notifyBySms(tenantId, paymentReceiveid); + // } + + /** + * Retrieve the SMS details of the given invoice. + * @param {number} tenantId - Tenant id. + * @param {number} paymentReceiveid - Payment receive id. + */ + // public getPaymentSmsDetails = async ( + // tenantId: number, + // paymentReceiveId: number, + // ): Promise => { + // return this.paymentSmsNotify.smsDetails(tenantId, paymentReceiveId); + // }; + + /** + * Notify customer via mail about payment receive details. + * @param {number} tenantId + * @param {number} paymentReceiveId + * @param {IPaymentReceiveMailOpts} messageOpts + * @returns {Promise} + */ + // public notifyPaymentByMail( + // tenantId: number, + // paymentReceiveId: number, + // messageOpts: PaymentReceiveMailOptsDTO, + // ): Promise { + // return this.paymentMailNotify.triggerMail( + // tenantId, + // paymentReceiveId, + // messageOpts, + // ); + // } + + /** + * Retrieves the default mail options of the given payment transaction. + * @param {number} tenantId + * @param {number} paymentReceiveId + * @returns {Promise} + */ + // public getPaymentMailOptions(tenantId: number, paymentReceiveId: number) { + // return this.paymentMailNotify.getMailOptions(tenantId, paymentReceiveId); + // } + + /** + * Retrieve pdf content of the given payment receive. + * @param {number} tenantId + * @param {PaymentReceive} paymentReceive + * @returns + */ + public getPaymentReceivePdf = ( + tenantId: number, + paymentReceiveId: number, + ) => { + return this.getPaymentReceivePdfService.getPaymentReceivePdf( + paymentReceiveId, + ); + }; + + /** + * Retrieves the create/edit initial state of the payment received. + * @returns {Promise} + */ + public getPaymentReceivedState = () => { + return this.getPaymentReceivedStateService.getPaymentReceivedState(); + }; +} diff --git a/packages/server-nest/src/modules/PaymentReceived/PaymentReceivedGLEntries.ts b/packages/server-nest/src/modules/PaymentReceived/PaymentReceivedGLEntries.ts new file mode 100644 index 000000000..1679d5059 --- /dev/null +++ b/packages/server-nest/src/modules/PaymentReceived/PaymentReceivedGLEntries.ts @@ -0,0 +1,299 @@ +// import { Service, Inject } from 'typedi'; +// import { sumBy } from 'lodash'; +// import { Knex } from 'knex'; +// import Ledger from '@/services/Accounting/Ledger'; +// import TenancyService from '@/services/Tenancy/TenancyService'; +// import { +// IPaymentReceived, +// ILedgerEntry, +// AccountNormal, +// IPaymentReceiveGLCommonEntry, +// } from '@/interfaces'; +// import LedgerStorageService from '@/services/Accounting/LedgerStorageService'; +// import { TenantMetadata } from '@/system/models'; + +// @Service() +// export class PaymentReceivedGLEntries { +// @Inject() +// private tenancy: TenancyService; + +// @Inject() +// private ledgerStorage: LedgerStorageService; + +// /** +// * Writes payment GL entries to the storage. +// * @param {number} tenantId +// * @param {number} paymentReceiveId +// * @param {Knex.Transaction} trx +// * @returns {Promise} +// */ +// public writePaymentGLEntries = async ( +// tenantId: number, +// paymentReceiveId: number, +// trx?: Knex.Transaction +// ): Promise => { +// const { PaymentReceive } = this.tenancy.models(tenantId); + +// // Retrieves the given tenant metadata. +// const tenantMeta = await TenantMetadata.query().findOne({ tenantId }); + +// // Retrieves the payment receive with associated entries. +// const paymentReceive = await PaymentReceive.query(trx) +// .findById(paymentReceiveId) +// .withGraphFetched('entries.invoice'); + +// // Retrives the payment receive ledger. +// const ledger = await this.getPaymentReceiveGLedger( +// tenantId, +// paymentReceive, +// tenantMeta.baseCurrency, +// trx +// ); +// // Commit the ledger entries to the storage. +// await this.ledgerStorage.commit(tenantId, ledger, trx); +// }; + +// /** +// * Reverts the given payment receive GL entries. +// * @param {number} tenantId +// * @param {number} paymentReceiveId +// * @param {Knex.Transaction} trx +// */ +// public revertPaymentGLEntries = async ( +// tenantId: number, +// paymentReceiveId: number, +// trx?: Knex.Transaction +// ) => { +// await this.ledgerStorage.deleteByReference( +// tenantId, +// paymentReceiveId, +// 'PaymentReceive', +// trx +// ); +// }; + +// /** +// * Rewrites the given payment receive GL entries. +// * @param {number} tenantId +// * @param {number} paymentReceiveId +// * @param {Knex.Transaction} trx +// */ +// public rewritePaymentGLEntries = async ( +// tenantId: number, +// paymentReceiveId: number, +// trx?: Knex.Transaction +// ) => { +// // Reverts the payment GL entries. +// await this.revertPaymentGLEntries(tenantId, paymentReceiveId, trx); + +// // Writes the payment GL entries. +// await this.writePaymentGLEntries(tenantId, paymentReceiveId, trx); +// }; + +// /** +// * Retrieves the payment receive general ledger. +// * @param {number} tenantId - +// * @param {IPaymentReceived} paymentReceive - +// * @param {string} baseCurrencyCode - +// * @param {Knex.Transaction} trx - +// * @returns {Ledger} +// */ +// public getPaymentReceiveGLedger = async ( +// tenantId: number, +// paymentReceive: IPaymentReceived, +// baseCurrencyCode: string, +// trx?: Knex.Transaction +// ): Promise => { +// const { Account } = this.tenancy.models(tenantId); +// const { accountRepository } = this.tenancy.repositories(tenantId); + +// // Retrieve the A/R account of the given currency. +// const receivableAccount = +// await accountRepository.findOrCreateAccountReceivable( +// paymentReceive.currencyCode +// ); +// // Exchange gain/loss account. +// const exGainLossAccount = await Account.query(trx).modify( +// 'findBySlug', +// 'exchange-grain-loss' +// ); +// const ledgerEntries = this.getPaymentReceiveGLEntries( +// paymentReceive, +// receivableAccount.id, +// exGainLossAccount.id, +// baseCurrencyCode +// ); +// return new Ledger(ledgerEntries); +// }; + +// /** +// * Calculates the payment total exchange gain/loss. +// * @param {IBillPayment} paymentReceive - Payment receive with entries. +// * @returns {number} +// */ +// private getPaymentExGainOrLoss = ( +// paymentReceive: IPaymentReceived +// ): number => { +// return sumBy(paymentReceive.entries, (entry) => { +// const paymentLocalAmount = +// entry.paymentAmount * paymentReceive.exchangeRate; +// const invoicePayment = entry.paymentAmount * entry.invoice.exchangeRate; + +// return paymentLocalAmount - invoicePayment; +// }); +// }; + +// /** +// * Retrieves the common entry of payment receive. +// * @param {IPaymentReceived} paymentReceive +// * @returns {} +// */ +// private getPaymentReceiveCommonEntry = ( +// paymentReceive: IPaymentReceived +// ): IPaymentReceiveGLCommonEntry => { +// return { +// debit: 0, +// credit: 0, + +// currencyCode: paymentReceive.currencyCode, +// exchangeRate: paymentReceive.exchangeRate, + +// transactionId: paymentReceive.id, +// transactionType: 'PaymentReceive', + +// transactionNumber: paymentReceive.paymentReceiveNo, +// referenceNumber: paymentReceive.referenceNo, + +// date: paymentReceive.paymentDate, +// userId: paymentReceive.userId, +// createdAt: paymentReceive.createdAt, + +// branchId: paymentReceive.branchId, +// }; +// }; + +// /** +// * Retrieves the payment exchange gain/loss entry. +// * @param {IPaymentReceived} paymentReceive - +// * @param {number} ARAccountId - +// * @param {number} exchangeGainOrLossAccountId - +// * @param {string} baseCurrencyCode - +// * @returns {ILedgerEntry[]} +// */ +// private getPaymentExchangeGainLossEntry = ( +// paymentReceive: IPaymentReceived, +// ARAccountId: number, +// exchangeGainOrLossAccountId: number, +// baseCurrencyCode: string +// ): ILedgerEntry[] => { +// const commonJournal = this.getPaymentReceiveCommonEntry(paymentReceive); +// const gainOrLoss = this.getPaymentExGainOrLoss(paymentReceive); +// const absGainOrLoss = Math.abs(gainOrLoss); + +// return gainOrLoss +// ? [ +// { +// ...commonJournal, +// currencyCode: baseCurrencyCode, +// exchangeRate: 1, +// debit: gainOrLoss > 0 ? absGainOrLoss : 0, +// credit: gainOrLoss < 0 ? absGainOrLoss : 0, +// accountId: ARAccountId, +// contactId: paymentReceive.customerId, +// index: 3, +// accountNormal: AccountNormal.CREDIT, +// }, +// { +// ...commonJournal, +// currencyCode: baseCurrencyCode, +// exchangeRate: 1, +// credit: gainOrLoss > 0 ? absGainOrLoss : 0, +// debit: gainOrLoss < 0 ? absGainOrLoss : 0, +// accountId: exchangeGainOrLossAccountId, +// index: 3, +// accountNormal: AccountNormal.DEBIT, +// }, +// ] +// : []; +// }; + +// /** +// * Retrieves the payment deposit GL entry. +// * @param {IPaymentReceived} paymentReceive +// * @returns {ILedgerEntry} +// */ +// private getPaymentDepositGLEntry = ( +// paymentReceive: IPaymentReceived +// ): ILedgerEntry => { +// const commonJournal = this.getPaymentReceiveCommonEntry(paymentReceive); + +// return { +// ...commonJournal, +// debit: paymentReceive.localAmount, +// accountId: paymentReceive.depositAccountId, +// index: 2, +// accountNormal: AccountNormal.DEBIT, +// }; +// }; + +// /** +// * Retrieves the payment receivable entry. +// * @param {IPaymentReceived} paymentReceive +// * @param {number} ARAccountId +// * @returns {ILedgerEntry} +// */ +// private getPaymentReceivableEntry = ( +// paymentReceive: IPaymentReceived, +// ARAccountId: number +// ): ILedgerEntry => { +// const commonJournal = this.getPaymentReceiveCommonEntry(paymentReceive); + +// return { +// ...commonJournal, +// credit: paymentReceive.localAmount, +// contactId: paymentReceive.customerId, +// accountId: ARAccountId, +// index: 1, +// accountNormal: AccountNormal.DEBIT, +// }; +// }; + +// /** +// * Records payment receive journal transactions. +// * +// * Invoice payment journals. +// * -------- +// * - Account receivable -> Debit +// * - Payment account [current asset] -> Credit +// * +// * @param {number} tenantId +// * @param {IPaymentReceived} paymentRecieve - Payment receive model. +// * @param {number} ARAccountId - A/R account id. +// * @param {number} exGainOrLossAccountId - Exchange gain/loss account id. +// * @param {string} baseCurrency - Base currency code. +// * @returns {Promise} +// */ +// public getPaymentReceiveGLEntries = ( +// paymentReceive: IPaymentReceived, +// ARAccountId: number, +// exGainOrLossAccountId: number, +// baseCurrency: string +// ): ILedgerEntry[] => { +// // Retrieve the payment deposit entry. +// const paymentDepositEntry = this.getPaymentDepositGLEntry(paymentReceive); + +// // Retrieves the A/R entry. +// const receivableEntry = this.getPaymentReceivableEntry( +// paymentReceive, +// ARAccountId +// ); +// // Exchange gain/loss entries. +// const gainLossEntries = this.getPaymentExchangeGainLossEntry( +// paymentReceive, +// ARAccountId, +// exGainOrLossAccountId, +// baseCurrency +// ); +// return [paymentDepositEntry, receivableEntry, ...gainLossEntries]; +// }; +// } diff --git a/packages/server-nest/src/modules/PaymentReceived/PaymentReceivedMailNotification.ts b/packages/server-nest/src/modules/PaymentReceived/PaymentReceivedMailNotification.ts new file mode 100644 index 000000000..02de87252 --- /dev/null +++ b/packages/server-nest/src/modules/PaymentReceived/PaymentReceivedMailNotification.ts @@ -0,0 +1,162 @@ +// import { Inject, Injectable } from '@nestjs/common'; +// import { +// PaymentReceiveMailOpts, +// PaymentReceiveMailOptsDTO, +// PaymentReceiveMailPresendEvent, +// SendInvoiceMailDTO, +// } from './types/PaymentReceived.types'; +// import Mail from '@/lib/Mail'; +// import { +// DEFAULT_PAYMENT_MAIL_CONTENT, +// DEFAULT_PAYMENT_MAIL_SUBJECT, +// } from './constants'; +// import { GetPaymentReceived } from './queries/GetPaymentReceived.service'; +// import { transformPaymentReceivedToMailDataArgs } from './utils'; +// import { PaymentReceived } from './models/PaymentReceived'; +// import { EventEmitter2 } from '@nestjs/event-emitter'; +// import { events } from '@/common/events/events'; + +// @Injectable() +// export class SendPaymentReceiveMailNotification { +// constructor( +// private getPaymentService: GetPaymentReceived, +// private contactMailNotification: ContactMailNotification, + +// @Inject('agenda') private agenda: any, +// private eventPublisher: EventEmitter2, + +// @Inject(PaymentReceived.name) +// private paymentReceiveModel: typeof PaymentReceived, +// ) {} + +// /** +// * Sends the mail of the given payment receive. +// * @param {number} tenantId +// * @param {number} paymentReceiveId +// * @param {PaymentReceiveMailOptsDTO} messageDTO +// * @returns {Promise} +// */ +// public async triggerMail( +// paymentReceiveId: number, +// messageDTO: PaymentReceiveMailOptsDTO, +// ): Promise { +// const payload = { +// paymentReceiveId, +// messageDTO, +// }; +// await this.agenda.now('payment-receive-mail-send', payload); + +// // Triggers `onPaymentReceivePreMailSend` event. +// await this.eventPublisher.emitAsync(events.paymentReceive.onPreMailSend, { +// paymentReceiveId, +// messageOptions: messageDTO, +// } as PaymentReceiveMailPresendEvent); +// } + +// /** +// * Retrieves the default payment mail options. +// * @param {number} paymentReceiveId - Payment receive id. +// * @returns {Promise} +// */ +// public getMailOptions = async ( +// paymentId: number, +// ): Promise => { +// const paymentReceive = await this.paymentReceiveModel +// .query() +// .findById(paymentId) +// .throwIfNotFound(); + +// const formatArgs = await this.textFormatter(paymentId); + +// const mailOptions = +// await this.contactMailNotification.getDefaultMailOptions( +// paymentReceive.customerId, +// ); +// return { +// ...mailOptions, +// subject: DEFAULT_PAYMENT_MAIL_SUBJECT, +// message: DEFAULT_PAYMENT_MAIL_CONTENT, +// ...formatArgs, +// }; +// }; + +// /** +// * Retrieves the formatted text of the given sale invoice. +// * @param {number} invoiceId - Sale invoice id. +// * @returns {Promise>} +// */ +// public textFormatter = async ( +// invoiceId: number, +// ): Promise> => { +// const payment = await this.getPaymentService.getPaymentReceive(invoiceId); +// return transformPaymentReceivedToMailDataArgs(payment); +// }; + +// /** +// * Retrieves the formatted mail options of the given payment receive. +// * @param {number} tenantId +// * @param {number} paymentReceiveId +// * @param {SendInvoiceMailDTO} messageDTO +// * @returns {Promise} +// */ +// public getFormattedMailOptions = async ( +// paymentReceiveId: number, +// messageDTO: SendInvoiceMailDTO, +// ) => { +// const formatterArgs = await this.textFormatter(paymentReceiveId); + +// // Default message options. +// const defaultMessageOpts = await this.getMailOptions(paymentReceiveId); +// // Parsed message opts with default options. +// const parsedMessageOpts = mergeAndValidateMailOptions( +// defaultMessageOpts, +// messageDTO, +// ); +// // Formats the message options. +// return this.contactMailNotification.formatMailOptions( +// parsedMessageOpts, +// formatterArgs, +// ); +// }; + +// /** +// * Triggers the mail invoice. +// * @param {number} tenantId +// * @param {number} saleInvoiceId - Invoice id. +// * @param {SendInvoiceMailDTO} messageDTO - Message options. +// * @returns {Promise} +// */ +// public async sendMail( +// paymentReceiveId: number, +// messageDTO: SendInvoiceMailDTO, +// ): Promise { +// // Retrieves the formatted mail options. +// const formattedMessageOptions = await this.getFormattedMailOptions( +// paymentReceiveId, +// messageDTO, +// ); +// const mail = new Mail() +// .setSubject(formattedMessageOptions.subject) +// .setTo(formattedMessageOptions.to) +// .setCC(formattedMessageOptions.cc) +// .setBCC(formattedMessageOptions.bcc) +// .setContent(formattedMessageOptions.message); + +// const eventPayload = { +// paymentReceiveId, +// messageOptions: formattedMessageOptions, +// }; +// // Triggers `onPaymentReceiveMailSend` event. +// await this.eventPublisher.emitAsync( +// events.paymentReceive.onMailSend, +// eventPayload, +// ); +// await mail.send(); + +// // Triggers `onPaymentReceiveMailSent` event. +// await this.eventPublisher.emitAsync( +// events.paymentReceive.onMailSent, +// eventPayload, +// ); +// } +// } diff --git a/packages/server-nest/src/modules/PaymentReceived/PaymentReceivedMailNotificationJob.ts b/packages/server-nest/src/modules/PaymentReceived/PaymentReceivedMailNotificationJob.ts new file mode 100644 index 000000000..71a23dc5e --- /dev/null +++ b/packages/server-nest/src/modules/PaymentReceived/PaymentReceivedMailNotificationJob.ts @@ -0,0 +1,32 @@ +// import Container, { Service } from 'typedi'; +// import { SendPaymentReceiveMailNotification } from './PaymentReceivedMailNotification'; + +// @Service() +// export class PaymentReceivedMailNotificationJob { +// /** +// * Constructor method. +// */ +// constructor(agenda) { +// agenda.define( +// 'payment-receive-mail-send', +// { priority: 'high', concurrency: 2 }, +// this.handler +// ); +// } + +// /** +// * Triggers sending payment notification via mail. +// */ +// private handler = async (job, done: Function) => { +// const { tenantId, paymentReceiveId, messageDTO } = job.attrs.data; +// const paymentMail = Container.get(SendPaymentReceiveMailNotification); + +// try { +// await paymentMail.sendMail(tenantId, paymentReceiveId, messageDTO); +// done(); +// } catch (error) { +// console.log(error); +// done(error); +// } +// }; +// } diff --git a/packages/server-nest/src/modules/PaymentReceived/PaymentReceivedSmsNotify.ts b/packages/server-nest/src/modules/PaymentReceived/PaymentReceivedSmsNotify.ts new file mode 100644 index 000000000..f0b06c471 --- /dev/null +++ b/packages/server-nest/src/modules/PaymentReceived/PaymentReceivedSmsNotify.ts @@ -0,0 +1,213 @@ +// import { Service, Inject } from 'typedi'; +// import HasTenancyService from '@/services/Tenancy/TenancyService'; +// import events from '@/subscribers/events'; +// import { +// IPaymentReceivedSmsDetails, +// SMS_NOTIFICATION_KEY, +// IPaymentReceived, +// IPaymentReceivedEntry, +// } from '@/interfaces'; +// import SmsNotificationsSettingsService from '@/services/Settings/SmsNotificationsSettings'; +// import { formatNumber, formatSmsMessage } from 'utils'; +// import { TenantMetadata } from '@/system/models'; +// import SaleNotifyBySms from '../SaleNotifyBySms'; +// import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; +// import { PaymentReceivedValidators } from './commands/PaymentReceivedValidators.service'; + +// @Service() +// export class PaymentReceiveNotifyBySms { +// @Inject() +// private tenancy: HasTenancyService; + +// @Inject() +// private eventPublisher: EventPublisher; + +// @Inject() +// private smsNotificationsSettings: SmsNotificationsSettingsService; + +// @Inject() +// private saleSmsNotification: SaleNotifyBySms; + +// @Inject() +// private validators: PaymentReceivedValidators; + +// /** +// * Notify customer via sms about payment receive details. +// * @param {number} tenantId - Tenant id. +// * @param {number} paymentReceiveid - Payment receive id. +// */ +// public async notifyBySms(tenantId: number, paymentReceiveid: number) { +// const { PaymentReceive } = this.tenancy.models(tenantId); + +// // Retrieve the payment receive or throw not found service error. +// const paymentReceive = await PaymentReceive.query() +// .findById(paymentReceiveid) +// .withGraphFetched('customer') +// .withGraphFetched('entries.invoice'); + +// // Validates the payment existance. +// this.validators.validatePaymentExistance(paymentReceive); + +// // Validate the customer phone number. +// this.saleSmsNotification.validateCustomerPhoneNumber( +// paymentReceive.customer.personalPhone +// ); +// // Triggers `onPaymentReceiveNotifySms` event. +// await this.eventPublisher.emitAsync(events.paymentReceive.onNotifySms, { +// tenantId, +// paymentReceive, +// }); +// // Sends the payment receive sms notification to the given customer. +// await this.sendSmsNotification(tenantId, paymentReceive); + +// // Triggers `onPaymentReceiveNotifiedSms` event. +// await this.eventPublisher.emitAsync(events.paymentReceive.onNotifiedSms, { +// tenantId, +// paymentReceive, +// }); +// return paymentReceive; +// } + +// /** +// * Sends the payment details sms notification of the given customer. +// * @param {number} tenantId +// * @param {IPaymentReceived} paymentReceive +// * @param {ICustomer} customer +// */ +// private sendSmsNotification = async ( +// tenantId: number, +// paymentReceive: IPaymentReceived +// ) => { +// const smsClient = this.tenancy.smsClient(tenantId); +// const tenantMetadata = await TenantMetadata.query().findOne({ tenantId }); + +// // Retrieve the formatted payment details sms notification message. +// const message = this.formattedPaymentDetailsMessage( +// tenantId, +// paymentReceive, +// tenantMetadata +// ); +// // The target phone number. +// const phoneNumber = paymentReceive.customer.personalPhone; + +// await smsClient.sendMessageJob(phoneNumber, message); +// }; + +// /** +// * Notify via SMS message after payment transaction creation. +// * @param {number} tenantId +// * @param {number} paymentReceiveId +// * @returns {Promise} +// */ +// public notifyViaSmsNotificationAfterCreation = async ( +// tenantId: number, +// paymentReceiveId: number +// ): Promise => { +// const notification = this.smsNotificationsSettings.getSmsNotificationMeta( +// tenantId, +// SMS_NOTIFICATION_KEY.PAYMENT_RECEIVE_DETAILS +// ); +// // Can't continue if the sms auto-notification is not enabled. +// if (!notification.isNotificationEnabled) return; + +// await this.notifyBySms(tenantId, paymentReceiveId); +// }; + +// /** +// * Formates the payment receive details sms message. +// * @param {number} tenantId - +// * @param {IPaymentReceived} payment - +// * @param {ICustomer} customer - +// */ +// private formattedPaymentDetailsMessage = ( +// tenantId: number, +// payment: IPaymentReceived, +// tenantMetadata: TenantMetadata +// ) => { +// const notification = this.smsNotificationsSettings.getSmsNotificationMeta( +// tenantId, +// SMS_NOTIFICATION_KEY.PAYMENT_RECEIVE_DETAILS +// ); +// return this.formatPaymentDetailsMessage( +// notification.smsMessage, +// payment, +// tenantMetadata +// ); +// }; + +// /** +// * Formattes the payment details sms notification messafge. +// * @param {string} smsMessage +// * @param {IPaymentReceived} payment +// * @param {ICustomer} customer +// * @param {TenantMetadata} tenantMetadata +// * @returns {string} +// */ +// private formatPaymentDetailsMessage = ( +// smsMessage: string, +// payment: IPaymentReceived, +// tenantMetadata: any +// ): string => { +// const invoiceNumbers = this.stringifyPaymentInvoicesNumber(payment); + +// // Formattes the payment number variable. +// const formattedPaymentNumber = formatNumber(payment.amount, { +// currencyCode: payment.currencyCode, +// }); + +// return formatSmsMessage(smsMessage, { +// Amount: formattedPaymentNumber, +// ReferenceNumber: payment.referenceNo, +// CustomerName: payment.customer.displayName, +// PaymentNumber: payment.paymentReceiveNo, +// InvoiceNumber: invoiceNumbers, +// CompanyName: tenantMetadata.name, +// }); +// }; + +// /** +// * Stringify payment receive invoices to numbers as string. +// * @param {IPaymentReceived} payment +// * @returns {string} +// */ +// private stringifyPaymentInvoicesNumber(payment: IPaymentReceived) { +// const invoicesNumberes = payment.entries.map( +// (entry: IPaymentReceivedEntry) => entry.invoice.invoiceNo +// ); +// return invoicesNumberes.join(', '); +// } + +// /** +// * Retrieve the SMS details of the given invoice. +// * @param {number} tenantId - Tenant id. +// * @param {number} paymentReceiveid - Payment receive id. +// */ +// public smsDetails = async ( +// tenantId: number, +// paymentReceiveid: number +// ): Promise => { +// const { PaymentReceive } = this.tenancy.models(tenantId); + +// // Retrieve the payment receive or throw not found service error. +// const paymentReceive = await PaymentReceive.query() +// .findById(paymentReceiveid) +// .withGraphFetched('customer') +// .withGraphFetched('entries.invoice'); + +// // Current tenant metadata. +// const tenantMetadata = await TenantMetadata.query().findOne({ tenantId }); + +// // Retrieve the formatted sms message of payment receive details. +// const smsMessage = this.formattedPaymentDetailsMessage( +// tenantId, +// paymentReceive, +// tenantMetadata +// ); + +// return { +// customerName: paymentReceive.customer.displayName, +// customerPhoneNumber: paymentReceive.customer.personalPhone, +// smsMessage, +// }; +// }; +// } diff --git a/packages/server-nest/src/modules/PaymentReceived/PaymentReceivedSmsSubscriber.ts b/packages/server-nest/src/modules/PaymentReceived/PaymentReceivedSmsSubscriber.ts new file mode 100644 index 000000000..7a009f294 --- /dev/null +++ b/packages/server-nest/src/modules/PaymentReceived/PaymentReceivedSmsSubscriber.ts @@ -0,0 +1,27 @@ +// import { Container } from 'typedi'; +// import { On, EventSubscriber } from 'event-dispatch'; +// import events from '@/subscribers/events'; +// import { PaymentReceiveNotifyBySms } from './PaymentReceivedSmsNotify'; + +// @EventSubscriber() +// export default class SendSmsNotificationPaymentReceive { +// paymentReceiveNotifyBySms: PaymentReceiveNotifyBySms; + +// constructor() { +// this.paymentReceiveNotifyBySms = Container.get(PaymentReceiveNotifyBySms); +// } + +// /** +// * +// */ +// @On(events.paymentReceive.onNotifySms) +// async sendSmsNotificationOnceInvoiceNotify({ +// paymentReceive, +// customer, +// }) { +// await this.paymentReceiveNotifyBySms.sendSmsNotification( +// paymentReceive, +// customer +// ); +// } +// } diff --git a/packages/server-nest/src/modules/PaymentReceived/PaymentsReceivedExportable.ts b/packages/server-nest/src/modules/PaymentReceived/PaymentsReceivedExportable.ts new file mode 100644 index 000000000..fdac2fda6 --- /dev/null +++ b/packages/server-nest/src/modules/PaymentReceived/PaymentsReceivedExportable.ts @@ -0,0 +1,39 @@ +// import { Inject, Service } from 'typedi'; +// import { IAccountsStructureType, IPaymentsReceivedFilter } from '@/interfaces'; +// import { Exportable } from '@/services/Export/Exportable'; +// import { PaymentReceivesApplication } from './PaymentReceived.application'; +// import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants'; + +// @Service() +// export class PaymentsReceivedExportable extends Exportable { +// @Inject() +// private paymentReceivedApp: PaymentReceivesApplication; + +// /** +// * Retrieves the accounts data to exportable sheet. +// * @param {number} tenantId +// * @param {IPaymentsReceivedFilter} query - +// * @returns +// */ +// public exportable(tenantId: number, query: IPaymentsReceivedFilter) { +// const filterQuery = (builder) => { +// builder.withGraphFetched('entries.invoice'); +// builder.withGraphFetched('branch'); +// }; + +// const parsedQuery = { +// sortOrder: 'desc', +// columnSortBy: 'created_at', +// inactiveMode: false, +// ...query, +// structure: IAccountsStructureType.Flat, +// page: 1, +// pageSize: EXPORT_SIZE_LIMIT, +// filterQuery, +// } as IPaymentsReceivedFilter; + +// return this.paymentReceivedApp +// .getPaymentReceives(tenantId, parsedQuery) +// .then((output) => output.paymentReceives); +// } +// } diff --git a/packages/server-nest/src/modules/PaymentReceived/PaymentsReceivedImportable.ts b/packages/server-nest/src/modules/PaymentReceived/PaymentsReceivedImportable.ts new file mode 100644 index 000000000..2b220af3d --- /dev/null +++ b/packages/server-nest/src/modules/PaymentReceived/PaymentsReceivedImportable.ts @@ -0,0 +1,46 @@ +// import { Inject, Service } from 'typedi'; +// import { Knex } from 'knex'; +// import { IPaymentReceivedCreateDTO } from '@/interfaces'; +// import { Importable } from '@/services/Import/Importable'; +// import { CreatePaymentReceived } from './commands/CreatePaymentReceived.serivce'; +// import { PaymentsReceiveSampleData } from './constants'; + +// @Service() +// export class PaymentsReceivedImportable extends Importable { +// @Inject() +// private createPaymentReceiveService: CreatePaymentReceived; + +// /** +// * Importing to account service. +// * @param {number} tenantId +// * @param {IAccountCreateDTO} createAccountDTO +// * @returns +// */ +// public importable( +// tenantId: number, +// createPaymentDTO: IPaymentReceivedCreateDTO, +// trx?: Knex.Transaction +// ) { +// return this.createPaymentReceiveService.createPaymentReceived( +// tenantId, +// createPaymentDTO, +// {}, +// trx +// ); +// } + +// /** +// * Concurrrency controlling of the importing process. +// * @returns {number} +// */ +// public get concurrency() { +// return 1; +// } + +// /** +// * Retrieves the sample data that used to download accounts sample sheet. +// */ +// public sampleData(): any[] { +// return PaymentsReceiveSampleData; +// } +// } diff --git a/packages/server-nest/src/modules/PaymentReceived/commands/CreatePaymentReceived.serivce.ts b/packages/server-nest/src/modules/PaymentReceived/commands/CreatePaymentReceived.serivce.ts new file mode 100644 index 000000000..fd14771b0 --- /dev/null +++ b/packages/server-nest/src/modules/PaymentReceived/commands/CreatePaymentReceived.serivce.ts @@ -0,0 +1,121 @@ +import { Knex } from 'knex'; +import { + IPaymentReceivedCreateDTO, + IPaymentReceivedCreatedPayload, + IPaymentReceivedCreatingPayload, +} from '../types/PaymentReceived.types'; +import { PaymentReceivedValidators } from './PaymentReceivedValidators.service'; +import { PaymentReceiveDTOTransformer } from './PaymentReceivedDTOTransformer'; +import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { PaymentReceived } from '../models/PaymentReceived'; +import { events } from '@/common/events/events'; +import { Customer } from '@/modules/Customers/models/Customer'; +import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service'; +import { Inject, Injectable } from '@nestjs/common'; + +@Injectable() +export class CreatePaymentReceivedService { + constructor( + private validators: PaymentReceivedValidators, + private eventPublisher: EventEmitter2, + private uow: UnitOfWork, + private transformer: PaymentReceiveDTOTransformer, + private tenancyContext: TenancyContext, + + @Inject(PaymentReceived.name) + private paymentReceived: typeof PaymentReceived, + + @Inject(Customer.name) + private customer: typeof Customer, + ) {} + + /** + * Creates a new payment receive and store it to the storage + * with associated invoices payment and journal transactions. + * @param {IPaymentReceivedCreateDTO} paymentReceiveDTO + * @param {Knex.Transaction} trx - Database transaction. + */ + public async createPaymentReceived( + paymentReceiveDTO: IPaymentReceivedCreateDTO, + trx?: Knex.Transaction, + ) { + const tenant = await this.tenancyContext.getTenant(true); + + // Validate customer existance. + const paymentCustomer = await this.customer + .query() + .modify('customer') + .findById(paymentReceiveDTO.customerId) + .throwIfNotFound(); + + // Transformes the payment receive DTO to model. + const paymentReceiveObj = await this.transformCreateDTOToModel( + paymentCustomer, + paymentReceiveDTO, + ); + // Validate payment receive number uniquiness. + await this.validators.validatePaymentReceiveNoExistance( + paymentReceiveObj.paymentReceiveNo, + ); + // Validate the deposit account existance and type. + const depositAccount = await this.validators.getDepositAccountOrThrowError( + paymentReceiveDTO.depositAccountId, + ); + // Validate payment receive invoices IDs existance. + await this.validators.validateInvoicesIDsExistance( + paymentReceiveDTO.customerId, + paymentReceiveDTO.entries, + ); + // Validate invoice payment amount. + await this.validators.validateInvoicesPaymentsAmount( + paymentReceiveDTO.entries, + ); + // Validates the payment account currency code. + this.validators.validatePaymentAccountCurrency( + depositAccount.currencyCode, + paymentCustomer.currencyCode, + tenant?.metadata.baseCurrency, + ); + // Creates a payment receive transaction under UOW envirment. + return this.uow.withTransaction(async (trx: Knex.Transaction) => { + // Triggers `onPaymentReceiveCreating` event. + await this.eventPublisher.emitAsync(events.paymentReceive.onCreating, { + trx, + paymentReceiveDTO, + } as IPaymentReceivedCreatingPayload); + + // Inserts the payment receive transaction. + const paymentReceive = await this.paymentReceived + .query(trx) + .insertGraphAndFetch({ + ...paymentReceiveObj, + }); + // Triggers `onPaymentReceiveCreated` event. + await this.eventPublisher.emitAsync(events.paymentReceive.onCreated, { + paymentReceive, + paymentReceiveId: paymentReceive.id, + paymentReceiveDTO, + trx, + } as IPaymentReceivedCreatedPayload); + + return paymentReceive; + }, trx); + } + + /** + * Transform the create payment receive DTO. + * @param {ICustomer} customer + * @param {IPaymentReceivedCreateDTO} paymentReceiveDTO + * @returns + */ + private transformCreateDTOToModel = async ( + customer: Customer, + paymentReceiveDTO: IPaymentReceivedCreateDTO, + ) => { + return this.transformer.transformPaymentReceiveDTOToModel( + customer, + paymentReceiveDTO, + ); + }; +} diff --git a/packages/server-nest/src/modules/PaymentReceived/commands/DeletePaymentReceived.service.ts b/packages/server-nest/src/modules/PaymentReceived/commands/DeletePaymentReceived.service.ts new file mode 100644 index 000000000..93cf7a789 --- /dev/null +++ b/packages/server-nest/src/modules/PaymentReceived/commands/DeletePaymentReceived.service.ts @@ -0,0 +1,73 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Knex } from 'knex'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service'; +import { PaymentReceived } from '../models/PaymentReceived'; +import { PaymentReceivedEntry } from '../models/PaymentReceivedEntry'; +import { events } from '@/common/events/events'; +import { IPaymentReceivedDeletingPayload } from '../types/PaymentReceived.types'; +import { IPaymentReceivedDeletedPayload } from '../types/PaymentReceived.types'; + +@Injectable() +export class DeletePaymentReceived { + constructor( + private eventPublisher: EventEmitter2, + private uow: UnitOfWork, + + @Inject(PaymentReceived.name) + private paymentReceiveModel: typeof PaymentReceived, + + @Inject(PaymentReceivedEntry.name) + private paymentReceiveEntryModel: typeof PaymentReceivedEntry, + ) {} + + /** + * Deletes the given payment receive with associated entries + * and journal transactions. + * ----- + * - Deletes the payment receive transaction. + * - Deletes the payment receive associated entries. + * - Deletes the payment receive associated journal transactions. + * - Revert the customer balance. + * - Revert the payment amount of the associated invoices. + * @async + * @param {Integer} paymentReceiveId - Payment receive id. + * @param {IPaymentReceived} paymentReceive - Payment receive object. + */ + public async deletePaymentReceive(paymentReceiveId: number) { + // Retreive payment receive or throw not found service error. + const oldPaymentReceive = await this.paymentReceiveModel + .query() + .withGraphFetched('entries') + .findById(paymentReceiveId) + .throwIfNotFound(); + + // Delete payment receive transaction and associate transactions under UOW env. + return this.uow.withTransaction(async (trx: Knex.Transaction) => { + // Triggers `onPaymentReceiveDeleting` event. + await this.eventPublisher.emitAsync(events.paymentReceive.onDeleting, { + oldPaymentReceive, + trx, + } as IPaymentReceivedDeletingPayload); + + // Deletes the payment receive associated entries. + await this.paymentReceiveEntryModel + .query(trx) + .where('payment_receive_id', paymentReceiveId) + .delete(); + + // Deletes the payment receive transaction. + await this.paymentReceiveModel + .query(trx) + .findById(paymentReceiveId) + .delete(); + + // Triggers `onPaymentReceiveDeleted` event. + await this.eventPublisher.emitAsync(events.paymentReceive.onDeleted, { + paymentReceiveId, + oldPaymentReceive, + trx, + } as IPaymentReceivedDeletedPayload); + }); + } +} diff --git a/packages/server-nest/src/modules/PaymentReceived/commands/EditPaymentReceived.service.ts b/packages/server-nest/src/modules/PaymentReceived/commands/EditPaymentReceived.service.ts new file mode 100644 index 000000000..7edef0cf9 --- /dev/null +++ b/packages/server-nest/src/modules/PaymentReceived/commands/EditPaymentReceived.service.ts @@ -0,0 +1,160 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Knex } from 'knex'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { + IPaymentReceivedEditDTO, + IPaymentReceivedEditedPayload, + IPaymentReceivedEditingPayload, +} from '../types/PaymentReceived.types'; +import { PaymentReceiveDTOTransformer } from './PaymentReceivedDTOTransformer'; +import { PaymentReceivedValidators } from './PaymentReceivedValidators.service'; +import { PaymentReceived } from '../models/PaymentReceived'; +import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service'; +import { events } from '@/common/events/events'; +import { Customer } from '@/modules/Customers/models/Customer'; +import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service'; + +@Injectable() +export class EditPaymentReceived { + constructor( + private readonly transformer: PaymentReceiveDTOTransformer, + private readonly validators: PaymentReceivedValidators, + private readonly eventPublisher: EventEmitter2, + private readonly uow: UnitOfWork, + private readonly tenancyContext: TenancyContext, + + @Inject(PaymentReceived) + private readonly paymentReceiveModel: typeof PaymentReceived, + + @Inject(Customer.name) + private readonly customerModel: typeof Customer, + ) {} + + /** + * Edit details the given payment receive with associated entries. + * ------ + * - Update the payment receive transactions. + * - Insert the new payment receive entries. + * - Update the given payment receive entries. + * - Delete the not presented payment receive entries. + * - Re-insert the journal transactions and update the different accounts balance. + * - Update the different customer balances. + * - Update the different invoice payment amount. + * @async + * @param {number} paymentReceiveId - + * @param {IPaymentReceivedEditDTO} paymentReceiveDTO - + */ + public async editPaymentReceive( + paymentReceiveId: number, + paymentReceiveDTO: IPaymentReceivedEditDTO, + ) { + const tenant = await this.tenancyContext.getTenant(true); + + // Validate the payment receive existance. + const oldPaymentReceive = await this.paymentReceiveModel + .query() + .withGraphFetched('entries') + .findById(paymentReceiveId) + .throwIfNotFound(); + + // Validates the payment existance. + this.validators.validatePaymentExistance(oldPaymentReceive); + + // Validate customer existance. + const customer = await this.customerModel + .query() + .findById(paymentReceiveDTO.customerId) + .throwIfNotFound(); + + // Transformes the payment receive DTO to model. + const paymentReceiveObj = await this.transformEditDTOToModel( + customer, + paymentReceiveDTO, + oldPaymentReceive, + ); + // Validate customer whether modified. + this.validators.validateCustomerNotModified( + paymentReceiveDTO, + oldPaymentReceive, + ); + // Validate payment receive number uniquiness. + if (paymentReceiveDTO.paymentReceiveNo) { + await this.validators.validatePaymentReceiveNoExistance( + paymentReceiveDTO.paymentReceiveNo, + paymentReceiveId, + ); + } + // Validate the deposit account existance and type. + const depositAccount = await this.validators.getDepositAccountOrThrowError( + paymentReceiveDTO.depositAccountId, + ); + // Validate the entries ids existance on payment receive type. + await this.validators.validateEntriesIdsExistance( + paymentReceiveId, + paymentReceiveDTO.entries, + ); + // Validate payment receive invoices IDs existance and associated + // to the given customer id. + await this.validators.validateInvoicesIDsExistance( + oldPaymentReceive.customerId, + paymentReceiveDTO.entries, + ); + // Validate invoice payment amount. + await this.validators.validateInvoicesPaymentsAmount( + paymentReceiveDTO.entries, + oldPaymentReceive.entries, + ); + // Validates the payment account currency code. + this.validators.validatePaymentAccountCurrency( + depositAccount.currencyCode, + customer.currencyCode, + tenant?.metadata.baseCurrency, + ); + // Creates payment receive transaction under UOW envirement. + return this.uow.withTransaction(async (trx: Knex.Transaction) => { + // Triggers `onPaymentReceiveEditing` event. + await this.eventPublisher.emitAsync(events.paymentReceive.onEditing, { + trx, + oldPaymentReceive, + paymentReceiveDTO, + } as IPaymentReceivedEditingPayload); + + // Update the payment receive transaction. + const paymentReceive = await this.paymentReceiveModel + .query(trx) + .upsertGraphAndFetch({ + id: paymentReceiveId, + ...paymentReceiveObj, + }); + // Triggers `onPaymentReceiveEdited` event. + await this.eventPublisher.emitAsync(events.paymentReceive.onEdited, { + paymentReceiveId, + paymentReceive, + oldPaymentReceive, + paymentReceiveDTO, + trx, + } as IPaymentReceivedEditedPayload); + + return paymentReceive; + }); + } + + /** + * Transform the edit payment receive DTO. + * @param {ICustomer} customer + * @param {IPaymentReceivedEditDTO} paymentReceiveDTO + * @param {IPaymentReceived} oldPaymentReceive + * @returns + */ + private transformEditDTOToModel = async ( + customer: Customer, + paymentReceiveDTO: IPaymentReceivedEditDTO, + oldPaymentReceive: PaymentReceived, + ) => { + return this.transformer.transformPaymentReceiveDTOToModel( + customer, + paymentReceiveDTO, + oldPaymentReceive, + ); + }; +} diff --git a/packages/server-nest/src/modules/PaymentReceived/commands/PaymentReceivedDTOTransformer.ts b/packages/server-nest/src/modules/PaymentReceived/commands/PaymentReceivedDTOTransformer.ts new file mode 100644 index 000000000..971508164 --- /dev/null +++ b/packages/server-nest/src/modules/PaymentReceived/commands/PaymentReceivedDTOTransformer.ts @@ -0,0 +1,83 @@ +import * as R from 'ramda'; +import { Inject, Injectable } from '@nestjs/common'; +import { omit, sumBy } from 'lodash'; +import composeAsync from 'async/compose'; +import { + IPaymentReceivedCreateDTO, + IPaymentReceivedEditDTO, +} from '../types/PaymentReceived.types'; +import { PaymentReceivedValidators } from './PaymentReceivedValidators.service'; +import { PaymentReceivedIncrement } from './PaymentReceivedIncrement.service'; +import { BranchTransactionDTOTransformer } from '@/modules/Branches/integrations/BranchTransactionDTOTransform'; +import { BrandingTemplateDTOTransformer } from '@/modules/PdfTemplate/BrandingTemplateDTOTransformer'; +import { PaymentReceived } from '../models/PaymentReceived'; +import { assocItemEntriesDefaultIndex } from '@/utils/associate-item-entries-index'; +import { Customer } from '@/modules/Customers/models/Customer'; +import { formatDateFields } from '@/utils/format-date-fields'; + +@Injectable() +export class PaymentReceiveDTOTransformer { + constructor( + private readonly validators: PaymentReceivedValidators, + private readonly increments: PaymentReceivedIncrement, + private readonly branchDTOTransform: BranchTransactionDTOTransformer, + private readonly brandingTemplatesTransformer: BrandingTemplateDTOTransformer, + + @Inject(PaymentReceived.name) + private readonly paymentReceivedModel: typeof PaymentReceived, + ) {} + + /** + * Transformes the create payment receive DTO to model object. + * @param {IPaymentReceivedCreateDTO|IPaymentReceivedEditDTO} paymentReceiveDTO - Payment receive DTO. + * @param {IPaymentReceived} oldPaymentReceive - + * @return {IPaymentReceived} + */ + public async transformPaymentReceiveDTOToModel( + customer: Customer, + paymentReceiveDTO: IPaymentReceivedCreateDTO | IPaymentReceivedEditDTO, + oldPaymentReceive?: PaymentReceived + ): Promise { + const amount = + paymentReceiveDTO.amount ?? + sumBy(paymentReceiveDTO.entries, 'paymentAmount'); + + // Retreive the next invoice number. + const autoNextNumber = + this.increments.getNextPaymentReceiveNumber(); + + // Retrieve the next payment receive number. + const paymentReceiveNo = + paymentReceiveDTO.paymentReceiveNo || + oldPaymentReceive?.paymentReceiveNo || + autoNextNumber; + + this.validators.validatePaymentNoRequire(paymentReceiveNo); + + const entries = R.compose( + // Associate the default index to each item entry line. + assocItemEntriesDefaultIndex + )(paymentReceiveDTO.entries); + + const initialDTO = { + ...formatDateFields(omit(paymentReceiveDTO, ['entries', 'attachments']), [ + 'paymentDate', + ]), + amount, + currencyCode: customer.currencyCode, + ...(paymentReceiveNo ? { paymentReceiveNo } : {}), + exchangeRate: paymentReceiveDTO.exchangeRate || 1, + entries, + }; + const initialAsyncDTO = await composeAsync( + // Assigns the default branding template id to the invoice DTO. + this.brandingTemplatesTransformer.assocDefaultBrandingTemplate( + 'SaleInvoice' + ) + )(initialDTO); + + return R.compose( + this.branchDTOTransform.transformDTO + )(initialAsyncDTO); + } +} diff --git a/packages/server-nest/src/modules/PaymentReceived/commands/PaymentReceivedIncrement.service.ts b/packages/server-nest/src/modules/PaymentReceived/commands/PaymentReceivedIncrement.service.ts new file mode 100644 index 000000000..6703b3082 --- /dev/null +++ b/packages/server-nest/src/modules/PaymentReceived/commands/PaymentReceivedIncrement.service.ts @@ -0,0 +1,31 @@ +import { Injectable } from '@nestjs/common'; +import { AutoIncrementOrdersService } from '@/modules/AutoIncrementOrders/AutoIncrementOrders.service'; + +@Injectable() +export class PaymentReceivedIncrement { + /** + * @param {AutoIncrementOrdersService} autoIncrementOrdersService - Auto increment orders service. + */ + constructor( + private readonly autoIncrementOrdersService: AutoIncrementOrdersService, + ) {} + + /** + * Retrieve the next unique payment receive number. + * @return {string} + */ + public getNextPaymentReceiveNumber(): string { + return this.autoIncrementOrdersService.getNextTransactionNumber( + 'payment_receives', + ); + } + + /** + * Increment the payment receive next number. + */ + public incrementNextPaymentReceiveNumber() { + return this.autoIncrementOrdersService.incrementSettingsNextNumber( + 'payment_receives', + ); + } +} diff --git a/packages/server-nest/src/modules/PaymentReceived/commands/PaymentReceivedInvoiceSync.service.ts b/packages/server-nest/src/modules/PaymentReceived/commands/PaymentReceivedInvoiceSync.service.ts new file mode 100644 index 000000000..74c4dd7f5 --- /dev/null +++ b/packages/server-nest/src/modules/PaymentReceived/commands/PaymentReceivedInvoiceSync.service.ts @@ -0,0 +1,46 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Knex } from 'knex'; +import { SaleInvoice } from '../../SaleInvoices/models/SaleInvoice'; +import { IPaymentReceivedEntryDTO } from '../types/PaymentReceived.types'; +import { entriesAmountDiff } from '@/utils/entries-amount-diff'; + +@Injectable() +export class PaymentReceivedInvoiceSync { + constructor( + @Inject(SaleInvoice.name) + private readonly saleInvoiceModel: typeof SaleInvoice, + ) {} + + /** + * Saves difference changing between old and new invoice payment amount. + * @param {Array} paymentReceiveEntries + * @param {Array} newPaymentReceiveEntries + * @return {Promise} + */ + public async saveChangeInvoicePaymentAmount( + newPaymentReceiveEntries: IPaymentReceivedEntryDTO[], + oldPaymentReceiveEntries?: IPaymentReceivedEntryDTO[], + trx?: Knex.Transaction + ): Promise { + const opers: Promise[] = []; + + const diffEntries = entriesAmountDiff( + newPaymentReceiveEntries, + oldPaymentReceiveEntries, + 'paymentAmount', + 'invoiceId' + ); + diffEntries.forEach((diffEntry: any) => { + if (diffEntry.paymentAmount === 0) { + return; + } + const oper = this.saleInvoiceModel.changePaymentAmount( + diffEntry.invoiceId, + diffEntry.paymentAmount, + trx + ); + opers.push(oper); + }); + await Promise.all([...opers]); + } +} diff --git a/packages/server-nest/src/modules/PaymentReceived/commands/PaymentReceivedValidators.service.ts b/packages/server-nest/src/modules/PaymentReceived/commands/PaymentReceivedValidators.service.ts new file mode 100644 index 000000000..c51ca70e9 --- /dev/null +++ b/packages/server-nest/src/modules/PaymentReceived/commands/PaymentReceivedValidators.service.ts @@ -0,0 +1,277 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { difference, sumBy } from 'lodash'; +import { + IPaymentReceivedEditDTO, + IPaymentReceivedEntryDTO, +} from '../types/PaymentReceived.types'; +import { ERRORS } from '../constants'; +import { PaymentReceived } from '../models/PaymentReceived'; +import { PaymentReceivedEntry } from '../models/PaymentReceivedEntry'; +import { Account } from '@/modules/Accounts/models/Account.model'; +import { SaleInvoice } from '@/modules/SaleInvoices/models/SaleInvoice'; +import { ServiceError } from '@/modules/Items/ServiceError'; +import { ACCOUNT_TYPE } from '@/constants/accounts'; + +@Injectable() +export class PaymentReceivedValidators { + constructor( + @Inject(PaymentReceived.name) + private readonly paymentReceiveModel: typeof PaymentReceived, + + @Inject(PaymentReceivedEntry.name) + private readonly paymentReceiveEntryModel: typeof PaymentReceivedEntry, + + @Inject(SaleInvoice.name) + private readonly saleInvoiceModel: typeof SaleInvoice, + + @Inject(Account.name) + private readonly accountModel: typeof Account, + ) {} + + /** + * Validates the payment existance. + * @param {PaymentReceive | null | undefined} payment + */ + public validatePaymentExistance(payment: PaymentReceived | null | undefined) { + if (!payment) { + throw new ServiceError(ERRORS.PAYMENT_RECEIVE_NOT_EXISTS); + } + } + + /** + * Validates the payment receive number existance. + * @param {number} tenantId - + * @param {string} paymentReceiveNo - + */ + public async validatePaymentReceiveNoExistance( + paymentReceiveNo: string, + notPaymentReceiveId?: number + ): Promise { + const paymentReceive = await this.paymentReceiveModel.query() + .findOne('payment_receive_no', paymentReceiveNo) + .onBuild((builder) => { + if (notPaymentReceiveId) { + builder.whereNot('id', notPaymentReceiveId); + } + }); + + if (paymentReceive) { + throw new ServiceError(ERRORS.PAYMENT_RECEIVE_NO_EXISTS); + } + } + + /** + * Validates the invoices IDs existance. + * @param {number} customerId - + * @param {IPaymentReceivedEntryDTO[]} paymentReceiveEntries - + */ + public async validateInvoicesIDsExistance( + customerId: number, + paymentReceiveEntries: { invoiceId: number }[] + ): Promise { + const invoicesIds = paymentReceiveEntries.map( + (e: { invoiceId: number }) => e.invoiceId + ); + const storedInvoices = await this.saleInvoiceModel.query() + .whereIn('id', invoicesIds) + .where('customer_id', customerId); + + const storedInvoicesIds = storedInvoices.map((invoice) => invoice.id); + const notFoundInvoicesIDs = difference(invoicesIds, storedInvoicesIds); + + if (notFoundInvoicesIDs.length > 0) { + throw new ServiceError(ERRORS.INVOICES_IDS_NOT_FOUND); + } + // Filters the not delivered invoices. + const notDeliveredInvoices = storedInvoices.filter( + (invoice) => !invoice.isDelivered + ); + if (notDeliveredInvoices.length > 0) { + throw new ServiceError(ERRORS.INVOICES_NOT_DELIVERED_YET, null, { + notDeliveredInvoices, + }); + } + return storedInvoices; + } + + /** + * Validates entries invoice payment amount. + * @param {IPaymentReceivedEntryDTO[]} paymentReceiveEntries + * @param {IPaymentReceivedEntry[]} oldPaymentEntries + */ + public async validateInvoicesPaymentsAmount( + paymentReceiveEntries: IPaymentReceivedEntryDTO[], + oldPaymentEntries: PaymentReceivedEntry[] = [] + ) { + const invoicesIds = paymentReceiveEntries.map( + (e: IPaymentReceivedEntryDTO) => e.invoiceId + ); + + const storedInvoices = await this.saleInvoiceModel.query().whereIn('id', invoicesIds); + + const storedInvoicesMap = new Map( + storedInvoices.map((invoice: SaleInvoice) => { + const oldEntries = oldPaymentEntries.filter((entry) => entry.invoiceId); + const oldPaymentAmount = sumBy(oldEntries, 'paymentAmount') || 0; + + return [ + invoice.id, + { ...invoice, dueAmount: invoice.dueAmount + oldPaymentAmount }, + ]; + }) + ); + const hasWrongPaymentAmount: any[] = []; + + paymentReceiveEntries.forEach( + (entry: IPaymentReceivedEntryDTO, index: number) => { + const entryInvoice = storedInvoicesMap.get(entry.invoiceId); + const { dueAmount } = entryInvoice; + + if (dueAmount < entry.paymentAmount) { + hasWrongPaymentAmount.push({ index, due_amount: dueAmount }); + } + } + ); + if (hasWrongPaymentAmount.length > 0) { + throw new ServiceError(ERRORS.INVALID_PAYMENT_AMOUNT); + } + } + + /** + * Validate the payment receive number require. + * @param {IPaymentReceived} paymentReceiveObj + */ + public validatePaymentReceiveNoRequire(paymentReceiveObj: PaymentReceived) { + if (!paymentReceiveObj.paymentReceiveNo) { + throw new ServiceError(ERRORS.PAYMENT_RECEIVE_NO_IS_REQUIRED); + } + } + + /** + * Validate the payment receive entries IDs existance. + * @param {number} paymentReceiveId + * @param {IPaymentReceivedEntryDTO[]} paymentReceiveEntries + */ + public async validateEntriesIdsExistance( + paymentReceiveId: number, + paymentReceiveEntries: IPaymentReceivedEntryDTO[] + ) { + const entriesIds = paymentReceiveEntries + .filter((entry) => entry.id) + .map((entry) => entry.id); + + const storedEntries = await this.paymentReceiveEntryModel.query().where( + 'payment_receive_id', + paymentReceiveId + ); + const storedEntriesIds = storedEntries.map((entry: any) => entry.id); + const notFoundEntriesIds = difference(entriesIds, storedEntriesIds); + + if (notFoundEntriesIds.length > 0) { + throw new ServiceError(ERRORS.ENTRIES_IDS_NOT_EXISTS); + } + } + + /** + * Validates the payment receive number require. + * @param {string} paymentReceiveNo + */ + public validatePaymentNoRequire(paymentReceiveNo: string) { + if (!paymentReceiveNo) { + throw new ServiceError(ERRORS.PAYMENT_RECEIVE_NO_REQUIRED); + } + } + + /** + * Validate the payment customer whether modified. + * @param {IPaymentReceivedEditDTO} paymentReceiveDTO + * @param {IPaymentReceived} oldPaymentReceive + */ + public validateCustomerNotModified( + paymentReceiveDTO: IPaymentReceivedEditDTO, + oldPaymentReceive: PaymentReceived + ) { + if (paymentReceiveDTO.customerId !== oldPaymentReceive.customerId) { + throw new ServiceError(ERRORS.PAYMENT_CUSTOMER_SHOULD_NOT_UPDATE); + } + } + + /** + * Validates the payment account currency code. The deposit account curreny + * should be equals the customer currency code or the base currency. + * @param {string} paymentAccountCurrency + * @param {string} customerCurrency + * @param {string} baseCurrency + * @throws {ServiceError(ERRORS.PAYMENT_ACCOUNT_CURRENCY_INVALID)} + */ + public validatePaymentAccountCurrency = ( + paymentAccountCurrency: string, + customerCurrency: string, + baseCurrency: string + ) => { + if ( + paymentAccountCurrency !== customerCurrency && + paymentAccountCurrency !== baseCurrency + ) { + throw new ServiceError(ERRORS.PAYMENT_ACCOUNT_CURRENCY_INVALID); + } + }; + + /** + * Validates the payment receive existance. + * @param {number} paymentReceiveId - Payment receive id. + */ + async getPaymentReceiveOrThrowError( + paymentReceiveId: number + ): Promise { + const paymentReceive = await this.paymentReceiveModel.query() + .withGraphFetched('entries') + .findById(paymentReceiveId); + + if (!paymentReceive) { + throw new ServiceError(ERRORS.PAYMENT_RECEIVE_NOT_EXISTS); + } + return paymentReceive; + } + + /** + * Validate the deposit account id existance. + * @param {number} depositAccountId - Deposit account id. + * @return {Promise} + */ + async getDepositAccountOrThrowError( + depositAccountId: number + ): Promise { + const depositAccount = await this.accountModel.query().findById(depositAccountId); + if (!depositAccount) { + throw new ServiceError(ERRORS.DEPOSIT_ACCOUNT_NOT_FOUND); + } + // Detarmines whether the account is cash, bank or other current asset. + if ( + !depositAccount.isAccountType([ + ACCOUNT_TYPE.CASH, + ACCOUNT_TYPE.BANK, + ACCOUNT_TYPE.OTHER_CURRENT_ASSET, + ]) + ) { + throw new ServiceError(ERRORS.DEPOSIT_ACCOUNT_INVALID_TYPE); + } + return depositAccount; + } + + /** + * Validate the given customer has no payments receives. + * @param {number} customerId - Customer id. + */ + public async validateCustomerHasNoPayments( + customerId: number + ) { + const paymentReceives = await this.paymentReceiveModel.query().where( + 'customer_id', + customerId + ); + if (paymentReceives.length > 0) { + throw new ServiceError(ERRORS.CUSTOMER_HAS_PAYMENT_RECEIVES); + } + } +} diff --git a/packages/server-nest/src/modules/PaymentReceived/constants.ts b/packages/server-nest/src/modules/PaymentReceived/constants.ts new file mode 100644 index 000000000..ffbce4a9f --- /dev/null +++ b/packages/server-nest/src/modules/PaymentReceived/constants.ts @@ -0,0 +1,97 @@ +export const DEFAULT_PAYMENT_MAIL_SUBJECT = + 'Payment Received for {Customer Name} from {Company Name}'; +export const DEFAULT_PAYMENT_MAIL_CONTENT = ` +

Dear {Customer Name}

+

Thank you for your payment. It was a pleasure doing business with you. We look forward to work together again!

+

+Payment Date : {Payment Date}
+Amount : {Payment Amount}
+

+ +

+Regards
+{Company Name} +

+`; + +export const ERRORS = { + PAYMENT_RECEIVE_NO_EXISTS: 'PAYMENT_RECEIVE_NO_EXISTS', + PAYMENT_RECEIVE_NOT_EXISTS: 'PAYMENT_RECEIVE_NOT_EXISTS', + DEPOSIT_ACCOUNT_NOT_FOUND: 'DEPOSIT_ACCOUNT_NOT_FOUND', + DEPOSIT_ACCOUNT_INVALID_TYPE: 'DEPOSIT_ACCOUNT_INVALID_TYPE', + INVALID_PAYMENT_AMOUNT: 'INVALID_PAYMENT_AMOUNT', + INVOICES_IDS_NOT_FOUND: 'INVOICES_IDS_NOT_FOUND', + ENTRIES_IDS_NOT_EXISTS: 'ENTRIES_IDS_NOT_EXISTS', + INVOICES_NOT_DELIVERED_YET: 'INVOICES_NOT_DELIVERED_YET', + PAYMENT_RECEIVE_NO_IS_REQUIRED: 'PAYMENT_RECEIVE_NO_IS_REQUIRED', + PAYMENT_RECEIVE_NO_REQUIRED: 'PAYMENT_RECEIVE_NO_REQUIRED', + PAYMENT_CUSTOMER_SHOULD_NOT_UPDATE: 'PAYMENT_CUSTOMER_SHOULD_NOT_UPDATE', + CUSTOMER_HAS_PAYMENT_RECEIVES: 'CUSTOMER_HAS_PAYMENT_RECEIVES', + PAYMENT_ACCOUNT_CURRENCY_INVALID: 'PAYMENT_ACCOUNT_CURRENCY_INVALID', + NO_INVOICE_CUSTOMER_EMAIL_ADDR: 'NO_INVOICE_CUSTOMER_EMAIL_ADDR', +}; + +export const DEFAULT_VIEWS = []; + +export const PaymentsReceiveSampleData = [ + { + Customer: 'Randall Kohler', + 'Payment Date': '2024-10-10', + 'Payment Receive No.': 'PAY-0001', + 'Reference No.': 'REF-0001', + 'Deposit Account': 'Petty Cash', + 'Exchange Rate': '', + Statement: 'Totam optio quisquam qui.', + Invoice: 'INV-00001', + 'Payment Amount': 850, + }, +]; + +export const defaultPaymentReceivedPdfTemplateAttributes = { + // # Colors + primaryColor: '#000', + secondaryColor: '#000', + + // # Company logo + showCompanyLogo: true, + companyLogoUri: '', + + // # Company name + companyName: 'Bigcapital Technology, Inc.', + + // # Customer address + showCustomerAddress: true, + customerAddress: '', + + // # Company address + showCompanyAddress: true, + companyAddress: '', + billedToLabel: 'Billed To', + + // Total + total: '$1000.00', + totalLabel: 'Total', + showTotal: true, + + // Subtotal + subtotal: '1000/00', + subtotalLabel: 'Subtotal', + showSubtotal: true, + + lines: [ + { + invoiceNumber: 'INV-00001', + invoiceAmount: '$1000.00', + paidAmount: '$1000.00', + }, + ], + // Payment received number + showPaymentReceivedNumber: true, + paymentReceivedNumberLabel: 'Payment Number', + paymentReceivedNumebr: '346D3D40-0001', + + // Payment date. + paymentReceivedDate: 'September 3, 2024', + showPaymentReceivedDate: true, + paymentReceivedDateLabel: 'Payment Date', +}; diff --git a/packages/server-nest/src/modules/PaymentReceived/models/PaymentReceived.ts b/packages/server-nest/src/modules/PaymentReceived/models/PaymentReceived.ts new file mode 100644 index 000000000..04873fd9c --- /dev/null +++ b/packages/server-nest/src/modules/PaymentReceived/models/PaymentReceived.ts @@ -0,0 +1,182 @@ +import { Model, mixin } from 'objection'; +// import TenantModel from 'models/TenantModel'; +// import ModelSetting from './ModelSetting'; +// import PaymentReceiveSettings from './PaymentReceive.Settings'; +// import CustomViewBaseModel from './CustomViewBaseModel'; +// import { DEFAULT_VIEWS } from '@/services/Sales/PaymentReceived/constants'; +// import ModelSearchable from './ModelSearchable'; +import { BaseModel } from '@/models/Model'; + +export class PaymentReceived extends BaseModel { + customerId: number; + paymentDate: string; + amount: number; + currencyCode: string; + referenceNo: string; + depositAccountId: number; + paymentReceiveNo: string; + exchangeRate: number; + statement: string; + + userId: number; + branchId: number; + pdfTemplateId: number; + + createdAt: string; + updatedAt: string; + + /** + * Table name. + */ + static get tableName() { + return 'payment_receives'; + } + + /** + * Timestamps columns. + */ + get timestamps() { + return ['created_at', 'updated_at']; + } + + /** + * Virtual attributes. + */ + static get virtualAttributes() { + return ['localAmount']; + } + + /** + * Payment receive amount in local currency. + * @returns {number} + */ + get localAmount() { + return this.amount * this.exchangeRate; + } + + /** + * Resourcable model. + */ + static get resourceable() { + return true; + } + + /* + * Relationship mapping. + */ + static get relationMappings() { + const PaymentReceiveEntry = require('models/PaymentReceiveEntry'); + const AccountTransaction = require('models/AccountTransaction'); + const Customer = require('models/Customer'); + const Account = require('models/Account'); + const Branch = require('models/Branch'); + const Document = require('models/Document'); + + return { + customer: { + relation: Model.BelongsToOneRelation, + modelClass: Customer.default, + join: { + from: 'payment_receives.customerId', + to: 'contacts.id', + }, + filter: (query) => { + query.where('contact_service', 'customer'); + }, + }, + depositAccount: { + relation: Model.BelongsToOneRelation, + modelClass: Account.default, + join: { + from: 'payment_receives.depositAccountId', + to: 'accounts.id', + }, + }, + entries: { + relation: Model.HasManyRelation, + modelClass: PaymentReceiveEntry.default, + join: { + from: 'payment_receives.id', + to: 'payment_receives_entries.paymentReceiveId', + }, + filter: (query) => { + query.orderBy('index', 'ASC'); + }, + }, + transactions: { + relation: Model.HasManyRelation, + modelClass: AccountTransaction.default, + join: { + from: 'payment_receives.id', + to: 'accounts_transactions.referenceId', + }, + filter: (builder) => { + builder.where('reference_type', 'PaymentReceive'); + }, + }, + + /** + * Payment receive may belongs to branch. + */ + branch: { + relation: Model.BelongsToOneRelation, + modelClass: Branch.default, + join: { + from: 'payment_receives.branchId', + to: 'branches.id', + }, + }, + + /** + * Payment transaction may has many attached attachments. + */ + attachments: { + relation: Model.ManyToManyRelation, + modelClass: Document.default, + join: { + from: 'payment_receives.id', + through: { + from: 'document_links.modelId', + to: 'document_links.documentId', + }, + to: 'documents.id', + }, + filter(query) { + query.where('model_ref', 'PaymentReceive'); + }, + }, + }; + } + + /** + * + */ + // static get meta() { + // return PaymentReceiveSettings; + // } + + // /** + // * Retrieve the default custom views, roles and columns. + // */ + // static get defaultViews() { + // return DEFAULT_VIEWS; + // } + + /** + * Model search attributes. + */ + static get searchRoles() { + return [ + { fieldKey: 'payment_receive_no', comparator: 'contains' }, + { condition: 'or', fieldKey: 'reference_no', comparator: 'contains' }, + { condition: 'or', fieldKey: 'amount', comparator: 'equals' }, + ]; + } + + /** + * Prevents mutate base currency since the model is not empty. + */ + static get preventMutateBaseCurrency() { + return true; + } +} diff --git a/packages/server-nest/src/modules/PaymentReceived/models/PaymentReceivedEntry.ts b/packages/server-nest/src/modules/PaymentReceived/models/PaymentReceivedEntry.ts new file mode 100644 index 000000000..920419cf3 --- /dev/null +++ b/packages/server-nest/src/modules/PaymentReceived/models/PaymentReceivedEntry.ts @@ -0,0 +1,55 @@ +import { BaseModel } from '@/models/Model'; +import { Model, mixin } from 'objection'; + +export class PaymentReceivedEntry extends BaseModel { + paymentReceiveId: number; + invoiceId: number; + paymentAmount: number; + + /** + * Table name + */ + static get tableName() { + return 'payment_receives_entries'; + } + + /** + * Timestamps columns. + */ + get timestamps() { + return []; + } + + /** + * Relationship mapping. + */ + static get relationMappings() { + const PaymentReceive = require('models/PaymentReceive'); + const SaleInvoice = require('models/SaleInvoice'); + + return { + /** + */ + payment: { + relation: Model.BelongsToOneRelation, + modelClass: PaymentReceive.default, + join: { + from: 'payment_receives_entries.paymentReceiveId', + to: 'payment_receives.id', + }, + }, + + /** + * The payment receive entry have have sale invoice. + */ + invoice: { + relation: Model.BelongsToOneRelation, + modelClass: SaleInvoice.default, + join: { + from: 'payment_receives_entries.invoiceId', + to: 'sales_invoices.id', + }, + }, + }; + } +} diff --git a/packages/server-nest/src/modules/PaymentReceived/queries/GetPaymentReceived.service.ts b/packages/server-nest/src/modules/PaymentReceived/queries/GetPaymentReceived.service.ts new file mode 100644 index 000000000..e76d568f6 --- /dev/null +++ b/packages/server-nest/src/modules/PaymentReceived/queries/GetPaymentReceived.service.ts @@ -0,0 +1,39 @@ +import { Injectable } from '@nestjs/common'; +import { ERRORS } from '../constants'; +import { PaymentReceiveTransfromer } from './PaymentReceivedTransformer'; +import { PaymentReceived } from '../models/PaymentReceived'; +import { TransformerInjectable } from '../../Transformer/TransformerInjectable.service'; +import { ServiceError } from '../../Items/ServiceError'; + +@Injectable() +export class GetPaymentReceived { + constructor( + private readonly paymentReceiveModel: typeof PaymentReceived, + private readonly transformer: TransformerInjectable, + ) {} + + /** + * Retrieve payment receive details. + * @param {number} paymentReceiveId - Payment receive id. + * @return {Promise} + */ + public async getPaymentReceive( + paymentReceiveId: number + ): Promise { + const paymentReceive = await this.paymentReceiveModel.query() + .withGraphFetched('customer') + .withGraphFetched('depositAccount') + .withGraphFetched('entries.invoice') + .withGraphFetched('transactions') + .withGraphFetched('branch') + .findById(paymentReceiveId); + + if (!paymentReceive) { + throw new ServiceError(ERRORS.PAYMENT_RECEIVE_NOT_EXISTS); + } + return this.transformer.transform( + paymentReceive, + new PaymentReceiveTransfromer() + ); + } +} diff --git a/packages/server-nest/src/modules/PaymentReceived/queries/GetPaymentReceivedInvoices.service.ts b/packages/server-nest/src/modules/PaymentReceived/queries/GetPaymentReceivedInvoices.service.ts new file mode 100644 index 000000000..5d4ed9822 --- /dev/null +++ b/packages/server-nest/src/modules/PaymentReceived/queries/GetPaymentReceivedInvoices.service.ts @@ -0,0 +1,37 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { PaymentReceivedValidators } from '../commands/PaymentReceivedValidators.service'; +import { SaleInvoice } from '../../SaleInvoices/models/SaleInvoice'; +import { PaymentReceived } from '../models/PaymentReceived'; + +@Injectable() +export class GetPaymentReceivedInvoices { + constructor( + @Inject(PaymentReceived.name) + private paymentReceiveModel: typeof PaymentReceived, + private validators: PaymentReceivedValidators, + ) {} + + /** + * Retrieve sale invoices that associated to the given payment receive. + * @param {number} paymentReceiveId - Payment receive id. + * @return {Promise} + */ + public async getPaymentReceiveInvoices(paymentReceiveId: number) { + const paymentReceive = await this.paymentReceiveModel + .query() + .findById(paymentReceiveId) + .withGraphFetched('entries'); + + // Validates the payment receive existence. + this.validators.validatePaymentExistance(paymentReceive); + + const paymentReceiveInvoicesIds = paymentReceive.entries.map( + (entry) => entry.invoiceId, + ); + const saleInvoices = await SaleInvoice.query().whereIn( + 'id', + paymentReceiveInvoicesIds, + ); + return saleInvoices; + } +} diff --git a/packages/server-nest/src/modules/PaymentReceived/queries/GetPaymentReceivedPdf.service.ts b/packages/server-nest/src/modules/PaymentReceived/queries/GetPaymentReceivedPdf.service.ts new file mode 100644 index 000000000..c6b590ecb --- /dev/null +++ b/packages/server-nest/src/modules/PaymentReceived/queries/GetPaymentReceivedPdf.service.ts @@ -0,0 +1,106 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { GetPaymentReceived } from './GetPaymentReceived.service'; +import { PaymentReceivedBrandingTemplate } from './PaymentReceivedBrandingTemplate.service'; +import { transformPaymentReceivedToPdfTemplate } from '../utils'; + +import { PaymentReceived } from '../models/PaymentReceived'; +import { PdfTemplateModel } from '@/modules/PdfTemplate/models/PdfTemplate'; +import { ChromiumlyTenancy } from '@/modules/ChromiumlyTenancy/ChromiumlyTenancy.service'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { TemplateInjectable } from '@/modules/TemplateInjectable/TemplateInjectable.service'; +import { PaymentReceivedPdfTemplateAttributes } from '../types/PaymentReceived.types'; +import { events } from '@/common/events/events'; + +@Injectable() +export default class GetPaymentReceivedPdf { + constructor( + private chromiumlyTenancy: ChromiumlyTenancy, + private templateInjectable: TemplateInjectable, + private getPaymentService: GetPaymentReceived, + private paymentBrandingTemplateService: PaymentReceivedBrandingTemplate, + private eventPublisher: EventEmitter2, + + @Inject(PaymentReceived.name) + private paymentReceiveModel: typeof PaymentReceived, + + @Inject(PdfTemplateModel.name) + private pdfTemplateModel: typeof PdfTemplateModel, + ) {} + + /** + * Retrieve sale invoice pdf content. + * @param {number} tenantId - + * @param {IPaymentReceived} paymentReceive - + * @returns {Promise} + */ + async getPaymentReceivePdf( + paymentReceivedId: number, + ): Promise<[Buffer, string]> { + const brandingAttributes = + await this.getPaymentBrandingAttributes(paymentReceivedId); + + const htmlContent = await this.templateInjectable.render( + 'modules/payment-receive-standard', + brandingAttributes, + ); + const filename = await this.getPaymentReceivedFilename(paymentReceivedId); + // Converts the given html content to pdf document. + const content = + await this.chromiumlyTenancy.convertHtmlContent(htmlContent); + const eventPayload = { paymentReceivedId }; + + // Triggers the `onCreditNotePdfViewed` event. + await this.eventPublisher.emitAsync( + events.paymentReceive.onPdfViewed, + eventPayload, + ); + return [content, filename]; + } + + /** + * Retrieves the filename of the given payment. + * @param {number} tenantId + * @param {number} paymentReceivedId + * @returns {Promise} + */ + private async getPaymentReceivedFilename( + paymentReceivedId: number, + ): Promise { + const payment = await this.paymentReceiveModel + .query() + .findById(paymentReceivedId); + + return `Payment-${payment.paymentReceiveNo}`; + } + + /** + * Retrieves the given payment received branding attributes. + * @param {number} paymentReceivedId - Payment received identifier. + * @returns {Promise} + */ + async getPaymentBrandingAttributes( + paymentReceivedId: number, + ): Promise { + const paymentReceived = + await this.getPaymentService.getPaymentReceive(paymentReceivedId); + + const templateId = + paymentReceived?.pdfTemplateId ?? + ( + await this.pdfTemplateModel.query().findOne({ + resource: 'PaymentReceive', + default: true, + }) + )?.id; + + const brandingTemplate = + await this.paymentBrandingTemplateService.getPaymentReceivedPdfTemplate( + templateId, + ); + + return { + ...brandingTemplate.attributes, + ...transformPaymentReceivedToPdfTemplate(paymentReceived), + }; + } +} diff --git a/packages/server-nest/src/modules/PaymentReceived/queries/GetPaymentReceivedState.service.ts b/packages/server-nest/src/modules/PaymentReceived/queries/GetPaymentReceivedState.service.ts new file mode 100644 index 000000000..02adfe8c5 --- /dev/null +++ b/packages/server-nest/src/modules/PaymentReceived/queries/GetPaymentReceivedState.service.ts @@ -0,0 +1,23 @@ +import { PdfTemplateModel } from '@/modules/PdfTemplate/models/PdfTemplate'; +import { Injectable } from '@nestjs/common'; +import { IPaymentReceivedState } from '../types/PaymentReceived.types'; + +@Injectable() +export class GetPaymentReceivedState { + constructor(private pdfTemplateModel: typeof PdfTemplateModel) {} + + /** + * Retrieves the create/edit initial state of the payment received. + * @returns {Promise} - A promise resolving to the payment received state. + */ + public async getPaymentReceivedState(): Promise { + const defaultPdfTemplate = await this.pdfTemplateModel + .query() + .findOne({ resource: 'PaymentReceive' }) + .modify('default'); + + return { + defaultTemplateId: defaultPdfTemplate?.id, + }; + } +} diff --git a/packages/server-nest/src/modules/PaymentReceived/queries/GetPaymentsReceived.ts b/packages/server-nest/src/modules/PaymentReceived/queries/GetPaymentsReceived.ts new file mode 100644 index 000000000..7c14f4ff1 --- /dev/null +++ b/packages/server-nest/src/modules/PaymentReceived/queries/GetPaymentsReceived.ts @@ -0,0 +1,79 @@ +// import { Inject, Service } from 'typedi'; +// import * as R from 'ramda'; +// import { +// IFilterMeta, +// IPaginationMeta, +// IPaymentReceived, +// IPaymentsReceivedFilter, +// } from '@/interfaces'; +// import { PaymentReceiveTransfromer } from './PaymentReceivedTransformer'; +// import HasTenancyService from '@/services/Tenancy/TenancyService'; +// import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; +// import DynamicListingService from '@/services/DynamicListing/DynamicListService'; + +// @Service() +// export class GetPaymentReceives { +// @Inject() +// private tenancy: HasTenancyService; + +// @Inject() +// private dynamicListService: DynamicListingService; + +// @Inject() +// private transformer: TransformerInjectable; + +// /** +// * Retrieve payment receives paginated and filterable list. +// * @param {number} tenantId +// * @param {IPaymentsReceivedFilter} paymentReceivesFilter +// */ +// public async getPaymentReceives( +// tenantId: number, +// filterDTO: IPaymentsReceivedFilter +// ): Promise<{ +// paymentReceives: IPaymentReceived[]; +// pagination: IPaginationMeta; +// filterMeta: IFilterMeta; +// }> { +// const { PaymentReceive } = this.tenancy.models(tenantId); + +// // Parses filter DTO. +// const filter = this.parseListFilterDTO(filterDTO); + +// // Dynamic list service. +// const dynamicList = await this.dynamicListService.dynamicList( +// tenantId, +// PaymentReceive, +// filter +// ); +// const { results, pagination } = await PaymentReceive.query() +// .onBuild((builder) => { +// builder.withGraphFetched('customer'); +// builder.withGraphFetched('depositAccount'); + +// dynamicList.buildQuery()(builder); +// filterDTO?.filterQuery && filterDTO.filterQuery(builder); +// }) +// .pagination(filter.page - 1, filter.pageSize); + +// // Transformer the payment receives models to POJO. +// const transformedPayments = await this.transformer.transform( +// tenantId, +// results, +// new PaymentReceiveTransfromer() +// ); +// return { +// paymentReceives: transformedPayments, +// pagination, +// filterMeta: dynamicList.getResponseMeta(), +// }; +// } + +// /** +// * Parses payments receive list filter DTO. +// * @param filterDTO +// */ +// private parseListFilterDTO(filterDTO) { +// return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO); +// } +// } diff --git a/packages/server-nest/src/modules/PaymentReceived/queries/PaymentReceivedBrandingTemplate.service.ts b/packages/server-nest/src/modules/PaymentReceived/queries/PaymentReceivedBrandingTemplate.service.ts new file mode 100644 index 000000000..c8fa78bfc --- /dev/null +++ b/packages/server-nest/src/modules/PaymentReceived/queries/PaymentReceivedBrandingTemplate.service.ts @@ -0,0 +1,49 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { defaultPaymentReceivedPdfTemplateAttributes } from '../constants'; +import { GetPdfTemplateService } from '../../PdfTemplate/queries/GetPdfTemplate.service'; +import { GetOrganizationBrandingAttributesService } from '../../PdfTemplate/queries/GetOrganizationBrandingAttributes.service'; +import { PdfTemplateModel } from '../../PdfTemplate/models/PdfTemplate'; +import { mergePdfTemplateWithDefaultAttributes } from '../../SaleInvoices/utils'; + +@Injectable() +export class PaymentReceivedBrandingTemplate { + constructor( + private readonly getPdfTemplateService: GetPdfTemplateService, + private readonly getOrgBrandingAttributes: GetOrganizationBrandingAttributesService, + + @Inject(PdfTemplateModel.name) + private readonly pdfTemplateModel: typeof PdfTemplateModel, + ) {} + + /** + * Retrieves the payment received pdf template. + * @param {number} paymentTemplateId + * @returns + */ + public async getPaymentReceivedPdfTemplate(paymentTemplateId: number) { + const template = await this.getPdfTemplateService.getPdfTemplate( + paymentTemplateId + ); + // Retrieves the organization branding attributes. + const commonOrgBrandingAttrs = + await this.getOrgBrandingAttributes.getOrganizationBrandingAttributes(); + + // Merges the default branding attributes with common organization branding attrs. + const organizationBrandingAttrs = { + ...defaultPaymentReceivedPdfTemplateAttributes, + ...commonOrgBrandingAttrs, + }; + const brandingTemplateAttrs = { + ...template.attributes, + companyLogoUri: template.companyLogoUri, + }; + const attributes = mergePdfTemplateWithDefaultAttributes( + brandingTemplateAttrs, + organizationBrandingAttrs + ); + return { + ...template, + attributes, + }; + } +} diff --git a/packages/server-nest/src/modules/PaymentReceived/queries/PaymentReceivedEntryTransformer.ts b/packages/server-nest/src/modules/PaymentReceived/queries/PaymentReceivedEntryTransformer.ts new file mode 100644 index 000000000..e5fc694d2 --- /dev/null +++ b/packages/server-nest/src/modules/PaymentReceived/queries/PaymentReceivedEntryTransformer.ts @@ -0,0 +1,29 @@ +import { SaleInvoiceTransformer } from "@/modules/SaleInvoices/queries/SaleInvoice.transformer"; +import { Transformer } from "@/modules/Transformer/Transformer"; + + +export class PaymentReceivedEntryTransfromer extends Transformer { + /** + * Include these attributes to payment receive entry object. + * @returns {Array} + */ + public includeAttributes = (): string[] => { + return ['paymentAmountFormatted', 'invoice']; + }; + + /** + * Retreives the payment amount formatted. + * @param entry + * @returns {string} + */ + protected paymentAmountFormatted(entry) { + return this.formatNumber(entry.paymentAmount, { money: false }); + } + + /** + * Retreives the transformed invoice. + */ + protected invoice(entry) { + return this.item(entry.invoice, new SaleInvoiceTransformer()); + } +} diff --git a/packages/server-nest/src/modules/PaymentReceived/queries/PaymentReceivedTransformer.ts b/packages/server-nest/src/modules/PaymentReceived/queries/PaymentReceivedTransformer.ts new file mode 100644 index 000000000..16fd3cbb5 --- /dev/null +++ b/packages/server-nest/src/modules/PaymentReceived/queries/PaymentReceivedTransformer.ts @@ -0,0 +1,80 @@ +import { Transformer } from '../../Transformer/Transformer'; +import { PaymentReceived } from '../models/PaymentReceived'; +import { PaymentReceivedEntry } from '../models/PaymentReceivedEntry'; +import { PaymentReceivedEntryTransfromer } from './PaymentReceivedEntryTransformer'; + +export class PaymentReceiveTransfromer extends Transformer { + /** + * Include these attributes to payment receive object. + * @returns {Array} + */ + public includeAttributes = (): string[] => { + return [ + 'subtotalFormatted', + 'formattedPaymentDate', + 'formattedCreatedAt', + 'formattedAmount', + 'formattedExchangeRate', + 'entries', + ]; + }; + + /** + * Retrieve formatted payment receive date. + * @param {PaymentReceived} invoice + * @returns {String} + */ + protected formattedPaymentDate = (payment: PaymentReceived): string => { + return this.formatDate(payment.paymentDate); + }; + + /** + * Retrieves the formatted created at date. + * @param {PaymentReceived} payment + * @returns {string} + */ + protected formattedCreatedAt = (payment: PaymentReceived): string => { + return this.formatDate(payment.createdAt); + }; + + /** + * Retrieve the formatted payment subtotal. + * @param {PaymentReceived} payment + * @returns {string} + */ + protected subtotalFormatted = (payment: PaymentReceived): string => { + return this.formatNumber(payment.amount, { + currencyCode: payment.currencyCode, + money: false, + }); + }; + + /** + * Retrieve formatted payment amount. + * @param {PaymentReceived} invoice + * @returns {string} + */ + protected formattedAmount = (payment: PaymentReceived): string => { + return this.formatNumber(payment.amount, { + currencyCode: payment.currencyCode, + }); + }; + + /** + * Retrieve the formatted exchange rate. + * @param {PaymentReceived} payment + * @returns {string} + */ + protected formattedExchangeRate = (payment: PaymentReceived): string => { + return this.formatNumber(payment.exchangeRate, { money: false }); + }; + + /** + * Retrieves the payment entries. + * @param {PaymentReceived} payment + * @returns {IPaymentReceivedEntry[]} + */ + protected entries = (payment: PaymentReceived): PaymentReceivedEntry[] => { + return this.item(payment.entries, new PaymentReceivedEntryTransfromer()); + }; +} diff --git a/packages/server-nest/src/modules/PaymentReceived/queries/PaymentsReceivedPages.service.ts b/packages/server-nest/src/modules/PaymentReceived/queries/PaymentsReceivedPages.service.ts new file mode 100644 index 000000000..9603f5f90 --- /dev/null +++ b/packages/server-nest/src/modules/PaymentReceived/queries/PaymentsReceivedPages.service.ts @@ -0,0 +1,111 @@ +import { Inject, Service } from 'typedi'; +import { omit } from 'lodash'; +import { IPaymentReceivePageEntry } from '../types/PaymentReceived.types'; +import { ERRORS } from '../constants'; +import { Injectable } from '@nestjs/common'; +import { SaleInvoice } from '@/modules/SaleInvoices/models/SaleInvoice'; +import { PaymentReceived } from '../models/PaymentReceived'; +import { ServiceError } from '@/modules/Items/ServiceError'; + +/** + * Payment receives edit/new pages service. + */ +@Injectable() +export class PaymentsReceivedPagesService { + constructor( + @Inject(SaleInvoice.name) + private readonly saleInvoice: typeof SaleInvoice, + + @Inject(PaymentReceived.name) + private readonly paymentReceived: typeof PaymentReceived, + ) {} + + /** + * Retrive page invoices entries from the given sale invoices models. + * @param {ISaleInvoice[]} invoices - Invoices. + * @return {IPaymentReceivePageEntry} + */ + private invoiceToPageEntry(invoice: SaleInvoice): IPaymentReceivePageEntry { + return { + entryType: 'invoice', + invoiceId: invoice.id, + invoiceNo: invoice.invoiceNo, + amount: invoice.balance, + dueAmount: invoice.dueAmount, + paymentAmount: invoice.paymentAmount, + totalPaymentAmount: invoice.paymentAmount, + currencyCode: invoice.currencyCode, + date: invoice.invoiceDate, + }; + } + + /** + * Retrieve payment receive new page receivable entries. + * @param {number} tenantId - Tenant id. + * @param {number} vendorId - Vendor id. + * @return {IPaymentReceivePageEntry[]} + */ + public async getNewPageEntries(tenantId: number, customerId: number) { + // Retrieve due invoices. + const entries = await this.saleInvoice + .query() + .modify('delivered') + .modify('dueInvoices') + .where('customer_id', customerId) + .orderBy('invoice_date', 'ASC'); + + return entries.map(this.invoiceToPageEntry); + } + + /** + * Retrieve the payment receive details of the given id. + * @param {number} tenantId - Tenant id. + * @param {Integer} paymentReceiveId - Payment receive id. + */ + public async getPaymentReceiveEditPage( + tenantId: number, + paymentReceiveId: number, + ): Promise<{ + paymentReceive: Omit; + entries: IPaymentReceivePageEntry[]; + }> { + // Retrieve payment receive. + const paymentReceive = await this.paymentReceived + .query() + .findById(paymentReceiveId) + .withGraphFetched('entries.invoice') + .withGraphFetched('attachments'); + + // Throw not found the payment receive. + if (!paymentReceive) { + throw new ServiceError(ERRORS.PAYMENT_RECEIVE_NOT_EXISTS); + } + const paymentEntries = paymentReceive.entries.map((entry) => ({ + ...this.invoiceToPageEntry(entry.invoice), + dueAmount: entry.invoice.dueAmount + entry.paymentAmount, + paymentAmount: entry.paymentAmount, + index: entry.index, + })); + // Retrieves all receivable bills that associated to the payment receive transaction. + const restReceivableInvoices = await this.saleInvoice + .query() + .modify('delivered') + .modify('dueInvoices') + .where('customer_id', paymentReceive.customerId) + .whereNotIn( + 'id', + paymentReceive.entries.map((entry) => entry.invoiceId), + ) + .orderBy('invoice_date', 'ASC'); + + const restReceivableEntries = restReceivableInvoices.map( + this.invoiceToPageEntry, + ); + const entries = [...paymentEntries, ...restReceivableEntries]; + + return { + paymentReceive: omit(paymentReceive, ['entries']), + entries, + }; + } +} diff --git a/packages/server-nest/src/modules/PaymentReceived/types/PaymentReceived.types.ts b/packages/server-nest/src/modules/PaymentReceived/types/PaymentReceived.types.ts new file mode 100644 index 000000000..2bdc7bf92 --- /dev/null +++ b/packages/server-nest/src/modules/PaymentReceived/types/PaymentReceived.types.ts @@ -0,0 +1,210 @@ +import { AttachmentLinkDTO } from '@/modules/Attachments/Attachments.types'; +import { Knex } from 'knex'; +import { PaymentReceived } from '../models/PaymentReceived'; + +export interface IPaymentReceivedCreateDTO { + customerId: number; + paymentDate: Date; + amount: number; + exchangeRate: number; + referenceNo: string; + depositAccountId: number; + paymentReceiveNo?: string; + statement: string; + entries: IPaymentReceivedEntryDTO[]; + + branchId?: number; + attachments?: AttachmentLinkDTO[]; +} + +export interface IPaymentReceivedEditDTO { + customerId: number; + paymentDate: Date; + amount: number; + exchangeRate: number; + referenceNo: string; + depositAccountId: number; + paymentReceiveNo?: string; + statement: string; + entries: IPaymentReceivedEntryDTO[]; + branchId?: number; + attachments?: AttachmentLinkDTO[]; +} + +export interface IPaymentReceivedEntryDTO { + id?: number; + index?: number; + paymentReceiveId?: number; + invoiceId: number; + paymentAmount: number; +} + +// export interface IPaymentsReceivedFilter extends IDynamicListFilterDTO { +// stringifiedFilterRoles?: string; +// filterQuery?: (trx: Knex.Transaction) => void; +// } + +export interface IPaymentReceivePageEntry { + invoiceId: number; + entryType: string; + invoiceNo: string; + dueAmount: number; + amount: number; + totalPaymentAmount: number; + paymentAmount: number; + currencyCode: string; + date: Date | string; +} + +export interface IPaymentReceivedEditPage { + paymentReceive: PaymentReceived; + entries: IPaymentReceivePageEntry[]; +} + +export interface IPaymentsReceivedService { + validateCustomerHasNoPayments( + tenantId: number, + customerId: number, + ): Promise; +} + +export interface IPaymentReceivedSmsDetails { + customerName: string; + customerPhoneNumber: string; + smsMessage: string; +} + +export interface IPaymentReceivedCreatingPayload { + tenantId: number; + paymentReceiveDTO: IPaymentReceivedCreateDTO; + trx: Knex.Transaction; +} + +export interface IPaymentReceivedCreatedPayload { + // tenantId: number; + paymentReceive: PaymentReceived; + paymentReceiveId: number; + // authorizedUser: ISystemUser; + paymentReceiveDTO: IPaymentReceivedCreateDTO; + trx: Knex.Transaction; +} + +export interface IPaymentReceivedEditedPayload { + // tenantId: number; + paymentReceiveId: number; + paymentReceive: PaymentReceived; + oldPaymentReceive: PaymentReceived; + paymentReceiveDTO: IPaymentReceivedEditDTO; + // authorizedUser: ISystemUser; + trx: Knex.Transaction; +} + +export interface IPaymentReceivedEditingPayload { + // tenantId: number; + oldPaymentReceive: PaymentReceived; + paymentReceiveDTO: IPaymentReceivedEditDTO; + trx: Knex.Transaction; +} + +export interface IPaymentReceivedDeletingPayload { + // tenantId: number; + oldPaymentReceive: PaymentReceived; + trx: Knex.Transaction; +} +export interface IPaymentReceivedDeletedPayload { + // tenantId: number; + paymentReceiveId: number; + oldPaymentReceive: PaymentReceived; + // authorizedUser: ISystemUser; + trx: Knex.Transaction; +} + +export enum PaymentReceiveAction { + Create = 'Create', + Edit = 'Edit', + Delete = 'Delete', + View = 'View', + NotifyBySms = 'NotifyBySms', +} + +// export type IPaymentReceiveGLCommonEntry = Pick< +// ILedgerEntry, +// | 'debit' +// | 'credit' +// | 'currencyCode' +// | 'exchangeRate' +// | 'transactionId' +// | 'transactionType' +// | 'transactionNumber' +// | 'referenceNumber' +// | 'date' +// | 'userId' +// | 'createdAt' +// | 'branchId' +// >; + +// export interface PaymentReceiveMailOpts extends CommonMailOptions {} + +// export interface PaymentReceiveMailOptsDTO extends CommonMailOptionsDTO {} + +// export interface PaymentReceiveMailPresendEvent { +// tenantId: number; +// paymentReceiveId: number; +// messageOptions: PaymentReceiveMailOptsDTO; +// } + +export interface PaymentReceivedPdfLineItem { + item: string; + description: string; + rate: string; + quantity: string; + total: string; +} + +export interface PaymentReceivedPdfTax { + label: string; + amount: string; +} + +export interface PaymentReceivedPdfTemplateAttributes { + primaryColor: string; + secondaryColor: string; + showCompanyLogo: boolean; + companyLogo: string; + companyName: string; + + // Customer Address + showCustomerAddress: boolean; + customerAddress: string; + + // Company address + showCompanyAddress: boolean; + companyAddress: string; + billedToLabel: string; + + total: string; + totalLabel: string; + showTotal: boolean; + + subtotal: string; + subtotalLabel: string; + showSubtotal: boolean; + + lines: Array<{ + invoiceNumber: string; + invoiceAmount: string; + paidAmount: string; + }>; + + showPaymentReceivedNumber: boolean; + paymentReceivedNumberLabel: string; + paymentReceivedNumebr: string; + + paymentReceivedDate: string; + showPaymentReceivedDate: boolean; + paymentReceivedDateLabel: string; +} + +export interface IPaymentReceivedState { + defaultTemplateId: number; +} diff --git a/packages/server-nest/src/modules/PaymentReceived/utils.ts b/packages/server-nest/src/modules/PaymentReceived/utils.ts new file mode 100644 index 000000000..dfa03f409 --- /dev/null +++ b/packages/server-nest/src/modules/PaymentReceived/utils.ts @@ -0,0 +1,32 @@ +import { PaymentReceived } from './models/PaymentReceived'; +import { + PaymentReceivedPdfTemplateAttributes, +} from './types/PaymentReceived.types'; +import { contactAddressTextFormat } from '@/utils/address-text-format'; + +export const transformPaymentReceivedToPdfTemplate = ( + payment: PaymentReceived +): Partial => { + return { + total: payment.formattedAmount, + subtotal: payment.subtotalFormatted, + paymentReceivedNumebr: payment.paymentReceiveNo, + paymentReceivedDate: payment.formattedPaymentDate, + customerName: payment.customer.displayName, + lines: payment.entries.map((entry) => ({ + invoiceNumber: entry.invoice.invoiceNo, + invoiceAmount: entry.invoice.totalFormatted, + paidAmount: entry.paymentAmountFormatted, + })), + customerAddress: contactAddressTextFormat(payment.customer), + }; +}; + +export const transformPaymentReceivedToMailDataArgs = (payment: any) => { + return { + 'Customer Name': payment.customer.displayName, + 'Payment Number': payment.paymentReceiveNo, + 'Payment Date': payment.formattedPaymentDate, + 'Payment Amount': payment.formattedAmount, + }; +}; diff --git a/packages/server-nest/src/modules/SaleInvoices/InvoiceGLEntries.ts b/packages/server-nest/src/modules/SaleInvoices/InvoiceGLEntries.ts new file mode 100644 index 000000000..0896639f5 --- /dev/null +++ b/packages/server-nest/src/modules/SaleInvoices/InvoiceGLEntries.ts @@ -0,0 +1,276 @@ +// import * as R from 'ramda'; +// import { Knex } from 'knex'; +// import { +// ISaleInvoice, +// IItemEntry, +// ILedgerEntry, +// AccountNormal, +// ILedger, +// } from '@/interfaces'; +// import { Service, Inject } from 'typedi'; +// import Ledger from '@/services/Accounting/Ledger'; +// import LedgerStorageService from '@/services/Accounting/LedgerStorageService'; +// import HasTenancyService from '@/services/Tenancy/TenancyService'; +// import ItemsEntriesService from '@/services/Items/ItemsEntriesService'; + +// @Service() +// export class SaleInvoiceGLEntries { +// @Inject() +// private tenancy: HasTenancyService; + +// @Inject() +// private ledegrRepository: LedgerStorageService; + +// @Inject() +// private itemsEntriesService: ItemsEntriesService; + +// /** +// * Writes a sale invoice GL entries. +// * @param {number} tenantId - Tenant id. +// * @param {number} saleInvoiceId - Sale invoice id. +// * @param {Knex.Transaction} trx +// */ +// public writeInvoiceGLEntries = async ( +// tenantId: number, +// saleInvoiceId: number, +// trx?: Knex.Transaction +// ) => { +// const { SaleInvoice } = this.tenancy.models(tenantId); +// const { accountRepository } = this.tenancy.repositories(tenantId); + +// const saleInvoice = await SaleInvoice.query(trx) +// .findById(saleInvoiceId) +// .withGraphFetched('entries.item'); + +// // Find or create the A/R account. +// const ARAccount = await accountRepository.findOrCreateAccountReceivable( +// saleInvoice.currencyCode, {}, trx +// ); +// // Find or create tax payable account. +// const taxPayableAccount = await accountRepository.findOrCreateTaxPayable( +// {}, +// trx +// ); +// // Retrieves the ledger of the invoice. +// const ledger = this.getInvoiceGLedger( +// saleInvoice, +// ARAccount.id, +// taxPayableAccount.id +// ); +// // Commits the ledger entries to the storage as UOW. +// await this.ledegrRepository.commit(tenantId, ledger, trx); +// }; + +// /** +// * Rewrites the given invoice GL entries. +// * @param {number} tenantId +// * @param {number} saleInvoiceId +// * @param {Knex.Transaction} trx +// */ +// public rewritesInvoiceGLEntries = async ( +// tenantId: number, +// saleInvoiceId: number, +// trx?: Knex.Transaction +// ) => { +// // Reverts the invoice GL entries. +// await this.revertInvoiceGLEntries(tenantId, saleInvoiceId, trx); + +// // Writes the invoice GL entries. +// await this.writeInvoiceGLEntries(tenantId, saleInvoiceId, trx); +// }; + +// /** +// * Reverts the given invoice GL entries. +// * @param {number} tenantId +// * @param {number} saleInvoiceId +// * @param {Knex.Transaction} trx +// */ +// public revertInvoiceGLEntries = async ( +// tenantId: number, +// saleInvoiceId: number, +// trx?: Knex.Transaction +// ) => { +// await this.ledegrRepository.deleteByReference( +// tenantId, +// saleInvoiceId, +// 'SaleInvoice', +// trx +// ); +// }; + +// /** +// * Retrieves the given invoice ledger. +// * @param {ISaleInvoice} saleInvoice +// * @param {number} ARAccountId +// * @returns {ILedger} +// */ +// public getInvoiceGLedger = ( +// saleInvoice: ISaleInvoice, +// ARAccountId: number, +// taxPayableAccountId: number +// ): ILedger => { +// const entries = this.getInvoiceGLEntries( +// saleInvoice, +// ARAccountId, +// taxPayableAccountId +// ); +// return new Ledger(entries); +// }; + +// /** +// * Retrieves the invoice GL common entry. +// * @param {ISaleInvoice} saleInvoice +// * @returns {Partial} +// */ +// private getInvoiceGLCommonEntry = ( +// saleInvoice: ISaleInvoice +// ): Partial => ({ +// credit: 0, +// debit: 0, +// currencyCode: saleInvoice.currencyCode, +// exchangeRate: saleInvoice.exchangeRate, + +// transactionType: 'SaleInvoice', +// transactionId: saleInvoice.id, + +// date: saleInvoice.invoiceDate, +// userId: saleInvoice.userId, + +// transactionNumber: saleInvoice.invoiceNo, +// referenceNumber: saleInvoice.referenceNo, + +// createdAt: saleInvoice.createdAt, +// indexGroup: 10, + +// branchId: saleInvoice.branchId, +// }); + +// /** +// * Retrieve receivable entry of the given invoice. +// * @param {ISaleInvoice} saleInvoice +// * @param {number} ARAccountId +// * @returns {ILedgerEntry} +// */ +// private getInvoiceReceivableEntry = ( +// saleInvoice: ISaleInvoice, +// ARAccountId: number +// ): ILedgerEntry => { +// const commonEntry = this.getInvoiceGLCommonEntry(saleInvoice); + +// return { +// ...commonEntry, +// debit: saleInvoice.totalLocal, +// accountId: ARAccountId, +// contactId: saleInvoice.customerId, +// accountNormal: AccountNormal.DEBIT, +// index: 1, +// } as ILedgerEntry; +// }; + +// /** +// * Retrieve item income entry of the given invoice. +// * @param {ISaleInvoice} saleInvoice - +// * @param {IItemEntry} entry - +// * @param {number} index - +// * @returns {ILedgerEntry} +// */ +// private getInvoiceItemEntry = R.curry( +// ( +// saleInvoice: ISaleInvoice, +// entry: IItemEntry, +// index: number +// ): ILedgerEntry => { +// const commonEntry = this.getInvoiceGLCommonEntry(saleInvoice); +// const localAmount = entry.amountExludingTax * saleInvoice.exchangeRate; + +// return { +// ...commonEntry, +// credit: localAmount, +// accountId: entry.sellAccountId, +// note: entry.description, +// index: index + 2, +// itemId: entry.itemId, +// itemQuantity: entry.quantity, +// accountNormal: AccountNormal.CREDIT, +// projectId: entry.projectId || saleInvoice.projectId, +// taxRateId: entry.taxRateId, +// taxRate: entry.taxRate, +// }; +// } +// ); + +// /** +// * Retreives the GL entry of tax payable. +// * @param {ISaleInvoice} saleInvoice - +// * @param {number} taxPayableAccountId - +// * @returns {ILedgerEntry} +// */ +// private getInvoiceTaxEntry = R.curry( +// ( +// saleInvoice: ISaleInvoice, +// taxPayableAccountId: number, +// entry: IItemEntry, +// index: number +// ): ILedgerEntry => { +// const commonEntry = this.getInvoiceGLCommonEntry(saleInvoice); + +// return { +// ...commonEntry, +// credit: entry.taxAmount, +// accountId: taxPayableAccountId, +// index: index + 1, +// indexGroup: 30, +// accountNormal: AccountNormal.CREDIT, +// taxRateId: entry.taxRateId, +// taxRate: entry.taxRate, +// }; +// } +// ); + +// /** +// * Retrieves the invoice tax GL entries. +// * @param {ISaleInvoice} saleInvoice +// * @param {number} taxPayableAccountId +// * @returns {ILedgerEntry[]} +// */ +// private getInvoiceTaxEntries = ( +// saleInvoice: ISaleInvoice, +// taxPayableAccountId: number +// ): ILedgerEntry[] => { +// // Retrieves the non-zero tax entries. +// const nonZeroTaxEntries = this.itemsEntriesService.getNonZeroEntries( +// saleInvoice.entries +// ); +// const transformTaxEntry = this.getInvoiceTaxEntry( +// saleInvoice, +// taxPayableAccountId +// ); +// // Transforms the non-zero tax entries to GL entries. +// return nonZeroTaxEntries.map(transformTaxEntry); +// }; + +// /** +// * Retrieves the invoice GL entries. +// * @param {ISaleInvoice} saleInvoice +// * @param {number} ARAccountId +// * @returns {ILedgerEntry[]} +// */ +// public getInvoiceGLEntries = ( +// saleInvoice: ISaleInvoice, +// ARAccountId: number, +// taxPayableAccountId: number +// ): ILedgerEntry[] => { +// const receivableEntry = this.getInvoiceReceivableEntry( +// saleInvoice, +// ARAccountId +// ); +// const transformItemEntry = this.getInvoiceItemEntry(saleInvoice); +// const creditEntries = saleInvoice.entries.map(transformItemEntry); + +// const taxEntries = this.getInvoiceTaxEntries( +// saleInvoice, +// taxPayableAccountId +// ); +// return [receivableEntry, ...creditEntries, ...taxEntries]; +// }; +// } diff --git a/packages/server-nest/src/modules/SaleInvoices/InvoiceInventoryTransactions.ts b/packages/server-nest/src/modules/SaleInvoices/InvoiceInventoryTransactions.ts new file mode 100644 index 000000000..9d6c73401 --- /dev/null +++ b/packages/server-nest/src/modules/SaleInvoices/InvoiceInventoryTransactions.ts @@ -0,0 +1,78 @@ +// import { Inject, Service } from 'typedi'; +// import { Knex } from 'knex'; +// import { ISaleInvoice } from '@/interfaces'; +// import ItemsEntriesService from '@/services/Items/ItemsEntriesService'; +// import InventoryService from '@/services/Inventory/Inventory'; + +// @Service() +// export class InvoiceInventoryTransactions { +// @Inject() +// private itemsEntriesService: ItemsEntriesService; + +// @Inject() +// private inventoryService: InventoryService; + +// /** +// * Records the inventory transactions of the given sale invoice in case +// * the invoice has inventory entries only. +// * +// * @param {number} tenantId - Tenant id. +// * @param {SaleInvoice} saleInvoice - Sale invoice DTO. +// * @param {number} saleInvoiceId - Sale invoice id. +// * @param {boolean} override - Allow to override old transactions. +// * @return {Promise} +// */ +// public async recordInventoryTranscactions( +// tenantId: number, +// saleInvoice: ISaleInvoice, +// override?: boolean, +// trx?: Knex.Transaction +// ): Promise { +// // Loads the inventory items entries of the given sale invoice. +// const inventoryEntries = +// await this.itemsEntriesService.filterInventoryEntries( +// tenantId, +// saleInvoice.entries, +// trx +// ); +// const transaction = { +// transactionId: saleInvoice.id, +// transactionType: 'SaleInvoice', +// transactionNumber: saleInvoice.invoiceNo, + +// exchangeRate: saleInvoice.exchangeRate, +// warehouseId: saleInvoice.warehouseId, + +// date: saleInvoice.invoiceDate, +// direction: 'OUT', +// entries: inventoryEntries, +// createdAt: saleInvoice.createdAt, +// }; +// await this.inventoryService.recordInventoryTransactionsFromItemsEntries( +// tenantId, +// transaction, +// override, +// trx +// ); +// } +// /** +// * Reverting the inventory transactions once the invoice deleted. +// * @param {number} tenantId - Tenant id. +// * @param {number} billId - Bill id. +// * @return {Promise} +// */ +// public async revertInventoryTransactions( +// tenantId: number, +// saleInvoiceId: number, +// trx?: Knex.Transaction +// ): Promise { +// // Delete the inventory transaction of the given sale invoice. +// const { oldInventoryTransactions } = +// await this.inventoryService.deleteInventoryTransactions( +// tenantId, +// saleInvoiceId, +// 'SaleInvoice', +// trx +// ); +// } +// } diff --git a/packages/server-nest/src/modules/SaleInvoices/InvoicePaymentsGLRewrite.ts b/packages/server-nest/src/modules/SaleInvoices/InvoicePaymentsGLRewrite.ts new file mode 100644 index 000000000..2a6cfd52c --- /dev/null +++ b/packages/server-nest/src/modules/SaleInvoices/InvoicePaymentsGLRewrite.ts @@ -0,0 +1,76 @@ +// import { Knex } from 'knex'; +// import async from 'async'; +// import { Inject, Service } from 'typedi'; +// import HasTenancyService from '@/services/Tenancy/TenancyService'; +// import { PaymentReceivedGLEntries } from '../PaymentReceived/PaymentReceivedGLEntries'; + +// @Service() +// export class InvoicePaymentsGLEntriesRewrite { +// @Inject() +// public tenancy: HasTenancyService; + +// @Inject() +// public paymentGLEntries: PaymentReceivedGLEntries; + +// /** +// * Rewrites the payment GL entries task. +// * @param {{ tenantId: number, paymentId: number, trx: Knex?.Transaction }} +// * @returns {Promise} +// */ +// public rewritePaymentsGLEntriesTask = async ({ +// tenantId, +// paymentId, +// trx, +// }) => { +// await this.paymentGLEntries.rewritePaymentGLEntries( +// tenantId, +// paymentId, +// trx +// ); +// }; + +// /** +// * Rewrites the payment GL entries of the given payments ids. +// * @param {number} tenantId +// * @param {number[]} paymentsIds +// * @param {Knex.Transaction} trx +// */ +// public rewritePaymentsGLEntriesQueue = async ( +// tenantId: number, +// paymentsIds: number[], +// trx?: Knex.Transaction +// ) => { +// // Initiate a new queue for accounts balance mutation. +// const rewritePaymentGL = async.queue(this.rewritePaymentsGLEntriesTask, 10); + +// paymentsIds.forEach((paymentId: number) => { +// rewritePaymentGL.push({ paymentId, trx, tenantId }); +// }); +// if (paymentsIds.length > 0) { +// await rewritePaymentGL.drain(); +// } +// }; + +// /** +// * Rewrites the payments GL entries that associated to the given invoice. +// * @param {number} tenantId +// * @param {number} invoiceId +// * @param {Knex.Transaction} trx +// * @returns {Promise} +// */ +// public invoicePaymentsGLEntriesRewrite = async ( +// tenantId: number, +// invoiceId: number, +// trx?: Knex.Transaction +// ) => { +// const { PaymentReceiveEntry } = this.tenancy.models(tenantId); + +// const invoicePaymentEntries = await PaymentReceiveEntry.query().where( +// 'invoiceId', +// invoiceId +// ); +// const paymentsIds = invoicePaymentEntries.map((e) => e.paymentReceiveId); + +// await this.rewritePaymentsGLEntriesQueue(tenantId, paymentsIds, trx); +// }; +// } diff --git a/packages/server-nest/src/modules/SaleInvoices/SaleInvoice.types.ts b/packages/server-nest/src/modules/SaleInvoices/SaleInvoice.types.ts new file mode 100644 index 000000000..bdfb51f27 --- /dev/null +++ b/packages/server-nest/src/modules/SaleInvoices/SaleInvoice.types.ts @@ -0,0 +1,320 @@ +import { Knex } from 'knex'; +import { IItemEntryDTO } from '../TransactionItemEntry/ItemEntry.types'; +import { AttachmentLinkDTO } from '../Attachments/Attachments.types'; +import SaleInvoice from './models/SaleInvoice'; +import { SystemUser } from '../System/models/SystemUser'; +// import { ISystemUser, IAccount, ITaxTransaction } from '@/interfaces'; +// import { CommonMailOptions, CommonMailOptionsDTO } from './Mailable'; +// import { IDynamicListFilter } from '@/interfaces/DynamicFilter'; +// import { IItemEntry, IItemEntryDTO } from './ItemEntry'; +// import { AttachmentLinkDTO } from './Attachments'; + +export interface PaymentIntegrationTransactionLink { + id: number; + enable: true; + paymentIntegrationId: number; + referenceType: string; + referenceId: number; +} + +export interface PaymentIntegrationTransactionLinkEventPayload { + tenantId: number; + enable: true; + paymentIntegrationId: number; + referenceType: string; + referenceId: number; + saleInvoiceId: number; + trx?: Knex.Transaction; +} + +export interface PaymentIntegrationTransactionLinkDeleteEventPayload { + tenantId: number; + enable: true; + paymentIntegrationId: number; + referenceType: string; + referenceId: number; + oldSaleInvoiceId: number; + trx?: Knex.Transaction; +} + +export interface ISaleInvoiceDTO { + invoiceDate: Date; + dueDate: Date; + referenceNo: string; + invoiceNo: string; + customerId: number; + exchangeRate?: number; + invoiceMessage: string; + termsConditions: string; + isTaxExclusive: boolean; + entries: IItemEntryDTO[]; + delivered: boolean; + + warehouseId?: number | null; + projectId?: number; + branchId?: number | null; + + isInclusiveTax?: boolean; + + attachments?: AttachmentLinkDTO[]; +} + +export interface ISaleInvoiceCreateDTO extends ISaleInvoiceDTO { + fromEstimateId: number; +} + +export interface ISaleInvoiceEditDTO extends ISaleInvoiceDTO {} + +// export interface ISalesInvoicesFilter extends IDynamicListFilter { +// page: number; +// pageSize: number; +// searchKeyword?: string; +// filterQuery?: (q: Knex.QueryBuilder) => void; +// } + +export interface ISaleInvoiceWriteoffDTO { + expenseAccountId: number; + date: Date; + reason: string; +} + +export type InvoiceNotificationType = 'details' | 'reminder'; + +export interface ISaleInvoiceCreatedPayload { + // tenantId: number; + saleInvoice: SaleInvoice; + saleInvoiceDTO: ISaleInvoiceCreateDTO; + saleInvoiceId: number; + // authorizedUser: SystemUser; + trx: Knex.Transaction; +} + +export interface ISaleInvoiceCreatingPaylaod { + tenantId: number; + saleInvoiceDTO: ISaleInvoiceCreateDTO; + trx: Knex.Transaction; +} + +export interface ISaleInvoiceEditedPayload { + // tenantId: number; + saleInvoice: SaleInvoice; + oldSaleInvoice: SaleInvoice; + saleInvoiceDTO: ISaleInvoiceEditDTO; + saleInvoiceId: number; + // authorizedUser: SystemUser; + trx: Knex.Transaction; +} + +export interface ISaleInvoiceEditingPayload { + // tenantId: number; + saleInvoiceDTO: ISaleInvoiceEditDTO; + oldSaleInvoice: SaleInvoice; + trx: Knex.Transaction; +} + +export interface ISaleInvoiceDeletePayload { + // tenantId: number; + oldSaleInvoice: SaleInvoice; + saleInvoiceId: number; +} + +export interface ISaleInvoiceDeletingPayload { + // tenantId: number; + oldSaleInvoice: SaleInvoice; + saleInvoiceId: number; + trx: Knex.Transaction; +} + +export interface ISaleInvoiceDeletedPayload { + // tenantId: number; + oldSaleInvoice: SaleInvoice; + saleInvoiceId: number; + trx: Knex.Transaction; +} + +export interface ISaleInvoiceWriteoffCreatePayload { + // tenantId: number; + saleInvoiceId: number; + saleInvoice: SaleInvoice; + writeoffDTO: ISaleInvoiceWriteoffDTO; + trx: Knex.Transaction; +} + +export interface ISaleInvoiceWriteoffCreatedPayload { + // tenantId: number; + saleInvoiceId: number; + saleInvoice: SaleInvoice; + writeoffDTO: ISaleInvoiceCreatedPayload; +} + +export interface ISaleInvoiceWrittenOffCancelPayload { + // tenantId: number; + saleInvoice: SaleInvoice; + trx: Knex.Transaction; +} + +export interface ISaleInvoiceWrittenOffCanceledPayload { + // tenantId: number; + saleInvoice: SaleInvoice; + trx: Knex.Transaction; +} + +export interface ISaleInvoiceEventDeliveredPayload { + // tenantId: number; + saleInvoiceId: number; + saleInvoice: SaleInvoice; + trx: Knex.Transaction; +} + +export interface ISaleInvoiceDeliveringPayload { + // tenantId: number; + oldSaleInvoice: SaleInvoice; + trx: Knex.Transaction; +} + +export enum SaleInvoiceAction { + Create = 'Create', + Edit = 'Edit', + Delete = 'Delete', + View = 'View', + Writeoff = 'Writeoff', + NotifyBySms = 'NotifyBySms', +} + +// export interface SaleInvoiceMailOptions extends CommonMailOptions { +// attachInvoice?: boolean; +// formatArgs?: Record; +// } + +// export interface SaleInvoiceMailState extends SaleInvoiceMailOptions { +// invoiceNo: string; + +// invoiceDate: string; +// invoiceDateFormatted: string; + +// dueDate: string; +// dueDateFormatted: string; + +// total: number; +// totalFormatted: string; + +// subtotal: number; +// subtotalFormatted: number; + +// companyName: string; +// companyLogoUri: string; + +// customerName: string; + +// // # Invoice entries +// entries?: Array<{ label: string; total: string; quantity: string | number }>; +// } + +// export interface SendInvoiceMailDTO extends CommonMailOptionsDTO { +// attachInvoice?: boolean; +// } + +// export interface ISaleInvoiceNotifyPayload { +// tenantId: number; +// saleInvoiceId: number; +// messageDTO: SendInvoiceMailDTO; +// } + +// export interface ISaleInvoiceMailSend { +// tenantId: number; +// saleInvoiceId: number; +// messageOptions: SendInvoiceMailDTO; +// formattedMessageOptions: SaleInvoiceMailOptions; +// } + +// export interface ISaleInvoiceMailSent { +// tenantId: number; +// saleInvoiceId: number; +// messageOptions: SendInvoiceMailDTO; +// } + +// Invoice Pdf Document +export interface InvoicePdfLine { + item: string; + description: string; + rate: string; + quantity: string; + total: string; +} + +export interface InvoicePdfTax { + label: string; + amount: string; +} + +export interface InvoicePdfTemplateAttributes { + primaryColor: string; + secondaryColor: string; + + companyName: string; + + showCompanyLogo: boolean; + companyLogo: string; + + dueDate: string; + dueDateLabel: string; + showDueDate: boolean; + + dateIssue: string; + dateIssueLabel: string; + showDateIssue: boolean; + + invoiceNumberLabel: string; + invoiceNumber: string; + showInvoiceNumber: boolean; + + // Customer Address + showCustomerAddress: boolean; + customerAddress: string; + + // Company address + showCompanyAddress: boolean; + companyAddress: string; + billedToLabel: string; + + lineItemLabel: string; + lineDescriptionLabel: string; + lineRateLabel: string; + lineTotalLabel: string; + + totalLabel: string; + subtotalLabel: string; + discountLabel: string; + paymentMadeLabel: string; + + showTotal: boolean; + showSubtotal: boolean; + showDiscount: boolean; + showTaxes: boolean; + showPaymentMade: boolean; + + total: string; + subtotal: string; + discount: string; + paymentMade: string; + + // Due Amount + dueAmount: string; + showDueAmount: boolean; + dueAmountLabel: string; + + termsConditionsLabel: string; + showTermsConditions: boolean; + termsConditions: string; + + lines: InvoicePdfLine[]; + taxes: InvoicePdfTax[]; + + statementLabel: string; + showStatement: boolean; + statement: string; +} + +export interface ISaleInvocieState { + defaultTemplateId: number; +} diff --git a/packages/server-nest/src/modules/SaleInvoices/SaleInvoiceCostGLEntries.ts b/packages/server-nest/src/modules/SaleInvoices/SaleInvoiceCostGLEntries.ts new file mode 100644 index 000000000..90d0296d5 --- /dev/null +++ b/packages/server-nest/src/modules/SaleInvoices/SaleInvoiceCostGLEntries.ts @@ -0,0 +1,146 @@ +// import { Service, Inject } from 'typedi'; +// import * as R from 'ramda'; +// import { Knex } from 'knex'; +// import { AccountNormal, IInventoryLotCost, ILedgerEntry } from '@/interfaces'; +// import { increment } from 'utils'; +// import HasTenancyService from '@/services/Tenancy/TenancyService'; +// import Ledger from '@/services/Accounting/Ledger'; +// import LedgerStorageService from '@/services/Accounting/LedgerStorageService'; +// import { groupInventoryTransactionsByTypeId } from '../../Inventory/utils'; + +// @Service() +// export class SaleInvoiceCostGLEntries { +// @Inject() +// private tenancy: HasTenancyService; + +// @Inject() +// private ledgerStorage: LedgerStorageService; + +// /** +// * Writes journal entries from sales invoices. +// * @param {number} tenantId - The tenant id. +// * @param {Date} startingDate - Starting date. +// * @param {boolean} override +// */ +// public writeInventoryCostJournalEntries = async ( +// tenantId: number, +// startingDate: Date, +// trx?: Knex.Transaction +// ): Promise => { +// const { InventoryCostLotTracker } = this.tenancy.models(tenantId); + +// const inventoryCostLotTrans = await InventoryCostLotTracker.query() +// .where('direction', 'OUT') +// .where('transaction_type', 'SaleInvoice') +// .where('cost', '>', 0) +// .modify('filterDateRange', startingDate) +// .orderBy('date', 'ASC') +// .withGraphFetched('invoice') +// .withGraphFetched('item'); + +// const ledger = this.getInventoryCostLotsLedger(inventoryCostLotTrans); + +// // Commit the ledger to the storage. +// await this.ledgerStorage.commit(tenantId, ledger, trx); +// }; + +// /** +// * Retrieves the inventory cost lots ledger. +// * @param {IInventoryLotCost[]} inventoryCostLots +// * @returns {Ledger} +// */ +// private getInventoryCostLotsLedger = ( +// inventoryCostLots: IInventoryLotCost[] +// ) => { +// // Groups the inventory cost lots transactions. +// const inventoryTransactions = +// groupInventoryTransactionsByTypeId(inventoryCostLots); + +// const entries = inventoryTransactions +// .map(this.getSaleInvoiceCostGLEntries) +// .flat(); +// return new Ledger(entries); +// }; + +// /** +// * +// * @param {IInventoryLotCost} inventoryCostLot +// * @returns {} +// */ +// private getInvoiceCostGLCommonEntry = ( +// inventoryCostLot: IInventoryLotCost +// ) => { +// return { +// currencyCode: inventoryCostLot.invoice.currencyCode, +// exchangeRate: inventoryCostLot.invoice.exchangeRate, + +// transactionType: inventoryCostLot.transactionType, +// transactionId: inventoryCostLot.transactionId, + +// date: inventoryCostLot.date, +// indexGroup: 20, +// costable: true, +// createdAt: inventoryCostLot.createdAt, + +// debit: 0, +// credit: 0, + +// branchId: inventoryCostLot.invoice.branchId, +// }; +// }; + +// /** +// * Retrieves the inventory cost GL entry. +// * @param {IInventoryLotCost} inventoryLotCost +// * @returns {ILedgerEntry[]} +// */ +// private getInventoryCostGLEntry = R.curry( +// ( +// getIndexIncrement, +// inventoryCostLot: IInventoryLotCost +// ): ILedgerEntry[] => { +// const commonEntry = this.getInvoiceCostGLCommonEntry(inventoryCostLot); +// const costAccountId = +// inventoryCostLot.costAccountId || inventoryCostLot.item.costAccountId; + +// // XXX Debit - Cost account. +// const costEntry = { +// ...commonEntry, +// debit: inventoryCostLot.cost, +// accountId: costAccountId, +// accountNormal: AccountNormal.DEBIT, +// itemId: inventoryCostLot.itemId, +// index: getIndexIncrement(), +// }; +// // XXX Credit - Inventory account. +// const inventoryEntry = { +// ...commonEntry, +// credit: inventoryCostLot.cost, +// accountId: inventoryCostLot.item.inventoryAccountId, +// accountNormal: AccountNormal.DEBIT, +// itemId: inventoryCostLot.itemId, +// index: getIndexIncrement(), +// }; +// return [costEntry, inventoryEntry]; +// } +// ); + +// /** +// * Writes journal entries for given sale invoice. +// * ----- +// * - Cost of goods sold -> Debit -> YYYY +// * - Inventory assets -> Credit -> YYYY +// *----- +// * @param {ISaleInvoice} saleInvoice +// * @param {JournalPoster} journal +// */ +// public getSaleInvoiceCostGLEntries = ( +// inventoryCostLots: IInventoryLotCost[] +// ): ILedgerEntry[] => { +// const getIndexIncrement = increment(0); +// const getInventoryLotEntry = +// this.getInventoryCostGLEntry(getIndexIncrement); + +// return inventoryCostLots.map(getInventoryLotEntry).flat(); +// }; +// } diff --git a/packages/server-nest/src/modules/SaleInvoices/SaleInvoiceNotifyBySms.ts b/packages/server-nest/src/modules/SaleInvoices/SaleInvoiceNotifyBySms.ts new file mode 100644 index 000000000..5f6a47bb6 --- /dev/null +++ b/packages/server-nest/src/modules/SaleInvoices/SaleInvoiceNotifyBySms.ts @@ -0,0 +1,260 @@ +// import { Service, Inject } from 'typedi'; +// import moment from 'moment'; +// import HasTenancyService from '@/services/Tenancy/TenancyService'; +// import events from '@/subscribers/events'; +// import { +// ISaleInvoice, +// ISaleInvoiceSmsDetailsDTO, +// ISaleInvoiceSmsDetails, +// SMS_NOTIFICATION_KEY, +// InvoiceNotificationType, +// ICustomer, +// } from '@/interfaces'; +// import SmsNotificationsSettingsService from '@/services/Settings/SmsNotificationsSettings'; +// import { formatSmsMessage, formatNumber } from 'utils'; +// import { TenantMetadata } from '@/system/models'; +// import SaleNotifyBySms from '../SaleNotifyBySms'; +// import { ServiceError } from '@/exceptions'; +// import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; +// import { ERRORS } from './constants'; +// import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators'; + +// @Service() +// export class SaleInvoiceNotifyBySms { +// @Inject() +// private tenancy: HasTenancyService; + +// @Inject() +// private eventPublisher: EventPublisher; + +// @Inject() +// private smsNotificationsSettings: SmsNotificationsSettingsService; + +// @Inject() +// private saleSmsNotification: SaleNotifyBySms; + +// @Inject() +// private validators: CommandSaleInvoiceValidators; + +// /** +// * Notify customer via sms about sale invoice. +// * @param {number} tenantId - Tenant id. +// * @param {number} saleInvoiceId - Sale invoice id. +// */ +// public notifyBySms = async ( +// tenantId: number, +// saleInvoiceId: number, +// invoiceNotificationType: InvoiceNotificationType +// ) => { +// const { SaleInvoice } = this.tenancy.models(tenantId); + +// // Retrieve the sale invoice or throw not found service error. +// const saleInvoice = await SaleInvoice.query() +// .findById(saleInvoiceId) +// .withGraphFetched('customer'); + +// // Validates the givne invoice existance. +// this.validators.validateInvoiceExistance(saleInvoice); + +// // Validate the customer phone number existance and number validation. +// this.saleSmsNotification.validateCustomerPhoneNumber( +// saleInvoice.customer.personalPhone +// ); +// // Transformes the invoice notification key to sms notification key. +// const notificationKey = this.transformDTOKeyToNotificationKey( +// invoiceNotificationType +// ); +// // Triggers `onSaleInvoiceNotifySms` event. +// await this.eventPublisher.emitAsync(events.saleInvoice.onNotifySms, { +// tenantId, +// saleInvoice, +// }); +// // Formattes the sms message and sends sms notification. +// await this.sendSmsNotification(tenantId, notificationKey, saleInvoice); + +// // Triggers `onSaleInvoiceNotifySms` event. +// await this.eventPublisher.emitAsync(events.saleInvoice.onNotifiedSms, { +// tenantId, +// saleInvoice, +// }); +// return saleInvoice; +// }; + +// /** +// * Notify invoice details by sms notification after invoice creation. +// * @param {number} tenantId +// * @param {number} saleInvoiceId +// * @returns {Promise} +// */ +// public notifyDetailsBySmsAfterCreation = async ( +// tenantId: number, +// saleInvoiceId: number +// ): Promise => { +// const notification = this.smsNotificationsSettings.getSmsNotificationMeta( +// tenantId, +// SMS_NOTIFICATION_KEY.SALE_INVOICE_DETAILS +// ); +// // Can't continue if the sms auto-notification is not enabled. +// if (!notification.isNotificationEnabled) return; + +// await this.notifyBySms(tenantId, saleInvoiceId, 'details'); +// }; + +// /** +// * Sends SMS notification. +// * @param {ISaleInvoice} invoice +// * @param {ICustomer} customer +// * @returns {Promise} +// */ +// private sendSmsNotification = async ( +// tenantId: number, +// notificationType: +// | SMS_NOTIFICATION_KEY.SALE_INVOICE_DETAILS +// | SMS_NOTIFICATION_KEY.SALE_INVOICE_REMINDER, +// invoice: ISaleInvoice & { customer: ICustomer } +// ): Promise => { +// const smsClient = this.tenancy.smsClient(tenantId); +// const tenantMetadata = await TenantMetadata.query().findOne({ tenantId }); + +// // Formates the given sms message. +// const message = this.formattedInvoiceDetailsMessage( +// tenantId, +// notificationType, +// invoice, +// tenantMetadata +// ); +// const phoneNumber = invoice.customer.personalPhone; + +// // Run the send sms notification message job. +// await smsClient.sendMessageJob(phoneNumber, message); +// }; + +// /** +// * Formates the invoice details sms message. +// * @param {number} tenantId +// * @param {ISaleInvoice} invoice +// * @param {ICustomer} customer +// * @returns {string} +// */ +// private formattedInvoiceDetailsMessage = ( +// tenantId: number, +// notificationKey: +// | SMS_NOTIFICATION_KEY.SALE_INVOICE_DETAILS +// | SMS_NOTIFICATION_KEY.SALE_INVOICE_REMINDER, +// invoice: ISaleInvoice, +// tenantMetadata: TenantMetadata +// ): string => { +// const notification = this.smsNotificationsSettings.getSmsNotificationMeta( +// tenantId, +// notificationKey +// ); +// return this.formatInvoiceDetailsMessage( +// notification.smsMessage, +// invoice, +// tenantMetadata +// ); +// }; + +// /** +// * Formattees the given invoice details sms message. +// * @param {string} smsMessage +// * @param {ISaleInvoice} invoice +// * @param {ICustomer} customer +// * @param {TenantMetadata} tenantMetadata +// */ +// private formatInvoiceDetailsMessage = ( +// smsMessage: string, +// invoice: ISaleInvoice & { customer: ICustomer }, +// tenantMetadata: TenantMetadata +// ) => { +// const formattedDueAmount = formatNumber(invoice.dueAmount, { +// currencyCode: invoice.currencyCode, +// }); +// const formattedAmount = formatNumber(invoice.balance, { +// currencyCode: invoice.currencyCode, +// }); + +// return formatSmsMessage(smsMessage, { +// InvoiceNumber: invoice.invoiceNo, +// ReferenceNumber: invoice.referenceNo, +// CustomerName: invoice.customer.displayName, +// DueAmount: formattedDueAmount, +// DueDate: moment(invoice.dueDate).format('YYYY/MM/DD'), +// Amount: formattedAmount, +// CompanyName: tenantMetadata.name, +// }); +// }; + +// /** +// * Retrieve the SMS details of the given invoice. +// * @param {number} tenantId - Tenant id. +// * @param {number} saleInvoiceId - Sale invoice id. +// */ +// public smsDetails = async ( +// tenantId: number, +// saleInvoiceId: number, +// invoiceSmsDetailsDTO: ISaleInvoiceSmsDetailsDTO +// ): Promise => { +// const { SaleInvoice } = this.tenancy.models(tenantId); + +// // Retrieve the sale invoice or throw not found service error. +// const saleInvoice = await SaleInvoice.query() +// .findById(saleInvoiceId) +// .withGraphFetched('customer'); + +// // Validates the sale invoice existance. +// this.validateSaleInvoiceExistance(saleInvoice); + +// // Current tenant metadata. +// const tenantMetadata = await TenantMetadata.query().findOne({ tenantId }); + +// // Transformes the invoice notification key to sms notification key. +// const notificationKey = this.transformDTOKeyToNotificationKey( +// invoiceSmsDetailsDTO.notificationKey +// ); +// // Formates the given sms message. +// const smsMessage = this.formattedInvoiceDetailsMessage( +// tenantId, +// notificationKey, +// saleInvoice, +// tenantMetadata +// ); + +// return { +// customerName: saleInvoice.customer.displayName, +// customerPhoneNumber: saleInvoice.customer.personalPhone, +// smsMessage, +// }; +// }; + +// /** +// * Transformes the invoice notification key DTO to notification key. +// * @param {string} invoiceNotifKey +// * @returns {SMS_NOTIFICATION_KEY.SALE_INVOICE_DETAILS +// * | SMS_NOTIFICATION_KEY.SALE_INVOICE_REMINDER} +// */ +// private transformDTOKeyToNotificationKey = ( +// invoiceNotifKey: string +// ): +// | SMS_NOTIFICATION_KEY.SALE_INVOICE_DETAILS +// | SMS_NOTIFICATION_KEY.SALE_INVOICE_REMINDER => { +// const invoiceNotifKeyPairs = { +// details: SMS_NOTIFICATION_KEY.SALE_INVOICE_DETAILS, +// reminder: SMS_NOTIFICATION_KEY.SALE_INVOICE_REMINDER, +// }; +// return ( +// invoiceNotifKeyPairs[invoiceNotifKey] || +// SMS_NOTIFICATION_KEY.SALE_INVOICE_DETAILS +// ); +// }; + +// /** +// * Validates the sale invoice existance. +// * @param {ISaleInvoice|null} saleInvoice +// */ +// private validateSaleInvoiceExistance(saleInvoice: ISaleInvoice | null) { +// if (!saleInvoice) { +// throw new ServiceError(ERRORS.SALE_INVOICE_NOT_FOUND); +// } +// } +// } diff --git a/packages/server-nest/src/modules/SaleInvoices/SaleInvoiceWriteoffGLEntries.ts b/packages/server-nest/src/modules/SaleInvoices/SaleInvoiceWriteoffGLEntries.ts new file mode 100644 index 000000000..ee4ad9782 --- /dev/null +++ b/packages/server-nest/src/modules/SaleInvoices/SaleInvoiceWriteoffGLEntries.ts @@ -0,0 +1,104 @@ +// import { Service } from 'typedi'; +// import { ISaleInvoice, AccountNormal, ILedgerEntry, ILedger } from '@/interfaces'; +// import Ledger from '@/services/Accounting/Ledger'; + +// @Service() +// export class SaleInvoiceWriteoffGLEntries { +// /** +// * Retrieves the invoice write-off common GL entry. +// * @param {ISaleInvoice} saleInvoice +// */ +// private getInvoiceWriteoffGLCommonEntry = (saleInvoice: ISaleInvoice) => { +// return { +// date: saleInvoice.invoiceDate, + +// currencyCode: saleInvoice.currencyCode, +// exchangeRate: saleInvoice.exchangeRate, + +// transactionId: saleInvoice.id, +// transactionType: 'InvoiceWriteOff', +// transactionNumber: saleInvoice.invoiceNo, + +// referenceNo: saleInvoice.referenceNo, +// branchId: saleInvoice.branchId, +// }; +// }; + +// /** +// * Retrieves the invoice write-off receiveable GL entry. +// * @param {number} ARAccountId +// * @param {ISaleInvoice} saleInvoice +// * @returns {ILedgerEntry} +// */ +// private getInvoiceWriteoffGLReceivableEntry = ( +// ARAccountId: number, +// saleInvoice: ISaleInvoice +// ): ILedgerEntry => { +// const commontEntry = this.getInvoiceWriteoffGLCommonEntry(saleInvoice); + +// return { +// ...commontEntry, +// credit: saleInvoice.localWrittenoffAmount, +// accountId: ARAccountId, +// contactId: saleInvoice.customerId, +// debit: 0, +// index: 1, +// indexGroup: 300, +// accountNormal: saleInvoice.writtenoffExpenseAccount.accountNormal, +// }; +// }; + +// /** +// * Retrieves the invoice write-off expense GL entry. +// * @param {ISaleInvoice} saleInvoice +// * @returns {ILedgerEntry} +// */ +// private getInvoiceWriteoffGLExpenseEntry = ( +// saleInvoice: ISaleInvoice +// ): ILedgerEntry => { +// const commontEntry = this.getInvoiceWriteoffGLCommonEntry(saleInvoice); + +// return { +// ...commontEntry, +// debit: saleInvoice.localWrittenoffAmount, +// accountId: saleInvoice.writtenoffExpenseAccountId, +// credit: 0, +// index: 2, +// indexGroup: 300, +// accountNormal: AccountNormal.DEBIT, +// }; +// }; + +// /** +// * Retrieves the invoice write-off GL entries. +// * @param {number} ARAccountId +// * @param {ISaleInvoice} saleInvoice +// * @returns {ILedgerEntry[]} +// */ +// public getInvoiceWriteoffGLEntries = ( +// ARAccountId: number, +// saleInvoice: ISaleInvoice +// ): ILedgerEntry[] => { +// const creditEntry = this.getInvoiceWriteoffGLExpenseEntry(saleInvoice); +// const debitEntry = this.getInvoiceWriteoffGLReceivableEntry( +// ARAccountId, +// saleInvoice +// ); +// return [debitEntry, creditEntry]; +// }; + +// /** +// * Retrieves the invoice write-off ledger. +// * @param {number} ARAccountId +// * @param {ISaleInvoice} saleInvoice +// * @returns {Ledger} +// */ +// public getInvoiceWriteoffLedger = ( +// ARAccountId: number, +// saleInvoice: ISaleInvoice +// ): ILedger => { +// const entries = this.getInvoiceWriteoffGLEntries(ARAccountId, saleInvoice); + +// return new Ledger(entries); +// }; +// } diff --git a/packages/server-nest/src/modules/SaleInvoices/SaleInvoiceWriteoffGLStorage.ts b/packages/server-nest/src/modules/SaleInvoices/SaleInvoiceWriteoffGLStorage.ts new file mode 100644 index 000000000..581cc8e26 --- /dev/null +++ b/packages/server-nest/src/modules/SaleInvoices/SaleInvoiceWriteoffGLStorage.ts @@ -0,0 +1,88 @@ +// import { Knex } from 'knex'; +// import LedgerStorageService from '@/services/Accounting/LedgerStorageService'; +// import HasTenancyService from '@/services/Tenancy/TenancyService'; +// import { Service, Inject } from 'typedi'; +// import { SaleInvoiceWriteoffGLEntries } from './SaleInvoiceWriteoffGLEntries'; + +// @Service() +// export class SaleInvoiceWriteoffGLStorage { +// @Inject() +// private invoiceWriteoffLedger: SaleInvoiceWriteoffGLEntries; + +// @Inject() +// private ledgerStorage: LedgerStorageService; + +// @Inject() +// private tenancy: HasTenancyService; + +// /** +// * Writes the invoice write-off GL entries. +// * @param {number} tenantId +// * @param {number} saleInvoiceId +// * @param {Knex.Transaction} trx +// * @returns {Promise} +// */ +// public writeInvoiceWriteoffEntries = async ( +// tenantId: number, +// saleInvoiceId: number, +// trx?: Knex.Transaction +// ) => { +// const { SaleInvoice } = this.tenancy.models(tenantId); +// const { accountRepository } = this.tenancy.repositories(tenantId); + +// // Retrieves the sale invoice. +// const saleInvoice = await SaleInvoice.query(trx) +// .findById(saleInvoiceId) +// .withGraphFetched('writtenoffExpenseAccount'); + +// // Find or create the A/R account. +// const ARAccount = await accountRepository.findOrCreateAccountReceivable( +// saleInvoice.currencyCode, +// {}, +// trx +// ); +// // Retrieves the invoice write-off ledger. +// const ledger = this.invoiceWriteoffLedger.getInvoiceWriteoffLedger( +// ARAccount.id, +// saleInvoice +// ); +// return this.ledgerStorage.commit(tenantId, ledger, trx); +// }; + +// /** +// * Rewrites the invoice write-off GL entries. +// * @param {number} tenantId +// * @param {number} saleInvoiceId +// * @param {Knex.Transactio} actiontrx +// * @returns {Promise} +// */ +// public rewriteInvoiceWriteoffEntries = async ( +// tenantId: number, +// saleInvoiceId: number, +// trx?: Knex.Transaction +// ) => { +// await this.revertInvoiceWriteoffEntries(tenantId, saleInvoiceId, trx); + +// await this.writeInvoiceWriteoffEntries(tenantId, saleInvoiceId, trx); +// }; + +// /** +// * Reverts the invoice write-off GL entries. +// * @param {number} tenantId +// * @param {number} saleInvoiceId +// * @param {Knex.Transaction} trx +// * @returns {Promise} +// */ +// public revertInvoiceWriteoffEntries = async ( +// tenantId: number, +// saleInvoiceId: number, +// trx?: Knex.Transaction +// ) => { +// await this.ledgerStorage.deleteByReference( +// tenantId, +// saleInvoiceId, +// 'InvoiceWriteOff', +// trx +// ); +// }; +// } diff --git a/packages/server-nest/src/modules/SaleInvoices/SaleInvoiceWriteoffSubscriber.ts b/packages/server-nest/src/modules/SaleInvoices/SaleInvoiceWriteoffSubscriber.ts new file mode 100644 index 000000000..d42855c1b --- /dev/null +++ b/packages/server-nest/src/modules/SaleInvoices/SaleInvoiceWriteoffSubscriber.ts @@ -0,0 +1,58 @@ +// import { Inject, Service } from 'typedi'; +// import events from '@/subscribers/events'; +// import { +// ISaleInvoiceWriteoffCreatePayload, +// ISaleInvoiceWrittenOffCanceledPayload, +// } from '@/interfaces'; +// import { SaleInvoiceWriteoffGLStorage } from './SaleInvoiceWriteoffGLStorage'; + +// @Service() +// export default class SaleInvoiceWriteoffSubscriber { +// @Inject() +// writeGLStorage: SaleInvoiceWriteoffGLStorage; + +// /** +// * Attaches events. +// */ +// public attach(bus) { +// bus.subscribe( +// events.saleInvoice.onWrittenoff, +// this.writeJournalEntriesOnceWriteoffCreate +// ); +// bus.subscribe( +// events.saleInvoice.onWrittenoffCanceled, +// this.revertJournalEntriesOnce +// ); +// } +// /** +// * Write the written-off sale invoice journal entries. +// * @param {ISaleInvoiceWriteoffCreatePayload} +// */ +// private writeJournalEntriesOnceWriteoffCreate = async ({ +// tenantId, +// saleInvoice, +// trx, +// }: ISaleInvoiceWriteoffCreatePayload) => { +// await this.writeGLStorage.writeInvoiceWriteoffEntries( +// tenantId, +// saleInvoice.id, +// trx +// ); +// }; + +// /** +// * Reverts the written-of sale invoice jounral entries. +// * @param {ISaleInvoiceWrittenOffCanceledPayload} +// */ +// private revertJournalEntriesOnce = async ({ +// tenantId, +// saleInvoice, +// trx, +// }: ISaleInvoiceWrittenOffCanceledPayload) => { +// await this.writeGLStorage.revertInvoiceWriteoffEntries( +// tenantId, +// saleInvoice.id, +// trx +// ); +// }; +// } diff --git a/packages/server-nest/src/modules/SaleInvoices/SaleInvoices.application.ts b/packages/server-nest/src/modules/SaleInvoices/SaleInvoices.application.ts new file mode 100644 index 000000000..3939c55b7 --- /dev/null +++ b/packages/server-nest/src/modules/SaleInvoices/SaleInvoices.application.ts @@ -0,0 +1,274 @@ +import { Injectable } from '@nestjs/common'; +import { CreateSaleInvoice } from './commands/CreateSaleInvoice.service'; +import { DeleteSaleInvoice } from './commands/DeleteSaleInvoice.service'; +import { GetSaleInvoice } from './queries/GetSaleInvoice.service'; +import { EditSaleInvoice } from './commands/EditSaleInvoice.service'; +// import { GetSaleInvoices } from './queries/GetSaleInvoices'; +import { DeliverSaleInvoice } from './commands/DeliverSaleInvoice.service'; +import { GetSaleInvoicesPayable } from './queries/GetSaleInvoicesPayable.service'; +import { WriteoffSaleInvoice } from './commands/WriteoffSaleInvoice.service'; +import { SaleInvoicePdf } from './queries/SaleInvoicePdf.service'; +import { GetInvoicePaymentsService } from './queries/GetInvoicePayments.service'; +// import { SaleInvoiceNotifyBySms } from './SaleInvoiceNotifyBySms'; +// import { SendInvoiceMailReminder } from './commands/SendSaleInvoiceMailReminder'; +// import { SendSaleInvoiceMail } from './commands/SendSaleInvoiceMail'; +import { GetSaleInvoiceState } from './queries/GetSaleInvoiceState.service'; +// import { GetSaleInvoiceMailState } from './queries/GetSaleInvoiceMailState.service'; +import { + ISaleInvoiceCreateDTO, + ISaleInvoiceEditDTO, + ISaleInvoiceWriteoffDTO, +} from './SaleInvoice.types'; + +@Injectable() +export class SaleInvoiceApplication { + constructor( + private createSaleInvoiceService: CreateSaleInvoice, + private deleteSaleInvoiceService: DeleteSaleInvoice, + private getSaleInvoiceService: GetSaleInvoice, + // private getSaleInvoicesService: GetSaleInvoices, + private editSaleInvoiceService: EditSaleInvoice, + private deliverSaleInvoiceService: DeliverSaleInvoice, + private getReceivableSaleInvoicesService: GetSaleInvoicesPayable, + private writeoffInvoiceService: WriteoffSaleInvoice, + private getInvoicePaymentsService: GetInvoicePaymentsService, + private pdfSaleInvoiceService: SaleInvoicePdf, + // private invoiceSms: SaleInvoiceNotifyBySms, + // private sendInvoiceReminderService: SendInvoiceMailReminder, + // private sendSaleInvoiceMailService: SendSaleInvoiceMail, + // private getSaleInvoiceMailStateService: GetSaleInvoiceMailState, + private getSaleInvoiceStateService: GetSaleInvoiceState, + ) {} + + /** + * Creates a new sale invoice with associated GL entries. + * @param {ISaleInvoiceCreateDTO} saleInvoiceDTO + * @returns {Promise} + */ + public createSaleInvoice(saleInvoiceDTO: ISaleInvoiceCreateDTO) { + return this.createSaleInvoiceService.createSaleInvoice(saleInvoiceDTO); + } + + /** + * Edits the given sale invoice with associated GL entries. + * @param {ISaleInvoiceEditDTO} saleInvoiceDTO + * @returns {Promise} + */ + public editSaleInvoice( + saleInvoiceId: number, + saleInvoiceDTO: ISaleInvoiceEditDTO, + ) { + return this.editSaleInvoiceService.editSaleInvoice( + saleInvoiceId, + saleInvoiceDTO, + ); + } + + /** + * Deletes the given sale invoice with given associated GL entries. + * @param {number} tenantId + * @param {number} saleInvoiceId + * @param {ISystemUser} authorizedUser + * @returns {Promise} + */ + public deleteSaleInvoice(saleInvoiceId: number) { + return this.deleteSaleInvoiceService.deleteSaleInvoice(saleInvoiceId); + } + + /** + * Retrieves the given sale invoice details. + * @param {number} tenantId + * @param {ISalesInvoicesFilter} filterDTO + * @returns + */ + // public getSaleInvoices(filterDTO: ISalesInvoicesFilter) { + // return this.getSaleInvoicesService.getSaleInvoices(filterDTO); + // } + + /** + * Retrieves sale invoice details. + * @param {number} saleInvoiceId - + * @return {Promise} + */ + public getSaleInvoice(saleInvoiceId: number) { + return this.getSaleInvoiceService.getSaleInvoice(saleInvoiceId); + } + + /** + * Retrieves the sale invoice state. + * @param {number} tenantId + * @param {number} saleInvoiceId + * @returns + */ + public getSaleInvoiceState() { + return this.getSaleInvoiceStateService.getSaleInvoiceState(); + } + + /** + * Mark the given sale invoice as delivered. + * @param {number} tenantId + * @param {number} saleInvoiceId + * @param {ISystemUser} authorizedUser + * @returns {} + */ + public deliverSaleInvoice(saleInvoiceId: number) { + return this.deliverSaleInvoiceService.deliverSaleInvoice(saleInvoiceId); + } + + /** + * Retrieves the receivable sale invoices of the given customer. + * @param {number} tenantId + * @param {number} customerId + * @returns + */ + public getReceivableSaleInvoices(customerId?: number) { + return this.getReceivableSaleInvoicesService.getPayableInvoices(customerId); + } + + /** + * Writes-off the sale invoice on bad debt expense account. + * @param {number} saleInvoiceId - Sale invoice id. + * @param {ISaleInvoiceWriteoffDTO} writeoffDTO - Writeoff data. + * @return {Promise} + */ + public async writeOff( + saleInvoiceId: number, + writeoffDTO: ISaleInvoiceWriteoffDTO, + ) { + return this.writeoffInvoiceService.writeOff(saleInvoiceId, writeoffDTO); + } + + /** + * Cancels the written-off sale invoice. + * @param {number} saleInvoiceId - Sale invoice id. + * @returns {Promise} + */ + public cancelWrittenoff(saleInvoiceId: number) { + return this.writeoffInvoiceService.cancelWrittenoff(saleInvoiceId); + } + + /** + * Retrieve the invoice assocaited payments transactions. + * @param {number} invoiceId - Invoice id. + */ + public getInvoicePayments = async (invoiceId: number) => { + return this.getInvoicePaymentsService.getInvoicePayments(invoiceId); + }; + + /** + * Retrieves the pdf buffer of the given sale invoice. + * @param {number} tenantId - Tenant id. + * @param {number} saleInvoice + * @returns {Promise} + */ + public saleInvoicePdf(saleInvoiceId: number) { + return this.pdfSaleInvoiceService.getSaleInvoicePdf(saleInvoiceId); + } + + /** + * Retrieves the html content of the given sale invoice. + * @param {number} saleInvoiceId - Sale invoice id. + * @returns {Promise} + */ + public saleInvoiceHtml(saleInvoiceId: number): Promise { + return this.pdfSaleInvoiceService.getSaleInvoiceHtml(saleInvoiceId); + } + + /** + * + * @param {number} tenantId + * @param {number} saleInvoiceId + * @param {InvoiceNotificationType} invoiceNotificationType + */ + // public notifySaleInvoiceBySms = async ( + // tenantId: number, + // saleInvoiceId: number, + // invoiceNotificationType: InvoiceNotificationType, + // ) => { + // return this.invoiceSms.notifyBySms( + // tenantId, + // saleInvoiceId, + // invoiceNotificationType, + // ); + // }; + + /** + * Retrieves the SMS details of the given invoice. + * @param {number} tenantId - Tenant id. + * @param {number} saleInvoiceId - Sale invoice id. + */ + // public getSaleInvoiceSmsDetails = async ( + // tenantId: number, + // saleInvoiceId: number, + // invoiceSmsDetailsDTO: ISaleInvoiceSmsDetailsDTO, + // ): Promise => { + // return this.invoiceSms.smsDetails( + // tenantId, + // saleInvoiceId, + // invoiceSmsDetailsDTO, + // ); + // }; + + /** + * Retrieves the metadata of invoice mail reminder. + * @param {number} tenantId + * @param {number} saleInvoiceId + * @returns {} + */ + // public getSaleInvoiceMailReminder(tenantId: number, saleInvoiceId: number) { + // return this.sendInvoiceReminderService.getMailOption( + // tenantId, + // saleInvoiceId, + // ); + // } + + /** + * Sends reminder of the given invoice to the invoice's customer. + * @param {number} tenantId + * @param {number} saleInvoiceId + * @returns {} + */ + // public sendSaleInvoiceMailReminder( + // tenantId: number, + // saleInvoiceId: number, + // messageDTO: SendInvoiceMailDTO, + // ) { + // return this.sendInvoiceReminderService.triggerMail( + // tenantId, + // saleInvoiceId, + // messageDTO, + // ); + // } + + /** + * Sends the invoice mail of the given sale invoice. + * @param {number} tenantId + * @param {number} saleInvoiceId + * @param {SendInvoiceMailDTO} messageDTO + * @returns {Promise} + */ + // public sendSaleInvoiceMail( + // tenantId: number, + // saleInvoiceId: number, + // messageDTO: SendInvoiceMailDTO, + // ) { + // return this.sendSaleInvoiceMailService.triggerMail( + // tenantId, + // saleInvoiceId, + // messageDTO, + // ); + // } + + /** + * Retrieves the default mail options of the given sale invoice. + * @param {number} saleInvoiceid + * @returns {Promise} + */ + // public getSaleInvoiceMailState( + // saleInvoiceid: number, + // ): Promise { + // return this.getSaleInvoiceMailStateService.getInvoiceMailState( + // saleInvoiceid, + // ); + // } +} diff --git a/packages/server-nest/src/modules/SaleInvoices/SaleInvoices.controller.ts b/packages/server-nest/src/modules/SaleInvoices/SaleInvoices.controller.ts new file mode 100644 index 000000000..94de97af7 --- /dev/null +++ b/packages/server-nest/src/modules/SaleInvoices/SaleInvoices.controller.ts @@ -0,0 +1,147 @@ +import { + Body, + Controller, + Delete, + Get, + Param, + ParseIntPipe, + Post, + Put, + Query, +} from '@nestjs/common'; +import { + ISaleInvoiceCreateDTO, + ISaleInvoiceEditDTO, + ISaleInvoiceWriteoffDTO, + InvoiceNotificationType, +} from './SaleInvoice.types'; +import { SaleInvoiceApplication } from './SaleInvoices.application'; + +@Controller('sale-invoices') +export class SaleInvoicesController { + constructor(private saleInvoiceApplication: SaleInvoiceApplication) {} + + @Post() + createSaleInvoice(@Body() saleInvoiceDTO: ISaleInvoiceCreateDTO) { + return this.saleInvoiceApplication.createSaleInvoice(saleInvoiceDTO); + } + + @Put(':id') + editSaleInvoice( + @Param('id', ParseIntPipe) id: number, + @Body() saleInvoiceDTO: ISaleInvoiceEditDTO, + ) { + return this.saleInvoiceApplication.editSaleInvoice(id, saleInvoiceDTO); + } + + @Delete(':id') + deleteSaleInvoice(@Param('id', ParseIntPipe) id: number) { + return this.saleInvoiceApplication.deleteSaleInvoice(id); + } + + // @Get() + // getSaleInvoices(@Query() filterDTO: ISalesInvoicesFilter) { + // return this.saleInvoiceApplication.getSaleInvoices(filterDTO); + // } + + @Get(':id') + getSaleInvoice(@Param('id', ParseIntPipe) id: number) { + return this.saleInvoiceApplication.getSaleInvoice(id); + } + + @Get(':id/state') + getSaleInvoiceState() { + return this.saleInvoiceApplication.getSaleInvoiceState(); + } + + @Post(':id/deliver') + deliverSaleInvoice(@Param('id', ParseIntPipe) id: number) { + return this.saleInvoiceApplication.deliverSaleInvoice(id); + } + + @Get('receivable/:customerId?') + getReceivableSaleInvoices(@Param('customerId') customerId?: number) { + return this.saleInvoiceApplication.getReceivableSaleInvoices(customerId); + } + + @Post(':id/writeoff') + writeOff( + @Param('id', ParseIntPipe) id: number, + @Body() writeoffDTO: ISaleInvoiceWriteoffDTO, + ) { + return this.saleInvoiceApplication.writeOff(id, writeoffDTO); + } + + @Post(':id/cancel-writeoff') + cancelWrittenoff(@Param('id', ParseIntPipe) id: number) { + return this.saleInvoiceApplication.cancelWrittenoff(id); + } + + @Get(':id/payments') + getInvoicePayments(@Param('id', ParseIntPipe) id: number) { + return this.saleInvoiceApplication.getInvoicePayments(id); + } + + @Get(':id/pdf') + saleInvoicePdf(@Param('id', ParseIntPipe) id: number) { + return this.saleInvoiceApplication.saleInvoicePdf(id); + } + + @Get(':id/html') + saleInvoiceHtml(@Param('id', ParseIntPipe) id: number) { + return this.saleInvoiceApplication.saleInvoiceHtml(id); + } + + @Post(':id/notify-sms') + notifySaleInvoiceBySms( + @Param('id', ParseIntPipe) id: number, + @Body('type') notificationType: InvoiceNotificationType, + ) { + // return this.saleInvoiceApplication.notifySaleInvoiceBySms( + // id, + // notificationType, + // ); + } + + // @Post(':id/sms-details') + // getSaleInvoiceSmsDetails( + // @Param('id', ParseIntPipe) id: number, + // @Body() smsDetailsDTO: ISaleInvoiceSmsDetailsDTO, + // ) { + // // return this.saleInvoiceApplication.getSaleInvoiceSmsDetails( + // // id, + // // smsDetailsDTO, + // // ); + // } + + @Get(':id/mail-reminder') + getSaleInvoiceMailReminder(@Param('id', ParseIntPipe) id: number) { + // return this.saleInvoiceApplication.getSaleInvoiceMailReminder(tenantId, id); + } + + // @Post(':id/mail-reminder') + // sendSaleInvoiceMailReminder( + // @Param('id', ParseIntPipe) id: number, + // @Body() messageDTO: SendInvoiceMailDTO, + // ) { + // // return this.saleInvoiceApplication.sendSaleInvoiceMailReminder( + // // id, + // // messageDTO, + // // ); + // } + + // @Post(':id/mail') + // sendSaleInvoiceMail( + // @Param('id', ParseIntPipe) id: number, + // @Body() messageDTO: SendInvoiceMailDTO, + // ) { + // // return this.saleInvoiceApplication.sendSaleInvoiceMail(id, messageDTO); + // } + + // @Get(':id/mail-state') + // getSaleInvoiceMailState( + // @Param('id', ParseIntPipe) id: number, + // ): Promise { + // // return this.saleInvoiceApplication.getSaleInvoiceMailState(id); + // } +} diff --git a/packages/server-nest/src/modules/SaleInvoices/SaleInvoices.module.ts b/packages/server-nest/src/modules/SaleInvoices/SaleInvoices.module.ts new file mode 100644 index 000000000..90bbd13c9 --- /dev/null +++ b/packages/server-nest/src/modules/SaleInvoices/SaleInvoices.module.ts @@ -0,0 +1,42 @@ +import { Module } from '@nestjs/common'; +import { TenancyContext } from '../Tenancy/TenancyContext.service'; +import { TenancyDatabaseModule } from '../Tenancy/TenancyDB/TenancyDB.module'; +import { TransformerInjectable } from '../Transformer/TransformerInjectable.service'; +import { CreateSaleInvoice } from './commands/CreateSaleInvoice.service'; +import { DeleteSaleInvoice } from './commands/DeleteSaleInvoice.service'; +import { DeliverSaleInvoice } from './commands/DeliverSaleInvoice.service'; +import { EditSaleInvoice } from './commands/EditSaleInvoice.service'; +import { GenerateShareLink } from './commands/GenerateInvoicePaymentLink.service'; +import { SaleInvoiceIncrement } from './commands/SaleInvoiceIncrement.service'; +// import { SendSaleInvoiceMail } from './commands/SendSaleInvoiceMail'; +// import { SendSaleInvoiceReminderMailJob } from './commands/SendSaleInvoiceMailReminderJob'; +import { GetInvoicePaymentMail } from './queries/GetInvoicePaymentMail.service'; +import { GetSaleInvoice } from './queries/GetSaleInvoice.service'; +import { GetSaleInvoicesPayable } from './queries/GetSaleInvoicesPayable.service'; +import { GetSaleInvoiceState } from './queries/GetSaleInvoiceState.service'; +import { SaleInvoicePdf } from './queries/SaleInvoicePdf.service'; +import { SaleInvoiceApplication } from './SaleInvoices.application'; + +@Module({ + imports: [TenancyDatabaseModule], + controllers: [], + providers: [ + CreateSaleInvoice, + EditSaleInvoice, + DeleteSaleInvoice, + GetSaleInvoicesPayable, + DeliverSaleInvoice, + // SendSaleInvoiceMail, + GenerateShareLink, + GetInvoicePaymentMail, + SaleInvoiceIncrement, + GetSaleInvoiceState, + GetSaleInvoice, + GetInvoicePaymentMail, + SaleInvoicePdf, + SaleInvoiceApplication, + TenancyContext, + TransformerInjectable, + ], +}) +export class SaleInvoicesModule {} diff --git a/packages/server-nest/src/modules/SaleInvoices/SaleInvoicesExportable.ts b/packages/server-nest/src/modules/SaleInvoices/SaleInvoicesExportable.ts new file mode 100644 index 000000000..319c1d801 --- /dev/null +++ b/packages/server-nest/src/modules/SaleInvoices/SaleInvoicesExportable.ts @@ -0,0 +1,35 @@ +// import { Inject, Service } from 'typedi'; +// import { ISalesInvoicesFilter } from '@/interfaces'; +// import { SaleInvoiceApplication } from './SaleInvoices.application'; +// import { Exportable } from '@/services/Export/Exportable'; +// import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants'; + +// @Service() +// export class SaleInvoicesExportable extends Exportable { +// @Inject() +// private saleInvoicesApplication: SaleInvoiceApplication; + +// /** +// * Retrieves the accounts data to exportable sheet. +// * @param {number} tenantId +// * @returns +// */ +// public exportable(tenantId: number, query: ISalesInvoicesFilter) { +// const filterQuery = (query) => { +// query.withGraphFetched('branch'); +// query.withGraphFetched('warehouse'); +// }; +// const parsedQuery = { +// sortOrder: 'desc', +// columnSortBy: 'created_at', +// ...query, +// page: 1, +// pageSize: EXPORT_SIZE_LIMIT, +// filterQuery, +// } as ISalesInvoicesFilter; + +// return this.saleInvoicesApplication +// .getSaleInvoices(tenantId, parsedQuery) +// .then((output) => output.salesInvoices); +// } +// } diff --git a/packages/server-nest/src/modules/SaleInvoices/SaleInvoicesImportable.ts b/packages/server-nest/src/modules/SaleInvoices/SaleInvoicesImportable.ts new file mode 100644 index 000000000..f0b48423c --- /dev/null +++ b/packages/server-nest/src/modules/SaleInvoices/SaleInvoicesImportable.ts @@ -0,0 +1,46 @@ +// import { Inject, Service } from 'typedi'; +// import { Knex } from 'knex'; +// import { ISaleInvoiceCreateDTO } from '@/interfaces'; +// import { CreateSaleInvoice } from './commands/CreateSaleInvoice.service'; +// import { Importable } from '@/services/Import/Importable'; +// import { SaleInvoicesSampleData } from './constants'; + +// @Service() +// export class SaleInvoicesImportable extends Importable { +// @Inject() +// private createInvoiceService: CreateSaleInvoice; + +// /** +// * Importing to account service. +// * @param {number} tenantId +// * @param {IAccountCreateDTO} createAccountDTO +// * @returns +// */ +// public importable( +// tenantId: number, +// createAccountDTO: ISaleInvoiceCreateDTO, +// trx?: Knex.Transaction +// ) { +// return this.createInvoiceService.createSaleInvoice( +// tenantId, +// createAccountDTO, +// {}, +// trx +// ); +// } + +// /** +// * Concurrrency controlling of the importing process. +// * @returns {number} +// */ +// public get concurrency() { +// return 1; +// } + +// /** +// * Retrieves the sample data that used to download accounts sample sheet. +// */ +// public sampleData(): any[] { +// return SaleInvoicesSampleData; +// } +// } diff --git a/packages/server-nest/src/modules/SaleInvoices/SalesInvoicesCost.ts b/packages/server-nest/src/modules/SaleInvoices/SalesInvoicesCost.ts new file mode 100644 index 000000000..a36c88e08 --- /dev/null +++ b/packages/server-nest/src/modules/SaleInvoices/SalesInvoicesCost.ts @@ -0,0 +1,161 @@ +// import { Mutex } from 'async-mutex'; +// import { Container, Service, Inject } from 'typedi'; +// import { chain } from 'lodash'; +// import moment from 'moment'; +// import { Knex } from 'knex'; +// import InventoryService from '@/services/Inventory/Inventory'; +// import { +// IInventoryCostLotsGLEntriesWriteEvent, +// IInventoryTransaction, +// } from '@/interfaces'; +// import UnitOfWork from '@/services/UnitOfWork'; +// import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; +// import events from '@/subscribers/events'; + +// @Service() +// export class SaleInvoicesCost { +// @Inject() +// private inventoryService: InventoryService; + +// @Inject() +// private uow: UnitOfWork; + +// @Inject() +// private eventPublisher: EventPublisher; + +// /** +// * Schedule sale invoice re-compute based on the item +// * cost method and starting date. +// * @param {number[]} itemIds - Inventory items ids. +// * @param {Date} startingDate - Starting compute cost date. +// * @return {Promise} +// */ +// async scheduleComputeCostByItemsIds( +// tenantId: number, +// inventoryItemsIds: number[], +// startingDate: Date +// ): Promise { +// const mutex = new Mutex(); + +// const asyncOpers = inventoryItemsIds.map( +// async (inventoryItemId: number) => { +// // @todo refactor the lock acquire to be distrbuted using Redis +// // and run the cost schedule job after running invoice transaction. +// const release = await mutex.acquire(); + +// try { +// await this.inventoryService.scheduleComputeItemCost( +// tenantId, +// inventoryItemId, +// startingDate +// ); +// } finally { +// release(); +// } +// } +// ); +// await Promise.all(asyncOpers); +// } + +// /** +// * Retrieve the max dated inventory transactions in the transactions that +// * have the same item id. +// * @param {IInventoryTransaction[]} inventoryTransactions +// * @return {IInventoryTransaction[]} +// */ +// getMaxDateInventoryTransactions( +// inventoryTransactions: IInventoryTransaction[] +// ): IInventoryTransaction[] { +// return chain(inventoryTransactions) +// .reduce((acc: any, transaction) => { +// const compatatorDate = acc[transaction.itemId]; + +// if ( +// !compatatorDate || +// moment(compatatorDate.date).isBefore(transaction.date) +// ) { +// return { +// ...acc, +// [transaction.itemId]: { +// ...transaction, +// }, +// }; +// } +// return acc; +// }, {}) +// .values() +// .value(); +// } + +// /** +// * Computes items costs by the given inventory transaction. +// * @param {number} tenantId +// * @param {IInventoryTransaction[]} inventoryTransactions +// */ +// async computeItemsCostByInventoryTransactions( +// tenantId: number, +// inventoryTransactions: IInventoryTransaction[] +// ) { +// const mutex = new Mutex(); +// const reducedTransactions = this.getMaxDateInventoryTransactions( +// inventoryTransactions +// ); +// const asyncOpers = reducedTransactions.map(async (transaction) => { +// const release = await mutex.acquire(); + +// try { +// await this.inventoryService.scheduleComputeItemCost( +// tenantId, +// transaction.itemId, +// transaction.date +// ); +// } finally { +// release(); +// } +// }); +// await Promise.all([...asyncOpers]); +// } + +// /** +// * Schedule writing journal entries. +// * @param {Date} startingDate +// * @return {Promise} +// */ +// scheduleWriteJournalEntries(tenantId: number, startingDate?: Date) { +// const agenda = Container.get('agenda'); + +// return agenda.schedule('in 3 seconds', 'rewrite-invoices-journal-entries', { +// startingDate, +// tenantId, +// }); +// } + +// /** +// * Writes cost GL entries from the inventory cost lots. +// * @param {number} tenantId - +// * @param {Date} startingDate - +// * @returns {Promise} +// */ +// public writeCostLotsGLEntries = (tenantId: number, startingDate: Date) => { +// return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { +// // Triggers event `onInventoryCostLotsGLEntriesBeforeWrite`. +// await this.eventPublisher.emitAsync( +// events.inventory.onCostLotsGLEntriesBeforeWrite, +// { +// tenantId, +// startingDate, +// trx, +// } as IInventoryCostLotsGLEntriesWriteEvent +// ); +// // Triggers event `onInventoryCostLotsGLEntriesWrite`. +// await this.eventPublisher.emitAsync( +// events.inventory.onCostLotsGLEntriesWrite, +// { +// tenantId, +// startingDate, +// trx, +// } as IInventoryCostLotsGLEntriesWriteEvent +// ); +// }); +// }; +// } diff --git a/packages/server-nest/src/modules/SaleInvoices/commands/CommandSaleInvoiceDTOTransformer.service.ts b/packages/server-nest/src/modules/SaleInvoices/commands/CommandSaleInvoiceDTOTransformer.service.ts new file mode 100644 index 000000000..637c5da67 --- /dev/null +++ b/packages/server-nest/src/modules/SaleInvoices/commands/CommandSaleInvoiceDTOTransformer.service.ts @@ -0,0 +1,151 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { omit, sumBy } from 'lodash'; +import * as R from 'ramda'; +import moment from 'moment'; +import composeAsync from 'async/compose'; +import { + ISaleInvoiceCreateDTO, + ISaleInvoiceEditDTO, +} from '../SaleInvoice.types'; +import { Customer } from '@/modules/Customers/models/Customer'; +import { BranchTransactionDTOTransformer } from '@/modules/Branches/integrations/BranchTransactionDTOTransform'; +import { WarehouseTransactionDTOTransform } from '@/modules/Warehouses/Integrations/WarehouseTransactionDTOTransform'; +import { ItemsEntriesService } from '@/modules/Items/ItemsEntries.service'; +import { ItemEntry } from '@/modules/Items/models/ItemEntry'; +import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators.service'; +import { SaleInvoiceIncrement } from './SaleInvoiceIncrement.service'; +import { BrandingTemplateDTOTransformer } from '@/modules/PdfTemplate/BrandingTemplateDTOTransformer'; +import { SaleInvoice } from '../models/SaleInvoice'; +import { assocItemEntriesDefaultIndex } from '@/utils/associate-item-entries-index'; +import { formatDateFields } from '@/utils/format-date-fields'; +import { ItemEntriesTaxTransactions } from '@/modules/TaxRates/ItemEntriesTaxTransactions.service'; +import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service'; + +@Injectable() +export class CommandSaleInvoiceDTOTransformer { + constructor( + private branchDTOTransform: BranchTransactionDTOTransformer, + private warehouseDTOTransform: WarehouseTransactionDTOTransform, + private itemsEntriesService: ItemsEntriesService, + private validators: CommandSaleInvoiceValidators, + private invoiceIncrement: SaleInvoiceIncrement, + private taxDTOTransformer: ItemEntriesTaxTransactions, + private brandingTemplatesTransformer: BrandingTemplateDTOTransformer, + private tenancyContext: TenancyContext, + + @Inject(SaleInvoice.name) private saleInvoiceModel: typeof SaleInvoice, + ) {} + + /** + * Transformes the create DTO to invoice object model. + * @param {ISaleInvoiceCreateDTO} saleInvoiceDTO - Sale invoice DTO. + * @param {ISaleInvoice} oldSaleInvoice - Old sale invoice. + * @return {ISaleInvoice} + */ + public async transformDTOToModel( + customer: Customer, + saleInvoiceDTO: ISaleInvoiceCreateDTO | ISaleInvoiceEditDTO, + oldSaleInvoice?: SaleInvoice, + ): Promise { + const entriesModels = this.transformDTOEntriesToModels(saleInvoiceDTO); + const amount = this.getDueBalanceItemEntries(entriesModels); + + // Retreive the next invoice number. + const autoNextNumber = this.invoiceIncrement.getNextInvoiceNumber(); + + // Retrieve the authorized user. + const authorizedUser = await this.tenancyContext.getSystemUser(); + + // Invoice number. + const invoiceNo = + saleInvoiceDTO.invoiceNo || oldSaleInvoice?.invoiceNo || autoNextNumber; + + // Validate the invoice is required. + this.validators.validateInvoiceNoRequire(invoiceNo); + + const initialEntries = saleInvoiceDTO.entries.map((entry) => ({ + referenceType: 'SaleInvoice', + isInclusiveTax: saleInvoiceDTO.isInclusiveTax, + ...entry, + })); + const asyncEntries = await composeAsync( + // Associate tax rate from tax id to entries. + this.taxDTOTransformer.assocTaxRateFromTaxIdToEntries, + // Associate tax rate id from tax code to entries. + this.taxDTOTransformer.assocTaxRateIdFromCodeToEntries, + // Sets default cost and sell account to invoice items entries. + this.itemsEntriesService.setItemsEntriesDefaultAccounts, + )(initialEntries); + + const entries = R.compose( + // Remove tax code from entries. + R.map(R.omit(['taxCode'])), + + // Associate the default index for each item entry lin. + assocItemEntriesDefaultIndex, + )(asyncEntries); + + const initialDTO = { + ...formatDateFields( + omit(saleInvoiceDTO, [ + 'delivered', + 'entries', + 'fromEstimateId', + 'attachments', + ]), + ['invoiceDate', 'dueDate'], + ), + // Avoid rewrite the deliver date in edit mode when already published. + balance: amount, + currencyCode: customer.currencyCode, + exchangeRate: saleInvoiceDTO.exchangeRate || 1, + ...(saleInvoiceDTO.delivered && + !oldSaleInvoice?.deliveredAt && { + deliveredAt: moment().toMySqlDateTime(), + }), + // Avoid override payment amount in edit mode. + ...(!oldSaleInvoice && { paymentAmount: 0 }), + ...(invoiceNo ? { invoiceNo } : {}), + entries, + userId: authorizedUser.id, + } as SaleInvoice; + + const initialAsyncDTO = await composeAsync( + // Assigns the default branding template id to the invoice DTO. + this.brandingTemplatesTransformer.assocDefaultBrandingTemplate( + 'SaleInvoice', + ), + )(initialDTO); + + return R.compose( + this.taxDTOTransformer.assocTaxAmountWithheldFromEntries, + this.branchDTOTransform.transformDTO, + this.warehouseDTOTransform.transformDTO, + )(initialAsyncDTO); + } + + /** + * Transforms the DTO entries to invoice entries models. + * @param {ISaleInvoiceCreateDTO | ISaleInvoiceEditDTO} entries + * @returns {IItemEntry[]} + */ + private transformDTOEntriesToModels = ( + saleInvoiceDTO: ISaleInvoiceCreateDTO | ISaleInvoiceEditDTO, + ): ItemEntry[] => { + return saleInvoiceDTO.entries.map((entry) => { + return ItemEntry.fromJson({ + ...entry, + isInclusiveTax: saleInvoiceDTO.isInclusiveTax, + }); + }); + }; + + /** + * Gets the due balance from the invoice entries. + * @param {IItemEntry[]} entries + * @returns {number} + */ + private getDueBalanceItemEntries = (entries: ItemEntry[]) => { + return sumBy(entries, (e) => e.amount); + }; +} diff --git a/packages/server-nest/src/modules/SaleInvoices/commands/CommandSaleInvoiceValidators.service.ts b/packages/server-nest/src/modules/SaleInvoices/commands/CommandSaleInvoiceValidators.service.ts new file mode 100644 index 000000000..8ff18a951 --- /dev/null +++ b/packages/server-nest/src/modules/SaleInvoices/commands/CommandSaleInvoiceValidators.service.ts @@ -0,0 +1,79 @@ +import { Injectable } from '@nestjs/common'; +import { SaleInvoice } from '../models/SaleInvoice'; +import { ServiceError } from '@/modules/Items/ServiceError'; +import { ERRORS } from '../constants'; + +@Injectable() +export class CommandSaleInvoiceValidators { + constructor(private readonly saleInvoiceModel: typeof SaleInvoice) {} + + /** + * Validates the given invoice is existance. + * @param {SaleInvoice | undefined} invoice + */ + public validateInvoiceExistance(invoice: SaleInvoice | undefined) { + if (!invoice) { + throw new ServiceError(ERRORS.SALE_INVOICE_NOT_FOUND); + } + } + + /** + * Validate whether sale invoice number unqiue on the storage. + * @param {string} invoiceNumber - + * @param {number} notInvoiceId - + */ + public async validateInvoiceNumberUnique( + invoiceNumber: string, + notInvoiceId?: number, + ) { + const saleInvoice = await this.saleInvoiceModel + .query() + .findOne('invoice_no', invoiceNumber) + .onBuild((builder) => { + if (notInvoiceId) { + builder.whereNot('id', notInvoiceId); + } + }); + if (saleInvoice) { + throw new ServiceError(ERRORS.INVOICE_NUMBER_NOT_UNIQUE); + } + } + + /** + * Validate the invoice amount is bigger than payment amount before edit the invoice. + * @param {number} saleInvoiceAmount + * @param {number} paymentAmount + */ + public validateInvoiceAmountBiggerPaymentAmount( + saleInvoiceAmount: number, + paymentAmount: number, + ) { + if (saleInvoiceAmount < paymentAmount) { + throw new ServiceError(ERRORS.INVOICE_AMOUNT_SMALLER_THAN_PAYMENT_AMOUNT); + } + } + + /** + * Validate the invoice number require. + * @param {ISaleInvoice} saleInvoiceObj + */ + public validateInvoiceNoRequire(invoiceNo: string) { + if (!invoiceNo) { + throw new ServiceError(ERRORS.SALE_INVOICE_NO_IS_REQUIRED); + } + } + + /** + * Validate the given customer has no sales invoices. + * @param {number} customerId - Customer id. + */ + public async validateCustomerHasNoInvoices(customerId: number) { + const invoices = await this.saleInvoiceModel + .query() + .where('customer_id', customerId); + + if (invoices.length > 0) { + throw new ServiceError(ERRORS.CUSTOMER_HAS_SALES_INVOICES); + } + } +} diff --git a/packages/server-nest/src/modules/SaleInvoices/commands/CreateSaleInvoice.service.ts b/packages/server-nest/src/modules/SaleInvoices/commands/CreateSaleInvoice.service.ts new file mode 100644 index 000000000..1647ff5a6 --- /dev/null +++ b/packages/server-nest/src/modules/SaleInvoices/commands/CreateSaleInvoice.service.ts @@ -0,0 +1,133 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { Knex } from 'knex'; +import { + ISaleInvoiceCreateDTO, + ISaleInvoiceCreatedPayload, + ISaleInvoiceCreatingPaylaod, +} from '../SaleInvoice.types'; +import { ItemsEntriesService } from '@/modules/Items/ItemsEntries.service'; +import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators.service'; +import { CommandSaleInvoiceDTOTransformer } from './CommandSaleInvoiceDTOTransformer.service'; +import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service'; +import { SaleEstimateValidators } from '@/modules/SaleEstimates/commands/SaleEstimateValidators.service'; +import { SaleInvoice } from '../models/SaleInvoice'; +import { SaleEstimate } from '@/modules/SaleEstimates/models/SaleEstimate'; +import { Customer } from '@/modules/Customers/models/Customer'; +import { events } from '@/common/events/events'; + +@Injectable() +export class CreateSaleInvoice { + constructor( + private readonly itemsEntriesService: ItemsEntriesService, + private readonly validators: CommandSaleInvoiceValidators, + private readonly transformerDTO: CommandSaleInvoiceDTOTransformer, + private readonly eventPublisher: EventEmitter2, + private readonly uow: UnitOfWork, + private readonly commandEstimateValidators: SaleEstimateValidators, + + @Inject(SaleInvoice.name) + private readonly saleInvoiceModel: typeof SaleInvoice, + + @Inject(SaleEstimate.name) + private readonly saleEstimateModel: typeof SaleEstimate, + + @Inject(Customer.name) + private readonly customerModel: typeof Customer, + ) {} + + /** + * Creates a new sale invoices and store it to the storage + * with associated to entries and journal transactions. + * @async + * @param {number} tenantId - Tenant id. + * @param {ISaleInvoice} saleInvoiceDTO - Sale invoice object DTO. + * @return {Promise} + */ + public createSaleInvoice = async ( + saleInvoiceDTO: ISaleInvoiceCreateDTO, + // authorizedUser: ITenantUser, + trx?: Knex.Transaction, + ): Promise => { + // Validate customer existance. + const customer = await this.customerModel + .query() + .findById(saleInvoiceDTO.customerId) + .throwIfNotFound(); + + // Validate the from estimate id exists on the storage. + if (saleInvoiceDTO.fromEstimateId) { + const fromEstimate = await this.saleEstimateModel + .query() + .findById(saleInvoiceDTO.fromEstimateId) + .throwIfNotFound(); + + // Validate the sale estimate is not already converted to invoice. + this.commandEstimateValidators.validateEstimateNotConverted(fromEstimate); + } + // Validate items ids existance. + await this.itemsEntriesService.validateItemsIdsExistance( + saleInvoiceDTO.entries, + ); + // Validate items should be sellable items. + await this.itemsEntriesService.validateNonSellableEntriesItems( + saleInvoiceDTO.entries, + ); + // Transform DTO object to model object. + const saleInvoiceObj = await this.transformCreateDTOToModel( + customer, + saleInvoiceDTO, + // authorizedUser, + ); + // Validate sale invoice number uniquiness. + if (saleInvoiceObj.invoiceNo) { + await this.validators.validateInvoiceNumberUnique( + saleInvoiceObj.invoiceNo, + ); + } + // Creates a new sale invoice and associated transactions under unit of work env. + return this.uow.withTransaction(async (trx: Knex.Transaction) => { + // Triggers `onSaleInvoiceCreating` event. + await this.eventPublisher.emitAsync(events.saleInvoice.onCreating, { + saleInvoiceDTO, + trx, + } as ISaleInvoiceCreatingPaylaod); + + // Create sale invoice graph to the storage. + const saleInvoice = await this.saleInvoiceModel + .query(trx) + .upsertGraph(saleInvoiceObj); + + const eventPayload: ISaleInvoiceCreatedPayload = { + saleInvoice, + saleInvoiceDTO, + saleInvoiceId: saleInvoice.id, + trx, + }; + // Triggers the event `onSaleInvoiceCreated`. + await this.eventPublisher.emitAsync( + events.saleInvoice.onCreated, + eventPayload, + ); + return saleInvoice; + }, trx); + }; + + /** + * Transformes create DTO to model. + * @param {number} tenantId - + * @param {ICustomer} customer - + * @param {ISaleInvoiceCreateDTO} saleInvoiceDTO - + */ + private transformCreateDTOToModel = async ( + customer: Customer, + saleInvoiceDTO: ISaleInvoiceCreateDTO, + // authorizedUser: SystemUser, + ) => { + return this.transformerDTO.transformDTOToModel( + customer, + saleInvoiceDTO, + // authorizedUser, + ); + }; +} diff --git a/packages/server-nest/src/modules/SaleInvoices/commands/DeleteSaleInvoice.service.ts b/packages/server-nest/src/modules/SaleInvoices/commands/DeleteSaleInvoice.service.ts new file mode 100644 index 000000000..1c676264c --- /dev/null +++ b/packages/server-nest/src/modules/SaleInvoices/commands/DeleteSaleInvoice.service.ts @@ -0,0 +1,125 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Knex } from 'knex'; +import { + ISaleInvoiceDeletePayload, + ISaleInvoiceDeletedPayload, + ISaleInvoiceDeletingPayload, +} from '../SaleInvoice.types'; +import { ItemEntry } from '@/modules/Items/models/ItemEntry'; +import { SaleInvoice } from '../models/SaleInvoice'; +import { UnlinkConvertedSaleEstimate } from '@/modules/SaleEstimates/commands/UnlinkConvertedSaleEstimate.service'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service'; +import { ServiceError } from '@/modules/Items/ServiceError'; +import { ERRORS } from '../constants'; +import { events } from '@/common/events/events'; +import { PaymentReceivedEntry } from '@/modules/PaymentReceived/models/PaymentReceivedEntry'; +import CreditNoteAppliedInvoice from '@/modules/CreditNotes/models/CreditNoteAppliedInvoice'; + +@Injectable() +export class DeleteSaleInvoice { + constructor( + @Inject(PaymentReceivedEntry) + private paymentReceivedEntryModel: typeof PaymentReceivedEntry, + + @Inject(CreditNoteAppliedInvoice) + private creditNoteAppliedInvoiceModel: typeof CreditNoteAppliedInvoice, + + @Inject(SaleInvoice) + private saleInvoiceModel: typeof SaleInvoice, + private unlockEstimateFromInvoice: UnlinkConvertedSaleEstimate, + private eventPublisher: EventEmitter2, + private uow: UnitOfWork, + ) {} + + /** + * Validate the sale invoice has no payment entries. + * @param {number} saleInvoiceId + */ + private async validateInvoiceHasNoPaymentEntries(saleInvoiceId: number) { + // Retrieve the sale invoice associated payment receive entries. + const entries = await this.paymentReceivedEntryModel + .query() + .where('invoice_id', saleInvoiceId); + + if (entries.length > 0) { + throw new ServiceError(ERRORS.INVOICE_HAS_ASSOCIATED_PAYMENT_ENTRIES); + } + return entries; + } + + /** + * Validate the sale invoice has no applied to credit note transaction. + * @param {number} invoiceId - Invoice id. + * @returns {Promise} + */ + public validateInvoiceHasNoAppliedToCredit = async ( + invoiceId: number, + ): Promise => { + const appliedTransactions = await this.creditNoteAppliedInvoiceModel + .query() + .where('invoiceId', invoiceId); + + if (appliedTransactions.length > 0) { + throw new ServiceError(ERRORS.SALE_INVOICE_HAS_APPLIED_TO_CREDIT_NOTES); + } + }; + + /** + * Deletes the given sale invoice with associated entries + * and journal transactions. + * @param {Number} saleInvoiceId - The given sale invoice id. + * @param {ISystemUser} authorizedUser - + */ + public async deleteSaleInvoice(saleInvoiceId: number): Promise { + // Retrieve the given sale invoice with associated entries + // or throw not found error. + const oldSaleInvoice = await this.saleInvoiceModel + .query() + .findById(saleInvoiceId) + .withGraphFetched('entries') + .withGraphFetched('paymentMethods') + .throwIfNotFound(); + + // Validate the sale invoice has no associated payment entries. + await this.validateInvoiceHasNoPaymentEntries(saleInvoiceId); + + // Validate the sale invoice has applied to credit note transaction. + await this.validateInvoiceHasNoAppliedToCredit(saleInvoiceId); + + // Triggers `onSaleInvoiceDelete` event. + await this.eventPublisher.emitAsync(events.saleInvoice.onDelete, { + oldSaleInvoice, + saleInvoiceId, + } as ISaleInvoiceDeletePayload); + + // Deletes sale invoice transaction and associate transactions with UOW env. + return this.uow.withTransaction(async (trx: Knex.Transaction) => { + // Triggers `onSaleInvoiceDeleting` event. + await this.eventPublisher.emitAsync(events.saleInvoice.onDeleting, { + oldSaleInvoice, + saleInvoiceId, + trx, + } as ISaleInvoiceDeletingPayload); + + // Unlink the converted sale estimates from the given sale invoice. + await this.unlockEstimateFromInvoice.unlinkConvertedEstimateFromInvoice( + saleInvoiceId, + trx, + ); + await ItemEntry.query(trx) + .where('reference_id', saleInvoiceId) + .where('reference_type', 'SaleInvoice') + .delete(); + + await SaleInvoice.query(trx).findById(saleInvoiceId).delete(); + + // Triggers `onSaleInvoiceDeleted` event. + await this.eventPublisher.emitAsync(events.saleInvoice.onDeleted, { + oldSaleInvoice, + saleInvoiceId, + trx, + } as ISaleInvoiceDeletedPayload); + }); + } +} diff --git a/packages/server-nest/src/modules/SaleInvoices/commands/DeliverSaleInvoice.service.ts b/packages/server-nest/src/modules/SaleInvoices/commands/DeliverSaleInvoice.service.ts new file mode 100644 index 000000000..f2240b35a --- /dev/null +++ b/packages/server-nest/src/modules/SaleInvoices/commands/DeliverSaleInvoice.service.ts @@ -0,0 +1,69 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Knex } from 'knex'; +import moment from 'moment'; +import { + ISaleInvoiceDeliveringPayload, + ISaleInvoiceEventDeliveredPayload, +} from '../SaleInvoice.types'; +import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators.service'; +import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { events } from '@/common/events/events'; +import { ERRORS } from '../constants'; +import { SaleInvoice } from '../models/SaleInvoice'; +import { ServiceError } from '@/modules/Items/ServiceError'; + +@Injectable() +export class DeliverSaleInvoice { + constructor( + private eventEmitter: EventEmitter2, + private uow: UnitOfWork, + private validators: CommandSaleInvoiceValidators, + + @Inject(SaleInvoice.name) private saleInvoiceModel: typeof SaleInvoice, + ) {} + + /** + * Deliver the given sale invoice. + * @param {number} saleInvoiceId - Sale invoice id. + * @return {Promise} + */ + public async deliverSaleInvoice(saleInvoiceId: number): Promise { + // Retrieve details of the given sale invoice id. + const oldSaleInvoice = await this.saleInvoiceModel + .query() + .findById(saleInvoiceId); + + // Validates the given invoice existence. + this.validators.validateInvoiceExistance(oldSaleInvoice); + + // Throws error in case the sale invoice already published. + if (oldSaleInvoice.isDelivered) { + throw new ServiceError(ERRORS.SALE_INVOICE_ALREADY_DELIVERED); + } + // Update sale invoice transaction with associate transactions + // under unit-of-work environment. + return this.uow.withTransaction(async (trx: Knex.Transaction) => { + // Triggers `onSaleInvoiceDelivering` event. + await this.eventEmitter.emitAsync(events.saleInvoice.onDelivering, { + oldSaleInvoice, + trx, + } as ISaleInvoiceDeliveringPayload); + + // Record the delivered at on the storage. + const saleInvoice = await this.saleInvoiceModel + .query(trx) + .patchAndFetchById(saleInvoiceId, { + deliveredAt: moment().toMySqlDateTime(), + }) + .withGraphFetched('entries'); + + // Triggers `onSaleInvoiceDelivered` event. + await this.eventEmitter.emitAsync(events.saleInvoice.onDelivered, { + saleInvoiceId, + saleInvoice, + trx, + } as ISaleInvoiceEventDeliveredPayload); + }); + } +} diff --git a/packages/server-nest/src/modules/SaleInvoices/commands/EditSaleInvoice.service.ts b/packages/server-nest/src/modules/SaleInvoices/commands/EditSaleInvoice.service.ts new file mode 100644 index 000000000..ea05a6a86 --- /dev/null +++ b/packages/server-nest/src/modules/SaleInvoices/commands/EditSaleInvoice.service.ts @@ -0,0 +1,142 @@ +import { Knex } from 'knex'; +import { Inject, Injectable } from '@nestjs/common'; +import { + ISaleInvoiceEditDTO, + ISaleInvoiceEditedPayload, + ISaleInvoiceEditingPayload, +} from '../SaleInvoice.types'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators.service'; +import { CommandSaleInvoiceDTOTransformer } from './CommandSaleInvoiceDTOTransformer.service'; +import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service'; +import { ItemsEntriesService } from '@/modules/Items/ItemsEntries.service'; +import { events } from '@/common/events/events'; +import { SaleInvoice } from '../models/SaleInvoice'; +import { Customer } from '@/modules/Customers/models/Customer'; + +@Injectable() +export class EditSaleInvoice { + constructor( + private readonly itemsEntriesService: ItemsEntriesService, + private readonly eventPublisher: EventEmitter2, + private readonly validators: CommandSaleInvoiceValidators, + private readonly transformerDTO: CommandSaleInvoiceDTOTransformer, + private readonly uow: UnitOfWork, + + @Inject(SaleInvoice.name) + private readonly saleInvoiceModel: typeof SaleInvoice, + + @Inject(Customer.name) private readonly customerModel: typeof Customer, + ) {} + + /** + * Edit the given sale invoice. + * @async + * @param {Number} saleInvoiceId - Sale invoice id. + * @param {ISaleInvoice} saleInvoice - Sale invoice DTO object. + * @return {Promise} + */ + public async editSaleInvoice( + saleInvoiceId: number, + saleInvoiceDTO: ISaleInvoiceEditDTO, + ): Promise { + // Retrieve the sale invoice or throw not found service error. + const oldSaleInvoice = await this.saleInvoiceModel + .query() + .findById(saleInvoiceId) + .withGraphJoined('entries'); + + // Validates the given invoice existance. + this.validators.validateInvoiceExistance(oldSaleInvoice); + + // Validate customer existance. + const customer = await this.customerModel + .query() + .findById(saleInvoiceDTO.customerId) + .throwIfNotFound(); + + // Validate items ids existance. + await this.itemsEntriesService.validateItemsIdsExistance( + saleInvoiceDTO.entries, + ); + // Validate non-sellable entries items. + await this.itemsEntriesService.validateNonSellableEntriesItems( + saleInvoiceDTO.entries, + ); + // Validate the items entries existance. + await this.itemsEntriesService.validateEntriesIdsExistance( + saleInvoiceId, + 'SaleInvoice', + saleInvoiceDTO.entries, + ); + // Transform DTO object to model object. + const saleInvoiceObj = await this.tranformEditDTOToModel( + customer, + saleInvoiceDTO, + oldSaleInvoice, + ); + // Validate sale invoice number uniquiness. + if (saleInvoiceObj.invoiceNo) { + await this.validators.validateInvoiceNumberUnique( + saleInvoiceObj.invoiceNo, + saleInvoiceId, + ); + } + // Validate the invoice amount is not smaller than the invoice payment amount. + this.validators.validateInvoiceAmountBiggerPaymentAmount( + saleInvoiceObj.balance, + oldSaleInvoice.paymentAmount, + ); + // Edit sale invoice transaction in UOW envirment. + return this.uow.withTransaction(async (trx: Knex.Transaction) => { + // Triggers `onSaleInvoiceEditing` event. + await this.eventPublisher.emitAsync(events.saleInvoice.onEditing, { + trx, + oldSaleInvoice, + saleInvoiceDTO, + } as ISaleInvoiceEditingPayload); + + // Upsert the the invoice graph to the storage. + const saleInvoice = await this.saleInvoiceModel + .query() + .upsertGraphAndFetch({ + id: saleInvoiceId, + ...saleInvoiceObj, + }); + // Edit event payload. + const editEventPayload: ISaleInvoiceEditedPayload = { + saleInvoiceId, + saleInvoice, + saleInvoiceDTO, + oldSaleInvoice, + trx, + }; + // Triggers `onSaleInvoiceEdited` event. + await this.eventPublisher.emitAsync( + events.saleInvoice.onEdited, + editEventPayload, + ); + return saleInvoice; + }); + } + + /** + * Transformes edit DTO to model. + * @param {ICustomer} customer - + * @param {ISaleInvoiceEditDTO} saleInvoiceDTO - + * @param {ISaleInvoice} oldSaleInvoice + */ + private tranformEditDTOToModel = async ( + customer: Customer, + saleInvoiceDTO: ISaleInvoiceEditDTO, + oldSaleInvoice: SaleInvoice, + // authorizedUser: ITenantUser, + ) => { + return this.transformerDTO.transformDTOToModel( + customer, + saleInvoiceDTO, + oldSaleInvoice, + // authorizedUser, + ); + }; +} diff --git a/packages/server-nest/src/modules/SaleInvoices/commands/GenerateInvoicePaymentLink.service.ts b/packages/server-nest/src/modules/SaleInvoices/commands/GenerateInvoicePaymentLink.service.ts new file mode 100644 index 000000000..d5558499e --- /dev/null +++ b/packages/server-nest/src/modules/SaleInvoices/commands/GenerateInvoicePaymentLink.service.ts @@ -0,0 +1,73 @@ +import { Knex } from 'knex'; +import { Inject, Injectable } from '@nestjs/common'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { v4 as uuidv4 } from 'uuid'; +import { GeneratePaymentLinkTransformer } from './GeneratePaymentLink.transformer'; +import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service'; +import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service'; +import { events } from '@/common/events/events'; +import { PaymentLink } from '@/modules/PaymentLinks/models/PaymentLink'; +import { SaleInvoice } from '../models/SaleInvoice'; + + +@Injectable() +export class GenerateShareLink { + constructor( + private uow: UnitOfWork, + private eventPublisher: EventEmitter2, + private transformer: TransformerInjectable, + @Inject(SaleInvoice) private saleInvoiceModel: typeof SaleInvoice, + @Inject(PaymentLink) private paymentLinkModel: typeof PaymentLink, + ) {} + + /** + * Generates private or public payment link for the given sale invoice. + * @param {number} saleInvoiceId - Sale invoice id. + * @param {string} publicity - Public or private. + * @param {string} expiryTime - Expiry time. + */ + async generatePaymentLink( + saleInvoiceId: number, + publicity: string = 'private', + expiryTime: string = '' + ) { + const foundInvoice = await this.saleInvoiceModel.query() + .findById(saleInvoiceId) + .throwIfNotFound(); + + // Generate unique uuid for sharable link. + const linkId = uuidv4() as string; + + const commonEventPayload = { + saleInvoiceId, + publicity, + expiryTime, + }; + return this.uow.withTransaction(async (trx: Knex.Transaction) => { + // Triggers `onPublicSharableLinkGenerating` event. + await this.eventPublisher.emitAsync( + events.saleInvoice.onPublicLinkGenerating, + { ...commonEventPayload, trx } + ); + const paymentLink = await this.paymentLinkModel.query().insert({ + linkId, + publicity, + resourceId: foundInvoice.id, + resourceType: 'SaleInvoice', + }); + // Triggers `onPublicSharableLinkGenerated` event. + await this.eventPublisher.emitAsync( + events.saleInvoice.onPublicLinkGenerated, + { + ...commonEventPayload, + paymentLink, + trx, + } + ); + return this.transformer.transform( + paymentLink, + new GeneratePaymentLinkTransformer() + ); + }); + } +} diff --git a/packages/server-nest/src/modules/SaleInvoices/commands/GeneratePaymentLink.transformer.ts b/packages/server-nest/src/modules/SaleInvoices/commands/GeneratePaymentLink.transformer.ts new file mode 100644 index 000000000..66aaa974d --- /dev/null +++ b/packages/server-nest/src/modules/SaleInvoices/commands/GeneratePaymentLink.transformer.ts @@ -0,0 +1,28 @@ +import { Transformer } from '@/modules/Transformer/Transformer'; +import { PUBLIC_PAYMENT_LINK } from '../constants'; + +export class GeneratePaymentLinkTransformer extends Transformer { + /** + * Exclude these attributes from payment link object. + * @returns {Array} + */ + public excludeAttributes = (): string[] => { + return ['linkId']; + }; + + /** + * Included attributes. + * @returns {string[]} + */ + public includeAttributes = (): string[] => { + return ['link']; + }; + + /** + * Retrieves the public/private payment linl + * @returns {string} + */ + public link(link) { + return PUBLIC_PAYMENT_LINK?.replace('{PAYMENT_LINK_ID}', link.linkId); + } +} diff --git a/packages/server-nest/src/modules/SaleInvoices/commands/SaleInvoiceIncrement.service.ts b/packages/server-nest/src/modules/SaleInvoices/commands/SaleInvoiceIncrement.service.ts new file mode 100644 index 000000000..73488e1ee --- /dev/null +++ b/packages/server-nest/src/modules/SaleInvoices/commands/SaleInvoiceIncrement.service.ts @@ -0,0 +1,30 @@ +import { Injectable } from '@nestjs/common'; +import { AutoIncrementOrdersService } from '../../AutoIncrementOrders/AutoIncrementOrders.service'; + +@Injectable() +export class SaleInvoiceIncrement { + constructor( + private readonly autoIncrementOrdersService: AutoIncrementOrdersService, + ) {} + + /** + * Retrieves the next unique invoice number. + * @param {number} tenantId - Tenant id. + * @return {string} + */ + public getNextInvoiceNumber(): string { + return this.autoIncrementOrdersService.getNextTransactionNumber( + 'sales_invoices', + ); + } + + /** + * Increment the invoice next number. + * @param {number} tenantId - + */ + public incrementNextInvoiceNumber() { + return this.autoIncrementOrdersService.incrementSettingsNextNumber( + 'sales_invoices', + ); + } +} diff --git a/packages/server-nest/src/modules/SaleInvoices/commands/SendInvoiceInvoiceMailCommon.service.ts b/packages/server-nest/src/modules/SaleInvoices/commands/SendInvoiceInvoiceMailCommon.service.ts new file mode 100644 index 000000000..42c98b021 --- /dev/null +++ b/packages/server-nest/src/modules/SaleInvoices/commands/SendInvoiceInvoiceMailCommon.service.ts @@ -0,0 +1,129 @@ +// import { Inject, Service } from 'typedi'; +// import { SaleInvoiceMailOptions } from '@/interfaces'; +// import HasTenancyService from '@/services/Tenancy/TenancyService'; +// import { GetSaleInvoice } from '../queries/GetSaleInvoice.service'; +// import { ContactMailNotification } from '@/services/MailNotification/ContactMailNotification'; +// import { +// DEFAULT_INVOICE_MAIL_CONTENT, +// DEFAULT_INVOICE_MAIL_SUBJECT, +// } from '../constants'; +// import { GetInvoicePaymentMail } from '../queries/GetInvoicePaymentMail.service'; +// import { GenerateShareLink } from './GenerateInvoicePaymentLink.service'; + +// @Service() +// export class SendSaleInvoiceMailCommon { +// constructor( +// private getSaleInvoiceService: GetSaleInvoice, +// private contactMailNotification: ContactMailNotification, +// private getInvoicePaymentMail: GetInvoicePaymentMail, +// private generatePaymentLinkService: GenerateShareLink, +// ) {} + +// /** +// * Retrieves the mail options. +// * @param {number} tenantId - Tenant id. +// * @param {number} invoiceId - Invoice id. +// * @param {string} defaultSubject - Subject text. +// * @param {string} defaultBody - Subject body. +// * @returns {Promise} +// */ +// public async getInvoiceMailOptions( +// invoiceId: number, +// defaultSubject: string = DEFAULT_INVOICE_MAIL_SUBJECT, +// defaultMessage: string = DEFAULT_INVOICE_MAIL_CONTENT, +// ): Promise { +// const { SaleInvoice } = this.tenancy.models(tenantId); + +// const saleInvoice = await SaleInvoice.query() +// .findById(invoiceId) +// .throwIfNotFound(); + +// const contactMailDefaultOptions = +// await this.contactMailNotification.getDefaultMailOptions( +// tenantId, +// saleInvoice.customerId, +// ); +// const formatArgs = await this.getInvoiceFormatterArgs(tenantId, invoiceId); + +// return { +// ...contactMailDefaultOptions, +// subject: defaultSubject, +// message: defaultMessage, +// attachInvoice: true, +// formatArgs, +// }; +// } + +// /** +// * Formats the given invoice mail options. +// * @param {number} tenantId +// * @param {number} invoiceId +// * @param {SaleInvoiceMailOptions} mailOptions +// * @returns {Promise} +// */ +// public async formatInvoiceMailOptions( +// tenantId: number, +// invoiceId: number, +// mailOptions: SaleInvoiceMailOptions, +// ): Promise { +// const formatterArgs = await this.getInvoiceFormatterArgs( +// tenantId, +// invoiceId, +// ); +// const formattedOptions = +// await this.contactMailNotification.formatMailOptions( +// tenantId, +// mailOptions, +// formatterArgs, +// ); +// // Generates the a new payment link for the given invoice. +// const paymentLink = +// await this.generatePaymentLinkService.generatePaymentLink( +// tenantId, +// invoiceId, +// 'public', +// ); +// const message = await this.getInvoicePaymentMail.getMailTemplate( +// tenantId, +// invoiceId, +// { +// // # Invoice message +// invoiceMessage: formattedOptions.message, +// preview: formattedOptions.message, + +// // # Payment link +// viewInvoiceButtonUrl: paymentLink.link, +// }, +// ); +// return { ...formattedOptions, message }; +// } + +// /** +// * Retrieves the formatted text of the given sale invoice. +// * @param {number} tenantId - Tenant id. +// * @param {number} invoiceId - Sale invoice id. +// * @param {string} text - The given text. +// * @returns {Promise} +// */ +// public getInvoiceFormatterArgs = async ( +// tenantId: number, +// invoiceId: number, +// ): Promise> => { +// const invoice = await this.getSaleInvoiceService.getSaleInvoice( +// tenantId, +// invoiceId, +// ); +// const commonArgs = +// await this.contactMailNotification.getCommonFormatArgs(tenantId); +// return { +// ...commonArgs, +// 'Customer Name': invoice.customer.displayName, +// 'Invoice Number': invoice.invoiceNo, +// 'Invoice Due Amount': invoice.dueAmountFormatted, +// 'Invoice Due Date': invoice.dueDateFormatted, +// 'Invoice Date': invoice.invoiceDateFormatted, +// 'Invoice Amount': invoice.totalFormatted, +// 'Overdue Days': invoice.overdueDays, +// }; +// }; +// } diff --git a/packages/server-nest/src/modules/SaleInvoices/commands/SendSaleInvoiceMail.ts b/packages/server-nest/src/modules/SaleInvoices/commands/SendSaleInvoiceMail.ts new file mode 100644 index 000000000..c54d8dc38 --- /dev/null +++ b/packages/server-nest/src/modules/SaleInvoices/commands/SendSaleInvoiceMail.ts @@ -0,0 +1,136 @@ +// import { Inject, Service } from 'typedi'; +// import Mail from '@/lib/Mail'; +// import { +// ISaleInvoiceMailSend, +// SaleInvoiceMailOptions, +// SendInvoiceMailDTO, +// } from '@/interfaces'; +// import { SaleInvoicePdf } from '../queries/SaleInvoicePdf.service'; +// import { SendSaleInvoiceMailCommon } from './SendInvoiceInvoiceMailCommon.service'; +// import { mergeAndValidateMailOptions } from '@/services/MailNotification/utils'; +// import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; +// import events from '@/subscribers/events'; + +// @Service() +// export class SendSaleInvoiceMail { +// @Inject() +// private invoicePdf: SaleInvoicePdf; + +// @Inject() +// private invoiceMail: SendSaleInvoiceMailCommon; + +// @Inject() +// private eventPublisher: EventPublisher; + +// @Inject('agenda') +// private agenda: any; + +// /** +// * Sends the invoice mail of the given sale invoice. +// * @param {number} tenantId +// * @param {number} saleInvoiceId +// * @param {SendInvoiceMailDTO} messageDTO +// */ +// public async triggerMail( +// tenantId: number, +// saleInvoiceId: number, +// messageOptions: SendInvoiceMailDTO +// ) { +// const payload = { +// tenantId, +// saleInvoiceId, +// messageOptions, +// }; +// await this.agenda.now('sale-invoice-mail-send', payload); + +// // Triggers the event `onSaleInvoicePreMailSend`. +// await this.eventPublisher.emitAsync(events.saleInvoice.onPreMailSend, { +// tenantId, +// saleInvoiceId, +// messageOptions, +// } as ISaleInvoiceMailSend); +// } + +// /** +// * Retrieves the formatted mail options. +// * @param {number} tenantId +// * @param {number} saleInvoiceId +// * @param {SendInvoiceMailDTO} messageOptions +// * @returns {Promise} +// */ +// async getFormattedMailOptions( +// tenantId: number, +// saleInvoiceId: number, +// messageOptions: SendInvoiceMailDTO +// ): Promise { +// const defaultMessageOptions = await this.invoiceMail.getInvoiceMailOptions( +// tenantId, +// saleInvoiceId +// ); +// // Merges message options with default options and parses the options values. +// const parsedMessageOptions = mergeAndValidateMailOptions( +// defaultMessageOptions, +// messageOptions +// ); +// return this.invoiceMail.formatInvoiceMailOptions( +// tenantId, +// saleInvoiceId, +// parsedMessageOptions +// ); +// } + +// /** +// * Triggers the mail invoice. +// * @param {number} tenantId +// * @param {number} saleInvoiceId +// * @param {SendInvoiceMailDTO} messageDTO +// * @returns {Promise} +// */ +// public async sendMail( +// tenantId: number, +// saleInvoiceId: number, +// messageOptions: SendInvoiceMailDTO +// ) { +// const formattedMessageOptions = await this.getFormattedMailOptions( +// tenantId, +// saleInvoiceId, +// messageOptions +// ); +// const mail = new Mail() +// .setSubject(formattedMessageOptions.subject) +// .setTo(formattedMessageOptions.to) +// .setCC(formattedMessageOptions.cc) +// .setBCC(formattedMessageOptions.bcc) +// .setContent(formattedMessageOptions.message); + +// // Attach invoice document. +// if (formattedMessageOptions.attachInvoice) { +// // Retrieves document buffer of the invoice pdf document. +// const [invoicePdfBuffer, invoiceFilename] = +// await this.invoicePdf.saleInvoicePdf(tenantId, saleInvoiceId); + +// mail.setAttachments([ +// { filename: `${invoiceFilename}.pdf`, content: invoicePdfBuffer }, +// ]); +// } +// const eventPayload = { +// tenantId, +// saleInvoiceId, +// messageOptions, +// formattedMessageOptions, +// } as ISaleInvoiceMailSend; + +// // Triggers the event `onSaleInvoiceSend`. +// await this.eventPublisher.emitAsync( +// events.saleInvoice.onMailSend, +// eventPayload +// ); +// await mail.send(); + +// // Triggers the event `onSaleInvoiceSend`. +// await this.eventPublisher.emitAsync( +// events.saleInvoice.onMailSent, +// eventPayload +// ); +// } +// } diff --git a/packages/server-nest/src/modules/SaleInvoices/commands/SendSaleInvoiceMailJob.ts b/packages/server-nest/src/modules/SaleInvoices/commands/SendSaleInvoiceMailJob.ts new file mode 100644 index 000000000..9f3f56c76 --- /dev/null +++ b/packages/server-nest/src/modules/SaleInvoices/commands/SendSaleInvoiceMailJob.ts @@ -0,0 +1,33 @@ +// import Container, { Service } from 'typedi'; +// import events from '@/subscribers/events'; +// import { SendSaleInvoiceMail } from './SendSaleInvoiceMail'; + +// @Service() +// export class SendSaleInvoiceMailJob { +// /** +// * Constructor method. +// */ +// constructor(agenda) { +// agenda.define( +// 'sale-invoice-mail-send', +// { priority: 'high', concurrency: 2 }, +// this.handler +// ); +// } + +// /** +// * Triggers sending invoice mail. +// */ +// private handler = async (job, done: Function) => { +// const { tenantId, saleInvoiceId, messageOptions } = job.attrs.data; +// const sendInvoiceMail = Container.get(SendSaleInvoiceMail); + +// try { +// await sendInvoiceMail.sendMail(tenantId, saleInvoiceId, messageOptions); +// done(); +// } catch (error) { +// console.log(error); +// done(error); +// } +// }; +// } diff --git a/packages/server-nest/src/modules/SaleInvoices/commands/SendSaleInvoiceMailReminder.ts b/packages/server-nest/src/modules/SaleInvoices/commands/SendSaleInvoiceMailReminder.ts new file mode 100644 index 000000000..6bd49e09c --- /dev/null +++ b/packages/server-nest/src/modules/SaleInvoices/commands/SendSaleInvoiceMailReminder.ts @@ -0,0 +1,112 @@ +// import { Inject, Service } from 'typedi'; +// import { +// ISaleInvoiceMailSend, +// ISaleInvoiceMailSent, +// SendInvoiceMailDTO, +// } from '@/interfaces'; +// import Mail from '@/lib/Mail'; +// import { SaleInvoicePdf } from '../queries/SaleInvoicePdf'; +// import { SendSaleInvoiceMailCommon } from './SendInvoiceInvoiceMailCommon'; +// import { +// DEFAULT_INVOICE_REMINDER_MAIL_CONTENT, +// DEFAULT_INVOICE_REMINDER_MAIL_SUBJECT, +// } from '../constants'; +// import { parseAndValidateMailOptions } from '@/services/MailNotification/utils'; +// import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; +// import events from '@/subscribers/events'; + +// @Service() +// export class SendInvoiceMailReminder { +// @Inject('agenda') +// private agenda: any; + +// @Inject() +// private invoicePdf: SaleInvoicePdf; + +// @Inject() +// private invoiceCommonMail: SendSaleInvoiceMailCommon; + +// @Inject() +// private eventPublisher: EventPublisher; + +// /** +// * Triggers the reminder mail of the given sale invoice. +// * @param {number} tenantId +// * @param {number} saleInvoiceId +// */ +// public async triggerMail( +// tenantId: number, +// saleInvoiceId: number, +// messageOptions: SendInvoiceMailDTO +// ) { +// const payload = { +// tenantId, +// saleInvoiceId, +// messageOptions, +// }; +// await this.agenda.now('sale-invoice-reminder-mail-send', payload); +// } + +// /** +// * Retrieves the mail options of the given sale invoice. +// * @param {number} tenantId +// * @param {number} saleInvoiceId +// * @returns {Promise} +// */ +// public async getMailOption(tenantId: number, saleInvoiceId: number) { +// return this.invoiceCommonMail.getMailOption( +// tenantId, +// saleInvoiceId, +// DEFAULT_INVOICE_REMINDER_MAIL_SUBJECT, +// DEFAULT_INVOICE_REMINDER_MAIL_CONTENT +// ); +// } + +// /** +// * Triggers the mail invoice. +// * @param {number} tenantId +// * @param {number} saleInvoiceId +// * @param {SendInvoiceMailDTO} messageOptions +// * @returns {Promise} +// */ +// public async sendMail( +// tenantId: number, +// saleInvoiceId: number, +// messageOptions: SendInvoiceMailDTO +// ) { +// const localMessageOpts = await this.getMailOption(tenantId, saleInvoiceId); + +// const messageOpts = parseAndValidateMailOptions( +// localMessageOpts, +// messageOptions +// ); +// const mail = new Mail() +// .setSubject(messageOpts.subject) +// .setTo(messageOpts.to) +// .setContent(messageOpts.body); + +// if (messageOpts.attachInvoice) { +// // Retrieves document buffer of the invoice pdf document. +// const invoicePdfBuffer = await this.invoicePdf.saleInvoicePdf( +// tenantId, +// saleInvoiceId +// ); +// mail.setAttachments([ +// { filename: 'invoice.pdf', content: invoicePdfBuffer }, +// ]); +// } +// // Triggers the event `onSaleInvoiceSend`. +// await this.eventPublisher.emitAsync(events.saleInvoice.onMailReminderSend, { +// saleInvoiceId, +// messageOptions, +// } as ISaleInvoiceMailSend); + +// await mail.send(); + +// // Triggers the event `onSaleInvoiceSent`. +// await this.eventPublisher.emitAsync(events.saleInvoice.onMailReminderSent, { +// saleInvoiceId, +// messageOptions, +// } as ISaleInvoiceMailSent); +// } +// } diff --git a/packages/server-nest/src/modules/SaleInvoices/commands/SendSaleInvoiceMailReminderJob.ts b/packages/server-nest/src/modules/SaleInvoices/commands/SendSaleInvoiceMailReminderJob.ts new file mode 100644 index 000000000..3dc8f73c3 --- /dev/null +++ b/packages/server-nest/src/modules/SaleInvoices/commands/SendSaleInvoiceMailReminderJob.ts @@ -0,0 +1,32 @@ +// import Container, { Service } from 'typedi'; +// import { SendInvoiceMailReminder } from './SendSaleInvoiceMailReminder'; + +// @Service() +// export class SendSaleInvoiceReminderMailJob { +// /** +// * Constructor method. +// */ +// constructor(agenda) { +// agenda.define( +// 'sale-invoice-reminder-mail-send', +// { priority: 'high', concurrency: 1 }, +// this.handler +// ); +// } + +// /** +// * Triggers sending invoice mail. +// */ +// private handler = async (job, done: Function) => { +// const { tenantId, saleInvoiceId, messageOptions } = job.attrs.data; +// const sendInvoiceMail = Container.get(SendInvoiceMailReminder); + +// try { +// await sendInvoiceMail.sendMail(tenantId, saleInvoiceId, messageOptions); +// done(); +// } catch (error) { +// console.log(error); +// done(error); +// } +// }; +// } diff --git a/packages/server-nest/src/modules/SaleInvoices/commands/WriteoffSaleInvoice.service.ts b/packages/server-nest/src/modules/SaleInvoices/commands/WriteoffSaleInvoice.service.ts new file mode 100644 index 000000000..fc246f4c5 --- /dev/null +++ b/packages/server-nest/src/modules/SaleInvoices/commands/WriteoffSaleInvoice.service.ts @@ -0,0 +1,155 @@ +import { Knex } from 'knex'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { Inject, Injectable } from '@nestjs/common'; +import { + ISaleInvoiceWriteoffCreatePayload, + ISaleInvoiceWriteoffDTO, + ISaleInvoiceWrittenOffCanceledPayload, + ISaleInvoiceWrittenOffCancelPayload, +} from '../SaleInvoice.types'; +import { ERRORS } from '../constants'; +import { UnitOfWork } from '../../Tenancy/TenancyDB/UnitOfWork.service'; +import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators.service'; +import { SaleInvoice } from '../models/SaleInvoice'; +import { events } from '@/common/events/events'; +import { ServiceError } from '../../Items/ServiceError'; + +@Injectable() +export class WriteoffSaleInvoice { + constructor( + private readonly eventPublisher: EventEmitter2, + private readonly uow: UnitOfWork, + private readonly validators: CommandSaleInvoiceValidators, + + @Inject(SaleInvoice.name) + private readonly saleInvoiceModel: typeof SaleInvoice, + ) {} + + /** + * Writes-off the sale invoice on bad debt expense account. + * @param {number} saleInvoiceId + * @param {ISaleInvoiceWriteoffDTO} writeoffDTO + * @return {Promise} + */ + public writeOff = async ( + saleInvoiceId: number, + writeoffDTO: ISaleInvoiceWriteoffDTO, + ): Promise => { + const saleInvoice = await this.saleInvoiceModel + .query() + .findById(saleInvoiceId) + .throwIfNotFound(); + + // Validates the given invoice existance. + this.validators.validateInvoiceExistance(saleInvoice); + + // Validate the sale invoice whether already written-off. + this.validateSaleInvoiceAlreadyWrittenoff(saleInvoice); + + // Saves the invoice write-off transaction with associated transactions + // under unit-of-work envirmenet. + return this.uow.withTransaction(async (trx: Knex.Transaction) => { + const eventPayload = { + // tenantId, + saleInvoiceId, + saleInvoice, + writeoffDTO, + trx, + } as ISaleInvoiceWriteoffCreatePayload; + + // Triggers `onSaleInvoiceWriteoff` event. + await this.eventPublisher.emitAsync( + events.saleInvoice.onWriteoff, + eventPayload, + ); + // Mark the sale invoice as written-off. + const newSaleInvoice = await this.saleInvoiceModel + .query(trx) + .patch({ + writtenoffExpenseAccountId: writeoffDTO.expenseAccountId, + writtenoffAmount: saleInvoice.dueAmount, + writtenoffAt: new Date(), + }) + .findById(saleInvoiceId); + + // Triggers `onSaleInvoiceWrittenoff` event. + await this.eventPublisher.emitAsync( + events.saleInvoice.onWrittenoff, + eventPayload, + ); + return newSaleInvoice; + }); + }; + + /** + * Cancels the written-off sale invoice. + * @param {number} tenantId + * @param {number} saleInvoiceId + * @returns {Promise} + */ + public cancelWrittenoff = async ( + saleInvoiceId: number, + ): Promise => { + // Validate the sale invoice existance. + + // Retrieve the sale invoice or throw not found service error. + const saleInvoice = await this.saleInvoiceModel + .query() + .findById(saleInvoiceId); + + // Validate the sale invoice existance. + this.validators.validateInvoiceExistance(saleInvoice); + + // Validate the sale invoice whether already written-off. + this.validateSaleInvoiceNotWrittenoff(saleInvoice); + + // Cancels the invoice written-off and removes the associated transactions. + return this.uow.withTransaction(async (trx: Knex.Transaction) => { + // Triggers `onSaleInvoiceWrittenoffCancel` event. + await this.eventPublisher.emitAsync( + events.saleInvoice.onWrittenoffCancel, + { + saleInvoice, + trx, + } as ISaleInvoiceWrittenOffCancelPayload, + ); + // Mark the sale invoice as written-off. + const newSaleInvoice = await SaleInvoice.query(trx) + .patch({ + writtenoffAmount: null, + writtenoffAt: null, + }) + .findById(saleInvoiceId); + + // Triggers `onSaleInvoiceWrittenoffCanceled`. + await this.eventPublisher.emitAsync( + events.saleInvoice.onWrittenoffCanceled, + { + saleInvoice, + trx, + } as ISaleInvoiceWrittenOffCanceledPayload, + ); + return newSaleInvoice; + }); + }; + + /** + * Should sale invoice not be written-off. + * @param {SaleInvoice} saleInvoice + */ + private validateSaleInvoiceNotWrittenoff(saleInvoice: SaleInvoice) { + if (!saleInvoice.isWrittenoff) { + throw new ServiceError(ERRORS.SALE_INVOICE_NOT_WRITTEN_OFF); + } + } + + /** + * Should sale invoice already written-off. + * @param {SaleInvoice} saleInvoice + */ + private validateSaleInvoiceAlreadyWrittenoff(saleInvoice: SaleInvoice) { + if (saleInvoice.isWrittenoff) { + throw new ServiceError(ERRORS.SALE_INVOICE_ALREADY_WRITTEN_OFF); + } + } +} diff --git a/packages/server-nest/src/modules/SaleInvoices/constants.ts b/packages/server-nest/src/modules/SaleInvoices/constants.ts new file mode 100644 index 000000000..42c783f10 --- /dev/null +++ b/packages/server-nest/src/modules/SaleInvoices/constants.ts @@ -0,0 +1,240 @@ +// import config from '@/config'; + +const BASE_URL = 'http://localhost:3000'; + +export const DEFAULT_INVOICE_MAIL_SUBJECT = + 'Invoice {Invoice Number} from {Company Name} for {Customer Name}'; +export const DEFAULT_INVOICE_MAIL_CONTENT = `Hi {Customer Name}, + +Here's invoice # {Invoice Number} for {Invoice Amount} + +The amount outstanding of {Invoice Due Amount} is due on {Invoice Due Date}. + +From your online payment page you can print a PDF or view your outstanding bills. + +If you have any questions, please let us know. + +Thanks, +{Company Name} +`; + +export const DEFAULT_INVOICE_REMINDER_MAIL_SUBJECT = + 'Invoice {InvoiceNumber} reminder from {CompanyName}'; +export const DEFAULT_INVOICE_REMINDER_MAIL_CONTENT = ` +

Dear {CustomerName}

+

You might have missed the payment date and the invoice is now overdue by {OverdueDays} days.

+

Invoice #{InvoiceNumber}
+Due Date : {InvoiceDueDate}
+Amount : {InvoiceAmount}

+ +

+Regards
+{CompanyName} +

+`; + +export const PUBLIC_PAYMENT_LINK = `${BASE_URL}/payment/{PAYMENT_LINK_ID}`; + +export const ERRORS = { + INVOICE_NUMBER_NOT_UNIQUE: 'INVOICE_NUMBER_NOT_UNIQUE', + SALE_INVOICE_NOT_FOUND: 'SALE_INVOICE_NOT_FOUND', + SALE_INVOICE_ALREADY_DELIVERED: 'SALE_INVOICE_ALREADY_DELIVERED', + ENTRIES_ITEMS_IDS_NOT_EXISTS: 'ENTRIES_ITEMS_IDS_NOT_EXISTS', + NOT_SELLABLE_ITEMS: 'NOT_SELLABLE_ITEMS', + SALE_INVOICE_NO_NOT_UNIQUE: 'SALE_INVOICE_NO_NOT_UNIQUE', + INVOICE_AMOUNT_SMALLER_THAN_PAYMENT_AMOUNT: + 'INVOICE_AMOUNT_SMALLER_THAN_PAYMENT_AMOUNT', + INVOICE_HAS_ASSOCIATED_PAYMENT_ENTRIES: + 'INVOICE_HAS_ASSOCIATED_PAYMENT_ENTRIES', + SALE_INVOICE_NO_IS_REQUIRED: 'SALE_INVOICE_NO_IS_REQUIRED', + CUSTOMER_HAS_SALES_INVOICES: 'CUSTOMER_HAS_SALES_INVOICES', + SALE_INVOICE_HAS_APPLIED_TO_CREDIT_NOTES: + 'SALE_INVOICE_HAS_APPLIED_TO_CREDIT_NOTES', + PAYMENT_ACCOUNT_CURRENCY_INVALID: 'PAYMENT_ACCOUNT_CURRENCY_INVALID', + SALE_INVOICE_ALREADY_WRITTEN_OFF: 'SALE_INVOICE_ALREADY_WRITTEN_OFF', + SALE_INVOICE_NOT_WRITTEN_OFF: 'SALE_INVOICE_NOT_WRITTEN_OFF', + NO_INVOICE_CUSTOMER_EMAIL_ADDR: 'NO_INVOICE_CUSTOMER_EMAIL_ADDR', +}; + +export const DEFAULT_VIEW_COLUMNS = []; +export const DEFAULT_VIEWS = [ + { + name: 'Draft', + slug: 'draft', + rolesLogicExpression: '1', + roles: [ + { index: 1, fieldKey: 'status', comparator: 'equals', value: 'draft' }, + ], + columns: DEFAULT_VIEW_COLUMNS, + }, + { + name: 'Delivered', + slug: 'delivered', + rolesLogicExpression: '1', + roles: [ + { + index: 1, + fieldKey: 'status', + comparator: 'equals', + value: 'delivered', + }, + ], + columns: DEFAULT_VIEW_COLUMNS, + }, + { + name: 'Unpaid', + slug: 'unpaid', + rolesLogicExpression: '1', + roles: [ + { index: 1, fieldKey: 'status', comparator: 'equals', value: 'unpaid' }, + ], + columns: DEFAULT_VIEW_COLUMNS, + }, + { + name: 'Partially paid', + slug: 'partially-paid', + rolesLogicExpression: '1', + roles: [ + { + index: 1, + fieldKey: 'status', + comparator: 'equals', + value: 'partially-paid', + }, + ], + columns: DEFAULT_VIEW_COLUMNS, + }, + { + name: 'Paid', + slug: 'paid', + rolesLogicExpression: '1', + roles: [ + { index: 1, fieldKey: 'status', comparator: 'equals', value: 'paid' }, + ], + columns: DEFAULT_VIEW_COLUMNS, + }, +]; + +export const SaleInvoicesSampleData = [ + { + 'Invoice No.': 'B-101', + 'Reference No.': 'REF0', + 'Invoice Date': '2024-01-01', + 'Due Date': '2024-03-01', + Customer: 'Harley Veum', + 'Exchange Rate': 1, + 'Invoice Message': 'Aspernatur doloremque amet quia aut.', + 'Terms & Conditions': 'Quia illum aut dolores.', + Delivered: 'T', + Item: 'VonRueden, Ruecker and Hettinger', + Quantity: 100, + Rate: 100, + Description: 'Description', + }, + { + 'Invoice No.': 'B-102', + 'Reference No.': 'REF0', + 'Invoice Date': '2024-01-01', + 'Due Date': '2024-03-01', + Customer: 'Harley Veum', + 'Exchange Rate': 1, + 'Invoice Message': 'Est omnis enim vel.', + 'Terms & Conditions': 'Iusto et sint nobis sit.', + Delivered: 'T', + Item: 'Thompson - Reichert', + Quantity: 200, + Rate: 50, + Description: 'Description', + }, + { + 'Invoice No.': 'B-103', + 'Reference No.': 'REF0', + 'Invoice Date': '2024-01-01', + 'Due Date': '2024-03-01', + Customer: 'Harley Veum', + 'Exchange Rate': 1, + 'Invoice Message': + 'Repudiandae voluptatibus repellat minima voluptatem rerum veniam.', + 'Terms & Conditions': 'Id quod inventore ex rerum velit sed.', + Delivered: 'T', + Item: 'VonRueden, Ruecker and Hettinger', + Quantity: 100, + Rate: 100, + Description: 'Description', + }, +]; + +export const defaultInvoicePdfTemplateAttributes = { + primaryColor: 'red', + secondaryColor: 'red', + + companyName: 'Bigcapital Technology, Inc.', + + showCompanyLogo: true, + companyLogoKey: '', + companyLogoUri: '', + + dueDateLabel: 'Date due', + showDueDate: true, + + dateIssueLabel: 'Date of issue', + showDateIssue: true, + + // # Invoice number, + invoiceNumberLabel: 'Invoice number', + showInvoiceNumber: true, + + // # Customer address + showCustomerAddress: true, + customerAddress: '', + + // # Company address + showCompanyAddress: true, + companyAddress: '', + billedToLabel: 'Billed To', + + // Entries + lineItemLabel: 'Item', + lineQuantityLabel: 'Qty', + lineRateLabel: 'Rate', + lineTotalLabel: 'Total', + + totalLabel: 'Total', + subtotalLabel: 'Subtotal', + discountLabel: 'Discount', + paymentMadeLabel: 'Payment Made', + balanceDueLabel: 'Balance Due', + + // Totals + showTotal: true, + showSubtotal: true, + showDiscount: true, + showTaxes: true, + showPaymentMade: true, + showDueAmount: true, + showBalanceDue: true, + + discount: '0.00', + + // Footer paragraphs. + termsConditionsLabel: 'Terms & Conditions', + showTermsConditions: true, + + lines: [ + { + item: 'Simply dummy text', + description: 'Simply dummy text of the printing and typesetting', + rate: '1', + quantity: '1000', + total: '$1000.00', + }, + ], + taxes: [ + { label: 'Sample Tax1 (4.70%)', amount: '11.75' }, + { label: 'Sample Tax2 (7.00%)', amount: '21.74' }, + ], + + // # Statement + statementLabel: 'Statement', + showStatement: true, +}; diff --git a/packages/server-nest/src/modules/SaleInvoices/models/SaleInvoice.ts b/packages/server-nest/src/modules/SaleInvoices/models/SaleInvoice.ts new file mode 100644 index 000000000..c376f7afc --- /dev/null +++ b/packages/server-nest/src/modules/SaleInvoices/models/SaleInvoice.ts @@ -0,0 +1,684 @@ +import { mixin, Model, raw } from 'objection'; +import { castArray, takeWhile } from 'lodash'; +import moment from 'moment'; +// import TenantModel from 'models/TenantModel'; +// import ModelSetting from './ModelSetting'; +// import SaleInvoiceMeta from './SaleInvoice.Settings'; +// import CustomViewBaseModel from './CustomViewBaseModel'; +// import { DEFAULT_VIEWS } from '@/services/Sales/Invoices/constants'; +// import ModelSearchable from './ModelSearchable'; +import { BaseModel } from '@/models/Model'; + +export class SaleInvoice extends BaseModel{ + public taxAmountWithheld: number; + public balance: number; + public paymentAmount: number; + public exchangeRate: number; + + public creditedAmount: number; + public isInclusiveTax: boolean; + + public dueDate: Date; + public deliveredAt: Date; + public currencyCode: string; + public invoiceDate: Date; + + public createdAt?: Date; + public updatedAt?: Date | null; + + public writtenoffExpenseAccountId: number; + public writtenoffAmount: number; + public writtenoffAt: Date; + + public customerId: number; + public invoiceNo: string; + public referenceNo: string; + + public pdfTemplateId: number; + + public branchId: number; + public warehouseId: number; + + // public taxes: TaxRateTransaction[]; + + /** + * Table name + */ + static get tableName() { + return 'sales_invoices'; + } + + /** + * Timestamps columns. + */ + get timestamps() { + return ['created_at', 'updated_at']; + } + + /** + * + */ + get pluralName() { + return 'asdfsdf'; + } + + /** + * Virtual attributes. + */ + static get virtualAttributes() { + return [ + 'isDelivered', + 'isOverdue', + 'isPartiallyPaid', + 'isFullyPaid', + 'isWrittenoff', + 'isPaid', + + 'dueAmount', + 'balanceAmount', + 'remainingDays', + 'overdueDays', + + 'subtotal', + 'subtotalLocal', + 'subtotalExludingTax', + + 'taxAmountWithheldLocal', + 'total', + 'totalLocal', + + 'writtenoffAmountLocal', + ]; + } + + /** + * Invoice amount. + * @todo Sugger attribute to balance, we need to rename the balance to amount. + * @returns {number} + */ + get amount() { + return this.balance; + } + + /** + * Invoice amount in base currency. + * @returns {number} + */ + get amountLocal() { + return this.amount * this.exchangeRate; + } + + /** + * Subtotal. (Tax inclusive) if the tax inclusive is enabled. + * @returns {number} + */ + get subtotal() { + return this.amount; + } + + /** + * Subtotal in base currency. (Tax inclusive) if the tax inclusive is enabled. + * @returns {number} + */ + get subtotalLocal() { + return this.amountLocal; + } + + /** + * Sale invoice amount excluding tax. + * @returns {number} + */ + get subtotalExludingTax() { + return this.isInclusiveTax + ? this.subtotal - this.taxAmountWithheld + : this.subtotal; + } + + /** + * Tax amount withheld in base currency. + * @returns {number} + */ + get taxAmountWithheldLocal() { + return this.taxAmountWithheld * this.exchangeRate; + } + + /** + * Invoice total. (Tax included) + * @returns {number} + */ + get total() { + return this.isInclusiveTax + ? this.subtotal + : this.subtotal + this.taxAmountWithheld; + } + + /** + * Invoice total in local currency. (Tax included) + * @returns {number} + */ + get totalLocal() { + return this.total * this.exchangeRate; + } + + /** + * Detarmines whether the invoice is delivered. + * @return {boolean} + */ + get isDelivered() { + return !!this.deliveredAt; + } + + /** + * Detarmines the due date is over. + * @return {boolean} + */ + get isOverdue() { + return this.overdueDays > 0; + } + + /** + * Retrieve the sale invoice balance. + * @return {number} + */ + get balanceAmount() { + return this.paymentAmount + this.writtenoffAmount + this.creditedAmount; + } + + /** + * Retrieve the invoice due amount. + * Equation (Invoice amount - payment amount = Due amount) + * @return {boolean} + */ + get dueAmount() { + return Math.max(this.total - this.balanceAmount, 0); + } + + /** + * Detarmine whether the invoice paid partially. + * @return {boolean} + */ + get isPartiallyPaid() { + return this.dueAmount !== this.total && this.dueAmount > 0; + } + + /** + * Deetarmine whether the invoice paid fully. + * @return {boolean} + */ + get isFullyPaid() { + return this.dueAmount === 0; + } + + /** + * Detarmines whether the invoice paid fully or partially. + * @return {boolean} + */ + get isPaid() { + return this.isPartiallyPaid || this.isFullyPaid; + } + + /** + * Detarmines whether the sale invoice is written-off. + * @return {boolean} + */ + get isWrittenoff() { + return Boolean(this.writtenoffAt); + } + + /** + * Retrieve the remaining days in number + * @return {number|null} + */ + get remainingDays() { + const dateMoment = moment(); + const dueDateMoment = moment(this.dueDate); + + return Math.max(dueDateMoment.diff(dateMoment, 'days'), 0); + } + + /** + * Retrieve the overdue days in number. + * @return {number|null} + */ + get overdueDays() { + const dateMoment = moment(); + const dueDateMoment = moment(this.dueDate); + + return Math.max(dateMoment.diff(dueDateMoment, 'days'), 0); + } + + /** + * Model modifiers. + */ + static get modifiers() { + return { + /** + * Filters the due invoices. + */ + dueInvoices(query) { + query.where( + raw(` + COALESCE(BALANCE, 0) - + COALESCE(PAYMENT_AMOUNT, 0) - + COALESCE(WRITTENOFF_AMOUNT, 0) - + COALESCE(CREDITED_AMOUNT, 0) > 0 + `) + ); + }, + /** + * Filters the invoices between the given date range. + */ + filterDateRange(query, startDate, endDate, type = 'day') { + const dateFormat = 'YYYY-MM-DD'; + const fromDate = moment(startDate).startOf(type).format(dateFormat); + const toDate = moment(endDate).endOf(type).format(dateFormat); + + if (startDate) { + query.where('invoice_date', '>=', fromDate); + } + if (endDate) { + query.where('invoice_date', '<=', toDate); + } + }, + /** + * Filters the invoices in draft status. + */ + draft(query) { + query.where('delivered_at', null); + }, + /** + * Filters the published invoices. + */ + published(query) { + query.whereNot('delivered_at', null); + }, + /** + * Filters the delivered invoices. + */ + delivered(query) { + query.whereNot('delivered_at', null); + }, + /** + * Filters the unpaid invoices. + */ + unpaid(query) { + query.where(raw('PAYMENT_AMOUNT = 0')); + }, + /** + * Filters the overdue invoices. + */ + overdue(query, asDate = moment().format('YYYY-MM-DD')) { + query.where('due_date', '<', asDate); + }, + /** + * Filters the not overdue invoices. + */ + notOverdue(query, asDate = moment().format('YYYY-MM-DD')) { + query.where('due_date', '>=', asDate); + }, + /** + * Filters the partially invoices. + */ + partiallyPaid(query) { + query.whereNot('payment_amount', 0); + query.whereNot(raw('`PAYMENT_AMOUNT` = `BALANCE`')); + }, + /** + * Filters the paid invoices. + */ + paid(query) { + query.where(raw('PAYMENT_AMOUNT = BALANCE')); + }, + /** + * Filters the sale invoices from the given date. + */ + fromDate(query, fromDate) { + query.where('invoice_date', '<=', fromDate); + }, + /** + * Sort the sale invoices by full-payment invoices. + */ + sortByStatus(query, order) { + query.orderByRaw(`PAYMENT_AMOUNT = BALANCE ${order}`); + }, + + /** + * Sort the sale invoices by the due amount. + */ + sortByDueAmount(query, order) { + query.orderByRaw(`BALANCE - PAYMENT_AMOUNT ${order}`); + }, + + /** + * Retrieve the max invoice + */ + maxInvoiceNo(query, prefix, number) { + query + .select(raw(`REPLACE(INVOICE_NO, "${prefix}", "") AS INV_NUMBER`)) + .havingRaw('CHAR_LENGTH(INV_NUMBER) = ??', [number.length]) + .orderBy('invNumber', 'DESC') + .limit(1) + .first(); + }, + + byPrefixAndNumber(query, prefix, number) { + query.where('invoice_no', `${prefix}${number}`); + }, + + /** + * Status filter. + */ + statusFilter(query, filterType) { + switch (filterType) { + case 'draft': + query.modify('draft'); + break; + case 'delivered': + query.modify('delivered'); + break; + case 'unpaid': + query.modify('unpaid'); + break; + case 'overdue': + default: + query.modify('overdue'); + break; + case 'partially-paid': + query.modify('partiallyPaid'); + break; + case 'paid': + query.modify('paid'); + break; + } + }, + + /** + * Filters by branches. + */ + filterByBranches(query, branchesIds) { + const formattedBranchesIds = castArray(branchesIds); + + query.whereIn('branchId', formattedBranchesIds); + }, + + dueInvoicesFromDate(query, asDate = moment().format('YYYY-MM-DD')) { + query.modify('dueInvoices'); + query.modify('notOverdue', asDate); + query.modify('fromDate', asDate); + }, + + overdueInvoicesFromDate(query, asDate = moment().format('YYYY-MM-DD')) { + query.modify('dueInvoices'); + query.modify('overdue', asDate); + query.modify('fromDate', asDate); + }, + }; + } + + /** + * Relationship mapping. + */ + // static get relationMappings() { + // const AccountTransaction = require('models/AccountTransaction'); + // const ItemEntry = require('models/ItemEntry'); + // const Customer = require('models/Customer'); + // const InventoryCostLotTracker = require('models/InventoryCostLotTracker'); + // const PaymentReceiveEntry = require('models/PaymentReceiveEntry'); + // const Branch = require('models/Branch'); + // const Warehouse = require('models/Warehouse'); + // const Account = require('models/Account'); + // const TaxRateTransaction = require('models/TaxRateTransaction'); + // const Document = require('models/Document'); + // const { MatchedBankTransaction } = require('models/MatchedBankTransaction'); + // const { + // TransactionPaymentServiceEntry, + // } = require('models/TransactionPaymentServiceEntry'); + // const { PdfTemplate } = require('models/PdfTemplate'); + + // return { + // /** + // * Sale invoice associated entries. + // */ + // entries: { + // relation: Model.HasManyRelation, + // modelClass: ItemEntry.default, + // join: { + // from: 'sales_invoices.id', + // to: 'items_entries.referenceId', + // }, + // filter(builder) { + // builder.where('reference_type', 'SaleInvoice'); + // builder.orderBy('index', 'ASC'); + // }, + // }, + + // /** + // * Belongs to customer model. + // */ + // customer: { + // relation: Model.BelongsToOneRelation, + // modelClass: Customer.default, + // join: { + // from: 'sales_invoices.customerId', + // to: 'contacts.id', + // }, + // filter(query) { + // query.where('contact_service', 'Customer'); + // }, + // }, + + // /** + // * Invoice has associated account transactions. + // */ + // transactions: { + // relation: Model.HasManyRelation, + // modelClass: AccountTransaction.default, + // join: { + // from: 'sales_invoices.id', + // to: 'accounts_transactions.referenceId', + // }, + // filter(builder) { + // builder.where('reference_type', 'SaleInvoice'); + // }, + // }, + + // /** + // * Invoice may has associated cost transactions. + // */ + // costTransactions: { + // relation: Model.HasManyRelation, + // modelClass: InventoryCostLotTracker.default, + // join: { + // from: 'sales_invoices.id', + // to: 'inventory_cost_lot_tracker.transactionId', + // }, + // filter(builder) { + // builder.where('transaction_type', 'SaleInvoice'); + // }, + // }, + + // /** + // * Invoice may has associated payment entries. + // */ + // paymentEntries: { + // relation: Model.HasManyRelation, + // modelClass: PaymentReceiveEntry.default, + // join: { + // from: 'sales_invoices.id', + // to: 'payment_receives_entries.invoiceId', + // }, + // }, + + // /** + // * Invoice may has associated branch. + // */ + // branch: { + // relation: Model.BelongsToOneRelation, + // modelClass: Branch.default, + // join: { + // from: 'sales_invoices.branchId', + // to: 'branches.id', + // }, + // }, + + // /** + // * Invoice may has associated warehouse. + // */ + // warehouse: { + // relation: Model.BelongsToOneRelation, + // modelClass: Warehouse.default, + // join: { + // from: 'sales_invoices.warehouseId', + // to: 'warehouses.id', + // }, + // }, + + // /** + // * Invoice may has associated written-off expense account. + // */ + // writtenoffExpenseAccount: { + // relation: Model.BelongsToOneRelation, + // modelClass: Account.default, + // join: { + // from: 'sales_invoices.writtenoffExpenseAccountId', + // to: 'accounts.id', + // }, + // }, + + // /** + // * Invoice may has associated tax rate transactions. + // */ + // taxes: { + // relation: Model.HasManyRelation, + // modelClass: TaxRateTransaction.default, + // join: { + // from: 'sales_invoices.id', + // to: 'tax_rate_transactions.referenceId', + // }, + // filter(builder) { + // builder.where('reference_type', 'SaleInvoice'); + // }, + // }, + + // /** + // * Sale invoice transaction may has many attached attachments. + // */ + // attachments: { + // relation: Model.ManyToManyRelation, + // modelClass: Document.default, + // join: { + // from: 'sales_invoices.id', + // through: { + // from: 'document_links.modelId', + // to: 'document_links.documentId', + // }, + // to: 'documents.id', + // }, + // filter(query) { + // query.where('model_ref', 'SaleInvoice'); + // }, + // }, + + // /** + // * Sale invocie may belongs to matched bank transaction. + // */ + // matchedBankTransaction: { + // relation: Model.HasManyRelation, + // modelClass: MatchedBankTransaction, + // join: { + // from: 'sales_invoices.id', + // to: 'matched_bank_transactions.referenceId', + // }, + // filter(query) { + // query.where('reference_type', 'SaleInvoice'); + // }, + // }, + + // /** + // * Sale invoice may belongs to payment methods entries. + // */ + // paymentMethods: { + // relation: Model.HasManyRelation, + // modelClass: TransactionPaymentServiceEntry, + // join: { + // from: 'sales_invoices.id', + // to: 'transactions_payment_methods.referenceId', + // }, + // beforeInsert: (model) => { + // model.referenceType = 'SaleInvoice'; + // }, + // filter: (query) => { + // query.where('reference_type', 'SaleInvoice'); + // }, + // }, + + // /** + // * Sale invoice may belongs to pdf branding template. + // */ + // pdfTemplate: { + // relation: Model.BelongsToOneRelation, + // modelClass: PdfTemplate, + // join: { + // from: 'sales_invoices.pdfTemplateId', + // to: 'pdf_templates.id', + // } + // }, + // }; + // } + + /** + * Change payment amount. + * @param {Integer} invoiceId + * @param {Numeric} amount + */ + static async changePaymentAmount(invoiceId, amount, trx) { + const changeMethod = amount > 0 ? 'increment' : 'decrement'; + + await this.query(trx) + .where('id', invoiceId) + [changeMethod]('payment_amount', Math.abs(amount)); + } + + /** + * Sale invoice meta. + */ + static get meta() { + return SaleInvoiceMeta; + } + + static dueAmountFieldSortQuery(query, role) { + query.modify('sortByDueAmount', role.order); + } + + /** + * Retrieve the default custom views, roles and columns. + */ + static get defaultViews() { + return DEFAULT_VIEWS; + } + + /** + * Model searchable. + */ + static get searchable() { + return true; + } + + /** + * Model search attributes. + */ + static get searchRoles() { + return [ + { fieldKey: 'invoice_no', comparator: 'contains' }, + { condition: 'or', fieldKey: 'reference_no', comparator: 'contains' }, + { condition: 'or', fieldKey: 'amount', comparator: 'equals' }, + ]; + } + + /** + * Prevents mutate base currency since the model is not empty. + */ + static get preventMutateBaseCurrency() { + return true; + } +} diff --git a/packages/server-nest/src/modules/SaleInvoices/queries/GetInvoicePaymentLink.transformer.ts b/packages/server-nest/src/modules/SaleInvoices/queries/GetInvoicePaymentLink.transformer.ts new file mode 100644 index 000000000..50368cc84 --- /dev/null +++ b/packages/server-nest/src/modules/SaleInvoices/queries/GetInvoicePaymentLink.transformer.ts @@ -0,0 +1,222 @@ +import { SaleInvoiceTaxEntryTransformer } from './SaleInvoiceTaxEntry.transformer'; +import { SaleInvoiceTransformer } from './SaleInvoice.transformer'; +import { contactAddressTextFormat } from '@/utils/address-text-format'; +import { Transformer } from '@/modules/Transformer/Transformer'; +import { ItemEntryTransformer } from '@/modules/TransactionItemEntry/ItemEntry.transformer'; +import { GetPdfTemplateTransformer } from '@/modules/PdfTemplate/queries/GetPdfTemplate.transformer'; + +export class GetInvoicePaymentLinkMetaTransformer extends SaleInvoiceTransformer { + /** + * Exclude these attributes from payment link object. + * @returns {Array} + */ + public excludeAttributes = (): string[] => { + return ['*']; + }; + + /** + * Included attributes. + * @returns {string[]} + */ + public includeAttributes = (): string[] => { + return [ + 'customerName', + 'dueAmount', + 'dueDateFormatted', + 'invoiceDateFormatted', + 'total', + 'totalFormatted', + 'totalLocalFormatted', + 'subtotal', + 'subtotalFormatted', + 'subtotalLocalFormatted', + 'dueAmount', + 'dueAmountFormatted', + 'paymentAmount', + 'paymentAmountFormatted', + 'dueDate', + 'dueDateFormatted', + 'invoiceNo', + 'invoiceMessage', + 'termsConditions', + 'entries', + 'taxes', + 'organization', + 'isReceivable', + 'hasStripePaymentMethod', + 'formattedCustomerAddress', + 'brandingTemplate', + ]; + }; + + public customerName(invoice) { + return invoice.customer.displayName; + } + + /** + * Retrieves the organization metadata for the payment link. + * @returns + */ + public organization(invoice) { + return this.item( + this.context.organization, + new GetPaymentLinkOrganizationMetaTransformer() + ); + } + + /** + * Retrieves the branding template for the payment link. + * @param {} invoice + * @returns + */ + public brandingTemplate(invoice) { + return this.item( + invoice.pdfTemplate, + new GetInvoicePaymentLinkBrandingTemplate() + ); + } + + /** + * Retrieves the entries of the sale invoice. + * @param {ISaleInvoice} invoice + * @returns {} + */ + protected entries = (invoice) => { + return this.item( + invoice.entries, + new GetInvoicePaymentLinkEntryMetaTransformer(), + { + currencyCode: invoice.currencyCode, + } + ); + }; + + /** + * Retrieves the sale invoice entries. + * @returns {} + */ + protected taxes = (invoice) => { + return this.item( + invoice.taxes, + new GetInvoicePaymentLinkTaxEntryTransformer(), + { + subtotal: invoice.subtotal, + isInclusiveTax: invoice.isInclusiveTax, + currencyCode: invoice.currencyCode, + } + ); + }; + + protected isReceivable(invoice) { + return invoice.dueAmount > 0; + } + + protected hasStripePaymentMethod(invoice) { + return invoice.paymentMethods.some( + (paymentMethod) => paymentMethod.paymentIntegration.service === 'Stripe' + ); + } + + get customerAddressFormat() { + return `{ADDRESS_1} +{ADDRESS_2} +{CITY} {STATE} {POSTAL_CODE} +{COUNTRY} +{PHONE}`; + } + + /** + * Retrieves the formatted customer address. + * @param invoice + * @returns {string} + */ + protected formattedCustomerAddress(invoice) { + return contactAddressTextFormat( + invoice.customer, + this.customerAddressFormat + ); + } +} + +class GetPaymentLinkOrganizationMetaTransformer extends Transformer { + /** + * Include these attributes to item entry object. + * @returns {Array} + */ + public includeAttributes = (): string[] => { + return [ + 'primaryColor', + 'name', + 'address', + 'logoUri', + 'addressTextFormatted', + ]; + }; + + public excludeAttributes = (): string[] => { + return ['*']; + }; + + /** + * Retrieves the formatted text of organization address. + * @returns {string} + */ + public addressTextFormatted() { + return this.context.organization.addressTextFormatted; + } +} + +class GetInvoicePaymentLinkEntryMetaTransformer extends ItemEntryTransformer { + /** + * Include these attributes to item entry object. + * @returns {Array} + */ + public includeAttributes = (): string[] => { + return [ + 'quantity', + 'quantityFormatted', + 'rate', + 'rateFormatted', + 'total', + 'totalFormatted', + 'itemName', + 'description', + ]; + }; + + public itemName(entry) { + return entry.item.name; + } + + /** + * Exclude these attributes from payment link object. + * @returns {Array} + */ + public excludeAttributes = (): string[] => { + return ['*']; + }; +} + +class GetInvoicePaymentLinkTaxEntryTransformer extends SaleInvoiceTaxEntryTransformer { + /** + * Included attributes. + * @returns {Array} + */ + public includeAttributes = (): string[] => { + return ['name', 'taxRateCode', 'taxRateAmount', 'taxRateAmountFormatted']; + }; +} + +class GetInvoicePaymentLinkBrandingTemplate extends GetPdfTemplateTransformer { + public includeAttributes = (): string[] => { + return ['companyLogoUri', 'primaryColor']; + }; + + public excludeAttributes = (): string[] => { + return ['*']; + }; + + primaryColor = (template) => { + return template.attributes?.primaryColor; + }; +} diff --git a/packages/server-nest/src/modules/SaleInvoices/queries/GetInvoicePaymentMail.service.ts b/packages/server-nest/src/modules/SaleInvoices/queries/GetInvoicePaymentMail.service.ts new file mode 100644 index 000000000..99723f8ee --- /dev/null +++ b/packages/server-nest/src/modules/SaleInvoices/queries/GetInvoicePaymentMail.service.ts @@ -0,0 +1,53 @@ +import { + InvoicePaymentEmailProps, + renderInvoicePaymentEmail, +} from '@bigcapital/email-components'; +import { GetSaleInvoice } from './GetSaleInvoice.service'; +import { GetPdfTemplateService } from '@/modules/PdfTemplate/queries/GetPdfTemplate.service'; +import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service'; +import { GetInvoicePaymentMailAttributesTransformer } from './GetInvoicePaymentMailAttributes.transformer'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class GetInvoicePaymentMail { + constructor( + private readonly getSaleInvoiceService: GetSaleInvoice, + private readonly getBrandingTemplate: GetPdfTemplateService, + private readonly transformer: TransformerInjectable, + ) {} + + /** + * Retrieves the mail template attributes of the given invoice. + * Invoice template attributes are composed of the invoice and branding template attributes. + * @param {number} invoiceId - Invoice id. + */ + public async getMailTemplateAttributes(invoiceId: number) { + const invoice = await this.getSaleInvoiceService.getSaleInvoice(invoiceId); + const brandingTemplate = await this.getBrandingTemplate.getPdfTemplate( + invoice.pdfTemplateId, + ); + const mailTemplateAttributes = await this.transformer.transform( + invoice, + new GetInvoicePaymentMailAttributesTransformer(), + { + invoice, + brandingTemplate, + }, + ); + return mailTemplateAttributes; + } + + /** + * Retrieves the mail template html content. + * @param {number} invoiceId - Invoice id. + */ + public async getMailTemplate( + invoiceId: number, + overrideAttributes?: Partial, + ): Promise { + const attributes = await this.getMailTemplateAttributes(invoiceId); + const mergedAttributes = { ...attributes, ...overrideAttributes }; + + return renderInvoicePaymentEmail(mergedAttributes); + } +} diff --git a/packages/server-nest/src/modules/SaleInvoices/queries/GetInvoicePaymentMailAttributes.transformer.ts b/packages/server-nest/src/modules/SaleInvoices/queries/GetInvoicePaymentMailAttributes.transformer.ts new file mode 100644 index 000000000..1ca1e81b5 --- /dev/null +++ b/packages/server-nest/src/modules/SaleInvoices/queries/GetInvoicePaymentMailAttributes.transformer.ts @@ -0,0 +1,136 @@ +import { Transformer } from "@/modules/Transformer/Transformer"; + +export class GetInvoicePaymentMailAttributesTransformer extends Transformer { + /** + * Include these attributes to item entry object. + * @returns {Array} + */ + public includeAttributes = (): string[] => { + return [ + 'companyLogoUri', + 'companyName', + + 'invoiceAmount', + + 'primaryColor', + + 'invoiceAmount', + 'invoiceMessage', + + 'dueDate', + 'dueDateLabel', + + 'invoiceNumber', + 'invoiceNumberLabel', + + 'total', + 'totalLabel', + + 'dueAmount', + 'dueAmountLabel', + + 'viewInvoiceButtonLabel', + 'viewInvoiceButtonUrl', + + 'items', + ]; + }; + + public excludeAttributes = (): string[] => { + return ['*']; + }; + + public companyLogoUri(): string { + return this.options.brandingTemplate?.companyLogoUri; + } + + public companyName(): string { + return this.context.organization.name; + } + + public invoiceAmount(): string { + return this.options.invoice.totalFormatted; + } + + public primaryColor(): string { + return this.options?.brandingTemplate?.attributes?.primaryColor; + } + + public invoiceMessage(): string { + return ''; + } + + public dueDate(): string { + return this.options?.invoice?.dueDateFormatted; + } + + public dueDateLabel(): string { + return 'Due {dueDate}'; + } + + public invoiceNumber(): string { + return this.options?.invoice?.invoiceNo; + } + + public invoiceNumberLabel(): string { + return 'Invoice # {invoiceNumber}'; + } + + public total(): string { + return this.options.invoice?.totalFormatted; + } + + public totalLabel(): string { + return 'Total'; + } + + public dueAmount(): string { + return this.options?.invoice.dueAmountFormatted; + } + + public dueAmountLabel(): string { + return 'Due Amount'; + } + + public viewInvoiceButtonLabel(): string { + return 'View Invoice'; + } + + public viewInvoiceButtonUrl(): string { + return ''; + } + + public items(): Array { + return this.item( + this.options.invoice?.entries, + new GetInvoiceMailTemplateItemAttrsTransformer() + ); + } +} + +class GetInvoiceMailTemplateItemAttrsTransformer extends Transformer { + /** + * Include these attributes to item entry object. + * @returns {Array} + */ + public includeAttributes = (): string[] => { + return ['quantity', 'label', 'rate']; + }; + + public excludeAttributes = (): string[] => { + return ['*']; + }; + + public quantity(entry): string { + return entry?.quantity; + } + + public label(entry): string { + console.log(entry); + return entry?.item?.name; + } + + public rate(entry): string { + return entry?.rateFormatted; + } +} diff --git a/packages/server-nest/src/modules/SaleInvoices/queries/GetInvoicePayments.service.ts b/packages/server-nest/src/modules/SaleInvoices/queries/GetInvoicePayments.service.ts new file mode 100644 index 000000000..267a42901 --- /dev/null +++ b/packages/server-nest/src/modules/SaleInvoices/queries/GetInvoicePayments.service.ts @@ -0,0 +1,33 @@ +import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service'; +import { InvoicePaymentTransactionTransformer } from './InvoicePaymentTransaction.transformer'; +import { Inject, Injectable } from '@nestjs/common'; +import { PaymentReceivedEntry } from '@/modules/PaymentReceived/models/PaymentReceivedEntry'; + +@Injectable() +export class GetInvoicePaymentsService { + constructor( + private readonly transformer: TransformerInjectable, + + @Inject(PaymentReceivedEntry.name) + private readonly paymentReceivedEntryModel: typeof PaymentReceivedEntry, + ) {} + + /** + * Retrieve the invoice associated payments transactions. + * @param {number} tenantId - Tenant id. + * @param {number} invoiceId - Invoice id. + */ + public getInvoicePayments = async (invoiceId: number) => { + const paymentsEntries = await this.paymentReceivedEntryModel + .query() + .where('invoiceId', invoiceId) + .withGraphJoined('payment.depositAccount') + .withGraphJoined('invoice') + .orderBy('payment:paymentDate', 'ASC'); + + return this.transformer.transform( + paymentsEntries, + new InvoicePaymentTransactionTransformer(), + ); + }; +} diff --git a/packages/server-nest/src/modules/SaleInvoices/queries/GetSaleInvoice.service.ts b/packages/server-nest/src/modules/SaleInvoices/queries/GetSaleInvoice.service.ts new file mode 100644 index 000000000..f19093f38 --- /dev/null +++ b/packages/server-nest/src/modules/SaleInvoices/queries/GetSaleInvoice.service.ts @@ -0,0 +1,55 @@ +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { Inject, Injectable } from '@nestjs/common'; +import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service'; +import { SaleInvoice } from '../models/SaleInvoice'; +import { SaleInvoiceTransformer } from './SaleInvoice.transformer'; +import { CommandSaleInvoiceValidators } from '../commands/CommandSaleInvoiceValidators.service'; +import { events } from '@/common/events/events'; + +@Injectable() +export class GetSaleInvoice { + constructor( + private transformer: TransformerInjectable, + private validators: CommandSaleInvoiceValidators, + private eventPublisher: EventEmitter2, + + @Inject(SaleInvoice.name) + private saleInvoiceModel: typeof SaleInvoice, + ) {} + + /** + * Retrieve sale invoice with associated entries. + * @param {Number} saleInvoiceId - + * @param {ISystemUser} authorizedUser - + * @return {Promise} + */ + public async getSaleInvoice(saleInvoiceId: number): Promise { + const saleInvoice = await this.saleInvoiceModel + .query() + .findById(saleInvoiceId) + .withGraphFetched('entries.item') + .withGraphFetched('entries.tax') + .withGraphFetched('customer') + .withGraphFetched('branch') + .withGraphFetched('taxes.taxRate') + .withGraphFetched('attachments') + .withGraphFetched('paymentMethods'); + + // Validates the given sale invoice existance. + this.validators.validateInvoiceExistance(saleInvoice); + + const transformed = await this.transformer.transform( + saleInvoice, + new SaleInvoiceTransformer(), + ); + const eventPayload = { + saleInvoiceId, + }; + // Triggers the `onSaleInvoiceItemViewed` event. + await this.eventPublisher.emitAsync( + events.saleInvoice.onViewed, + eventPayload, + ); + return transformed; + } +} diff --git a/packages/server-nest/src/modules/SaleInvoices/queries/GetSaleInvoiceMailReminder.ts b/packages/server-nest/src/modules/SaleInvoices/queries/GetSaleInvoiceMailReminder.ts new file mode 100644 index 000000000..2a65d316e --- /dev/null +++ b/packages/server-nest/src/modules/SaleInvoices/queries/GetSaleInvoiceMailReminder.ts @@ -0,0 +1,3 @@ +export class GetSaleInvoiceMailReminder { + public getInvoiceMailReminder(tenantId: number, saleInvoiceId: number) {} +} diff --git a/packages/server-nest/src/modules/SaleInvoices/queries/GetSaleInvoiceMailState.service.ts b/packages/server-nest/src/modules/SaleInvoices/queries/GetSaleInvoiceMailState.service.ts new file mode 100644 index 000000000..240638f25 --- /dev/null +++ b/packages/server-nest/src/modules/SaleInvoices/queries/GetSaleInvoiceMailState.service.ts @@ -0,0 +1,46 @@ +// import { Inject, Injectable } from '@nestjs/common'; +// import { GetSaleInvoiceMailStateTransformer } from './GetSaleInvoiceMailState.transformer'; +// import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service'; +// import { SaleInvoice } from '../models/SaleInvoice'; + +// @Injectable() +// export class GetSaleInvoiceMailState { +// constructor( +// private transformer: TransformerInjectable, +// // private invoiceMail: SendSaleInvoiceMailCommon, + +// @Inject(SaleInvoice.name) +// private saleInvoiceModel: typeof SaleInvoice, +// ) {} + +// /** +// * Retrieves the invoice mail state of the given sale invoice. +// * Invoice mail state includes the mail options, branding attributes and the invoice details. +// * @param {number} saleInvoiceId - Sale invoice id. +// * @returns {Promise} +// */ +// async getInvoiceMailState( +// saleInvoiceId: number, +// ): Promise { +// const saleInvoice = await this.saleInvoiceModel +// .query() +// .findById(saleInvoiceId) +// .withGraphFetched('customer') +// .withGraphFetched('entries.item') +// .withGraphFetched('pdfTemplate') +// .throwIfNotFound(); + +// const mailOptions = +// await this.invoiceMail.getInvoiceMailOptions(saleInvoiceId); + +// // Transforms the sale invoice mail state. +// const transformed = await this.transformer.transform( +// saleInvoice, +// new GetSaleInvoiceMailStateTransformer(), +// { +// mailOptions, +// }, +// ); +// return transformed; +// } +// } diff --git a/packages/server-nest/src/modules/SaleInvoices/queries/GetSaleInvoiceMailState.transformer.ts b/packages/server-nest/src/modules/SaleInvoices/queries/GetSaleInvoiceMailState.transformer.ts new file mode 100644 index 000000000..d28b05d6c --- /dev/null +++ b/packages/server-nest/src/modules/SaleInvoices/queries/GetSaleInvoiceMailState.transformer.ts @@ -0,0 +1,129 @@ +import { ItemEntryTransformer } from '@/modules/TransactionItemEntry/ItemEntry.transformer'; +import { SaleInvoiceTransformer } from './SaleInvoice.transformer'; + +export class GetSaleInvoiceMailStateTransformer extends SaleInvoiceTransformer { + /** + * Exclude these attributes from user object. + * @returns {Array} + */ + public excludeAttributes = (): string[] => { + return ['*']; + }; + + /** + * Included attributes. + * @returns {Array} + */ + public includeAttributes = (): string[] => { + return [ + 'invoiceDate', + 'invoiceDateFormatted', + + 'dueDate', + 'dueDateFormatted', + + 'dueAmount', + 'dueAmountFormatted', + + 'total', + 'totalFormatted', + + 'subtotal', + 'subtotalFormatted', + + 'invoiceNo', + + 'entries', + + 'companyName', + 'companyLogoUri', + + 'primaryColor', + + 'customerName', + ]; + }; + + /** + * Retrieves the customer name of the invoice. + * @returns {string} + */ + protected customerName = (invoice) => { + return invoice.customer.displayName; + }; + + /** + * Retrieves the company name. + * @returns {string} + */ + protected companyName = () => { + return this.context.organization.name; + }; + + /** + * Retrieves the company logo uri. + * @returns {string | null} + */ + protected companyLogoUri = (invoice) => { + return invoice.pdfTemplate?.companyLogoUri; + }; + + /** + * Retrieves the primary color. + * @returns {string} + */ + protected primaryColor = (invoice) => { + return invoice.pdfTemplate?.attributes?.primaryColor; + }; + + /** + * + * @param invoice + * @returns + */ + protected entries = (invoice) => { + return this.item( + invoice.entries, + new GetSaleInvoiceMailStateEntryTransformer(), + { + currencyCode: invoice.currencyCode, + } + ); + }; + + /** + * Merges the mail options with the invoice object. + */ + public transform = (object: any) => { + return { + ...this.options.mailOptions, + ...object, + }; + }; +} + +class GetSaleInvoiceMailStateEntryTransformer extends ItemEntryTransformer { + /** + * Exclude these attributes from user object. + * @returns {Array} + */ + public excludeAttributes = (): string[] => { + return ['*']; + }; + + public name = (entry) => { + return entry.item.name; + }; + + public includeAttributes = (): string[] => { + return [ + 'name', + 'quantity', + 'quantityFormatted', + 'rate', + 'rateFormatted', + 'total', + 'totalFormatted', + ]; + }; +} diff --git a/packages/server-nest/src/modules/SaleInvoices/queries/GetSaleInvoiceState.service.ts b/packages/server-nest/src/modules/SaleInvoices/queries/GetSaleInvoiceState.service.ts new file mode 100644 index 000000000..548c52638 --- /dev/null +++ b/packages/server-nest/src/modules/SaleInvoices/queries/GetSaleInvoiceState.service.ts @@ -0,0 +1,26 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { PdfTemplateModel } from '@/modules/PdfTemplate/models/PdfTemplate'; +import { ISaleInvocieState } from '../SaleInvoice.types'; + +@Injectable() +export class GetSaleInvoiceState { + constructor( + @Inject(PdfTemplateModel.name) + private pdfTemplateModel: typeof PdfTemplateModel, + ) {} + + /** + * Retrieves the create/edit invoice state. + * @return {Promise} + */ + public async getSaleInvoiceState(): Promise { + const defaultPdfTemplate = await this.pdfTemplateModel + .query() + .findOne({ resource: 'SaleInvoice' }) + .modify('default'); + + return { + defaultTemplateId: defaultPdfTemplate?.id, + }; + } +} diff --git a/packages/server-nest/src/modules/SaleInvoices/queries/GetSaleInvoices.ts b/packages/server-nest/src/modules/SaleInvoices/queries/GetSaleInvoices.ts new file mode 100644 index 000000000..472ddeb01 --- /dev/null +++ b/packages/server-nest/src/modules/SaleInvoices/queries/GetSaleInvoices.ts @@ -0,0 +1,80 @@ +// import { Inject, Service } from 'typedi'; +// import * as R from 'ramda'; +// import { +// IFilterMeta, +// IPaginationMeta, +// ISaleInvoice, +// ISalesInvoicesFilter, +// } from '@/interfaces'; +// import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; +// import HasTenancyService from '@/services/Tenancy/TenancyService'; +// import DynamicListingService from '@/services/DynamicListing/DynamicListService'; +// import { SaleInvoiceTransformer } from './SaleInvoice.transformer'; + +// @Service() +// export class GetSaleInvoices { +// @Inject() +// private tenancy: HasTenancyService; + +// @Inject() +// private dynamicListService: DynamicListingService; + +// @Inject() +// private transformer: TransformerInjectable; + +// /** +// * Retrieve sales invoices filterable and paginated list. +// * @param {Request} req +// * @param {Response} res +// * @param {NextFunction} next +// */ +// public async getSaleInvoices( +// filterDTO: ISalesInvoicesFilter +// ): Promise<{ +// salesInvoices: ISaleInvoice[]; +// pagination: IPaginationMeta; +// filterMeta: IFilterMeta; +// }> { +// const { SaleInvoice } = this.tenancy.models(tenantId); + +// // Parses stringified filter roles. +// const filter = this.parseListFilterDTO(filterDTO); + +// // Dynamic list service. +// const dynamicFilter = await this.dynamicListService.dynamicList( +// tenantId, +// SaleInvoice, +// filter +// ); +// const { results, pagination } = await SaleInvoice.query() +// .onBuild((builder) => { +// builder.withGraphFetched('entries.item'); +// builder.withGraphFetched('customer'); +// dynamicFilter.buildQuery()(builder); +// filterDTO?.filterQuery && filterDTO?.filterQuery(builder); +// }) +// .pagination(filter.page - 1, filter.pageSize); + +// // Retrieves the transformed sale invoices. +// const salesInvoices = await this.transformer.transform( +// tenantId, +// results, +// new SaleInvoiceTransformer() +// ); + +// return { +// salesInvoices, +// pagination, +// filterMeta: dynamicFilter.getResponseMeta(), +// }; +// } + +// /** +// * Parses the sale invoice list filter DTO. +// * @param filterDTO +// * @returns +// */ +// private parseListFilterDTO(filterDTO) { +// return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO); +// } +// } diff --git a/packages/server-nest/src/modules/SaleInvoices/queries/GetSaleInvoicesPayable.service.ts b/packages/server-nest/src/modules/SaleInvoices/queries/GetSaleInvoicesPayable.service.ts new file mode 100644 index 000000000..7dc030704 --- /dev/null +++ b/packages/server-nest/src/modules/SaleInvoices/queries/GetSaleInvoicesPayable.service.ts @@ -0,0 +1,30 @@ +import { Injectable, Inject } from '@nestjs/common'; +import { SaleInvoice } from '../models/SaleInvoice'; + +@Injectable() +export class GetSaleInvoicesPayable { + constructor( + @Inject(SaleInvoice.name) + private readonly saleInvoiceModel: typeof SaleInvoice, + ) {} + + /** + * Retrieve due sales invoices. + * @param {number} customerId - Customer id. + */ + public async getPayableInvoices( + customerId?: number, + ): Promise> { + const salesInvoices = await this.saleInvoiceModel + .query() + .onBuild((query) => { + query.modify('dueInvoices'); + query.modify('delivered'); + + if (customerId) { + query.where('customer_id', customerId); + } + }); + return salesInvoices; + } +} diff --git a/packages/server-nest/src/modules/SaleInvoices/queries/InvoicePaymentTransaction.transformer.ts b/packages/server-nest/src/modules/SaleInvoices/queries/InvoicePaymentTransaction.transformer.ts new file mode 100644 index 000000000..ea3223fda --- /dev/null +++ b/packages/server-nest/src/modules/SaleInvoices/queries/InvoicePaymentTransaction.transformer.ts @@ -0,0 +1,60 @@ +import { Transformer } from "../../Transformer/Transformer"; + +export class InvoicePaymentTransactionTransformer extends Transformer { + /** + * Include these attributes to sale credit note object. + * @returns {Array} + */ + public includeAttributes = (): string[] => { + return ['formattedPaymentAmount', 'formattedPaymentDate']; + }; + + /** + * Retrieve formatted invoice amount. + * @param {ICreditNote} credit + * @returns {string} + */ + protected formattedPaymentAmount = (entry): string => { + return this.formatNumber(entry.paymentAmount, { + currencyCode: entry.payment.currencyCode, + }); + }; + + /** + * Formatted payment date. + * @param entry + * @returns {string} + */ + protected formattedPaymentDate = (entry): string => { + return this.formatDate(entry.payment.paymentDate); + }; + + /** + * + * @param entry + * @returns + */ + public transform = (entry) => { + return { + invoiceId: entry.invoiceId, + paymentReceiveId: entry.paymentReceiveId, + + paymentDate: entry.payment.paymentDate, + formattedPaymentDate: entry.formattedPaymentDate, + + paymentAmount: entry.paymentAmount, + formattedPaymentAmount: entry.formattedPaymentAmount, + currencyCode: entry.payment.currencyCode, + + paymentNumber: entry.payment.paymentReceiveNo, + paymentReferenceNo: entry.payment.referenceNo, + + invoiceNumber: entry.invoice.invoiceNo, + invoiceReferenceNo: entry.invoice.referenceNo, + + depositAccountId: entry.payment.depositAccountId, + depositAccountName: entry.payment.depositAccount.name, + depositAccountSlug: entry.payment.depositAccount.slug, + }; + }; +} diff --git a/packages/server-nest/src/modules/SaleInvoices/queries/SaleEstimatePdfTemplate.service.ts b/packages/server-nest/src/modules/SaleInvoices/queries/SaleEstimatePdfTemplate.service.ts new file mode 100644 index 000000000..c8e34fdb0 --- /dev/null +++ b/packages/server-nest/src/modules/SaleInvoices/queries/SaleEstimatePdfTemplate.service.ts @@ -0,0 +1,44 @@ +import { Injectable } from '@nestjs/common'; +import { mergePdfTemplateWithDefaultAttributes } from '../utils'; +import { GetPdfTemplateService } from '@/modules/PdfTemplate/queries/GetPdfTemplate.service'; +import { GetOrganizationBrandingAttributesService } from '@/modules/PdfTemplate/queries/GetOrganizationBrandingAttributes.service'; +import { defaultEstimatePdfBrandingAttributes } from '@/modules/SaleEstimates/constants'; + +@Injectable() +export class SaleEstimatePdfTemplate { + constructor( + private readonly getPdfTemplateService: GetPdfTemplateService, + private readonly getOrgBrandingAttrs: GetOrganizationBrandingAttributesService, + ) {} + + /** + * Retrieves the estimate pdf template. + * @param {number} invoiceTemplateId + * @returns + */ + public async getEstimatePdfTemplate(estimateTemplateId: number) { + const template = + await this.getPdfTemplateService.getPdfTemplate(estimateTemplateId); + // Retreives the organization branding attributes. + const commonOrgBrandingAttrs = + await this.getOrgBrandingAttrs.getOrganizationBrandingAttributes(); + + // Merge the default branding attributes with organization attrs. + const orgainizationBrandingAttrs = { + ...defaultEstimatePdfBrandingAttributes, + ...commonOrgBrandingAttrs, + }; + const brandingTemplateAttrs = { + ...template.attributes, + companyLogoUri: template.companyLogoUri, + }; + const attributes = mergePdfTemplateWithDefaultAttributes( + brandingTemplateAttrs, + orgainizationBrandingAttrs, + ); + return { + ...template, + attributes, + }; + } +} diff --git a/packages/server-nest/src/modules/SaleInvoices/queries/SaleInvoice.transformer.ts b/packages/server-nest/src/modules/SaleInvoices/queries/SaleInvoice.transformer.ts new file mode 100644 index 000000000..54ea5b52e --- /dev/null +++ b/packages/server-nest/src/modules/SaleInvoices/queries/SaleInvoice.transformer.ts @@ -0,0 +1,214 @@ +import { Transformer } from '@/modules/Transformer/Transformer'; +import { SaleInvoice } from '../models/SaleInvoice'; +import { ItemEntryTransformer } from '../../TransactionItemEntry/ItemEntry.transformer'; +import { AttachmentTransformer } from '../../Attachments/Attachment.transformer'; +import { SaleInvoiceTaxEntryTransformer } from './SaleInvoiceTaxEntry.transformer'; + +export class SaleInvoiceTransformer extends Transformer { + /** + * Include these attributes to sale invoice object. + * @returns {Array} + */ + public includeAttributes = (): string[] => { + return [ + 'invoiceDateFormatted', + 'dueDateFormatted', + 'createdAtFormatted', + 'dueAmountFormatted', + 'paymentAmountFormatted', + 'balanceAmountFormatted', + 'exchangeRateFormatted', + 'subtotalFormatted', + 'subtotalLocalFormatted', + 'subtotalExludingTaxFormatted', + 'taxAmountWithheldFormatted', + 'taxAmountWithheldLocalFormatted', + 'totalFormatted', + 'totalLocalFormatted', + 'taxes', + 'entries', + 'attachments', + ]; + }; + + /** + * Retrieve formatted invoice date. + * @param {ISaleInvoice} invoice + * @returns {String} + */ + protected invoiceDateFormatted = (invoice: SaleInvoice): string => { + return this.formatDate(invoice.invoiceDate); + }; + + /** + * Retrieve formatted due date. + * @param {ISaleInvoice} invoice + * @returns {string} + */ + protected dueDateFormatted = (invoice: SaleInvoice): string => { + return this.formatDate(invoice.dueDate); + }; + + /** + * Retrieve the formatted created at date. + * @param invoice + * @returns {string} + */ + protected createdAtFormatted = (invoice: SaleInvoice): string => { + return this.formatDate(invoice.createdAt); + }; + + /** + * Retrieve formatted invoice due amount. + * @param {ISaleInvoice} invoice + * @returns {string} + */ + protected dueAmountFormatted = (invoice: SaleInvoice): string => { + return this.formatNumber(invoice.dueAmount, { + currencyCode: invoice.currencyCode, + }); + }; + + /** + * Retrieve formatted payment amount. + * @param {ISaleInvoice} invoice + * @returns {string} + */ + protected paymentAmountFormatted = (invoice: SaleInvoice): string => { + return this.formatNumber(invoice.paymentAmount, { + currencyCode: invoice.currencyCode, + }); + }; + + /** + * Retrieve the formatted invoice balance. + * @param {ISaleInvoice} invoice + * @returns {string} + */ + protected balanceAmountFormatted = (invoice: SaleInvoice): string => { + return this.formatNumber(invoice.balanceAmount, { + currencyCode: invoice.currencyCode, + }); + }; + + /** + * Retrieve the formatted exchange rate. + * @param {ISaleInvoice} invoice + * @returns {string} + */ + protected exchangeRateFormatted = (invoice: SaleInvoice): string => { + return this.formatNumber(invoice.exchangeRate, { money: false }); + }; + + /** + * Retrieves formatted subtotal in base currency. + * (Tax inclusive if the tax inclusive is enabled) + * @param invoice + * @returns {string} + */ + protected subtotalFormatted = (invoice: SaleInvoice): string => { + return this.formatNumber(invoice.subtotal, { + currencyCode: this.context.organization.baseCurrency, + money: false, + }); + }; + + /** + * Retrieves formatted subtotal in foreign currency. + * (Tax inclusive if the tax inclusive is enabled) + * @param invoice + * @returns {string} + */ + protected subtotalLocalFormatted = (invoice: SaleInvoice): string => { + return this.formatNumber(invoice.subtotalLocal, { + currencyCode: invoice.currencyCode, + }); + }; + + /** + * Retrieves formatted subtotal excluding tax in foreign currency. + * @param invoice + * @returns {string} + */ + protected subtotalExludingTaxFormatted = (invoice: SaleInvoice): string => { + return this.formatNumber(invoice.subtotalExludingTax, { + currencyCode: invoice.currencyCode, + }); + }; + + /** + * Retrieves formatted tax amount withheld in foreign currency. + * @param invoice + * @returns {string} + */ + protected taxAmountWithheldFormatted = (invoice: SaleInvoice): string => { + return this.formatNumber(invoice.taxAmountWithheld, { + currencyCode: invoice.currencyCode, + }); + }; + + /** + * Retrieves formatted tax amount withheld in base currency. + * @param invoice + * @returns {string} + */ + protected taxAmountWithheldLocalFormatted = (invoice: SaleInvoice): string => { + return this.formatNumber(invoice.taxAmountWithheldLocal, { + currencyCode: this.context.organization.baseCurrency, + }); + }; + + /** + * Retrieves formatted total in foreign currency. + * @param invoice + * @returns {string} + */ + protected totalFormatted = (invoice: SaleInvoice): string => { + return this.formatNumber(invoice.total, { + currencyCode: invoice.currencyCode, + }); + }; + + /** + * Retrieves formatted total in base currency. + * @param invoice + * @returns {string} + */ + protected totalLocalFormatted = (invoice: SaleInvoice): string => { + return this.formatNumber(invoice.totalLocal, { + currencyCode: this.context.organization.baseCurrency, + }); + }; + + /** + * Retrieve the taxes lines of sale invoice. + * @param {ISaleInvoice} invoice + */ + // protected taxes = (invoice: SaleInvoice) => { + // return this.item(invoice.taxes, new SaleInvoiceTaxEntryTransformer(), { + // subtotal: invoice.subtotal, + // isInclusiveTax: invoice.isInclusiveTax, + // currencyCode: invoice.currencyCode, + // }); + // }; + + /** + * Retrieves the entries of the sale invoice. + * @param {ISaleInvoice} invoice + * @returns {} + */ + // protected entries = (invoice: SaleInvoice) => { + // return this.item(invoice.entries, new ItemEntryTransformer(), { + // currencyCode: invoice.currencyCode, + // }); + // }; + + /** + * Retrieves the sale invoice attachments. + * @param {ISaleInvoice} invoice + * @returns + */ + // protected attachments = (invoice: SaleInvoice) => { + // return this.item(invoice.attachments, new AttachmentTransformer()); + // }; +} diff --git a/packages/server-nest/src/modules/SaleInvoices/queries/SaleInvoicePdf.service.ts b/packages/server-nest/src/modules/SaleInvoices/queries/SaleInvoicePdf.service.ts new file mode 100644 index 000000000..320a6d873 --- /dev/null +++ b/packages/server-nest/src/modules/SaleInvoices/queries/SaleInvoicePdf.service.ts @@ -0,0 +1,106 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { renderInvoicePaperTemplateHtml } from '@bigcapital/pdf-templates'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { GetSaleInvoice } from './GetSaleInvoice.service'; +import { transformInvoiceToPdfTemplate } from '../utils'; +import { SaleInvoicePdfTemplate } from './SaleInvoicePdfTemplate.service'; +import { ChromiumlyTenancy } from '@/modules/ChromiumlyTenancy/ChromiumlyTenancy.service'; +import { SaleInvoice } from '../models/SaleInvoice'; +import { PdfTemplateModel } from '@/modules/PdfTemplate/models/PdfTemplate'; +import { events } from '@/common/events/events'; +import { InvoicePdfTemplateAttributes } from '../SaleInvoice.types'; + +@Injectable() +export class SaleInvoicePdf { + constructor( + private chromiumlyTenancy: ChromiumlyTenancy, + private getInvoiceService: GetSaleInvoice, + private invoiceBrandingTemplateService: SaleInvoicePdfTemplate, + private eventPublisher: EventEmitter2, + + @Inject(SaleInvoice.name) + private saleInvoiceModel: typeof SaleInvoice, + + @Inject(PdfTemplateModel.name) + private pdfTemplateModel: typeof PdfTemplateModel, + ) {} + + /** + * Retrieve sale invoice html content. + * @param {ISaleInvoice} saleInvoice - + * @returns {Promise} + */ + public async getSaleInvoiceHtml(invoiceId: number): Promise { + const brandingAttributes = + await this.getInvoiceBrandingAttributes(invoiceId); + + return renderInvoicePaperTemplateHtml({ + ...brandingAttributes, + }); + } + + /** + * Retrieve sale invoice pdf content. + * @param {ISaleInvoice} saleInvoice - + * @returns {Promise<[Buffer, string]>} + */ + public async getSaleInvoicePdf(invoiceId: number): Promise<[Buffer, string]> { + const filename = await this.getInvoicePdfFilename(invoiceId); + + const htmlContent = await this.saleInvoiceHtml(invoiceId); + + // Converts the given html content to pdf document. + const buffer = await this.chromiumlyTenancy.convertHtmlContent(htmlContent); + const eventPayload = { saleInvoiceId: invoiceId }; + + // Triggers the `onSaleInvoicePdfViewed` event. + await this.eventPublisher.emitAsync( + events.saleInvoice.onPdfViewed, + eventPayload, + ); + return [buffer, filename]; + } + + /** + * Retrieves the filename pdf document of the given invoice. + * @param {number} invoiceId + * @returns {Promise} + */ + private async getInvoicePdfFilename(invoiceId: number): Promise { + const invoice = await this.saleInvoiceModel.query().findById(invoiceId); + return `Invoice-${invoice.invoiceNo}`; + } + + /** + * Retrieves the branding attributes of the given sale invoice. + * @param {number} invoiceId + * @returns {Promise} + */ + private async getInvoiceBrandingAttributes( + invoiceId: number, + ): Promise { + const invoice = await this.getInvoiceService.getSaleInvoice(invoiceId); + + // Retrieve the invoice template id or get the default template id if not found. + const templateId = + invoice.pdfTemplateId ?? + ( + await this.pdfTemplateModel.query().findOne({ + resource: 'SaleInvoice', + default: true, + }) + )?.id; + + // Get the branding template attributes. + const brandingTemplate = + await this.invoiceBrandingTemplateService.getInvoicePdfTemplate( + templateId, + ); + + // Merge the branding template attributes with the invoice. + return { + ...brandingTemplate.attributes, + ...transformInvoiceToPdfTemplate(invoice), + }; + } +} diff --git a/packages/server-nest/src/modules/SaleInvoices/queries/SaleInvoicePdfTemplate.service.ts b/packages/server-nest/src/modules/SaleInvoices/queries/SaleInvoicePdfTemplate.service.ts new file mode 100644 index 000000000..1a8960a5e --- /dev/null +++ b/packages/server-nest/src/modules/SaleInvoices/queries/SaleInvoicePdfTemplate.service.ts @@ -0,0 +1,43 @@ +import { mergePdfTemplateWithDefaultAttributes } from '../utils'; +import { defaultInvoicePdfTemplateAttributes } from '../constants'; +import { GetOrganizationBrandingAttributesService } from '@/modules/PdfTemplate/queries/GetOrganizationBrandingAttributes.service'; +import { GetPdfTemplateService } from '@/modules/PdfTemplate/queries/GetPdfTemplate.service'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class SaleInvoicePdfTemplate { + constructor( + private readonly getPdfTemplateService: GetPdfTemplateService, + private readonly getOrgBrandingAttributes: GetOrganizationBrandingAttributesService, + ) {} + + /** + * Retrieves the invoice pdf template. + * @param {number} invoiceTemplateId + * @returns + */ + async getInvoicePdfTemplate(invoiceTemplateId: number) { + const template = + await this.getPdfTemplateService.getPdfTemplate(invoiceTemplateId); + // Retrieves the organization branding attributes. + const commonOrgBrandingAttrs = + await this.getOrgBrandingAttributes.getOrganizationBrandingAttributes(); + + const organizationBrandingAttrs = { + ...defaultInvoicePdfTemplateAttributes, + ...commonOrgBrandingAttrs, + }; + const brandingTemplateAttrs = { + ...template.attributes, + companyLogoUri: template.companyLogoUri, + }; + const attributes = mergePdfTemplateWithDefaultAttributes( + brandingTemplateAttrs, + organizationBrandingAttrs, + ); + return { + ...template, + attributes, + }; + } +} diff --git a/packages/server-nest/src/modules/SaleInvoices/queries/SaleInvoiceTaxEntry.transformer.ts b/packages/server-nest/src/modules/SaleInvoices/queries/SaleInvoiceTaxEntry.transformer.ts new file mode 100644 index 000000000..6b3b01e3a --- /dev/null +++ b/packages/server-nest/src/modules/SaleInvoices/queries/SaleInvoiceTaxEntry.transformer.ts @@ -0,0 +1,77 @@ + +import { Transformer } from '@/modules/Transformer/Transformer'; +import { getExlusiveTaxAmount, getInclusiveTaxAmount } from '../../TaxRates/utils'; + +export class SaleInvoiceTaxEntryTransformer extends Transformer { + /** + * Included attributes. + * @returns {Array} + */ + public includeAttributes = (): string[] => { + return [ + 'name', + 'taxRateCode', + 'taxRate', + 'taxRateId', + 'taxRateAmount', + 'taxRateAmountFormatted', + ]; + }; + + /** + * Exclude attributes. + * @returns {string[]} + */ + public excludeAttributes = (): string[] => { + return ['*']; + }; + + /** + * Retrieve tax rate code. + * @param taxEntry + * @returns {string} + */ + protected taxRateCode = (taxEntry) => { + return taxEntry.taxRate.code; + }; + + /** + * Retrieve tax rate id. + * @param taxEntry + * @returns {number} + */ + protected taxRate = (taxEntry) => { + return taxEntry.taxAmount || taxEntry.taxRate.rate; + }; + + /** + * Retrieve tax rate name. + * @param taxEntry + * @returns {string} + */ + protected name = (taxEntry) => { + return taxEntry.taxRate.name; + }; + + /** + * Retrieve tax rate amount. + * @param taxEntry + */ + protected taxRateAmount = (taxEntry) => { + const taxRate = this.taxRate(taxEntry); + + return this.options.isInclusiveTax + ? getInclusiveTaxAmount(this.options.subtotal, taxRate) + : getExlusiveTaxAmount(this.options.subtotal, taxRate); + }; + + /** + * Retrieve formatted tax rate amount. + * @returns {string} + */ + protected taxRateAmountFormatted = (taxEntry) => { + return this.formatNumber(this.taxRateAmount(taxEntry), { + currencyCode: this.options.currencyCode, + }); + }; +} diff --git a/packages/server-nest/src/modules/SaleInvoices/subscribers/InvoiceChangeStatusOnMailSentSubscriber.ts b/packages/server-nest/src/modules/SaleInvoices/subscribers/InvoiceChangeStatusOnMailSentSubscriber.ts new file mode 100644 index 000000000..e572859fb --- /dev/null +++ b/packages/server-nest/src/modules/SaleInvoices/subscribers/InvoiceChangeStatusOnMailSentSubscriber.ts @@ -0,0 +1,49 @@ +// import { Inject, Service } from 'typedi'; +// import events from '@/subscribers/events'; +// import { ISaleInvoiceMailSent } from '@/interfaces'; +// import { DeliverSaleInvoice } from '../commands/DeliverSaleInvoice.service'; +// import { ServiceError } from '@/exceptions'; +// import { ERRORS } from '../constants'; + +// @Service() +// export class InvoiceChangeStatusOnMailSentSubscriber { +// @Inject() +// private markInvoiceDelivedService: DeliverSaleInvoice; + +// /** +// * Attaches events. +// */ +// public attach(bus) { +// bus.subscribe(events.saleInvoice.onPreMailSend, this.markInvoiceDelivered); +// bus.subscribe( +// events.saleInvoice.onMailReminderSent, +// this.markInvoiceDelivered +// ); +// } + +// /** +// * Marks the invoice delivered once the invoice mail sent. +// * @param {ISaleInvoiceMailSent} +// * @returns {Promise} +// */ +// private markInvoiceDelivered = async ({ +// tenantId, +// saleInvoiceId, +// messageOptions, +// }: ISaleInvoiceMailSent) => { +// try { +// await this.markInvoiceDelivedService.deliverSaleInvoice( +// tenantId, +// saleInvoiceId +// ); +// } catch (error) { +// if ( +// error instanceof ServiceError && +// error.errorType === ERRORS.SALE_INVOICE_ALREADY_DELIVERED +// ) { +// } else { +// throw error; +// } +// } +// }; +// } diff --git a/packages/server-nest/src/modules/SaleInvoices/subscribers/InvoiceCostGLEntriesSubscriber.ts b/packages/server-nest/src/modules/SaleInvoices/subscribers/InvoiceCostGLEntriesSubscriber.ts new file mode 100644 index 000000000..e6907fd38 --- /dev/null +++ b/packages/server-nest/src/modules/SaleInvoices/subscribers/InvoiceCostGLEntriesSubscriber.ts @@ -0,0 +1,36 @@ +// import { Inject, Service } from 'typedi'; +// import events from '@/subscribers/events'; +// import { IInventoryCostLotsGLEntriesWriteEvent } from '@/interfaces'; +// import { SaleInvoiceCostGLEntries } from '../SaleInvoiceCostGLEntries'; + +// @Service() +// export class InvoiceCostGLEntriesSubscriber { +// @Inject() +// invoiceCostEntries: SaleInvoiceCostGLEntries; + +// /** +// * Attaches events. +// */ +// public attach(bus) { +// bus.subscribe( +// events.inventory.onCostLotsGLEntriesWrite, +// this.writeInvoicesCostEntriesOnCostLotsWritten +// ); +// } + +// /** +// * Writes the invoices cost GL entries once the inventory cost lots be written. +// * @param {IInventoryCostLotsGLEntriesWriteEvent} +// */ +// private writeInvoicesCostEntriesOnCostLotsWritten = async ({ +// trx, +// startingDate, +// tenantId, +// }: IInventoryCostLotsGLEntriesWriteEvent) => { +// await this.invoiceCostEntries.writeInventoryCostJournalEntries( +// tenantId, +// startingDate, +// trx +// ); +// }; +// } diff --git a/packages/server-nest/src/modules/SaleInvoices/subscribers/InvoicePaymentGLRewriteSubscriber.ts b/packages/server-nest/src/modules/SaleInvoices/subscribers/InvoicePaymentGLRewriteSubscriber.ts new file mode 100644 index 000000000..968f72989 --- /dev/null +++ b/packages/server-nest/src/modules/SaleInvoices/subscribers/InvoicePaymentGLRewriteSubscriber.ts @@ -0,0 +1,37 @@ +// import { Service, Inject } from 'typedi'; +// import events from '@/subscribers/events'; +// import { ISaleInvoiceEditingPayload } from '@/interfaces'; +// import { InvoicePaymentsGLEntriesRewrite } from '../InvoicePaymentsGLRewrite'; + +// @Service() +// export class InvoicePaymentGLRewriteSubscriber { +// @Inject() +// private invoicePaymentsRewriteGLEntries: InvoicePaymentsGLEntriesRewrite; + +// /** +// * Attaches events with handlers. +// */ +// public attach = (bus) => { +// bus.subscribe( +// events.saleInvoice.onEdited, +// this.paymentGLEntriesRewriteOnPaymentEdit +// ); +// return bus; +// }; + +// /** +// * Writes associated invoiceso of payment receive once edit. +// * @param {ISaleInvoiceEditingPayload} - +// */ +// private paymentGLEntriesRewriteOnPaymentEdit = async ({ +// tenantId, +// oldSaleInvoice, +// trx, +// }: ISaleInvoiceEditingPayload) => { +// await this.invoicePaymentsRewriteGLEntries.invoicePaymentsGLEntriesRewrite( +// tenantId, +// oldSaleInvoice.id, +// trx +// ); +// }; +// } diff --git a/packages/server-nest/src/modules/SaleInvoices/subscribers/InvoicePaymentIntegrationSubscriber.ts b/packages/server-nest/src/modules/SaleInvoices/subscribers/InvoicePaymentIntegrationSubscriber.ts new file mode 100644 index 000000000..5256b5806 --- /dev/null +++ b/packages/server-nest/src/modules/SaleInvoices/subscribers/InvoicePaymentIntegrationSubscriber.ts @@ -0,0 +1,93 @@ +// import { Service, Inject } from 'typedi'; +// import { omit } from 'lodash'; +// import events from '@/subscribers/events'; +// import { +// ISaleInvoiceCreatedPayload, +// ISaleInvoiceDeletingPayload, +// PaymentIntegrationTransactionLink, +// PaymentIntegrationTransactionLinkDeleteEventPayload, +// PaymentIntegrationTransactionLinkEventPayload, +// } from '@/interfaces'; +// import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; + +// @Service() +// export class InvoicePaymentIntegrationSubscriber { +// @Inject() +// private eventPublisher: EventPublisher; + +// /** +// * Attaches events with handlers. +// */ +// public attach = (bus) => { +// bus.subscribe( +// events.saleInvoice.onCreated, +// this.handleCreatePaymentIntegrationEvents +// ); +// bus.subscribe( +// events.saleInvoice.onDeleting, +// this.handleCreatePaymentIntegrationEventsOnDeleteInvoice +// ); +// return bus; +// }; + +// /** +// * Handles the creation of payment integration events when a sale invoice is created. +// * This method filters enabled payment methods from the invoice and emits a payment +// * integration link event for each method. +// * @param {ISaleInvoiceCreatedPayload} payload - The payload containing sale invoice creation details. +// */ +// private handleCreatePaymentIntegrationEvents = ({ +// tenantId, +// saleInvoiceDTO, +// saleInvoice, +// trx, +// }: ISaleInvoiceCreatedPayload) => { +// const paymentMethods = +// saleInvoice.paymentMethods?.filter((method) => method.enable) || []; + +// paymentMethods.map( +// async (paymentMethod: PaymentIntegrationTransactionLink) => { +// const payload = { +// ...omit(paymentMethod, ['id']), +// tenantId, +// saleInvoiceId: saleInvoice.id, +// trx, +// }; +// await this.eventPublisher.emitAsync( +// events.paymentIntegrationLink.onPaymentIntegrationLink, +// payload as PaymentIntegrationTransactionLinkEventPayload +// ); +// } +// ); +// }; + +// /** +// * +// * @param {ISaleInvoiceDeletingPayload} payload +// */ +// private handleCreatePaymentIntegrationEventsOnDeleteInvoice = ({ +// tenantId, +// oldSaleInvoice, +// trx, +// }: ISaleInvoiceDeletingPayload) => { +// const paymentMethods = +// oldSaleInvoice.paymentMethods?.filter((method) => method.enable) || []; + +// paymentMethods.map( +// async (paymentMethod: PaymentIntegrationTransactionLink) => { +// const payload = { +// ...omit(paymentMethod, ['id']), +// tenantId, +// oldSaleInvoiceId: oldSaleInvoice.id, +// trx, +// } as PaymentIntegrationTransactionLinkDeleteEventPayload; + +// // Triggers `onPaymentIntegrationDeleteLink` event. +// await this.eventPublisher.emitAsync( +// events.paymentIntegrationLink.onPaymentIntegrationDeleteLink, +// payload +// ); +// } +// ); +// }; +// } diff --git a/packages/server-nest/src/modules/SaleInvoices/utils.ts b/packages/server-nest/src/modules/SaleInvoices/utils.ts new file mode 100644 index 000000000..0db99bf55 --- /dev/null +++ b/packages/server-nest/src/modules/SaleInvoices/utils.ts @@ -0,0 +1,49 @@ +import { pickBy } from 'lodash'; +import { InvoicePdfTemplateAttributes, ISaleInvoice } from '@/interfaces'; +import { contactAddressTextFormat } from '@/utils/address-text-format'; + +export const mergePdfTemplateWithDefaultAttributes = ( + brandingTemplate?: Record, + defaultAttributes: Record = {} +) => { + const brandingAttributes = pickBy( + brandingTemplate, + (val, key) => val !== null && Object.keys(defaultAttributes).includes(key) + ); + return { + ...defaultAttributes, + ...brandingAttributes, + }; +}; + +export const transformInvoiceToPdfTemplate = ( + invoice: ISaleInvoice +): Partial => { + return { + dueDate: invoice.dueDateFormatted, + dateIssue: invoice.invoiceDateFormatted, + invoiceNumber: invoice.invoiceNo, + + total: invoice.totalFormatted, + subtotal: invoice.subtotalFormatted, + paymentMade: invoice.paymentAmountFormatted, + dueAmount: invoice.dueAmountFormatted, + + termsConditions: invoice.termsConditions, + statement: invoice.invoiceMessage, + + lines: invoice.entries.map((entry) => ({ + item: entry.item.name, + description: entry.description, + rate: entry.rateFormatted, + quantity: entry.quantityFormatted, + total: entry.totalFormatted, + })), + taxes: invoice.taxes.map((tax) => ({ + label: tax.name, + amount: tax.taxRateAmountFormatted, + })), + + customerAddress: contactAddressTextFormat(invoice.customer), + }; +}; diff --git a/packages/server-nest/src/modules/SaleReceipts/SaleReceiptApplication.service.ts b/packages/server-nest/src/modules/SaleReceipts/SaleReceiptApplication.service.ts new file mode 100644 index 000000000..fad6b7153 --- /dev/null +++ b/packages/server-nest/src/modules/SaleReceipts/SaleReceiptApplication.service.ts @@ -0,0 +1,181 @@ +import { Knex } from 'knex'; +import { Injectable } from '@nestjs/common'; +import { CreateSaleReceipt } from './commands/CreateSaleReceipt.service'; +import { GetSaleReceiptState } from './queries/GetSaleReceiptState.service'; +import { SaleReceiptsPdfService } from './queries/SaleReceiptsPdf.service'; +import { CloseSaleReceipt } from './commands/CloseSaleReceipt.service'; +// import { GetSaleReceipts } from './queries/GetSaleReceipts'; +import { DeleteSaleReceipt } from './commands/DeleteSaleReceipt.service'; +import { GetSaleReceipt } from './queries/GetSaleReceipt.service'; +import { EditSaleReceipt } from './commands/EditSaleReceipt.service'; +import { ISaleReceiptDTO, ISaleReceiptState } from './types/SaleReceipts.types'; + +@Injectable() +export class SaleReceiptApplication { + constructor( + private createSaleReceiptService: CreateSaleReceipt, + private editSaleReceiptService: EditSaleReceipt, + private getSaleReceiptService: GetSaleReceipt, + private deleteSaleReceiptService: DeleteSaleReceipt, + // private getSaleReceiptsService: GetSaleReceipts, + private closeSaleReceiptService: CloseSaleReceipt, + private getSaleReceiptPdfService: SaleReceiptsPdfService, + // private saleReceiptNotifyBySmsService: SaleReceiptNotifyBySms, + // private saleReceiptNotifyByMailService: SaleReceiptMailNotification, + private getSaleReceiptStateService: GetSaleReceiptState, + ) {} + + /** + * Creates a new sale receipt with associated entries. + * @param {ISaleReceiptDTO} saleReceiptDTO + * @returns {Promise} + */ + public async createSaleReceipt( + saleReceiptDTO: ISaleReceiptDTO, + trx?: Knex.Transaction, + ) { + return this.createSaleReceiptService.createSaleReceipt(saleReceiptDTO, trx); + } + + /** + * Edit details sale receipt with associated entries. + * @param {number} tenantId + * @param {number} saleReceiptId + * @param {} saleReceiptDTO + * @returns + */ + public async editSaleReceipt( + saleReceiptId: number, + saleReceiptDTO: ISaleReceiptDTO, + ) { + return this.editSaleReceiptService.editSaleReceipt( + saleReceiptId, + saleReceiptDTO, + ); + } + + /** + * Retrieve sale receipt with associated entries. + * @param {number} saleReceiptId - Sale receipt identifier. + * @returns {Promise} + */ + public async getSaleReceipt(saleReceiptId: number) { + return this.getSaleReceiptService.getSaleReceipt(saleReceiptId); + } + + /** + * Deletes the sale receipt with associated entries. + * @param {number} saleReceiptId - Sale receipt identifier. + * @returns {Promise} + */ + public async deleteSaleReceipt(saleReceiptId: number) { + return this.deleteSaleReceiptService.deleteSaleReceipt(saleReceiptId); + } + + /** + * Retrieve sales receipts paginated and filterable list. + * @param {number} tenantId + * @param {ISalesReceiptsFilter} filterDTO + * @returns + */ + // public async getSaleReceipts( + // tenantId: number, + // filterDTO: ISalesReceiptsFilter, + // ): Promise<{ + // data: ISaleReceipt[]; + // pagination: IPaginationMeta; + // filterMeta: IFilterMeta; + // }> { + // return this.getSaleReceiptsService.getSaleReceipts(tenantId, filterDTO); + // } + + /** + * Closes the given sale receipt. + * @param {number} tenantId + * @param {number} saleReceiptId + * @returns {Promise} + */ + public async closeSaleReceipt(saleReceiptId: number) { + return this.closeSaleReceiptService.closeSaleReceipt(saleReceiptId); + } + + /** + * Retrieves the given sale receipt pdf. + * @param {number} tenantId + * @param {number} saleReceiptId + * @returns + */ + public getSaleReceiptPdf(tenantId: number, saleReceiptId: number) { + return this.getSaleReceiptPdfService.saleReceiptPdf( + saleReceiptId, + ); + } + + /** + * Notify receipt customer by SMS of the given sale receipt. + * @param {number} tenantId + * @param {number} saleReceiptId + * @returns + */ + // public saleReceiptNotifyBySms(tenantId: number, saleReceiptId: number) { + // return this.saleReceiptNotifyBySmsService.notifyBySms( + // tenantId, + // saleReceiptId, + // ); + // } + + /** + * Retrieves sms details of the given sale receipt. + * @param {number} tenantId + * @param {number} saleReceiptId + * @returns + */ + // public getSaleReceiptSmsDetails(tenantId: number, saleReceiptId: number) { + // return this.saleReceiptNotifyBySmsService.smsDetails( + // tenantId, + // saleReceiptId, + // ); + // } + + /** + * Sends the receipt mail of the given sale receipt. + * @param {number} tenantId + * @param {number} saleReceiptId + * @param {SaleReceiptMailOptsDTO} messageOpts + * @returns {Promise} + */ + // public sendSaleReceiptMail( + // saleReceiptId: number, + // messageOpts: SaleReceiptMailOptsDTO, + // ): Promise { + // return this.saleReceiptNotifyByMailService.triggerMail( + // tenantId, + // saleReceiptId, + // messageOpts, + // ); + // } + + /** + * Retrieves the default mail options of the given sale receipt. + * @param {number} tenantId + * @param {number} saleReceiptId + * @returns {Promise} + */ + // public getSaleReceiptMail( + // tenantId: number, + // saleReceiptId: number, + // ): Promise { + // return this.saleReceiptNotifyByMailService.getMailOptions( + // tenantId, + // saleReceiptId, + // ); + // } + + /** + * Retrieves the current state of the sale receipt. + * @returns {Promise} - A promise resolving to the sale receipt state. + */ + public getSaleReceiptState(): Promise { + return this.getSaleReceiptStateService.getSaleReceiptState(); + } +} diff --git a/packages/server-nest/src/modules/SaleReceipts/SaleReceipts.module.ts b/packages/server-nest/src/modules/SaleReceipts/SaleReceipts.module.ts new file mode 100644 index 000000000..9b7f8b69a --- /dev/null +++ b/packages/server-nest/src/modules/SaleReceipts/SaleReceipts.module.ts @@ -0,0 +1,19 @@ +import { Module } from '@nestjs/common'; +import { SaleReceiptApplication } from './SaleReceiptApplication.service'; +import { CreateSaleReceipt } from './commands/CreateSaleReceipt.service'; +import { EditSaleReceipt } from './commands/EditSaleReceipt.service'; +import { GetSaleReceipt } from './queries/GetSaleReceipt.service'; +import { DeleteSaleReceipt } from './commands/DeleteSaleReceipt.service'; +import { CloseSaleReceipt } from './commands/CloseSaleReceipt.service'; + +@Module({ + providers: [ + SaleReceiptApplication, + CreateSaleReceipt, + EditSaleReceipt, + GetSaleReceipt, + DeleteSaleReceipt, + CloseSaleReceipt, + ], +}) +export class SaleReceiptsModule {} diff --git a/packages/server-nest/src/modules/SaleReceipts/commands/CloseSaleReceipt.service.ts b/packages/server-nest/src/modules/SaleReceipts/commands/CloseSaleReceipt.service.ts new file mode 100644 index 000000000..2b93f06b5 --- /dev/null +++ b/packages/server-nest/src/modules/SaleReceipts/commands/CloseSaleReceipt.service.ts @@ -0,0 +1,70 @@ +import { Inject, Injectable } from '@nestjs/common'; +import moment from 'moment'; +import { Knex } from 'knex'; +import { + ISaleReceiptEventClosedPayload, + ISaleReceiptEventClosingPayload, +} from '../types/SaleReceipts.types'; +import { SaleReceiptValidators } from './SaleReceiptValidators.service'; +import { SaleReceipt } from '../models/SaleReceipt'; +import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { events } from '@/common/events/events'; + +@Injectable() +export class CloseSaleReceipt { + /** + * @param {EventEmitter2} eventEmitter - Event emitter. + * @param {UnitOfWork} uow - Unit of work. + * @param {SaleReceiptValidators} validators - Sale receipt validators. + * @param {typeof SaleReceipt} saleReceiptModel - Sale receipt model. + */ + constructor( + private readonly eventEmitter: EventEmitter2, + private readonly uow: UnitOfWork, + private readonly validators: SaleReceiptValidators, + + @Inject(SaleReceipt.name) + private readonly saleReceiptModel: typeof SaleReceipt, + ) {} + + /** + * Mark the given sale receipt as closed. + * @param {number} saleReceiptId - Sale receipt identifier. + * @return {Promise} + */ + public async closeSaleReceipt(saleReceiptId: number): Promise { + // Retrieve sale receipt or throw not found service error. + const oldSaleReceipt = await this.saleReceiptModel + .query() + .findById(saleReceiptId) + .withGraphFetched('entries') + .throwIfNotFound(); + + // Throw service error if the sale receipt already closed. + this.validators.validateReceiptNotClosed(oldSaleReceipt); + + // Updates the sale receipt transaction under unit-of-work environment. + return this.uow.withTransaction(async (trx: Knex.Transaction) => { + // Triggers `onSaleReceiptClosing` event. + await this.eventEmitter.emitAsync(events.saleReceipt.onClosing, { + oldSaleReceipt, + trx, + } as ISaleReceiptEventClosingPayload); + + // Mark the sale receipt as closed on the storage. + const saleReceipt = await this.saleReceiptModel + .query(trx) + .patchAndFetchById(saleReceiptId, { + closedAt: moment().toMySqlDateTime(), + }); + + // Triggers `onSaleReceiptClosed` event. + await this.eventEmitter.emitAsync(events.saleReceipt.onClosed, { + saleReceiptId, + saleReceipt, + trx, + } as ISaleReceiptEventClosedPayload); + }); + } +} diff --git a/packages/server-nest/src/modules/SaleReceipts/commands/CreateSaleReceipt.service.ts b/packages/server-nest/src/modules/SaleReceipts/commands/CreateSaleReceipt.service.ts new file mode 100644 index 000000000..d7775e9e5 --- /dev/null +++ b/packages/server-nest/src/modules/SaleReceipts/commands/CreateSaleReceipt.service.ts @@ -0,0 +1,105 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Knex } from 'knex'; +import { + ISaleReceiptCreatedPayload, + ISaleReceiptCreatingPayload, + ISaleReceiptDTO, +} from '../types/SaleReceipts.types'; +import { SaleReceiptDTOTransformer } from './SaleReceiptDTOTransformer.service'; +import { SaleReceiptValidators } from './SaleReceiptValidators.service'; +import { ItemsEntriesService } from '@/modules/Items/ItemsEntries.service'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service'; +import { SaleReceipt } from '../models/SaleReceipt'; +import { Customer } from '@/modules/Customers/models/Customer'; +import { events } from '@/common/events/events'; + +@Injectable() +export class CreateSaleReceipt { + /** + * @param {ItemsEntriesService} itemsEntriesService - Items entries service. + * @param {EventEmitter2} eventPublisher - Event emitter. + * @param {UnitOfWork} uow - Unit of work. + * @param {SaleReceiptDTOTransformer} transformer - Sale receipt DTO transformer. + * @param {SaleReceiptValidators} validators - Sale receipt validators. + * @param {typeof SaleReceipt} saleReceiptModel - Sale receipt model. + * @param {typeof Customer} customerModel - Customer model. + */ + constructor( + private readonly itemsEntriesService: ItemsEntriesService, + private readonly eventEmitter: EventEmitter2, + private readonly uow: UnitOfWork, + private readonly transformer: SaleReceiptDTOTransformer, + private readonly validators: SaleReceiptValidators, + + @Inject(SaleReceipt.name) + private readonly saleReceiptModel: typeof SaleReceipt, + + @Inject(Customer.name) + private readonly customerModel: typeof Customer, + ) {} + + /** + * Creates a new sale receipt with associated entries. + * @async + * @param {ISaleReceiptDTO} saleReceiptDTO + * @return {Promise} + */ + public async createSaleReceipt( + saleReceiptDTO: ISaleReceiptDTO, + trx?: Knex.Transaction, + ): Promise { + // Retrieves the payment customer model. + const paymentCustomer = await this.customerModel + .query() + .findById(saleReceiptDTO.customerId) + .throwIfNotFound(); + + // Transform sale receipt DTO to model. + const saleReceiptObj = await this.transformer.transformDTOToModel( + saleReceiptDTO, + paymentCustomer, + ); + // Validate receipt deposit account existence and type. + await this.validators.validateReceiptDepositAccountExistence( + saleReceiptDTO.depositAccountId, + ); + // Validate items IDs existence on the storage. + await this.itemsEntriesService.validateItemsIdsExistance( + saleReceiptDTO.entries, + ); + // Validate the sellable items. + await this.itemsEntriesService.validateNonSellableEntriesItems( + saleReceiptDTO.entries, + ); + // Validate sale receipt number uniqueness. + if (saleReceiptDTO.receiptNumber) { + await this.validators.validateReceiptNumberUnique( + saleReceiptDTO.receiptNumber, + ); + } + // Creates a sale receipt transaction and associated transactions under UOW env. + return this.uow.withTransaction(async (trx: Knex.Transaction) => { + // Triggers `onSaleReceiptCreating` event. + await this.eventEmitter.emitAsync(events.saleReceipt.onCreating, { + saleReceiptDTO, + trx, + } as ISaleReceiptCreatingPayload); + + // Inserts the sale receipt graph to the storage. + const saleReceipt = await this.saleReceiptModel.query().upsertGraph({ + ...saleReceiptObj, + }); + + // Triggers `onSaleReceiptCreated` event. + await this.eventEmitter.emitAsync(events.saleReceipt.onCreated, { + saleReceipt, + saleReceiptId: saleReceipt.id, + saleReceiptDTO, + trx, + } as ISaleReceiptCreatedPayload); + + return saleReceipt; + }, trx); + } +} diff --git a/packages/server-nest/src/modules/SaleReceipts/commands/DeleteSaleReceipt.service.ts b/packages/server-nest/src/modules/SaleReceipts/commands/DeleteSaleReceipt.service.ts new file mode 100644 index 000000000..5f828047d --- /dev/null +++ b/packages/server-nest/src/modules/SaleReceipts/commands/DeleteSaleReceipt.service.ts @@ -0,0 +1,69 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Knex } from 'knex'; +import { + ISaleReceiptDeletingPayload, + ISaleReceiptEventDeletedPayload, +} from '../types/SaleReceipts.types'; +import { SaleReceiptValidators } from './SaleReceiptValidators.service'; +import { SaleReceipt } from '../models/SaleReceipt'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service'; +import { ItemEntry } from '@/modules/Items/models/ItemEntry'; +import { events } from '@/common/events/events'; + +@Injectable() +export class DeleteSaleReceipt { + constructor( + private readonly eventPublisher: EventEmitter2, + private readonly uow: UnitOfWork, + private readonly validators: SaleReceiptValidators, + + @Inject(SaleReceipt.name) + private readonly saleReceiptModel: typeof SaleReceipt, + + @Inject(ItemEntry.name) + private readonly itemEntryModel: typeof ItemEntry, + ) {} + + /** + * Deletes the sale receipt with associated entries. + * @param {Integer} saleReceiptId - Sale receipt identifier. + * @return {void} + */ + public async deleteSaleReceipt(saleReceiptId: number) { + const oldSaleReceipt = await this.saleReceiptModel + .query() + .findById(saleReceiptId) + .withGraphFetched('entries'); + + // Validates the sale receipt existence. + this.validators.validateReceiptExistence(oldSaleReceipt); + + return this.uow.withTransaction(async (trx: Knex.Transaction) => { + // Triggers `onSaleReceiptsDeleting` event. + await this.eventPublisher.emitAsync(events.saleReceipt.onDeleting, { + trx, + oldSaleReceipt, + } as ISaleReceiptDeletingPayload); + + await this.itemEntryModel + .query(trx) + .where('reference_id', saleReceiptId) + .where('reference_type', 'SaleReceipt') + .delete(); + + // Delete the sale receipt transaction. + await this.saleReceiptModel + .query(trx) + .where('id', saleReceiptId) + .delete(); + + // Triggers `onSaleReceiptsDeleted` event. + await this.eventPublisher.emitAsync(events.saleReceipt.onDeleted, { + saleReceiptId, + oldSaleReceipt, + trx, + } as ISaleReceiptEventDeletedPayload); + }); + } +} diff --git a/packages/server-nest/src/modules/SaleReceipts/commands/EditSaleReceipt.service.ts b/packages/server-nest/src/modules/SaleReceipts/commands/EditSaleReceipt.service.ts new file mode 100644 index 000000000..878ddb8e3 --- /dev/null +++ b/packages/server-nest/src/modules/SaleReceipts/commands/EditSaleReceipt.service.ts @@ -0,0 +1,105 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Knex } from 'knex'; +import { + ISaleReceiptEditedPayload, + ISaleReceiptEditingPayload, +} from '../types/SaleReceipts.types'; +import { SaleReceiptValidators } from './SaleReceiptValidators.service'; +import { SaleReceiptDTOTransformer } from './SaleReceiptDTOTransformer.service'; +import { SaleReceipt } from '../models/SaleReceipt'; +import { ItemsEntriesService } from '@/modules/Items/ItemsEntries.service'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service'; +import { Contact } from '@/modules/Contacts/models/Contact'; +import { events } from '@/common/events/events'; +import { Customer } from '@/modules/Customers/models/Customer'; + +@Injectable() +export class EditSaleReceipt { + constructor( + private readonly itemsEntriesService: ItemsEntriesService, + private readonly eventPublisher: EventEmitter2, + private readonly uow: UnitOfWork, + private readonly validators: SaleReceiptValidators, + private readonly dtoTransformer: SaleReceiptDTOTransformer, + + @Inject(SaleReceipt.name) + private readonly saleReceiptModel: typeof SaleReceipt, + + @Inject(Customer.name) + private readonly customerModel: typeof Customer, + ) {} + + /** + * Edit details sale receipt with associated entries. + * @param {Integer} saleReceiptId + * @param {ISaleReceipt} saleReceipt + * @return {void} + */ + public async editSaleReceipt(saleReceiptId: number, saleReceiptDTO: any) { + // Retrieve sale receipt or throw not found service error. + const oldSaleReceipt = await this.saleReceiptModel + .query() + .findById(saleReceiptId) + .withGraphFetched('entries') + .throwIfNotFound(); + + // Retrieves the payment customer model. + const paymentCustomer = await this.customerModel + .query() + .findById(saleReceiptDTO.customerId) + .throwIfNotFound(); + + // Transform sale receipt DTO to model. + const saleReceiptObj = await this.dtoTransformer.transformDTOToModel( + saleReceiptDTO, + paymentCustomer, + oldSaleReceipt, + ); + // Validate receipt deposit account existance and type. + await this.validators.validateReceiptDepositAccountExistence( + saleReceiptDTO.depositAccountId, + ); + // Validate items IDs existance on the storage. + await this.itemsEntriesService.validateItemsIdsExistance( + saleReceiptDTO.entries, + ); + // Validate the sellable items. + await this.itemsEntriesService.validateNonSellableEntriesItems( + saleReceiptDTO.entries, + ); + // Validate sale receipt number uniuqiness. + if (saleReceiptDTO.receiptNumber) { + await this.validators.validateReceiptNumberUnique( + saleReceiptDTO.receiptNumber, + saleReceiptId, + ); + } + // Edits the sale receipt tranasctions with associated transactions under UOW env. + return this.uow.withTransaction(async (trx: Knex.Transaction) => { + // Triggers `onSaleReceiptsEditing` event. + await this.eventPublisher.emitAsync(events.saleReceipt.onEditing, { + oldSaleReceipt, + saleReceiptDTO, + trx, + } as ISaleReceiptEditingPayload); + + // Upsert the receipt graph to the storage. + const saleReceipt = await this.saleReceiptModel + .query(trx) + .upsertGraphAndFetch({ + id: saleReceiptId, + ...saleReceiptObj, + }); + // Triggers `onSaleReceiptEdited` event. + await this.eventPublisher.emitAsync(events.saleReceipt.onEdited, { + oldSaleReceipt, + saleReceipt, + saleReceiptDTO, + trx, + } as ISaleReceiptEditedPayload); + + return saleReceipt; + }); + } +} diff --git a/packages/server-nest/src/modules/SaleReceipts/commands/SaleReceiptCostGLEntries.ts b/packages/server-nest/src/modules/SaleReceipts/commands/SaleReceiptCostGLEntries.ts new file mode 100644 index 000000000..f94474bbd --- /dev/null +++ b/packages/server-nest/src/modules/SaleReceipts/commands/SaleReceiptCostGLEntries.ts @@ -0,0 +1,148 @@ +// import { Service, Inject } from 'typedi'; +// import * as R from 'ramda'; +// import { Knex } from 'knex'; +// import { AccountNormal, IInventoryLotCost, ILedgerEntry } from '@/interfaces'; +// import { increment } from 'utils'; +// import HasTenancyService from '@/services/Tenancy/TenancyService'; +// import Ledger from '@/services/Accounting/Ledger'; +// import LedgerStorageService from '@/services/Accounting/LedgerStorageService'; +// import { groupInventoryTransactionsByTypeId } from '../../Inventory/utils'; + +// @Service() +// export class SaleReceiptCostGLEntries { +// @Inject() +// private tenancy: HasTenancyService; + +// @Inject() +// private ledgerStorage: LedgerStorageService; + +// /** +// * Writes journal entries from sales invoices. +// * @param {number} tenantId - The tenant id. +// * @param {Date} startingDate - Starting date. +// * @param {boolean} override +// */ +// public writeInventoryCostJournalEntries = async ( +// tenantId: number, +// startingDate: Date, +// trx?: Knex.Transaction +// ): Promise => { +// const { InventoryCostLotTracker } = this.tenancy.models(tenantId); + +// const inventoryCostLotTrans = await InventoryCostLotTracker.query() +// .where('direction', 'OUT') +// .where('transaction_type', 'SaleReceipt') +// .where('cost', '>', 0) +// .modify('filterDateRange', startingDate) +// .orderBy('date', 'ASC') +// .withGraphFetched('receipt') +// .withGraphFetched('item'); + +// const ledger = this.getInventoryCostLotsLedger(inventoryCostLotTrans); + +// // Commit the ledger to the storage. +// await this.ledgerStorage.commit(tenantId, ledger, trx); +// }; + +// /** +// * Retrieves the inventory cost lots ledger. +// * @param {} inventoryCostLots +// * @returns {Ledger} +// */ +// private getInventoryCostLotsLedger = ( +// inventoryCostLots: IInventoryLotCost[] +// ) => { +// // Groups the inventory cost lots transactions. +// const inventoryTransactions = +// groupInventoryTransactionsByTypeId(inventoryCostLots); + +// // +// const entries = inventoryTransactions +// .map(this.getSaleInvoiceCostGLEntries) +// .flat(); + +// return new Ledger(entries); +// }; + +// /** +// * +// * @param {IInventoryLotCost} inventoryCostLot +// * @returns {} +// */ +// private getInvoiceCostGLCommonEntry = ( +// inventoryCostLot: IInventoryLotCost +// ) => { +// return { +// currencyCode: inventoryCostLot.receipt.currencyCode, +// exchangeRate: inventoryCostLot.receipt.exchangeRate, + +// transactionType: inventoryCostLot.transactionType, +// transactionId: inventoryCostLot.transactionId, + +// date: inventoryCostLot.date, +// indexGroup: 20, +// costable: true, +// createdAt: inventoryCostLot.createdAt, + +// debit: 0, +// credit: 0, + +// branchId: inventoryCostLot.receipt.branchId, +// }; +// }; + +// /** +// * Retrieves the inventory cost GL entry. +// * @param {IInventoryLotCost} inventoryLotCost +// * @returns {ILedgerEntry[]} +// */ +// private getInventoryCostGLEntry = R.curry( +// ( +// getIndexIncrement, +// inventoryCostLot: IInventoryLotCost +// ): ILedgerEntry[] => { +// const commonEntry = this.getInvoiceCostGLCommonEntry(inventoryCostLot); +// const costAccountId = +// inventoryCostLot.costAccountId || inventoryCostLot.item.costAccountId; + +// // XXX Debit - Cost account. +// const costEntry = { +// ...commonEntry, +// debit: inventoryCostLot.cost, +// accountId: costAccountId, +// accountNormal: AccountNormal.DEBIT, +// itemId: inventoryCostLot.itemId, +// index: getIndexIncrement(), +// }; +// // XXX Credit - Inventory account. +// const inventoryEntry = { +// ...commonEntry, +// credit: inventoryCostLot.cost, +// accountId: inventoryCostLot.item.inventoryAccountId, +// accountNormal: AccountNormal.DEBIT, +// itemId: inventoryCostLot.itemId, +// index: getIndexIncrement(), +// }; +// return [costEntry, inventoryEntry]; +// } +// ); + +// /** +// * Writes journal entries for given sale invoice. +// * ------- +// * - Cost of goods sold -> Debit -> YYYY +// * - Inventory assets -> Credit -> YYYY +// * -------- +// * @param {ISaleInvoice} saleInvoice +// * @param {JournalPoster} journal +// */ +// public getSaleInvoiceCostGLEntries = ( +// inventoryCostLots: IInventoryLotCost[] +// ): ILedgerEntry[] => { +// const getIndexIncrement = increment(0); +// const getInventoryLotEntry = +// this.getInventoryCostGLEntry(getIndexIncrement); + +// return inventoryCostLots.map(getInventoryLotEntry).flat(); +// }; +// } diff --git a/packages/server-nest/src/modules/SaleReceipts/commands/SaleReceiptDTOTransformer.service.ts b/packages/server-nest/src/modules/SaleReceipts/commands/SaleReceiptDTOTransformer.service.ts new file mode 100644 index 000000000..36cbae751 --- /dev/null +++ b/packages/server-nest/src/modules/SaleReceipts/commands/SaleReceiptDTOTransformer.service.ts @@ -0,0 +1,102 @@ +import { Inject, Injectable } from '@nestjs/common'; +import * as R from 'ramda'; +import { sumBy, omit } from 'lodash'; +import composeAsync from 'async/compose'; +import moment from 'moment'; +import { SaleReceiptIncrement } from './SaleReceiptIncrement.service'; +import { ItemsEntriesService } from '@/modules/Items/ItemsEntries.service'; +import { BranchTransactionDTOTransformer } from '@/modules/Branches/integrations/BranchTransactionDTOTransform'; +import { WarehouseTransactionDTOTransform } from '@/modules/Warehouses/Integrations/WarehouseTransactionDTOTransform'; +import { SaleReceiptValidators } from './SaleReceiptValidators.service'; +import { BrandingTemplateDTOTransformer } from '@/modules/PdfTemplate/BrandingTemplateDTOTransformer'; +import { ItemEntry } from '@/modules/Items/models/ItemEntry'; +import { formatDateFields } from '@/utils/format-date-fields'; +import { assocItemEntriesDefaultIndex } from '@/utils/associate-item-entries-index'; +import { SaleReceipt } from '../models/SaleReceipt'; +import { ISaleReceiptDTO } from '../types/SaleReceipts.types'; +import { Customer } from '@/modules/Customers/models/Customer'; + +@Injectable() +export class SaleReceiptDTOTransformer { + constructor( + private readonly itemsEntriesService: ItemsEntriesService, + private readonly branchDTOTransform: BranchTransactionDTOTransformer, + private readonly warehouseDTOTransform: WarehouseTransactionDTOTransform, + private readonly validators: SaleReceiptValidators, + private readonly receiptIncrement: SaleReceiptIncrement, + private readonly brandingTemplatesTransformer: BrandingTemplateDTOTransformer, + + @Inject(ItemEntry.name) + private readonly itemEntryModel: typeof ItemEntry, + ) {} + + /** + * Transform create DTO object to model object. + * @param {ISaleReceiptDTO} saleReceiptDTO - + * @param {ISaleReceipt} oldSaleReceipt - + * @returns {ISaleReceipt} + */ + async transformDTOToModel( + saleReceiptDTO: ISaleReceiptDTO, + paymentCustomer: Customer, + oldSaleReceipt?: SaleReceipt, + ): Promise { + const amount = sumBy(saleReceiptDTO.entries, (e) => + this.itemEntryModel.calcAmount(e), + ); + // Retrieve the next invoice number. + const autoNextNumber = await this.receiptIncrement.getNextReceiptNumber(); + + // Retrieve the receipt number. + const receiptNumber = + saleReceiptDTO.receiptNumber || + oldSaleReceipt?.receiptNumber || + autoNextNumber; + + // Validate receipt number require. + this.validators.validateReceiptNoRequire(receiptNumber); + + const initialEntries = saleReceiptDTO.entries.map((entry) => ({ + reference_type: 'SaleReceipt', + ...entry, + })); + + const asyncEntries = await composeAsync( + // Sets default cost and sell account to receipt items entries. + this.itemsEntriesService.setItemsEntriesDefaultAccounts(), + )(initialEntries); + + const entries = R.compose( + // Associate the default index for each item entry. + assocItemEntriesDefaultIndex, + )(asyncEntries); + + const initialDTO = { + amount, + ...formatDateFields( + omit(saleReceiptDTO, ['closed', 'entries', 'attachments']), + ['receiptDate'], + ), + currencyCode: paymentCustomer.currencyCode, + exchangeRate: saleReceiptDTO.exchangeRate || 1, + receiptNumber, + // Avoid rewrite the deliver date in edit mode when already published. + ...(saleReceiptDTO.closed && + !oldSaleReceipt?.closedAt && { + closedAt: moment().toMySqlDateTime(), + }), + entries, + }; + const initialAsyncDTO = await composeAsync( + // Assigns the default branding template id to the invoice DTO. + this.brandingTemplatesTransformer.assocDefaultBrandingTemplate( + 'SaleReceipt', + ), + )(initialDTO); + + return R.compose( + this.branchDTOTransform.transformDTO, + this.warehouseDTOTransform.transformDTO, + )(initialAsyncDTO); + } +} diff --git a/packages/server-nest/src/modules/SaleReceipts/commands/SaleReceiptGLEntries.ts b/packages/server-nest/src/modules/SaleReceipts/commands/SaleReceiptGLEntries.ts new file mode 100644 index 000000000..64580a02b --- /dev/null +++ b/packages/server-nest/src/modules/SaleReceipts/commands/SaleReceiptGLEntries.ts @@ -0,0 +1,184 @@ +// import { Knex } from 'knex'; +// import { Service, Inject } from 'typedi'; +// import * as R from 'ramda'; +// import HasTenancyService from '@/services/Tenancy/TenancyService'; +// import LedgerStorageService from '@/services/Accounting/LedgerStorageService'; +// import { +// AccountNormal, +// ILedgerEntry, +// ISaleReceipt, +// IItemEntry, +// } from '@/interfaces'; +// import Ledger from '@/services/Accounting/Ledger'; + +// @Service() +// export class SaleReceiptGLEntries { +// @Inject() +// private tenancy: HasTenancyService; + +// @Inject() +// private ledgerStorage: LedgerStorageService; + +// /** +// * Creates income GL entries. +// * @param {number} tenantId +// * @param {number} saleReceiptId +// * @param {Knex.Transaction} trx +// */ +// public writeIncomeGLEntries = async ( +// tenantId: number, +// saleReceiptId: number, +// trx?: Knex.Transaction +// ): Promise => { +// const { SaleReceipt } = this.tenancy.models(tenantId); + +// const saleReceipt = await SaleReceipt.query(trx) +// .findById(saleReceiptId) +// .withGraphFetched('entries.item'); + +// // Retrieve the income entries ledger. +// const incomeLedger = this.getIncomeEntriesLedger(saleReceipt); + +// // Commits the ledger entries to the storage. +// await this.ledgerStorage.commit(tenantId, incomeLedger, trx); +// }; + +// /** +// * Reverts the receipt GL entries. +// * @param {number} tenantId +// * @param {number} saleReceiptId +// * @param {Knex.Transaction} trx +// * @returns {Promise} +// */ +// public revertReceiptGLEntries = async ( +// tenantId: number, +// saleReceiptId: number, +// trx?: Knex.Transaction +// ): Promise => { +// await this.ledgerStorage.deleteByReference( +// tenantId, +// saleReceiptId, +// 'SaleReceipt', +// trx +// ); +// }; + +// /** +// * Rewrites the receipt GL entries. +// * @param {number} tenantId +// * @param {number} saleReceiptId +// * @param {Knex.Transaction} trx +// * @returns {Promise} +// */ +// public rewriteReceiptGLEntries = async ( +// tenantId: number, +// saleReceiptId: number, +// trx?: Knex.Transaction +// ): Promise => { +// // Reverts the receipt GL entries. +// await this.revertReceiptGLEntries(tenantId, saleReceiptId, trx); + +// // Writes the income GL entries. +// await this.writeIncomeGLEntries(tenantId, saleReceiptId, trx); +// }; + +// /** +// * Retrieves the income GL ledger. +// * @param {ISaleReceipt} saleReceipt +// * @returns {Ledger} +// */ +// private getIncomeEntriesLedger = (saleReceipt: ISaleReceipt): Ledger => { +// const entries = this.getIncomeGLEntries(saleReceipt); + +// return new Ledger(entries); +// }; + +// /** +// * Retireves the income GL common entry. +// * @param {ISaleReceipt} saleReceipt - +// */ +// private getIncomeGLCommonEntry = (saleReceipt: ISaleReceipt) => { +// return { +// currencyCode: saleReceipt.currencyCode, +// exchangeRate: saleReceipt.exchangeRate, + +// transactionType: 'SaleReceipt', +// transactionId: saleReceipt.id, + +// date: saleReceipt.receiptDate, + +// transactionNumber: saleReceipt.receiptNumber, +// referenceNumber: saleReceipt.referenceNo, + +// createdAt: saleReceipt.createdAt, + +// credit: 0, +// debit: 0, + +// userId: saleReceipt.userId, +// branchId: saleReceipt.branchId, +// }; +// }; + +// /** +// * Retrieve receipt income item GL entry. +// * @param {ISaleReceipt} saleReceipt - +// * @param {IItemEntry} entry - +// * @param {number} index - +// * @returns {ILedgerEntry} +// */ +// private getReceiptIncomeItemEntry = R.curry( +// ( +// saleReceipt: ISaleReceipt, +// entry: IItemEntry, +// index: number +// ): ILedgerEntry => { +// const commonEntry = this.getIncomeGLCommonEntry(saleReceipt); +// const itemIncome = entry.amount * saleReceipt.exchangeRate; + +// return { +// ...commonEntry, +// credit: itemIncome, +// accountId: entry.item.sellAccountId, +// note: entry.description, +// index: index + 2, +// itemId: entry.itemId, +// itemQuantity: entry.quantity, +// accountNormal: AccountNormal.CREDIT, +// }; +// } +// ); + +// /** +// * Retrieves the receipt deposit GL deposit entry. +// * @param {ISaleReceipt} saleReceipt +// * @returns {ILedgerEntry} +// */ +// private getReceiptDepositEntry = ( +// saleReceipt: ISaleReceipt +// ): ILedgerEntry => { +// const commonEntry = this.getIncomeGLCommonEntry(saleReceipt); + +// return { +// ...commonEntry, +// debit: saleReceipt.localAmount, +// accountId: saleReceipt.depositAccountId, +// index: 1, +// accountNormal: AccountNormal.DEBIT, +// }; +// }; + +// /** +// * Retrieves the income GL entries. +// * @param {ISaleReceipt} saleReceipt - +// * @returns {ILedgerEntry[]} +// */ +// private getIncomeGLEntries = (saleReceipt: ISaleReceipt): ILedgerEntry[] => { +// const getItemEntry = this.getReceiptIncomeItemEntry(saleReceipt); + +// const creditEntries = saleReceipt.entries.map(getItemEntry); +// const depositEntry = this.getReceiptDepositEntry(saleReceipt); + +// return [depositEntry, ...creditEntries]; +// }; +// } diff --git a/packages/server-nest/src/modules/SaleReceipts/commands/SaleReceiptIncrement.service.ts b/packages/server-nest/src/modules/SaleReceipts/commands/SaleReceiptIncrement.service.ts new file mode 100644 index 000000000..4b95703f9 --- /dev/null +++ b/packages/server-nest/src/modules/SaleReceipts/commands/SaleReceiptIncrement.service.ts @@ -0,0 +1,30 @@ +import { Injectable } from '@nestjs/common'; +import { AutoIncrementOrdersService } from '@/modules/AutoIncrementOrders/AutoIncrementOrders.service'; + +@Injectable() +export class SaleReceiptIncrement { + constructor( + private readonly autoIncrementOrdersService: AutoIncrementOrdersService, + ) {} + + /** + * Retrieve the next unique receipt number. + * @param {number} tenantId - Tenant id. + * @return {string} + */ + public getNextReceiptNumber(): string { + return this.autoIncrementOrdersService.getNextTransactionNumber( + 'sales_receipts', + ); + } + + /** + * Increment the receipt next number. + * @param {number} tenantId - + */ + public incrementNextReceiptNumber() { + return this.autoIncrementOrdersService.incrementSettingsNextNumber( + 'sales_receipts', + ); + } +} diff --git a/packages/server-nest/src/modules/SaleReceipts/commands/SaleReceiptInventoryTransactions.ts b/packages/server-nest/src/modules/SaleReceipts/commands/SaleReceiptInventoryTransactions.ts new file mode 100644 index 000000000..6ba7d6335 --- /dev/null +++ b/packages/server-nest/src/modules/SaleReceipts/commands/SaleReceiptInventoryTransactions.ts @@ -0,0 +1,72 @@ +// import { Knex } from 'knex'; +// import { Inject, Service } from 'typedi'; +// import { ISaleReceipt } from '@/interfaces'; +// import InventoryService from '@/services/Inventory/Inventory'; +// import ItemsEntriesService from '@/services/Items/ItemsEntriesService'; + +// @Service() +// export class SaleReceiptInventoryTransactions { +// @Inject() +// private inventoryService: InventoryService; + +// @Inject() +// private itemsEntriesService: ItemsEntriesService; + +// /** +// * Records the inventory transactions from the given bill input. +// * @param {Bill} bill - Bill model object. +// * @param {number} billId - Bill id. +// * @return {Promise} +// */ +// public async recordInventoryTransactions( +// tenantId: number, +// saleReceipt: ISaleReceipt, +// override?: boolean, +// trx?: Knex.Transaction +// ): Promise { +// // Loads the inventory items entries of the given sale invoice. +// const inventoryEntries = +// await this.itemsEntriesService.filterInventoryEntries( +// tenantId, +// saleReceipt.entries +// ); +// const transaction = { +// transactionId: saleReceipt.id, +// transactionType: 'SaleReceipt', +// transactionNumber: saleReceipt.receiptNumber, +// exchangeRate: saleReceipt.exchangeRate, + +// date: saleReceipt.receiptDate, +// direction: 'OUT', +// entries: inventoryEntries, +// createdAt: saleReceipt.createdAt, + +// warehouseId: saleReceipt.warehouseId, +// }; +// return this.inventoryService.recordInventoryTransactionsFromItemsEntries( +// tenantId, +// transaction, +// override, +// trx +// ); +// } + +// /** +// * Reverts the inventory transactions of the given bill id. +// * @param {number} tenantId - Tenant id. +// * @param {number} billId - Bill id. +// * @return {Promise} +// */ +// public async revertInventoryTransactions( +// tenantId: number, +// receiptId: number, +// trx?: Knex.Transaction +// ) { +// return this.inventoryService.deleteInventoryTransactions( +// tenantId, +// receiptId, +// 'SaleReceipt', +// trx +// ); +// } +// } diff --git a/packages/server-nest/src/modules/SaleReceipts/commands/SaleReceiptMailNotification.ts b/packages/server-nest/src/modules/SaleReceipts/commands/SaleReceiptMailNotification.ts new file mode 100644 index 000000000..9cbb00563 --- /dev/null +++ b/packages/server-nest/src/modules/SaleReceipts/commands/SaleReceiptMailNotification.ts @@ -0,0 +1,220 @@ +// import HasTenancyService from '@/services/Tenancy/TenancyService'; +// import { Inject, Service } from 'typedi'; +// import Mail from '@/lib/Mail'; +// import { GetSaleReceipt } from '../queries/GetSaleReceipt'; +// import { SaleReceiptsPdf } from '../queries/SaleReceiptsPdfService'; +// import { +// DEFAULT_RECEIPT_MAIL_CONTENT, +// DEFAULT_RECEIPT_MAIL_SUBJECT, +// } from '../constants'; +// import { +// ISaleReceiptMailPresend, +// SaleReceiptMailOpts, +// SaleReceiptMailOptsDTO, +// } from '@/interfaces'; +// import { ContactMailNotification } from '@/services/MailNotification/ContactMailNotification'; +// import { mergeAndValidateMailOptions } from '@/services/MailNotification/utils'; +// import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; +// import events from '@/subscribers/events'; +// import { transformReceiptToMailDataArgs } from '../utils'; + +// @Service() +// export class SaleReceiptMailNotification { +// @Inject() +// private tenancy: HasTenancyService; + +// @Inject() +// private getSaleReceiptService: GetSaleReceipt; + +// @Inject() +// private receiptPdfService: SaleReceiptsPdf; + +// @Inject() +// private contactMailNotification: ContactMailNotification; + +// @Inject() +// private eventPublisher: EventPublisher; + +// @Inject('agenda') +// private agenda: any; + +// /** +// * Sends the receipt mail of the given sale receipt. +// * @param {number} tenantId +// * @param {number} saleReceiptId +// * @param {SaleReceiptMailOptsDTO} messageDTO +// */ +// public async triggerMail( +// tenantId: number, +// saleReceiptId: number, +// messageOptions: SaleReceiptMailOptsDTO +// ) { +// const payload = { +// tenantId, +// saleReceiptId, +// messageOpts: messageOptions, +// }; +// await this.agenda.now('sale-receipt-mail-send', payload); + +// // Triggers the event `onSaleReceiptPreMailSend`. +// await this.eventPublisher.emitAsync(events.saleReceipt.onPreMailSend, { +// tenantId, +// saleReceiptId, +// messageOptions, +// } as ISaleReceiptMailPresend); +// } + +// /** +// * Retrieves the mail options of the given sale receipt. +// * @param {number} tenantId +// * @param {number} saleReceiptId +// * @returns {Promise} +// */ +// public async getMailOptions( +// tenantId: number, +// saleReceiptId: number +// ): Promise { +// const { SaleReceipt } = this.tenancy.models(tenantId); + +// const saleReceipt = await SaleReceipt.query() +// .findById(saleReceiptId) +// .throwIfNotFound(); + +// const formatArgs = await this.textFormatterArgs(tenantId, saleReceiptId); + +// const mailOptions = +// await this.contactMailNotification.getDefaultMailOptions( +// tenantId, +// saleReceipt.customerId +// ); +// return { +// ...mailOptions, +// message: DEFAULT_RECEIPT_MAIL_CONTENT, +// subject: DEFAULT_RECEIPT_MAIL_SUBJECT, +// attachReceipt: true, +// formatArgs, +// }; +// } + +// /** +// * Retrieves the formatted text of the given sale receipt. +// * @param {number} tenantId - Tenant id. +// * @param {number} receiptId - Sale receipt id. +// * @param {string} text - The given text. +// * @returns {Promise} +// */ +// public textFormatterArgs = async ( +// tenantId: number, +// receiptId: number +// ): Promise> => { +// const receipt = await this.getSaleReceiptService.getSaleReceipt( +// tenantId, +// receiptId +// ); +// return transformReceiptToMailDataArgs(receipt); +// }; + +// /** +// * Formats the mail options of the given sale receipt. +// * @param {number} tenantId +// * @param {number} receiptId +// * @param {SaleReceiptMailOpts} mailOptions +// * @returns {Promise} +// */ +// public async formatEstimateMailOptions( +// tenantId: number, +// receiptId: number, +// mailOptions: SaleReceiptMailOpts +// ): Promise { +// const formatterArgs = await this.textFormatterArgs(tenantId, receiptId); +// const formattedOptions = +// (await this.contactMailNotification.formatMailOptions( +// tenantId, +// mailOptions, +// formatterArgs +// )) as SaleReceiptMailOpts; +// return formattedOptions; +// } + +// /** +// * Retrieves the formatted mail options of the given sale receipt. +// * @param {number} tenantId +// * @param {number} saleReceiptId +// * @param {SaleReceiptMailOptsDTO} messageOpts +// * @returns {Promise} +// */ +// public getFormatMailOptions = async ( +// tenantId: number, +// saleReceiptId: number, +// messageOpts: SaleReceiptMailOptsDTO +// ): Promise => { +// const defaultMessageOptions = await this.getMailOptions( +// tenantId, +// saleReceiptId +// ); +// // Merges message opts with default options. +// const parsedMessageOpts = mergeAndValidateMailOptions( +// defaultMessageOptions, +// messageOpts +// ) as SaleReceiptMailOpts; + +// // Formats the message options. +// return this.formatEstimateMailOptions( +// tenantId, +// saleReceiptId, +// parsedMessageOpts +// ); +// }; + +// /** +// * Triggers the mail notification of the given sale receipt. +// * @param {number} tenantId - Tenant id. +// * @param {number} saleReceiptId - Sale receipt id. +// * @param {SaleReceiptMailOpts} messageDTO - message options. +// * @returns {Promise} +// */ +// public async sendMail( +// tenantId: number, +// saleReceiptId: number, +// messageOpts: SaleReceiptMailOptsDTO +// ) { +// // Formats the message options. +// const formattedMessageOptions = await this.getFormatMailOptions( +// tenantId, +// saleReceiptId, +// messageOpts +// ); +// const mail = new Mail() +// .setSubject(formattedMessageOptions.subject) +// .setTo(formattedMessageOptions.to) +// .setCC(formattedMessageOptions.cc) +// .setBCC(formattedMessageOptions.bcc) +// .setContent(formattedMessageOptions.message); + +// // Attaches the receipt pdf document. +// if (formattedMessageOptions.attachReceipt) { +// // Retrieves document buffer of the receipt pdf document. +// const [receiptPdfBuffer, filename] = +// await this.receiptPdfService.saleReceiptPdf(tenantId, saleReceiptId); + +// mail.setAttachments([ +// { filename: `${filename}.pdf`, content: receiptPdfBuffer }, +// ]); +// } +// const eventPayload = { +// tenantId, +// saleReceiptId, +// messageOptions: {}, +// }; +// await this.eventPublisher.emitAsync( +// events.saleReceipt.onMailSend, +// eventPayload +// ); +// await mail.send(); + +// await this.eventPublisher.emitAsync( +// events.saleReceipt.onMailSent, +// eventPayload +// ); +// } +// } diff --git a/packages/server-nest/src/modules/SaleReceipts/commands/SaleReceiptMailNotificationJob.ts b/packages/server-nest/src/modules/SaleReceipts/commands/SaleReceiptMailNotificationJob.ts new file mode 100644 index 000000000..071df4ecc --- /dev/null +++ b/packages/server-nest/src/modules/SaleReceipts/commands/SaleReceiptMailNotificationJob.ts @@ -0,0 +1,36 @@ +// import Container, { Service } from 'typedi'; +// import { SaleReceiptMailNotification } from './SaleReceiptMailNotification'; + +// @Service() +// export class SaleReceiptMailNotificationJob { +// /** +// * Constructor method. +// */ +// constructor(agenda) { +// agenda.define( +// 'sale-receipt-mail-send', +// { priority: 'high', concurrency: 2 }, +// this.handler +// ); +// } + +// /** +// * Triggers sending invoice mail. +// */ +// private handler = async (job, done: Function) => { +// const { tenantId, saleReceiptId, messageOpts } = job.attrs.data; +// const receiveMailNotification = Container.get(SaleReceiptMailNotification); + +// try { +// await receiveMailNotification.sendMail( +// tenantId, +// saleReceiptId, +// messageOpts +// ); +// done(); +// } catch (error) { +// console.log(error); +// done(error); +// } +// }; +// } diff --git a/packages/server-nest/src/modules/SaleReceipts/commands/SaleReceiptNotifyBySms.ts b/packages/server-nest/src/modules/SaleReceipts/commands/SaleReceiptNotifyBySms.ts new file mode 100644 index 000000000..bbdcbe087 --- /dev/null +++ b/packages/server-nest/src/modules/SaleReceipts/commands/SaleReceiptNotifyBySms.ts @@ -0,0 +1,206 @@ +// import { Service, Inject } from 'typedi'; +// import HasTenancyService from '@/services/Tenancy/TenancyService'; +// import events from '@/subscribers/events'; +// import { +// ISaleReceiptSmsDetails, +// ISaleReceipt, +// SMS_NOTIFICATION_KEY, +// ICustomer, +// } from '@/interfaces'; +// import SmsNotificationsSettingsService from '@/services/Settings/SmsNotificationsSettings'; +// import { formatNumber, formatSmsMessage } from 'utils'; +// import { TenantMetadata } from '@/system/models'; +// import { ServiceError } from '@/exceptions'; +// import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; +// import SaleNotifyBySms from '../SaleNotifyBySms'; +// import { ERRORS } from './constants'; + +// @Service() +// export class SaleReceiptNotifyBySms { +// @Inject() +// private tenancy: HasTenancyService; + +// @Inject() +// private eventPublisher: EventPublisher; + +// @Inject() +// private smsNotificationsSettings: SmsNotificationsSettingsService; + +// @Inject() +// private saleSmsNotification: SaleNotifyBySms; + +// /** +// * Notify customer via sms about sale receipt. +// * @param {number} tenantId - Tenant id. +// * @param {number} saleReceiptId - Sale receipt id. +// */ +// public async notifyBySms(tenantId: number, saleReceiptId: number) { +// const { SaleReceipt } = this.tenancy.models(tenantId); + +// // Retrieve the sale receipt or throw not found service error. +// const saleReceipt = await SaleReceipt.query() +// .findById(saleReceiptId) +// .withGraphFetched('customer'); + +// // Validates the receipt receipt existance. +// this.validateSaleReceiptExistance(saleReceipt); + +// // Validate the customer phone number. +// this.saleSmsNotification.validateCustomerPhoneNumber( +// saleReceipt.customer.personalPhone +// ); +// // Triggers `onSaleReceiptNotifySms` event. +// await this.eventPublisher.emitAsync(events.saleReceipt.onNotifySms, { +// tenantId, +// saleReceipt, +// }); +// // Sends the payment receive sms notification to the given customer. +// await this.sendSmsNotification(tenantId, saleReceipt); + +// // Triggers `onSaleReceiptNotifiedSms` event. +// await this.eventPublisher.emitAsync(events.saleReceipt.onNotifiedSms, { +// tenantId, +// saleReceipt, +// }); +// return saleReceipt; +// } + +// /** +// * Sends SMS notification. +// * @param {ISaleReceipt} invoice +// * @param {ICustomer} customer +// * @returns +// */ +// public sendSmsNotification = async ( +// tenantId: number, +// saleReceipt: ISaleReceipt & { customer: ICustomer } +// ) => { +// const smsClient = this.tenancy.smsClient(tenantId); +// const tenantMetadata = await TenantMetadata.query().findOne({ tenantId }); + +// // Retrieve formatted sms notification message of receipt details. +// const formattedSmsMessage = this.formattedReceiptDetailsMessage( +// tenantId, +// saleReceipt, +// tenantMetadata +// ); +// const phoneNumber = saleReceipt.customer.personalPhone; + +// // Run the send sms notification message job. +// return smsClient.sendMessageJob(phoneNumber, formattedSmsMessage); +// }; + +// /** +// * Notify via SMS message after receipt creation. +// * @param {number} tenantId +// * @param {number} receiptId +// * @returns {Promise} +// */ +// public notifyViaSmsAfterCreation = async ( +// tenantId: number, +// receiptId: number +// ): Promise => { +// const notification = this.smsNotificationsSettings.getSmsNotificationMeta( +// tenantId, +// SMS_NOTIFICATION_KEY.SALE_RECEIPT_DETAILS +// ); +// // Can't continue if the sms auto-notification is not enabled. +// if (!notification.isNotificationEnabled) return; + +// await this.notifyBySms(tenantId, receiptId); +// }; + +// /** +// * Retrieve the formatted sms notification message of the given sale receipt. +// * @param {number} tenantId +// * @param {ISaleReceipt} saleReceipt +// * @param {TenantMetadata} tenantMetadata +// * @returns {string} +// */ +// private formattedReceiptDetailsMessage = ( +// tenantId: number, +// saleReceipt: ISaleReceipt & { customer: ICustomer }, +// tenantMetadata: TenantMetadata +// ): string => { +// const notification = this.smsNotificationsSettings.getSmsNotificationMeta( +// tenantId, +// SMS_NOTIFICATION_KEY.SALE_RECEIPT_DETAILS +// ); +// return this.formatReceiptDetailsMessage( +// notification.smsMessage, +// saleReceipt, +// tenantMetadata +// ); +// }; + +// /** +// * Formattes the receipt sms notification message. +// * @param {string} smsMessage +// * @param {ISaleReceipt} saleReceipt +// * @param {TenantMetadata} tenantMetadata +// * @returns {string} +// */ +// private formatReceiptDetailsMessage = ( +// smsMessage: string, +// saleReceipt: ISaleReceipt & { customer: ICustomer }, +// tenantMetadata: TenantMetadata +// ): string => { +// // Format the receipt amount. +// const formattedAmount = formatNumber(saleReceipt.amount, { +// currencyCode: saleReceipt.currencyCode, +// }); + +// return formatSmsMessage(smsMessage, { +// ReceiptNumber: saleReceipt.receiptNumber, +// ReferenceNumber: saleReceipt.referenceNo, +// CustomerName: saleReceipt.customer.displayName, +// Amount: formattedAmount, +// CompanyName: tenantMetadata.name, +// }); +// }; + +// /** +// * Retrieve the SMS details of the given invoice. +// * @param {number} tenantId - +// * @param {number} saleReceiptId - Sale receipt id. +// */ +// public smsDetails = async ( +// tenantId: number, +// saleReceiptId: number +// ): Promise => { +// const { SaleReceipt } = this.tenancy.models(tenantId); + +// // Retrieve the sale receipt or throw not found service error. +// const saleReceipt = await SaleReceipt.query() +// .findById(saleReceiptId) +// .withGraphFetched('customer'); + +// // Validates the receipt receipt existance. +// this.validateSaleReceiptExistance(saleReceipt); + +// // Current tenant metadata. +// const tenantMetadata = await TenantMetadata.query().findOne({ tenantId }); + +// // Retrieve the sale receipt formatted sms notification message. +// const formattedSmsMessage = this.formattedReceiptDetailsMessage( +// tenantId, +// saleReceipt, +// tenantMetadata +// ); +// return { +// customerName: saleReceipt.customer.displayName, +// customerPhoneNumber: saleReceipt.customer.personalPhone, +// smsMessage: formattedSmsMessage, +// }; +// }; + +// /** +// * Validates the receipt receipt existance. +// * @param {ISaleReceipt|null} saleReceipt +// */ +// private validateSaleReceiptExistance(saleReceipt: ISaleReceipt | null) { +// if (!saleReceipt) { +// throw new ServiceError(ERRORS.SALE_RECEIPT_NOT_FOUND); +// } +// } +// } diff --git a/packages/server-nest/src/modules/SaleReceipts/commands/SaleReceiptValidators.service.ts b/packages/server-nest/src/modules/SaleReceipts/commands/SaleReceiptValidators.service.ts new file mode 100644 index 000000000..3068929cb --- /dev/null +++ b/packages/server-nest/src/modules/SaleReceipts/commands/SaleReceiptValidators.service.ts @@ -0,0 +1,100 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { ERRORS } from '../constants'; +import { SaleReceipt } from '../models/SaleReceipt'; +import { Account } from '@/modules/Accounts/models/Account.model'; +import { ServiceError } from '@/modules/Items/ServiceError'; +import { ACCOUNT_PARENT_TYPE } from '@/constants/accounts'; + +@Injectable() +export class SaleReceiptValidators { + /** + * @param {typeof SaleReceipt} saleReceiptModel - Sale receipt model. + * @param {typeof Account} accountModel - Account model. + */ + constructor( + @Inject(SaleReceipt) private saleReceiptModel: typeof SaleReceipt, + @Inject(Account) private accountModel: typeof Account, + ) {} + + /** + * Validates the sale receipt existence. + * @param {SaleEstimate | undefined | null} estimate + */ + public validateReceiptExistence(receipt: SaleReceipt | undefined | null) { + if (!receipt) { + throw new ServiceError(ERRORS.SALE_RECEIPT_NOT_FOUND); + } + } + + /** + * Validates the receipt not closed. + * @param {SaleReceipt} receipt + */ + public validateReceiptNotClosed(receipt: SaleReceipt) { + if (receipt.isClosed) { + throw new ServiceError(ERRORS.SALE_RECEIPT_IS_ALREADY_CLOSED); + } + } + + /** + * Validate whether sale receipt deposit account exists on the storage. + * @param {number} accountId - Account id. + */ + public async validateReceiptDepositAccountExistence(accountId: number) { + const depositAccount = await this.accountModel.query().findById(accountId); + + if (!depositAccount) { + throw new ServiceError(ERRORS.DEPOSIT_ACCOUNT_NOT_FOUND); + } + if (!depositAccount.isParentType(ACCOUNT_PARENT_TYPE.CURRENT_ASSET)) { + throw new ServiceError(ERRORS.DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET); + } + } + + /** + * Validate sale receipt number uniqueness on the storage. + * @param {string} receiptNumber - + * @param {number} notReceiptId - + */ + public async validateReceiptNumberUnique( + receiptNumber: string, + notReceiptId?: number, + ) { + const saleReceipt = await this.saleReceiptModel + .query() + .findOne('receipt_number', receiptNumber) + .onBuild((builder) => { + if (notReceiptId) { + builder.whereNot('id', notReceiptId); + } + }); + + if (saleReceipt) { + throw new ServiceError(ERRORS.SALE_RECEIPT_NUMBER_NOT_UNIQUE); + } + } + + /** + * Validate the sale receipt number require. + * @param {ISaleReceipt} saleReceipt + */ + public validateReceiptNoRequire(receiptNumber: string) { + if (!receiptNumber) { + throw new ServiceError(ERRORS.SALE_RECEIPT_NO_IS_REQUIRED); + } + } + + /** + * Validate the given customer has no sales receipts. + * @param {number} customerId - Customer id. + */ + public async validateCustomerHasNoReceipts(customerId: number) { + const receipts = await this.saleReceiptModel + .query() + .where('customer_id', customerId); + + if (receipts.length > 0) { + throw new ServiceError(ERRORS.CUSTOMER_HAS_SALES_INVOICES); + } + } +} diff --git a/packages/server-nest/src/modules/SaleReceipts/commands/SaleReceiptsExportable.ts b/packages/server-nest/src/modules/SaleReceipts/commands/SaleReceiptsExportable.ts new file mode 100644 index 000000000..a8f91bea5 --- /dev/null +++ b/packages/server-nest/src/modules/SaleReceipts/commands/SaleReceiptsExportable.ts @@ -0,0 +1,35 @@ +// import { Inject, Service } from 'typedi'; +// import { ISalesReceiptsFilter } from '@/interfaces'; +// import { Exportable } from '@/services/Export/Exportable'; +// import { SaleReceiptApplication } from './SaleReceiptApplication'; +// import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants'; + +// @Service() +// export class SaleReceiptsExportable extends Exportable { +// @Inject() +// private saleReceiptsApp: SaleReceiptApplication; + +// /** +// * Retrieves the accounts data to exportable sheet. +// * @param {number} tenantId +// * @returns +// */ +// public exportable(tenantId: number, query: ISalesReceiptsFilter) { +// const filterQuery = (query) => { +// query.withGraphFetched('branch'); +// query.withGraphFetched('warehouse'); +// }; +// const parsedQuery = { +// sortOrder: 'desc', +// columnSortBy: 'created_at', +// ...query, +// page: 1, +// pageSize: EXPORT_SIZE_LIMIT, +// filterQuery, +// } as ISalesReceiptsFilter; + +// return this.saleReceiptsApp +// .getSaleReceipts(tenantId, parsedQuery) +// .then((output) => output.data); +// } +// } diff --git a/packages/server-nest/src/modules/SaleReceipts/commands/SaleReceiptsImportable.ts b/packages/server-nest/src/modules/SaleReceipts/commands/SaleReceiptsImportable.ts new file mode 100644 index 000000000..601f96a3f --- /dev/null +++ b/packages/server-nest/src/modules/SaleReceipts/commands/SaleReceiptsImportable.ts @@ -0,0 +1,45 @@ +// import { Inject, Service } from 'typedi'; +// import { Knex } from 'knex'; +// import { IAccountCreateDTO, ISaleReceiptDTO } from '@/interfaces'; +// import { CreateSaleReceipt } from './commands/CreateSaleReceipt.service'; +// import { Importable } from '@/services/Import/Importable'; +// import { SaleReceiptsSampleData } from './constants'; + +// @Service() +// export class SaleReceiptsImportable extends Importable { +// @Inject() +// private createReceiptService: CreateSaleReceipt; + +// /** +// * Importing to sale receipts service. +// * @param {number} tenantId +// * @param {IAccountCreateDTO} createAccountDTO +// * @returns +// */ +// public importable( +// tenantId: number, +// createAccountDTO: ISaleReceiptDTO, +// trx?: Knex.Transaction +// ) { +// return this.createReceiptService.createSaleReceipt( +// tenantId, +// createAccountDTO, +// trx +// ); +// } + +// /** +// * Concurrrency controlling of the importing process. +// * @returns {number} +// */ +// public get concurrency() { +// return 1; +// } + +// /** +// * Retrieves the sample data that used to download accounts sample sheet. +// */ +// public sampleData(): any[] { +// return SaleReceiptsSampleData; +// } +// } diff --git a/packages/server-nest/src/modules/SaleReceipts/constants.ts b/packages/server-nest/src/modules/SaleReceipts/constants.ts new file mode 100644 index 000000000..b1a1c3898 --- /dev/null +++ b/packages/server-nest/src/modules/SaleReceipts/constants.ts @@ -0,0 +1,123 @@ +export const DEFAULT_RECEIPT_MAIL_SUBJECT = + 'Receipt {Receipt Number} from {Company Name}'; +export const DEFAULT_RECEIPT_MAIL_CONTENT = ` +

Dear {Customer Name}

+

Thank you for your business, You can view or print your receipt from attachements.

+

+Receipt #{Receipt Number}
+Amount : {Receipt Amount}
+

+ +

+Regards
+{Company Name} +

+`; + +export const ERRORS = { + SALE_RECEIPT_NOT_FOUND: 'SALE_RECEIPT_NOT_FOUND', + DEPOSIT_ACCOUNT_NOT_FOUND: 'DEPOSIT_ACCOUNT_NOT_FOUND', + DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET: 'DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET', + SALE_RECEIPT_NUMBER_NOT_UNIQUE: 'SALE_RECEIPT_NUMBER_NOT_UNIQUE', + SALE_RECEIPT_IS_ALREADY_CLOSED: 'SALE_RECEIPT_IS_ALREADY_CLOSED', + SALE_RECEIPT_NO_IS_REQUIRED: 'SALE_RECEIPT_NO_IS_REQUIRED', + CUSTOMER_HAS_SALES_INVOICES: 'CUSTOMER_HAS_SALES_INVOICES', + NO_INVOICE_CUSTOMER_EMAIL_ADDR: 'NO_INVOICE_CUSTOMER_EMAIL_ADDR', +}; + +export const DEFAULT_VIEW_COLUMNS = []; +export const DEFAULT_VIEWS = [ + { + name: 'Draft', + slug: 'draft', + rolesLogicExpression: '1', + roles: [ + { index: 1, fieldKey: 'status', comparator: 'equals', value: 'draft' }, + ], + columns: DEFAULT_VIEW_COLUMNS, + }, + { + name: 'Closed', + slug: 'closed', + rolesLogicExpression: '1', + roles: [ + { index: 1, fieldKey: 'status', comparator: 'equals', value: 'closed' }, + ], + columns: DEFAULT_VIEW_COLUMNS, + }, +]; + +export const SaleReceiptsSampleData = [ + { + 'Receipt Date': '2023-01-01', + Customer: 'Randall Kohler', + 'Deposit Account': 'Petty Cash', + 'Exchange Rate': '', + 'Receipt Number': 'REC-00001', + 'Reference No.': 'REF-0001', + Statement: 'Delectus unde aut soluta et accusamus placeat.', + 'Receipt Message': 'Vitae asperiores dicta.', + Closed: 'T', + Item: 'Schmitt Group', + Quantity: 100, + Rate: 200, + 'Line Description': + 'Distinctio distinctio sit veritatis consequatur iste quod veritatis.', + }, +]; + +export const defaultSaleReceiptBrandingAttributes = { + primaryColor: '', + secondaryColor: '', + companyName: 'Bigcapital Technology, Inc.', + + // # Company logo + showCompanyLogo: true, + companyLogoUri: '', + companyLogoKey: '', + + // # Customer address + showCustomerAddress: true, + customerAddress: '', + + // # Company address + showCompanyAddress: true, + companyAddress: '', + billedToLabel: 'Billed To', + + // # Total + total: '$1000.00', + totalLabel: 'Total', + showTotal: true, + + subtotal: '1000/00', + subtotalLabel: 'Subtotal', + showSubtotal: true, + + showCustomerNote: true, + customerNote: + 'It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.', + customerNoteLabel: 'Customer Note', + + showTermsConditions: true, + termsConditions: + 'It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.', + termsConditionsLabel: 'Terms & Conditions', + + lines: [ + { + item: 'Simply dummy text', + description: 'Simply dummy text of the printing and typesetting', + rate: '1', + quantity: '1000', + total: '$1000.00', + }, + ], + showReceiptNumber: true, + receiptNumberLabel: 'Receipt Number', + receiptNumebr: '346D3D40-0001', + + receiptDate: 'September 3, 2024', + showReceiptDate: true, + receiptDateLabel: 'Receipt Date', +}; diff --git a/packages/server-nest/src/modules/SaleReceipts/models/SaleReceipt.ts b/packages/server-nest/src/modules/SaleReceipts/models/SaleReceipt.ts new file mode 100644 index 000000000..8f8b784ff --- /dev/null +++ b/packages/server-nest/src/modules/SaleReceipts/models/SaleReceipt.ts @@ -0,0 +1,253 @@ +import { Model, mixin } from 'objection'; +// import TenantModel from 'models/TenantModel'; +// import ModelSetting from './ModelSetting'; +// import SaleReceiptSettings from './SaleReceipt.Settings'; +// import CustomViewBaseModel from './CustomViewBaseModel'; +// import { DEFAULT_VIEWS } from '@/services/Sales/Receipts/constants'; +// import ModelSearchable from './ModelSearchable'; +import { BaseModel } from '@/models/Model'; + +export class SaleReceipt extends BaseModel { + amount: number; + exchangeRate: number; + currencyCode: string; + depositAccountId: number; + customerId: number; + receiptDate: Date; + receiptNumber: string; + referenceNo: string; + sendToEmail: string; + receiptMessage: string; + statement: string; + closedAt: Date | string; + + branchId: number; + warehouseId: number; + + createdAt: Date; + updatedAt: Date | null; + + /** + * Table name + */ + static get tableName() { + return 'sales_receipts'; + } + + /** + * Timestamps columns. + */ + get timestamps() { + return ['created_at', 'updated_at']; + } + + /** + * Virtual attributes. + */ + static get virtualAttributes() { + return ['localAmount', 'isClosed', 'isDraft']; + } + + /** + * Estimate amount in local currency. + * @returns {number} + */ + get localAmount() { + return this.amount * this.exchangeRate; + } + + /** + * Detarmine whether the sale receipt closed. + * @return {boolean} + */ + get isClosed() { + return !!this.closedAt; + } + + /** + * Detarmines whether the sale receipt drafted. + * @return {boolean} + */ + get isDraft() { + return !this.closedAt; + } + + /** + * Model modifiers. + */ + static get modifiers() { + return { + /** + * Filters the closed receipts. + */ + closed(query) { + query.whereNot('closed_at', null); + }, + + /** + * Filters the invoices in draft status. + */ + draft(query) { + query.where('closed_at', null); + }, + + /** + * Sorting the receipts order by status. + */ + sortByStatus(query, order) { + query.orderByRaw(`CLOSED_AT IS NULL ${order}`); + }, + + /** + * Filtering the receipts orders by status. + */ + filterByStatus(query, status) { + switch (status) { + case 'draft': + query.modify('draft'); + break; + case 'closed': + default: + query.modify('closed'); + break; + } + }, + }; + } + + /** + * Relationship mapping. + */ + static get relationMappings() { + const Customer = require('@/modules/Customers/models/Customer'); + const Account = require('@/modules/Accounts/models/Account.model'); + const AccountTransaction = require('@/modules/AccountsTransactions/models/AccountTransaction.model'); + const ItemEntry = require('@/modules/ItemsEntries/models/ItemEntry'); + const Branch = require('@/modules/Branches/models/Branch'); + const Document = require('@/modules/Documents/models/Document'); + const Warehouse = require('@/modules/Warehouses/models/Warehouse'); + + return { + customer: { + relation: Model.BelongsToOneRelation, + modelClass: Customer.default, + join: { + from: 'sales_receipts.customerId', + to: 'contacts.id', + }, + filter(query) { + query.where('contact_service', 'customer'); + }, + }, + + depositAccount: { + relation: Model.BelongsToOneRelation, + modelClass: Account.default, + join: { + from: 'sales_receipts.depositAccountId', + to: 'accounts.id', + }, + }, + + entries: { + relation: Model.HasManyRelation, + modelClass: ItemEntry.default, + join: { + from: 'sales_receipts.id', + to: 'items_entries.referenceId', + }, + filter(builder) { + builder.where('reference_type', 'SaleReceipt'); + builder.orderBy('index', 'ASC'); + }, + }, + + transactions: { + relation: Model.HasManyRelation, + modelClass: AccountTransaction.default, + join: { + from: 'sales_receipts.id', + to: 'accounts_transactions.referenceId', + }, + filter(builder) { + builder.where('reference_type', 'SaleReceipt'); + }, + }, + + /** + * Sale receipt may belongs to branch. + */ + branch: { + relation: Model.BelongsToOneRelation, + modelClass: Branch.default, + join: { + from: 'sales_receipts.branchId', + to: 'branches.id', + }, + }, + + /** + * Sale receipt may has associated warehouse. + */ + warehouse: { + relation: Model.BelongsToOneRelation, + modelClass: Warehouse.default, + join: { + from: 'sales_receipts.warehouseId', + to: 'warehouses.id', + }, + }, + + /** + * Sale receipt transaction may has many attached attachments. + */ + attachments: { + relation: Model.ManyToManyRelation, + modelClass: Document.default, + join: { + from: 'sales_receipts.id', + through: { + from: 'document_links.modelId', + to: 'document_links.documentId', + }, + to: 'documents.id', + }, + filter(query) { + query.where('model_ref', 'SaleReceipt'); + }, + }, + }; + } + + /** + * Sale invoice meta. + */ + // static get meta() { + // return SaleReceiptSettings; + // } + + /** + * Retrieve the default custom views, roles and columns. + */ + // static get defaultViews() { + // return DEFAULT_VIEWS; + // } + + /** + * Model search attributes. + */ + static get searchRoles() { + return [ + { fieldKey: 'receipt_number', comparator: 'contains' }, + { condition: 'or', fieldKey: 'reference_no', comparator: 'contains' }, + { condition: 'or', fieldKey: 'amount', comparator: 'equals' }, + ]; + } + + /** + * Prevents mutate base currency since the model is not empty. + */ + static get preventMutateBaseCurrency() { + return true; + } +} diff --git a/packages/server-nest/src/modules/SaleReceipts/queries/GetSaleReceipt.service.ts b/packages/server-nest/src/modules/SaleReceipts/queries/GetSaleReceipt.service.ts new file mode 100644 index 000000000..ad3a7174b --- /dev/null +++ b/packages/server-nest/src/modules/SaleReceipts/queries/GetSaleReceipt.service.ts @@ -0,0 +1,36 @@ +import { Injectable } from '@nestjs/common'; +import { SaleReceiptTransformer } from './SaleReceiptTransformer'; +import { SaleReceiptValidators } from '../commands/SaleReceiptValidators.service'; +import { SaleReceipt } from '../models/SaleReceipt'; +import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service'; + +@Injectable() +export class GetSaleReceipt { + constructor( + private readonly saleReceiptModel: typeof SaleReceipt, + private readonly transformer: TransformerInjectable, + private readonly validators: SaleReceiptValidators, + ) {} + + /** + * Retrieve sale receipt with associated entries. + * @param {Integer} saleReceiptId + * @return {ISaleReceipt} + */ + public async getSaleReceipt(saleReceiptId: number) { + const saleReceipt = await this.saleReceiptModel + .query() + .findById(saleReceiptId) + .withGraphFetched('entries.item') + .withGraphFetched('customer') + .withGraphFetched('depositAccount') + .withGraphFetched('branch') + .withGraphFetched('attachments') + .throwIfNotFound() + + return this.transformer.transform( + saleReceipt, + new SaleReceiptTransformer() + ); + } +} diff --git a/packages/server-nest/src/modules/SaleReceipts/queries/GetSaleReceiptState.service.ts b/packages/server-nest/src/modules/SaleReceipts/queries/GetSaleReceiptState.service.ts new file mode 100644 index 000000000..0c65b06ea --- /dev/null +++ b/packages/server-nest/src/modules/SaleReceipts/queries/GetSaleReceiptState.service.ts @@ -0,0 +1,26 @@ +import { PdfTemplateModel } from '@/modules/PdfTemplate/models/PdfTemplate'; +import { Inject, Injectable } from '@nestjs/common'; +import { ISaleReceiptState } from '../types/SaleReceipts.types'; + +@Injectable() +export class GetSaleReceiptState { + constructor( + @Inject(PdfTemplateModel.name) + private pdfTemplateModel: typeof PdfTemplateModel, + ) {} + + /** + * Retrieves the sale receipt state. + * @return {Promise} + */ + public async getSaleReceiptState(): Promise { + const defaultPdfTemplate = await this.pdfTemplateModel + .query() + .findOne({ resource: 'SaleReceipt' }) + .modify('default'); + + return { + defaultTemplateId: defaultPdfTemplate?.id, + }; + } +} diff --git a/packages/server-nest/src/modules/SaleReceipts/queries/GetSaleReceipts.ts b/packages/server-nest/src/modules/SaleReceipts/queries/GetSaleReceipts.ts new file mode 100644 index 000000000..eaa0d38a2 --- /dev/null +++ b/packages/server-nest/src/modules/SaleReceipts/queries/GetSaleReceipts.ts @@ -0,0 +1,84 @@ +// import * as R from 'ramda'; +// import { +// IFilterMeta, +// IPaginationMeta, +// ISaleReceipt, +// ISalesReceiptsFilter, +// } from '@/interfaces'; +// import HasTenancyService from '@/services/Tenancy/TenancyService'; +// import { Inject, Service } from 'typedi'; +// import { SaleReceiptTransformer } from './SaleReceiptTransformer'; +// import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; +// import DynamicListingService from '@/services/DynamicListing/DynamicListService'; + +// interface GetSaleReceiptsSettings { +// fetchEntriesGraph?: boolean; +// } +// @Service() +// export class GetSaleReceipts { +// @Inject() +// private tenancy: HasTenancyService; + +// @Inject() +// private transformer: TransformerInjectable; + +// @Inject() +// private dynamicListService: DynamicListingService; + +// /** +// * Retrieve sales receipts paginated and filterable list. +// * @param {number} tenantId +// * @param {ISaleReceiptFilter} salesReceiptsFilter +// */ +// public async getSaleReceipts( +// tenantId: number, +// filterDTO: ISalesReceiptsFilter +// ): Promise<{ +// data: ISaleReceipt[]; +// pagination: IPaginationMeta; +// filterMeta: IFilterMeta; +// }> { +// const { SaleReceipt } = this.tenancy.models(tenantId); + +// // Parses the stringified filter roles. +// const filter = this.parseListFilterDTO(filterDTO); + +// // Dynamic list service. +// const dynamicFilter = await this.dynamicListService.dynamicList( +// tenantId, +// SaleReceipt, +// filter +// ); +// const { results, pagination } = await SaleReceipt.query() +// .onBuild((builder) => { +// builder.withGraphFetched('depositAccount'); +// builder.withGraphFetched('customer'); +// builder.withGraphFetched('entries.item'); + +// dynamicFilter.buildQuery()(builder); +// filterDTO?.filterQuery && filterDTO?.filterQuery(builder); +// }) +// .pagination(filter.page - 1, filter.pageSize); + +// // Transformes the estimates models to POJO. +// const salesEstimates = await this.transformer.transform( +// tenantId, +// results, +// new SaleReceiptTransformer() +// ); +// return { +// data: salesEstimates, +// pagination, +// filterMeta: dynamicFilter.getResponseMeta(), +// }; +// } + +// /** +// * Parses the sale invoice list filter DTO. +// * @param filterDTO +// * @returns +// */ +// private parseListFilterDTO(filterDTO) { +// return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO); +// } +// } diff --git a/packages/server-nest/src/modules/SaleReceipts/queries/SaleReceiptBrandingTemplate.service.ts b/packages/server-nest/src/modules/SaleReceipts/queries/SaleReceiptBrandingTemplate.service.ts new file mode 100644 index 000000000..81a5de2ba --- /dev/null +++ b/packages/server-nest/src/modules/SaleReceipts/queries/SaleReceiptBrandingTemplate.service.ts @@ -0,0 +1,48 @@ +import { Injectable } from '@nestjs/common'; +import { defaultSaleReceiptBrandingAttributes } from '../constants'; +import { GetPdfTemplateService } from '@/modules/PdfTemplate/queries/GetPdfTemplate.service'; +import { GetOrganizationBrandingAttributesService } from '@/modules/PdfTemplate/queries/GetOrganizationBrandingAttributes.service'; +import { mergePdfTemplateWithDefaultAttributes } from '@/modules/SaleInvoices/utils'; + +@Injectable() +export class SaleReceiptBrandingTemplate { + /** + * @param {GetPdfTemplate} getPdfTemplateService - + * @param {GetOrganizationBrandingAttributes} getOrgBrandingAttributes - + */ + constructor( + private readonly getPdfTemplateService: GetPdfTemplateService, + private readonly getOrgBrandingAttributes: GetOrganizationBrandingAttributesService, + ) {} + + /** + * Retrieves the sale receipt branding template. + * @param {number} templateId - The ID of the PDF template. + * @returns {Promise} The sale receipt branding template with merged attributes. + */ + public async getSaleReceiptBrandingTemplate(templateId: number) { + const template = + await this.getPdfTemplateService.getPdfTemplate(templateId); + // Retrieves the organization branding attributes. + const commonOrgBrandingAttrs = + await this.getOrgBrandingAttributes.getOrganizationBrandingAttributes(); + + // Merges the default branding attributes with organization common branding attrs. + const organizationBrandingAttrs = { + ...defaultSaleReceiptBrandingAttributes, + ...commonOrgBrandingAttrs, + }; + const brandingTemplateAttrs = { + ...template.attributes, + companyLogoUri: template.companyLogoUri, + }; + const attributes = mergePdfTemplateWithDefaultAttributes( + brandingTemplateAttrs, + organizationBrandingAttrs, + ); + return { + ...template, + attributes, + }; + } +} diff --git a/packages/server-nest/src/modules/SaleReceipts/queries/SaleReceiptTransformer.ts b/packages/server-nest/src/modules/SaleReceipts/queries/SaleReceiptTransformer.ts new file mode 100644 index 000000000..407d52eed --- /dev/null +++ b/packages/server-nest/src/modules/SaleReceipts/queries/SaleReceiptTransformer.ts @@ -0,0 +1,89 @@ +import { Transformer } from '@/modules/Transformer/Transformer'; +import { SaleReceipt } from '../models/SaleReceipt'; +import { ItemEntryTransformer } from '@/modules/TransactionItemEntry/ItemEntry.transformer'; +import { AttachmentTransformer } from '@/modules/Attachments/Attachment.transformer'; + +export class SaleReceiptTransformer extends Transformer { + /** + * Include these attributes to sale invoice object. + * @returns {Array} + */ + public includeAttributes = (): string[] => { + return [ + 'formattedSubtotal', + 'formattedAmount', + 'formattedReceiptDate', + 'formattedClosedAtDate', + 'formattedCreatedAt', + 'entries', + 'attachments', + ]; + }; + + /** + * Retrieve formatted receipt date. + * @param {ISaleReceipt} invoice + * @returns {String} + */ + protected formattedReceiptDate = (receipt: SaleReceipt): string => { + return this.formatDate(receipt.receiptDate); + }; + + /** + * Retrieve formatted estimate closed at date. + * @param {ISaleReceipt} invoice + * @returns {String} + */ + protected formattedClosedAtDate = (receipt: SaleReceipt): string => { + return this.formatDate(receipt.closedAt); + }; + + /** + * Retrieve formatted receipt created at date. + * @param receipt + * @returns {string} + */ + protected formattedCreatedAt = (receipt: SaleReceipt): string => { + return this.formatDate(receipt.createdAt); + }; + + /** + * Retrieves the estimate formatted subtotal. + * @param {ISaleReceipt} receipt + * @returns {string} + */ + protected formattedSubtotal = (receipt: SaleReceipt): string => { + return this.formatNumber(receipt.amount, { money: false }); + }; + + /** + * Retrieve formatted invoice amount. + * @param {ISaleReceipt} estimate + * @returns {string} + */ + protected formattedAmount = (receipt: SaleReceipt): string => { + return this.formatNumber(receipt.amount, { + currencyCode: receipt.currencyCode, + }); + }; + + /** + * Retrieves the entries of the credit note. + * @param {ISaleReceipt} credit + * @returns {} + */ + // protected entries = (receipt: SaleReceipt) => { + // return this.item(receipt.entries, new ItemEntryTransformer(), { + // currencyCode: receipt.currencyCode, + // }); + // }; + + /** + * Retrieves the sale receipt attachments. + * @param {SaleReceipt} receipt + * @returns + */ + // protected attachments = (receipt: SaleReceipt) => { + // return this.item(receipt.attachments, new AttachmentTransformer()); + // }; +} diff --git a/packages/server-nest/src/modules/SaleReceipts/queries/SaleReceiptsPdf.service.ts b/packages/server-nest/src/modules/SaleReceipts/queries/SaleReceiptsPdf.service.ts new file mode 100644 index 000000000..7d26c44a9 --- /dev/null +++ b/packages/server-nest/src/modules/SaleReceipts/queries/SaleReceiptsPdf.service.ts @@ -0,0 +1,109 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { GetSaleReceipt } from './GetSaleReceipt.service'; +import { SaleReceiptBrandingTemplate } from './SaleReceiptBrandingTemplate.service'; +import { transformReceiptToBrandingTemplateAttributes } from '../utils'; +import { SaleReceipt } from '../models/SaleReceipt'; +import { ChromiumlyTenancy } from '@/modules/ChromiumlyTenancy/ChromiumlyTenancy.service'; +import { TemplateInjectable } from '@/modules/TemplateInjectable/TemplateInjectable.service'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { PdfTemplateModel } from '@/modules/PdfTemplate/models/PdfTemplate'; +import { ISaleReceiptBrandingTemplateAttributes } from '../types/SaleReceipts.types'; +import { events } from '@/common/events/events'; + +@Injectable() +export class SaleReceiptsPdfService { + /** + * @param {ChromiumlyTenancy} chromiumlyTenancy - + * @param {TemplateInjectable} templateInjectable - + * @param {GetSaleReceipt} getSaleReceiptService - + * @param {SaleReceiptBrandingTemplate} saleReceiptBrandingTemplate - + * @param {EventEmitter2} eventPublisher - + * @param {typeof SaleReceipt} saleReceiptModel - + * @param {typeof PdfTemplateModel} pdfTemplateModel - + */ + constructor( + private readonly chromiumlyTenancy: ChromiumlyTenancy, + private readonly templateInjectable: TemplateInjectable, + private readonly getSaleReceiptService: GetSaleReceipt, + private readonly saleReceiptBrandingTemplate: SaleReceiptBrandingTemplate, + private readonly eventPublisher: EventEmitter2, + + @Inject(SaleReceipt.name) + private readonly saleReceiptModel: typeof SaleReceipt, + + @Inject(PdfTemplateModel.name) + private readonly pdfTemplateModel: typeof PdfTemplateModel, + ) {} + + /** + * Retrieves sale invoice pdf content. + * @param {number} saleReceiptId - Sale receipt identifier. + * @returns {Promise} + */ + public async saleReceiptPdf( + saleReceiptId: number, + ): Promise<[Buffer, string]> { + const filename = await this.getSaleReceiptFilename(saleReceiptId); + + const brandingAttributes = + await this.getReceiptBrandingAttributes(saleReceiptId); + // Converts the receipt template to html content. + const htmlContent = await this.templateInjectable.render( + 'modules/receipt-regular', + brandingAttributes, + ); + // Renders the html content to pdf document. + const content = + await this.chromiumlyTenancy.convertHtmlContent(htmlContent); + const eventPayload = { saleReceiptId }; + + // Triggers the `onSaleReceiptPdfViewed` event. + await this.eventPublisher.emitAsync( + events.saleReceipt.onPdfViewed, + eventPayload, + ); + return [content, filename]; + } + + /** + * Retrieves the filename file document of the given sale receipt. + * @param {number} receiptId + * @returns {Promise} + */ + public async getSaleReceiptFilename(receiptId: number): Promise { + const receipt = await this.saleReceiptModel.query().findById(receiptId); + + return `Receipt-${receipt.receiptNumber}`; + } + + /** + * Retrieves receipt branding attributes. + * @param {number} receiptId - Sale receipt identifier. + * @returns {Promise} + */ + public async getReceiptBrandingAttributes( + receiptId: number, + ): Promise { + const saleReceipt = + await this.getSaleReceiptService.getSaleReceipt(receiptId); + + // Retrieve the invoice template id of not found get the default template id. + const templateId = + saleReceipt.pdfTemplateId ?? + ( + await this.pdfTemplateModel.query().findOne({ + resource: 'SaleReceipt', + default: true, + }) + )?.id; + // Retrieves the receipt branding template. + const brandingTemplate = + await this.saleReceiptBrandingTemplate.getSaleReceiptBrandingTemplate( + templateId, + ); + return { + ...brandingTemplate.attributes, + ...transformReceiptToBrandingTemplateAttributes(saleReceipt), + }; + } +} diff --git a/packages/server-nest/src/modules/SaleReceipts/subscribers/SaleReceiptCostGLEntriesSubscriber.ts b/packages/server-nest/src/modules/SaleReceipts/subscribers/SaleReceiptCostGLEntriesSubscriber.ts new file mode 100644 index 000000000..57209ea12 --- /dev/null +++ b/packages/server-nest/src/modules/SaleReceipts/subscribers/SaleReceiptCostGLEntriesSubscriber.ts @@ -0,0 +1,36 @@ +// import { Inject, Service } from 'typedi'; +// import events from '@/subscribers/events'; +// import { IInventoryCostLotsGLEntriesWriteEvent } from '@/interfaces'; +// import { SaleReceiptCostGLEntries } from '../SaleReceiptCostGLEntries'; + +// @Service() +// export class SaleReceiptCostGLEntriesSubscriber { +// @Inject() +// private saleReceiptCostEntries: SaleReceiptCostGLEntries; + +// /** +// * Attaches events. +// */ +// public attach(bus) { +// bus.subscribe( +// events.inventory.onCostLotsGLEntriesWrite, +// this.writeJournalEntriesOnceWriteoffCreate +// ); +// } + +// /** +// * Writes the receipts cost GL entries once the inventory cost lots be written. +// * @param {IInventoryCostLotsGLEntriesWriteEvent} +// */ +// private writeJournalEntriesOnceWriteoffCreate = async ({ +// trx, +// startingDate, +// tenantId, +// }: IInventoryCostLotsGLEntriesWriteEvent) => { +// await this.saleReceiptCostEntries.writeInventoryCostJournalEntries( +// tenantId, +// startingDate, +// trx +// ); +// }; +// } diff --git a/packages/server-nest/src/modules/SaleReceipts/subscribers/SaleReceiptMarkClosedOnMailSentSubcriber.ts b/packages/server-nest/src/modules/SaleReceipts/subscribers/SaleReceiptMarkClosedOnMailSentSubcriber.ts new file mode 100644 index 000000000..172028554 --- /dev/null +++ b/packages/server-nest/src/modules/SaleReceipts/subscribers/SaleReceiptMarkClosedOnMailSentSubcriber.ts @@ -0,0 +1,41 @@ +// import { ISaleReceiptMailPresend } from '@/interfaces'; +// import events from '@/subscribers/events'; +// import { CloseSaleReceipt } from '../commands/CloseSaleReceipt.service'; +// import { Inject, Service } from 'typedi'; +// import { ServiceError } from '@/exceptions'; +// import { ERRORS } from '../constants'; + +// @Service() +// export class SaleReceiptMarkClosedOnMailSentSubcriber { +// @Inject() +// private closeReceiptService: CloseSaleReceipt; + +// /** +// * Attaches events. +// */ +// public attach(bus) { +// bus.subscribe(events.saleReceipt.onPreMailSend, this.markReceiptClosed); +// } + +// /** +// * Marks the sale receipt closed on submitting mail. +// * @param {ISaleReceiptMailPresend} +// */ +// private markReceiptClosed = async ({ +// tenantId, +// saleReceiptId, +// messageOptions, +// }: ISaleReceiptMailPresend) => { +// try { +// await this.closeReceiptService.closeSaleReceipt(tenantId, saleReceiptId); +// } catch (error) { +// if ( +// error instanceof ServiceError && +// error.errorType === ERRORS.SALE_RECEIPT_IS_ALREADY_CLOSED +// ) { +// } else { +// throw error; +// } +// } +// }; +// } diff --git a/packages/server-nest/src/modules/SaleReceipts/types/SaleReceipts.types.ts b/packages/server-nest/src/modules/SaleReceipts/types/SaleReceipts.types.ts new file mode 100644 index 000000000..9130c6183 --- /dev/null +++ b/packages/server-nest/src/modules/SaleReceipts/types/SaleReceipts.types.ts @@ -0,0 +1,168 @@ +import { Knex } from 'knex'; +// import { IItemEntry } from './ItemEntry'; +// import { CommonMailOptions, CommonMailOptionsDTO } from '../SaleInvoices/types/Mailable'; +import { AttachmentLinkDTO } from '../../Attachments/Attachments.types'; +import { SaleReceipt } from '../models/SaleReceipt'; + +export interface ISalesReceiptsFilter { + filterQuery?: (query: any) => void; +} + +export interface ISaleReceiptDTO { + customerId: number; + exchangeRate?: number; + depositAccountId: number; + receiptDate: Date; + sendToEmail: string; + referenceNo?: string; + receiptNumber?: string; + receiptMessage: string; + statement: string; + closed: boolean; + entries: any[]; + branchId?: number; + attachments?: AttachmentLinkDTO[]; +} + +export interface ISaleReceiptSmsDetails { + customerName: string; + customerPhoneNumber: string; + smsMessage: string; +} +export interface ISaleReceiptCreatingPayload { + saleReceiptDTO: ISaleReceiptDTO; + // tenantId: number; + trx: Knex.Transaction; +} + +export interface ISaleReceiptCreatedPayload { + // tenantId: number; + saleReceipt: SaleReceipt; + saleReceiptId: number; + saleReceiptDTO: ISaleReceiptDTO; + trx: Knex.Transaction; +} + +export interface ISaleReceiptEditedPayload { + // tenantId: number; + oldSaleReceipt: SaleReceipt; + saleReceipt: SaleReceipt; + // saleReceiptId: number; + saleReceiptDTO: ISaleReceiptDTO; + trx: Knex.Transaction; +} + +export interface ISaleReceiptEditingPayload { + // tenantId: number; + oldSaleReceipt: SaleReceipt; + saleReceiptDTO: ISaleReceiptDTO; + trx: Knex.Transaction; +} +export interface ISaleReceiptEventClosedPayload { + // tenantId: number; + saleReceiptId: number; + saleReceipt: SaleReceipt; + trx: Knex.Transaction; +} + +export interface ISaleReceiptEventClosingPayload { + // tenantId: number; + oldSaleReceipt: SaleReceipt; + trx: Knex.Transaction; +} + +export interface ISaleReceiptEventDeletedPayload { + tenantId: number; + saleReceiptId: number; + oldSaleReceipt: SaleReceipt; + trx: Knex.Transaction; +} + +export enum SaleReceiptAction { + Create = 'Create', + Edit = 'Edit', + Delete = 'Delete', + View = 'View', + NotifyBySms = 'NotifyBySms', +} + +export interface ISaleReceiptDeletingPayload { + tenantId: number; + oldSaleReceipt: SaleReceipt; + trx: Knex.Transaction; +} + +// export interface SaleReceiptMailOpts extends CommonMailOptions { +// attachReceipt: boolean; +// } + +// export interface SaleReceiptMailOptsDTO extends CommonMailOptionsDTO { +// attachReceipt?: boolean; +// } + +// export interface ISaleReceiptMailPresend { +// tenantId: number; +// saleReceiptId: number; +// messageOptions: SaleReceiptMailOptsDTO; +// } + +export interface ISaleReceiptBrandingTemplateAttributes { + primaryColor: string; + secondaryColor: string; + showCompanyLogo: boolean; + companyLogo: string; + companyName: string; + + // Customer Address + showCustomerAddress: boolean; + customerAddress: string; + + // Company address + showCompanyAddress: boolean; + companyAddress: string; + billedToLabel: string; + + // Total + total: string; + totalLabel: string; + showTotal: boolean; + + // Subtotal + subtotal: string; + subtotalLabel: string; + showSubtotal: boolean; + + // Customer Note + showCustomerNote: boolean; + customerNote: string; + customerNoteLabel: string; + + // Terms & Conditions + showTermsConditions: boolean; + termsConditions: string; + termsConditionsLabel: string; + + // Lines + lines: Array<{ + item: string; + description: string; + rate: string; + quantity: string; + total: string; + }>; + + // Receipt Number + showReceiptNumber: boolean; + receiptNumberLabel: string; + receiptNumebr: string; + + // Receipt Date + receiptDate: string; + showReceiptDate: boolean; + receiptDateLabel: string; +} + + +export interface ISaleReceiptState { + defaultTemplateId: number; +} \ No newline at end of file diff --git a/packages/server-nest/src/modules/SaleReceipts/utils.ts b/packages/server-nest/src/modules/SaleReceipts/utils.ts new file mode 100644 index 000000000..bc971e1d0 --- /dev/null +++ b/packages/server-nest/src/modules/SaleReceipts/utils.ts @@ -0,0 +1,34 @@ +// @ts-nocheck +import { + ISaleReceipt, + ISaleReceiptBrandingTemplateAttributes, +} from '@/interfaces'; +import { contactAddressTextFormat } from '@/utils/address-text-format'; + +export const transformReceiptToBrandingTemplateAttributes = ( + saleReceipt: ISaleReceipt +): Partial => { + return { + total: saleReceipt.formattedAmount, + subtotal: saleReceipt.formattedSubtotal, + lines: saleReceipt.entries?.map((entry) => ({ + item: entry.item.name, + description: entry.description, + rate: entry.rateFormatted, + quantity: entry.quantityFormatted, + total: entry.totalFormatted, + })), + receiptNumber: saleReceipt.receiptNumber, + receiptDate: saleReceipt.formattedReceiptDate, + customerAddress: contactAddressTextFormat(saleReceipt.customer), + }; +}; + +export const transformReceiptToMailDataArgs = (saleReceipt: any) => { + return { + 'Customer Name': saleReceipt.customer.displayName, + 'Receipt Number': saleReceipt.receiptNumber, + 'Receipt Date': saleReceipt.formattedReceiptDate, + 'Receipt Amount': saleReceipt.formattedAmount, + }; +}; diff --git a/packages/server-nest/src/modules/System/models/TenantMetadataModel.ts b/packages/server-nest/src/modules/System/models/TenantMetadataModel.ts index 11f299baf..91c23587e 100644 --- a/packages/server-nest/src/modules/System/models/TenantMetadataModel.ts +++ b/packages/server-nest/src/modules/System/models/TenantMetadataModel.ts @@ -1,9 +1,9 @@ -// import { -// defaultOrganizationAddressFormat, -// organizationAddressTextFormat, -// } from '@/utils/address-text-format'; import { BaseModel } from '@/models/Model'; -// import { findByIsoCountryCode } from '@bigcapital/utils'; +import { + defaultOrganizationAddressFormat, + organizationAddressTextFormat, +} from '@/utils/address-text-format'; +import { findByIsoCountryCode } from '@bigcapital/utils'; // import { getUploadedObjectUri } from '../../services/Attachments/utils'; export class TenantMetadata extends BaseModel { @@ -56,30 +56,30 @@ export class TenantMetadata extends BaseModel { return ['logoUri']; } - // /** - // * Organization logo url. - // * @returns {string | null} - // */ + /** + * Organization logo url. + * @returns {string | null} + */ // public get logoUri() { - // return this.logoKey ? getUploadedObjectUri(this.logoKey) : null; + // return this.logoKey ? getUploadedObjectUri(this.logoKey) : null; // } // /** // * Retrieves the organization address formatted text. // * @returns {string} // */ - // public get addressTextFormatted() { - // const addressCountry = findByIsoCountryCode(this.location); + public get addressTextFormatted() { + const addressCountry = findByIsoCountryCode(this.location); - // return organizationAddressTextFormat(defaultOrganizationAddressFormat, { - // organizationName: this.name, - // address1: this.address?.address1, - // address2: this.address?.address2, - // state: this.address?.stateProvince, - // city: this.address?.city, - // postalCode: this.address?.postalCode, - // phone: this.address?.phone, - // country: addressCountry?.name ?? '', - // }); - // } + return organizationAddressTextFormat(defaultOrganizationAddressFormat, { + organizationName: this.name, + address1: this.address?.address1, + address2: this.address?.address2, + state: this.address?.stateProvince, + city: this.address?.city, + postalCode: this.address?.postalCode, + phone: this.address?.phone, + country: addressCountry?.name ?? '', + }); + } } diff --git a/packages/server-nest/src/modules/TaxRates/ItemEntriesTaxTransactions.service.ts b/packages/server-nest/src/modules/TaxRates/ItemEntriesTaxTransactions.service.ts new file mode 100644 index 000000000..760b6bccb --- /dev/null +++ b/packages/server-nest/src/modules/TaxRates/ItemEntriesTaxTransactions.service.ts @@ -0,0 +1,71 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { keyBy, sumBy } from 'lodash'; +import { ItemEntry } from '@/modules/Items/models/ItemEntry'; +import { TaxRateModel } from './models/TaxRate.model'; + +@Injectable() +export class ItemEntriesTaxTransactions { + constructor( + @Inject(ItemEntry.name) private itemEntryModel: typeof ItemEntry, + @Inject(TaxRateModel.name) private taxRateModel: typeof TaxRateModel, + ) {} + + /** + * Associates tax amount withheld to the model. + * @param model + * @returns + */ + public assocTaxAmountWithheldFromEntries(model: any) { + const entries = model.entries.map((entry) => + this.itemEntryModel.fromJson(entry), + ); + const taxAmountWithheld = sumBy(entries, 'taxAmount'); + + if (taxAmountWithheld) { + model.taxAmountWithheld = taxAmountWithheld; + } + return model; + } + + /** + * Associates tax rate id from tax code to entries. + * @param {any} entries + */ + public assocTaxRateIdFromCodeToEntries = async (entries: any) => { + const entriesWithCode = entries.filter((entry) => entry.taxCode); + const taxCodes = entriesWithCode.map((entry) => entry.taxCode); + const foundTaxCodes = await this.taxRateModel + .query() + .whereIn('code', taxCodes); + + const taxCodesMap = keyBy(foundTaxCodes, 'code'); + + return entries.map((entry) => { + if (entry.taxCode) { + entry.taxRateId = taxCodesMap[entry.taxCode]?.id; + } + return entry; + }); + }; + + /** + * Associates tax rate from tax id to entries. + * @returns {Promise} + */ + public assocTaxRateFromTaxIdToEntries = async (entries: ItemEntry[]) => { + const entriesWithId = entries.filter((e) => e.taxRateId); + const taxRateIds = entriesWithId.map((e) => e.taxRateId); + const foundTaxes = await this.taxRateModel + .query() + .whereIn('id', taxRateIds); + + const taxRatesMap = keyBy(foundTaxes, 'id'); + + return entries.map((entry) => { + if (entry.taxRateId) { + entry.taxRate = taxRatesMap[entry.taxRateId]?.rate; + } + return entry; + }); + }; +} diff --git a/packages/server-nest/src/modules/TaxRates/ItemEntriesTaxTransactions.ts b/packages/server-nest/src/modules/TaxRates/ItemEntriesTaxTransactions.ts deleted file mode 100644 index 2d999def5..000000000 --- a/packages/server-nest/src/modules/TaxRates/ItemEntriesTaxTransactions.ts +++ /dev/null @@ -1,72 +0,0 @@ -// import { Inject, Service } from 'typedi'; -// import { keyBy, sumBy } from 'lodash'; -// import { ItemEntry } from '@/models'; -// import HasTenancyService from '../Tenancy/TenancyService'; -// import { IItem, IItemEntry, IItemEntryDTO } from '@/interfaces'; - -// @Service() -// export class ItemEntriesTaxTransactions { -// @Inject() -// private tenancy: HasTenancyService; - -// /** -// * Associates tax amount withheld to the model. -// * @param model -// * @returns -// */ -// public assocTaxAmountWithheldFromEntries(model: any) { -// const entries = model.entries.map((entry) => ItemEntry.fromJson(entry)); -// const taxAmountWithheld = sumBy(entries, 'taxAmount'); - -// if (taxAmountWithheld) { -// model.taxAmountWithheld = taxAmountWithheld; -// } -// return model; -// } - -// /** -// * Associates tax rate id from tax code to entries. -// * @param {number} tenantId -// * @param {} model -// */ -// public assocTaxRateIdFromCodeToEntries = -// (tenantId: number) => async (entries: any) => { -// const entriesWithCode = entries.filter((entry) => entry.taxCode); -// const taxCodes = entriesWithCode.map((entry) => entry.taxCode); - -// const { TaxRate } = this.tenancy.models(tenantId); -// const foundTaxCodes = await TaxRate.query().whereIn('code', taxCodes); - -// const taxCodesMap = keyBy(foundTaxCodes, 'code'); - -// return entries.map((entry) => { -// if (entry.taxCode) { -// entry.taxRateId = taxCodesMap[entry.taxCode]?.id; -// } -// return entry; -// }); -// }; - -// /** -// * Associates tax rate from tax id to entries. -// * @param {number} tenantId -// * @returns {Promise} -// */ -// public assocTaxRateFromTaxIdToEntries = -// (tenantId: number) => async (entries: IItemEntry[]) => { -// const entriesWithId = entries.filter((e) => e.taxRateId); -// const taxRateIds = entriesWithId.map((e) => e.taxRateId); - -// const { TaxRate } = this.tenancy.models(tenantId); -// const foundTaxes = await TaxRate.query().whereIn('id', taxRateIds); - -// const taxRatesMap = keyBy(foundTaxes, 'id'); - -// return entries.map((entry) => { -// if (entry.taxRateId) { -// entry.taxRate = taxRatesMap[entry.taxRateId]?.rate; -// } -// return entry; -// }); -// }; -// } diff --git a/packages/server-nest/src/modules/TaxRates/commands/ActivateTaxRate.service.ts b/packages/server-nest/src/modules/TaxRates/commands/ActivateTaxRate.service.ts index a9f8c0bea..7208c1d35 100644 --- a/packages/server-nest/src/modules/TaxRates/commands/ActivateTaxRate.service.ts +++ b/packages/server-nest/src/modules/TaxRates/commands/ActivateTaxRate.service.ts @@ -12,6 +12,12 @@ import { events } from '@/common/events/events'; @Injectable() export class ActivateTaxRateService { + /** + * @param {EventEmitter2} eventEmitter - The event emitter. + * @param {UnitOfWork} uow - The unit of work. + * @param {CommandTaxRatesValidators} validators - The tax rates validators. + * @param {typeof TaxRateModel} taxRateModel - The tax rate model. + */ constructor( private readonly eventEmitter: EventEmitter2, private readonly uow: UnitOfWork, diff --git a/packages/server-nest/src/modules/TaxRates/commands/CommandTaxRatesValidator.service.ts b/packages/server-nest/src/modules/TaxRates/commands/CommandTaxRatesValidator.service.ts index fb6264116..a26da8992 100644 --- a/packages/server-nest/src/modules/TaxRates/commands/CommandTaxRatesValidator.service.ts +++ b/packages/server-nest/src/modules/TaxRates/commands/CommandTaxRatesValidator.service.ts @@ -9,6 +9,9 @@ import { ServiceError } from '@/modules/Items/ServiceError'; @Injectable() export class CommandTaxRatesValidators { + /** + * @param {typeof TaxRateModel} taxRateModel - The tax rate model. + */ constructor( @Inject(TaxRateModel.name) private readonly taxRateModel: typeof TaxRateModel, diff --git a/packages/server-nest/src/modules/TaxRates/commands/CreateTaxRate.service.ts b/packages/server-nest/src/modules/TaxRates/commands/CreateTaxRate.service.ts index 9c1d630f6..a06027b68 100644 --- a/packages/server-nest/src/modules/TaxRates/commands/CreateTaxRate.service.ts +++ b/packages/server-nest/src/modules/TaxRates/commands/CreateTaxRate.service.ts @@ -13,6 +13,12 @@ import { events } from '@/common/events/events'; @Injectable() export class CreateTaxRate { + /** + * @param {EventEmitter2} eventEmitter - The event emitter. + * @param {UnitOfWork} uow - The unit of work. + * @param {CommandTaxRatesValidators} validators - The tax rates validators. + * @param {typeof TaxRateModel} taxRateModel - The tax rate model. + */ constructor( private readonly eventEmitter: EventEmitter2, private readonly uow: UnitOfWork, diff --git a/packages/server-nest/src/modules/TaxRates/commands/DeleteTaxRate.service.ts b/packages/server-nest/src/modules/TaxRates/commands/DeleteTaxRate.service.ts index 83e461c4a..987e6d060 100644 --- a/packages/server-nest/src/modules/TaxRates/commands/DeleteTaxRate.service.ts +++ b/packages/server-nest/src/modules/TaxRates/commands/DeleteTaxRate.service.ts @@ -12,6 +12,12 @@ import { events } from '@/common/events/events'; @Injectable() export class DeleteTaxRateService { + /** + * @param {EventEmitter2} eventEmitter - The event emitter. + * @param {UnitOfWork} uow - The unit of work. + * @param {CommandTaxRatesValidators} validators - The tax rates validators. + * @param {typeof TaxRateModel} taxRateModel - The tax rate model. + */ constructor( private readonly eventEmitter: EventEmitter2, private readonly uow: UnitOfWork, diff --git a/packages/server-nest/src/modules/TaxRates/commands/EditTaxRate.service.ts b/packages/server-nest/src/modules/TaxRates/commands/EditTaxRate.service.ts index 86b958e17..98fda576b 100644 --- a/packages/server-nest/src/modules/TaxRates/commands/EditTaxRate.service.ts +++ b/packages/server-nest/src/modules/TaxRates/commands/EditTaxRate.service.ts @@ -14,6 +14,12 @@ import { EventEmitter2 } from '@nestjs/event-emitter'; @Injectable() export class EditTaxRateService { + /** + * @param {EventEmitter2} eventEmitter - The event emitter. + * @param {UnitOfWork} uow - The unit of work. + * @param {CommandTaxRatesValidators} validators - The tax rates validators. + * @param {typeof TaxRateModel} taxRateModel - The tax rate model. + */ constructor( private readonly eventEmitter: EventEmitter2, private readonly uow: UnitOfWork, @@ -73,6 +79,12 @@ export class EditTaxRateService { } } + /** + * Edits the given tax rate. + * @param {number} taxRateId - The tax rate id. + * @param {IEditTaxRateDTO} editTaxRateDTO - The tax rate data to edit. + * @returns {Promise} + */ public async editTaxRate( taxRateId: number, editTaxRateDTO: IEditTaxRateDTO diff --git a/packages/server-nest/src/modules/TaxRates/commands/InactivateTaxRate.ts b/packages/server-nest/src/modules/TaxRates/commands/InactivateTaxRate.ts index f0dce26a5..f6f4bcff9 100644 --- a/packages/server-nest/src/modules/TaxRates/commands/InactivateTaxRate.ts +++ b/packages/server-nest/src/modules/TaxRates/commands/InactivateTaxRate.ts @@ -12,6 +12,12 @@ import { events } from '@/common/events/events'; @Injectable() export class InactivateTaxRateService { + /** + * @param {EventEmitter2} eventEmitter - The event emitter. + * @param {UnitOfWork} uow - The unit of work. + * @param {CommandTaxRatesValidators} validators - The tax rates validators. + * @param {typeof TaxRateModel} taxRateModel - The tax rate model. + */ constructor( private readonly eventEmitter: EventEmitter2, private readonly uow: UnitOfWork, diff --git a/packages/server-nest/src/modules/TaxRates/queries/GetTaxRate.service.ts b/packages/server-nest/src/modules/TaxRates/queries/GetTaxRate.service.ts index d287c117d..666b69712 100644 --- a/packages/server-nest/src/modules/TaxRates/queries/GetTaxRate.service.ts +++ b/packages/server-nest/src/modules/TaxRates/queries/GetTaxRate.service.ts @@ -6,6 +6,11 @@ import { CommandTaxRatesValidators } from '../commands/CommandTaxRatesValidator. @Injectable() export class GetTaxRateService { + /** + * @param {typeof TaxRateModel} taxRateModel - The tax rate model. + * @param {CommandTaxRatesValidators} validators - The tax rates validators. + * @param {TransformerInjectable} transformer - The transformer. + */ constructor( @Inject(TaxRateModel.name) private readonly taxRateModel: typeof TaxRateModel, diff --git a/packages/server-nest/src/modules/TemplateInjectable/TemplateInjectable.module.ts b/packages/server-nest/src/modules/TemplateInjectable/TemplateInjectable.module.ts new file mode 100644 index 000000000..362147d80 --- /dev/null +++ b/packages/server-nest/src/modules/TemplateInjectable/TemplateInjectable.module.ts @@ -0,0 +1,7 @@ +import { Module } from '@nestjs/common'; +import { TemplateInjectable } from './TemplateInjectable.service'; + +@Module({ + providers: [TemplateInjectable], +}) +export class TemplateInjectableModule {} diff --git a/packages/server-nest/src/modules/TemplateInjectable/TemplateInjectable.service.ts b/packages/server-nest/src/modules/TemplateInjectable/TemplateInjectable.service.ts new file mode 100644 index 000000000..452a52d45 --- /dev/null +++ b/packages/server-nest/src/modules/TemplateInjectable/TemplateInjectable.service.ts @@ -0,0 +1,29 @@ +import { I18nService } from 'nestjs-i18n'; +import { Injectable } from '@nestjs/common'; +import { TenancyContext } from '../Tenancy/TenancyContext.service'; +import { templateRender } from '@/utils/template-render'; + +@Injectable() +export class TemplateInjectable { + constructor( + private readonly tenancyContext: TenancyContext, + private readonly i18n: I18nService, + ) {} + + /** + * Renders the given filename of the template. + * @param {string} filename + * @param {Record} options + * @returns {string} + */ + public async render(filename: string, options: Record) { + const organization = await this.tenancyContext.getTenant(true); + + return templateRender(filename, { + organizationName: organization.metadata.name, + organizationEmail: organization.metadata.email, + __: this.i18n.t, + ...options, + }); + } +} diff --git a/packages/server-nest/src/modules/Tenancy/TenancyModels/Tenancy.module.ts b/packages/server-nest/src/modules/Tenancy/TenancyModels/Tenancy.module.ts index ec31ca2b2..4f85a051b 100644 --- a/packages/server-nest/src/modules/Tenancy/TenancyModels/Tenancy.module.ts +++ b/packages/server-nest/src/modules/Tenancy/TenancyModels/Tenancy.module.ts @@ -17,6 +17,9 @@ import { Branch } from '@/modules/Branches/models/Branch.model'; import { SaleEstimate } from '@/modules/SaleEstimates/models/SaleEstimate'; import { Customer } from '@/modules/Customers/models/Customer'; import { Contact } from '@/modules/Contacts/models/Contact'; +import { Document } from '@/modules/ChromiumlyTenancy/models/Document'; +import { DocumentLink } from '@/modules/ChromiumlyTenancy/models/DocumentLink'; +import { Vendor } from '@/modules/Vendors/models/Vendor'; const models = [ Item, @@ -33,7 +36,10 @@ const models = [ Branch, SaleEstimate, Customer, - Contact + Contact, + Document, + DocumentLink, + Vendor ]; const modelProviders = models.map((model) => { diff --git a/packages/server-nest/src/modules/Vendors/VendorGLEntries.ts b/packages/server-nest/src/modules/Vendors/VendorGLEntries.ts new file mode 100644 index 000000000..073fe3d96 --- /dev/null +++ b/packages/server-nest/src/modules/Vendors/VendorGLEntries.ts @@ -0,0 +1,115 @@ +// import { Service } from 'typedi'; +// import { IVendor, AccountNormal, ILedgerEntry } from '@/interfaces'; +// import Ledger from '@/services/Accounting/Ledger'; + +// @Service() +// export class VendorGLEntries { +// /** +// * Retrieves the opening balance GL common entry. +// * @param {IVendor} vendor - +// */ +// private getOpeningBalanceGLCommonEntry = (vendor: IVendor) => { +// return { +// exchangeRate: vendor.openingBalanceExchangeRate, +// currencyCode: vendor.currencyCode, + +// transactionType: 'VendorOpeningBalance', +// transactionId: vendor.id, + +// date: vendor.openingBalanceAt, +// userId: vendor.userId, +// contactId: vendor.id, + +// credit: 0, +// debit: 0, + +// branchId: vendor.openingBalanceBranchId, +// }; +// }; + +// /** +// * Retrieves the opening balance GL debit entry. +// * @param {number} costAccountId - +// * @param {IVendor} vendor +// * @returns {ILedgerEntry} +// */ +// private getOpeningBalanceGLDebitEntry = ( +// costAccountId: number, +// vendor: IVendor +// ): ILedgerEntry => { +// const commonEntry = this.getOpeningBalanceGLCommonEntry(vendor); + +// return { +// ...commonEntry, +// accountId: costAccountId, +// accountNormal: AccountNormal.DEBIT, +// debit: vendor.localOpeningBalance, +// credit: 0, +// index: 2, +// }; +// }; + +// /** +// * Retrieves the opening balance GL credit entry. +// * @param {number} APAccountId +// * @param {IVendor} vendor +// * @returns {ILedgerEntry} +// */ +// private getOpeningBalanceGLCreditEntry = ( +// APAccountId: number, +// vendor: IVendor +// ): ILedgerEntry => { +// const commonEntry = this.getOpeningBalanceGLCommonEntry(vendor); + +// return { +// ...commonEntry, +// accountId: APAccountId, +// accountNormal: AccountNormal.CREDIT, +// credit: vendor.localOpeningBalance, +// index: 1, +// }; +// }; + +// /** +// * Retrieves the opening balance GL entries. +// * @param {number} APAccountId +// * @param {number} costAccountId - +// * @param {IVendor} vendor +// * @returns {ILedgerEntry[]} +// */ +// public getOpeningBalanceGLEntries = ( +// APAccountId: number, +// costAccountId: number, +// vendor: IVendor +// ): ILedgerEntry[] => { +// const debitEntry = this.getOpeningBalanceGLDebitEntry( +// costAccountId, +// vendor +// ); +// const creditEntry = this.getOpeningBalanceGLCreditEntry( +// APAccountId, +// vendor +// ); +// return [debitEntry, creditEntry]; +// }; + +// /** +// * Retrieves the opening balance ledger. +// * @param {number} APAccountId +// * @param {number} costAccountId - +// * @param {IVendor} vendor +// * @returns {Ledger} +// */ +// public getOpeningBalanceLedger = ( +// APAccountId: number, +// costAccountId: number, +// vendor: IVendor +// ) => { +// const entries = this.getOpeningBalanceGLEntries( +// APAccountId, +// costAccountId, +// vendor +// ); +// return new Ledger(entries); +// }; +// } diff --git a/packages/server-nest/src/modules/Vendors/VendorGLEntriesStorage.ts b/packages/server-nest/src/modules/Vendors/VendorGLEntriesStorage.ts new file mode 100644 index 000000000..a41bebb28 --- /dev/null +++ b/packages/server-nest/src/modules/Vendors/VendorGLEntriesStorage.ts @@ -0,0 +1,88 @@ +// import { Knex } from 'knex'; +// import { Service, Inject } from 'typedi'; +// import LedgerStorageService from '@/services/Accounting/LedgerStorageService'; +// import HasTenancyService from '@/services/Tenancy/TenancyService'; +// import { VendorGLEntries } from './VendorGLEntries'; + +// @Service() +// export class VendorGLEntriesStorage { +// @Inject() +// private tenancy: HasTenancyService; + +// @Inject() +// private ledegrRepository: LedgerStorageService; + +// @Inject() +// private vendorGLEntries: VendorGLEntries; + +// /** +// * Vendor opening balance journals. +// * @param {number} tenantId +// * @param {number} vendorId +// * @param {Knex.Transaction} trx +// */ +// public writeVendorOpeningBalance = async ( +// tenantId: number, +// vendorId: number, +// trx?: Knex.Transaction +// ) => { +// const { Vendor } = this.tenancy.models(tenantId); +// const { accountRepository } = this.tenancy.repositories(tenantId); + +// const vendor = await Vendor.query(trx).findById(vendorId); + +// // Finds the expense account. +// const expenseAccount = await accountRepository.findOne({ +// slug: 'other-expenses', +// }); +// // Find or create the A/P account. +// const APAccount = await accountRepository.findOrCreateAccountsPayable( +// vendor.currencyCode, +// {}, +// trx +// ); +// // Retrieves the vendor opening balance ledger. +// const ledger = this.vendorGLEntries.getOpeningBalanceLedger( +// APAccount.id, +// expenseAccount.id, +// vendor +// ); +// // Commits the ledger entries to the storage. +// await this.ledegrRepository.commit(tenantId, ledger, trx); +// }; + +// /** +// * Reverts the vendor opening balance GL entries. +// * @param {number} tenantId +// * @param {number} vendorId +// * @param {Knex.Transaction} trx +// */ +// public revertVendorOpeningBalance = async ( +// tenantId: number, +// vendorId: number, +// trx?: Knex.Transaction +// ) => { +// await this.ledegrRepository.deleteByReference( +// tenantId, +// vendorId, +// 'VendorOpeningBalance', +// trx +// ); +// }; + +// /** +// * Writes the vendor opening balance GL entries. +// * @param {number} tenantId +// * @param {number} vendorId +// * @param {Knex.Transaction} trx +// */ +// public rewriteVendorOpeningBalance = async ( +// tenantId: number, +// vendorId: number, +// trx?: Knex.Transaction +// ) => { +// await this.writeVendorOpeningBalance(tenantId, vendorId, trx); + +// await this.revertVendorOpeningBalance(tenantId, vendorId, trx); +// }; +// } diff --git a/packages/server-nest/src/modules/Vendors/Vendors.controller.ts b/packages/server-nest/src/modules/Vendors/Vendors.controller.ts new file mode 100644 index 000000000..a98951e7f --- /dev/null +++ b/packages/server-nest/src/modules/Vendors/Vendors.controller.ts @@ -0,0 +1,51 @@ +import { + Body, + Controller, + Delete, + Get, + Param, + Post, + Put, +} from '@nestjs/common'; +import { VendorsApplication } from './VendorsApplication.service'; +import { + IVendorEditDTO, + IVendorNewDTO, + IVendorOpeningBalanceEditDTO, +} from './types/Vendors.types'; + +@Controller('vendors') +export class VendorsController { + constructor(private vendorsApplication: VendorsApplication) {} + + @Get(':id') + getVendor(@Param('id') vendorId: number) { + return this.vendorsApplication.getVendor(vendorId); + } + + @Post() + createVendor(@Body() vendorDTO: IVendorNewDTO) { + return this.vendorsApplication.createVendor(vendorDTO); + } + + @Put(':id') + editVendor(@Param('id') vendorId: number, @Body() vendorDTO: IVendorEditDTO) { + return this.vendorsApplication.editVendor(vendorId, vendorDTO); + } + + @Delete(':id') + deleteVendor(@Param('id') vendorId: number) { + return this.vendorsApplication.deleteVendor(vendorId); + } + + @Put(':id/opening-balance') + editOpeningBalance( + @Param('id') vendorId: number, + @Body() openingBalanceDTO: IVendorOpeningBalanceEditDTO, + ) { + return this.vendorsApplication.editOpeningBalance( + vendorId, + openingBalanceDTO, + ); + } +} diff --git a/packages/server-nest/src/modules/Vendors/Vendors.module.ts b/packages/server-nest/src/modules/Vendors/Vendors.module.ts new file mode 100644 index 000000000..e49c879c2 --- /dev/null +++ b/packages/server-nest/src/modules/Vendors/Vendors.module.ts @@ -0,0 +1,33 @@ +import { Module } from '@nestjs/common'; +import { TenancyDatabaseModule } from '../Tenancy/TenancyDB/TenancyDB.module'; +import { TransformerInjectable } from '../Transformer/TransformerInjectable.service'; +import { ActivateVendorService } from './commands/ActivateVendor.service'; +import { CreateEditVendorDTOService } from './commands/CreateEditVendorDTO'; +import { CreateVendorService } from './commands/CreateVendor.service'; +import { DeleteVendorService } from './commands/DeleteVendor.service'; +import { EditOpeningBalanceVendorService } from './commands/EditOpeningBalanceVendor.service'; +import { EditVendorService } from './commands/EditVendor.service'; +import { GetVendorService } from './queries/GetVendor'; +import { VendorValidators } from './commands/VendorValidators'; +import { VendorsApplication } from './VendorsApplication.service'; +import { TenancyContext } from '../Tenancy/TenancyContext.service'; +import { VendorsController } from './Vendors.controller'; + +@Module({ + imports: [TenancyDatabaseModule], + controllers: [VendorsController], + providers: [ + ActivateVendorService, + CreateEditVendorDTOService, + CreateVendorService, + EditVendorService, + EditOpeningBalanceVendorService, + GetVendorService, + VendorValidators, + DeleteVendorService, + VendorsApplication, + TransformerInjectable, + TenancyContext, + ], +}) +export class VendorsModule {} diff --git a/packages/server-nest/src/modules/Vendors/VendorsApplication.service.ts b/packages/server-nest/src/modules/Vendors/VendorsApplication.service.ts new file mode 100644 index 000000000..04abcc68c --- /dev/null +++ b/packages/server-nest/src/modules/Vendors/VendorsApplication.service.ts @@ -0,0 +1,87 @@ +import { Injectable } from '@nestjs/common'; +import { Knex } from 'knex'; +import { CreateVendorService } from './commands/CreateVendor.service'; +import { EditVendorService } from './commands/EditVendor.service'; +import { DeleteVendorService } from './commands/DeleteVendor.service'; +import { EditOpeningBalanceVendorService } from './commands/EditOpeningBalanceVendor.service'; +import { GetVendorService } from './queries/GetVendor'; +import { + IVendorEditDTO, + IVendorNewDTO, + IVendorOpeningBalanceEditDTO, +} from './types/Vendors.types'; + +@Injectable() +export class VendorsApplication { + constructor( + private createVendorService: CreateVendorService, + private editVendorService: EditVendorService, + private deleteVendorService: DeleteVendorService, + private editOpeningBalanceService: EditOpeningBalanceVendorService, + private getVendorService: GetVendorService, + // private getVendorsService: GetVendors, + ) {} + + /** + * Creates a new vendor. + * @param {IVendorNewDTO} vendorDTO + * @return {Promise} + */ + public createVendor(vendorDTO: IVendorNewDTO, trx?: Knex.Transaction) { + return this.createVendorService.createVendor(vendorDTO, trx); + } + + /** + * Edits details of the given vendor. + * @param {number} vendorId - + * @param {IVendorEditDTO} vendorDTO - + * @returns {Promise} + */ + public editVendor(vendorId: number, vendorDTO: IVendorEditDTO) { + return this.editVendorService.editVendor(vendorId, vendorDTO); + } + + /** + * Deletes the given vendor. + * @param {number} vendorId + * @return {Promise} + */ + public deleteVendor(vendorId: number) { + return this.deleteVendorService.deleteVendor(vendorId); + } + + /** + * Changes the opening balance of the given customer. + * @param {number} vendorId + * @param {IVendorOpeningBalanceEditDTO} openingBalanceEditDTO + * @returns {Promise} + */ + public editOpeningBalance( + vendorId: number, + openingBalanceEditDTO: IVendorOpeningBalanceEditDTO, + ) { + return this.editOpeningBalanceService.editOpeningBalance( + vendorId, + openingBalanceEditDTO, + ); + } + + /** + * Retrieves the vendor details. + * @param {number} vendorId - Vendor ID. + * @returns + */ + public getVendor(vendorId: number) { + return this.getVendorService.getVendor(vendorId); + } + + /** + * Retrieves the vendors paginated list. + * @param {number} tenantId + * @param {IVendorsFilter} filterDTO + * @returns + */ + // public getVendors = (tenantId: number, filterDTO: IVendorsFilter) => { + // return this.getVendorsService.getVendorsList(tenantId, filterDTO); + // }; +} diff --git a/packages/server-nest/src/modules/Vendors/VendorsExportable.ts b/packages/server-nest/src/modules/Vendors/VendorsExportable.ts new file mode 100644 index 000000000..4db7de1bd --- /dev/null +++ b/packages/server-nest/src/modules/Vendors/VendorsExportable.ts @@ -0,0 +1,30 @@ +// import { Inject, Service } from 'typedi'; +// import { IItemsFilter } from '@/interfaces'; +// import { Exportable } from '@/services/Export/Exportable'; +// import { VendorsApplication } from './VendorsApplication'; +// import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants'; + +// @Service() +// export class VendorsExportable extends Exportable { +// @Inject() +// private vendorsApplication: VendorsApplication; + +// /** +// * Retrieves the accounts data to exportable sheet. +// * @param {number} tenantId +// * @returns +// */ +// public exportable(tenantId: number, query: IItemsFilter) { +// const parsedQuery = { +// sortOrder: 'DESC', +// columnSortBy: 'created_at', +// ...query, +// page: 1, +// pageSize: EXPORT_SIZE_LIMIT, +// } as IItemsFilter; + +// return this.vendorsApplication +// .getVendors(tenantId, parsedQuery) +// .then((output) => output.vendors); +// } +// } diff --git a/packages/server-nest/src/modules/Vendors/VendorsImportable.ts b/packages/server-nest/src/modules/Vendors/VendorsImportable.ts new file mode 100644 index 000000000..5836bb1c2 --- /dev/null +++ b/packages/server-nest/src/modules/Vendors/VendorsImportable.ts @@ -0,0 +1,32 @@ +// import { Importable } from '@/services/Import/Importable'; +// import { CreateVendor } from './CRUD/CreateVendor.service'; +// import { Knex } from 'knex'; +// import { Inject, Service } from 'typedi'; +// import { VendorsSampleData } from './_SampleData'; + +// @Service() +// export class VendorsImportable extends Importable { +// @Inject() +// private createVendorService: CreateVendor; + +// /** +// * Maps the imported data to create a new vendor service. +// * @param {number} tenantId +// * @param {} createDTO +// * @param {Knex.Transaction} trx +// */ +// public async importable( +// tenantId: number, +// createDTO: any, +// trx?: Knex.Transaction +// ): Promise { +// await this.createVendorService.createVendor(tenantId, createDTO, trx); +// } + +// /** +// * Retrieves the sample data of vendors sample sheet. +// */ +// public sampleData(): any[] { +// return VendorsSampleData; +// } +// } diff --git a/packages/server-nest/src/modules/Vendors/_SampleData.ts b/packages/server-nest/src/modules/Vendors/_SampleData.ts new file mode 100644 index 000000000..5e6e4edda --- /dev/null +++ b/packages/server-nest/src/modules/Vendors/_SampleData.ts @@ -0,0 +1,122 @@ +export const VendorsSampleData = [ + { + "First Name": "Nicolette", + "Last Name": "Schamberger", + "Company Name": "Homenick - Hane", + "Display Name": "Rowland Rowe", + "Email": "cicero86@yahoo.com", + "Personal Phone Number": "811-603-2235", + "Work Phone Number": "906-993-5190", + "Website": "http://google.com", + "Opening Balance": 54302.23, + "Opening Balance At": "2022-02-02", + "Opening Balance Ex. Rate": 2, + "Currency": "LYD", + "Active": "T", + "Note": "Doloribus autem optio temporibus dolores mollitia sit.", + "Billing Address 1": "862 Jessika Well", + "Billing Address 2": "1091 Dorthy Mount", + "Billing Address City": "Deckowfort", + "Billing Address Country": "Ghana", + "Billing Address Phone": "825-011-5207", + "Billing Address Postcode": "38228", + "Billing Address State": "Oregon", + "Shipping Address 1": "37626 Thiel Villages", + "Shipping Address 2": "132 Batz Avenue", + "Shipping Address City": "Pagacburgh", + "Shipping Address Country": "Albania", + "Shipping Address Phone": "171-546-3701", + "Shipping Address Postcode": "13709", + "Shipping Address State": "Georgia" + }, + { + "First Name": "Hermann", + "Last Name": "Crooks", + "Company Name": "Veum - Schaefer", + "Display Name": "Harley Veum", + "Email": "immanuel56@hotmail.com", + "Personal Phone Number": "449-780-9999", + "Work Phone Number": "970-473-5785", + "Website": "http://google.com", + "Opening Balance": 54302.23, + "Opening Balance At": "2022-02-02", + "Opening Balance Ex. Rate": 2, + "Currency": "LYD", + "Active": "T", + "Note": "Doloribus dolore dolor dicta vitae in fugit nisi quibusdam.", + "Billing Address 1": "532 Simonis Spring", + "Billing Address 2": "3122 Nicolas Inlet", + "Billing Address City": "East Matteofort", + "Billing Address Country": "Holy See (Vatican City State)", + "Billing Address Phone": "366-084-8629", + "Billing Address Postcode": "41607", + "Billing Address State": "Montana", + "Shipping Address 1": "2889 Tremblay Plaza", + "Shipping Address 2": "71355 Kutch Isle", + "Shipping Address City": "D'Amorehaven", + "Shipping Address Country": "Monaco", + "Shipping Address Phone": "614-189-3328", + "Shipping Address Postcode": "09634-0435", + "Shipping Address State": "Nevada" + }, + { + "First Name": "Nellie", + "Last Name": "Gulgowski", + "Company Name": "Boyle, Heller and Jones", + "Display Name": "Randall Kohler", + "Email": "anibal_frami@yahoo.com", + "Personal Phone Number": "498-578-0740", + "Work Phone Number": "394-550-6827", + "Website": "http://google.com", + "Opening Balance": 54302.23, + "Opening Balance At": "2022-02-02", + "Opening Balance Ex. Rate": 2, + "Currency": "LYD", + "Active": "T", + "Note": "Vero quibusdam rem fugit aperiam est modi.", + "Billing Address 1": "214 Sauer Villages", + "Billing Address 2": "30687 Kacey Square", + "Billing Address City": "Jayceborough", + "Billing Address Country": "Benin", + "Billing Address Phone": "332-820-1127", + "Billing Address Postcode": "16425-3887", + "Billing Address State": "Mississippi", + "Shipping Address 1": "562 Diamond Loaf", + "Shipping Address 2": "9595 Satterfield Trafficway", + "Shipping Address City": "Alexandrinefort", + "Shipping Address Country": "Puerto Rico", + "Shipping Address Phone": "776-500-8456", + "Shipping Address Postcode": "30258", + "Shipping Address State": "South Dakota" + }, + { + "First Name": "Stone", + "Last Name": "Jerde", + "Company Name": "Cassin, Casper and Maggio", + "Display Name": "Clint McLaughlin", + "Email": "nathanael22@yahoo.com", + "Personal Phone Number": "562-790-6059", + "Work Phone Number": "686-838-0027", + "Website": "http://google.com", + "Opening Balance": 54302.23, + "Opening Balance At": "2022-02-02", + "Opening Balance Ex. Rate": 2, + "Currency": "LYD", + "Active": "T", + "Note": "Quis cumque molestias rerum.", + "Billing Address 1": "22590 Cathy Harbor", + "Billing Address 2": "24493 Brycen Brooks", + "Billing Address City": "Elnorashire", + "Billing Address Country": "Andorra", + "Billing Address Phone": "701-852-8005", + "Billing Address Postcode": "5680", + "Billing Address State": "Nevada", + "Shipping Address 1": "5355 Erdman Bridge", + "Shipping Address 2": "421 Jeanette Camp", + "Shipping Address City": "East Philip", + "Shipping Address Country": "Venezuela", + "Shipping Address Phone": "426-119-0858", + "Shipping Address Postcode": "34929-0501", + "Shipping Address State": "Tennessee" + } +] diff --git a/packages/server-nest/src/modules/Vendors/commands/ActivateVendor.service.ts b/packages/server-nest/src/modules/Vendors/commands/ActivateVendor.service.ts new file mode 100644 index 000000000..9d4637847 --- /dev/null +++ b/packages/server-nest/src/modules/Vendors/commands/ActivateVendor.service.ts @@ -0,0 +1,58 @@ +import { Knex } from 'knex'; +import { Inject, Injectable } from '@nestjs/common'; +import { VendorValidators } from './VendorValidators'; +import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { Vendor } from '../models/Vendor'; +import { events } from '@/common/events/events'; +import { IVendorActivatedPayload } from '../types/Vendors.types'; + +@Injectable() +export class ActivateVendorService { + constructor( + private readonly uow: UnitOfWork, + private readonly eventPublisher: EventEmitter2, + private readonly validators: VendorValidators, + + @Inject(Vendor.name) + private readonly vendorModel: typeof Vendor, + ) {} + + /** + * Inactive the given contact. + * @param {number} vendorId - Vendor id. + * @returns {Promise} + */ + public async activateVendor(vendorId: number): Promise { + // Retrieves the old vendor or throw not found error. + const oldVendor = await this.vendorModel + .query() + .findById(vendorId) + .throwIfNotFound(); + + // Validate whether the vendor is already published. + this.validators.validateNotAlreadyPublished(oldVendor); + + // Edits the vendor with associated transactions on unit-of-work environment. + return this.uow.withTransaction(async (trx: Knex.Transaction) => { + // Triggers `onVendorActivating` event. + await this.eventPublisher.emitAsync(events.vendors.onActivating, { + trx, + oldVendor, + } as IVendorActivatedPayload); + + // Updates the vendor on the storage. + const vendor = await this.vendorModel + .query(trx) + .updateAndFetchById(vendorId, { + active: true, + }); + // Triggers `onVendorActivated` event. + await this.eventPublisher.emitAsync(events.vendors.onActivated, { + trx, + oldVendor, + vendor, + } as IVendorActivatedPayload); + }); + } +} diff --git a/packages/server-nest/src/modules/Vendors/commands/CreateEditVendorDTO.ts b/packages/server-nest/src/modules/Vendors/commands/CreateEditVendorDTO.ts new file mode 100644 index 000000000..ed8aa2925 --- /dev/null +++ b/packages/server-nest/src/modules/Vendors/commands/CreateEditVendorDTO.ts @@ -0,0 +1,72 @@ +import moment from 'moment'; +import { defaultTo, isEmpty } from 'lodash'; +import { Injectable } from '@nestjs/common'; +import { IVendorEditDTO, IVendorNewDTO } from '../types/Vendors.types'; +import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service'; +import { ContactService } from '@/modules/Contacts/types/Contacts.types'; +import { Vendor } from '../models/Vendor'; + +@Injectable() +export class CreateEditVendorDTOService { + /** + * @param {TenancyContext} tenancyContext - Tenancy context service. + */ + constructor(private readonly tenancyContext: TenancyContext) {} + + /** + * Transforms the common vendor DTO. + * @param {IVendorNewDTO | IVendorEditDTO} vendorDTO + * @returns {IVendorNewDTO | IVendorEditDTO} + */ + private transformCommonDTO = (vendorDTO: IVendorNewDTO | IVendorEditDTO) => { + return { + ...vendorDTO, + }; + }; + + /** + * Transformes the create vendor DTO. + * @param {IVendorNewDTO} vendorDTO - + * @returns {IVendorNewDTO} + */ + public transformCreateDTO = async ( + vendorDTO: IVendorNewDTO, + ): Promise> => { + const commonDTO = this.transformCommonDTO(vendorDTO); + + // Retrieves the tenant metadata. + const tenant = await this.tenancyContext.getTenant(true); + + return { + ...commonDTO, + currencyCode: vendorDTO.currencyCode || tenant.metadata.baseCurrency, + active: defaultTo(vendorDTO.active, true), + contactService: ContactService.Vendor, + + ...(!isEmpty(vendorDTO.openingBalanceAt) + ? { + openingBalanceAt: moment( + vendorDTO?.openingBalanceAt, + ).toMySqlDateTime(), + } + : {}), + openingBalanceExchangeRate: defaultTo( + vendorDTO.openingBalanceExchangeRate, + 1, + ), + }; + }; + + /** + * Transformes the edit vendor DTO. + * @param {IVendorEditDTO} vendorDTO + * @returns {IVendorEditDTO} + */ + public transformEditDTO = (vendorDTO: IVendorEditDTO) => { + const commonDTO = this.transformCommonDTO(vendorDTO); + + return { + ...commonDTO, + }; + }; +} diff --git a/packages/server-nest/src/modules/Vendors/commands/CreateVendor.service.ts b/packages/server-nest/src/modules/Vendors/commands/CreateVendor.service.ts new file mode 100644 index 000000000..13fe2416b --- /dev/null +++ b/packages/server-nest/src/modules/Vendors/commands/CreateVendor.service.ts @@ -0,0 +1,62 @@ +import { Knex } from 'knex'; +import { Inject, Injectable } from '@nestjs/common'; +import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { Vendor } from '../models/Vendor'; +import { events } from '@/common/events/events'; +import { + IVendorEventCreatedPayload, + IVendorEventCreatingPayload, + IVendorNewDTO, +} from '../types/Vendors.types'; +import { CreateEditVendorDTOService } from './CreateEditVendorDTO'; + +@Injectable() +export class CreateVendorService { + /** + * @param {UnitOfWork} uow - Unit of work service. + * @param {EventEmitter2} eventPublisher - Event emitter service. + * @param {CreateEditVendorDTOService} transformDTO - Create edit vendor DTO service. + * @param {typeof Vendor} vendorModel - Vendor model. + */ + constructor( + private readonly uow: UnitOfWork, + private readonly eventPublisher: EventEmitter2, + private readonly transformDTO: CreateEditVendorDTOService, + + @Inject(Vendor.name) + private readonly vendorModel: typeof Vendor, + ) {} + + /** + * Creates a new vendor. + * @param {IVendorNewDTO} vendorDTO + * @return {Promise} + */ + public async createVendor(vendorDTO: IVendorNewDTO, trx?: Knex.Transaction) { + // Transforms create DTO to customer object. + const vendorObject = await this.transformDTO.transformCreateDTO(vendorDTO); + + // Creates vendor contact under unit-of-work environment. + return this.uow.withTransaction(async (trx: Knex.Transaction) => { + // Triggers `onVendorCreating` event. + await this.eventPublisher.emitAsync(events.vendors.onCreating, { + vendorDTO, + trx, + } as IVendorEventCreatingPayload); + + // Creates a new contact as vendor. + const vendor = await this.vendorModel.query(trx).insertAndFetch({ + ...vendorObject, + }); + // Triggers `onVendorCreated` event. + await this.eventPublisher.emitAsync(events.vendors.onCreated, { + vendorId: vendor.id, + vendor, + trx, + } as IVendorEventCreatedPayload); + + return vendor; + }, trx); + } +} diff --git a/packages/server-nest/src/modules/Vendors/commands/DeleteVendor.service.ts b/packages/server-nest/src/modules/Vendors/commands/DeleteVendor.service.ts new file mode 100644 index 000000000..49b3ab82e --- /dev/null +++ b/packages/server-nest/src/modules/Vendors/commands/DeleteVendor.service.ts @@ -0,0 +1,60 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Knex } from 'knex'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service'; +import { Vendor } from '../models/Vendor'; +import { events } from '@/common/events/events'; +import { + IVendorEventDeletedPayload, + IVendorEventDeletingPayload, +} from '../types/Vendors.types'; + +@Injectable() +export class DeleteVendorService { + /** + * @param {EventEmitter2} eventPublisher - Event emitter service. + * @param {UnitOfWork} uow - Unit of work service. + * @param {typeof Vendor} contactModel - Vendor model. + */ + constructor( + private eventPublisher: EventEmitter2, + private uow: UnitOfWork, + @Inject(Vendor.name) private contactModel: typeof Vendor, + ) {} + + /** + * Deletes the given vendor. + * @param {number} vendorId + * @return {Promise} + */ + public async deleteVendor(vendorId: number) { + // Retrieves the old vendor or throw not found service error. + const oldVendor = await this.contactModel + .query() + .modify('vendor') + .findById(vendorId) + .throwIfNotFound(); + // .queryAndThrowIfHasRelations({ + // type: ERRORS.VENDOR_HAS_TRANSACTIONS, + // }); + + // Triggers `onVendorDeleting` event. + await this.eventPublisher.emitAsync(events.vendors.onDeleting, { + vendorId, + oldVendor, + } as IVendorEventDeletingPayload); + + // Deletes vendor contact under unit-of-work. + return this.uow.withTransaction(async (trx: Knex.Transaction) => { + // Deletes the vendor contact from the storage. + await this.contactModel.query(trx).findById(vendorId).delete(); + + // Triggers `onVendorDeleted` event. + await this.eventPublisher.emitAsync(events.vendors.onDeleted, { + vendorId, + oldVendor, + trx, + } as IVendorEventDeletedPayload); + }); + } +} diff --git a/packages/server-nest/src/modules/Vendors/commands/EditOpeningBalanceVendor.service.ts b/packages/server-nest/src/modules/Vendors/commands/EditOpeningBalanceVendor.service.ts new file mode 100644 index 000000000..11166258d --- /dev/null +++ b/packages/server-nest/src/modules/Vendors/commands/EditOpeningBalanceVendor.service.ts @@ -0,0 +1,77 @@ +import { Knex } from 'knex'; +import { Inject, Injectable } from '@nestjs/common'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { + IVendorOpeningBalanceEditDTO, + IVendorOpeningBalanceEditedPayload, + IVendorOpeningBalanceEditingPayload, +} from '../types/Vendors.types'; +import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service'; +import { Vendor } from '../models/Vendor'; +import { events } from '@/common/events/events'; + +@Injectable() +export class EditOpeningBalanceVendorService { + /** + * @param {EventEmitter2} eventPublisher - Event emitter service. + * @param {UnitOfWork} uow - Unit of work service. + * @param {typeof Vendor} vendorModel - Vendor model. + */ + constructor( + private readonly eventPublisher: EventEmitter2, + private readonly uow: UnitOfWork, + + @Inject(Vendor.name) + private readonly vendorModel: typeof Vendor, + ) {} + + /** + * Changes the opening balance of the given customer. + * @param {number} vendorId + * @param {IVendorOpeningBalanceEditDTO} openingBalanceEditDTO + * @returns {Promise} + */ + public async editOpeningBalance( + vendorId: number, + openingBalanceEditDTO: IVendorOpeningBalanceEditDTO, + ) { + // Retrieves the old vendor or throw not found error. + const oldVendor = await this.vendorModel + .query() + .findById(vendorId) + .throwIfNotFound(); + + // Mutates the customer opening balance under unit-of-work. + return this.uow.withTransaction(async (trx: Knex.Transaction) => { + // Triggers `onVendorOpeingBalanceChanging` event. + await this.eventPublisher.emitAsync( + events.vendors.onOpeningBalanceChanging, + { + oldVendor, + openingBalanceEditDTO, + trx, + } as IVendorOpeningBalanceEditingPayload, + ); + + // Mutates the vendor on the storage. + const vendor = await this.vendorModel + .query() + .patchAndFetchById(vendorId, { + ...openingBalanceEditDTO, + }); + + // Triggers `onVendorOpeingBalanceChanged` event. + await this.eventPublisher.emitAsync( + events.vendors.onOpeningBalanceChanged, + { + vendor, + oldVendor, + openingBalanceEditDTO, + trx, + } as IVendorOpeningBalanceEditedPayload, + ); + + return vendor; + }); + } +} diff --git a/packages/server-nest/src/modules/Vendors/commands/EditVendor.service.ts b/packages/server-nest/src/modules/Vendors/commands/EditVendor.service.ts new file mode 100644 index 000000000..34767ab42 --- /dev/null +++ b/packages/server-nest/src/modules/Vendors/commands/EditVendor.service.ts @@ -0,0 +1,65 @@ +import { + IVendorEditDTO, + IVendorEventEditedPayload, + IVendorEventEditingPayload, +} from '../types/Vendors.types'; +import { Knex } from 'knex'; +import { Inject, Injectable } from '@nestjs/common'; +import { CreateEditVendorDTOService } from './CreateEditVendorDTO'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service'; +import { Vendor } from '../models/Vendor'; +import { events } from '@/common/events/events'; + +@Injectable() +export class EditVendorService { + constructor( + private readonly eventPublisher: EventEmitter2, + private readonly uow: UnitOfWork, + private readonly transformDTO: CreateEditVendorDTOService, + + @Inject(Vendor.name) private readonly vendorModel: typeof Vendor, + ) {} + + /** + * Edits details of the given vendor. + * @param {number} vendorId - + * @param {IVendorEditDTO} vendorDTO - + * @returns {Promise} + */ + public async editVendor(vendorId: number, vendorDTO: IVendorEditDTO) { + // Retrieve the vendor or throw not found error. + const oldVendor = await this.vendorModel + .query() + .findById(vendorId) + .throwIfNotFound(); + + // Transforms vendor DTO to object. + const vendorObj = this.transformDTO.transformEditDTO(vendorDTO); + + // Edits vendor contact under unit-of-work environment. + return this.uow.withTransaction(async (trx: Knex.Transaction) => { + // Triggers `onVendorEditing` event. + await this.eventPublisher.emitAsync(events.vendors.onEditing, { + trx, + vendorDTO, + } as IVendorEventEditingPayload); + + // Edits the vendor contact. + const vendor = await this.vendorModel + .query() + .updateAndFetchById(vendorId, { + ...vendorObj, + }); + + // Triggers `onVendorEdited` event. + await this.eventPublisher.emitAsync(events.vendors.onEdited, { + vendorId, + vendor, + trx, + } as IVendorEventEditedPayload); + + return vendor; + }); + } +} diff --git a/packages/server-nest/src/modules/Vendors/commands/VendorValidators.ts b/packages/server-nest/src/modules/Vendors/commands/VendorValidators.ts new file mode 100644 index 000000000..bfeb45668 --- /dev/null +++ b/packages/server-nest/src/modules/Vendors/commands/VendorValidators.ts @@ -0,0 +1,16 @@ +import { Injectable } from '@nestjs/common'; +import { ServiceError } from '@/modules/Items/ServiceError'; +import { ERRORS } from '../constants'; + +@Injectable() +export class VendorValidators { + /** + * Validates the given vendor is not already activated. + * @param {IVendor} vendor + */ + public validateNotAlreadyPublished = (vendor) => { + if (vendor.active) { + throw new ServiceError(ERRORS.VENDOR_ALREADY_ACTIVE); + } + }; +} diff --git a/packages/server-nest/src/modules/Vendors/constants.ts b/packages/server-nest/src/modules/Vendors/constants.ts new file mode 100644 index 000000000..25f31f36a --- /dev/null +++ b/packages/server-nest/src/modules/Vendors/constants.ts @@ -0,0 +1,27 @@ +export const DEFAULT_VIEW_COLUMNS = []; + +export const DEFAULT_VIEWS = [ + { + name: 'Overdue', + slug: 'overdue', + rolesLogicExpression: '1', + roles: [ + { index: 1, fieldKey: 'status', comparator: 'equals', value: 'overdue' }, + ], + columns: DEFAULT_VIEW_COLUMNS, + }, + { + name: 'Unpaid', + slug: 'unpaid', + rolesLogicExpression: '1', + roles: [ + { index: 1, fieldKey: 'status', comparator: 'equals', value: 'unpaid' }, + ], + columns: DEFAULT_VIEW_COLUMNS, + }, +]; + +export const ERRORS = { + VENDOR_HAS_TRANSACTIONS: 'VENDOR_HAS_TRANSACTIONS', + VENDOR_ALREADY_ACTIVE: 'VENDOR_ALREADY_ACTIVE', +}; diff --git a/packages/server-nest/src/modules/Vendors/models/Vendor.ts b/packages/server-nest/src/modules/Vendors/models/Vendor.ts new file mode 100644 index 000000000..36be15685 --- /dev/null +++ b/packages/server-nest/src/modules/Vendors/models/Vendor.ts @@ -0,0 +1,219 @@ +import { Model, mixin } from 'objection'; +// import TenantModel from 'models/TenantModel'; +// import PaginationQueryBuilder from './Pagination'; +// import ModelSetting from './ModelSetting'; +// import VendorSettings from './Vendor.Settings'; +// import CustomViewBaseModel from './CustomViewBaseModel'; +// import { DEFAULT_VIEWS } from '@/services/Contacts/Vendors/constants'; +// import ModelSearchable from './ModelSearchable'; +import { BaseModel } from '@/models/Model'; + +// class VendorQueryBuilder extends PaginationQueryBuilder { +// constructor(...args) { +// super(...args); + +// this.onBuild((builder) => { +// if (builder.isFind() || builder.isDelete() || builder.isUpdate()) { +// builder.where('contact_service', 'vendor'); +// } +// }); +// } +// } + +export class Vendor extends BaseModel { + contactService: string; + contactType: string; + + balance: number; + currencyCode: string; + + openingBalance: number; + openingBalanceExchangeRate: number; + openingBalanceAt: Date | string; + + salutation: string; + firstName: string; + lastName: string; + companyName: string; + + displayName: string; + + email: string; + workPhone: string; + personalPhone: string; + website: string; + + billingAddress1: string; + billingAddress2: string; + billingAddressCity: string; + billingAddressCountry: string; + billingAddressEmail: string; + billingAddressPostcode: string; + billingAddressPhone: string; + billingAddressState: string; + + shippingAddress1: string; + shippingAddress2: string; + shippingAddressCity: string; + shippingAddressCountry: string; + shippingAddressEmail: string; + shippingAddressPostcode: string; + shippingAddressPhone: string; + shippingAddressState: string; + + note: string; + active: boolean; + + /** + * Query builder. + */ + // static get QueryBuilder() { + // return VendorQueryBuilder; + // } + + /** + * Table name + */ + static get tableName() { + return 'contacts'; + } + + /** + * Model timestamps. + */ + get timestamps() { + return ['createdAt', 'updatedAt']; + } + + /** + * Defined virtual attributes. + */ + static get virtualAttributes() { + return ['closingBalance', 'contactNormal', 'localOpeningBalance']; + } + + /** + * Closing balance attribute. + */ + get closingBalance() { + return this.balance; + } + + /** + * Retrieves the local opening balance. + * @returns {number} + */ + get localOpeningBalance() { + return this.openingBalance + ? this.openingBalance * this.openingBalanceExchangeRate + : 0; + } + + /** + * Retrieve the contact noraml; + */ + get contactNormal() { + return 'debit'; + } + + /** + * Model modifiers. + */ + static get modifiers() { + return { + /** + * Inactive/Active mode. + */ + inactiveMode(query, active = false) { + query.where('active', !active); + }, + + /** + * Filters the active customers. + */ + active(query) { + query.where('active', 1); + }, + /** + * Filters the inactive customers. + */ + inactive(query) { + query.where('active', 0); + }, + /** + * Filters the vendors that have overdue invoices. + */ + overdue(query) { + query.select( + '*', + Vendor.relatedQuery('overdueBills', query.knex()) + .count() + .as('countOverdue'), + ); + query.having('countOverdue', '>', 0); + }, + /** + * Filters the unpaid customers. + */ + unpaid(query) { + query.whereRaw('`BALANCE` + `OPENING_BALANCE` <> 0'); + }, + }; + } + + /** + * Relationship mapping. + */ + static get relationMappings() { + const { Bill } = require('../../Bills/models/Bill'); + + return { + bills: { + relation: Model.HasManyRelation, + modelClass: Bill.default, + join: { + from: 'contacts.id', + to: 'bills.vendorId', + }, + }, + overdueBills: { + relation: Model.HasManyRelation, + modelClass: Bill.default, + join: { + from: 'contacts.id', + to: 'bills.vendorId', + }, + filter: (query) => { + query.modify('overdue'); + }, + }, + }; + } + + // static get meta() { + // return VendorSettings; + // } + + // /** + // * Retrieve the default custom views, roles and columns. + // */ + // static get defaultViews() { + // return DEFAULT_VIEWS; + // } + + /** + * Model search attributes. + */ + static get searchRoles() { + return [ + { fieldKey: 'display_name', comparator: 'contains' }, + { condition: 'or', fieldKey: 'first_name', comparator: 'contains' }, + { condition: 'or', fieldKey: 'last_name', comparator: 'equals' }, + { condition: 'or', fieldKey: 'company_name', comparator: 'equals' }, + { condition: 'or', fieldKey: 'email', comparator: 'equals' }, + { condition: 'or', fieldKey: 'work_phone', comparator: 'equals' }, + { condition: 'or', fieldKey: 'personal_phone', comparator: 'equals' }, + { condition: 'or', fieldKey: 'website', comparator: 'equals' }, + ]; + } +} diff --git a/packages/server-nest/src/modules/Vendors/queries/GetVendor.ts b/packages/server-nest/src/modules/Vendors/queries/GetVendor.ts new file mode 100644 index 000000000..bd29569ac --- /dev/null +++ b/packages/server-nest/src/modules/Vendors/queries/GetVendor.ts @@ -0,0 +1,27 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service'; +import { Vendor } from '../models/Vendor'; +import { VendorTransfromer } from './VendorTransformer'; + +@Injectable() +export class GetVendorService { + constructor( + private readonly transformer: TransformerInjectable, + @Inject(Vendor.name) private readonly vendorModel: typeof Vendor, + ) {} + + /** + * Retrieve the given vendor details. + * @param {number} vendorId + */ + public async getVendor(vendorId: number) { + const vendor = await this.vendorModel + .query() + .findById(vendorId) + .modify('vendor') + .throwIfNotFound(); + + // Transformes the vendor. + return this.transformer.transform(vendor, new VendorTransfromer()); + } +} diff --git a/packages/server-nest/src/modules/Vendors/queries/GetVendors.ts b/packages/server-nest/src/modules/Vendors/queries/GetVendors.ts new file mode 100644 index 000000000..53bfd3f3f --- /dev/null +++ b/packages/server-nest/src/modules/Vendors/queries/GetVendors.ts @@ -0,0 +1,80 @@ +// import * as R from 'ramda'; +// import { Service, Inject } from 'typedi'; +// import { +// IFilterMeta, +// IPaginationMeta, +// IVendor, +// IVendorsFilter, +// } from '@/interfaces'; +// import HasTenancyService from '@/services/Tenancy/TenancyService'; +// import DynamicListingService from '@/services/DynamicListing/DynamicListService'; +// import VendorTransfromer from '../VendorTransformer'; +// import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; + +// @Service() +// export class GetVendors { +// @Inject() +// private tenancy: HasTenancyService; + +// @Inject() +// private dynamicListService: DynamicListingService; + +// @Inject() +// private transformer: TransformerInjectable; + +// /** +// * Retrieve vendors datatable list. +// * @param {number} tenantId - Tenant id. +// * @param {IVendorsFilter} vendorsFilter - Vendors filter. +// */ +// public async getVendorsList( +// tenantId: number, +// filterDTO: IVendorsFilter +// ): Promise<{ +// vendors: IVendor[]; +// pagination: IPaginationMeta; +// filterMeta: IFilterMeta; +// }> { +// const { Vendor } = this.tenancy.models(tenantId); + +// // Parses vendors list filter DTO. +// const filter = this.parseVendorsListFilterDTO(filterDTO); + +// // Dynamic list service. +// const dynamicList = await this.dynamicListService.dynamicList( +// tenantId, +// Vendor, +// filter +// ); +// // Vendors list. +// const { results, pagination } = await Vendor.query() +// .onBuild((builder) => { +// dynamicList.buildQuery()(builder); + +// // Switches between active/inactive modes. +// builder.modify('inactiveMode', filter.inactiveMode); +// }) +// .pagination(filter.page - 1, filter.pageSize); + +// // Transform the vendors. +// const transformedVendors = await this.transformer.transform( +// tenantId, +// results, +// new VendorTransfromer() +// ); +// return { +// vendors: transformedVendors, +// pagination, +// filterMeta: dynamicList.getResponseMeta(), +// }; +// } + +// /** +// * +// * @param filterDTO +// * @returns +// */ +// private parseVendorsListFilterDTO(filterDTO) { +// return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO); +// } +// } diff --git a/packages/server-nest/src/modules/Vendors/queries/VendorTransformer.ts b/packages/server-nest/src/modules/Vendors/queries/VendorTransformer.ts new file mode 100644 index 000000000..68f16a4ce --- /dev/null +++ b/packages/server-nest/src/modules/Vendors/queries/VendorTransformer.ts @@ -0,0 +1,15 @@ +import { ContactTransfromer } from "../../Contacts/Contact.transformer"; + +export class VendorTransfromer extends ContactTransfromer { + /** + * Include these attributes to expense object. + * @returns {Array} + */ + public includeAttributes = (): string[] => { + return [ + 'formattedBalance', + 'formattedOpeningBalance', + 'formattedOpeningBalanceAt' + ]; + }; +} diff --git a/packages/server-nest/src/modules/Vendors/subscribers/VendorGLEntriesSubscriber.ts b/packages/server-nest/src/modules/Vendors/subscribers/VendorGLEntriesSubscriber.ts new file mode 100644 index 000000000..69b66dc18 --- /dev/null +++ b/packages/server-nest/src/modules/Vendors/subscribers/VendorGLEntriesSubscriber.ts @@ -0,0 +1,91 @@ +// import { Inject, Service } from 'typedi'; +// import events from '@/subscribers/events'; +// import { VendorGLEntriesStorage } from '../VendorGLEntriesStorage'; +// import { +// IVendorEventCreatedPayload, +// IVendorEventDeletedPayload, +// IVendorOpeningBalanceEditedPayload, +// } from '@/interfaces'; + +// @Service() +// export class VendorsWriteGLOpeningSubscriber { +// @Inject() +// private vendorGLEntriesStorage: VendorGLEntriesStorage; + +// /** +// * Constructor method. +// */ +// public attach(bus) { +// bus.subscribe( +// events.vendors.onCreated, +// this.handleWriteOpeningBalanceEntries +// ); +// bus.subscribe( +// events.vendors.onDeleted, +// this.handleRevertOpeningBalanceEntries +// ); +// bus.subscribe( +// events.vendors.onOpeningBalanceChanged, +// this.handleRewriteOpeningEntriesOnChanged +// ); +// } + +// /** +// * Writes the open balance journal entries once the vendor created. +// * @param {IVendorEventCreatedPayload} payload - +// */ +// private handleWriteOpeningBalanceEntries = async ({ +// tenantId, +// vendor, +// trx, +// }: IVendorEventCreatedPayload) => { +// // Writes the vendor opening balance journal entries. +// if (vendor.openingBalance) { +// await this.vendorGLEntriesStorage.writeVendorOpeningBalance( +// tenantId, +// vendor.id, +// trx +// ); +// } +// }; + +// /** +// * Revert the opening balance journal entries once the vendor deleted. +// * @param {IVendorEventDeletedPayload} payload - +// */ +// private handleRevertOpeningBalanceEntries = async ({ +// tenantId, +// vendorId, +// trx, +// }: IVendorEventDeletedPayload) => { +// await this.vendorGLEntriesStorage.revertVendorOpeningBalance( +// tenantId, +// vendorId, +// trx +// ); +// }; + +// /** +// * Handles the rewrite opening balance entries once opening balnace changed. +// * @param {ICustomerOpeningBalanceEditedPayload} payload - +// */ +// private handleRewriteOpeningEntriesOnChanged = async ({ +// tenantId, +// vendor, +// trx, +// }: IVendorOpeningBalanceEditedPayload) => { +// if (vendor.openingBalance) { +// await this.vendorGLEntriesStorage.rewriteVendorOpeningBalance( +// tenantId, +// vendor.id, +// trx +// ); +// } else { +// await this.vendorGLEntriesStorage.revertVendorOpeningBalance( +// tenantId, +// vendor.id, +// trx +// ); +// } +// }; +// } diff --git a/packages/server-nest/src/modules/Vendors/types/Vendors.types.ts b/packages/server-nest/src/modules/Vendors/types/Vendors.types.ts new file mode 100644 index 000000000..6ba0137de --- /dev/null +++ b/packages/server-nest/src/modules/Vendors/types/Vendors.types.ts @@ -0,0 +1,128 @@ +// Vendor Interfaces. + +import { Knex } from 'knex'; +import { Vendor } from '../models/Vendor'; +import { IContactAddressDTO } from '@/modules/Contacts/types/Contacts.types'; + +// ---------------------------------- +export interface IVendorNewDTO extends IContactAddressDTO { + currencyCode: string; + + openingBalance?: number; + openingBalanceAt?: string; + openingBalanceExchangeRate?: number; + openingBalanceBranchId?: number; + + salutation?: string; + firstName?: string; + lastName?: string; + companyName?: string; + displayName: string; + + website?: string; + email?: string; + workPhone?: string; + personalPhone?: string; + + note?: string; + active?: boolean; +} +export interface IVendorEditDTO extends IContactAddressDTO { + salutation?: string; + firstName?: string; + lastName?: string; + companyName?: string; + displayName?: string; + + website?: string; + email?: string; + workPhone?: string; + personalPhone?: string; + + note?: string; + active?: boolean; +} + +// export interface IVendorsFilter extends IDynamicListFilter { +// stringifiedFilterRoles?: string; +// page?: number; +// pageSize?: number; +// } + +// Vendor Events. +// ---------------------------------- +export interface IVendorEventCreatingPayload { + // tenantId: number; + vendorDTO: IVendorNewDTO; + // authorizedUser: ISystemUser; + trx: Knex.Transaction; +} + +export interface IVendorEventCreatedPayload { + // tenantId: number; + vendorId: number; + vendor: Vendor; + // authorizedUser: ISystemUser; + trx: Knex.Transaction; +} + +export interface IVendorEventDeletingPayload { + // tenantId: number; + vendorId: number; + oldVendor: Vendor; +} + +export interface IVendorEventDeletedPayload { + // tenantId: number; + vendorId: number; + // authorizedUser: ISystemUser; + oldVendor: Vendor; + trx?: Knex.Transaction; +} +export interface IVendorEventEditingPayload { + // tenantId: number; + vendorDTO: IVendorEditDTO; + trx?: Knex.Transaction; +} +export interface IVendorEventEditedPayload { + // tenantId: number; + vendorId: number; + vendor: Vendor; + // authorizedUser: ISystemUser; + trx?: Knex.Transaction; +} + +export interface IVendorOpeningBalanceEditDTO { + openingBalance: number; + openingBalanceAt: Date | string; + openingBalanceExchangeRate: number; + openingBalanceBranchId?: number; +} + +export interface IVendorOpeningBalanceEditingPayload { + // tenantId: number; + oldVendor: Vendor; + openingBalanceEditDTO: IVendorOpeningBalanceEditDTO; + trx?: Knex.Transaction; +} + +export interface IVendorOpeningBalanceEditedPayload { + // tenantId: number; + vendor: Vendor; + oldVendor: Vendor; + openingBalanceEditDTO: IVendorOpeningBalanceEditDTO; + trx: Knex.Transaction; +} + +export interface IVendorActivatingPayload { + // tenantId: number; + oldVendor: Vendor; + trx: Knex.Transaction; +} + +export interface IVendorActivatedPayload { + // tenantId: number; + vendor: Vendor; + oldVendor: Vendor; + trx?: Knex.Transaction; +} diff --git a/packages/server-nest/src/utils/entries-amount-diff.ts b/packages/server-nest/src/utils/entries-amount-diff.ts new file mode 100644 index 000000000..0a0ebad1c --- /dev/null +++ b/packages/server-nest/src/utils/entries-amount-diff.ts @@ -0,0 +1,30 @@ +import _ from 'lodash'; + +export const entriesAmountDiff = ( + newEntries, + oldEntries, + amountAttribute, + idAttribute, +) => { + const oldEntriesTable = _.chain(oldEntries) + .groupBy(idAttribute) + .mapValues((group) => _.sumBy(group, amountAttribute) || 0) + .value(); + + const newEntriesTable = _.chain(newEntries) + .groupBy(idAttribute) + .mapValues((group) => _.sumBy(group, amountAttribute) || 0) + .mergeWith(oldEntriesTable, (objValue, srcValue) => { + return _.isNumber(objValue) ? objValue - srcValue : srcValue * -1; + }) + .value(); + + return _.chain(newEntriesTable) + .mapValues((value, key) => ({ + [idAttribute]: key, + [amountAttribute]: value, + })) + .filter((entry) => entry[amountAttribute] != 0) + .values() + .value(); +}; diff --git a/packages/server-nest/src/utils/template-render.ts b/packages/server-nest/src/utils/template-render.ts new file mode 100644 index 000000000..54b25e1ee --- /dev/null +++ b/packages/server-nest/src/utils/template-render.ts @@ -0,0 +1,7 @@ +import path from 'path'; +import pug from 'pug'; + +export function templateRender(filePath: string, options: Record) { + const basePath = path.join(global.__resources_dir, '/views'); + return pug.renderFile(`${basePath}/${filePath}.pug`, options); +} diff --git a/packages/server/src/models/BillPayment.ts b/packages/server/src/models/BillPayment.ts index ab4b41bec..fa20f8bde 100644 --- a/packages/server/src/models/BillPayment.ts +++ b/packages/server/src/models/BillPayment.ts @@ -1,16 +1,22 @@ -import { Model, mixin } from 'objection'; -import TenantModel from 'models/TenantModel'; -import ModelSetting from './ModelSetting'; +import ModelBase from './Model'; import BillPaymentSettings from './BillPayment.Settings'; -import CustomViewBaseModel from './CustomViewBaseModel'; import { DEFAULT_VIEWS } from '@/services/Sales/PaymentReceived/constants'; -import ModelSearchable from './ModelSearchable'; -export default class BillPayment extends mixin(TenantModel, [ - ModelSetting, - CustomViewBaseModel, - ModelSearchable, -]) { +export class BillPayment extends ModelBase { + vendorId: number; + amount: number; + currencyCode: string; + reference: string; + paymentAccountId: number; + paymentNumber: string; + paymentDate: Date; + exchangeRate: number | null; + userId: number; + statement: string; + + createdAt: Date; + updatedAt: Date; + /** * Table name */ diff --git a/packages/server/src/models/SaleInvoice.ts b/packages/server/src/models/SaleInvoice.ts index 883b39ce6..c75e4884a 100644 --- a/packages/server/src/models/SaleInvoice.ts +++ b/packages/server/src/models/SaleInvoice.ts @@ -23,6 +23,7 @@ export default class SaleInvoice extends mixin(TenantModel, [ public writtenoffAt: Date; public dueDate: Date; public deliveredAt: Date; + public pdfTemplateId: number; /** * Table name diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3d2689f0c..033a98934 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -484,6 +484,15 @@ importers: packages/server-nest: dependencies: + '@bigcapital/email-components': + specifier: '*' + version: link:../../shared/email-components + '@bigcapital/pdf-templates': + specifier: '*' + version: link:../../shared/pdf-templates + '@bigcapital/utils': + specifier: '*' + version: link:../../shared/bigcapital-utils '@nestjs/bull': specifier: ^10.2.1 version: 10.2.2(@nestjs/common@10.4.7)(@nestjs/core@10.4.7)(bull@4.16.4) @@ -526,6 +535,15 @@ importers: '@types/ramda': specifier: ^0.30.2 version: 0.30.2 + accounting: + specifier: ^0.4.1 + version: 0.4.1 + async: + specifier: ^3.2.0 + version: 3.2.5 + axios: + specifier: ^1.6.0 + version: 1.7.7 bull: specifier: ^4.16.3 version: 4.16.4 @@ -547,9 +565,15 @@ importers: express-validator: specifier: ^7.2.0 version: 7.2.0 + form-data: + specifier: ^4.0.0 + version: 4.0.0 fp-ts: specifier: ^2.16.9 version: 2.16.9 + js-money: + specifier: ^0.6.3 + version: 0.6.3 knex: specifier: ^3.1.0 version: 3.1.0(mysql2@3.11.4)(mysql@2.18.1) @@ -574,6 +598,9 @@ importers: nestjs-i18n: specifier: ^10.4.9 version: 10.5.0(@nestjs/common@10.4.7)(@nestjs/core@10.4.7)(class-validator@0.14.1)(rxjs@7.8.1) + object-hash: + specifier: ^2.0.3 + version: 2.2.0 objection: specifier: ^3.1.5 version: 3.1.5(knex@3.1.0) @@ -586,6 +613,9 @@ importers: passport-local: specifier: ^1.0.0 version: 1.0.0 + pug: + specifier: ^3.0.2 + version: 3.0.2 ramda: specifier: ^0.30.1 version: 0.30.1 @@ -598,6 +628,15 @@ importers: rxjs: specifier: ^7.8.1 version: 7.8.1 + serialize-interceptor: + specifier: ^1.1.7 + version: 1.1.7(cache-manager@6.1.3)(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2) + strategy: + specifier: ^1.1.1 + version: 1.1.1 + uuid: + specifier: ^10.0.0 + version: 10.0.0 zod: specifier: ^3.23.8 version: 3.23.8 @@ -655,7 +694,7 @@ importers: version: 29.2.5(@babel/core@7.26.0)(esbuild@0.23.1)(jest@29.7.0)(typescript@5.6.3) ts-loader: specifier: ^9.4.3 - version: 9.5.1(typescript@5.6.3)(webpack@5.91.0) + version: 9.5.1(typescript@5.6.3)(webpack@5.96.1) ts-node: specifier: ^10.9.1 version: 10.9.2(@types/node@20.5.1)(typescript@5.6.3) @@ -728,9 +767,6 @@ importers: '@stripe/react-connect-js': specifier: ^3.3.13 version: 3.3.13(@stripe/connect-js@3.3.12)(react-dom@18.3.1)(react@18.3.1) - '@testing-library/jest-dom': - specifier: ^4.2.4 - version: 4.2.4 '@testing-library/react': specifier: ^9.4.0 version: 9.5.0(react-dom@18.3.1)(react@18.3.1) @@ -758,9 +794,6 @@ importers: '@tiptap/starter-kit': specifier: 2.1.13 version: 2.1.13(@tiptap/pm@2.1.13) - '@types/jest': - specifier: ^26.0.15 - version: 26.0.24 '@types/js-money': specifier: ^0.6.1 version: 0.6.5 @@ -841,7 +874,7 @@ importers: version: 0.11.0 dotenv-webpack: specifier: ^8.0.1 - version: 8.1.0(webpack@5.91.0) + version: 8.1.0(webpack@5.96.1) eslint: specifier: ^8.33.0 version: 8.57.0 @@ -863,18 +896,6 @@ importers: http-proxy-middleware: specifier: ^1.0.0 version: 1.3.1 - jest: - specifier: 24.9.0 - version: 24.9.0 - jest-environment-jsdom-fourteen: - specifier: 1.0.1 - version: 1.0.1 - jest-resolve: - specifier: 24.9.0 - version: 24.9.0 - jest-watch-typeahead: - specifier: 0.4.2 - version: 0.4.2 js-cookie: specifier: 2.2.1 version: 2.2.1 @@ -928,7 +949,7 @@ importers: version: 6.2.1(react@18.3.1) react-dev-utils: specifier: ^11.0.4 - version: 11.0.4(eslint@8.57.0)(typescript@4.9.5)(webpack@5.91.0) + version: 11.0.4(eslint@8.57.0)(typescript@4.9.5)(webpack@5.96.1) react-dom: specifier: ^18.2.0 version: 18.3.1(react@18.3.1) @@ -1093,7 +1114,7 @@ importers: version: 7.2.2(react-dom@18.3.1)(react@18.3.1) '@storybook/addon-styling': specifier: 1.3.6 - version: 1.3.6(@types/react-dom@18.3.0)(@types/react@18.3.4)(less@4.2.0)(postcss@8.4.47)(react-dom@18.3.1)(react@18.3.1)(typescript@5.6.3)(webpack@5.91.0) + version: 1.3.6(@types/react-dom@18.3.0)(@types/react@18.3.4)(less@4.2.0)(postcss@8.4.47)(react-dom@18.3.1)(react@18.3.1)(typescript@5.6.3)(webpack@5.96.1) '@storybook/blocks': specifier: 7.2.2 version: 7.2.2(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1) @@ -1451,7 +1472,7 @@ packages: '@smithy/util-middleware': 3.0.0 '@smithy/util-retry': 3.0.0 '@smithy/util-utf8': 3.0.0 - tslib: 2.6.2 + tslib: 2.8.0 transitivePeerDependencies: - aws-crt dev: false @@ -1613,7 +1634,7 @@ packages: '@smithy/util-middleware': 3.0.0 '@smithy/util-retry': 3.0.0 '@smithy/util-utf8': 3.0.0 - tslib: 2.6.2 + tslib: 2.8.0 transitivePeerDependencies: - aws-crt dev: false @@ -1688,7 +1709,7 @@ packages: '@aws-sdk/types': 3.577.0 '@smithy/property-provider': 3.0.0 '@smithy/types': 3.0.0 - tslib: 2.6.2 + tslib: 2.8.0 transitivePeerDependencies: - aws-crt dev: false @@ -1701,7 +1722,7 @@ packages: '@aws-sdk/types': 3.577.0 '@smithy/property-provider': 3.0.0 '@smithy/types': 3.0.0 - tslib: 2.6.2 + tslib: 2.8.0 dev: false /@aws-sdk/credential-provider-http@3.582.0: @@ -1716,7 +1737,7 @@ packages: '@smithy/smithy-client': 3.0.1 '@smithy/types': 3.0.0 '@smithy/util-stream': 3.0.1 - tslib: 2.6.2 + tslib: 2.8.0 dev: false /@aws-sdk/credential-provider-ini@3.583.0(@aws-sdk/client-sso-oidc@3.583.0)(@aws-sdk/client-sts@3.583.0): @@ -1735,7 +1756,7 @@ packages: '@smithy/property-provider': 3.0.0 '@smithy/shared-ini-file-loader': 3.0.0 '@smithy/types': 3.0.0 - tslib: 2.6.2 + tslib: 2.8.0 transitivePeerDependencies: - '@aws-sdk/client-sso-oidc' - aws-crt @@ -1771,7 +1792,7 @@ packages: '@smithy/property-provider': 3.0.0 '@smithy/shared-ini-file-loader': 3.0.0 '@smithy/types': 3.0.0 - tslib: 2.6.2 + tslib: 2.8.0 dev: false /@aws-sdk/credential-provider-sso@3.583.0(@aws-sdk/client-sso-oidc@3.583.0): @@ -1784,7 +1805,7 @@ packages: '@smithy/property-provider': 3.0.0 '@smithy/shared-ini-file-loader': 3.0.0 '@smithy/types': 3.0.0 - tslib: 2.6.2 + tslib: 2.8.0 transitivePeerDependencies: - '@aws-sdk/client-sso-oidc' - aws-crt @@ -1800,7 +1821,7 @@ packages: '@aws-sdk/types': 3.577.0 '@smithy/property-provider': 3.0.0 '@smithy/types': 3.0.0 - tslib: 2.6.2 + tslib: 2.8.0 dev: false /@aws-sdk/credential-providers@3.583.0(@aws-sdk/client-sso-oidc@3.583.0): @@ -1823,7 +1844,7 @@ packages: '@smithy/credential-provider-imds': 3.0.0 '@smithy/property-provider': 3.0.0 '@smithy/types': 3.0.0 - tslib: 2.6.2 + tslib: 2.8.0 transitivePeerDependencies: - '@aws-sdk/client-sso-oidc' - aws-crt @@ -2018,7 +2039,7 @@ packages: '@smithy/property-provider': 3.0.0 '@smithy/shared-ini-file-loader': 3.0.0 '@smithy/types': 3.0.0 - tslib: 2.6.2 + tslib: 2.8.0 dev: false /@aws-sdk/types@3.577.0: @@ -2033,7 +2054,7 @@ packages: resolution: {integrity: sha512-XUKJWWo+KOB7fbnPP0+g/o5Ulku/X53t7i/h+sPHr5xxYTJJ9CYnbToo95mzxe7xWvkLrsNtJ8L+MnNn9INs2w==} engines: {node: '>=16.0.0'} dependencies: - tslib: 2.6.2 + tslib: 2.8.0 dev: false /@aws-sdk/util-endpoints@3.583.0: @@ -2060,7 +2081,7 @@ packages: resolution: {integrity: sha512-3nh4TINkXYr+H41QaPelCceEB2FXP3fxp93YZXB/kqJvX0U9j0N0Uk45gvsjmEPzG8XxkPEeLIfT2I1M7A6Lig==} engines: {node: '>=16.0.0'} dependencies: - tslib: 2.6.2 + tslib: 2.8.0 dev: false /@aws-sdk/util-user-agent-browser@3.577.0: @@ -2090,7 +2111,7 @@ packages: /@aws-sdk/util-utf8-browser@3.259.0: resolution: {integrity: sha512-UvFa/vR+e19XookZF8RzFZBrw2EUkQWxiBW0yYQAhvk3C+QVGl0H3ouca8LDBlBfQKXwmW3huo/59H8rwb1wJw==} dependencies: - tslib: 2.6.2 + tslib: 2.8.0 dev: false /@aws-sdk/xml-builder@3.575.0: @@ -4680,6 +4701,7 @@ packages: '@babel/code-frame': 7.26.0 '@babel/parser': 7.26.1 '@babel/types': 7.26.0 + dev: true /@babel/template@7.25.9: resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==} @@ -4979,15 +5001,6 @@ packages: commander: 2.20.3 dev: false - /@cnakazawa/watch@1.0.4: - resolution: {integrity: sha512-v9kIhKwjeZThiWrLmj0y17CWoyddASLj9O2yvbZkbvw/N3rWOYy9zkV66ursAoVr0mV15bL8g0c4QZUE6cdDoQ==} - engines: {node: '>=0.1.95'} - hasBin: true - dependencies: - exec-sh: 0.3.6 - minimist: 1.2.8 - dev: false - /@colors/colors@1.5.0: resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} @@ -6430,15 +6443,6 @@ packages: resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} engines: {node: '>=8'} - /@jest/console@24.9.0: - resolution: {integrity: sha512-Zuj6b8TnKXi3q4ymac8EQfc3ea/uhLeCGThFqXeC8H9/raaH8ARPUTdId+XyGd03Z4In0/VjD2OYFcBF09fNLQ==} - engines: {node: '>= 6'} - dependencies: - '@jest/source-map': 24.9.0 - chalk: 2.4.2 - slash: 2.0.0 - dev: false - /@jest/console@27.5.1: resolution: {integrity: sha512-kZ/tNpS3NXn0mlXXXPNuDZnb4c0oZ20r4K5eemM2k30ZC3G0T02nXUvyhf5YdbXWHPEJLc9qGLxEZ216MdL+Zg==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -6475,44 +6479,6 @@ packages: slash: 3.0.0 dev: true - /@jest/core@24.9.0: - resolution: {integrity: sha512-Fogg3s4wlAr1VX7q+rhV9RVnUv5tD7VuWfYy1+whMiWUrvl7U3QJSJyWcDio9Lq2prqYsZaeTv2Rz24pWGkJ2A==} - engines: {node: '>= 6'} - dependencies: - '@jest/console': 24.9.0 - '@jest/reporters': 24.9.0 - '@jest/test-result': 24.9.0 - '@jest/transform': 24.9.0 - '@jest/types': 24.9.0 - ansi-escapes: 3.2.0 - chalk: 2.4.2 - exit: 0.1.2 - graceful-fs: 4.2.11 - jest-changed-files: 24.9.0 - jest-config: 24.9.0 - jest-haste-map: 24.9.0 - jest-message-util: 24.9.0 - jest-regex-util: 24.9.0 - jest-resolve: 24.9.0 - jest-resolve-dependencies: 24.9.0 - jest-runner: 24.9.0 - jest-runtime: 24.9.0 - jest-snapshot: 24.9.0 - jest-util: 24.9.0 - jest-validate: 24.9.0 - jest-watcher: 24.9.0 - micromatch: 3.1.10(supports-color@5.5.0) - p-each-series: 1.0.0 - realpath-native: 1.1.0 - rimraf: 2.7.1 - slash: 2.0.0 - strip-ansi: 5.2.0 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - dev: false - /@jest/core@27.5.1(ts-node@10.9.2): resolution: {integrity: sha512-AK6/UTrvQD0Cd24NSqmIA6rKsu0tKIxfiCducZvqxYdmMisOYAsdItspT+fQDQYARPf8XgjAFZi0ogW2agH5nQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -6601,18 +6567,6 @@ packages: - ts-node dev: true - /@jest/environment@24.9.0: - resolution: {integrity: sha512-5A1QluTPhvdIPFYnO3sZC3smkNeXPVELz7ikPbhUj0bQjB07EoE9qtLrem14ZUYWdVayYbsjVwIiL4WBIMV4aQ==} - engines: {node: '>= 6'} - dependencies: - '@jest/fake-timers': 24.9.0 - '@jest/transform': 24.9.0 - '@jest/types': 24.9.0 - jest-mock: 24.9.0 - transitivePeerDependencies: - - supports-color - dev: false - /@jest/environment@27.5.1: resolution: {integrity: sha512-/WQjhPJe3/ghaol/4Bq480JKXV/Rfw8nQdN7f41fM8VDHLcxKXou6QyXAh3EFr9/bVG3x74z1NWDkP87EiY8gA==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -6650,17 +6604,6 @@ packages: - supports-color dev: true - /@jest/fake-timers@24.9.0: - resolution: {integrity: sha512-eWQcNa2YSwzXWIMC5KufBh3oWRIijrQFROsIqt6v/NS9Io/gknw1jsAC9c+ih/RQX4A3O7SeWAhQeN0goKhT9A==} - engines: {node: '>= 6'} - dependencies: - '@jest/types': 24.9.0 - jest-message-util: 24.9.0 - jest-mock: 24.9.0 - transitivePeerDependencies: - - supports-color - dev: false - /@jest/fake-timers@27.5.1: resolution: {integrity: sha512-/aPowoolwa07k7/oM3aASneNeBGCmGQsc3ugN4u6s4C/+s5M64MFo/+djTdiwcbQlRfFElGuDXWzaWj6QgKObQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -6706,37 +6649,6 @@ packages: - supports-color dev: true - /@jest/reporters@24.9.0: - resolution: {integrity: sha512-mu4X0yjaHrffOsWmVLzitKmmmWSQ3GGuefgNscUSWNiUNcEOSEQk9k3pERKEQVBb0Cnn88+UESIsZEMH3o88Gw==} - engines: {node: '>= 6'} - dependencies: - '@jest/environment': 24.9.0 - '@jest/test-result': 24.9.0 - '@jest/transform': 24.9.0 - '@jest/types': 24.9.0 - chalk: 2.4.2 - exit: 0.1.2 - glob: 7.2.3 - istanbul-lib-coverage: 2.0.5 - istanbul-lib-instrument: 3.3.0 - istanbul-lib-report: 2.0.8 - istanbul-lib-source-maps: 3.0.6 - istanbul-reports: 2.2.7 - jest-haste-map: 24.9.0 - jest-resolve: 24.9.0 - jest-runtime: 24.9.0 - jest-util: 24.9.0 - jest-worker: 24.9.0 - node-notifier: 5.4.5 - slash: 2.0.0 - source-map: 0.6.1 - string-length: 2.0.0 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - dev: false - /@jest/reporters@27.5.1: resolution: {integrity: sha512-cPXh9hWIlVJMQkVk84aIvXuBB4uQQmFqZiacloFuGiP3ah1sbCxCosidXFDfqG8+6fO1oR2dTJTlsOy4VFmUfw==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -6826,15 +6738,6 @@ packages: '@sinclair/typebox': 0.27.8 dev: true - /@jest/source-map@24.9.0: - resolution: {integrity: sha512-/Xw7xGlsZb4MJzNDgB7PW5crou5JqWiBQaz6xyPd3ArOg2nfn/PunV8+olXbbEZzNl591o5rWKE9BRDaFAuIBg==} - engines: {node: '>= 6'} - dependencies: - callsites: 3.1.0 - graceful-fs: 4.2.11 - source-map: 0.6.1 - dev: false - /@jest/source-map@27.5.1: resolution: {integrity: sha512-y9NIHUYF3PJRlHk98NdC/N1gl88BL08aQQgu4k4ZopQkCw9t9cV8mtl3TV8b/YCB8XaVTFrmUTAJvjsntDireg==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -6853,15 +6756,6 @@ packages: graceful-fs: 4.2.11 dev: true - /@jest/test-result@24.9.0: - resolution: {integrity: sha512-XEFrHbBonBJ8dGp2JmF8kP/nQI/ImPpygKHwQ/SY+es59Z3L5PI4Qb9TQQMAEeYsThG1xF0k6tmG0tIKATNiiA==} - engines: {node: '>= 6'} - dependencies: - '@jest/console': 24.9.0 - '@jest/types': 24.9.0 - '@types/istanbul-lib-coverage': 2.0.6 - dev: false - /@jest/test-result@27.5.1: resolution: {integrity: sha512-EW35l2RYFUcUQxFJz5Cv5MTOxlJIQs4I7gxzi2zVU7PJhOwfYq1MdC5nhSmYjX1gmMmLPvB3sIaC+BkcHRBfag==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -6892,20 +6786,6 @@ packages: collect-v8-coverage: 1.0.2 dev: true - /@jest/test-sequencer@24.9.0: - resolution: {integrity: sha512-6qqsU4o0kW1dvA95qfNog8v8gkRN9ph6Lz7r96IvZpHdNipP2cBcb07J1Z45mz/VIS01OHJ3pY8T5fUY38tg4A==} - engines: {node: '>= 6'} - dependencies: - '@jest/test-result': 24.9.0 - jest-haste-map: 24.9.0 - jest-runner: 24.9.0 - jest-runtime: 24.9.0 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - dev: false - /@jest/test-sequencer@27.5.1: resolution: {integrity: sha512-LCheJF7WB2+9JuCS7VB/EmGIdQuhtqjRNI9A43idHv3E4KltCTsPsLxvdaubFHSYwY/fNjMWjl6vNRhDiN7vpQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -6928,30 +6808,6 @@ packages: slash: 3.0.0 dev: true - /@jest/transform@24.9.0: - resolution: {integrity: sha512-TcQUmyNRxV94S0QpMOnZl0++6RMiqpbH/ZMccFB/amku6Uwvyb1cjYX7xkp5nGNkbX4QPH/FcB6q1HBTHynLmQ==} - engines: {node: '>= 6'} - dependencies: - '@babel/core': 7.26.0 - '@jest/types': 24.9.0 - babel-plugin-istanbul: 5.2.0 - chalk: 2.4.2 - convert-source-map: 1.9.0 - fast-json-stable-stringify: 2.1.0 - graceful-fs: 4.2.11 - jest-haste-map: 24.9.0 - jest-regex-util: 24.9.0 - jest-util: 24.9.0 - micromatch: 3.1.10(supports-color@5.5.0) - pirates: 4.0.6 - realpath-native: 1.1.0 - slash: 2.0.0 - source-map: 0.6.1 - write-file-atomic: 2.4.1 - transitivePeerDependencies: - - supports-color - dev: false - /@jest/transform@27.5.1: resolution: {integrity: sha512-ipON6WtYgl/1329g5AIJVbUuEh0wZVbdpGwC99Jw4LwuoBNS95MVphU6zOeD9pDkon+LLbFL7lOQRapbB8SCHw==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -7017,17 +6873,6 @@ packages: chalk: 3.0.0 dev: false - /@jest/types@26.6.2: - resolution: {integrity: sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==} - engines: {node: '>= 10.14.2'} - dependencies: - '@types/istanbul-lib-coverage': 2.0.6 - '@types/istanbul-reports': 3.0.4 - '@types/node': 20.5.1 - '@types/yargs': 15.0.19 - chalk: 4.1.2 - dev: false - /@jest/types@27.5.1: resolution: {integrity: sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -7504,6 +7349,35 @@ packages: tslib: 2.7.0 uid: 2.0.2 + /@nestjs/common@8.4.7(cache-manager@6.1.3)(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1): + resolution: {integrity: sha512-m/YsbcBal+gA5CFrDpqXqsSfylo+DIQrkFY3qhVIltsYRfu8ct8J9pqsTO6OPf3mvqdOpFGrV5sBjoyAzOBvsw==} + peerDependencies: + cache-manager: '*' + class-transformer: '*' + class-validator: '*' + reflect-metadata: ^0.1.12 + rxjs: ^7.1.0 + peerDependenciesMeta: + cache-manager: + optional: true + class-transformer: + optional: true + class-validator: + optional: true + dependencies: + axios: 0.27.2 + cache-manager: 6.1.3 + class-transformer: 0.5.1 + class-validator: 0.14.1 + iterare: 1.2.1 + reflect-metadata: 0.2.2 + rxjs: 7.8.1 + tslib: 2.4.0 + uuid: 8.3.2 + transitivePeerDependencies: + - debug + dev: false + /@nestjs/config@3.3.0(@nestjs/common@10.4.7)(rxjs@7.8.1): resolution: {integrity: sha512-pdGTp8m9d0ZCrjTpjkUbZx6gyf2IKf+7zlkrPNMsJzYZ4bFRRTpXrnj+556/5uiI6AfL5mMrJc2u7dB6bvM+VA==} peerDependencies: @@ -7719,7 +7593,7 @@ packages: /@newrelic/security-agent@1.2.0: resolution: {integrity: sha512-Snk++TQmqHKuxPYOH5bEU4GCr5xKYurUZWx3oiuoQUV73pw61qeEMrb/8iuGgAghwpCEC/8n+308efqCIZkiiQ==} dependencies: - axios: 1.7.2 + axios: 1.7.7 check-disk-space: 3.4.0 content-type: 1.0.5 fast-safe-stringify: 2.1.1 @@ -9610,20 +9484,20 @@ packages: engines: {node: '>=16.0.0'} dependencies: '@smithy/types': 3.0.0 - tslib: 2.6.2 + tslib: 2.8.0 dev: false /@smithy/chunked-blob-reader-native@3.0.0: resolution: {integrity: sha512-VDkpCYW+peSuM4zJip5WDfqvg2Mo/e8yxOv3VF1m11y7B8KKMKVFtmZWDe36Fvk8rGuWrPZHHXZ7rR7uM5yWyg==} dependencies: '@smithy/util-base64': 3.0.0 - tslib: 2.6.2 + tslib: 2.8.0 dev: false /@smithy/chunked-blob-reader@3.0.0: resolution: {integrity: sha512-sbnURCwjF0gSToGlsBiAmd1lRCmSn72nu9axfJu5lIx6RUEgHu6GwTMbqCdhQSi0Pumcm5vFxsi9XWXb2mTaoA==} dependencies: - tslib: 2.6.2 + tslib: 2.8.0 dev: false /@smithy/config-resolver@3.0.0: @@ -9659,7 +9533,7 @@ packages: '@smithy/property-provider': 3.0.0 '@smithy/types': 3.0.0 '@smithy/url-parser': 3.0.0 - tslib: 2.6.2 + tslib: 2.8.0 dev: false /@smithy/eventstream-codec@3.0.0: @@ -9668,7 +9542,7 @@ packages: '@aws-crypto/crc32': 3.0.0 '@smithy/types': 3.0.0 '@smithy/util-hex-encoding': 3.0.0 - tslib: 2.6.2 + tslib: 2.8.0 dev: false /@smithy/eventstream-serde-browser@3.0.0: @@ -9703,7 +9577,7 @@ packages: dependencies: '@smithy/eventstream-codec': 3.0.0 '@smithy/types': 3.0.0 - tslib: 2.6.2 + tslib: 2.8.0 dev: false /@smithy/fetch-http-handler@3.0.1: @@ -9755,7 +9629,7 @@ packages: resolution: {integrity: sha512-+Fsu6Q6C4RSJiy81Y8eApjEB5gVtM+oFKTffg+jSuwtvomJJrhUJBu2zS8wjXSgH/g1MKEWrzyChTBe6clb5FQ==} engines: {node: '>=16.0.0'} dependencies: - tslib: 2.6.2 + tslib: 2.8.0 dev: false /@smithy/md5-js@3.0.0: @@ -9845,7 +9719,7 @@ packages: engines: {node: '>=16.0.0'} dependencies: '@smithy/types': 3.0.0 - tslib: 2.6.2 + tslib: 2.8.0 dev: false /@smithy/protocol-http@4.0.0: @@ -9862,7 +9736,7 @@ packages: dependencies: '@smithy/types': 3.0.0 '@smithy/util-uri-escape': 3.0.0 - tslib: 2.6.2 + tslib: 2.8.0 dev: false /@smithy/querystring-parser@3.0.0: @@ -9870,7 +9744,7 @@ packages: engines: {node: '>=16.0.0'} dependencies: '@smithy/types': 3.0.0 - tslib: 2.6.2 + tslib: 2.8.0 dev: false /@smithy/service-error-classification@3.0.0: @@ -9885,7 +9759,7 @@ packages: engines: {node: '>=16.0.0'} dependencies: '@smithy/types': 3.0.0 - tslib: 2.6.2 + tslib: 2.8.0 dev: false /@smithy/signature-v4@3.0.0: @@ -9898,7 +9772,7 @@ packages: '@smithy/util-middleware': 3.0.0 '@smithy/util-uri-escape': 3.0.0 '@smithy/util-utf8': 3.0.0 - tslib: 2.6.2 + tslib: 2.8.0 dev: false /@smithy/smithy-client@3.0.1: @@ -9955,14 +9829,14 @@ packages: engines: {node: '>=16.0.0'} dependencies: '@smithy/is-array-buffer': 3.0.0 - tslib: 2.6.2 + tslib: 2.8.0 dev: false /@smithy/util-config-provider@3.0.0: resolution: {integrity: sha512-pbjk4s0fwq3Di/ANL+rCvJMKM5bzAQdE5S/6RL5NXgMExFAi6UgQMPOm5yPaIWPpr+EOXKXRonJ3FoxKf4mCJQ==} engines: {node: '>=16.0.0'} dependencies: - tslib: 2.6.2 + tslib: 2.8.0 dev: false /@smithy/util-defaults-mode-browser@3.0.1: @@ -10002,7 +9876,7 @@ packages: resolution: {integrity: sha512-eFndh1WEK5YMUYvy3lPlVmYY/fZcQE1D8oSf41Id2vCeIkKJXPcYDCZD+4+xViI6b1XSd7tE+s5AmXzz5ilabQ==} engines: {node: '>=16.0.0'} dependencies: - tslib: 2.6.2 + tslib: 2.8.0 dev: false /@smithy/util-middleware@3.0.0: @@ -10010,7 +9884,7 @@ packages: engines: {node: '>=16.0.0'} dependencies: '@smithy/types': 3.0.0 - tslib: 2.6.2 + tslib: 2.8.0 dev: false /@smithy/util-retry@3.0.0: @@ -10040,7 +9914,7 @@ packages: resolution: {integrity: sha512-LqR7qYLgZTD7nWLBecUi4aqolw8Mhza9ArpNEQ881MJJIU2sE5iHCK6TdyqqzcDLy0OPe10IY4T8ctVdtynubg==} engines: {node: '>=16.0.0'} dependencies: - tslib: 2.6.2 + tslib: 2.8.0 dev: false /@smithy/util-utf8@3.0.0: @@ -10399,6 +10273,64 @@ packages: - typescript dev: true + /@storybook/addon-styling@1.3.6(@types/react-dom@18.3.0)(@types/react@18.3.4)(less@4.2.0)(postcss@8.4.47)(react-dom@18.3.1)(react@18.3.1)(typescript@5.6.3)(webpack@5.96.1): + resolution: {integrity: sha512-g4e9vLnNlpjR3hHcyC8iCtgqcWQj6IPK+HZ8PdF92O95sa0nus+NG4meahEBwCsZm6CtYV/QMymFtLnp2NvAmg==} + deprecated: 'This package has been split into @storybook/addon-styling-webpack and @storybook/addon-themes. More info: https://github.com/storybookjs/addon-styling' + hasBin: true + peerDependencies: + less: ^3.5.0 || ^4.0.0 + postcss: ^7.0.0 || ^8.0.1 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + webpack: ^5.0.0 + peerDependenciesMeta: + less: + optional: true + postcss: + optional: true + react: + optional: true + react-dom: + optional: true + webpack: + optional: true + dependencies: + '@babel/template': 7.25.9 + '@babel/types': 7.26.0 + '@storybook/api': 7.6.17(react-dom@18.3.1)(react@18.3.1) + '@storybook/components': 7.6.20(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1) + '@storybook/core-common': 7.6.20 + '@storybook/core-events': 7.6.20 + '@storybook/manager-api': 7.6.20(react-dom@18.3.1)(react@18.3.1) + '@storybook/node-logger': 7.6.20 + '@storybook/preview-api': 7.6.20 + '@storybook/theming': 7.6.20(react-dom@18.3.1)(react@18.3.1) + '@storybook/types': 7.6.20 + css-loader: 6.11.0(webpack@5.96.1) + less: 4.2.0 + less-loader: 11.1.4(less@4.2.0)(webpack@5.96.1) + postcss: 8.4.47 + postcss-loader: 7.3.4(postcss@8.4.47)(typescript@5.6.3)(webpack@5.96.1) + prettier: 2.8.8 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + resolve-url-loader: 5.0.0 + sass-loader: 13.3.3(webpack@5.96.1) + style-loader: 3.3.4(webpack@5.96.1) + webpack: 5.96.1(esbuild@0.18.20) + transitivePeerDependencies: + - '@rspack/core' + - '@types/react' + - '@types/react-dom' + - encoding + - fibers + - node-sass + - sass + - sass-embedded + - supports-color + - typescript + dev: true + /@storybook/addon-toolbars@7.2.2(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-KQkFwLEqi/Xm9wq6mqaqO6VEW7+Sgbr8BUtVRAB9sSpy2mlhZEzZpcWKjuHmyMb9/eiRvnicM9kXxz0tHAF0jA==} peerDependencies: @@ -11546,21 +11478,6 @@ packages: pretty-format: 27.5.1 dev: true - /@testing-library/jest-dom@4.2.4: - resolution: {integrity: sha512-j31Bn0rQo12fhCWOUWy9fl7wtqkp7In/YP2p5ZFyRuiiB9Qs3g+hS4gAmDWONbAHcRmVooNJ5eOHQDCOmUFXHg==} - engines: {node: '>=8', npm: '>=6'} - dependencies: - '@babel/runtime': 7.24.5 - chalk: 2.4.2 - css: 2.2.4 - css.escape: 1.5.1 - jest-diff: 24.9.0 - jest-matcher-utils: 24.9.0 - lodash: 4.17.21 - pretty-format: 24.9.0 - redent: 3.0.0 - dev: false - /@testing-library/react@9.5.0(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-di1b+D0p+rfeboHO5W7gTVeZDIK5+maEgstrZbWZSSvxDyfDRkkyBE1AJR5Psd6doNldluXlCWqXriUfqu/9Qg==} engines: {node: '>=8'} @@ -12236,13 +12153,6 @@ packages: dependencies: '@types/istanbul-lib-report': 3.0.3 - /@types/jest@26.0.24: - resolution: {integrity: sha512-E/X5Vib8BWqZNRlDxj9vYXhsDwPYbPINqKF9BsnSoon4RQ0D9moEuLD8txgyypFLH7J4+Lho9Nr/c8H0Fi+17w==} - dependencies: - jest-diff: 26.6.2 - pretty-format: 26.6.2 - dev: false - /@types/jest@29.5.14: resolution: {integrity: sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==} dependencies: @@ -12558,10 +12468,6 @@ packages: resolution: {integrity: sha512-b79830lW+RZfwaztgs1aVPgbasJ8e7AXtZYHTELNXZPsERt4ymJdjV4OccDbHQAvHrCcFpbF78jkm0R6h/pZVg==} dev: false - /@types/stack-utils@1.0.1: - resolution: {integrity: sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==} - dev: false - /@types/stack-utils@2.0.3: resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} @@ -13593,7 +13499,7 @@ packages: esbuild: '>=0.10.0' dependencies: esbuild: 0.18.20 - tslib: 2.6.2 + tslib: 2.8.0 dev: true /@yarnpkg/fslib@2.10.3: @@ -13663,13 +13569,6 @@ packages: resolution: {integrity: sha512-RU6KY9Y5wllyaCNBo1W11ZOTnTHMMgOZkIwdOOs6W5ibMTp72i4xIbEA48djxVGqMNTUNbvrP/1nWg5Af5m2gQ==} dev: false - /acorn-globals@4.3.4: - resolution: {integrity: sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A==} - dependencies: - acorn: 6.4.2 - acorn-walk: 6.2.0 - dev: false - /acorn-globals@6.0.0: resolution: {integrity: sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==} dependencies: @@ -13691,12 +13590,12 @@ packages: dependencies: acorn: 8.13.0 - /acorn-import-attributes@1.9.5(acorn@8.13.0): + /acorn-import-attributes@1.9.5(acorn@8.14.0): resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} peerDependencies: acorn: ^8 dependencies: - acorn: 8.13.0 + acorn: 8.14.0 dev: false /acorn-jsx@5.3.2(acorn@7.4.1): @@ -13722,11 +13621,6 @@ packages: acorn: 8.13.0 dev: true - /acorn-walk@6.2.0: - resolution: {integrity: sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==} - engines: {node: '>=0.4.0'} - dev: false - /acorn-walk@7.2.0: resolution: {integrity: sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==} engines: {node: '>=0.4.0'} @@ -13735,18 +13629,6 @@ packages: resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==} engines: {node: '>=0.4.0'} - /acorn@5.7.4: - resolution: {integrity: sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==} - engines: {node: '>=0.4.0'} - hasBin: true - dev: false - - /acorn@6.4.2: - resolution: {integrity: sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==} - engines: {node: '>=0.4.0'} - hasBin: true - dev: false - /acorn@7.4.1: resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==} engines: {node: '>=0.4.0'} @@ -13766,7 +13648,6 @@ packages: resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} engines: {node: '>=0.4.0'} hasBin: true - dev: true /add-dom-event-listener@1.1.0: resolution: {integrity: sha512-WCxx1ixHT0GQU9hb0KI/mhgRQhnU+U3GvwY6ZvVjYq8rsihIGoaIOUbY0yMPBxLH5MDtr0kz3fisWGNcbWW7Jw==} @@ -14003,11 +13884,6 @@ packages: engines: {node: '>=6'} dev: true - /ansi-escapes@3.2.0: - resolution: {integrity: sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==} - engines: {node: '>=4'} - dev: false - /ansi-escapes@4.3.2: resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} engines: {node: '>=8'} @@ -14153,7 +14029,7 @@ packages: resolution: {integrity: sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==} engines: {node: '>=10'} dependencies: - tslib: 2.6.2 + tslib: 2.8.0 dev: true /aria-query@4.2.2: @@ -14222,10 +14098,6 @@ packages: engines: {node: '>=0.10.0'} dev: false - /array-equal@1.0.2: - resolution: {integrity: sha512-gUHx76KtnhEgB3HOuFYiCm3FIdEs6ocM2asHvNTkfu/Y09qQVrrVVaOKENmS2KkSaGoxgXNqC+ZVtR/n0MOkSA==} - dev: false - /array-find@1.0.0: resolution: {integrity: sha512-kO/vVCacW9mnpn3WPWbTVlEnOabK2L7LWi2HViURtCM46y1zb6I8UMjx4LgbiqadTgHnLInUronwn3ampNTJtQ==} dev: true @@ -14411,21 +14283,10 @@ packages: minimalistic-assert: 1.0.1 dev: true - /asn1@0.2.6: - resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} - dependencies: - safer-buffer: 2.1.2 - dev: false - /assert-never@1.2.1: resolution: {integrity: sha512-TaTivMB6pYI1kXwrFlEhLeGfOqoDNdTxjCdwRfFFkEA30Eu+k48W34nlok2EYWJfFFzqaEmichdNM7th6M5HNw==} dev: false - /assert-plus@1.0.0: - resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==} - engines: {node: '>=0.8'} - dev: false - /assert@1.5.1: resolution: {integrity: sha512-zzw1uCAgLbsKwBfFc8CX78DDg+xZeBksSO3vwVIDDN5i94eOrPsSSyiVhmsSABFDM/OcpE2aagCat9dnWQLG1A==} dependencies: @@ -14462,21 +14323,16 @@ packages: resolution: {integrity: sha512-c27loCv9QkZinsa5ProX751khO9DJl/AcB5c2KNtA6NRvHKS0PgLfcftz72KVq504vB0Gku5s2kUZzDBvQWvHg==} engines: {node: '>=4'} dependencies: - tslib: 2.6.2 + tslib: 2.8.0 dev: true /ast-types@0.16.1: resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} engines: {node: '>=4'} dependencies: - tslib: 2.6.2 + tslib: 2.8.0 dev: true - /astral-regex@1.0.0: - resolution: {integrity: sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==} - engines: {node: '>=4'} - dev: false - /async-done@1.3.2: resolution: {integrity: sha512-uYkTP8dw2og1tu1nmza1n1CMW0qb8gWWlwqMmLb7MhBVs4BXrFziT6HXUd+/RlRA/i4H9AkofYloUbs1fwMqlw==} engines: {node: '>= 0.10'} @@ -14493,6 +14349,7 @@ packages: /async-limiter@1.0.1: resolution: {integrity: sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==} + dev: true /async-mutex@0.5.0: resolution: {integrity: sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==} @@ -14551,19 +14408,11 @@ packages: dependencies: possible-typed-array-names: 1.0.0 - /aws-sign2@0.7.0: - resolution: {integrity: sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==} - dev: false - /aws-ssl-profiles@1.1.2: resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==} engines: {node: '>= 6.0.0'} dev: false - /aws4@1.13.0: - resolution: {integrity: sha512-3AungXC4I8kKsS9PuS4JH2nc+0bVY/mjgrephHTIi8fpEeGsTHBUJeosp0Wc1myYMElmD0B3Oc4XL/HVJ4PV2g==} - dev: false - /axe-core@4.7.0: resolution: {integrity: sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==} engines: {node: '>=4'} @@ -14577,6 +14426,15 @@ packages: - debug dev: false + /axios@0.27.2: + resolution: {integrity: sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==} + dependencies: + follow-redirects: 1.15.6 + form-data: 4.0.0 + transitivePeerDependencies: + - debug + dev: false + /axios@1.7.2: resolution: {integrity: sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==} dependencies: @@ -14585,6 +14443,7 @@ packages: proxy-from-env: 1.1.0 transitivePeerDependencies: - debug + dev: false /axios@1.7.7: resolution: {integrity: sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==} @@ -14594,7 +14453,6 @@ packages: proxy-from-env: 1.1.0 transitivePeerDependencies: - debug - dev: false /axobject-query@3.2.1: resolution: {integrity: sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==} @@ -14610,24 +14468,6 @@ packages: '@babel/core': 7.26.0 dev: true - /babel-jest@24.9.0(@babel/core@7.24.5): - resolution: {integrity: sha512-ntuddfyiN+EhMw58PTNL1ph4C9rECiQXjI4nMMBKBaNjXvqLdkXpPRcMSr4iyBrJg/+wz9brFUD6RhOAT6r4Iw==} - engines: {node: '>= 6'} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.24.5 - '@jest/transform': 24.9.0 - '@jest/types': 24.9.0 - '@types/babel__core': 7.20.5 - babel-plugin-istanbul: 5.2.0 - babel-preset-jest: 24.9.0(@babel/core@7.24.5) - chalk: 2.4.2 - slash: 2.0.0 - transitivePeerDependencies: - - supports-color - dev: false - /babel-jest@27.5.1(@babel/core@7.24.5): resolution: {integrity: sha512-cdQ5dXjGRd0IBRATiQ4mZGlGlRE8kJpjPOixdNRdT+m3UcNqmYWN6rK6nvtXYfY3D76cb8s/O1Ss8ea24PIwcg==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -14729,18 +14569,6 @@ packages: - supports-color dev: false - /babel-plugin-istanbul@5.2.0: - resolution: {integrity: sha512-5LphC0USA8t4i1zCtjbbNb6jJj/9+X6P37Qfirc/70EQ34xKlMW+a1RHGwxGI+SwWpNwZ27HqvzAobeqaXwiZw==} - engines: {node: '>=6'} - dependencies: - '@babel/helper-plugin-utils': 7.24.5 - find-up: 3.0.0 - istanbul-lib-instrument: 3.3.0 - test-exclude: 5.2.3 - transitivePeerDependencies: - - supports-color - dev: false - /babel-plugin-istanbul@6.1.1: resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} engines: {node: '>=8'} @@ -14753,13 +14581,6 @@ packages: transitivePeerDependencies: - supports-color - /babel-plugin-jest-hoist@24.9.0: - resolution: {integrity: sha512-2EMA2P8Vp7lG0RAzr4HXqtYwacfMErOuv1U3wrvxHX6rD1sV6xS3WXG3r8TRQ2r6w8OhvSdWt+z41hQNwNm3Xw==} - engines: {node: '>= 6'} - dependencies: - '@types/babel__traverse': 7.20.6 - dev: false - /babel-plugin-jest-hoist@27.5.1: resolution: {integrity: sha512-50wCwD5EMNW4aRpOwtqzyZHIewTYNxLA4nhB+09d8BIssfNfzBRhkBIHiaPv1Si226TQSvp8gxAJm2iY2qs2hQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -14937,17 +14758,6 @@ packages: '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.26.0) '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.26.0) - /babel-preset-jest@24.9.0(@babel/core@7.24.5): - resolution: {integrity: sha512-izTUuhE4TMfTRPF92fFwD2QfdXaZW08qvWTFCI51V8rW5x00UuPgc3ajRoWofXOuxjfcOM5zzSYsQS3H8KGCAg==} - engines: {node: '>= 6'} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.24.5 - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.24.5) - babel-plugin-jest-hoist: 24.9.0 - dev: false - /babel-preset-jest@27.5.1(@babel/core@7.24.5): resolution: {integrity: sha512-Nptf2FzlPCWYuJg41HBqXVT8ym6bXOevuCTbhxlUpjwtysGaIWFvDEjp4y+G7fl13FgOdjs7P/DmErqH7da0Ag==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -15134,12 +14944,6 @@ packages: resolution: {integrity: sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==} dev: false - /bcrypt-pbkdf@1.0.2: - resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} - dependencies: - tweetnacl: 0.14.5 - dev: false - /bcryptjs@2.4.3: resolution: {integrity: sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==} dev: false @@ -15380,12 +15184,6 @@ packages: resolution: {integrity: sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==} dev: false - /browser-resolve@1.11.3: - resolution: {integrity: sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ==} - dependencies: - resolve: 1.1.7 - dev: false - /browser-stdout@1.3.1: resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==} dev: true @@ -15760,7 +15558,7 @@ packages: resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==} dependencies: pascal-case: 3.1.2 - tslib: 2.6.2 + tslib: 2.8.0 dev: false /camelcase-css@2.0.1: @@ -15821,17 +15619,10 @@ packages: resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==} dependencies: no-case: 3.0.4 - tslib: 2.6.2 + tslib: 2.8.0 upper-case-first: 2.0.2 dev: false - /capture-exit@2.0.0: - resolution: {integrity: sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g==} - engines: {node: 6.* || 8.* || >= 10.*} - dependencies: - rsvp: 4.8.5 - dev: false - /capture-stack-trace@1.0.2: resolution: {integrity: sha512-X/WM2UQs6VMHUtjUDnZTRI+i1crWteJySFzr9UpGoQa4WQffXVTTXuekjl7TjZRlcF2XfjgITT0HxZ9RnxeT0w==} engines: {node: '>=0.10.0'} @@ -15947,7 +15738,7 @@ packages: path-case: 3.0.4 sentence-case: 3.0.4 snake-case: 3.0.4 - tslib: 2.6.2 + tslib: 2.8.0 dev: false /char-regex@1.0.2: @@ -16044,10 +15835,6 @@ packages: resolution: {integrity: sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==} dev: false - /ci-info@2.0.0: - resolution: {integrity: sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==} - dev: false - /ci-info@3.9.0: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} @@ -16158,6 +15945,7 @@ packages: string-width: 3.1.0 strip-ansi: 5.2.0 wrap-ansi: 5.1.0 + dev: true /cliui@7.0.4: resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} @@ -16542,7 +16330,7 @@ packages: resolution: {integrity: sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==} dependencies: no-case: 3.0.4 - tslib: 2.6.2 + tslib: 2.8.0 upper-case: 2.0.2 dev: false @@ -16747,10 +16535,6 @@ packages: requiresBuild: true dev: false - /core-util-is@1.0.2: - resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} - dev: false - /core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -16987,6 +16771,7 @@ packages: semver: 5.7.2 shebang-command: 1.2.0 which: 1.3.1 + dev: true /cross-spawn@7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} @@ -17108,6 +16893,29 @@ packages: semver: 7.6.2 webpack: 5.91.0(esbuild@0.23.1) + /css-loader@6.11.0(webpack@5.96.1): + resolution: {integrity: sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==} + engines: {node: '>= 12.13.0'} + peerDependencies: + '@rspack/core': 0.x || 1.x + webpack: ^5.0.0 + peerDependenciesMeta: + '@rspack/core': + optional: true + webpack: + optional: true + dependencies: + icss-utils: 5.1.0(postcss@8.4.47) + postcss: 8.4.47 + postcss-modules-extract-imports: 3.1.0(postcss@8.4.47) + postcss-modules-local-by-default: 4.0.5(postcss@8.4.47) + postcss-modules-scope: 3.2.0(postcss@8.4.47) + postcss-modules-values: 4.0.0(postcss@8.4.47) + postcss-value-parser: 4.2.0 + semver: 7.6.2 + webpack: 5.96.1(esbuild@0.18.20) + dev: true + /css-minimizer-webpack-plugin@3.4.1(esbuild@0.23.1)(webpack@5.91.0): resolution: {integrity: sha512-1u6D71zeIfgngN2XNRJefc/hY7Ybsxd74Jm4qngIXyUEk7fss3VUzuHxLAq/R8NAba4QU9OUSaMZlbpRc7bM4Q==} engines: {node: '>= 12.13.0'} @@ -17204,19 +17012,6 @@ packages: engines: {node: '>= 6'} dev: false - /css.escape@1.5.1: - resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} - dev: false - - /css@2.2.4: - resolution: {integrity: sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw==} - dependencies: - inherits: 2.0.4 - source-map: 0.6.1 - source-map-resolve: 0.5.3 - urix: 0.1.0 - dev: false - /cssdb@7.11.2: resolution: {integrity: sha512-lhQ32TFkc1X4eTefGfYPvgovRSzIMofHkigfH8nWtyRL4XJLsRhJFreRvEgKzept7x1rjBuy3J/MurXLaFxW/A==} dev: false @@ -17300,12 +17095,6 @@ packages: resolution: {integrity: sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==} dev: false - /cssstyle@1.4.0: - resolution: {integrity: sha512-GBrLZYZ4X4x6/QEoBnIrqb8B/f5l4+8me2dkom/j1Gtbxy0kBv6OGzKuAsGM75bkGwGAFkt56Iwg28S3XTZgSA==} - dependencies: - cssom: 0.3.8 - dev: false - /cssstyle@2.3.0: resolution: {integrity: sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==} engines: {node: '>=8'} @@ -17348,25 +17137,10 @@ packages: engines: {node: '>=8'} dev: true - /dashdash@1.14.1: - resolution: {integrity: sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==} - engines: {node: '>=0.10'} - dependencies: - assert-plus: 1.0.0 - dev: false - /dasherize@2.0.0: resolution: {integrity: sha512-APql/TZ6FdLEpf2z7/X2a2zyqK8juYtqaSVqxw9mYoQ64CXkfU15AeLh8pUszT8+fnYjgm6t0aIYpWKJbnLkuA==} dev: false - /data-urls@1.1.0: - resolution: {integrity: sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==} - dependencies: - abab: 2.0.6 - whatwg-mimetype: 2.3.0 - whatwg-url: 7.1.0 - dev: false - /data-urls@2.0.0: resolution: {integrity: sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==} engines: {node: '>=10'} @@ -17794,11 +17568,6 @@ packages: dev: false optional: true - /detect-newline@2.1.0: - resolution: {integrity: sha512-CwffZFvlJffUg9zZA0uqrjQayUTC8ob94pnr5sFwaVv3IOmkfUHcWH+jXaQK3askE51Cqe8/9Ql/0uXNwqZ8Zg==} - engines: {node: '>=0.10.0'} - dev: false - /detect-newline@3.1.0: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} @@ -17855,16 +17624,6 @@ packages: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} dev: false - /diff-sequences@24.9.0: - resolution: {integrity: sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew==} - engines: {node: '>= 6'} - dev: false - - /diff-sequences@26.6.2: - resolution: {integrity: sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==} - engines: {node: '>= 10.14.2'} - dev: false - /diff-sequences@27.5.1: resolution: {integrity: sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -17996,13 +17755,6 @@ packages: resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} dev: false - /domexception@1.0.1: - resolution: {integrity: sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==} - deprecated: Use your platform's native DOMException instead - dependencies: - webidl-conversions: 4.0.2 - dev: false - /domexception@2.0.1: resolution: {integrity: sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==} engines: {node: '>=8'} @@ -18057,7 +17809,7 @@ packages: resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} dependencies: no-case: 3.0.4 - tslib: 2.6.2 + tslib: 2.8.0 dev: false /dot-prop@4.2.1: @@ -18095,14 +17847,14 @@ packages: resolution: {integrity: sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==} dev: false - /dotenv-webpack@8.1.0(webpack@5.91.0): + /dotenv-webpack@8.1.0(webpack@5.96.1): resolution: {integrity: sha512-owK1JcsPkIobeqjVrk6h7jPED/W6ZpdFsMPR+5ursB7/SdgDyO+VzAU+szK8C8u3qUhtENyYnj8eyXMR5kkGag==} engines: {node: '>=10'} peerDependencies: webpack: ^4 || ^5 dependencies: dotenv-defaults: 2.0.2 - webpack: 5.91.0(esbuild@0.23.1) + webpack: 5.96.1(esbuild@0.23.1) dev: false /dotenv@10.0.0: @@ -18156,13 +17908,6 @@ packages: /eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - /ecc-jsbn@0.1.2: - resolution: {integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==} - dependencies: - jsbn: 0.1.1 - safer-buffer: 2.1.2 - dev: false - /ecdsa-sig-formatter@1.0.11: resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} dependencies: @@ -18225,6 +17970,7 @@ packages: /emoji-regex@7.0.3: resolution: {integrity: sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==} + dev: true /emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -18321,7 +18067,6 @@ packages: dependencies: graceful-fs: 4.2.11 tapable: 2.2.1 - dev: true /enquirer@2.3.6: resolution: {integrity: sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==} @@ -19450,10 +19195,6 @@ packages: safe-buffer: 5.2.1 dev: true - /exec-sh@0.3.6: - resolution: {integrity: sha512-nQn+hI3yp+oD0huYhKwvYI32+JFeq+XkNcD1GAo3Y/MjxsfVGmrrzrnzjWiNY6f+pUCP440fThsFh5gZrRAU/w==} - dev: false - /execa@0.7.0: resolution: {integrity: sha512-RztN09XglpYI7aBBrJCPW95jEH7YF1UEPOoX9yDhUTPdp7mK+CQvnLTuD10BNXZ3byLTu2uehZ8EcKT/4CGiFw==} engines: {node: '>=4'} @@ -19467,19 +19208,6 @@ packages: strip-eof: 1.0.0 dev: false - /execa@1.0.0: - resolution: {integrity: sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==} - engines: {node: '>=6'} - dependencies: - cross-spawn: 6.0.5 - get-stream: 4.1.0 - is-stream: 1.1.0 - npm-run-path: 2.0.2 - p-finally: 1.0.0 - signal-exit: 3.0.7 - strip-eof: 1.0.0 - dev: false - /execa@5.0.0: resolution: {integrity: sha512-ov6w/2LCiuyO4RLYGdpFGjkcs0wMTgGE8PrkTHikeUy5iJekXyPIKUjifk5CsE0pt7sMCrMZ3YNqoCj6idQOnQ==} engines: {node: '>=10'} @@ -19554,20 +19282,6 @@ packages: homedir-polyfill: 1.0.3 dev: false - /expect@24.9.0: - resolution: {integrity: sha512-wvVAx8XIol3Z5m9zvZXiyZOQ+sRJqNTIm6sGjdWlaZIeupQGO3WbYI+15D/AmEwZywL6wtJkbAbJtzkOfBuR0Q==} - engines: {node: '>= 6'} - dependencies: - '@jest/types': 24.9.0 - ansi-styles: 3.2.1 - jest-get-type: 24.9.0 - jest-matcher-utils: 24.9.0 - jest-message-util: 24.9.0 - jest-regex-util: 24.9.0 - transitivePeerDependencies: - - supports-color - dev: false - /expect@27.5.1: resolution: {integrity: sha512-E1q5hSUG2AmYQwQJ041nvgpkODHQvB+RKlB4IYdru6uJsyFTRyZAP463M+1lINorwbqAmUggi6+WwkD8lCS/Dw==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -19782,11 +19496,6 @@ packages: - supports-color dev: false - /extsprintf@1.3.0: - resolution: {integrity: sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==} - engines: {'0': node >=0.6.0} - dev: false - /faker@4.1.0: resolution: {integrity: sha512-ILKg69P6y/D8/wSmDXw35Ly0re8QzQ8pMfBCflsGiZG2ZjMUNLYNexA6lz5pkmJlepVdsiDFUxYAzPQ9/+iGLA==} dev: true @@ -20264,11 +19973,7 @@ packages: cross-spawn: 7.0.3 signal-exit: 4.1.0 - /forever-agent@0.6.1: - resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==} - dev: false - - /fork-ts-checker-webpack-plugin@4.1.6(eslint@8.57.0)(typescript@4.9.5)(webpack@5.91.0): + /fork-ts-checker-webpack-plugin@4.1.6(eslint@8.57.0)(typescript@4.9.5)(webpack@5.96.1): resolution: {integrity: sha512-DUxuQaKoqfNne8iikd14SAkh5uw4+8vNifp6gmA73yYNS6ywLIWSLD/n/mBzHQRpW3J7rbATEakmiA8JvkTyZw==} engines: {node: '>=6.11.5', yarn: '>=1.0.0'} peerDependencies: @@ -20290,7 +19995,7 @@ packages: semver: 5.7.2 tapable: 1.1.3 typescript: 4.9.5 - webpack: 5.91.0(esbuild@0.23.1) + webpack: 5.96.1(esbuild@0.23.1) worker-rpc: 0.1.1 transitivePeerDependencies: - supports-color @@ -20374,15 +20079,6 @@ packages: webpack: 5.96.1(esbuild@0.23.1) dev: true - /form-data@2.3.3: - resolution: {integrity: sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==} - engines: {node: '>= 0.12'} - dependencies: - asynckit: 0.4.0 - combined-stream: 1.0.8 - mime-types: 2.1.35 - dev: false - /form-data@2.5.1: resolution: {integrity: sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==} engines: {node: '>= 0.12'} @@ -20704,13 +20400,6 @@ packages: engines: {node: '>=4'} dev: false - /get-stream@4.1.0: - resolution: {integrity: sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==} - engines: {node: '>=6'} - dependencies: - pump: 3.0.0 - dev: false - /get-stream@5.2.0: resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} engines: {node: '>=8'} @@ -20758,12 +20447,6 @@ packages: /getopts@2.3.0: resolution: {integrity: sha512-5eDf9fuSXwxBL6q5HX+dhDj+dslFGWzU5thZ9kNKUkcPtaPdatmUFKwHFrLb/uf/WpA4BHET+AX3Scl56cAjpA==} - /getpass@0.1.7: - resolution: {integrity: sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==} - dependencies: - assert-plus: 1.0.0 - dev: false - /giget@1.2.3: resolution: {integrity: sha512-8EHPljDvs7qKykr6uw8b+lqLiUc/vUg+KVTI0uND4s63TdsZM2Xus3mflvF0DDG9SiM4RlCkFGL+7aAjRmV7KA==} hasBin: true @@ -21123,10 +20806,6 @@ packages: engines: {node: '>=4.x'} dev: true - /growly@1.3.0: - resolution: {integrity: sha512-+xGQY0YyAWCnqy7Cd++hc2JqMYzlm0dG30Jd0beaA64sROr8C4nt8Yc9V5Ro3avlSUDTN0ulqP/VBKi1/lLygw==} - dev: false - /gud@1.0.0: resolution: {integrity: sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw==} dev: false @@ -21256,20 +20935,6 @@ packages: uglify-js: 3.17.4 dev: true - /har-schema@2.0.0: - resolution: {integrity: sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==} - engines: {node: '>=4'} - dev: false - - /har-validator@5.1.5: - resolution: {integrity: sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==} - engines: {node: '>=6'} - deprecated: this library is no longer supported - dependencies: - ajv: 6.12.6 - har-schema: 2.0.0 - dev: false - /hard-rejection@2.1.0: resolution: {integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==} engines: {node: '>=6'} @@ -21410,7 +21075,7 @@ packages: resolution: {integrity: sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==} dependencies: capital-case: 1.0.4 - tslib: 2.6.2 + tslib: 2.8.0 dev: false /helmet-crossdomain@0.4.0: @@ -21561,12 +21226,6 @@ packages: resolution: {integrity: sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ==} dev: false - /html-encoding-sniffer@1.0.2: - resolution: {integrity: sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==} - dependencies: - whatwg-encoding: 1.0.5 - dev: false - /html-encoding-sniffer@2.0.1: resolution: {integrity: sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==} engines: {node: '>=10'} @@ -21792,15 +21451,6 @@ packages: '@types/node': 10.17.60 dev: false - /http-signature@1.2.0: - resolution: {integrity: sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==} - engines: {node: '>=0.8', npm: '>=1.3.7'} - dependencies: - assert-plus: 1.0.0 - jsprim: 1.4.2 - sshpk: 1.18.0 - dev: false - /https-browserify@1.0.0: resolution: {integrity: sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==} dev: true @@ -21989,8 +21639,8 @@ packages: /import-in-the-middle@1.7.4: resolution: {integrity: sha512-Lk+qzWmiQuRPPulGQeK5qq0v32k2bHnWrRPFgqyvhw7Kkov5L6MOLOIU3pcWeujc9W4q54Cp3Q2WV16eQkc7Bg==} dependencies: - acorn: 8.13.0 - acorn-import-attributes: 1.9.5(acorn@8.13.0) + acorn: 8.14.0 + acorn-import-attributes: 1.9.5(acorn@8.14.0) cjs-module-lexer: 1.3.1 module-details-from-path: 1.0.3 dev: false @@ -22005,15 +21655,6 @@ packages: engines: {node: '>=8'} dev: false - /import-local@2.0.0: - resolution: {integrity: sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==} - engines: {node: '>=6'} - hasBin: true - dependencies: - pkg-dir: 3.0.0 - resolve-cwd: 2.0.0 - dev: false - /import-local@3.1.0: resolution: {integrity: sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==} engines: {node: '>=8'} @@ -22286,13 +21927,6 @@ packages: ci-info: 1.6.0 dev: false - /is-ci@2.0.0: - resolution: {integrity: sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==} - hasBin: true - dependencies: - ci-info: 2.0.0 - dev: false - /is-ci@3.0.1: resolution: {integrity: sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==} hasBin: true @@ -22722,11 +22356,6 @@ packages: engines: {node: '>=0.10.0'} dev: false - /is-wsl@1.1.0: - resolution: {integrity: sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==} - engines: {node: '>=4'} - dev: false - /is-wsl@2.2.0: resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} engines: {node: '>=8'} @@ -22760,13 +22389,10 @@ packages: resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} engines: {node: '>=0.10.0'} - /isstream@0.1.2: - resolution: {integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==} - dev: false - /istanbul-lib-coverage@2.0.5: resolution: {integrity: sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==} engines: {node: '>=6'} + dev: true /istanbul-lib-coverage@3.2.2: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} @@ -22792,6 +22418,7 @@ packages: semver: 6.3.0 transitivePeerDependencies: - supports-color + dev: true /istanbul-lib-instrument@5.2.1: resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} @@ -22825,6 +22452,7 @@ packages: istanbul-lib-coverage: 2.0.5 make-dir: 2.1.0 supports-color: 6.1.0 + dev: true /istanbul-lib-report@3.0.1: resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} @@ -22845,6 +22473,7 @@ packages: source-map: 0.6.1 transitivePeerDependencies: - supports-color + dev: true /istanbul-lib-source-maps@4.0.1: resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} @@ -22861,6 +22490,7 @@ packages: engines: {node: '>=6'} dependencies: html-escaper: 2.0.2 + dev: true /istanbul-reports@3.1.7: resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==} @@ -22905,15 +22535,6 @@ packages: resolution: {integrity: sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==} dev: false - /jest-changed-files@24.9.0: - resolution: {integrity: sha512-6aTWpe2mHF0DhL28WjdkO8LyGjs3zItPET4bMSeXU6T3ub4FPMw+mcOcbdGXQOAfmLcxofD23/5Bl9Z4AkFwqg==} - engines: {node: '>= 6'} - dependencies: - '@jest/types': 24.9.0 - execa: 1.0.0 - throat: 4.1.0 - dev: false - /jest-changed-files@27.5.1: resolution: {integrity: sha512-buBLMiByfWGCoMsLLzGUUSpAmIAGnbR2KJoMN10ziLhOLvP4e0SlypHnAel8iqQXTrcbmfEY9sSqae5sgUsTvw==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -22988,30 +22609,6 @@ packages: - supports-color dev: true - /jest-cli@24.9.0: - resolution: {integrity: sha512-+VLRKyitT3BWoMeSUIHRxV/2g8y9gw91Jh5z2UmXZzkZKpbC08CSehVxgHUwTpy+HwGcns/tqafQDJW7imYvGg==} - engines: {node: '>= 6'} - hasBin: true - dependencies: - '@jest/core': 24.9.0 - '@jest/test-result': 24.9.0 - '@jest/types': 24.9.0 - chalk: 2.4.2 - exit: 0.1.2 - import-local: 2.0.0 - is-ci: 2.0.0 - jest-config: 24.9.0 - jest-util: 24.9.0 - jest-validate: 24.9.0 - prompts: 2.4.2 - realpath-native: 1.1.0 - yargs: 13.3.2 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - dev: false - /jest-cli@27.5.1(ts-node@10.9.2): resolution: {integrity: sha512-Hc6HOOwYq4/74/c62dEE3r5elx8wjYqxY0r0G/nFrLDPMFRu6RA/u8qINOIkvhxG7mMQ5EJsOGfRpI8L6eFUVw==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -23070,33 +22667,6 @@ packages: - ts-node dev: true - /jest-config@24.9.0: - resolution: {integrity: sha512-RATtQJtVYQrp7fvWg6f5y3pEFj9I+H8sWw4aKxnDZ96mob5i5SD6ZEGWgMLXQ4LE8UurrjbdlLWdUeo+28QpfQ==} - engines: {node: '>= 6'} - dependencies: - '@babel/core': 7.24.5 - '@jest/test-sequencer': 24.9.0 - '@jest/types': 24.9.0 - babel-jest: 24.9.0(@babel/core@7.24.5) - chalk: 2.4.2 - glob: 7.2.3 - jest-environment-jsdom: 24.9.0 - jest-environment-node: 24.9.0 - jest-get-type: 24.9.0 - jest-jasmine2: 24.9.0 - jest-regex-util: 24.9.0 - jest-resolve: 24.9.0 - jest-util: 24.9.0 - jest-validate: 24.9.0 - micromatch: 3.1.10(supports-color@5.5.0) - pretty-format: 24.9.0 - realpath-native: 1.1.0 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - dev: false - /jest-config@27.5.1(ts-node@10.9.2): resolution: {integrity: sha512-5sAsjm6tGdsVbW9ahcChPAFCk4IlkQUknH5AvKjuLTSlcO/wCZKyFdn7Rg0EkC+OGgWODEy2hDpWB1PgzH0JNA==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -23179,26 +22749,6 @@ packages: - supports-color dev: true - /jest-diff@24.9.0: - resolution: {integrity: sha512-qMfrTs8AdJE2iqrTp0hzh7kTd2PQWrsFyj9tORoKmu32xjPjeE4NyjVRDz8ybYwqS2ik8N4hsIpiVTyFeo2lBQ==} - engines: {node: '>= 6'} - dependencies: - chalk: 2.4.2 - diff-sequences: 24.9.0 - jest-get-type: 24.9.0 - pretty-format: 24.9.0 - dev: false - - /jest-diff@26.6.2: - resolution: {integrity: sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==} - engines: {node: '>= 10.14.2'} - dependencies: - chalk: 4.1.2 - diff-sequences: 26.6.2 - jest-get-type: 26.3.0 - pretty-format: 26.6.2 - dev: false - /jest-diff@27.5.1: resolution: {integrity: sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -23219,13 +22769,6 @@ packages: pretty-format: 29.7.0 dev: true - /jest-docblock@24.9.0: - resolution: {integrity: sha512-F1DjdpDMJMA1cN6He0FNYNZlo3yYmOtRUnktrT9Q37njYzC5WEaDdmbynIgy0L/IvXvvgsG8OsqhLPXTpfmZAA==} - engines: {node: '>= 6'} - dependencies: - detect-newline: 2.1.0 - dev: false - /jest-docblock@27.5.1: resolution: {integrity: sha512-rl7hlABeTsRYxKiUfpHrQrG4e2obOiTQWfMEH3PxPjOtdsfLQO4ReWSZaQ7DETm4xu07rl4q/h4zcKXyU0/OzQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -23240,19 +22783,6 @@ packages: detect-newline: 3.1.0 dev: true - /jest-each@24.9.0: - resolution: {integrity: sha512-ONi0R4BvW45cw8s2Lrx8YgbeXL1oCQ/wIDwmsM3CqM/nlblNCPmnC3IPQlMbRFZu3wKdQ2U8BqM6lh3LJ5Bsog==} - engines: {node: '>= 6'} - dependencies: - '@jest/types': 24.9.0 - chalk: 2.4.2 - jest-get-type: 24.9.0 - jest-util: 24.9.0 - pretty-format: 24.9.0 - transitivePeerDependencies: - - supports-color - dev: false - /jest-each@27.5.1: resolution: {integrity: sha512-1Ff6p+FbhT/bXQnEouYy00bkNSY7OUpfIcmdl8vZ31A1UUaurOLPA8a8BbJOF2RDUElwJhmeaV7LnagI+5UwNQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -23275,37 +22805,6 @@ packages: pretty-format: 29.7.0 dev: true - /jest-environment-jsdom-fourteen@1.0.1: - resolution: {integrity: sha512-DojMX1sY+at5Ep+O9yME34CdidZnO3/zfPh8UW+918C5fIZET5vCjfkegixmsi7AtdYfkr4bPlIzmWnlvQkP7Q==} - dependencies: - '@jest/environment': 24.9.0 - '@jest/fake-timers': 24.9.0 - '@jest/types': 24.9.0 - jest-mock: 24.9.0 - jest-util: 24.9.0 - jsdom: 14.1.0 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - dev: false - - /jest-environment-jsdom@24.9.0: - resolution: {integrity: sha512-Zv9FV9NBRzLuALXjvRijO2351DRQeLYXtpD4xNvfoVFw21IOKNhZAEUKcbiEtjTkm2GsJ3boMVgkaR7rN8qetA==} - engines: {node: '>= 6'} - dependencies: - '@jest/environment': 24.9.0 - '@jest/fake-timers': 24.9.0 - '@jest/types': 24.9.0 - jest-mock: 24.9.0 - jest-util: 24.9.0 - jsdom: 11.12.0 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - dev: false - /jest-environment-jsdom@27.5.1: resolution: {integrity: sha512-TFBvkTC1Hnnnrka/fUb56atfDtJ9VMZ94JkjTbggl1PEpwrYtUBKMezB3inLmWqQsXYLcMwNoDQwoBTAvFfsfw==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -23324,19 +22823,6 @@ packages: - utf-8-validate dev: false - /jest-environment-node@24.9.0: - resolution: {integrity: sha512-6d4V2f4nxzIzwendo27Tr0aFm+IXWa0XEUnaH6nU0FMaozxovt+sfRvh4J47wL1OvF83I3SSTu0XK+i4Bqe7uA==} - engines: {node: '>= 6'} - dependencies: - '@jest/environment': 24.9.0 - '@jest/fake-timers': 24.9.0 - '@jest/types': 24.9.0 - jest-mock: 24.9.0 - jest-util: 24.9.0 - transitivePeerDependencies: - - supports-color - dev: false - /jest-environment-node@27.5.1: resolution: {integrity: sha512-Jt4ZUnxdOsTGwSRAfKEnE6BcwsSPNOijjwifq5sDFSA2kesnXTvNqKHYgM0hDq3549Uf/KzdXNYn4wMZJPlFLw==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -23361,16 +22847,6 @@ packages: jest-util: 29.7.0 dev: true - /jest-get-type@24.9.0: - resolution: {integrity: sha512-lUseMzAley4LhIcpSP9Jf+fTrQ4a1yHQwLNeeVa2cEmbCGeoZAtYPOIv8JaxLD/sUpKxetKGP+gsHl8f8TSj8Q==} - engines: {node: '>= 6'} - dev: false - - /jest-get-type@26.3.0: - resolution: {integrity: sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==} - engines: {node: '>= 10.14.2'} - dev: false - /jest-get-type@27.5.1: resolution: {integrity: sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -23381,27 +22857,6 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dev: true - /jest-haste-map@24.9.0: - resolution: {integrity: sha512-kfVFmsuWui2Sj1Rp1AJ4D9HqJwE4uwTlS/vO+eRUaMmd54BFpli2XhMQnPC2k4cHFVbB2Q2C+jtI1AGLgEnCjQ==} - engines: {node: '>= 6'} - dependencies: - '@jest/types': 24.9.0 - anymatch: 2.0.0(supports-color@5.5.0) - fb-watchman: 2.0.2 - graceful-fs: 4.2.11 - invariant: 2.2.4 - jest-serializer: 24.9.0 - jest-util: 24.9.0 - jest-worker: 24.9.0 - micromatch: 3.1.10(supports-color@5.5.0) - sane: 4.1.0 - walker: 1.0.8 - optionalDependencies: - fsevents: 1.2.13 - transitivePeerDependencies: - - supports-color - dev: false - /jest-haste-map@27.5.1: resolution: {integrity: sha512-7GgkZ4Fw4NFbMSDSpZwXeBiIbx+t/46nJ2QitkOjvwPYyZmqttu2TDSimMHP1EkPOi4xUZAN1doE5Vd25H4Jng==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -23441,30 +22896,6 @@ packages: fsevents: 2.3.3 dev: true - /jest-jasmine2@24.9.0: - resolution: {integrity: sha512-Cq7vkAgaYKp+PsX+2/JbTarrk0DmNhsEtqBXNwUHkdlbrTBLtMJINADf2mf5FkowNsq8evbPc07/qFO0AdKTzw==} - engines: {node: '>= 6'} - dependencies: - '@babel/traverse': 7.25.9(supports-color@5.5.0) - '@jest/environment': 24.9.0 - '@jest/test-result': 24.9.0 - '@jest/types': 24.9.0 - chalk: 2.4.2 - co: 4.6.0 - expect: 24.9.0 - is-generator-fn: 2.1.0 - jest-each: 24.9.0 - jest-matcher-utils: 24.9.0 - jest-message-util: 24.9.0 - jest-runtime: 24.9.0 - jest-snapshot: 24.9.0 - jest-util: 24.9.0 - pretty-format: 24.9.0 - throat: 4.1.0 - transitivePeerDependencies: - - supports-color - dev: false - /jest-jasmine2@27.5.1: resolution: {integrity: sha512-jtq7VVyG8SqAorDpApwiJJImd0V2wv1xzdheGHRGyuT7gZm6gG47QEskOlzsN1PG/6WNaCo5pmwMHDf3AkG2pQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -23490,14 +22921,6 @@ packages: - supports-color dev: false - /jest-leak-detector@24.9.0: - resolution: {integrity: sha512-tYkFIDsiKTGwb2FG1w8hX9V0aUb2ot8zY/2nFg087dUageonw1zrLMP4W6zsRO59dPkTSKie+D4rhMuP9nRmrA==} - engines: {node: '>= 6'} - dependencies: - jest-get-type: 24.9.0 - pretty-format: 24.9.0 - dev: false - /jest-leak-detector@27.5.1: resolution: {integrity: sha512-POXfWAMvfU6WMUXftV4HolnJfnPOGEu10fscNCA76KBpRRhcMN2c8d3iT2pxQS3HLbA+5X4sOUPzYO2NUyIlHQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -23514,16 +22937,6 @@ packages: pretty-format: 29.7.0 dev: true - /jest-matcher-utils@24.9.0: - resolution: {integrity: sha512-OZz2IXsu6eaiMAwe67c1T+5tUAtQyQx27/EMEkbFAGiw52tB9em+uGbzpcgYVpA8wl0hlxKPZxrly4CXU/GjHA==} - engines: {node: '>= 6'} - dependencies: - chalk: 2.4.2 - jest-diff: 24.9.0 - jest-get-type: 24.9.0 - pretty-format: 24.9.0 - dev: false - /jest-matcher-utils@27.5.1: resolution: {integrity: sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -23544,22 +22957,6 @@ packages: pretty-format: 29.7.0 dev: true - /jest-message-util@24.9.0: - resolution: {integrity: sha512-oCj8FiZ3U0hTP4aSui87P4L4jC37BtQwUMqk+zk/b11FR19BJDeZsZAvIHutWnmtw7r85UmR3CEWZ0HWU2mAlw==} - engines: {node: '>= 6'} - dependencies: - '@babel/code-frame': 7.26.0 - '@jest/test-result': 24.9.0 - '@jest/types': 24.9.0 - '@types/stack-utils': 1.0.1 - chalk: 2.4.2 - micromatch: 3.1.10(supports-color@5.5.0) - slash: 2.0.0 - stack-utils: 1.0.5 - transitivePeerDependencies: - - supports-color - dev: false - /jest-message-util@27.5.1: resolution: {integrity: sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -23605,13 +23002,6 @@ packages: stack-utils: 2.0.6 dev: true - /jest-mock@24.9.0: - resolution: {integrity: sha512-3BEYN5WbSq9wd+SyLDES7AHnjH9A/ROBwmz7l2y+ol+NtSFO8DYiEBzoO1CeFc9a8DYy10EO4dDFVv/wN3zl1w==} - engines: {node: '>= 6'} - dependencies: - '@jest/types': 24.9.0 - dev: false - /jest-mock@27.5.1: resolution: {integrity: sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -23628,18 +23018,6 @@ packages: jest-util: 29.7.0 dev: true - /jest-pnp-resolver@1.2.3(jest-resolve@24.9.0): - resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} - engines: {node: '>=6'} - peerDependencies: - jest-resolve: '*' - peerDependenciesMeta: - jest-resolve: - optional: true - dependencies: - jest-resolve: 24.9.0 - dev: false - /jest-pnp-resolver@1.2.3(jest-resolve@27.5.1): resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} engines: {node: '>=6'} @@ -23664,11 +23042,6 @@ packages: jest-resolve: 29.7.0 dev: true - /jest-regex-util@24.9.0: - resolution: {integrity: sha512-05Cmb6CuxaA+Ys6fjr3PhvV3bGQmO+2p2La4hFbU+W5uOc479f7FdLXUWXw4pYMAhhSZIuKHwSXSu6CsSBAXQA==} - engines: {node: '>= 6'} - dev: false - /jest-regex-util@27.5.1: resolution: {integrity: sha512-4bfKq2zie+x16okqDXjXn9ql2B0dScQu+vcwe4TvFVhkVyuWLqpZrZtXxLLWoXYgn0E87I6r6GRYHF7wFZBUvg==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -23684,17 +23057,6 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dev: true - /jest-resolve-dependencies@24.9.0: - resolution: {integrity: sha512-Fm7b6AlWnYhT0BXy4hXpactHIqER7erNgIsIozDXWl5dVm+k8XdGVe1oTg1JyaFnOxarMEbax3wyRJqGP2Pq+g==} - engines: {node: '>= 6'} - dependencies: - '@jest/types': 24.9.0 - jest-regex-util: 24.9.0 - jest-snapshot: 24.9.0 - transitivePeerDependencies: - - supports-color - dev: false - /jest-resolve-dependencies@27.5.1: resolution: {integrity: sha512-QQOOdY4PE39iawDn5rzbIePNigfe5B9Z91GDD1ae/xNDlu9kaat8QQ5EKnNmVWPV54hUdxCVwwj6YMgR2O7IOg==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -23716,17 +23078,6 @@ packages: - supports-color dev: true - /jest-resolve@24.9.0: - resolution: {integrity: sha512-TaLeLVL1l08YFZAt3zaPtjiVvyy4oSA6CRe+0AFPPVX3Q/VI0giIWWoAvoS5L96vj9Dqxj4fB5p2qrHCmTU/MQ==} - engines: {node: '>= 6'} - dependencies: - '@jest/types': 24.9.0 - browser-resolve: 1.11.3 - chalk: 2.4.2 - jest-pnp-resolver: 1.2.3(jest-resolve@24.9.0) - realpath-native: 1.1.0 - dev: false - /jest-resolve@27.5.1: resolution: {integrity: sha512-FFDy8/9E6CV83IMbDpcjOhumAQPDyETnU2KZ1O98DwTnz8AOBsW/Xv3GySr1mOZdItLR+zDZ7I/UdTFbgSOVCw==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -23758,35 +23109,6 @@ packages: slash: 3.0.0 dev: true - /jest-runner@24.9.0: - resolution: {integrity: sha512-KksJQyI3/0mhcfspnxxEOBueGrd5E4vV7ADQLT9ESaCzz02WnbdbKWIf5Mkaucoaj7obQckYPVX6JJhgUcoWWg==} - engines: {node: '>= 6'} - dependencies: - '@jest/console': 24.9.0 - '@jest/environment': 24.9.0 - '@jest/test-result': 24.9.0 - '@jest/types': 24.9.0 - chalk: 2.4.2 - exit: 0.1.2 - graceful-fs: 4.2.11 - jest-config: 24.9.0 - jest-docblock: 24.9.0 - jest-haste-map: 24.9.0 - jest-jasmine2: 24.9.0 - jest-leak-detector: 24.9.0 - jest-message-util: 24.9.0 - jest-resolve: 24.9.0 - jest-runtime: 24.9.0 - jest-util: 24.9.0 - jest-worker: 24.9.0 - source-map-support: 0.5.21 - throat: 4.1.0 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - dev: false - /jest-runner@27.5.1: resolution: {integrity: sha512-g4NPsM4mFCOwFKXO4p/H/kWGdJp9V8kURY2lX8Me2drgXqG7rrZAx5kv+5H7wtt/cdFIjhqYx1HrlqWHaOvDaQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -23848,40 +23170,6 @@ packages: - supports-color dev: true - /jest-runtime@24.9.0: - resolution: {integrity: sha512-8oNqgnmF3v2J6PVRM2Jfuj8oX3syKmaynlDMMKQ4iyzbQzIG6th5ub/lM2bCMTmoTKM3ykcUYI2Pw9xwNtjMnw==} - engines: {node: '>= 6'} - hasBin: true - dependencies: - '@jest/console': 24.9.0 - '@jest/environment': 24.9.0 - '@jest/source-map': 24.9.0 - '@jest/transform': 24.9.0 - '@jest/types': 24.9.0 - '@types/yargs': 13.0.12 - chalk: 2.4.2 - exit: 0.1.2 - glob: 7.2.3 - graceful-fs: 4.2.11 - jest-config: 24.9.0 - jest-haste-map: 24.9.0 - jest-message-util: 24.9.0 - jest-mock: 24.9.0 - jest-regex-util: 24.9.0 - jest-resolve: 24.9.0 - jest-snapshot: 24.9.0 - jest-util: 24.9.0 - jest-validate: 24.9.0 - realpath-native: 1.1.0 - slash: 2.0.0 - strip-bom: 3.0.0 - yargs: 13.3.2 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - dev: false - /jest-runtime@27.5.1: resolution: {integrity: sha512-o7gxw3Gf+H2IGt8fv0RiyE1+r83FJBRruoA+FXrlHw6xEyBsU8ugA6IPfTdVyA0w8HClpbK+DGJxH59UrNMx8A==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -23942,11 +23230,6 @@ packages: - supports-color dev: true - /jest-serializer@24.9.0: - resolution: {integrity: sha512-DxYipDr8OvfrKH3Kel6NdED3OXxjvxXZ1uIY2I9OFbGg+vUkkg7AGvi65qbhbWNPvDckXmzMPbK3u3HaDO49bQ==} - engines: {node: '>= 6'} - dev: false - /jest-serializer@27.5.1: resolution: {integrity: sha512-jZCyo6iIxO1aqUxpuBlwTDMkzOAJS4a3eYz3YzgxxVQFwLeSA7Jfq5cbqCY+JLvTDrWirgusI/0KwxKMgrdf7w==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -23955,27 +23238,6 @@ packages: graceful-fs: 4.2.11 dev: false - /jest-snapshot@24.9.0: - resolution: {integrity: sha512-uI/rszGSs73xCM0l+up7O7a40o90cnrk429LOiK3aeTvfC0HHmldbd81/B7Ix81KSFe1lwkbl7GnBGG4UfuDew==} - engines: {node: '>= 6'} - dependencies: - '@babel/types': 7.26.0 - '@jest/types': 24.9.0 - chalk: 2.4.2 - expect: 24.9.0 - jest-diff: 24.9.0 - jest-get-type: 24.9.0 - jest-matcher-utils: 24.9.0 - jest-message-util: 24.9.0 - jest-resolve: 24.9.0 - mkdirp: 0.5.6 - natural-compare: 1.4.0 - pretty-format: 24.9.0 - semver: 6.3.0 - transitivePeerDependencies: - - supports-color - dev: false - /jest-snapshot@27.5.1: resolution: {integrity: sha512-yYykXI5a0I31xX67mgeLw1DZ0bJB+gpq5IpSuCAoyDi0+BhgU/RIrL+RTzDmkNTchvDFWKP8lp+w/42Z3us5sA==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -24034,26 +23296,6 @@ packages: - supports-color dev: true - /jest-util@24.9.0: - resolution: {integrity: sha512-x+cZU8VRmOJxbA1K5oDBdxQmdq0OIdADarLxk0Mq+3XS4jgvhG/oKGWcIDCtPG0HgjxOYvF+ilPJQsAyXfbNOg==} - engines: {node: '>= 6'} - dependencies: - '@jest/console': 24.9.0 - '@jest/fake-timers': 24.9.0 - '@jest/source-map': 24.9.0 - '@jest/test-result': 24.9.0 - '@jest/types': 24.9.0 - callsites: 3.1.0 - chalk: 2.4.2 - graceful-fs: 4.2.11 - is-ci: 2.0.0 - mkdirp: 0.5.6 - slash: 2.0.0 - source-map: 0.6.1 - transitivePeerDependencies: - - supports-color - dev: false - /jest-util@27.5.1: resolution: {integrity: sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -24090,18 +23332,6 @@ packages: picomatch: 2.3.1 dev: true - /jest-validate@24.9.0: - resolution: {integrity: sha512-HPIt6C5ACwiqSiwi+OfSSHbK8sG7akG8eATl+IPKaeIjtPOeBUd/g3J7DghugzxrGjI93qS/+RPKe1H6PqvhRQ==} - engines: {node: '>= 6'} - dependencies: - '@jest/types': 24.9.0 - camelcase: 5.3.1 - chalk: 2.4.2 - jest-get-type: 24.9.0 - leven: 3.1.0 - pretty-format: 24.9.0 - dev: false - /jest-validate@27.5.1: resolution: {integrity: sha512-thkNli0LYTmOI1tDB3FI1S1RTp/Bqyd9pTarJwL87OIBFuqEb5Apv5EaApEudYg4g86e3CT6kM0RowkhtEnCBQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -24126,20 +23356,6 @@ packages: pretty-format: 29.7.0 dev: true - /jest-watch-typeahead@0.4.2: - resolution: {integrity: sha512-f7VpLebTdaXs81rg/oj4Vg/ObZy2QtGzAmGLNsqUS5G5KtSN68tFcIsbvNODfNyQxU78g7D8x77o3bgfBTR+2Q==} - dependencies: - ansi-escapes: 4.3.2 - chalk: 2.4.2 - jest-regex-util: 24.9.0 - jest-watcher: 24.9.0 - slash: 3.0.0 - string-length: 3.1.0 - strip-ansi: 5.2.0 - transitivePeerDependencies: - - supports-color - dev: false - /jest-watch-typeahead@1.1.0(jest@27.5.1): resolution: {integrity: sha512-Va5nLSJTN7YFtC2jd+7wsoe1pNe5K4ShLux/E5iHEwlB9AxaxmggY7to9KUqKojhaJw3aXqt5WAb4jGPOolpEw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -24156,21 +23372,6 @@ packages: strip-ansi: 7.1.0 dev: false - /jest-watcher@24.9.0: - resolution: {integrity: sha512-+/fLOfKPXXYJDYlks62/4R4GoT+GU1tYZed99JSCOsmzkkF7727RqKrjNAxtfO4YpGv11wybgRvCjR73lK2GZw==} - engines: {node: '>= 6'} - dependencies: - '@jest/test-result': 24.9.0 - '@jest/types': 24.9.0 - '@types/yargs': 13.0.12 - ansi-escapes: 3.2.0 - chalk: 2.4.2 - jest-util: 24.9.0 - string-length: 2.0.0 - transitivePeerDependencies: - - supports-color - dev: false - /jest-watcher@27.5.1: resolution: {integrity: sha512-z676SuD6Z8o8qbmEGhoEUFOM1+jfEiL3DXHK/xgEiG2EyNYfFG60jluWcupY6dATjfEsKQuibReS1djInQnoVw==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -24212,14 +23413,6 @@ packages: string-length: 4.0.2 dev: true - /jest-worker@24.9.0: - resolution: {integrity: sha512-51PE4haMSXcHohnSMdM42anbvZANYTqMrr52tVKPqqsPJMzoP6FYYDVqahX/HrAoKEKz3uUPzSvKs9A3qR4iVw==} - engines: {node: '>= 6'} - dependencies: - merge-stream: 2.0.0 - supports-color: 6.1.0 - dev: false - /jest-worker@26.6.2: resolution: {integrity: sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==} engines: {node: '>= 10.13.0'} @@ -24256,19 +23449,6 @@ packages: supports-color: 8.1.1 dev: true - /jest@24.9.0: - resolution: {integrity: sha512-YvkBL1Zm7d2B1+h5fHEOdyjCG+sGMz4f8D86/0HiqJ6MB4MnDc8FgP5vdWsGnemOQro7lnYo8UakZ3+5A0jxGw==} - engines: {node: '>= 6'} - hasBin: true - dependencies: - import-local: 2.0.0 - jest-cli: 24.9.0 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - dev: false - /jest@27.5.1(ts-node@10.9.2): resolution: {integrity: sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -24375,10 +23555,6 @@ packages: dependencies: argparse: 2.0.1 - /jsbn@0.1.1: - resolution: {integrity: sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==} - dev: false - /jsbn@1.1.0: resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==} @@ -24412,75 +23588,6 @@ packages: - supports-color dev: true - /jsdom@11.12.0: - resolution: {integrity: sha512-y8Px43oyiBM13Zc1z780FrfNLJCXTL40EWlty/LXUtcjykRBNgLlCjWXpfSPBl2iv+N7koQN+dvqszHZgT/Fjw==} - dependencies: - abab: 2.0.6 - acorn: 5.7.4 - acorn-globals: 4.3.4 - array-equal: 1.0.2 - cssom: 0.3.8 - cssstyle: 1.4.0 - data-urls: 1.1.0 - domexception: 1.0.1 - escodegen: 1.14.3 - html-encoding-sniffer: 1.0.2 - left-pad: 1.3.0 - nwsapi: 2.2.10 - parse5: 4.0.0 - pn: 1.1.0 - request: 2.88.2 - request-promise-native: 1.0.9(request@2.88.2) - sax: 1.3.0 - symbol-tree: 3.2.4 - tough-cookie: 2.5.0 - w3c-hr-time: 1.0.2 - webidl-conversions: 4.0.2 - whatwg-encoding: 1.0.5 - whatwg-mimetype: 2.3.0 - whatwg-url: 6.5.0 - ws: 5.2.3 - xml-name-validator: 3.0.0 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - dev: false - - /jsdom@14.1.0: - resolution: {integrity: sha512-O901mfJSuTdwU2w3Sn+74T+RnDVP+FuV5fH8tcPWyqrseRAb0s5xOtPgCFiPOtLcyK7CLIJwPyD83ZqQWvA5ng==} - engines: {node: '>=8'} - dependencies: - abab: 2.0.6 - acorn: 6.4.2 - acorn-globals: 4.3.4 - array-equal: 1.0.2 - cssom: 0.3.8 - cssstyle: 1.4.0 - data-urls: 1.1.0 - domexception: 1.0.1 - escodegen: 1.14.3 - html-encoding-sniffer: 1.0.2 - nwsapi: 2.2.10 - parse5: 5.1.0 - pn: 1.1.0 - request: 2.88.2 - request-promise-native: 1.0.9(request@2.88.2) - saxes: 3.1.11 - symbol-tree: 3.2.4 - tough-cookie: 2.5.0 - w3c-hr-time: 1.0.2 - w3c-xmlserializer: 1.1.2 - webidl-conversions: 4.0.2 - whatwg-encoding: 1.0.5 - whatwg-mimetype: 2.3.0 - whatwg-url: 7.1.0 - ws: 6.2.2 - xml-name-validator: 3.0.0 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - dev: false - /jsdom@16.7.0: resolution: {integrity: sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw==} engines: {node: '>=10'} @@ -24491,7 +23598,7 @@ packages: optional: true dependencies: abab: 2.0.6 - acorn: 8.13.0 + acorn: 8.14.0 acorn-globals: 6.0.0 cssom: 0.4.4 cssstyle: 2.3.0 @@ -24548,6 +23655,7 @@ packages: /json-parse-better-errors@1.0.2: resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==} + dev: true /json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} @@ -24663,16 +23771,6 @@ packages: semver: 7.6.2 dev: false - /jsprim@1.4.2: - resolution: {integrity: sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==} - engines: {node: '>=0.6.0'} - dependencies: - assert-plus: 1.0.0 - extsprintf: 1.3.0 - json-schema: 0.4.0 - verror: 1.10.0 - dev: false - /jstransformer@1.0.0: resolution: {integrity: sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A==} dependencies: @@ -24853,7 +23951,7 @@ packages: colorette: 2.0.19 commander: 10.0.1 debug: 4.3.4 - escalade: 3.1.2 + escalade: 3.2.0 esm: 3.2.25 get-package-type: 0.1.0 getopts: 2.3.0 @@ -24997,11 +24095,6 @@ packages: flush-write-stream: 1.1.1 dev: false - /left-pad@1.3.0: - resolution: {integrity: sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA==} - deprecated: use String.prototype.padStart() - dev: false - /lerna@8.1.3: resolution: {integrity: sha512-Dg/r1dGnRCXKsOUC3lol7o6ggYTA6WWiPQzZJNKqyygn4fzYGuA3Dro2d5677pajaqFnFA72mdCjzSyF16Vi2Q==} engines: {node: '>=18.0.0'} @@ -25101,6 +24194,17 @@ packages: webpack: 5.91.0(esbuild@0.18.20)(webpack-cli@5.1.4) dev: true + /less-loader@11.1.4(less@4.2.0)(webpack@5.96.1): + resolution: {integrity: sha512-6/GrYaB6QcW6Vj+/9ZPgKKs6G10YZai/l/eJ4SLwbzqNTBsAqt5hSLVF47TgsiBxV1P6eAU0GYRH3YRuQU9V3A==} + engines: {node: '>= 14.15.0'} + peerDependencies: + less: ^3.5.0 || ^4.0.0 + webpack: ^5.0.0 + dependencies: + less: 4.2.0 + webpack: 5.96.1(esbuild@0.18.20) + dev: true + /less@4.2.0: resolution: {integrity: sha512-P3b3HJDBtSzsXUl0im2L7gTO5Ubg8mEN6G8qoTS77iXxXX4Hvu4Qj540PZDvQ8V6DmX6iXo98k7Md0Cm1PrLaA==} engines: {node: '>=6'} @@ -25108,7 +24212,7 @@ packages: dependencies: copy-anything: 2.0.6 parse-node-version: 1.0.1 - tslib: 2.6.2 + tslib: 2.8.0 optionalDependencies: errno: 0.1.8 graceful-fs: 4.2.11 @@ -25224,6 +24328,7 @@ packages: parse-json: 4.0.0 pify: 3.0.0 strip-bom: 3.0.0 + dev: true /load-json-file@6.2.0: resolution: {integrity: sha512-gUD/epcRms75Cw8RT1pUdHugZYM5ce64ucs2GEISABwkRsOQr0q2wm/MV2TKThycIe5e0ytRweW2RZxclogCdQ==} @@ -25514,7 +24619,7 @@ packages: /lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} dependencies: - tslib: 2.6.2 + tslib: 2.8.0 dev: false /lowercase-keys@1.0.1: @@ -26200,7 +25305,7 @@ packages: /mlly@1.7.2: resolution: {integrity: sha512-tN3dvVHYVz4DhSXinXIk7u9syPYaJvio118uomkovAtWBT+RdbP6Lfh/5Lvo519YMmwBafwlh20IPTXIStscpA==} dependencies: - acorn: 8.13.0 + acorn: 8.14.0 pathe: 1.1.2 pkg-types: 1.2.1 ufo: 1.5.4 @@ -26733,6 +25838,7 @@ packages: /nice-try@1.0.5: resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} + dev: true /nise@1.5.3: resolution: {integrity: sha512-Ymbac/94xeIrMf59REBPOv0thr+CJVFMhrlAkW/gjCIE58BGQdCj0x7KRCb3yz+Ga2Rz3E9XXSvUyyxqqhjQAQ==} @@ -26748,7 +25854,7 @@ packages: resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} dependencies: lower-case: 2.0.2 - tslib: 2.6.2 + tslib: 2.8.0 dev: false /nocache@2.1.0: @@ -26883,16 +25989,6 @@ packages: resolution: {integrity: sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==} dev: true - /node-notifier@5.4.5: - resolution: {integrity: sha512-tVbHs7DyTLtzOiN78izLA85zRqB9NvEXkAf014Vx3jtSvn/xBl6bR8ZYifj+dFcFrKI21huSQgJZ6ZtL3B4HfQ==} - dependencies: - growly: 1.3.0 - is-wsl: 1.1.0 - semver: 5.7.2 - shellwords: 0.1.1 - which: 1.3.1 - dev: false - /node-releases@1.1.77: resolution: {integrity: sha512-rB1DUFUNAN4Gn9keO2K1efO35IDK7yKHCdCaIMvFO7yUYmmZYeDjnGKle26G4rwj+LKRQpjyUUvMkPglwGCYNQ==} dev: false @@ -27217,7 +26313,7 @@ packages: '@nrwl/tao': 19.0.7 '@yarnpkg/lockfile': 1.1.0 '@yarnpkg/parsers': 3.0.0-rc.46 - axios: 1.7.2 + axios: 1.7.7 chalk: 4.1.2 cli-cursor: 3.1.0 cli-spinners: 2.6.1 @@ -27309,10 +26405,6 @@ packages: ufo: 1.5.4 dev: true - /oauth-sign@0.9.0: - resolution: {integrity: sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==} - dev: false - /oauth2-server@3.0.0: resolution: {integrity: sha512-TlDDkKECOTjQQ9pQobw/EESLbd7YVY1i0Ebos996Au88FqiLUbQ+X/cRBCq6gvpkoA0ByrDsF8c97SyRygfE6Q==} engines: {node: '>=4.0'} @@ -27696,13 +26788,6 @@ packages: p-map: 2.1.0 dev: false - /p-each-series@1.0.0: - resolution: {integrity: sha512-J/e9xiZZQNrt+958FFzJ+auItsBGq+UrQ7nE89AUP7UOTtjHnkISANXLdayhVzh538UnLMCSlf13lFfRIAKQOA==} - engines: {node: '>=4'} - dependencies: - p-reduce: 1.0.0 - dev: false - /p-event@4.2.0: resolution: {integrity: sha512-KXatOjCRXXkSePPb1Nbi0p0m+gQAwdlbhi4wQKJPI1HsMQS9g+Sqp2o+QHziPr7eYJyOZet836KoHEVM1mwOrQ==} engines: {node: '>=8'} @@ -27815,11 +26900,6 @@ packages: p-timeout: 3.2.0 dev: true - /p-reduce@1.0.0: - resolution: {integrity: sha512-3Tx1T3oM1xO/Y8Gj0sWyE78EIJZ+t+aEmXUdvQgvGmSMri7aPTHoovbXEreWKkL5j21Er60XAWLTzKbAKYOujQ==} - engines: {node: '>=4'} - dev: false - /p-reduce@2.1.0: resolution: {integrity: sha512-2USApvnsutq8uoxZBGbbWM0JIYLiEMJ9RlaN7fAzVNb9OZN0SHjjTTfIcb667XynS5Y1VhwDJVDa72TnPzAYWw==} engines: {node: '>=8'} @@ -27919,7 +26999,7 @@ packages: resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} dependencies: dot-case: 3.0.4 - tslib: 2.6.2 + tslib: 2.8.0 dev: false /parent-module@1.0.1: @@ -27966,6 +27046,7 @@ packages: dependencies: error-ex: 1.3.2 json-parse-better-errors: 1.0.2 + dev: true /parse-json@5.2.0: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} @@ -27997,14 +27078,6 @@ packages: parse-path: 7.0.0 dev: true - /parse5@4.0.0: - resolution: {integrity: sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==} - dev: false - - /parse5@5.1.0: - resolution: {integrity: sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==} - dev: false - /parse5@6.0.1: resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} dev: false @@ -28024,7 +27097,7 @@ packages: resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} dependencies: no-case: 3.0.4 - tslib: 2.6.2 + tslib: 2.8.0 dev: false /pascalcase@0.1.1: @@ -28072,7 +27145,7 @@ packages: resolution: {integrity: sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg==} dependencies: dot-case: 3.0.4 - tslib: 2.6.2 + tslib: 2.8.0 dev: false /path-dirname@1.0.2: @@ -28304,6 +27377,7 @@ packages: engines: {node: '>=6'} dependencies: find-up: 3.0.0 + dev: true /pkg-dir@4.2.0: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} @@ -28414,10 +27488,6 @@ packages: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} - /pn@1.1.0: - resolution: {integrity: sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==} - dev: false - /pnpm@9.1.2: resolution: {integrity: sha512-En3IO56hDDK+ZdIqjvtKZfuVLo/vvf3tOb3DyX78MtMbSLAEIN8sEYes4oySHJAvDLWhNKTQMri1KVy/osaB4g==} engines: {node: '>=18.12'} @@ -28821,6 +27891,22 @@ packages: - typescript dev: true + /postcss-loader@7.3.4(postcss@8.4.47)(typescript@5.6.3)(webpack@5.96.1): + resolution: {integrity: sha512-iW5WTTBSC5BfsBJ9daFMPVrLT36MrNiC6fqOZTTaHjBNX6Pfd5p+hSBqe/fEeNd7pc13QiAyGt7VdGMw4eRC4A==} + engines: {node: '>= 14.15.0'} + peerDependencies: + postcss: ^7.0.0 || ^8.0.1 + webpack: ^5.0.0 + dependencies: + cosmiconfig: 8.3.6(typescript@5.6.3) + jiti: 1.21.0 + postcss: 8.4.47 + semver: 7.6.2 + webpack: 5.96.1(esbuild@0.18.20) + transitivePeerDependencies: + - typescript + dev: true + /postcss-logical@5.0.4(postcss@8.4.38): resolution: {integrity: sha512-RHXxplCeLh9VjinvMrZONq7im4wjWGlRJAqmAVLXyZaXwfDWP73/oq4NdIp+OZwhQUMj0zjqDfM5Fj7qby+B4g==} engines: {node: ^12 || ^14 || >=16} @@ -29378,16 +28464,6 @@ packages: react-is: 16.13.1 dev: false - /pretty-format@26.6.2: - resolution: {integrity: sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==} - engines: {node: '>= 10'} - dependencies: - '@jest/types': 26.6.2 - ansi-regex: 5.0.1 - ansi-styles: 4.3.0 - react-is: 17.0.2 - dev: false - /pretty-format@27.5.1: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -29944,11 +29020,6 @@ packages: dependencies: side-channel: 1.0.6 - /qs@6.5.3: - resolution: {integrity: sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==} - engines: {node: '>=0.6'} - dev: false - /query-string@7.1.3: resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==} engines: {node: '>=6'} @@ -30163,7 +29234,7 @@ packages: react: 18.3.1 dev: false - /react-dev-utils@11.0.4(eslint@8.57.0)(typescript@4.9.5)(webpack@5.91.0): + /react-dev-utils@11.0.4(eslint@8.57.0)(typescript@4.9.5)(webpack@5.96.1): resolution: {integrity: sha512-dx0LvIGHcOPtKbeiSUM4jqpBl3TcY7CDjZdfOIcKeznE7BWr9dg0iPG90G5yfVQ+p/rGNMXdbfStvzQZEVEi4A==} engines: {node: '>=10'} peerDependencies: @@ -30182,7 +29253,7 @@ packages: escape-string-regexp: 2.0.0 filesize: 6.1.0 find-up: 4.1.0 - fork-ts-checker-webpack-plugin: 4.1.6(eslint@8.57.0)(typescript@4.9.5)(webpack@5.91.0) + fork-ts-checker-webpack-plugin: 4.1.6(eslint@8.57.0)(typescript@4.9.5)(webpack@5.96.1) global-modules: 2.0.0 globby: 11.0.1 gzip-size: 5.1.1 @@ -30198,7 +29269,7 @@ packages: strip-ansi: 6.0.0 text-table: 0.2.0 typescript: 4.9.5 - webpack: 5.91.0(esbuild@0.23.1) + webpack: 5.96.1(esbuild@0.23.1) transitivePeerDependencies: - eslint - supports-color @@ -30580,7 +29651,7 @@ packages: '@types/react': 18.3.4 react: 18.3.1 react-style-singleton: 2.2.1(@types/react@18.3.4)(react@18.3.1) - tslib: 2.6.2 + tslib: 2.8.0 dev: true /react-remove-scroll@2.5.5(@types/react@18.3.4)(react@18.3.1): @@ -30597,7 +29668,7 @@ packages: react: 18.3.1 react-remove-scroll-bar: 2.3.6(@types/react@18.3.4)(react@18.3.1) react-style-singleton: 2.2.1(@types/react@18.3.4)(react@18.3.1) - tslib: 2.6.2 + tslib: 2.8.0 use-callback-ref: 1.3.2(@types/react@18.3.4)(react@18.3.1) use-sidecar: 1.1.2(@types/react@18.3.4)(react@18.3.1) dev: true @@ -30854,7 +29925,7 @@ packages: get-nonce: 1.0.1 invariant: 2.2.4 react: 18.3.1 - tslib: 2.6.2 + tslib: 2.8.0 dev: true /react-table-sticky@1.1.3: @@ -31010,6 +30081,7 @@ packages: dependencies: find-up: 3.0.0 read-pkg: 3.0.0 + dev: true /read-pkg-up@7.0.1: resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} @@ -31035,6 +30107,7 @@ packages: load-json-file: 4.0.0 normalize-package-data: 2.5.0 path-type: 3.0.0 + dev: true /read-pkg@5.2.0: resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==} @@ -31116,13 +30189,6 @@ packages: dependencies: picomatch: 2.3.1 - /realpath-native@1.1.0: - resolution: {integrity: sha512-wlgPA6cCIIg9gKz0fgAPjnzh4yR/LnXovwuo9hvyGvx3h8nX4+/iLZplfUWasXpqD8BdnGnP5njOFjkUwPzvjA==} - engines: {node: '>=4'} - dependencies: - util.promisify: 1.1.2 - dev: false - /recast@0.21.5: resolution: {integrity: sha512-hjMmLaUXAm1hIuTqOdeYObMslq/q+Xff6QE3Y2P+uoHAg2nmVlLBps2hzh1UJDdMtDTMXOFewK6ky51JQIeECg==} engines: {node: '>= 4'} @@ -31130,7 +30196,7 @@ packages: ast-types: 0.15.2 esprima: 4.0.1 source-map: 0.6.1 - tslib: 2.6.2 + tslib: 2.8.0 dev: true /recast@0.23.9: @@ -31141,7 +30207,7 @@ packages: esprima: 4.0.1 source-map: 0.6.1 tiny-invariant: 1.3.3 - tslib: 2.6.2 + tslib: 2.8.0 dev: true /rechoir@0.6.2: @@ -31479,56 +30545,6 @@ packages: resolution: {integrity: sha512-cA6Xh6e0fDBBBwH77SLJaJPBmD3nWVAcF9/XAcsrIHdjhFzFiB5aNQFytdjCGPezU3ROwrR11IddKAM08vohxA==} dev: false - /request-promise-core@1.1.4(request@2.88.2): - resolution: {integrity: sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==} - engines: {node: '>=0.10.0'} - peerDependencies: - request: ^2.34 - dependencies: - lodash: 4.17.21 - request: 2.88.2 - dev: false - - /request-promise-native@1.0.9(request@2.88.2): - resolution: {integrity: sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g==} - engines: {node: '>=0.12.0'} - deprecated: request-promise-native has been deprecated because it extends the now deprecated request package, see https://github.com/request/request/issues/3142 - peerDependencies: - request: ^2.34 - dependencies: - request: 2.88.2 - request-promise-core: 1.1.4(request@2.88.2) - stealthy-require: 1.1.1 - tough-cookie: 2.5.0 - dev: false - - /request@2.88.2: - resolution: {integrity: sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==} - engines: {node: '>= 6'} - deprecated: request has been deprecated, see https://github.com/request/request/issues/3142 - dependencies: - aws-sign2: 0.7.0 - aws4: 1.13.0 - caseless: 0.12.0 - combined-stream: 1.0.8 - extend: 3.0.2 - forever-agent: 0.6.1 - form-data: 2.3.3 - har-validator: 5.1.5 - http-signature: 1.2.0 - is-typedarray: 1.0.0 - isstream: 0.1.2 - json-stringify-safe: 5.0.1 - mime-types: 2.1.35 - oauth-sign: 0.9.0 - performance-now: 2.1.0 - qs: 6.5.3 - safe-buffer: 5.2.1 - tough-cookie: 2.5.0 - tunnel-agent: 0.6.0 - uuid: 3.4.0 - dev: false - /require-at@1.0.6: resolution: {integrity: sha512-7i1auJbMUrXEAZCOQ0VNJgmcT2VOKPRl2YGJwgpHpC9CE91Mv4/4UYIUm4chGJaI381ZDq1JUicFii64Hapd8g==} engines: {node: '>=4'} @@ -31548,6 +30564,7 @@ packages: /require-main-filename@2.0.0: resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + dev: true /requireindex@1.2.0: resolution: {integrity: sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==} @@ -31570,13 +30587,6 @@ packages: resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==} dev: false - /resolve-cwd@2.0.0: - resolution: {integrity: sha512-ccu8zQTrzVr954472aUVPLEcB3YpKSYR3cg/3lo1okzobPBM+1INXBbBZlDbnI/hbEocnf8j0QVo43hQKrbchg==} - engines: {node: '>=4'} - dependencies: - resolve-from: 3.0.0 - dev: false - /resolve-cwd@3.0.0: resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} engines: {node: '>=8'} @@ -31591,11 +30601,6 @@ packages: global-modules: 1.0.0 dev: false - /resolve-from@3.0.0: - resolution: {integrity: sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==} - engines: {node: '>=4'} - dev: false - /resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -31671,10 +30676,6 @@ packages: engines: {node: '>=10'} dev: true - /resolve@1.1.7: - resolution: {integrity: sha512-9znBF0vBcaSN3W2j7wKvdERPwqTxSpCq+if5C0WoTCyV9n24rua28jeuQ2pL/HOf+yUe/Mef+H/5p60K0Id3bg==} - dev: false - /resolve@1.22.8: resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} hasBin: true @@ -31735,6 +30736,7 @@ packages: hasBin: true dependencies: glob: 7.2.3 + dev: true /rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} @@ -31814,11 +30816,6 @@ packages: resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==} dev: false - /rsvp@4.8.5: - resolution: {integrity: sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==} - engines: {node: 6.* || >= 7.*} - dev: false - /rtl-css-js@1.16.1: resolution: {integrity: sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==} dependencies: @@ -31920,25 +30917,6 @@ packages: /safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - /sane@4.1.0: - resolution: {integrity: sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA==} - engines: {node: 6.* || 8.* || >= 10.*} - deprecated: some dependency vulnerabilities fixed, support for node < 10 dropped, and newer ECMAScript syntax/features added - hasBin: true - dependencies: - '@cnakazawa/watch': 1.0.4 - anymatch: 2.0.0(supports-color@5.5.0) - capture-exit: 2.0.0 - exec-sh: 0.3.6 - execa: 1.0.0 - fb-watchman: 2.0.2 - micromatch: 3.1.10(supports-color@5.5.0) - minimist: 1.2.8 - walker: 1.0.8 - transitivePeerDependencies: - - supports-color - dev: false - /sanitize.css@13.0.0: resolution: {integrity: sha512-ZRwKbh/eQ6w9vmTjkuG0Ioi3HBwPFce0O+v//ve+aOq1oeCy7jMV2qzzAlpsNuqpqCBjjriM1lbtZbF/Q8jVyA==} dev: false @@ -32000,6 +30978,29 @@ packages: webpack: 5.91.0(esbuild@0.18.20)(webpack-cli@5.1.4) dev: true + /sass-loader@13.3.3(webpack@5.96.1): + resolution: {integrity: sha512-mt5YN2F1MOZr3d/wBRcZxeFgwgkH44wVc2zohO2YF6JiOMkiXe4BYRZpSu2sO1g71mo/j16txzUhsKZlqjVGzA==} + engines: {node: '>= 14.15.0'} + peerDependencies: + fibers: '>= 3.1.0' + node-sass: ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 + sass: ^1.3.0 + sass-embedded: '*' + webpack: ^5.0.0 + peerDependenciesMeta: + fibers: + optional: true + node-sass: + optional: true + sass: + optional: true + sass-embedded: + optional: true + dependencies: + neo-async: 2.6.2 + webpack: 5.96.1(esbuild@0.18.20) + dev: true + /sass@1.77.2: resolution: {integrity: sha512-eb4GZt1C3avsX3heBNlrc7I09nyT00IUuo4eFhAbeXWU2fvA7oXI53SxODVAA+zgZCk9aunAZgO+losjR3fAwA==} engines: {node: '>=14.0.0'} @@ -32015,13 +31016,8 @@ packages: /sax@1.3.0: resolution: {integrity: sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==} - - /saxes@3.1.11: - resolution: {integrity: sha512-Ydydq3zC+WYDJK1+gRxRapLIED9PWeSuuS41wqyoRmzvhhh9nc+QQrVMKJYzJFULazeGhzSV0QleN2wD3boh2g==} - engines: {node: '>=8'} - dependencies: - xmlchars: 2.2.0 - dev: false + requiresBuild: true + optional: true /saxes@5.0.1: resolution: {integrity: sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==} @@ -32195,7 +31191,7 @@ packages: resolution: {integrity: sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==} dependencies: no-case: 3.0.4 - tslib: 2.6.2 + tslib: 2.8.0 upper-case-first: 2.0.2 dev: false @@ -32203,6 +31199,19 @@ packages: resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==} dev: false + /serialize-interceptor@1.1.7(cache-manager@6.1.3)(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2): + resolution: {integrity: sha512-JQ/jSFWRQRyCyJI48USkaAeq+nkxsw5A0cJG7oNLWGYXgpWkyWTCoCfvfdZeaMNsdVzIk/cR54feq9ppM/zgBA==} + dependencies: + '@nestjs/common': 8.4.7(cache-manager@6.1.3)(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) + rxjs: 7.8.1 + transitivePeerDependencies: + - cache-manager + - class-transformer + - class-validator + - debug + - reflect-metadata + dev: false + /serialize-javascript@4.0.0: resolution: {integrity: sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==} dependencies: @@ -32360,10 +31369,6 @@ packages: /shell-quote@1.8.1: resolution: {integrity: sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==} - /shellwords@0.1.1: - resolution: {integrity: sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==} - dev: false - /side-channel@1.0.6: resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} engines: {node: '>= 0.4'} @@ -32471,7 +31476,7 @@ packages: resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} dependencies: dot-case: 3.0.4 - tslib: 2.6.2 + tslib: 2.8.0 dev: false /snapdragon-node@2.1.1: @@ -32823,22 +31828,6 @@ packages: frac: 1.1.2 dev: false - /sshpk@1.18.0: - resolution: {integrity: sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==} - engines: {node: '>=0.10.0'} - hasBin: true - dependencies: - asn1: 0.2.6 - assert-plus: 1.0.0 - bcrypt-pbkdf: 1.0.2 - dashdash: 1.14.1 - ecc-jsbn: 0.1.2 - getpass: 0.1.7 - jsbn: 0.1.1 - safer-buffer: 2.1.2 - tweetnacl: 0.14.5 - dev: false - /ssri@10.0.6: resolution: {integrity: sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -32867,13 +31856,6 @@ packages: resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} dev: false - /stack-utils@1.0.5: - resolution: {integrity: sha512-KZiTzuV3CnSnSvgMRrARVCj+Ht7rMbauGDK0LdVFRGyenwdylpajAp4Q0i6SX8rEmbTpMMf6ryq2gb8pPq2WgQ==} - engines: {node: '>=8'} - dependencies: - escape-string-regexp: 2.0.0 - dev: false - /stack-utils@2.0.6: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} @@ -32943,11 +31925,6 @@ packages: resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} dev: false - /stealthy-require@1.1.1: - resolution: {integrity: sha512-ZnWpYnYugiOVEY5GkcuJK1io5V8QmNYChG62gSit9pQVGErXtrKuPC55ITaVSukmMta5qpMU7vqLt2Lnni4f/g==} - engines: {node: '>=0.10.0'} - dev: false - /stop-iteration-iterator@1.0.0: resolution: {integrity: sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==} engines: {node: '>= 0.4'} @@ -32971,6 +31948,10 @@ packages: - utf-8-validate dev: true + /strategy@1.1.1: + resolution: {integrity: sha512-oSSXKnD6mBC/7bqX7b2GPzFhwGRfjR9jbuPeBBmLS99grRWj9XZh47W3EihppKbWFWCEvTwTgaWdA56C9hpmxQ==} + dev: false + /stream-browserify@2.0.2: resolution: {integrity: sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==} dependencies: @@ -33031,22 +32012,6 @@ packages: resolution: {integrity: sha512-bbEs3scLeYNXLecRRuk6uJxdXUSj6le/8rNPHChIJTn2V79aXVTR1EH2OH5zLKKoz0V02fOUKZZcw01pLUShZA==} dev: false - /string-length@2.0.0: - resolution: {integrity: sha512-Qka42GGrS8Mm3SZ+7cH8UXiIWI867/b/Z/feQSpQx/rbfB8UGknGEZVaUQMOUVj+soY6NpWAxily63HI1OckVQ==} - engines: {node: '>=4'} - dependencies: - astral-regex: 1.0.0 - strip-ansi: 4.0.0 - dev: false - - /string-length@3.1.0: - resolution: {integrity: sha512-Ttp5YvkGm5v9Ijagtaz1BnN+k9ObpvS0eIBblPMp2YWL8FBmi9qblQ9fexc2k/CXFgrTIteU3jAw3payCnwSTA==} - engines: {node: '>=8'} - dependencies: - astral-regex: 1.0.0 - strip-ansi: 5.2.0 - dev: false - /string-length@4.0.2: resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} engines: {node: '>=10'} @@ -33090,6 +32055,7 @@ packages: emoji-regex: 7.0.3 is-fullwidth-code-point: 2.0.0 strip-ansi: 5.2.0 + dev: true /string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} @@ -33204,6 +32170,7 @@ packages: engines: {node: '>=6'} dependencies: ansi-regex: 4.1.1 + dev: true /strip-ansi@6.0.0: resolution: {integrity: sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==} @@ -33311,6 +32278,15 @@ packages: dependencies: webpack: 5.91.0(esbuild@0.23.1) + /style-loader@3.3.4(webpack@5.96.1): + resolution: {integrity: sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w==} + engines: {node: '>= 12.13.0'} + peerDependencies: + webpack: ^5.0.0 + dependencies: + webpack: 5.96.1(esbuild@0.18.20) + dev: true + /styled-components@5.3.11(@babel/core@7.26.0)(react-dom@18.3.1)(react-is@18.3.1)(react@18.3.1): resolution: {integrity: sha512-uuzIIfnVkagcVHv9nE0VPlHPSCmXIUGKfJ42LNjxCCTDTL5sgnJ8Z7GZBq0EnLYGln77tPpEpExt2+qa+cZqSw==} engines: {node: '>=10'} @@ -33456,6 +32432,7 @@ packages: engines: {node: '>=6'} dependencies: has-flag: 3.0.0 + dev: true /supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} @@ -33742,6 +32719,31 @@ packages: terser: 5.31.0 webpack: 5.91.0(esbuild@0.18.20)(webpack-cli@5.1.4) + /terser-webpack-plugin@5.3.10(esbuild@0.18.20)(webpack@5.96.1): + resolution: {integrity: sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==} + engines: {node: '>= 10.13.0'} + peerDependencies: + '@swc/core': '*' + esbuild: '*' + uglify-js: '*' + webpack: ^5.1.0 + peerDependenciesMeta: + '@swc/core': + optional: true + esbuild: + optional: true + uglify-js: + optional: true + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + esbuild: 0.18.20 + jest-worker: 27.5.1 + schema-utils: 3.3.0 + serialize-javascript: 6.0.2 + terser: 5.31.0 + webpack: 5.96.1(esbuild@0.18.20) + dev: true + /terser-webpack-plugin@5.3.10(esbuild@0.23.1)(webpack@5.91.0): resolution: {integrity: sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==} engines: {node: '>= 10.13.0'} @@ -33789,7 +32791,6 @@ packages: serialize-javascript: 6.0.2 terser: 5.31.0 webpack: 5.96.1(esbuild@0.23.1) - dev: true /terser@5.31.0: resolution: {integrity: sha512-Q1JFAoUKE5IMfI4Z/lkE/E6+SwgzO+x4tq4v1AyBLRj8VSYvRO6A/rQrPg1yud4g0En9EKI1TvFRF2tQFcoUkg==} @@ -33809,6 +32810,7 @@ packages: minimatch: 3.1.2 read-pkg-up: 4.0.0 require-main-filename: 2.0.0 + dev: true /test-exclude@6.0.0: resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} @@ -33876,10 +32878,6 @@ packages: any-promise: 1.3.0 dev: false - /throat@4.1.0: - resolution: {integrity: sha512-wCVxLDcFxw7ujDxaeJC6nfl2XfHJNYs8yUYJnvMgtPEFlttP9tHSfRUv2vBe6C4hkVFPWoP1P6ZccbYjmSEkKA==} - dev: false - /throat@6.0.2: resolution: {integrity: sha512-WKexMoJj3vEuK0yFEapj8y64V0A6xcuPuK9Gt1d0R+dzCSJc0lHqQytAbSB4cDAK0dWh4T0E2ETkoLE2WZ41OQ==} dev: false @@ -34096,14 +33094,6 @@ packages: hasBin: true dev: false - /tough-cookie@2.5.0: - resolution: {integrity: sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==} - engines: {node: '>=0.8'} - dependencies: - psl: 1.9.0 - punycode: 2.3.1 - dev: false - /tough-cookie@4.1.4: resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} engines: {node: '>=6'} @@ -34251,7 +33241,24 @@ packages: semver: 7.6.2 source-map: 0.7.4 typescript: 5.6.3 - webpack: 5.91.0(esbuild@0.23.1) + webpack: 5.91.0(esbuild@0.18.20)(webpack-cli@5.1.4) + dev: false + + /ts-loader@9.5.1(typescript@5.6.3)(webpack@5.96.1): + resolution: {integrity: sha512-rNH3sK9kGZcH9dYzC7CewQm4NtxJTjSEVRJ2DyBZR7f8/wcta+iV44UPCXc5+nzDzivKtlzV6c9P4e+oFhDLYg==} + engines: {node: '>=12.0.0'} + peerDependencies: + typescript: '*' + webpack: ^5.0.0 + dependencies: + chalk: 4.1.2 + enhanced-resolve: 5.16.1 + micromatch: 4.0.7 + semver: 7.6.2 + source-map: 0.7.4 + typescript: 5.6.3 + webpack: 5.96.1(esbuild@0.23.1) + dev: true /ts-node@10.9.2(@types/node@20.5.1)(typescript@5.6.3): resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} @@ -34343,6 +33350,10 @@ packages: /tslib@1.14.1: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + /tslib@2.4.0: + resolution: {integrity: sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==} + dev: false + /tslib@2.5.3: resolution: {integrity: sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w==} dev: false @@ -34355,7 +33366,6 @@ packages: /tslib@2.8.0: resolution: {integrity: sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==} - dev: false /tsscmp@1.0.6: resolution: {integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==} @@ -34468,16 +33478,6 @@ packages: - supports-color dev: true - /tunnel-agent@0.6.0: - resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} - dependencies: - safe-buffer: 5.2.1 - dev: false - - /tweetnacl@0.14.5: - resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==} - dev: false - /type-check@0.3.2: resolution: {integrity: sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==} engines: {node: '>= 0.8.0'} @@ -34877,7 +33877,7 @@ packages: webpack-sources: optional: true dependencies: - acorn: 8.13.0 + acorn: 8.14.0 webpack-virtual-modules: 0.6.2 dev: true @@ -34952,13 +33952,13 @@ packages: /upper-case-first@2.0.2: resolution: {integrity: sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==} dependencies: - tslib: 2.6.2 + tslib: 2.8.0 dev: false /upper-case@2.0.2: resolution: {integrity: sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==} dependencies: - tslib: 2.6.2 + tslib: 2.8.0 dev: false /uri-js@4.4.1: @@ -35004,7 +34004,7 @@ packages: dependencies: '@types/react': 18.3.4 react: 18.3.1 - tslib: 2.6.2 + tslib: 2.8.0 dev: true /use-resize-observer@9.1.0(react-dom@18.3.1)(react@18.3.1): @@ -35031,7 +34031,7 @@ packages: '@types/react': 18.3.4 detect-node-es: 1.1.0 react: 18.3.1 - tslib: 2.6.2 + tslib: 2.8.0 dev: true /use@3.1.1: @@ -35051,18 +34051,6 @@ packages: object.getownpropertydescriptors: 2.1.8 dev: false - /util.promisify@1.1.2: - resolution: {integrity: sha512-PBdZ03m1kBnQ5cjjO0ZvJMJS+QsbyIcFwi4hY4U76OQsCO9JrOYjbCFgIF76ccFg9xnJo7ZHPkqyj1GqmdS7MA==} - dependencies: - call-bind: 1.0.7 - define-properties: 1.2.1 - for-each: 0.3.3 - has-proto: 1.0.3 - has-symbols: 1.0.3 - object.getownpropertydescriptors: 2.1.8 - safe-array-concat: 1.1.2 - dev: false - /util@0.10.4: resolution: {integrity: sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==} dependencies: @@ -35102,6 +34090,7 @@ packages: resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details. hasBin: true + dev: true /uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} @@ -35176,15 +34165,6 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} - /verror@1.10.0: - resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==} - engines: {'0': node >=0.6.0} - dependencies: - assert-plus: 1.0.0 - core-util-is: 1.0.2 - extsprintf: 1.3.0 - dev: false - /vinyl-fs@3.0.3: resolution: {integrity: sha512-vIu34EkyNyJxmP0jscNzWBSygh7VWhqun6RmqVfXePrOwi9lhvRs//dOaGOTRUQr4tx7/zd26Tk5WeSVZitgng==} engines: {node: '>= 0.10'} @@ -35406,14 +34386,6 @@ packages: resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} dev: false - /w3c-xmlserializer@1.1.2: - resolution: {integrity: sha512-p10l/ayESzrBMYWRID6xbuCKh2Fp77+sA0doRuGn4tTIMrrZVeqfpKjXHY+oDh3K4nLdPgNwMTVP6Vp4pvqbNg==} - dependencies: - domexception: 1.0.1 - webidl-conversions: 4.0.2 - xml-name-validator: 3.0.0 - dev: false - /w3c-xmlserializer@2.0.0: resolution: {integrity: sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==} engines: {node: '>=10'} @@ -35790,6 +34762,45 @@ packages: - esbuild - uglify-js + /webpack@5.96.1(esbuild@0.18.20): + resolution: {integrity: sha512-l2LlBSvVZGhL4ZrPwyr8+37AunkcYj5qh8o6u2/2rzoPc8gxFJkLj1WxNgooi9pnoc06jh0BjuXnamM4qlujZA==} + engines: {node: '>=10.13.0'} + hasBin: true + peerDependencies: + webpack-cli: '*' + peerDependenciesMeta: + webpack-cli: + optional: true + dependencies: + '@types/eslint-scope': 3.7.7 + '@types/estree': 1.0.6 + '@webassemblyjs/ast': 1.12.1 + '@webassemblyjs/wasm-edit': 1.12.1 + '@webassemblyjs/wasm-parser': 1.12.1 + acorn: 8.14.0 + browserslist: 4.24.2 + chrome-trace-event: 1.0.3 + enhanced-resolve: 5.17.1 + es-module-lexer: 1.5.3 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + json-parse-even-better-errors: 2.3.1 + loader-runner: 4.3.0 + mime-types: 2.1.35 + neo-async: 2.6.2 + schema-utils: 3.3.0 + tapable: 2.2.1 + terser-webpack-plugin: 5.3.10(esbuild@0.18.20)(webpack@5.96.1) + watchpack: 2.4.1 + webpack-sources: 3.2.3 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - uglify-js + dev: true + /webpack@5.96.1(esbuild@0.23.1): resolution: {integrity: sha512-l2LlBSvVZGhL4ZrPwyr8+37AunkcYj5qh8o6u2/2rzoPc8gxFJkLj1WxNgooi9pnoc06jh0BjuXnamM4qlujZA==} engines: {node: '>=10.13.0'} @@ -35827,7 +34838,6 @@ packages: - '@swc/core' - esbuild - uglify-js - dev: true /websocket-driver@0.7.4: resolution: {integrity: sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==} @@ -35879,14 +34889,6 @@ packages: tr46: 0.0.3 webidl-conversions: 3.0.1 - /whatwg-url@6.5.0: - resolution: {integrity: sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==} - dependencies: - lodash.sortby: 4.7.0 - tr46: 1.0.1 - webidl-conversions: 4.0.2 - dev: false - /whatwg-url@7.1.0: resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} dependencies: @@ -35946,6 +34948,7 @@ packages: /which-module@2.0.1: resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + dev: true /which-typed-array@1.1.15: resolution: {integrity: sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==} @@ -36242,6 +35245,7 @@ packages: ansi-styles: 3.2.1 string-width: 3.1.0 strip-ansi: 5.2.0 + dev: true /wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} @@ -36271,14 +35275,6 @@ packages: /wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - /write-file-atomic@2.4.1: - resolution: {integrity: sha512-TGHFeZEZMnv+gBFRfjAcxL5bPHrsGKtnb4qsFAws7/vlh+QfwAaySIw4AXP9ZskTTh5GWu3FLuJhsWVdiJPGvg==} - dependencies: - graceful-fs: 4.2.11 - imurmurhash: 0.1.4 - signal-exit: 3.0.7 - dev: false - /write-file-atomic@2.4.3: resolution: {integrity: sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==} dependencies: @@ -36343,20 +35339,6 @@ packages: write-json-file: 3.2.0 dev: true - /ws@5.2.3: - resolution: {integrity: sha512-jZArVERrMsKUatIdnLzqvcfydI85dvd/Fp1u/VOpfdDWQ4c9qWXe+VIeAbQ5FrDwciAkr+lzofXLz3Kuf26AOA==} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: ^5.0.2 - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - dependencies: - async-limiter: 1.0.1 - dev: false - /ws@6.2.2: resolution: {integrity: sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==} peerDependencies: @@ -36369,6 +35351,7 @@ packages: optional: true dependencies: async-limiter: 1.0.1 + dev: true /ws@7.4.6: resolution: {integrity: sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==} @@ -36475,6 +35458,7 @@ packages: /y18n@4.0.3: resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + dev: true /y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} @@ -36503,6 +35487,7 @@ packages: dependencies: camelcase: 5.3.1 decamelize: 1.2.0 + dev: true /yargs-parser@18.1.3: resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} @@ -36540,6 +35525,7 @@ packages: which-module: 2.0.1 y18n: 4.0.3 yargs-parser: 13.1.2 + dev: true /yargs@16.2.0: resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} diff --git a/src/errors/service-error.ts b/src/errors/service-error.ts index 4125440fa..0519ecba6 100644 --- a/src/errors/service-error.ts +++ b/src/errors/service-error.ts @@ -1,14 +1 @@ -import { HttpStatus } from '@nestjs/common'; - -export class ServiceError extends Error { - constructor( - public message: string, - private status: HttpStatus = HttpStatus.INTERNAL_SERVER_ERROR, - ) { - super(message); - } - - getStatus(): HttpStatus { - return this.status; - } -} \ No newline at end of file + \ No newline at end of file diff --git a/src/filters/service-error.filter.ts b/src/filters/service-error.filter.ts index 6542ee4d5..0519ecba6 100644 --- a/src/filters/service-error.filter.ts +++ b/src/filters/service-error.filter.ts @@ -1,19 +1 @@ -import { ExceptionFilter, Catch, ArgumentsHost, HttpStatus } from '@nestjs/common'; -import { Response } from 'express'; -import { ServiceError } from '../errors/service-error'; - -@Catch(ServiceError) -export class ServiceErrorFilter implements ExceptionFilter { - catch(exception: ServiceError, host: ArgumentsHost) { - const ctx = host.switchToHttp(); - const response = ctx.getResponse(); - const status = exception.getStatus(); - - response - .status(status) - .json({ - statusCode: status, - message: exception.message, - }); - } -} \ No newline at end of file + \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index c077060b0..0519ecba6 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,13 +1 @@ -import { NestFactory } from '@nestjs/core'; -import { AppModule } from './app.module'; -import { ServiceErrorFilter } from './filters/service-error.filter'; - -async function bootstrap() { - const app = await NestFactory.create(AppModule); - - // Register the ServiceErrorFilter globally - app.useGlobalFilters(new ServiceErrorFilter()); - - await app.listen(3000); -} -bootstrap(); \ No newline at end of file + \ No newline at end of file diff --git a/test/jest-e2e.json b/test/jest-e2e.json new file mode 100644 index 000000000..12fc14a84 --- /dev/null +++ b/test/jest-e2e.json @@ -0,0 +1,13 @@ +{ + "moduleFileExtensions": ["js", "json", "ts"], + "rootDir": ".", + "testEnvironment": "node", + "testRegex": ".e2e-spec.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + }, + "watchPlugins": [ + "jest-watch-typeahead/filename", + "jest-watch-typeahead/testname" + ] +} \ No newline at end of file