lbx-invoice
Version:
Provides functionality around generating invoices.
223 lines (205 loc) • 9.72 kB
text/typescript
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.
*/
({ 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(
(LbxInvoiceBindings.INVOICE_REPOSITORY)
private readonly invoiceRepository: BaseInvoiceRepository,
(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!`);
}
}
}