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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,46 @@
// import { Inject, Service } from 'typedi';
// import { Knex } from 'knex';
// import { IPaymentReceivedCreateDTO } from '@/interfaces';
// import { Importable } from '@/services/Import/Importable';
// import { CreatePaymentReceived } from './commands/CreatePaymentReceived.serivce';
// import { PaymentsReceiveSampleData } from './constants';
// @Service()
// export class PaymentsReceivedImportable extends Importable {
// @Inject()
// private createPaymentReceiveService: CreatePaymentReceived;
// /**
// * Importing to account service.
// * @param {number} tenantId
// * @param {IAccountCreateDTO} createAccountDTO
// * @returns
// */
// public importable(
// tenantId: number,
// createPaymentDTO: IPaymentReceivedCreateDTO,
// trx?: Knex.Transaction
// ) {
// return this.createPaymentReceiveService.createPaymentReceived(
// tenantId,
// createPaymentDTO,
// {},
// trx
// );
// }
// /**
// * Concurrrency controlling of the importing process.
// * @returns {number}
// */
// public get concurrency() {
// return 1;
// }
// /**
// * Retrieves the sample data that used to download accounts sample sheet.
// */
// public sampleData(): any[] {
// return PaymentsReceiveSampleData;
// }
// }

View File

@@ -0,0 +1,121 @@
import { Knex } from 'knex';
import {
IPaymentReceivedCreateDTO,
IPaymentReceivedCreatedPayload,
IPaymentReceivedCreatingPayload,
} from '../types/PaymentReceived.types';
import { PaymentReceivedValidators } from './PaymentReceivedValidators.service';
import { PaymentReceiveDTOTransformer } from './PaymentReceivedDTOTransformer';
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { PaymentReceived } from '../models/PaymentReceived';
import { events } from '@/common/events/events';
import { Customer } from '@/modules/Customers/models/Customer';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
import { Inject, Injectable } from '@nestjs/common';
@Injectable()
export class CreatePaymentReceivedService {
constructor(
private validators: PaymentReceivedValidators,
private eventPublisher: EventEmitter2,
private uow: UnitOfWork,
private transformer: PaymentReceiveDTOTransformer,
private tenancyContext: TenancyContext,
@Inject(PaymentReceived.name)
private paymentReceived: typeof PaymentReceived,
@Inject(Customer.name)
private customer: typeof Customer,
) {}
/**
* Creates a new payment receive and store it to the storage
* with associated invoices payment and journal transactions.
* @param {IPaymentReceivedCreateDTO} paymentReceiveDTO
* @param {Knex.Transaction} trx - Database transaction.
*/
public async createPaymentReceived(
paymentReceiveDTO: IPaymentReceivedCreateDTO,
trx?: Knex.Transaction,
) {
const tenant = await this.tenancyContext.getTenant(true);
// Validate customer existance.
const paymentCustomer = await this.customer
.query()
.modify('customer')
.findById(paymentReceiveDTO.customerId)
.throwIfNotFound();
// Transformes the payment receive DTO to model.
const paymentReceiveObj = await this.transformCreateDTOToModel(
paymentCustomer,
paymentReceiveDTO,
);
// Validate payment receive number uniquiness.
await this.validators.validatePaymentReceiveNoExistance(
paymentReceiveObj.paymentReceiveNo,
);
// Validate the deposit account existance and type.
const depositAccount = await this.validators.getDepositAccountOrThrowError(
paymentReceiveDTO.depositAccountId,
);
// Validate payment receive invoices IDs existance.
await this.validators.validateInvoicesIDsExistance(
paymentReceiveDTO.customerId,
paymentReceiveDTO.entries,
);
// Validate invoice payment amount.
await this.validators.validateInvoicesPaymentsAmount(
paymentReceiveDTO.entries,
);
// Validates the payment account currency code.
this.validators.validatePaymentAccountCurrency(
depositAccount.currencyCode,
paymentCustomer.currencyCode,
tenant?.metadata.baseCurrency,
);
// Creates a payment receive transaction under UOW envirment.
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
// Triggers `onPaymentReceiveCreating` event.
await this.eventPublisher.emitAsync(events.paymentReceive.onCreating, {
trx,
paymentReceiveDTO,
} as IPaymentReceivedCreatingPayload);
// Inserts the payment receive transaction.
const paymentReceive = await this.paymentReceived
.query(trx)
.insertGraphAndFetch({
...paymentReceiveObj,
});
// Triggers `onPaymentReceiveCreated` event.
await this.eventPublisher.emitAsync(events.paymentReceive.onCreated, {
paymentReceive,
paymentReceiveId: paymentReceive.id,
paymentReceiveDTO,
trx,
} as IPaymentReceivedCreatedPayload);
return paymentReceive;
}, trx);
}
/**
* Transform the create payment receive DTO.
* @param {ICustomer} customer
* @param {IPaymentReceivedCreateDTO} paymentReceiveDTO
* @returns
*/
private transformCreateDTOToModel = async (
customer: Customer,
paymentReceiveDTO: IPaymentReceivedCreateDTO,
) => {
return this.transformer.transformPaymentReceiveDTOToModel(
customer,
paymentReceiveDTO,
);
};
}

View File

@@ -0,0 +1,73 @@
import { Inject, Injectable } from '@nestjs/common';
import { Knex } from 'knex';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
import { PaymentReceived } from '../models/PaymentReceived';
import { PaymentReceivedEntry } from '../models/PaymentReceivedEntry';
import { events } from '@/common/events/events';
import { IPaymentReceivedDeletingPayload } from '../types/PaymentReceived.types';
import { IPaymentReceivedDeletedPayload } from '../types/PaymentReceived.types';
@Injectable()
export class DeletePaymentReceived {
constructor(
private eventPublisher: EventEmitter2,
private uow: UnitOfWork,
@Inject(PaymentReceived.name)
private paymentReceiveModel: typeof PaymentReceived,
@Inject(PaymentReceivedEntry.name)
private paymentReceiveEntryModel: typeof PaymentReceivedEntry,
) {}
/**
* Deletes the given payment receive with associated entries
* and journal transactions.
* -----
* - Deletes the payment receive transaction.
* - Deletes the payment receive associated entries.
* - Deletes the payment receive associated journal transactions.
* - Revert the customer balance.
* - Revert the payment amount of the associated invoices.
* @async
* @param {Integer} paymentReceiveId - Payment receive id.
* @param {IPaymentReceived} paymentReceive - Payment receive object.
*/
public async deletePaymentReceive(paymentReceiveId: number) {
// Retreive payment receive or throw not found service error.
const oldPaymentReceive = await this.paymentReceiveModel
.query()
.withGraphFetched('entries')
.findById(paymentReceiveId)
.throwIfNotFound();
// Delete payment receive transaction and associate transactions under UOW env.
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
// Triggers `onPaymentReceiveDeleting` event.
await this.eventPublisher.emitAsync(events.paymentReceive.onDeleting, {
oldPaymentReceive,
trx,
} as IPaymentReceivedDeletingPayload);
// Deletes the payment receive associated entries.
await this.paymentReceiveEntryModel
.query(trx)
.where('payment_receive_id', paymentReceiveId)
.delete();
// Deletes the payment receive transaction.
await this.paymentReceiveModel
.query(trx)
.findById(paymentReceiveId)
.delete();
// Triggers `onPaymentReceiveDeleted` event.
await this.eventPublisher.emitAsync(events.paymentReceive.onDeleted, {
paymentReceiveId,
oldPaymentReceive,
trx,
} as IPaymentReceivedDeletedPayload);
});
}
}

View File

@@ -0,0 +1,160 @@
import { Inject, Injectable } from '@nestjs/common';
import { Knex } from 'knex';
import { EventEmitter2 } from '@nestjs/event-emitter';
import {
IPaymentReceivedEditDTO,
IPaymentReceivedEditedPayload,
IPaymentReceivedEditingPayload,
} from '../types/PaymentReceived.types';
import { PaymentReceiveDTOTransformer } from './PaymentReceivedDTOTransformer';
import { PaymentReceivedValidators } from './PaymentReceivedValidators.service';
import { PaymentReceived } from '../models/PaymentReceived';
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
import { events } from '@/common/events/events';
import { Customer } from '@/modules/Customers/models/Customer';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
@Injectable()
export class EditPaymentReceived {
constructor(
private readonly transformer: PaymentReceiveDTOTransformer,
private readonly validators: PaymentReceivedValidators,
private readonly eventPublisher: EventEmitter2,
private readonly uow: UnitOfWork,
private readonly tenancyContext: TenancyContext,
@Inject(PaymentReceived)
private readonly paymentReceiveModel: typeof PaymentReceived,
@Inject(Customer.name)
private readonly customerModel: typeof Customer,
) {}
/**
* Edit details the given payment receive with associated entries.
* ------
* - Update the payment receive transactions.
* - Insert the new payment receive entries.
* - Update the given payment receive entries.
* - Delete the not presented payment receive entries.
* - Re-insert the journal transactions and update the different accounts balance.
* - Update the different customer balances.
* - Update the different invoice payment amount.
* @async
* @param {number} paymentReceiveId -
* @param {IPaymentReceivedEditDTO} paymentReceiveDTO -
*/
public async editPaymentReceive(
paymentReceiveId: number,
paymentReceiveDTO: IPaymentReceivedEditDTO,
) {
const tenant = await this.tenancyContext.getTenant(true);
// Validate the payment receive existance.
const oldPaymentReceive = await this.paymentReceiveModel
.query()
.withGraphFetched('entries')
.findById(paymentReceiveId)
.throwIfNotFound();
// Validates the payment existance.
this.validators.validatePaymentExistance(oldPaymentReceive);
// Validate customer existance.
const customer = await this.customerModel
.query()
.findById(paymentReceiveDTO.customerId)
.throwIfNotFound();
// Transformes the payment receive DTO to model.
const paymentReceiveObj = await this.transformEditDTOToModel(
customer,
paymentReceiveDTO,
oldPaymentReceive,
);
// Validate customer whether modified.
this.validators.validateCustomerNotModified(
paymentReceiveDTO,
oldPaymentReceive,
);
// Validate payment receive number uniquiness.
if (paymentReceiveDTO.paymentReceiveNo) {
await this.validators.validatePaymentReceiveNoExistance(
paymentReceiveDTO.paymentReceiveNo,
paymentReceiveId,
);
}
// Validate the deposit account existance and type.
const depositAccount = await this.validators.getDepositAccountOrThrowError(
paymentReceiveDTO.depositAccountId,
);
// Validate the entries ids existance on payment receive type.
await this.validators.validateEntriesIdsExistance(
paymentReceiveId,
paymentReceiveDTO.entries,
);
// Validate payment receive invoices IDs existance and associated
// to the given customer id.
await this.validators.validateInvoicesIDsExistance(
oldPaymentReceive.customerId,
paymentReceiveDTO.entries,
);
// Validate invoice payment amount.
await this.validators.validateInvoicesPaymentsAmount(
paymentReceiveDTO.entries,
oldPaymentReceive.entries,
);
// Validates the payment account currency code.
this.validators.validatePaymentAccountCurrency(
depositAccount.currencyCode,
customer.currencyCode,
tenant?.metadata.baseCurrency,
);
// Creates payment receive transaction under UOW envirement.
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
// Triggers `onPaymentReceiveEditing` event.
await this.eventPublisher.emitAsync(events.paymentReceive.onEditing, {
trx,
oldPaymentReceive,
paymentReceiveDTO,
} as IPaymentReceivedEditingPayload);
// Update the payment receive transaction.
const paymentReceive = await this.paymentReceiveModel
.query(trx)
.upsertGraphAndFetch({
id: paymentReceiveId,
...paymentReceiveObj,
});
// Triggers `onPaymentReceiveEdited` event.
await this.eventPublisher.emitAsync(events.paymentReceive.onEdited, {
paymentReceiveId,
paymentReceive,
oldPaymentReceive,
paymentReceiveDTO,
trx,
} as IPaymentReceivedEditedPayload);
return paymentReceive;
});
}
/**
* Transform the edit payment receive DTO.
* @param {ICustomer} customer
* @param {IPaymentReceivedEditDTO} paymentReceiveDTO
* @param {IPaymentReceived} oldPaymentReceive
* @returns
*/
private transformEditDTOToModel = async (
customer: Customer,
paymentReceiveDTO: IPaymentReceivedEditDTO,
oldPaymentReceive: PaymentReceived,
) => {
return this.transformer.transformPaymentReceiveDTOToModel(
customer,
paymentReceiveDTO,
oldPaymentReceive,
);
};
}

View File

@@ -0,0 +1,83 @@
import * as R from 'ramda';
import { Inject, Injectable } from '@nestjs/common';
import { omit, sumBy } from 'lodash';
import composeAsync from 'async/compose';
import {
IPaymentReceivedCreateDTO,
IPaymentReceivedEditDTO,
} from '../types/PaymentReceived.types';
import { PaymentReceivedValidators } from './PaymentReceivedValidators.service';
import { PaymentReceivedIncrement } from './PaymentReceivedIncrement.service';
import { BranchTransactionDTOTransformer } from '@/modules/Branches/integrations/BranchTransactionDTOTransform';
import { BrandingTemplateDTOTransformer } from '@/modules/PdfTemplate/BrandingTemplateDTOTransformer';
import { PaymentReceived } from '../models/PaymentReceived';
import { assocItemEntriesDefaultIndex } from '@/utils/associate-item-entries-index';
import { Customer } from '@/modules/Customers/models/Customer';
import { formatDateFields } from '@/utils/format-date-fields';
@Injectable()
export class PaymentReceiveDTOTransformer {
constructor(
private readonly validators: PaymentReceivedValidators,
private readonly increments: PaymentReceivedIncrement,
private readonly branchDTOTransform: BranchTransactionDTOTransformer,
private readonly brandingTemplatesTransformer: BrandingTemplateDTOTransformer,
@Inject(PaymentReceived.name)
private readonly paymentReceivedModel: typeof PaymentReceived,
) {}
/**
* Transformes the create payment receive DTO to model object.
* @param {IPaymentReceivedCreateDTO|IPaymentReceivedEditDTO} paymentReceiveDTO - Payment receive DTO.
* @param {IPaymentReceived} oldPaymentReceive -
* @return {IPaymentReceived}
*/
public async transformPaymentReceiveDTOToModel(
customer: Customer,
paymentReceiveDTO: IPaymentReceivedCreateDTO | IPaymentReceivedEditDTO,
oldPaymentReceive?: PaymentReceived
): Promise<PaymentReceived> {
const amount =
paymentReceiveDTO.amount ??
sumBy(paymentReceiveDTO.entries, 'paymentAmount');
// Retreive the next invoice number.
const autoNextNumber =
this.increments.getNextPaymentReceiveNumber();
// Retrieve the next payment receive number.
const paymentReceiveNo =
paymentReceiveDTO.paymentReceiveNo ||
oldPaymentReceive?.paymentReceiveNo ||
autoNextNumber;
this.validators.validatePaymentNoRequire(paymentReceiveNo);
const entries = R.compose(
// Associate the default index to each item entry line.
assocItemEntriesDefaultIndex
)(paymentReceiveDTO.entries);
const initialDTO = {
...formatDateFields(omit(paymentReceiveDTO, ['entries', 'attachments']), [
'paymentDate',
]),
amount,
currencyCode: customer.currencyCode,
...(paymentReceiveNo ? { paymentReceiveNo } : {}),
exchangeRate: paymentReceiveDTO.exchangeRate || 1,
entries,
};
const initialAsyncDTO = await composeAsync(
// Assigns the default branding template id to the invoice DTO.
this.brandingTemplatesTransformer.assocDefaultBrandingTemplate(
'SaleInvoice'
)
)(initialDTO);
return R.compose(
this.branchDTOTransform.transformDTO<PaymentReceived>
)(initialAsyncDTO);
}
}

View File

@@ -0,0 +1,31 @@
import { Injectable } from '@nestjs/common';
import { AutoIncrementOrdersService } from '@/modules/AutoIncrementOrders/AutoIncrementOrders.service';
@Injectable()
export class PaymentReceivedIncrement {
/**
* @param {AutoIncrementOrdersService} autoIncrementOrdersService - Auto increment orders service.
*/
constructor(
private readonly autoIncrementOrdersService: AutoIncrementOrdersService,
) {}
/**
* Retrieve the next unique payment receive number.
* @return {string}
*/
public getNextPaymentReceiveNumber(): string {
return this.autoIncrementOrdersService.getNextTransactionNumber(
'payment_receives',
);
}
/**
* Increment the payment receive next number.
*/
public incrementNextPaymentReceiveNumber() {
return this.autoIncrementOrdersService.incrementSettingsNextNumber(
'payment_receives',
);
}
}

View File

@@ -0,0 +1,46 @@
import { Inject, Injectable } from '@nestjs/common';
import { Knex } from 'knex';
import { SaleInvoice } from '../../SaleInvoices/models/SaleInvoice';
import { IPaymentReceivedEntryDTO } from '../types/PaymentReceived.types';
import { entriesAmountDiff } from '@/utils/entries-amount-diff';
@Injectable()
export class PaymentReceivedInvoiceSync {
constructor(
@Inject(SaleInvoice.name)
private readonly saleInvoiceModel: typeof SaleInvoice,
) {}
/**
* Saves difference changing between old and new invoice payment amount.
* @param {Array} paymentReceiveEntries
* @param {Array} newPaymentReceiveEntries
* @return {Promise<void>}
*/
public async saveChangeInvoicePaymentAmount(
newPaymentReceiveEntries: IPaymentReceivedEntryDTO[],
oldPaymentReceiveEntries?: IPaymentReceivedEntryDTO[],
trx?: Knex.Transaction
): Promise<void> {
const opers: Promise<void>[] = [];
const diffEntries = entriesAmountDiff(
newPaymentReceiveEntries,
oldPaymentReceiveEntries,
'paymentAmount',
'invoiceId'
);
diffEntries.forEach((diffEntry: any) => {
if (diffEntry.paymentAmount === 0) {
return;
}
const oper = this.saleInvoiceModel.changePaymentAmount(
diffEntry.invoiceId,
diffEntry.paymentAmount,
trx
);
opers.push(oper);
});
await Promise.all([...opers]);
}
}

View File

@@ -0,0 +1,277 @@
import { Inject, Injectable } from '@nestjs/common';
import { difference, sumBy } from 'lodash';
import {
IPaymentReceivedEditDTO,
IPaymentReceivedEntryDTO,
} from '../types/PaymentReceived.types';
import { ERRORS } from '../constants';
import { PaymentReceived } from '../models/PaymentReceived';
import { PaymentReceivedEntry } from '../models/PaymentReceivedEntry';
import { Account } from '@/modules/Accounts/models/Account.model';
import { SaleInvoice } from '@/modules/SaleInvoices/models/SaleInvoice';
import { ServiceError } from '@/modules/Items/ServiceError';
import { ACCOUNT_TYPE } from '@/constants/accounts';
@Injectable()
export class PaymentReceivedValidators {
constructor(
@Inject(PaymentReceived.name)
private readonly paymentReceiveModel: typeof PaymentReceived,
@Inject(PaymentReceivedEntry.name)
private readonly paymentReceiveEntryModel: typeof PaymentReceivedEntry,
@Inject(SaleInvoice.name)
private readonly saleInvoiceModel: typeof SaleInvoice,
@Inject(Account.name)
private readonly accountModel: typeof Account,
) {}
/**
* Validates the payment existance.
* @param {PaymentReceive | null | undefined} payment
*/
public validatePaymentExistance(payment: PaymentReceived | null | undefined) {
if (!payment) {
throw new ServiceError(ERRORS.PAYMENT_RECEIVE_NOT_EXISTS);
}
}
/**
* Validates the payment receive number existance.
* @param {number} tenantId -
* @param {string} paymentReceiveNo -
*/
public async validatePaymentReceiveNoExistance(
paymentReceiveNo: string,
notPaymentReceiveId?: number
): Promise<void> {
const paymentReceive = await this.paymentReceiveModel.query()
.findOne('payment_receive_no', paymentReceiveNo)
.onBuild((builder) => {
if (notPaymentReceiveId) {
builder.whereNot('id', notPaymentReceiveId);
}
});
if (paymentReceive) {
throw new ServiceError(ERRORS.PAYMENT_RECEIVE_NO_EXISTS);
}
}
/**
* Validates the invoices IDs existance.
* @param {number} customerId -
* @param {IPaymentReceivedEntryDTO[]} paymentReceiveEntries -
*/
public async validateInvoicesIDsExistance(
customerId: number,
paymentReceiveEntries: { invoiceId: number }[]
): Promise<SaleInvoice[]> {
const invoicesIds = paymentReceiveEntries.map(
(e: { invoiceId: number }) => e.invoiceId
);
const storedInvoices = await this.saleInvoiceModel.query()
.whereIn('id', invoicesIds)
.where('customer_id', customerId);
const storedInvoicesIds = storedInvoices.map((invoice) => invoice.id);
const notFoundInvoicesIDs = difference(invoicesIds, storedInvoicesIds);
if (notFoundInvoicesIDs.length > 0) {
throw new ServiceError(ERRORS.INVOICES_IDS_NOT_FOUND);
}
// Filters the not delivered invoices.
const notDeliveredInvoices = storedInvoices.filter(
(invoice) => !invoice.isDelivered
);
if (notDeliveredInvoices.length > 0) {
throw new ServiceError(ERRORS.INVOICES_NOT_DELIVERED_YET, null, {
notDeliveredInvoices,
});
}
return storedInvoices;
}
/**
* Validates entries invoice payment amount.
* @param {IPaymentReceivedEntryDTO[]} paymentReceiveEntries
* @param {IPaymentReceivedEntry[]} oldPaymentEntries
*/
public async validateInvoicesPaymentsAmount(
paymentReceiveEntries: IPaymentReceivedEntryDTO[],
oldPaymentEntries: PaymentReceivedEntry[] = []
) {
const invoicesIds = paymentReceiveEntries.map(
(e: IPaymentReceivedEntryDTO) => e.invoiceId
);
const storedInvoices = await this.saleInvoiceModel.query().whereIn('id', invoicesIds);
const storedInvoicesMap = new Map(
storedInvoices.map((invoice: SaleInvoice) => {
const oldEntries = oldPaymentEntries.filter((entry) => entry.invoiceId);
const oldPaymentAmount = sumBy(oldEntries, 'paymentAmount') || 0;
return [
invoice.id,
{ ...invoice, dueAmount: invoice.dueAmount + oldPaymentAmount },
];
})
);
const hasWrongPaymentAmount: any[] = [];
paymentReceiveEntries.forEach(
(entry: IPaymentReceivedEntryDTO, index: number) => {
const entryInvoice = storedInvoicesMap.get(entry.invoiceId);
const { dueAmount } = entryInvoice;
if (dueAmount < entry.paymentAmount) {
hasWrongPaymentAmount.push({ index, due_amount: dueAmount });
}
}
);
if (hasWrongPaymentAmount.length > 0) {
throw new ServiceError(ERRORS.INVALID_PAYMENT_AMOUNT);
}
}
/**
* Validate the payment receive number require.
* @param {IPaymentReceived} paymentReceiveObj
*/
public validatePaymentReceiveNoRequire(paymentReceiveObj: PaymentReceived) {
if (!paymentReceiveObj.paymentReceiveNo) {
throw new ServiceError(ERRORS.PAYMENT_RECEIVE_NO_IS_REQUIRED);
}
}
/**
* Validate the payment receive entries IDs existance.
* @param {number} paymentReceiveId
* @param {IPaymentReceivedEntryDTO[]} paymentReceiveEntries
*/
public async validateEntriesIdsExistance(
paymentReceiveId: number,
paymentReceiveEntries: IPaymentReceivedEntryDTO[]
) {
const entriesIds = paymentReceiveEntries
.filter((entry) => entry.id)
.map((entry) => entry.id);
const storedEntries = await this.paymentReceiveEntryModel.query().where(
'payment_receive_id',
paymentReceiveId
);
const storedEntriesIds = storedEntries.map((entry: any) => entry.id);
const notFoundEntriesIds = difference(entriesIds, storedEntriesIds);
if (notFoundEntriesIds.length > 0) {
throw new ServiceError(ERRORS.ENTRIES_IDS_NOT_EXISTS);
}
}
/**
* Validates the payment receive number require.
* @param {string} paymentReceiveNo
*/
public validatePaymentNoRequire(paymentReceiveNo: string) {
if (!paymentReceiveNo) {
throw new ServiceError(ERRORS.PAYMENT_RECEIVE_NO_REQUIRED);
}
}
/**
* Validate the payment customer whether modified.
* @param {IPaymentReceivedEditDTO} paymentReceiveDTO
* @param {IPaymentReceived} oldPaymentReceive
*/
public validateCustomerNotModified(
paymentReceiveDTO: IPaymentReceivedEditDTO,
oldPaymentReceive: PaymentReceived
) {
if (paymentReceiveDTO.customerId !== oldPaymentReceive.customerId) {
throw new ServiceError(ERRORS.PAYMENT_CUSTOMER_SHOULD_NOT_UPDATE);
}
}
/**
* Validates the payment account currency code. The deposit account curreny
* should be equals the customer currency code or the base currency.
* @param {string} paymentAccountCurrency
* @param {string} customerCurrency
* @param {string} baseCurrency
* @throws {ServiceError(ERRORS.PAYMENT_ACCOUNT_CURRENCY_INVALID)}
*/
public validatePaymentAccountCurrency = (
paymentAccountCurrency: string,
customerCurrency: string,
baseCurrency: string
) => {
if (
paymentAccountCurrency !== customerCurrency &&
paymentAccountCurrency !== baseCurrency
) {
throw new ServiceError(ERRORS.PAYMENT_ACCOUNT_CURRENCY_INVALID);
}
};
/**
* Validates the payment receive existance.
* @param {number} paymentReceiveId - Payment receive id.
*/
async getPaymentReceiveOrThrowError(
paymentReceiveId: number
): Promise<PaymentReceived> {
const paymentReceive = await this.paymentReceiveModel.query()
.withGraphFetched('entries')
.findById(paymentReceiveId);
if (!paymentReceive) {
throw new ServiceError(ERRORS.PAYMENT_RECEIVE_NOT_EXISTS);
}
return paymentReceive;
}
/**
* Validate the deposit account id existance.
* @param {number} depositAccountId - Deposit account id.
* @return {Promise<IAccount>}
*/
async getDepositAccountOrThrowError(
depositAccountId: number
): Promise<Account> {
const depositAccount = await this.accountModel.query().findById(depositAccountId);
if (!depositAccount) {
throw new ServiceError(ERRORS.DEPOSIT_ACCOUNT_NOT_FOUND);
}
// Detarmines whether the account is cash, bank or other current asset.
if (
!depositAccount.isAccountType([
ACCOUNT_TYPE.CASH,
ACCOUNT_TYPE.BANK,
ACCOUNT_TYPE.OTHER_CURRENT_ASSET,
])
) {
throw new ServiceError(ERRORS.DEPOSIT_ACCOUNT_INVALID_TYPE);
}
return depositAccount;
}
/**
* Validate the given customer has no payments receives.
* @param {number} customerId - Customer id.
*/
public async validateCustomerHasNoPayments(
customerId: number
) {
const paymentReceives = await this.paymentReceiveModel.query().where(
'customer_id',
customerId
);
if (paymentReceives.length > 0) {
throw new ServiceError(ERRORS.CUSTOMER_HAS_PAYMENT_RECEIVES);
}
}
}

View File

@@ -0,0 +1,97 @@
export const DEFAULT_PAYMENT_MAIL_SUBJECT =
'Payment Received for {Customer Name} from {Company Name}';
export const DEFAULT_PAYMENT_MAIL_CONTENT = `
<p>Dear {Customer Name}</p>
<p>Thank you for your payment. It was a pleasure doing business with you. We look forward to work together again!</p>
<p>
Payment Date : <strong>{Payment Date}</strong><br />
Amount : <strong>{Payment Amount}</strong></br />
</p>
<p>
<i>Regards</i><br />
<i>{Company Name}</i>
</p>
`;
export const ERRORS = {
PAYMENT_RECEIVE_NO_EXISTS: 'PAYMENT_RECEIVE_NO_EXISTS',
PAYMENT_RECEIVE_NOT_EXISTS: 'PAYMENT_RECEIVE_NOT_EXISTS',
DEPOSIT_ACCOUNT_NOT_FOUND: 'DEPOSIT_ACCOUNT_NOT_FOUND',
DEPOSIT_ACCOUNT_INVALID_TYPE: 'DEPOSIT_ACCOUNT_INVALID_TYPE',
INVALID_PAYMENT_AMOUNT: 'INVALID_PAYMENT_AMOUNT',
INVOICES_IDS_NOT_FOUND: 'INVOICES_IDS_NOT_FOUND',
ENTRIES_IDS_NOT_EXISTS: 'ENTRIES_IDS_NOT_EXISTS',
INVOICES_NOT_DELIVERED_YET: 'INVOICES_NOT_DELIVERED_YET',
PAYMENT_RECEIVE_NO_IS_REQUIRED: 'PAYMENT_RECEIVE_NO_IS_REQUIRED',
PAYMENT_RECEIVE_NO_REQUIRED: 'PAYMENT_RECEIVE_NO_REQUIRED',
PAYMENT_CUSTOMER_SHOULD_NOT_UPDATE: 'PAYMENT_CUSTOMER_SHOULD_NOT_UPDATE',
CUSTOMER_HAS_PAYMENT_RECEIVES: 'CUSTOMER_HAS_PAYMENT_RECEIVES',
PAYMENT_ACCOUNT_CURRENCY_INVALID: 'PAYMENT_ACCOUNT_CURRENCY_INVALID',
NO_INVOICE_CUSTOMER_EMAIL_ADDR: 'NO_INVOICE_CUSTOMER_EMAIL_ADDR',
};
export const DEFAULT_VIEWS = [];
export const PaymentsReceiveSampleData = [
{
Customer: 'Randall Kohler',
'Payment Date': '2024-10-10',
'Payment Receive No.': 'PAY-0001',
'Reference No.': 'REF-0001',
'Deposit Account': 'Petty Cash',
'Exchange Rate': '',
Statement: 'Totam optio quisquam qui.',
Invoice: 'INV-00001',
'Payment Amount': 850,
},
];
export const defaultPaymentReceivedPdfTemplateAttributes = {
// # Colors
primaryColor: '#000',
secondaryColor: '#000',
// # Company logo
showCompanyLogo: true,
companyLogoUri: '',
// # Company name
companyName: 'Bigcapital Technology, Inc.',
// # Customer address
showCustomerAddress: true,
customerAddress: '',
// # Company address
showCompanyAddress: true,
companyAddress: '',
billedToLabel: 'Billed To',
// Total
total: '$1000.00',
totalLabel: 'Total',
showTotal: true,
// Subtotal
subtotal: '1000/00',
subtotalLabel: 'Subtotal',
showSubtotal: true,
lines: [
{
invoiceNumber: 'INV-00001',
invoiceAmount: '$1000.00',
paidAmount: '$1000.00',
},
],
// Payment received number
showPaymentReceivedNumber: true,
paymentReceivedNumberLabel: 'Payment Number',
paymentReceivedNumebr: '346D3D40-0001',
// Payment date.
paymentReceivedDate: 'September 3, 2024',
showPaymentReceivedDate: true,
paymentReceivedDateLabel: 'Payment Date',
};

View File

@@ -0,0 +1,182 @@
import { Model, mixin } from 'objection';
// import TenantModel from 'models/TenantModel';
// import ModelSetting from './ModelSetting';
// import PaymentReceiveSettings from './PaymentReceive.Settings';
// import CustomViewBaseModel from './CustomViewBaseModel';
// import { DEFAULT_VIEWS } from '@/services/Sales/PaymentReceived/constants';
// import ModelSearchable from './ModelSearchable';
import { BaseModel } from '@/models/Model';
export class PaymentReceived extends BaseModel {
customerId: number;
paymentDate: string;
amount: number;
currencyCode: string;
referenceNo: string;
depositAccountId: number;
paymentReceiveNo: string;
exchangeRate: number;
statement: string;
userId: number;
branchId: number;
pdfTemplateId: number;
createdAt: string;
updatedAt: string;
/**
* Table name.
*/
static get tableName() {
return 'payment_receives';
}
/**
* Timestamps columns.
*/
get timestamps() {
return ['created_at', 'updated_at'];
}
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return ['localAmount'];
}
/**
* Payment receive amount in local currency.
* @returns {number}
*/
get localAmount() {
return this.amount * this.exchangeRate;
}
/**
* Resourcable model.
*/
static get resourceable() {
return true;
}
/*
* Relationship mapping.
*/
static get relationMappings() {
const PaymentReceiveEntry = require('models/PaymentReceiveEntry');
const AccountTransaction = require('models/AccountTransaction');
const Customer = require('models/Customer');
const Account = require('models/Account');
const Branch = require('models/Branch');
const Document = require('models/Document');
return {
customer: {
relation: Model.BelongsToOneRelation,
modelClass: Customer.default,
join: {
from: 'payment_receives.customerId',
to: 'contacts.id',
},
filter: (query) => {
query.where('contact_service', 'customer');
},
},
depositAccount: {
relation: Model.BelongsToOneRelation,
modelClass: Account.default,
join: {
from: 'payment_receives.depositAccountId',
to: 'accounts.id',
},
},
entries: {
relation: Model.HasManyRelation,
modelClass: PaymentReceiveEntry.default,
join: {
from: 'payment_receives.id',
to: 'payment_receives_entries.paymentReceiveId',
},
filter: (query) => {
query.orderBy('index', 'ASC');
},
},
transactions: {
relation: Model.HasManyRelation,
modelClass: AccountTransaction.default,
join: {
from: 'payment_receives.id',
to: 'accounts_transactions.referenceId',
},
filter: (builder) => {
builder.where('reference_type', 'PaymentReceive');
},
},
/**
* Payment receive may belongs to branch.
*/
branch: {
relation: Model.BelongsToOneRelation,
modelClass: Branch.default,
join: {
from: 'payment_receives.branchId',
to: 'branches.id',
},
},
/**
* Payment transaction may has many attached attachments.
*/
attachments: {
relation: Model.ManyToManyRelation,
modelClass: Document.default,
join: {
from: 'payment_receives.id',
through: {
from: 'document_links.modelId',
to: 'document_links.documentId',
},
to: 'documents.id',
},
filter(query) {
query.where('model_ref', 'PaymentReceive');
},
},
};
}
/**
*
*/
// static get meta() {
// return PaymentReceiveSettings;
// }
// /**
// * Retrieve the default custom views, roles and columns.
// */
// static get defaultViews() {
// return DEFAULT_VIEWS;
// }
/**
* Model search attributes.
*/
static get searchRoles() {
return [
{ fieldKey: 'payment_receive_no', comparator: 'contains' },
{ condition: 'or', fieldKey: 'reference_no', comparator: 'contains' },
{ condition: 'or', fieldKey: 'amount', comparator: 'equals' },
];
}
/**
* Prevents mutate base currency since the model is not empty.
*/
static get preventMutateBaseCurrency() {
return true;
}
}

View File

@@ -0,0 +1,55 @@
import { BaseModel } from '@/models/Model';
import { Model, mixin } from 'objection';
export class PaymentReceivedEntry extends BaseModel {
paymentReceiveId: number;
invoiceId: number;
paymentAmount: number;
/**
* Table name
*/
static get tableName() {
return 'payment_receives_entries';
}
/**
* Timestamps columns.
*/
get timestamps() {
return [];
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const PaymentReceive = require('models/PaymentReceive');
const SaleInvoice = require('models/SaleInvoice');
return {
/**
*/
payment: {
relation: Model.BelongsToOneRelation,
modelClass: PaymentReceive.default,
join: {
from: 'payment_receives_entries.paymentReceiveId',
to: 'payment_receives.id',
},
},
/**
* The payment receive entry have have sale invoice.
*/
invoice: {
relation: Model.BelongsToOneRelation,
modelClass: SaleInvoice.default,
join: {
from: 'payment_receives_entries.invoiceId',
to: 'sales_invoices.id',
},
},
};
}
}

View File

@@ -0,0 +1,39 @@
import { Injectable } from '@nestjs/common';
import { ERRORS } from '../constants';
import { PaymentReceiveTransfromer } from './PaymentReceivedTransformer';
import { PaymentReceived } from '../models/PaymentReceived';
import { TransformerInjectable } from '../../Transformer/TransformerInjectable.service';
import { ServiceError } from '../../Items/ServiceError';
@Injectable()
export class GetPaymentReceived {
constructor(
private readonly paymentReceiveModel: typeof PaymentReceived,
private readonly transformer: TransformerInjectable,
) {}
/**
* Retrieve payment receive details.
* @param {number} paymentReceiveId - Payment receive id.
* @return {Promise<IPaymentReceived>}
*/
public async getPaymentReceive(
paymentReceiveId: number
): Promise<PaymentReceived> {
const paymentReceive = await this.paymentReceiveModel.query()
.withGraphFetched('customer')
.withGraphFetched('depositAccount')
.withGraphFetched('entries.invoice')
.withGraphFetched('transactions')
.withGraphFetched('branch')
.findById(paymentReceiveId);
if (!paymentReceive) {
throw new ServiceError(ERRORS.PAYMENT_RECEIVE_NOT_EXISTS);
}
return this.transformer.transform(
paymentReceive,
new PaymentReceiveTransfromer()
);
}
}

View File

@@ -0,0 +1,37 @@
import { Inject, Injectable } from '@nestjs/common';
import { PaymentReceivedValidators } from '../commands/PaymentReceivedValidators.service';
import { SaleInvoice } from '../../SaleInvoices/models/SaleInvoice';
import { PaymentReceived } from '../models/PaymentReceived';
@Injectable()
export class GetPaymentReceivedInvoices {
constructor(
@Inject(PaymentReceived.name)
private paymentReceiveModel: typeof PaymentReceived,
private validators: PaymentReceivedValidators,
) {}
/**
* Retrieve sale invoices that associated to the given payment receive.
* @param {number} paymentReceiveId - Payment receive id.
* @return {Promise<ISaleInvoice>}
*/
public async getPaymentReceiveInvoices(paymentReceiveId: number) {
const paymentReceive = await this.paymentReceiveModel
.query()
.findById(paymentReceiveId)
.withGraphFetched('entries');
// Validates the payment receive existence.
this.validators.validatePaymentExistance(paymentReceive);
const paymentReceiveInvoicesIds = paymentReceive.entries.map(
(entry) => entry.invoiceId,
);
const saleInvoices = await SaleInvoice.query().whereIn(
'id',
paymentReceiveInvoicesIds,
);
return saleInvoices;
}
}

View File

@@ -0,0 +1,106 @@
import { Inject, Injectable } from '@nestjs/common';
import { GetPaymentReceived } from './GetPaymentReceived.service';
import { PaymentReceivedBrandingTemplate } from './PaymentReceivedBrandingTemplate.service';
import { transformPaymentReceivedToPdfTemplate } from '../utils';
import { PaymentReceived } from '../models/PaymentReceived';
import { PdfTemplateModel } from '@/modules/PdfTemplate/models/PdfTemplate';
import { ChromiumlyTenancy } from '@/modules/ChromiumlyTenancy/ChromiumlyTenancy.service';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { TemplateInjectable } from '@/modules/TemplateInjectable/TemplateInjectable.service';
import { PaymentReceivedPdfTemplateAttributes } from '../types/PaymentReceived.types';
import { events } from '@/common/events/events';
@Injectable()
export default class GetPaymentReceivedPdf {
constructor(
private chromiumlyTenancy: ChromiumlyTenancy,
private templateInjectable: TemplateInjectable,
private getPaymentService: GetPaymentReceived,
private paymentBrandingTemplateService: PaymentReceivedBrandingTemplate,
private eventPublisher: EventEmitter2,
@Inject(PaymentReceived.name)
private paymentReceiveModel: typeof PaymentReceived,
@Inject(PdfTemplateModel.name)
private pdfTemplateModel: typeof PdfTemplateModel,
) {}
/**
* Retrieve sale invoice pdf content.
* @param {number} tenantId -
* @param {IPaymentReceived} paymentReceive -
* @returns {Promise<Buffer>}
*/
async getPaymentReceivePdf(
paymentReceivedId: number,
): Promise<[Buffer, string]> {
const brandingAttributes =
await this.getPaymentBrandingAttributes(paymentReceivedId);
const htmlContent = await this.templateInjectable.render(
'modules/payment-receive-standard',
brandingAttributes,
);
const filename = await this.getPaymentReceivedFilename(paymentReceivedId);
// Converts the given html content to pdf document.
const content =
await this.chromiumlyTenancy.convertHtmlContent(htmlContent);
const eventPayload = { paymentReceivedId };
// Triggers the `onCreditNotePdfViewed` event.
await this.eventPublisher.emitAsync(
events.paymentReceive.onPdfViewed,
eventPayload,
);
return [content, filename];
}
/**
* Retrieves the filename of the given payment.
* @param {number} tenantId
* @param {number} paymentReceivedId
* @returns {Promise<string>}
*/
private async getPaymentReceivedFilename(
paymentReceivedId: number,
): Promise<string> {
const payment = await this.paymentReceiveModel
.query()
.findById(paymentReceivedId);
return `Payment-${payment.paymentReceiveNo}`;
}
/**
* Retrieves the given payment received branding attributes.
* @param {number} paymentReceivedId - Payment received identifier.
* @returns {Promise<PaymentReceivedPdfTemplateAttributes>}
*/
async getPaymentBrandingAttributes(
paymentReceivedId: number,
): Promise<PaymentReceivedPdfTemplateAttributes> {
const paymentReceived =
await this.getPaymentService.getPaymentReceive(paymentReceivedId);
const templateId =
paymentReceived?.pdfTemplateId ??
(
await this.pdfTemplateModel.query().findOne({
resource: 'PaymentReceive',
default: true,
})
)?.id;
const brandingTemplate =
await this.paymentBrandingTemplateService.getPaymentReceivedPdfTemplate(
templateId,
);
return {
...brandingTemplate.attributes,
...transformPaymentReceivedToPdfTemplate(paymentReceived),
};
}
}

View File

@@ -0,0 +1,23 @@
import { PdfTemplateModel } from '@/modules/PdfTemplate/models/PdfTemplate';
import { Injectable } from '@nestjs/common';
import { IPaymentReceivedState } from '../types/PaymentReceived.types';
@Injectable()
export class GetPaymentReceivedState {
constructor(private pdfTemplateModel: typeof PdfTemplateModel) {}
/**
* Retrieves the create/edit initial state of the payment received.
* @returns {Promise<IPaymentReceivedState>} - A promise resolving to the payment received state.
*/
public async getPaymentReceivedState(): Promise<IPaymentReceivedState> {
const defaultPdfTemplate = await this.pdfTemplateModel
.query()
.findOne({ resource: 'PaymentReceive' })
.modify('default');
return {
defaultTemplateId: defaultPdfTemplate?.id,
};
}
}

View File

@@ -0,0 +1,79 @@
// import { Inject, Service } from 'typedi';
// import * as R from 'ramda';
// import {
// IFilterMeta,
// IPaginationMeta,
// IPaymentReceived,
// IPaymentsReceivedFilter,
// } from '@/interfaces';
// import { PaymentReceiveTransfromer } from './PaymentReceivedTransformer';
// import HasTenancyService from '@/services/Tenancy/TenancyService';
// import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
// import DynamicListingService from '@/services/DynamicListing/DynamicListService';
// @Service()
// export class GetPaymentReceives {
// @Inject()
// private tenancy: HasTenancyService;
// @Inject()
// private dynamicListService: DynamicListingService;
// @Inject()
// private transformer: TransformerInjectable;
// /**
// * Retrieve payment receives paginated and filterable list.
// * @param {number} tenantId
// * @param {IPaymentsReceivedFilter} paymentReceivesFilter
// */
// public async getPaymentReceives(
// tenantId: number,
// filterDTO: IPaymentsReceivedFilter
// ): Promise<{
// paymentReceives: IPaymentReceived[];
// pagination: IPaginationMeta;
// filterMeta: IFilterMeta;
// }> {
// const { PaymentReceive } = this.tenancy.models(tenantId);
// // Parses filter DTO.
// const filter = this.parseListFilterDTO(filterDTO);
// // Dynamic list service.
// const dynamicList = await this.dynamicListService.dynamicList(
// tenantId,
// PaymentReceive,
// filter
// );
// const { results, pagination } = await PaymentReceive.query()
// .onBuild((builder) => {
// builder.withGraphFetched('customer');
// builder.withGraphFetched('depositAccount');
// dynamicList.buildQuery()(builder);
// filterDTO?.filterQuery && filterDTO.filterQuery(builder);
// })
// .pagination(filter.page - 1, filter.pageSize);
// // Transformer the payment receives models to POJO.
// const transformedPayments = await this.transformer.transform(
// tenantId,
// results,
// new PaymentReceiveTransfromer()
// );
// return {
// paymentReceives: transformedPayments,
// pagination,
// filterMeta: dynamicList.getResponseMeta(),
// };
// }
// /**
// * Parses payments receive list filter DTO.
// * @param filterDTO
// */
// private parseListFilterDTO(filterDTO) {
// return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO);
// }
// }

View File

@@ -0,0 +1,49 @@
import { Inject, Injectable } from '@nestjs/common';
import { defaultPaymentReceivedPdfTemplateAttributes } from '../constants';
import { GetPdfTemplateService } from '../../PdfTemplate/queries/GetPdfTemplate.service';
import { GetOrganizationBrandingAttributesService } from '../../PdfTemplate/queries/GetOrganizationBrandingAttributes.service';
import { PdfTemplateModel } from '../../PdfTemplate/models/PdfTemplate';
import { mergePdfTemplateWithDefaultAttributes } from '../../SaleInvoices/utils';
@Injectable()
export class PaymentReceivedBrandingTemplate {
constructor(
private readonly getPdfTemplateService: GetPdfTemplateService,
private readonly getOrgBrandingAttributes: GetOrganizationBrandingAttributesService,
@Inject(PdfTemplateModel.name)
private readonly pdfTemplateModel: typeof PdfTemplateModel,
) {}
/**
* Retrieves the payment received pdf template.
* @param {number} paymentTemplateId
* @returns
*/
public async getPaymentReceivedPdfTemplate(paymentTemplateId: number) {
const template = await this.getPdfTemplateService.getPdfTemplate(
paymentTemplateId
);
// Retrieves the organization branding attributes.
const commonOrgBrandingAttrs =
await this.getOrgBrandingAttributes.getOrganizationBrandingAttributes();
// Merges the default branding attributes with common organization branding attrs.
const organizationBrandingAttrs = {
...defaultPaymentReceivedPdfTemplateAttributes,
...commonOrgBrandingAttrs,
};
const brandingTemplateAttrs = {
...template.attributes,
companyLogoUri: template.companyLogoUri,
};
const attributes = mergePdfTemplateWithDefaultAttributes(
brandingTemplateAttrs,
organizationBrandingAttrs
);
return {
...template,
attributes,
};
}
}

View File

@@ -0,0 +1,29 @@
import { SaleInvoiceTransformer } from "@/modules/SaleInvoices/queries/SaleInvoice.transformer";
import { Transformer } from "@/modules/Transformer/Transformer";
export class PaymentReceivedEntryTransfromer extends Transformer {
/**
* Include these attributes to payment receive entry object.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return ['paymentAmountFormatted', 'invoice'];
};
/**
* Retreives the payment amount formatted.
* @param entry
* @returns {string}
*/
protected paymentAmountFormatted(entry) {
return this.formatNumber(entry.paymentAmount, { money: false });
}
/**
* Retreives the transformed invoice.
*/
protected invoice(entry) {
return this.item(entry.invoice, new SaleInvoiceTransformer());
}
}

View File

@@ -0,0 +1,80 @@
import { Transformer } from '../../Transformer/Transformer';
import { PaymentReceived } from '../models/PaymentReceived';
import { PaymentReceivedEntry } from '../models/PaymentReceivedEntry';
import { PaymentReceivedEntryTransfromer } from './PaymentReceivedEntryTransformer';
export class PaymentReceiveTransfromer extends Transformer {
/**
* Include these attributes to payment receive object.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return [
'subtotalFormatted',
'formattedPaymentDate',
'formattedCreatedAt',
'formattedAmount',
'formattedExchangeRate',
'entries',
];
};
/**
* Retrieve formatted payment receive date.
* @param {PaymentReceived} invoice
* @returns {String}
*/
protected formattedPaymentDate = (payment: PaymentReceived): string => {
return this.formatDate(payment.paymentDate);
};
/**
* Retrieves the formatted created at date.
* @param {PaymentReceived} payment
* @returns {string}
*/
protected formattedCreatedAt = (payment: PaymentReceived): string => {
return this.formatDate(payment.createdAt);
};
/**
* Retrieve the formatted payment subtotal.
* @param {PaymentReceived} payment
* @returns {string}
*/
protected subtotalFormatted = (payment: PaymentReceived): string => {
return this.formatNumber(payment.amount, {
currencyCode: payment.currencyCode,
money: false,
});
};
/**
* Retrieve formatted payment amount.
* @param {PaymentReceived} invoice
* @returns {string}
*/
protected formattedAmount = (payment: PaymentReceived): string => {
return this.formatNumber(payment.amount, {
currencyCode: payment.currencyCode,
});
};
/**
* Retrieve the formatted exchange rate.
* @param {PaymentReceived} payment
* @returns {string}
*/
protected formattedExchangeRate = (payment: PaymentReceived): string => {
return this.formatNumber(payment.exchangeRate, { money: false });
};
/**
* Retrieves the payment entries.
* @param {PaymentReceived} payment
* @returns {IPaymentReceivedEntry[]}
*/
protected entries = (payment: PaymentReceived): PaymentReceivedEntry[] => {
return this.item(payment.entries, new PaymentReceivedEntryTransfromer());
};
}

View File

@@ -0,0 +1,111 @@
import { Inject, Service } from 'typedi';
import { omit } from 'lodash';
import { IPaymentReceivePageEntry } from '../types/PaymentReceived.types';
import { ERRORS } from '../constants';
import { Injectable } from '@nestjs/common';
import { SaleInvoice } from '@/modules/SaleInvoices/models/SaleInvoice';
import { PaymentReceived } from '../models/PaymentReceived';
import { ServiceError } from '@/modules/Items/ServiceError';
/**
* Payment receives edit/new pages service.
*/
@Injectable()
export class PaymentsReceivedPagesService {
constructor(
@Inject(SaleInvoice.name)
private readonly saleInvoice: typeof SaleInvoice,
@Inject(PaymentReceived.name)
private readonly paymentReceived: typeof PaymentReceived,
) {}
/**
* Retrive page invoices entries from the given sale invoices models.
* @param {ISaleInvoice[]} invoices - Invoices.
* @return {IPaymentReceivePageEntry}
*/
private invoiceToPageEntry(invoice: SaleInvoice): IPaymentReceivePageEntry {
return {
entryType: 'invoice',
invoiceId: invoice.id,
invoiceNo: invoice.invoiceNo,
amount: invoice.balance,
dueAmount: invoice.dueAmount,
paymentAmount: invoice.paymentAmount,
totalPaymentAmount: invoice.paymentAmount,
currencyCode: invoice.currencyCode,
date: invoice.invoiceDate,
};
}
/**
* Retrieve payment receive new page receivable entries.
* @param {number} tenantId - Tenant id.
* @param {number} vendorId - Vendor id.
* @return {IPaymentReceivePageEntry[]}
*/
public async getNewPageEntries(tenantId: number, customerId: number) {
// Retrieve due invoices.
const entries = await this.saleInvoice
.query()
.modify('delivered')
.modify('dueInvoices')
.where('customer_id', customerId)
.orderBy('invoice_date', 'ASC');
return entries.map(this.invoiceToPageEntry);
}
/**
* Retrieve the payment receive details of the given id.
* @param {number} tenantId - Tenant id.
* @param {Integer} paymentReceiveId - Payment receive id.
*/
public async getPaymentReceiveEditPage(
tenantId: number,
paymentReceiveId: number,
): Promise<{
paymentReceive: Omit<PaymentReceived, 'entries'>;
entries: IPaymentReceivePageEntry[];
}> {
// Retrieve payment receive.
const paymentReceive = await this.paymentReceived
.query()
.findById(paymentReceiveId)
.withGraphFetched('entries.invoice')
.withGraphFetched('attachments');
// Throw not found the payment receive.
if (!paymentReceive) {
throw new ServiceError(ERRORS.PAYMENT_RECEIVE_NOT_EXISTS);
}
const paymentEntries = paymentReceive.entries.map((entry) => ({
...this.invoiceToPageEntry(entry.invoice),
dueAmount: entry.invoice.dueAmount + entry.paymentAmount,
paymentAmount: entry.paymentAmount,
index: entry.index,
}));
// Retrieves all receivable bills that associated to the payment receive transaction.
const restReceivableInvoices = await this.saleInvoice
.query()
.modify('delivered')
.modify('dueInvoices')
.where('customer_id', paymentReceive.customerId)
.whereNotIn(
'id',
paymentReceive.entries.map((entry) => entry.invoiceId),
)
.orderBy('invoice_date', 'ASC');
const restReceivableEntries = restReceivableInvoices.map(
this.invoiceToPageEntry,
);
const entries = [...paymentEntries, ...restReceivableEntries];
return {
paymentReceive: omit(paymentReceive, ['entries']),
entries,
};
}
}

View File

@@ -0,0 +1,210 @@
import { AttachmentLinkDTO } from '@/modules/Attachments/Attachments.types';
import { Knex } from 'knex';
import { PaymentReceived } from '../models/PaymentReceived';
export interface IPaymentReceivedCreateDTO {
customerId: number;
paymentDate: Date;
amount: number;
exchangeRate: number;
referenceNo: string;
depositAccountId: number;
paymentReceiveNo?: string;
statement: string;
entries: IPaymentReceivedEntryDTO[];
branchId?: number;
attachments?: AttachmentLinkDTO[];
}
export interface IPaymentReceivedEditDTO {
customerId: number;
paymentDate: Date;
amount: number;
exchangeRate: number;
referenceNo: string;
depositAccountId: number;
paymentReceiveNo?: string;
statement: string;
entries: IPaymentReceivedEntryDTO[];
branchId?: number;
attachments?: AttachmentLinkDTO[];
}
export interface IPaymentReceivedEntryDTO {
id?: number;
index?: number;
paymentReceiveId?: number;
invoiceId: number;
paymentAmount: number;
}
// export interface IPaymentsReceivedFilter extends IDynamicListFilterDTO {
// stringifiedFilterRoles?: string;
// filterQuery?: (trx: Knex.Transaction) => void;
// }
export interface IPaymentReceivePageEntry {
invoiceId: number;
entryType: string;
invoiceNo: string;
dueAmount: number;
amount: number;
totalPaymentAmount: number;
paymentAmount: number;
currencyCode: string;
date: Date | string;
}
export interface IPaymentReceivedEditPage {
paymentReceive: PaymentReceived;
entries: IPaymentReceivePageEntry[];
}
export interface IPaymentsReceivedService {
validateCustomerHasNoPayments(
tenantId: number,
customerId: number,
): Promise<void>;
}
export interface IPaymentReceivedSmsDetails {
customerName: string;
customerPhoneNumber: string;
smsMessage: string;
}
export interface IPaymentReceivedCreatingPayload {
tenantId: number;
paymentReceiveDTO: IPaymentReceivedCreateDTO;
trx: Knex.Transaction;
}
export interface IPaymentReceivedCreatedPayload {
// tenantId: number;
paymentReceive: PaymentReceived;
paymentReceiveId: number;
// authorizedUser: ISystemUser;
paymentReceiveDTO: IPaymentReceivedCreateDTO;
trx: Knex.Transaction;
}
export interface IPaymentReceivedEditedPayload {
// tenantId: number;
paymentReceiveId: number;
paymentReceive: PaymentReceived;
oldPaymentReceive: PaymentReceived;
paymentReceiveDTO: IPaymentReceivedEditDTO;
// authorizedUser: ISystemUser;
trx: Knex.Transaction;
}
export interface IPaymentReceivedEditingPayload {
// tenantId: number;
oldPaymentReceive: PaymentReceived;
paymentReceiveDTO: IPaymentReceivedEditDTO;
trx: Knex.Transaction;
}
export interface IPaymentReceivedDeletingPayload {
// tenantId: number;
oldPaymentReceive: PaymentReceived;
trx: Knex.Transaction;
}
export interface IPaymentReceivedDeletedPayload {
// tenantId: number;
paymentReceiveId: number;
oldPaymentReceive: PaymentReceived;
// authorizedUser: ISystemUser;
trx: Knex.Transaction;
}
export enum PaymentReceiveAction {
Create = 'Create',
Edit = 'Edit',
Delete = 'Delete',
View = 'View',
NotifyBySms = 'NotifyBySms',
}
// export type IPaymentReceiveGLCommonEntry = Pick<
// ILedgerEntry,
// | 'debit'
// | 'credit'
// | 'currencyCode'
// | 'exchangeRate'
// | 'transactionId'
// | 'transactionType'
// | 'transactionNumber'
// | 'referenceNumber'
// | 'date'
// | 'userId'
// | 'createdAt'
// | 'branchId'
// >;
// export interface PaymentReceiveMailOpts extends CommonMailOptions {}
// export interface PaymentReceiveMailOptsDTO extends CommonMailOptionsDTO {}
// export interface PaymentReceiveMailPresendEvent {
// tenantId: number;
// paymentReceiveId: number;
// messageOptions: PaymentReceiveMailOptsDTO;
// }
export interface PaymentReceivedPdfLineItem {
item: string;
description: string;
rate: string;
quantity: string;
total: string;
}
export interface PaymentReceivedPdfTax {
label: string;
amount: string;
}
export interface PaymentReceivedPdfTemplateAttributes {
primaryColor: string;
secondaryColor: string;
showCompanyLogo: boolean;
companyLogo: string;
companyName: string;
// Customer Address
showCustomerAddress: boolean;
customerAddress: string;
// Company address
showCompanyAddress: boolean;
companyAddress: string;
billedToLabel: string;
total: string;
totalLabel: string;
showTotal: boolean;
subtotal: string;
subtotalLabel: string;
showSubtotal: boolean;
lines: Array<{
invoiceNumber: string;
invoiceAmount: string;
paidAmount: string;
}>;
showPaymentReceivedNumber: boolean;
paymentReceivedNumberLabel: string;
paymentReceivedNumebr: string;
paymentReceivedDate: string;
showPaymentReceivedDate: boolean;
paymentReceivedDateLabel: string;
}
export interface IPaymentReceivedState {
defaultTemplateId: number;
}

View File

@@ -0,0 +1,32 @@
import { PaymentReceived } from './models/PaymentReceived';
import {
PaymentReceivedPdfTemplateAttributes,
} from './types/PaymentReceived.types';
import { contactAddressTextFormat } from '@/utils/address-text-format';
export const transformPaymentReceivedToPdfTemplate = (
payment: PaymentReceived
): Partial<PaymentReceivedPdfTemplateAttributes> => {
return {
total: payment.formattedAmount,
subtotal: payment.subtotalFormatted,
paymentReceivedNumebr: payment.paymentReceiveNo,
paymentReceivedDate: payment.formattedPaymentDate,
customerName: payment.customer.displayName,
lines: payment.entries.map((entry) => ({
invoiceNumber: entry.invoice.invoiceNo,
invoiceAmount: entry.invoice.totalFormatted,
paidAmount: entry.paymentAmountFormatted,
})),
customerAddress: contactAddressTextFormat(payment.customer),
};
};
export const transformPaymentReceivedToMailDataArgs = (payment: any) => {
return {
'Customer Name': payment.customer.displayName,
'Payment Number': payment.paymentReceiveNo,
'Payment Date': payment.formattedPaymentDate,
'Payment Amount': payment.formattedAmount,
};
};