import { Response, NextFunction } from 'express'; import { z } from 'zod'; import { currenciesService, CreateCurrencyDto, UpdateCurrencyDto } from './currencies.service.js'; import { countriesService } from './countries.service.js'; import { statesService, CreateStateDto, UpdateStateDto } from './states.service.js'; import { currencyRatesService, CreateCurrencyRateDto, ConvertCurrencyDto } from './currency-rates.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(), }); // States Schemas const createStateSchema = z.object({ country_id: z.string().uuid().optional(), countryId: z.string().uuid().optional(), code: z.string().min(1).max(10).toUpperCase(), name: z.string().min(1).max(255), timezone: z.string().max(50).optional(), is_active: z.boolean().optional(), isActive: z.boolean().optional(), }).refine((data) => data.country_id !== undefined || data.countryId !== undefined, { message: 'country_id or countryId is required', }); const updateStateSchema = z.object({ name: z.string().min(1).max(255).optional(), timezone: z.string().max(50).optional().nullable(), is_active: z.boolean().optional(), isActive: z.boolean().optional(), }); // Currency Rates Schemas const createCurrencyRateSchema = z.object({ from_currency_code: z.string().length(3).toUpperCase().optional(), fromCurrencyCode: z.string().length(3).toUpperCase().optional(), to_currency_code: z.string().length(3).toUpperCase().optional(), toCurrencyCode: z.string().length(3).toUpperCase().optional(), rate: z.number().positive(), rate_date: z.string().optional(), rateDate: z.string().optional(), source: z.enum(['manual', 'banxico', 'xe', 'openexchange']).optional(), }).refine((data) => data.from_currency_code !== undefined || data.fromCurrencyCode !== undefined, { message: 'from_currency_code or fromCurrencyCode is required', }).refine((data) => data.to_currency_code !== undefined || data.toCurrencyCode !== undefined, { message: 'to_currency_code or toCurrencyCode is required', }); const convertCurrencySchema = z.object({ amount: z.number().min(0), from_currency_code: z.string().length(3).toUpperCase().optional(), fromCurrencyCode: z.string().length(3).toUpperCase().optional(), to_currency_code: z.string().length(3).toUpperCase().optional(), toCurrencyCode: z.string().length(3).toUpperCase().optional(), date: z.string().optional(), }).refine((data) => data.from_currency_code !== undefined || data.fromCurrencyCode !== undefined, { message: 'from_currency_code or fromCurrencyCode is required', }).refine((data) => data.to_currency_code !== undefined || data.toCurrencyCode !== undefined, { message: 'to_currency_code or toCurrencyCode is required', }); class CoreController { // ========== CURRENCIES ========== async getCurrencies(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { 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 { 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 { 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 { 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 { 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 { try { const country = await countriesService.findById(req.params.id); res.json({ success: true, data: country }); } catch (error) { next(error); } } // ========== STATES ========== async getStates(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const filter = { countryId: req.query.country_id as string | undefined, countryCode: req.query.country_code as string | undefined, isActive: req.query.active === 'true' ? true : undefined, }; const states = await statesService.findAll(filter); res.json({ success: true, data: states }); } catch (error) { next(error); } } async getState(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const state = await statesService.findById(req.params.id); res.json({ success: true, data: state }); } catch (error) { next(error); } } async getStatesByCountry(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const states = await statesService.findByCountry(req.params.countryId); res.json({ success: true, data: states }); } catch (error) { next(error); } } async getStatesByCountryCode(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const states = await statesService.findByCountryCode(req.params.countryCode); res.json({ success: true, data: states }); } catch (error) { next(error); } } async createState(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const parseResult = createStateSchema.safeParse(req.body); if (!parseResult.success) { throw new ValidationError('Datos de estado inválidos', parseResult.error.errors); } const data = parseResult.data; const dto: CreateStateDto = { countryId: data.country_id ?? data.countryId!, code: data.code, name: data.name, timezone: data.timezone, isActive: data.is_active ?? data.isActive, }; const state = await statesService.create(dto); res.status(201).json({ success: true, data: state, message: 'Estado creado exitosamente' }); } catch (error) { next(error); } } async updateState(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const parseResult = updateStateSchema.safeParse(req.body); if (!parseResult.success) { throw new ValidationError('Datos de estado inválidos', parseResult.error.errors); } const data = parseResult.data; const dto: UpdateStateDto = { name: data.name, timezone: data.timezone ?? undefined, isActive: data.is_active ?? data.isActive, }; const state = await statesService.update(req.params.id, dto); res.json({ success: true, data: state, message: 'Estado actualizado exitosamente' }); } catch (error) { next(error); } } async deleteState(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { await statesService.delete(req.params.id); res.json({ success: true, message: 'Estado eliminado exitosamente' }); } catch (error) { next(error); } } // ========== CURRENCY RATES ========== async getCurrencyRates(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const filter = { tenantId: req.tenantId, fromCurrencyCode: req.query.from as string | undefined, toCurrencyCode: req.query.to as string | undefined, limit: req.query.limit ? parseInt(req.query.limit as string, 10) : 100, }; const rates = await currencyRatesService.findAll(filter); res.json({ success: true, data: rates }); } catch (error) { next(error); } } async getCurrencyRate(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const rate = await currencyRatesService.findById(req.params.id); res.json({ success: true, data: rate }); } catch (error) { next(error); } } async getLatestRate(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const fromCode = req.params.from.toUpperCase(); const toCode = req.params.to.toUpperCase(); const dateStr = req.query.date as string | undefined; const date = dateStr ? new Date(dateStr) : new Date(); const rate = await currencyRatesService.getRate(fromCode, toCode, date, req.tenantId); if (rate === null) { res.status(404).json({ success: false, message: `No se encontró tipo de cambio para ${fromCode}/${toCode}` }); return; } res.json({ success: true, data: { from: fromCode, to: toCode, rate, date: date.toISOString().split('T')[0] } }); } catch (error) { next(error); } } async createCurrencyRate(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const parseResult = createCurrencyRateSchema.safeParse(req.body); if (!parseResult.success) { throw new ValidationError('Datos de tipo de cambio inválidos', parseResult.error.errors); } const data = parseResult.data; const dto: CreateCurrencyRateDto = { tenantId: req.tenantId, fromCurrencyCode: data.from_currency_code ?? data.fromCurrencyCode!, toCurrencyCode: data.to_currency_code ?? data.toCurrencyCode!, rate: data.rate, rateDate: data.rate_date ?? data.rateDate ? new Date(data.rate_date ?? data.rateDate!) : new Date(), source: data.source, createdBy: req.user?.userId, }; const rate = await currencyRatesService.create(dto); res.status(201).json({ success: true, data: rate, message: 'Tipo de cambio creado exitosamente' }); } catch (error) { next(error); } } async convertCurrency(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const parseResult = convertCurrencySchema.safeParse(req.body); if (!parseResult.success) { throw new ValidationError('Datos de conversión inválidos', parseResult.error.errors); } const data = parseResult.data; const dto: ConvertCurrencyDto = { amount: data.amount, fromCurrencyCode: data.from_currency_code ?? data.fromCurrencyCode!, toCurrencyCode: data.to_currency_code ?? data.toCurrencyCode!, date: data.date ? new Date(data.date) : new Date(), tenantId: req.tenantId, }; const result = await currencyRatesService.convert(dto); if (result === null) { res.status(404).json({ success: false, message: `No se encontró tipo de cambio para ${dto.fromCurrencyCode}/${dto.toCurrencyCode}` }); return; } res.json({ success: true, data: { originalAmount: dto.amount, convertedAmount: result.amount, rate: result.rate, from: dto.fromCurrencyCode, to: dto.toCurrencyCode, } }); } catch (error) { next(error); } } async deleteCurrencyRate(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { await currencyRatesService.delete(req.params.id); res.json({ success: true, message: 'Tipo de cambio eliminado exitosamente' }); } catch (error) { next(error); } } async getCurrencyRateHistory(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const fromCode = req.params.from.toUpperCase(); const toCode = req.params.to.toUpperCase(); const days = req.query.days ? parseInt(req.query.days as string, 10) : 30; const history = await currencyRatesService.getHistory(fromCode, toCode, days, req.tenantId); res.json({ success: true, data: history }); } catch (error) { next(error); } } async getLatestRates(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const baseCurrency = (req.query.base as string) || 'MXN'; const ratesMap = await currencyRatesService.getLatestRates(baseCurrency, req.tenantId); // Convert Map to object for JSON response const rates: Record = {}; ratesMap.forEach((value, key) => { rates[key] = value; }); res.json({ success: true, data: { base: baseCurrency, rates, date: new Date().toISOString().split('T')[0], } }); } catch (error) { next(error); } } // ========== UOM CATEGORIES ========== async getUomCategories(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { 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 { 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 { 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 { 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 { 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 { 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); } } async getUomByCode(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const uom = await uomService.findByCode(req.params.code); if (!uom) { res.status(404).json({ success: false, message: 'Unidad de medida no encontrada' }); return; } res.json({ success: true, data: uom }); } catch (error) { next(error); } } async convertUom(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const { quantity, from_uom_id, fromUomId, to_uom_id, toUomId } = req.body; const fromId = from_uom_id ?? fromUomId; const toId = to_uom_id ?? toUomId; if (!quantity || !fromId || !toId) { throw new ValidationError('Se requiere quantity, from_uom_id y to_uom_id'); } const result = await uomService.convertQuantity(quantity, fromId, toId); const fromUom = await uomService.findById(fromId); const toUom = await uomService.findById(toId); res.json({ success: true, data: { originalQuantity: quantity, originalUom: fromUom.name, convertedQuantity: result, targetUom: toUom.name, } }); } catch (error) { next(error); } } async getUomConversions(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const result = await uomService.getConversionTable(req.params.categoryId); res.json({ success: true, data: result }); } catch (error) { next(error); } } // ========== PRODUCT CATEGORIES ========== async getProductCategories(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { try { const standardTerms = paymentTermsService.getStandardTerms(); res.json({ success: true, data: standardTerms }); } catch (error) { next(error); } } async initializePaymentTerms(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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();