UNPKG

lbx-invoice

Version:

Provides functionality around generating invoices.

223 lines (205 loc) 9.72 kB
import { BindingScope, inject, injectable } from '@loopback/core'; import { juggler } from '@loopback/repository'; import { HttpErrors } from '@loopback/rest'; import { LbxInvoiceBindings } from '../keys'; import { NumberInvoices } from '../models'; import { AddressData } from '../models/invoice/address-data.model'; import { BaseInvoiceRepository, NumberInvoicesRepository } from '../repositories'; /** * Handles generating unique and consecutive numbers for invoices. */ @injectable({ scope: BindingScope.TRANSIENT }) export class BaseInvoiceNumberService { /** * The number of digits used to generate the consecutive number. * @default 4 */ protected readonly NUMBER_OF_DIGITS: number = 4; /** * The separator for the parts of the invoice number. * @default '-' */ protected readonly SEPARATOR: string = '-'; /** * How many characters of the company name should be used in the invoice number. * @default 6 */ protected readonly NUMBER_COMPANY_ABBREVIATION_CHARACTERS: number = 6; /** * How many characters of the private customer first and last name should be used in the invoice number. * @default 3 */ protected readonly NUMBER_PRIVATE_CUSTOMER_ABBREVIATION_CHARACTERS: number = 3; constructor( @inject(LbxInvoiceBindings.INVOICE_REPOSITORY) private readonly invoiceRepository: BaseInvoiceRepository, @inject(LbxInvoiceBindings.NUMBER_INVOICES_REPOSITORY) private readonly numberInvoicesRepository: NumberInvoicesRepository ) {} /** * Generates a new invoice number. * @param recipientId - The id of the recipient of the invoice. * @param invoiceAddress - The address data of the customer. * @param transaction - An optional transaction from outside to make sure any changes only apply when the transaction is committed. * @param nameAbbreviation - An optional name abbreviation if you don't want to generate one. * @returns A promise of the new invoice number. */ async generateInvoiceNumber( recipientId: string, invoiceAddress: AddressData, transaction?: juggler.Transaction, nameAbbreviation?: string ): Promise<string> { const currentYear: string = `${new Date().getFullYear()}`; const customerNameAbbreviation: string = nameAbbreviation ?? this.getCustomerNameAbbreviation(invoiceAddress); const consecutiveNumber: string = await this.getConsecutiveNumber(recipientId, transaction); const result: string = `${currentYear}${this.SEPARATOR}${customerNameAbbreviation}${this.SEPARATOR}${consecutiveNumber}`; await this.validateInvoiceNumber(result); return result; } /** * Generates a temporary invoice number with the prefix temp. * This does not increase the number of invoices which can be helpful if you create an invoice that might not be sent out. * @param recipientId - The id of the recipient of the invoice. * @param invoiceAddress - The address data of the customer. * @param nameAbbreviation - An optional name abbreviation if you don't want to generate one. * @returns A promise of the new temporary invoice number. */ async generateTemporaryInvoiceNumber(recipientId: string, invoiceAddress: AddressData, nameAbbreviation?: string): Promise<string> { const currentYear: string = `${new Date().getFullYear()}`; const customerNameAbbreviation: string = nameAbbreviation ?? this.getCustomerNameAbbreviation(invoiceAddress); const consecutiveNumber: string = await this.getTemporaryConsecutiveNumber(recipientId); // eslint-disable-next-line stylistic/max-len const result: string = `TEMP${this.SEPARATOR}${currentYear}${this.SEPARATOR}${customerNameAbbreviation}${this.SEPARATOR}${consecutiveNumber}`; if (!await this.invoiceRepository.findOne({ where: { number: result } })) { return result; } let suffix: number = 2; while (await this.invoiceRepository.findOne({ where: { number: `${result}${this.SEPARATOR}${suffix}` } })) { suffix++; } return `${result}${this.SEPARATOR}${suffix}`; } /** * Gets the temporary consecutive number of the invoices in this year and for the provided recipientId. * Prefixes it with zeroes until the result has the same length as this.NUMBER_OF_DIGITS. * @param recipientId - The id of the recipient of the invoice. * @returns The number of invoices over a year filled up with zeroes to match this.NUMBER_OF_DIGITS. */ protected async getTemporaryConsecutiveNumber(recipientId: string): Promise<string> { const currentYear: number = new Date(Date.now()).getFullYear(); let numberOfInvoices: NumberInvoices | null = await this.numberInvoicesRepository.findOne({ where: { year: currentYear, recipientId: recipientId } }); if (numberOfInvoices) { numberOfInvoices.number++; } else { numberOfInvoices = { number: 1, year: currentYear, recipientId: recipientId } as NumberInvoices; } const consecutiveNumber: string = `${numberOfInvoices.number}`; if (consecutiveNumber.length > this.NUMBER_OF_DIGITS) { return consecutiveNumber; } const difference: number = this.NUMBER_OF_DIGITS - consecutiveNumber.length; let prefix: string = ''; for (let i: number = 0; i < difference; i++) { prefix = `${prefix}0`; } return `${prefix}${consecutiveNumber}`; } /** * Gets the consecutive number of the invoices in this year. * Prefixes it with zeroes until the result has the same length as this.NUMBER_OF_DIGITS. * @param recipientId - The id of the recipient of the invoice. * @param transaction - An optional transaction from outside to make sure any changes only apply when the transaction is committed. * @returns The number of invoices over a year filled up with zeroes to match this.NUMBER_OF_DIGITS. */ protected async getConsecutiveNumber(recipientId: string, transaction?: juggler.Transaction): Promise<string> { const currentYear: number = new Date(Date.now()).getFullYear(); let numberOfInvoices: NumberInvoices | null = await this.numberInvoicesRepository.findOne({ where: { year: currentYear, recipientId: recipientId } }); if (numberOfInvoices) { numberOfInvoices.number++; await this.numberInvoicesRepository.updateById(numberOfInvoices.id, numberOfInvoices, { transaction: transaction }); } else { numberOfInvoices = await this.numberInvoicesRepository.create( { number: 1, year: currentYear, recipientId: recipientId }, { transaction: transaction } ); } const consecutiveNumber: string = `${numberOfInvoices.number}`; if (consecutiveNumber.length > this.NUMBER_OF_DIGITS) { return consecutiveNumber; } const difference: number = this.NUMBER_OF_DIGITS - consecutiveNumber.length; let prefix: string = ''; for (let i: number = 0; i < difference; i++) { prefix = `${prefix}0`; } return `${prefix}${consecutiveNumber}`; } /** * Gets the customer name abbreviation. * @param invoiceAddress - The address data to get the name abbreviation from. * @returns The first char of first and last name for private customers * and the first two chars of the company name for company customers. */ protected getCustomerNameAbbreviation(invoiceAddress: AddressData): string { if (invoiceAddress.company && invoiceAddress.companyName) { return this.getCompanyNameAbbreviation(invoiceAddress.companyName); } return this.getPrivateCustomerAbbreviation(invoiceAddress); } private getPrivateCustomerAbbreviation(invoiceAddress: AddressData): string { let res: string = ''; for (let i: number = 0; i < this.NUMBER_PRIVATE_CUSTOMER_ABBREVIATION_CHARACTERS; i++) { if (invoiceAddress.firstName[i]) { res += invoiceAddress.firstName[i]; } } for (let i: number = 0; i < this.NUMBER_PRIVATE_CUSTOMER_ABBREVIATION_CHARACTERS; i++) { if (invoiceAddress.lastName[i]) { res += invoiceAddress.lastName[i]; } } return res.toUpperCase(); } private getCompanyNameAbbreviation(companyName: string): string { companyName = companyName.replaceAll(/\s/g, ''); companyName = companyName.replaceAll(/[^\da-z]/gi, ''); let res: string = ''; for (let i: number = 0; i < this.NUMBER_COMPANY_ABBREVIATION_CHARACTERS; i++) { if (companyName[i]) { res += companyName[i]; } } return res.toUpperCase(); } // TODO: Is this still needed? /** * Validates the given invoice number. * @param invoiceNumber - The invoice number to validate. */ protected async validateInvoiceNumber(invoiceNumber: string): Promise<void> { if (await this.invoiceRepository.findOne({ where: { number: invoiceNumber } })) { throw new HttpErrors.Conflict(`The generated invoice-number ${invoiceNumber} already exists!`); } } }