mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-21 15:20:34 +00:00
refactor: wip to nestjs
This commit is contained in:
@@ -10,7 +10,8 @@
|
|||||||
"build:server": "lerna run build --scope \"@bigcapital/server\" --scope \"@bigcapital/utils\" --scope \"@bigcapital/pdf-templates\" --scope \"@bigcapital/email-components\"",
|
"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\"",
|
"serve:server": "lerna run serve --scope \"@bigcapital/server\" --scope \"@bigcapital/utils\"",
|
||||||
"server2:start": "lerna run start:dev --scope \"@bigcapital/server2\"",
|
"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"
|
"prepare": "husky install"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -20,6 +20,9 @@
|
|||||||
"test:e2e": "jest --config ./test/jest-e2e.json --watchAll"
|
"test:e2e": "jest --config ./test/jest-e2e.json --watchAll"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@bigcapital/utils": "*",
|
||||||
|
"@bigcapital/email-components": "*",
|
||||||
|
"@bigcapital/pdf-templates": "*",
|
||||||
"@nestjs/bull": "^10.2.1",
|
"@nestjs/bull": "^10.2.1",
|
||||||
"@nestjs/bullmq": "^10.2.1",
|
"@nestjs/bullmq": "^10.2.1",
|
||||||
"@nestjs/cache-manager": "^2.2.2",
|
"@nestjs/cache-manager": "^2.2.2",
|
||||||
@@ -36,6 +39,8 @@
|
|||||||
"@types/ramda": "^0.30.2",
|
"@types/ramda": "^0.30.2",
|
||||||
"accounting": "^0.4.1",
|
"accounting": "^0.4.1",
|
||||||
"async": "^3.2.0",
|
"async": "^3.2.0",
|
||||||
|
"axios": "^1.6.0",
|
||||||
|
"form-data": "^4.0.0",
|
||||||
"bull": "^4.16.3",
|
"bull": "^4.16.3",
|
||||||
"bullmq": "^5.21.1",
|
"bullmq": "^5.21.1",
|
||||||
"cache-manager": "^6.1.1",
|
"cache-manager": "^6.1.1",
|
||||||
@@ -53,6 +58,8 @@
|
|||||||
"mysql2": "^3.11.3",
|
"mysql2": "^3.11.3",
|
||||||
"nestjs-cls": "^4.4.1",
|
"nestjs-cls": "^4.4.1",
|
||||||
"nestjs-i18n": "^10.4.9",
|
"nestjs-i18n": "^10.4.9",
|
||||||
|
"uuid": "^10.0.0",
|
||||||
|
"pug": "^3.0.2",
|
||||||
"object-hash": "^2.0.3",
|
"object-hash": "^2.0.3",
|
||||||
"objection": "^3.1.5",
|
"objection": "^3.1.5",
|
||||||
"passport": "^0.7.0",
|
"passport": "^0.7.0",
|
||||||
|
|||||||
26
packages/server-nest/src/libs/chromiumly/Chromiumly.ts
Normal file
26
packages/server-nest/src/libs/chromiumly/Chromiumly.ts
Normal file
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
66
packages/server-nest/src/libs/chromiumly/ConvertUtils.ts
Normal file
66
packages/server-nest/src/libs/chromiumly/ConvertUtils.ts
Normal file
@@ -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}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
packages/server-nest/src/libs/chromiumly/Converter.ts
Normal file
10
packages/server-nest/src/libs/chromiumly/Converter.ts
Normal file
@@ -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]}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
24
packages/server-nest/src/libs/chromiumly/GotenbergUtils.ts
Normal file
24
packages/server-nest/src/libs/chromiumly/GotenbergUtils.ts
Normal file
@@ -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<Buffer> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
38
packages/server-nest/src/libs/chromiumly/HTMLConvert.ts
Normal file
38
packages/server-nest/src/libs/chromiumly/HTMLConvert.ts
Normal file
@@ -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<Buffer> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
38
packages/server-nest/src/libs/chromiumly/UrlConvert.ts
Normal file
38
packages/server-nest/src/libs/chromiumly/UrlConvert.ts
Normal file
@@ -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<Buffer> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
51
packages/server-nest/src/libs/chromiumly/_types.ts
Normal file
51
packages/server-nest/src/libs/chromiumly/_types.ts
Normal file
@@ -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<Buffer>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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',
|
||||||
|
}
|
||||||
@@ -29,7 +29,6 @@ import { APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core';
|
|||||||
import { JwtAuthGuard } from '../Auth/Jwt.guard';
|
import { JwtAuthGuard } from '../Auth/Jwt.guard';
|
||||||
import { UserIpInterceptor } from '@/interceptors/user-ip.interceptor';
|
import { UserIpInterceptor } from '@/interceptors/user-ip.interceptor';
|
||||||
import { TenancyGlobalMiddleware } from '../Tenancy/TenancyGlobal.middleware';
|
import { TenancyGlobalMiddleware } from '../Tenancy/TenancyGlobal.middleware';
|
||||||
import { TransformerInjectable } from '../Transformer/TransformerInjectable.service';
|
|
||||||
import { TransformerModule } from '../Transformer/Transformer.module';
|
import { TransformerModule } from '../Transformer/Transformer.module';
|
||||||
import { AccountsModule } from '../Accounts/Accounts.module';
|
import { AccountsModule } from '../Accounts/Accounts.module';
|
||||||
import { ExpensesModule } from '../Expenses/Expenses.module';
|
import { ExpensesModule } from '../Expenses/Expenses.module';
|
||||||
@@ -40,6 +39,11 @@ import { BranchesModule } from '../Branches/Branches.module';
|
|||||||
import { WarehousesModule } from '../Warehouses/Warehouses.module';
|
import { WarehousesModule } from '../Warehouses/Warehouses.module';
|
||||||
import { SaleEstimatesModule } from '../SaleEstimates/SaleEstimates.module';
|
import { SaleEstimatesModule } from '../SaleEstimates/SaleEstimates.module';
|
||||||
import { SerializeInterceptor } from '@/common/interceptors/serialize.interceptor';
|
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({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -93,6 +97,8 @@ import { SerializeInterceptor } from '@/common/interceptors/serialize.intercepto
|
|||||||
}),
|
}),
|
||||||
TenancyDatabaseModule,
|
TenancyDatabaseModule,
|
||||||
TenancyModelsModule,
|
TenancyModelsModule,
|
||||||
|
ChromiumlyTenancyModule,
|
||||||
|
TransformerModule,
|
||||||
ItemsModule,
|
ItemsModule,
|
||||||
ItemCategoryModule,
|
ItemCategoryModule,
|
||||||
AccountsModule,
|
AccountsModule,
|
||||||
@@ -101,7 +107,11 @@ import { SerializeInterceptor } from '@/common/interceptors/serialize.intercepto
|
|||||||
PdfTemplatesModule,
|
PdfTemplatesModule,
|
||||||
BranchesModule,
|
BranchesModule,
|
||||||
WarehousesModule,
|
WarehousesModule,
|
||||||
|
CustomersModule,
|
||||||
|
VendorsModule,
|
||||||
SaleEstimatesModule,
|
SaleEstimatesModule,
|
||||||
|
BillsModule,
|
||||||
|
BillPaymentsModule,
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [
|
providers: [
|
||||||
|
|||||||
@@ -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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {}
|
||||||
@@ -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<IBillPayment>}
|
||||||
|
*/
|
||||||
|
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<IBillPayment>}
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<void> {
|
||||||
|
const opers: Promise<void>[] = [];
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
@@ -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<void> => {
|
||||||
|
// 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<void> => {
|
||||||
|
// // 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<void> => {
|
||||||
|
// 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);
|
||||||
|
// };
|
||||||
|
// }
|
||||||
@@ -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
|
||||||
|
// );
|
||||||
|
// };
|
||||||
|
// }
|
||||||
@@ -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<IAccountType>}
|
||||||
|
*/
|
||||||
|
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<IBillPayment>}
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
@@ -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<BillPayment, 'entries'>;
|
||||||
|
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<IBillReceivePageEntry[]> {
|
||||||
|
// 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<IBillPayment>}
|
||||||
|
*/
|
||||||
|
public async transformDTOToModel(
|
||||||
|
billPaymentDTO: IBillPaymentDTO,
|
||||||
|
vendor: Vendor,
|
||||||
|
oldBillPayment?: BillPayment,
|
||||||
|
): Promise<BillPayment> {
|
||||||
|
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<BillPayment>)(
|
||||||
|
initialDTO,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<IBillPayment> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<BillPayment> {
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
50
packages/server-nest/src/modules/BillPayments/constants.ts
Normal file
50
packages/server-nest/src/modules/BillPayments/constants.ts
Normal file
@@ -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,
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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<BillPayment>}
|
||||||
|
*/
|
||||||
|
public async getBillPayment(billPyamentId: number): Promise<BillPayment> {
|
||||||
|
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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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',
|
||||||
|
}
|
||||||
105
packages/server-nest/src/modules/Bills/Bills.application.ts
Normal file
105
packages/server-nest/src/modules/Bills/Bills.application.ts
Normal file
@@ -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<void>}
|
||||||
|
*/
|
||||||
|
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<void>}
|
||||||
|
*/
|
||||||
|
public openBill(billId: number): Promise<void> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
123
packages/server-nest/src/modules/Bills/Bills.constants.ts
Normal file
123
packages/server-nest/src/modules/Bills/Bills.constants.ts
Normal file
@@ -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.',
|
||||||
|
},
|
||||||
|
];
|
||||||
17
packages/server-nest/src/modules/Bills/Bills.module.ts
Normal file
17
packages/server-nest/src/modules/Bills/Bills.module.ts
Normal file
@@ -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 {}
|
||||||
112
packages/server-nest/src/modules/Bills/Bills.types.ts
Normal file
112
packages/server-nest/src/modules/Bills/Bills.types.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
288
packages/server-nest/src/modules/Bills/commands/BillGLEntries.ts
Normal file
288
packages/server-nest/src/modules/Bills/commands/BillGLEntries.ts
Normal file
@@ -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);
|
||||||
|
// };
|
||||||
|
// }
|
||||||
@@ -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);
|
||||||
|
// };
|
||||||
|
// }
|
||||||
@@ -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<void>}
|
||||||
|
// */
|
||||||
|
// public async recordInventoryTransactions(
|
||||||
|
// tenantId: number,
|
||||||
|
// billId: number,
|
||||||
|
// override?: boolean,
|
||||||
|
// trx?: Knex.Transaction
|
||||||
|
// ): Promise<void> {
|
||||||
|
// 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<void>}
|
||||||
|
// */
|
||||||
|
// 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
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// }
|
||||||
@@ -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<void>}
|
||||||
|
// */
|
||||||
|
// public rewritePaymentsGLEntriesTask = async ({
|
||||||
|
// tenantId,
|
||||||
|
// paymentId,
|
||||||
|
// trx,
|
||||||
|
// }) => {
|
||||||
|
// await this.paymentGLEntries.rewritePaymentGLEntries(
|
||||||
|
// tenantId,
|
||||||
|
// paymentId,
|
||||||
|
// trx
|
||||||
|
// );
|
||||||
|
// };
|
||||||
|
// }
|
||||||
@@ -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
|
||||||
|
// );
|
||||||
|
// };
|
||||||
|
// }
|
||||||
@@ -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);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
@@ -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;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<IBill>}
|
||||||
|
*/
|
||||||
|
public async createBill(
|
||||||
|
billDTO: IBillDTO,
|
||||||
|
trx?: Knex.Transaction,
|
||||||
|
): Promise<Bill> {
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<IBill>}
|
||||||
|
*/
|
||||||
|
public async editBill(
|
||||||
|
billId: number,
|
||||||
|
billDTO: IBillEditDTO,
|
||||||
|
): Promise<Bill> {
|
||||||
|
// 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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<void>}
|
||||||
|
*/
|
||||||
|
public async openBill(billId: number): Promise<void> {
|
||||||
|
// 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
585
packages/server-nest/src/modules/Bills/models/Bill.ts
Normal file
585
packages/server-nest/src/modules/Bills/models/Bill.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
// };
|
||||||
|
}
|
||||||
38
packages/server-nest/src/modules/Bills/queries/GetBill.ts
Normal file
38
packages/server-nest/src/modules/Bills/queries/GetBill.ts
Normal file
@@ -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<IBill>}
|
||||||
|
*/
|
||||||
|
public async getBill(billId: number): Promise<Bill> {
|
||||||
|
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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
72
packages/server-nest/src/modules/Bills/queries/GetBills.ts
Normal file
72
packages/server-nest/src/modules/Bills/queries/GetBills.ts
Normal file
@@ -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);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
@@ -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<Bill[]> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<void>]>}
|
||||||
|
*/
|
||||||
|
async writeTempHtmlFile(
|
||||||
|
content: string,
|
||||||
|
): Promise<[string, () => Promise<void>]> {
|
||||||
|
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<Buffer>}
|
||||||
|
*/
|
||||||
|
async convert(
|
||||||
|
html: string,
|
||||||
|
properties?: PageProperties,
|
||||||
|
pdfFormat?: PdfFormat,
|
||||||
|
): Promise<Buffer> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {}
|
||||||
@@ -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<Buffer>}
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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'];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
14
packages/server-nest/src/modules/ChromiumlyTenancy/utils.ts
Normal file
14
packages/server-nest/src/modules/ChromiumlyTenancy/utils.ts
Normal file
@@ -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);
|
||||||
|
};
|
||||||
@@ -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)
|
||||||
|
: '';
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
117
packages/server-nest/src/modules/Customers/CustomerGLEntries.ts
Normal file
117
packages/server-nest/src/modules/Customers/CustomerGLEntries.ts
Normal file
@@ -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);
|
||||||
|
// };
|
||||||
|
// }
|
||||||
@@ -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);
|
||||||
|
// };
|
||||||
|
// }
|
||||||
@@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {}
|
||||||
@@ -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<ICustomer>}
|
||||||
|
*/
|
||||||
|
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<ICustomer>}
|
||||||
|
*/
|
||||||
|
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<void>}
|
||||||
|
*/
|
||||||
|
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<ICustomer>}
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
// };
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
@@ -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<void>}
|
||||||
|
// */
|
||||||
|
// public async importable(
|
||||||
|
// tenantId: number,
|
||||||
|
// createDTO: ICustomerNewDTO,
|
||||||
|
// trx?: Knex.Transaction<any, any[]>
|
||||||
|
// ): Promise<void> {
|
||||||
|
// await this.createCustomerService.createCustomer(tenantId, createDTO, trx);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Retrieves the sample data of customers used to download sample sheet.
|
||||||
|
// */
|
||||||
|
// public sampleData(): any[] {
|
||||||
|
// return CustomersSampleData;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
158
packages/server-nest/src/modules/Customers/_SampleData.ts
Normal file
158
packages/server-nest/src/modules/Customers/_SampleData.ts
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -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<void>}
|
||||||
|
*/
|
||||||
|
public async activateCustomer(customerId: number): Promise<void> {
|
||||||
|
// 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<ICustomer>}
|
||||||
|
*/
|
||||||
|
public async createCustomer(
|
||||||
|
customerDTO: ICustomerNewDTO,
|
||||||
|
trx?: Knex.Transaction,
|
||||||
|
): Promise<Customer> {
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<Customer> => {
|
||||||
|
return {
|
||||||
|
...omit(customerDTO, ['customerType']),
|
||||||
|
contactType: customerDTO.customerType,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transformes the create DTO.
|
||||||
|
* @param {ICustomerNewDTO} customerDTO
|
||||||
|
* @returns {Promise<Partial<Customer>>}
|
||||||
|
*/
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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<void>}
|
||||||
|
*/
|
||||||
|
public async deleteCustomer(
|
||||||
|
customerId: number,
|
||||||
|
): Promise<void> {
|
||||||
|
// 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<ICustomer>}
|
||||||
|
*/
|
||||||
|
public async editCustomer(
|
||||||
|
customerId: number,
|
||||||
|
customerDTO: ICustomerEditDTO,
|
||||||
|
): Promise<Customer> {
|
||||||
|
// 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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<Customer> {
|
||||||
|
// 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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(),
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
// }
|
||||||
27
packages/server-nest/src/modules/Customers/constants.ts
Normal file
27
packages/server-nest/src/modules/Customers/constants.ts
Normal file
@@ -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',
|
||||||
|
};
|
||||||
@@ -28,8 +28,9 @@ export class Customer extends BaseModel{
|
|||||||
currencyCode: string;
|
currencyCode: string;
|
||||||
|
|
||||||
openingBalance: number;
|
openingBalance: number;
|
||||||
openingBalanceAt: Date;
|
openingBalanceAt: Date | string;
|
||||||
openingBalanceExchangeRate: number;
|
openingBalanceExchangeRate: number;
|
||||||
|
openingBalanceBranchId?: number;
|
||||||
|
|
||||||
salutation?: string;
|
salutation?: string;
|
||||||
firstName?: string;
|
firstName?: string;
|
||||||
|
|||||||
@@ -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] || '');
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
// }
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -213,8 +213,7 @@ export class ItemsEntriesService {
|
|||||||
/**
|
/**
|
||||||
* Sets the cost/sell accounts to the invoice entries.
|
* Sets the cost/sell accounts to the invoice entries.
|
||||||
*/
|
*/
|
||||||
public setItemsEntriesDefaultAccounts() {
|
public async setItemsEntriesDefaultAccounts(entries: ItemEntry[]) {
|
||||||
return async (entries: ItemEntry[]) => {
|
|
||||||
const entriesItemsIds = entries.map((e) => e.itemId);
|
const entriesItemsIds = entries.map((e) => e.itemId);
|
||||||
const items = await this.itemModel.query().whereIn('id', entriesItemsIds);
|
const items = await this.itemModel.query().whereIn('id', entriesItemsIds);
|
||||||
|
|
||||||
@@ -229,7 +228,6 @@ export class ItemsEntriesService {
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ export class ItemEntry extends BaseModel {
|
|||||||
public quantity: number;
|
public quantity: number;
|
||||||
public rate: number;
|
public rate: number;
|
||||||
public isInclusiveTax: number;
|
public isInclusiveTax: number;
|
||||||
|
public itemId: number;
|
||||||
|
public costAccountId: number;
|
||||||
|
public taxRateId: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Table name.
|
* Table name.
|
||||||
|
|||||||
@@ -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'];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<void>}
|
||||||
|
*/
|
||||||
|
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<IPaymentReceived>}
|
||||||
|
*/
|
||||||
|
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<IPaymentReceivedSmsDetails> => {
|
||||||
|
// 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<void>}
|
||||||
|
*/
|
||||||
|
// public notifyPaymentByMail(
|
||||||
|
// tenantId: number,
|
||||||
|
// paymentReceiveId: number,
|
||||||
|
// messageOpts: PaymentReceiveMailOptsDTO,
|
||||||
|
// ): Promise<void> {
|
||||||
|
// 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<void>}
|
||||||
|
*/
|
||||||
|
// 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<IPaymentReceivedState>}
|
||||||
|
*/
|
||||||
|
public getPaymentReceivedState = () => {
|
||||||
|
return this.getPaymentReceivedStateService.getPaymentReceivedState();
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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<void>}
|
||||||
|
// */
|
||||||
|
// public writePaymentGLEntries = async (
|
||||||
|
// tenantId: number,
|
||||||
|
// paymentReceiveId: number,
|
||||||
|
// trx?: Knex.Transaction
|
||||||
|
// ): Promise<void> => {
|
||||||
|
// const { PaymentReceive } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
// // Retrieves the given tenant metadata.
|
||||||
|
// const tenantMeta = await TenantMetadata.query().findOne({ tenantId });
|
||||||
|
|
||||||
|
// // Retrieves the payment receive with associated entries.
|
||||||
|
// const paymentReceive = await PaymentReceive.query(trx)
|
||||||
|
// .findById(paymentReceiveId)
|
||||||
|
// .withGraphFetched('entries.invoice');
|
||||||
|
|
||||||
|
// // 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<Ledger> => {
|
||||||
|
// 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<ILedgerEntry>}
|
||||||
|
// */
|
||||||
|
// 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];
|
||||||
|
// };
|
||||||
|
// }
|
||||||
@@ -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<void>}
|
||||||
|
// */
|
||||||
|
// public async triggerMail(
|
||||||
|
// paymentReceiveId: number,
|
||||||
|
// messageDTO: PaymentReceiveMailOptsDTO,
|
||||||
|
// ): Promise<void> {
|
||||||
|
// 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<PaymentReceiveMailOpts>}
|
||||||
|
// */
|
||||||
|
// public getMailOptions = async (
|
||||||
|
// paymentId: number,
|
||||||
|
// ): Promise<PaymentReceiveMailOpts> => {
|
||||||
|
// 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<Record<string, string>>}
|
||||||
|
// */
|
||||||
|
// public textFormatter = async (
|
||||||
|
// invoiceId: number,
|
||||||
|
// ): Promise<Record<string, string>> => {
|
||||||
|
// 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<PaymentReceiveMailOpts>}
|
||||||
|
// */
|
||||||
|
// 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<void>}
|
||||||
|
// */
|
||||||
|
// public async sendMail(
|
||||||
|
// paymentReceiveId: number,
|
||||||
|
// messageDTO: SendInvoiceMailDTO,
|
||||||
|
// ): Promise<void> {
|
||||||
|
// // 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,
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// }
|
||||||
@@ -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);
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
// }
|
||||||
@@ -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<void>}
|
||||||
|
// */
|
||||||
|
// public notifyViaSmsNotificationAfterCreation = async (
|
||||||
|
// tenantId: number,
|
||||||
|
// paymentReceiveId: number
|
||||||
|
// ): Promise<void> => {
|
||||||
|
// 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<IPaymentReceivedSmsDetails> => {
|
||||||
|
// 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,
|
||||||
|
// };
|
||||||
|
// };
|
||||||
|
// }
|
||||||
@@ -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
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// }
|
||||||
@@ -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);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user