Duplicar una propuesta (servidor)

This commit is contained in:
David Arranz 2024-11-13 16:08:20 +01:00
parent 9329f5abbe
commit adcc6e3028
12 changed files with 389 additions and 3 deletions

View File

@ -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<never, IUseCaseError> // Misc errors (value objects)
| Result<Quote, never>; // Success!
export class DuplicateQuoteUseCase
implements IUseCase<IDuplicateQuoteUseCaseRequest, Promise<DuplicateQuoteResponseOrError>>
{
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<DuplicateQuoteResponseOrError> {
const { sourceId } = request;
const targetId = UniqueID.generateNewID().object;
const transaction = this._adapter.startTransaction();
const QuoteRepoBuilder = this._repositoryManager.getRepository<IQuoteRepository>("Quote");
let sourceQuote: Quote | null = null;
let quoteReference: QuoteReference;
let targetQuoteOrError: Result<Quote, IDomainError>;
// 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<Quote>(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<Quote, IDomainError> {
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<QuoteItem>;
try {
items = new Collection<QuoteItem>(
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
);
}
}

View File

@ -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";

View File

@ -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<Quote | null> => {
return quoteRepository.getById(id);
};
export { findQuoteById, generateQuoteReferenceForDealer };

View File

@ -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 = <Quote>result.object;
return this.created<IDuplicateQuote_Response_DTO>(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);
}
}
}

View File

@ -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
);
};

View File

@ -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<QuoteItem>,
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(),
}))
: [];

View File

@ -0,0 +1 @@
export * from "./DuplicateQuote.presenter";

View File

@ -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";

View File

@ -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",

View File

@ -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;
}

View File

@ -0,0 +1 @@
export * from "./IDuplicateQuote_Response.dto";

View File

@ -1,4 +1,5 @@
export * from "./CreateQuote.dto";
export * from "./DuplicateQuote.dto";
export * from "./GetQuote.dto";
export * from "./ListQuotes.dto";
export * from "./SendQuote.dto";