first commit

This commit is contained in:
2026-07-16 20:48:43 -03:00
commit 5696cee264
1111 changed files with 322270 additions and 0 deletions
+582
View File
@@ -0,0 +1,582 @@
En este documento de texto voy a indicarte como quiero que procedas para crear nuevas funcionalidades siguiendo
el estilo de programación del proyecto.
Te voy a detallas los pasos a seguir para crear nuevas funcionalidades. Para ello voy a darte archivos de ejemplo
que quiero que utilices como ejemplo para crear las nuevas funciones que te pida.
Primero te voy a describir la estructura que estoy manejando:
La carpeta principal del proyecto es turnosxpress para una funcionalidad de ejemplo. En este caso vamos a ver como
quiero que desarrolles futuras funcionalidades basandonos en la funcion existente llamada Payments.
En el arbol de directorio que te voy a pasar veras que una funcionalidad se divide en dos areas. Una dentro
de la carpeta api y otra dentro de la carpeta Models.
Dentro de la carpeta api debe ir un directorio con el nombre de la funcionalidad, en este caso "Payments" y
dentro de esa carpeta dos archivos, uno llamado Payments.service.ts y otro llamado Payments.Controller.ts
Dentro de la carpeta Models debe ir un directorio con el nombre de la funcionalidad, en este caso "Payments"
y dentro de esa carpeta tres archivos, en este caso Payments.Adapter.Mongoose.ts, Payments.interface.ts y Payments.ts
Luego de mostrarte el arbol de directorios te voy a detallar lo que quiero que contenga cada archivo en su interior.
ARBOL DE DIRECTORIOS
turnosxpress/
server/
src/
api/
Payments/
Payments.Controller.ts
Payments.service.ts
Models/
Payments/
Payments.Adapter.Mongoose.ts
Payments.interface.ts
Payments.ts
Vamos a suponer que queremos crear una nueva funcionalidad llamada Payments que tenga las siguientes propiedades.
Propiedades:
id, amount, clientId, description y createdAt
En este caso espero que me crees la estructura de directorios y archivos mencionada anterriormente en la seccion
ARBOL DE DIRECTORIOS
CONTENIDO DE CADA ARCHIVO - Models/Payments.interface.ts:
debe haber una interfaz que describa como es un pago, en este caso:
export interface IPayment {
id?: string;
_id?: string;
amount: number;
clientId: string;
description: string;
createdAt: Date;
}
Se debe definir un adaptador con los metodos, create, delete, find, findOne y paginate. Cada metodo
debe tener como parametros un tipo definido previamente llamado con el nombre del metodo - nombre de la
funcionalidad - terminado en Params. Por ejemplo para el metodo create el tipo debe llamarse CreatePaymentsParams
para el metodo find, el tipo debe llamarse FindPaymentsParams y asi sucesivamente.
Tambien nota que el metodo find devuelve un array con la interfaz definida anteriormente y el metodo findOne
devuelve un solo elemento del tipo IPaymentDocument, que estara definido en el archivo Models/Payments.Adapter.Mongoose.ts.
export interface IPaymentsAdapter {
create(data: CreatePaymentExtraParams): Promise<IPayment>;
delete(data: DeletePaymentParams): Promise<void>;
find(filters: FindPaymentsParams): Promise<IPayment[]>;
findOne(filters: FindPaymentsParams): Promise<IPaymentDocument | null>;
paginate(filters: PaginatePaymentsParams): Promise<PaginatePaymentsResults>;
}
entonces los tipos que se deben generar serian:
export type FindPaymentsParams = {
id?: string;
clientId?: string;
startDate?: Date;
endDate?: Date;
sessionUser: string;
};
export type PaginatePaymentsParams = FindPaymentsParams & {
page: number;
limit: number;
};
export type PaginatePaymentsResults = {
data: IPayment[];
page: number;
pages: number;
};
export type CreatePaymentParams = {
amount: number;
clientId: string;
description?: string;
sessionUser: string;
};
export type DeletePaymentParams = {
id: string;
sessionUser: string;
};
Tambien vamos a definir un Manager:
export interface IPaymentsManager {
payments: IPaymentsAdapter;
createPayment(data: CreatePaymentParams): Promise<IPayment>;
deletePayment(data: DeletePaymentParams): Promise<void>;
paginate(data: PaginatePaymentsViewParams): Promise<PaginatePaymentsViewResults>;
}
CONTENIDO DE CADA ARCHIVO - Models/Payments.Adapter.Mongoose.ts:
Aqui se implementa el adaptador y se define como es un Documento.
export interface IPaymentDocument extends Omit<IPayment, "id" | "_id">, Document {
_id: string;
}
export class PaymentsAdapterMongoose implements IPaymentsAdapter {
schema: Schema;
paymentsList: Model<IPaymentDocument>;
constructor() {
this.schema = new Schema({
clientId: {
type: Schema.Types.ObjectId,
required: true,
ref: "Client",
},
amount: { type: Number, required: true, default: 0 },
description: { type: String, required: false, default: "" },
createdAt: { type: Date, required: true, default: Date.now },
});
this.paymentsList = model<IPaymentDocument>("Payment", this.schema);
}
public async create(data: CreatePaymentExtraParams): Promise<IPayment> {
return await this.paymentsList.create(data);
}
public async delete(id: string): Promise<void> {
await this.paymentsList.deleteOne({ _id: id });
}
public async find(filters: Omit<FindPaymentsParams, "sessionUser">): Promise<IPayment[]> {
const searchCriteria = this.buildSearchCriteria(filters);
return await this.paymentsList.find(searchCriteria).exec();
}
public async findOne(
filters: Omit<FindPaymentsParams, "sessionUser">
): Promise<IPaymentDocument | null> {
return await this.paymentsList.findOne(filters).exec();
}
private buildSearchCriteria(
filters: Omit<FindPaymentsParams, "sessionUser">
): FilterQuery<IPayment> {
const searchCriteria: FilterQuery<IPayment> = {};
if (filters.startDate) {
searchCriteria.createdAt = {
$gte: filters.startDate,
$lte: filters.endDate,
};
}
if (filters.status) {
searchCriteria.status = filters.status;
}
if (filters.clientId) {
searchCriteria.clientId = filters.clientId;
}
if (filters.method) {
searchCriteria.method = filters.method;
}
if (filters.companyId) {
searchCriteria.companyId = filters.companyId;
}
if (filters.employeeId) {
searchCriteria.employeeId = filters.employeeId;
}
if (filters.serviceId) {
searchCriteria.serviceId = filters.serviceId;
}
if (filters.appointmentId) {
searchCriteria.appointmentId = filters.appointmentId;
}
return searchCriteria;
}
public async paginate(filters: PaginatePaymentsParams): Promise<PaginatePaymentsResults> {
const searchCriteria = this.buildSearchCriteria(filters);
const count = await this.paymentsList.countDocuments(searchCriteria).exec();
const skip = (filters.page - 1) * filters.limit;
const results = await this.paymentsList
.find(searchCriteria)
.skip(skip)
.limit(filters.limit)
.sort({ createdAt: -1 })
.exec();
return {
data: results,
page: filters.page,
pages: Math.ceil(count / filters.limit),
};
}
}
CONTENIDO DE CADA ARCHIVO - Models/Payments.ts:
En este archivo dejariamos el esqueleto de todas las funcionalidades definidas en el Manager creados
pero sin contenido para ser definido posteriormente por mi.
CONTENIDO DE CADA ARCHIVO - api/Payments/Payments.Service.ts: ver archivo para tener el ejemplo concreto,
pero basicamente creamos las llamadas de todos los metodos definidos en el Manager.
CONTENIDO DE CADA ARCHIVO - api/Payments/Payments.Controller.ts: ver archivo para tener el ejemplo concreto,
Se deben crear los enpoints de todos los metodos accesibles por el arhivo terminado en .Services, tendiendo
en cuenta que se debe utilizar el middleware @Middlewares(authenticateMiddleware) y que siempre usamos el metodo Post.
Asi deberia verse:
@Route("payments/delete")
@Middlewares(authenticateMiddleware)
export class DeletePaymentController extends Controller {
@SuccessResponse(200, "Done")
@Post()
public async deletePayment(
@Body() requestBody: DeletePaymentParams
): Promise<ApiVoidResult | ApiValidationError> {
try {
await new PaymentsService().deletePayment(requestBody);
this.setStatus(200);
return {
success: true,
message: "",
};
} catch (e) {
const errorOccurred: Error = e as Error;
this.setStatus(500);
return new ApiValidationError(500, errorOccurred.message);
}
}
}
notar que si devuelve void tenemos definido el tipo ApiVoidResult y si no devolvera el tipo definido en el archivo
Payments.interface.ts
Por favor recuerda guiarte estrictamente por el ejemplo proporcionado y no te inventes nada nuevo.
Entonces, para entendernos mejor: Si te paso este propmpt, "Quiero generar una nueva funcionalidad llamada Discounts,
con las propiedades: id, companyId, name, code, value, type, createdAt, donde type sera un enum con los elementos PERCENTAJE y FIXED" deberas generarme lo siguiente:
File: turnosxpress/server/src/Models/Discounts/Discounts.interface.ts
Contenido:
import { Document } from "mongoose";
export enum DiscountType {
FIXED = "fixed",
PERCENTAGE = "percentage",
}
export interface IDiscount {
id?: string;
_id?: string;
companyId: string;
name: string;
code: string;
type: DiscountType;
value: number;
createdAt: Date;
}
export interface IDiscountDocument extends Omit<IDiscount, "id" | "_id">, Document {
_id: string;
}
export interface IDiscountsAdapter {
create(data: Omit<CreateDiscountParams, "sessionUser">): Promise<IDiscount>;
delete(data: Omit<DeleteDiscountParams, "sessionUser">): Promise<void>;
find(filters: Omit<FindDiscountsParams, "sessionUser">): Promise<IDiscount[]>;
findOne(filters: Omit<FindDiscountsParams, "sessionUser">): Promise<IDiscountDocument | null>;
paginate(filters: PaginateDiscountsParams): Promise<PaginateDiscountsResults>;
}
export type FindDiscountsParams = {
id?: string;
companyId: string;
code?: string;
type?: DiscountType;
sessionUser: string;
startDate?: Date;
endDate?: Date;
startValue?: number;
endValue?: number;
};
export type PaginateDiscountsParams = FindDiscountsParams & {
page: number;
limit: number;
};
export type PaginateDiscountsResults = {
data: IDiscount[];
page: number;
pages: number;
};
export type CreateDiscountParams = {
companyId: string;
name: string;
code: string;
type: DiscountType;
value: number;
sessionUser: string;
};
export type DeleteDiscountParams = {
id: string;
companyId: string;
sessionUser: string;
};
export interface IDiscountsManager {
discounts: IDiscountsAdapter;
createDiscount(data: CreateDiscountParams): Promise<IDiscount>;
deleteDiscount(data: DeleteDiscountParams): Promise<void>;
paginate(data: PaginateDiscountsParams): Promise<PaginateDiscountsResults>;
}
File: turnosxpress/server/src/Models/Discounts/Discounts.Adapter.Mongoose.ts
Contenido:
import {
IDiscount,
IDiscountDocument,
IDiscountsAdapter,
DiscountType,
CreateDiscountParams,
DeleteDiscountParams,
FindDiscountsParams,
PaginateDiscountsParams,
PaginateDiscountsResults,
} from "./Discounts.interface";
import { Schema, model, Model, FilterQuery } from "mongoose";
export class DiscountsAdapterMongoose implements IDiscountsAdapter {
schema: Schema;
discountsList: Model<IDiscountDocument>;
constructor() {
this.schema = new Schema({
companyId: {
type: Schema.Types.ObjectId,
required: true,
ref: "Company",
},
name: { type: String, required: true },
code: { type: String, required: true },
type: { type: String, enum: Object.values(DiscountType), required: true },
value: { type: Number, required: true },
createdAt: { type: Date, default: Date.now, required: true },
});
this.discountsList = model<IDiscountDocument>("Discount", this.schema);
}
public async create(data: Omit<CreateDiscountParams, "sessionUser">): Promise<IDiscount> {
return await this.discountsList.create(data);
}
public async delete(data: Omit<DeleteDiscountParams, "sessionUser">): Promise<void> {
await this.discountsList.deleteOne({ _id: data.id });
}
public async find(filters: Omit<FindDiscountsParams, "sessionUser">): Promise<IDiscount[]> {
const searchCriteria = this.buildSearchCriteria(filters);
return await this.discountsList.find(searchCriteria).exec();
}
public async findOne(
filters: Omit<FindDiscountsParams, "sessionUser">
): Promise<IDiscountDocument | null> {
const searchCriteria = this.buildSearchCriteria(filters);
return await this.discountsList.findOne(searchCriteria).exec();
}
private buildSearchCriteria(
filters: Omit<FindDiscountsParams, "sessionUser">
): FilterQuery<IDiscount> {
const searchCriteria: FilterQuery<IDiscount> = {};
if (filters.id) searchCriteria._id = filters.id;
if (filters.companyId) searchCriteria.companyId = filters.companyId;
if (filters.code) searchCriteria.code = { $regex: filters.code, $options: "i" };
if (filters.type) searchCriteria.type = filters.type;
if (filters.startDate && filters.endDate) {
searchCriteria.createdAt = {
$gte: filters.startDate,
$lte: filters.endDate,
};
}
if (filters.startValue !== undefined && filters.endValue !== undefined) {
searchCriteria.value = {
$gte: filters.startValue,
$lte: filters.endValue,
};
}
return searchCriteria;
}
public async paginate(filters: PaginateDiscountsParams): Promise<PaginateDiscountsResults> {
const searchCriteria = this.buildSearchCriteria(filters);
const count = await this.discountsList.countDocuments(searchCriteria).exec();
const skip = (filters.page - 1) * filters.limit;
const results = await this.discountsList
.find(searchCriteria)
.skip(skip)
.limit(filters.limit)
.sort({ createdAt: -1 })
.exec();
return { data: results, page: filters.page, pages: Math.ceil(count / filters.limit) };
}
}
File: turnosxpress/server/src/Models/Discounts/Discounts.ts
Contenido:
import { validatePermissionsByCompany, validateSessionUser } from "src/helpers/check";
import {
IDiscountsManager,
IDiscountsAdapter,
CreateDiscountParams,
DeleteDiscountParams,
IDiscount,
PaginateDiscountsParams,
PaginateDiscountsResults,
} from "./Discounts.interface";
export class DiscountsManager implements IDiscountsManager {
discounts: IDiscountsAdapter;
constructor(discountsAdapter: IDiscountsAdapter) {
this.discounts = discountsAdapter;
}
async createDiscount(data: CreateDiscountParams): Promise<IDiscount> {
await validateSessionUser({
sessionUser: data.sessionUser,
});
await validatePermissionsByCompany({
companyId: data.companyId,
sessionUser: data.sessionUser,
});
// La lógica de negocio se implementará aquí
const discount = this.discounts.create(data);
if (!discount) {
throw new Error("Error creating discount");
}
return discount;
}
async deleteDiscount(data: DeleteDiscountParams): Promise<void> {
await validateSessionUser({
sessionUser: data.sessionUser,
});
const discount = await this.discounts.findOne({ id: data.id, companyId: data.companyId });
if (!discount) {
throw new Error("El descuento que intenta eliminar no existe");
}
await validatePermissionsByCompany({
companyId: String(discount.companyId),
sessionUser: data.sessionUser,
});
await this.discounts.delete(data);
}
async paginate(data: PaginateDiscountsParams): Promise<PaginateDiscountsResults> {
return this.discounts.paginate(data);
}
}
File: turnosxpress/server/src/api/Discounts/Discounts.Controller.ts
Contenido:
import { Controller, Post, Route, SuccessResponse, Body, Middlewares } from "tsoa";
import { DiscountsService } from "./Discounts.Service";
import {
CreateDiscountParams,
DeleteDiscountParams,
IDiscount,
PaginateDiscountsParams,
PaginateDiscountsResults,
} from "../../Models/Discounts/Discounts.Interface";
import { ApiValidationError } from "src/Models/Server.Error.model";
import { ApiVoidResult } from "src/Models/Api.VoidResult.type";
import { authenticateMiddleware } from "src/middleware/authentication";
@Route("discounts")
export class DiscountsController extends Controller {
@SuccessResponse(201, "Created")
@Post("create")
@Middlewares(authenticateMiddleware)
public async createDiscount(
@Body() requestBody: CreateDiscountParams
): Promise<IDiscount | ApiValidationError> {
try {
const discount = await new DiscountsService().createDiscount(requestBody);
this.setStatus(201);
return discount;
} catch (e) {
const errorOccurred: Error = e as Error;
this.setStatus(500);
return new ApiValidationError(500, errorOccurred.message);
}
}
@SuccessResponse(200, "Done")
@Post("delete")
@Middlewares(authenticateMiddleware)
public async deleteDiscount(
@Body() requestBody: DeleteDiscountParams
): Promise<ApiVoidResult | ApiValidationError> {
try {
await new DiscountsService().deleteDiscount(requestBody);
this.setStatus(200);
return { success: true, message: "Discount deleted successfully." };
} catch (e) {
const errorOccurred: Error = e as Error;
this.setStatus(500);
return new ApiValidationError(500, errorOccurred.message);
}
}
@SuccessResponse(200, "Done")
@Post("paginate")
@Middlewares(authenticateMiddleware)
public async paginate(
@Body() requestBody: PaginateDiscountsParams
): Promise<PaginateDiscountsResults | ApiValidationError> {
try {
const result = await new DiscountsService().paginate(requestBody);
this.setStatus(200);
return result;
} catch (e) {
const errorOccurred: Error = e as Error;
this.setStatus(500);
return new ApiValidationError(500, errorOccurred.message);
}
}
}
Y asi sucesivamente dependiendo del nombre de la funcionalidad y de las propiedades que te pida.