Presupuestador_web/server/src/contexts/sales/application/Quote/UpdateQuote.useCase.ts

365 lines
12 KiB
TypeScript
Raw Normal View History

2026-10-19 11:55:16 +00:00
import { createQuoteChapters } from "../services/createQuoteChapters";
import { ensureQuoteChapterStructureIsValid } from "@shared/contexts";
2024-05-26 17:09:43 +00:00
import {
IUseCase,
IUseCaseError,
IUseCaseRequest,
UseCaseError,
} from "@/contexts/common/application";
import { IRepositoryManager } from "@/contexts/common/domain";
import { ISequelizeAdapter } from "@/contexts/common/infrastructure/sequelize";
import {
Collection,
2024-07-09 16:21:21 +00:00
CurrencyData,
2024-05-26 17:09:43 +00:00
Description,
DomainError,
IDomainError,
Language,
2024-07-02 20:15:59 +00:00
Note,
2024-07-09 16:21:21 +00:00
Percentage,
2024-05-26 17:09:43 +00:00
Quantity,
Result,
2024-07-12 16:37:41 +00:00
TextValueObject,
2024-05-26 17:09:43 +00:00
UTCDateValue,
UniqueID,
UnitPrice,
} from "@shared/contexts";
2024-07-17 18:10:07 +00:00
import { ArticleIdentifier } from "@/contexts/catalog/domain";
2024-05-26 17:09:43 +00:00
import { IUpdateQuote_Request_DTO } from "@shared/contexts";
2024-07-09 16:21:21 +00:00
import {
Dealer,
IQuoteRepository,
Quote,
QuoteCustomer,
2024-08-23 16:47:51 +00:00
QuoteCustomerReference,
2024-07-09 16:21:21 +00:00
QuoteItem,
QuoteReference,
QuoteStatus,
} from "../../domain";
import { ISalesContext } from "../../infrastructure";
2024-05-26 17:09:43 +00:00
export interface IUpdateQuoteUseCaseRequest extends IUseCaseRequest {
id: UniqueID;
2024-07-09 16:21:21 +00:00
quoteDTO: IUpdateQuote_Request_DTO;
2024-05-26 17:09:43 +00:00
}
export type UpdateQuoteResponseOrError =
| Result<never, IUseCaseError> // Misc errors (value objects)
| Result<Quote, never>; // Success!
export class UpdateQuoteUseCase
implements IUseCase<IUpdateQuoteUseCaseRequest, Promise<UpdateQuoteResponseOrError>>
{
private _adapter: ISequelizeAdapter;
private _repositoryManager: IRepositoryManager;
2024-07-09 16:21:21 +00:00
private _dealer?: Dealer;
private _catalogArticles?: ISalesContext["catalogArticles"];
2024-05-26 17:09:43 +00:00
2024-07-09 16:21:21 +00:00
constructor(context: ISalesContext) {
this._adapter = context.adapter;
this._repositoryManager = context.repositoryManager;
this._dealer = context.dealer;
this._catalogArticles = context.catalogArticles;
2024-05-26 17:09:43 +00:00
}
async execute(request: IUpdateQuoteUseCaseRequest): Promise<UpdateQuoteResponseOrError> {
2024-07-09 16:21:21 +00:00
const { id, quoteDTO } = request;
2024-05-26 17:09:43 +00:00
const QuoteRepository = this._getQuoteRepository();
2024-07-09 16:21:21 +00:00
// Validaciones de datos
if (!this._dealer) {
const message = "Error. Missing Dealer";
return Result.fail(UseCaseError.create(UseCaseError.INVALID_INPUT_DATA, message));
}
const dealerId = this._dealer.id;
2026-10-19 11:55:16 +00:00
const chapterValidation = ensureQuoteChapterStructureIsValid(quoteDTO);
if (chapterValidation.isFailure) {
return Result.fail(UseCaseError.create(UseCaseError.INVALID_INPUT_DATA, chapterValidation.error.message));
2024-05-26 17:09:43 +00:00
}
const catalogValidation = await this._validateCatalogItems(quoteDTO.items ?? []);
if (catalogValidation.isFailure) return Result.fail(catalogValidation.error);
2024-05-26 17:09:43 +00:00
const transaction = this._adapter.startTransaction();
try {
2026-10-19 11:55:16 +00:00
return await transaction.complete(async (t) => {
const quoteRepo = QuoteRepository({ transaction: t });
// Comprobar que existe el Quote
const existingQuote = await quoteRepo.getById(id, { forUpdate: true });
if (!existingQuote) {
const message = `Quote ID not found`;
return Result.fail(
UseCaseError.create(UseCaseError.NOT_FOUND_ERROR, message, {
path: "id",
})
);
}
if ((existingQuote.chapters?.length ?? 0) > 0 && quoteDTO.chapters === undefined) {
return Result.fail(UseCaseError.create(
UseCaseError.INVALID_INPUT_DATA,
"Updating a grouped quote requires an explicit chapters structure",
{ path: "chapters" }
));
}
// //
// regla de negocio -> no poder modificar un quote enviado
// Crear quote
const quoteOrError = this._tryCreateQuoteInstance(
quoteDTO,
id,
dealerId,
existingQuote.showLineDiscountColumn,
existingQuote.taxLabel,
existingQuote.showChapterLineAmounts
);
if (quoteOrError.isFailure) {
const { error: domainError } = quoteOrError;
let errorCode = "";
let message = "";
switch (domainError.code) {
// Errores manuales
case DomainError.INVALID_INPUT_DATA:
errorCode = UseCaseError.INVALID_INPUT_DATA;
message = "El presupuesto tiene algún dato erróneo.";
break;
default:
errorCode = UseCaseError.UNEXCEPTED_ERROR;
message = domainError.message;
break;
}
2024-05-26 17:09:43 +00:00
2026-10-19 11:55:16 +00:00
return Result.fail(UseCaseError.create(errorCode, message, domainError));
}
await quoteRepo.update(quoteOrError.object);
return Result.ok<Quote>(quoteOrError.object);
});
2024-05-26 17:09:43 +00:00
} catch (error: unknown) {
2026-10-19 11:55:16 +00:00
return Result.fail(UseCaseError.create(UseCaseError.REPOSITORY_ERROR, (error as Error).message));
2024-05-26 17:09:43 +00:00
}
}
private _tryCreateQuoteInstance(
quoteDTO: IUpdateQuote_Request_DTO,
2024-07-09 16:21:21 +00:00
quoteId: UniqueID,
dealerId: UniqueID,
2026-10-05 10:04:43 +00:00
existingShowLineDiscountColumn: boolean,
2026-10-19 11:55:16 +00:00
existingTaxLabel: Note,
existingShowChapterLineAmounts: boolean = true
2024-05-26 17:09:43 +00:00
): Result<Quote, IDomainError> {
const statusOrError = QuoteStatus.create(quoteDTO.status);
if (statusOrError.isFailure) {
return Result.fail(statusOrError.error);
}
const dateOrError = UTCDateValue.create(quoteDTO.date);
if (dateOrError.isFailure) {
return Result.fail(dateOrError.error);
}
2024-07-09 16:21:21 +00:00
const referenceOrError = QuoteReference.create(quoteDTO.reference);
2024-07-02 20:15:59 +00:00
if (referenceOrError.isFailure) {
return Result.fail(referenceOrError.error);
}
const languageOrError = Language.createFromCode(
quoteDTO.lang_code || Language.DEFAULT_LANGUAGE_CODE
);
2024-05-26 17:09:43 +00:00
if (languageOrError.isFailure) {
return Result.fail(languageOrError.error);
}
2024-08-23 16:47:51 +00:00
const customerReferenceOrError = QuoteCustomerReference.create(quoteDTO.customer_reference);
if (customerReferenceOrError.isFailure) {
return Result.fail(customerReferenceOrError.error);
}
2024-07-02 20:15:59 +00:00
const customerOrError = QuoteCustomer.create(quoteDTO.customer_information);
if (customerOrError.isFailure) {
return Result.fail(customerOrError.error);
}
2024-07-09 16:21:21 +00:00
const currencyOrError = CurrencyData.createFromCode(quoteDTO.currency_code);
2024-05-26 17:09:43 +00:00
if (currencyOrError.isFailure) {
return Result.fail(currencyOrError.error);
}
const additionalCurrencyOrError = quoteDTO.additional_currency_code
? CurrencyData.createFromCode(quoteDTO.additional_currency_code)
: undefined;
if (additionalCurrencyOrError?.isFailure ||
(additionalCurrencyOrError && (!quoteDTO.additional_currency_rate || !quoteDTO.additional_currency_date))) {
return Result.fail(additionalCurrencyOrError?.error ?? DomainError.create(DomainError.INVALID_INPUT_DATA, "Additional currency requires rate and date"));
}
const additionalCurrencyDateOrError = quoteDTO.additional_currency_code
? UTCDateValue.create(quoteDTO.additional_currency_date ?? "")
: undefined;
if (additionalCurrencyDateOrError?.isFailure) return Result.fail(additionalCurrencyDateOrError.error);
2024-07-02 20:15:59 +00:00
const paymentOrError = Note.create(quoteDTO.payment_method);
if (paymentOrError.isFailure) {
return Result.fail(paymentOrError.error);
}
2024-07-12 16:37:41 +00:00
const notesOrError = TextValueObject.create(quoteDTO.notes);
2024-07-02 20:15:59 +00:00
if (notesOrError.isFailure) {
return Result.fail(notesOrError.error);
}
const validityOrError = Note.create(quoteDTO.validity);
if (validityOrError.isFailure) {
return Result.fail(validityOrError.error);
}
2024-07-09 16:21:21 +00:00
const discountOrError = Percentage.create(quoteDTO.discount);
if (discountOrError.isFailure) {
return Result.fail(discountOrError.error);
}
2024-07-19 15:02:19 +00:00
const taxOrError = Percentage.create(quoteDTO.tax);
if (taxOrError.isFailure) {
return Result.fail(taxOrError.error);
}
2024-10-03 14:23:46 +00:00
const dateSentOrError = UTCDateValue.create(null);
if (dateSentOrError.isFailure) {
return Result.fail(dateSentOrError.error);
}
2026-10-19 11:55:16 +00:00
const chapters = createQuoteChapters(quoteDTO.chapters, quoteId);
if (chapters.isFailure) return Result.fail(chapters.error);
2024-10-03 14:23:46 +00:00
// Items
2024-07-17 18:10:07 +00:00
let items: Collection<QuoteItem>;
try {
items = new Collection<QuoteItem>(
quoteDTO.items?.map((item) => {
2024-09-23 17:37:09 +00:00
const idArticleOrError = ArticleIdentifier.create(item.id_article);
if (idArticleOrError.isFailure) {
throw idArticleOrError.error;
2024-07-17 18:10:07 +00:00
}
const descriptionOrError = Description.create(item.description);
if (descriptionOrError.isFailure) {
throw descriptionOrError.error;
}
const quantityOrError = Quantity.create({
amount: item.quantity.amount,
scale: item.quantity.scale,
});
if (quantityOrError.isFailure) {
throw quantityOrError.error;
}
const unitPriceOrError = UnitPrice.create({
amount: item.unit_price?.amount,
currencyCode: item.unit_price?.currency_code,
scale: item.unit_price?.scale,
});
if (unitPriceOrError.isFailure) {
throw unitPriceOrError.error;
}
const percentageOrError = Percentage.create({
amount: item.discount?.amount,
scale: item.discount?.scale,
});
if (percentageOrError.isFailure) {
throw percentageOrError.error;
}
2026-10-19 11:55:16 +00:00
const chapterId = item.chapter_id ? UniqueID.create(item.chapter_id) : undefined;
if (chapterId?.isFailure) throw chapterId.error;
2024-07-17 18:10:07 +00:00
const quoteItemOrError = QuoteItem.create({
2026-10-19 11:55:16 +00:00
chapterId: chapterId?.object ?? null,
2024-09-23 17:37:09 +00:00
idArticle: idArticleOrError.object,
2024-07-17 18:10:07 +00:00
description: descriptionOrError.object,
quantity: quantityOrError.object,
unitPrice: unitPriceOrError.object,
discount: percentageOrError.object,
});
if (quoteItemOrError.isFailure) {
throw quoteItemOrError.error;
}
return quoteItemOrError.object;
})
);
} catch (e: unknown) {
return Result.fail(e as IDomainError);
}
2024-05-26 17:09:43 +00:00
return Quote.create(
{
status: statusOrError.object,
date: dateOrError.object,
2024-07-02 20:15:59 +00:00
reference: referenceOrError.object,
2024-05-26 17:09:43 +00:00
language: languageOrError.object,
2024-08-23 16:47:51 +00:00
customerReference: customerReferenceOrError.object,
2024-07-02 20:15:59 +00:00
customer: customerOrError.object,
2024-05-26 17:09:43 +00:00
currency: currencyOrError.object,
2024-07-02 20:15:59 +00:00
paymentMethod: paymentOrError.object,
notes: notesOrError.object,
validity: validityOrError.object,
showLineDiscountColumn:
quoteDTO.show_line_discount_column ?? existingShowLineDiscountColumn,
additionalCurrency: additionalCurrencyOrError?.object,
additionalCurrencyRate: quoteDTO.additional_currency_code ? quoteDTO.additional_currency_rate ?? null : null,
additionalCurrencyDate: additionalCurrencyDateOrError?.object,
2024-07-02 20:15:59 +00:00
2024-07-09 16:21:21 +00:00
discount: discountOrError.object,
2024-07-19 15:02:19 +00:00
tax: taxOrError.object,
2026-10-05 10:04:43 +00:00
taxLabel: existingTaxLabel,
2024-07-09 16:21:21 +00:00
2024-05-26 17:09:43 +00:00
items,
2026-10-19 11:55:16 +00:00
chapters: chapters.object,
showChapterLineAmounts: quoteDTO.show_chapter_line_amounts ?? existingShowChapterLineAmounts,
2024-07-09 16:21:21 +00:00
dealerId,
2024-10-03 14:23:46 +00:00
dateSent: dateSentOrError.object,
2024-05-26 17:09:43 +00:00
},
quoteId
);
}
private _getQuoteRepository() {
return this._repositoryManager.getRepository<IQuoteRepository>("Quote");
}
private async _validateCatalogItems(items: IUpdateQuote_Request_DTO["items"]): Promise<Result<boolean, IUseCaseError>> {
const ids: ArticleIdentifier[] = [];
for (const item of items) {
const idOrError = ArticleIdentifier.create(item.id_article);
if (idOrError.isFailure) {
return Result.fail(UseCaseError.create(UseCaseError.INVALID_INPUT_DATA, idOrError.error.message));
}
if (!idOrError.object.isNull()) ids.push(idOrError.object);
}
if (!ids.length) return Result.ok(true);
if (!this._catalogArticles) {
return Result.fail(UseCaseError.create(UseCaseError.REPOSITORY_ERROR, "Catalog lookup is not configured"));
}
const lookup = await this._catalogArticles.execute(ids);
if (lookup.isFailure) return Result.fail(lookup.error);
const found = new Set(lookup.object.map((article) => article.idArticle.toString()));
const missing = ids.find((id) => !found.has(id.toString()));
return missing
? Result.fail(UseCaseError.create(UseCaseError.INVALID_INPUT_DATA, `Catalog article ${missing.toString()} not found`))
: Result.ok(true);
}
2024-05-26 17:09:43 +00:00
}