This commit is contained in:
David Arranz 2024-07-23 13:19:00 +02:00
parent b6721c1b56
commit ebb2d06903
25 changed files with 265 additions and 145 deletions

View File

@ -63,6 +63,7 @@
"dotenv": "^16.3.1",
"express": "^4.18.2",
"express-openapi-validator": "^5.0.4",
"handlebars": "^4.7.8",
"helmet": "^7.0.0",
"http-status": "^1.7.4",
"joi": "^17.12.3",
@ -77,6 +78,7 @@
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
"path": "^0.12.7",
"puppeteer": "^22.13.1",
"remove": "^0.1.5",
"response-time": "^2.3.2",
"sequelize": "^6.37.3",

View File

@ -57,10 +57,14 @@ export abstract class ExpressController implements IController {
return this.res.status(httpStatus.NO_CONTENT).send();
}
public download(filepath: string, filename: string, done?: any) {
public downloadFile(filepath: string, filename: string, done?: any) {
return this.res.download(filepath, filename, done);
}
public downloadPDF(pdfBuffer: Buffer, filename: string) {
return this._download(pdfBuffer, "application/pdf", `${filename}.pdf`);
}
public clientError(message?: string) {
return this._errorResponse(httpStatus.BAD_REQUEST, message);
}
@ -116,4 +120,14 @@ export abstract class ExpressController implements IController {
): express.Response<IError_Response_DTO> {
return generateExpressError(this.req, this.res, statusCode, message, error);
}
private _download(buffer: Buffer, contentType: string, filename: string) {
this.res.set({
"Content-Type": contentType,
"Content-Disposition": `attachment; filename=${filename}`,
"Content-Length": buffer.length,
});
return this.res.send(buffer);
}
}

View File

@ -7,7 +7,7 @@ import {
import { IRepositoryManager } from "@/contexts/common/domain";
import { IInfrastructureError } from "@/contexts/common/infrastructure";
import { ISequelizeAdapter } from "@/contexts/common/infrastructure/sequelize";
import { DomainError, IUpdateProfile_Request_DTO, Result, UniqueID } from "@shared/contexts";
import { IUpdateProfile_Request_DTO, Note, Result, UniqueID } from "@shared/contexts";
import { IProfileRepository, Profile } from "../domain";
export interface IUpdateProfileUseCaseRequest extends IUseCaseRequest {
@ -32,7 +32,6 @@ export class UpdateProfileUseCase
async execute(request: IUpdateProfileUseCaseRequest): Promise<UpdateProfileResponseOrError> {
const { userId, profileDTO } = request;
const profileRepository = this._getProfileRepository();
// Comprobar que existe el profile
const exitsOrError = await this._getProfileDealer(userId);
@ -45,43 +44,17 @@ export class UpdateProfileUseCase
);
}
const oldProfile = exitsOrError.object;
const profile = exitsOrError.object;
// Crear perfil con datos actualizados
const profileOrError = Profile.create(
{
contactInformation: profileDTO.contact_information,
defaultPaymentMethod: profileDTO.default_payment_method,
defaultLegalTerms: profileDTO.default_legal_terms,
defaultNotes: profileDTO.default_notes,
defaultQuoteValidity: profileDTO.default_quote_validity,
},
oldProfile.id
);
if (profileOrError.isFailure) {
const { error: domainError } = profileOrError;
let errorCode = "";
let message = "";
switch (domainError.code) {
// Errores manuales
case DomainError.INVALID_INPUT_DATA:
errorCode = UseCaseError.INVALID_INPUT_DATA;
message = "The profile has some incorrect data";
break;
default:
errorCode = UseCaseError.UNEXCEPTED_ERROR;
message = domainError.message;
break;
}
return Result.fail(UseCaseError.create(errorCode, message, domainError));
}
// Actualizar el perfil con datos actualizados
profile.contactInformation = Note.create(profileDTO.contact_information).object;
profile.defaultPaymentMethod = Note.create(profileDTO.default_payment_method).object;
profile.defaultLegalTerms = Note.create(profileDTO.default_legal_terms).object;
profile.defaultNotes = Note.create(profileDTO.default_notes).object;
profile.defaultQuoteValidity = Note.create(profileDTO.default_quote_validity).object;
// Guardar los cambios
return this._saveProfile(profileOrError.object);
return this._saveProfile(profile);
}
private async _saveProfile(updatedProfile: Profile) {

View File

@ -63,19 +63,39 @@ export class Profile extends AggregateRoot<IProfileProps> implements IProfile {
return this.props.contactInformation;
}
set contactInformation(newNote: Note) {
this.props.contactInformation = newNote;
}
get defaultPaymentMethod(): Note {
return this.props.defaultPaymentMethod;
}
set defaultPaymentMethod(newPaymentMethod: Note) {
this.props.defaultPaymentMethod = newPaymentMethod;
}
get defaultNotes(): Note {
return this.props.defaultNotes;
}
set defaultNotes(newDefaultNotes: Note) {
this.props.defaultNotes = newDefaultNotes;
}
get defaultLegalTerms(): Note {
return this.props.defaultLegalTerms;
}
set defaultLegalTerms(newDefaultLegalTerms: Note) {
this.props.defaultLegalTerms = newDefaultLegalTerms;
}
get defaultQuoteValidity(): Note {
return this.props.defaultQuoteValidity;
}
set defaultQuoteValidity(newDefaultQuoteValidity: Note) {
this.props.defaultQuoteValidity = newDefaultQuoteValidity;
}
}

View File

@ -1,2 +1,2 @@
export * from "./Profile.context";
export * from "./express";
export * from "./Profile.context";

View File

@ -118,11 +118,16 @@ export class CreateDealerUseCase
return Result.fail(nameOrError.error);
}
const languageOrError = Language.createFromCode(dealerDTO.lang_code);
if (languageOrError.isFailure) {
return Result.fail(languageOrError.error);
}
return Dealer.create(
{
name: nameOrError.object,
currency: CurrencyData.createDefaultCode().object,
language: Language.createDefaultCode().object,
language: languageOrError.object,
status: DealerStatus.createActive(),
//user_id: user
},

View File

@ -8,6 +8,7 @@ import { IRepositoryManager } from "@/contexts/common/domain";
import { IInfrastructureError } from "@/contexts/common/infrastructure";
import { ISequelizeAdapter } from "@/contexts/common/infrastructure/sequelize";
import {
CurrencyData,
DomainError,
IDomainError,
IUpdateDealer_Request_DTO,
@ -109,15 +110,28 @@ export class UpdateDealerUseCase
return Result.fail(nameOrError.error);
}
const languageOrError = Language.createFromCode(dealerDTO.language);
const languageOrError = Language.createFromCode(dealerDTO.lang_code);
if (languageOrError.isFailure) {
return Result.fail(languageOrError.error);
}
const currencyOrError = CurrencyData.createDefaultCode();
if (currencyOrError.isFailure) {
return Result.fail(currencyOrError.error);
}
const DealerStatusOrError = CurrencyData.createDefaultCode();
if (currencyOrError.isFailure) {
return Result.fail(currencyOrError.error);
}
return Dealer.create(
{
name: nameOrError.object,
logo: "",
currency: currencyOrError.object,
//status:
//logo: "",
language: languageOrError.object,
additionalInfo: KeyValueMap.create([

View File

@ -1,8 +1,8 @@
export * from "./CreateDealer.useCase";
export * from "./DeleteDealer.useCase";
//export * from "./CreateDealer.useCase";
//export * from "./DeleteDealer.useCase";
export * from "./GetDealer.useCase";
export * from "./GetDealerByUser.useCase";
export * from "./ListDealers.useCase";
export * from "./UpdateDealer.useCase";
//export * from "./UpdateDealer.useCase";
export * from "./dealerServices";

View File

@ -1,77 +0,0 @@
import {
IUseCase,
IUseCaseError,
IUseCaseRequest,
UseCaseError,
} from "@/contexts/common/application/useCases";
import { IRepositoryManager } from "@/contexts/common/domain";
import { ISequelizeAdapter } from "@/contexts/common/infrastructure/sequelize";
import { Result, UniqueID } from "@shared/contexts";
import { IQuoteRepository } from "../../domain";
import { IInfrastructureError } from "@/contexts/common/infrastructure";
import { Quote } from "../../domain/entities/Quotes/Quote";
export interface IGetQuoteByUserByUserUseCaseRequest extends IUseCaseRequest {
userId: UniqueID;
}
export type GetQuoteByUserResponseOrError =
| Result<never, IUseCaseError> // Misc errors (value objects)
| Result<Quote, never>; // Success!
export class GetQuoteByUserUseCase
implements IUseCase<IGetQuoteByUserByUserUseCaseRequest, Promise<GetQuoteByUserResponseOrError>>
{
private _adapter: ISequelizeAdapter;
private _repositoryManager: IRepositoryManager;
constructor(props: { adapter: ISequelizeAdapter; repositoryManager: IRepositoryManager }) {
this._adapter = props.adapter;
this._repositoryManager = props.repositoryManager;
}
private getRepositoryByName<T>(name: string) {
return this._repositoryManager.getRepository<T>(name);
}
async execute(
request: IGetQuoteByUserByUserUseCaseRequest
): Promise<GetQuoteByUserResponseOrError> {
const { userId } = request;
// Validación de datos
// No hay en este caso
return await this.getUserQuote(userId);
}
private async getUserQuote(userId: UniqueID) {
const transaction = this._adapter.startTransaction();
const QuoteRepoBuilder = this.getRepositoryByName<IQuoteRepository>("Quote");
let Quote: Quote | null = null;
try {
await transaction.complete(async (t) => {
const QuoteRepo = QuoteRepoBuilder({ transaction: t });
Quote = await QuoteRepo.getByUserId(userId);
});
if (!Quote) {
return Result.fail(UseCaseError.create(UseCaseError.NOT_FOUND_ERROR, "Quote not found"));
}
return Result.ok<Quote>(Quote!);
} catch (error: unknown) {
const _error = error as IInfrastructureError;
return Result.fail(
UseCaseError.create(
UseCaseError.REPOSITORY_ERROR,
"Error al consultar la cotización",
_error
)
);
}
}
}

View File

@ -1,6 +1,5 @@
export * from "./CreateQuote.useCase";
export * from "./DeleteQuote.useCase";
export * from "./GetQuote.useCase";
export * from "./GetQuoteByUser.useCase";
export * from "./ListQuotes.useCase";
export * from "./UpdateQuote.useCase";

View File

@ -12,6 +12,7 @@ export const GetDealerByUserPresenter: IGetDealerByUserPresenter = {
return {
id: dealer.id.toString(),
name: dealer.name.toString(),
lang_code: dealer.language.code,
};
},
};

View File

@ -1,5 +1,5 @@
export * from "./createDealer";
export * from "./deleteDealer";
//export * from "./createDealer";
//export * from "./deleteDealer";
export * from "./getDealer";
export * from "./listDealers";
export * from "./updateDealer";
//export * from "./updateDealer";

View File

@ -2,4 +2,5 @@ export * from "./createQuote";
export * from "./deleteQuote";
export * from "./getQuote";
export * from "./listQuotes";
export * from "./reportQuote";
export * from "./updateQuote";

View File

@ -0,0 +1,97 @@
import { IUseCaseError, UseCaseError } from "@/contexts/common/application/useCases";
import { ExpressController } from "@/contexts/common/infrastructure/express";
import { IServerError } from "@/contexts/common/domain/errors";
import { IInfrastructureError, InfrastructureError } from "@/contexts/common/infrastructure";
import { GetQuoteUseCase } from "@/contexts/sales/application";
import { Quote } from "@/contexts/sales/domain";
import { ensureIdIsValid } from "@shared/contexts";
import { ISalesContext } from "../../../../Sales.context";
import { IReportQuoteReporter } from "./reporter/ReportQuote.reporter";
export class ReportQuoteController extends ExpressController {
private useCase: GetQuoteUseCase;
private reporter: IReportQuoteReporter;
private context: ISalesContext;
constructor(
props: {
useCase: GetQuoteUseCase;
reporter: IReportQuoteReporter;
},
context: ISalesContext
) {
super();
const { useCase, reporter: presenter } = props;
this.useCase = useCase;
this.reporter = presenter;
this.context = context;
}
async executeImpl(): Promise<any> {
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);
}
try {
const result = await this.useCase.execute({
id: quoteIdOrError.object,
});
if (result.isFailure) {
return this._handleExecuteError(result.error);
}
const quote = <Quote>result.object;
return this.downloadPDF(await this.reporter.toPDF(quote, this.context), "prueba.pdf");
} catch (e: unknown) {
return this.fail(e as IServerError);
}
}
private _handleExecuteError(error: IUseCaseError) {
let errorMessage: string;
let infraError: IInfrastructureError;
switch (error.code) {
case UseCaseError.NOT_FOUND_ERROR:
errorMessage = "Quote not found";
infraError = InfrastructureError.create(
InfrastructureError.RESOURCE_NOT_FOUND_ERROR,
errorMessage,
error
);
return this.notFoundError(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,24 @@
import { GetQuoteUseCase } from "@/contexts/sales/application";
import Express from "express";
import { registerQuoteRepository } from "../../../../Quote.repository";
import { ISalesContext } from "../../../../Sales.context";
import { ReportQuotePresenter } from "./reporter/ReportQuote.reporter";
import { ReportQuoteController } from "./ReportQuote.controller";
export const reportQuoteController = (
req: Express.Request,
res: Express.Response,
next: Express.NextFunction
) => {
const context: ISalesContext = res.locals.context;
registerQuoteRepository(context);
return new ReportQuoteController(
{
useCase: new GetQuoteUseCase(context),
reporter: ReportQuotePresenter,
},
context
).execute(req, res, next);
};

View File

@ -0,0 +1,40 @@
import * as handlebars from "handlebars";
import * as puppeteer from "puppeteer";
import { Quote } from "../../../../../../domain";
import { ISalesContext } from "../../../../../Sales.context";
export interface IReportQuoteReporter {
toPDF: (quote: Quote, context: ISalesContext) => any;
}
export const ReportQuotePresenter: IReportQuoteReporter = {
toPDF: async (quote: Quote, context: ISalesContext): Promise<Buffer> => {
// Obtener y compilar la plantilla HTML
const templateHtml = obtenerPlantillaHTML();
const template = handlebars.compile(templateHtml);
const html = template(quote);
// Generar el PDF con Puppeteer
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setContent(html);
const pdfBuffer = await page.pdf({ format: "A4" });
await browser.close();
return pdfBuffer;
},
};
const obtenerPlantillaHTML = (): string => {
// Implementar la lógica para obtener la plantilla HTML
return `
<html>
<head>
<title>Factura</title>
</head>
<body>
<h1>Factura: </h1>
<p>Cliente:</p>
</body>
</html>`;
};

View File

@ -0,0 +1 @@
export * from "./ReportQuote.reporter";

View File

@ -1 +1 @@
export * from "../../../../infrastructure/express/api/routes/sales.routes";
export * from "./controllers";

View File

@ -100,6 +100,8 @@ export const initializeSampleUser = async (
await repository({ transaction: t }).create(user);
console.log("Usuario creado");
return user;
} else {
return false;
}
});
};

View File

@ -1,10 +1,7 @@
import { checkUser, checkisAdmin } from "@/contexts/auth";
import {
createDealerController,
deleteDealerController,
getDealerController,
listDealersController,
updateDealerController,
} from "@/contexts/sales/infrastructure/express/controllers/dealers";
import { getDealerMiddleware } from "@/contexts/sales/infrastructure/express/middlewares/dealerMiddleware";
import Express from "express";
@ -14,9 +11,9 @@ export const DealerRouter = (appRouter: Express.Router) => {
dealerRoutes.get("/", checkisAdmin, listDealersController);
dealerRoutes.get("/:dealerId", checkUser, getDealerMiddleware, getDealerController);
dealerRoutes.post("/", checkisAdmin, createDealerController);
dealerRoutes.put("/:dealerId", checkisAdmin, updateDealerController);
dealerRoutes.delete("/:dealerId", checkisAdmin, deleteDealerController);
///dealerRoutes.post("/", checkisAdmin, createDealerController);
//dealerRoutes.put("/:dealerId", checkisAdmin, updateDealerController);
//dealerRoutes.delete("/:dealerId", checkisAdmin, deleteDealerController);
// Anidar quotes en /dealers/:dealerId
//dealerRoutes.use("/:dealerId/quotes", quoteRoutes);

View File

@ -3,6 +3,7 @@ import {
createQuoteController,
getQuoteController,
listQuotesController,
reportQuoteController,
updateQuoteController,
} from "@/contexts/sales/infrastructure/express/controllers";
import { getDealerMiddleware } from "@/contexts/sales/infrastructure/express/middlewares/dealerMiddleware";
@ -11,12 +12,15 @@ import Express from "express";
export const QuoteRouter = (appRouter: Express.Router) => {
const quoteRoutes: Express.Router = Express.Router({ mergeParams: true });
// Users CRUD
quoteRoutes.get("/", checkUser, getDealerMiddleware, listQuotesController);
quoteRoutes.get("/:quoteId", checkUser, getDealerMiddleware, getQuoteController);
//quoteRoutes.get("/:quoteId/report", checkUser, getDealerMiddleware, getReportController;
quoteRoutes.post("/", checkUser, getDealerMiddleware, createQuoteController);
quoteRoutes.put("/:quoteId", checkUser, getDealerMiddleware, updateQuoteController);
// Reports
quoteRoutes.get("/:quoteId/report", checkUser, getDealerMiddleware, reportQuoteController);
/*
quoteRoutes.post("/", isAdmin, createQuoteController);

View File

@ -3,10 +3,7 @@
/* Basic Options */
"target": "ES2022" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */,
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
"lib": [
"ES2022",
"dom"
] /* Specify library files to be included in the compilation. */,
"lib": ["ES2022", "dom"] /* Specify library files to be included in the compilation. */,
"allowJs": false /* Allow javascript files to be compiled. */,
"pretty": true,
@ -76,6 +73,15 @@
"src/**/__tests__/*",
"src/**/*.mock.*",
"src/**/*.test.*",
"node_modules"
"node_modules",
"src/**/firebird/*",
"src/**/CreateDealer.useCase.ts",
"src/**/UpdateDealer.useCase.ts",
"src/**/createDealer/*",
"src/**/updateDealer/*",
"src/**/deleteDealer/*"
]
}

View File

@ -10,7 +10,7 @@ export interface IEntityProps {
export abstract class Entity<T extends IEntityProps> {
protected readonly _id: UniqueID;
protected readonly props: T;
protected props: T;
public get id(): UniqueID {
return this._id;
@ -40,8 +40,6 @@ export abstract class Entity<T extends IEntityProps> {
public toString(): { [s: string]: string } {
const flattenProps = this._flattenProps(this.props);
console.log(flattenProps);
return {
id: this._id.toString(),
...flattenProps.map((prop: any) => String(prop)),
@ -51,8 +49,6 @@ export abstract class Entity<T extends IEntityProps> {
public toPrimitives(): { [s: string]: any } {
const flattenProps = this._flattenProps(this.props);
console.log(flattenProps);
return {
id: this._id.value,
...flattenProps,

View File

@ -4,6 +4,7 @@ import { Result, RuleValidator } from "../../../../../common";
export interface ICreateDealer_Request_DTO {
id: string;
name: string;
lang_code: string;
}
export function ensureCreateDealer_Request_DTOIsValid(dealerDTO: ICreateDealer_Request_DTO) {

View File

@ -3,7 +3,7 @@ import { Result, RuleValidator } from "../../../../../common";
export interface IUpdateDealer_Request_DTO {
name: string;
language: string;
lang_code: string;
contact_information: string;
default_payment_method: string;
default_notes: string;
@ -14,7 +14,7 @@ export interface IUpdateDealer_Request_DTO {
export function ensureUpdateDealer_Request_DTOIsValid(dealerDTO: IUpdateDealer_Request_DTO) {
const schema = Joi.object({
name: Joi.string(),
language: Joi.string(),
lang_code: Joi.string(),
contact_information: Joi.string(),
default_payment_method: Joi.string(),
default_notes: Joi.string(),