399 lines
12 KiB
TypeScript
399 lines
12 KiB
TypeScript
|
|
import { UniqueID } from "@common/domain";
|
||
|
|
|
||
|
|
import { Result } from "@common/helpers";
|
||
|
|
import { ITransactionManager } from "@common/infrastructure/database";
|
||
|
|
import { logger } from "@common/infrastructure/logger";
|
||
|
|
import { IUpdateInvoiceRequestDTO } from "../presentation/dto";
|
||
|
|
|
||
|
|
export class CreateInvoiceUseCase {
|
||
|
|
constructor(
|
||
|
|
private readonly invoiceService: IInvoiceService,
|
||
|
|
private readonly transactionManager: ITransactionManager
|
||
|
|
) {}
|
||
|
|
|
||
|
|
public execute(
|
||
|
|
invoiceID: UniqueID,
|
||
|
|
dto: Partial<IUpdateInvoiceRequestDTO>
|
||
|
|
): Promise<Result<Invoice, Error>> {
|
||
|
|
return this.transactionManager.complete(async (transaction) => {
|
||
|
|
try {
|
||
|
|
const validOrErrors = this.validateInvoiceData(dto);
|
||
|
|
if (validOrErrors.isFailure) {
|
||
|
|
return Result.fail(validOrErrors.error);
|
||
|
|
}
|
||
|
|
|
||
|
|
const data = validOrErrors.data;
|
||
|
|
|
||
|
|
// Update invoice with dto
|
||
|
|
return await this.invoiceService.updateInvoiceById(invoiceID, data, transaction);
|
||
|
|
} catch (error: unknown) {
|
||
|
|
logger.error(error as Error);
|
||
|
|
return Result.fail(error as Error);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
private validateInvoiceData(
|
||
|
|
dto: Partial<IUpdateInvoiceRequestDTO>
|
||
|
|
): Result<Partial<IInvoiceProps>, Error> {
|
||
|
|
const errors: Error[] = [];
|
||
|
|
const validatedData: Partial<IInvoiceProps> = {};
|
||
|
|
|
||
|
|
// Create invoice
|
||
|
|
let invoice_status = InvoiceStatus.create(invoiceDTO.status).object;
|
||
|
|
if (invoice_status.isEmpty()) {
|
||
|
|
invoice_status = InvoiceStatus.createDraft();
|
||
|
|
}
|
||
|
|
|
||
|
|
let invoice_series = InvoiceSeries.create(invoiceDTO.invoice_series).object;
|
||
|
|
if (invoice_series.isEmpty()) {
|
||
|
|
invoice_series = InvoiceSeries.create(invoiceDTO.invoice_series).object;
|
||
|
|
}
|
||
|
|
|
||
|
|
let issue_date = InvoiceDate.create(invoiceDTO.issue_date).object;
|
||
|
|
if (issue_date.isEmpty()) {
|
||
|
|
issue_date = InvoiceDate.createCurrentDate().object;
|
||
|
|
}
|
||
|
|
|
||
|
|
let operation_date = InvoiceDate.create(invoiceDTO.operation_date).object;
|
||
|
|
if (operation_date.isEmpty()) {
|
||
|
|
operation_date = InvoiceDate.createCurrentDate().object;
|
||
|
|
}
|
||
|
|
|
||
|
|
let invoiceCurrency = Currency.createFromCode(invoiceDTO.currency).object;
|
||
|
|
|
||
|
|
if (invoiceCurrency.isEmpty()) {
|
||
|
|
invoiceCurrency = Currency.createDefaultCode().object;
|
||
|
|
}
|
||
|
|
|
||
|
|
let invoiceLanguage = Language.createFromCode(invoiceDTO.language_code).object;
|
||
|
|
|
||
|
|
if (invoiceLanguage.isEmpty()) {
|
||
|
|
invoiceLanguage = Language.createDefaultCode().object;
|
||
|
|
}
|
||
|
|
|
||
|
|
const items = new Collection<InvoiceItem>(
|
||
|
|
invoiceDTO.items?.map(
|
||
|
|
(item) =>
|
||
|
|
InvoiceSimpleItem.create({
|
||
|
|
description: Description.create(item.description).object,
|
||
|
|
quantity: Quantity.create(item.quantity).object,
|
||
|
|
unitPrice: UnitPrice.create({
|
||
|
|
amount: item.unit_price.amount,
|
||
|
|
currencyCode: item.unit_price.currency,
|
||
|
|
precision: item.unit_price.precision,
|
||
|
|
}).object,
|
||
|
|
}).object
|
||
|
|
)
|
||
|
|
);
|
||
|
|
|
||
|
|
if (!invoice_status.isDraft()) {
|
||
|
|
throw Error("Error al crear una factura que no es borrador");
|
||
|
|
}
|
||
|
|
|
||
|
|
return DraftInvoice.create(
|
||
|
|
{
|
||
|
|
invoiceSeries: invoice_series,
|
||
|
|
issueDate: issue_date,
|
||
|
|
operationDate: operation_date,
|
||
|
|
invoiceCurrency,
|
||
|
|
language: invoiceLanguage,
|
||
|
|
invoiceNumber: InvoiceNumber.create(undefined).object,
|
||
|
|
//notes: Note.create(invoiceDTO.notes).object,
|
||
|
|
|
||
|
|
//senderId: UniqueID.create(null).object,
|
||
|
|
recipient,
|
||
|
|
|
||
|
|
items,
|
||
|
|
},
|
||
|
|
invoiceId
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export type UpdateInvoiceResponseOrError =
|
||
|
|
| Result<never, IUseCaseError> // Misc errors (value objects)
|
||
|
|
| Result<Invoice, never>; // Success!
|
||
|
|
|
||
|
|
export class UpdateInvoiceUseCase2
|
||
|
|
implements
|
||
|
|
IUseCase<{ id: UniqueID; data: IUpdateInvoice_DTO }, Promise<UpdateInvoiceResponseOrError>>
|
||
|
|
{
|
||
|
|
private _context: IInvoicingContext;
|
||
|
|
private _adapter: ISequelizeAdapter;
|
||
|
|
private _repositoryManager: IRepositoryManager;
|
||
|
|
|
||
|
|
constructor(context: IInvoicingContext) {
|
||
|
|
this._context = context;
|
||
|
|
this._adapter = context.adapter;
|
||
|
|
this._repositoryManager = context.repositoryManager;
|
||
|
|
}
|
||
|
|
|
||
|
|
private getRepository<T>(name: string) {
|
||
|
|
return this._repositoryManager.getRepository<T>(name);
|
||
|
|
}
|
||
|
|
|
||
|
|
private handleValidationFailure(
|
||
|
|
validationError: Error,
|
||
|
|
message?: string
|
||
|
|
): Result<never, IUseCaseError> {
|
||
|
|
return Result.fail<IUseCaseError>(
|
||
|
|
UseCaseError.create(
|
||
|
|
UseCaseError.INVALID_INPUT_DATA,
|
||
|
|
message ? message : validationError.message,
|
||
|
|
validationError
|
||
|
|
)
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
async execute(request: {
|
||
|
|
id: UniqueID;
|
||
|
|
data: IUpdateInvoice_DTO;
|
||
|
|
}): Promise<UpdateInvoiceResponseOrError> {
|
||
|
|
const { id, data: invoiceDTO } = request;
|
||
|
|
|
||
|
|
// Validaciones
|
||
|
|
const invoiceDTOOrError = ensureUpdateInvoice_DTOIsValid(invoiceDTO);
|
||
|
|
if (invoiceDTOOrError.isFailure) {
|
||
|
|
return this.handleValidationFailure(invoiceDTOOrError.error);
|
||
|
|
}
|
||
|
|
|
||
|
|
const transaction = this._adapter.startTransaction();
|
||
|
|
|
||
|
|
const invoiceRepoBuilder = this.getRepository<IInvoiceRepository>("Invoice");
|
||
|
|
|
||
|
|
let invoice: Invoice | null = null;
|
||
|
|
|
||
|
|
try {
|
||
|
|
await transaction.complete(async (t) => {
|
||
|
|
invoice = await invoiceRepoBuilder({ transaction: t }).getById(id);
|
||
|
|
});
|
||
|
|
|
||
|
|
if (invoice === null) {
|
||
|
|
return Result.fail<IUseCaseError>(
|
||
|
|
UseCaseError.create(UseCaseError.NOT_FOUND_ERROR, `Invoice not found`, {
|
||
|
|
id: request.id.toString(),
|
||
|
|
entity: "invoice",
|
||
|
|
})
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
return Result.ok<Invoice>(invoice);
|
||
|
|
} catch (error: unknown) {
|
||
|
|
const _error = error as Error;
|
||
|
|
if (invoiceRepoBuilder().isRepositoryError(_error)) {
|
||
|
|
return this.handleRepositoryError(error as BaseError, invoiceRepoBuilder());
|
||
|
|
} else {
|
||
|
|
return this.handleUnexceptedError(error);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Recipient validations
|
||
|
|
/*const recipientIdOrError = ensureParticipantIdIsValid(
|
||
|
|
invoiceDTO?.recipient?.id,
|
||
|
|
);
|
||
|
|
if (recipientIdOrError.isFailure) {
|
||
|
|
return this.handleValidationFailure(
|
||
|
|
recipientIdOrError.error,
|
||
|
|
"Recipient ID not valid",
|
||
|
|
);
|
||
|
|
}
|
||
|
|
const recipientId = recipientIdOrError.object;
|
||
|
|
|
||
|
|
const recipientBillingIdOrError = ensureParticipantAddressIdIsValid(
|
||
|
|
invoiceDTO?.recipient?.billing_address_id,
|
||
|
|
);
|
||
|
|
if (recipientBillingIdOrError.isFailure) {
|
||
|
|
return this.handleValidationFailure(
|
||
|
|
recipientBillingIdOrError.error,
|
||
|
|
"Recipient billing address ID not valid",
|
||
|
|
);
|
||
|
|
}
|
||
|
|
const recipientBillingId = recipientBillingIdOrError.object;
|
||
|
|
|
||
|
|
const recipientShippingIdOrError = ensureParticipantAddressIdIsValid(
|
||
|
|
invoiceDTO?.recipient?.shipping_address_id,
|
||
|
|
);
|
||
|
|
if (recipientShippingIdOrError.isFailure) {
|
||
|
|
return this.handleValidationFailure(
|
||
|
|
recipientShippingIdOrError.error,
|
||
|
|
"Recipient shipping address ID not valid",
|
||
|
|
);
|
||
|
|
}
|
||
|
|
const recipientShippingId = recipientShippingIdOrError.object;
|
||
|
|
|
||
|
|
const recipientContact = await this.findContact(
|
||
|
|
recipientId,
|
||
|
|
recipientBillingId,
|
||
|
|
recipientShippingId,
|
||
|
|
);
|
||
|
|
|
||
|
|
if (!recipientContact) {
|
||
|
|
return this.handleValidationFailure(
|
||
|
|
new Error(`Recipient with ID ${recipientId.toString()} does not exist`),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Crear invoice
|
||
|
|
const invoiceOrError = await this.tryUpdateInvoiceInstance(
|
||
|
|
invoiceDTO,
|
||
|
|
invoiceIdOrError.object,
|
||
|
|
//senderId,
|
||
|
|
//senderBillingId,
|
||
|
|
//senderShippingId,
|
||
|
|
recipientContact,
|
||
|
|
);
|
||
|
|
|
||
|
|
if (invoiceOrError.isFailure) {
|
||
|
|
const { error: domainError } = invoiceOrError;
|
||
|
|
let errorCode = "";
|
||
|
|
let message = "";
|
||
|
|
|
||
|
|
switch (domainError.code) {
|
||
|
|
case Invoice.ERROR_CUSTOMER_WITHOUT_NAME:
|
||
|
|
errorCode = UseCaseError.INVALID_INPUT_DATA;
|
||
|
|
message =
|
||
|
|
"El cliente debe ser una compañía o tener nombre y apellidos.";
|
||
|
|
break;
|
||
|
|
|
||
|
|
default:
|
||
|
|
errorCode = UseCaseError.UNEXCEPTED_ERROR;
|
||
|
|
message = "";
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
return Result.fail<IUseCaseError>(
|
||
|
|
UseCaseError.create(errorCode, message, domainError),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
return this.saveInvoice(invoiceOrError.object);
|
||
|
|
*/
|
||
|
|
}
|
||
|
|
|
||
|
|
private async tryUpdateInvoiceInstance(invoiceDTO, invoiceId, recipient) {
|
||
|
|
// Create invoice
|
||
|
|
let invoice_status = InvoiceStatus.create(invoiceDTO.status).object;
|
||
|
|
if (invoice_status.isEmpty()) {
|
||
|
|
invoice_status = InvoiceStatus.createDraft();
|
||
|
|
}
|
||
|
|
|
||
|
|
let invoice_series = InvoiceSeries.create(invoiceDTO.invoice_series).object;
|
||
|
|
if (invoice_series.isEmpty()) {
|
||
|
|
invoice_series = InvoiceSeries.create(invoiceDTO.invoice_series).object;
|
||
|
|
}
|
||
|
|
|
||
|
|
let issue_date = InvoiceDate.create(invoiceDTO.issue_date).object;
|
||
|
|
if (issue_date.isEmpty()) {
|
||
|
|
issue_date = InvoiceDate.createCurrentDate().object;
|
||
|
|
}
|
||
|
|
|
||
|
|
let operation_date = InvoiceDate.create(invoiceDTO.operation_date).object;
|
||
|
|
if (operation_date.isEmpty()) {
|
||
|
|
operation_date = InvoiceDate.createCurrentDate().object;
|
||
|
|
}
|
||
|
|
|
||
|
|
let invoiceCurrency = Currency.createFromCode(invoiceDTO.currency).object;
|
||
|
|
|
||
|
|
if (invoiceCurrency.isEmpty()) {
|
||
|
|
invoiceCurrency = Currency.createDefaultCode().object;
|
||
|
|
}
|
||
|
|
|
||
|
|
let invoiceLanguage = Language.createFromCode(invoiceDTO.language_code).object;
|
||
|
|
|
||
|
|
if (invoiceLanguage.isEmpty()) {
|
||
|
|
invoiceLanguage = Language.createDefaultCode().object;
|
||
|
|
}
|
||
|
|
|
||
|
|
const items = new Collection<InvoiceItem>(
|
||
|
|
invoiceDTO.items?.map(
|
||
|
|
(item) =>
|
||
|
|
InvoiceSimpleItem.create({
|
||
|
|
description: Description.create(item.description).object,
|
||
|
|
quantity: Quantity.create(item.quantity).object,
|
||
|
|
unitPrice: UnitPrice.create({
|
||
|
|
amount: item.unit_price.amount,
|
||
|
|
currencyCode: item.unit_price.currency,
|
||
|
|
precision: item.unit_price.precision,
|
||
|
|
}).object,
|
||
|
|
}).object
|
||
|
|
)
|
||
|
|
);
|
||
|
|
|
||
|
|
if (!invoice_status.isDraft()) {
|
||
|
|
throw Error("Error al crear una factura que no es borrador");
|
||
|
|
}
|
||
|
|
|
||
|
|
return DraftInvoice.create(
|
||
|
|
{
|
||
|
|
invoiceSeries: invoice_series,
|
||
|
|
issueDate: issue_date,
|
||
|
|
operationDate: operation_date,
|
||
|
|
invoiceCurrency,
|
||
|
|
language: invoiceLanguage,
|
||
|
|
invoiceNumber: InvoiceNumber.create(undefined).object,
|
||
|
|
//notes: Note.create(invoiceDTO.notes).object,
|
||
|
|
|
||
|
|
//senderId: UniqueID.create(null).object,
|
||
|
|
recipient,
|
||
|
|
|
||
|
|
items,
|
||
|
|
},
|
||
|
|
invoiceId
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
private async findContact(
|
||
|
|
contactId: UniqueID,
|
||
|
|
billingAddressId: UniqueID,
|
||
|
|
shippingAddressId: UniqueID
|
||
|
|
) {
|
||
|
|
const contactRepoBuilder = this.getRepository<IContactRepository>("Contact");
|
||
|
|
|
||
|
|
const contact = await contactRepoBuilder().getById2(
|
||
|
|
contactId,
|
||
|
|
billingAddressId,
|
||
|
|
shippingAddressId
|
||
|
|
);
|
||
|
|
|
||
|
|
return contact;
|
||
|
|
}
|
||
|
|
|
||
|
|
private async saveInvoice(invoice: DraftInvoice) {
|
||
|
|
const transaction = this._adapter.startTransaction();
|
||
|
|
const invoiceRepoBuilder = this.getRepository<IInvoiceRepository>("Invoice");
|
||
|
|
|
||
|
|
try {
|
||
|
|
await transaction.complete(async (t) => {
|
||
|
|
const invoiceRepo = invoiceRepoBuilder({ transaction: t });
|
||
|
|
await invoiceRepo.save(invoice);
|
||
|
|
});
|
||
|
|
|
||
|
|
return Result.ok<DraftInvoice>(invoice);
|
||
|
|
} catch (error: unknown) {
|
||
|
|
const _error = error as Error;
|
||
|
|
if (invoiceRepoBuilder().isRepositoryError(_error)) {
|
||
|
|
return this.handleRepositoryError(error as BaseError, invoiceRepoBuilder());
|
||
|
|
} else {
|
||
|
|
return this.handleUnexceptedError(error);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private handleUnexceptedError(error): Result<never, IUseCaseError> {
|
||
|
|
return Result.fail<IUseCaseError>(
|
||
|
|
UseCaseError.create(UseCaseError.UNEXCEPTED_ERROR, error.message, error)
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
private handleRepositoryError(
|
||
|
|
error: BaseError,
|
||
|
|
repository: IInvoiceRepository
|
||
|
|
): Result<never, IUseCaseError> {
|
||
|
|
const { message, details } = repository.handleRepositoryError(error);
|
||
|
|
return Result.fail<IUseCaseError>(
|
||
|
|
UseCaseError.create(UseCaseError.REPOSITORY_ERROR, message, details)
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|