feat: implement invisible 12% buyer fee

Business model:
- Seller registers R$10,00 → Seller sees R$10,00 → Seller receives R$10,00
- Buyer searches → sees R$11,20 (+12%) → pays R$11,20
- Marketplace keeps R$1,20 (12%)

Changes:
- config.go: Add BuyerFeeRate (default 0.12)
- handler.go: Add buyerFeeRate field to Handler struct
- product_handler.go: SearchProducts inflates prices for buyers
- server.go: Pass cfg.BuyerFeeRate to handler.New()
- handler_test.go: Fix 3 New() calls with fee rate

Env var: BUYER_FEE_RATE (default: 0.12)
This commit is contained in:
Tiago Yamamoto 2025-12-26 23:23:18 -03:00
parent 285f586371
commit 12e2503244
5 changed files with 21 additions and 7 deletions

View file

@ -17,6 +17,7 @@ type Config struct {
DatabaseURL string DatabaseURL string
MercadoPagoBaseURL string MercadoPagoBaseURL string
MarketplaceCommission float64 MarketplaceCommission float64
BuyerFeeRate float64 // Fee rate applied to buyer prices (e.g., 0.12 = 12%)
JWTSecret string JWTSecret string
JWTExpiresIn time.Duration JWTExpiresIn time.Duration
PasswordPepper string PasswordPepper string
@ -40,6 +41,7 @@ func Load() Config {
DatabaseURL: getEnv("DATABASE_URL", "postgres://postgres:postgres@localhost:5432/saveinmed?sslmode=disable"), DatabaseURL: getEnv("DATABASE_URL", "postgres://postgres:postgres@localhost:5432/saveinmed?sslmode=disable"),
MercadoPagoBaseURL: getEnv("MERCADOPAGO_BASE_URL", "https://api.mercadopago.com"), MercadoPagoBaseURL: getEnv("MERCADOPAGO_BASE_URL", "https://api.mercadopago.com"),
MarketplaceCommission: getEnvFloat("MARKETPLACE_COMMISSION", 2.5), MarketplaceCommission: getEnvFloat("MARKETPLACE_COMMISSION", 2.5),
BuyerFeeRate: getEnvFloat("BUYER_FEE_RATE", 0.12), // 12% invisible fee
JWTSecret: getEnv("JWT_SECRET", "dev-secret"), JWTSecret: getEnv("JWT_SECRET", "dev-secret"),
JWTExpiresIn: getEnvDuration("JWT_EXPIRES_IN", 24*time.Hour), JWTExpiresIn: getEnvDuration("JWT_EXPIRES_IN", 24*time.Hour),
PasswordPepper: getEnv("PASSWORD_PEPPER", ""), PasswordPepper: getEnv("PASSWORD_PEPPER", ""),

View file

@ -16,11 +16,12 @@ import (
var json = jsoniter.ConfigCompatibleWithStandardLibrary var json = jsoniter.ConfigCompatibleWithStandardLibrary
type Handler struct { type Handler struct {
svc *usecase.Service svc *usecase.Service
buyerFeeRate float64 // Rate to inflate prices for buyers (e.g., 0.12 = 12%)
} }
func New(svc *usecase.Service) *Handler { func New(svc *usecase.Service, buyerFeeRate float64) *Handler {
return &Handler{svc: svc} return &Handler{svc: svc, buyerFeeRate: buyerFeeRate}
} }
// Register godoc // Register godoc

View file

@ -312,7 +312,7 @@ func newTestHandler() *Handler {
repo := NewMockRepository() repo := NewMockRepository()
gateway := &MockPaymentGateway{} gateway := &MockPaymentGateway{}
svc := usecase.NewService(repo, gateway, 0.05, "test-secret", time.Hour, "test-pepper") svc := usecase.NewService(repo, gateway, 0.05, "test-secret", time.Hour, "test-pepper")
return New(svc) return New(svc, 0.12) // 12% buyer fee rate for testing
} }
func TestListProducts(t *testing.T) { func TestListProducts(t *testing.T) {
@ -398,7 +398,7 @@ func TestAdminLogin_Success(t *testing.T) {
repo := NewMockRepository() repo := NewMockRepository()
gateway := &MockPaymentGateway{} gateway := &MockPaymentGateway{}
svc := usecase.NewService(repo, gateway, 0.05, "test-secret", time.Hour, "test-pepper") svc := usecase.NewService(repo, gateway, 0.05, "test-secret", time.Hour, "test-pepper")
h := New(svc) h := New(svc, 0.12)
// Create admin user through service (which hashes password) // Create admin user through service (which hashes password)
companyID, _ := uuid.NewV7() companyID, _ := uuid.NewV7()
@ -442,7 +442,7 @@ func TestAdminLogin_WrongPassword(t *testing.T) {
repo := NewMockRepository() repo := NewMockRepository()
gateway := &MockPaymentGateway{} gateway := &MockPaymentGateway{}
svc := usecase.NewService(repo, gateway, 0.05, "test-secret", time.Hour, "test-pepper") svc := usecase.NewService(repo, gateway, 0.05, "test-secret", time.Hour, "test-pepper")
h := New(svc) h := New(svc, 0.12)
// Create admin user // Create admin user
companyID, _ := uuid.NewV7() companyID, _ := uuid.NewV7()

View file

@ -142,6 +142,17 @@ func (h *Handler) SearchProducts(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusInternalServerError, err) writeError(w, http.StatusInternalServerError, err)
return return
} }
// Apply invisible buyer fee: inflate prices by buyerFeeRate (e.g., 12%)
// The buyer sees inflated prices, but the DB stores the original seller price
if h.buyerFeeRate > 0 {
for i := range result.Products {
originalPrice := result.Products[i].PriceCents
inflatedPrice := int64(float64(originalPrice) * (1 + h.buyerFeeRate))
result.Products[i].PriceCents = inflatedPrice
}
}
writeJSON(w, http.StatusOK, result) writeJSON(w, http.StatusOK, result)
} }

View file

@ -37,7 +37,7 @@ func New(cfg config.Config) (*Server, error) {
repo := postgres.New(db) repo := postgres.New(db)
gateway := payments.NewMercadoPagoGateway(cfg.MercadoPagoBaseURL, cfg.MarketplaceCommission) gateway := payments.NewMercadoPagoGateway(cfg.MercadoPagoBaseURL, cfg.MarketplaceCommission)
svc := usecase.NewService(repo, gateway, cfg.MarketplaceCommission, cfg.JWTSecret, cfg.JWTExpiresIn, cfg.PasswordPepper) svc := usecase.NewService(repo, gateway, cfg.MarketplaceCommission, cfg.JWTSecret, cfg.JWTExpiresIn, cfg.PasswordPepper)
h := handler.New(svc) h := handler.New(svc, cfg.BuyerFeeRate)
mux := http.NewServeMux() mux := http.NewServeMux()