workspace-v1/orchestration/patrones/PATRON-TRANSACCIONES.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

18 KiB

PATRON DE TRANSACCIONES

Version: 1.0.0 Fecha: 2025-12-08 Prioridad: OBLIGATORIA - Seguir para integridad de datos Sistema: SIMCO + CAPVED


PROPOSITO

Definir patrones de manejo de transacciones de base de datos para garantizar integridad de datos, consistencia y correcta recuperacion de errores.


1. PRINCIPIOS ACID

╔══════════════════════════════════════════════════════════════════════╗
║  PROPIEDADES ACID                                                    ║
╠══════════════════════════════════════════════════════════════════════╣
║                                                                       ║
║  A - Atomicity (Atomicidad)                                          ║
║      Todo o nada. Si una parte falla, se revierte todo.              ║
║                                                                       ║
║  C - Consistency (Consistencia)                                      ║
║      La BD pasa de un estado valido a otro estado valido.            ║
║                                                                       ║
║  I - Isolation (Aislamiento)                                         ║
║      Transacciones concurrentes no interfieren entre si.             ║
║                                                                       ║
║  D - Durability (Durabilidad)                                        ║
║      Una vez confirmada, la transaccion persiste.                    ║
║                                                                       ║
╚══════════════════════════════════════════════════════════════════════╝

2. CUANDO USAR TRANSACCIONES

SI Usar Transaccion

✅ Multiples operaciones que deben ser atomicas
✅ Operaciones que afectan multiples tablas relacionadas
✅ Operaciones financieras o criticas
✅ Creacion de entidades con relaciones obligatorias
✅ Actualizaciones que requieren consistencia

NO Necesitas Transaccion

❌ Una sola query simple (SELECT, INSERT, UPDATE)
❌ Operaciones de solo lectura
❌ Operaciones independientes sin relacion

3. TYPEORM - METODOS DE TRANSACCION

3.1 QueryRunner (Recomendado para control total)

// src/modules/order/services/order.service.ts
import { Injectable } from '@nestjs/common';
import { DataSource, QueryRunner } from 'typeorm';

@Injectable()
export class OrderService {
  constructor(private readonly dataSource: DataSource) {}

  async createOrder(dto: CreateOrderDto): Promise<Order> {
    // Crear QueryRunner
    const queryRunner = this.dataSource.createQueryRunner();

    // Conectar y comenzar transaccion
    await queryRunner.connect();
    await queryRunner.startTransaction();

    try {
      // 1. Crear la orden
      const order = queryRunner.manager.create(OrderEntity, {
        userId: dto.userId,
        status: OrderStatus.PENDING,
        total: 0,
      });
      await queryRunner.manager.save(order);

      // 2. Crear items y calcular total
      let total = 0;
      for (const item of dto.items) {
        // Verificar stock
        const product = await queryRunner.manager.findOne(ProductEntity, {
          where: { id: item.productId },
          lock: { mode: 'pessimistic_write' },  // Lock para evitar race condition
        });

        if (!product || product.stock < item.quantity) {
          throw new BadRequestException(
            `Stock insuficiente para producto ${item.productId}`,
          );
        }

        // Crear item
        const orderItem = queryRunner.manager.create(OrderItemEntity, {
          orderId: order.id,
          productId: item.productId,
          quantity: item.quantity,
          price: product.price,
        });
        await queryRunner.manager.save(orderItem);

        // Actualizar stock
        product.stock -= item.quantity;
        await queryRunner.manager.save(product);

        total += product.price * item.quantity;
      }

      // 3. Actualizar total de la orden
      order.total = total;
      await queryRunner.manager.save(order);

      // 4. Confirmar transaccion
      await queryRunner.commitTransaction();

      return order;

    } catch (error) {
      // Revertir en caso de error
      await queryRunner.rollbackTransaction();
      throw error;

    } finally {
      // Liberar QueryRunner
      await queryRunner.release();
    }
  }
}

3.2 DataSource.transaction() (Mas simple)

// Para casos mas simples
async createUserWithProfile(dto: CreateUserWithProfileDto): Promise<User> {
  return this.dataSource.transaction(async (manager) => {
    // Crear usuario
    const user = manager.create(UserEntity, {
      email: dto.email,
      password: await hashPassword(dto.password),
    });
    await manager.save(user);

    // Crear perfil
    const profile = manager.create(ProfileEntity, {
      userId: user.id,
      firstName: dto.firstName,
      lastName: dto.lastName,
    });
    await manager.save(profile);

    return user;
  });
}

3.3 @Transaction Decorator (NestJS)

// src/shared/decorators/transactional.decorator.ts
import { DataSource } from 'typeorm';

export function Transactional() {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor,
  ) {
    const originalMethod = descriptor.value;

    descriptor.value = async function (...args: any[]) {
      const dataSource: DataSource = this.dataSource;

      return dataSource.transaction(async (manager) => {
        // Inyectar manager temporal
        const originalManager = this.manager;
        this.manager = manager;

        try {
          return await originalMethod.apply(this, args);
        } finally {
          this.manager = originalManager;
        }
      });
    };

    return descriptor;
  };
}

// Uso
@Injectable()
export class OrderService {
  constructor(
    private readonly dataSource: DataSource,
    @InjectRepository(OrderEntity)
    private readonly orderRepository: Repository<OrderEntity>,
  ) {}

  private manager: EntityManager;

  @Transactional()
  async createOrder(dto: CreateOrderDto): Promise<Order> {
    // this.manager es el manager transaccional
    const order = this.manager.create(OrderEntity, dto);
    return this.manager.save(order);
  }
}

4. NIVELES DE AISLAMIENTO

Niveles Disponibles

Nivel Dirty Reads Non-Repeatable Reads Phantom Reads
READ UNCOMMITTED Posible Posible Posible
READ COMMITTED No Posible Posible
REPEATABLE READ No No Posible
SERIALIZABLE No No No

Configurar Nivel de Aislamiento

// Por defecto PostgreSQL usa READ COMMITTED
// Para operaciones criticas, usar SERIALIZABLE

async processPayment(orderId: string): Promise<void> {
  const queryRunner = this.dataSource.createQueryRunner();
  await queryRunner.connect();

  // Configurar nivel de aislamiento
  await queryRunner.startTransaction('SERIALIZABLE');

  try {
    // Operaciones criticas aqui
    await queryRunner.commitTransaction();
  } catch (error) {
    await queryRunner.rollbackTransaction();

    // SERIALIZABLE puede fallar por conflictos
    if (error.code === '40001') {  // Serialization failure
      // Reintentar la transaccion
      return this.processPayment(orderId);
    }
    throw error;
  } finally {
    await queryRunner.release();
  }
}

5. LOCKS (BLOQUEOS)

5.1 Pessimistic Locking

// Bloquear fila para escritura (otros esperan)
async updateStock(productId: string, quantity: number): Promise<void> {
  return this.dataSource.transaction(async (manager) => {
    // Obtener producto con lock exclusivo
    const product = await manager.findOne(ProductEntity, {
      where: { id: productId },
      lock: { mode: 'pessimistic_write' },
    });

    if (product.stock < quantity) {
      throw new BadRequestException('Stock insuficiente');
    }

    product.stock -= quantity;
    await manager.save(product);
  });
}

5.2 Optimistic Locking (Version)

// Entity con columna de version
@Entity()
export class ProductEntity {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column()
  stock: number;

  @VersionColumn()  // Auto-incrementa en cada update
  version: number;
}

// El update falla si version cambio (alguien mas modifico)
async updateStock(productId: string, quantity: number): Promise<void> {
  const product = await this.productRepository.findOne({
    where: { id: productId },
  });

  product.stock -= quantity;

  try {
    await this.productRepository.save(product);
  } catch (error) {
    if (error instanceof OptimisticLockVersionMismatchError) {
      // Reintentar o notificar conflicto
      throw new ConflictException('El producto fue modificado. Reintente.');
    }
    throw error;
  }
}

Cuando Usar Cada Tipo

Scenario Lock Recomendado
Alta concurrencia, conflictos raros Optimistic
Operaciones financieras criticas Pessimistic
Updates frecuentes al mismo registro Pessimistic
Lecturas frecuentes, updates raros Optimistic

6. PATRONES COMUNES

6.1 Unit of Work Pattern

// src/shared/database/unit-of-work.ts
@Injectable()
export class UnitOfWork {
  private queryRunner: QueryRunner;

  constructor(private readonly dataSource: DataSource) {}

  async begin(): Promise<void> {
    this.queryRunner = this.dataSource.createQueryRunner();
    await this.queryRunner.connect();
    await this.queryRunner.startTransaction();
  }

  getRepository<T>(entity: EntityTarget<T>): Repository<T> {
    return this.queryRunner.manager.getRepository(entity);
  }

  async commit(): Promise<void> {
    await this.queryRunner.commitTransaction();
    await this.queryRunner.release();
  }

  async rollback(): Promise<void> {
    await this.queryRunner.rollbackTransaction();
    await this.queryRunner.release();
  }
}

// Uso
@Injectable()
export class OrderService {
  constructor(private readonly uow: UnitOfWork) {}

  async createOrder(dto: CreateOrderDto): Promise<Order> {
    await this.uow.begin();

    try {
      const orderRepo = this.uow.getRepository(OrderEntity);
      const productRepo = this.uow.getRepository(ProductEntity);

      // Operaciones...

      await this.uow.commit();
      return order;
    } catch (error) {
      await this.uow.rollback();
      throw error;
    }
  }
}

6.2 Saga Pattern (Para transacciones distribuidas)

// Cuando no puedes usar transaccion de BD (microservicios)
interface SagaStep {
  execute(): Promise<void>;
  compensate(): Promise<void>;  // Revertir si falla
}

class CreateOrderSaga {
  private executedSteps: SagaStep[] = [];

  async execute(steps: SagaStep[]): Promise<void> {
    try {
      for (const step of steps) {
        await step.execute();
        this.executedSteps.push(step);
      }
    } catch (error) {
      // Compensar en orden inverso
      for (const step of this.executedSteps.reverse()) {
        await step.compensate();
      }
      throw error;
    }
  }
}

// Uso
const saga = new CreateOrderSaga();
await saga.execute([
  {
    execute: () => this.orderService.create(orderDto),
    compensate: () => this.orderService.delete(orderId),
  },
  {
    execute: () => this.inventoryService.reserve(items),
    compensate: () => this.inventoryService.release(items),
  },
  {
    execute: () => this.paymentService.charge(payment),
    compensate: () => this.paymentService.refund(payment),
  },
]);

6.3 Retry con Backoff

// Para manejar deadlocks y conflictos
async withRetry<T>(
  operation: () => Promise<T>,
  maxRetries: number = 3,
  baseDelay: number = 100,
): Promise<T> {
  let lastError: Error;

  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await operation();
    } catch (error) {
      lastError = error;

      // Solo reintentar errores transitorios
      const isRetryable =
        error.code === '40001' ||  // Serialization failure
        error.code === '40P01' ||  // Deadlock
        error.code === '55P03';    // Lock not available

      if (!isRetryable || attempt === maxRetries) {
        throw error;
      }

      // Exponential backoff con jitter
      const delay = baseDelay * Math.pow(2, attempt - 1) * (0.5 + Math.random());
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }

  throw lastError;
}

// Uso
async createOrder(dto: CreateOrderDto): Promise<Order> {
  return this.withRetry(() => this.createOrderTransaction(dto));
}

7. ERRORES COMUNES Y SOLUCIONES

7.1 Deadlock

// ❌ PROBLEMA: Deadlock por orden inconsistente de locks
// Transaction 1: Lock A, then B
// Transaction 2: Lock B, then A

// ✅ SOLUCION: Siempre lockear en el mismo orden
async transferFunds(fromId: string, toId: string, amount: number): Promise<void> {
  // Ordenar IDs para lockear siempre en el mismo orden
  const [firstId, secondId] = [fromId, toId].sort();

  return this.dataSource.transaction(async (manager) => {
    const firstAccount = await manager.findOne(AccountEntity, {
      where: { id: firstId },
      lock: { mode: 'pessimistic_write' },
    });

    const secondAccount = await manager.findOne(AccountEntity, {
      where: { id: secondId },
      lock: { mode: 'pessimistic_write' },
    });

    // Ahora operar
    const from = firstId === fromId ? firstAccount : secondAccount;
    const to = firstId === toId ? firstAccount : secondAccount;

    from.balance -= amount;
    to.balance += amount;

    await manager.save([from, to]);
  });
}

7.2 Connection Leak

// ❌ PROBLEMA: QueryRunner no liberado
async badMethod(): Promise<void> {
  const queryRunner = this.dataSource.createQueryRunner();
  await queryRunner.connect();
  await queryRunner.startTransaction();

  const data = await queryRunner.manager.find(Entity);
  // Si hay error aqui, queryRunner nunca se libera!

  await queryRunner.commitTransaction();
  await queryRunner.release();
}

// ✅ SOLUCION: Siempre usar try/finally
async goodMethod(): Promise<void> {
  const queryRunner = this.dataSource.createQueryRunner();
  await queryRunner.connect();
  await queryRunner.startTransaction();

  try {
    const data = await queryRunner.manager.find(Entity);
    await queryRunner.commitTransaction();
  } catch (error) {
    await queryRunner.rollbackTransaction();
    throw error;
  } finally {
    await queryRunner.release();  // SIEMPRE se ejecuta
  }
}

7.3 Long-Running Transactions

// ❌ PROBLEMA: Transaccion muy larga
async processAllOrders(): Promise<void> {
  return this.dataSource.transaction(async (manager) => {
    const orders = await manager.find(OrderEntity);  // Puede ser miles
    for (const order of orders) {
      await this.processOrder(order);  // Minutos de bloqueo
    }
  });
}

// ✅ SOLUCION: Procesar en batches con transacciones cortas
async processAllOrders(): Promise<void> {
  const BATCH_SIZE = 100;
  let processed = 0;

  while (true) {
    const orders = await this.orderRepository.find({
      where: { status: OrderStatus.PENDING },
      take: BATCH_SIZE,
    });

    if (orders.length === 0) break;

    // Transaccion corta por batch
    await this.dataSource.transaction(async (manager) => {
      for (const order of orders) {
        await this.processOrderInTransaction(manager, order);
      }
    });

    processed += orders.length;
  }
}

8. TESTING DE TRANSACCIONES

// src/modules/order/order.service.spec.ts
describe('OrderService', () => {
  let service: OrderService;
  let dataSource: DataSource;

  beforeEach(async () => {
    const module = await Test.createTestingModule({
      imports: [TypeOrmModule.forRoot(testConfig)],
      providers: [OrderService],
    }).compile();

    service = module.get(OrderService);
    dataSource = module.get(DataSource);
  });

  afterEach(async () => {
    // Limpiar despues de cada test
    await dataSource.synchronize(true);
  });

  describe('createOrder', () => {
    it('should rollback if stock is insufficient', async () => {
      // Arrange
      const product = await productRepo.save({
        name: 'Test',
        stock: 5,
        price: 100,
      });

      // Act & Assert
      await expect(
        service.createOrder({
          items: [{ productId: product.id, quantity: 10 }],
        }),
      ).rejects.toThrow('Stock insuficiente');

      // Verificar que stock no cambio (rollback funciono)
      const updatedProduct = await productRepo.findOne({
        where: { id: product.id },
      });
      expect(updatedProduct.stock).toBe(5);
    });

    it('should commit successfully with valid data', async () => {
      // ...
    });
  });
});

9. CHECKLIST DE TRANSACCIONES

Antes de implementar:
[ ] ¿Necesito atomicidad? (multiples operaciones)
[ ] ¿Que nivel de aislamiento necesito?
[ ] ¿Necesito locks? ¿Pesimista u optimista?

Durante implementacion:
[ ] QueryRunner siempre liberado (finally)
[ ] Rollback en catch
[ ] Manejo de errores transitorios (retry)
[ ] Transacciones lo mas cortas posible
[ ] Orden consistente de locks (evitar deadlocks)

Testing:
[ ] Test de rollback en error
[ ] Test de commit exitoso
[ ] Test de concurrencia (si aplica)

Version: 1.0.0 | Sistema: SIMCO | Tipo: Patron de Codigo