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
8.2 Performance
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