205 lines
5.9 KiB
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)
|
|
}
|