package payments import ( "bytes" "context" "encoding/json" "fmt" "net/http" "strconv" "strings" "github.com/saveinmed/backend-go/internal/domain" ) type MercadoPagoGateway struct { BaseURL string AccessToken string BackendURL string MarketplaceCommission float64 } func NewMercadoPagoGateway(baseURL, accessToken, backendURL string, commission float64) *MercadoPagoGateway { return &MercadoPagoGateway{ BaseURL: baseURL, AccessToken: accessToken, BackendURL: backendURL, MarketplaceCommission: commission, } } func (g *MercadoPagoGateway) CreatePreference(ctx context.Context, order *domain.Order, payer *domain.User, sellerAcc *domain.SellerPaymentAccount) (*domain.PaymentPreference, error) { // Construct items var items []map[string]interface{} for _, i := range order.Items { items = append(items, map[string]interface{}{ "id": i.ProductID.String(), "title": "Produto", // Fallback "description": fmt.Sprintf("Product ID %s", i.ProductID), "quantity": int(i.Quantity), "unit_price": float64(i.UnitCents) / 100.0, "currency_id": "BRL", }) } shipmentCost := float64(order.ShippingFeeCents) / 100.0 notificationURL := g.BackendURL + "/api/v1/payments/webhook" payerData := map[string]interface{}{ "email": payer.Email, "name": payer.Name, } if payer.CPF != "" { payerData["identification"] = map[string]interface{}{ "type": "CPF", "number": payer.CPF, } } payload := map[string]interface{}{ "items": items, "payer": payerData, "shipments": map[string]interface{}{ "cost": shipmentCost, "mode": "not_specified", }, "external_reference": order.ID.String(), "notification_url": notificationURL, "binary_mode": true, "back_urls": map[string]string{ "success": g.BackendURL + "/checkout/success", "failure": g.BackendURL + "/checkout/failure", "pending": g.BackendURL + "/checkout/pending", }, "auto_return": "approved", } // Calculate Fee svcFeeCents := int64(float64(order.TotalCents) * (g.MarketplaceCommission / 100)) sellerRecCents := order.TotalCents - svcFeeCents // Add Split Payment logic (Transfers) if SellerAccount is provided if sellerAcc != nil && sellerAcc.AccountID != "" { // Try to parse AccountID (e.g. 123456789) // If AccountID contains non-digits, this might fail, MP User IDs are integers. // We'll trust it's a valid string representation of int. sellerMPID, err := strconv.ParseInt(sellerAcc.AccountID, 10, 64) if err == nil { // We transfer the seller's share to them. // Marketplace keeps the rest (which equals svcFee + Shipping if shipping is ours? Or Seller pays shipping?) // Usually, total = items + shipping. // If Seller pays commission on Total, then Seller gets (Total - Fee). // If Shipping is pass-through, we need to be careful. // Simple logic: Seller receives 'sellerRecCents'. payload["purpose"] = "wallet_purchase" payload["transfers"] = []map[string]interface{}{ { "amount": float64(sellerRecCents) / 100.0, "collector_id": sellerMPID, "description": fmt.Sprintf("Venda SaveInMed #%s", order.ID.String()), }, } } else { // Log error but proceed without split? Or fail? // Ideally we fail if we can't split. return nil, fmt.Errorf("invalid seller account id for split: %w", err) } } body, err := json.Marshal(payload) if err != nil { return nil, fmt.Errorf("failed to marshal request: %w", err) } req, err := http.NewRequestWithContext(ctx, "POST", g.BaseURL+"/checkout/preferences", bytes.NewBuffer(body)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+g.AccessToken) client := &http.Client{} resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("failed to call MP API: %w", err) } defer resp.Body.Close() if resp.StatusCode >= 400 { return nil, fmt.Errorf("MP API failed with status %d", resp.StatusCode) } var result struct { ID string `json:"id"` InitPoint string `json:"init_point"` } if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return nil, fmt.Errorf("failed to decode response: %w", err) } return &domain.PaymentPreference{ OrderID: order.ID, Gateway: "mercadopago", PaymentID: result.ID, PaymentURL: result.InitPoint, CommissionPct: g.MarketplaceCommission, MarketplaceFee: svcFeeCents, SellerReceivable: sellerRecCents, }, nil } // CreatePayment executes a direct payment (Card/Pix) using a token (Bricks). func (g *MercadoPagoGateway) CreatePayment(ctx context.Context, order *domain.Order, token, issuerID, paymentMethodID string, installments int, payer *domain.User, sellerAcc *domain.SellerPaymentAccount) (*domain.PaymentResult, error) { // shipmentCost folded into transaction_amount payerData := map[string]interface{}{ "email": payer.Email, "first_name": strings.Split(payer.Name, " ")[0], } // Handle Last Name if possible, or just send email. MP is lenient. if payer.CPF != "" { docType := "CPF" cleanDoc := strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(payer.CPF, ".", ""), "-", ""), "/", "") if len(cleanDoc) > 11 { docType = "CNPJ" } payerData["identification"] = map[string]interface{}{ "type": docType, "number": cleanDoc, } } payload := map[string]interface{}{ "transaction_amount": float64(order.TotalCents+order.ShippingFeeCents) / 100.0, "token": token, "description": fmt.Sprintf("Pedido #%s", order.ID.String()), "installments": installments, "payment_method_id": paymentMethodID, "issuer_id": issuerID, "payer": payerData, "external_reference": order.ID.String(), "notification_url": g.BackendURL + "/api/v1/payments/webhook", "binary_mode": true, } // [Remains unchanged...] // Determine Logic for Total Amount // If order.TotalCents is items, and ShippingFee is extra: // realTotal = TotalCents + ShippingFeeCents. // In CreatePreference, cost was separate. Here it's one blob. // Fee Calculation chargeAmountCents := order.TotalCents + order.ShippingFeeCents payload["transaction_amount"] = float64(chargeAmountCents) / 100.0 // Split Payment Logic svcFeeCents := int64(float64(order.TotalCents) * (g.MarketplaceCommission / 100)) sellerRecCents := chargeAmountCents - svcFeeCents if sellerAcc != nil && sellerAcc.AccountID != "" { sellerMPID, err := strconv.ParseInt(sellerAcc.AccountID, 10, 64) if err == nil { payload["application_fee"] = nil payload["transfers"] = []map[string]interface{}{ { "amount": float64(sellerRecCents) / 100.0, "collector_id": sellerMPID, "description": fmt.Sprintf("Venda SaveInMed #%s", order.ID.String()), }, } } } body, err := json.Marshal(payload) if err != nil { return nil, fmt.Errorf("marshal error: %w", err) } req, err := http.NewRequestWithContext(ctx, "POST", g.BaseURL+"/v1/payments", bytes.NewBuffer(body)) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+g.AccessToken) req.Header.Set("X-Idempotency-Key", order.ID.String()) client := &http.Client{} resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("api error: %w", err) } defer resp.Body.Close() if resp.StatusCode >= 400 { buf := new(bytes.Buffer) buf.ReadFrom(resp.Body) bodyStr := buf.String() fmt.Printf("MP Error Body: %s\n", bodyStr) // Log to stdout return nil, fmt.Errorf("mp status %d: %s", resp.StatusCode, bodyStr) } var res struct { ID int64 `json:"id"` Status string `json:"status"` StatusDetail string `json:"status_detail"` } if err := json.NewDecoder(resp.Body).Decode(&res); err != nil { return nil, err } return &domain.PaymentResult{ PaymentID: fmt.Sprintf("%d", res.ID), Status: res.Status, Gateway: "mercadopago", Message: res.StatusDetail, }, nil }