package usecase import ( "context" "errors" "fmt" "math" "strings" "time" "github.com/gofrs/uuid/v5" "github.com/saveinmed/backend-go/internal/domain" ) // RegisterProduct generates an ID and persists a new product. func (s *Service) RegisterProduct(ctx context.Context, product *domain.Product) error { // Business Rule: Pharmacy-to-Pharmacy model. company, err := s.repo.GetCompany(ctx, product.SellerID) if err != nil { return fmt.Errorf("failed to verify seller: %w", err) } if company.Category != "farmacia" && company.Category != "distribuidora" { return errors.New("business rule violation: only registered pharmacies or distributors can register products") } product.ID = uuid.Must(uuid.NewV7()) return s.repo.CreateProduct(ctx, product) } // ListProducts returns a paginated list of products matching the filter. func (s *Service) ListProducts(ctx context.Context, filter domain.ProductFilter, page, pageSize int) (*domain.ProductPage, error) { if pageSize <= 0 { pageSize = 20 } if page <= 0 { page = 1 } filter.Limit = pageSize filter.Offset = (page - 1) * pageSize products, total, err := s.repo.ListProducts(ctx, filter) if err != nil { return nil, err } return &domain.ProductPage{Products: products, Total: total, Page: page, PageSize: pageSize}, nil } // StartStockCleanupWorker runs a background goroutine to expire old reservations. func (s *Service) StartStockCleanupWorker(ctx context.Context) { ticker := time.NewTicker(5 * time.Minute) go func() { for { select { case <-ticker.C: if err := s.repo.ExpireReservations(context.Background()); err != nil { fmt.Printf("ERROR: failed to expire stock reservations: %v\n", err) } case <-ctx.Done(): ticker.Stop() return } } }() } // ReserveStock creates a temporary hold on inventory. func (s *Service) ReserveStock(ctx context.Context, productID, inventoryItemID, buyerID uuid.UUID, quantity int64) (*domain.StockReservation, error) { // 1. Check availability (physical stock - active reservations) item, err := s.repo.GetInventoryItem(ctx, inventoryItemID) if err != nil { return nil, err } reserved, err := s.repo.GetActiveReservations(ctx, inventoryItemID) if err != nil { return nil, err } if item.StockQuantity-reserved < quantity { return nil, errors.New("insufficient available stock (some units are reserved in checkouts)") } res := &domain.StockReservation{ ID: uuid.Must(uuid.NewV7()), ProductID: productID, InventoryItemID: inventoryItemID, BuyerID: buyerID, Quantity: quantity, Status: "active", ExpiresAt: time.Now().Add(15 * time.Minute), CreatedAt: time.Now().UTC(), } if err := s.repo.ReserveStock(ctx, res); err != nil { return nil, err } return res, nil } // SearchProducts returns products with distance, ordered by expiration date. // Seller info is anonymised until checkout. func (s *Service) SearchProducts(ctx context.Context, filter domain.ProductSearchFilter, page, pageSize int) (*domain.ProductSearchPage, error) { if pageSize <= 0 { pageSize = 20 } if page <= 0 { page = 1 } filter.Limit = pageSize filter.Offset = (page - 1) * pageSize products, total, err := s.repo.SearchProducts(ctx, filter) if err != nil { return nil, err } // Adjust displayed stock by subtracting reservations // In production, the Repo query itself would handle this for better performance for i := range products { // This is a simplified adjustment. // Ideally, SearchProducts Repo method should do: // stock = (SUM(i.stock_quantity) - (SELECT SUM(quantity) FROM stock_reservations WHERE product_id = p.id AND status='active')) } return &domain.ProductSearchPage{Products: products, Total: total, Page: page, PageSize: pageSize}, nil } // ListRecords provides advanced search for marketplace listings. func (s *Service) ListRecords(ctx context.Context, req domain.SearchRequest) (*domain.PaginationResponse[domain.Product], error) { page := req.Page pageSize := req.PageSize if pageSize <= 0 { pageSize = 20 } if page <= 0 { page = 1 } sortBy := strings.TrimSpace(req.SortBy) if sortBy == "" { sortBy = "updated_at" } sortOrder := strings.ToLower(strings.TrimSpace(req.SortOrder)) if sortOrder == "" { sortOrder = "desc" } filter := domain.RecordSearchFilter{ Query: strings.TrimSpace(req.Query), SortBy: sortBy, SortOrder: sortOrder, CreatedAfter: req.CreatedAfter, CreatedBefore: req.CreatedBefore, Limit: pageSize, Offset: (page - 1) * pageSize, } items, total, err := s.repo.ListRecords(ctx, filter) if err != nil { return nil, err } totalPages := int(math.Ceil(float64(total) / float64(pageSize))) return &domain.PaginationResponse[domain.Product]{ Items: items, TotalCount: total, CurrentPage: page, TotalPages: totalPages, }, nil } // GetProduct retrieves a product by ID. func (s *Service) GetProduct(ctx context.Context, id uuid.UUID) (*domain.Product, error) { return s.repo.GetProduct(ctx, id) } // UpdateProduct persists changes to a product. func (s *Service) UpdateProduct(ctx context.Context, product *domain.Product) error { return s.repo.UpdateProduct(ctx, product) } // DeleteProduct removes a product by ID. func (s *Service) DeleteProduct(ctx context.Context, id uuid.UUID) error { return s.repo.DeleteProduct(ctx, id) } // ListInventory returns a paginated list of inventory items for a seller. func (s *Service) ListInventory(ctx context.Context, filter domain.InventoryFilter, page, pageSize int) (*domain.InventoryPage, error) { if pageSize <= 0 { pageSize = 20 } if page <= 0 { page = 1 } filter.Limit = pageSize filter.Offset = (page - 1) * pageSize items, total, err := s.repo.ListInventory(ctx, filter) if err != nil { return nil, err } return &domain.InventoryPage{Items: items, Total: total, Page: page, PageSize: pageSize}, nil } // AdjustInventory increments or decrements the stock of a product. func (s *Service) AdjustInventory(ctx context.Context, productID uuid.UUID, delta int64, reason string) (*domain.InventoryItem, error) { return s.repo.AdjustInventory(ctx, productID, delta, reason) } // ListManufacturers returns all distinct manufacturer names in the catalogue. func (s *Service) ListManufacturers(ctx context.Context) ([]string, error) { return s.repo.ListManufacturers(ctx) }