saveinmed/backend/internal/usecase/payment_usecase.go

182 lines
5.4 KiB
Go

package usecase
import (
"context"
"fmt"
"math"
"strings"
"github.com/gofrs/uuid/v5"
"github.com/saveinmed/backend-go/internal/domain"
)
// CreatePaymentPreference builds a checkout preference (e.g. Mercado Pago) for an order.
func (s *Service) CreatePaymentPreference(ctx context.Context, id uuid.UUID) (*domain.PaymentPreference, error) {
order, err := s.repo.GetOrder(ctx, id)
if err != nil {
return nil, err
}
buyer, err := s.repo.GetUser(ctx, order.BuyerID)
if err != nil {
return nil, fmt.Errorf("failed to fetch buyer: %w", err)
}
sellerAcc, err := s.repo.GetSellerPaymentAccount(ctx, order.SellerID)
if err != nil {
sellerAcc = nil // Proceed without split if not configured
}
return s.pay.CreatePreference(ctx, order, buyer, sellerAcc)
}
// ProcessOrderPayment processes a direct card payment for an order.
// It resolves the buyer (user or B2B company) and delegates to the payment gateway.
func (s *Service) ProcessOrderPayment(ctx context.Context, id uuid.UUID, token, issuerID, paymentMethodID string, installments int, payerEmail, payerDoc string) (*domain.PaymentResult, error) {
order, err := s.repo.GetOrder(ctx, id)
if err != nil {
return nil, err
}
var buyer *domain.User
user, err := s.repo.GetUser(ctx, order.BuyerID)
if err == nil {
buyer = user
} else {
// B2B fallback: resolve company as payer
company, errComp := s.repo.GetCompany(ctx, order.BuyerID)
if errComp != nil {
return nil, fmt.Errorf("failed to fetch buyer (user or company): %v / %v", err, errComp)
}
email := payerEmail
if email == "" {
email = "financeiro@saveinmed.com"
}
buyer = &domain.User{
ID: company.ID,
Name: company.CorporateName,
Email: email,
CPF: payerDoc,
}
if buyer.CPF == "" {
buyer.CPF = company.CNPJ
}
}
sellerAcc, err := s.repo.GetSellerPaymentAccount(ctx, order.SellerID)
if err != nil {
sellerAcc = nil
}
res, err := s.pay.CreatePayment(ctx, order, token, issuerID, paymentMethodID, installments, buyer, sellerAcc)
if err != nil {
return nil, err
}
if res.Status == "approved" {
if err := s.UpdateOrderStatus(ctx, order.ID, domain.OrderStatusPaid); err != nil {
// Payment succeeded; status update failure should be reconciled async.
fmt.Printf("WARN: payment approved but status update failed: %v\n", err)
}
}
return res, nil
}
// CreatePixPayment generates a Pix payment for an order via the selected gateway.
func (s *Service) CreatePixPayment(ctx context.Context, id uuid.UUID) (*domain.PixPaymentResult, error) {
order, err := s.repo.GetOrder(ctx, id)
if err != nil {
return nil, err
}
return s.pay.CreatePixPayment(ctx, order)
}
// CreateBoletoPayment generates a Boleto payment for an order via the selected gateway.
func (s *Service) CreateBoletoPayment(ctx context.Context, id uuid.UUID) (*domain.BoletoPaymentResult, error) {
order, err := s.repo.GetOrder(ctx, id)
if err != nil {
return nil, err
}
buyer, err := s.repo.GetUser(ctx, order.BuyerID)
if err != nil {
// Fallback to company data if user not found
company, errComp := s.repo.GetCompany(ctx, order.BuyerID)
if errComp != nil {
return nil, fmt.Errorf("failed to fetch buyer for boleto: %v", errComp)
}
buyer = &domain.User{
ID: company.ID,
Name: company.CorporateName,
Email: "financeiro@saveinmed.com.br",
CPF: company.CNPJ,
}
}
return s.pay.CreateBoletoPayment(ctx, order, buyer)
}
// HandlePaymentWebhook processes payment gateway callbacks, updates the order status
// and records the split in the financial ledger.
func (s *Service) HandlePaymentWebhook(ctx context.Context, event domain.PaymentWebhookEvent) (*domain.PaymentSplitResult, error) {
order, err := s.repo.GetOrder(ctx, event.OrderID)
if err != nil {
return nil, err
}
// Business Rule: 6% + 6% Psychological Split.
// order.TotalCents already includes the 6% buyer fee (inflated in search).
// Base Price = Total / 1.06
// Seller Fee = Base Price * 0.06
// Buyer Fee = Total - Base Price
// Total Marketplace Fee = Seller Fee + Buyer Fee
basePriceCents := float64(order.TotalCents) / 1.06
sellerFeeCents := basePriceCents * 0.06
buyerFeeCents := float64(order.TotalCents) - basePriceCents
expectedMarketplaceFee := int64(math.Round(sellerFeeCents + buyerFeeCents))
marketplaceFee := event.MarketplaceFee
if marketplaceFee == 0 {
marketplaceFee = expectedMarketplaceFee
}
sellerReceivable := order.TotalCents - marketplaceFee
if event.SellerAmount > 0 {
sellerReceivable = event.SellerAmount
}
if strings.EqualFold(event.Status, "approved") || strings.EqualFold(event.Status, "paid") {
if err := s.repo.UpdateOrderStatus(ctx, order.ID, domain.OrderStatusPaid); err != nil {
return nil, err
}
_ = s.repo.RecordLedgerEntry(ctx, &domain.LedgerEntry{
ID: uuid.Must(uuid.NewV7()),
CompanyID: order.SellerID,
AmountCents: order.TotalCents,
Type: "SALE",
Description: "Order #" + order.ID.String(),
ReferenceID: &order.ID,
})
_ = s.repo.RecordLedgerEntry(ctx, &domain.LedgerEntry{
ID: uuid.Must(uuid.NewV7()),
CompanyID: order.SellerID,
AmountCents: -marketplaceFee,
Type: "FEE",
Description: "Marketplace Fee #" + order.ID.String(),
ReferenceID: &order.ID,
})
}
return &domain.PaymentSplitResult{
OrderID: order.ID,
PaymentID: event.PaymentID,
Status: event.Status,
MarketplaceFee: marketplaceFee,
SellerReceivable: sellerReceivable,
TotalPaidAmount: event.TotalPaidAmount,
}, nil
}