.
This commit is contained in:
parent
71913c7c73
commit
8d280cbb48
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -8,5 +8,6 @@
|
||||
"editor.formatOnPaste": false,
|
||||
"prettier.useEditorConfig": false,
|
||||
"prettier.useTabs": false,
|
||||
"prettier.configPath": ".prettierrc"
|
||||
"prettier.configPath": ".prettierrc",
|
||||
"asciidoc.antora.enableAntoraSupport": true
|
||||
}
|
||||
|
||||
297
doc/readme.adoc
Normal file
297
doc/readme.adoc
Normal file
@ -0,0 +1,297 @@
|
||||
= SPEC-1: Aplicación Web para Generación de Presupuestos de Muebles
|
||||
:sectnums:
|
||||
:toc:
|
||||
|
||||
== Input
|
||||
|
||||
|
||||
Necesito planificar un proyecto web formado por un cliente web hecho en React y una REST API hecha en NodeJS, ExpressJS y Sequelize. La base de datos es MySQL.
|
||||
|
||||
El proyecto web consiste en una aplicación que sirve para generar presupuestos a partir de un catálogo de artículos. La aplicación es proporcionada por una fábrica de muebles a distribuidores ("dealers") donde venden esos muebles a clientes finales.
|
||||
|
||||
Cuando un cliente final llega a una tienda y pide un presupuesto, el distribuidor utiliza esta aplicación web para calcular una cotización ("quote") de lo que el cliente necesita.
|
||||
|
||||
Para realizar la cotización ("quote"), el distribuidor puede utilizar un catálogo de artículos ("catalog") que existe en la aplicación web ya de forma precargado y que no se puede modificar por parte del distribuidor. A la cotización también se le pueden añadir conceptos libres. Es importante que en los detalles/conceptos de la cotización se mantenga un orden determinado.
|
||||
|
||||
El fabricante es el único que puede modificar ese catálogo.
|
||||
|
||||
Si el cliente final acepta la cotización, el distribuidor "envia" la cotización a la fábrica. Junto con la cotización, el distribuidor también puede enviar documentos como ficheros PDF, fotos, imágenes, planos, etc.
|
||||
|
||||
La fábrica estudia la cotización del distribuidor junto con los documentos enviados y genera un contrato ("contract") asociado que reune todo, junto el cálculo real de todos los importes.
|
||||
|
||||
Ese contrato se vuelve a enviar al distribuidor para que sepa realmente lo que cuesta lo que ha pedido su cliente final.
|
||||
|
||||
Añade a "must have" que la fábrica debe poder gestionar los distribuidores en la aplicación, pudiente añadir, modificar, o dar de baja distribuidores.
|
||||
|
||||
Cada distribuidor tiene un usuario formado por un email y una contraseña para entrar en la aplicación web. La fábrica también tiene un email y contraseña para entrar en la aplicación.
|
||||
|
||||
Añade a "shoud have" la posibilidad, por parte de la fábrica, de poder bloquear un distribuidor de manera que cuando un distribuidor está bloqueado, este puede acceder al sistema y ver sus cotizaciones pero no puede ni añadir cotizaciones nuevas ni modificar las que hubiera ni eliminarlas.
|
||||
|
||||
Quiero que el sistema esté desarrollado siguiente los principios SOLID y según los principios de la Clean Architecture o arquitectura de puertos y adaptadores.
|
||||
|
||||
|
||||
|
||||
== Background
|
||||
|
||||
Desarrollo de una aplicación web destinada a la generación de presupuestos de muebles. Esta aplicación será utilizada por distribuidores de una fábrica de muebles para calcular cotizaciones para los clientes finales en el punto de venta. El proceso comienza cuando un cliente solicita un presupuesto en una tienda. El vendedor usa la aplicación web para seleccionar los artículos del catálogo predefinido y generar una cotización.
|
||||
|
||||
El catálogo de artículos está precargado en la aplicación web y solo puede ser modificado por el fabricante. Una vez que el cliente final acepta la cotización, el distribuidor puede enviarla a la fábrica junto con documentos adicionales, como PDFs, fotos, imágenes, y planos. La fábrica revisa la cotización y los documentos adjuntos, luego genera un contrato que incluye el cálculo final de los importes y lo envía de vuelta al distribuidor.
|
||||
|
||||
La aplicación consta de un cliente web desarrollado en React y una REST API implementada con NodeJS, ExpressJS y Sequelize, utilizando MySQL como base de datos.
|
||||
|
||||
== Requirements
|
||||
|
||||
=== Must Have
|
||||
- La aplicación debe permitir a los distribuidores generar cotizaciones a partir de un catálogo de artículos precargado.
|
||||
- Los distribuidores deben poder enviar cotizaciones a la fábrica junto con documentos adicionales (PDFs, fotos, imágenes, planos).
|
||||
- La fábrica debe poder modificar el catálogo de artículos y actualizarlo en la aplicación web.
|
||||
- La fábrica debe recibir y revisar las cotizaciones enviadas por los distribuidores, junto con los documentos adjuntos.
|
||||
- La fábrica debe poder generar y enviar un contrato que incluya el cálculo final de los importes al distribuidor.
|
||||
- La fábrica debe poder gestionar los distribuidores en la aplicación, pudiendo añadir, modificar, o dar de baja distribuidores.
|
||||
- Cada distribuidor debe tener un usuario formado por un email y una contraseña para entrar en la aplicación web.
|
||||
- La fábrica también debe tener un email y contraseña para entrar en la aplicación.
|
||||
|
||||
=== Should Have
|
||||
- La aplicación debe ofrecer autenticación y autorización para distribuidores y empleados de la fábrica.
|
||||
- La fábrica debe poder bloquear un distribuidor de manera que cuando un distribuidor está bloqueado, este pueda acceder al sistema y ver sus cotizaciones, pero no pueda ni añadir cotizaciones nuevas ni modificar las existentes ni eliminarlas.
|
||||
- La aplicación debe tener una interfaz de usuario intuitiva y fácil de usar para la generación de cotizaciones.
|
||||
|
||||
=== Could Have
|
||||
- La aplicación podría permitir a los distribuidores revisar el historial de cotizaciones enviadas.
|
||||
- La aplicación podría ofrecer reportes y estadísticas sobre las cotizaciones generadas y enviadas.
|
||||
|
||||
=== Won't Have
|
||||
- Los distribuidores no podrán modificar el catálogo de artículos.
|
||||
- La aplicación no tendrá funcionalidad de chat en tiempo real entre distribuidores y la fábrica.
|
||||
|
||||
== Method
|
||||
|
||||
=== Arquitectura del Sistema
|
||||
|
||||
El sistema se compondrá de los siguientes componentes principales siguiendo los principios SOLID y Clean Architecture:
|
||||
|
||||
1. Cliente Web (React)
|
||||
- Interfaz de usuario para que los distribuidores generen cotizaciones.
|
||||
- Interfaz de usuario para que la fábrica gestione el catálogo y los distribuidores.
|
||||
- Interfaz de usuario para que la fábrica reciba y revise las cotizaciones enviadas por los distribuidores.
|
||||
- Interfaz de usuario para que la fábrica genere y envíe contratos asociados a las cotizaciones.
|
||||
|
||||
2. REST API (NodeJS, ExpressJS)
|
||||
- Controladores para manejar las solicitudes HTTP.
|
||||
- Casos de uso que representan la lógica de negocio.
|
||||
- Entidades que representan los objetos de dominio.
|
||||
- Repositorios para interactuar con la base de datos.
|
||||
|
||||
3. Base de Datos (MySQL)
|
||||
- Almacenamiento de usuarios, catálogos, cotizaciones, documentos adjuntos y contratos.
|
||||
|
||||
=== Diagrama de Componentes
|
||||
|
||||
[plantuml, components]
|
||||
----
|
||||
@startuml
|
||||
package "Cliente Web" {
|
||||
[React App]
|
||||
}
|
||||
|
||||
package "REST API" {
|
||||
[Express Server]
|
||||
package "Controllers" {
|
||||
[UserController]
|
||||
[ArticuloController]
|
||||
[CotizacionController]
|
||||
[DocumentoController]
|
||||
[ContratoController]
|
||||
}
|
||||
package "Use Cases" {
|
||||
[UserUseCases]
|
||||
[ArticuloUseCases]
|
||||
[CotizacionUseCases]
|
||||
[DocumentoUseCases]
|
||||
[ContratoUseCases]
|
||||
}
|
||||
package "Entities" {
|
||||
[User]
|
||||
[Articulo]
|
||||
[Cotizacion]
|
||||
[Documento]
|
||||
[Contrato]
|
||||
}
|
||||
package "Repositories" {
|
||||
[UserRepository]
|
||||
[ArticuloRepository]
|
||||
[CotizacionRepository]
|
||||
[DocumentoRepository]
|
||||
[ContratoRepository]
|
||||
}
|
||||
}
|
||||
|
||||
package "Base de Datos" {
|
||||
[MySQL Database]
|
||||
}
|
||||
|
||||
[React App] --> [Express Server] : HTTP Requests
|
||||
[Express Server] --> [Controllers] : Route Handlers
|
||||
[Controllers] --> [Use Cases] : Application Logic
|
||||
[Use Cases] --> [Entities] : Domain Logic
|
||||
[Use Cases] --> [Repositories] : Data Access
|
||||
[Repositories] --> [MySQL Database] : CRUD Operations
|
||||
@enduml
|
||||
----
|
||||
|
||||
=== Esquema de Base de Datos
|
||||
|
||||
El esquema de la base de datos incluye las siguientes tablas:
|
||||
|
||||
1. **Usuarios**
|
||||
- `id` (INT, PK, AUTO_INCREMENT)
|
||||
- `email` (VARCHAR, UNIQUE)
|
||||
- `password` (VARCHAR)
|
||||
- `role` (ENUM: 'distribuidor', 'fabrica')
|
||||
- `status` (ENUM: 'activo', 'bloqueado')
|
||||
|
||||
2. **Articulos**
|
||||
- `id` (INT, PK, UUID)
|
||||
- `nombre` (VARCHAR)
|
||||
- `descripcion` (TEXT)
|
||||
- `precio` (DECIMAL)
|
||||
|
||||
3. **Cotizaciones**
|
||||
- `id` (INT, PK, AUTO_INCREMENT)
|
||||
- `distribuidor_id` (INT, FK -> Usuarios.id)
|
||||
- `fecha` (DATETIME)
|
||||
- `estado` (ENUM: 'pendiente', 'enviada', 'aceptada', 'rechazada')
|
||||
|
||||
4. **Detalles_Cotizacion**
|
||||
- `id` (INT, PK, AUTO_INCREMENT)
|
||||
- `cotizacion_id` (INT, FK -> Cotizaciones.id)
|
||||
- `articulo_id` (INT, FK -> Articulos.id)
|
||||
- `cantidad` (INT)
|
||||
- `precio_total` (DECIMAL)
|
||||
|
||||
5. **Documentos**
|
||||
- `id` (INT, PK, AUTO_INCREMENT)
|
||||
- `cotizacion_id` (INT, FK -> Cotizaciones.id)
|
||||
- `tipo` (VARCHAR)
|
||||
- `ruta` (VARCHAR)
|
||||
|
||||
6. **Contratos**
|
||||
- `id` (INT, PK, AUTO_INCREMENT)
|
||||
- `cotizacion_id` (INT, FK -> Cotizaciones.id)
|
||||
- `fabrica_id` (INT, FK -> Usuarios.id)
|
||||
- `distribuidor_id` (INT, FK -> Usuarios.id)
|
||||
- `fecha_envio` (DATETIME)
|
||||
- `contenido` (TEXT)
|
||||
- `ruta_documento` (VARCHAR)
|
||||
|
||||
=== Diagrama de Base de Datos
|
||||
|
||||
[plantuml, database_schema]
|
||||
----
|
||||
@startuml
|
||||
entity "Usuarios" {
|
||||
+ id: INT <<PK>>
|
||||
+ email: VARCHAR <<UNIQUE>>
|
||||
+ password: VARCHAR
|
||||
+ role: ENUM('distribuidor', 'fabrica')
|
||||
+ status: ENUM('activo', 'bloqueado')
|
||||
}
|
||||
|
||||
entity "Articulos" {
|
||||
+ id: INT <<PK>>
|
||||
+ nombre: VARCHAR
|
||||
+ descripcion: TEXT
|
||||
+ precio: DECIMAL
|
||||
}
|
||||
|
||||
entity "Cotizaciones" {
|
||||
+ id: INT <<PK>>
|
||||
+ distribuidor_id: INT <<FK>>
|
||||
+ fecha: DATETIME
|
||||
+ estado: ENUM('pendiente', 'enviada', 'aceptada', 'rechazada')
|
||||
}
|
||||
|
||||
entity "Detalles_Cotizacion" {
|
||||
+ id: INT <<PK>>
|
||||
+ cotizacion_id: INT <<FK>>
|
||||
+ articulo_id: INT <<FK>>
|
||||
+ cantidad: INT
|
||||
+ precio_total: DECIMAL
|
||||
}
|
||||
|
||||
entity "Documentos" {
|
||||
+ id: INT <<PK>>
|
||||
+ cotizacion_id: INT <<FK>>
|
||||
+ tipo: VARCHAR
|
||||
+ ruta: VARCHAR
|
||||
}
|
||||
|
||||
entity "Contratos" {
|
||||
+ id: INT <<PK>>
|
||||
+ cotizacion_id: INT <<FK>>
|
||||
+ fabrica_id: INT <<FK>>
|
||||
+ distribuidor_id: INT <<FK>>
|
||||
+ fecha_envio: DATETIME
|
||||
+ contenido: TEXT
|
||||
+ ruta_documento: VARCHAR
|
||||
}
|
||||
|
||||
Usuarios ||--o{ Cotizaciones : "distribuidor_id"
|
||||
Cotizaciones ||--|{ Detalles_Cotizacion : "cotizacion_id"
|
||||
Articulos ||--o{ Detalles_Cotizacion : "articulo_id"
|
||||
Cotizaciones ||--o{ Documentos : "cotizacion_id"
|
||||
Cotizaciones ||--|{ Contratos : "cotizacion_id"
|
||||
Usuarios ||--o{ Contratos : "fabrica_id"
|
||||
Usuarios ||--o{ Contratos : "distribuidor_id"
|
||||
@enduml
|
||||
----
|
||||
|
||||
=== Endpoints de la API
|
||||
|
||||
1. **Usuarios**
|
||||
- `POST /usuarios` : Crear un nuevo usuario.
|
||||
- `POST /login` : Iniciar sesión y obtener token de autenticación.
|
||||
- `GET /usuarios/:id` : Obtener información de un usuario.
|
||||
- `PUT /usuarios/:id` : Actualizar información de un usuario.
|
||||
- `DELETE /usuarios/:id` : Eliminar un usuario.
|
||||
|
||||
2. **Articulos**
|
||||
- `GET /articulos` : Obtener lista de artículos.
|
||||
- `GET /articulos/:id` : Obtener detalles de un artículo.
|
||||
- `POST /articulos` : Crear un nuevo artículo (solo fábrica).
|
||||
- `PUT /articulos/:id` : Actualizar un artículo (solo fábrica).
|
||||
- `DELETE /articulos/:id` : Eliminar un artículo (solo fábrica).
|
||||
|
||||
3. **Cotizaciones**
|
||||
- `GET /cotizaciones` : Obtener lista de cotizaciones (por distribuidor o fábrica).
|
||||
- `GET /cotizaciones/:id` : Obtener detalles de una cotización.
|
||||
- `POST /cotizaciones` : Crear una nueva cotización (solo distribuidor).
|
||||
- `PUT /cotizaciones/:id` : Actualizar una cotización (solo fábrica).
|
||||
- `DELETE /cotizaciones/:id` : Eliminar una cotización (solo fábrica).
|
||||
|
||||
4. **Documentos**
|
||||
- `POST /cotizaciones/:id/documentos` : Subir un documento para una cotización.
|
||||
- `GET /cotizaciones/:id/documentos` : Obtener lista de documentos de una cotización.
|
||||
- `DELETE /documentos/:id` : Eliminar un documento.
|
||||
|
||||
5. **Contratos**
|
||||
- `GET /contratos` : Obtener lista de contratos (por distribuidor o fábrica).
|
||||
- `GET /contratos/:id` : Obtener detalles de un contrato.
|
||||
- `POST /contratos` : Crear un nuevo contrato (solo fábrica).
|
||||
- `PUT /contratos/:id` : Actualizar un contrato (solo fábrica).
|
||||
- `DELETE /contratos/:id` : Eliminar un contrato.
|
||||
|
||||
=== Funcionalidades del Cliente Web
|
||||
|
||||
1. **Distribuidores**
|
||||
- Generar nuevas cotizaciones a partir del catálogo de artículos.
|
||||
- Enviar cotizaciones a la fábrica con documentos adjuntos.
|
||||
- Ver el historial de cotizaciones enviadas.
|
||||
- Acceder a contratos generados por la fábrica.
|
||||
|
||||
2. **Fábrica**
|
||||
- Gestionar el catálogo de artículos (añadir, modificar, eliminar artículos).
|
||||
- Gestionar distribuidores (añadir, modificar, eliminar distribuidores, bloquear/desbloquear distribuidores).
|
||||
- Revisar cotizaciones enviadas por distribuidores.
|
||||
- Generar y enviar contratos basados en cotizaciones revisadas.
|
||||
- Acceder al historial de contratos enviados a los distribuidores.
|
||||
@ -1,13 +1,6 @@
|
||||
import {
|
||||
DataTypes,
|
||||
InferAttributes,
|
||||
InferCreationAttributes,
|
||||
Model,
|
||||
Sequelize,
|
||||
} from "sequelize";
|
||||
import { DataTypes, InferAttributes, InferCreationAttributes, Model, Sequelize } from "sequelize";
|
||||
|
||||
export type AuthUserCreationAttributes =
|
||||
InferCreationAttributes<AuthUser_Model>;
|
||||
export type AuthUserCreationAttributes = InferCreationAttributes<AuthUser_Model>;
|
||||
|
||||
export class AuthUser_Model extends Model<
|
||||
InferAttributes<AuthUser_Model>,
|
||||
@ -18,6 +11,7 @@ export class AuthUser_Model extends Model<
|
||||
return Promise.resolve();
|
||||
}*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
static associate(connection: Sequelize) {}
|
||||
|
||||
declare id: string;
|
||||
@ -61,7 +55,7 @@ export default (sequelize: Sequelize) => {
|
||||
deletedAt: "deleted_at",
|
||||
|
||||
indexes: [{ name: "email_idx", fields: ["email"] }],
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return AuthUser_Model;
|
||||
|
||||
@ -12,7 +12,7 @@ import {
|
||||
ensureIdIsValid,
|
||||
ensureNameIsValid,
|
||||
} from "@shared/contexts";
|
||||
import { Dealer, IDealerRepository } from "../domain";
|
||||
import { Dealer, IDealerRepository } from "../../domain";
|
||||
|
||||
export type CreateDealerResponseOrError =
|
||||
| Result<never, IUseCaseError> // Misc errors (value objects)
|
||||
@ -7,7 +7,7 @@ import {
|
||||
import { IRepositoryManager } from "@/contexts/common/domain";
|
||||
import { ISequelizeAdapter } from "@/contexts/common/infrastructure/sequelize";
|
||||
import { Result, UniqueID } from "@shared/contexts";
|
||||
import { IDealerRepository } from "../domain";
|
||||
import { IDealerRepository } from "../../domain";
|
||||
|
||||
export interface IDeleteDealerUseCaseRequest extends IUseCaseRequest {
|
||||
id: UniqueID;
|
||||
@ -7,10 +7,10 @@ import {
|
||||
import { IRepositoryManager } from "@/contexts/common/domain";
|
||||
import { ISequelizeAdapter } from "@/contexts/common/infrastructure/sequelize";
|
||||
import { Result, UniqueID } from "@shared/contexts";
|
||||
import { IDealerRepository } from "../domain";
|
||||
import { IDealerRepository } from "../../domain";
|
||||
|
||||
import { IInfrastructureError } from "@/contexts/common/infrastructure";
|
||||
import { Dealer } from "../domain/entities/Dealer";
|
||||
import { Dealer } from "../../domain/entities/Dealer";
|
||||
|
||||
export interface IGetDealerUseCaseRequest extends IUseCaseRequest {
|
||||
id: UniqueID;
|
||||
@ -7,10 +7,10 @@ import {
|
||||
import { IRepositoryManager } from "@/contexts/common/domain";
|
||||
import { ISequelizeAdapter } from "@/contexts/common/infrastructure/sequelize";
|
||||
import { Result, UniqueID } from "@shared/contexts";
|
||||
import { IDealerRepository } from "../domain";
|
||||
import { IDealerRepository } from "../../domain";
|
||||
|
||||
import { IInfrastructureError } from "@/contexts/common/infrastructure";
|
||||
import { Dealer } from "../domain/entities/Dealer";
|
||||
import { Dealer } from "../../domain/entities/Dealer";
|
||||
|
||||
export interface IGetDealerByUserByUserUseCaseRequest extends IUseCaseRequest {
|
||||
userId: UniqueID;
|
||||
@ -4,8 +4,8 @@ import { Collection, ICollection, IQueryCriteria, Result } from "@shared/context
|
||||
|
||||
import { IInfrastructureError } from "@/contexts/common/infrastructure";
|
||||
import { ISequelizeAdapter } from "@/contexts/common/infrastructure/sequelize";
|
||||
import { Dealer } from "../domain";
|
||||
import { IDealerRepository } from "../domain/repository";
|
||||
import { Dealer } from "../../domain";
|
||||
import { IDealerRepository } from "../../domain/repository";
|
||||
|
||||
export interface IListDealersParams {
|
||||
queryCriteria: IQueryCriteria;
|
||||
@ -50,7 +50,11 @@ export class ListDealersUseCase
|
||||
} catch (error: unknown) {
|
||||
const _error = error as IInfrastructureError;
|
||||
return Result.fail(
|
||||
UseCaseError.create(UseCaseError.REPOSITORY_ERROR, "Error al listar los usurios", _error)
|
||||
UseCaseError.create(
|
||||
UseCaseError.REPOSITORY_ERROR,
|
||||
"Error al listar los distribuidores",
|
||||
_error
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -4,19 +4,18 @@ import {
|
||||
IUseCaseRequest,
|
||||
UseCaseError,
|
||||
} from "@/contexts/common/application";
|
||||
import { IRepositoryManager, Password } from "@/contexts/common/domain";
|
||||
import { IRepositoryManager } from "@/contexts/common/domain";
|
||||
import { IInfrastructureError } from "@/contexts/common/infrastructure";
|
||||
import { ISequelizeAdapter } from "@/contexts/common/infrastructure/sequelize";
|
||||
import {
|
||||
DomainError,
|
||||
Email,
|
||||
IDomainError,
|
||||
IUpdateDealer_Request_DTO,
|
||||
Name,
|
||||
Result,
|
||||
UniqueID,
|
||||
} from "@shared/contexts";
|
||||
import { Dealer, IDealerRepository } from "../domain";
|
||||
import { Dealer, IDealerRepository } from "../../domain";
|
||||
|
||||
export interface IUpdateDealerUseCaseRequest extends IUseCaseRequest {
|
||||
id: UniqueID;
|
||||
@ -108,21 +107,9 @@ export class UpdateDealerUseCase
|
||||
return Result.fail(nameOrError.error);
|
||||
}
|
||||
|
||||
const emailOrError = Email.create(dealerDTO.email);
|
||||
if (emailOrError.isFailure) {
|
||||
return Result.fail(emailOrError.error);
|
||||
}
|
||||
|
||||
const passwordOrError = Password.createFromPlainTextPassword(dealerDTO.password);
|
||||
if (passwordOrError.isFailure) {
|
||||
return Result.fail(passwordOrError.error);
|
||||
}
|
||||
|
||||
return Dealer.create(
|
||||
{
|
||||
name: nameOrError.object,
|
||||
email: emailOrError.object,
|
||||
password: passwordOrError.object,
|
||||
},
|
||||
dealerId
|
||||
);
|
||||
6
server/src/contexts/sales/application/Dealer/index.ts
Normal file
6
server/src/contexts/sales/application/Dealer/index.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export * from "./CreateDealer.useCase";
|
||||
export * from "./DeleteDealer.useCase";
|
||||
export * from "./GetDealer.useCase";
|
||||
export * from "./GetDealerByUser.useCase";
|
||||
export * from "./ListDealers.useCase";
|
||||
export * from "./UpdateDealer.useCase";
|
||||
@ -0,0 +1,161 @@
|
||||
import { IUseCase, IUseCaseError, UseCaseError } from "@/contexts/common/application";
|
||||
import { IRepositoryManager } from "@/contexts/common/domain";
|
||||
import { IInfrastructureError } from "@/contexts/common/infrastructure";
|
||||
import { ISequelizeAdapter } from "@/contexts/common/infrastructure/sequelize";
|
||||
import {
|
||||
Collection,
|
||||
Currency,
|
||||
Description,
|
||||
DomainError,
|
||||
ICreateQuote_Request_DTO,
|
||||
IDomainError,
|
||||
Language,
|
||||
Quantity,
|
||||
Result,
|
||||
UTCDateValue,
|
||||
UniqueID,
|
||||
UnitPrice,
|
||||
ensureIdIsValid,
|
||||
} from "@shared/contexts";
|
||||
import { IQuoteRepository, Quote, QuoteItem, QuoteStatus } from "../../domain";
|
||||
|
||||
export type CreateQuoteResponseOrError =
|
||||
| Result<never, IUseCaseError> // Misc errors (value objects)
|
||||
| Result<Quote, never>; // Success!
|
||||
|
||||
export class CreateQuoteUseCase
|
||||
implements IUseCase<ICreateQuote_Request_DTO, Promise<CreateQuoteResponseOrError>>
|
||||
{
|
||||
private _adapter: ISequelizeAdapter;
|
||||
private _repositoryManager: IRepositoryManager;
|
||||
|
||||
constructor(props: { adapter: ISequelizeAdapter; repositoryManager: IRepositoryManager }) {
|
||||
this._adapter = props.adapter;
|
||||
this._repositoryManager = props.repositoryManager;
|
||||
}
|
||||
|
||||
async execute(request: ICreateQuote_Request_DTO) {
|
||||
const { id } = request;
|
||||
|
||||
// Validaciones de datos
|
||||
const idOrError = ensureIdIsValid(id);
|
||||
if (idOrError.isFailure) {
|
||||
const message = idOrError.error.message; //`Quote ID ${quoteDTO.id} is not valid`;
|
||||
return Result.fail(
|
||||
UseCaseError.create(UseCaseError.INVALID_INPUT_DATA, message, [{ path: "id" }])
|
||||
);
|
||||
}
|
||||
|
||||
// Comprobar que no existe un usuario previo con esos datos
|
||||
const quoteRepository = this._getQuoteRepository();
|
||||
|
||||
const idExists = await quoteRepository().exists(idOrError.object);
|
||||
if (idExists) {
|
||||
const message = `Another quote with same ID exists`;
|
||||
return Result.fail(
|
||||
UseCaseError.create(UseCaseError.RESOURCE_ALREADY_EXITS, message, {
|
||||
path: "id",
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Crear quote
|
||||
const quoteOrError = this._tryCreateQuoteInstance(request, idOrError.object);
|
||||
|
||||
if (quoteOrError.isFailure) {
|
||||
const { error: domainError } = quoteOrError;
|
||||
let errorCode = "";
|
||||
let message = "";
|
||||
|
||||
switch (domainError.code) {
|
||||
case DomainError.INVALID_INPUT_DATA:
|
||||
errorCode = UseCaseError.INVALID_INPUT_DATA;
|
||||
message = "El usuario tiene algún dato erróneo.";
|
||||
break;
|
||||
|
||||
default:
|
||||
errorCode = UseCaseError.UNEXCEPTED_ERROR;
|
||||
message = domainError.message;
|
||||
break;
|
||||
}
|
||||
|
||||
return Result.fail(UseCaseError.create(errorCode, message, domainError));
|
||||
}
|
||||
|
||||
return this._saveQuote(quoteOrError.object);
|
||||
}
|
||||
|
||||
private async _saveQuote(quote: Quote) {
|
||||
// Guardar el contacto
|
||||
const transaction = this._adapter.startTransaction();
|
||||
const quoteRepository = this._getQuoteRepository();
|
||||
let quoteRepo: IQuoteRepository;
|
||||
|
||||
try {
|
||||
await transaction.complete(async (t) => {
|
||||
quoteRepo = quoteRepository({ transaction: t });
|
||||
await quoteRepo.create(quote);
|
||||
});
|
||||
|
||||
return Result.ok<Quote>(quote);
|
||||
} catch (error: unknown) {
|
||||
const _error = error as IInfrastructureError;
|
||||
return Result.fail(UseCaseError.create(UseCaseError.REPOSITORY_ERROR, _error.message));
|
||||
}
|
||||
}
|
||||
|
||||
private _tryCreateQuoteInstance(
|
||||
quoteDTO: ICreateQuote_Request_DTO,
|
||||
quoteId: UniqueID
|
||||
): 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);
|
||||
}
|
||||
|
||||
const languageOrError = Language.createFromCode(quoteDTO.language_code);
|
||||
if (languageOrError.isFailure) {
|
||||
return Result.fail(languageOrError.error);
|
||||
}
|
||||
|
||||
const currencyOrError = Currency.createFromCode(quoteDTO.currency_code);
|
||||
if (currencyOrError.isFailure) {
|
||||
return Result.fail(currencyOrError.error);
|
||||
}
|
||||
|
||||
const items = new Collection<QuoteItem>(
|
||||
quoteDTO.items?.map(
|
||||
(item) =>
|
||||
QuoteItem.create({
|
||||
description: Description.create(item.description).object,
|
||||
quantity: Quantity.create(item.quantity).object,
|
||||
unitPrice: UnitPrice.create({
|
||||
amount: item.unit_price.amount,
|
||||
currencyCode: item.unit_price.currency,
|
||||
precision: item.unit_price.precision,
|
||||
}).object,
|
||||
}).object
|
||||
)
|
||||
);
|
||||
|
||||
return Quote.create(
|
||||
{
|
||||
status: statusOrError.object,
|
||||
date: dateOrError.object,
|
||||
language: languageOrError.object,
|
||||
currency: currencyOrError.object,
|
||||
items,
|
||||
},
|
||||
quoteId
|
||||
);
|
||||
}
|
||||
|
||||
private _getQuoteRepository() {
|
||||
return this._repositoryManager.getRepository<IQuoteRepository>("Quote");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,55 @@
|
||||
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";
|
||||
|
||||
export interface IDeleteQuoteUseCaseRequest extends IUseCaseRequest {
|
||||
id: UniqueID;
|
||||
}
|
||||
|
||||
export type DeleteQuoteResponseOrError =
|
||||
| Result<never, IUseCaseError> // Misc errors (value objects)
|
||||
| Result<void, never>; // Success!
|
||||
|
||||
export class DeleteQuoteUseCase
|
||||
implements IUseCase<IDeleteQuoteUseCaseRequest, Promise<DeleteQuoteResponseOrError>>
|
||||
{
|
||||
private _adapter: ISequelizeAdapter;
|
||||
private _repositoryManager: IRepositoryManager;
|
||||
|
||||
constructor(props: { adapter: ISequelizeAdapter; repositoryManager: IRepositoryManager }) {
|
||||
this._adapter = props.adapter;
|
||||
this._repositoryManager = props.repositoryManager;
|
||||
}
|
||||
|
||||
async execute(request: IDeleteQuoteUseCaseRequest): Promise<DeleteQuoteResponseOrError> {
|
||||
const { id: QuoteId } = request;
|
||||
|
||||
const transaction = this._adapter.startTransaction();
|
||||
const QuoteRepoBuilder = this._getQuoteRepository();
|
||||
|
||||
try {
|
||||
await transaction.complete(async (t) => {
|
||||
const invoiceRepo = QuoteRepoBuilder({ transaction: t });
|
||||
await invoiceRepo.removeById(QuoteId);
|
||||
});
|
||||
|
||||
return Result.ok<void>();
|
||||
} catch (error: unknown) {
|
||||
//const _error = error as IInfrastructureError;
|
||||
return Result.fail(
|
||||
UseCaseError.create(UseCaseError.REPOSITORY_ERROR, "Error al eliminar el usuario")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private _getQuoteRepository() {
|
||||
return this._repositoryManager.getRepository<IQuoteRepository>("Quote");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,75 @@
|
||||
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/Quote";
|
||||
|
||||
export interface IGetQuoteUseCaseRequest extends IUseCaseRequest {
|
||||
id: UniqueID;
|
||||
}
|
||||
|
||||
export type GetQuoteResponseOrError =
|
||||
| Result<never, IUseCaseError> // Misc errors (value objects)
|
||||
| Result<Quote, never>; // Success!
|
||||
|
||||
export class GetQuoteUseCase
|
||||
implements IUseCase<IGetQuoteUseCaseRequest, Promise<GetQuoteResponseOrError>>
|
||||
{
|
||||
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: IGetQuoteUseCaseRequest): Promise<GetQuoteResponseOrError> {
|
||||
const { id } = request;
|
||||
|
||||
// Validación de datos
|
||||
// No hay en este caso
|
||||
|
||||
return await this.findQuote(id);
|
||||
}
|
||||
|
||||
private async findQuote(id: UniqueID) {
|
||||
const transaction = this._adapter.startTransaction();
|
||||
const QuoteRepoBuilder = this._getQuoteRepository();
|
||||
|
||||
let Quote: Quote | null = null;
|
||||
|
||||
try {
|
||||
await transaction.complete(async (t) => {
|
||||
const QuoteRepo = QuoteRepoBuilder({ transaction: t });
|
||||
Quote = await QuoteRepo.getById(id);
|
||||
});
|
||||
|
||||
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 el usuario", _error)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private _getQuoteRepository() {
|
||||
return this._repositoryManager.getRepository<IQuoteRepository>("Quote");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,73 @@
|
||||
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/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 el usuario", _error)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,59 @@
|
||||
import { IUseCase, IUseCaseError, UseCaseError } from "@/contexts/common/application/useCases";
|
||||
import { IRepositoryManager } from "@/contexts/common/domain";
|
||||
import { Collection, ICollection, IQueryCriteria, Result } from "@shared/contexts";
|
||||
|
||||
import { IInfrastructureError } from "@/contexts/common/infrastructure";
|
||||
import { ISequelizeAdapter } from "@/contexts/common/infrastructure/sequelize";
|
||||
import { Quote } from "../../domain";
|
||||
import { IQuoteRepository } from "../../domain/repository";
|
||||
|
||||
export interface IListQuotesParams {
|
||||
queryCriteria: IQueryCriteria;
|
||||
}
|
||||
|
||||
export type ListQuotesResult =
|
||||
| Result<never, IUseCaseError> // Misc errors (value objects)
|
||||
| Result<ICollection<Quote>, never>; // Success!
|
||||
|
||||
export class ListQuotesUseCase implements IUseCase<IListQuotesParams, Promise<ListQuotesResult>> {
|
||||
private _adapter: ISequelizeAdapter;
|
||||
private _repositoryManager: IRepositoryManager;
|
||||
|
||||
constructor(props: { adapter: ISequelizeAdapter; repositoryManager: IRepositoryManager }) {
|
||||
this._adapter = props.adapter;
|
||||
this._repositoryManager = props.repositoryManager;
|
||||
}
|
||||
|
||||
async execute(params: Partial<IListQuotesParams>): Promise<ListQuotesResult> {
|
||||
const { queryCriteria } = params;
|
||||
|
||||
return this.findQuotes(queryCriteria);
|
||||
}
|
||||
|
||||
private async findQuotes(queryCriteria) {
|
||||
const transaction = this._adapter.startTransaction();
|
||||
const QuoteRepoBuilder = this._getQuoteRepository();
|
||||
|
||||
let Quotes: ICollection<Quote> = new Collection();
|
||||
|
||||
try {
|
||||
await transaction.complete(async (t) => {
|
||||
Quotes = await QuoteRepoBuilder({ transaction: t }).findAll(queryCriteria);
|
||||
});
|
||||
return Result.ok(Quotes);
|
||||
} catch (error: unknown) {
|
||||
const _error = error as IInfrastructureError;
|
||||
return Result.fail(
|
||||
UseCaseError.create(
|
||||
UseCaseError.REPOSITORY_ERROR,
|
||||
"Error al listar las cotizaciones",
|
||||
_error
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private _getQuoteRepository() {
|
||||
return this._repositoryManager.getRepository<IQuoteRepository>("Quote");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,162 @@
|
||||
import {
|
||||
IUseCase,
|
||||
IUseCaseError,
|
||||
IUseCaseRequest,
|
||||
UseCaseError,
|
||||
} from "@/contexts/common/application";
|
||||
import { IRepositoryManager } from "@/contexts/common/domain";
|
||||
import { IInfrastructureError } from "@/contexts/common/infrastructure";
|
||||
import { ISequelizeAdapter } from "@/contexts/common/infrastructure/sequelize";
|
||||
import {
|
||||
Collection,
|
||||
Currency,
|
||||
Description,
|
||||
DomainError,
|
||||
IDomainError,
|
||||
Language,
|
||||
Quantity,
|
||||
Result,
|
||||
UTCDateValue,
|
||||
UniqueID,
|
||||
UnitPrice,
|
||||
} from "@shared/contexts";
|
||||
|
||||
import { IUpdateQuote_Request_DTO } from "@shared/contexts";
|
||||
import { IQuoteRepository, Quote, QuoteItem, QuoteStatus } from "../../domain";
|
||||
|
||||
export interface IUpdateQuoteUseCaseRequest extends IUseCaseRequest {
|
||||
id: UniqueID;
|
||||
QuoteDTO: IUpdateQuote_Request_DTO;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
constructor(props: { adapter: ISequelizeAdapter; repositoryManager: IRepositoryManager }) {
|
||||
this._adapter = props.adapter;
|
||||
this._repositoryManager = props.repositoryManager;
|
||||
}
|
||||
|
||||
async execute(request: IUpdateQuoteUseCaseRequest): Promise<UpdateQuoteResponseOrError> {
|
||||
const { id, QuoteDTO } = request;
|
||||
const QuoteRepository = this._getQuoteRepository();
|
||||
|
||||
// Comprobar que existe el Quote
|
||||
const idExists = await QuoteRepository().exists(id);
|
||||
if (!idExists) {
|
||||
const message = `Quote ID not found`;
|
||||
return Result.fail(
|
||||
UseCaseError.create(UseCaseError.NOT_FOUND_ERROR, message, {
|
||||
path: "id",
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Crear usuario
|
||||
const QuoteOrError = this._tryCreateQuoteInstance(QuoteDTO, id);
|
||||
|
||||
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 usuario tiene algún dato erróneo.";
|
||||
break;
|
||||
|
||||
default:
|
||||
errorCode = UseCaseError.UNEXCEPTED_ERROR;
|
||||
message = domainError.message;
|
||||
break;
|
||||
}
|
||||
|
||||
return Result.fail(UseCaseError.create(errorCode, message, domainError));
|
||||
}
|
||||
|
||||
return this._updateQuote(QuoteOrError.object);
|
||||
}
|
||||
|
||||
private async _updateQuote(Quote: Quote) {
|
||||
// Guardar el contacto
|
||||
const transaction = this._adapter.startTransaction();
|
||||
const QuoteRepository = this._getQuoteRepository();
|
||||
let QuoteRepo: IQuoteRepository;
|
||||
|
||||
try {
|
||||
await transaction.complete(async (t) => {
|
||||
QuoteRepo = QuoteRepository({ transaction: t });
|
||||
await QuoteRepo.update(Quote);
|
||||
});
|
||||
|
||||
return Result.ok<Quote>(Quote);
|
||||
} catch (error: unknown) {
|
||||
const _error = error as IInfrastructureError;
|
||||
return Result.fail(UseCaseError.create(UseCaseError.REPOSITORY_ERROR, _error.message));
|
||||
}
|
||||
}
|
||||
|
||||
private _tryCreateQuoteInstance(
|
||||
quoteDTO: IUpdateQuote_Request_DTO,
|
||||
quoteId: UniqueID
|
||||
): 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);
|
||||
}
|
||||
|
||||
const languageOrError = Language.createFromCode(quoteDTO.language_code);
|
||||
if (languageOrError.isFailure) {
|
||||
return Result.fail(languageOrError.error);
|
||||
}
|
||||
|
||||
const currencyOrError = Currency.createFromCode(quoteDTO.currency_code);
|
||||
if (currencyOrError.isFailure) {
|
||||
return Result.fail(currencyOrError.error);
|
||||
}
|
||||
|
||||
const items = new Collection<QuoteItem>(
|
||||
quoteDTO.items?.map(
|
||||
(item) =>
|
||||
QuoteItem.create({
|
||||
description: Description.create(item.description).object,
|
||||
quantity: Quantity.create(item.quantity).object,
|
||||
unitPrice: UnitPrice.create({
|
||||
amount: item.unit_price.amount,
|
||||
currencyCode: item.unit_price.currency,
|
||||
precision: item.unit_price.precision,
|
||||
}).object,
|
||||
}).object
|
||||
)
|
||||
);
|
||||
|
||||
return Quote.create(
|
||||
{
|
||||
status: statusOrError.object,
|
||||
date: dateOrError.object,
|
||||
language: languageOrError.object,
|
||||
currency: currencyOrError.object,
|
||||
items,
|
||||
},
|
||||
quoteId
|
||||
);
|
||||
}
|
||||
|
||||
private _getQuoteRepository() {
|
||||
return this._repositoryManager.getRepository<IQuoteRepository>("Quote");
|
||||
}
|
||||
}
|
||||
6
server/src/contexts/sales/application/Quote/index.ts
Normal file
6
server/src/contexts/sales/application/Quote/index.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export * from "./CreateQuote.useCase";
|
||||
export * from "./DeleteQuote.useCase";
|
||||
export * from "./GetQuote.useCase";
|
||||
export * from "./GetQuoteByUser.useCase";
|
||||
export * from "./ListQuotes.useCase";
|
||||
export * from "./UpdateQuote.useCase";
|
||||
8
server/src/contexts/sales/application/Quote/renamer.sh
Executable file
8
server/src/contexts/sales/application/Quote/renamer.sh
Executable file
@ -0,0 +1,8 @@
|
||||
|
||||
# Cambia "Quote" por "Quote" en todos los archivos y directorios de forma recursiva
|
||||
find . -depth -name "*Quote*" | while read file; do
|
||||
# Obtén el nuevo nombre con 'Reseller' reemplazado por 'Quote'
|
||||
newfile=$(echo "$file" | sed 's/Quote/Quote/g')
|
||||
# Renombra el archivo/directorio
|
||||
mv "$file" "$newfile"
|
||||
done
|
||||
@ -1,6 +1,2 @@
|
||||
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 "./Dealer";
|
||||
export * from "./Quote";
|
||||
|
||||
74
server/src/contexts/sales/domain/entities/Quote.ts
Normal file
74
server/src/contexts/sales/domain/entities/Quote.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import {
|
||||
AggregateRoot,
|
||||
Currency,
|
||||
ICollection,
|
||||
IDomainError,
|
||||
Language,
|
||||
Result,
|
||||
UTCDateValue,
|
||||
UniqueID,
|
||||
} from "@shared/contexts";
|
||||
import { QuoteItem } from "./QuoteItem";
|
||||
import { QuoteStatus } from "./QuoteStatus";
|
||||
|
||||
export interface IQuoteProps {
|
||||
status: QuoteStatus;
|
||||
date: UTCDateValue;
|
||||
language: Language;
|
||||
currency: Currency;
|
||||
items: ICollection<QuoteItem>;
|
||||
}
|
||||
|
||||
export interface IQuote {
|
||||
id: UniqueID;
|
||||
|
||||
status: QuoteStatus;
|
||||
date: UTCDateValue;
|
||||
language: Language;
|
||||
currency: Currency;
|
||||
items: ICollection<QuoteItem>;
|
||||
}
|
||||
|
||||
export class Quote extends AggregateRoot<IQuoteProps> implements IQuote {
|
||||
public static create(props: IQuoteProps, id?: UniqueID): Result<Quote, IDomainError> {
|
||||
const quote = new Quote(props, id);
|
||||
|
||||
// Reglas de negocio / validaciones
|
||||
// ...
|
||||
// ...
|
||||
|
||||
return Result.ok<Quote>(quote);
|
||||
}
|
||||
|
||||
protected _items: ICollection<QuoteItem>;
|
||||
|
||||
protected constructor(props: IQuoteProps, id?: UniqueID) {
|
||||
super(props, id);
|
||||
|
||||
this._items = props.items;
|
||||
}
|
||||
|
||||
get id(): UniqueID {
|
||||
return this._id;
|
||||
}
|
||||
|
||||
get date() {
|
||||
return this.props.date;
|
||||
}
|
||||
|
||||
get status() {
|
||||
return this.props.status;
|
||||
}
|
||||
|
||||
get language() {
|
||||
return this.props.language;
|
||||
}
|
||||
|
||||
get currency() {
|
||||
return this.props.currency;
|
||||
}
|
||||
|
||||
get items() {
|
||||
return this._items;
|
||||
}
|
||||
}
|
||||
40
server/src/contexts/sales/domain/entities/QuoteItem.ts
Normal file
40
server/src/contexts/sales/domain/entities/QuoteItem.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import {
|
||||
Description,
|
||||
Entity,
|
||||
IDomainError,
|
||||
IEntityProps,
|
||||
MoneyValue,
|
||||
Quantity,
|
||||
Result,
|
||||
UniqueID,
|
||||
} from "@shared/contexts";
|
||||
|
||||
export interface IQuoteItemProps extends IEntityProps {
|
||||
description: Description; // Descripción del artículo o servicio
|
||||
quantity: Quantity; // Cantidad de unidades
|
||||
unitPrice: MoneyValue; // Precio unitario en la moneda de la factura
|
||||
}
|
||||
|
||||
export interface IQuoteItem {
|
||||
description: Description;
|
||||
quantity: Quantity;
|
||||
unitPrice: MoneyValue;
|
||||
}
|
||||
|
||||
export class QuoteItem extends Entity<IQuoteItemProps> implements IQuoteItem {
|
||||
public static create(props: IQuoteItemProps, id?: UniqueID): Result<QuoteItem, IDomainError> {
|
||||
return Result.ok(new QuoteItem(props, id));
|
||||
}
|
||||
|
||||
get description(): Description {
|
||||
return this.props.description;
|
||||
}
|
||||
|
||||
get quantity(): Quantity {
|
||||
return this.props.quantity;
|
||||
}
|
||||
|
||||
get unitPrice(): MoneyValue {
|
||||
return this.props.unitPrice;
|
||||
}
|
||||
}
|
||||
85
server/src/contexts/sales/domain/entities/QuoteStatus.ts
Normal file
85
server/src/contexts/sales/domain/entities/QuoteStatus.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import {
|
||||
DomainError,
|
||||
IValueObjectOptions,
|
||||
Result,
|
||||
RuleValidator,
|
||||
ValueObject,
|
||||
handleDomainError,
|
||||
} from "@shared/contexts";
|
||||
import Joi from "joi";
|
||||
|
||||
export enum QUOTE_STATUS {
|
||||
DRAFT = "draft",
|
||||
EMITTED = "emitted",
|
||||
SENT = "sent",
|
||||
REJECTED = "rejected",
|
||||
}
|
||||
export interface IQuoteStatusOptions extends IValueObjectOptions {}
|
||||
|
||||
export class QuoteStatus extends ValueObject<string> {
|
||||
public static readonly QUOTE_STATUS = QUOTE_STATUS;
|
||||
|
||||
protected static validate(value: string, options: IValueObjectOptions) {
|
||||
const rule = Joi.string()
|
||||
.valid(QUOTE_STATUS.DRAFT, QUOTE_STATUS.EMITTED, QUOTE_STATUS.SENT, QUOTE_STATUS.REJECTED)
|
||||
.label(options.label ? options.label : "status");
|
||||
|
||||
return RuleValidator.validate<string>(rule, value);
|
||||
}
|
||||
|
||||
private static sanitize(status: string): string {
|
||||
return String(status).trim().toLowerCase();
|
||||
}
|
||||
|
||||
public static createDraft(): QuoteStatus {
|
||||
return new QuoteStatus(QUOTE_STATUS.DRAFT);
|
||||
}
|
||||
|
||||
public static createEmitted(): QuoteStatus {
|
||||
return new QuoteStatus(QUOTE_STATUS.EMITTED);
|
||||
}
|
||||
|
||||
public static createSent(): QuoteStatus {
|
||||
return new QuoteStatus(QUOTE_STATUS.SENT);
|
||||
}
|
||||
|
||||
public static createRejected(): QuoteStatus {
|
||||
return new QuoteStatus(QUOTE_STATUS.REJECTED);
|
||||
}
|
||||
|
||||
public static create(status: string, options: IQuoteStatusOptions = {}) {
|
||||
const _options = {
|
||||
label: "status",
|
||||
...options,
|
||||
};
|
||||
const validationResult = QuoteStatus.validate(status, _options);
|
||||
|
||||
if (validationResult.isFailure) {
|
||||
return Result.fail(
|
||||
handleDomainError(DomainError.INVALID_INPUT_DATA, validationResult.error.message, _options)
|
||||
);
|
||||
}
|
||||
|
||||
return Result.ok(new QuoteStatus(QuoteStatus.sanitize(validationResult.object)));
|
||||
}
|
||||
|
||||
get value(): string {
|
||||
return String(this.props);
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return String(this.props);
|
||||
}
|
||||
|
||||
public toPrimitive(): string {
|
||||
return this.toString();
|
||||
}
|
||||
|
||||
public isDraft(): boolean {
|
||||
return this.equals(QuoteStatus.createDraft());
|
||||
}
|
||||
|
||||
public isEmpty(): boolean {
|
||||
return this.props === "undefined";
|
||||
}
|
||||
}
|
||||
@ -1 +1,4 @@
|
||||
export * from "./Dealer";
|
||||
export * from "./Quote";
|
||||
export * from "./QuoteItem";
|
||||
export * from "./QuoteStatus";
|
||||
|
||||
@ -0,0 +1,15 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
import { IRepository } from "@/contexts/common/domain/repositories";
|
||||
import { ICollection, IQueryCriteria, UniqueID } from "@shared/contexts";
|
||||
import { Quote } from "../entities";
|
||||
|
||||
export interface IQuoteRepository extends IRepository<Quote> {
|
||||
exists(id: UniqueID): Promise<boolean>;
|
||||
create(quote: Quote): Promise<void>;
|
||||
update(quote: Quote): Promise<void>;
|
||||
|
||||
getById(id: UniqueID): Promise<Quote | null>;
|
||||
findAll(queryCriteria?: IQueryCriteria): Promise<ICollection<Quote>>;
|
||||
|
||||
removeById(id: UniqueID): Promise<void>;
|
||||
}
|
||||
@ -1 +1,2 @@
|
||||
export * from "./DealerRepository.interface";
|
||||
export * from "./QuoteRepository.interface";
|
||||
|
||||
86
server/src/contexts/sales/infrastructure/Quote.repository.ts
Normal file
86
server/src/contexts/sales/infrastructure/Quote.repository.ts
Normal file
@ -0,0 +1,86 @@
|
||||
import { ISequelizeAdapter, SequelizeRepository } from "@/contexts/common/infrastructure/sequelize";
|
||||
import { ICollection, IQueryCriteria, UniqueID } from "@shared/contexts";
|
||||
import { Transaction } from "sequelize";
|
||||
|
||||
import { IQuoteRepository } from "../domain";
|
||||
import { Quote } from "../domain/entities";
|
||||
import { ISalesContext } from "./Sales.context";
|
||||
import { IQuoteMapper, createQuoteMapper } from "./mappers/quote.mapper";
|
||||
|
||||
export type QueryParams = {
|
||||
pagination: Record<string, any>;
|
||||
filters: Record<string, any>;
|
||||
};
|
||||
|
||||
export class QuoteRepository extends SequelizeRepository<Quote> implements IQuoteRepository {
|
||||
protected mapper: IQuoteMapper;
|
||||
|
||||
public constructor(props: {
|
||||
mapper: IQuoteMapper;
|
||||
adapter: ISequelizeAdapter;
|
||||
transaction: Transaction;
|
||||
}) {
|
||||
const { adapter, mapper, transaction } = props;
|
||||
super({ adapter, transaction });
|
||||
this.mapper = mapper;
|
||||
}
|
||||
|
||||
public async exists(id: UniqueID): Promise<boolean> {
|
||||
return this._exists("Quote_Model", "id", id.toPrimitive());
|
||||
}
|
||||
|
||||
public async create(user: Quote): Promise<void> {
|
||||
const userData = this.mapper.mapToPersistence(user);
|
||||
await this._save("Quote_Model", user.id, userData);
|
||||
}
|
||||
|
||||
public async update(user: Quote): Promise<void> {
|
||||
const userData = this.mapper.mapToPersistence(user);
|
||||
|
||||
// borrando y luego creando
|
||||
// await this.removeById(user.id, true);
|
||||
await this._save("Quote_Model", user.id, userData, {});
|
||||
}
|
||||
|
||||
public async getById(id: UniqueID): Promise<Quote | null> {
|
||||
const rawQuote: any = await this._getById("Quote_Model", id);
|
||||
|
||||
if (!rawQuote === true) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.mapper.mapToDomain(rawQuote);
|
||||
}
|
||||
|
||||
public async findAll(queryCriteria?: IQueryCriteria): Promise<ICollection<any>> {
|
||||
const { rows, count } = await this._findAll(
|
||||
"Quote_Model",
|
||||
queryCriteria
|
||||
/*{
|
||||
include: [], // esto es para quitar las asociaciones al hacer la consulta
|
||||
}*/
|
||||
);
|
||||
|
||||
return this.mapper.mapArrayAndCountToDomain(rows, count);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
public async removeById(id: UniqueID, force: boolean = false): Promise<void> {
|
||||
return this._removeById("Quote_Model", id);
|
||||
}
|
||||
}
|
||||
|
||||
export const registerQuoteRepository = (context: ISalesContext) => {
|
||||
const adapter = context.adapter;
|
||||
const repoManager = context.repositoryManager;
|
||||
|
||||
repoManager.registerRepository("Quote", (params = { transaction: null }) => {
|
||||
const { transaction } = params;
|
||||
|
||||
return new QuoteRepository({
|
||||
transaction,
|
||||
adapter,
|
||||
mapper: createQuoteMapper(context),
|
||||
});
|
||||
});
|
||||
};
|
||||
@ -0,0 +1,119 @@
|
||||
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 { CreateQuoteUseCase } from "@/contexts/sales/application";
|
||||
import { Quote } from "@/contexts/sales/domain/entities";
|
||||
import {
|
||||
ICreateQuote_Request_DTO,
|
||||
ICreateQuote_Response_DTO,
|
||||
ensureCreateQuote_Request_DTOIsValid,
|
||||
} from "@shared/contexts";
|
||||
import { ISalesContext } from "../../../../Sales.context";
|
||||
import { ICreateQuotePresenter } from "./presenter";
|
||||
|
||||
export class CreateQuoteController extends ExpressController {
|
||||
private useCase: CreateQuoteUseCase;
|
||||
private presenter: ICreateQuotePresenter;
|
||||
private context: ISalesContext;
|
||||
|
||||
constructor(
|
||||
props: {
|
||||
useCase: CreateQuoteUseCase;
|
||||
presenter: ICreateQuotePresenter;
|
||||
},
|
||||
context: ISalesContext
|
||||
) {
|
||||
super();
|
||||
|
||||
const { useCase, presenter } = props;
|
||||
this.useCase = useCase;
|
||||
this.presenter = presenter;
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
async executeImpl() {
|
||||
try {
|
||||
const quoteDTO: ICreateQuote_Request_DTO = this.req.body;
|
||||
|
||||
// Validaciones de DTO
|
||||
const quoteDTOOrError = ensureCreateQuote_Request_DTOIsValid(quoteDTO);
|
||||
|
||||
if (quoteDTOOrError.isFailure) {
|
||||
const errorMessage = "Quote data not valid";
|
||||
const infraError = InfrastructureError.create(
|
||||
InfrastructureError.INVALID_INPUT_DATA,
|
||||
errorMessage,
|
||||
quoteDTOOrError.error
|
||||
);
|
||||
return this.invalidInputError(errorMessage, infraError);
|
||||
}
|
||||
|
||||
// Llamar al caso de uso
|
||||
const result = await this.useCase.execute(quoteDTO);
|
||||
|
||||
if (result.isFailure) {
|
||||
return this._handleExecuteError(result.error);
|
||||
}
|
||||
|
||||
const quote = <Quote>result.object;
|
||||
|
||||
return this.created<ICreateQuote_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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
import { CreateQuoteUseCase } from "@/contexts/sales/application";
|
||||
import Express from "express";
|
||||
import { registerQuoteRepository } from "../../../../Quote.repository";
|
||||
import { ISalesContext } from "../../../../Sales.context";
|
||||
import { CreateQuoteController } from "./CreateQuote.controller";
|
||||
import { CreateQuotePresenter } from "./presenter";
|
||||
|
||||
export const createQuoteController = (
|
||||
req: Express.Request,
|
||||
res: Express.Response,
|
||||
next: Express.NextFunction
|
||||
) => {
|
||||
const context: ISalesContext = res.locals.context;
|
||||
|
||||
registerQuoteRepository(context);
|
||||
return new CreateQuoteController(
|
||||
{
|
||||
useCase: new CreateQuoteUseCase(context),
|
||||
presenter: CreateQuotePresenter,
|
||||
},
|
||||
context
|
||||
).execute(req, res, next);
|
||||
};
|
||||
@ -0,0 +1,56 @@
|
||||
import { ICollection, ICreateQuote_Response_DTO } from "@shared/contexts";
|
||||
import { Quote, QuoteItem } from "../../../../../../domain";
|
||||
import { ISalesContext } from "../../../../../Sales.context";
|
||||
|
||||
export interface ICreateQuotePresenter {
|
||||
map: (quote: Quote, context: ISalesContext) => ICreateQuote_Response_DTO;
|
||||
}
|
||||
|
||||
export const CreateQuotePresenter: ICreateQuotePresenter = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
map: (quote: Quote, context: ISalesContext): ICreateQuote_Response_DTO => {
|
||||
return {
|
||||
id: quote.id.toString(),
|
||||
status: quote.status.toString(),
|
||||
date: quote.date.toString(),
|
||||
language_code: quote.date.toString(),
|
||||
currency_code: quote.currency.toString(),
|
||||
subtotal: {
|
||||
amount: 0,
|
||||
precision: 2,
|
||||
currency: "EUR",
|
||||
},
|
||||
total: {
|
||||
amount: 0,
|
||||
precision: 2,
|
||||
currency: "EUR",
|
||||
},
|
||||
items: quoteItemPresenter(quote.items, context),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const quoteItemPresenter = (items: ICollection<QuoteItem>, context: ISalesContext) =>
|
||||
items.totalCount > 0
|
||||
? items.items.map((item: QuoteItem) => ({
|
||||
description: item.description.toString(),
|
||||
quantity: item.quantity.toString(),
|
||||
unit_measure: "",
|
||||
unit_price: {
|
||||
amount: 0,
|
||||
precision: 2,
|
||||
currency: "EUR",
|
||||
},
|
||||
subtotal: {
|
||||
amount: 0,
|
||||
precision: 2,
|
||||
currency: "EUR",
|
||||
},
|
||||
total: {
|
||||
amount: 0,
|
||||
precision: 2,
|
||||
currency: "EUR",
|
||||
},
|
||||
}))
|
||||
: [];
|
||||
@ -0,0 +1 @@
|
||||
export * from "./CreateQuote.presenter";
|
||||
@ -0,0 +1,84 @@
|
||||
import { IUseCaseError, UseCaseError } from "@/contexts/common/application/useCases";
|
||||
import { IServerError } from "@/contexts/common/domain/errors";
|
||||
import { IInfrastructureError, InfrastructureError } from "@/contexts/common/infrastructure";
|
||||
import { ExpressController } from "@/contexts/common/infrastructure/express";
|
||||
import { DeleteQuoteUseCase } from "@/contexts/sales/application";
|
||||
import { ensureIdIsValid } from "@shared/contexts";
|
||||
import { ISalesContext } from "../../../../Sales.context";
|
||||
|
||||
export class DeleteQuoteController extends ExpressController {
|
||||
private useCase: DeleteQuoteUseCase;
|
||||
private context: ISalesContext;
|
||||
|
||||
constructor(props: { useCase: DeleteQuoteUseCase }, context: ISalesContext) {
|
||||
super();
|
||||
|
||||
const { useCase } = props;
|
||||
this.useCase = useCase;
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
async executeImpl(): Promise<any> {
|
||||
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({
|
||||
id: quoteIdOrError.object,
|
||||
});
|
||||
|
||||
if (result.isFailure) {
|
||||
return this._handleExecuteError(result.error);
|
||||
}
|
||||
return this.noContent();
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
import { DeleteQuoteUseCase } from "@/contexts/sales/application";
|
||||
import Express from "express";
|
||||
import { registerQuoteRepository } from "../../../../Quote.repository";
|
||||
import { ISalesContext } from "../../../../Sales.context";
|
||||
import { DeleteQuoteController } from "./DeleteQuote.controller";
|
||||
|
||||
export const deleteQuoteController = (
|
||||
req: Express.Request,
|
||||
res: Express.Response,
|
||||
next: Express.NextFunction
|
||||
) => {
|
||||
const context: ISalesContext = res.locals.context;
|
||||
|
||||
registerQuoteRepository(context);
|
||||
return new DeleteQuoteController(
|
||||
{
|
||||
useCase: new DeleteQuoteUseCase(context),
|
||||
},
|
||||
context
|
||||
).execute(req, res, next);
|
||||
};
|
||||
@ -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 { IGetQuote_Response_DTO, ensureIdIsValid } from "@shared/contexts";
|
||||
import { ISalesContext } from "../../../../Sales.context";
|
||||
import { IGetQuotePresenter } from "./presenter/GetQuote.presenter";
|
||||
|
||||
export class GetQuoteController extends ExpressController {
|
||||
private useCase: GetQuoteUseCase;
|
||||
private presenter: IGetQuotePresenter;
|
||||
private context: ISalesContext;
|
||||
|
||||
constructor(
|
||||
props: {
|
||||
useCase: GetQuoteUseCase;
|
||||
presenter: IGetQuotePresenter;
|
||||
},
|
||||
context: ISalesContext
|
||||
) {
|
||||
super();
|
||||
|
||||
const { useCase, presenter } = props;
|
||||
this.useCase = useCase;
|
||||
this.presenter = 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.ok<IGetQuote_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.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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 { GetQuoteController } from "./GetQuote.controller";
|
||||
import { GetQuotePresenter } from "./presenter";
|
||||
|
||||
export const getQuoteController = (
|
||||
req: Express.Request,
|
||||
res: Express.Response,
|
||||
next: Express.NextFunction
|
||||
) => {
|
||||
const context: ISalesContext = res.locals.context;
|
||||
|
||||
registerQuoteRepository(context);
|
||||
|
||||
return new GetQuoteController(
|
||||
{
|
||||
useCase: new GetQuoteUseCase(context),
|
||||
presenter: GetQuotePresenter,
|
||||
},
|
||||
context
|
||||
).execute(req, res, next);
|
||||
};
|
||||
@ -0,0 +1,56 @@
|
||||
import { ICollection, IGetQuote_Response_DTO } from "@shared/contexts";
|
||||
import { Quote, QuoteItem } from "../../../../../../domain";
|
||||
import { ISalesContext } from "../../../../../Sales.context";
|
||||
|
||||
export interface IGetQuotePresenter {
|
||||
map: (quote: Quote, context: ISalesContext) => IGetQuote_Response_DTO;
|
||||
}
|
||||
|
||||
export const GetQuotePresenter: IGetQuotePresenter = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
map: (quote: Quote, context: ISalesContext): IGetQuote_Response_DTO => {
|
||||
return {
|
||||
id: quote.id.toString(),
|
||||
status: quote.status.toString(),
|
||||
date: quote.date.toString(),
|
||||
language_code: quote.date.toString(),
|
||||
currency_code: quote.currency.toString(),
|
||||
subtotal: {
|
||||
amount: 0,
|
||||
precision: 2,
|
||||
currency: "EUR",
|
||||
},
|
||||
total: {
|
||||
amount: 0,
|
||||
precision: 2,
|
||||
currency: "EUR",
|
||||
},
|
||||
items: quoteItemPresenter(quote.items, context),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const quoteItemPresenter = (items: ICollection<QuoteItem>, context: ISalesContext) =>
|
||||
items.totalCount > 0
|
||||
? items.items.map((item: QuoteItem) => ({
|
||||
description: item.description.toString(),
|
||||
quantity: item.quantity.toString(),
|
||||
unit_measure: "",
|
||||
unit_price: {
|
||||
amount: 0,
|
||||
precision: 2,
|
||||
currency: "EUR",
|
||||
},
|
||||
subtotal: {
|
||||
amount: 0,
|
||||
precision: 2,
|
||||
currency: "EUR",
|
||||
},
|
||||
total: {
|
||||
amount: 0,
|
||||
precision: 2,
|
||||
currency: "EUR",
|
||||
},
|
||||
}))
|
||||
: [];
|
||||
@ -0,0 +1 @@
|
||||
export * from "./GetQuote.presenter";
|
||||
@ -0,0 +1,5 @@
|
||||
export * from "./createQuote";
|
||||
export * from "./deleteQuote";
|
||||
export * from "./getQuote";
|
||||
export * from "./listQuotes";
|
||||
export * from "./updateQuote";
|
||||
@ -0,0 +1,84 @@
|
||||
import Joi from "joi";
|
||||
|
||||
import { QueryCriteriaService } from "@/contexts/common/application/services";
|
||||
import { IServerError } from "@/contexts/common/domain/errors";
|
||||
import { ExpressController } from "@/contexts/common/infrastructure/express";
|
||||
import { ListQuotesResult, ListQuotesUseCase } from "@/contexts/sales/application";
|
||||
import { Quote } from "@/contexts/sales/domain";
|
||||
import {
|
||||
ICollection,
|
||||
IListQuotes_Response_DTO,
|
||||
IListResponse_DTO,
|
||||
IQueryCriteria,
|
||||
Result,
|
||||
RuleValidator,
|
||||
} from "@shared/contexts";
|
||||
import { ISalesContext } from "../../../../Sales.context";
|
||||
import { IListQuotesPresenter } from "./presenter";
|
||||
|
||||
export class ListQuotesController extends ExpressController {
|
||||
private useCase: ListQuotesUseCase;
|
||||
private presenter: IListQuotesPresenter;
|
||||
private context: ISalesContext;
|
||||
|
||||
constructor(
|
||||
props: {
|
||||
useCase: ListQuotesUseCase;
|
||||
presenter: IListQuotesPresenter;
|
||||
},
|
||||
context: ISalesContext
|
||||
) {
|
||||
super();
|
||||
|
||||
const { useCase, presenter } = props;
|
||||
this.useCase = useCase;
|
||||
this.presenter = presenter;
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
protected validateQuery(query): Result<any> {
|
||||
const schema = Joi.object({
|
||||
page: Joi.number().optional(),
|
||||
limit: Joi.number().optional(),
|
||||
$sort_by: Joi.string().optional(),
|
||||
$filters: Joi.string().optional(),
|
||||
q: Joi.string().optional(),
|
||||
}).optional();
|
||||
|
||||
return RuleValidator.validate(schema, query);
|
||||
}
|
||||
|
||||
async executeImpl() {
|
||||
const queryOrError = this.validateQuery(this.req.query);
|
||||
if (queryOrError.isFailure) {
|
||||
return this.clientError(queryOrError.error.message);
|
||||
}
|
||||
|
||||
const queryParams = queryOrError.object;
|
||||
|
||||
try {
|
||||
const queryCriteria: IQueryCriteria = QueryCriteriaService.parse(queryParams);
|
||||
|
||||
console.log(queryCriteria);
|
||||
|
||||
const result: ListQuotesResult = await this.useCase.execute({
|
||||
queryCriteria,
|
||||
});
|
||||
|
||||
if (result.isFailure) {
|
||||
return this.clientError(result.error.message);
|
||||
}
|
||||
|
||||
const quotes = <ICollection<Quote>>result.object;
|
||||
|
||||
return this.ok<IListResponse_DTO<IListQuotes_Response_DTO>>(
|
||||
this.presenter.mapArray(quotes, this.context, {
|
||||
page: queryCriteria.pagination.offset,
|
||||
limit: queryCriteria.pagination.limit,
|
||||
})
|
||||
);
|
||||
} catch (e: unknown) {
|
||||
return this.fail(e as IServerError);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
import { ListQuotesUseCase } from "@/contexts/sales/application";
|
||||
import { registerQuoteRepository } from "@/contexts/sales/infrastructure/Quote.repository";
|
||||
import Express from "express";
|
||||
import { ISalesContext } from "../../../../Sales.context";
|
||||
import { ListQuotesController } from "./ListQuotes.controller";
|
||||
import { ListQuotesPresenter } from "./presenter";
|
||||
|
||||
export const listQuotesController = (
|
||||
req: Express.Request,
|
||||
res: Express.Response,
|
||||
next: Express.NextFunction
|
||||
) => {
|
||||
const context: ISalesContext = res.locals.context;
|
||||
|
||||
registerQuoteRepository(context);
|
||||
return new ListQuotesController(
|
||||
{
|
||||
useCase: new ListQuotesUseCase(context),
|
||||
presenter: ListQuotesPresenter,
|
||||
},
|
||||
context
|
||||
).execute(req, res, next);
|
||||
};
|
||||
@ -0,0 +1,56 @@
|
||||
import { ICollection, IUpdateQuote_Response_DTO } from "@shared/contexts";
|
||||
import { Quote, QuoteItem } from "../../../../../../domain";
|
||||
import { ISalesContext } from "../../../../../Sales.context";
|
||||
|
||||
export interface IListQuotesPresenter {
|
||||
map: (quote: Quote, context: ISalesContext) => IUpdateQuote_Response_DTO;
|
||||
}
|
||||
|
||||
export const ListQuotesPresenter: IListQuotesPresenter = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
map: (quote: Quote, context: ISalesContext): IUpdateQuote_Response_DTO => {
|
||||
return {
|
||||
id: quote.id.toString(),
|
||||
status: quote.status.toString(),
|
||||
date: quote.date.toString(),
|
||||
language_code: quote.date.toISO8601(),
|
||||
currency_code: quote.currency.toString(),
|
||||
subtotal: {
|
||||
amount: 0,
|
||||
precision: 2,
|
||||
currency: "EUR",
|
||||
},
|
||||
total: {
|
||||
amount: 0,
|
||||
precision: 2,
|
||||
currency: "EUR",
|
||||
},
|
||||
items: quoteItemPresenter(quote.items, context),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const quoteItemPresenter = (items: ICollection<QuoteItem>, context: ISalesContext) =>
|
||||
items.totalCount > 0
|
||||
? items.items.map((item: QuoteItem) => ({
|
||||
description: item.description.toString(),
|
||||
quantity: item.quantity.toString(),
|
||||
unit_measure: "",
|
||||
unit_price: {
|
||||
amount: 0,
|
||||
precision: 2,
|
||||
currency: "EUR",
|
||||
},
|
||||
subtotal: {
|
||||
amount: 0,
|
||||
precision: 2,
|
||||
currency: "EUR",
|
||||
},
|
||||
total: {
|
||||
amount: 0,
|
||||
precision: 2,
|
||||
currency: "EUR",
|
||||
},
|
||||
}))
|
||||
: [];
|
||||
@ -0,0 +1 @@
|
||||
export * from "./ListQuotes.presenter";
|
||||
@ -0,0 +1,138 @@
|
||||
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 { UpdateQuoteResponseOrError, UpdateQuoteUseCase } from "@/contexts/sales/application";
|
||||
import { Quote } from "@/contexts/sales/domain";
|
||||
import {
|
||||
IUpdateQuote_Request_DTO,
|
||||
IUpdateQuote_Response_DTO,
|
||||
ensureIdIsValid,
|
||||
ensureUpdateQuote_Request_DTOIsValid,
|
||||
} from "@shared/contexts";
|
||||
import { ISalesContext } from "../../../../Sales.context";
|
||||
import { IUpdateQuotePresenter } from "./presenter/UpdateQuote.presenter";
|
||||
|
||||
export class UpdateQuoteController extends ExpressController {
|
||||
private useCase: UpdateQuoteUseCase;
|
||||
private presenter: IUpdateQuotePresenter;
|
||||
private context: ISalesContext;
|
||||
|
||||
constructor(
|
||||
props: {
|
||||
useCase: UpdateQuoteUseCase;
|
||||
presenter: IUpdateQuotePresenter;
|
||||
},
|
||||
context: ISalesContext
|
||||
) {
|
||||
super();
|
||||
|
||||
const { useCase, presenter } = props;
|
||||
this.useCase = useCase;
|
||||
this.presenter = presenter;
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
async executeImpl() {
|
||||
try {
|
||||
const { quoteId } = this.req.params;
|
||||
const quoteDTO: IUpdateQuote_Request_DTO = this.req.body;
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Validar DTO de datos
|
||||
const quoteDTOOrError = ensureUpdateQuote_Request_DTOIsValid(quoteDTO);
|
||||
|
||||
if (quoteDTOOrError.isFailure) {
|
||||
const errorMessage = "Quote data not valid";
|
||||
const infraError = InfrastructureError.create(
|
||||
InfrastructureError.INVALID_INPUT_DATA,
|
||||
errorMessage,
|
||||
quoteDTOOrError.error
|
||||
);
|
||||
return this.invalidInputError(errorMessage, infraError);
|
||||
}
|
||||
|
||||
// Llamar al caso de uso
|
||||
const result: UpdateQuoteResponseOrError = await this.useCase.execute({
|
||||
id: quoteIdOrError.object,
|
||||
quoteDTO,
|
||||
});
|
||||
|
||||
if (result.isFailure) {
|
||||
return this._handleExecuteError(result.error);
|
||||
}
|
||||
|
||||
const quote = <Quote>result.object;
|
||||
|
||||
return this.ok<IUpdateQuote_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.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.INVALID_INPUT_DATA:
|
||||
errorMessage = "Quote data not valid";
|
||||
|
||||
infraError = InfrastructureError.create(
|
||||
InfrastructureError.INVALID_INPUT_DATA,
|
||||
"Datos del cliente a actulizar erróneos",
|
||||
error
|
||||
);
|
||||
return this.invalidInputError(errorMessage, infraError);
|
||||
break;
|
||||
|
||||
case UseCaseError.REPOSITORY_ERROR:
|
||||
errorMessage = "Error updating 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
import { UpdateQuoteUseCase } from "@/contexts/sales/application";
|
||||
import Express from "express";
|
||||
import { registerQuoteRepository } from "../../../../Quote.repository";
|
||||
import { ISalesContext } from "../../../../Sales.context";
|
||||
import { UpdateQuoteController } from "./UpdateQuote.controller";
|
||||
import { UpdateQuotePresenter } from "./presenter/UpdateQuote.presenter";
|
||||
|
||||
export const updateQuoteController = (
|
||||
req: Express.Request,
|
||||
res: Express.Response,
|
||||
next: Express.NextFunction
|
||||
) => {
|
||||
const context: ISalesContext = res.locals.context;
|
||||
|
||||
registerQuoteRepository(context);
|
||||
return new UpdateQuoteController(
|
||||
{
|
||||
useCase: new UpdateQuoteUseCase(context),
|
||||
presenter: UpdateQuotePresenter,
|
||||
},
|
||||
context
|
||||
).execute(req, res, next);
|
||||
};
|
||||
@ -0,0 +1,56 @@
|
||||
import { Quote, QuoteItem } from "@/contexts/sales/domain";
|
||||
import { ISalesContext } from "@/contexts/sales/infrastructure/Sales.context";
|
||||
import { ICollection, IUpdateQuote_Response_DTO } from "@shared/contexts";
|
||||
|
||||
export interface IUpdateQuotePresenter {
|
||||
map: (quote: Quote, context: ISalesContext) => IUpdateQuote_Response_DTO;
|
||||
}
|
||||
|
||||
export const UpdateQuotePresenter: IUpdateQuotePresenter = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
map: (quote: Quote, context: ISalesContext): IUpdateQuote_Response_DTO => {
|
||||
return {
|
||||
id: quote.id.toString(),
|
||||
status: quote.status.toString(),
|
||||
date: quote.date.toString(),
|
||||
language_code: quote.date.toString(),
|
||||
currency_code: quote.currency.toString(),
|
||||
subtotal: {
|
||||
amount: 0,
|
||||
precision: 2,
|
||||
currency: "EUR",
|
||||
},
|
||||
total: {
|
||||
amount: 0,
|
||||
precision: 2,
|
||||
currency: "EUR",
|
||||
},
|
||||
items: quoteItemPresenter(quote.items, context),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const quoteItemPresenter = (items: ICollection<QuoteItem>, context: ISalesContext) =>
|
||||
items.totalCount > 0
|
||||
? items.items.map((item: QuoteItem) => ({
|
||||
description: item.description.toString(),
|
||||
quantity: item.quantity.toString(),
|
||||
unit_measure: "",
|
||||
unit_price: {
|
||||
amount: 0,
|
||||
precision: 2,
|
||||
currency: "EUR",
|
||||
},
|
||||
subtotal: {
|
||||
amount: 0,
|
||||
precision: 2,
|
||||
currency: "EUR",
|
||||
},
|
||||
total: {
|
||||
amount: 0,
|
||||
precision: 2,
|
||||
currency: "EUR",
|
||||
},
|
||||
}))
|
||||
: [];
|
||||
@ -0,0 +1 @@
|
||||
export * from "./UpdateQuote.presenter";
|
||||
@ -3,13 +3,11 @@ import Express from "express";
|
||||
|
||||
export const quoteRoutes: Express.Router = Express.Router({ mergeParams: true });
|
||||
|
||||
//quoteRoutes.use(isUser, xxx);
|
||||
|
||||
/*quoteRoutes.get("/", listQuotesController);
|
||||
quoteRoutes.get("/:quoteId", getQuoteController);
|
||||
quoteRoutes.post("/", createQuoteController);
|
||||
quoteRoutes.put("/:quoteId", updateQuoteController);
|
||||
quoteRoutes.delete("/:quoteId", deleteQuoteController);*/
|
||||
/*quoteRoutes.get("/", isAdmin, listQuotesController);
|
||||
quoteRoutes.get("/:quoteId", isUser, getQuoteMiddleware, getQuoteController);
|
||||
quoteRoutes.post("/", isAdmin, createQuoteController);
|
||||
quoteRoutes.put("/:quoteId", isAdmin, updateQuoteController);
|
||||
quoteRoutes.delete("/:quoteId", isAdmin, deleteQuoteController);*/
|
||||
|
||||
quoteRoutes.get("/", isAdmin, (req, res) => {
|
||||
console.log(req.params);
|
||||
|
||||
@ -0,0 +1,71 @@
|
||||
import { Currency, Language, UTCDateValue, UniqueID } from "@shared/contexts";
|
||||
|
||||
import { ISequelizeMapper, SequelizeMapper } from "@/contexts/common/infrastructure";
|
||||
import { IQuoteProps, Quote } from "../../domain";
|
||||
import { QuoteStatus } from "../../domain/entities/QuoteStatus";
|
||||
import { ISalesContext } from "../Sales.context";
|
||||
import { QuoteCreationAttributes, Quote_Model } from "../sequelize";
|
||||
import { IQuoteItemMapper, createQuoteItemMapper } from "./quoteItem.mapper";
|
||||
|
||||
export interface IQuoteMapper
|
||||
extends ISequelizeMapper<Quote_Model, QuoteCreationAttributes, Quote> {}
|
||||
|
||||
export const createQuoteMapper = (context: ISalesContext): IQuoteMapper =>
|
||||
new QuoteMapper({
|
||||
context,
|
||||
quoteItemMapper: createQuoteItemMapper(context),
|
||||
});
|
||||
|
||||
class QuoteMapper
|
||||
extends SequelizeMapper<Quote_Model, QuoteCreationAttributes, Quote>
|
||||
implements IQuoteMapper
|
||||
{
|
||||
public constructor(props: { quoteItemMapper: IQuoteItemMapper; context: ISalesContext }) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
protected toDomainMappingImpl(source: Quote_Model): Quote {
|
||||
const id = this.mapsValue(source, "id", UniqueID.create);
|
||||
|
||||
const items = (this.props.quoteItemMapper as IQuoteItemMapper).mapArrayToDomain(source.items, {
|
||||
sourceParent: source,
|
||||
});
|
||||
|
||||
const props: IQuoteProps = {
|
||||
status: this.mapsValue(source, "status", QuoteStatus.create),
|
||||
date: this.mapsValue(source, "issue_date", UTCDateValue.create),
|
||||
currency: this.mapsValue(source, "quote_currency", Currency.createFromCode),
|
||||
language: this.mapsValue(source, "quote_language", Language.createFromCode),
|
||||
|
||||
items,
|
||||
};
|
||||
|
||||
const quoteOrError = Quote.create(props, id);
|
||||
|
||||
if (quoteOrError.isFailure) {
|
||||
throw quoteOrError.error;
|
||||
}
|
||||
|
||||
return quoteOrError.object;
|
||||
}
|
||||
|
||||
protected toPersistenceMappingImpl(source: Quote) {
|
||||
const items = (this.props.quoteItemMapper as IQuoteItemMapper).mapCollectionToPersistence(
|
||||
source.items,
|
||||
{ sourceParent: source }
|
||||
);
|
||||
|
||||
const quote: QuoteCreationAttributes = {
|
||||
id: source.id.toPrimitive(),
|
||||
status: source.status.toPrimitive(),
|
||||
date: source.date.toPrimitive(),
|
||||
currency_code: source.currency.toPrimitive(),
|
||||
language_code: source.language.toPrimitive(),
|
||||
subtotal: 0,
|
||||
total: 0,
|
||||
items,
|
||||
};
|
||||
|
||||
return quote;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,65 @@
|
||||
import { ISequelizeMapper, SequelizeMapper } from "@/contexts/common/infrastructure";
|
||||
import { Description, Quantity, UniqueID, UnitPrice } from "@shared/contexts";
|
||||
import { IQuoteItemProps, Quote, QuoteItem } from "../../domain";
|
||||
import { ISalesContext } from "../Sales.context";
|
||||
import { Quote_Model } from "../sequelize";
|
||||
import { QuoteItemCreationAttributes, QuoteItem_Model } from "../sequelize/quoteItem.model";
|
||||
|
||||
export interface IQuoteItemMapper
|
||||
extends ISequelizeMapper<QuoteItem_Model, QuoteItemCreationAttributes, QuoteItem> {}
|
||||
|
||||
export const createQuoteItemMapper = (context: ISalesContext): IQuoteItemMapper =>
|
||||
new QuoteItemMapper({ context });
|
||||
|
||||
class QuoteItemMapper
|
||||
extends SequelizeMapper<QuoteItem_Model, QuoteItemCreationAttributes, QuoteItem>
|
||||
implements IQuoteItemMapper
|
||||
{
|
||||
protected toDomainMappingImpl(
|
||||
source: QuoteItem_Model,
|
||||
params: { sourceParent: Quote_Model }
|
||||
): QuoteItem {
|
||||
const { sourceParent } = params;
|
||||
const id = this.mapsValue(source, "item_id", UniqueID.create);
|
||||
|
||||
const props: IQuoteItemProps = {
|
||||
description: this.mapsValue(source, "description", Description.create),
|
||||
quantity: this.mapsValue(source, "quantity", Quantity.create),
|
||||
unitPrice: this.mapsValue(source, "unit_price", (unit_price) =>
|
||||
UnitPrice.create({
|
||||
amount: unit_price,
|
||||
currencyCode: sourceParent.currency_code,
|
||||
precision: 4,
|
||||
})
|
||||
),
|
||||
};
|
||||
|
||||
const quoteItemOrError = QuoteItem.create(props, id);
|
||||
|
||||
if (quoteItemOrError.isFailure) {
|
||||
throw quoteItemOrError.error;
|
||||
}
|
||||
|
||||
return quoteItemOrError.object;
|
||||
}
|
||||
|
||||
protected toPersistenceMappingImpl(
|
||||
source: QuoteItem,
|
||||
params: { index: number; sourceParent: Quote }
|
||||
): QuoteItemCreationAttributes {
|
||||
const { index, sourceParent } = params;
|
||||
|
||||
return {
|
||||
quote_id: sourceParent.id.toPrimitive(),
|
||||
position: index,
|
||||
item_id: "", //article_id: source.id.toPrimitive(),
|
||||
description: source.description.toPrimitive(),
|
||||
quantity: source.quantity.toPrimitive(),
|
||||
unit_price: source.unitPrice.toPrimitive(),
|
||||
subtotal: 0,
|
||||
total: 0,
|
||||
//subtotal: source.calculateSubtotal().toPrimitive(),
|
||||
//total: source.calculateTotal().toPrimitive(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -6,6 +6,7 @@ import {
|
||||
Op,
|
||||
Sequelize,
|
||||
} from "sequelize";
|
||||
import { Quote_Model } from "./quote.model";
|
||||
|
||||
export enum DealerStatus {
|
||||
ACTIVE = "active",
|
||||
@ -32,6 +33,12 @@ export class Dealer_Model extends Model<
|
||||
foreignKey: "dealer_id",
|
||||
onDelete: "RESTRICT",
|
||||
});
|
||||
|
||||
Dealer_Model.hasMany(Quote_Model, {
|
||||
as: "quotes",
|
||||
foreignKey: "dealer_id",
|
||||
onDelete: "RESTRICT",
|
||||
});
|
||||
}
|
||||
|
||||
declare id: string;
|
||||
|
||||
@ -1 +1,2 @@
|
||||
export * from "./dealer.model";
|
||||
export * from "./quote.model";
|
||||
|
||||
@ -0,0 +1,103 @@
|
||||
import {
|
||||
CreationOptional,
|
||||
DataTypes,
|
||||
InferAttributes,
|
||||
InferCreationAttributes,
|
||||
Model,
|
||||
NonAttribute,
|
||||
Sequelize,
|
||||
} from "sequelize";
|
||||
import { QuoteItem_Model } from "./quoteItem.model";
|
||||
|
||||
export type QuoteCreationAttributes = InferCreationAttributes<Quote_Model, { omit: "items" }>;
|
||||
|
||||
export class Quote_Model extends Model<
|
||||
InferAttributes<Quote_Model, { omit: "items" }>,
|
||||
InferCreationAttributes<Quote_Model, { omit: "items" }>
|
||||
> {
|
||||
static associate(connection: Sequelize) {
|
||||
const { Quote_Model, QuoteItem_Model, Dealer_Model } = connection.models;
|
||||
|
||||
Quote_Model.hasMany(QuoteItem_Model, {
|
||||
as: "items",
|
||||
foreignKey: "quote_id",
|
||||
onDelete: "CASCADE",
|
||||
});
|
||||
|
||||
Quote_Model.belongsTo(Dealer_Model, {
|
||||
as: "dealer",
|
||||
foreignKey: "dealer_id",
|
||||
onDelete: "RESTRICT",
|
||||
});
|
||||
}
|
||||
|
||||
declare id: string;
|
||||
declare status: string;
|
||||
declare date: CreationOptional<string>;
|
||||
declare language_code: string;
|
||||
declare currency_code: string;
|
||||
declare subtotal: number;
|
||||
declare total: number;
|
||||
|
||||
declare items: NonAttribute<QuoteItem_Model[]>;
|
||||
}
|
||||
|
||||
export default (sequelize: Sequelize) => {
|
||||
Quote_Model.init(
|
||||
{
|
||||
id: {
|
||||
type: new DataTypes.UUID(),
|
||||
primaryKey: true,
|
||||
},
|
||||
|
||||
status: {
|
||||
type: new DataTypes.STRING(),
|
||||
allowNull: false,
|
||||
},
|
||||
|
||||
date: {
|
||||
type: new DataTypes.DATE(),
|
||||
allowNull: false,
|
||||
},
|
||||
|
||||
language_code: {
|
||||
type: new DataTypes.STRING(),
|
||||
allowNull: false,
|
||||
},
|
||||
|
||||
currency_code: {
|
||||
type: new DataTypes.STRING(),
|
||||
allowNull: false,
|
||||
},
|
||||
|
||||
subtotal: {
|
||||
type: new DataTypes.BIGINT(),
|
||||
allowNull: true,
|
||||
},
|
||||
|
||||
total: {
|
||||
type: new DataTypes.BIGINT(),
|
||||
allowNull: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
tableName: "quotes",
|
||||
|
||||
paranoid: true, // softs deletes
|
||||
timestamps: true,
|
||||
//version: true,
|
||||
|
||||
createdAt: "created_at",
|
||||
updatedAt: "updated_at",
|
||||
deletedAt: "deleted_at",
|
||||
|
||||
indexes: [
|
||||
{ name: "status_idx", fields: ["status"] },
|
||||
{ name: "deleted_at_idx", fields: ["deleted_at"] },
|
||||
],
|
||||
}
|
||||
);
|
||||
|
||||
return Quote_Model;
|
||||
};
|
||||
@ -0,0 +1,90 @@
|
||||
import {
|
||||
CreationOptional,
|
||||
DataTypes,
|
||||
InferAttributes,
|
||||
InferCreationAttributes,
|
||||
Model,
|
||||
NonAttribute,
|
||||
Sequelize,
|
||||
} from "sequelize";
|
||||
import { Quote_Model } from "./quote.model";
|
||||
|
||||
export type QuoteItemCreationAttributes = InferCreationAttributes<
|
||||
QuoteItem_Model,
|
||||
{ omit: "quote" }
|
||||
>;
|
||||
|
||||
export class QuoteItem_Model extends Model<
|
||||
InferAttributes<QuoteItem_Model, { omit: "quote" }>,
|
||||
InferCreationAttributes<QuoteItem_Model, { omit: "quote" }>
|
||||
> {
|
||||
static associate(connection: Sequelize) {
|
||||
const { Quote_Model, QuoteItem_Model } = connection.models;
|
||||
|
||||
QuoteItem_Model.belongsTo(Quote_Model, {
|
||||
as: "quote",
|
||||
foreignKey: "quote_id",
|
||||
onDelete: "CASCADE",
|
||||
});
|
||||
}
|
||||
|
||||
declare quote_id: string;
|
||||
declare item_id: string;
|
||||
declare position: number;
|
||||
declare description: CreationOptional<string>;
|
||||
declare quantity: CreationOptional<number>;
|
||||
declare unit_price: CreationOptional<number>;
|
||||
declare subtotal: CreationOptional<number>;
|
||||
declare total: CreationOptional<number>;
|
||||
|
||||
declare quote?: NonAttribute<Quote_Model>;
|
||||
}
|
||||
|
||||
export default (sequelize: Sequelize) => {
|
||||
QuoteItem_Model.init(
|
||||
{
|
||||
item_id: {
|
||||
type: new DataTypes.UUID(),
|
||||
primaryKey: true,
|
||||
},
|
||||
quote_id: {
|
||||
type: new DataTypes.UUID(),
|
||||
primaryKey: true,
|
||||
},
|
||||
position: {
|
||||
type: new DataTypes.MEDIUMINT(),
|
||||
autoIncrement: false,
|
||||
allowNull: false,
|
||||
},
|
||||
description: {
|
||||
type: new DataTypes.TEXT(),
|
||||
allowNull: true,
|
||||
},
|
||||
quantity: {
|
||||
type: DataTypes.BIGINT(),
|
||||
allowNull: true,
|
||||
},
|
||||
unit_price: {
|
||||
type: new DataTypes.BIGINT(),
|
||||
allowNull: true,
|
||||
},
|
||||
subtotal: {
|
||||
type: new DataTypes.BIGINT(),
|
||||
allowNull: true,
|
||||
},
|
||||
total: {
|
||||
type: new DataTypes.BIGINT(),
|
||||
allowNull: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
tableName: "quote_items",
|
||||
timestamps: false,
|
||||
|
||||
indexes: [],
|
||||
}
|
||||
);
|
||||
|
||||
return QuoteItem_Model;
|
||||
};
|
||||
@ -1,42 +1,87 @@
|
||||
import { Email, Name, Phone, Result, UniqueID } from "..";
|
||||
import {
|
||||
Currency,
|
||||
Description,
|
||||
Email,
|
||||
Language,
|
||||
Measure,
|
||||
Name,
|
||||
Phone,
|
||||
Quantity,
|
||||
Result,
|
||||
UTCDateValue,
|
||||
UniqueID,
|
||||
UnitPrice,
|
||||
} from "..";
|
||||
import { UndefinedOr } from "../../../utilities";
|
||||
|
||||
export const ensureIdIsValid = (value: string) =>
|
||||
UniqueID.create(value, { generateOnEmpty: false });
|
||||
|
||||
export const ensureNameIsValid = (
|
||||
value: UndefinedOr<string>,
|
||||
label: string = "name",
|
||||
) => {
|
||||
export const ensureNameIsValid = (value: UndefinedOr<string>, label: string = "name") => {
|
||||
const valueOrError = Name.create(value, {
|
||||
label,
|
||||
});
|
||||
|
||||
return valueOrError.isSuccess
|
||||
? Result.ok(valueOrError.object)
|
||||
: Result.fail(valueOrError.error);
|
||||
return valueOrError.isSuccess ? Result.ok(valueOrError.object) : Result.fail(valueOrError.error);
|
||||
};
|
||||
|
||||
export const ensureEmailIsValid = (
|
||||
value: UndefinedOr<string>,
|
||||
label: string = "email",
|
||||
) => {
|
||||
export const ensureEmailIsValid = (value: UndefinedOr<string>, label: string = "email") => {
|
||||
const valueOrError = Email.create(value, {
|
||||
label,
|
||||
});
|
||||
|
||||
return valueOrError.isSuccess
|
||||
? Result.ok(valueOrError.object)
|
||||
: Result.fail(valueOrError.error);
|
||||
return valueOrError.isSuccess ? Result.ok(valueOrError.object) : Result.fail(valueOrError.error);
|
||||
};
|
||||
|
||||
export const ensurePhoneIsValid = (
|
||||
value: UndefinedOr<string>,
|
||||
label: string = "phone",
|
||||
) => {
|
||||
export const ensurePhoneIsValid = (value: UndefinedOr<string>, label: string = "phone") => {
|
||||
const valueOrError = Phone.create(value, { label });
|
||||
|
||||
return valueOrError.isSuccess
|
||||
? Result.ok(valueOrError.object)
|
||||
: Result.fail(valueOrError.error);
|
||||
return valueOrError.isSuccess ? Result.ok(valueOrError.object) : Result.fail(valueOrError.error);
|
||||
};
|
||||
|
||||
export const ensureDateIsValid = (value: string): Result<boolean, Error> => {
|
||||
const dateOrError = UTCDateValue.create(value);
|
||||
|
||||
return dateOrError.isSuccess ? Result.ok(true) : Result.fail(dateOrError.error);
|
||||
};
|
||||
|
||||
export const ensureCurrencyCodeIsValid = (value: string): Result<boolean, Error> => {
|
||||
const currencyOrError = Currency.createFromCode(value);
|
||||
|
||||
return currencyOrError.isSuccess ? Result.ok(true) : Result.fail(currencyOrError.error);
|
||||
};
|
||||
|
||||
export const ensureLanguageCodeIsValid = (value: string): Result<boolean, Error> => {
|
||||
const currencyOrError = Language.createFromCode(value);
|
||||
|
||||
return currencyOrError.isSuccess ? Result.ok(true) : Result.fail(currencyOrError.error);
|
||||
};
|
||||
|
||||
export const ensureDescriptionIsValid = (value: string): Result<boolean, Error> => {
|
||||
const descriptionOrError = Description.create(value);
|
||||
|
||||
return descriptionOrError.isSuccess ? Result.ok(true) : Result.fail(descriptionOrError.error);
|
||||
};
|
||||
|
||||
export const ensureQuantityIsValid = (value: string): Result<boolean, Error> => {
|
||||
const descriptionOrError = Quantity.create(value);
|
||||
|
||||
return descriptionOrError.isSuccess ? Result.ok(true) : Result.fail(descriptionOrError.error);
|
||||
};
|
||||
|
||||
export const ensureUnitPriceIsValid = (value: any): Result<boolean, Error> => {
|
||||
const { amount, currency, precision } = value;
|
||||
const descriptionOrError = UnitPrice.create({
|
||||
amount,
|
||||
currencyCode: currency,
|
||||
precision,
|
||||
});
|
||||
|
||||
return descriptionOrError.isSuccess ? Result.ok(true) : Result.fail(descriptionOrError.error);
|
||||
};
|
||||
|
||||
export const ensureUnitMeasureIsValid = (value: string): Result<boolean, Error> => {
|
||||
const descriptionOrError = Measure.create(value);
|
||||
|
||||
return descriptionOrError.isSuccess ? Result.ok(true) : Result.fail(descriptionOrError.error);
|
||||
};
|
||||
|
||||
@ -1,7 +1,26 @@
|
||||
export interface IMoney_DTO {
|
||||
import Joi from "joi";
|
||||
import { Result, RuleValidator } from "../../domain";
|
||||
|
||||
export interface IMoney_Request_DTO {
|
||||
amount: number;
|
||||
precision: number;
|
||||
currency: string;
|
||||
}
|
||||
|
||||
export interface IMoney_Response_DTO extends IMoney_DTO {}
|
||||
export function ensureMoney_DTOIsValid(money: IMoney_Request_DTO) {
|
||||
const schema = Joi.object({
|
||||
amount: Joi.number(),
|
||||
precision: Joi.number(),
|
||||
currency: Joi.string(),
|
||||
});
|
||||
|
||||
const result = RuleValidator.validate<IMoney_Request_DTO>(schema, money);
|
||||
|
||||
if (result.isFailure) {
|
||||
return Result.fail(result.error);
|
||||
}
|
||||
|
||||
return Result.ok(true);
|
||||
}
|
||||
|
||||
export interface IMoney_Response_DTO extends IMoney_Request_DTO {}
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
import Joi from "joi";
|
||||
import { RuleValidator } from "../../RuleValidator";
|
||||
import {
|
||||
INullableValueObjectOptions,
|
||||
NullableValueObject,
|
||||
} from "../NullableValueObject";
|
||||
import { DomainError, handleDomainError } from "../../errors";
|
||||
import { INullableValueObjectOptions, NullableValueObject } from "../NullableValueObject";
|
||||
import { Result } from "../Result";
|
||||
import { Currencies } from "./currencies";
|
||||
|
||||
@ -37,16 +35,13 @@ export class Currency extends NullableValueObject<ICurrency> {
|
||||
Joi.string()
|
||||
.uppercase()
|
||||
.valid(...Object.keys(Currencies))
|
||||
.label(String(options.label)),
|
||||
.label(String(options.label))
|
||||
);
|
||||
|
||||
return RuleValidator.validate<string>(rule, value);
|
||||
}
|
||||
|
||||
public static createFromCode(
|
||||
currencyCode: string,
|
||||
options: ICurrencyOptions = {},
|
||||
) {
|
||||
public static createFromCode(currencyCode: string, options: ICurrencyOptions = {}) {
|
||||
const _options = {
|
||||
...options,
|
||||
label: options.label ? options.label : "current_code",
|
||||
@ -55,9 +50,10 @@ export class Currency extends NullableValueObject<ICurrency> {
|
||||
const validationResult = Currency.validate(currencyCode, _options);
|
||||
|
||||
if (validationResult.isFailure) {
|
||||
return Result.fail(validationResult.error);
|
||||
return Result.fail(
|
||||
handleDomainError(DomainError.INVALID_INPUT_DATA, validationResult.error.message, _options)
|
||||
);
|
||||
}
|
||||
|
||||
return Result.ok(new Currency(Currencies[validationResult.object]));
|
||||
}
|
||||
|
||||
|
||||
@ -4,10 +4,8 @@ import { Result } from "../Result";
|
||||
import Joi from "joi";
|
||||
|
||||
import { UndefinedOr } from "../../../../../utilities";
|
||||
import {
|
||||
INullableValueObjectOptions,
|
||||
NullableValueObject,
|
||||
} from "../NullableValueObject";
|
||||
import { DomainError, handleDomainError } from "../../errors";
|
||||
import { INullableValueObjectOptions, NullableValueObject } from "../NullableValueObject";
|
||||
import { LANGUAGES_LIST } from "./languages_data";
|
||||
|
||||
export interface ILanguage {
|
||||
@ -22,16 +20,13 @@ export class Language extends NullableValueObject<ILanguage> {
|
||||
public static readonly DEFAULT_LANGUAGE_CODE = "es";
|
||||
public static readonly LANGUAGES = LANGUAGES_LIST;
|
||||
|
||||
protected static validate(
|
||||
value: UndefinedOr<string>,
|
||||
options: INullableValueObjectOptions,
|
||||
) {
|
||||
protected static validate(value: UndefinedOr<string>, options: INullableValueObjectOptions) {
|
||||
const rule = Joi.alternatives(
|
||||
RuleValidator.RULE_ALLOW_EMPTY.default(""),
|
||||
Joi.string()
|
||||
.lowercase()
|
||||
.valid(...Object.keys(LANGUAGES_LIST))
|
||||
.label(String(options.label)),
|
||||
.label(String(options.label))
|
||||
);
|
||||
|
||||
return RuleValidator.validate<string>(rule, value);
|
||||
@ -41,10 +36,7 @@ export class Language extends NullableValueObject<ILanguage> {
|
||||
return value ? String(value).toLowerCase() : undefined;
|
||||
}
|
||||
|
||||
public static createFromCode(
|
||||
languageCode: string,
|
||||
options: ILanguageOptions = {},
|
||||
) {
|
||||
public static createFromCode(languageCode: string, options: ILanguageOptions = {}) {
|
||||
const _options = {
|
||||
...options,
|
||||
label: options.label ? options.label : "language_code",
|
||||
@ -53,7 +45,9 @@ export class Language extends NullableValueObject<ILanguage> {
|
||||
const validationResult = Language.validate(languageCode, _options);
|
||||
|
||||
if (validationResult.isFailure) {
|
||||
return Result.fail(validationResult.error);
|
||||
return Result.fail(
|
||||
handleDomainError(DomainError.INVALID_INPUT_DATA, validationResult.error.message, _options)
|
||||
);
|
||||
}
|
||||
|
||||
const code = Language.sanitize(validationResult.object);
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import Joi from "joi";
|
||||
import { EmptyOr } from "../../../../utilities";
|
||||
import { RuleValidator } from "../RuleValidator";
|
||||
import { DomainError, handleDomainError } from "../errors";
|
||||
import { Result } from "./Result";
|
||||
import { IValueObjectOptions, ValueObject } from "./ValueObject";
|
||||
|
||||
@ -9,10 +10,7 @@ export interface IDateValueOptions extends IValueObjectOptions {
|
||||
}
|
||||
|
||||
export class UTCDateValue extends ValueObject<Date> {
|
||||
protected static validate(
|
||||
value: EmptyOr<string>,
|
||||
options: IDateValueOptions,
|
||||
) {
|
||||
protected static validate(value: EmptyOr<string>, options: IDateValueOptions) {
|
||||
const ruleIsEmpty = RuleValidator.RULE_ALLOW_EMPTY.default(0);
|
||||
const rulesIsDate = Joi.date()
|
||||
//.format(String(options.dateFormat))
|
||||
@ -27,10 +25,7 @@ export class UTCDateValue extends ValueObject<Date> {
|
||||
return Result.ok<UTCDateValue>(new UTCDateValue(new Date()));
|
||||
}
|
||||
|
||||
public static create(
|
||||
value: EmptyOr<string>,
|
||||
options: IDateValueOptions = {},
|
||||
) {
|
||||
public static create(value: EmptyOr<string>, options: IDateValueOptions = {}) {
|
||||
const _options = {
|
||||
...options,
|
||||
dateFormat: options.dateFormat ? options.dateFormat : "YYYY-MM-DD",
|
||||
@ -40,12 +35,12 @@ export class UTCDateValue extends ValueObject<Date> {
|
||||
const validationResult = UTCDateValue.validate(value, _options);
|
||||
|
||||
if (validationResult.isFailure) {
|
||||
return Result.fail(validationResult.error);
|
||||
return Result.fail(
|
||||
handleDomainError(DomainError.INVALID_INPUT_DATA, validationResult.error.message, _options)
|
||||
);
|
||||
}
|
||||
|
||||
return Result.ok<UTCDateValue>(
|
||||
new UTCDateValue(new Date(validationResult.object)),
|
||||
);
|
||||
return Result.ok<UTCDateValue>(new UTCDateValue(new Date(validationResult.object)));
|
||||
}
|
||||
|
||||
public isValid = (): boolean => {
|
||||
|
||||
@ -1,22 +0,0 @@
|
||||
import Joi from "joi";
|
||||
import { Result, RuleValidator } from "../../../../common";
|
||||
|
||||
export interface ICreateDealer_Request_DTO {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export function ensureCreateDealer_Request_DTOIsValid(dealerDTO: ICreateDealer_Request_DTO) {
|
||||
const schema = Joi.object({
|
||||
id: Joi.string(),
|
||||
name: Joi.string(),
|
||||
}).unknown(true);
|
||||
|
||||
const result = RuleValidator.validate<ICreateDealer_Request_DTO>(schema, dealerDTO);
|
||||
|
||||
if (result.isFailure) {
|
||||
return Result.fail(result.error);
|
||||
}
|
||||
|
||||
return Result.ok(true);
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
import Joi from "joi";
|
||||
import { Result, RuleValidator } from "../../../../common";
|
||||
import { Result, RuleValidator } from "../../../../../common";
|
||||
|
||||
export interface ICreateDealer_Request_DTO {
|
||||
id: string;
|
||||
@ -1,5 +1,5 @@
|
||||
import Joi from "joi";
|
||||
import { Result, RuleValidator } from "../../../../common";
|
||||
import { Result, RuleValidator } from "../../../../../common";
|
||||
|
||||
export interface IUpdateDealer_Request_DTO {
|
||||
name: string;
|
||||
@ -0,0 +1,4 @@
|
||||
export * from "./CreateDealer.dto";
|
||||
export * from "./GetDealer.dto";
|
||||
export * from "./ListDealers.dto";
|
||||
export * from "./UpdateDealer.dto";
|
||||
@ -0,0 +1,48 @@
|
||||
import Joi from "joi";
|
||||
import { IMoney_Request_DTO, Result, RuleValidator } from "../../../../../common";
|
||||
|
||||
export interface ICreateQuote_Request_DTO {
|
||||
id: string;
|
||||
status: string;
|
||||
date: string;
|
||||
language_code: string;
|
||||
currency_code: string;
|
||||
|
||||
items: ICreateQuoteItem_Request_DTO[];
|
||||
}
|
||||
|
||||
export interface ICreateQuoteItem_Request_DTO {
|
||||
description: string;
|
||||
quantity: string;
|
||||
unit_measure: string;
|
||||
unit_price: IMoney_Request_DTO;
|
||||
}
|
||||
|
||||
export function ensureCreateQuote_Request_DTOIsValid(quoteDTO: ICreateQuote_Request_DTO) {
|
||||
const schema = Joi.object({
|
||||
id: Joi.string(),
|
||||
date: Joi.string(),
|
||||
language: Joi.string(),
|
||||
currency: Joi.string(),
|
||||
items: Joi.array().items(
|
||||
Joi.object({
|
||||
description: Joi.string(),
|
||||
quantity: Joi.string(),
|
||||
unit_measure: Joi.string(),
|
||||
unit_price: Joi.object({
|
||||
amount: Joi.number(),
|
||||
precision: Joi.number(),
|
||||
currency: Joi.string(),
|
||||
}),
|
||||
}).unknown(true)
|
||||
),
|
||||
}).unknown(true);
|
||||
|
||||
const result = RuleValidator.validate<ICreateQuote_Request_DTO>(schema, quoteDTO);
|
||||
|
||||
if (result.isFailure) {
|
||||
return Result.fail(result.error);
|
||||
}
|
||||
|
||||
return Result.ok(true);
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
import { IMoney_Response_DTO } from "shared/lib/contexts/common";
|
||||
|
||||
export interface ICreateQuote_Response_DTO {
|
||||
id: string;
|
||||
status: string;
|
||||
date: string;
|
||||
language_code: string;
|
||||
currency_code: string;
|
||||
|
||||
subtotal: IMoney_Response_DTO;
|
||||
total: IMoney_Response_DTO;
|
||||
|
||||
items: ICreateQuote_QuoteItem_Response_DTO[];
|
||||
}
|
||||
|
||||
export interface ICreateQuote_QuoteItem_Response_DTO {
|
||||
description: string;
|
||||
quantity: string;
|
||||
unit_measure: string;
|
||||
unit_price: IMoney_Response_DTO;
|
||||
subtotal: IMoney_Response_DTO;
|
||||
total: IMoney_Response_DTO;
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
export * from "./ICreateQuote_Request.dto";
|
||||
export * from "./ICreateQuote_Response.dto";
|
||||
@ -0,0 +1,23 @@
|
||||
import { IMoney_Response_DTO } from "shared/lib/contexts/common";
|
||||
|
||||
export interface IGetQuote_Response_DTO {
|
||||
id: string;
|
||||
status: string;
|
||||
date: string;
|
||||
language_code: string;
|
||||
currency_code: string;
|
||||
|
||||
subtotal: IMoney_Response_DTO;
|
||||
total: IMoney_Response_DTO;
|
||||
|
||||
items: IGetQuote_QuoteItem_Response_DTO[];
|
||||
}
|
||||
|
||||
export interface IGetQuote_QuoteItem_Response_DTO {
|
||||
description: string;
|
||||
quantity: string;
|
||||
unit_measure: string;
|
||||
unit_price: IMoney_Response_DTO;
|
||||
subtotal: IMoney_Response_DTO;
|
||||
total: IMoney_Response_DTO;
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export * from "./IGetQuote_Response.dto";
|
||||
@ -0,0 +1,13 @@
|
||||
import { IMoney_Response_DTO } from "shared/lib/contexts/common";
|
||||
|
||||
export interface IListQuotes_Response_DTO {
|
||||
id: string;
|
||||
|
||||
status: string;
|
||||
date: string;
|
||||
language_code: string;
|
||||
currency_code: string;
|
||||
|
||||
subtotal: IMoney_Response_DTO;
|
||||
total: IMoney_Response_DTO;
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export * from "./IListQuotes_Response.dto";
|
||||
@ -0,0 +1,46 @@
|
||||
import Joi from "joi";
|
||||
import { IMoney_Request_DTO, Result, RuleValidator } from "../../../../../common";
|
||||
|
||||
export interface IUpdateQuote_Request_DTO {
|
||||
status: string;
|
||||
date: string;
|
||||
language_code: string;
|
||||
currency_code: string;
|
||||
|
||||
items: IUpdateQuoteItem_Request_DTO[];
|
||||
}
|
||||
|
||||
export interface IUpdateQuoteItem_Request_DTO {
|
||||
description: string;
|
||||
quantity: string;
|
||||
unit_measure: string;
|
||||
unit_price: IMoney_Request_DTO;
|
||||
}
|
||||
|
||||
export function ensureUpdateQuote_Request_DTOIsValid(quoteDTO: IUpdateQuote_Request_DTO) {
|
||||
const schema = Joi.object({
|
||||
date: Joi.string(),
|
||||
language: Joi.string(),
|
||||
currency: Joi.string(),
|
||||
items: Joi.array().items(
|
||||
Joi.object({
|
||||
description: Joi.string(),
|
||||
quantity: Joi.string(),
|
||||
unit_measure: Joi.string(),
|
||||
unit_price: Joi.object({
|
||||
amount: Joi.number(),
|
||||
precision: Joi.number(),
|
||||
currency: Joi.string(),
|
||||
}),
|
||||
}).unknown(true)
|
||||
),
|
||||
}).unknown(true);
|
||||
|
||||
const result = RuleValidator.validate<IUpdateQuote_Request_DTO>(schema, quoteDTO);
|
||||
|
||||
if (result.isFailure) {
|
||||
return Result.fail(result.error);
|
||||
}
|
||||
|
||||
return Result.ok(true);
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
import { IMoney_Response_DTO } from "shared/lib/contexts/common";
|
||||
|
||||
export interface IUpdateQuote_Response_DTO {
|
||||
id: string;
|
||||
status: string;
|
||||
date: string;
|
||||
language_code: string;
|
||||
currency_code: string;
|
||||
|
||||
subtotal: IMoney_Response_DTO;
|
||||
total: IMoney_Response_DTO;
|
||||
|
||||
items: IUpdateQuote_QuoteItem_Response_DTO[];
|
||||
}
|
||||
|
||||
export interface IUpdateQuote_QuoteItem_Response_DTO {
|
||||
description: string;
|
||||
quantity: string;
|
||||
unit_measure: string;
|
||||
unit_price: IMoney_Response_DTO;
|
||||
subtotal: IMoney_Response_DTO;
|
||||
total: IMoney_Response_DTO;
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
export * from "./IUpdateQuote_Request.dto";
|
||||
export * from "./IUpdateQuote_Response.dto";
|
||||
4
shared/lib/contexts/sales/application/dto/Quote/index.ts
Normal file
4
shared/lib/contexts/sales/application/dto/Quote/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from "./CreateQuote.dto";
|
||||
export * from "./GetQuote.dto";
|
||||
export * from "./ListQuotes.dto";
|
||||
export * from "./UpdateQuote.dto";
|
||||
@ -1,4 +1,2 @@
|
||||
export * from "./CreateDealer.dto";
|
||||
export * from "./GetDealer.dto";
|
||||
export * from "./IListDealers.dto";
|
||||
export * from "./UpdateDealer.dto";
|
||||
export * from "./Dealer";
|
||||
export * from "./Quote";
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Cambia "Dealer" por "Dealer" en todos los archivos y directorios de forma recursiva
|
||||
find . -depth -name "*Reseller*" | while read file; do
|
||||
# Obtén el nuevo nombre con 'Reseller' reemplazado por 'Dealer'
|
||||
newfile=$(echo "$file" | sed 's/Reseller/Dealer/g')
|
||||
# Renombra el archivo/directorio
|
||||
mv "$file" "$newfile"
|
||||
done
|
||||
Loading…
Reference in New Issue
Block a user