diff --git a/migrations/migrate.go b/migrations/migrate.go index 0c22366..cb067d2 100755 --- a/migrations/migrate.go +++ b/migrations/migrate.go @@ -41,6 +41,7 @@ func Migrate() { &models.QrVcard{}, &models.OssApi{}, &models.VcfVcard{}, + &models.DlocalApi{}, ); err != nil { log.Fatalf("Error during main migration: %v", err) } diff --git a/pkg/models/dlocal_api.go b/pkg/models/dlocal_api.go new file mode 100644 index 0000000..65e4f8a --- /dev/null +++ b/pkg/models/dlocal_api.go @@ -0,0 +1,97 @@ +package models + +import ( + "log" + + "github.com/sujit-baniya/fiber-boilerplate/app" + "gorm.io/gorm" +) + +type DlocalApi struct { + gorm.Model + AccessKeyID string `gorm:"not null" json:"access_key_id"` // Access Key ID + AccessKeySecret string `gorm:"not null" json:"access_key_secret"` // Access Key Secret + AccessKeyIDdev string `gorm:"not null" json:"access_key_id_dev"` // Access Key ID + AccessKeySecretdev string `gorm:"not null" json:"access_key_secret_dev"` + IsActive bool `gorm:"default:true" json:"is_active"` // Activar o desactivar config + UrlDev string `gorm:"type:text" json:"url_dev"` + UrlProd string `gorm:"type:text" json:"url_prod"` + Modo string `gorm:"default:'prod'" json:"modo"` +} +type PlanRequest struct { + Name string `json:"name"` + Description string `json:"description"` + Country string `json:"country"` + Currency string `json:"currency"` + Amount float64 `json:"amount"` + FrequencyType string `json:"frequency_type"` + FrequencyValue int `json:"frequency_value"` + DayOfMonth int `json:"day_of_month"` + NotificationURL string `json:"notification_url"` + SuccessURL string `json:"success_url"` + BackURL string `json:"back_url"` + ErrorURL string `json:"error_url"` +} + +// TableName asegura que GORM use la tabla 'qrvcard' +func (DlocalApi) TableName() string { + return "dlocal_api" +} + +// GetAllQrVcard obtiene todos los registros de QrVcard con paginación y búsqueda +func GetAllDlocalApi(limit, offset int, search string) ([]DlocalApi, int64, error) { + var items []DlocalApi + var total int64 + db := app.Http.Database.DB.Model(&DlocalApi{}) + + // Contamos el total de registros + if err := db.Count(&total).Error; err != nil { + log.Printf("Error counting DlocalApi: %v", err) + return nil, 0, err + } + + // Obtenemos los registros con paginación + if err := db.Order("id DESC").Limit(limit).Offset(offset).Find(&items).Error; err != nil { + log.Printf("Error retrieving DlocalApi: %v", err) + return nil, 0, err + } + + return items, total, nil +} + +// CreateQrVcard crea un nuevo registro de QrVcard en la base de datos +func CreateDlocalApi(DlocalApi *DlocalApi) error { + if err := app.Http.Database.DB.Create(&DlocalApi).Error; err != nil { + return err + } + return nil +} + +// UpdateQrVcard actualiza un registro de QrVcard existente +func UpdateDlocalApi(DlocalApi *DlocalApi) error { + if err := app.Http.Database.DB.Model(&DlocalApi).Updates(DlocalApi).Error; err != nil { + return err + } + return nil +} + +// DeleteQrVcard elimina un registro de QrVcard de la base de datos +func DeleteDlocalApi(DlocalApi *DlocalApi) error { + if err := app.Http.Database.DB.Delete(&DlocalApi).Error; err != nil { + return err + } + return nil +} + +// GetLastActiveDlocalApi obtiene el último registro activo +func GetLastActiveDlocalApi() (*DlocalApi, error) { + var dlocalConfig DlocalApi + err := app.Http.Database.DB. + Where("is_active = ?", true). + Order("id DESC"). + First(&dlocalConfig).Error + if err != nil { + return nil, err + } + return &dlocalConfig, nil +} diff --git a/pkg/services/dlocal_service.go b/pkg/services/dlocal_service.go new file mode 100644 index 0000000..8a68e4d --- /dev/null +++ b/pkg/services/dlocal_service.go @@ -0,0 +1,357 @@ +package services + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "time" + + "github.com/sujit-baniya/fiber-boilerplate/pkg/models" + "gorm.io/gorm" +) + +// Modelo que refleja la tabla en la base de datos +type DlocalApi struct { + ID uint `gorm:"primaryKey"` + Modo string + AccessKeyID string + AccessKeySecret string + AccessKeyIDdev string + AccessKeySecretdev string + UrlProd string + UrlDev string +} + +// Estructura de configuración para usar en la llamada +type dlocalConfig struct { + Modo string + AccessKeyID string + AccessKeySecret string + AccessKeyIDdev string + AccessKeySecretdev string + UrlProd string + UrlDev string +} + +// Obtiene la configuración desde la base de datos +func GetDlocalConfigFromDB(db *gorm.DB) (dlocalConfig, error) { + var api DlocalApi + if err := db.First(&api).Error; err != nil { + return dlocalConfig{}, err + } + + return dlocalConfig{ + Modo: api.Modo, + AccessKeyID: api.AccessKeyID, + AccessKeySecret: api.AccessKeySecret, + AccessKeyIDdev: api.AccessKeyIDdev, + AccessKeySecretdev: api.AccessKeySecretdev, + UrlProd: api.UrlProd, + UrlDev: api.UrlDev, + }, nil +} + +type PlanRequest struct { + Name string `json:"name"` + Description string `json:"description"` + Country string `json:"country,omitempty"` + Currency string `json:"currency"` + Amount float64 `json:"amount"` + FrequencyType string `json:"frequency_type"` + FrequencyValue int `json:"frequency_value,omitempty"` + DayOfMonth int `json:"day_of_month,omitempty"` + NotificationURL string `json:"notification_url,omitempty"` + SuccessURL string `json:"success_url"` + BackURL string `json:"back_url"` + ErrorURL string `json:"error_url"` +} + +type PlanUpdated struct { + Name string `json:"name"` // Plan name + Description string `json:"description"` // Plan description + Amount float64 `json:"amount"` // Plan amount + NotificationURL string `json:"notification_url"` // Webhook notification URL + BackURL string `json:"back_url"` // URL to return client to merchant site + SuccessURL string `json:"success_url"` // Redirect URL on payment success + ErrorURL string `json:"error_url"` // Redirect URL on payment error +} + +// Llama al endpoint /v1/me de DLocal usando la configuración +func GetDLocalMe(cfg dlocalConfig) ([]byte, error) { + // Determinar URL base según el modo + baseURL := cfg.UrlDev + AccessKeyID := cfg.AccessKeyIDdev + AccessKeySecret := cfg.AccessKeySecretdev + if cfg.Modo == "prod" { + baseURL = cfg.UrlProd + AccessKeyID = cfg.AccessKeyID + AccessKeySecret = cfg.AccessKeySecret + } + + // Construir token de autorización + authToken := fmt.Sprintf("Bearer %s:%s", AccessKeyID, AccessKeySecret) + + // Crear la solicitud HTTP + req, err := http.NewRequest("GET", baseURL+"/v1/me", bytes.NewBuffer([]byte("{}"))) + if err != nil { + return nil, err + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", authToken) + + // Ejecutar la solicitud + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + // Leer la respuesta + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + return body, nil +} + +// Función para crear el plan en DLocal +func CreatePlan(cfg models.DlocalApi, plan models.PlanRequest) ([]byte, error) { + // Determinar la URL base según el modo + baseURL := cfg.UrlDev + AccessKeyID := cfg.AccessKeyIDdev + AccessKeySecret := cfg.AccessKeySecretdev + if cfg.Modo == "prod" { + baseURL = cfg.UrlProd + AccessKeyID = cfg.AccessKeyID + AccessKeySecret = cfg.AccessKeySecret + } + // URL completa para el endpoint + url := baseURL + "/v1/subscription/plan" + + // Construir el token de autorización + authToken := fmt.Sprintf("Bearer %s:%s", AccessKeyID, AccessKeySecret) + + // Convertir el cuerpo de la solicitud a JSON + requestBody, err := json.Marshal(plan) + 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) + } + + // Agregar los encabezados a la solicitud + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", authToken) + + // Configurar el cliente HTTP + 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) + } + + // Si la respuesta no es exitosa, devolver un error + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("error en la respuesta de DLocal: %s", string(responseBody)) + } + + // Devolver la respuesta exitosa + return responseBody, nil +} + +func SeePlan(cfg models.DlocalApi, planID string) ([]byte, error) { + // Determinar la URL base según el modo + baseURL := cfg.UrlDev + accessKeyID := cfg.AccessKeyIDdev + accessKeySecret := cfg.AccessKeySecretdev + + if cfg.Modo == "prod" { + baseURL = cfg.UrlProd + accessKeyID = cfg.AccessKeyID + accessKeySecret = cfg.AccessKeySecret + } + + // Validar planID (opcional: depende de tus requisitos) + if planID == "" { + return nil, fmt.Errorf("planID no puede estar vacío") + } + + // Construir la URL completa para el endpoint + url := fmt.Sprintf("%s/v1/subscription/plan/%s", baseURL, planID) + + // Construir el token de autorización + authToken := fmt.Sprintf("Bearer %s:%s", accessKeyID, accessKeySecret) + + // Crear la solicitud HTTP + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return nil, fmt.Errorf("error al crear la solicitud: %v", err) + } + + // Agregar los encabezados a la solicitud + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", authToken) + + // Configurar el cliente HTTP + 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 := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("error al leer la respuesta: %v", err) + } + + // Si la respuesta no es exitosa, devolver un error + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("error en la respuesta de DLocal (%d): %s", resp.StatusCode, string(responseBody)) + } + + // Devolver la respuesta exitosa + return responseBody, nil +} + +func UpdatedPlan(cfg models.DlocalApi, plan PlanUpdated, planID string) ([]byte, error) { + // Determinar la URL base según el modo + baseURL := cfg.UrlDev + accessKeyID := cfg.AccessKeyIDdev + accessKeySecret := cfg.AccessKeySecretdev + + if cfg.Modo == "prod" { + baseURL = cfg.UrlProd + accessKeyID = cfg.AccessKeyID + accessKeySecret = cfg.AccessKeySecret + } + + // Construir la URL completa para el endpoint + url := fmt.Sprintf("%s/v1/subscription/plan/%s", baseURL, planID) + + // Construir el token de autorización + authToken := fmt.Sprintf("Bearer %s:%s", accessKeyID, accessKeySecret) + + // Convertir el cuerpo de la solicitud a JSON + requestBody, err := json.Marshal(plan) + 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("PATCH", url, bytes.NewBuffer(requestBody)) + if err != nil { + return nil, fmt.Errorf("error al crear la solicitud: %v", err) + } + + // Agregar los encabezados a la solicitud + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", authToken) + + // Configurar el cliente HTTP + 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 := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("error al leer la respuesta: %v", err) + } + + // Si la respuesta no es exitosa, devolver un error + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("error en la respuesta de DLocal (%d): %s", resp.StatusCode, string(responseBody)) + } + + // Devolver la respuesta exitosa + return responseBody, nil +} + +func DeactivatePlan(cfg models.DlocalApi, planID string, subscriptionId string) ([]byte, error) { + // Determinar la URL base según el modo + baseURL := cfg.UrlDev + accessKeyID := cfg.AccessKeyIDdev + accessKeySecret := cfg.AccessKeySecretdev + + if cfg.Modo == "prod" { + baseURL = cfg.UrlProd + accessKeyID = cfg.AccessKeyID + accessKeySecret = cfg.AccessKeySecret + } + + // Construir la URL completa para el endpoint + url := fmt.Sprintf("%s/v1/subscription/plan/%s/subscription/%s/deactivate", baseURL, planID, subscriptionId) + + // Construir el token de autorización + authToken := fmt.Sprintf("Bearer %s:%s", accessKeyID, accessKeySecret) + + // Crear la solicitud HTTP + req, err := http.NewRequest("PATCH", url, nil) + if err != nil { + return nil, fmt.Errorf("error al crear la solicitud: %v", err) + } + + // Agregar los encabezados a la solicitud + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", authToken) + + // Configurar el cliente HTTP + 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 := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("error al leer la respuesta: %v", err) + } + + // Si la respuesta no es exitosa, devolver un error + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("error en la respuesta de DLocal (%d): %s", resp.StatusCode, string(responseBody)) + } + + // Devolver la respuesta exitosa + return responseBody, nil +} diff --git a/rest/controllers/api/dlocal_controller.go b/rest/controllers/api/dlocal_controller.go new file mode 100644 index 0000000..601599d --- /dev/null +++ b/rest/controllers/api/dlocal_controller.go @@ -0,0 +1,138 @@ +package controllers + +import ( + "fmt" + + "github.com/gofiber/fiber/v2" + "github.com/sujit-baniya/fiber-boilerplate/pkg/models" + "github.com/sujit-baniya/fiber-boilerplate/pkg/services" +) + +// Función para manejar la solicitud de creación de un plan +func CreatePlan(c *fiber.Ctx) error { + var plan models.PlanRequest + + // Parsear el cuerpo de la solicitud a la estructura PlanRequest + if err := c.BodyParser(&plan); 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 plan + response, err := services.CreatePlan(*dlocalConfig, plan) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": fmt.Sprintf("Error al crear el plan: %v", err), + }) + } + + // Devolver la respuesta exitosa + return c.Status(fiber.StatusOK).JSON(fiber.Map{ + "message": "Plan creado exitosamente.", + "response": string(response), + }) +} + +func SeePlan(c *fiber.Ctx) error { + planID := c.Params("planID") + + // Verifica si se recibió correctamente + if planID == "" { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "PlanID es requerido.", + }) + } + + // 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 ver el plan + response, err := services.SeePlan(*dlocalConfig, planID) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": fmt.Sprintf("Error al obtener el plan: %v", err), + }) + } + + // Devolver la respuesta exitosa + return c.Status(fiber.StatusOK).JSON(fiber.Map{ + "message": "Plan obtenido exitosamente.", + "response": string(response), + }) +} + +func UpdatedPlan(c *fiber.Ctx) error { + planID := c.Params("planID") + var plan services.PlanUpdated + // Parsear el cuerpo de la solicitud a la estructura PlanRequest + if err := c.BodyParser(&plan); 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 plan + response, err := services.UpdatedPlan(*dlocalConfig, plan, planID) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": fmt.Sprintf("Error al crear el plan: %v", err), + }) + } + + // Devolver la respuesta exitosa + return c.Status(fiber.StatusOK).JSON(fiber.Map{ + "message": "Plan actualizado exitosamente.", + "response": string(response), + }) +} + +func DeactivatePlan(c *fiber.Ctx) error { + planID := c.Params("planID") + subscriptionId := c.Params("subscriptionId") + + + + // 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 plan + response, err := services.DeactivatePlan(*dlocalConfig, planID, subscriptionId) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": fmt.Sprintf("Error al crear el plan: %v", err), + }) + } + + // Devolver la respuesta exitosa + return c.Status(fiber.StatusOK).JSON(fiber.Map{ + "message": "Suscripción cancelada exitosamente.", + "response": string(response), + }) +} diff --git a/rest/routes/api.go b/rest/routes/api.go index 019128c..97c83d2 100755 --- a/rest/routes/api.go +++ b/rest/routes/api.go @@ -14,6 +14,10 @@ func v1AuthRoutes(api fiber.Router) { api.Post("/generate-qr-tmp", apiControllers.CreateQrTmp) api.Post("/generate-qr", apiControllers.CreateQr) api.Post("/generate-vcf", apiControllers.CreateVcf) + api.Post("/dlocal/subscription/crear-plan", apiControllers.CreatePlan) + api.Get("/dlocal/subscription/ver-plan/:planID", apiControllers.SeePlan) + api.Patch("/dlocal/subscription/actualizar-plan/:planID", apiControllers.UpdatedPlan) + api.Patch("/dlocal/subscription/plan/:planId/subscription/:subscriptionId/deactivate", apiControllers.DeactivatePlan) }