saveinmed/backend-old/internal/usecase/product_service.go
NANDO9322 e8c29877de feat(produto): implementação do fluxo de cadastro e gestão de estoque
Backend:
- Adição das migrações SQL 0012 e 0013 para estrutura de produtos e itens de estoque.
- Implementação do método [CreateInventoryItem](cci:1://file:///c:/Projetos/saveinmed/backend-old/internal/http/handler/handler_test.go:168:0-170:1) no repositório Postgres e mocks de teste.
- Atualização do [product_handler.go](cci:7://file:///c:/Projetos/saveinmed/backend-old/internal/http/handler/product_handler.go:0:0-0:0) para suportar `original_price_cents` e corrigir filtragem de estoque.
- Mapeamento da rota GET `/api/v1/produtos-venda` no [server.go](cci:7://file:///c:/Projetos/saveinmed/backend-old/internal/server/server.go:0:0-0:0).
- Ajuste no endpoint `/auth/me` para retornar `empresasDados` (ID da empresa) necessário ao frontend.
- Refatoração da query [ListInventory](cci:1://file:///c:/Projetos/saveinmed/backend-old/internal/repository/postgres/postgres.go:771:0-805:1) para buscar da tabela correta e incluir nome do produto.

Frontend:
- Correção no mapeamento de dados (snake_case para camelCase) na página de Gestão de Produtos.
- Ajustes de integração no Wizard de Cadastro de Produtos (`CadastroProdutoWizard.tsx`).
- Atualização da tipagem para exibir corretamente preços e estoque a partir da API.
2026-01-22 18:59:21 -03:00

130 lines
3.5 KiB
Go

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