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 } // CreatePixPayment generates a Pix payment using Mercado Pago. func (g *MercadoPagoGateway) CreatePixPayment(ctx context.Context, order *domain.Order) (*domain.PixPaymentResult, error) { payload := map[string]interface{}{ "transaction_amount": float64(order.TotalCents+order.ShippingFeeCents) / 100.0, "description": fmt.Sprintf("Pedido SaveInMed #%s", order.ID.String()), "payment_method_id": "pix", "external_reference": order.ID.String(), "notification_url": g.BackendURL + "/api/v1/payments/webhook", "payer": map[string]interface{}{ "email": "cliente@saveinmed.com.br", // In prod, use real payer email }, } body, _ := json.Marshal(payload) req, _ := http.NewRequestWithContext(ctx, "POST", g.BaseURL+"/v1/payments", bytes.NewBuffer(body)) 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, err } defer resp.Body.Close() if resp.StatusCode >= 400 { return nil, fmt.Errorf("MP Pix API error: %d", resp.StatusCode) } var res struct { ID int64 `json:"id"` Status string `json:"status"` PointOfInteraction struct { TransactionData struct { QRCode string `json:"qr_code"` QRCodeBase64 string `json:"qr_code_base64"` TicketURL string `json:"ticket_url"` } `json:"transaction_data"` } `json:"point_of_interaction"` } json.NewDecoder(resp.Body).Decode(&res) return &domain.PixPaymentResult{ PaymentID: fmt.Sprintf("%d", res.ID), OrderID: order.ID, Gateway: "mercadopago", QRCode: res.PointOfInteraction.TransactionData.QRCode, QRCodeBase64: "data:image/png;base64," + res.PointOfInteraction.TransactionData.QRCodeBase64, CopyPasta: res.PointOfInteraction.TransactionData.QRCode, AmountCents: order.TotalCents + order.ShippingFeeCents, Status: res.Status, ExpiresAt: time.Now().Add(30 * time.Minute), }, nil } // CreateBoletoPayment generates a Boleto payment using Mercado Pago. func (g *MercadoPagoGateway) CreateBoletoPayment(ctx context.Context, order *domain.Order, payer *domain.User) (*domain.BoletoPaymentResult, error) { // In production, this would call /v1/payments with payment_method_id="bolbradesco" return &domain.BoletoPaymentResult{ PaymentID: fmt.Sprintf("bol_%s", order.ID.String()[:8]), OrderID: order.ID, Gateway: "mercadopago", BoletoURL: "https://www.mercadopago.com.br/payments/ticket/helper/...", BarCode: "23793381286000000000300000000401...", DigitableLine: "23793.38128 60000.000003 00000.000400 1 ...", AmountCents: order.TotalCents + order.ShippingFeeCents, DueDate: time.Now().AddDate(0, 0, 3), Status: "pending", }, nil } // GetPaymentStatus fetches payment details from Mercado Pago API. func (g *MercadoPagoGateway) GetPaymentStatus(ctx context.Context, paymentID string) (*domain.PaymentWebhookEvent, error) { req, _ := http.NewRequestWithContext(ctx, "GET", g.BaseURL+"/v1/payments/"+paymentID, nil) req.Header.Set("Authorization", "Bearer "+g.AccessToken) client := &http.Client{} resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() var res struct { ID int64 `json:"id"` Status string `json:"status"` ExternalReference string `json:"external_reference"` TransactionAmount float64 `json:"transaction_amount"` } json.NewDecoder(resp.Body).Decode(&res) orderID, _ := uuid.FromString(res.ExternalReference) return &domain.PaymentWebhookEvent{ PaymentID: fmt.Sprintf("%d", res.ID), OrderID: orderID, Status: res.Status, TotalPaidAmount: int64(res.TransactionAmount * 100), Gateway: "mercadopago", }, nil }