491 lines
11 KiB
Markdown
491 lines
11 KiB
Markdown
# 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
|
|
|
|
```typescript
|
|
import { PricelistsService } from '@erp-core/sales';
|
|
```
|
|
|
|
### 2.3 Servicios a Extender
|
|
|
|
```typescript
|
|
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)
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
@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
|
|
|
|
```yaml
|
|
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
|
|
|
|
```yaml
|
|
tipo: fixed_amount
|
|
ejemplo:
|
|
nombre: "$100 en compras mayores a $500"
|
|
descuento: 100
|
|
minimo_compra: 500
|
|
```
|
|
|
|
### 4.3 Compra X Lleva Y (NxM)
|
|
|
|
```yaml
|
|
tipo: buy_x_get_y
|
|
ejemplo:
|
|
nombre: "3x2 en bebidas"
|
|
compra: 3
|
|
paga: 2
|
|
aplica_a: [categoria: "BEBIDAS"]
|
|
```
|
|
|
|
### 4.4 Descuento por Volumen
|
|
|
|
```yaml
|
|
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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```sql
|
|
-- 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
|
|
|
|
```sql
|
|
-- 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
|