package usecase import ( "context" "encoding/csv" "errors" "fmt" "io" "strconv" "strings" "time" "github.com/gofrs/uuid/v5" "github.com/saveinmed/backend-go/internal/domain" ) type ImportReport struct { TotalProcessed int `json:"total_processed"` SuccessCount int `json:"success_count"` FailedCount int `json:"failed_count"` Errors []string `json:"errors"` } // ImportProducts parses a CSV file and batch inserts valid products. // CSV Headers expected: name,ean,price,stock,description func (s *Service) ImportProducts(ctx context.Context, sellerID uuid.UUID, r io.Reader) (*ImportReport, error) { reader := csv.NewReader(r) rows, err := reader.ReadAll() if err != nil { return nil, err } if len(rows) < 2 { // Header + at least 1 row return nil, errors.New("csv file is empty or missing headers") } report := &ImportReport{} var products []domain.Product // Header mapping (simple index search) headers := rows[0] idxMap := make(map[string]int) for i, h := range headers { idxMap[strings.ToLower(strings.TrimSpace(h))] = i } required := []string{"name", "price"} for _, req := range required { if _, ok := idxMap[req]; !ok { return nil, fmt.Errorf("missing required header: %s", req) } } for i, row := range rows[1:] { report.TotalProcessed++ lineNum := i + 2 // 1-based, +header // Parse Name name := strings.TrimSpace(row[idxMap["name"]]) if name == "" { report.FailedCount++ report.Errors = append(report.Errors, fmt.Sprintf("Line %d: name is required", lineNum)) continue } // Parse Price (float or int string) priceStr := strings.TrimSpace(row[idxMap["price"]]) priceFloat, err := strconv.ParseFloat(priceStr, 64) if err != nil { report.FailedCount++ report.Errors = append(report.Errors, fmt.Sprintf("Line %d: invalid price '%s'", lineNum, priceStr)) continue } priceCents := int64(priceFloat * 100) // Defaults / Optionals // var stock int64 // Removed for Dictionary Mode // if idx, ok := idxMap["stock"]; ok && idx < len(row) { // if s, err := strconv.ParseInt(strings.TrimSpace(row[idx]), 10, 64); err == nil { // stock = s // } // } var description string if idx, ok := idxMap["description"]; ok && idx < len(row) { description = strings.TrimSpace(row[idx]) } var ean string if idx, ok := idxMap["ean"]; ok && idx < len(row) { ean = strings.TrimSpace(row[idx]) } prod := domain.Product{ ID: uuid.Must(uuid.NewV7()), SellerID: sellerID, Name: name, Description: description, EANCode: ean, PriceCents: priceCents, // Stock & ExpiresAt removed from Catalog Dictionary CreatedAt: time.Now().UTC(), UpdatedAt: time.Now().UTC(), } products = append(products, prod) } if len(products) > 0 { if err := s.repo.BatchCreateProducts(ctx, products); err != nil { // If batch fails, we fail mostly everything? // Or we could implement line-by-line insert in repo. // For ImportProducts, failing the whole batch is acceptable if DB constraint fails. return nil, fmt.Errorf("batch insert failed: %w", err) } } return report, nil } func (s *Service) ListCategories(ctx context.Context) ([]string, error) { return s.repo.ListCategories(ctx) } func (s *Service) GetProductByEAN(ctx context.Context, ean string) (*domain.Product, error) { return s.repo.GetProductByEAN(ctx, ean) } func (s *Service) RegisterInventoryItem(ctx context.Context, item *domain.InventoryItem) error { return s.repo.CreateInventoryItem(ctx, item) } func (s *Service) UpdateInventoryItem(ctx context.Context, itemID uuid.UUID, sellerID uuid.UUID, priceCents int64, stockQuantity int64) error { // We construct a partial item just for update item := &domain.InventoryItem{ ID: itemID, SellerID: sellerID, SalePriceCents: priceCents, StockQuantity: stockQuantity, } // Future improvement: check if item exists and belongs to seller first, strict validation return s.repo.UpdateInventoryItem(ctx, item) }