182 lines
5.4 KiB
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
|
|
}
|