erp-retail/orchestration/planes/fase-2-analisis-modulos/ANALISIS-RT-006-precios.md

11 KiB

ANALISIS MODULO RT-006: PRECIOS Y PROMOCIONES

Fecha: 2025-12-18 Fase: 2 - Analisis por Modulo Modulo: RT-006 Precios Herencia: 30% Story Points: 36 Prioridad: P0


1. DESCRIPCION GENERAL

1.1 Proposito

Motor de reglas de precios con listas de precios, promociones, descuentos por volumen y sistema de cupones.

1.2 Funcionalidades Principales

Funcionalidad Descripcion Criticidad
Listas de precios Por canal/sucursal Critica
Promociones Multiples tipos Alta
Descuentos volumen Por cantidad Media
Cupones Generacion y canje Media
Motor reglas Evaluacion rapida Critica

2. HERENCIA DEL CORE

2.1 Componentes Heredados (30%)

Componente Core % Uso Accion
sales.pricelists 100% HEREDAR
sales.pricelist_items 100% HEREDAR

2.2 Servicios a Heredar

import { PricelistsService } from '@erp-core/sales';

2.3 Servicios a Extender

class RetailPriceService extends PricelistsService {
  // Motor de precios
  async calculatePrice(productId: string, context: PriceContext): Promise<PriceResult>;

  // Promociones
  async getActivePromotions(branchId: string): Promise<Promotion[]>;
  async applyPromotion(orderId: string, promotionId: string): Promise<void>;
}

3. COMPONENTES NUEVOS

3.1 Entidades (TypeORM)

// 1. Promotion - Promocion
@Entity('promotions', { schema: 'retail' })
export class Promotion {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column('uuid')
  tenantId: string;

  @Column()
  code: string;

  @Column()
  name: string;

  @Column({ nullable: true })
  description: string;

  @Column({ type: 'enum', enum: PromotionType })
  promotionType: PromotionType;
  // percentage, fixed_amount, buy_x_get_y, bundle

  @Column({ type: 'decimal', precision: 10, scale: 2 })
  discountValue: number;  // % o monto segun tipo

  @Column({ type: 'date' })
  startDate: Date;

  @Column({ type: 'date' })
  endDate: Date;

  @Column({ type: 'boolean', default: false })
  appliesToAll: boolean;

  @Column({ type: 'int', nullable: true })
  minQuantity: number;

  @Column({ type: 'decimal', precision: 12, scale: 2, nullable: true })
  minAmount: number;

  @Column({ type: 'uuid', array: true, nullable: true })
  branchIds: string[];  // null = todas

  @Column({ type: 'boolean', default: true })
  isActive: boolean;

  @Column({ type: 'int', nullable: true })
  maxUses: number;

  @Column({ type: 'int', default: 0 })
  currentUses: number;

  @OneToMany(() => PromotionProduct, pp => pp.promotion)
  products: PromotionProduct[];
}

// 2. PromotionProduct - Productos en promocion
@Entity('promotion_products', { schema: 'retail' })
export class PromotionProduct {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @ManyToOne(() => Promotion)
  promotion: Promotion;

  @ManyToOne(() => Product)
  product: Product;
}

// 3. Coupon - Cupon
@Entity('coupons', { schema: 'retail' })
export class Coupon {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column('uuid')
  tenantId: string;

  @Column()
  code: string;

  @Column({ type: 'enum', enum: CouponType })
  couponType: CouponType;  // percentage, fixed_amount

  @Column({ type: 'decimal', precision: 10, scale: 2 })
  discountValue: number;

  @Column({ type: 'decimal', precision: 12, scale: 2, nullable: true })
  minPurchase: number;

  @Column({ type: 'decimal', precision: 12, scale: 2, nullable: true })
  maxDiscount: number;

  @Column({ type: 'date' })
  validFrom: Date;

  @Column({ type: 'date' })
  validUntil: Date;

  @Column({ type: 'int', default: 1 })
  maxUses: number;

  @Column({ type: 'int', default: 0 })
  timesUsed: number;

  @Column({ type: 'boolean', default: true })
  isActive: boolean;
}

// 4. CouponRedemption - Uso de cupon
@Entity('coupon_redemptions', { schema: 'retail' })
export class CouponRedemption {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @ManyToOne(() => Coupon)
  coupon: Coupon;

  @ManyToOne(() => POSOrder)
  order: POSOrder;

  @Column({ type: 'decimal', precision: 12, scale: 2 })
  discountApplied: number;

  @Column({ type: 'timestamptz' })
  redeemedAt: Date;
}

3.2 Motor de Precios

interface PriceContext {
  productId: string;
  quantity: number;
  customerId?: string;
  branchId: string;
  channel: 'pos' | 'ecommerce';
  date: Date;
}

interface PriceResult {
  basePrice: number;
  finalPrice: number;
  discounts: DiscountApplied[];
  taxes: TaxApplied[];
  total: number;
}

interface DiscountApplied {
  type: 'pricelist' | 'promotion' | 'volume' | 'coupon' | 'loyalty';
  name: string;
  amount: number;
}

class PriceEngine {
  async calculatePrice(context: PriceContext): Promise<PriceResult> {
    // 1. Obtener precio base del producto
    const basePrice = await this.getBasePrice(context.productId);

    // 2. Aplicar lista de precios del canal
    const pricelistPrice = await this.applyPricelist(basePrice, context);

    // 3. Evaluar promociones activas
    const promotionDiscount = await this.evaluatePromotions(pricelistPrice, context);

    // 4. Evaluar descuento por volumen
    const volumeDiscount = await this.evaluateVolumeDiscount(context);

    // 5. Calcular precio final
    const finalPrice = pricelistPrice - promotionDiscount - volumeDiscount;

    // 6. Calcular impuestos
    const taxes = await this.calculateTaxes(finalPrice, context);

    return {
      basePrice,
      finalPrice,
      discounts: [...],
      taxes,
      total: finalPrice + taxes.total
    };
  }
}

3.3 Servicios Backend

Servicio Metodos Principales
PriceEngineService calculatePrice(), evaluatePromotions()
PromotionService create(), activate(), deactivate(), getActive()
CouponService generate(), validate(), redeem()
VolumeDiscountService configure(), calculate()

3.4 Controladores

@Controller('pricing')
export class PricingController {
  // Calculo de precios
  @Post('calculate')
  calculatePrice(@Body() dto: PriceRequestDto): Promise<PriceResult>;

  // Promociones
  @Get('promotions')
  getPromotions(@Query() filters: PromotionFilters): Promise<Promotion[]>;

  @Get('promotions/active')
  getActivePromotions(@Query('branchId') branchId: string): Promise<Promotion[]>;

  @Post('promotions')
  createPromotion(@Body() dto: CreatePromotionDto): Promise<Promotion>;

  @Put('promotions/:id')
  updatePromotion(@Param('id') id: string, @Body() dto: UpdatePromotionDto): Promise<Promotion>;

  @Post('promotions/:id/activate')
  activatePromotion(@Param('id') id: string): Promise<Promotion>;

  @Post('promotions/:id/deactivate')
  deactivatePromotion(@Param('id') id: string): Promise<Promotion>;

  // Cupones
  @Post('coupons/generate')
  generateCoupons(@Body() dto: GenerateCouponsDto): Promise<Coupon[]>;

  @Get('coupons/:code/validate')
  validateCoupon(@Param('code') code: string): Promise<CouponValidation>;

  @Post('coupons/:code/redeem')
  redeemCoupon(@Param('code') code: string, @Body() dto: RedeemDto): Promise<CouponRedemption>;
}

4. TIPOS DE PROMOCIONES

4.1 Descuento Porcentual

tipo: percentage
ejemplo:
  nombre: "20% en ropa"
  descuento: 20
  aplica_a: [categoria: "ROPA"]
  vigencia: "2025-12-01 al 2025-12-31"

4.2 Descuento Monto Fijo

tipo: fixed_amount
ejemplo:
  nombre: "$100 en compras mayores a $500"
  descuento: 100
  minimo_compra: 500

4.3 Compra X Lleva Y (NxM)

tipo: buy_x_get_y
ejemplo:
  nombre: "3x2 en bebidas"
  compra: 3
  paga: 2
  aplica_a: [categoria: "BEBIDAS"]

4.4 Descuento por Volumen

tipo: volume
ejemplo:
  nombre: "Descuento por volumen"
  rangos:
    - cantidad_min: 5, descuento: 5%
    - cantidad_min: 10, descuento: 10%
    - cantidad_min: 20, descuento: 15%

5. REGLAS DE EVALUACION

5.1 Orden de Prioridad

1. Precio base del producto
2. Lista de precios del canal (si existe)
3. Promociones activas (por prioridad, NO acumulables)
4. Descuento por volumen (si aplica)
5. Cupon (si proporciona el cliente)
6. Puntos de lealtad (como descuento)

5.2 Conflicto de Promociones

// Solo aplica la mejor promocion (no acumulables)
function selectBestPromotion(promotions: Promotion[], orderTotal: number): Promotion {
  return promotions
    .filter(p => isApplicable(p, orderTotal))
    .sort((a, b) => calculateDiscount(b, orderTotal) - calculateDiscount(a, orderTotal))
    [0];
}

6. TABLAS DDL

6.1 Tablas Definidas

-- Ya en 03-retail-tables.sql
CREATE TABLE retail.promotions (...);
CREATE TABLE retail.promotion_products (...);

-- Agregar cupones
CREATE TABLE retail.coupons (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  tenant_id UUID NOT NULL REFERENCES auth.tenants(id),
  code VARCHAR(20) NOT NULL,
  coupon_type VARCHAR(20) NOT NULL,
  discount_value DECIMAL(10,2) NOT NULL,
  min_purchase DECIMAL(12,2),
  max_discount DECIMAL(12,2),
  valid_from DATE NOT NULL,
  valid_until DATE NOT NULL,
  max_uses INT DEFAULT 1,
  times_used INT DEFAULT 0,
  is_active BOOLEAN DEFAULT TRUE,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  UNIQUE(tenant_id, code)
);

CREATE TABLE retail.coupon_redemptions (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  tenant_id UUID NOT NULL REFERENCES auth.tenants(id),
  coupon_id UUID NOT NULL REFERENCES retail.coupons(id),
  order_id UUID NOT NULL REFERENCES retail.pos_orders(id),
  discount_applied DECIMAL(12,2) NOT NULL,
  redeemed_at TIMESTAMPTZ DEFAULT NOW()
);

6.2 Indices

-- Promociones activas
CREATE INDEX idx_promotions_active ON retail.promotions(is_active, start_date, end_date);
CREATE INDEX idx_promotions_branch ON retail.promotions USING GIN(branch_ids);

-- Cupones
CREATE UNIQUE INDEX idx_coupons_code ON retail.coupons(tenant_id, code);
CREATE INDEX idx_coupons_valid ON retail.coupons(valid_from, valid_until, is_active);

7. DEPENDENCIAS

7.1 Dependencias de Core

Modulo Estado Requerido Para
MGN-013 Sales 50% Pricelists
SPEC-PRICING-RULES Planificado Motor de precios

7.2 Dependencias de Retail

Modulo Tipo
RT-001 Fundamentos Prerequisito

7.3 Bloquea a

Modulo Razon
RT-002 POS Precios en ventas
RT-009 E-commerce Precios online

8. CRITERIOS DE ACEPTACION

8.1 Funcionales

  • Crear lista de precios
  • Asignar precios por producto
  • Crear promocion porcentual
  • Crear promocion monto fijo
  • Crear promocion NxM
  • Configurar descuento por volumen
  • Generar cupones
  • Validar cupon
  • Canjear cupon
  • Calcular precio con todas las reglas
  • Motor < 100ms

8.2 Performance

  • Calculo precio < 100ms
  • Soportar 100+ promociones activas

9. ESTIMACION DETALLADA

Componente SP Backend SP Frontend Total
Entities + Migrations 3 - 3
PriceEngineService 8 - 8
PromotionService 5 - 5
CouponService 5 - 5
Controllers 3 - 3
Promotion Pages - 8 8
Coupon Pages - 4 4
TOTAL 24 12 36

Estado: ANALISIS COMPLETO Bloqueado por: RT-001, SPEC-PRICING-RULES