Overview
TechStore’s order management system handles the complete order lifecycle from creation to fulfillment, including automatic stock management, email notifications, and PDF report generation for both customers and administrators.
Orders automatically trigger email confirmations with purchase details and can generate PDF reports for record-keeping.
Order Model
The order system uses two related entities:
@ Entity
@ Table ( name = "pedidos" )
public class Pedido {
private Long id ;
@ ManyToOne
@ JoinColumn ( name = "id_usuario" )
private Usuario usuario ;
private LocalDateTime fechaPedido ;
private BigDecimal total ;
private String estado ; // PENDIENTE, PAGADO, ENVIADO, ENTREGADO, CANCELADO
@ OneToMany ( mappedBy = "pedido" , cascade = CascadeType . ALL , orphanRemoval = true )
private List < PedidoDetalle > detalles ;
}
@ Entity
public class PedidoDetalle {
private Long id ;
@ ManyToOne
private Pedido pedido ;
@ ManyToOne
private Producto producto ;
private Integer cantidad ;
private BigDecimal precioUnitario ; // Snapshot of price at purchase time
}
Creating Orders
Submit order request
Client sends order with user ID and product details
Validate user
System verifies user exists in the database
Check stock availability
For each product, validate sufficient stock exists
Deduct stock
Reduce product stock by ordered quantities
Calculate total
Sum all line items to compute order total
Save order
Persist order to database with PENDIENTE status
Send confirmation email
Email customer with order details
Create Order Endpoint
POST /api/pedidos
Authorization : Bearer {token}
Request Body :
{
"usuario" : {
"id" : 5
},
"detalles" : [
{
"producto" : { "id" : 10 },
"cantidad" : 2
},
{
"producto" : { "id" : 15 },
"cantidad" : 1
}
]
}
Implementation :
@ PostMapping
public ResponseEntity < ? > crear (@ RequestBody Pedido pedido) {
try {
Pedido nuevoPedido = pedidoService . createPedido (pedido);
return new ResponseEntity <>(nuevoPedido, HttpStatus . CREATED );
} catch ( RuntimeException e ) {
// Catches "Stock insuficiente" or "Usuario no encontrado" errors
return ResponseEntity . status ( HttpStatus . BAD_REQUEST )
. body ( e . getMessage ());
} catch ( Exception e ) {
return ResponseEntity . status ( HttpStatus . INTERNAL_SERVER_ERROR )
. body ( "Error interno en el servidor" );
}
}
Order Creation Logic
@ Transactional
public Pedido createPedido ( Pedido pedido) {
// 1. Validate user
Usuario usuarioExistente = usuarioRepository . findById ( pedido . getUsuario (). getId ())
. orElseThrow (() -> new RuntimeException ( "Usuario no encontrado" ));
pedido . setUsuario (usuarioExistente);
// 2. Set initial status
if ( pedido . getEstado () == null ) {
pedido . setEstado ( "PENDIENTE" );
}
BigDecimal total = BigDecimal . ZERO ;
// 3. Process each line item
for ( PedidoDetalle detalle : pedido . getDetalles ()) {
Producto producto = productRepository . findById ( detalle . getProducto (). getId ())
. orElseThrow (() -> new RuntimeException ( "Producto no encontrado" ));
// Validate stock
if ( producto . getStock () < detalle . getCantidad ()) {
throw new RuntimeException ( "Stock insuficiente para: " + producto . getNombre ());
}
// Deduct stock
producto . setStock ( producto . getStock () - detalle . getCantidad ());
productRepository . save (producto);
// Set relationships and pricing
detalle . setPedido (pedido);
detalle . setPrecioUnitario ( producto . getPrecio ());
detalle . setProducto (producto);
// Calculate subtotal
BigDecimal subtotal = producto . getPrecio ()
. multiply ( new BigDecimal ( detalle . getCantidad ()));
total = total . add (subtotal);
}
pedido . setTotal (total);
// 4. Save to database
Pedido pedidoGuardado = pedidoRepository . save (pedido);
// 5. Send confirmation email
try {
emailService . enviarInformeCompra (pedidoGuardado);
} catch ( Exception e ) {
System . err . println ( "Error al enviar correo: " + e . getMessage ());
// Don't fail the transaction if email fails
}
return pedidoGuardado;
}
The order creation is transactional. If stock validation fails for any item, the entire order is rolled back.
Viewing Orders
Get User’s Orders
GET /api/pedidos/usuario/{usuarioId}
Authorization : Bearer {token}
Returns all orders for a specific user:
@ GetMapping ( "/usuario/{usuarioId}" )
public List < Pedido > listarPorUsuario (@ PathVariable Long usuarioId) {
return pedidoService . listarPedidoPorUsuario (usuarioId);
}
Use Case : Customer order history page showing all past purchases.
Get All Orders (Admin)
GET /api/pedidos
Authorization : Bearer {admin-token}
Requires ROLE_ADMIN authentication.
@ GetMapping
public List < Pedido > listarTodos () {
return pedidoService . obtenerTodosLosPedidos ();
}
Use Case : Admin dashboard showing all customer orders for fulfillment.
Get Single Order
GET /api/pedidos/{id}
Authorization : Bearer {token}
Returns complete order details including line items:
@ GetMapping ( "/{id}" )
public ResponseEntity < Pedido > getById (@ PathVariable Long id) {
Pedido pedido = pedidoService . obtenerPorId (id);
return pedido != null
? ResponseEntity . ok (pedido)
: ResponseEntity . notFound (). build ();
}
Order Status Management
Available Statuses
PENDIENTE Initial status when order is created. Awaiting payment or processing.
PAGADO Payment confirmed. Ready for fulfillment.
ENVIADO Order shipped to customer. Tracking available.
ENTREGADO Successfully delivered to customer.
CANCELADO Order cancelled. Stock automatically restored.
Update Order Status
PUT /api/pedidos/{id}/estado?nuevoEstado={status}
Authorization : Bearer {admin-token}
Example : PUT /api/pedidos/42/estado?nuevoEstado=ENVIADO
Implementation :
@ PutMapping ( "/{id}/estado" )
public ResponseEntity < Pedido > actualizarEstado (
@ PathVariable Long id,
@ RequestParam String nuevoEstado
) {
Pedido pedidoActualizado = pedidoService . actualizarEstado (id, nuevoEstado);
return ResponseEntity . ok (pedidoActualizado);
}
Automatic Stock Restoration on Cancellation
When an order is cancelled, the system automatically returns products to inventory:
@ Transactional
public Pedido actualizarEstado ( Long id, String nuevoEstado) {
Pedido pedido = pedidoRepository . findById (id)
. orElseThrow (() -> new RuntimeException ( "Pedido no encontrado" ));
// If changing TO cancelled status (and wasn't already cancelled)
if ( "CANCELADO" . equalsIgnoreCase (nuevoEstado)
&& ! "CANCELADO" . equals ( pedido . getEstado ())) {
reponerStock (pedido);
}
pedido . setEstado ( nuevoEstado . toUpperCase ());
return pedidoRepository . save (pedido);
}
private void reponerStock ( Pedido pedido) {
for ( PedidoDetalle detalle : pedido . getDetalles ()) {
Producto producto = detalle . getProducto ();
if (producto != null ) {
producto . setStock ( producto . getStock () + detalle . getCantidad ());
productRepository . save (producto);
}
}
}
Stock restoration only happens when transitioning TO cancelled status. Re-cancelling an already cancelled order won’t double the stock.
PDF Report Generation
Administrators can generate comprehensive sales reports in PDF format:
POST /api/pedidos/reporte/pdf
Authorization : Bearer {admin-token}
Request Body (filtered orders):
[
{
"id" : 1 ,
"fechaPedido" : "2026-03-01T10:30:00" ,
"usuario" : {
"nombre" : "John" ,
"apellido" : "Doe" ,
"email" : "john@example.com"
},
"estado" : "ENTREGADO" ,
"total" : 1599.99
},
...
]
Implementation :
@ PostMapping ( "/reporte/pdf" )
public ResponseEntity < byte [] > descargarReporte (
@ RequestBody List < Pedido > pedidosFiltrados
) {
byte [] pdf = pedidoReporteService . generarReportePedidos (pedidosFiltrados);
HttpHeaders headers = new HttpHeaders ();
headers . setContentType ( MediaType . APPLICATION_PDF );
headers . setContentDispositionFormData ( "attachment" , "reporte-ventas.pdf" );
return new ResponseEntity <>(pdf, headers, HttpStatus . OK );
}
PDF Report Structure
The report includes:
Header : “TECHSTORE - REPORTE DE VENTAS”
Table with columns:
ID
Fecha (Order date)
Cliente (Customer name/email)
Estado (Status)
Total (Order amount)
Footer : Grand total of all orders
PDF Generation Code :
public byte [] generarReportePedidos ( List < Pedido > pedidos) {
ByteArrayOutputStream out = new ByteArrayOutputStream ();
Document documento = new Document ( PageSize . A4 );
try {
PdfWriter . getInstance (documento, out);
documento . open ();
// Title
Font fuenteTitulo = FontFactory . getFont ( FontFactory . HELVETICA_BOLD , 18 );
Paragraph titulo = new Paragraph ( "TECHSTORE - REPORTE DE VENTAS" , fuenteTitulo);
titulo . setAlignment ( Element . ALIGN_CENTER );
titulo . setSpacingAfter ( 20 );
documento . add (titulo);
// Table setup
PdfPTable tabla = new PdfPTable ( 5 );
tabla . setWidthPercentage ( 100 );
tabla . setWidths ( new float []{ 1.0f , 2.5f , 3.5f , 2.0f , 2.0f });
// Headers
String [] cabeceras = { "ID" , "Fecha" , "Cliente" , "Estado" , "Total" };
for ( String h : cabeceras) {
PdfPCell cell = new PdfPCell (
new Paragraph (h, FontFactory . getFont ( FontFactory . HELVETICA_BOLD ))
);
cell . setHorizontalAlignment ( Element . ALIGN_CENTER );
cell . setPadding ( 5 );
cell . setBackgroundColor ( java . awt . Color . LIGHT_GRAY );
tabla . addCell (cell);
}
// Data rows
BigDecimal granTotal = BigDecimal . ZERO ;
for ( Pedido p : pedidos) {
tabla . addCell ( String . valueOf ( p . getId ()));
tabla . addCell ( p . getFechaPedido (). toString ());
String clienteInfo = p . getUsuario (). getNombre () + " "
+ p . getUsuario (). getApellido ();
if ( clienteInfo . trim (). isEmpty ()) {
clienteInfo = p . getUsuario (). getEmail ();
}
tabla . addCell (clienteInfo);
tabla . addCell ( p . getEstado ());
BigDecimal valorPedido = p . getTotal () != null
? p . getTotal ()
: BigDecimal . ZERO ;
tabla . addCell ( "$" + String . format ( "%.2f" , valorPedido));
granTotal = granTotal . add (valorPedido);
}
documento . add (tabla);
// Grand total
Paragraph totalFinal = new Paragraph (
" \n TOTAL RECAUDADO: $" + String . format ( "%.2f" , granTotal),
FontFactory . getFont ( FontFactory . HELVETICA_BOLD , 14 )
);
totalFinal . setAlignment ( Element . ALIGN_RIGHT );
documento . add (totalFinal);
documento . close ();
} catch ( Exception e ) {
e . printStackTrace ();
}
return out . toByteArray ();
}
Generate reports for specific date ranges or statuses by filtering orders before sending to the endpoint.
Deleting Orders
DELETE /api/pedidos/{id}
Authorization : Bearer {admin-token}
Deleting an order does NOT restore stock. Cancel the order first if stock restoration is needed.
@ DeleteMapping ( "/{id}" )
public ResponseEntity < ? > eliminar (@ PathVariable Long id) {
try {
pedidoService . eliminarPedido (id);
return ResponseEntity . ok (). body ( "Pedido eliminado correctamente" );
} catch ( Exception e ) {
return ResponseEntity . status ( HttpStatus . INTERNAL_SERVER_ERROR )
. body ( "Error al eliminar el pedido" );
}
}
Email Notifications
When an order is successfully created, the system automatically sends a purchase confirmation email:
try {
emailService . enviarInformeCompra (pedidoGuardado);
} catch ( Exception e ) {
System . err . println ( "Error al enviar correo de compra: " + e . getMessage ());
// Don't cancel transaction if email fails
}
Email failures do not roll back the order transaction. Orders are saved even if email delivery fails.
API Reference
Method Endpoint Description Auth POST /api/pedidosCreate new order USER GET /api/pedidos/usuario/{userId}Get user’s orders USER GET /api/pedidos/{id}Get order details USER GET /api/pedidosList all orders ADMIN PUT /api/pedidos/{id}/estadoUpdate order status ADMIN POST /api/pedidos/reporte/pdfGenerate PDF report ADMIN DELETE /api/pedidos/{id}Delete order ADMIN
Workflow Example
Customer adds items to cart
Products added with desired quantities
Proceed to checkout
Cart converted to order via /api/pedidos endpoint
Order created (PENDIENTE)
Stock deducted, confirmation email sent
Admin processes payment
Status updated to PAGADO via admin dashboard
Order fulfilled
Status progresses: ENVIADO → ENTREGADO
Generate report
Monthly sales report created as PDF
Best Practices
Price Snapshots Store precioUnitario in order details to preserve historical pricing.
Stock Validation Always validate stock before accepting orders to prevent overselling.
Transaction Safety Use @Transactional to ensure order creation is atomic.
Status Tracking Implement clear status progression for better customer communication.