refactor: wip to nestjs

This commit is contained in:
Ahmed Bouhuolia
2024-12-25 00:43:55 +02:00
parent 336171081e
commit a6932d76f3
249 changed files with 21314 additions and 1616 deletions

View 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);
// };
// }

View File

@@ -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);
// };
// }

View File

@@ -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,
);
}
}

View File

@@ -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 {}

View File

@@ -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);
// };
}

View File

@@ -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);
// }
// }

View File

@@ -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;
// }
// }

View 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"
}
]

View File

@@ -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);
});
}
}

View File

@@ -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);
}
}

View File

@@ -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,
};
};
}

View File

@@ -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);
}
};
}

View File

@@ -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);
});
}
}

View File

@@ -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;
});
}
}

View File

@@ -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;
});
}
}

View File

@@ -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()
);
}
}

View File

@@ -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(),
// };
// }
// }

View 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',
};

View File

@@ -28,8 +28,9 @@ export class Customer extends BaseModel{
currencyCode: string;
openingBalance: number;
openingBalanceAt: Date;
openingBalanceAt: Date | string;
openingBalanceExchangeRate: number;
openingBalanceBranchId?: number;
salutation?: string;
firstName?: string;

View File

@@ -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] || '');
};
}

View File

@@ -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
// );
// }
// };
// }

View File

@@ -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;
}