package usecase import ( "context" "errors" "fmt" "github.com/gofrs/uuid/v5" "github.com/saveinmed/backend-go/internal/domain" ) // CreateOrder validates and persists a new order, calculates shipping, reserves // stock and fires async notifications. Any pre-existing pending order for the // same buyer is deleted first to prevent zombie drafts. func (s *Service) CreateOrder(ctx context.Context, order *domain.Order) error { // Delete any stale pending orders from this buyer before creating a new one. orders, _, err := s.repo.ListOrders(ctx, domain.OrderFilter{BuyerID: &order.BuyerID, Limit: 100}) if err == nil { for _, o := range orders { if o.Status == domain.OrderStatusPending { _ = s.DeleteOrder(ctx, o.ID) } } } order.ID = uuid.Must(uuid.NewV7()) order.Status = domain.OrderStatusPending // Calculate and apply shipping cost. buyerAddr := &domain.Address{ Latitude: order.Shipping.Latitude, Longitude: order.Shipping.Longitude, } fee, dist, err := s.CalculateShipping(ctx, buyerAddr, order.SellerID, order.TotalCents) if err == nil { order.ShippingFeeCents = fee order.DistanceKm = dist order.TotalCents += fee } else { if err.Error() == "address out of delivery range" { return err } } if err := s.repo.CreateOrder(ctx, order); err != nil { return err } // Fire async notifications without blocking the HTTP response. go func() { buyer, err := s.repo.GetUser(context.Background(), order.BuyerID) if err != nil { return } users, _, _ := s.repo.ListUsers(context.Background(), domain.UserFilter{CompanyID: &order.SellerID, Limit: 1}) var seller *domain.User if len(users) > 0 { seller = &users[0] } else { seller = &domain.User{Name: "Seller", Email: "seller@platform.com"} } s.notify.NotifyOrderCreated(context.Background(), order, buyer, seller) }() return nil } // ListOrders returns a paginated list of orders. func (s *Service) ListOrders(ctx context.Context, filter domain.OrderFilter, page, pageSize int) (*domain.OrderPage, error) { if pageSize <= 0 { pageSize = 20 } if page <= 0 { page = 1 } filter.Limit = pageSize filter.Offset = (page - 1) * pageSize orders, total, err := s.repo.ListOrders(ctx, filter) if err != nil { return nil, err } return &domain.OrderPage{Orders: orders, Total: total, Page: page, PageSize: pageSize}, nil } // GetOrder retrieves an order by ID. func (s *Service) GetOrder(ctx context.Context, id uuid.UUID) (*domain.Order, error) { return s.repo.GetOrder(ctx, id) } // UpdateOrderStatus enforces the order state machine, restores stock // on cancellation, and fires async notifications. func (s *Service) UpdateOrderStatus(ctx context.Context, id uuid.UUID, status domain.OrderStatus) error { order, err := s.repo.GetOrder(ctx, id) if err != nil { return err } // State machine transitions. switch order.Status { case domain.OrderStatusPending: if status != domain.OrderStatusPaid && status != domain.OrderStatusCancelled { return errors.New("invalid transition from Pending") } case domain.OrderStatusPaid: if status != domain.OrderStatusInvoiced && status != domain.OrderStatusShipped && status != domain.OrderStatusCancelled { return errors.New("invalid transition from Paid") } case domain.OrderStatusInvoiced: if status != domain.OrderStatusShipped && status != domain.OrderStatusCancelled { return errors.New("invalid transition from Invoiced") } case domain.OrderStatusShipped: if status != domain.OrderStatusDelivered { return errors.New("invalid transition from Shipped") } case domain.OrderStatusDelivered: if status != domain.OrderStatusCompleted { return errors.New("invalid transition from Delivered") } case domain.OrderStatusCompleted, domain.OrderStatusCancelled: return errors.New("order is in terminal state") } if err := s.repo.UpdateOrderStatus(ctx, id, status); err != nil { return err } // Restore stock when cancelling a non-shipped order. if status == domain.OrderStatusCancelled && (order.Status == domain.OrderStatusPending || order.Status == domain.OrderStatusPaid || order.Status == domain.OrderStatusInvoiced) { for _, item := range order.Items { if _, err := s.repo.AdjustInventory(ctx, item.ProductID, int64(item.Quantity), "Order Cancelled"); err != nil { // TODO: enqueue a reconciliation job instead of silently ignoring. fmt.Printf("WARN: failed to restore stock for item %s: %v\n", item.ProductID, err) } } } // Async buyer notification. go func() { updatedOrder, _ := s.repo.GetOrder(context.Background(), id) if updatedOrder == nil { return } buyer, err := s.repo.GetUser(context.Background(), updatedOrder.BuyerID) if err != nil { return } s.notify.NotifyOrderStatusChanged(context.Background(), updatedOrder, buyer) }() return nil } // DeleteOrder removes an order and restores reserved stock for non-shipped orders. func (s *Service) DeleteOrder(ctx context.Context, id uuid.UUID) error { order, err := s.repo.GetOrder(ctx, id) if err != nil { return err } if order.Status == domain.OrderStatusPending || order.Status == domain.OrderStatusPaid || order.Status == domain.OrderStatusInvoiced { for _, item := range order.Items { if _, err := s.repo.AdjustInventory(ctx, item.ProductID, int64(item.Quantity), "Order Deleted"); err != nil { fmt.Printf("WARN: failed to restore stock for item %s: %v\n", item.ProductID, err) } } } return s.repo.DeleteOrder(ctx, id) } // UpdateOrderItems replaces the line-items and total on an existing order. func (s *Service) UpdateOrderItems(ctx context.Context, orderID uuid.UUID, items []domain.OrderItem, totalCents int64) error { if _, err := s.repo.GetOrder(ctx, orderID); err != nil { return err } return s.repo.UpdateOrderItems(ctx, orderID, items, totalCents) }