feat(nestjs): migrate to NestJS

This commit is contained in:
Ahmed Bouhuolia
2025-04-07 11:51:24 +02:00
parent f068218a16
commit 55fcc908ef
3779 changed files with 631 additions and 195332 deletions

View File

@@ -0,0 +1,73 @@
import { Knex } from 'knex';
import { Injectable } from '@nestjs/common';
import { ItemsEntriesService } from '../Items/ItemsEntries.service';
import { ModelObject } from 'objection';
import { SaleInvoice } from './models/SaleInvoice';
@Injectable()
export class InvoiceInventoryTransactions {
constructor(
private readonly itemsEntriesService: ItemsEntriesService,
// private readonly inventoryService: InventoryService,
) {}
/**
* Records the inventory transactions of the given sale invoice in case
* the invoice has inventory entries only.
*
* @param {number} tenantId - Tenant id.
* @param {SaleInvoice} saleInvoice - Sale invoice DTO.
* @param {number} saleInvoiceId - Sale invoice id.
* @param {boolean} override - Allow to override old transactions.
* @return {Promise<void>}
*/
public async recordInventoryTranscactions(
saleInvoice: ModelObject<SaleInvoice>,
override?: boolean,
trx?: Knex.Transaction,
): Promise<void> {
// Loads the inventory items entries of the given sale invoice.
const inventoryEntries =
await this.itemsEntriesService.filterInventoryEntries(
saleInvoice.entries,
trx,
);
const transaction = {
transactionId: saleInvoice.id,
transactionType: 'SaleInvoice',
transactionNumber: saleInvoice.invoiceNo,
exchangeRate: saleInvoice.exchangeRate,
warehouseId: saleInvoice.warehouseId,
date: saleInvoice.invoiceDate,
direction: 'OUT',
entries: inventoryEntries,
createdAt: saleInvoice.createdAt,
};
// await this.inventoryService.recordInventoryTransactionsFromItemsEntries(
// transaction,
// override,
// trx,
// );
}
/**
* Reverting the inventory transactions once the invoice deleted.
* @param {number} tenantId - Tenant id.
* @param {number} billId - Bill id.
* @return {Promise<void>}
*/
public async revertInventoryTransactions(
tenantId: number,
saleInvoiceId: number,
trx?: Knex.Transaction,
): Promise<void> {
// Delete the inventory transaction of the given sale invoice.
// const { oldInventoryTransactions } =
// await this.inventoryService.deleteInventoryTransactions(
// saleInvoiceId,
// 'SaleInvoice',
// trx,
// );
}
}

View File

@@ -0,0 +1,70 @@
import { Knex } from 'knex';
import async from 'async';
import { Inject, Injectable } from '@nestjs/common';
import { PaymentReceivedGLEntries } from '../PaymentReceived/commands/PaymentReceivedGLEntries';
import { TenantModelProxy } from '../System/models/TenantBaseModel';
import { PaymentReceivedEntry } from '../PaymentReceived/models/PaymentReceivedEntry';
@Injectable()
export class InvoicePaymentsGLEntriesRewrite {
constructor(
private readonly paymentGLEntries: PaymentReceivedGLEntries,
@Inject(PaymentReceivedEntry.name)
private readonly paymentReceivedEntryModel: TenantModelProxy<typeof PaymentReceivedEntry>
) {}
/**
* Rewrites the payment GL entries task.
* @param {{ tenantId: number, paymentId: number, trx: Knex?.Transaction }}
* @returns {Promise<void>}
*/
public rewritePaymentsGLEntriesTask = async ({
paymentId,
trx,
}) => {
await this.paymentGLEntries.rewritePaymentGLEntries(
paymentId,
trx
);
};
/**
* Rewrites the payment GL entries of the given payments ids.
* @param {number[]} paymentsIds
* @param {Knex.Transaction} trx
*/
public rewritePaymentsGLEntriesQueue = async (
paymentsIds: number[],
trx?: Knex.Transaction
) => {
// Initiate a new queue for accounts balance mutation.
const rewritePaymentGL = async.queue(this.rewritePaymentsGLEntriesTask, 10);
paymentsIds.forEach((paymentId: number) => {
rewritePaymentGL.push({ paymentId, trx });
});
if (paymentsIds.length > 0) {
await rewritePaymentGL.drain();
}
};
/**
* Rewrites the payments GL entries that associated to the given invoice.
* @param {number} invoiceId
* @param {Knex.Transaction} trx
* @ {Promise<void>}
*/
public invoicePaymentsGLEntriesRewrite = async (
invoiceId: number,
trx?: Knex.Transaction
) => {
const invoicePaymentEntries = await this.paymentReceivedEntryModel()
.query()
.where('invoiceId', invoiceId);
const paymentsIds = invoicePaymentEntries.map((e) => e.paymentReceiveId);
await this.rewritePaymentsGLEntriesQueue(paymentsIds, trx);
};
}

View File

@@ -0,0 +1,316 @@
import { Knex } from 'knex';
import { IItemEntryDTO } from '../TransactionItemEntry/ItemEntry.types';
import { AttachmentLinkDTO } from '../Attachments/Attachments.types';
import { SaleInvoice } from './models/SaleInvoice';
import { IDynamicListFilter } from '../DynamicListing/DynamicFilter/DynamicFilter.types';
import {
CommonMailOptions,
CommonMailOptionsDTO,
} from '../MailNotification/MailNotification.types';
import { TenantJobPayload } from '@/interfaces/Tenant';
import { CreateSaleInvoiceDto, EditSaleInvoiceDto } from './dtos/SaleInvoice.dto';
export interface PaymentIntegrationTransactionLink {
id: number;
enable: true;
paymentIntegrationId: number;
referenceType: string;
referenceId: number;
}
export interface PaymentIntegrationTransactionLinkEventPayload {
tenantId: number;
enable: true;
paymentIntegrationId: number;
referenceType: string;
referenceId: number;
saleInvoiceId: number;
trx?: Knex.Transaction;
}
export interface PaymentIntegrationTransactionLinkDeleteEventPayload {
tenantId: number;
enable: true;
paymentIntegrationId: number;
referenceType: string;
referenceId: number;
oldSaleInvoiceId: number;
trx?: Knex.Transaction;
}
export interface ISaleInvoiceDTO {
invoiceDate: Date;
dueDate: Date;
referenceNo: string;
invoiceNo: string;
customerId: number;
exchangeRate?: number;
invoiceMessage: string;
termsConditions: string;
isTaxExclusive: boolean;
entries: IItemEntryDTO[];
delivered: boolean;
warehouseId?: number | null;
projectId?: number;
branchId?: number | null;
isInclusiveTax?: boolean;
attachments?: AttachmentLinkDTO[];
}
export interface ISaleInvoiceCreateDTO extends ISaleInvoiceDTO {
fromEstimateId: number;
}
export interface ISaleInvoiceEditDTO extends ISaleInvoiceDTO {}
export interface ISalesInvoicesFilter extends IDynamicListFilter {
page: number;
pageSize: number;
searchKeyword?: string;
filterQuery?: (q: Knex.QueryBuilder) => void;
}
export interface ISaleInvoiceWriteoffDTO {
expenseAccountId: number;
date: Date;
reason: string;
}
export type InvoiceNotificationType = 'details' | 'reminder';
export interface ISaleInvoiceCreatedPayload {
saleInvoice: SaleInvoice;
saleInvoiceDTO: CreateSaleInvoiceDto;
saleInvoiceId: number;
trx: Knex.Transaction;
}
export interface ISaleInvoiceCreatingPaylaod {
tenantId: number;
saleInvoiceDTO: CreateSaleInvoiceDto;
trx: Knex.Transaction;
}
export interface ISaleInvoiceEditedPayload {
saleInvoice: SaleInvoice;
oldSaleInvoice: SaleInvoice;
saleInvoiceDTO: EditSaleInvoiceDto;
saleInvoiceId: number;
trx: Knex.Transaction;
}
export interface ISaleInvoiceEditingPayload {
saleInvoiceDTO: EditSaleInvoiceDto;
oldSaleInvoice: SaleInvoice;
trx: Knex.Transaction;
}
export interface ISaleInvoiceDeletePayload {
// tenantId: number;
oldSaleInvoice: SaleInvoice;
saleInvoiceId: number;
trx: Knex.Transaction;
}
export interface ISaleInvoiceDeletingPayload {
oldSaleInvoice: SaleInvoice;
saleInvoiceId: number;
trx: Knex.Transaction;
}
export interface ISaleInvoiceDeletedPayload {
oldSaleInvoice: SaleInvoice;
saleInvoiceId: number;
trx: Knex.Transaction;
}
export interface ISaleInvoiceWriteoffCreatePayload {
saleInvoiceId: number;
saleInvoice: SaleInvoice;
writeoffDTO: ISaleInvoiceWriteoffDTO;
trx: Knex.Transaction;
}
export interface ISaleInvoiceWriteoffCreatedPayload {
saleInvoiceId: number;
saleInvoice: SaleInvoice;
writeoffDTO: ISaleInvoiceCreatedPayload;
}
export interface ISaleInvoiceWrittenOffCancelPayload {
saleInvoice: SaleInvoice;
trx: Knex.Transaction;
}
export interface ISaleInvoiceWrittenOffCanceledPayload {
saleInvoice: SaleInvoice;
trx: Knex.Transaction;
}
export interface ISaleInvoiceEventDeliveredPayload {
saleInvoiceId: number;
saleInvoice: SaleInvoice;
trx: Knex.Transaction;
}
export interface ISaleInvoiceDeliveringPayload {
oldSaleInvoice: SaleInvoice;
trx: Knex.Transaction;
}
export enum SaleInvoiceAction {
Create = 'Create',
Edit = 'Edit',
Delete = 'Delete',
View = 'View',
Writeoff = 'Writeoff',
NotifyBySms = 'NotifyBySms',
}
export interface SaleInvoiceMailOptions extends CommonMailOptions {
attachInvoice?: boolean;
formatArgs?: Record<string, any>;
}
export interface SaleInvoiceMailState extends SaleInvoiceMailOptions {
invoiceNo: string;
invoiceDate: string;
invoiceDateFormatted: string;
dueDate: string;
dueDateFormatted: string;
total: number;
totalFormatted: string;
subtotal: number;
subtotalFormatted: number;
companyName: string;
companyLogoUri: string;
customerName: string;
// # Invoice entries
entries?: Array<{ label: string; total: string; quantity: string | number }>;
}
export interface SendInvoiceMailDTO extends CommonMailOptionsDTO {
attachInvoice?: boolean;
}
export interface ISaleInvoiceNotifyPayload {
saleInvoiceId: number;
messageDTO: SendInvoiceMailDTO;
}
export interface ISaleInvoiceMailSend {
saleInvoiceId: number;
messageOptions: SendInvoiceMailDTO;
// formattedMessageOptions: SaleInvoiceMailOptions;
}
export interface ISaleInvoiceMailSent {
saleInvoiceId: number;
messageOptions: SendInvoiceMailDTO;
}
// Invoice Pdf Document
export interface InvoicePdfLine {
item: string;
description: string;
rate: string;
quantity: string;
total: string;
}
export interface InvoicePdfTax {
label: string;
amount: string;
}
export interface InvoicePdfTemplateAttributes {
primaryColor: string;
secondaryColor: string;
companyName: string;
showCompanyLogo: boolean;
companyLogo: string;
dueDate: string;
dueDateLabel: string;
showDueDate: boolean;
dateIssue: string;
dateIssueLabel: string;
showDateIssue: boolean;
invoiceNumberLabel: string;
invoiceNumber: string;
showInvoiceNumber: boolean;
// Customer Address
showCustomerAddress: boolean;
customerAddress: string;
// Company address
showCompanyAddress: boolean;
companyAddress: string;
billedToLabel: string;
lineItemLabel: string;
lineDescriptionLabel: string;
lineRateLabel: string;
lineTotalLabel: string;
totalLabel: string;
subtotalLabel: string;
discountLabel: string;
paymentMadeLabel: string;
showTotal: boolean;
showSubtotal: boolean;
showDiscount: boolean;
showTaxes: boolean;
showPaymentMade: boolean;
total: string;
subtotal: string;
discount: string;
paymentMade: string;
// Due Amount
dueAmount: string;
showDueAmount: boolean;
dueAmountLabel: string;
termsConditionsLabel: string;
showTermsConditions: boolean;
termsConditions: string;
lines: InvoicePdfLine[];
taxes: InvoicePdfTax[];
statementLabel: string;
showStatement: boolean;
statement: string;
}
export interface ISaleInvocieState {
defaultTemplateId: number;
}
export interface SaleInvoiceSendMailData {
saleInvoiceId: number;
messageOptions: SendInvoiceMailDTO;
}
export interface SendSaleInvoiceMailJobPayload extends TenantJobPayload {
messageOptions: any;
saleInvoiceId: number;
}

View File

@@ -0,0 +1,148 @@
import * as R from 'ramda';
import { Knex } from 'knex';
import { Inject, Injectable } from '@nestjs/common';
import { TenantModelProxy } from '../System/models/TenantBaseModel';
import { InventoryCostLotTracker } from '../InventoryCost/models/InventoryCostLotTracker';
import { LedgerStorageService } from '../Ledger/LedgerStorage.service';
import { groupInventoryTransactionsByTypeId } from '../InventoryCost/utils';
import { Ledger } from '../Ledger/Ledger';
import { AccountNormal } from '@/interfaces/Account';
import { ILedgerEntry } from '../Ledger/types/Ledger.types';
import { increment } from '@/utils/increment';
@Injectable()
export class SaleInvoiceCostGLEntries {
constructor(
private readonly ledgerStorage: LedgerStorageService,
@Inject(InventoryCostLotTracker.name)
private readonly inventoryCostLotTracker: TenantModelProxy<
typeof InventoryCostLotTracker
>,
) {}
/**
* Writes journal entries from sales invoices.
* @param {Date} startingDate - Starting date.
* @param {boolean} override
*/
public writeInventoryCostJournalEntries = async (
startingDate: Date,
trx?: Knex.Transaction,
): Promise<void> => {
const inventoryCostLotTrans = await this.inventoryCostLotTracker()
.query()
.where('direction', 'OUT')
.where('transaction_type', 'SaleInvoice')
.where('cost', '>', 0)
.modify('filterDateRange', startingDate)
.orderBy('date', 'ASC')
.withGraphFetched('invoice')
.withGraphFetched('item');
const ledger = this.getInventoryCostLotsLedger(inventoryCostLotTrans);
// Commit the ledger to the storage.
await this.ledgerStorage.commit(ledger, trx);
};
/**
* Retrieves the inventory cost lots ledger.
* @param {IInventoryLotCost[]} inventoryCostLots
* @returns {Ledger}
*/
private getInventoryCostLotsLedger = (
inventoryCostLots: InventoryCostLotTracker[],
) => {
// Groups the inventory cost lots transactions.
const inventoryTransactions =
groupInventoryTransactionsByTypeId(inventoryCostLots);
const entries = inventoryTransactions
.map(this.getSaleInvoiceCostGLEntries)
.flat();
return new Ledger(entries);
};
/**
*
* @param {IInventoryLotCost} inventoryCostLot
* @returns {}
*/
private getInvoiceCostGLCommonEntry = (
inventoryCostLot: InventoryCostLotTracker,
) => {
return {
currencyCode: inventoryCostLot.invoice.currencyCode,
exchangeRate: inventoryCostLot.invoice.exchangeRate,
transactionType: inventoryCostLot.transactionType,
transactionId: inventoryCostLot.transactionId,
date: inventoryCostLot.date,
indexGroup: 20,
costable: true,
createdAt: inventoryCostLot.createdAt,
debit: 0,
credit: 0,
branchId: inventoryCostLot.invoice.branchId,
};
};
/**
* Retrieves the inventory cost GL entry.
* @param {IInventoryLotCost} inventoryLotCost
* @returns {ILedgerEntry[]}
*/
private getInventoryCostGLEntry = R.curry(
(
getIndexIncrement,
inventoryCostLot: InventoryCostLotTracker,
): ILedgerEntry[] => {
const commonEntry = this.getInvoiceCostGLCommonEntry(inventoryCostLot);
const costAccountId =
inventoryCostLot.costAccountId || inventoryCostLot.item.costAccountId;
// XXX Debit - Cost account.
const costEntry = {
...commonEntry,
debit: inventoryCostLot.cost,
accountId: costAccountId,
accountNormal: AccountNormal.DEBIT,
itemId: inventoryCostLot.itemId,
index: getIndexIncrement(),
};
// XXX Credit - Inventory account.
const inventoryEntry = {
...commonEntry,
credit: inventoryCostLot.cost,
accountId: inventoryCostLot.item.inventoryAccountId,
accountNormal: AccountNormal.DEBIT,
itemId: inventoryCostLot.itemId,
index: getIndexIncrement(),
};
return [costEntry, inventoryEntry];
},
);
/**
* Writes journal entries for given sale invoice.
* -----
* - Cost of goods sold -> Debit -> YYYY
* - Inventory assets -> Credit -> YYYY
*-----
* @param {ISaleInvoice} saleInvoice
* @param {JournalPoster} journal
*/
public getSaleInvoiceCostGLEntries = (
inventoryCostLots: InventoryCostLotTracker[],
): ILedgerEntry[] => {
const getIndexIncrement = increment(0);
const getInventoryLotEntry =
this.getInventoryCostGLEntry(getIndexIncrement);
return inventoryCostLots.map((t) => getInventoryLotEntry(t)).flat();
};
}

View File

@@ -0,0 +1,260 @@
// import { Service, Inject } from 'typedi';
// import moment from 'moment';
// import HasTenancyService from '@/services/Tenancy/TenancyService';
// import events from '@/subscribers/events';
// import {
// ISaleInvoice,
// ISaleInvoiceSmsDetailsDTO,
// ISaleInvoiceSmsDetails,
// SMS_NOTIFICATION_KEY,
// InvoiceNotificationType,
// ICustomer,
// } from '@/interfaces';
// import SmsNotificationsSettingsService from '@/services/Settings/SmsNotificationsSettings';
// import { formatSmsMessage, formatNumber } from 'utils';
// import { TenantMetadata } from '@/system/models';
// import SaleNotifyBySms from '../SaleNotifyBySms';
// import { ServiceError } from '@/exceptions';
// import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
// import { ERRORS } from './constants';
// import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators';
// @Service()
// export class SaleInvoiceNotifyBySms {
// @Inject()
// private tenancy: HasTenancyService;
// @Inject()
// private eventPublisher: EventPublisher;
// @Inject()
// private smsNotificationsSettings: SmsNotificationsSettingsService;
// @Inject()
// private saleSmsNotification: SaleNotifyBySms;
// @Inject()
// private validators: CommandSaleInvoiceValidators;
// /**
// * Notify customer via sms about sale invoice.
// * @param {number} tenantId - Tenant id.
// * @param {number} saleInvoiceId - Sale invoice id.
// */
// public notifyBySms = async (
// tenantId: number,
// saleInvoiceId: number,
// invoiceNotificationType: InvoiceNotificationType
// ) => {
// const { SaleInvoice } = this.tenancy.models(tenantId);
// // Retrieve the sale invoice or throw not found service error.
// const saleInvoice = await SaleInvoice.query()
// .findById(saleInvoiceId)
// .withGraphFetched('customer');
// // Validates the givne invoice existance.
// this.validators.validateInvoiceExistance(saleInvoice);
// // Validate the customer phone number existance and number validation.
// this.saleSmsNotification.validateCustomerPhoneNumber(
// saleInvoice.customer.personalPhone
// );
// // Transformes the invoice notification key to sms notification key.
// const notificationKey = this.transformDTOKeyToNotificationKey(
// invoiceNotificationType
// );
// // Triggers `onSaleInvoiceNotifySms` event.
// await this.eventPublisher.emitAsync(events.saleInvoice.onNotifySms, {
// tenantId,
// saleInvoice,
// });
// // Formattes the sms message and sends sms notification.
// await this.sendSmsNotification(tenantId, notificationKey, saleInvoice);
// // Triggers `onSaleInvoiceNotifySms` event.
// await this.eventPublisher.emitAsync(events.saleInvoice.onNotifiedSms, {
// tenantId,
// saleInvoice,
// });
// return saleInvoice;
// };
// /**
// * Notify invoice details by sms notification after invoice creation.
// * @param {number} tenantId
// * @param {number} saleInvoiceId
// * @returns {Promise<void>}
// */
// public notifyDetailsBySmsAfterCreation = async (
// tenantId: number,
// saleInvoiceId: number
// ): Promise<void> => {
// const notification = this.smsNotificationsSettings.getSmsNotificationMeta(
// tenantId,
// SMS_NOTIFICATION_KEY.SALE_INVOICE_DETAILS
// );
// // Can't continue if the sms auto-notification is not enabled.
// if (!notification.isNotificationEnabled) return;
// await this.notifyBySms(tenantId, saleInvoiceId, 'details');
// };
// /**
// * Sends SMS notification.
// * @param {ISaleInvoice} invoice
// * @param {ICustomer} customer
// * @returns {Promise<void>}
// */
// private sendSmsNotification = async (
// tenantId: number,
// notificationType:
// | SMS_NOTIFICATION_KEY.SALE_INVOICE_DETAILS
// | SMS_NOTIFICATION_KEY.SALE_INVOICE_REMINDER,
// invoice: ISaleInvoice & { customer: ICustomer }
// ): Promise<void> => {
// const smsClient = this.tenancy.smsClient(tenantId);
// const tenantMetadata = await TenantMetadata.query().findOne({ tenantId });
// // Formates the given sms message.
// const message = this.formattedInvoiceDetailsMessage(
// tenantId,
// notificationType,
// invoice,
// tenantMetadata
// );
// const phoneNumber = invoice.customer.personalPhone;
// // Run the send sms notification message job.
// await smsClient.sendMessageJob(phoneNumber, message);
// };
// /**
// * Formates the invoice details sms message.
// * @param {number} tenantId
// * @param {ISaleInvoice} invoice
// * @param {ICustomer} customer
// * @returns {string}
// */
// private formattedInvoiceDetailsMessage = (
// tenantId: number,
// notificationKey:
// | SMS_NOTIFICATION_KEY.SALE_INVOICE_DETAILS
// | SMS_NOTIFICATION_KEY.SALE_INVOICE_REMINDER,
// invoice: ISaleInvoice,
// tenantMetadata: TenantMetadata
// ): string => {
// const notification = this.smsNotificationsSettings.getSmsNotificationMeta(
// tenantId,
// notificationKey
// );
// return this.formatInvoiceDetailsMessage(
// notification.smsMessage,
// invoice,
// tenantMetadata
// );
// };
// /**
// * Formattees the given invoice details sms message.
// * @param {string} smsMessage
// * @param {ISaleInvoice} invoice
// * @param {ICustomer} customer
// * @param {TenantMetadata} tenantMetadata
// */
// private formatInvoiceDetailsMessage = (
// smsMessage: string,
// invoice: ISaleInvoice & { customer: ICustomer },
// tenantMetadata: TenantMetadata
// ) => {
// const formattedDueAmount = formatNumber(invoice.dueAmount, {
// currencyCode: invoice.currencyCode,
// });
// const formattedAmount = formatNumber(invoice.balance, {
// currencyCode: invoice.currencyCode,
// });
// return formatSmsMessage(smsMessage, {
// InvoiceNumber: invoice.invoiceNo,
// ReferenceNumber: invoice.referenceNo,
// CustomerName: invoice.customer.displayName,
// DueAmount: formattedDueAmount,
// DueDate: moment(invoice.dueDate).format('YYYY/MM/DD'),
// Amount: formattedAmount,
// CompanyName: tenantMetadata.name,
// });
// };
// /**
// * Retrieve the SMS details of the given invoice.
// * @param {number} tenantId - Tenant id.
// * @param {number} saleInvoiceId - Sale invoice id.
// */
// public smsDetails = async (
// tenantId: number,
// saleInvoiceId: number,
// invoiceSmsDetailsDTO: ISaleInvoiceSmsDetailsDTO
// ): Promise<ISaleInvoiceSmsDetails> => {
// const { SaleInvoice } = this.tenancy.models(tenantId);
// // Retrieve the sale invoice or throw not found service error.
// const saleInvoice = await SaleInvoice.query()
// .findById(saleInvoiceId)
// .withGraphFetched('customer');
// // Validates the sale invoice existance.
// this.validateSaleInvoiceExistance(saleInvoice);
// // Current tenant metadata.
// const tenantMetadata = await TenantMetadata.query().findOne({ tenantId });
// // Transformes the invoice notification key to sms notification key.
// const notificationKey = this.transformDTOKeyToNotificationKey(
// invoiceSmsDetailsDTO.notificationKey
// );
// // Formates the given sms message.
// const smsMessage = this.formattedInvoiceDetailsMessage(
// tenantId,
// notificationKey,
// saleInvoice,
// tenantMetadata
// );
// return {
// customerName: saleInvoice.customer.displayName,
// customerPhoneNumber: saleInvoice.customer.personalPhone,
// smsMessage,
// };
// };
// /**
// * Transformes the invoice notification key DTO to notification key.
// * @param {string} invoiceNotifKey
// * @returns {SMS_NOTIFICATION_KEY.SALE_INVOICE_DETAILS
// * | SMS_NOTIFICATION_KEY.SALE_INVOICE_REMINDER}
// */
// private transformDTOKeyToNotificationKey = (
// invoiceNotifKey: string
// ):
// | SMS_NOTIFICATION_KEY.SALE_INVOICE_DETAILS
// | SMS_NOTIFICATION_KEY.SALE_INVOICE_REMINDER => {
// const invoiceNotifKeyPairs = {
// details: SMS_NOTIFICATION_KEY.SALE_INVOICE_DETAILS,
// reminder: SMS_NOTIFICATION_KEY.SALE_INVOICE_REMINDER,
// };
// return (
// invoiceNotifKeyPairs[invoiceNotifKey] ||
// SMS_NOTIFICATION_KEY.SALE_INVOICE_DETAILS
// );
// };
// /**
// * Validates the sale invoice existance.
// * @param {ISaleInvoice|null} saleInvoice
// */
// private validateSaleInvoiceExistance(saleInvoice: ISaleInvoice | null) {
// if (!saleInvoice) {
// throw new ServiceError(ERRORS.SALE_INVOICE_NOT_FOUND);
// }
// }
// }

View File

@@ -0,0 +1,205 @@
import { Injectable } from '@nestjs/common';
import { CreateSaleInvoice } from './commands/CreateSaleInvoice.service';
import { DeleteSaleInvoice } from './commands/DeleteSaleInvoice.service';
import { GetSaleInvoice } from './queries/GetSaleInvoice.service';
import { EditSaleInvoice } from './commands/EditSaleInvoice.service';
import { DeliverSaleInvoice } from './commands/DeliverSaleInvoice.service';
import { GetSaleInvoicesPayable } from './queries/GetSaleInvoicesPayable.service';
import { WriteoffSaleInvoice } from './commands/WriteoffSaleInvoice.service';
import { SaleInvoicePdf } from './queries/SaleInvoicePdf.service';
import { GetInvoicePaymentsService } from './queries/GetInvoicePayments.service';
import { GetSaleInvoiceState } from './queries/GetSaleInvoiceState.service';
import { GetSaleInvoiceMailState } from './queries/GetSaleInvoiceMailState.service';
import {
ISaleInvoiceWriteoffDTO,
ISalesInvoicesFilter,
SaleInvoiceMailState,
SendInvoiceMailDTO,
} from './SaleInvoice.types';
import { GetSaleInvoicesService } from './queries/GetSaleInvoices';
import { SendSaleInvoiceMail } from './commands/SendSaleInvoiceMail';
import {
CreateSaleInvoiceDto,
EditSaleInvoiceDto,
} from './dtos/SaleInvoice.dto';
@Injectable()
export class SaleInvoiceApplication {
constructor(
private createSaleInvoiceService: CreateSaleInvoice,
private deleteSaleInvoiceService: DeleteSaleInvoice,
private getSaleInvoiceService: GetSaleInvoice,
private getSaleInvoicesService: GetSaleInvoicesService,
private editSaleInvoiceService: EditSaleInvoice,
private deliverSaleInvoiceService: DeliverSaleInvoice,
private getReceivableSaleInvoicesService: GetSaleInvoicesPayable,
private writeoffInvoiceService: WriteoffSaleInvoice,
private getInvoicePaymentsService: GetInvoicePaymentsService,
private pdfSaleInvoiceService: SaleInvoicePdf,
private getSaleInvoiceStateService: GetSaleInvoiceState,
private sendSaleInvoiceMailService: SendSaleInvoiceMail,
private getSaleInvoiceMailStateService: GetSaleInvoiceMailState,
) {}
/**
* Creates a new sale invoice with associated GL entries.
* @param {ISaleInvoiceCreateDTO} saleInvoiceDTO
* @returns {Promise<ISaleInvoice>}
*/
public createSaleInvoice(saleInvoiceDTO: CreateSaleInvoiceDto) {
return this.createSaleInvoiceService.createSaleInvoice(saleInvoiceDTO);
}
/**
* Edits the given sale invoice with associated GL entries.
* @param {ISaleInvoiceEditDTO} saleInvoiceDTO
* @returns {Promise<ISaleInvoice>}
*/
public editSaleInvoice(
saleInvoiceId: number,
saleInvoiceDTO: EditSaleInvoiceDto,
) {
return this.editSaleInvoiceService.editSaleInvoice(
saleInvoiceId,
saleInvoiceDTO,
);
}
/**
* Deletes the given sale invoice with given associated GL entries.
* @param {number} tenantId
* @param {number} saleInvoiceId
* @param {ISystemUser} authorizedUser
* @returns {Promise<void>}
*/
public deleteSaleInvoice(saleInvoiceId: number) {
return this.deleteSaleInvoiceService.deleteSaleInvoice(saleInvoiceId);
}
/**
* Retrieves the given sale invoice details.
* @param {ISalesInvoicesFilter} filterDTO
* @returns {Promise<{ salesInvoices: SaleInvoice[]; pagination: IPaginationMeta; filterMeta: IFilterMeta; }>}
*/
public getSaleInvoices(filterDTO: ISalesInvoicesFilter) {
return this.getSaleInvoicesService.getSaleInvoices(filterDTO);
}
/**
* Retrieves sale invoice details.
* @param {number} saleInvoiceId -
* @return {Promise<ISaleInvoice>}
*/
public getSaleInvoice(saleInvoiceId: number) {
return this.getSaleInvoiceService.getSaleInvoice(saleInvoiceId);
}
/**
* Retrieves the sale invoice state.
* @param {number} tenantId
* @param {number} saleInvoiceId
* @returns
*/
public getSaleInvoiceState() {
return this.getSaleInvoiceStateService.getSaleInvoiceState();
}
/**
* Mark the given sale invoice as delivered.
* @param {number} tenantId
* @param {number} saleInvoiceId
* @param {ISystemUser} authorizedUser
* @returns {}
*/
public deliverSaleInvoice(saleInvoiceId: number) {
return this.deliverSaleInvoiceService.deliverSaleInvoice(saleInvoiceId);
}
/**
* Retrieves the receivable sale invoices of the given customer.
* @param {number} tenantId
* @param {number} customerId
* @returns
*/
public getReceivableSaleInvoices(customerId?: number) {
return this.getReceivableSaleInvoicesService.getPayableInvoices(customerId);
}
/**
* Writes-off the sale invoice on bad debt expense account.
* @param {number} saleInvoiceId - Sale invoice id.
* @param {ISaleInvoiceWriteoffDTO} writeoffDTO - Writeoff data.
* @return {Promise<ISaleInvoice>}
*/
public async writeOff(
saleInvoiceId: number,
writeoffDTO: ISaleInvoiceWriteoffDTO,
) {
return this.writeoffInvoiceService.writeOff(saleInvoiceId, writeoffDTO);
}
/**
* Cancels the written-off sale invoice.
* @param {number} saleInvoiceId - Sale invoice id.
* @returns {Promise<ISaleInvoice>}
*/
public cancelWrittenoff(saleInvoiceId: number) {
return this.writeoffInvoiceService.cancelWrittenoff(saleInvoiceId);
}
/**
* Retrieve the invoice assocaited payments transactions.
* @param {number} invoiceId - Invoice id.
*/
public getInvoicePayments = async (invoiceId: number) => {
return this.getInvoicePaymentsService.getInvoicePayments(invoiceId);
};
/**
* Retrieves the pdf buffer of the given sale invoice.
* @param {number} tenantId - Tenant id.
* @param {number} saleInvoice
* @returns {Promise<Buffer>}
*/
public saleInvoicePdf(saleInvoiceId: number) {
return this.pdfSaleInvoiceService.getSaleInvoicePdf(saleInvoiceId);
}
/**
* Retrieves the html content of the given sale invoice.
* @param {number} saleInvoiceId - Sale invoice id.
* @returns {Promise<string>}
*/
public saleInvoiceHtml(saleInvoiceId: number): Promise<string> {
return this.pdfSaleInvoiceService.getSaleInvoiceHtml(saleInvoiceId);
}
/**
* Sends the invoice mail of the given sale invoice.
* @param {number} saleInvoiceId - Sale invoice id.
* @param {SendInvoiceMailDTO} messageDTO - Message data.
* @returns {Promise<void>}
*/
public sendSaleInvoiceMail(
saleInvoiceId: number,
messageDTO: SendInvoiceMailDTO,
) {
return this.sendSaleInvoiceMailService.triggerMail(
saleInvoiceId,
messageDTO,
);
}
/**
* Retrieves the default mail options of the given sale invoice.
* @param {number} saleInvoiceid - Sale invoice id.
* @returns {Promise<SaleInvoiceMailState>}
*/
public getSaleInvoiceMailState(
saleInvoiceid: number,
): Promise<SaleInvoiceMailState> {
return this.getSaleInvoiceMailStateService.getInvoiceMailState(
saleInvoiceid,
);
}
}

View File

@@ -0,0 +1,285 @@
import {
Body,
Controller,
Delete,
Get,
HttpCode,
Param,
ParseIntPipe,
Post,
Put,
Query,
} from '@nestjs/common';
import {
ISaleInvoiceWriteoffDTO,
ISalesInvoicesFilter,
SaleInvoiceMailState,
SendInvoiceMailDTO,
} from './SaleInvoice.types';
import { SaleInvoiceApplication } from './SaleInvoices.application';
import {
ApiHeader,
ApiOperation,
ApiParam,
ApiResponse,
ApiTags,
} from '@nestjs/swagger';
import {
CreateSaleInvoiceDto,
EditSaleInvoiceDto,
} from './dtos/SaleInvoice.dto';
@Controller('sale-invoices')
@ApiTags('sale-invoices')
@ApiHeader({
name: 'organization-id',
description: 'The organization id',
required: true,
})
@ApiHeader({
name: 'x-access-token',
description: 'The authentication token',
required: true,
})
export class SaleInvoicesController {
constructor(private saleInvoiceApplication: SaleInvoiceApplication) {}
@Post()
@ApiOperation({ summary: 'Create a new sale invoice.' })
@ApiResponse({
status: 201,
description: 'Sale invoice created successfully',
})
createSaleInvoice(@Body() saleInvoiceDTO: CreateSaleInvoiceDto) {
return this.saleInvoiceApplication.createSaleInvoice(saleInvoiceDTO);
}
@Put(':id/mail')
@ApiOperation({ summary: 'Send the sale invoice mail.' })
@ApiResponse({
status: 200,
description: 'Sale invoice mail sent successfully',
})
@ApiResponse({ status: 404, description: 'The sale invoice not found.' })
@ApiParam({
name: 'id',
required: true,
type: Number,
description: 'The sale invoice id',
})
sendSaleInvoiceMail(
@Param('id', ParseIntPipe) id: number,
@Body() messageDTO: SendInvoiceMailDTO,
) {
return this.saleInvoiceApplication.sendSaleInvoiceMail(id, messageDTO);
}
@Put(':id')
@ApiOperation({ summary: 'Edit the given sale invoice.' })
@ApiResponse({
status: 200,
description: 'Sale invoice edited successfully',
})
@ApiResponse({ status: 404, description: 'The sale invoice not found.' })
@ApiParam({
name: 'id',
required: true,
type: Number,
description: 'The sale invoice id',
})
editSaleInvoice(
@Param('id', ParseIntPipe) id: number,
@Body() saleInvoiceDTO: EditSaleInvoiceDto,
) {
return this.saleInvoiceApplication.editSaleInvoice(id, saleInvoiceDTO);
}
@Delete(':id')
@ApiOperation({ summary: 'Delete the given sale invoice.' })
@ApiResponse({
status: 200,
description: 'The sale invoice has been successfully deleted.',
})
@ApiResponse({ status: 404, description: 'The sale invoice not found.' })
@ApiParam({
name: 'id',
required: true,
type: Number,
description: 'The sale invoice id',
})
deleteSaleInvoice(@Param('id', ParseIntPipe) id: number) {
return this.saleInvoiceApplication.deleteSaleInvoice(id);
}
@Get()
@ApiOperation({ summary: 'Retrieves the sale invoices.' })
@ApiResponse({
status: 200,
description: 'The sale invoices have been successfully retrieved.',
})
getSaleInvoices(@Query() filterDTO: ISalesInvoicesFilter) {
return this.saleInvoiceApplication.getSaleInvoices(filterDTO);
}
@Get(':id')
@ApiOperation({ summary: 'Retrieves the sale invoice details.' })
@ApiResponse({
status: 200,
description: 'The sale invoice details have been successfully retrieved.',
})
@ApiResponse({ status: 404, description: 'The sale invoice not found.' })
@ApiParam({
name: 'id',
required: true,
type: Number,
description: 'The sale invoice id',
})
getSaleInvoice(@Param('id', ParseIntPipe) id: number) {
return this.saleInvoiceApplication.getSaleInvoice(id);
}
@Get(':id/state')
@ApiOperation({ summary: 'Retrieves the sale invoice state.' })
@ApiResponse({
status: 200,
description: 'The sale invoice state has been successfully retrieved.',
})
@ApiResponse({ status: 404, description: 'The sale invoice not found.' })
getSaleInvoiceState() {
return this.saleInvoiceApplication.getSaleInvoiceState();
}
@Put(':id/deliver')
@ApiOperation({ summary: 'Deliver the given sale invoice.' })
@ApiResponse({
status: 200,
description: 'The sale invoice has been successfully marked asdelivered.',
})
@ApiResponse({ status: 404, description: 'The sale invoice not found.' })
@ApiParam({
name: 'id',
required: true,
type: Number,
description: 'The sale invoice id',
})
@HttpCode(200)
deliverSaleInvoice(@Param('id', ParseIntPipe) id: number) {
return this.saleInvoiceApplication.deliverSaleInvoice(id);
}
@Get('receivable/:customerId?')
@ApiOperation({ summary: 'Retrieves the receivable sale invoices.' })
@ApiResponse({
status: 200,
description:
'The receivable sale invoices have been successfully retrieved.',
})
@ApiResponse({ status: 404, description: 'The customer not found.' })
@ApiParam({
name: 'customerId',
required: false,
type: Number,
description: 'The customer id',
})
getReceivableSaleInvoices(@Param('customerId') customerId?: number) {
return this.saleInvoiceApplication.getReceivableSaleInvoices(customerId);
}
@Post(':id/writeoff')
@ApiOperation({ summary: 'Write off the given sale invoice.' })
@HttpCode(200)
@ApiResponse({
status: 200,
description: 'The sale invoice has been successfully written off.',
})
@ApiResponse({ status: 404, description: 'The sale invoice not found.' })
@ApiParam({
name: 'id',
required: true,
type: Number,
description: 'The sale invoice id',
})
writeOff(
@Param('id', ParseIntPipe) id: number,
@Body() writeoffDTO: ISaleInvoiceWriteoffDTO,
) {
return this.saleInvoiceApplication.writeOff(id, writeoffDTO);
}
@Post(':id/cancel-writeoff')
@ApiOperation({ summary: 'Cancel the written off sale invoice.' })
@ApiResponse({
status: 200,
description:
'The sale invoice has been successfully marked as not written off.',
})
@ApiResponse({ status: 404, description: 'The sale invoice not found.' })
@ApiParam({
name: 'id',
required: true,
type: Number,
description: 'The sale invoice id',
})
@HttpCode(200)
cancelWrittenoff(@Param('id', ParseIntPipe) id: number) {
return this.saleInvoiceApplication.cancelWrittenoff(id);
}
@Get(':id/payments')
@ApiOperation({ summary: 'Retrieves the sale invoice payments.' })
@ApiResponse({ status: 404, description: 'The sale invoice not found.' })
@ApiParam({
name: 'id',
required: true,
type: Number,
description: 'The sale invoice id',
})
getInvoicePayments(@Param('id', ParseIntPipe) id: number) {
return this.saleInvoiceApplication.getInvoicePayments(id);
}
@Get(':id/pdf')
@ApiOperation({ summary: 'Retrieves the sale invoice PDF.' })
@ApiResponse({ status: 404, description: 'The sale invoice not found.' })
@ApiParam({
name: 'id',
required: true,
type: Number,
description: 'The sale invoice id',
})
saleInvoicePdf(@Param('id', ParseIntPipe) id: number) {
return this.saleInvoiceApplication.saleInvoicePdf(id);
}
@Get(':id/html')
@ApiOperation({ summary: 'Retrieves the sale invoice HTML.' })
@ApiResponse({ status: 404, description: 'The sale invoice not found.' })
@ApiParam({
name: 'id',
required: true,
type: Number,
description: 'The sale invoice id',
})
saleInvoiceHtml(@Param('id', ParseIntPipe) id: number) {
return this.saleInvoiceApplication.saleInvoiceHtml(id);
}
@Get(':id/mail-state')
@ApiOperation({ summary: 'Retrieves the sale invoice mail state.' })
@ApiResponse({
status: 200,
description: 'Sale invoice mail state retrieved successfully',
})
@ApiResponse({ status: 404, description: 'The sale invoice not found.' })
@ApiParam({
name: 'id',
required: true,
type: Number,
description: 'The sale invoice id',
})
getSaleInvoiceMailState(
@Param('id', ParseIntPipe) id: number,
): Promise<SaleInvoiceMailState> {
return this.saleInvoiceApplication.getSaleInvoiceMailState(id);
}
}

View File

@@ -0,0 +1,124 @@
import { forwardRef, Module } from '@nestjs/common';
import { TenancyContext } from '../Tenancy/TenancyContext.service';
import { TenancyDatabaseModule } from '../Tenancy/TenancyDB/TenancyDB.module';
import { TransformerInjectable } from '../Transformer/TransformerInjectable.service';
import { CreateSaleInvoice } from './commands/CreateSaleInvoice.service';
import { DeleteSaleInvoice } from './commands/DeleteSaleInvoice.service';
import { DeliverSaleInvoice } from './commands/DeliverSaleInvoice.service';
import { EditSaleInvoice } from './commands/EditSaleInvoice.service';
import { GenerateShareLink } from './commands/GenerateInvoicePaymentLink.service';
import { SaleInvoiceIncrement } from './commands/SaleInvoiceIncrement.service';
import { GetInvoicePaymentMail } from './queries/GetInvoicePaymentMail.service';
import { GetSaleInvoice } from './queries/GetSaleInvoice.service';
import { GetSaleInvoicesPayable } from './queries/GetSaleInvoicesPayable.service';
import { GetSaleInvoiceState } from './queries/GetSaleInvoiceState.service';
import { SaleInvoicePdf } from './queries/SaleInvoicePdf.service';
import { SaleInvoiceApplication } from './SaleInvoices.application';
import { ItemsEntriesService } from '../Items/ItemsEntries.service';
import { CommandSaleInvoiceValidators } from './commands/CommandSaleInvoiceValidators.service';
import { CommandSaleInvoiceDTOTransformer } from './commands/CommandSaleInvoiceDTOTransformer.service';
import { SaleEstimateValidators } from '../SaleEstimates/commands/SaleEstimateValidators.service';
import { UnlinkConvertedSaleEstimate } from '../SaleEstimates/commands/UnlinkConvertedSaleEstimate.service';
import { PdfTemplatesModule } from '../PdfTemplate/PdfTemplates.module';
import { AutoIncrementOrdersModule } from '../AutoIncrementOrders/AutoIncrementOrders.module';
import { ChromiumlyTenancyModule } from '../ChromiumlyTenancy/ChromiumlyTenancy.module';
import { SaleInvoicePdfTemplate } from './queries/SaleInvoicePdfTemplate.service';
import { WriteoffSaleInvoice } from './commands/WriteoffSaleInvoice.service';
import { GetInvoicePaymentsService } from './queries/GetInvoicePayments.service';
import { BranchesModule } from '../Branches/Branches.module';
import { WarehousesModule } from '../Warehouses/Warehouses.module';
import { TaxRatesModule } from '../TaxRates/TaxRate.module';
import { SaleInvoicesController } from './SaleInvoices.controller';
import { InvoiceGLEntriesSubscriber } from './subscribers/InvoiceGLEntriesSubscriber';
import { SaleInvoiceGLEntries } from './ledger/InvoiceGLEntries';
import { LedgerModule } from '../Ledger/Ledger.module';
import { AccountsModule } from '../Accounts/Accounts.module';
import { SaleInvoiceWriteoffSubscriber } from './subscribers/SaleInvoiceWriteoffSubscriber';
import { SaleInvoiceWriteoffGLStorage } from './commands/writeoff/SaleInvoiceWriteoffGLStorage';
import { InvoiceInventoryTransactions } from './commands/inventory/InvoiceInventoryTransactions';
import { MailModule } from '../Mail/Mail.module';
import { GetSaleInvoicesService } from './queries/GetSaleInvoices';
import { SendSaleInvoiceMail } from './commands/SendSaleInvoiceMail';
import { GetSaleInvoiceMailState } from './queries/GetSaleInvoiceMailState.service';
import { InventoryCostModule } from '../InventoryCost/InventoryCost.module';
import { SendSaleInvoiceMailCommon } from './commands/SendInvoiceInvoiceMailCommon.service';
import { DynamicListModule } from '../DynamicListing/DynamicList.module';
import { MailNotificationModule } from '../MailNotification/MailNotification.module';
import { SendSaleInvoiceMailProcessor } from './processors/SendSaleInvoiceMail.processor';
import { BullModule } from '@nestjs/bull';
import { SendSaleInvoiceQueue } from './constants';
import { InvoicePaymentIntegrationSubscriber } from './subscribers/InvoicePaymentIntegrationSubscriber';
import { InvoiceChangeStatusOnMailSentSubscriber } from './subscribers/InvoiceChangeStatusOnMailSentSubscriber';
import { InvoiceCostGLEntriesSubscriber } from './subscribers/InvoiceCostGLEntriesSubscriber';
import { InvoicePaymentGLRewriteSubscriber } from './subscribers/InvoicePaymentGLRewriteSubscriber';
import { SaleInvoiceWriteInventoryTransactionsSubscriber } from './subscribers/InvoiceWriteInventoryTransactions';
import { SaleInvoiceCostGLEntries } from './SaleInvoiceCostGLEntries';
import { InvoicePaymentsGLEntriesRewrite } from './InvoicePaymentsGLRewrite';
import { PaymentsReceivedModule } from '../PaymentReceived/PaymentsReceived.module';
import { SaleInvoicesCost } from './SalesInvoicesCost';
@Module({
imports: [
TenancyDatabaseModule,
PdfTemplatesModule,
AutoIncrementOrdersModule,
ChromiumlyTenancyModule,
BranchesModule,
WarehousesModule,
TaxRatesModule,
PaymentsReceivedModule,
LedgerModule,
AccountsModule,
MailModule,
MailNotificationModule,
forwardRef(() => InventoryCostModule),
DynamicListModule,
BullModule.registerQueue({ name: SendSaleInvoiceQueue }),
],
controllers: [SaleInvoicesController],
providers: [
CreateSaleInvoice,
EditSaleInvoice,
DeleteSaleInvoice,
GetSaleInvoicesPayable,
DeliverSaleInvoice,
GenerateShareLink,
GetInvoicePaymentMail,
SaleInvoiceIncrement,
GetSaleInvoiceState,
GetSaleInvoice,
GetInvoicePaymentMail,
SaleInvoicePdf,
SaleInvoiceApplication,
TenancyContext,
TransformerInjectable,
ItemsEntriesService,
CommandSaleInvoiceValidators,
CommandSaleInvoiceDTOTransformer,
SaleEstimateValidators,
UnlinkConvertedSaleEstimate,
SaleInvoicePdfTemplate,
WriteoffSaleInvoice,
GetInvoicePaymentsService,
SaleInvoiceGLEntries,
InvoiceGLEntriesSubscriber,
SaleInvoiceWriteoffGLStorage,
SaleInvoiceWriteoffSubscriber,
InvoiceInventoryTransactions,
SendSaleInvoiceMail,
GetSaleInvoicesService,
GetSaleInvoiceMailState,
SendSaleInvoiceMailCommon,
SendSaleInvoiceMailProcessor,
SaleInvoiceCostGLEntries,
InvoicePaymentIntegrationSubscriber,
InvoiceChangeStatusOnMailSentSubscriber,
InvoiceCostGLEntriesSubscriber,
InvoicePaymentGLRewriteSubscriber,
SaleInvoiceWriteInventoryTransactionsSubscriber,
InvoicePaymentsGLEntriesRewrite,
SaleInvoicesCost,
],
exports: [GetSaleInvoice, SaleInvoicesCost, SaleInvoicePdf],
})
export class SaleInvoicesModule {}

View File

@@ -0,0 +1,150 @@
import { Mutex } from 'async-mutex';
import { chain } from 'lodash';
import moment from 'moment';
import { Knex } from 'knex';
import { Injectable } from '@nestjs/common';
import { UnitOfWork } from '../Tenancy/TenancyDB/UnitOfWork.service';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { events } from '@/common/events/events';
import { ModelObject } from 'objection';
import { InventoryTransaction } from '../InventoryCost/models/InventoryTransaction';
import { IInventoryCostLotsGLEntriesWriteEvent } from '../InventoryCost/types/InventoryCost.types';
import { InventoryComputeCostService } from '../InventoryCost/commands/InventoryComputeCost.service';
@Injectable()
export class SaleInvoicesCost {
constructor(
private readonly inventoryService: InventoryComputeCostService,
private readonly uow: UnitOfWork,
private readonly eventPublisher: EventEmitter2,
) {}
/**
* Schedule sale invoice re-compute based on the item
* cost method and starting date.
* @param {number[]} itemIds - Inventory items ids.
* @param {Date} startingDate - Starting compute cost date.
* @return {Promise<Agenda>}
*/
async scheduleComputeCostByItemsIds(
inventoryItemsIds: number[],
startingDate: Date,
): Promise<void> {
const mutex = new Mutex();
const asyncOpers = inventoryItemsIds.map(
async (inventoryItemId: number) => {
// @todo refactor the lock acquire to be distrbuted using Redis
// and run the cost schedule job after running invoice transaction.
const release = await mutex.acquire();
try {
await this.inventoryService.scheduleComputeItemCost(
inventoryItemId,
startingDate,
);
} finally {
release();
}
},
);
await Promise.all(asyncOpers);
}
/**
* Retrieve the max dated inventory transactions in the transactions that
* have the same item id.
* @param {ModelObject<InventoryTransaction>[]} inventoryTransactions
* @return {ModelObject<InventoryTransaction>[]}
*/
getMaxDateInventoryTransactions(
inventoryTransactions: ModelObject<InventoryTransaction>[],
): ModelObject<InventoryTransaction>[] {
return chain(inventoryTransactions)
.reduce((acc: any, transaction) => {
const compatatorDate = acc[transaction.itemId];
if (
!compatatorDate ||
moment(compatatorDate.date).isBefore(transaction.date)
) {
return {
...acc,
[transaction.itemId]: {
...transaction,
},
};
}
return acc;
}, {})
.values()
.value();
}
/**
* Computes items costs by the given inventory transaction.
* @param {number} tenantId
* @param {IInventoryTransaction[]} inventoryTransactions
*/
async computeItemsCostByInventoryTransactions(
inventoryTransactions: ModelObject<InventoryTransaction>[],
) {
const mutex = new Mutex();
const reducedTransactions = this.getMaxDateInventoryTransactions(
inventoryTransactions,
);
const asyncOpers = reducedTransactions.map(async (transaction) => {
const release = await mutex.acquire();
try {
await this.inventoryService.scheduleComputeItemCost(
transaction.itemId,
transaction.date,
);
} finally {
release();
}
});
await Promise.all([...asyncOpers]);
}
/**
* Schedule writing journal entries.
* @param {Date} startingDate - Starting date.
* @return {Promise<agenda>}
*/
scheduleWriteJournalEntries(startingDate?: Date) {
// const agenda = Container.get('agenda');
// return agenda.schedule('in 3 seconds', 'rewrite-invoices-journal-entries', {
// startingDate,
// tenantId,
// });
}
/**
* Writes cost GL entries from the inventory cost lots.
* @param {number} tenantId -
* @param {Date} startingDate -
* @returns {Promise<void>}
*/
public writeCostLotsGLEntries = (startingDate: Date) => {
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
// Triggers event `onInventoryCostLotsGLEntriesBeforeWrite`.
await this.eventPublisher.emitAsync(
events.inventory.onCostLotsGLEntriesBeforeWrite,
{
startingDate,
trx,
} as IInventoryCostLotsGLEntriesWriteEvent,
);
// Triggers event `onInventoryCostLotsGLEntriesWrite`.
await this.eventPublisher.emitAsync(
events.inventory.onCostLotsGLEntriesWrite,
{
startingDate,
trx,
} as IInventoryCostLotsGLEntriesWriteEvent,
);
});
};
}

View File

@@ -0,0 +1,162 @@
import { Inject, Injectable } from '@nestjs/common';
import { omit, sumBy } from 'lodash';
import * as R from 'ramda';
import * as moment from 'moment';
import '../../../utils/moment-mysql';
import * as composeAsync from 'async/compose';
import { Customer } from '@/modules/Customers/models/Customer';
import { BranchTransactionDTOTransformer } from '@/modules/Branches/integrations/BranchTransactionDTOTransform';
import { WarehouseTransactionDTOTransform } from '@/modules/Warehouses/Integrations/WarehouseTransactionDTOTransform';
import { ItemsEntriesService } from '@/modules/Items/ItemsEntries.service';
import { ItemEntry } from '@/modules/TransactionItemEntry/models/ItemEntry';
import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators.service';
import { SaleInvoiceIncrement } from './SaleInvoiceIncrement.service';
import { BrandingTemplateDTOTransformer } from '@/modules/PdfTemplate/BrandingTemplateDTOTransformer';
import { SaleInvoice } from '../models/SaleInvoice';
import { assocItemEntriesDefaultIndex } from '@/utils/associate-item-entries-index';
import { formatDateFields } from '@/utils/format-date-fields';
import { ItemEntriesTaxTransactions } from '@/modules/TaxRates/ItemEntriesTaxTransactions.service';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
import {
CreateSaleInvoiceDto,
EditSaleInvoiceDto,
} from '../dtos/SaleInvoice.dto';
@Injectable()
export class CommandSaleInvoiceDTOTransformer {
/**
* @param {BranchTransactionDTOTransformer} branchDTOTransform - Branch transaction DTO transformer.
* @param {WarehouseTransactionDTOTransform} warehouseDTOTransform - Warehouse transaction DTO transformer.
* @param {ItemsEntriesService} itemsEntriesService - Items entries service.
* @param {CommandSaleInvoiceValidators} validators - Command sale invoice validators.
* @param {SaleInvoiceIncrement} invoiceIncrement - Sale invoice increment.
* @param {ItemEntriesTaxTransactions} taxDTOTransformer - Item entries tax transactions.
* @param {BrandingTemplateDTOTransformer} brandingTemplatesTransformer - Branding template DTO transformer.
* @param {TenancyContext} tenancyContext - Tenancy context.
* @param {SaleInvoice} saleInvoiceModel - Sale invoice model.
*/
constructor(
private branchDTOTransform: BranchTransactionDTOTransformer,
private warehouseDTOTransform: WarehouseTransactionDTOTransform,
private itemsEntriesService: ItemsEntriesService,
private validators: CommandSaleInvoiceValidators,
private invoiceIncrement: SaleInvoiceIncrement,
private taxDTOTransformer: ItemEntriesTaxTransactions,
private brandingTemplatesTransformer: BrandingTemplateDTOTransformer,
private tenancyContext: TenancyContext,
) {}
/**
* Transformes the create DTO to invoice object model.
* @param {ISaleInvoiceCreateDTO} saleInvoiceDTO - Sale invoice DTO.
* @param {ISaleInvoice} oldSaleInvoice - Old sale invoice.
* @return {ISaleInvoice}
*/
public async transformDTOToModel(
customer: Customer,
saleInvoiceDTO: CreateSaleInvoiceDto | EditSaleInvoiceDto,
oldSaleInvoice?: SaleInvoice,
): Promise<SaleInvoice> {
const entriesModels = this.transformDTOEntriesToModels(saleInvoiceDTO);
const amount = this.getDueBalanceItemEntries(entriesModels);
// Retreive the next invoice number.
const autoNextNumber = await this.invoiceIncrement.getNextInvoiceNumber();
// Retrieve the authorized user.
const authorizedUser = await this.tenancyContext.getSystemUser();
// Invoice number.
const invoiceNo =
saleInvoiceDTO.invoiceNo || oldSaleInvoice?.invoiceNo || autoNextNumber;
// Validate the invoice is required.
this.validators.validateInvoiceNoRequire(invoiceNo);
const initialEntries = saleInvoiceDTO.entries.map((entry) => ({
referenceType: 'SaleInvoice',
isInclusiveTax: saleInvoiceDTO.isInclusiveTax,
...entry,
}));
const asyncEntries = await composeAsync(
// Associate tax rate from tax id to entries.
this.taxDTOTransformer.assocTaxRateFromTaxIdToEntries,
// Associate tax rate id from tax code to entries.
this.taxDTOTransformer.assocTaxRateIdFromCodeToEntries,
// Sets default cost and sell account to invoice items entries.
this.itemsEntriesService.setItemsEntriesDefaultAccounts,
)(initialEntries);
const entries = R.compose(
// Remove tax code from entries.
R.map(R.omit(['taxCode'])),
// Associate the default index for each item entry lin.
assocItemEntriesDefaultIndex,
)(asyncEntries);
const initialDTO = {
...formatDateFields(
omit(saleInvoiceDTO, [
'delivered',
'entries',
'fromEstimateId',
'attachments',
]),
['invoiceDate', 'dueDate'],
),
// Avoid rewrite the deliver date in edit mode when already published.
balance: amount,
currencyCode: customer.currencyCode,
exchangeRate: saleInvoiceDTO.exchangeRate || 1,
...(saleInvoiceDTO.delivered &&
!oldSaleInvoice?.deliveredAt && {
deliveredAt: moment().toMySqlDateTime(),
}),
// Avoid override payment amount in edit mode.
...(!oldSaleInvoice && { paymentAmount: 0 }),
...(invoiceNo ? { invoiceNo } : {}),
entries,
userId: authorizedUser.id,
} as SaleInvoice;
const initialAsyncDTO = await composeAsync(
this.branchDTOTransform.transformDTO<SaleInvoice>,
this.warehouseDTOTransform.transformDTO<SaleInvoice>,
// Assigns the default branding template id to the invoice DTO.
this.brandingTemplatesTransformer.assocDefaultBrandingTemplate(
'SaleInvoice',
),
)(initialDTO);
return R.compose(this.taxDTOTransformer.assocTaxAmountWithheldFromEntries)(
initialAsyncDTO,
);
}
/**
* Transforms the DTO entries to invoice entries models.
* @param {ISaleInvoiceCreateDTO | ISaleInvoiceEditDTO} entries
* @returns {IItemEntry[]}
*/
private transformDTOEntriesToModels = (
saleInvoiceDTO: CreateSaleInvoiceDto | EditSaleInvoiceDto,
): ItemEntry[] => {
return saleInvoiceDTO.entries.map((entry) => {
return ItemEntry.fromJson({
...entry,
isInclusiveTax: saleInvoiceDTO.isInclusiveTax,
});
});
};
/**
* Gets the due balance from the invoice entries.
* @param {IItemEntry[]} entries
* @returns {number}
*/
private getDueBalanceItemEntries = (entries: ItemEntry[]) => {
return sumBy(entries, (e) => e.amount);
};
}

View File

@@ -0,0 +1,83 @@
import { Inject, Injectable } from '@nestjs/common';
import { SaleInvoice } from '../models/SaleInvoice';
import { ServiceError } from '@/modules/Items/ServiceError';
import { ERRORS } from '../constants';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable()
export class CommandSaleInvoiceValidators {
constructor(
@Inject(SaleInvoice.name)
private readonly saleInvoiceModel: TenantModelProxy<typeof SaleInvoice>,
) {}
/**
* Validates the given invoice is existance.
* @param {SaleInvoice | undefined} invoice
*/
public validateInvoiceExistance(invoice: SaleInvoice | undefined) {
if (!invoice) {
throw new ServiceError(ERRORS.SALE_INVOICE_NOT_FOUND);
}
}
/**
* Validate whether sale invoice number unqiue on the storage.
* @param {string} invoiceNumber -
* @param {number} notInvoiceId -
*/
public async validateInvoiceNumberUnique(
invoiceNumber: string,
notInvoiceId?: number,
) {
const saleInvoice = await this.saleInvoiceModel()
.query()
.findOne('invoice_no', invoiceNumber)
.onBuild((builder) => {
if (notInvoiceId) {
builder.whereNot('id', notInvoiceId);
}
});
if (saleInvoice) {
throw new ServiceError(ERRORS.INVOICE_NUMBER_NOT_UNIQUE);
}
}
/**
* Validate the invoice amount is bigger than payment amount before edit the invoice.
* @param {number} saleInvoiceAmount
* @param {number} paymentAmount
*/
public validateInvoiceAmountBiggerPaymentAmount(
saleInvoiceAmount: number,
paymentAmount: number,
) {
if (saleInvoiceAmount < paymentAmount) {
throw new ServiceError(ERRORS.INVOICE_AMOUNT_SMALLER_THAN_PAYMENT_AMOUNT);
}
}
/**
* Validate the invoice number require.
* @param {ISaleInvoice} saleInvoiceObj
*/
public validateInvoiceNoRequire(invoiceNo: string) {
if (!invoiceNo) {
throw new ServiceError(ERRORS.SALE_INVOICE_NO_IS_REQUIRED);
}
}
/**
* Validate the given customer has no sales invoices.
* @param {number} customerId - Customer id.
*/
public async validateCustomerHasNoInvoices(customerId: number) {
const invoices = await this.saleInvoiceModel()
.query()
.where('customer_id', customerId);
if (invoices.length > 0) {
throw new ServiceError(ERRORS.CUSTOMER_HAS_SALES_INVOICES);
}
}
}

View File

@@ -0,0 +1,138 @@
import { Inject, Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { Knex } from 'knex';
import {
ISaleInvoiceCreatedPayload,
ISaleInvoiceCreatingPaylaod,
} from '../SaleInvoice.types';
import { ItemsEntriesService } from '@/modules/Items/ItemsEntries.service';
import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators.service';
import { CommandSaleInvoiceDTOTransformer } from './CommandSaleInvoiceDTOTransformer.service';
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
import { SaleEstimateValidators } from '@/modules/SaleEstimates/commands/SaleEstimateValidators.service';
import { SaleInvoice } from '../models/SaleInvoice';
import { SaleEstimate } from '@/modules/SaleEstimates/models/SaleEstimate';
import { Customer } from '@/modules/Customers/models/Customer';
import { events } from '@/common/events/events';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { CreateSaleInvoiceDto } from '../dtos/SaleInvoice.dto';
@Injectable()
export class CreateSaleInvoice {
/**
* @param {ItemsEntriesService} itemsEntriesService - Items entries service.
* @param {CommandSaleInvoiceValidators} validators - Command sale invoice validators.
* @param {CommandSaleInvoiceDTOTransformer} transformerDTO - Command sale invoice DTO transformer.
* @param {EventEmitter2} eventPublisher - Event emitter.
* @param {SaleEstimateValidators} commandEstimateValidators - Command sale estimate validators.
* @param {UnitOfWork} uow - Unit of work.
* @param {TenantModelProxy<typeof SaleInvoice>} saleInvoiceModel - Sale invoice model.
* @param {TenantModelProxy<typeof SaleEstimate>} saleEstimateModel - Sale estimate model.
* @param {TenantModelProxy<typeof Customer>} customerModel - Customer model.
*/
constructor(
private readonly itemsEntriesService: ItemsEntriesService,
private readonly validators: CommandSaleInvoiceValidators,
private readonly transformerDTO: CommandSaleInvoiceDTOTransformer,
private readonly eventPublisher: EventEmitter2,
private readonly commandEstimateValidators: SaleEstimateValidators,
private readonly uow: UnitOfWork,
@Inject(SaleInvoice.name)
private readonly saleInvoiceModel: TenantModelProxy<typeof SaleInvoice>,
@Inject(SaleEstimate.name)
private readonly saleEstimateModel: TenantModelProxy<typeof SaleEstimate>,
@Inject(Customer.name)
private readonly customerModel: TenantModelProxy<typeof Customer>,
) {}
/**
* Creates a new sale invoices and store it to the storage
* with associated to entries and journal transactions.
* @async
* @param {number} tenantId - Tenant id.
* @param {ISaleInvoice} saleInvoiceDTO - Sale invoice object DTO.
* @return {Promise<ISaleInvoice>}
*/
public createSaleInvoice = async (
saleInvoiceDTO: CreateSaleInvoiceDto,
trx?: Knex.Transaction,
): Promise<SaleInvoice> => {
// Validate customer existance.
const customer = await this.customerModel()
.query()
.findById(saleInvoiceDTO.customerId)
.throwIfNotFound();
// Validate the from estimate id exists on the storage.
if (saleInvoiceDTO.fromEstimateId) {
const fromEstimate = await this.saleEstimateModel()
.query()
.findById(saleInvoiceDTO.fromEstimateId)
.throwIfNotFound();
// Validate the sale estimate is not already converted to invoice.
this.commandEstimateValidators.validateEstimateNotConverted(fromEstimate);
}
// Validate items ids existance.
await this.itemsEntriesService.validateItemsIdsExistance(
saleInvoiceDTO.entries,
);
// Validate items should be sellable items.
await this.itemsEntriesService.validateNonSellableEntriesItems(
saleInvoiceDTO.entries,
);
// Transform DTO object to model object.
const saleInvoiceObj = await this.transformCreateDTOToModel(
customer,
saleInvoiceDTO,
// authorizedUser,
);
// Validate sale invoice number uniquiness.
if (saleInvoiceObj.invoiceNo) {
await this.validators.validateInvoiceNumberUnique(
saleInvoiceObj.invoiceNo,
);
}
// Creates a new sale invoice and associated transactions under unit of work env.
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
// Triggers `onSaleInvoiceCreating` event.
await this.eventPublisher.emitAsync(events.saleInvoice.onCreating, {
saleInvoiceDTO,
trx,
} as ISaleInvoiceCreatingPaylaod);
// Create sale invoice graph to the storage.
const saleInvoice = await this.saleInvoiceModel()
.query(trx)
.upsertGraph(saleInvoiceObj);
const eventPayload: ISaleInvoiceCreatedPayload = {
saleInvoice,
saleInvoiceDTO,
saleInvoiceId: saleInvoice.id,
trx,
};
// Triggers the event `onSaleInvoiceCreated`.
await this.eventPublisher.emitAsync(
events.saleInvoice.onCreated,
eventPayload,
);
return saleInvoice;
}, trx);
};
/**
* Transformes create DTO to model.
* @param {Customer} customer -
* @param {ISaleInvoiceCreateDTO} saleInvoiceDTO -
*/
private transformCreateDTOToModel = async (
customer: Customer,
saleInvoiceDTO: CreateSaleInvoiceDto,
) => {
return this.transformerDTO.transformDTOToModel(customer, saleInvoiceDTO);
};
}

View File

@@ -0,0 +1,143 @@
import { Inject, Injectable } from '@nestjs/common';
import { Knex } from 'knex';
import {
ISaleInvoiceDeletePayload,
ISaleInvoiceDeletedPayload,
ISaleInvoiceDeletingPayload,
} from '../SaleInvoice.types';
import { ItemEntry } from '@/modules/TransactionItemEntry/models/ItemEntry';
import { SaleInvoice } from '../models/SaleInvoice';
import { UnlinkConvertedSaleEstimate } from '@/modules/SaleEstimates/commands/UnlinkConvertedSaleEstimate.service';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
import { ServiceError } from '@/modules/Items/ServiceError';
import { ERRORS } from '../constants';
import { events } from '@/common/events/events';
import { PaymentReceivedEntry } from '@/modules/PaymentReceived/models/PaymentReceivedEntry';
import { CreditNoteAppliedInvoice } from '@/modules/CreditNotesApplyInvoice/models/CreditNoteAppliedInvoice';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable()
export class DeleteSaleInvoice {
/**
* @param {UnlinkConvertedSaleEstimate} unlockEstimateFromInvoice - Unlink converted sale estimate service.
* @param {EventEmitter2} eventPublisher - Event emitter.
* @param {UnitOfWork} uow - Unit of work.
* @param {TenantModelProxy<typeof PaymentReceivedEntry>} paymentReceivedEntryModel - Payment received entry model.
* @param {TenantModelProxy<typeof CreditNoteAppliedInvoice>} creditNoteAppliedInvoiceModel - Credit note applied invoice model.
* @param {TenantModelProxy<typeof SaleInvoice>} saleInvoiceModel - Sale invoice model.
*/
constructor(
private unlockEstimateFromInvoice: UnlinkConvertedSaleEstimate,
private eventPublisher: EventEmitter2,
private uow: UnitOfWork,
@Inject(PaymentReceivedEntry.name)
private paymentReceivedEntryModel: TenantModelProxy<
typeof PaymentReceivedEntry
>,
@Inject(CreditNoteAppliedInvoice.name)
private creditNoteAppliedInvoiceModel: TenantModelProxy<
typeof CreditNoteAppliedInvoice
>,
@Inject(SaleInvoice.name)
private saleInvoiceModel: TenantModelProxy<typeof SaleInvoice>,
@Inject(ItemEntry.name)
private itemEntryModel: TenantModelProxy<typeof ItemEntry>,
) {}
/**
* Validate the sale invoice has no payment entries.
* @param {number} saleInvoiceId
*/
private async validateInvoiceHasNoPaymentEntries(saleInvoiceId: number) {
// Retrieve the sale invoice associated payment receive entries.
const entries = await this.paymentReceivedEntryModel()
.query()
.where('invoice_id', saleInvoiceId);
if (entries.length > 0) {
throw new ServiceError(ERRORS.INVOICE_HAS_ASSOCIATED_PAYMENT_ENTRIES);
}
return entries;
}
/**
* Validate the sale invoice has no applied to credit note transaction.
* @param {number} invoiceId - Invoice id.
* @returns {Promise<void>}
*/
public validateInvoiceHasNoAppliedToCredit = async (
invoiceId: number,
): Promise<void> => {
const appliedTransactions = await this.creditNoteAppliedInvoiceModel()
.query()
.where('invoiceId', invoiceId);
if (appliedTransactions.length > 0) {
throw new ServiceError(ERRORS.SALE_INVOICE_HAS_APPLIED_TO_CREDIT_NOTES);
}
};
/**
* Deletes the given sale invoice with associated entries
* and journal transactions.
* @param {Number} saleInvoiceId - The given sale invoice id.
* @param {ISystemUser} authorizedUser -
*/
public async deleteSaleInvoice(saleInvoiceId: number): Promise<void> {
// Retrieve the given sale invoice with associated entries
// or throw not found error.
const oldSaleInvoice = await this.saleInvoiceModel()
.query()
.findById(saleInvoiceId)
.withGraphFetched('entries')
.withGraphFetched('paymentMethods')
.throwIfNotFound();
// Validate the sale invoice has no associated payment entries.
await this.validateInvoiceHasNoPaymentEntries(saleInvoiceId);
// Validate the sale invoice has applied to credit note transaction.
await this.validateInvoiceHasNoAppliedToCredit(saleInvoiceId);
// Triggers `onSaleInvoiceDelete` event.
await this.eventPublisher.emitAsync(events.saleInvoice.onDelete, {
oldSaleInvoice,
saleInvoiceId,
} as ISaleInvoiceDeletePayload);
// Deletes sale invoice transaction and associate transactions with UOW env.
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
// Triggers `onSaleInvoiceDeleting` event.
await this.eventPublisher.emitAsync(events.saleInvoice.onDeleting, {
oldSaleInvoice,
saleInvoiceId,
trx,
} as ISaleInvoiceDeletingPayload);
// Unlink the converted sale estimates from the given sale invoice.
await this.unlockEstimateFromInvoice.unlinkConvertedEstimateFromInvoice(
saleInvoiceId,
trx,
);
await this.itemEntryModel()
.query(trx)
.where('reference_id', saleInvoiceId)
.where('reference_type', 'SaleInvoice')
.delete();
await this.saleInvoiceModel().query(trx).findById(saleInvoiceId).delete();
// Triggers `onSaleInvoiceDeleted` event.
await this.eventPublisher.emitAsync(events.saleInvoice.onDeleted, {
oldSaleInvoice,
saleInvoiceId,
trx,
} as ISaleInvoiceDeletedPayload);
});
}
}

View File

@@ -0,0 +1,77 @@
import { Inject, Injectable } from '@nestjs/common';
import { Knex } from 'knex';
import * as moment from 'moment';
import {
ISaleInvoiceDeliveringPayload,
ISaleInvoiceEventDeliveredPayload,
} from '../SaleInvoice.types';
import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators.service';
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { events } from '@/common/events/events';
import { ERRORS } from '../constants';
import { SaleInvoice } from '../models/SaleInvoice';
import { ServiceError } from '@/modules/Items/ServiceError';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable()
export class DeliverSaleInvoice {
/**
* @param {EventEmitter2} eventEmitter - Event emitter.
* @param {UnitOfWork} uow - Unit of work.
* @param {CommandSaleInvoiceValidators} validators - Command sale invoice validators.
* @param {TenantModelProxy<typeof SaleInvoice>} saleInvoiceModel - Sale invoice model.
*/
constructor(
private eventEmitter: EventEmitter2,
private uow: UnitOfWork,
private validators: CommandSaleInvoiceValidators,
@Inject(SaleInvoice.name)
private saleInvoiceModel: TenantModelProxy<typeof SaleInvoice>,
) {}
/**
* Deliver the given sale invoice.
* @param {number} saleInvoiceId - Sale invoice id.
* @return {Promise<void>}
*/
public async deliverSaleInvoice(saleInvoiceId: number): Promise<void> {
// Retrieve details of the given sale invoice id.
const oldSaleInvoice = await this.saleInvoiceModel()
.query()
.findById(saleInvoiceId);
// Validates the given invoice existence.
this.validators.validateInvoiceExistance(oldSaleInvoice);
// Throws error in case the sale invoice already published.
if (oldSaleInvoice.isDelivered) {
throw new ServiceError(ERRORS.SALE_INVOICE_ALREADY_DELIVERED);
}
// Update sale invoice transaction with associate transactions
// under unit-of-work environment.
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
// Triggers `onSaleInvoiceDelivering` event.
await this.eventEmitter.emitAsync(events.saleInvoice.onDelivering, {
oldSaleInvoice,
trx,
} as ISaleInvoiceDeliveringPayload);
// Record the delivered at on the storage.
const saleInvoice = await this.saleInvoiceModel()
.query(trx)
.patchAndFetchById(saleInvoiceId, {
deliveredAt: moment().toMySqlDateTime(),
})
.withGraphFetched('entries');
// Triggers `onSaleInvoiceDelivered` event.
await this.eventEmitter.emitAsync(events.saleInvoice.onDelivered, {
saleInvoiceId,
saleInvoice,
trx,
} as ISaleInvoiceEventDeliveredPayload);
});
}
}

View File

@@ -0,0 +1,152 @@
import { Knex } from 'knex';
import { Inject, Injectable } from '@nestjs/common';
import {
ISaleInvoiceEditDTO,
ISaleInvoiceEditedPayload,
ISaleInvoiceEditingPayload,
} from '../SaleInvoice.types';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators.service';
import { CommandSaleInvoiceDTOTransformer } from './CommandSaleInvoiceDTOTransformer.service';
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
import { ItemsEntriesService } from '@/modules/Items/ItemsEntries.service';
import { events } from '@/common/events/events';
import { SaleInvoice } from '../models/SaleInvoice';
import { Customer } from '@/modules/Customers/models/Customer';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { EditSaleInvoiceDto } from '../dtos/SaleInvoice.dto';
@Injectable()
export class EditSaleInvoice {
/**
* @param {ItemsEntriesService} itemsEntriesService - Items entries service.
* @param {EventEmitter2} eventPublisher - Event emitter.
* @param {CommandSaleInvoiceValidators} validators - Command sale invoice validators.
* @param {CommandSaleInvoiceDTOTransformer} transformerDTO - Command sale invoice DTO transformer.
* @param {UnitOfWork} uow - Unit of work.
* @param {TenantModelProxy<typeof SaleInvoice>} saleInvoiceModel - Sale invoice model.
* @param {TenantModelProxy<typeof Customer>} customerModel - Customer model.
*/
constructor(
private readonly itemsEntriesService: ItemsEntriesService,
private readonly eventPublisher: EventEmitter2,
private readonly validators: CommandSaleInvoiceValidators,
private readonly transformerDTO: CommandSaleInvoiceDTOTransformer,
private readonly uow: UnitOfWork,
@Inject(SaleInvoice.name)
private readonly saleInvoiceModel: TenantModelProxy<typeof SaleInvoice>,
@Inject(Customer.name)
private readonly customerModel: TenantModelProxy<typeof Customer>,
) {}
/**
* Edit the given sale invoice.
* @async
* @param {Number} saleInvoiceId - Sale invoice id.
* @param {ISaleInvoice} saleInvoice - Sale invoice DTO object.
* @return {Promise<ISaleInvoice>}
*/
public async editSaleInvoice(
saleInvoiceId: number,
saleInvoiceDTO: EditSaleInvoiceDto,
): Promise<SaleInvoice> {
// Retrieve the sale invoice or throw not found service error.
const oldSaleInvoice = await this.saleInvoiceModel()
.query()
.findById(saleInvoiceId)
.withGraphJoined('entries');
// Validates the given invoice existance.
this.validators.validateInvoiceExistance(oldSaleInvoice);
// Validate customer existance.
const customer = await this.customerModel()
.query()
.findById(saleInvoiceDTO.customerId)
.throwIfNotFound();
// Validate items ids existance.
await this.itemsEntriesService.validateItemsIdsExistance(
saleInvoiceDTO.entries,
);
// Validate non-sellable entries items.
await this.itemsEntriesService.validateNonSellableEntriesItems(
saleInvoiceDTO.entries,
);
// Validate the items entries existance.
await this.itemsEntriesService.validateEntriesIdsExistance(
saleInvoiceId,
'SaleInvoice',
saleInvoiceDTO.entries,
);
// Transform DTO object to model object.
const saleInvoiceObj = await this.tranformEditDTOToModel(
customer,
saleInvoiceDTO,
oldSaleInvoice,
);
// Validate sale invoice number uniquiness.
if (saleInvoiceObj.invoiceNo) {
await this.validators.validateInvoiceNumberUnique(
saleInvoiceObj.invoiceNo,
saleInvoiceId,
);
}
// Validate the invoice amount is not smaller than the invoice payment amount.
this.validators.validateInvoiceAmountBiggerPaymentAmount(
saleInvoiceObj.balance,
oldSaleInvoice.paymentAmount,
);
// Edit sale invoice transaction in UOW envirment.
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
// Triggers `onSaleInvoiceEditing` event.
await this.eventPublisher.emitAsync(events.saleInvoice.onEditing, {
trx,
oldSaleInvoice,
saleInvoiceDTO,
} as ISaleInvoiceEditingPayload);
// Upsert the the invoice graph to the storage.
const saleInvoice = await this.saleInvoiceModel()
.query()
.upsertGraphAndFetch({
id: saleInvoiceId,
...saleInvoiceObj,
});
// Edit event payload.
const editEventPayload: ISaleInvoiceEditedPayload = {
saleInvoiceId,
saleInvoice,
saleInvoiceDTO,
oldSaleInvoice,
trx,
};
// Triggers `onSaleInvoiceEdited` event.
await this.eventPublisher.emitAsync(
events.saleInvoice.onEdited,
editEventPayload,
);
return saleInvoice;
});
}
/**
* Transformes edit DTO to model.
* @param {ICustomer} customer -
* @param {ISaleInvoiceEditDTO} saleInvoiceDTO -
* @param {ISaleInvoice} oldSaleInvoice
*/
private tranformEditDTOToModel = async (
customer: Customer,
saleInvoiceDTO: EditSaleInvoiceDto,
oldSaleInvoice: SaleInvoice,
) => {
return this.transformerDTO.transformDTOToModel(
customer,
saleInvoiceDTO,
oldSaleInvoice,
);
};
}

View File

@@ -0,0 +1,78 @@
import { Knex } from 'knex';
import { Inject, Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { v4 as uuidv4 } from 'uuid';
import { GeneratePaymentLinkTransformer } from './GeneratePaymentLink.transformer';
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
import { events } from '@/common/events/events';
import { PaymentLink } from '@/modules/PaymentLinks/models/PaymentLink';
import { SaleInvoice } from '../models/SaleInvoice';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable()
export class GenerateShareLink {
constructor(
private uow: UnitOfWork,
private eventPublisher: EventEmitter2,
private transformer: TransformerInjectable,
@Inject(SaleInvoice.name)
private saleInvoiceModel: TenantModelProxy<typeof SaleInvoice>,
@Inject(PaymentLink.name)
private paymentLinkModel: TenantModelProxy<typeof PaymentLink>,
) {}
/**
* Generates private or public payment link for the given sale invoice.
* @param {number} saleInvoiceId - Sale invoice id.
* @param {string} publicity - Public or private.
* @param {string} expiryTime - Expiry time.
*/
async generatePaymentLink(
saleInvoiceId: number,
publicity: string = 'private',
expiryTime: string = '',
) {
const foundInvoice = await this.saleInvoiceModel()
.query()
.findById(saleInvoiceId)
.throwIfNotFound();
// Generate unique uuid for sharable link.
const linkId = uuidv4() as string;
const commonEventPayload = {
saleInvoiceId,
publicity,
expiryTime,
};
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
// Triggers `onPublicSharableLinkGenerating` event.
await this.eventPublisher.emitAsync(
events.saleInvoice.onPublicLinkGenerating,
{ ...commonEventPayload, trx },
);
const paymentLink = await this.paymentLinkModel().query().insert({
linkId,
publicity,
resourceId: foundInvoice.id,
resourceType: 'SaleInvoice',
});
// Triggers `onPublicSharableLinkGenerated` event.
await this.eventPublisher.emitAsync(
events.saleInvoice.onPublicLinkGenerated,
{
...commonEventPayload,
paymentLink,
trx,
},
);
return this.transformer.transform(
paymentLink,
new GeneratePaymentLinkTransformer(),
);
});
}
}

View File

@@ -0,0 +1,28 @@
import { Transformer } from '@/modules/Transformer/Transformer';
import { PUBLIC_PAYMENT_LINK } from '../constants';
export class GeneratePaymentLinkTransformer extends Transformer {
/**
* Exclude these attributes from payment link object.
* @returns {Array}
*/
public excludeAttributes = (): string[] => {
return ['linkId'];
};
/**
* Included attributes.
* @returns {string[]}
*/
public includeAttributes = (): string[] => {
return ['link'];
};
/**
* Retrieves the public/private payment linl
* @returns {string}
*/
public link(link) {
return PUBLIC_PAYMENT_LINK?.replace('{PAYMENT_LINK_ID}', link.linkId);
}
}

View File

@@ -0,0 +1,30 @@
import { Injectable } from '@nestjs/common';
import { AutoIncrementOrdersService } from '../../AutoIncrementOrders/AutoIncrementOrders.service';
@Injectable()
export class SaleInvoiceIncrement {
constructor(
private readonly autoIncrementOrdersService: AutoIncrementOrdersService,
) {}
/**
* Retrieves the next unique invoice number.
* @param {number} tenantId - Tenant id.
* @return {Promise<string>}
*/
public getNextInvoiceNumber(): Promise<string> {
return this.autoIncrementOrdersService.getNextTransactionNumber(
'sales_invoices',
);
}
/**
* Increment the invoice next number.
* @param {number} tenantId -
*/
public incrementNextInvoiceNumber() {
return this.autoIncrementOrdersService.incrementSettingsNextNumber(
'sales_invoices',
);
}
}

View File

@@ -0,0 +1,35 @@
// import { Inject, Service } from 'typedi';
// import { ISalesInvoicesFilter } from '@/interfaces';
// import { SaleInvoiceApplication } from './SaleInvoices.application';
// import { Exportable } from '@/services/Export/Exportable';
// import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants';
// @Service()
// export class SaleInvoicesExportable extends Exportable {
// @Inject()
// private saleInvoicesApplication: SaleInvoiceApplication;
// /**
// * Retrieves the accounts data to exportable sheet.
// * @param {number} tenantId
// * @returns
// */
// public exportable(tenantId: number, query: ISalesInvoicesFilter) {
// const filterQuery = (query) => {
// query.withGraphFetched('branch');
// query.withGraphFetched('warehouse');
// };
// const parsedQuery = {
// sortOrder: 'desc',
// columnSortBy: 'created_at',
// ...query,
// page: 1,
// pageSize: EXPORT_SIZE_LIMIT,
// filterQuery,
// } as ISalesInvoicesFilter;
// return this.saleInvoicesApplication
// .getSaleInvoices(tenantId, parsedQuery)
// .then((output) => output.salesInvoices);
// }
// }

View File

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

View File

@@ -0,0 +1,118 @@
// @ts-nocheck
import { GetSaleInvoice } from '../queries/GetSaleInvoice.service';
import {
DEFAULT_INVOICE_MAIL_CONTENT,
DEFAULT_INVOICE_MAIL_SUBJECT,
} from '../constants';
import { GetInvoicePaymentMail } from '../queries/GetInvoicePaymentMail.service';
import { GenerateShareLink } from './GenerateInvoicePaymentLink.service';
import { Inject, Injectable } from '@nestjs/common';
import { SaleInvoice } from '../models/SaleInvoice';
import { ContactMailNotification } from '@/modules/MailNotification/ContactMailNotification';
import { SaleInvoiceMailOptions } from '../SaleInvoice.types';
@Injectable()
export class SendSaleInvoiceMailCommon {
constructor(
private getSaleInvoiceService: GetSaleInvoice,
private contactMailNotification: ContactMailNotification,
private getInvoicePaymentMail: GetInvoicePaymentMail,
private generatePaymentLinkService: GenerateShareLink,
@Inject(SaleInvoice.name)
private readonly saleInvoiceModel: () => typeof SaleInvoice,
) {}
/**
* Retrieves the mail options.
* @param {number} invoiceId - Invoice id.
* @param {string} defaultSubject - Subject text.
* @param {string} defaultBody - Subject body.
* @returns {Promise<SaleInvoiceMailOptions>}
*/
public async getInvoiceMailOptions(
invoiceId: number,
defaultSubject: string = DEFAULT_INVOICE_MAIL_SUBJECT,
defaultMessage: string = DEFAULT_INVOICE_MAIL_CONTENT,
): Promise<SaleInvoiceMailOptions> {
const saleInvoice = await this.saleInvoiceModel()
.query()
.findById(invoiceId)
.throwIfNotFound();
const contactMailDefaultOptions =
await this.contactMailNotification.getDefaultMailOptions(
saleInvoice.customerId,
);
const formatArgs = await this.getInvoiceFormatterArgs(invoiceId);
return {
...contactMailDefaultOptions,
subject: defaultSubject,
message: defaultMessage,
attachInvoice: true,
formatArgs,
};
}
/**
* Formats the given invoice mail options.
* @param {number} invoiceId
* @param {SaleInvoiceMailOptions} mailOptions
* @returns {Promise<SaleInvoiceMailOptions>}
*/
public async formatInvoiceMailOptions(
invoiceId: number,
mailOptions: SaleInvoiceMailOptions,
): Promise<SaleInvoiceMailOptions> {
const formatterArgs = await this.getInvoiceFormatterArgs(invoiceId);
const formattedOptions =
await this.contactMailNotification.formatMailOptions(
mailOptions,
formatterArgs,
);
// Generates the a new payment link for the given invoice.
const paymentLink =
await this.generatePaymentLinkService.generatePaymentLink(
invoiceId,
'public',
);
const message = await this.getInvoicePaymentMail.getMailTemplate(
invoiceId,
{
// # Invoice message
invoiceMessage: formattedOptions.message,
preview: formattedOptions.message,
// # Payment link
viewInvoiceButtonUrl: paymentLink.link,
},
);
return { ...formattedOptions, message };
}
/**
* Retrieves the formatted text of the given sale invoice.
* @param {number} invoiceId - Sale invoice id.
* @param {string} text - The given text.
* @returns {Promise<string>}
*/
// @ts-nocheck
public getInvoiceFormatterArgs = async (
invoiceId: number,
): Promise<Record<string, string | number>> => {
const invoice = await this.getSaleInvoiceService.getSaleInvoice(invoiceId);
const commonArgs = await this.contactMailNotification.getCommonFormatArgs();
return {
...commonArgs,
'Customer Name': invoice.customer.displayName,
'Invoice Number': invoice.invoiceNo,
'Invoice Due Amount': invoice.dueAmountFormatted,
'Invoice Due Date': invoice.dueDateFormatted,
'Invoice Date': invoice.invoiceDateFormatted,
'Invoice Amount': invoice.totalFormatted,
'Overdue Days': invoice.overdueDays,
};
};
}

View File

@@ -0,0 +1,143 @@
import { Injectable } from '@nestjs/common';
import { Queue } from 'bullmq';
import { InjectQueue } from '@nestjs/bullmq';
import { SaleInvoicePdf } from '../queries/SaleInvoicePdf.service';
import { SendSaleInvoiceMailCommon } from './SendInvoiceInvoiceMailCommon.service';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { events } from '@/common/events/events';
import { mergeAndValidateMailOptions } from '@/modules/MailNotification/utils';
import {
SaleInvoiceMailOptions,
SendInvoiceMailDTO,
SendSaleInvoiceMailJobPayload,
} from '../SaleInvoice.types';
import { ISaleInvoiceMailSend } from '../SaleInvoice.types';
import { Mail } from '@/modules/Mail/Mail';
import { MailTransporter } from '@/modules/Mail/MailTransporter.service';
import { SendSaleInvoiceMailJob, SendSaleInvoiceQueue } from '../constants';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
@Injectable()
export class SendSaleInvoiceMail {
/**
* @param {SaleInvoicePdf} invoicePdf - Sale invoice pdf service.
* @param {SendSaleInvoiceMailCommon} invoiceMail - Sale invoice mail service.
* @param {EventEmitter2} eventEmitter - Event emitter.
* @param {MailTransporter} mailTransporter - Mail transporter service.
*/
constructor(
private readonly invoicePdf: SaleInvoicePdf,
private readonly invoiceMail: SendSaleInvoiceMailCommon,
private readonly eventEmitter: EventEmitter2,
private readonly mailTransporter: MailTransporter,
private readonly tenancyContect: TenancyContext,
@InjectQueue(SendSaleInvoiceQueue) private readonly sendInvoiceQueue: Queue,
) {}
/**
* Sends the invoice mail of the given sale invoice.
* @param {number} saleInvoiceId - Sale invoice id.
* @param {SendInvoiceMailDTO} messageDTO - Message DTO.
*/
public async triggerMail(
saleInvoiceId: number,
messageOptions: SendInvoiceMailDTO,
) {
const tenant = await this.tenancyContect.getTenant();
const user = await this.tenancyContect.getSystemUser();
const organizationId = tenant.organizationId;
const userId = user.id;
const payload = {
saleInvoiceId,
messageOptions,
userId,
organizationId,
} as SendSaleInvoiceMailJobPayload;
await this.sendInvoiceQueue.add(SendSaleInvoiceMailJob, payload);
// Triggers the event `onSaleInvoicePreMailSend`.
await this.eventEmitter.emitAsync(events.saleInvoice.onPreMailSend, {
saleInvoiceId,
messageOptions,
} as ISaleInvoiceMailSend);
}
/**
* Retrieves the formatted mail options.
* @param {number} saleInvoiceId
* @param {SendInvoiceMailDTO} messageOptions
* @returns {Promise<SaleInvoiceMailOptions>}
*/
async getFormattedMailOptions(
saleInvoiceId: number,
messageOptions: SendInvoiceMailDTO,
): Promise<SaleInvoiceMailOptions> {
const defaultMessageOptions =
await this.invoiceMail.getInvoiceMailOptions(saleInvoiceId);
// Merges message options with default options and parses the options values.
const parsedMessageOptions = mergeAndValidateMailOptions(
defaultMessageOptions,
messageOptions,
);
return this.invoiceMail.formatInvoiceMailOptions(
saleInvoiceId,
parsedMessageOptions,
);
}
/**
* Triggers the mail invoice.
* @param {number} saleInvoiceId - Sale invoice id.
* @param {SendInvoiceMailDTO} messageDTO - Message options.
* @returns {Promise<void>}
*/
public async sendMail(
saleInvoiceId: number,
messageOptions: SendInvoiceMailDTO,
) {
const formattedMessageOptions = await this.getFormattedMailOptions(
saleInvoiceId,
messageOptions,
);
const mail = new Mail()
.setSubject(formattedMessageOptions.subject)
.setTo(formattedMessageOptions.to)
.setCC(formattedMessageOptions.cc)
.setBCC(formattedMessageOptions.bcc)
.setContent(formattedMessageOptions.message);
// Attach invoice document.
if (formattedMessageOptions.attachInvoice) {
// Retrieves document buffer of the invoice pdf document.
const [invoicePdfBuffer, invoiceFilename] =
await this.invoicePdf.getSaleInvoicePdf(saleInvoiceId);
mail.setAttachments([
{ filename: `${invoiceFilename}.pdf`, content: invoicePdfBuffer },
]);
}
const eventPayload = {
saleInvoiceId,
messageOptions,
formattedMessageOptions,
} as ISaleInvoiceMailSend;
// Triggers the event `onSaleInvoiceSend`.
await this.eventEmitter.emitAsync(
events.saleInvoice.onMailSend,
eventPayload,
);
await this.mailTransporter.send(mail);
// Triggers the event `onSaleInvoiceSend`.
await this.eventEmitter.emitAsync(
events.saleInvoice.onMailSent,
eventPayload,
);
}
}

View File

@@ -0,0 +1,33 @@
// import Container, { Service } from 'typedi';
// import events from '@/subscribers/events';
// import { SendSaleInvoiceMail } from './SendSaleInvoiceMail';
// @Service()
// export class SendSaleInvoiceMailJob {
// /**
// * Constructor method.
// */
// constructor(agenda) {
// agenda.define(
// 'sale-invoice-mail-send',
// { priority: 'high', concurrency: 2 },
// this.handler
// );
// }
// /**
// * Triggers sending invoice mail.
// */
// private handler = async (job, done: Function) => {
// const { tenantId, saleInvoiceId, messageOptions } = job.attrs.data;
// const sendInvoiceMail = Container.get(SendSaleInvoiceMail);
// try {
// await sendInvoiceMail.sendMail(tenantId, saleInvoiceId, messageOptions);
// done();
// } catch (error) {
// console.log(error);
// done(error);
// }
// };
// }

View File

@@ -0,0 +1,32 @@
// import Container, { Service } from 'typedi';
// import { SendInvoiceMailReminder } from './SendSaleInvoiceMailReminder';
// @Service()
// export class SendSaleInvoiceReminderMailJob {
// /**
// * Constructor method.
// */
// constructor(agenda) {
// agenda.define(
// 'sale-invoice-reminder-mail-send',
// { priority: 'high', concurrency: 1 },
// this.handler
// );
// }
// /**
// * Triggers sending invoice mail.
// */
// private handler = async (job, done: Function) => {
// const { tenantId, saleInvoiceId, messageOptions } = job.attrs.data;
// const sendInvoiceMail = Container.get(SendInvoiceMailReminder);
// try {
// await sendInvoiceMail.sendMail(tenantId, saleInvoiceId, messageOptions);
// done();
// } catch (error) {
// console.log(error);
// done(error);
// }
// };
// }

View File

@@ -0,0 +1,162 @@
import { Knex } from 'knex';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { Inject, Injectable } from '@nestjs/common';
import {
ISaleInvoiceWriteoffCreatePayload,
ISaleInvoiceWriteoffDTO,
ISaleInvoiceWrittenOffCanceledPayload,
ISaleInvoiceWrittenOffCancelPayload,
} from '../SaleInvoice.types';
import { ERRORS } from '../constants';
import { UnitOfWork } from '../../Tenancy/TenancyDB/UnitOfWork.service';
import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators.service';
import { SaleInvoice } from '../models/SaleInvoice';
import { events } from '@/common/events/events';
import { ServiceError } from '../../Items/ServiceError';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable()
export class WriteoffSaleInvoice {
/**
* @param {EventEmitter2} eventPublisher - Event emitter.
* @param {UnitOfWork} uow - Unit of work.
* @param {CommandSaleInvoiceValidators} validators - Command sale invoice validators.
* @param {typeof SaleInvoice} saleInvoiceModel - Sale invoice model.
*/
constructor(
private readonly eventPublisher: EventEmitter2,
private readonly uow: UnitOfWork,
private readonly validators: CommandSaleInvoiceValidators,
@Inject(SaleInvoice.name)
private readonly saleInvoiceModel: TenantModelProxy<typeof SaleInvoice>,
) {}
/**
* Writes-off the sale invoice on bad debt expense account.
* @param {number} saleInvoiceId
* @param {ISaleInvoiceWriteoffDTO} writeoffDTO
* @return {Promise<ISaleInvoice>}
*/
public writeOff = async (
saleInvoiceId: number,
writeoffDTO: ISaleInvoiceWriteoffDTO,
): Promise<SaleInvoice> => {
const saleInvoice = await this.saleInvoiceModel()
.query()
.findById(saleInvoiceId)
.throwIfNotFound();
// Validates the given invoice existance.
this.validators.validateInvoiceExistance(saleInvoice);
// Validate the sale invoice whether already written-off.
this.validateSaleInvoiceAlreadyWrittenoff(saleInvoice);
// Saves the invoice write-off transaction with associated transactions
// under unit-of-work envirmenet.
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
const eventPayload = {
// tenantId,
saleInvoiceId,
saleInvoice,
writeoffDTO,
trx,
} as ISaleInvoiceWriteoffCreatePayload;
// Triggers `onSaleInvoiceWriteoff` event.
await this.eventPublisher.emitAsync(
events.saleInvoice.onWriteoff,
eventPayload,
);
// Mark the sale invoice as written-off.
const newSaleInvoice = await this.saleInvoiceModel()
.query(trx)
.patch({
writtenoffExpenseAccountId: writeoffDTO.expenseAccountId,
writtenoffAmount: saleInvoice.dueAmount,
writtenoffAt: new Date(),
})
.findById(saleInvoiceId);
// Triggers `onSaleInvoiceWrittenoff` event.
await this.eventPublisher.emitAsync(
events.saleInvoice.onWrittenoff,
eventPayload,
);
return newSaleInvoice;
});
};
/**
* Cancels the written-off sale invoice.
* @param {number} tenantId
* @param {number} saleInvoiceId
* @returns {Promise<ISaleInvoice>}
*/
public cancelWrittenoff = async (
saleInvoiceId: number,
): Promise<SaleInvoice> => {
// Validate the sale invoice existance.
// Retrieve the sale invoice or throw not found service error.
const saleInvoice = await this.saleInvoiceModel()
.query()
.findById(saleInvoiceId);
// Validate the sale invoice existance.
this.validators.validateInvoiceExistance(saleInvoice);
// Validate the sale invoice whether already written-off.
this.validateSaleInvoiceNotWrittenoff(saleInvoice);
// Cancels the invoice written-off and removes the associated transactions.
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
// Triggers `onSaleInvoiceWrittenoffCancel` event.
await this.eventPublisher.emitAsync(
events.saleInvoice.onWrittenoffCancel,
{
saleInvoice,
trx,
} as ISaleInvoiceWrittenOffCancelPayload,
);
// Mark the sale invoice as written-off.
const newSaleInvoice = await SaleInvoice.query(trx)
.patch({
writtenoffAmount: null,
writtenoffAt: null,
})
.findById(saleInvoiceId);
// Triggers `onSaleInvoiceWrittenoffCanceled`.
await this.eventPublisher.emitAsync(
events.saleInvoice.onWrittenoffCanceled,
{
saleInvoice,
trx,
} as ISaleInvoiceWrittenOffCanceledPayload,
);
return newSaleInvoice;
});
};
/**
* Should sale invoice not be written-off.
* @param {SaleInvoice} saleInvoice
*/
private validateSaleInvoiceNotWrittenoff(saleInvoice: SaleInvoice) {
if (!saleInvoice.isWrittenoff) {
throw new ServiceError(ERRORS.SALE_INVOICE_NOT_WRITTEN_OFF);
}
}
/**
* Should sale invoice already written-off.
* @param {SaleInvoice} saleInvoice
*/
private validateSaleInvoiceAlreadyWrittenoff(saleInvoice: SaleInvoice) {
if (saleInvoice.isWrittenoff) {
throw new ServiceError(ERRORS.SALE_INVOICE_ALREADY_WRITTEN_OFF);
}
}
}

View File

@@ -0,0 +1,71 @@
// @ts-nocheck
import { InventoryTransactionsService } from '@/modules/InventoryCost/commands/InventoryTransactions.service';
import { ItemsEntriesService } from '@/modules/Items/ItemsEntries.service';
import { Injectable } from '@nestjs/common';
import { Knex } from 'knex';
import { SaleInvoice } from '../../models/SaleInvoice';
@Injectable()
export class InvoiceInventoryTransactions {
constructor(
private readonly itemsEntriesService: ItemsEntriesService,
private readonly inventoryService: InventoryTransactionsService,
) {}
/**
* Records the inventory transactions of the given sale invoice in case
* the invoice has inventory entries only.
* @param {SaleInvoice} saleInvoice - Sale invoice DTO.
* @param {number} saleInvoiceId - Sale invoice id.
* @param {boolean} override - Allow to override old transactions.
* @return {Promise<void>}
*/
public async recordInventoryTranscactions(
saleInvoice: SaleInvoice,
override?: boolean,
trx?: Knex.Transaction,
): Promise<void> {
// Loads the inventory items entries of the given sale invoice.
const inventoryEntries =
await this.itemsEntriesService.filterInventoryEntries(
saleInvoice.entries,
trx,
);
const transaction = {
transactionId: saleInvoice.id,
transactionType: 'SaleInvoice',
transactionNumber: saleInvoice.invoiceNo,
exchangeRate: saleInvoice.exchangeRate,
warehouseId: saleInvoice.warehouseId,
date: saleInvoice.invoiceDate,
direction: 'OUT',
entries: inventoryEntries,
createdAt: saleInvoice.createdAt,
};
await this.inventoryService.recordInventoryTransactionsFromItemsEntries(
transaction,
override,
trx,
);
}
/**
* Reverting the inventory transactions once the invoice deleted.
* @param {number} billId - Bill id.
* @return {Promise<void>}
*/
public async revertInventoryTransactions(
saleInvoiceId: number,
trx?: Knex.Transaction,
): Promise<void> {
// Delete the inventory transaction of the given sale invoice.
const { oldInventoryTransactions } =
await this.inventoryService.deleteInventoryTransactions(
saleInvoiceId,
'SaleInvoice',
trx,
);
}
}

View File

@@ -0,0 +1,111 @@
import { SaleInvoice } from '../../models/SaleInvoice';
import { ILedger } from '@/modules/Ledger/types/Ledger.types';
import { ILedgerEntry } from '@/modules/Ledger/types/Ledger.types';
import { AccountNormal } from '@/interfaces/Account';
import { Ledger } from '@/modules/Ledger/Ledger';
export class SaleInvoiceWriteoffGL {
private saleInvoiceModel: SaleInvoice;
private ARAccountId: number;
/**
* Sets the sale invoice model.
* @param {SaleInvoice} saleInvoiceModel -
*/
constructor(saleInvoiceModel: SaleInvoice) {
this.saleInvoiceModel = saleInvoiceModel;
}
/**
* Sets the A/R account ID.
* @param {number} ARAccountId -
*/
setARAccountId(ARAccountId: number) {
this.ARAccountId = ARAccountId;
return this;
}
/**
* Retrieves the invoice write-off common GL entry.
* @param {ISaleInvoice} saleInvoice
*/
private get invoiceWriteoffGLCommonEntry() {
return {
date: this.saleInvoiceModel.invoiceDate,
currencyCode: this.saleInvoiceModel.currencyCode,
exchangeRate: this.saleInvoiceModel.exchangeRate,
transactionId: this.saleInvoiceModel.id,
transactionType: 'InvoiceWriteOff',
transactionNumber: this.saleInvoiceModel.invoiceNo,
referenceNo: this.saleInvoiceModel.referenceNo,
branchId: this.saleInvoiceModel.branchId,
};
}
/**
* Retrieves the invoice write-off receiveable GL entry.
* @param {number} ARAccountId
* @param {ISaleInvoice} saleInvoice
* @returns {ILedgerEntry}
*/
private get invoiceWriteoffGLReceivableEntry(): ILedgerEntry {
const commontEntry = this.invoiceWriteoffGLCommonEntry;
return {
...commontEntry,
credit: this.saleInvoiceModel.writtenoffAmountLocal,
accountId: this.ARAccountId,
contactId: this.saleInvoiceModel.customerId,
debit: 0,
index: 1,
indexGroup: 300,
accountNormal:
this.saleInvoiceModel.writtenoffExpenseAccount.accountNormal,
};
}
/**
* Retrieves the invoice write-off expense GL entry.
* @param {ISaleInvoice} saleInvoice
* @returns {ILedgerEntry}
*/
private get invoiceWriteoffGLExpenseEntry(): ILedgerEntry {
const commontEntry = this.invoiceWriteoffGLCommonEntry;
return {
...commontEntry,
debit: this.saleInvoiceModel.writtenoffAmount,
accountId: this.saleInvoiceModel.writtenoffExpenseAccountId,
credit: 0,
index: 2,
indexGroup: 300,
accountNormal: AccountNormal.DEBIT,
};
}
/**
* Retrieves the invoice write-off GL entries.
* @returns {ILedgerEntry[]}
*/
public getInvoiceWriteoffGLEntries(): ILedgerEntry[] {
const creditEntry = this.invoiceWriteoffGLExpenseEntry;
const debitEntry = this.invoiceWriteoffGLReceivableEntry;
return [debitEntry, creditEntry];
}
/**
* Retrieves the invoice write-off ledger.
* @param {number} ARAccountId
* @param {ISaleInvoice} saleInvoice
* @returns {Ledger}
*/
public getInvoiceWriteoffLedger(): ILedger {
const entries = this.getInvoiceWriteoffGLEntries();
return new Ledger(entries);
}
}

View File

@@ -0,0 +1,88 @@
import { Inject, Injectable } from '@nestjs/common';
import { Knex } from 'knex';
import { Account } from '@/modules/Accounts/models/Account.model';
import { AccountRepository } from '@/modules/Accounts/repositories/Account.repository';
import { LedgerStorageService } from '@/modules/Ledger/LedgerStorage.service';
import { SaleInvoice } from '../../models/SaleInvoice';
import { SaleInvoiceWriteoffGL } from './SaleInvoiceWriteoffGL';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable()
export class SaleInvoiceWriteoffGLStorage {
/**
* @param {LedgerStorageService} ledgerStorage - Ledger storage service.
* @param {AccountRepository} accountRepository - Account repository.
* @param {Account} accountModel - Account model.
* @param {SaleInvoice} saleInvoiceModel - Sale invoice model.
*/
constructor(
private readonly ledgerStorage: LedgerStorageService,
private readonly accountRepository: AccountRepository,
@Inject(Account.name)
private readonly accountModel: TenantModelProxy<typeof Account>,
@Inject(SaleInvoice.name)
private readonly saleInvoiceModel: TenantModelProxy<typeof SaleInvoice>,
) {}
/**
* Writes the invoice write-off GL entries.
* @param {number} saleInvoiceId - Sale invoice id.
* @param {Knex.Transaction} trx - Knex transaction.
* @returns {Promise<void>}
*/
public async writeInvoiceWriteoffEntries(
saleInvoiceId: number,
trx?: Knex.Transaction,
) {
// Retrieves the sale invoice.
const saleInvoice = await this.saleInvoiceModel()
.query(trx)
.findById(saleInvoiceId)
.withGraphFetched('writtenoffExpenseAccount');
// Find or create the A/R account.
const ARAccount =
await this.accountRepository.findOrCreateAccountReceivable(
saleInvoice.currencyCode,
{},
trx,
);
const ledger = new SaleInvoiceWriteoffGL(saleInvoice)
.setARAccountId(ARAccount.id)
.getInvoiceWriteoffLedger();
return this.ledgerStorage.commit(ledger, trx);
}
/**
* Rewrites the invoice write-off GL entries.
* @param {number} saleInvoiceId - Sale invoice id.
* @param {Knex.Transaction} trx - Knex transaction.
* @returns {Promise<void>}
*/
public async rewriteInvoiceWriteoffEntries(
saleInvoiceId: number,
trx?: Knex.Transaction,
) {
await this.revertInvoiceWriteoffEntries(saleInvoiceId, trx);
await this.writeInvoiceWriteoffEntries(saleInvoiceId, trx);
}
/**
* Reverts the invoice write-off GL entries.
* @param {number} saleInvoiceId - Sale invoice id.
* @param {Knex.Transaction} trx - Knex transaction.
* @returns {Promise<void>}
*/
public revertInvoiceWriteoffEntries = async (
saleInvoiceId: number,
trx?: Knex.Transaction,
) => {
await this.ledgerStorage.deleteByReference(
saleInvoiceId,
'InvoiceWriteOff',
trx,
);
};
}

View File

@@ -0,0 +1,243 @@
// import config from '@/config';
export const SendSaleInvoiceQueue = 'SendSaleInvoiceQueue';
export const SendSaleInvoiceMailJob = 'SendSaleInvoiceMailJob';
const BASE_URL = 'http://localhost:3000';
export const DEFAULT_INVOICE_MAIL_SUBJECT =
'Invoice {Invoice Number} from {Company Name} for {Customer Name}';
export const DEFAULT_INVOICE_MAIL_CONTENT = `Hi {Customer Name},
Here's invoice # {Invoice Number} for {Invoice Amount}
The amount outstanding of {Invoice Due Amount} is due on {Invoice Due Date}.
From your online payment page you can print a PDF or view your outstanding bills.
If you have any questions, please let us know.
Thanks,
{Company Name}
`;
export const DEFAULT_INVOICE_REMINDER_MAIL_SUBJECT =
'Invoice {InvoiceNumber} reminder from {CompanyName}';
export const DEFAULT_INVOICE_REMINDER_MAIL_CONTENT = `
<p>Dear {CustomerName}</p>
<p>You might have missed the payment date and the invoice is now overdue by {OverdueDays} days.</p>
<p>Invoice <strong>#{InvoiceNumber}</strong><br />
Due Date : <strong>{InvoiceDueDate}</strong><br />
Amount : <strong>{InvoiceAmount}</strong></p>
<p>
<i>Regards</i><br />
<i>{CompanyName}</i>
</p>
`;
export const PUBLIC_PAYMENT_LINK = `${BASE_URL}/payment/{PAYMENT_LINK_ID}`;
export const ERRORS = {
INVOICE_NUMBER_NOT_UNIQUE: 'INVOICE_NUMBER_NOT_UNIQUE',
SALE_INVOICE_NOT_FOUND: 'SALE_INVOICE_NOT_FOUND',
SALE_INVOICE_ALREADY_DELIVERED: 'SALE_INVOICE_ALREADY_DELIVERED',
ENTRIES_ITEMS_IDS_NOT_EXISTS: 'ENTRIES_ITEMS_IDS_NOT_EXISTS',
NOT_SELLABLE_ITEMS: 'NOT_SELLABLE_ITEMS',
SALE_INVOICE_NO_NOT_UNIQUE: 'SALE_INVOICE_NO_NOT_UNIQUE',
INVOICE_AMOUNT_SMALLER_THAN_PAYMENT_AMOUNT:
'INVOICE_AMOUNT_SMALLER_THAN_PAYMENT_AMOUNT',
INVOICE_HAS_ASSOCIATED_PAYMENT_ENTRIES:
'INVOICE_HAS_ASSOCIATED_PAYMENT_ENTRIES',
SALE_INVOICE_NO_IS_REQUIRED: 'SALE_INVOICE_NO_IS_REQUIRED',
CUSTOMER_HAS_SALES_INVOICES: 'CUSTOMER_HAS_SALES_INVOICES',
SALE_INVOICE_HAS_APPLIED_TO_CREDIT_NOTES:
'SALE_INVOICE_HAS_APPLIED_TO_CREDIT_NOTES',
PAYMENT_ACCOUNT_CURRENCY_INVALID: 'PAYMENT_ACCOUNT_CURRENCY_INVALID',
SALE_INVOICE_ALREADY_WRITTEN_OFF: 'SALE_INVOICE_ALREADY_WRITTEN_OFF',
SALE_INVOICE_NOT_WRITTEN_OFF: 'SALE_INVOICE_NOT_WRITTEN_OFF',
NO_INVOICE_CUSTOMER_EMAIL_ADDR: 'NO_INVOICE_CUSTOMER_EMAIL_ADDR',
};
export const DEFAULT_VIEW_COLUMNS = [];
export const DEFAULT_VIEWS = [
{
name: 'Draft',
slug: 'draft',
rolesLogicExpression: '1',
roles: [
{ index: 1, fieldKey: 'status', comparator: 'equals', value: 'draft' },
],
columns: DEFAULT_VIEW_COLUMNS,
},
{
name: 'Delivered',
slug: 'delivered',
rolesLogicExpression: '1',
roles: [
{
index: 1,
fieldKey: 'status',
comparator: 'equals',
value: 'delivered',
},
],
columns: DEFAULT_VIEW_COLUMNS,
},
{
name: 'Unpaid',
slug: 'unpaid',
rolesLogicExpression: '1',
roles: [
{ index: 1, fieldKey: 'status', comparator: 'equals', value: 'unpaid' },
],
columns: DEFAULT_VIEW_COLUMNS,
},
{
name: 'Partially paid',
slug: 'partially-paid',
rolesLogicExpression: '1',
roles: [
{
index: 1,
fieldKey: 'status',
comparator: 'equals',
value: 'partially-paid',
},
],
columns: DEFAULT_VIEW_COLUMNS,
},
{
name: 'Paid',
slug: 'paid',
rolesLogicExpression: '1',
roles: [
{ index: 1, fieldKey: 'status', comparator: 'equals', value: 'paid' },
],
columns: DEFAULT_VIEW_COLUMNS,
},
];
export const SaleInvoicesSampleData = [
{
'Invoice No.': 'B-101',
'Reference No.': 'REF0',
'Invoice Date': '2024-01-01',
'Due Date': '2024-03-01',
Customer: 'Harley Veum',
'Exchange Rate': 1,
'Invoice Message': 'Aspernatur doloremque amet quia aut.',
'Terms & Conditions': 'Quia illum aut dolores.',
Delivered: 'T',
Item: 'VonRueden, Ruecker and Hettinger',
Quantity: 100,
Rate: 100,
Description: 'Description',
},
{
'Invoice No.': 'B-102',
'Reference No.': 'REF0',
'Invoice Date': '2024-01-01',
'Due Date': '2024-03-01',
Customer: 'Harley Veum',
'Exchange Rate': 1,
'Invoice Message': 'Est omnis enim vel.',
'Terms & Conditions': 'Iusto et sint nobis sit.',
Delivered: 'T',
Item: 'Thompson - Reichert',
Quantity: 200,
Rate: 50,
Description: 'Description',
},
{
'Invoice No.': 'B-103',
'Reference No.': 'REF0',
'Invoice Date': '2024-01-01',
'Due Date': '2024-03-01',
Customer: 'Harley Veum',
'Exchange Rate': 1,
'Invoice Message':
'Repudiandae voluptatibus repellat minima voluptatem rerum veniam.',
'Terms & Conditions': 'Id quod inventore ex rerum velit sed.',
Delivered: 'T',
Item: 'VonRueden, Ruecker and Hettinger',
Quantity: 100,
Rate: 100,
Description: 'Description',
},
];
export const defaultInvoicePdfTemplateAttributes = {
primaryColor: 'red',
secondaryColor: 'red',
companyName: 'Bigcapital Technology, Inc.',
showCompanyLogo: true,
companyLogoKey: '',
companyLogoUri: '',
dueDateLabel: 'Date due',
showDueDate: true,
dateIssueLabel: 'Date of issue',
showDateIssue: true,
// # Invoice number,
invoiceNumberLabel: 'Invoice number',
showInvoiceNumber: true,
// # Customer address
showCustomerAddress: true,
customerAddress: '',
// # Company address
showCompanyAddress: true,
companyAddress: '',
billedToLabel: 'Billed To',
// Entries
lineItemLabel: 'Item',
lineQuantityLabel: 'Qty',
lineRateLabel: 'Rate',
lineTotalLabel: 'Total',
totalLabel: 'Total',
subtotalLabel: 'Subtotal',
discountLabel: 'Discount',
paymentMadeLabel: 'Payment Made',
balanceDueLabel: 'Balance Due',
// Totals
showTotal: true,
showSubtotal: true,
showDiscount: true,
showTaxes: true,
showPaymentMade: true,
showDueAmount: true,
showBalanceDue: true,
discount: '0.00',
// Footer paragraphs.
termsConditionsLabel: 'Terms & Conditions',
showTermsConditions: true,
lines: [
{
item: 'Simply dummy text',
description: 'Simply dummy text of the printing and typesetting',
rate: '1',
quantity: '1000',
total: '$1000.00',
},
],
taxes: [
{ label: 'Sample Tax1 (4.70%)', amount: '11.75' },
{ label: 'Sample Tax2 (7.00%)', amount: '21.74' },
],
// # Statement
statementLabel: 'Statement',
showStatement: true,
};

View File

@@ -0,0 +1,208 @@
import { ItemEntryDto } from '@/modules/TransactionItemEntry/dto/ItemEntry.dto';
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import {
ArrayMinSize,
IsArray,
IsBoolean,
IsDate,
IsEnum,
IsInt,
IsNotEmpty,
IsNumber,
IsOptional,
IsString,
Min,
ValidateNested,
} from 'class-validator';
enum DiscountType {
Percentage = 'percentage',
Amount = 'amount',
}
class PaymentMethodDto {
@IsInt()
paymentIntegrationId: number;
@IsBoolean()
enable: boolean;
}
class AttachmentDto {
@IsString()
key: string;
}
class CommandSaleInvoiceDto {
@IsInt()
@IsNotEmpty()
@ApiProperty({ description: 'Customer ID', example: 1 })
customerId: number;
@IsDate()
@Type(() => Date)
@IsNotEmpty()
@ApiProperty({ description: 'Invoice date', example: '2023-01-01T00:00:00Z' })
invoiceDate: Date;
@IsDate()
@Type(() => Date)
@IsNotEmpty()
@ApiProperty({ description: 'Due date', example: '2023-01-15T00:00:00Z' })
dueDate: Date;
@IsOptional()
@IsString()
@ApiProperty({
description: 'Invoice number',
required: false,
example: 'INV-001',
})
invoiceNo?: string;
@IsOptional()
@IsString()
@ApiProperty({
description: 'Reference number',
required: false,
example: 'REF-001',
})
referenceNo?: string;
@IsOptional()
@IsBoolean()
@ApiProperty({
description: 'Whether the invoice is delivered',
default: false,
required: false,
})
delivered: boolean = false;
@IsOptional()
@IsString()
@ApiProperty({
description: 'Invoice message',
required: false,
example: 'Thank you for your business',
})
invoiceMessage?: string;
@IsOptional()
@IsString()
@ApiProperty({
description: 'Terms and conditions',
required: false,
example: 'Payment due within 14 days',
})
termsConditions?: string;
@IsOptional()
@IsNumber()
@Min(0)
@ApiProperty({
description: 'Exchange rate',
required: false,
minimum: 0,
example: 1.0,
})
exchangeRate?: number;
@IsOptional()
@IsInt()
@ApiProperty({ description: 'Warehouse ID', required: false, example: 1 })
warehouseId?: number;
@IsOptional()
@IsInt()
@ApiProperty({ description: 'Branch ID', required: false, example: 1 })
branchId?: number;
@IsOptional()
@IsInt()
@ApiProperty({ description: 'Project ID', required: false, example: 1 })
projectId?: number;
@IsOptional()
@IsBoolean()
@ApiProperty({
description: 'Whether tax is inclusive',
required: false,
example: false,
})
isInclusiveTax?: boolean;
@IsArray()
@ValidateNested({ each: true })
@Type(() => ItemEntryDto)
@ArrayMinSize(1)
@ApiProperty({
description: 'Invoice line items',
type: [ItemEntryDto],
minItems: 1,
})
entries: ItemEntryDto[];
@IsOptional()
@IsInt()
@ApiProperty({ description: 'PDF template ID', required: false, example: 1 })
pdfTemplateId?: number;
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => PaymentMethodDto)
@ApiProperty({
description: 'Payment methods',
type: [PaymentMethodDto],
required: false,
})
paymentMethods?: PaymentMethodDto[];
@IsOptional()
@IsNumber()
@ApiProperty({ description: 'Discount value', required: false, example: 10 })
discount?: number;
@IsOptional()
@IsEnum(DiscountType)
@ApiProperty({
description: 'Discount type',
enum: DiscountType,
required: false,
example: DiscountType.Percentage,
})
discountType?: DiscountType;
@IsOptional()
@IsNumber()
@ApiProperty({
description: 'Adjustment amount',
required: false,
example: 5,
})
adjustment?: number;
@IsOptional()
@IsInt()
@ApiProperty({
description: 'ID of the estimate this invoice is created from',
required: false,
example: 1,
})
fromEstimateId?: number;
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => AttachmentDto)
@ApiProperty({
description: 'The attachments of the sale receipt',
example: [{ key: '123456' }],
})
attachments?: AttachmentDto[];
}
export class CreateSaleInvoiceDto extends CommandSaleInvoiceDto {}
export class EditSaleInvoiceDto extends CommandSaleInvoiceDto {}

View File

@@ -0,0 +1,211 @@
import * as R from 'ramda';
import { ILedger } from '@/modules/Ledger/types/Ledger.types';
import { AccountNormal } from '@/modules/Accounts/Accounts.types';
import { ILedgerEntry } from '@/modules/Ledger/types/Ledger.types';
import { ItemEntry } from '@/modules/TransactionItemEntry/models/ItemEntry';
import { Ledger } from '@/modules/Ledger/Ledger';
import { SaleInvoice } from '../models/SaleInvoice';
export class InvoiceGL {
private saleInvoice: SaleInvoice;
private ARAccountId: number;
private taxPayableAccountId: number;
private discountAccountId: number;
private otherChargesAccountId: number;
/**
* Constructor method.
* @param {SaleInvoice} saleInvoice - Sale invoice.
*/
constructor(saleInvoice: SaleInvoice) {
this.saleInvoice = saleInvoice;
}
/**
* Set the receivable account id.
* @param {number} ARAccountId - Receivable account id.
*/
setARAccountId(ARAccountId: number) {
this.ARAccountId = ARAccountId;
}
/**
* Set the tax payable account id.
* @param {number} taxPayableAccountId - Tax payable account id.
*/
setTaxPayableAccountId(taxPayableAccountId: number) {
this.taxPayableAccountId = taxPayableAccountId;
}
/**
* Set the discount account id.
* @param {number} discountAccountId - Discount account id.
*/
setDiscountAccountId(discountAccountId: number) {
this.discountAccountId = discountAccountId;
}
/**
* Set the other charges account id.
* @param {number} otherChargesAccountId - Other charges account id.
*/
setOtherChargesAccountId(otherChargesAccountId: number) {
this.otherChargesAccountId = otherChargesAccountId;
}
/**
* Retrieves the invoice GL common entry.
*/
private get invoiceGLCommonEntry() {
return {
credit: 0,
debit: 0,
currencyCode: this.saleInvoice.currencyCode,
exchangeRate: this.saleInvoice.exchangeRate,
transactionType: 'SaleInvoice',
transactionId: this.saleInvoice.id,
date: this.saleInvoice.invoiceDate,
userId: this.saleInvoice.userId,
transactionNumber: this.saleInvoice.invoiceNo,
referenceNumber: this.saleInvoice.referenceNo,
createdAt: this.saleInvoice.createdAt,
indexGroup: 10,
branchId: this.saleInvoice.branchId,
};
}
/**
* Retrieve receivable entry of the invoice.
* @returns {ILedgerEntry}
*/
public get invoiceReceivableEntry(): ILedgerEntry {
const commonEntry = this.invoiceGLCommonEntry;
return {
...commonEntry,
debit: this.saleInvoice.totalLocal,
accountId: this.ARAccountId,
contactId: this.saleInvoice.customerId,
accountNormal: AccountNormal.DEBIT,
index: 1,
};
}
/**
* Retrieve item income entry of the invoice.
* @param {ItemEntry} entry - Item entry.
* @param {number} index - Index.
* @returns {ILedgerEntry}
*/
private getInvoiceItemEntry = R.curry(
(entry: ItemEntry, index: number): ILedgerEntry => {
const commonEntry = this.invoiceGLCommonEntry;
const localAmount =
entry.totalExcludingTax * this.saleInvoice.exchangeRate;
return {
...commonEntry,
credit: localAmount,
accountId: entry.sellAccountId,
note: entry.description,
index: index + 2,
itemId: entry.itemId,
accountNormal: AccountNormal.CREDIT,
taxRateId: entry.taxRateId,
taxRate: entry.taxRate,
};
},
);
/**
* Retreives the GL entry of tax payable.
* @param {ItemEntry} entry - Item entry.
* @param {number} index - Index.
* @returns {ILedgerEntry}
*/
private getInvoiceTaxEntry(entry: ItemEntry, index: number): ILedgerEntry {
const commonEntry = this.invoiceGLCommonEntry;
return {
...commonEntry,
credit: entry.taxAmount,
accountId: this.taxPayableAccountId,
index: index + 1,
indexGroup: 30,
accountNormal: AccountNormal.CREDIT,
taxRateId: entry.taxRateId,
taxRate: entry.taxRate,
};
}
/**
* Retrieves the invoice discount GL entry.
* @returns {ILedgerEntry}
*/
private get invoiceDiscountEntry(): ILedgerEntry {
const commonEntry = this.invoiceGLCommonEntry;
return {
...commonEntry,
debit: this.saleInvoice.discountAmountLocal,
accountId: this.discountAccountId,
accountNormal: AccountNormal.CREDIT,
index: 1,
} as ILedgerEntry;
};
/**
* Retrieves the invoice adjustment GL entry.
* @returns {ILedgerEntry}
*/
private get adjustmentEntry(): ILedgerEntry {
const commonEntry = this.invoiceGLCommonEntry;
const adjustmentAmount = Math.abs(this.saleInvoice.adjustmentLocal);
return {
...commonEntry,
debit: this.saleInvoice.adjustmentLocal < 0 ? adjustmentAmount : 0,
credit: this.saleInvoice.adjustmentLocal > 0 ? adjustmentAmount : 0,
accountId: this.otherChargesAccountId,
accountNormal: AccountNormal.CREDIT,
index: 1,
};
};
/**
* Retrieves the invoice GL entries.
* @returns {ILedgerEntry[]}
*/
public getInvoiceGLEntries = (): ILedgerEntry[] => {
const creditEntries = this.saleInvoice.entries.map(
(entry, index) => this.getInvoiceItemEntry(entry, index),
);
const taxEntries = this.saleInvoice.entries
.filter((entry) => entry.taxAmount > 0)
.map((entry, index) => this.getInvoiceTaxEntry(entry, index));
return [
this.invoiceReceivableEntry,
...creditEntries,
...taxEntries,
this.invoiceDiscountEntry,
this.adjustmentEntry,
];
};
/**
* Retrieves the invoice ledger.
* @returns {ILedger}
*/
public getInvoiceLedger = (): ILedger => {
const entries = this.getInvoiceGLEntries();
return new Ledger(entries);
};
}

View File

@@ -0,0 +1,96 @@
import { Knex } from 'knex';
import { Inject, Injectable } from '@nestjs/common';
import { LedgerStorageService } from '../../Ledger/LedgerStorage.service';
import { SaleInvoice } from '../models/SaleInvoice';
import { AccountRepository } from '../../Accounts/repositories/Account.repository';
import { InvoiceGL } from './InvoiceGL';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable()
export class SaleInvoiceGLEntries {
constructor(
private readonly ledegrRepository: LedgerStorageService,
private readonly accountRepository: AccountRepository,
@Inject(SaleInvoice.name)
private readonly saleInvoiceModel: TenantModelProxy<typeof SaleInvoice>,
) {}
/**
* Writes a sale invoice GL entries.
* @param {number} saleInvoiceId - Sale invoice id.
* @param {Knex.Transaction} trx
*/
public writeInvoiceGLEntries = async (
saleInvoiceId: number,
trx?: Knex.Transaction,
) => {
const saleInvoice = await this.saleInvoiceModel()
.query(trx)
.findById(saleInvoiceId)
.withGraphFetched('entries.item');
// Find or create the A/R account.
const ARAccount =
await this.accountRepository.findOrCreateAccountReceivable(
saleInvoice.currencyCode,
{},
trx,
);
// Find or create tax payable account.
const taxPayableAccount =
await this.accountRepository.findOrCreateTaxPayable({}, trx);
// Find or create the discount expense account.
const discountAccount =
await this.accountRepository.findOrCreateDiscountAccount({}, trx);
// Find or create the other charges account.
const otherChargesAccount =
await this.accountRepository.findOrCreateOtherChargesAccount({}, trx);
// Retrieves the ledger of the invoice.
const invoiceGL = new InvoiceGL(saleInvoice);
invoiceGL.setARAccountId(ARAccount.id);
invoiceGL.setTaxPayableAccountId(taxPayableAccount.id);
invoiceGL.setDiscountAccountId(discountAccount.id);
invoiceGL.setOtherChargesAccountId(otherChargesAccount.id);
const ledger = invoiceGL.getInvoiceLedger();
// Commits the ledger entries to the storage as UOW.
await this.ledegrRepository.commit(ledger, trx);
};
/**
* Rewrites the given invoice GL entries.
* @param {number} tenantId
* @param {number} saleInvoiceId
* @param {Knex.Transaction} trx
*/
public rewritesInvoiceGLEntries = async (
saleInvoiceId: number,
trx?: Knex.Transaction,
) => {
// Reverts the invoice GL entries.
await this.revertInvoiceGLEntries(saleInvoiceId, trx);
// Writes the invoice GL entries.
await this.writeInvoiceGLEntries(saleInvoiceId, trx);
};
/**
* Reverts the given invoice GL entries.
* @param {number} saleInvoiceId - Sale invoice id.
* @param {Knex.Transaction} trx
*/
public revertInvoiceGLEntries = async (
saleInvoiceId: number,
trx?: Knex.Transaction,
) => {
await this.ledegrRepository.deleteByReference(
saleInvoiceId,
'SaleInvoice',
trx,
);
};
}

View File

@@ -0,0 +1,765 @@
import { Model, raw } from 'objection';
import { castArray } from 'lodash';
import * as moment from 'moment';
import * as R from 'ramda';
import { MomentInput, unitOfTime } from 'moment';
import { defaultTo } from 'ramda';
import { TaxRateTransaction } from '@/modules/TaxRates/models/TaxRateTransaction.model';
import { ItemEntry } from '@/modules/TransactionItemEntry/models/ItemEntry';
import { Document } from '@/modules/ChromiumlyTenancy/models/Document';
import { DiscountType } from '@/common/types/Discount';
import { Account } from '@/modules/Accounts/models/Account.model';
import { ISearchRole } from '@/modules/DynamicListing/DynamicFilter/DynamicFilter.types';
import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel';
import { PaymentIntegrationTransactionLink } from '../SaleInvoice.types';
import { TransactionPaymentServiceEntry } from '@/modules/PaymentServices/models/TransactionPaymentServiceEntry.model';
import { InjectAttachable } from '@/modules/Attachments/decorators/InjectAttachable.decorator';
@InjectAttachable()
export class SaleInvoice extends TenantBaseModel{
public taxAmountWithheld: number;
public balance: number;
public paymentAmount: number;
public exchangeRate: number;
public creditedAmount: number;
public isInclusiveTax: boolean;
public dueDate: Date;
public deliveredAt: Date | string;
public currencyCode: string;
public invoiceDate: Date;
public createdAt?: Date;
public updatedAt?: Date | null;
public writtenoffExpenseAccountId: number;
public writtenoffAmount: number;
public writtenoffAt: Date;
public discountType: DiscountType;
public discount: number;
public adjustment: number;
public customerId: number;
public invoiceNo: string;
public referenceNo: string;
public pdfTemplateId: number;
public userId: number;
public branchId: number;
public warehouseId: number;
public taxes!: TaxRateTransaction[];
public entries!: ItemEntry[];
public attachments!: Document[];
public writtenoffExpenseAccount!: Account;
public paymentMethods!: TransactionPaymentServiceEntry[];
/**
* Table name
*/
static get tableName() {
return 'sales_invoices';
}
/**
* Timestamps columns.
*/
get timestamps() {
return ['created_at', 'updated_at'];
}
/**
*
*/
get pluralName() {
return 'asdfsdf';
}
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return [
'isDelivered',
'isOverdue',
'isPartiallyPaid',
'isFullyPaid',
'isWrittenoff',
'isPaid',
'dueAmount',
'balanceAmount',
'remainingDays',
'overdueDays',
'subtotal',
'subtotalLocal',
'subtotalExludingTax',
'taxAmountWithheldLocal',
'discountAmount',
'discountAmountLocal',
'discountPercentage',
'total',
'totalLocal',
'writtenoffAmountLocal',
'adjustmentLocal',
];
}
/**
* Invoice amount.
* @todo Sugger attribute to balance, we need to rename the balance to amount.
* @returns {number}
*/
get amount() {
return this.balance;
}
/**
* Invoice amount in base currency.
* @returns {number}
*/
get amountLocal() {
return this.amount * this.exchangeRate;
}
/**
* Subtotal. (Tax inclusive) if the tax inclusive is enabled.
* @returns {number}
*/
get subtotal() {
return this.amount;
}
/**
* Subtotal in base currency. (Tax inclusive) if the tax inclusive is enabled.
* @returns {number}
*/
get subtotalLocal() {
return this.amountLocal;
}
/**
* Sale invoice amount excluding tax.
* @returns {number}
*/
get subtotalExludingTax() {
return this.isInclusiveTax
? this.subtotal - this.taxAmountWithheld
: this.subtotal;
}
/**
* Tax amount withheld in base currency.
* @returns {number}
*/
get taxAmountWithheldLocal() {
return this.taxAmountWithheld * this.exchangeRate;
}
/**
* Discount amount.
* @returns {number}
*/
get discountAmount() {
return this.discountType === DiscountType.Amount
? this.discount
: this.subtotal * (this.discount / 100);
}
/**
* Local discount amount.
* @returns {number | null}
*/
get discountAmountLocal() {
return this.discountAmount ? this.discountAmount * this.exchangeRate : null;
}
/**
* Discount percentage.
* @returns {number | null}
*/
get discountPercentage(): number | null {
return this.discountType === DiscountType.Percentage ? this.discount : null;
}
/**
* Adjustment amount in local currency.
* @returns {number | null}
*/
get adjustmentLocal(): number | null {
return this.adjustment ? this.adjustment * this.exchangeRate : null;
}
/**
* Invoice total. (Tax included)
* @returns {number}
*/
get total() {
const adjustmentAmount = defaultTo(this.adjustment, 0);
return R.compose(
R.add(adjustmentAmount),
R.subtract(R.__, this.discountAmount),
R.when(R.always(this.isInclusiveTax), R.add(this.taxAmountWithheld)),
)(this.subtotal);
}
/**
* Invoice total in local currency. (Tax included)
* @returns {number}
*/
get totalLocal() {
return this.total * this.exchangeRate;
}
/**
* Detarmines whether the invoice is delivered.
* @return {boolean}
*/
get isDelivered() {
return !!this.deliveredAt;
}
/**
* Detarmines the due date is over.
* @return {boolean}
*/
get isOverdue() {
return this.overdueDays > 0;
}
/**
* Retrieve the sale invoice balance.
* @return {number}
*/
get balanceAmount() {
return this.paymentAmount + this.writtenoffAmount + this.creditedAmount;
}
/**
* Retrieve the invoice due amount.
* Equation (Invoice amount - payment amount = Due amount)
* @return {boolean}
*/
get dueAmount() {
return Math.max(this.total - this.balanceAmount, 0);
}
/**
* Detarmine whether the invoice paid partially.
* @return {boolean}
*/
get isPartiallyPaid() {
return this.dueAmount !== this.total && this.dueAmount > 0;
}
/**
* Deetarmine whether the invoice paid fully.
* @return {boolean}
*/
get isFullyPaid() {
return this.dueAmount === 0;
}
/**
* Detarmines whether the invoice paid fully or partially.
* @return {boolean}
*/
get isPaid() {
return this.isPartiallyPaid || this.isFullyPaid;
}
/**
* Detarmines whether the sale invoice is written-off.
* @return {boolean}
*/
get isWrittenoff() {
return Boolean(this.writtenoffAt);
}
/**
* Retrieve the remaining days in number
* @return {number|null}
*/
get remainingDays() {
const dateMoment = moment();
const dueDateMoment = moment(this.dueDate);
return Math.max(dueDateMoment.diff(dateMoment, 'days'), 0);
}
/**
* Written-off amount in local currency.
* @returns {number}
*/
get writtenoffAmountLocal() {
return this.writtenoffAmount * this.exchangeRate;
}
/**
* Retrieve the overdue days in number.
* @return {number|null}
*/
get overdueDays() {
const dateMoment = moment();
const dueDateMoment = moment(this.dueDate);
return Math.max(dateMoment.diff(dueDateMoment, 'days'), 0);
}
/**
* Model modifiers.
*/
static get modifiers() {
return {
/**
* Filters the due invoices.
*/
dueInvoices(query) {
query.where(
raw(`
COALESCE(BALANCE, 0) -
COALESCE(PAYMENT_AMOUNT, 0) -
COALESCE(WRITTENOFF_AMOUNT, 0) -
COALESCE(CREDITED_AMOUNT, 0) > 0
`),
);
},
/**
* Filters the invoices between the given date range.
*/
filterDateRange(
query,
startDate: MomentInput,
endDate?: MomentInput,
type: unitOfTime.StartOf = 'day',
) {
const dateFormat = 'YYYY-MM-DD';
const fromDate = moment(startDate).startOf(type).format(dateFormat);
const toDate = moment(endDate).endOf(type).format(dateFormat);
if (startDate) {
query.where('invoice_date', '>=', fromDate);
}
if (endDate) {
query.where('invoice_date', '<=', toDate);
}
},
/**
* Filters the invoices in draft status.
*/
draft(query) {
query.where('delivered_at', null);
},
/**
* Filters the published invoices.
*/
published(query) {
query.whereNot('delivered_at', null);
},
/**
* Filters the delivered invoices.
*/
delivered(query) {
query.whereNot('delivered_at', null);
},
/**
* Filters the unpaid invoices.
*/
unpaid(query) {
query.where(raw('PAYMENT_AMOUNT = 0'));
},
/**
* Filters the overdue invoices.
*/
overdue(query, asDate = moment().format('YYYY-MM-DD')) {
query.where('due_date', '<', asDate);
},
/**
* Filters the not overdue invoices.
*/
notOverdue(query, asDate = moment().format('YYYY-MM-DD')) {
query.where('due_date', '>=', asDate);
},
/**
* Filters the partially invoices.
*/
partiallyPaid(query) {
query.whereNot('payment_amount', 0);
query.whereNot(raw('`PAYMENT_AMOUNT` = `BALANCE`'));
},
/**
* Filters the paid invoices.
*/
paid(query) {
query.where(raw('PAYMENT_AMOUNT = BALANCE'));
},
/**
* Filters the sale invoices from the given date.
*/
fromDate(query, fromDate) {
query.where('invoice_date', '<=', fromDate);
},
/**
* Sort the sale invoices by full-payment invoices.
*/
sortByStatus(query, order) {
query.orderByRaw(`PAYMENT_AMOUNT = BALANCE ${order}`);
},
/**
* Sort the sale invoices by the due amount.
*/
sortByDueAmount(query, order) {
query.orderByRaw(`BALANCE - PAYMENT_AMOUNT ${order}`);
},
/**
* Retrieve the max invoice
*/
maxInvoiceNo(query, prefix, number) {
query
.select(raw(`REPLACE(INVOICE_NO, "${prefix}", "") AS INV_NUMBER`))
.havingRaw('CHAR_LENGTH(INV_NUMBER) = ??', [number.length])
.orderBy('invNumber', 'DESC')
.limit(1)
.first();
},
byPrefixAndNumber(query, prefix, number) {
query.where('invoice_no', `${prefix}${number}`);
},
/**
* Status filter.
*/
statusFilter(query, filterType) {
switch (filterType) {
case 'draft':
query.modify('draft');
break;
case 'delivered':
query.modify('delivered');
break;
case 'unpaid':
query.modify('unpaid');
break;
case 'overdue':
default:
query.modify('overdue');
break;
case 'partially-paid':
query.modify('partiallyPaid');
break;
case 'paid':
query.modify('paid');
break;
}
},
/**
* Filters by branches.
*/
filterByBranches(query, branchesIds) {
const formattedBranchesIds = castArray(branchesIds);
query.whereIn('branchId', formattedBranchesIds);
},
dueInvoicesFromDate(query, asDate = moment().format('YYYY-MM-DD')) {
query.modify('dueInvoices');
query.modify('notOverdue', asDate);
query.modify('fromDate', asDate);
},
overdueInvoicesFromDate(query, asDate = moment().format('YYYY-MM-DD')) {
query.modify('dueInvoices');
query.modify('overdue', asDate);
query.modify('fromDate', asDate);
},
};
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const {
AccountTransaction,
} = require('../../Accounts/models/AccountTransaction.model');
const {
ItemEntry,
} = require('../../TransactionItemEntry/models/ItemEntry');
const { Customer } = require('../../Customers/models/Customer');
// const InventoryCostLotTracker = require('models/InventoryCostLotTracker');
const {
PaymentReceivedEntry,
} = require('../../PaymentReceived/models/PaymentReceivedEntry');
const { Branch } = require('../../Branches/models/Branch.model');
const { Warehouse } = require('../../Warehouses/models/Warehouse.model');
const { Account } = require('../../Accounts/models/Account.model');
const {
TaxRateTransaction,
} = require('../../TaxRates/models/TaxRateTransaction.model');
const { Document } = require('../../ChromiumlyTenancy/models/Document');
// const { MatchedBankTransaction } = require('models/MatchedBankTransaction');
const {
TransactionPaymentServiceEntry,
} = require('../../PaymentServices/models/TransactionPaymentServiceEntry.model');
const {
PdfTemplateModel,
} = require('../../PdfTemplate/models/PdfTemplate');
return {
/**
* Sale invoice associated entries.
*/
entries: {
relation: Model.HasManyRelation,
modelClass: ItemEntry,
join: {
from: 'sales_invoices.id',
to: 'items_entries.referenceId',
},
filter(builder) {
builder.where('reference_type', 'SaleInvoice');
builder.orderBy('index', 'ASC');
},
},
/**
* Belongs to customer model.
*/
customer: {
relation: Model.BelongsToOneRelation,
modelClass: Customer,
join: {
from: 'sales_invoices.customerId',
to: 'contacts.id',
},
filter(query) {
query.where('contact_service', 'Customer');
},
},
/**
* Invoice has associated account transactions.
*/
transactions: {
relation: Model.HasManyRelation,
modelClass: AccountTransaction,
join: {
from: 'sales_invoices.id',
to: 'accounts_transactions.referenceId',
},
filter(builder) {
builder.where('reference_type', 'SaleInvoice');
},
},
/**
* Invoice may has associated cost transactions.
*/
// costTransactions: {
// relation: Model.HasManyRelation,
// modelClass: InventoryCostLotTracker.default,
// join: {
// from: 'sales_invoices.id',
// to: 'inventory_cost_lot_tracker.transactionId',
// },
// filter(builder) {
// builder.where('transaction_type', 'SaleInvoice');
// },
// },
/**
* Invoice may has associated payment entries.
*/
paymentEntries: {
relation: Model.HasManyRelation,
modelClass: PaymentReceivedEntry,
join: {
from: 'sales_invoices.id',
to: 'payment_receives_entries.invoiceId',
},
},
/**
* Invoice may has associated branch.
*/
branch: {
relation: Model.BelongsToOneRelation,
modelClass: Branch,
join: {
from: 'sales_invoices.branchId',
to: 'branches.id',
},
},
/**
* Invoice may has associated warehouse.
*/
warehouse: {
relation: Model.BelongsToOneRelation,
modelClass: Warehouse,
join: {
from: 'sales_invoices.warehouseId',
to: 'warehouses.id',
},
},
/**
* Invoice may has associated written-off expense account.
*/
writtenoffExpenseAccount: {
relation: Model.BelongsToOneRelation,
modelClass: Account,
join: {
from: 'sales_invoices.writtenoffExpenseAccountId',
to: 'accounts.id',
},
},
/**
* Invoice may has associated tax rate transactions.
*/
taxes: {
relation: Model.HasManyRelation,
modelClass: TaxRateTransaction,
join: {
from: 'sales_invoices.id',
to: 'tax_rate_transactions.referenceId',
},
filter(builder) {
builder.where('reference_type', 'SaleInvoice');
},
},
/**
* Sale invoice transaction may has many attached attachments.
*/
attachments: {
relation: Model.ManyToManyRelation,
modelClass: Document,
join: {
from: 'sales_invoices.id',
through: {
from: 'document_links.modelId',
to: 'document_links.documentId',
},
to: 'documents.id',
},
filter(query) {
query.where('model_ref', 'SaleInvoice');
},
},
/**
* Sale invocie may belongs to matched bank transaction.
*/
// matchedBankTransaction: {
// relation: Model.HasManyRelation,
// modelClass: MatchedBankTransaction,
// join: {
// from: 'sales_invoices.id',
// to: 'matched_bank_transactions.referenceId',
// },
// filter(query) {
// query.where('reference_type', 'SaleInvoice');
// },
// },
/**
* Sale invoice may belongs to payment methods entries.
*/
paymentMethods: {
relation: Model.HasManyRelation,
modelClass: TransactionPaymentServiceEntry,
join: {
from: 'sales_invoices.id',
to: 'transactions_payment_methods.referenceId',
},
beforeInsert: (model) => {
model.referenceType = 'SaleInvoice';
},
filter: (query) => {
query.where('reference_type', 'SaleInvoice');
},
},
/**
* Sale invoice may belongs to pdf branding template.
*/
pdfTemplate: {
relation: Model.BelongsToOneRelation,
modelClass: PdfTemplateModel,
join: {
from: 'sales_invoices.pdfTemplateId',
to: 'pdf_templates.id',
},
},
};
}
/**
* Change payment amount.
* @param {Integer} invoiceId
* @param {Numeric} amount
*/
static async changePaymentAmount(invoiceId, amount, trx) {
const changeMethod = amount > 0 ? 'increment' : 'decrement';
await this.query(trx)
.where('id', invoiceId)
[changeMethod]('payment_amount', Math.abs(amount));
}
/**
* Sale invoice meta.
*/
// static get meta() {
// return SaleInvoiceMeta;
// }
static dueAmountFieldSortQuery(query, role) {
query.modify('sortByDueAmount', role.order);
}
/**
* Retrieve the default custom views, roles and columns.
*/
// static get defaultViews() {
// return DEFAULT_VIEWS;
// }
/**
* Model searchable.
*/
static get searchable() {
return true;
}
/**
* Model search attributes.
*/
static get searchRoles(): ISearchRole[] {
return [
{ fieldKey: 'invoice_no', comparator: 'contains' },
// { condition: 'or', fieldKey: 'reference_no', comparator: 'contains' },
// { condition: 'or', fieldKey: 'amount', comparator: 'equals' },
];
}
/**
* Prevents mutate base currency since the model is not empty.
*/
static get preventMutateBaseCurrency() {
return true;
}
}

View File

@@ -0,0 +1,37 @@
import { JOB_REF, Process, Processor } from '@nestjs/bull';
import { Job } from 'bull';
import { SendSaleInvoiceMailJob, SendSaleInvoiceQueue } from '../constants';
import { SendSaleInvoiceMail } from '../commands/SendSaleInvoiceMail';
import { Inject, Scope } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { ClsService, UseCls } from 'nestjs-cls';
import { SendSaleInvoiceMailJobPayload } from '../SaleInvoice.types';
@Processor({
name: SendSaleInvoiceQueue,
scope: Scope.REQUEST,
})
export class SendSaleInvoiceMailProcessor {
constructor(
private readonly sendSaleInvoiceMail: SendSaleInvoiceMail,
@Inject(REQUEST) private readonly request: Request,
@Inject(JOB_REF)
private readonly jobRef: Job<SendSaleInvoiceMailJobPayload>,
private readonly clsService: ClsService,
) {}
@Process(SendSaleInvoiceMailJob)
async handleSendInvoice() {
const { messageOptions, saleInvoiceId, organizationId, userId } =
this.jobRef.data;
this.clsService.set('organizationId', organizationId);
this.clsService.set('userId', userId);
try {
await this.sendSaleInvoiceMail.sendMail(saleInvoiceId, messageOptions);
} catch (error) {
console.log(error);
}
}
}

View File

@@ -0,0 +1,222 @@
import { SaleInvoiceTaxEntryTransformer } from './SaleInvoiceTaxEntry.transformer';
import { SaleInvoiceTransformer } from './SaleInvoice.transformer';
import { contactAddressTextFormat } from '@/utils/address-text-format';
import { Transformer } from '@/modules/Transformer/Transformer';
import { ItemEntryTransformer } from '@/modules/TransactionItemEntry/ItemEntry.transformer';
import { GetPdfTemplateTransformer } from '@/modules/PdfTemplate/queries/GetPdfTemplate.transformer';
export class GetInvoicePaymentLinkMetaTransformer extends SaleInvoiceTransformer {
/**
* Exclude these attributes from payment link object.
* @returns {Array}
*/
public excludeAttributes = (): string[] => {
return ['*'];
};
/**
* Included attributes.
* @returns {string[]}
*/
public includeAttributes = (): string[] => {
return [
'customerName',
'dueAmount',
'dueDateFormatted',
'invoiceDateFormatted',
'total',
'totalFormatted',
'totalLocalFormatted',
'subtotal',
'subtotalFormatted',
'subtotalLocalFormatted',
'dueAmount',
'dueAmountFormatted',
'paymentAmount',
'paymentAmountFormatted',
'dueDate',
'dueDateFormatted',
'invoiceNo',
'invoiceMessage',
'termsConditions',
'entries',
'taxes',
'organization',
'isReceivable',
'hasStripePaymentMethod',
'formattedCustomerAddress',
'brandingTemplate',
];
};
public customerName(invoice) {
return invoice.customer.displayName;
}
/**
* Retrieves the organization metadata for the payment link.
* @returns
*/
public organization(invoice) {
return this.item(
this.context.organization,
new GetPaymentLinkOrganizationMetaTransformer()
);
}
/**
* Retrieves the branding template for the payment link.
* @param {} invoice
* @returns
*/
public brandingTemplate(invoice) {
return this.item(
invoice.pdfTemplate,
new GetInvoicePaymentLinkBrandingTemplate()
);
}
/**
* Retrieves the entries of the sale invoice.
* @param {ISaleInvoice} invoice
* @returns {}
*/
protected entries = (invoice) => {
return this.item(
invoice.entries,
new GetInvoicePaymentLinkEntryMetaTransformer(),
{
currencyCode: invoice.currencyCode,
}
);
};
/**
* Retrieves the sale invoice entries.
* @returns {}
*/
protected taxes = (invoice) => {
return this.item(
invoice.taxes,
new GetInvoicePaymentLinkTaxEntryTransformer(),
{
subtotal: invoice.subtotal,
isInclusiveTax: invoice.isInclusiveTax,
currencyCode: invoice.currencyCode,
}
);
};
protected isReceivable(invoice) {
return invoice.dueAmount > 0;
}
protected hasStripePaymentMethod(invoice) {
return invoice.paymentMethods.some(
(paymentMethod) => paymentMethod.paymentIntegration.service === 'Stripe'
);
}
get customerAddressFormat() {
return `{ADDRESS_1}
{ADDRESS_2}
{CITY} {STATE} {POSTAL_CODE}
{COUNTRY}
{PHONE}`;
}
/**
* Retrieves the formatted customer address.
* @param invoice
* @returns {string}
*/
protected formattedCustomerAddress(invoice) {
return contactAddressTextFormat(
invoice.customer,
this.customerAddressFormat
);
}
}
class GetPaymentLinkOrganizationMetaTransformer extends Transformer {
/**
* Include these attributes to item entry object.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return [
'primaryColor',
'name',
'address',
'logoUri',
'addressTextFormatted',
];
};
public excludeAttributes = (): string[] => {
return ['*'];
};
/**
* Retrieves the formatted text of organization address.
* @returns {string}
*/
public addressTextFormatted() {
return this.context.organization.addressTextFormatted;
}
}
class GetInvoicePaymentLinkEntryMetaTransformer extends ItemEntryTransformer {
/**
* Include these attributes to item entry object.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return [
'quantity',
'quantityFormatted',
'rate',
'rateFormatted',
'total',
'totalFormatted',
'itemName',
'description',
];
};
public itemName(entry) {
return entry.item.name;
}
/**
* Exclude these attributes from payment link object.
* @returns {Array}
*/
public excludeAttributes = (): string[] => {
return ['*'];
};
}
class GetInvoicePaymentLinkTaxEntryTransformer extends SaleInvoiceTaxEntryTransformer {
/**
* Included attributes.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return ['name', 'taxRateCode', 'taxRateAmount', 'taxRateAmountFormatted'];
};
}
class GetInvoicePaymentLinkBrandingTemplate extends GetPdfTemplateTransformer {
public includeAttributes = (): string[] => {
return ['companyLogoUri', 'primaryColor'];
};
public excludeAttributes = (): string[] => {
return ['*'];
};
primaryColor = (template) => {
return template.attributes?.primaryColor;
};
}

View File

@@ -0,0 +1,53 @@
import {
InvoicePaymentEmailProps,
renderInvoicePaymentEmail,
} from '@bigcapital/email-components';
import { GetSaleInvoice } from './GetSaleInvoice.service';
import { GetPdfTemplateService } from '@/modules/PdfTemplate/queries/GetPdfTemplate.service';
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
import { GetInvoicePaymentMailAttributesTransformer } from './GetInvoicePaymentMailAttributes.transformer';
import { Injectable } from '@nestjs/common';
@Injectable()
export class GetInvoicePaymentMail {
constructor(
private readonly getSaleInvoiceService: GetSaleInvoice,
private readonly getBrandingTemplate: GetPdfTemplateService,
private readonly transformer: TransformerInjectable,
) {}
/**
* Retrieves the mail template attributes of the given invoice.
* Invoice template attributes are composed of the invoice and branding template attributes.
* @param {number} invoiceId - Invoice id.
*/
public async getMailTemplateAttributes(invoiceId: number) {
const invoice = await this.getSaleInvoiceService.getSaleInvoice(invoiceId);
const brandingTemplate = await this.getBrandingTemplate.getPdfTemplate(
invoice.pdfTemplateId,
);
const mailTemplateAttributes = await this.transformer.transform(
invoice,
new GetInvoicePaymentMailAttributesTransformer(),
{
invoice,
brandingTemplate,
},
);
return mailTemplateAttributes;
}
/**
* Retrieves the mail template html content.
* @param {number} invoiceId - Invoice id.
*/
public async getMailTemplate(
invoiceId: number,
overrideAttributes?: Partial<InvoicePaymentEmailProps>,
): Promise<string> {
const attributes = await this.getMailTemplateAttributes(invoiceId);
const mergedAttributes = { ...attributes, ...overrideAttributes };
return renderInvoicePaymentEmail(mergedAttributes);
}
}

View File

@@ -0,0 +1,135 @@
import { Transformer } from "@/modules/Transformer/Transformer";
export class GetInvoicePaymentMailAttributesTransformer extends Transformer {
/**
* Include these attributes to item entry object.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return [
'companyLogoUri',
'companyName',
'invoiceAmount',
'primaryColor',
'invoiceAmount',
'invoiceMessage',
'dueDate',
'dueDateLabel',
'invoiceNumber',
'invoiceNumberLabel',
'total',
'totalLabel',
'dueAmount',
'dueAmountLabel',
'viewInvoiceButtonLabel',
'viewInvoiceButtonUrl',
'items',
];
};
public excludeAttributes = (): string[] => {
return ['*'];
};
public companyLogoUri(): string {
return this.options.brandingTemplate?.companyLogoUri;
}
public companyName(): string {
return this.context.organization.name;
}
public invoiceAmount(): string {
return this.options.invoice.totalFormatted;
}
public primaryColor(): string {
return this.options?.brandingTemplate?.attributes?.primaryColor;
}
public invoiceMessage(): string {
return '';
}
public dueDate(): string {
return this.options?.invoice?.dueDateFormatted;
}
public dueDateLabel(): string {
return 'Due {dueDate}';
}
public invoiceNumber(): string {
return this.options?.invoice?.invoiceNo;
}
public invoiceNumberLabel(): string {
return 'Invoice # {invoiceNumber}';
}
public total(): string {
return this.options.invoice?.totalFormatted;
}
public totalLabel(): string {
return 'Total';
}
public dueAmount(): string {
return this.options?.invoice.dueAmountFormatted;
}
public dueAmountLabel(): string {
return 'Due Amount';
}
public viewInvoiceButtonLabel(): string {
return 'View Invoice';
}
public viewInvoiceButtonUrl(): string {
return '';
}
public items(): Array<any> {
return this.item(
this.options.invoice?.entries,
new GetInvoiceMailTemplateItemAttrsTransformer()
);
}
}
class GetInvoiceMailTemplateItemAttrsTransformer extends Transformer {
/**
* Include these attributes to item entry object.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return ['quantity', 'label', 'rate'];
};
public excludeAttributes = (): string[] => {
return ['*'];
};
public quantity(entry): string {
return entry?.quantity;
}
public label(entry): string {
return entry?.item?.name;
}
public rate(entry): string {
return entry?.rateFormatted;
}
}

View File

@@ -0,0 +1,36 @@
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
import { InvoicePaymentTransactionTransformer } from './InvoicePaymentTransaction.transformer';
import { Inject, Injectable } from '@nestjs/common';
import { PaymentReceivedEntry } from '@/modules/PaymentReceived/models/PaymentReceivedEntry';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable()
export class GetInvoicePaymentsService {
constructor(
private readonly transformer: TransformerInjectable,
@Inject(PaymentReceivedEntry.name)
private readonly paymentReceivedEntryModel: TenantModelProxy<
typeof PaymentReceivedEntry
>,
) {}
/**
* Retrieve the invoice associated payments transactions.
* @param {number} tenantId - Tenant id.
* @param {number} invoiceId - Invoice id.
*/
public getInvoicePayments = async (invoiceId: number) => {
const paymentsEntries = await this.paymentReceivedEntryModel()
.query()
.where('invoiceId', invoiceId)
.withGraphJoined('payment.depositAccount')
.withGraphJoined('invoice')
.orderBy('payment:paymentDate', 'ASC');
return this.transformer.transform(
paymentsEntries,
new InvoicePaymentTransactionTransformer(),
);
};
}

View File

@@ -0,0 +1,56 @@
import { EventEmitter2 } from '@nestjs/event-emitter';
import { Inject, Injectable } from '@nestjs/common';
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
import { SaleInvoice } from '../models/SaleInvoice';
import { SaleInvoiceTransformer } from './SaleInvoice.transformer';
import { CommandSaleInvoiceValidators } from '../commands/CommandSaleInvoiceValidators.service';
import { events } from '@/common/events/events';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable()
export class GetSaleInvoice {
constructor(
private transformer: TransformerInjectable,
private validators: CommandSaleInvoiceValidators,
private eventPublisher: EventEmitter2,
@Inject(SaleInvoice.name)
private saleInvoiceModel: TenantModelProxy<typeof SaleInvoice>,
) {}
/**
* Retrieve sale invoice with associated entries.
* @param {Number} saleInvoiceId -
* @param {ISystemUser} authorizedUser -
* @return {Promise<ISaleInvoice>}
*/
public async getSaleInvoice(saleInvoiceId: number): Promise<SaleInvoice> {
const saleInvoice = await this.saleInvoiceModel()
.query()
.findById(saleInvoiceId)
.withGraphFetched('entries.item')
.withGraphFetched('entries.tax')
.withGraphFetched('customer')
.withGraphFetched('branch')
.withGraphFetched('taxes.taxRate')
.withGraphFetched('attachments')
.withGraphFetched('paymentMethods');
// Validates the given sale invoice existance.
this.validators.validateInvoiceExistance(saleInvoice);
const transformed = await this.transformer.transform(
saleInvoice,
new SaleInvoiceTransformer(),
);
const eventPayload = {
saleInvoiceId,
};
// Triggers the `onSaleInvoiceItemViewed` event.
await this.eventPublisher.emitAsync(
events.saleInvoice.onViewed,
eventPayload,
);
return transformed;
}
}

View File

@@ -0,0 +1,3 @@
export class GetSaleInvoiceMailReminder {
public getInvoiceMailReminder(tenantId: number, saleInvoiceId: number) {}
}

View File

@@ -0,0 +1,49 @@
import { Inject, Injectable } from '@nestjs/common';
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
import { GetSaleInvoiceMailStateTransformer } from './GetSaleInvoiceMailState.transformer';
import { SendSaleInvoiceMailCommon } from '../commands/SendInvoiceInvoiceMailCommon.service';
import { SaleInvoice } from '../models/SaleInvoice';
import { SaleInvoiceMailState } from '../SaleInvoice.types';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable()
export class GetSaleInvoiceMailState {
constructor(
private transformer: TransformerInjectable,
private invoiceMail: SendSaleInvoiceMailCommon,
@Inject(SaleInvoice.name)
private saleInvoiceModel: TenantModelProxy<typeof SaleInvoice>,
) {}
/**
* Retrieves the invoice mail state of the given sale invoice.
* Invoice mail state includes the mail options, branding attributes and the invoice details.
* @param {number} saleInvoiceId - Sale invoice id.
* @returns {Promise<SaleInvoiceMailState>}
*/
public async getInvoiceMailState(
saleInvoiceId: number,
): Promise<SaleInvoiceMailState> {
const saleInvoice = await this.saleInvoiceModel()
.query()
.findById(saleInvoiceId)
.withGraphFetched('customer')
.withGraphFetched('entries.item')
.withGraphFetched('pdfTemplate')
.throwIfNotFound();
const mailOptions =
await this.invoiceMail.getInvoiceMailOptions(saleInvoiceId);
// Transforms the sale invoice mail state.
const transformed = await this.transformer.transform(
saleInvoice,
new GetSaleInvoiceMailStateTransformer(),
{
mailOptions,
},
);
return transformed;
}
}

View File

@@ -0,0 +1,129 @@
import { ItemEntryTransformer } from '@/modules/TransactionItemEntry/ItemEntry.transformer';
import { SaleInvoiceTransformer } from './SaleInvoice.transformer';
export class GetSaleInvoiceMailStateTransformer extends SaleInvoiceTransformer {
/**
* Exclude these attributes from user object.
* @returns {Array}
*/
public excludeAttributes = (): string[] => {
return ['*'];
};
/**
* Included attributes.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return [
'invoiceDate',
'invoiceDateFormatted',
'dueDate',
'dueDateFormatted',
'dueAmount',
'dueAmountFormatted',
'total',
'totalFormatted',
'subtotal',
'subtotalFormatted',
'invoiceNo',
'entries',
'companyName',
'companyLogoUri',
'primaryColor',
'customerName',
];
};
/**
* Retrieves the customer name of the invoice.
* @returns {string}
*/
protected customerName = (invoice) => {
return invoice.customer.displayName;
};
/**
* Retrieves the company name.
* @returns {string}
*/
protected companyName = () => {
return this.context.organization.name;
};
/**
* Retrieves the company logo uri.
* @returns {string | null}
*/
protected companyLogoUri = (invoice) => {
return invoice.pdfTemplate?.companyLogoUri;
};
/**
* Retrieves the primary color.
* @returns {string}
*/
protected primaryColor = (invoice) => {
return invoice.pdfTemplate?.attributes?.primaryColor;
};
/**
*
* @param invoice
* @returns
*/
protected entries = (invoice) => {
return this.item(
invoice.entries,
new GetSaleInvoiceMailStateEntryTransformer(),
{
currencyCode: invoice.currencyCode,
}
);
};
/**
* Merges the mail options with the invoice object.
*/
public transform = (object: any) => {
return {
...this.options.mailOptions,
...object,
};
};
}
class GetSaleInvoiceMailStateEntryTransformer extends ItemEntryTransformer {
/**
* Exclude these attributes from user object.
* @returns {Array}
*/
public excludeAttributes = (): string[] => {
return ['*'];
};
public name = (entry) => {
return entry.item.name;
};
public includeAttributes = (): string[] => {
return [
'name',
'quantity',
'quantityFormatted',
'rate',
'rateFormatted',
'total',
'totalFormatted',
];
};
}

View File

@@ -0,0 +1,27 @@
import { Inject, Injectable } from '@nestjs/common';
import { PdfTemplateModel } from '@/modules/PdfTemplate/models/PdfTemplate';
import { ISaleInvocieState } from '../SaleInvoice.types';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable()
export class GetSaleInvoiceState {
constructor(
@Inject(PdfTemplateModel.name)
private pdfTemplateModel: TenantModelProxy<typeof PdfTemplateModel>,
) {}
/**
* Retrieves the create/edit invoice state.
* @return {Promise<ISaleInvoice>}
*/
public async getSaleInvoiceState(): Promise<ISaleInvocieState> {
const defaultPdfTemplate = await this.pdfTemplateModel()
.query()
.findOne({ resource: 'SaleInvoice' })
.modify('default');
return {
defaultTemplateId: defaultPdfTemplate?.id,
};
}
}

View File

@@ -0,0 +1,67 @@
import * as R from 'ramda';
import { SaleInvoiceTransformer } from './SaleInvoice.transformer';
import { Injectable } from '@nestjs/common';
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
import { DynamicListService } from '@/modules/DynamicListing/DynamicList.service';
import { IFilterMeta, IPaginationMeta } from '@/interfaces/Model';
import { SaleInvoice } from '../models/SaleInvoice';
import { ISalesInvoicesFilter } from '../SaleInvoice.types';
import { Knex } from 'knex';
@Injectable()
export class GetSaleInvoicesService {
constructor(
private readonly dynamicListService: DynamicListService,
private readonly transformer: TransformerInjectable,
) {}
/**
* Retrieve sales invoices filterable and paginated list.
* @param {ISalesInvoicesFilter} filterDTO -
* @returns {Promise<{ salesInvoices: SaleInvoice[]; pagination: IPaginationMeta; filterMeta: IFilterMeta; }>}
*/
public async getSaleInvoices(filterDTO: ISalesInvoicesFilter): Promise<{
salesInvoices: SaleInvoice[];
pagination: IPaginationMeta;
filterMeta: IFilterMeta;
}> {
// Parses stringified filter roles.
const filter = this.parseListFilterDTO(filterDTO);
// Dynamic list service.
const dynamicFilter = await this.dynamicListService.dynamicList(
SaleInvoice,
filter,
);
const { results, pagination } = await SaleInvoice.query()
.onBuild((builder) => {
builder.withGraphFetched('entries.item');
builder.withGraphFetched('customer');
dynamicFilter.buildQuery()(builder);
filterDTO?.filterQuery?.(builder as any);
})
.pagination(filter.page - 1, filter.pageSize);
// Retrieves the transformed sale invoices.
const salesInvoices = await this.transformer.transform(
results,
new SaleInvoiceTransformer(),
);
return {
salesInvoices,
pagination,
filterMeta: dynamicFilter.getResponseMeta(),
};
}
/**
* Parses the sale invoice list filter DTO.
* @param filterDTO
* @returns
*/
private parseListFilterDTO(filterDTO) {
return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO);
}
}

View File

@@ -0,0 +1,31 @@
import { Injectable, Inject } from '@nestjs/common';
import { SaleInvoice } from '../models/SaleInvoice';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable()
export class GetSaleInvoicesPayable {
constructor(
@Inject(SaleInvoice.name)
private readonly saleInvoiceModel: TenantModelProxy<typeof SaleInvoice>,
) {}
/**
* Retrieve due sales invoices.
* @param {number} customerId - Customer id.
*/
public async getPayableInvoices(
customerId?: number,
): Promise<Array<SaleInvoice>> {
const salesInvoices = await this.saleInvoiceModel()
.query()
.onBuild((query) => {
query.modify('dueInvoices');
query.modify('delivered');
if (customerId) {
query.where('customer_id', customerId);
}
});
return salesInvoices;
}
}

View File

@@ -0,0 +1,60 @@
import { Transformer } from "../../Transformer/Transformer";
export class InvoicePaymentTransactionTransformer extends Transformer {
/**
* Include these attributes to sale credit note object.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return ['formattedPaymentAmount', 'formattedPaymentDate'];
};
/**
* Retrieve formatted invoice amount.
* @param {ICreditNote} credit
* @returns {string}
*/
protected formattedPaymentAmount = (entry): string => {
return this.formatNumber(entry.paymentAmount, {
currencyCode: entry.payment.currencyCode,
});
};
/**
* Formatted payment date.
* @param entry
* @returns {string}
*/
protected formattedPaymentDate = (entry): string => {
return this.formatDate(entry.payment.paymentDate);
};
/**
*
* @param entry
* @returns
*/
public transform = (entry) => {
return {
invoiceId: entry.invoiceId,
paymentReceiveId: entry.paymentReceiveId,
paymentDate: entry.payment.paymentDate,
formattedPaymentDate: entry.formattedPaymentDate,
paymentAmount: entry.paymentAmount,
formattedPaymentAmount: entry.formattedPaymentAmount,
currencyCode: entry.payment.currencyCode,
paymentNumber: entry.payment.paymentReceiveNo,
paymentReferenceNo: entry.payment.referenceNo,
invoiceNumber: entry.invoice.invoiceNo,
invoiceReferenceNo: entry.invoice.referenceNo,
depositAccountId: entry.payment.depositAccountId,
depositAccountName: entry.payment.depositAccount.name,
depositAccountSlug: entry.payment.depositAccount.slug,
};
};
}

View File

@@ -0,0 +1,44 @@
import { Injectable } from '@nestjs/common';
import { mergePdfTemplateWithDefaultAttributes } from '../utils';
import { GetPdfTemplateService } from '@/modules/PdfTemplate/queries/GetPdfTemplate.service';
import { GetOrganizationBrandingAttributesService } from '@/modules/PdfTemplate/queries/GetOrganizationBrandingAttributes.service';
import { defaultEstimatePdfBrandingAttributes } from '@/modules/SaleEstimates/constants';
@Injectable()
export class SaleEstimatePdfTemplate {
constructor(
private readonly getPdfTemplateService: GetPdfTemplateService,
private readonly getOrgBrandingAttrs: GetOrganizationBrandingAttributesService,
) {}
/**
* Retrieves the estimate pdf template.
* @param {number} invoiceTemplateId
* @returns
*/
public async getEstimatePdfTemplate(estimateTemplateId: number) {
const template =
await this.getPdfTemplateService.getPdfTemplate(estimateTemplateId);
// Retreives the organization branding attributes.
const commonOrgBrandingAttrs =
await this.getOrgBrandingAttrs.getOrganizationBrandingAttributes();
// Merge the default branding attributes with organization attrs.
const orgainizationBrandingAttrs = {
...defaultEstimatePdfBrandingAttributes,
...commonOrgBrandingAttrs,
};
const brandingTemplateAttrs = {
...template.attributes,
companyLogoUri: template.companyLogoUri,
};
const attributes = mergePdfTemplateWithDefaultAttributes(
brandingTemplateAttrs,
orgainizationBrandingAttrs,
);
return {
...template,
attributes,
};
}
}

View File

@@ -0,0 +1,214 @@
import { Transformer } from '@/modules/Transformer/Transformer';
import { SaleInvoice } from '../models/SaleInvoice';
import { ItemEntryTransformer } from '../../TransactionItemEntry/ItemEntry.transformer';
import { AttachmentTransformer } from '../../Attachments/Attachment.transformer';
import { SaleInvoiceTaxEntryTransformer } from './SaleInvoiceTaxEntry.transformer';
export class SaleInvoiceTransformer extends Transformer {
/**
* Include these attributes to sale invoice object.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return [
'invoiceDateFormatted',
'dueDateFormatted',
'createdAtFormatted',
'dueAmountFormatted',
'paymentAmountFormatted',
'balanceAmountFormatted',
'exchangeRateFormatted',
'subtotalFormatted',
'subtotalLocalFormatted',
'subtotalExludingTaxFormatted',
'taxAmountWithheldFormatted',
'taxAmountWithheldLocalFormatted',
'totalFormatted',
'totalLocalFormatted',
'taxes',
'entries',
'attachments',
];
};
/**
* Retrieve formatted invoice date.
* @param {ISaleInvoice} invoice
* @returns {String}
*/
protected invoiceDateFormatted = (invoice: SaleInvoice): string => {
return this.formatDate(invoice.invoiceDate);
};
/**
* Retrieve formatted due date.
* @param {ISaleInvoice} invoice
* @returns {string}
*/
protected dueDateFormatted = (invoice: SaleInvoice): string => {
return this.formatDate(invoice.dueDate);
};
/**
* Retrieve the formatted created at date.
* @param invoice
* @returns {string}
*/
protected createdAtFormatted = (invoice: SaleInvoice): string => {
return this.formatDate(invoice.createdAt);
};
/**
* Retrieve formatted invoice due amount.
* @param {ISaleInvoice} invoice
* @returns {string}
*/
protected dueAmountFormatted = (invoice: SaleInvoice): string => {
return this.formatNumber(invoice.dueAmount, {
currencyCode: invoice.currencyCode,
});
};
/**
* Retrieve formatted payment amount.
* @param {ISaleInvoice} invoice
* @returns {string}
*/
protected paymentAmountFormatted = (invoice: SaleInvoice): string => {
return this.formatNumber(invoice.paymentAmount, {
currencyCode: invoice.currencyCode,
});
};
/**
* Retrieve the formatted invoice balance.
* @param {ISaleInvoice} invoice
* @returns {string}
*/
protected balanceAmountFormatted = (invoice: SaleInvoice): string => {
return this.formatNumber(invoice.balanceAmount, {
currencyCode: invoice.currencyCode,
});
};
/**
* Retrieve the formatted exchange rate.
* @param {ISaleInvoice} invoice
* @returns {string}
*/
protected exchangeRateFormatted = (invoice: SaleInvoice): string => {
return this.formatNumber(invoice.exchangeRate, { money: false });
};
/**
* Retrieves formatted subtotal in base currency.
* (Tax inclusive if the tax inclusive is enabled)
* @param invoice
* @returns {string}
*/
protected subtotalFormatted = (invoice: SaleInvoice): string => {
return this.formatNumber(invoice.subtotal, {
currencyCode: this.context.organization.baseCurrency,
money: false,
});
};
/**
* Retrieves formatted subtotal in foreign currency.
* (Tax inclusive if the tax inclusive is enabled)
* @param invoice
* @returns {string}
*/
protected subtotalLocalFormatted = (invoice: SaleInvoice): string => {
return this.formatNumber(invoice.subtotalLocal, {
currencyCode: invoice.currencyCode,
});
};
/**
* Retrieves formatted subtotal excluding tax in foreign currency.
* @param invoice
* @returns {string}
*/
protected subtotalExludingTaxFormatted = (invoice: SaleInvoice): string => {
return this.formatNumber(invoice.subtotalExludingTax, {
currencyCode: invoice.currencyCode,
});
};
/**
* Retrieves formatted tax amount withheld in foreign currency.
* @param invoice
* @returns {string}
*/
protected taxAmountWithheldFormatted = (invoice: SaleInvoice): string => {
return this.formatNumber(invoice.taxAmountWithheld, {
currencyCode: invoice.currencyCode,
});
};
/**
* Retrieves formatted tax amount withheld in base currency.
* @param invoice
* @returns {string}
*/
protected taxAmountWithheldLocalFormatted = (invoice: SaleInvoice): string => {
return this.formatNumber(invoice.taxAmountWithheldLocal, {
currencyCode: this.context.organization.baseCurrency,
});
};
/**
* Retrieves formatted total in foreign currency.
* @param invoice
* @returns {string}
*/
protected totalFormatted = (invoice: SaleInvoice): string => {
return this.formatNumber(invoice.total, {
currencyCode: invoice.currencyCode,
});
};
/**
* Retrieves formatted total in base currency.
* @param invoice
* @returns {string}
*/
protected totalLocalFormatted = (invoice: SaleInvoice): string => {
return this.formatNumber(invoice.totalLocal, {
currencyCode: this.context.organization.baseCurrency,
});
};
/**
* Retrieve the taxes lines of sale invoice.
* @param {ISaleInvoice} invoice
*/
protected taxes = (invoice: SaleInvoice) => {
return this.item(invoice.taxes, new SaleInvoiceTaxEntryTransformer(), {
subtotal: invoice.subtotal,
isInclusiveTax: invoice.isInclusiveTax,
currencyCode: invoice.currencyCode,
});
};
/**
* Retrieves the entries of the sale invoice.
* @param {ISaleInvoice} invoice
* @returns {}
*/
protected entries = (invoice: SaleInvoice) => {
return this.item(invoice.entries, new ItemEntryTransformer(), {
currencyCode: invoice.currencyCode,
});
};
/**
* Retrieves the sale invoice attachments.
* @param {ISaleInvoice} invoice
* @returns
*/
protected attachments = (invoice: SaleInvoice) => {
return this.item(invoice.attachments, new AttachmentTransformer());
};
}

View File

@@ -0,0 +1,107 @@
import { Inject, Injectable } from '@nestjs/common';
import { renderInvoicePaperTemplateHtml } from '@bigcapital/pdf-templates';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { GetSaleInvoice } from './GetSaleInvoice.service';
import { transformInvoiceToPdfTemplate } from '../utils';
import { SaleInvoicePdfTemplate } from './SaleInvoicePdfTemplate.service';
import { ChromiumlyTenancy } from '@/modules/ChromiumlyTenancy/ChromiumlyTenancy.service';
import { SaleInvoice } from '../models/SaleInvoice';
import { PdfTemplateModel } from '@/modules/PdfTemplate/models/PdfTemplate';
import { events } from '@/common/events/events';
import { InvoicePdfTemplateAttributes } from '../SaleInvoice.types';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable()
export class SaleInvoicePdf {
constructor(
private chromiumlyTenancy: ChromiumlyTenancy,
private getInvoiceService: GetSaleInvoice,
private invoiceBrandingTemplateService: SaleInvoicePdfTemplate,
private eventPublisher: EventEmitter2,
@Inject(SaleInvoice.name)
private saleInvoiceModel: TenantModelProxy<typeof SaleInvoice>,
@Inject(PdfTemplateModel.name)
private pdfTemplateModel: TenantModelProxy<typeof PdfTemplateModel>,
) {}
/**
* Retrieve sale invoice html content.
* @param {ISaleInvoice} saleInvoice -
* @returns {Promise<string>}
*/
public async getSaleInvoiceHtml(invoiceId: number): Promise<string> {
const brandingAttributes =
await this.getInvoiceBrandingAttributes(invoiceId);
return renderInvoicePaperTemplateHtml({
...brandingAttributes,
});
}
/**
* Retrieve sale invoice pdf content.
* @param {ISaleInvoice} saleInvoice -
* @returns {Promise<[Buffer, string]>}
*/
public async getSaleInvoicePdf(invoiceId: number): Promise<[Buffer, string]> {
const filename = await this.getInvoicePdfFilename(invoiceId);
const htmlContent = await this.getSaleInvoiceHtml(invoiceId);
// Converts the given html content to pdf document.
const buffer = await this.chromiumlyTenancy.convertHtmlContent(htmlContent);
const eventPayload = { saleInvoiceId: invoiceId };
// Triggers the `onSaleInvoicePdfViewed` event.
await this.eventPublisher.emitAsync(
events.saleInvoice.onPdfViewed,
eventPayload,
);
return [buffer, filename];
}
/**
* Retrieves the filename pdf document of the given invoice.
* @param {number} invoiceId
* @returns {Promise<string>}
*/
private async getInvoicePdfFilename(invoiceId: number): Promise<string> {
const invoice = await this.saleInvoiceModel().query().findById(invoiceId);
return `Invoice-${invoice.invoiceNo}`;
}
/**
* Retrieves the branding attributes of the given sale invoice.
* @param {number} invoiceId
* @returns {Promise<InvoicePdfTemplateAttributes>}
*/
private async getInvoiceBrandingAttributes(
invoiceId: number,
): Promise<InvoicePdfTemplateAttributes> {
const invoice = await this.getInvoiceService.getSaleInvoice(invoiceId);
// Retrieve the invoice template id or get the default template id if not found.
const templateId =
invoice.pdfTemplateId ??
(
await this.pdfTemplateModel().query().findOne({
resource: 'SaleInvoice',
default: true,
})
)?.id;
// Get the branding template attributes.
const brandingTemplate =
await this.invoiceBrandingTemplateService.getInvoicePdfTemplate(
templateId,
);
// Merge the branding template attributes with the invoice.
return {
...brandingTemplate.attributes,
...transformInvoiceToPdfTemplate(invoice),
};
}
}

View File

@@ -0,0 +1,43 @@
import { mergePdfTemplateWithDefaultAttributes } from '../utils';
import { defaultInvoicePdfTemplateAttributes } from '../constants';
import { GetOrganizationBrandingAttributesService } from '@/modules/PdfTemplate/queries/GetOrganizationBrandingAttributes.service';
import { GetPdfTemplateService } from '@/modules/PdfTemplate/queries/GetPdfTemplate.service';
import { Injectable } from '@nestjs/common';
@Injectable()
export class SaleInvoicePdfTemplate {
constructor(
private readonly getPdfTemplateService: GetPdfTemplateService,
private readonly getOrgBrandingAttributes: GetOrganizationBrandingAttributesService,
) {}
/**
* Retrieves the invoice pdf template.
* @param {number} invoiceTemplateId
* @returns
*/
async getInvoicePdfTemplate(invoiceTemplateId: number) {
const template =
await this.getPdfTemplateService.getPdfTemplate(invoiceTemplateId);
// Retrieves the organization branding attributes.
const commonOrgBrandingAttrs =
await this.getOrgBrandingAttributes.getOrganizationBrandingAttributes();
const organizationBrandingAttrs = {
...defaultInvoicePdfTemplateAttributes,
...commonOrgBrandingAttrs,
};
const brandingTemplateAttrs = {
...template.attributes,
companyLogoUri: template.companyLogoUri,
};
const attributes = mergePdfTemplateWithDefaultAttributes(
brandingTemplateAttrs,
organizationBrandingAttrs,
);
return {
...template,
attributes,
};
}
}

View File

@@ -0,0 +1,77 @@
import { Transformer } from '@/modules/Transformer/Transformer';
import { getExlusiveTaxAmount, getInclusiveTaxAmount } from '../../TaxRates/utils';
export class SaleInvoiceTaxEntryTransformer extends Transformer {
/**
* Included attributes.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return [
'name',
'taxRateCode',
'taxRate',
'taxRateId',
'taxRateAmount',
'taxRateAmountFormatted',
];
};
/**
* Exclude attributes.
* @returns {string[]}
*/
public excludeAttributes = (): string[] => {
return ['*'];
};
/**
* Retrieve tax rate code.
* @param taxEntry
* @returns {string}
*/
protected taxRateCode = (taxEntry) => {
return taxEntry.taxRate.code;
};
/**
* Retrieve tax rate id.
* @param taxEntry
* @returns {number}
*/
protected taxRate = (taxEntry) => {
return taxEntry.taxAmount || taxEntry.taxRate.rate;
};
/**
* Retrieve tax rate name.
* @param taxEntry
* @returns {string}
*/
protected name = (taxEntry) => {
return taxEntry.taxRate.name;
};
/**
* Retrieve tax rate amount.
* @param taxEntry
*/
protected taxRateAmount = (taxEntry) => {
const taxRate = this.taxRate(taxEntry);
return this.options.isInclusiveTax
? getInclusiveTaxAmount(this.options.subtotal, taxRate)
: getExlusiveTaxAmount(this.options.subtotal, taxRate);
};
/**
* Retrieve formatted tax rate amount.
* @returns {string}
*/
protected taxRateAmountFormatted = (taxEntry) => {
return this.formatNumber(this.taxRateAmount(taxEntry), {
currencyCode: this.options.currencyCode,
});
};
}

View File

@@ -0,0 +1,36 @@
import { Injectable } from '@nestjs/common';
import { DeliverSaleInvoice } from '../commands/DeliverSaleInvoice.service';
import { ERRORS } from '../constants';
import { OnEvent } from '@nestjs/event-emitter';
import { events } from '@/common/events/events';
import { ServiceError } from '@/modules/Items/ServiceError';
import { ISaleInvoiceMailSent } from '../SaleInvoice.types';
@Injectable()
export class InvoiceChangeStatusOnMailSentSubscriber {
constructor(private readonly markInvoiceDelivedService: DeliverSaleInvoice) {}
/**
* Marks the invoice delivered once the invoice mail sent.
* @param {ISaleInvoiceMailSent}
* @returns {Promise<void>}
*/
@OnEvent(events.saleInvoice.onMailReminderSent)
@OnEvent(events.saleInvoice.onMailSent)
async markInvoiceDelivered({
saleInvoiceId,
messageOptions,
}: ISaleInvoiceMailSent) {
try {
await this.markInvoiceDelivedService.deliverSaleInvoice(saleInvoiceId);
} catch (error) {
if (
error instanceof ServiceError &&
error.errorType === ERRORS.SALE_INVOICE_ALREADY_DELIVERED
) {
} else {
throw error;
}
}
}
}

View File

@@ -0,0 +1,22 @@
import { Injectable } from '@nestjs/common';
import { IInventoryCostLotsGLEntriesWriteEvent } from '@/modules/InventoryCost/types/InventoryCost.types';
import { SaleInvoiceCostGLEntries } from '../SaleInvoiceCostGLEntries';
@Injectable()
export class InvoiceCostGLEntriesSubscriber {
constructor(private readonly invoiceCostEntries: SaleInvoiceCostGLEntries) {}
/**
* Writes the invoices cost GL entries once the inventory cost lots be written.
* @param {IInventoryCostLotsGLEntriesWriteEvent}
*/
async writeInvoicesCostEntriesOnCostLotsWritten({
trx,
startingDate,
}: IInventoryCostLotsGLEntriesWriteEvent) {
await this.invoiceCostEntries.writeInventoryCostJournalEntries(
startingDate,
trx,
);
}
}

View File

@@ -0,0 +1,64 @@
import { Injectable } from '@nestjs/common';
import {
ISaleInvoiceCreatedPayload,
ISaleInvoiceDeletePayload,
ISaleInvoiceEditedPayload,
} from '../SaleInvoice.types';
import { OnEvent } from '@nestjs/event-emitter';
import { SaleInvoiceGLEntries } from '../ledger/InvoiceGLEntries';
import { events } from '@/common/events/events';
@Injectable()
export class InvoiceGLEntriesSubscriber {
constructor(public readonly saleInvoiceGLEntries: SaleInvoiceGLEntries) {}
/**
* Records journal entries of the non-inventory invoice.
* @param {ISaleInvoiceCreatedPayload} payload -
* @returns {Promise<void>}
*/
@OnEvent(events.saleInvoice.onCreated)
@OnEvent(events.saleInvoice.onDelivered)
public async handleWriteJournalEntriesOnInvoiceCreated({
saleInvoiceId,
saleInvoice,
trx,
}: ISaleInvoiceCreatedPayload) {
// Can't continue if the sale invoice is not delivered yet.
if (!saleInvoice.deliveredAt) return null;
await this.saleInvoiceGLEntries.writeInvoiceGLEntries(saleInvoiceId, trx);
}
/**
* Records journal entries of the non-inventory invoice.
* @param {ISaleInvoiceEditedPayload} payload -
* @returns {Promise<void>}
*/
@OnEvent(events.saleInvoice.onEdited)
public async handleRewriteJournalEntriesOnceInvoiceEdit({
saleInvoice,
trx,
}: ISaleInvoiceEditedPayload) {
// Can't continue if the sale invoice is not delivered yet.
if (!saleInvoice.deliveredAt) return null;
await this.saleInvoiceGLEntries.rewritesInvoiceGLEntries(
saleInvoice.id,
trx,
);
}
/**
* Handle reverting journal entries once sale invoice delete.
* @param {ISaleInvoiceDeletePayload} payload -
* @returns {Promise<void>}
*/
@OnEvent(events.saleInvoice.onDeleted)
public async handleRevertingInvoiceJournalEntriesOnDelete({
saleInvoiceId,
trx,
}: ISaleInvoiceDeletePayload) {
await this.saleInvoiceGLEntries.revertInvoiceGLEntries(saleInvoiceId, trx);
}
}

View File

@@ -0,0 +1,27 @@
import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { events } from '@/common/events/events';
import { ISaleInvoiceEditingPayload } from '../SaleInvoice.types';
import { InvoicePaymentsGLEntriesRewrite } from '../InvoicePaymentsGLRewrite';
@Injectable()
export class InvoicePaymentGLRewriteSubscriber {
constructor(
private readonly invoicePaymentsRewriteGLEntries: InvoicePaymentsGLEntriesRewrite,
) {}
/**
* Writes associated invoiceso of payment receive once edit.
* @param {ISaleInvoiceEditingPayload} -
*/
@OnEvent(events.saleInvoice.onEdited)
async paymentGLEntriesRewriteOnPaymentEdit({
oldSaleInvoice,
trx,
}: ISaleInvoiceEditingPayload) {
await this.invoicePaymentsRewriteGLEntries.invoicePaymentsGLEntriesRewrite(
oldSaleInvoice.id,
trx,
);
}
}

View File

@@ -0,0 +1,76 @@
import { Injectable } from '@nestjs/common';
import { EventEmitter2, OnEvent } from '@nestjs/event-emitter';
import { PaymentIntegrationTransactionLinkDeleteEventPayload } from '../SaleInvoice.types';
import { PaymentIntegrationTransactionLinkEventPayload } from '../SaleInvoice.types';
import { PaymentIntegrationTransactionLink } from '../SaleInvoice.types';
import { omit } from 'lodash';
import {
ISaleInvoiceCreatedPayload,
ISaleInvoiceDeletingPayload,
} from '../SaleInvoice.types';
import { events } from '@/common/events/events';
import { TransactionPaymentServiceEntry } from '@/modules/PaymentServices/models/TransactionPaymentServiceEntry.model';
@Injectable()
export class InvoicePaymentIntegrationSubscriber {
constructor(private readonly eventPublisher: EventEmitter2) {}
/**
* Handles the creation of payment integration events when a sale invoice is created.
* This method filters enabled payment methods from the invoice and emits a payment
* integration link event for each method.
* @param {ISaleInvoiceCreatedPayload} payload - The payload containing sale invoice creation details.
*/
@OnEvent(events.saleInvoice.onCreated)
public handleCreatePaymentIntegrationEvents({
saleInvoiceDTO,
saleInvoice,
trx,
}: ISaleInvoiceCreatedPayload) {
const paymentMethods =
saleInvoice.paymentMethods?.filter((method) => method.enable) || [];
paymentMethods.map(
async (paymentMethod: TransactionPaymentServiceEntry) => {
const payload = {
...omit(paymentMethod, ['id']),
saleInvoiceId: saleInvoice.id,
trx,
};
await this.eventPublisher.emitAsync(
events.paymentIntegrationLink.onPaymentIntegrationLink,
payload as PaymentIntegrationTransactionLinkEventPayload,
);
},
);
}
/**
*
* @param {ISaleInvoiceDeletingPayload} payload
*/
@OnEvent(events.saleInvoice.onDeleting)
public handleCreatePaymentIntegrationEventsOnDeleteInvoice({
oldSaleInvoice,
trx,
}: ISaleInvoiceDeletingPayload) {
const paymentMethods =
oldSaleInvoice.paymentMethods?.filter((method) => method.enable) || [];
paymentMethods.map(
async (paymentMethod: TransactionPaymentServiceEntry) => {
const payload = {
...omit(paymentMethod, ['id']),
oldSaleInvoiceId: oldSaleInvoice.id,
trx,
} as PaymentIntegrationTransactionLinkDeleteEventPayload;
// Triggers `onPaymentIntegrationDeleteLink` event.
await this.eventPublisher.emitAsync(
events.paymentIntegrationLink.onPaymentIntegrationDeleteLink,
payload,
);
},
);
}
}

View File

@@ -0,0 +1,68 @@
import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { InvoiceInventoryTransactions } from '../commands/inventory/InvoiceInventoryTransactions';
import { events } from '@/common/events/events';
import {
ISaleInvoiceCreatedPayload,
ISaleInvoiceEditedPayload,
ISaleInvoiceDeletedPayload,
ISaleInvoiceEventDeliveredPayload,
} from '../SaleInvoice.types';
@Injectable()
export class SaleInvoiceWriteInventoryTransactionsSubscriber {
constructor(
private readonly saleInvoiceInventory: InvoiceInventoryTransactions,
) {}
/**
* Handles the writing inventory transactions once the invoice created.
* @param {ISaleInvoiceCreatedPayload} payload
*/
@OnEvent(events.saleInvoice.onCreated)
public async handleWritingInventoryTransactions({
saleInvoice,
trx,
}: ISaleInvoiceCreatedPayload | ISaleInvoiceEventDeliveredPayload) {
// Can't continue if the sale invoice is not delivered yet.
if (!saleInvoice.deliveredAt) return null;
await this.saleInvoiceInventory.recordInventoryTranscactions(
saleInvoice,
false,
trx,
);
}
/**
* Rewriting the inventory transactions once the sale invoice be edited.
* @param {ISaleInvoiceEditPayload} payload -
*/
@OnEvent(events.saleInvoice.onEdited)
public async handleRewritingInventoryTransactions({
saleInvoice,
trx,
}: ISaleInvoiceEditedPayload) {
await this.saleInvoiceInventory.recordInventoryTranscactions(
saleInvoice,
true,
trx,
);
}
/**
* Handles deleting the inventory transactions once the invoice deleted.
* @param {ISaleInvoiceDeletedPayload} payload -
*/
@OnEvent(events.saleInvoice.onDeleted)
public async handleDeletingInventoryTransactions({
saleInvoiceId,
oldSaleInvoice,
trx,
}: ISaleInvoiceDeletedPayload) {
await this.saleInvoiceInventory.revertInventoryTransactions(
saleInvoiceId,
trx,
);
}
}

View File

@@ -0,0 +1,37 @@
import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import {
ISaleInvoiceWriteoffCreatePayload,
ISaleInvoiceWrittenOffCanceledPayload,
} from '../SaleInvoice.types';
import { SaleInvoiceWriteoffGLStorage } from '../commands/writeoff/SaleInvoiceWriteoffGLStorage';
import { events } from '@/common/events/events';
@Injectable()
export class SaleInvoiceWriteoffSubscriber {
constructor(private readonly writeGLStorage: SaleInvoiceWriteoffGLStorage) {}
/**
* Write the written-off sale invoice journal entries.
* @param {ISaleInvoiceWriteoffCreatePayload}
*/
@OnEvent(events.saleInvoice.onWrittenoff)
public async writeJournalEntriesOnceWriteoffCreate({
saleInvoice,
trx,
}: ISaleInvoiceWriteoffCreatePayload) {
await this.writeGLStorage.writeInvoiceWriteoffEntries(saleInvoice.id, trx);
}
/**
* Reverts the written-of sale invoice jounral entries.
* @param {ISaleInvoiceWrittenOffCanceledPayload}
*/
@OnEvent(events.saleInvoice.onWrittenoffCanceled)
public async revertJournalEntriesOnce({
saleInvoice,
trx,
}: ISaleInvoiceWrittenOffCanceledPayload) {
await this.writeGLStorage.revertInvoiceWriteoffEntries(saleInvoice.id, trx);
}
}

View File

@@ -0,0 +1,50 @@
// @ts-nocheck
import { pickBy } from 'lodash';
import { InvoicePdfTemplateAttributes, ISaleInvoice } from '@/interfaces';
import { contactAddressTextFormat } from '@/utils/address-text-format';
export const mergePdfTemplateWithDefaultAttributes = (
brandingTemplate?: Record<string, any>,
defaultAttributes: Record<string, any> = {}
) => {
const brandingAttributes = pickBy(
brandingTemplate,
(val, key) => val !== null && Object.keys(defaultAttributes).includes(key)
);
return {
...defaultAttributes,
...brandingAttributes,
};
};
export const transformInvoiceToPdfTemplate = (
invoice: ISaleInvoice
): Partial<InvoicePdfTemplateAttributes> => {
return {
dueDate: invoice.dueDateFormatted,
dateIssue: invoice.invoiceDateFormatted,
invoiceNumber: invoice.invoiceNo,
total: invoice.totalFormatted,
subtotal: invoice.subtotalFormatted,
paymentMade: invoice.paymentAmountFormatted,
dueAmount: invoice.dueAmountFormatted,
termsConditions: invoice.termsConditions,
statement: invoice.invoiceMessage,
lines: invoice.entries.map((entry) => ({
item: entry.item.name,
description: entry.description,
rate: entry.rateFormatted,
quantity: entry.quantityFormatted,
total: entry.totalFormatted,
})),
taxes: invoice.taxes.map((tax) => ({
label: tax.name,
amount: tax.taxRateAmountFormatted,
})),
customerAddress: contactAddressTextFormat(invoice.customer),
};
};