workspace-v1/projects/erp-retail/orchestration/planes/fase-3-implementacion/PLAN-IMPL-BACKEND.md
rckrdmrd 66161b1566 feat: Workspace-v1 complete migration with NEXUS v3.4
Sistema NEXUS v3.4 migrado con:

Estructura principal:
- core/orchestration: Sistema SIMCO + CAPVED (27 directivas, 28 perfiles)
- core/catalog: Catalogo de funcionalidades reutilizables
- shared/knowledge-base: Base de conocimiento compartida
- devtools/scripts: Herramientas de desarrollo
- control-plane/registries: Control de servicios y CI/CD
- orchestration/: Configuracion de orquestacion de agentes

Proyectos incluidos (11):
- gamilit (submodule -> GitHub)
- trading-platform (OrbiquanTIA)
- erp-suite con 5 verticales:
  - erp-core, construccion, vidrio-templado
  - mecanicas-diesel, retail, clinicas
- betting-analytics
- inmobiliaria-analytics
- platform_marketing_content
- pos-micro, erp-basico

Configuracion:
- .gitignore completo para Node.js/Python/Docker
- gamilit como submodule (git@github.com:rckrdmrd/gamilit-workspace.git)
- Sistema de puertos estandarizado (3005-3199)

Generated with NEXUS v3.4 Migration System
EPIC-010: Configuracion Git y Repositorios
2026-01-04 03:37:42 -06:00

61 KiB

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%)

// 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:

// 1. POSSessionService
@Injectable()
export class POSSessionService {
  constructor(
    @InjectRepository(POSSession)
    private sessionRepo: Repository<POSSession>,
    private cashRegisterService: CashRegisterService,
  ) {}

  async openSession(dto: OpenSessionDto): Promise<POSSession> {
    // 1. Verificar caja disponible
    // 2. Crear sesion
    // 3. Registrar apertura
    return session;
  }

  async closeSession(sessionId: string, dto: CloseSessionDto): Promise<POSSession> {
    // 1. Calcular totales esperados
    // 2. Validar declaracion
    // 3. Crear corte
    // 4. Cerrar sesion
    return session;
  }

  async getActiveSession(userId: string): Promise<POSSession | null> {
    return this.sessionRepo.findOne({
      where: { userId, status: In(['opening', 'open']) }
    });
  }
}

// 2. POSOrderService
@Injectable()
export class POSOrderService {
  constructor(
    @InjectRepository(POSOrder)
    private orderRepo: Repository<POSOrder>,
    private priceEngine: PriceEngineService,
    private stockService: RetailStockService,
    private loyaltyService: LoyaltyService,
  ) {}

  async createOrder(sessionId: string): Promise<POSOrder> {
    const orderNumber = await this.generateOrderNumber();
    return this.orderRepo.save({
      sessionId,
      orderNumber,
      status: 'draft',
    });
  }

  async addLine(orderId: string, dto: AddLineDto): Promise<POSOrderLine> {
    // 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<POSOrder> {
    // 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<POSOrder> {
    // 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<SyncResult> {
    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:

// 1. RetailStockService (extiende StockService del core)
@Injectable()
export class RetailStockService extends StockService {
  constructor(
    @InjectRepository(StockQuant)
    stockQuantRepo: Repository<StockQuant>,
    @InjectRepository(Branch)
    private branchRepo: Repository<Branch>,
  ) {
    super(stockQuantRepo);
  }

  // Metodos heredados: getStock, reserveStock, etc.

  // Metodos nuevos para retail
  async getStockByBranch(branchId: string, productId?: string): Promise<BranchStock[]> {
    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<BranchStockSummary[]> {
    // 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<void> {
    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<StockTransfer> {
    const transfer = await this.transferRepo.save({
      ...dto,
      transferNumber: await this.generateNumber('TRF'),
      status: 'draft',
    });
    return transfer;
  }

  async confirmTransfer(id: string): Promise<StockTransfer> {
    // 1. Validar stock en origen
    // 2. Reservar stock
    // 3. Cambiar status a 'in_transit'
    return transfer;
  }

  async receiveTransfer(id: string, dto: ReceiveDto): Promise<StockTransfer> {
    // 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<StockAdjustment> {
    // Similar a transferencias
  }

  async confirmAdjustment(id: string): Promise<StockAdjustment> {
    // 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:

// 1. RetailPurchaseService (extiende PurchaseService)
@Injectable()
export class RetailPurchaseService extends PurchaseService {
  // Hereda: createOrder, confirmOrder, receiveGoods

  async suggestRestock(branchId: string): Promise<PurchaseSuggestion[]> {
    // 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<GoodsReceipt> {
    // 1. Crear recepcion
    // 2. Vincular con orden de compra si existe
    return receipt;
  }

  async confirmReceipt(id: string): Promise<GoodsReceipt> {
    // 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:

// 1. RetailCustomersService (extiende PartnersService)
@Injectable()
export class RetailCustomersService extends PartnersService {
  constructor(
    partnersRepo: Repository<Partner>,
    private membershipRepo: Repository<CustomerMembership>,
    private loyaltyService: LoyaltyService,
  ) {
    super(partnersRepo);
  }

  async createCustomer(dto: CreateCustomerDto): Promise<Partner> {
    // 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<CustomerWithMembership> {
    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<CustomerMembership> {
    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<LoyaltyTransaction> {
    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<RedemptionResult> {
    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<void> {
    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:

// 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<PriceResult> {
    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<Promotion> {
    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<Promotion[]> {
    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<void> {
    await this.promotionRepo.increment({ id: promotionId }, 'currentUses', 1);
  }
}

// 3. CouponsService
@Injectable()
export class CouponsService {
  async generate(dto: GenerateCouponsDto): Promise<Coupon[]> {
    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<CouponValidation> {
    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<CouponRedemption> {
    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:

// 1. CashSessionService
@Injectable()
export class CashSessionService {
  async getSummary(sessionId: string): Promise<SessionSummary> {
    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<CashMovement> {
    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<ClosingPreparation> {
    const summary = await this.sessionService.getSummary(sessionId);

    return {
      ...summary,
      denominations: this.getDenominationTemplate(),
    };
  }

  async createClosing(sessionId: string, dto: CreateClosingDto): Promise<CashClosing> {
    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:

// 1. DashboardService
@Injectable()
export class DashboardService {
  async getDashboard(branchId?: string): Promise<RetailDashboard> {
    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<SalesReport> {
    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:

// 1. CatalogService
@Injectable()
export class CatalogService {
  async search(filters: CatalogFilters): Promise<PaginatedProducts> {
    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<ProductDetail> {
    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<Cart> {
    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<Cart> {
    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<Cart> {
    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<void> {
    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<CheckoutValidation> {
    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<EcommerceOrder> {
    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<string, PaymentGateway>;

  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<PaymentIntent> {
    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<void> {
    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<void> {
    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:

// 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<CFDI> {
    // 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<CFDI> {
    // 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<CancelResult> {
    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<CFDI40> {
    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<string, PACProvider>;

  constructor(
    private finkokProvider: FinkokPAC,
    private facturamaProvider: FacturamaPAC,
  ) {
    this.providers = new Map([
      ['finkok', finkokProvider],
      ['facturama', facturamaProvider],
    ]);
  }

  async timbrar(xml: string): Promise<TimbradoResult> {
    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<CancelResult> {
    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<TicketValidation> {
    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<CFDI> {
    // 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

@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

@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)

@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