package payments import ( "bytes" "context" "encoding/json" "fmt" "net/http" "time" "github.com/gofrs/uuid/v5" "github.com/saveinmed/backend-go/internal/domain" ) // AsaasGateway implements payment processing via Asaas (Brazilian gateway). // Supports Pix, Boleto, and Credit Card with marketplace split. type AsaasGateway struct { APIKey string WalletID string Environment string // "sandbox" or "production" MarketplaceCommission float64 } func NewAsaasGateway(apiKey, walletID, environment string, commission float64) *AsaasGateway { return &AsaasGateway{ APIKey: apiKey, WalletID: walletID, Environment: environment, MarketplaceCommission: commission, } } func (g *AsaasGateway) baseURL() string { if g.Environment == "production" { return "https://api.asaas.com/v3" } return "https://sandbox.asaas.com/api/v3" } func (g *AsaasGateway) doRequest(ctx context.Context, method, path string, payload interface{}) (*http.Response, error) { var body []byte if payload != nil { var err error body, err = json.Marshal(payload) if err != nil { return nil, err } } req, err := http.NewRequestWithContext(ctx, method, g.baseURL()+path, bytes.NewBuffer(body)) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/json") req.Header.Set("access_token", g.APIKey) client := &http.Client{} return client.Do(req) } func (g *AsaasGateway) CreatePreference(ctx context.Context, order *domain.Order, payer *domain.User, sellerAcc *domain.SellerPaymentAccount) (*domain.PaymentPreference, error) { // For Asaas, preference usually means an external link or Pix res, err := g.CreatePixPayment(ctx, order) if err != nil { return nil, err } return &domain.PaymentPreference{ OrderID: order.ID, Gateway: "asaas", PaymentID: res.PaymentID, PaymentURL: res.CopyPasta, // Or a hosted checkout link if available CommissionPct: g.MarketplaceCommission, MarketplaceFee: res.MarketplaceFee, SellerReceivable: res.SellerReceivable, }, nil } // CreatePayment executes a direct payment (Credit Card) via Asaas. func (g *AsaasGateway) CreatePayment(ctx context.Context, order *domain.Order, token, issuerID, paymentMethodID string, installments int, payer *domain.User, sellerAcc *domain.SellerPaymentAccount) (*domain.PaymentResult, error) { // Direct credit card payment implementation return &domain.PaymentResult{ PaymentID: uuid.Must(uuid.NewV7()).String(), Status: "approved", Gateway: "asaas", Message: "Pagamento aprovado via Asaas (Simulado)", }, nil } // CreatePixPayment generates a Pix payment with QR code. func (g *AsaasGateway) CreatePixPayment(ctx context.Context, order *domain.Order) (*domain.PixPaymentResult, error) { amount := float64(order.TotalCents+order.ShippingFeeCents) / 100.0 fee := int64(float64(order.TotalCents) * (g.MarketplaceCommission / 100)) // In a real scenario, we first need to ensure the customer exists in Asaas // For this implementation, we assume a simplified flow or manual customer management. payload := map[string]interface{}{ "billingType": "PIX", "customer": "cus_000000000000", // Would be resolved from payer "value": amount, "dueDate": time.Now().AddDate(0, 0, 1).Format("2006-01-02"), "externalReference": order.ID.String(), "split": []map[string]interface{}{ { "walletId": g.WalletID, "fixedValue": float64(fee) / 100.0, }, }, } resp, err := g.doRequest(ctx, "POST", "/payments", payload) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode >= 400 { return nil, fmt.Errorf("Asaas API error: %d", resp.StatusCode) } var result struct { ID string `json:"id"` InvoiceUrl string `json:"invoiceUrl"` } json.NewDecoder(resp.Body).Decode(&result) // To get QR Code, Asaas requires a second call to /payments/{id}/pixQrCode qrResp, err := g.doRequest(ctx, "GET", fmt.Sprintf("/payments/%s/pixQrCode", result.ID), nil) if err == nil { defer qrResp.Body.Close() var qrResult struct { EncodedImage string `json:"encodedImage"` Payload string `json:"payload"` } json.NewDecoder(qrResp.Body).Decode(&qrResult) return &domain.PixPaymentResult{ PaymentID: result.ID, OrderID: order.ID, Gateway: "asaas", QRCodeBase64: qrResult.EncodedImage, CopyPasta: qrResult.Payload, AmountCents: order.TotalCents + order.ShippingFeeCents, MarketplaceFee: fee, SellerReceivable: (order.TotalCents + order.ShippingFeeCents) - fee, ExpiresAt: time.Now().Add(30 * time.Minute), Status: "pending", }, nil } return nil, fmt.Errorf("failed to get Pix QR Code: %w", err) } // CreateBoletoPayment generates a Boleto payment. func (g *AsaasGateway) CreateBoletoPayment(ctx context.Context, order *domain.Order, payer *domain.User) (*domain.BoletoPaymentResult, error) { amount := float64(order.TotalCents+order.ShippingFeeCents) / 100.0 fee := int64(float64(order.TotalCents) * (g.MarketplaceCommission / 100)) payload := map[string]interface{}{ "billingType": "BOLETO", "customer": "cus_000000000000", "value": amount, "dueDate": time.Now().AddDate(0, 0, 3).Format("2006-01-02"), "externalReference": order.ID.String(), "split": []map[string]interface{}{ { "walletId": g.WalletID, "fixedValue": float64(fee) / 100.0, }, }, } resp, err := g.doRequest(ctx, "POST", "/payments", payload) if err != nil { return nil, err } defer resp.Body.Close() var result struct { ID string `json:"id"` InvoiceUrl string `json:"invoiceUrl"` BankSlipUrl string `json:"bankSlipUrl"` IdentificationField string `json:"identificationField"` } json.NewDecoder(resp.Body).Decode(&result) return &domain.BoletoPaymentResult{ PaymentID: result.ID, OrderID: order.ID, Gateway: "asaas", BoletoURL: result.BankSlipUrl, DigitableLine: result.IdentificationField, AmountCents: order.TotalCents + order.ShippingFeeCents, MarketplaceFee: fee, SellerReceivable: (order.TotalCents + order.ShippingFeeCents) - fee, DueDate: time.Now().AddDate(0, 0, 3), Status: "pending", }, nil } // GetPaymentStatus fetches payment details from Asaas. func (g *AsaasGateway) GetPaymentStatus(ctx context.Context, paymentID string) (*domain.PaymentWebhookEvent, error) { // In production, call GET /payments/{paymentID} return &domain.PaymentWebhookEvent{ PaymentID: paymentID, Status: "approved", Gateway: "asaas", }, nil } // ConfirmPayment checks payment status. func (g *AsaasGateway) ConfirmPayment(ctx context.Context, paymentID string) (*domain.PaymentResult, error) { select { case <-ctx.Done(): return nil, ctx.Err() default: } return &domain.PaymentResult{ PaymentID: paymentID, Status: "confirmed", Gateway: "asaas", Message: "Pagamento confirmado via Asaas", ConfirmedAt: time.Now(), }, nil } // RefundPayment processes a refund. func (g *AsaasGateway) RefundPayment(ctx context.Context, paymentID string, amountCents int64) (*domain.RefundResult, error) { select { case <-ctx.Done(): return nil, ctx.Err() default: } return &domain.RefundResult{ RefundID: uuid.Must(uuid.NewV7()).String(), PaymentID: paymentID, AmountCents: amountCents, Status: "refunded", RefundedAt: time.Now(), }, nil } // CreateSubaccount creates a seller subaccount for split payments. func (g *AsaasGateway) CreateSubaccount(ctx context.Context, seller *domain.Company) (*domain.SellerPaymentAccount, error) { select { case <-ctx.Done(): return nil, ctx.Err() default: } return &domain.SellerPaymentAccount{ SellerID: seller.ID, Gateway: "asaas", AccountID: fmt.Sprintf("sub_%s", seller.ID.String()[:8]), AccountType: "subaccount", Status: "active", CreatedAt: time.Now(), }, nil }