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