saveinmed/backend/internal/usecase/product_usecase.go

205 lines
5.9 KiB
Go

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
}
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)
}