diff --git a/pkg/services/dlocal_service.go b/pkg/services/dlocal_service.go index 5bd6b8c..843fa4b 100644 --- a/pkg/services/dlocal_service.go +++ b/pkg/services/dlocal_service.go @@ -79,6 +79,16 @@ type PlanUpdated struct { ErrorURL string `json:"error_url"` // Redirect URL on payment error } +type PagoRequest struct { + Currency string `json:"currency"` + Amount int `json:"amount"` + OrderID int `json:"order_id"` + Description string `json:"description"` + SuccessURL string `json:"success_url"` + BackURL string `json:"back_url"` + NotificationURL string `json:"notification_url"` +} + // Llama al endpoint /v1/me de DLocal usando la configuración func GetDLocalMe(cfg dlocalConfig) ([]byte, error) { // Determinar URL base según el modo @@ -462,3 +472,62 @@ func SeeSubscription(cfg models.DlocalApi, subscriptionId string, invoiceId stri return body, nil } + +func CreatePago(cfg models.DlocalApi, pago PagoRequest) ([]byte, error) { + // Determinar la URL base y credenciales + baseURL := cfg.UrlDev + AccessKeyID := cfg.AccessKeyIDdev + AccessKeySecret := cfg.AccessKeySecretdev + if cfg.Modo == "prod" { + baseURL = cfg.UrlProd + AccessKeyID = cfg.AccessKeyID + AccessKeySecret = cfg.AccessKeySecret + } + + // URL del endpoint de pagos + url := baseURL + "/v1/payments" + + // Token de autenticación + authToken := fmt.Sprintf("Bearer %s:%s", AccessKeyID, AccessKeySecret) + + // Convertir el cuerpo de la solicitud a JSON + requestBody, err := json.Marshal(pago) + if err != nil { + return nil, fmt.Errorf("error al convertir el cuerpo de la solicitud a JSON: %v", err) + } + + // Crear la solicitud HTTP + req, err := http.NewRequest("POST", url, bytes.NewBuffer(requestBody)) + if err != nil { + return nil, fmt.Errorf("error al crear la solicitud: %v", err) + } + + // Encabezados + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", authToken) + + // Cliente HTTP con timeout + client := &http.Client{ + Timeout: 30 * time.Second, + } + + // Ejecutar la solicitud + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("error al enviar la solicitud: %v", err) + } + defer resp.Body.Close() + + // Leer la respuesta + responseBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("error al leer la respuesta: %v", err) + } + + // Verificar si la respuesta fue exitosa + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { + return nil, fmt.Errorf("error en la respuesta de DLocal: %s", string(responseBody)) + } + + return responseBody, nil +} diff --git a/pkg/services/rapyd_service.go b/pkg/services/rapyd_service.go new file mode 100644 index 0000000..7757072 --- /dev/null +++ b/pkg/services/rapyd_service.go @@ -0,0 +1,180 @@ +package services + +import ( + "bytes" + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "math/rand" + "net/http" + "net/url" + "strconv" + "strings" + "time" +) + +const ( + BaseURL = "https://sandboxapi.rapyd.net" + SecretKey = "rsk_a97addbe02a32a153eaedf1db35c11ce24e4b6c7257c01eb5953d59ad5d281d8f1b1c6a60b68b" + AccessKey = "rak_EC66D246035CA48157FA" +) + +func generateSalt(length int) string { + const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + b := make([]byte, length) + for i := range b { + b[i] = charset[rand.Intn(len(charset))] + } + return string(b) +} + +func getUnixTime() int64 { + return 1747194248 +} + +func createSignature(method, urlPath string, body []byte, salt string, timestamp int64) string { + bodyStr := string(body) + + toSign := strings.ToLower(method) + + urlPath + + salt + + strconv.FormatInt(timestamp, 10) + + AccessKey + + bodyStr + + fmt.Println("Data to sign (Go):", toSign) + + h := hmac.New(sha256.New, []byte(SecretKey)) + h.Write([]byte(toSign)) + + // Elimina padding '=' al final + encoder := base64.URLEncoding.WithPadding(base64.NoPadding) + signature := encoder.EncodeToString(h.Sum(nil)) + + fmt.Printf("HMAC (Go hex): %x\n", h.Sum(nil)) + fmt.Println("Signature (Go):", signature) + + return signature +} + +func createHeaders(method, fullURL string, body interface{}) ([]byte, map[string]string, error) { + // Serializar cuerpo como JSON sin espacios + var bodyBytes []byte + var err error + if body != nil { + bodyBytes, err = json.Marshal(body) + if err != nil { + return nil, nil, err + } + } else { + bodyBytes = []byte{} + } + + // Obtener solo la ruta `/v1/...` desde fullURL + u, err := url.Parse(fullURL) + if err != nil { + return nil, nil, err + } + urlPath := u.Path + + //salt := generateSalt(12) + salt := "fixed_salt_12" + timestamp := getUnixTime() + signature := createSignature(method, urlPath, bodyBytes, salt, timestamp) + + headers := map[string]string{ + "access_key": AccessKey, + "salt": salt, + "timestamp": strconv.FormatInt(timestamp, 10), + "signature": signature, + "idempotency": strconv.FormatInt(time.Now().UnixNano(), 10), + "Content-Type": "application/json", + } + + return bodyBytes, headers, nil +} + +func MakeRequest(method, path string, body interface{}) (map[string]interface{}, error) { + fullURL := BaseURL + path + urlPath := path + + // Serializa el cuerpo sin espacios ni reordenamientos + bodyBytes, err := json.Marshal(body) + if err != nil { + return nil, err + } + + salt := "fixed_salt_12" + timestamp := int64(1747194248) // Fijo para comparar con Python + signature := createSignature(method, urlPath, bodyBytes, salt, timestamp) + + headers := map[string]string{ + "access_key": AccessKey, + "salt": salt, + "timestamp": strconv.FormatInt(timestamp, 10), + "signature": signature, + "idempotency": strconv.FormatInt(time.Now().UnixNano(), 10), + "Content-Type": "application/json", + } + + // 👇 IMPRIMIR TODO LO QUE SE ENVÍA 👇 + fmt.Println("➡️ REQUEST ENVIADO:") + fmt.Println("Method:", method) + fmt.Println("URL:", fullURL) + fmt.Println("Headers:") + for k, v := range headers { + fmt.Printf(" %s: %s\n", k, v) + } + fmt.Println("Body:") + fmt.Println(string(bodyBytes)) + fmt.Println(strings.Repeat("-", 50)) + + // Crea request + req, err := http.NewRequest(strings.ToUpper(method), fullURL, bytes.NewReader(bodyBytes)) + if err != nil { + return nil, err + } + + for k, v := range headers { + req.Header.Set(k, v) + } + + // 👇 OPCIONAL: Imprimir curl equivalente 👇 + curlCmd := fmt.Sprintf("curl -X %s '%s' \\\n", req.Method, req.URL.String()) + for k, v := range req.Header { + curlCmd += fmt.Sprintf(" -H '%s: %s' \\\n", k, v[0]) + } + if len(bodyBytes) > 0 { + curlCmd += fmt.Sprintf(" -d '%s'", string(bodyBytes)) + } + fmt.Println("📋 Comando curl equivalente:") + fmt.Println(curlCmd) + fmt.Println(strings.Repeat("-", 50)) + + // Envía la solicitud + client := &http.Client{Timeout: 30 * time.Second} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + if resp.StatusCode != 200 { + return nil, fmt.Errorf("status %d: %s", resp.StatusCode, string(respBody)) + } + + var result map[string]interface{} + if err := json.Unmarshal(respBody, &result); err != nil { + return nil, err + } + + return result, nil +} diff --git a/rest/controllers/api/dlocal_controller.go b/rest/controllers/api/dlocal_controller.go index cd4be43..4d5fc82 100644 --- a/rest/controllers/api/dlocal_controller.go +++ b/rest/controllers/api/dlocal_controller.go @@ -185,3 +185,36 @@ func SeeSubscription(c *fiber.Ctx) error { "response": string(response), }) } + +func CreatePago(c *fiber.Ctx) error { + var pago services.PagoRequest + + // Parsear el cuerpo de la solicitud a la estructura PagoRequest + if err := c.BodyParser(&pago); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": fmt.Sprintf("Error al parsear el cuerpo de la solicitud: %v", err), + }) + } + + // Obtener la configuración activa de DlocalApi + dlocalConfig, err := models.GetLastActiveDlocalApi() + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "Error al obtener la configuración activa de DlocalApi.", + }) + } + + // Llamar al servicio para crear el pago + response, err := services.CreatePago(*dlocalConfig, pago) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": fmt.Sprintf("Error al crear el pago: %v", err), + }) + } + + // Devolver la respuesta exitosa + return c.Status(fiber.StatusOK).JSON(fiber.Map{ + "message": "Pago creado exitosamente.", + "response": string(response), + }) +} diff --git a/rest/controllers/api/rapyd_controller.go b/rest/controllers/api/rapyd_controller.go new file mode 100644 index 0000000..101fb6e --- /dev/null +++ b/rest/controllers/api/rapyd_controller.go @@ -0,0 +1,83 @@ +package controllers + +import ( + "encoding/json" + "fmt" + + "github.com/gofiber/fiber/v2" + "github.com/sujit-baniya/fiber-boilerplate/pkg/services" +) + +// Estructuras definidas por el cliente (ya están bien) +type Address struct { + Name string `json:"name"` + Line1 string `json:"line_1"` + Line2 string `json:"line_2"` + Line3 string `json:"line_3"` + City string `json:"city"` + State string `json:"state"` + Country string `json:"country"` + Zip string `json:"zip"` + PhoneNumber string `json:"phone_number"` + Metadata map[string]interface{} `json:"metadata"` + Canton string `json:"canton"` + District string `json:"district"` +} + +type Contact struct { + PhoneNumber string `json:"phone_number"` + Email string `json:"email"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + MothersName string `json:"mothers_name"` + ContactType string `json:"contact_type"` + Address Address `json:"address"` + IdentificationType string `json:"identification_type"` + IdentificationNumber string `json:"identification_number"` + DateOfBirth string `json:"date_of_birth"` + Country string `json:"country"` + Nationality string `json:"nationality"` + Metadata map[string]interface{} `json:"metadata"` +} + +type WalletRequest struct { + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + EwalletReferenceID string `json:"ewallet_reference_id"` + Metadata map[string]interface{} `json:"metadata"` + Type string `json:"type"` + Contact Contact `json:"contact"` +} + +func MakeWallet(c *fiber.Ctx) error { + var body WalletRequest + + // 1. Parsear el cuerpo de la solicitud + if err := c.BodyParser(&body); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "Error al parsear el cuerpo de la solicitud: " + err.Error(), + }) + } + + // 2. Imprimir cuerpo recibido para debug + bodyBytes, _ := json.MarshalIndent(body, "", " ") + fmt.Println("Cuerpo recibido:\n", string(bodyBytes)) + + // 3. Enviar solicitud a Rapyd + response, err := services.MakeRequest("post", "/v1/ewallets", body) + if err != nil { + fmt.Println("Error al hacer la solicitud a Rapyd:", err) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "Error al comunicarse con Rapyd", + "details": err.Error(), + }) + } + + // 4. Formatear y devolver la respuesta de Rapyd + prettyJSON, _ := json.MarshalIndent(response, "", " ") + + + fmt.Println("Respuesta de Rapyd:\n", string(prettyJSON)) + + return c.Status(fiber.StatusOK).JSON(response) +} diff --git a/rest/routes/api.go b/rest/routes/api.go index c65c3de..ba996d7 100755 --- a/rest/routes/api.go +++ b/rest/routes/api.go @@ -20,6 +20,8 @@ func v1AuthRoutes(api fiber.Router) { api.Patch("/dlocal/subscription/plan/:planId/subscription/:subscriptionId/deactivate", apiControllers.DeactivatePlan) api.Get("/dlocal/subscription/plan/all", apiControllers.SeePlanes) api.Get("/dlocal/subscription/:subscriptionId/execution/:invoiceId", apiControllers.SeeSubscription) + api.Post("/dlocal/payment/crear-pago", apiControllers.CreatePago) + api.Post("/rapyd/wallet/create", apiControllers.MakeWallet) } func v1Routes(api fiber.Router) {