EPIC-001: Complete Core Module - Add PaymentTerm entity with multi-line support (30/60/90 days, early payment discounts) - Add PaymentTerms service with calculateDueDate() functionality - Add DiscountRule entity with volume/time-based conditions - Add DiscountRules service with applyDiscounts() and rule combination logic - Add REST endpoints for payment-terms and discount-rules in core module - Register new entities in TypeORM configuration EPIC-002: Entity Consolidation - Add inventoryProductId FK to products.products for linking to inventory module - Consolidate Warehouse entity in warehouses module as canonical source - Add companyId and Location relation to canonical Warehouse - Update inventory module to re-export Warehouse from warehouses module - Remove deprecated warehouse.entity.ts from inventory module - Update inventory/warehouses.service.ts to use canonical Warehouse Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
591 lines
24 KiB
TypeScript
591 lines
24 KiB
TypeScript
import { Response, NextFunction } from 'express';
|
|
import { z } from 'zod';
|
|
import { currenciesService, CreateCurrencyDto, UpdateCurrencyDto } from './currencies.service.js';
|
|
import { countriesService } from './countries.service.js';
|
|
import { uomService, CreateUomDto, UpdateUomDto } from './uom.service.js';
|
|
import { productCategoriesService, CreateProductCategoryDto, UpdateProductCategoryDto } from './product-categories.service.js';
|
|
import { paymentTermsService, CreatePaymentTermDto, UpdatePaymentTermDto } from './payment-terms.service.js';
|
|
import { discountRulesService, CreateDiscountRuleDto, UpdateDiscountRuleDto, ApplyDiscountContext } from './discount-rules.service.js';
|
|
import { PaymentTermLineType } from './entities/payment-term.entity.js';
|
|
import { DiscountType, DiscountAppliesTo, DiscountCondition } from './entities/discount-rule.entity.js';
|
|
import { AuthenticatedRequest } from '../../shared/middleware/auth.middleware.js';
|
|
import { ValidationError } from '../../shared/errors/index.js';
|
|
|
|
// Schemas
|
|
const createCurrencySchema = z.object({
|
|
code: z.string().length(3, 'El código debe tener 3 caracteres').toUpperCase(),
|
|
name: z.string().min(1, 'El nombre es requerido').max(100),
|
|
symbol: z.string().min(1).max(10),
|
|
decimal_places: z.number().int().min(0).max(6).optional(),
|
|
decimals: z.number().int().min(0).max(6).optional(), // Accept camelCase
|
|
}).refine((data) => data.decimal_places !== undefined || data.decimals !== undefined, {
|
|
message: 'decimal_places or decimals is required',
|
|
});
|
|
|
|
const updateCurrencySchema = z.object({
|
|
name: z.string().min(1).max(100).optional(),
|
|
symbol: z.string().min(1).max(10).optional(),
|
|
decimal_places: z.number().int().min(0).max(6).optional(),
|
|
decimals: z.number().int().min(0).max(6).optional(), // Accept camelCase
|
|
active: z.boolean().optional(),
|
|
});
|
|
|
|
const createUomSchema = z.object({
|
|
name: z.string().min(1, 'El nombre es requerido').max(100),
|
|
code: z.string().min(1).max(20),
|
|
category_id: z.string().uuid().optional(),
|
|
categoryId: z.string().uuid().optional(), // Accept camelCase
|
|
uom_type: z.enum(['reference', 'bigger', 'smaller']).optional(),
|
|
uomType: z.enum(['reference', 'bigger', 'smaller']).optional(), // Accept camelCase
|
|
ratio: z.number().positive().default(1),
|
|
}).refine((data) => data.category_id !== undefined || data.categoryId !== undefined, {
|
|
message: 'category_id or categoryId is required',
|
|
});
|
|
|
|
const updateUomSchema = z.object({
|
|
name: z.string().min(1).max(100).optional(),
|
|
ratio: z.number().positive().optional(),
|
|
active: z.boolean().optional(),
|
|
});
|
|
|
|
const createCategorySchema = z.object({
|
|
name: z.string().min(1, 'El nombre es requerido').max(100),
|
|
code: z.string().min(1).max(50),
|
|
parent_id: z.string().uuid().optional(),
|
|
parentId: z.string().uuid().optional(), // Accept camelCase
|
|
});
|
|
|
|
const updateCategorySchema = z.object({
|
|
name: z.string().min(1).max(100).optional(),
|
|
parent_id: z.string().uuid().optional().nullable(),
|
|
parentId: z.string().uuid().optional().nullable(), // Accept camelCase
|
|
active: z.boolean().optional(),
|
|
});
|
|
|
|
// Payment Terms Schemas
|
|
const paymentTermLineSchema = z.object({
|
|
sequence: z.number().int().min(1).optional(),
|
|
line_type: z.enum(['balance', 'percent', 'fixed']).optional(),
|
|
lineType: z.enum(['balance', 'percent', 'fixed']).optional(),
|
|
value_percent: z.number().min(0).max(100).optional(),
|
|
valuePercent: z.number().min(0).max(100).optional(),
|
|
value_amount: z.number().min(0).optional(),
|
|
valueAmount: z.number().min(0).optional(),
|
|
days: z.number().int().min(0).optional(),
|
|
day_of_month: z.number().int().min(1).max(31).optional(),
|
|
dayOfMonth: z.number().int().min(1).max(31).optional(),
|
|
end_of_month: z.boolean().optional(),
|
|
endOfMonth: z.boolean().optional(),
|
|
});
|
|
|
|
const createPaymentTermSchema = z.object({
|
|
code: z.string().min(1).max(50),
|
|
name: z.string().min(1).max(255),
|
|
description: z.string().optional(),
|
|
due_days: z.number().int().min(0).optional(),
|
|
dueDays: z.number().int().min(0).optional(),
|
|
discount_percent: z.number().min(0).max(100).optional(),
|
|
discountPercent: z.number().min(0).max(100).optional(),
|
|
discount_days: z.number().int().min(0).optional(),
|
|
discountDays: z.number().int().min(0).optional(),
|
|
is_immediate: z.boolean().optional(),
|
|
isImmediate: z.boolean().optional(),
|
|
lines: z.array(paymentTermLineSchema).optional(),
|
|
});
|
|
|
|
const updatePaymentTermSchema = z.object({
|
|
name: z.string().min(1).max(255).optional(),
|
|
description: z.string().optional().nullable(),
|
|
due_days: z.number().int().min(0).optional(),
|
|
dueDays: z.number().int().min(0).optional(),
|
|
discount_percent: z.number().min(0).max(100).optional().nullable(),
|
|
discountPercent: z.number().min(0).max(100).optional().nullable(),
|
|
discount_days: z.number().int().min(0).optional().nullable(),
|
|
discountDays: z.number().int().min(0).optional().nullable(),
|
|
is_immediate: z.boolean().optional(),
|
|
isImmediate: z.boolean().optional(),
|
|
is_active: z.boolean().optional(),
|
|
isActive: z.boolean().optional(),
|
|
lines: z.array(paymentTermLineSchema).optional(),
|
|
});
|
|
|
|
const calculateDueDateSchema = z.object({
|
|
invoice_date: z.string().datetime().optional(),
|
|
invoiceDate: z.string().datetime().optional(),
|
|
total_amount: z.number().min(0),
|
|
totalAmount: z.number().min(0).optional(),
|
|
});
|
|
|
|
// Discount Rules Schemas
|
|
const createDiscountRuleSchema = z.object({
|
|
code: z.string().min(1).max(50),
|
|
name: z.string().min(1).max(255),
|
|
description: z.string().optional(),
|
|
discount_type: z.enum(['percentage', 'fixed', 'price_override']).optional(),
|
|
discountType: z.enum(['percentage', 'fixed', 'price_override']).optional(),
|
|
discount_value: z.number().min(0),
|
|
discountValue: z.number().min(0).optional(),
|
|
max_discount_amount: z.number().min(0).optional().nullable(),
|
|
maxDiscountAmount: z.number().min(0).optional().nullable(),
|
|
applies_to: z.enum(['all', 'category', 'product', 'customer', 'customer_group']).optional(),
|
|
appliesTo: z.enum(['all', 'category', 'product', 'customer', 'customer_group']).optional(),
|
|
applies_to_id: z.string().uuid().optional().nullable(),
|
|
appliesToId: z.string().uuid().optional().nullable(),
|
|
condition_type: z.enum(['none', 'min_quantity', 'min_amount', 'date_range', 'first_purchase']).optional(),
|
|
conditionType: z.enum(['none', 'min_quantity', 'min_amount', 'date_range', 'first_purchase']).optional(),
|
|
condition_value: z.number().optional().nullable(),
|
|
conditionValue: z.number().optional().nullable(),
|
|
start_date: z.string().datetime().optional().nullable(),
|
|
startDate: z.string().datetime().optional().nullable(),
|
|
end_date: z.string().datetime().optional().nullable(),
|
|
endDate: z.string().datetime().optional().nullable(),
|
|
priority: z.number().int().min(0).optional(),
|
|
combinable: z.boolean().optional(),
|
|
usage_limit: z.number().int().min(0).optional().nullable(),
|
|
usageLimit: z.number().int().min(0).optional().nullable(),
|
|
});
|
|
|
|
const updateDiscountRuleSchema = z.object({
|
|
name: z.string().min(1).max(255).optional(),
|
|
description: z.string().optional().nullable(),
|
|
discount_type: z.enum(['percentage', 'fixed', 'price_override']).optional(),
|
|
discountType: z.enum(['percentage', 'fixed', 'price_override']).optional(),
|
|
discount_value: z.number().min(0).optional(),
|
|
discountValue: z.number().min(0).optional(),
|
|
max_discount_amount: z.number().min(0).optional().nullable(),
|
|
maxDiscountAmount: z.number().min(0).optional().nullable(),
|
|
applies_to: z.enum(['all', 'category', 'product', 'customer', 'customer_group']).optional(),
|
|
appliesTo: z.enum(['all', 'category', 'product', 'customer', 'customer_group']).optional(),
|
|
applies_to_id: z.string().uuid().optional().nullable(),
|
|
appliesToId: z.string().uuid().optional().nullable(),
|
|
condition_type: z.enum(['none', 'min_quantity', 'min_amount', 'date_range', 'first_purchase']).optional(),
|
|
conditionType: z.enum(['none', 'min_quantity', 'min_amount', 'date_range', 'first_purchase']).optional(),
|
|
condition_value: z.number().optional().nullable(),
|
|
conditionValue: z.number().optional().nullable(),
|
|
start_date: z.string().datetime().optional().nullable(),
|
|
startDate: z.string().datetime().optional().nullable(),
|
|
end_date: z.string().datetime().optional().nullable(),
|
|
endDate: z.string().datetime().optional().nullable(),
|
|
priority: z.number().int().min(0).optional(),
|
|
combinable: z.boolean().optional(),
|
|
usage_limit: z.number().int().min(0).optional().nullable(),
|
|
usageLimit: z.number().int().min(0).optional().nullable(),
|
|
is_active: z.boolean().optional(),
|
|
isActive: z.boolean().optional(),
|
|
});
|
|
|
|
const applyDiscountsSchema = z.object({
|
|
product_id: z.string().uuid().optional(),
|
|
productId: z.string().uuid().optional(),
|
|
category_id: z.string().uuid().optional(),
|
|
categoryId: z.string().uuid().optional(),
|
|
customer_id: z.string().uuid().optional(),
|
|
customerId: z.string().uuid().optional(),
|
|
customer_group_id: z.string().uuid().optional(),
|
|
customerGroupId: z.string().uuid().optional(),
|
|
quantity: z.number().min(0),
|
|
unit_price: z.number().min(0),
|
|
unitPrice: z.number().min(0).optional(),
|
|
total_amount: z.number().min(0),
|
|
totalAmount: z.number().min(0).optional(),
|
|
is_first_purchase: z.boolean().optional(),
|
|
isFirstPurchase: z.boolean().optional(),
|
|
});
|
|
|
|
class CoreController {
|
|
// ========== CURRENCIES ==========
|
|
async getCurrencies(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const activeOnly = req.query.active === 'true';
|
|
const currencies = await currenciesService.findAll(activeOnly);
|
|
res.json({ success: true, data: currencies });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async getCurrency(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const currency = await currenciesService.findById(req.params.id);
|
|
res.json({ success: true, data: currency });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async createCurrency(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const parseResult = createCurrencySchema.safeParse(req.body);
|
|
if (!parseResult.success) {
|
|
throw new ValidationError('Datos de moneda inválidos', parseResult.error.errors);
|
|
}
|
|
const dto: CreateCurrencyDto = parseResult.data;
|
|
const currency = await currenciesService.create(dto);
|
|
res.status(201).json({ success: true, data: currency, message: 'Moneda creada exitosamente' });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async updateCurrency(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const parseResult = updateCurrencySchema.safeParse(req.body);
|
|
if (!parseResult.success) {
|
|
throw new ValidationError('Datos de moneda inválidos', parseResult.error.errors);
|
|
}
|
|
const dto: UpdateCurrencyDto = parseResult.data;
|
|
const currency = await currenciesService.update(req.params.id, dto);
|
|
res.json({ success: true, data: currency, message: 'Moneda actualizada exitosamente' });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
// ========== COUNTRIES ==========
|
|
async getCountries(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const countries = await countriesService.findAll();
|
|
res.json({ success: true, data: countries });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async getCountry(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const country = await countriesService.findById(req.params.id);
|
|
res.json({ success: true, data: country });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
// ========== UOM CATEGORIES ==========
|
|
async getUomCategories(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const activeOnly = req.query.active === 'true';
|
|
const categories = await uomService.findAllCategories(activeOnly);
|
|
res.json({ success: true, data: categories });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async getUomCategory(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const category = await uomService.findCategoryById(req.params.id);
|
|
res.json({ success: true, data: category });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
// ========== UOM ==========
|
|
async getUoms(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const activeOnly = req.query.active === 'true';
|
|
const categoryId = req.query.category_id as string | undefined;
|
|
const uoms = await uomService.findAll(categoryId, activeOnly);
|
|
res.json({ success: true, data: uoms });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async getUom(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const uom = await uomService.findById(req.params.id);
|
|
res.json({ success: true, data: uom });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async createUom(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const parseResult = createUomSchema.safeParse(req.body);
|
|
if (!parseResult.success) {
|
|
throw new ValidationError('Datos de UdM inválidos', parseResult.error.errors);
|
|
}
|
|
const dto: CreateUomDto = parseResult.data;
|
|
const uom = await uomService.create(dto);
|
|
res.status(201).json({ success: true, data: uom, message: 'Unidad de medida creada exitosamente' });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async updateUom(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const parseResult = updateUomSchema.safeParse(req.body);
|
|
if (!parseResult.success) {
|
|
throw new ValidationError('Datos de UdM inválidos', parseResult.error.errors);
|
|
}
|
|
const dto: UpdateUomDto = parseResult.data;
|
|
const uom = await uomService.update(req.params.id, dto);
|
|
res.json({ success: true, data: uom, message: 'Unidad de medida actualizada exitosamente' });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
// ========== PRODUCT CATEGORIES ==========
|
|
async getProductCategories(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const activeOnly = req.query.active === 'true';
|
|
const parentId = req.query.parent_id as string | undefined;
|
|
const categories = await productCategoriesService.findAll(req.tenantId!, parentId, activeOnly);
|
|
res.json({ success: true, data: categories });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async getProductCategory(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const category = await productCategoriesService.findById(req.params.id, req.tenantId!);
|
|
res.json({ success: true, data: category });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async createProductCategory(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const parseResult = createCategorySchema.safeParse(req.body);
|
|
if (!parseResult.success) {
|
|
throw new ValidationError('Datos de categoría inválidos', parseResult.error.errors);
|
|
}
|
|
const dto: CreateProductCategoryDto = parseResult.data;
|
|
const category = await productCategoriesService.create(dto, req.tenantId!, req.user!.userId);
|
|
res.status(201).json({ success: true, data: category, message: 'Categoría creada exitosamente' });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async updateProductCategory(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const parseResult = updateCategorySchema.safeParse(req.body);
|
|
if (!parseResult.success) {
|
|
throw new ValidationError('Datos de categoría inválidos', parseResult.error.errors);
|
|
}
|
|
const dto: UpdateProductCategoryDto = parseResult.data;
|
|
const category = await productCategoriesService.update(req.params.id, dto, req.tenantId!, req.user!.userId);
|
|
res.json({ success: true, data: category, message: 'Categoría actualizada exitosamente' });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async deleteProductCategory(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
await productCategoriesService.delete(req.params.id, req.tenantId!);
|
|
res.json({ success: true, message: 'Categoría eliminada exitosamente' });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
// ========== PAYMENT TERMS ==========
|
|
async getPaymentTerms(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const activeOnly = req.query.active === 'true';
|
|
const paymentTerms = await paymentTermsService.findAll(req.tenantId!, activeOnly);
|
|
res.json({ success: true, data: paymentTerms });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async getPaymentTerm(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const paymentTerm = await paymentTermsService.findById(req.params.id, req.tenantId!);
|
|
res.json({ success: true, data: paymentTerm });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async createPaymentTerm(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const parseResult = createPaymentTermSchema.safeParse(req.body);
|
|
if (!parseResult.success) {
|
|
throw new ValidationError('Datos de término de pago inválidos', parseResult.error.errors);
|
|
}
|
|
const dto: CreatePaymentTermDto = parseResult.data;
|
|
const paymentTerm = await paymentTermsService.create(dto, req.tenantId!, req.user?.userId);
|
|
res.status(201).json({ success: true, data: paymentTerm, message: 'Término de pago creado exitosamente' });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async updatePaymentTerm(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const parseResult = updatePaymentTermSchema.safeParse(req.body);
|
|
if (!parseResult.success) {
|
|
throw new ValidationError('Datos de término de pago inválidos', parseResult.error.errors);
|
|
}
|
|
const dto: UpdatePaymentTermDto = parseResult.data;
|
|
const paymentTerm = await paymentTermsService.update(req.params.id, dto, req.tenantId!, req.user?.userId);
|
|
res.json({ success: true, data: paymentTerm, message: 'Término de pago actualizado exitosamente' });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async deletePaymentTerm(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
await paymentTermsService.delete(req.params.id, req.tenantId!, req.user?.userId);
|
|
res.json({ success: true, message: 'Término de pago eliminado exitosamente' });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async calculateDueDate(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const parseResult = calculateDueDateSchema.safeParse(req.body);
|
|
if (!parseResult.success) {
|
|
throw new ValidationError('Datos inválidos para cálculo', parseResult.error.errors);
|
|
}
|
|
const paymentTerm = await paymentTermsService.findById(req.params.id, req.tenantId!);
|
|
const invoiceDate = parseResult.data.invoice_date ?? parseResult.data.invoiceDate ?? new Date().toISOString();
|
|
const totalAmount = parseResult.data.total_amount ?? parseResult.data.totalAmount ?? 0;
|
|
const result = paymentTermsService.calculateDueDate(paymentTerm, new Date(invoiceDate), totalAmount);
|
|
res.json({ success: true, data: result });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async getStandardPaymentTerms(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const standardTerms = paymentTermsService.getStandardTerms();
|
|
res.json({ success: true, data: standardTerms });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async initializePaymentTerms(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
await paymentTermsService.initializeForTenant(req.tenantId!, req.user?.userId);
|
|
res.json({ success: true, message: 'Términos de pago inicializados exitosamente' });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
// ========== DISCOUNT RULES ==========
|
|
async getDiscountRules(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const activeOnly = req.query.active === 'true';
|
|
const discountRules = await discountRulesService.findAll(req.tenantId!, activeOnly);
|
|
res.json({ success: true, data: discountRules });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async getDiscountRule(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const discountRule = await discountRulesService.findById(req.params.id, req.tenantId!);
|
|
res.json({ success: true, data: discountRule });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async createDiscountRule(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const parseResult = createDiscountRuleSchema.safeParse(req.body);
|
|
if (!parseResult.success) {
|
|
throw new ValidationError('Datos de regla de descuento inválidos', parseResult.error.errors);
|
|
}
|
|
const dto: CreateDiscountRuleDto = parseResult.data;
|
|
const discountRule = await discountRulesService.create(dto, req.tenantId!, req.user?.userId);
|
|
res.status(201).json({ success: true, data: discountRule, message: 'Regla de descuento creada exitosamente' });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async updateDiscountRule(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const parseResult = updateDiscountRuleSchema.safeParse(req.body);
|
|
if (!parseResult.success) {
|
|
throw new ValidationError('Datos de regla de descuento inválidos', parseResult.error.errors);
|
|
}
|
|
const dto: UpdateDiscountRuleDto = parseResult.data;
|
|
const discountRule = await discountRulesService.update(req.params.id, dto, req.tenantId!, req.user?.userId);
|
|
res.json({ success: true, data: discountRule, message: 'Regla de descuento actualizada exitosamente' });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async deleteDiscountRule(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
await discountRulesService.delete(req.params.id, req.tenantId!, req.user?.userId);
|
|
res.json({ success: true, message: 'Regla de descuento eliminada exitosamente' });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async applyDiscounts(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const parseResult = applyDiscountsSchema.safeParse(req.body);
|
|
if (!parseResult.success) {
|
|
throw new ValidationError('Datos inválidos para aplicar descuentos', parseResult.error.errors);
|
|
}
|
|
const data = parseResult.data;
|
|
const context: ApplyDiscountContext = {
|
|
productId: data.product_id ?? data.productId,
|
|
categoryId: data.category_id ?? data.categoryId,
|
|
customerId: data.customer_id ?? data.customerId,
|
|
customerGroupId: data.customer_group_id ?? data.customerGroupId,
|
|
quantity: data.quantity,
|
|
unitPrice: data.unit_price ?? data.unitPrice ?? 0,
|
|
totalAmount: data.total_amount ?? data.totalAmount ?? 0,
|
|
isFirstPurchase: data.is_first_purchase ?? data.isFirstPurchase,
|
|
};
|
|
const result = await discountRulesService.applyDiscounts(req.tenantId!, context);
|
|
res.json({ success: true, data: result });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async resetDiscountRuleUsage(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const discountRule = await discountRulesService.resetUsageCount(req.params.id, req.tenantId!);
|
|
res.json({ success: true, data: discountRule, message: 'Contador de uso reiniciado exitosamente' });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async getDiscountRulesByProduct(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const discountRules = await discountRulesService.findByProduct(req.params.productId, req.tenantId!);
|
|
res.json({ success: true, data: discountRules });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async getDiscountRulesByCustomer(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const discountRules = await discountRulesService.findByCustomer(req.params.customerId, req.tenantId!);
|
|
res.json({ success: true, data: discountRules });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
}
|
|
|
|
export const coreController = new CoreController();
|