saveinmed/backend/internal/usecase/shipping_usecase_test.go

269 lines
8.4 KiB
Go

package usecase
// shipping_usecase_test.go — Testes de unidade: cálculo de frete
//
// Cobre:
// - Frete grátis por threshold
// - Frete mínimo
// - Endereço fora do raio de entrega
// - Frete calculado por distância
// - Sem configuração de shipping (frete grátis implícito)
// - Retirada em loja disponível
//
// Execute com:
// go test ./internal/usecase/... -run TestShipping -v
import (
"context"
"testing"
"github.com/gofrs/uuid/v5"
"github.com/saveinmed/backend-go/internal/domain"
"github.com/saveinmed/backend-go/internal/infrastructure/mapbox"
"github.com/saveinmed/backend-go/internal/infrastructure/notifications"
)
func newShippingTestService() (*Service, *MockRepository) {
repo := NewMockRepository()
notify := notifications.NewLoggerNotificationService()
// Usar um cliente mapbox com token vazio — fará a requisição HTTP falhar,
// o que faz o CalculateShipping usar haversine como fallback (linha 71 de shipping_usecase.go).
// Isso evita nil pointer panic e permite testar a lógica de negócio localmente.
mb := mapbox.New("test-token-invalid")
svc := NewService(repo, nil, mb, notify, 0.05, 0.12, "test-secret", 0, "")
return svc, repo
}
// baseSettings retorna uma configuração de frete padrão para testes.
func baseShippingSettings(vendorID uuid.UUID) domain.ShippingSettings {
threshold := int64(10000) // frete grátis acima de R$100
return domain.ShippingSettings{
VendorID: vendorID,
Active: true,
MaxRadiusKm: 50.0,
PricePerKmCents: 200, // R$2/km
MinFeeCents: 1500, // R$15 mínimo
FreeShippingThresholdCents: &threshold,
Latitude: -23.55, // São Paulo centro
Longitude: -46.63,
}
}
// =============================================================================
// Happy path — Cálculo de frete
// =============================================================================
func TestShippingCalculate_FreeByThreshold(t *testing.T) {
svc, repo := newShippingTestService()
vendorID := uuid.Must(uuid.NewV7())
settings := baseShippingSettings(vendorID)
repo.shippingSettings[vendorID] = settings
// Compra acima do threshold → frete grátis
buyerAddr := &domain.Address{
Latitude: -23.56, // próximo ao vendedor
Longitude: -46.64,
}
cartTotal := int64(15000) // R$150 > threshold de R$100
fee, _, err := svc.CalculateShipping(context.Background(), buyerAddr, vendorID, cartTotal)
if err != nil {
t.Fatalf("CalculateShipping não deve falhar para compra acima do threshold: %v", err)
}
if fee != 0 {
t.Errorf("Frete deve ser GRÁTIS para cart_total=%d acima do threshold=%d, got fee=%d",
cartTotal, *settings.FreeShippingThresholdCents, fee)
}
}
func TestShippingCalculate_MinFee(t *testing.T) {
svc, repo := newShippingTestService()
vendorID := uuid.Must(uuid.NewV7())
settings := baseShippingSettings(vendorID)
settings.FreeShippingThresholdCents = nil // sem threshold de frete grátis
settings.MaxRadiusKm = 1000 // raio gigante para não rejeitar
settings.PricePerKmCents = 1 // tarifa mínima por km
repo.shippingSettings[vendorID] = settings
// Comprando muito perto — deveria pagar só o mínimo
buyerAddr := &domain.Address{
Latitude: settings.Latitude + 0.001, // ~100m de distância
Longitude: settings.Longitude,
}
fee, _, err := svc.CalculateShipping(context.Background(), buyerAddr, vendorID, 5000)
if err != nil {
t.Fatalf("CalculateShipping falhou: %v", err)
}
if fee < settings.MinFeeCents {
t.Errorf("Taxa calculada (%d) deve ser pelo menos o mínimo (%d)", fee, settings.MinFeeCents)
}
}
func TestShippingCalculate_OutOfRange(t *testing.T) {
svc, repo := newShippingTestService()
vendorID := uuid.Must(uuid.NewV7())
settings := baseShippingSettings(vendorID)
settings.MaxRadiusKm = 5 // raio pequeno
settings.Latitude = -23.55
settings.Longitude = -46.63
repo.shippingSettings[vendorID] = settings
// Comprador longe demais (Rio de Janeiro ~360km de SP)
buyerAddr := &domain.Address{
Latitude: -22.90,
Longitude: -43.17,
}
_, _, err := svc.CalculateShipping(context.Background(), buyerAddr, vendorID, 5000)
if err == nil {
t.Fatal("CalculateShipping deve retornar erro quando endereço está fora do raio de entrega")
}
}
func TestShippingCalculate_NoSettings(t *testing.T) {
svc, repo := newShippingTestService()
vendorID := uuid.Must(uuid.NewV7())
// Não adicionar settings → frete grátis implícito
_ = repo // sem settings configurados
buyerAddr := &domain.Address{
Latitude: -23.56,
Longitude: -46.64,
}
fee, dist, err := svc.CalculateShipping(context.Background(), buyerAddr, vendorID, 5000)
if err != nil {
t.Fatalf("Sem configuração de frete deve retornar fee=0 sem erro: %v", err)
}
if fee != 0 {
t.Errorf("Sem configuração, fee deve ser 0, got %d", fee)
}
if dist != 0 {
t.Errorf("Sem configuração, dist deve ser 0, got %f", dist)
}
}
func TestShippingCalculate_InactiveSettings(t *testing.T) {
svc, repo := newShippingTestService()
vendorID := uuid.Must(uuid.NewV7())
settings := baseShippingSettings(vendorID)
settings.Active = false // frete desativado pelo vendedor
repo.shippingSettings[vendorID] = settings
buyerAddr := &domain.Address{
Latitude: -23.56,
Longitude: -46.64,
}
fee, _, err := svc.CalculateShipping(context.Background(), buyerAddr, vendorID, 5000)
if err != nil {
t.Fatalf("Frete inativo deve retornar fee=0 sem erro: %v", err)
}
if fee != 0 {
t.Errorf("Frete inativo deve retornar fee=0, got %d", fee)
}
}
func TestShippingCalculate_MissingCoordinates(t *testing.T) {
svc, repo := newShippingTestService()
vendorID := uuid.Must(uuid.NewV7())
settings := baseShippingSettings(vendorID)
repo.shippingSettings[vendorID] = settings
// Endereço sem coordenadas → deve usar fee mínimo
buyerAddr := &domain.Address{
Latitude: 0,
Longitude: 0,
}
fee, _, err := svc.CalculateShipping(context.Background(), buyerAddr, vendorID, 1000)
if err != nil {
t.Fatalf("Endereço sem coordenadas não deve retornar erro: %v", err)
}
// Sem coordenadas retorna o mínimo configurado
if fee != settings.MinFeeCents {
t.Errorf("Sem coordenadas, fee deve ser MinFeeCents=%d, got %d", settings.MinFeeCents, fee)
}
}
// =============================================================================
// Opções de shipping (delivery + pickup)
// =============================================================================
func TestShippingCalculateOptions_DeliveryAndPickup(t *testing.T) {
svc, repo := newShippingTestService()
vendorID := uuid.Must(uuid.NewV7())
_ = repo.CreateCompany(context.Background(), &domain.Company{
ID: vendorID,
CorporateName: "Farmacia Central",
Latitude: -23.55,
Longitude: -46.63,
})
settings := baseShippingSettings(vendorID)
settings.PickupActive = true
settings.PickupAddress = "Av. Paulista, 1000 - SP"
settings.PickupHours = "08:00-18:00"
repo.shippingSettings[vendorID] = settings
options, err := svc.CalculateShippingOptions(context.Background(), vendorID, -23.56, -46.64, 5000)
if err != nil {
t.Fatalf("CalculateShippingOptions falhou: %v", err)
}
hasDelivery := false
hasPickup := false
for _, opt := range options {
switch opt.Type {
case "delivery":
hasDelivery = true
case "pickup":
hasPickup = true
if opt.ValueCents != 0 {
t.Errorf("Pickup deve ter custo zero, got %d", opt.ValueCents)
}
}
}
if !hasDelivery {
t.Error("Deve haver opção de entrega (delivery)")
}
if !hasPickup {
t.Error("Pickup ativo deve gerar opção de retirada")
}
}
func TestShippingCalculateOptions_PickupOnly(t *testing.T) {
svc, repo := newShippingTestService()
vendorID := uuid.Must(uuid.NewV7())
_ = repo.CreateCompany(context.Background(), &domain.Company{
ID: vendorID,
CorporateName: "Farmacia Central",
Latitude: -23.55,
Longitude: -46.63,
})
settings := baseShippingSettings(vendorID)
settings.Active = false // entrega desativada
settings.PickupActive = true
settings.PickupAddress = "Rua Teste, 123"
settings.PickupHours = "09:00-17:00"
repo.shippingSettings[vendorID] = settings
options, err := svc.CalculateShippingOptions(context.Background(), vendorID, -23.56, -46.64, 5000)
if err != nil {
t.Fatalf("CalculateShippingOptions falhou: %v", err)
}
for _, opt := range options {
if opt.Type == "delivery" {
t.Error("Entrega desativada não deve gerar opção delivery")
}
}
}