saveinmed/backend/internal/usecase/usecase_test.go
Tiago Yamamoto 4bb848788f feat: tenant model, seeder, and product search with distance
Tenant Model:
- Renamed Company→Tenant (Company alias for compatibility)
- Added: lat/lng, city, state, category
- Updated: postgres, handlers, DTOs, schema SQL

Seeder (cmd/seeder):
- Generates 400 pharmacies in Anápolis/GO
- 20-500 products per tenant
- Haversine distance variation ±5km from center

Product Search:
- GET /products/search with advanced filters
- Filters: price (min/max), expiration, distance
- Haversine distance calculation (approx km)
- Anonymous seller (only city/state shown until checkout)
- Ordered by expiration date (nearest first)

New domain types:
- ProductWithDistance, ProductSearchFilter, ProductSearchPage
- HaversineDistance function

Updated tests for Category instead of Role
2025-12-20 09:03:13 -03:00

788 lines
20 KiB
Go

package usecase
import (
"context"
"testing"
"time"
"github.com/gofrs/uuid/v5"
"github.com/saveinmed/backend-go/internal/domain"
)
// MockRepository implements Repository interface for testing
type MockRepository struct {
companies []domain.Company
products []domain.Product
users []domain.User
orders []domain.Order
cartItems []domain.CartItem
reviews []domain.Review
}
func NewMockRepository() *MockRepository {
return &MockRepository{
companies: make([]domain.Company, 0),
products: make([]domain.Product, 0),
users: make([]domain.User, 0),
orders: make([]domain.Order, 0),
cartItems: make([]domain.CartItem, 0),
reviews: make([]domain.Review, 0),
}
}
// Company methods
func (m *MockRepository) CreateCompany(ctx context.Context, company *domain.Company) error {
company.CreatedAt = time.Now()
company.UpdatedAt = time.Now()
m.companies = append(m.companies, *company)
return nil
}
func (m *MockRepository) ListCompanies(ctx context.Context, filter domain.CompanyFilter) ([]domain.Company, int64, error) {
return m.companies, int64(len(m.companies)), nil
}
func (m *MockRepository) GetCompany(ctx context.Context, id uuid.UUID) (*domain.Company, error) {
for _, c := range m.companies {
if c.ID == id {
return &c, nil
}
}
return nil, nil
}
func (m *MockRepository) UpdateCompany(ctx context.Context, company *domain.Company) error {
for i, c := range m.companies {
if c.ID == company.ID {
m.companies[i] = *company
return nil
}
}
return nil
}
func (m *MockRepository) DeleteCompany(ctx context.Context, id uuid.UUID) error {
for i, c := range m.companies {
if c.ID == id {
m.companies = append(m.companies[:i], m.companies[i+1:]...)
return nil
}
}
return nil
}
// Product methods
func (m *MockRepository) CreateProduct(ctx context.Context, product *domain.Product) error {
product.CreatedAt = time.Now()
product.UpdatedAt = time.Now()
m.products = append(m.products, *product)
return nil
}
func (m *MockRepository) ListProducts(ctx context.Context, filter domain.ProductFilter) ([]domain.Product, int64, error) {
return m.products, int64(len(m.products)), nil
}
func (m *MockRepository) GetProduct(ctx context.Context, id uuid.UUID) (*domain.Product, error) {
for _, p := range m.products {
if p.ID == id {
return &p, nil
}
}
return nil, nil
}
func (m *MockRepository) UpdateProduct(ctx context.Context, product *domain.Product) error {
for i, p := range m.products {
if p.ID == product.ID {
m.products[i] = *product
return nil
}
}
return nil
}
func (m *MockRepository) DeleteProduct(ctx context.Context, id uuid.UUID) error {
for i, p := range m.products {
if p.ID == id {
m.products = append(m.products[:i], m.products[i+1:]...)
return nil
}
}
return nil
}
func (m *MockRepository) AdjustInventory(ctx context.Context, productID uuid.UUID, delta int64, reason string) (*domain.InventoryItem, error) {
return &domain.InventoryItem{ProductID: productID, Quantity: delta}, nil
}
func (m *MockRepository) ListInventory(ctx context.Context, filter domain.InventoryFilter) ([]domain.InventoryItem, int64, error) {
return []domain.InventoryItem{}, 0, nil
}
func (m *MockRepository) SearchProducts(ctx context.Context, filter domain.ProductSearchFilter) ([]domain.ProductWithDistance, int64, error) {
return []domain.ProductWithDistance{}, 0, nil
}
// Order methods
func (m *MockRepository) CreateOrder(ctx context.Context, order *domain.Order) error {
m.orders = append(m.orders, *order)
return nil
}
func (m *MockRepository) ListOrders(ctx context.Context, filter domain.OrderFilter) ([]domain.Order, int64, error) {
return m.orders, int64(len(m.orders)), nil
}
func (m *MockRepository) GetOrder(ctx context.Context, id uuid.UUID) (*domain.Order, error) {
for _, o := range m.orders {
if o.ID == id {
return &o, nil
}
}
return nil, nil
}
func (m *MockRepository) UpdateOrderStatus(ctx context.Context, id uuid.UUID, status domain.OrderStatus) error {
for i, o := range m.orders {
if o.ID == id {
m.orders[i].Status = status
return nil
}
}
return nil
}
func (m *MockRepository) DeleteOrder(ctx context.Context, id uuid.UUID) error {
for i, o := range m.orders {
if o.ID == id {
m.orders = append(m.orders[:i], m.orders[i+1:]...)
return nil
}
}
return nil
}
func (m *MockRepository) CreateShipment(ctx context.Context, shipment *domain.Shipment) error {
return nil
}
func (m *MockRepository) GetShipmentByOrderID(ctx context.Context, orderID uuid.UUID) (*domain.Shipment, error) {
return nil, nil
}
// User methods
func (m *MockRepository) CreateUser(ctx context.Context, user *domain.User) error {
m.users = append(m.users, *user)
return nil
}
func (m *MockRepository) ListUsers(ctx context.Context, filter domain.UserFilter) ([]domain.User, int64, error) {
return m.users, int64(len(m.users)), nil
}
func (m *MockRepository) GetUser(ctx context.Context, id uuid.UUID) (*domain.User, error) {
for _, u := range m.users {
if u.ID == id {
return &u, nil
}
}
return nil, nil
}
func (m *MockRepository) GetUserByEmail(ctx context.Context, email string) (*domain.User, error) {
for _, u := range m.users {
if u.Email == email {
return &u, nil
}
}
return nil, nil
}
func (m *MockRepository) UpdateUser(ctx context.Context, user *domain.User) error {
return nil
}
func (m *MockRepository) DeleteUser(ctx context.Context, id uuid.UUID) error {
return nil
}
// Cart methods
func (m *MockRepository) AddCartItem(ctx context.Context, item *domain.CartItem) (*domain.CartItem, error) {
m.cartItems = append(m.cartItems, *item)
return item, nil
}
func (m *MockRepository) ListCartItems(ctx context.Context, buyerID uuid.UUID) ([]domain.CartItem, error) {
var items []domain.CartItem
for _, c := range m.cartItems {
if c.BuyerID == buyerID {
items = append(items, c)
}
}
return items, nil
}
func (m *MockRepository) DeleteCartItem(ctx context.Context, id uuid.UUID, buyerID uuid.UUID) error {
for i, c := range m.cartItems {
if c.ID == id && c.BuyerID == buyerID {
m.cartItems = append(m.cartItems[:i], m.cartItems[i+1:]...)
return nil
}
}
return nil
}
// Review methods
func (m *MockRepository) CreateReview(ctx context.Context, review *domain.Review) error {
m.reviews = append(m.reviews, *review)
return nil
}
func (m *MockRepository) GetCompanyRating(ctx context.Context, companyID uuid.UUID) (*domain.CompanyRating, error) {
return &domain.CompanyRating{CompanyID: companyID, AverageScore: 4.5, TotalReviews: 10}, nil
}
func (m *MockRepository) SellerDashboard(ctx context.Context, sellerID uuid.UUID) (*domain.SellerDashboard, error) {
return &domain.SellerDashboard{SellerID: sellerID}, nil
}
func (m *MockRepository) AdminDashboard(ctx context.Context, since time.Time) (*domain.AdminDashboard, error) {
return &domain.AdminDashboard{GMVCents: 1000000}, nil
}
// MockPaymentGateway for testing
type MockPaymentGateway struct{}
func (m *MockPaymentGateway) CreatePreference(ctx context.Context, order *domain.Order) (*domain.PaymentPreference, error) {
return &domain.PaymentPreference{
OrderID: order.ID,
Gateway: "mock",
CommissionPct: 2.5,
MarketplaceFee: int64(float64(order.TotalCents) * 0.025),
SellerReceivable: order.TotalCents - int64(float64(order.TotalCents)*0.025),
PaymentURL: "https://mock.payment.url",
}, nil
}
// Helper to create a test service
func newTestService() (*Service, *MockRepository) {
repo := NewMockRepository()
gateway := &MockPaymentGateway{}
svc := NewService(repo, gateway, 2.5, "test-secret", time.Hour, "test-pepper")
return svc, repo
}
// --- Company Tests ---
func TestRegisterCompany(t *testing.T) {
svc, _ := newTestService()
ctx := context.Background()
company := &domain.Company{
Category: "farmacia",
CNPJ: "12345678901234",
CorporateName: "Test Pharmacy",
LicenseNumber: "LIC-001",
}
err := svc.RegisterCompany(ctx, company)
if err != nil {
t.Fatalf("failed to register company: %v", err)
}
if company.ID == uuid.Nil {
t.Error("expected company ID to be set")
}
}
func TestListCompanies(t *testing.T) {
svc, _ := newTestService()
ctx := context.Background()
page, err := svc.ListCompanies(ctx, domain.CompanyFilter{}, 1, 20)
if err != nil {
t.Fatalf("failed to list companies: %v", err)
}
if len(page.Companies) != 0 {
t.Errorf("expected 0 companies, got %d", len(page.Companies))
}
}
func TestVerifyCompany(t *testing.T) {
svc, repo := newTestService()
ctx := context.Background()
company := &domain.Company{
ID: uuid.Must(uuid.NewV4()),
Category: "farmacia",
CNPJ: "12345678901234",
CorporateName: "Test Pharmacy",
IsVerified: false,
}
repo.companies = append(repo.companies, *company)
verified, err := svc.VerifyCompany(ctx, company.ID)
if err != nil {
t.Fatalf("failed to verify company: %v", err)
}
if !verified.IsVerified {
t.Error("expected company to be verified")
}
}
// --- Product Tests ---
func TestRegisterProduct(t *testing.T) {
svc, _ := newTestService()
ctx := context.Background()
product := &domain.Product{
SellerID: uuid.Must(uuid.NewV4()),
Name: "Test Product",
Description: "A test product",
Batch: "BATCH-001",
ExpiresAt: time.Now().AddDate(1, 0, 0),
PriceCents: 1000,
Stock: 100,
}
err := svc.RegisterProduct(ctx, product)
if err != nil {
t.Fatalf("failed to register product: %v", err)
}
if product.ID == uuid.Nil {
t.Error("expected product ID to be set")
}
}
func TestListProducts(t *testing.T) {
svc, _ := newTestService()
ctx := context.Background()
page, err := svc.ListProducts(ctx, domain.ProductFilter{}, 1, 20)
if err != nil {
t.Fatalf("failed to list products: %v", err)
}
if len(page.Products) != 0 {
t.Errorf("expected 0 products, got %d", len(page.Products))
}
}
// --- Order Tests ---
func TestCreateOrder(t *testing.T) {
svc, _ := newTestService()
ctx := context.Background()
order := &domain.Order{
BuyerID: uuid.Must(uuid.NewV4()),
SellerID: uuid.Must(uuid.NewV4()),
TotalCents: 10000,
}
err := svc.CreateOrder(ctx, order)
if err != nil {
t.Fatalf("failed to create order: %v", err)
}
if order.ID == uuid.Nil {
t.Error("expected order ID to be set")
}
if order.Status != domain.OrderStatusPending {
t.Errorf("expected status 'Pendente', got '%s'", order.Status)
}
}
func TestUpdateOrderStatus(t *testing.T) {
svc, repo := newTestService()
ctx := context.Background()
order := &domain.Order{
ID: uuid.Must(uuid.NewV4()),
BuyerID: uuid.Must(uuid.NewV4()),
SellerID: uuid.Must(uuid.NewV4()),
Status: domain.OrderStatusPending,
TotalCents: 10000,
}
repo.orders = append(repo.orders, *order)
err := svc.UpdateOrderStatus(ctx, order.ID, domain.OrderStatusPaid)
if err != nil {
t.Fatalf("failed to update order status: %v", err)
}
}
// --- User Tests ---
func TestCreateUser(t *testing.T) {
svc, _ := newTestService()
ctx := context.Background()
user := &domain.User{
CompanyID: uuid.Must(uuid.NewV4()),
Role: "admin",
Name: "Test User",
Email: "test@example.com",
}
err := svc.CreateUser(ctx, user, "password123")
if err != nil {
t.Fatalf("failed to create user: %v", err)
}
if user.ID == uuid.Nil {
t.Error("expected user ID to be set")
}
if user.PasswordHash == "" {
t.Error("expected password to be hashed")
}
}
func TestListUsers(t *testing.T) {
svc, _ := newTestService()
ctx := context.Background()
page, err := svc.ListUsers(ctx, domain.UserFilter{}, 1, 20)
if err != nil {
t.Fatalf("failed to list users: %v", err)
}
if page.Page != 1 {
t.Errorf("expected page 1, got %d", page.Page)
}
if page.PageSize != 20 {
t.Errorf("expected pageSize 20, got %d", page.PageSize)
}
}
func TestListUsersPagination(t *testing.T) {
svc, _ := newTestService()
ctx := context.Background()
// Test with page < 1 (should default to 1)
page, err := svc.ListUsers(ctx, domain.UserFilter{}, 0, 10)
if err != nil {
t.Fatalf("failed to list users: %v", err)
}
if page.Page != 1 {
t.Errorf("expected page 1, got %d", page.Page)
}
// Test with pageSize <= 0 (should default to 20)
page2, err := svc.ListUsers(ctx, domain.UserFilter{}, 1, 0)
if err != nil {
t.Fatalf("failed to list users: %v", err)
}
if page2.PageSize != 20 {
t.Errorf("expected pageSize 20, got %d", page2.PageSize)
}
}
// --- Authentication Tests ---
func TestAuthenticate(t *testing.T) {
svc, repo := newTestService()
ctx := context.Background()
// First create a user
user := &domain.User{
CompanyID: uuid.Must(uuid.NewV4()),
Role: "admin",
Name: "Test User",
Email: "auth@example.com",
}
err := svc.CreateUser(ctx, user, "testpass123")
if err != nil {
t.Fatalf("failed to create user: %v", err)
}
// Update the mock with the hashed password
repo.users[0] = *user
// Test authentication
token, expiresAt, err := svc.Authenticate(ctx, "auth@example.com", "testpass123")
if err != nil {
t.Fatalf("failed to authenticate: %v", err)
}
if token == "" {
t.Error("expected token to be returned")
}
if expiresAt.Before(time.Now()) {
t.Error("expected expiration to be in the future")
}
}
func TestAuthenticateInvalidPassword(t *testing.T) {
svc, repo := newTestService()
ctx := context.Background()
user := &domain.User{
CompanyID: uuid.Must(uuid.NewV4()),
Role: "admin",
Name: "Test User",
Email: "fail@example.com",
}
svc.CreateUser(ctx, user, "correctpass")
repo.users[0] = *user
_, _, err := svc.Authenticate(ctx, "fail@example.com", "wrongpass")
if err == nil {
t.Error("expected authentication to fail")
}
}
// --- Cart Tests ---
func TestAddItemToCart(t *testing.T) {
svc, repo := newTestService()
ctx := context.Background()
buyerID := uuid.Must(uuid.NewV4())
product := &domain.Product{
ID: uuid.Must(uuid.NewV4()),
SellerID: uuid.Must(uuid.NewV4()),
Name: "Test Product",
PriceCents: 1000,
Stock: 100,
Batch: "BATCH-001",
ExpiresAt: time.Now().AddDate(1, 0, 0),
}
repo.products = append(repo.products, *product)
summary, err := svc.AddItemToCart(ctx, buyerID, product.ID, 5)
if err != nil {
t.Fatalf("failed to add item to cart: %v", err)
}
if summary.SubtotalCents != 5000 {
t.Errorf("expected subtotal 5000, got %d", summary.SubtotalCents)
}
}
func TestAddItemToCartInvalidQuantity(t *testing.T) {
svc, _ := newTestService()
ctx := context.Background()
buyerID := uuid.Must(uuid.NewV4())
productID := uuid.Must(uuid.NewV4())
_, err := svc.AddItemToCart(ctx, buyerID, productID, 0)
if err == nil {
t.Error("expected error for zero quantity")
}
}
func TestCartB2BDiscount(t *testing.T) {
svc, repo := newTestService()
ctx := context.Background()
buyerID := uuid.Must(uuid.NewV4())
product := &domain.Product{
ID: uuid.Must(uuid.NewV4()),
SellerID: uuid.Must(uuid.NewV4()),
Name: "Expensive Product",
PriceCents: 50000, // R$500 per unit
Stock: 1000,
Batch: "BATCH-001",
ExpiresAt: time.Now().AddDate(1, 0, 0),
}
repo.products = append(repo.products, *product)
// Add enough to trigger B2B discount (>R$1000)
summary, err := svc.AddItemToCart(ctx, buyerID, product.ID, 3) // R$1500
if err != nil {
t.Fatalf("failed to add item to cart: %v", err)
}
if summary.DiscountCents == 0 {
t.Error("expected B2B discount to be applied")
}
if summary.DiscountReason == "" {
t.Error("expected discount reason to be set")
}
}
// --- Review Tests ---
func TestCreateReview(t *testing.T) {
svc, repo := newTestService()
ctx := context.Background()
buyerID := uuid.Must(uuid.NewV4())
sellerID := uuid.Must(uuid.NewV4())
order := &domain.Order{
ID: uuid.Must(uuid.NewV4()),
BuyerID: buyerID,
SellerID: sellerID,
Status: domain.OrderStatusDelivered,
TotalCents: 10000,
}
repo.orders = append(repo.orders, *order)
review, err := svc.CreateReview(ctx, buyerID, order.ID, 5, "Great service!")
if err != nil {
t.Fatalf("failed to create review: %v", err)
}
if review.Rating != 5 {
t.Errorf("expected rating 5, got %d", review.Rating)
}
}
func TestCreateReviewInvalidRating(t *testing.T) {
svc, _ := newTestService()
ctx := context.Background()
_, err := svc.CreateReview(ctx, uuid.Must(uuid.NewV4()), uuid.Must(uuid.NewV4()), 6, "Invalid")
if err == nil {
t.Error("expected error for invalid rating")
}
}
func TestCreateReviewNotDelivered(t *testing.T) {
svc, repo := newTestService()
ctx := context.Background()
buyerID := uuid.Must(uuid.NewV4())
order := &domain.Order{
ID: uuid.Must(uuid.NewV4()),
BuyerID: buyerID,
Status: domain.OrderStatusPending, // Not delivered
TotalCents: 10000,
}
repo.orders = append(repo.orders, *order)
_, err := svc.CreateReview(ctx, buyerID, order.ID, 5, "Great!")
if err == nil {
t.Error("expected error for non-delivered order")
}
}
// --- Dashboard Tests ---
func TestGetSellerDashboard(t *testing.T) {
svc, _ := newTestService()
ctx := context.Background()
sellerID := uuid.Must(uuid.NewV4())
dashboard, err := svc.GetSellerDashboard(ctx, sellerID)
if err != nil {
t.Fatalf("failed to get seller dashboard: %v", err)
}
if dashboard.SellerID != sellerID {
t.Errorf("expected seller ID %s, got %s", sellerID, dashboard.SellerID)
}
}
func TestGetAdminDashboard(t *testing.T) {
svc, _ := newTestService()
ctx := context.Background()
dashboard, err := svc.GetAdminDashboard(ctx)
if err != nil {
t.Fatalf("failed to get admin dashboard: %v", err)
}
if dashboard.GMVCents != 1000000 {
t.Errorf("expected GMV 1000000, got %d", dashboard.GMVCents)
}
}
// --- Payment Tests ---
func TestCreatePaymentPreference(t *testing.T) {
svc, repo := newTestService()
ctx := context.Background()
order := &domain.Order{
ID: uuid.Must(uuid.NewV4()),
BuyerID: uuid.Must(uuid.NewV4()),
SellerID: uuid.Must(uuid.NewV4()),
TotalCents: 10000,
}
repo.orders = append(repo.orders, *order)
pref, err := svc.CreatePaymentPreference(ctx, order.ID)
if err != nil {
t.Fatalf("failed to create payment preference: %v", err)
}
if pref.PaymentURL == "" {
t.Error("expected payment URL to be set")
}
if pref.MarketplaceFee == 0 {
t.Error("expected marketplace fee to be calculated")
}
}
func TestHandlePaymentWebhook(t *testing.T) {
svc, repo := newTestService()
ctx := context.Background()
order := &domain.Order{
ID: uuid.Must(uuid.NewV4()),
BuyerID: uuid.Must(uuid.NewV4()),
SellerID: uuid.Must(uuid.NewV4()),
Status: domain.OrderStatusPending,
TotalCents: 10000,
}
repo.orders = append(repo.orders, *order)
event := domain.PaymentWebhookEvent{
PaymentID: "PAY-123",
OrderID: order.ID,
Status: "approved",
TotalPaidAmount: 10000,
}
result, err := svc.HandlePaymentWebhook(ctx, event)
if err != nil {
t.Fatalf("failed to handle webhook: %v", err)
}
if result.Status != "approved" {
t.Errorf("expected status 'approved', got '%s'", result.Status)
}
if result.MarketplaceFee == 0 {
t.Error("expected marketplace fee to be calculated")
}
}
// --- Register Account Tests ---
func TestRegisterAccount(t *testing.T) {
svc, _ := newTestService()
ctx := context.Background()
company := &domain.Company{
Category: "farmacia",
CNPJ: "12345678901234",
CorporateName: "Test Pharmacy",
}
user := &domain.User{
Role: "admin",
Name: "Admin User",
Email: "admin@example.com",
}
err := svc.RegisterAccount(ctx, company, user, "password123")
if err != nil {
t.Fatalf("failed to register account: %v", err)
}
if company.ID == uuid.Nil {
t.Error("expected company ID to be set")
}
if user.CompanyID != company.ID {
t.Error("expected user to be linked to company")
}
}