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;
|
2026-10-07 16:30:41 +00:00
|
|
|
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;
|
2026-10-07 16:30:41 +00:00
|
|
|
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
|
|
|
}
|
|
|
|
|
|
2026-10-07 16:30:41 +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,
|
2026-10-03 11:57:15 +00:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-08 10:35:37 +00:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-13 12:05:02 +00:00
|
|
|
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,
|
2026-10-03 11:57:15 +00:00
|
|
|
showLineDiscountColumn:
|
|
|
|
|
quoteDTO.show_line_discount_column ?? existingShowLineDiscountColumn,
|
2026-10-13 12:05:02 +00:00
|
|
|
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");
|
|
|
|
|
}
|
2026-10-07 16:30:41 +00:00
|
|
|
|
|
|
|
|
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
|
|
|
}
|