From adcc6e302899748e85b1b074ef8d60db8b488d2e Mon Sep 17 00:00:00 2001 From: David Arranz Date: Wed, 13 Nov 2024 16:08:20 +0100 Subject: [PATCH] Duplicar una propuesta (servidor) --- .../Quote/DuplicateQuote.useCase.ts | 130 ++++++++++++++++++ .../contexts/sales/application/Quote/index.ts | 1 + .../application/services/QuoteService.ts | 12 +- .../DuplicateQuote.controller.ts | 116 ++++++++++++++++ .../quotes/duplicateQuote/index.ts | 16 +++ .../presenter/DuplicateQuote.presenter.ts | 63 +++++++++ .../quotes/duplicateQuote/presenter/index.ts | 1 + .../express/controllers/quotes/index.ts | 2 + .../express/api/routes/quote.routes.ts | 11 +- .../IDuplicateQuote_Response.dto.ts | 38 +++++ .../dto/Quote/DuplicateQuote.dto/index.ts | 1 + .../sales/application/dto/Quote/index.ts | 1 + 12 files changed, 389 insertions(+), 3 deletions(-) create mode 100644 server/src/contexts/sales/application/Quote/DuplicateQuote.useCase.ts create mode 100644 server/src/contexts/sales/infrastructure/express/controllers/quotes/duplicateQuote/DuplicateQuote.controller.ts create mode 100644 server/src/contexts/sales/infrastructure/express/controllers/quotes/duplicateQuote/index.ts create mode 100644 server/src/contexts/sales/infrastructure/express/controllers/quotes/duplicateQuote/presenter/DuplicateQuote.presenter.ts create mode 100644 server/src/contexts/sales/infrastructure/express/controllers/quotes/duplicateQuote/presenter/index.ts create mode 100644 shared/lib/contexts/sales/application/dto/Quote/DuplicateQuote.dto/IDuplicateQuote_Response.dto.ts create mode 100644 shared/lib/contexts/sales/application/dto/Quote/DuplicateQuote.dto/index.ts diff --git a/server/src/contexts/sales/application/Quote/DuplicateQuote.useCase.ts b/server/src/contexts/sales/application/Quote/DuplicateQuote.useCase.ts new file mode 100644 index 0000000..b11ff4b --- /dev/null +++ b/server/src/contexts/sales/application/Quote/DuplicateQuote.useCase.ts @@ -0,0 +1,130 @@ +import { + IUseCase, + IUseCaseError, + IUseCaseRequest, + UseCaseError, +} from "@/contexts/common/application/useCases"; +import { IRepositoryManager } from "@/contexts/common/domain"; +import { ISequelizeAdapter } from "@/contexts/common/infrastructure/sequelize"; +import { Collection, IDomainError, Result, UniqueID, UTCDateValue } from "@shared/contexts"; +import { + Dealer, + IQuoteRepository, + QuoteCustomerReference, + QuoteItem, + QuoteReference, + QuoteStatus, +} from "../../domain"; + +import { IInfrastructureError } from "@/contexts/common/infrastructure"; +import { SequelizeBusinessTransactionType } from "@/contexts/common/infrastructure/sequelize/SequelizeBusinessTransaction"; +import { Quote } from "../../domain/entities/Quotes/Quote"; +import { ISalesContext } from "../../infrastructure"; +import { findQuoteById, generateQuoteReferenceForDealer } from "../services"; + +export interface IDuplicateQuoteUseCaseRequest extends IUseCaseRequest { + sourceId: UniqueID; +} + +export type DuplicateQuoteResponseOrError = + | Result // Misc errors (value objects) + | Result; // Success! + +export class DuplicateQuoteUseCase + implements IUseCase> +{ + private _adapter: ISequelizeAdapter; + private _repositoryManager: IRepositoryManager; + private _dealer: Dealer; + private _transaction: SequelizeBusinessTransactionType; + + constructor(context: ISalesContext) { + this._adapter = context.adapter; + this._repositoryManager = context.repositoryManager; + this._dealer = context.dealer!; + } + + async execute(request: IDuplicateQuoteUseCaseRequest): Promise { + const { sourceId } = request; + const targetId = UniqueID.generateNewID().object; + + const transaction = this._adapter.startTransaction(); + const QuoteRepoBuilder = this._repositoryManager.getRepository("Quote"); + + let sourceQuote: Quote | null = null; + let quoteReference: QuoteReference; + let targetQuoteOrError: Result; + + // Buscar el quote + try { + return await transaction.complete(async (t) => { + const quoteRepo = QuoteRepoBuilder({ transaction: t }); + + sourceQuote = await findQuoteById(sourceId, quoteRepo); + + if (!sourceQuote) { + return Result.fail(UseCaseError.create(UseCaseError.NOT_FOUND_ERROR, "Quote not found")); + } + + // Generate Reference + quoteReference = await generateQuoteReferenceForDealer(this._dealer, quoteRepo); + + targetQuoteOrError = this._tryDuplicateQuoteInstance(sourceQuote, targetId, quoteReference); + + await quoteRepo.create(targetQuoteOrError.object); + return Result.ok(targetQuoteOrError.object); + }); + } catch (error: unknown) { + const _error = error as IInfrastructureError; + return Result.fail(UseCaseError.create(UseCaseError.REPOSITORY_ERROR, _error.message)); + } + } + + private _tryDuplicateQuoteInstance( + sourceQuote: Quote, + targetId: UniqueID, + targetReference: QuoteReference + ): Result { + const dealerId: UniqueID = this._dealer.id; + + const status = QuoteStatus.createDraft(); + const date = UTCDateValue.createCurrentDate().object; + const customerReference = QuoteCustomerReference.create("").object; + const dateSent = UTCDateValue.create(null).object; + + // items + let items: Collection; + + try { + items = new Collection( + sourceQuote.items.toArray().map((item) => QuoteItem.create(item).object) + ); + } catch (e: unknown) { + return Result.fail(e as IDomainError); + } + + return Quote.create( + { + status, + date, + reference: targetReference, + language: sourceQuote.language, + customerReference, + customer: sourceQuote.customer, + currency: sourceQuote.currency, + paymentMethod: sourceQuote.paymentMethod, + notes: sourceQuote.notes, + validity: sourceQuote.validity, + + discount: sourceQuote.discount, + tax: sourceQuote.tax, + + items, + + dealerId, + dateSent, + }, + targetId + ); + } +} diff --git a/server/src/contexts/sales/application/Quote/index.ts b/server/src/contexts/sales/application/Quote/index.ts index f4d3996..fcbfb5f 100644 --- a/server/src/contexts/sales/application/Quote/index.ts +++ b/server/src/contexts/sales/application/Quote/index.ts @@ -1,5 +1,6 @@ export * from "./CreateQuote.useCase"; export * from "./DeleteQuote.useCase"; +export * from "./DuplicateQuote.useCase"; export * from "./GetQuote.useCase"; export * from "./ListQuotes.useCase"; export * from "./SendQuote.useCase"; diff --git a/server/src/contexts/sales/application/services/QuoteService.ts b/server/src/contexts/sales/application/services/QuoteService.ts index 1ae8fda..a968260 100644 --- a/server/src/contexts/sales/application/services/QuoteService.ts +++ b/server/src/contexts/sales/application/services/QuoteService.ts @@ -1,4 +1,5 @@ -import { IDealer, IQuoteRepository, QuoteReference } from "../../domain"; +import { UniqueID } from "@shared/contexts"; +import { IDealer, IQuoteRepository, Quote, QuoteReference } from "../../domain"; const generateQuoteReferenceForDealer = async ( dealer: IDealer, @@ -38,4 +39,11 @@ const generateQuoteReferenceForDealer = async ( return newQuoteReference; }; -export { generateQuoteReferenceForDealer }; +const findQuoteById = async ( + id: UniqueID, + quoteRepository: IQuoteRepository +): Promise => { + return quoteRepository.getById(id); +}; + +export { findQuoteById, generateQuoteReferenceForDealer }; diff --git a/server/src/contexts/sales/infrastructure/express/controllers/quotes/duplicateQuote/DuplicateQuote.controller.ts b/server/src/contexts/sales/infrastructure/express/controllers/quotes/duplicateQuote/DuplicateQuote.controller.ts new file mode 100644 index 0000000..b5a38e0 --- /dev/null +++ b/server/src/contexts/sales/infrastructure/express/controllers/quotes/duplicateQuote/DuplicateQuote.controller.ts @@ -0,0 +1,116 @@ +import { IUseCaseError, UseCaseError } from "@/contexts/common/application"; +import { IServerError } from "@/contexts/common/domain/errors"; +import { IInfrastructureError, InfrastructureError } from "@/contexts/common/infrastructure"; +import { ExpressController } from "@/contexts/common/infrastructure/express"; +import { DuplicateQuoteUseCase } from "@/contexts/sales/application"; +import { Quote } from "@/contexts/sales/domain/entities"; +import { ensureIdIsValid, IDuplicateQuote_Response_DTO } from "@shared/contexts"; +import { ISalesContext } from "../../../../Sales.context"; +import { IDuplicateQuotePresenter } from "./presenter"; + +export class DuplicateQuoteController extends ExpressController { + private useCase: DuplicateQuoteUseCase; + private presenter: IDuplicateQuotePresenter; + private context: ISalesContext; + + constructor( + props: { + useCase: DuplicateQuoteUseCase; + presenter: IDuplicateQuotePresenter; + }, + context: ISalesContext + ) { + super(); + + const { useCase, presenter } = props; + this.useCase = useCase; + this.presenter = presenter; + this.context = context; + } + + async executeImpl() { + try { + const { quoteId } = this.req.params; + + // Validar ID + const quoteIdOrError = ensureIdIsValid(quoteId); + if (quoteIdOrError.isFailure) { + const errorMessage = "Quote ID is not valid"; + const infraError = InfrastructureError.create( + InfrastructureError.INVALID_INPUT_DATA, + errorMessage, + quoteIdOrError.error + ); + return this.invalidInputError(errorMessage, infraError); + } + + // Llamar al caso de uso + const result = await this.useCase.execute({ + sourceId: quoteIdOrError.object, + }); + + if (result.isFailure) { + return this._handleExecuteError(result.error); + } + + const quote = result.object; + + return this.created(this.presenter.map(quote, this.context)); + } catch (e: unknown) { + return this.fail(e as IServerError); + } + } + + private _handleExecuteError(error: IUseCaseError) { + let errorMessage: string; + let infraError: IInfrastructureError; + + switch (error.code) { + case UseCaseError.INVALID_INPUT_DATA: + errorMessage = "Quote data not valid"; + infraError = InfrastructureError.create( + InfrastructureError.INVALID_INPUT_DATA, + errorMessage, + error + ); + return this.invalidInputError(errorMessage, infraError); + break; + + case UseCaseError.RESOURCE_ALREADY_EXITS: + errorMessage = "Quote already exists"; + + infraError = InfrastructureError.create( + InfrastructureError.RESOURCE_ALREADY_REGISTERED, + errorMessage, + error + ); + return this.conflictError(errorMessage, infraError); + break; + + case UseCaseError.REPOSITORY_ERROR: + errorMessage = "Error saving quote"; + infraError = InfrastructureError.create( + InfrastructureError.UNEXCEPTED_ERROR, + errorMessage, + error + ); + return this.conflictError(errorMessage, infraError); + break; + + case UseCaseError.UNEXCEPTED_ERROR: + errorMessage = error.message; + + infraError = InfrastructureError.create( + InfrastructureError.UNEXCEPTED_ERROR, + errorMessage, + error + ); + return this.internalServerError(errorMessage, infraError); + break; + + default: + errorMessage = error.message; + return this.clientError(errorMessage); + } + } +} diff --git a/server/src/contexts/sales/infrastructure/express/controllers/quotes/duplicateQuote/index.ts b/server/src/contexts/sales/infrastructure/express/controllers/quotes/duplicateQuote/index.ts new file mode 100644 index 0000000..cb378b3 --- /dev/null +++ b/server/src/contexts/sales/infrastructure/express/controllers/quotes/duplicateQuote/index.ts @@ -0,0 +1,16 @@ +import { DuplicateQuoteUseCase } from "@/contexts/sales/application"; +import { registerQuoteRepository } from "../../../../Quote.repository"; +import { ISalesContext } from "../../../../Sales.context"; +import { DuplicateQuoteController } from "./DuplicateQuote.controller"; +import { DuplicateQuotePresenter } from "./presenter"; + +export const duplicateQuoteController = (context: ISalesContext) => { + registerQuoteRepository(context); + return new DuplicateQuoteController( + { + useCase: new DuplicateQuoteUseCase(context), + presenter: DuplicateQuotePresenter, + }, + context + ); +}; diff --git a/server/src/contexts/sales/infrastructure/express/controllers/quotes/duplicateQuote/presenter/DuplicateQuote.presenter.ts b/server/src/contexts/sales/infrastructure/express/controllers/quotes/duplicateQuote/presenter/DuplicateQuote.presenter.ts new file mode 100644 index 0000000..8a80a82 --- /dev/null +++ b/server/src/contexts/sales/infrastructure/express/controllers/quotes/duplicateQuote/presenter/DuplicateQuote.presenter.ts @@ -0,0 +1,63 @@ +import { + ICollection, + IDuplicateQuote_QuoteItem_Response_DTO, + IDuplicateQuote_Response_DTO, +} from "@shared/contexts"; +import { Quote, QuoteItem } from "../../../../../../domain"; +import { ISalesContext } from "../../../../../Sales.context"; + +export interface IDuplicateQuotePresenter { + map: (quote: Quote, context: ISalesContext) => IDuplicateQuote_Response_DTO; +} + +export const DuplicateQuotePresenter: IDuplicateQuotePresenter = { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + map: (quote: Quote, context: ISalesContext): IDuplicateQuote_Response_DTO => { + return { + id: quote.id.toString(), + status: quote.status.toString(), + date: quote.date.toISO8601(), + reference: quote.reference.toString(), + customer_reference: quote.customerReference.toString(), + customer_information: quote.customer.toString(), + lang_code: quote.language.toString(), + currency_code: quote.currency.toString(), + + payment_method: quote.paymentMethod.toString(), + validity: quote.validity.toString(), + notes: quote.notes.toString(), + + subtotal_price: quote.subtotalPrice.convertScale(2).toObject(), + + discount: quote.discount.convertScale(2).toObject(), + discount_price: quote.discountPrice.convertScale(2).toObject(), + + before_tax_price: quote.beforeTaxPrice.convertScale(2).toObject(), + + tax: quote.tax.convertScale(2).toObject(), + tax_price: quote.taxPrice.convertScale(2).toObject(), + + total_price: quote.totalPrice.convertScale(2).toObject(), + + items: quoteItemPresenter(quote.items, context), + dealer_id: quote.dealerId.toString(), + }; + }, +}; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const quoteItemPresenter = ( + items: ICollection, + context: ISalesContext +): IDuplicateQuote_QuoteItem_Response_DTO[] => + items.totalCount > 0 + ? items.items.map((item: QuoteItem) => ({ + id_article: item.idArticle.toString(), + description: item.description.toString(), + quantity: item.quantity.convertScale(2).toObject(), + unit_price: item.unitPrice.convertScale(2).toObject(), + subtotal_price: item.subtotalPrice.convertScale(2).toObject(), + discount: item.discount.convertScale(2).toObject(), + total_price: item.totalPrice.convertScale(2).toObject(), + })) + : []; diff --git a/server/src/contexts/sales/infrastructure/express/controllers/quotes/duplicateQuote/presenter/index.ts b/server/src/contexts/sales/infrastructure/express/controllers/quotes/duplicateQuote/presenter/index.ts new file mode 100644 index 0000000..d822e69 --- /dev/null +++ b/server/src/contexts/sales/infrastructure/express/controllers/quotes/duplicateQuote/presenter/index.ts @@ -0,0 +1 @@ +export * from "./DuplicateQuote.presenter"; diff --git a/server/src/contexts/sales/infrastructure/express/controllers/quotes/index.ts b/server/src/contexts/sales/infrastructure/express/controllers/quotes/index.ts index cfdf99b..bec401b 100644 --- a/server/src/contexts/sales/infrastructure/express/controllers/quotes/index.ts +++ b/server/src/contexts/sales/infrastructure/express/controllers/quotes/index.ts @@ -1,7 +1,9 @@ export * from "./createQuote"; export * from "./deleteQuote"; +export * from "./duplicateQuote"; export * from "./getQuote"; export * from "./listQuotes"; export * from "./reportQuote"; +export * from "./sendQuote"; export * from "./setStatusQuote"; export * from "./updateQuote"; diff --git a/server/src/infrastructure/express/api/routes/quote.routes.ts b/server/src/infrastructure/express/api/routes/quote.routes.ts index 47de02e..fa2eb71 100644 --- a/server/src/infrastructure/express/api/routes/quote.routes.ts +++ b/server/src/infrastructure/express/api/routes/quote.routes.ts @@ -2,13 +2,14 @@ import { checkUser } from "@/contexts/auth"; import { handleRequest } from "@/contexts/common/infrastructure/express"; import { createQuoteController, + duplicateQuoteController, getQuoteController, listQuotesController, reportQuoteController, + sendQuoteController, setStatusQuoteController, updateQuoteController, } from "@/contexts/sales/infrastructure/express/controllers"; -import { sendQuoteController } from "@/contexts/sales/infrastructure/express/controllers/quotes/sendQuote"; import { getDealerMiddleware } from "@/contexts/sales/infrastructure/express/middlewares/Dealer.middleware"; import { Router } from "express"; @@ -26,6 +27,14 @@ export const quoteRouter = (appRouter: Router): void => { handleRequest(updateQuoteController) ); + // Duplicate + quoteRoutes.post( + "/:quoteId/duplicate", + checkUser, + getDealerMiddleware, + handleRequest(duplicateQuoteController) + ); + // Reports quoteRoutes.get( "/:quoteId/report", diff --git a/shared/lib/contexts/sales/application/dto/Quote/DuplicateQuote.dto/IDuplicateQuote_Response.dto.ts b/shared/lib/contexts/sales/application/dto/Quote/DuplicateQuote.dto/IDuplicateQuote_Response.dto.ts new file mode 100644 index 0000000..f70fb9b --- /dev/null +++ b/shared/lib/contexts/sales/application/dto/Quote/DuplicateQuote.dto/IDuplicateQuote_Response.dto.ts @@ -0,0 +1,38 @@ +import { IMoney_DTO, IPercentage_DTO, IQuantity_DTO } from "../../../../../common"; + +export interface IDuplicateQuote_Response_DTO { + id: string; + status: string; + date: string; + reference: string; + customer_reference: string; + customer_information: string; + lang_code: string; + currency_code: string; + + payment_method: string; + notes: string; + validity: string; + + subtotal_price: IMoney_DTO; + discount: IPercentage_DTO; + discount_price: IMoney_DTO; + before_tax_price: IMoney_DTO; + tax: IPercentage_DTO; + tax_price: IMoney_DTO; + total_price: IMoney_DTO; + + items: IDuplicateQuote_QuoteItem_Response_DTO[]; + + dealer_id: string; +} + +export interface IDuplicateQuote_QuoteItem_Response_DTO { + id_article: string; + quantity: IQuantity_DTO; + description: string; + unit_price: IMoney_DTO; + subtotal_price: IMoney_DTO; + discount: IPercentage_DTO; + total_price: IMoney_DTO; +} diff --git a/shared/lib/contexts/sales/application/dto/Quote/DuplicateQuote.dto/index.ts b/shared/lib/contexts/sales/application/dto/Quote/DuplicateQuote.dto/index.ts new file mode 100644 index 0000000..c75c079 --- /dev/null +++ b/shared/lib/contexts/sales/application/dto/Quote/DuplicateQuote.dto/index.ts @@ -0,0 +1 @@ +export * from "./IDuplicateQuote_Response.dto"; diff --git a/shared/lib/contexts/sales/application/dto/Quote/index.ts b/shared/lib/contexts/sales/application/dto/Quote/index.ts index 18c0e95..6376f64 100644 --- a/shared/lib/contexts/sales/application/dto/Quote/index.ts +++ b/shared/lib/contexts/sales/application/dto/Quote/index.ts @@ -1,4 +1,5 @@ export * from "./CreateQuote.dto"; +export * from "./DuplicateQuote.dto"; export * from "./GetQuote.dto"; export * from "./ListQuotes.dto"; export * from "./SendQuote.dto";