# PLAN DE IMPLEMENTACION - BACKEND **Fecha:** 2025-12-18 **Fase:** 3 - Plan de Implementaciones **Capa:** Backend (Node.js + Express + TypeScript + TypeORM) --- ## 1. RESUMEN EJECUTIVO ### 1.1 Alcance - **Servicios a heredar:** 12 - **Servicios a extender:** 8 - **Servicios nuevos:** 28 - **Controllers nuevos:** 15 - **Middleware nuevo:** 5 ### 1.2 Arquitectura ``` ┌─────────────────────────────────────────────────────────────┐ │ RETAIL BACKEND │ ├─────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │ │ │ Controllers │ │ Middleware │ │ WebSocket Gateway │ │ │ └──────┬──────┘ └──────┬──────┘ └──────────┬──────────┘ │ │ │ │ │ │ │ ┌──────┴──────────────────────────────────────┴──────┐ │ │ │ Services │ │ │ │ ┌──────────────────────────────────────────────┐ │ │ │ │ │ Retail Services (new) │ │ │ │ │ │ POSService, CashService, LoyaltyService... │ │ │ │ │ └──────────────────────────────────────────────┘ │ │ │ │ ┌──────────────────────────────────────────────┐ │ │ │ │ │ Extended Services │ │ │ │ │ │ RetailProductsService extends ProductsService│ │ │ │ │ └──────────────────────────────────────────────┘ │ │ │ │ ┌──────────────────────────────────────────────┐ │ │ │ │ │ Core Services (inherited) │ │ │ │ │ │ AuthService, PartnersService, TaxesService │ │ │ │ │ └──────────────────────────────────────────────┘ │ │ │ └────────────────────────┬───────────────────────────┘ │ │ │ │ │ ┌────────────────────────┴───────────────────────────┐ │ │ │ Repositories │ │ │ │ TypeORM EntityRepository │ │ │ └────────────────────────┬───────────────────────────┘ │ │ │ │ │ ┌────────────────────────┴───────────────────────────┐ │ │ │ PostgreSQL + RLS │ │ │ └─────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ ``` --- ## 2. ESTRUCTURA DE PROYECTO ### 2.1 Estructura de Carpetas ``` retail/backend/ ├── src/ │ ├── config/ │ │ ├── database.ts │ │ ├── redis.ts │ │ └── app.ts │ │ │ ├── modules/ │ │ ├── auth/ # Heredado + extensiones │ │ │ ├── entities/ │ │ │ ├── services/ │ │ │ ├── controllers/ │ │ │ └── middleware/ │ │ │ │ │ ├── branches/ # NUEVO │ │ │ ├── entities/ │ │ │ │ ├── branch.entity.ts │ │ │ │ ├── cash-register.entity.ts │ │ │ │ └── branch-user.entity.ts │ │ │ ├── services/ │ │ │ │ └── branches.service.ts │ │ │ ├── controllers/ │ │ │ │ └── branches.controller.ts │ │ │ └── dto/ │ │ │ │ │ ├── pos/ # NUEVO │ │ │ ├── entities/ │ │ │ │ ├── pos-session.entity.ts │ │ │ │ ├── pos-order.entity.ts │ │ │ │ ├── pos-order-line.entity.ts │ │ │ │ └── pos-payment.entity.ts │ │ │ ├── services/ │ │ │ │ ├── pos-session.service.ts │ │ │ │ ├── pos-order.service.ts │ │ │ │ └── pos-sync.service.ts │ │ │ ├── controllers/ │ │ │ │ └── pos.controller.ts │ │ │ └── gateway/ │ │ │ └── pos.gateway.ts │ │ │ │ │ ├── cash/ # NUEVO │ │ │ ├── entities/ │ │ │ │ ├── cash-movement.entity.ts │ │ │ │ ├── cash-closing.entity.ts │ │ │ │ └── cash-count.entity.ts │ │ │ ├── services/ │ │ │ │ ├── cash-session.service.ts │ │ │ │ └── cash-closing.service.ts │ │ │ └── controllers/ │ │ │ │ │ ├── inventory/ # EXTENDIDO │ │ │ ├── entities/ │ │ │ │ ├── stock-transfer.entity.ts │ │ │ │ └── stock-adjustment.entity.ts │ │ │ ├── services/ │ │ │ │ ├── retail-stock.service.ts # extiende │ │ │ │ ├── transfers.service.ts │ │ │ │ └── adjustments.service.ts │ │ │ └── controllers/ │ │ │ │ │ ├── customers/ # EXTENDIDO │ │ │ ├── entities/ │ │ │ │ ├── loyalty-program.entity.ts │ │ │ │ ├── membership-level.entity.ts │ │ │ │ ├── loyalty-transaction.entity.ts │ │ │ │ └── customer-membership.entity.ts │ │ │ ├── services/ │ │ │ │ ├── retail-customers.service.ts │ │ │ │ └── loyalty.service.ts │ │ │ └── controllers/ │ │ │ │ │ ├── pricing/ # EXTENDIDO │ │ │ ├── entities/ │ │ │ │ ├── promotion.entity.ts │ │ │ │ ├── coupon.entity.ts │ │ │ │ └── coupon-redemption.entity.ts │ │ │ ├── services/ │ │ │ │ ├── price-engine.service.ts │ │ │ │ ├── promotions.service.ts │ │ │ │ └── coupons.service.ts │ │ │ └── controllers/ │ │ │ │ │ ├── purchases/ # EXTENDIDO │ │ │ ├── entities/ │ │ │ │ ├── purchase-suggestion.entity.ts │ │ │ │ ├── supplier-order.entity.ts │ │ │ │ └── goods-receipt.entity.ts │ │ │ ├── services/ │ │ │ │ ├── purchase-suggestions.service.ts │ │ │ │ └── supplier-orders.service.ts │ │ │ └── controllers/ │ │ │ │ │ ├── invoicing/ # NUEVO │ │ │ ├── entities/ │ │ │ │ ├── cfdi-config.entity.ts │ │ │ │ └── cfdi.entity.ts │ │ │ ├── services/ │ │ │ │ ├── cfdi.service.ts │ │ │ │ ├── cfdi-builder.service.ts │ │ │ │ ├── pac.service.ts │ │ │ │ ├── xml.service.ts │ │ │ │ └── pdf.service.ts │ │ │ └── controllers/ │ │ │ ├── cfdi.controller.ts │ │ │ └── autofactura.controller.ts │ │ │ │ │ ├── ecommerce/ # NUEVO │ │ │ ├── entities/ │ │ │ │ ├── cart.entity.ts │ │ │ │ ├── ecommerce-order.entity.ts │ │ │ │ └── shipping-rate.entity.ts │ │ │ ├── services/ │ │ │ │ ├── catalog.service.ts │ │ │ │ ├── cart.service.ts │ │ │ │ ├── checkout.service.ts │ │ │ │ ├── payment-gateway.service.ts │ │ │ │ └── shipping.service.ts │ │ │ └── controllers/ │ │ │ ├── storefront.controller.ts │ │ │ └── ecommerce-admin.controller.ts │ │ │ │ │ └── reports/ # EXTENDIDO │ │ ├── services/ │ │ │ ├── dashboard.service.ts │ │ │ ├── sales-report.service.ts │ │ │ ├── product-report.service.ts │ │ │ └── cash-report.service.ts │ │ └── controllers/ │ │ │ ├── shared/ │ │ ├── entities/ │ │ │ └── base.entity.ts │ │ ├── services/ │ │ │ └── base.service.ts │ │ ├── dto/ │ │ │ └── pagination.dto.ts │ │ ├── interfaces/ │ │ │ └── tenant-context.interface.ts │ │ └── utils/ │ │ ├── sequence.util.ts │ │ └── decimal.util.ts │ │ │ ├── middleware/ │ │ ├── tenant.middleware.ts │ │ ├── auth.middleware.ts │ │ ├── branch.middleware.ts │ │ └── error.middleware.ts │ │ │ ├── integrations/ │ │ ├── pac/ │ │ │ ├── finkok.provider.ts │ │ │ ├── facturama.provider.ts │ │ │ └── pac.interface.ts │ │ ├── payments/ │ │ │ ├── stripe.provider.ts │ │ │ ├── conekta.provider.ts │ │ │ └── payment.interface.ts │ │ └── shipping/ │ │ ├── fedex.provider.ts │ │ └── shipping.interface.ts │ │ │ ├── migrations/ │ │ └── ... │ │ │ └── server.ts │ ├── package.json ├── tsconfig.json └── .env.example ``` --- ## 3. SERVICIOS POR MODULO ### 3.1 RT-001 Fundamentos (Herencia 100%) ```typescript // HEREDADOS - Solo configurar importaciones import { AuthService } from '@erp-core/auth'; import { UsersService } from '@erp-core/users'; import { TenantsService } from '@erp-core/tenants'; import { RolesService } from '@erp-core/roles'; ``` **Servicios:** | Servicio | Accion | Fuente | |----------|--------|--------| | AuthService | HEREDAR | @erp-core/auth | | UsersService | HEREDAR | @erp-core/users | | TenantsService | HEREDAR | @erp-core/tenants | | RolesService | HEREDAR | @erp-core/roles | --- ### 3.2 RT-002 POS (20% herencia) **Servicios Nuevos:** ```typescript // 1. POSSessionService @Injectable() export class POSSessionService { constructor( @InjectRepository(POSSession) private sessionRepo: Repository, private cashRegisterService: CashRegisterService, ) {} async openSession(dto: OpenSessionDto): Promise { // 1. Verificar caja disponible // 2. Crear sesion // 3. Registrar apertura return session; } async closeSession(sessionId: string, dto: CloseSessionDto): Promise { // 1. Calcular totales esperados // 2. Validar declaracion // 3. Crear corte // 4. Cerrar sesion return session; } async getActiveSession(userId: string): Promise { return this.sessionRepo.findOne({ where: { userId, status: In(['opening', 'open']) } }); } } // 2. POSOrderService @Injectable() export class POSOrderService { constructor( @InjectRepository(POSOrder) private orderRepo: Repository, private priceEngine: PriceEngineService, private stockService: RetailStockService, private loyaltyService: LoyaltyService, ) {} async createOrder(sessionId: string): Promise { const orderNumber = await this.generateOrderNumber(); return this.orderRepo.save({ sessionId, orderNumber, status: 'draft', }); } async addLine(orderId: string, dto: AddLineDto): Promise { // 1. Calcular precio con motor de precios const priceResult = await this.priceEngine.calculatePrice({ productId: dto.productId, quantity: dto.quantity, branchId: order.session.branchId, }); // 2. Crear linea // 3. Recalcular totales return line; } async confirmOrder(orderId: string, dto: ConfirmOrderDto): Promise { // 1. Validar stock // 2. Procesar pagos // 3. Descontar inventario // 4. Otorgar puntos lealtad // 5. Aplicar cupon si existe // 6. Marcar como done return order; } async refundOrder(orderId: string, dto: RefundDto): Promise { // 1. Validar orden // 2. Revertir inventario // 3. Revertir puntos // 4. Crear registro de devolucion return order; } } // 3. POSSyncService (para offline) @Injectable() export class POSSyncService { constructor( private orderService: POSOrderService, private redis: Redis, ) {} async syncOfflineOrders(orders: OfflineOrder[]): Promise { const results: SyncResult = { synced: [], failed: [] }; for (const offlineOrder of orders) { try { // Verificar si ya fue sincronizada const exists = await this.orderService.findByOfflineId(offlineOrder.offlineId); if (exists) { results.synced.push({ offlineId: offlineOrder.offlineId, orderId: exists.id }); continue; } // Crear orden const order = await this.orderService.createFromOffline(offlineOrder); results.synced.push({ offlineId: offlineOrder.offlineId, orderId: order.id }); } catch (error) { results.failed.push({ offlineId: offlineOrder.offlineId, error: error.message }); } } return results; } } ``` **Servicios:** | Servicio | Accion | Dependencias | |----------|--------|--------------| | POSSessionService | NUEVO | CashRegisterService | | POSOrderService | NUEVO | PriceEngineService, RetailStockService, LoyaltyService | | POSPaymentService | NUEVO | - | | POSSyncService | NUEVO | POSOrderService, Redis | --- ### 3.3 RT-003 Inventario (60% herencia) **Servicios Extendidos:** ```typescript // 1. RetailStockService (extiende StockService del core) @Injectable() export class RetailStockService extends StockService { constructor( @InjectRepository(StockQuant) stockQuantRepo: Repository, @InjectRepository(Branch) private branchRepo: Repository, ) { super(stockQuantRepo); } // Metodos heredados: getStock, reserveStock, etc. // Metodos nuevos para retail async getStockByBranch(branchId: string, productId?: string): Promise { const branch = await this.branchRepo.findOne({ where: { id: branchId }, relations: ['warehouse'], }); return this.stockQuantRepo.find({ where: { warehouseId: branch.warehouse.id, ...(productId && { productId }), }, }); } async getMultiBranchStock(productId: string): Promise { // Stock del producto en todas las sucursales return this.stockQuantRepo .createQueryBuilder('sq') .select(['b.id as branchId', 'b.name as branchName', 'sq.quantity']) .innerJoin('retail.branches', 'b', 'b.warehouse_id = sq.warehouse_id') .where('sq.product_id = :productId', { productId }) .getRawMany(); } async decrementStock(branchId: string, productId: string, quantity: number): Promise { const branch = await this.branchRepo.findOne({ where: { id: branchId } }); await this.stockQuantRepo.decrement( { warehouseId: branch.warehouseId, productId }, 'quantity', quantity ); } } // 2. TransfersService @Injectable() export class TransfersService { async createTransfer(dto: CreateTransferDto): Promise { const transfer = await this.transferRepo.save({ ...dto, transferNumber: await this.generateNumber('TRF'), status: 'draft', }); return transfer; } async confirmTransfer(id: string): Promise { // 1. Validar stock en origen // 2. Reservar stock // 3. Cambiar status a 'in_transit' return transfer; } async receiveTransfer(id: string, dto: ReceiveDto): Promise { // 1. Descontar de origen // 2. Agregar a destino // 3. Registrar diferencias si las hay // 4. Cambiar status a 'received' return transfer; } } // 3. AdjustmentsService @Injectable() export class AdjustmentsService { async createAdjustment(dto: CreateAdjustmentDto): Promise { // Similar a transferencias } async confirmAdjustment(id: string): Promise { // Aplicar diferencias al stock } } ``` **Servicios:** | Servicio | Accion | Hereda de | |----------|--------|-----------| | RetailStockService | EXTENDER | StockService | | TransfersService | NUEVO | - | | AdjustmentsService | NUEVO | - | --- ### 3.4 RT-004 Compras (80% herencia) **Servicios Extendidos:** ```typescript // 1. RetailPurchaseService (extiende PurchaseService) @Injectable() export class RetailPurchaseService extends PurchaseService { // Hereda: createOrder, confirmOrder, receiveGoods async suggestRestock(branchId: string): Promise { // Algoritmo de sugerencia: // 1. Productos con stock < minimo // 2. Prioridad basada en dias de stock // 3. Cantidad sugerida basada en historico de ventas const suggestions = await this.stockRepo .createQueryBuilder('sq') .select([ 'sq.product_id', 'sq.quantity as current_stock', 'p.min_stock', 'p.max_stock', 'p.default_supplier_id', ]) .innerJoin('inventory.products', 'p', 'p.id = sq.product_id') .innerJoin('retail.branches', 'b', 'b.warehouse_id = sq.warehouse_id') .where('b.id = :branchId', { branchId }) .andWhere('sq.quantity <= p.min_stock') .getRawMany(); return suggestions.map(s => ({ productId: s.product_id, currentStock: s.current_stock, minStock: s.min_stock, maxStock: s.max_stock, suggestedQty: s.max_stock - s.current_stock, supplierId: s.default_supplier_id, priority: this.calculatePriority(s), })); } private calculatePriority(s: any): 'critical' | 'high' | 'medium' | 'low' { const daysOfStock = s.current_stock / s.avg_daily_sales; if (daysOfStock <= 0) return 'critical'; if (daysOfStock <= 3) return 'high'; if (daysOfStock <= 7) return 'medium'; return 'low'; } } // 2. GoodsReceiptService @Injectable() export class GoodsReceiptService { async createReceipt(dto: CreateReceiptDto): Promise { // 1. Crear recepcion // 2. Vincular con orden de compra si existe return receipt; } async confirmReceipt(id: string): Promise { // 1. Agregar stock al warehouse de la sucursal // 2. Actualizar costos si es necesario // 3. Actualizar orden de compra return receipt; } } ``` --- ### 3.5 RT-005 Clientes (40% herencia) **Servicios Extendidos:** ```typescript // 1. RetailCustomersService (extiende PartnersService) @Injectable() export class RetailCustomersService extends PartnersService { constructor( partnersRepo: Repository, private membershipRepo: Repository, private loyaltyService: LoyaltyService, ) { super(partnersRepo); } async createCustomer(dto: CreateCustomerDto): Promise { // 1. Crear partner base const customer = await super.create({ ...dto, type: 'customer' }); // 2. Inscribir en programa de lealtad si esta activo if (dto.enrollInLoyalty) { await this.loyaltyService.enrollCustomer(customer.id); } return customer; } async getCustomerWithMembership(customerId: string): Promise { const customer = await this.findById(customerId); const membership = await this.membershipRepo.findOne({ where: { customerId }, relations: ['program', 'level'], }); return { ...customer, membership }; } } // 2. LoyaltyService @Injectable() export class LoyaltyService { async enrollCustomer(customerId: string, programId?: string): Promise { const program = programId ? await this.programRepo.findOne({ where: { id: programId } }) : await this.programRepo.findOne({ where: { isActive: true } }); const membershipNumber = await this.generateMembershipNumber(); return this.membershipRepo.save({ customerId, programId: program.id, membershipNumber, currentPoints: 0, lifetimePoints: 0, status: 'active', }); } async earnPoints(customerId: string, orderId: string, amount: number): Promise { const membership = await this.membershipRepo.findOne({ where: { customerId }, relations: ['program', 'level'], }); if (!membership) return null; // Calcular puntos con multiplicador de nivel const basePoints = Math.floor(amount * membership.program.pointsPerCurrency); const multiplier = membership.level?.pointsMultiplier || 1; const points = Math.floor(basePoints * multiplier); // Crear transaccion const transaction = await this.transactionRepo.save({ customerId, programId: membership.programId, orderId, transactionType: 'earn', points, balanceAfter: membership.currentPoints + points, expiresAt: this.calculateExpiry(), }); // Actualizar balance await this.membershipRepo.update(membership.id, { currentPoints: () => `current_points + ${points}`, lifetimePoints: () => `lifetime_points + ${points}`, lastActivityAt: new Date(), }); // Verificar upgrade de nivel await this.checkLevelUpgrade(membership); return transaction; } async redeemPoints(customerId: string, orderId: string, points: number): Promise { const membership = await this.membershipRepo.findOne({ where: { customerId }, relations: ['program'], }); // Validaciones if (points > membership.currentPoints) { throw new BadRequestException('Insufficient points'); } if (points < membership.program.minPointsRedeem) { throw new BadRequestException(`Minimum redemption is ${membership.program.minPointsRedeem} points`); } // Calcular descuento const discount = points * membership.program.currencyPerPoint; // Crear transaccion await this.transactionRepo.save({ customerId, programId: membership.programId, orderId, transactionType: 'redeem', points: -points, balanceAfter: membership.currentPoints - points, }); // Actualizar balance await this.membershipRepo.decrement({ id: membership.id }, 'currentPoints', points); return { points, discount }; } private async checkLevelUpgrade(membership: CustomerMembership): Promise { const newLevel = await this.levelRepo .createQueryBuilder('level') .where('level.program_id = :programId', { programId: membership.programId }) .andWhere('level.min_points <= :points', { points: membership.lifetimePoints }) .orderBy('level.min_points', 'DESC') .getOne(); if (newLevel && newLevel.id !== membership.levelId) { await this.membershipRepo.update(membership.id, { levelId: newLevel.id }); // TODO: Notificar al cliente del upgrade } } } ``` --- ### 3.6 RT-006 Precios (30% herencia) **Servicios Nuevos y Extendidos:** ```typescript // 1. PriceEngineService (CORE - Motor de precios) @Injectable() export class PriceEngineService { constructor( private pricelistService: PricelistsService, private promotionsService: PromotionsService, private couponsService: CouponsService, private loyaltyService: LoyaltyService, ) {} async calculatePrice(context: PriceContext): Promise { const { productId, quantity, branchId, customerId, couponCode } = context; // 1. Precio base del producto const product = await this.productRepo.findOne({ where: { id: productId } }); const basePrice = product.salePrice; // 2. Aplicar lista de precios (si existe para el canal) const pricelistPrice = await this.applyPricelist(basePrice, context); // 3. Evaluar promociones activas const promotions = await this.promotionsService.getActiveForProduct(productId, branchId); const bestPromotion = this.selectBestPromotion(promotions, pricelistPrice, quantity); const promotionDiscount = bestPromotion?.discount || 0; // 4. Calcular precio despues de promocion let finalPrice = pricelistPrice - promotionDiscount; // 5. Aplicar cupon si existe let couponDiscount = 0; if (couponCode) { const couponResult = await this.couponsService.calculateDiscount(couponCode, finalPrice); couponDiscount = couponResult.discount; finalPrice -= couponDiscount; } // 6. Calcular descuento por puntos de lealtad (si aplica) let loyaltyDiscount = 0; if (context.loyaltyPointsToUse && customerId) { const loyaltyResult = await this.loyaltyService.calculateRedemption( customerId, context.loyaltyPointsToUse ); loyaltyDiscount = loyaltyResult.discount; finalPrice -= loyaltyDiscount; } // 7. Calcular impuestos const taxes = await this.calculateTaxes(finalPrice, product.taxIds); return { basePrice, pricelistPrice, discounts: [ ...(bestPromotion ? [{ type: 'promotion', name: bestPromotion.name, amount: promotionDiscount }] : []), ...(couponDiscount > 0 ? [{ type: 'coupon', name: couponCode, amount: couponDiscount }] : []), ...(loyaltyDiscount > 0 ? [{ type: 'loyalty', name: 'Puntos', amount: loyaltyDiscount }] : []), ], subtotal: finalPrice, taxes, total: finalPrice + taxes.reduce((sum, t) => sum + t.amount, 0), }; } private selectBestPromotion( promotions: Promotion[], price: number, quantity: number ): Promotion | null { const applicable = promotions.filter(p => this.isPromotionApplicable(p, price, quantity)); if (applicable.length === 0) return null; // Ordenar por descuento mayor return applicable.sort((a, b) => { const discountA = this.calculatePromotionDiscount(a, price, quantity); const discountB = this.calculatePromotionDiscount(b, price, quantity); return discountB - discountA; })[0]; } private calculatePromotionDiscount(promo: Promotion, price: number, qty: number): number { switch (promo.promotionType) { case 'percentage': return price * (promo.discountValue / 100); case 'fixed_amount': return Math.min(promo.discountValue, price); case 'buy_x_get_y': const freeItems = Math.floor(qty / promo.buyQuantity) * (promo.buyQuantity - promo.getQuantity); return (price / qty) * freeItems; default: return 0; } } } // 2. PromotionsService @Injectable() export class PromotionsService { async create(dto: CreatePromotionDto): Promise { const promotion = await this.promotionRepo.save({ ...dto, code: dto.code || this.generateCode(), }); // Agregar productos si se especificaron if (dto.productIds?.length) { await this.addProducts(promotion.id, dto.productIds); } return promotion; } async getActiveForProduct(productId: string, branchId: string): Promise { const today = new Date(); return this.promotionRepo .createQueryBuilder('p') .leftJoin('retail.promotion_products', 'pp', 'pp.promotion_id = p.id') .where('p.is_active = true') .andWhere('p.start_date <= :today', { today }) .andWhere('p.end_date >= :today', { today }) .andWhere('(p.applies_to_all = true OR pp.product_id = :productId)', { productId }) .andWhere('(p.branch_ids IS NULL OR :branchId = ANY(p.branch_ids))', { branchId }) .andWhere('(p.max_uses IS NULL OR p.current_uses < p.max_uses)') .getMany(); } async incrementUse(promotionId: string): Promise { await this.promotionRepo.increment({ id: promotionId }, 'currentUses', 1); } } // 3. CouponsService @Injectable() export class CouponsService { async generate(dto: GenerateCouponsDto): Promise { const coupons: Coupon[] = []; for (let i = 0; i < dto.quantity; i++) { coupons.push(await this.couponRepo.save({ code: this.generateCode(dto.prefix), couponType: dto.couponType, discountValue: dto.discountValue, minPurchase: dto.minPurchase, maxDiscount: dto.maxDiscount, validFrom: dto.validFrom, validUntil: dto.validUntil, maxUses: dto.maxUses || 1, customerId: dto.customerId, })); } return coupons; } async validate(code: string, orderTotal: number): Promise { const coupon = await this.couponRepo.findOne({ where: { code } }); if (!coupon) return { valid: false, error: 'Cupon no encontrado' }; if (!coupon.isActive) return { valid: false, error: 'Cupon inactivo' }; if (coupon.timesUsed >= coupon.maxUses) return { valid: false, error: 'Cupon agotado' }; if (new Date() < coupon.validFrom) return { valid: false, error: 'Cupon no vigente' }; if (new Date() > coupon.validUntil) return { valid: false, error: 'Cupon expirado' }; if (coupon.minPurchase && orderTotal < coupon.minPurchase) { return { valid: false, error: `Compra minima: $${coupon.minPurchase}` }; } const discount = this.calculateDiscount(coupon, orderTotal); return { valid: true, coupon, discount }; } async redeem(code: string, orderId: string, discount: number): Promise { const coupon = await this.couponRepo.findOne({ where: { code } }); await this.couponRepo.increment({ id: coupon.id }, 'timesUsed', 1); return this.redemptionRepo.save({ couponId: coupon.id, orderId, discountApplied: discount, }); } private calculateDiscount(coupon: Coupon, total: number): number { let discount = coupon.couponType === 'percentage' ? total * (coupon.discountValue / 100) : coupon.discountValue; if (coupon.maxDiscount) { discount = Math.min(discount, coupon.maxDiscount); } return discount; } } ``` --- ### 3.7 RT-007 Caja (10% herencia) **Servicios Nuevos:** ```typescript // 1. CashSessionService @Injectable() export class CashSessionService { async getSummary(sessionId: string): Promise { const session = await this.sessionRepo.findOne({ where: { id: sessionId }, relations: ['orders', 'orders.payments', 'movements'], }); // Calcular esperados const cashSales = session.orders .filter(o => o.status === 'done') .flatMap(o => o.payments) .filter(p => p.paymentMethod === 'cash') .reduce((sum, p) => sum + Number(p.amount), 0); const changeGiven = session.orders .filter(o => o.status === 'done') .flatMap(o => o.payments) .filter(p => p.paymentMethod === 'cash') .reduce((sum, p) => sum + Number(p.changeAmount || 0), 0); const cashIn = session.movements .filter(m => m.movementType === 'in') .reduce((sum, m) => sum + Number(m.amount), 0); const cashOut = session.movements .filter(m => m.movementType === 'out') .reduce((sum, m) => sum + Number(m.amount), 0); const cardSales = session.orders .filter(o => o.status === 'done') .flatMap(o => o.payments) .filter(p => p.paymentMethod === 'card') .reduce((sum, p) => sum + Number(p.amount), 0); const transferSales = session.orders .filter(o => o.status === 'done') .flatMap(o => o.payments) .filter(p => p.paymentMethod === 'transfer') .reduce((sum, p) => sum + Number(p.amount), 0); const expectedCash = Number(session.openingBalance) + cashSales - changeGiven + cashIn - cashOut; return { sessionId, openingBalance: session.openingBalance, cashSales, changeGiven, cashIn, cashOut, expectedCash, cardSales, transferSales, totalOrders: session.orders.filter(o => o.status === 'done').length, }; } } // 2. CashMovementService @Injectable() export class CashMovementService { async createMovement(dto: CreateMovementDto): Promise { const session = await this.sessionRepo.findOne({ where: { id: dto.sessionId, status: 'open' }, }); if (!session) { throw new BadRequestException('Session not found or not open'); } // Si es retiro alto, requerir autorizacion if (dto.movementType === 'out' && dto.amount > this.config.maxWithdrawalWithoutAuth) { if (!dto.authorizedBy) { throw new BadRequestException('Authorization required for this amount'); } // Validar que el autorizador tenga permiso await this.validateAuthorizer(dto.authorizedBy); } return this.movementRepo.save({ sessionId: dto.sessionId, movementType: dto.movementType, amount: dto.amount, reason: dto.reason, notes: dto.notes, authorizedBy: dto.authorizedBy, }); } } // 3. CashClosingService @Injectable() export class CashClosingService { async prepareClosing(sessionId: string): Promise { const summary = await this.sessionService.getSummary(sessionId); return { ...summary, denominations: this.getDenominationTemplate(), }; } async createClosing(sessionId: string, dto: CreateClosingDto): Promise { const summary = await this.sessionService.getSummary(sessionId); // Calcular total declarado de denominaciones const declaredFromDenominations = this.calculateDenominations(dto.denominationDetail); // Validar que coincida con el declarado if (Math.abs(declaredFromDenominations - dto.declaredCash) > 0.01) { throw new BadRequestException('Denomination detail does not match declared cash'); } const closing = await this.closingRepo.save({ sessionId, closingDate: new Date(), expectedCash: summary.expectedCash, expectedCard: summary.cardSales, expectedTransfer: summary.transferSales, declaredCash: dto.declaredCash, declaredCard: dto.declaredCard, declaredTransfer: dto.declaredTransfer, denominationDetail: dto.denominationDetail, notes: dto.notes, closedBy: dto.userId, }); // Cerrar sesion await this.sessionRepo.update(sessionId, { status: 'closed', closingBalance: dto.declaredCash, closedAt: new Date(), }); // Si hay diferencia significativa, marcar para aprobacion const tolerance = this.config.cashDifferenceTolerance || 10; if (Math.abs(closing.cashDifference) > tolerance) { // TODO: Notificar a supervisor } return closing; } private calculateDenominations(detail: DenominationDetail): number { let total = 0; for (const [denom, count] of Object.entries(detail.bills)) { total += Number(denom) * count; } for (const [denom, count] of Object.entries(detail.coins)) { total += Number(denom) * count; } return total; } private getDenominationTemplate(): DenominationTemplate { return { bills: { '1000': 0, '500': 0, '200': 0, '100': 0, '50': 0, '20': 0 }, coins: { '20': 0, '10': 0, '5': 0, '2': 0, '1': 0, '0.50': 0 }, }; } } ``` --- ### 3.8 RT-008 Reportes (70% herencia) **Servicios Extendidos:** ```typescript // 1. DashboardService @Injectable() export class DashboardService { async getDashboard(branchId?: string): Promise { const today = new Date(); today.setHours(0, 0, 0, 0); const baseWhere = branchId ? { branchId } : {}; // KPIs del dia const todayStats = await this.getStatsForPeriod(today, new Date(), branchId); // Comparativos const yesterday = new Date(today); yesterday.setDate(yesterday.getDate() - 1); const lastWeek = new Date(today); lastWeek.setDate(lastWeek.getDate() - 7); const lastMonth = new Date(today); lastMonth.setMonth(lastMonth.getMonth() - 1); const yesterdayStats = await this.getStatsForPeriod(yesterday, today, branchId); const lastWeekStats = await this.getStatsForPeriod(lastWeek, new Date(lastWeek.getTime() + 86400000), branchId); // Graficos const salesByHour = await this.getSalesByHour(today, branchId); const salesByPaymentMethod = await this.getSalesByPaymentMethod(today, branchId); const topProducts = await this.getTopProducts(today, branchId, 10); // Alertas const lowStockCount = await this.stockService.getLowStockCount(branchId); const pendingTransfers = await this.transferService.getPendingCount(branchId); return { today: todayStats, comparison: { vsYesterday: this.calculateChange(todayStats.totalSales, yesterdayStats.totalSales), vsLastWeek: this.calculateChange(todayStats.totalSales, lastWeekStats.totalSales), }, charts: { salesByHour, salesByPaymentMethod, topProducts, }, alerts: { lowStock: lowStockCount, pendingTransfers, }, }; } private async getStatsForPeriod(from: Date, to: Date, branchId?: string) { // Usar vista materializada si es de dias anteriores const result = await this.orderRepo .createQueryBuilder('o') .select([ 'SUM(o.total) as totalSales', 'COUNT(*) as totalTransactions', 'AVG(o.total) as avgTicket', ]) .innerJoin('retail.pos_sessions', 's', 's.id = o.session_id') .where('o.order_date >= :from', { from }) .andWhere('o.order_date < :to', { to }) .andWhere('o.status = :status', { status: 'done' }) .andWhere(branchId ? 's.branch_id = :branchId' : '1=1', { branchId }) .getRawOne(); return { totalSales: Number(result.totalSales) || 0, totalTransactions: Number(result.totalTransactions) || 0, avgTicket: Number(result.avgTicket) || 0, }; } } // 2. SalesReportService @Injectable() export class SalesReportService extends ReportsService { async generate(filters: SalesReportFilters): Promise { const { from, to, branchId, groupBy } = filters; let query = this.orderRepo .createQueryBuilder('o') .innerJoin('retail.pos_sessions', 's', 's.id = o.session_id') .where('o.order_date >= :from', { from }) .andWhere('o.order_date <= :to', { to }) .andWhere('o.status = :status', { status: 'done' }); if (branchId) { query = query.andWhere('s.branch_id = :branchId', { branchId }); } // Agrupar segun criterio switch (groupBy) { case 'day': query = query.select([ 'DATE(o.order_date) as date', 'SUM(o.total) as totalSales', 'COUNT(*) as transactions', ]).groupBy('DATE(o.order_date)'); break; case 'branch': query = query.select([ 's.branch_id', 'b.name as branchName', 'SUM(o.total) as totalSales', 'COUNT(*) as transactions', ]) .innerJoin('retail.branches', 'b', 'b.id = s.branch_id') .groupBy('s.branch_id, b.name'); break; // ... otros casos } const details = await query.getRawMany(); // Calcular sumario const summary = { totalSales: details.reduce((sum, d) => sum + Number(d.totalSales), 0), totalTransactions: details.reduce((sum, d) => sum + Number(d.transactions), 0), avgTicket: 0, }; summary.avgTicket = summary.totalSales / summary.totalTransactions || 0; return { period: { from, to }, groupBy, summary, details, }; } } ``` --- ### 3.9 RT-009 E-commerce (20% herencia) **Servicios Nuevos:** ```typescript // 1. CatalogService @Injectable() export class CatalogService { async search(filters: CatalogFilters): Promise { let query = this.productRepo .createQueryBuilder('p') .where('p.is_active = true') .andWhere('p.is_sellable = true'); if (filters.categoryId) { query = query.andWhere('p.category_id = :categoryId', { categoryId: filters.categoryId }); } if (filters.search) { query = query.andWhere( '(p.name ILIKE :search OR p.default_code ILIKE :search)', { search: `%${filters.search}%` } ); } if (filters.minPrice) { query = query.andWhere('p.sale_price >= :minPrice', { minPrice: filters.minPrice }); } if (filters.maxPrice) { query = query.andWhere('p.sale_price <= :maxPrice', { maxPrice: filters.maxPrice }); } // Ordenamiento const orderBy = filters.sortBy || 'name'; const orderDir = filters.sortDir || 'ASC'; query = query.orderBy(`p.${orderBy}`, orderDir); // Paginacion const [items, total] = await query .skip(filters.offset || 0) .take(filters.limit || 20) .getManyAndCount(); return { items, total, page: Math.floor((filters.offset || 0) / (filters.limit || 20)) + 1 }; } async getProduct(productId: string): Promise { const product = await this.productRepo.findOne({ where: { id: productId }, relations: ['category', 'images'], }); // Stock disponible const stock = await this.stockService.getAvailableStock(productId); // Promociones activas const promotions = await this.promotionsService.getActiveForProduct(productId); return { ...product, stock, promotions }; } } // 2. CartService @Injectable() export class CartService { async getOrCreateCart(customerId?: string, sessionId?: string): Promise { let cart: Cart; if (customerId) { cart = await this.cartRepo.findOne({ where: { customerId }, relations: ['items', 'items.product'], }); } else if (sessionId) { cart = await this.cartRepo.findOne({ where: { sessionId }, relations: ['items', 'items.product'], }); } if (!cart) { cart = await this.cartRepo.save({ customerId, sessionId, subtotal: 0, expiresAt: this.calculateExpiry(), }); cart.items = []; } return cart; } async addItem(cartId: string, dto: AddItemDto): Promise { const cart = await this.cartRepo.findOne({ where: { id: cartId }, relations: ['items'], }); // Verificar stock const stock = await this.stockService.getAvailableStock(dto.productId); if (stock < dto.quantity) { throw new BadRequestException('Insufficient stock'); } // Obtener precio const product = await this.productRepo.findOne({ where: { id: dto.productId } }); const price = product.salePrice; // Buscar si ya existe en carrito const existingItem = cart.items.find(i => i.productId === dto.productId); if (existingItem) { existingItem.quantity += dto.quantity; existingItem.total = existingItem.quantity * existingItem.unitPrice; await this.cartItemRepo.save(existingItem); } else { await this.cartItemRepo.save({ cartId, productId: dto.productId, quantity: dto.quantity, unitPrice: price, total: dto.quantity * price, }); } // Recalcular subtotal await this.recalculateSubtotal(cartId); return this.getCart(cartId); } async updateItem(cartId: string, itemId: string, quantity: number): Promise { if (quantity <= 0) { await this.cartItemRepo.delete(itemId); } else { const item = await this.cartItemRepo.findOne({ where: { id: itemId } }); item.quantity = quantity; item.total = quantity * item.unitPrice; await this.cartItemRepo.save(item); } await this.recalculateSubtotal(cartId); return this.getCart(cartId); } private async recalculateSubtotal(cartId: string): Promise { const items = await this.cartItemRepo.find({ where: { cartId } }); const subtotal = items.reduce((sum, i) => sum + Number(i.total), 0); await this.cartRepo.update(cartId, { subtotal, updatedAt: new Date() }); } } // 3. CheckoutService @Injectable() export class CheckoutService { async validate(cartId: string, dto: CheckoutDto): Promise { const cart = await this.cartService.getCart(cartId); const errors: string[] = []; // Validar items for (const item of cart.items) { const stock = await this.stockService.getAvailableStock(item.productId); if (stock < item.quantity) { errors.push(`Stock insuficiente para ${item.product.name}`); } } // Validar direccion si es envio if (dto.deliveryMethod === 'shipping' && !dto.shippingAddress) { errors.push('Direccion de envio requerida'); } // Validar sucursal si es pickup if (dto.deliveryMethod === 'pickup' && !dto.pickupBranchId) { errors.push('Sucursal de recoleccion requerida'); } return { valid: errors.length === 0, errors, summary: await this.calculateTotals(cart, dto), }; } async complete(cartId: string, dto: CompleteCheckoutDto): Promise { const cart = await this.cartService.getCart(cartId); // Validar una vez mas const validation = await this.validate(cartId, dto); if (!validation.valid) { throw new BadRequestException(validation.errors.join(', ')); } // Crear orden const order = await this.orderRepo.save({ orderNumber: await this.generateOrderNumber(), customerId: cart.customerId || dto.customerId, status: 'pending', orderDate: new Date(), subtotal: cart.subtotal, discountAmount: validation.summary.discountAmount, shippingCost: validation.summary.shippingCost, taxAmount: validation.summary.taxAmount, total: validation.summary.total, paymentStatus: 'pending', deliveryMethod: dto.deliveryMethod, pickupBranchId: dto.pickupBranchId, shippingAddress: dto.shippingAddress, }); // Crear lineas for (const item of cart.items) { await this.orderLineRepo.save({ orderId: order.id, productId: item.productId, productName: item.product.name, quantity: item.quantity, unitPrice: item.unitPrice, total: item.total, }); } // Reservar stock for (const item of cart.items) { await this.stockService.reserveStock(item.productId, item.quantity); } // Limpiar carrito await this.cartService.clear(cartId); return order; } } // 4. PaymentGatewayService @Injectable() export class PaymentGatewayService { private gateways: Map; constructor( private stripeGateway: StripeGateway, private conektaGateway: ConektaGateway, private mercadoPagoGateway: MercadoPagoGateway, ) { this.gateways = new Map([ ['stripe', stripeGateway], ['conekta', conektaGateway], ['mercadopago', mercadoPagoGateway], ]); } async createPayment(orderId: string, gateway: string): Promise { const order = await this.orderRepo.findOne({ where: { id: orderId } }); const provider = this.gateways.get(gateway); if (!provider) { throw new BadRequestException(`Gateway ${gateway} not supported`); } return provider.createPayment(order); } async handleWebhook(gateway: string, payload: any): Promise { const provider = this.gateways.get(gateway); const event = await provider.parseWebhook(payload); switch (event.type) { case 'payment.succeeded': await this.handlePaymentSuccess(event.orderId); break; case 'payment.failed': await this.handlePaymentFailed(event.orderId); break; } } private async handlePaymentSuccess(orderId: string): Promise { await this.orderRepo.update(orderId, { paymentStatus: 'paid', status: 'paid', }); // Confirmar stock (quitar reserva y decrementar) const lines = await this.orderLineRepo.find({ where: { orderId } }); for (const line of lines) { await this.stockService.confirmReservation(line.productId, line.quantity); } // TODO: Enviar email de confirmacion } } ``` --- ### 3.10 RT-010 Facturacion (60% herencia) **Servicios Nuevos:** ```typescript // 1. CFDIService @Injectable() export class CFDIService { constructor( private builderService: CFDIBuilderService, private pacService: PACService, private xmlService: XMLService, private pdfService: PDFService, ) {} async generateFromPOS(orderId: string, dto: CFDIRequestDto): Promise { // 1. Obtener orden const order = await this.posOrderRepo.findOne({ where: { id: orderId }, relations: ['lines', 'lines.product'], }); // 2. Construir CFDI const cfdiData = await this.builderService.fromPOSOrder(order, dto); // 3. Generar XML const xml = this.xmlService.buildXML(cfdiData); // 4. Firmar con certificado const signedXml = await this.xmlService.sign(xml); // 5. Timbrar con PAC const timbrado = await this.pacService.timbrar(signedXml); // 6. Guardar CFDI const cfdi = await this.cfdiRepo.save({ sourceType: 'pos_order', sourceId: orderId, serie: cfdiData.serie, folio: cfdiData.folio, uuid: timbrado.uuid, fechaEmision: new Date(), tipoComprobante: 'I', formaPago: dto.formaPago, metodoPago: dto.metodoPago, receptorRfc: dto.receptorRfc, receptorNombre: dto.receptorNombre, receptorRegimen: dto.receptorRegimen, receptorCp: dto.receptorCp, usoCfdi: dto.usoCfdi, subtotal: order.subtotal, descuento: order.discountAmount, totalImpuestos: order.taxAmount, total: order.total, status: 'vigente', xmlContent: timbrado.xml, fechaTimbrado: timbrado.fechaTimbrado, rfcPac: timbrado.rfcProvCertif, selloCfd: timbrado.selloCFD, selloSat: timbrado.selloSAT, noCertificadoSat: timbrado.noCertificadoSAT, }); // 7. Generar PDF const pdf = await this.pdfService.generate(cfdi); await this.cfdiRepo.update(cfdi.id, { pdfPath: pdf.path }); // 8. Marcar orden como facturada await this.posOrderRepo.update(orderId, { isInvoiced: true, invoiceId: cfdi.id, }); return cfdi; } async generatePublicInvoice(orderId: string): Promise { // Factura a publico general return this.generateFromPOS(orderId, { receptorRfc: 'XAXX010101000', receptorNombre: 'PUBLICO EN GENERAL', receptorRegimen: '616', receptorCp: this.config.emisorCp, usoCfdi: 'S01', formaPago: '99', metodoPago: 'PUE', }); } async cancel(cfdiId: string, dto: CancelDto): Promise { const cfdi = await this.cfdiRepo.findOne({ where: { id: cfdiId } }); // Validar que se puede cancelar if (cfdi.status !== 'vigente') { throw new BadRequestException('CFDI ya esta cancelado'); } // Enviar cancelacion al PAC const result = await this.pacService.cancelar(cfdi.uuid, dto.motivo); if (result.success) { await this.cfdiRepo.update(cfdiId, { status: 'cancelado', cancelDate: new Date(), cancelReason: dto.motivo, }); } return result; } } // 2. CFDIBuilderService @Injectable() export class CFDIBuilderService { async fromPOSOrder(order: POSOrder, dto: CFDIRequestDto): Promise { const config = await this.getConfig(); return { version: '4.0', serie: config.serieFactura, folio: await this.getNextFolio(), fecha: new Date(), formaPago: dto.formaPago, metodoPago: dto.metodoPago, tipoDeComprobante: 'I', lugarExpedicion: config.emisorCp, emisor: { rfc: config.emisorRfc, nombre: config.emisorNombre, regimenFiscal: config.emisorRegimen, }, receptor: { rfc: dto.receptorRfc, nombre: dto.receptorNombre, domicilioFiscalReceptor: dto.receptorCp, regimenFiscalReceptor: dto.receptorRegimen, usoCFDI: dto.usoCfdi, }, conceptos: order.lines.map(line => ({ claveProdServ: line.product.satProductCode || '01010101', noIdentificacion: line.product.defaultCode, cantidad: line.quantity, claveUnidad: line.product.satUnitCode || 'H87', unidad: line.product.uom?.name || 'Pieza', descripcion: line.productName, valorUnitario: Number(line.unitPrice), importe: Number(line.total) - Number(line.taxAmount), objetoImp: '02', impuestos: { traslados: [{ base: Number(line.total) - Number(line.taxAmount), impuesto: '002', tipoFactor: 'Tasa', tasaOCuota: 0.16, importe: Number(line.taxAmount), }], }, })), impuestos: { totalImpuestosTrasladados: Number(order.taxAmount), totalImpuestosRetenidos: 0, traslados: [{ base: Number(order.subtotal) - Number(order.discountAmount), impuesto: '002', tipoFactor: 'Tasa', tasaOCuota: 0.16, importe: Number(order.taxAmount), }], }, subTotal: Number(order.subtotal), descuento: Number(order.discountAmount), total: Number(order.total), }; } } // 3. PACService @Injectable() export class PACService { private providers: Map; constructor( private finkokProvider: FinkokPAC, private facturamaProvider: FacturamaPAC, ) { this.providers = new Map([ ['finkok', finkokProvider], ['facturama', facturamaProvider], ]); } async timbrar(xml: string): Promise { const config = await this.getConfig(); const primaryProvider = this.providers.get(config.pacProvider); const backupProvider = this.providers.get(config.pacBackup); try { return await primaryProvider.timbrar(xml); } catch (error) { if (backupProvider) { return await backupProvider.timbrar(xml); } throw error; } } async cancelar(uuid: string, motivo: string): Promise { const config = await this.getConfig(); const provider = this.providers.get(config.pacProvider); return provider.cancelar(uuid, motivo); } } // 4. AutofacturaService @Injectable() export class AutofacturaService { async validateTicket(ticketNumber: string): Promise { const order = await this.posOrderRepo.findOne({ where: { orderNumber: ticketNumber }, relations: ['lines'], }); if (!order) { return { valid: false, error: 'Ticket no encontrado' }; } if (order.isInvoiced) { return { valid: false, error: 'Ticket ya fue facturado' }; } // Validar plazo (30 dias por defecto) const config = await this.getConfig(); const maxDays = config.autofacturaDias || 30; const daysSinceOrder = this.daysBetween(order.orderDate, new Date()); if (daysSinceOrder > maxDays) { return { valid: false, error: `Plazo de ${maxDays} dias excedido` }; } return { valid: true, order: { orderNumber: order.orderNumber, orderDate: order.orderDate, subtotal: order.subtotal, tax: order.taxAmount, total: order.total, items: order.lines.map(l => ({ name: l.productName, quantity: l.quantity, unitPrice: l.unitPrice, total: l.total, })), }, }; } async generateFromTicket(dto: AutofacturaDto): Promise { // Validar ticket const validation = await this.validateTicket(dto.ticketNumber); if (!validation.valid) { throw new BadRequestException(validation.error); } // Buscar orden const order = await this.posOrderRepo.findOne({ where: { orderNumber: dto.ticketNumber }, }); // Generar factura return this.cfdiService.generateFromPOS(order.id, { receptorRfc: dto.rfc, receptorNombre: dto.nombre, receptorRegimen: dto.regimenFiscal, receptorCp: dto.codigoPostal, usoCfdi: dto.usoCfdi, formaPago: dto.formaPago || '99', metodoPago: 'PUE', }); } } ``` --- ## 4. MIDDLEWARE ### 4.1 TenantMiddleware ```typescript @Injectable() export class TenantMiddleware implements NestMiddleware { async use(req: Request, res: Response, next: NextFunction) { const tenantId = req.headers['x-tenant-id'] as string; if (!tenantId) { throw new UnauthorizedException('Tenant ID required'); } // Validar que el tenant existe const tenant = await this.tenantRepo.findOne({ where: { id: tenantId } }); if (!tenant || !tenant.isActive) { throw new UnauthorizedException('Invalid tenant'); } // Establecer variable de sesion para RLS await this.dataSource.query( `SELECT set_config('app.current_tenant_id', $1, false)`, [tenantId] ); req['tenant'] = tenant; next(); } } ``` ### 4.2 BranchMiddleware ```typescript @Injectable() export class BranchMiddleware implements NestMiddleware { async use(req: Request, res: Response, next: NextFunction) { const branchId = req.headers['x-branch-id'] as string; if (branchId) { const branch = await this.branchRepo.findOne({ where: { id: branchId }, }); if (!branch || !branch.isActive) { throw new UnauthorizedException('Invalid branch'); } req['branch'] = branch; } next(); } } ``` --- ## 5. WEBSOCKET GATEWAY (Para sincronizacion POS) ```typescript @WebSocketGateway({ namespace: 'pos', cors: { origin: '*' }, }) export class POSGateway { @WebSocketServer() server: Server; @SubscribeMessage('sync:orders') async handleSyncOrders( @MessageBody() data: { orders: OfflineOrder[] }, @ConnectedSocket() client: Socket, ) { const result = await this.syncService.syncOfflineOrders(data.orders); client.emit('sync:result', result); } @SubscribeMessage('order:created') async handleOrderCreated( @MessageBody() data: { orderId: string; branchId: string }, ) { // Notificar a otros clientes de la misma sucursal this.server.to(`branch:${data.branchId}`).emit('order:new', data); } handleConnection(client: Socket) { const branchId = client.handshake.query.branchId as string; if (branchId) { client.join(`branch:${branchId}`); } } } ``` --- ## 6. CHECKLIST DE IMPLEMENTACION ### Modulo RT-001 (Fundamentos) - [ ] Configurar importaciones de @erp-core - [ ] Verificar middleware de tenant - [ ] Configurar autenticacion ### Modulo RT-002 (POS) - [ ] POSSessionService - [ ] POSOrderService - [ ] POSPaymentService - [ ] POSSyncService - [ ] POSController - [ ] POSGateway (WebSocket) ### Modulo RT-003 (Inventario) - [ ] RetailStockService - [ ] TransfersService - [ ] AdjustmentsService - [ ] InventoryController ### Modulo RT-004 (Compras) - [ ] RetailPurchaseService - [ ] GoodsReceiptService - [ ] PurchaseController ### Modulo RT-005 (Clientes) - [ ] RetailCustomersService - [ ] LoyaltyService - [ ] CustomersController ### Modulo RT-006 (Precios) - [ ] PriceEngineService - [ ] PromotionsService - [ ] CouponsService - [ ] PricingController ### Modulo RT-007 (Caja) - [ ] CashSessionService - [ ] CashMovementService - [ ] CashClosingService - [ ] CashController ### Modulo RT-008 (Reportes) - [ ] DashboardService - [ ] SalesReportService - [ ] ProductReportService - [ ] CashReportService - [ ] ReportsController ### Modulo RT-009 (E-commerce) - [ ] CatalogService - [ ] CartService - [ ] CheckoutService - [ ] PaymentGatewayService - [ ] ShippingService - [ ] StorefrontController - [ ] EcommerceAdminController ### Modulo RT-010 (Facturacion) - [ ] CFDIService - [ ] CFDIBuilderService - [ ] PACService - [ ] XMLService - [ ] PDFService - [ ] AutofacturaService - [ ] CFDIController - [ ] AutofacturaController --- **Estado:** PLAN COMPLETO **Total servicios:** 48 (12 heredados, 8 extendidos, 28 nuevos) **Total controllers:** 15