Skip to main content

Domain Model Overview

Sistema de Ventas implements a rich domain model with clear entity relationships and business logic. The data model supports the complete sales workflow from product catalog to purchase, order, and payment processing.

Entity Relationship Diagram

Note on Relationships: Foreign key relationships across microservices (shown with dashed lines in some diagrams) are logical references only, not database-enforced foreign keys. They are maintained through application-level validation via Feign client calls.

Authentication & Authorization Domain

Usuario (User Profile)

@Entity
public class Usuario {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String nombres;
    private String apellidoPaterno;
    private String apellidoMaterno;
    private String dni;                    // National ID
    private String direccion;
    private String telefono;
    private Boolean estado;
    
    @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    @JoinColumn(name = "auth_user_id")
    private AuthUser authUser;
}
Business Rules:
  • One Usuario has exactly one AuthUser (authentication credentials)
  • Usuario can be active or inactive (estado)
  • Stores personal information separately from credentials

AuthUser (Authentication Credentials)

@Entity
public class AuthUser {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String username;               // Unique
    private String password;               // BCrypt encrypted
    private String email;                  // Unique
    private Boolean activo;
}
Security Features:
  • Password stored with BCrypt hashing
  • Unique username and email constraints
  • Active status for account management

Rol (Role)

@Entity
public class Rol {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String nombre;                 // Role name
    private String descripcion;
    
    @ManyToMany
    private Set<Acceso> accesos;
}

Acceso (Permission)

@Entity
public class Acceso {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String nombre;                 // Permission name
    private String recurso;                // Resource/endpoint
    private String accion;                 // Action (GET, POST, etc.)
}

RBAC Model


Product Catalog Domain

Categoria (Category)

@Entity
public class Categoria {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @NotBlank(message = "El nombre es obligatorio")
    @Column(nullable = false, unique = true)
    private String nombre;
    
    private String descripcion;
    private boolean estado = true;
    
    @CreationTimestamp
    private LocalDateTime fechaCreacion;
}
Validation:
  • Name is required and unique
  • Active by default
  • Automatic timestamp on creation

Producto (Product)

@Entity
public class Producto {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false, unique = true)
    private String codigo;                 // Product code
    
    @Column(nullable = false, unique = true)
    private String nombre;                 // Product name
    
    private String descripcion;
    private Integer cantidad;              // Stock quantity
    private Double precioVenta;            // Sale price
    private Double costoCompra;            // Purchase cost
    private boolean estado = true;
    private LocalDateTime fechaCreacion;
    private LocalDateTime fechaActualizacion;
    private String imagen;                 // Image path
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "categoria_id")
    private Categoria categoria;
    
    @PrePersist
    protected void onCreate() {
        fechaCreacion = LocalDateTime.now();
        fechaActualizacion = fechaCreacion;
    }
    
    @PreUpdate
    protected void onUpdate() {
        fechaActualizacion = LocalDateTime.now();
    }
}
Business Logic:
  • Unique product code and name
  • Default quantity: 0
  • Automatic timestamp management
  • Soft delete via estado flag
  • Price margin: precioVenta - costoCompra

Category-Product Relationship


Customer Domain

Cliente (Customer)

@Entity
@Table(name = "cliente", uniqueConstraints = {
    @UniqueConstraint(columnNames = {"dni"})
})
public class Cliente {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false, unique = true)
    private String dni;                    // National ID
    
    @Column(nullable = false)
    private String nombre;
    
    private String apellido;
    private String direccion;
    private String telefono;
    
    @Column(nullable = false, unique = true)
    private String email;
    
    private LocalDateTime fechaRegistro;
    private Boolean activo = true;
    
    @PrePersist
    public void prePersist() {
        this.fechaRegistro = LocalDateTime.now();
        if (activo == null) {
            activo = true;
        }
    }
}
Unique Constraints:
  • DNI (national ID)
  • Email address
Default Values:
  • Active status: true
  • Registration date: current timestamp

Supplier Domain

Proveedor (Supplier)

@Entity
public class Proveedor {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String ruc;                    // Tax ID
    private String nombre;                 // Company name
    private String telefono;
    private String direccion;
    private String correo;                 // Email
    private Boolean estado;                // Active status
    
    @PrePersist
    public void prePersist() {
        this.estado = true;
    }
}
Business Rules:
  • RUC serves as unique business identifier
  • Active by default
  • Stores company contact information

Transaction Domains

Venta (Sale)

@Entity
public class Venta {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    
    @Column(nullable = false, unique = true)
    private String serie;                  // 3-letter code (auto-generated)
    
    @Column(nullable = false, unique = true)
    private String numero;                 // 6-digit number (auto-generated)
    
    private String descripcion;
    
    @Column(name = "cliente_id")
    private Long clienteId;
    
    @Transient
    private Cliente cliente;               // Fetched from Cliente Service
    
    @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    @JoinColumn(name = "venta_id")
    private List<VentaDetalle> detalle;
    
    private LocalDateTime fechaVenta;
    private Double baseImponible;          // Taxable base
    private Double igv;                    // 18% tax (Peru)
    private Double total;
    
    @Column(name = "formapago_id")
    private Long formapagoId;
    
    @Transient
    private FormaPago formaPago;           // Fetched from Pagos Service
    
    @PrePersist
    public void prePersist() {
        calcularTotales();
        generarSerieYNumero();
    }
    
    @PreUpdate
    public void preUpdate() {
        calcularTotales();
    }
    
    private void calcularTotales() {
        this.baseImponible = 0.0;
        this.igv = 0.0;
        this.total = 0.0;
        
        if (detalle != null) {
            for (VentaDetalle d : detalle) {
                d.calcularMontos();
                this.baseImponible += d.getBaseImponible();
                this.igv += d.getIgv();
                this.total += d.getTotal();
            }
        }
    }
}

VentaDetalle (Sale Line Item)

@Entity
public class VentaDetalle {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    
    private Double cantidad;
    private Double precio;
    private Double baseImponible;
    private Double igv;
    private Double total;
    
    @Column(name = "producto_id")
    private Long productoId;
    
    @Transient
    private Producto producto;             // Fetched from Catalogo Service
    
    @PrePersist
    @PreUpdate
    public void calcularMontos() {
        if (precio != null && cantidad != null) {
            double baseUnitario = precio / 1.18;     // Peru: 18% IGV
            double igvUnitario = precio - baseUnitario;
            
            this.baseImponible = baseUnitario * cantidad;
            this.igv = igvUnitario * cantidad;
            this.total = precio * cantidad;
        }
    }
}

Tax Calculation Logic

Peru Tax System (IGV)The system implements Peru’s 18% IGV (Impuesto General a las Ventas) tax:
  • Price includes tax: precio is the final price with tax
  • Base calculation: baseImponible = precio / 1.18
  • Tax calculation: igv = precio - baseImponible
  • Total: total = precio × cantidad
Example:
  • Product price: S/. 100.00 (includes tax)
  • Base: S/. 84.75
  • IGV (18%): S/. 15.25
  • Total for 2 units: S/. 200.00

Compra (Purchase)

@Entity
public class Compra {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false, unique = true)
    private String serie;
    
    @Column(nullable = false, unique = true)
    private String numero;
    
    private String descripcion;
    
    @Column(name = "proveedor_id")
    private Long proveedorId;
    
    @Transient
    private Proveedor proveedor;           // Fetched from Proveedor Service
    
    @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    @JoinColumn(name = "compra_id")
    private List<CompraDetalle> detalle;
    
    private LocalDateTime fechaCompra;
    private Double baseImponible;
    private Double igv;
    private Double total;
    
    @Column(name = "formapago_id")
    private Long formapagoId;
    
    @Transient
    private FormaPago formaPago;
    
    // Similar calculation logic as Venta
}

CompraDetalle (Purchase Line Item)

Similar structure to VentaDetalle:
  • Quantity, price, tax calculations
  • Reference to Producto (via productoId)
  • Automatic calculation on persist/update

Pedido (Order)

@Entity
public class Pedido {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    
    @Column(nullable = false, unique = true)
    private String codigo;
    
    @Column(nullable = false, unique = true)
    private String serie;
    
    private String descripcion;
    private String estado;                 // Order status
    
    @Column(name = "cliente_id")
    private Long clienteId;
    
    @Transient
    private Cliente cliente;
    
    @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    @JoinColumn(name = "pedido_id")
    private List<PedidoDetalle> detalle;
    
    private LocalDateTime fechaPedido;
    private LocalDate fechaEntrega;        // Delivery date
    private Double baseImponible;
    private Double igv;
    private Double total;
    
    @Column(name = "formapago_id")
    private Long formapagoId;
    
    @Transient
    private FormaPago formaPago;
    
    public Pedido() {
        this.fechaPedido = LocalDateTime.now();
        this.fechaEntrega = this.fechaPedido.toLocalDate().plusWeeks(1);
        this.estado = "PENDIENTE";
    }
}
Order Status Values:
  • PENDIENTE: Pending
  • EN_PROCESO: In process
  • COMPLETADO: Completed
  • CANCELADO: Cancelled
Default Behavior:
  • Status: PENDIENTE
  • Delivery date: +7 days from order date
  • Automatic timestamp on creation

Payment Domain

FormaPago (Payment Method)

@Entity
public class FormaPago {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String nombre;                 // Payment method name
}
Standard Payment Methods:
  • Efectivo (Cash)
  • Transferencia Bancaria (Bank Transfer)
  • Cheque (Check)
  • Tarjeta de Crédito (Credit Card)
  • Tarjeta de Débito (Debit Card)
  • Depósito Bancario (Bank Deposit)
  • Letra de Cambio (Bill of Exchange)
  • Pagaré (Promissory Note)

Transaction Pattern

All transaction entities (Venta, Compra, Pedido) follow a common pattern:

Shared Characteristics

Header Features

  • Unique series and number
  • Date tracking
  • Tax calculation
  • Total aggregation
  • Payment method reference

Detail Features

  • Line item tracking
  • Quantity and price
  • Tax breakdown
  • Product reference
  • Automatic calculations

Cross-Service Data Flow

Creating a Sale (Venta)

Data Validation Flow


Data Integrity Patterns

Application-Level Foreign Keys

Since database foreign keys don’t span microservices:
// In VentaService
public Venta createVenta(VentaRequest request) {
    // Validate cliente exists
    Cliente cliente = clienteFeign.listarcliente(request.getClienteId())
        .orElseThrow(() -> new ClienteNotFoundException());
    
    // Validate each producto exists and has stock
    for (VentaDetalleRequest detalle : request.getDetalle()) {
        Producto producto = productoFeign.listarProducto(detalle.getProductoId())
            .orElseThrow(() -> new ProductoNotFoundException());
        
        if (producto.getCantidad() < detalle.getCantidad()) {
            throw new InsufficientStockException();
        }
    }
    
    // Validate payment method exists
    FormaPago formaPago = formaPagoFeign.buscarFormaPago(request.getFormapagoId())
        .orElseThrow(() -> new FormaPagoNotFoundException());
    
    // All validations passed, create venta
    return ventaRepository.save(venta);
}

Eventual Consistency

Some operations may have eventual consistency:
  1. Sale Created → Venta saved to database
  2. Inventory Update → Async call to Catalogo service
  3. Update Confirmation → Eventual consistency achieved
If inventory update fails:
  • Circuit breaker triggers fallback
  • Retry mechanism kicks in
  • Compensation transaction may be needed

Common Data Transfer Objects (DTOs)

Product DTO (used in Venta, Compra, Pedido services)

public class Producto {
    private Long id;
    private String codigo;
    private String nombre;
    private String descripcion;
    private Integer cantidad;
    private Double precioVenta;
    private Double costoCompra;
    private boolean estado;
    private String imagen;
    private Categoria categoria;
}

Cliente DTO (used in Venta, Pedido services)

public class Cliente {
    private Long id;
    private String dni;
    private String nombre;
    private String apellido;
    private String direccion;
    private String telefono;
    private String email;
    private Boolean activo;
}

Best Practices

Entity Design

Use @Transient for Cross-Service Data

Store IDs in database, fetch full objects via Feign when needed

Implement Lifecycle Callbacks

Use @PrePersist, @PreUpdate for automatic calculations and timestamps

Validate at Multiple Levels

  • Bean validation (@NotNull, @NotBlank)
  • Service-level business rules
  • Cross-service validation via Feign

Use Soft Deletes

Implement estado or activo flags instead of hard deletes

Performance Optimization

  • Use FetchType.LAZY for collections and relationships
  • Implement pagination for large result sets
  • Cache reference data (categories, payment methods)
  • Use DTOs to avoid lazy initialization exceptions

Data Consistency

  • Validate foreign IDs before saving
  • Implement compensation logic for failed operations
  • Use circuit breakers for resilience
  • Log all cross-service validation failures

Next Steps

Architecture Overview

Return to architecture overview

Microservices

Explore microservice implementations

Build docs developers (and LLMs) love