From 2447df5ce7d3311c845f5d593abcb8ccdd7a31b0 Mon Sep 17 00:00:00 2001 From: victor mendoza Date: Tue, 12 May 2026 18:02:01 -0300 Subject: [PATCH 01/13] feat: add .env to .gitignore to prevent environment file from being tracked --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index a01c531..4a81aba 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ Test.java build gradle .DS_Store +.env From 183e4a45ceaa4414ba8cba550209737798fbff59 Mon Sep 17 00:00:00 2001 From: victor mendoza Date: Tue, 12 May 2026 18:02:12 -0300 Subject: [PATCH 02/13] feat: add .env.example file and update README with environment variable instructions --- .env.example | 4 ++++ README.md | 9 +++++++++ 2 files changed, 13 insertions(+) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..86a26d6 --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +ONECLICK_MALL_PROMOTIONS_API_KEY=tu-api-key +ONECLICK_MALL_PROMOTIONS_COMMERCE_CODE=tu-commerce-code +ONECLICK_MALL_PROMOTIONS_CHILD1_COMMERCE_CODE=tu-child-commerce-code-1 +ONECLICK_MALL_PROMOTIONS_CHILD2_COMMERCE_CODE=tu-child-commerce-code-2 diff --git a/README.md b/README.md index 72d5146..e1f4a52 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,15 @@ mvn clean install ## Ejecución +El flujo `Webpay Oneclick Mall Promociones` usa estas variables de entorno: + +```bash +ONECLICK_MALL_PROMOTIONS_API_KEY=tu-api-key +ONECLICK_MALL_PROMOTIONS_COMMERCE_CODE=tu-commerce-code +ONECLICK_MALL_PROMOTIONS_CHILD1_COMMERCE_CODE=tu-child-commerce-code-1 +ONECLICK_MALL_PROMOTIONS_CHILD2_COMMERCE_CODE=tu-child-commerce-code-2 +``` + Para poder correr el proyecto en modo desarrollo, debes utilizar el siguiente comando en una consola: ```bash From c347620d7a9dc8390a9284548cfe1fb6de6caa0e Mon Sep 17 00:00:00 2001 From: victor mendoza Date: Tue, 12 May 2026 18:03:37 -0300 Subject: [PATCH 03/13] feat: add OneClick Mall promotions configuration to application properties --- src/main/resources/application.properties | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index f3fc4f8..21666f8 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,6 @@ spring.application.name=transbank-sdk-java-example + +oneclick.mall.promotions.api-key=${ONECLICK_MALL_PROMOTIONS_API_KEY:} +oneclick.mall.promotions.commerce-code=${ONECLICK_MALL_PROMOTIONS_COMMERCE_CODE:} +oneclick.mall.promotions.child1-commerce-code=${ONECLICK_MALL_PROMOTIONS_CHILD1_COMMERCE_CODE:} +oneclick.mall.promotions.child2-commerce-code=${ONECLICK_MALL_PROMOTIONS_CHILD2_COMMERCE_CODE:} From 2fbf6ac007490ed83e4a8de7dbaf435fc4467fe9 Mon Sep 17 00:00:00 2001 From: victor mendoza Date: Tue, 12 May 2026 18:11:02 -0300 Subject: [PATCH 04/13] feat: implement PromotionsOneclickMallController for handling Oneclick Mall promotions --- .../PromotionsOneclickMallController.java | 350 ++++++++++++++++++ 1 file changed, 350 insertions(+) create mode 100644 src/main/java/cl/transbank/webpay/example/controllers/PromotionsOneclickMallController.java diff --git a/src/main/java/cl/transbank/webpay/example/controllers/PromotionsOneclickMallController.java b/src/main/java/cl/transbank/webpay/example/controllers/PromotionsOneclickMallController.java new file mode 100644 index 0000000..3d26a40 --- /dev/null +++ b/src/main/java/cl/transbank/webpay/example/controllers/PromotionsOneclickMallController.java @@ -0,0 +1,350 @@ +package cl.transbank.webpay.example.controllers; + +import cl.transbank.common.IntegrationType; +import cl.transbank.webpay.common.WebpayOptions; +import cl.transbank.webpay.exception.*; +import cl.transbank.webpay.oneclick.Oneclick; +import cl.transbank.webpay.oneclick.model.MallTransactionCreateDetails; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +@Log4j2 +@Controller +@RequestMapping("/promotions-oneclick-mall") +public class PromotionsOneclickMallController extends BaseController { + + private static final int AUTHORIZED = 0; + private static final String TEMPLATE_FOLDER = "promotions_oneclick_mall"; + private static final String BASE_URL = "/promotions-oneclick-mall"; + private static final String PRODUCT = "Webpay Oneclick Mall Promociones"; + private static final String MODEL_NAVIGATION = "navigation"; + private static final String MODEL_RESPONSE = "response_data"; + private static final String MODEL_RESPONSE_JSON = "response_data_json"; + private static final String REQUEST_DATA_JSON = "request_data_json"; + private static final String REQUEST = "Petición"; + private static final String RESPONSE = "Respuesta"; + + private static final String VIEW_START = TEMPLATE_FOLDER + "/start"; + private static final String VIEW_FINISH = TEMPLATE_FOLDER + "/finish"; + private static final String VIEW_AUTHORIZE = TEMPLATE_FOLDER + "/authorize"; + private static final String VIEW_DELETE = TEMPLATE_FOLDER + "/delete"; + private static final String VIEW_STATUS = TEMPLATE_FOLDER + "/status"; + private static final String VIEW_REFUND = TEMPLATE_FOLDER + "/refund"; + private static final String VIEW_INFO_BIN = TEMPLATE_FOLDER + "/info_bin"; + + private static final String ENV_API_KEY = "ONECLICK_MALL_PROMOTIONS_API_KEY"; + private static final String ENV_COMMERCE_CODE = "ONECLICK_MALL_PROMOTIONS_COMMERCE_CODE"; + private static final String ENV_CHILD1_COMMERCE_CODE = "ONECLICK_MALL_PROMOTIONS_CHILD1_COMMERCE_CODE"; + private static final String ENV_CHILD2_COMMERCE_CODE = "ONECLICK_MALL_PROMOTIONS_CHILD2_COMMERCE_CODE"; + private static final String TBK_USER = "tbkUser"; + private static final String REQUEST_KEY = "request"; + private static final String RESPONSE_KEY = "response"; + private static final String DATA_KEY = "Datos"; + private static final String USERNAME = "username"; + private static final String REQUEST_DATA = "request_data"; + + private static final Map NAV_START; + private static final Map NAV_FINISH; + private static final Map NAV_FINISH_RECOVER; + private static final Map NAV_FINISH_REJECTED; + private static final Map NAV_AUTHORIZE; + private static final Map NAV_DELETE; + private static final Map NAV_STATUS; + private static final Map NAV_REFUND; + private static final Map NAV_INFO_BIN; + private static final Map DOTENV = loadDotenv(); + + @Value("${oneclick.mall.promotions.api-key:}") + private String apiKey; + + @Value("${oneclick.mall.promotions.commerce-code:}") + private String commerceCode; + + @Value("${oneclick.mall.promotions.child1-commerce-code:}") + private String child1CommerceCode; + + @Value("${oneclick.mall.promotions.child2-commerce-code:}") + private String child2CommerceCode; + + static { + NAV_START = new LinkedHashMap<>(); + NAV_START.put(REQUEST_KEY, REQUEST); + NAV_START.put(RESPONSE_KEY, RESPONSE); + NAV_START.put("form", "Creación del formulario"); + NAV_START.put("example", "Ejemplo"); + + NAV_FINISH = new LinkedHashMap<>(); + NAV_FINISH.put("data", DATA_KEY); + NAV_FINISH.put(REQUEST_KEY, REQUEST); + NAV_FINISH.put(RESPONSE_KEY, RESPONSE); + NAV_FINISH.put("authorize", "Autorizar una transacción"); + + NAV_FINISH_RECOVER = new LinkedHashMap<>(); + NAV_FINISH_RECOVER.put("data", DATA_KEY); + + NAV_FINISH_REJECTED = new LinkedHashMap<>(); + NAV_FINISH_REJECTED.put("data", DATA_KEY); + NAV_FINISH_REJECTED.put(REQUEST_KEY, REQUEST); + NAV_FINISH_REJECTED.put(RESPONSE_KEY, RESPONSE); + + NAV_AUTHORIZE = new LinkedHashMap<>(); + NAV_AUTHORIZE.put(REQUEST_KEY, REQUEST); + NAV_AUTHORIZE.put(RESPONSE_KEY, RESPONSE); + NAV_AUTHORIZE.put("done", "Listo"); + + NAV_DELETE = new LinkedHashMap<>(); + NAV_DELETE.put(REQUEST_KEY, REQUEST); + NAV_DELETE.put(RESPONSE_KEY, RESPONSE); + + NAV_STATUS = new LinkedHashMap<>(); + NAV_STATUS.put(REQUEST_KEY, REQUEST); + NAV_STATUS.put(RESPONSE_KEY, RESPONSE); + + NAV_REFUND = NAV_STATUS; + NAV_INFO_BIN = NAV_STATUS; + } + + @GetMapping({"", "/", "/start"}) + public String start(HttpServletRequest req, Model model) + throws IOException, InscriptionStartException { + model.addAttribute(MODEL_NAVIGATION, NAV_START); + addBreadcrumbs(model, "Iniciar inscripción", "#"); + + String username = "User-" + getRandomNumber(); + String email = "user." + getRandomNumber() + "@example.com"; + String requestUrl = req.getRequestURL().toString(); + String returnUrl = requestUrl.replaceFirst("/start/?$", "").replaceFirst("/$", "") + "/finish"; + + var resp = getInscription().start(username, email, returnUrl); + + Map requestData = Map.of( + USERNAME, username, + "email", email, + "returnUrl", returnUrl + ); + + model.addAttribute(REQUEST_DATA, requestData); + model.addAttribute(REQUEST_DATA_JSON, toJson(requestData)); + model.addAttribute(MODEL_RESPONSE, resp); + model.addAttribute(MODEL_RESPONSE_JSON, toJson(resp)); + + req.getSession().setAttribute(USERNAME, username); + req.getSession().setAttribute("email", email); + + return VIEW_START; + } + + @GetMapping("/finish") + public String finish(HttpServletRequest req, + @RequestParam Map params, + @RequestParam(name = "TBK_TOKEN", required = false) String token, + @RequestParam(name = "TBK_ORDEN_COMPRA", required = false) String ordenCompra, + Model model) + throws IOException, InscriptionFinishException { + model.addAttribute(MODEL_NAVIGATION, NAV_FINISH); + addBreadcrumbs(model, "Finalizar inscripción", "#"); + + if (ordenCompra != null) { + model.addAttribute(MODEL_NAVIGATION, NAV_FINISH_RECOVER); + model.addAttribute(REQUEST_DATA_JSON, toJson(params)); + return VIEW_RECOVER_ERROR; + } + + String username = (String) req.getSession().getAttribute(USERNAME); + var resp = getInscription().finish(token); + + model.addAttribute(MODEL_RESPONSE, resp); + model.addAttribute(MODEL_RESPONSE_JSON, toJson(resp)); + + if (resp.getResponseCode() != AUTHORIZED) { + model.addAttribute(MODEL_NAVIGATION, NAV_FINISH_REJECTED); + model.addAttribute(REQUEST_DATA_JSON, toJson(params)); + return VIEW_REJECTED_ERROR; + } + + req.getSession().setAttribute(TBK_USER, resp.getTbkUser()); + + model.addAttribute(REQUEST_DATA, Map.of( + USERNAME, username, + TBK_USER, resp.getTbkUser() + )); + model.addAttribute("token", token); + model.addAttribute(USERNAME, username); + model.addAttribute("tbk_user", resp.getTbkUser()); + model.addAttribute("child_commerce_code1", getConfiguredValue(child1CommerceCode, ENV_CHILD1_COMMERCE_CODE)); + model.addAttribute("child_commerce_code2", getConfiguredValue(child2CommerceCode, ENV_CHILD2_COMMERCE_CODE)); + + return VIEW_FINISH; + } + + @GetMapping("/delete") + public String delete(@RequestParam String username, + @RequestParam("tbk_user") String tbkUser, + Model model) + throws IOException, InscriptionDeleteException { + model.addAttribute(MODEL_NAVIGATION, NAV_DELETE); + addBreadcrumbs(model, "Eliminar inscripción", "#"); + getInscription().delete(tbkUser, username); + return VIEW_DELETE; + } + + @GetMapping("/authorize") + public String authorize( + @RequestParam String username, + @RequestParam("tbk_user") String tbkUser, + @RequestParam("child_commerce_code1") String childCode1, + @RequestParam("child_commerce_code2") String childCode2, + @RequestParam("child_commerce_amount1") double amount1, + @RequestParam("child_commerce_amount2") double amount2, + @RequestParam("child_commerce_installments1") int installments1, + @RequestParam("child_commerce_installments2") int installments2, + Model model) + throws IOException, TransactionAuthorizeException { + model.addAttribute(MODEL_NAVIGATION, NAV_AUTHORIZE); + addBreadcrumbs(model, "Autorizar transacción", "#"); + + String buyOrder = "buyOrder_" + getRandomNumber(); + String childBuyOrder1 = "childBuyOrder1_" + getRandomNumber(); + String childBuyOrder2 = "childBuyOrder2_" + getRandomNumber(); + + var details = MallTransactionCreateDetails + .build() + .add(amount1, childCode1, childBuyOrder1, (byte) installments1) + .add(amount2, childCode2, childBuyOrder2, (byte) installments2); + + var resp = getTransaction().authorize(username, tbkUser, buyOrder, details); + model.addAttribute(MODEL_RESPONSE, resp); + model.addAttribute(MODEL_RESPONSE_JSON, toJson(resp)); + return VIEW_AUTHORIZE; + } + + @GetMapping("/status") + public String status(@RequestParam("buy_order") String buyOrder, Model model) + throws IOException, TransactionStatusException { + model.addAttribute(MODEL_NAVIGATION, NAV_STATUS); + addBreadcrumbs(model, "Consultar estado", "#"); + var resp = getTransaction().status(buyOrder); + model.addAttribute(MODEL_RESPONSE_JSON, toJson(resp)); + return VIEW_STATUS; + } + + @GetMapping("/refund") + public String refund(@RequestParam("buy_order") String buyOrder, + @RequestParam("child_buy_order") String childBuyOrder, + @RequestParam("child_commerce_code") String childCommerceCode, + @RequestParam double amount, + Model model) + throws IOException, TransactionRefundException { + model.addAttribute(MODEL_NAVIGATION, NAV_REFUND); + addBreadcrumbs(model, "Reembolso", "#"); + var resp = getTransaction().refund(buyOrder, childCommerceCode, childBuyOrder, amount); + model.addAttribute("buy_order", buyOrder); + model.addAttribute(MODEL_RESPONSE_JSON, toJson(resp)); + return VIEW_REFUND; + } + + @GetMapping("/info-bin") + public String infoBin(@RequestParam("tbk_user") String tbkUser, Model model) + throws IOException, QueryBinException { + model.addAttribute(MODEL_NAVIGATION, NAV_INFO_BIN); + addBreadcrumbs(model, "Consulta servicio de bines", "#"); + var requestData = Map.of(TBK_USER, tbkUser); + var resp = getBinInfo().queryBin(tbkUser); + model.addAttribute(REQUEST_DATA, requestData); + model.addAttribute(REQUEST_DATA_JSON, toJson(requestData)); + model.addAttribute(MODEL_RESPONSE_JSON, toJson(resp)); + return VIEW_INFO_BIN; + } + + @ExceptionHandler(Exception.class) + public String handleException(Exception e, Model model) { + log.error("Error inesperado", e); + model.addAttribute("error", getDisplayableErrorMessage(e)); + return VIEW_ERROR; + } + + private void addBreadcrumbs(Model model, String label, String url) { + Map breadcrumbs = new LinkedHashMap<>(); + breadcrumbs.put("Inicio", "/"); + breadcrumbs.put(PRODUCT, BASE_URL); + if (label != null) breadcrumbs.put(label, url); + model.addAttribute("product", PRODUCT); + model.addAttribute("base_url", BASE_URL); + model.addAttribute("breadcrumbs", breadcrumbs); + } + + private Oneclick.MallInscription getInscription() { + return new Oneclick.MallInscription(getOptions()); + } + + private Oneclick.MallTransaction getTransaction() { + return new Oneclick.MallTransaction(getOptions()); + } + + private Oneclick.MallBinInfo getBinInfo() { + return new Oneclick.MallBinInfo(getOptions()); + } + + private WebpayOptions getOptions() { + return new WebpayOptions( + getConfiguredValue(commerceCode, ENV_COMMERCE_CODE), + getConfiguredValue(apiKey, ENV_API_KEY), + IntegrationType.TEST + ); + } + + private String getConfiguredValue(String propertyValue, String envName) { + if (propertyValue != null && !propertyValue.isBlank()) { + return propertyValue; + } + + return getEnv(envName); + } + + private String getEnv(String name) { + String value = System.getenv(name); + if (value == null || value.isBlank()) { + value = DOTENV.get(name); + } + if (value == null || value.isBlank()) { + throw new IllegalStateException("La variable de entorno " + name + " es obligatoria."); + } + return value; + } + + private static Map loadDotenv() { + Path path = Path.of(".env"); + if (!Files.exists(path)) { + return Map.of(); + } + + Map values = new HashMap<>(); + try { + for (String line : Files.readAllLines(path)) { + String trimmed = line.trim(); + if (trimmed.isEmpty() || trimmed.startsWith("#") || !trimmed.contains("=")) { + continue; + } + String[] parts = trimmed.split("=", 2); + values.put(parts[0].trim(), parts[1].trim()); + } + } catch (IOException e) { + return Map.of(); + } + return values; + } +} From 6e39db9c764bc5c6720384dff69c4c02371e2052 Mon Sep 17 00:00:00 2001 From: victor mendoza Date: Tue, 12 May 2026 18:11:12 -0300 Subject: [PATCH 05/13] feat: add start.html template for Webpay Oneclick Mall inscription process --- .../promotions_oneclick_mall/start.html | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 src/main/resources/templates/promotions_oneclick_mall/start.html diff --git a/src/main/resources/templates/promotions_oneclick_mall/start.html b/src/main/resources/templates/promotions_oneclick_mall/start.html new file mode 100644 index 0000000..95760bf --- /dev/null +++ b/src/main/resources/templates/promotions_oneclick_mall/start.html @@ -0,0 +1,34 @@ +
+
+

Webpay Oneclick Mall Promociones - Creación de Inscripción

+

En esta etapa comienza el proceso de inscripción del medio de pago. Este paso inicial es fundamental para dirigir al Tarjetahabiente al formulario de inscripción.

+

Todas las transacciones en este proyecto de ejemplo son realizadas en ambiente de integración.

+

Paso 1: Petición

+
    +
  1. Comienza por importar la librería Oneclick en tu proyecto.
  2. +
  3. Luego, inicia una inscripción utilizando las funciones proporcionadas mediante el SDK.
  4. +
+
var options = new WebpayOptions(commerceCode, apiKey, IntegrationType.TEST);
+var inscription = new Oneclick.MallInscription(options);
+var resp = inscription.start(username, email, responseUrl);
+

Paso 2: Respuesta

+

Una vez que hayas iniciado la inscripción, aquí encontrarás los datos de respuesta generados por el proceso.

+
+

Paso 3: Creación del formulario

+

Utiliza estos datos de respuesta para redireccionar al usuario al formulario de inscripción del Tarjetahabiente.

+
+

Ejemplo

+

Para llevar a cabo una inscripción en nuestro sistema, primero debemos crearla.

+
+

Para fines de este ejemplo, haremos visible el campo "TBK_TOKEN", el cual es esencial para completar el proceso.

+ Antes de continuar al formulario de Webpay, asegúrate de contar con los datos de las tarjetas de prueba que están en la documentación. +
+
+ Formulario de redirección + + + +
+
+
+
From 7388d08d97ccbfe8d5e92799ca707577b72bed53 Mon Sep 17 00:00:00 2001 From: victor mendoza Date: Tue, 12 May 2026 18:11:32 -0300 Subject: [PATCH 06/13] feat: add finish.html template for Webpay Oneclick Mall enrollment completion --- .../promotions_oneclick_mall/finish.html | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 src/main/resources/templates/promotions_oneclick_mall/finish.html diff --git a/src/main/resources/templates/promotions_oneclick_mall/finish.html b/src/main/resources/templates/promotions_oneclick_mall/finish.html new file mode 100644 index 0000000..5dd4f51 --- /dev/null +++ b/src/main/resources/templates/promotions_oneclick_mall/finish.html @@ -0,0 +1,42 @@ +
+
+

Webpay Oneclick Mall Promociones - Finalizar inscripción

+

En esta fase, completaremos el proceso de inscripción, permitiéndonos posteriormente realizar cargos a la tarjeta inscrita.

+

Paso 1: Datos recibidos

+

Después de finalizar el flujo en el formulario de inscripción, recibirás un GET con la siguiente información:

+
{"TBK_TOKEN":""}
+

Paso 2: Petición de autorización

+

Utiliza el token recibido para finalizar la inscripción mediante una nueva llamada a Oneclick.

+
var response = inscription.finish(token);
+

Paso 3: Respuesta

+

Transbank responderá con información crucial. Guarda estos detalles, ya que serán necesarios para autorizar transacciones futuras.

+
+

¡La tarjeta ya está inscrita!

+

Con la inscripción exitosa se pueden autorizar transacciones.

+

Autorizar una transacción

+

Asegúrate de guardar los datos de la respuesta obtenidos durante la inscripción.

+
+

Después de una inscripción exitosa, tienes tres opciones: autorizar un pago, consultar bines o borrar al usuario que se acaba de inscribir.

+
+ + + + +
+

Tienda 1

+
+
+
+
+

Tienda 2

+
+
+
+
+ +
+
+ CONSULTA BINES + BORRAR USUARIO +
+
From f6965ab4bf868cba29dca1e833972d5b177e7bc6 Mon Sep 17 00:00:00 2001 From: victor mendoza Date: Tue, 12 May 2026 18:11:40 -0300 Subject: [PATCH 07/13] feat: add info_bin.html template for Webpay Oneclick Mall BIN consultation --- .../promotions_oneclick_mall/info_bin.html | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 src/main/resources/templates/promotions_oneclick_mall/info_bin.html diff --git a/src/main/resources/templates/promotions_oneclick_mall/info_bin.html b/src/main/resources/templates/promotions_oneclick_mall/info_bin.html new file mode 100644 index 0000000..2bed3fa --- /dev/null +++ b/src/main/resources/templates/promotions_oneclick_mall/info_bin.html @@ -0,0 +1,13 @@ +
+
+

Webpay Oneclick Mall Promociones - Consulta servicio de bines

+

Con esta operación puedes consultar el BIN asociado al medio de pago inscrito usando el valor de tbkUser. Si el comercio no tiene habilitado este servicio, la respuesta incluirá un error.

+

Paso 1: Petición

+

Para realizar la consulta, necesitarás el tbkUser obtenido al finalizar la inscripción.

+
var resp = binInfo.queryBin(tbkUser);
+
+

Paso 2: Respuesta

+

Transbank responderá con la información del BIN consultado.

+
+
+
From 4497e87674b0b08eefa8cbbc94a4c3adf8c603ad Mon Sep 17 00:00:00 2001 From: victor mendoza Date: Tue, 12 May 2026 18:11:47 -0300 Subject: [PATCH 08/13] feat: add authorize.html template for Webpay Oneclick Mall payment authorization --- .../promotions_oneclick_mall/authorize.html | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 src/main/resources/templates/promotions_oneclick_mall/authorize.html diff --git a/src/main/resources/templates/promotions_oneclick_mall/authorize.html b/src/main/resources/templates/promotions_oneclick_mall/authorize.html new file mode 100644 index 0000000..dc72a19 --- /dev/null +++ b/src/main/resources/templates/promotions_oneclick_mall/authorize.html @@ -0,0 +1,35 @@ +
+
+

Webpay Oneclick Mall Promociones - Autorizar pago

+

En este primer paso, procederemos a autorizar una transacción en la tarjeta que ha sido previamente inscrita.

+

Paso 1: Petición

+

Ahora que contamos con el username y el tbkUser, estamos listos para autorizar transacciones.

+
var details = MallTransactionCreateDetails.build()
+    .add(amount1, childCode1, childBuyOrder1, (byte) installments1)
+    .add(amount2, childCode2, childBuyOrder2, (byte) installments2);
+var resp = transaction.authorize(username, tbkUser, buyOrder, details);
+

Paso 2: Respuesta

+

Verifica que el campo responseCode tenga valor 0 y que el campo status sea AUTHORIZED.

+
+

¡Listo!

+

Después de autorizar la transacción, considera las siguientes utilidades adicionales:

+
    +
  • Reembolsar: Puedes reversar o anular el pago según ciertas condiciones comerciales.
  • +
  • Consultar Estado: Hasta 7 días después de realizada la transacción, podrás consultar el estado.
  • +
+
+
+
+
+
+
+
+
+
+ +
+
+
+ CONSULTAR ESTADO +
+
From 6d25fd5dca94115135afe5309b362e19f22bfc64 Mon Sep 17 00:00:00 2001 From: victor mendoza Date: Tue, 12 May 2026 18:11:58 -0300 Subject: [PATCH 09/13] feat: add delete, refund, and status templates for Webpay Oneclick Mall promotions --- .../templates/promotions_oneclick_mall/delete.html | 12 ++++++++++++ .../templates/promotions_oneclick_mall/refund.html | 14 ++++++++++++++ .../templates/promotions_oneclick_mall/status.html | 12 ++++++++++++ 3 files changed, 38 insertions(+) create mode 100644 src/main/resources/templates/promotions_oneclick_mall/delete.html create mode 100644 src/main/resources/templates/promotions_oneclick_mall/refund.html create mode 100644 src/main/resources/templates/promotions_oneclick_mall/status.html diff --git a/src/main/resources/templates/promotions_oneclick_mall/delete.html b/src/main/resources/templates/promotions_oneclick_mall/delete.html new file mode 100644 index 0000000..30225b2 --- /dev/null +++ b/src/main/resources/templates/promotions_oneclick_mall/delete.html @@ -0,0 +1,12 @@ +
+
+

Webpay Oneclick Mall Promociones - Borrar usuario

+

En este paso fundamental, procederemos a eliminar la inscripción del usuario y su medio de pago.

+

Paso 1: Petición

+

Para llevar a cabo la eliminación, necesitas el "username" y el "tbkUser".

+
inscription.delete(tbkUser, username);
+

Paso 2: Respuesta

+

En caso de éxito, Transbank responderá con un status code 204 (No Content), y el SDK no retornará ningún valor.

+

En el caso de que no se encuentre el "username" o el "tbkUser", el SDK lanzará una excepción del tipo TransbankException.

+
+
diff --git a/src/main/resources/templates/promotions_oneclick_mall/refund.html b/src/main/resources/templates/promotions_oneclick_mall/refund.html new file mode 100644 index 0000000..361d9c3 --- /dev/null +++ b/src/main/resources/templates/promotions_oneclick_mall/refund.html @@ -0,0 +1,14 @@ +
+
+

Webpay Oneclick Mall Promociones - Reembolsar

+

En esta etapa, tienes la opción de solicitar el reembolso del monto al titular de la tarjeta.

+

Paso 1 - Petición

+

Para llevar a cabo el reembolso, necesitas la orden de compra, el código de comercio, la orden hija y el monto.

+

En este link podrás ver mayor información sobre las condiciones y casos para anular o reversar transacciones.

+
var resp = transaction.refund(buyOrder, childCommerceCode, childBuyOrder, amount);
+

Paso 2: Respuesta

+

Transbank responderá con el resultado del proceso de reembolso.

+
+ CONSULTAR ESTADO +
+
diff --git a/src/main/resources/templates/promotions_oneclick_mall/status.html b/src/main/resources/templates/promotions_oneclick_mall/status.html new file mode 100644 index 0000000..8c1bc45 --- /dev/null +++ b/src/main/resources/templates/promotions_oneclick_mall/status.html @@ -0,0 +1,12 @@ +
+
+

Webpay Oneclick Mall Promociones - Consultar estado de transacción

+

Puedes solicitar el estado de una transacción hasta 7 días después de su realización. No hay límite de solicitudes durante ese período.

+

Paso 1 - Petición:

+

Para realizar la consulta, necesitarás el buyOrder de la transacción de interés.

+
var response = transaction.status(buyOrder);
+

Paso 2: Respuesta

+

Transbank responderá con la siguiente información.

+
+
+
From 070f7350b45ced2f0ad0b993707e4c43a2d30503 Mon Sep 17 00:00:00 2001 From: victor mendoza Date: Tue, 12 May 2026 18:20:20 -0300 Subject: [PATCH 10/13] style: improve formatting and readability of info_bin.html content --- .../promotions_oneclick_mall/info_bin.html | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/main/resources/templates/promotions_oneclick_mall/info_bin.html b/src/main/resources/templates/promotions_oneclick_mall/info_bin.html index 2bed3fa..ca71de9 100644 --- a/src/main/resources/templates/promotions_oneclick_mall/info_bin.html +++ b/src/main/resources/templates/promotions_oneclick_mall/info_bin.html @@ -1,13 +1,24 @@

Webpay Oneclick Mall Promociones - Consulta servicio de bines

-

Con esta operación puedes consultar el BIN asociado al medio de pago inscrito usando el valor de tbkUser. Si el comercio no tiene habilitado este servicio, la respuesta incluirá un error.

+

+ Con esta operación puedes consultar el BIN asociado al medio de pago + inscrito usando el valor de tbkUser. Si el comercio no tiene + habilitado este servicio, la respuesta incluirá un error. +

Paso 1: Petición

-

Para realizar la consulta, necesitarás el tbkUser obtenido al finalizar la inscripción.

-
var resp = binInfo.queryBin(tbkUser);
+

+ Para realizar la consulta, necesitarás el tbkUser obtenido al + finalizar la inscripción. +

+
var resp = binInfo.queryBin(tbkUser);

Paso 2: Respuesta

-

Transbank responderá con la información del BIN consultado.

+

+ Transbank responderá con la información del BIN consultado. +

From 70900b1c93d51c538193512273428021605811f8 Mon Sep 17 00:00:00 2001 From: victor mendoza Date: Wed, 13 May 2026 13:13:07 -0300 Subject: [PATCH 11/13] feat: update transbank-sdk-java dependency version to 6.1.0 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 7141c09..fd9be83 100644 --- a/pom.xml +++ b/pom.xml @@ -58,7 +58,7 @@ com.github.transbankdevelopers transbank-sdk-java - 6.0.0 + 6.1.0 From 3d9654d86bbc06f263b985d19fcaab640f9e3019 Mon Sep 17 00:00:00 2001 From: victor mendoza Date: Wed, 13 May 2026 13:32:12 -0300 Subject: [PATCH 12/13] feat: update labels in authorize and capture templates for clarity --- .../oneclick_mall_deferred/authorize.html | 8 +- .../oneclick_mall_deferred/capture.html | 133 ++++++++++-------- .../promotions_oneclick_mall/refund.html | 37 ++++- 3 files changed, 107 insertions(+), 71 deletions(-) diff --git a/src/main/resources/templates/oneclick_mall_deferred/authorize.html b/src/main/resources/templates/oneclick_mall_deferred/authorize.html index 221dc6b..3833576 100644 --- a/src/main/resources/templates/oneclick_mall_deferred/authorize.html +++ b/src/main/resources/templates/oneclick_mall_deferred/authorize.html @@ -70,7 +70,7 @@

¡Casi listo!

Código de comercio (tienda): ¡Casi listo!
Orden de compra (tienda): ¡Casi listo!
Código de autorización (tienda): ¡Casi listo!
Monto a capturar (tienda): -
+
+

Webpay Oneclick Mall Diferido - Capturar transacción diferida

-

Webpay Oneclick Mall Diferido - Capturar transacción diferida

+

+ En este paso debemos capturar la transacción para realmente capturar el + dinero que había sido previamente reservado al hacer la transacción. +

-

- En este paso debemos capturar la transacción para realmente capturar el dinero que había sido - previamente reservado al hacer la transacción. -

+

Paso 1: Petición

-

Paso 1: Petición

+

+ Para capturar una transacción necesitaremos el código de comercio de la + tienda, la orden de compra de la tienda, el código de autorización y el + monto a capturar. Se hace de la siguiente manera: +

-

- Para capturar una transacción necesitaremos el código de comercio de la tienda hija, la orden de compra - de la tienda hija, el código de autorización y el monto a capturar. Se hace de la siguiente manera: -

- -

+    

 var options = new WebpayOptions(
     IntegrationCommerceCodes.ONECLICK_MALL_DEFERRED,
     IntegrationApiKeys.WEBPAY,
@@ -25,58 +25,69 @@ 

Paso 1: Petición

var response = transaction.capture(childCommerceCode, childBuyOrder, authorizationCode, amount);
-

Paso 2: Respuesta

- -

- Una vez creada la transacción, recibirás los siguientes datos de respuesta: -

+

Paso 2: Respuesta

-
+

+ Una vez creada la transacción, recibirás los siguientes datos de + respuesta: +

-

¡Transacción Capturada!

-

- Con la transacción capturada, puedes mostrar al usuario una página de éxito de la transacción, - proporcionándole la confirmación de que el proceso se ha completado con éxito. -

-

- Otras Utilidades: Después de confirmar la transacción, considera las siguientes utilidades - adicionales: -

-

- Reembolso: Evalúa la posibilidad de reversar o anular el pago según ciertas condiciones - comerciales. -

-

- Consulta de Estado: Hasta 7 días después de la transacción, puedes consultar su estado para - obtener más detalles. -

+
-
- - - +

¡Transacción Capturada!

+

+ Con la transacción capturada, puedes mostrar al usuario una página de + éxito de la transacción, proporcionándole la confirmación de que el + proceso se ha completado con éxito. +

+

+ Otras Utilidades: Después de confirmar la transacción, + considera las siguientes utilidades adicionales: +

+

+ Reembolso: Evalúa la posibilidad de reversar o anular el + pago según ciertas condiciones comerciales. +

+

+ Consulta de Estado: Hasta 7 días después de la + transacción, puedes consultar su estado para obtener más detalles. +

-
-
- - -
+ + + + -
- - - CONSULTAR ESTADO - -
-
-
+
+
+ + +
-
+
+ + + CONSULTAR ESTADO + +
+
+ +
diff --git a/src/main/resources/templates/promotions_oneclick_mall/refund.html b/src/main/resources/templates/promotions_oneclick_mall/refund.html index 361d9c3..5732feb 100644 --- a/src/main/resources/templates/promotions_oneclick_mall/refund.html +++ b/src/main/resources/templates/promotions_oneclick_mall/refund.html @@ -1,14 +1,39 @@

Webpay Oneclick Mall Promociones - Reembolsar

-

En esta etapa, tienes la opción de solicitar el reembolso del monto al titular de la tarjeta.

+

+ En esta etapa, tienes la opción de solicitar el reembolso del monto al + titular de la tarjeta. +

Paso 1 - Petición

-

Para llevar a cabo el reembolso, necesitas la orden de compra, el código de comercio, la orden hija y el monto.

-

En este link podrás ver mayor información sobre las condiciones y casos para anular o reversar transacciones.

+

+ Para llevar a cabo el reembolso, necesitas la orden de compra, el código + de comercio(tienda), la orden de compra(tienda) y el monto. +

+

+ En + este link + podrás ver mayor información sobre las condiciones y casos para anular o + reversar transacciones. +

var resp = transaction.refund(buyOrder, childCommerceCode, childBuyOrder, amount);

Paso 2: Respuesta

-

Transbank responderá con el resultado del proceso de reembolso.

-
- CONSULTAR ESTADO +

+ Transbank responderá con el resultado del proceso de reembolso. +

+
+ CONSULTAR ESTADO
From 53a8538a4b0c18872bafc7f2c463fd36c5c8ab8b Mon Sep 17 00:00:00 2001 From: victor mendoza Date: Wed, 13 May 2026 14:42:13 -0300 Subject: [PATCH 13/13] feat: refactor navigation maps in PromotionsOneclickMallController for improved readability and maintainability --- .../example/controllers/BaseController.java | 10 ++ .../PromotionsOneclickMallController.java | 108 +++++++----------- 2 files changed, 54 insertions(+), 64 deletions(-) diff --git a/src/main/java/cl/transbank/webpay/example/controllers/BaseController.java b/src/main/java/cl/transbank/webpay/example/controllers/BaseController.java index 2cb4300..7dccba6 100644 --- a/src/main/java/cl/transbank/webpay/example/controllers/BaseController.java +++ b/src/main/java/cl/transbank/webpay/example/controllers/BaseController.java @@ -6,6 +6,8 @@ import com.google.gson.JsonSerializer; import java.math.BigDecimal; +import java.util.LinkedHashMap; +import java.util.Map; import java.util.Random; public abstract class BaseController { @@ -49,4 +51,12 @@ protected String getDisplayableErrorMessage(Exception e) { } return GENERIC_ERROR_MESSAGE; } + + protected static Map navigation(String... entries) { + Map navigation = new LinkedHashMap<>(); + for (int i = 0; i < entries.length; i += 2) { + navigation.put(entries[i], entries[i + 1]); + } + return navigation; + } } diff --git a/src/main/java/cl/transbank/webpay/example/controllers/PromotionsOneclickMallController.java b/src/main/java/cl/transbank/webpay/example/controllers/PromotionsOneclickMallController.java index 3d26a40..fec997c 100644 --- a/src/main/java/cl/transbank/webpay/example/controllers/PromotionsOneclickMallController.java +++ b/src/main/java/cl/transbank/webpay/example/controllers/PromotionsOneclickMallController.java @@ -57,15 +57,31 @@ public class PromotionsOneclickMallController extends BaseController { private static final String USERNAME = "username"; private static final String REQUEST_DATA = "request_data"; - private static final Map NAV_START; - private static final Map NAV_FINISH; - private static final Map NAV_FINISH_RECOVER; - private static final Map NAV_FINISH_REJECTED; - private static final Map NAV_AUTHORIZE; - private static final Map NAV_DELETE; - private static final Map NAV_STATUS; - private static final Map NAV_REFUND; - private static final Map NAV_INFO_BIN; + private static final Map NAV_START = navigation( + REQUEST_KEY, REQUEST, + RESPONSE_KEY, RESPONSE, + "form", "Creación del formulario", + "example", "Ejemplo" + ); + private static final Map NAV_FINISH = navigation( + "data", DATA_KEY, + REQUEST_KEY, REQUEST, + RESPONSE_KEY, RESPONSE, + "authorize", "Autorizar una transacción" + ); + private static final Map NAV_FINISH_RECOVER = navigation("data", DATA_KEY); + private static final Map NAV_FINISH_REJECTED = navigation( + "data", DATA_KEY, + REQUEST_KEY, REQUEST, + RESPONSE_KEY, RESPONSE + ); + private static final Map NAV_AUTHORIZE = navigation( + REQUEST_KEY, REQUEST, + RESPONSE_KEY, RESPONSE, + "done", "Listo" + ); + private static final Map NAV_DELETE = navigation(REQUEST_KEY, REQUEST, RESPONSE_KEY, RESPONSE); + private static final Map NAV_TWO_STEP = navigation(REQUEST_KEY, REQUEST, RESPONSE_KEY, RESPONSE); private static final Map DOTENV = loadDotenv(); @Value("${oneclick.mall.promotions.api-key:}") @@ -80,49 +96,10 @@ public class PromotionsOneclickMallController extends BaseController { @Value("${oneclick.mall.promotions.child2-commerce-code:}") private String child2CommerceCode; - static { - NAV_START = new LinkedHashMap<>(); - NAV_START.put(REQUEST_KEY, REQUEST); - NAV_START.put(RESPONSE_KEY, RESPONSE); - NAV_START.put("form", "Creación del formulario"); - NAV_START.put("example", "Ejemplo"); - - NAV_FINISH = new LinkedHashMap<>(); - NAV_FINISH.put("data", DATA_KEY); - NAV_FINISH.put(REQUEST_KEY, REQUEST); - NAV_FINISH.put(RESPONSE_KEY, RESPONSE); - NAV_FINISH.put("authorize", "Autorizar una transacción"); - - NAV_FINISH_RECOVER = new LinkedHashMap<>(); - NAV_FINISH_RECOVER.put("data", DATA_KEY); - - NAV_FINISH_REJECTED = new LinkedHashMap<>(); - NAV_FINISH_REJECTED.put("data", DATA_KEY); - NAV_FINISH_REJECTED.put(REQUEST_KEY, REQUEST); - NAV_FINISH_REJECTED.put(RESPONSE_KEY, RESPONSE); - - NAV_AUTHORIZE = new LinkedHashMap<>(); - NAV_AUTHORIZE.put(REQUEST_KEY, REQUEST); - NAV_AUTHORIZE.put(RESPONSE_KEY, RESPONSE); - NAV_AUTHORIZE.put("done", "Listo"); - - NAV_DELETE = new LinkedHashMap<>(); - NAV_DELETE.put(REQUEST_KEY, REQUEST); - NAV_DELETE.put(RESPONSE_KEY, RESPONSE); - - NAV_STATUS = new LinkedHashMap<>(); - NAV_STATUS.put(REQUEST_KEY, REQUEST); - NAV_STATUS.put(RESPONSE_KEY, RESPONSE); - - NAV_REFUND = NAV_STATUS; - NAV_INFO_BIN = NAV_STATUS; - } - @GetMapping({"", "/", "/start"}) public String start(HttpServletRequest req, Model model) throws IOException, InscriptionStartException { - model.addAttribute(MODEL_NAVIGATION, NAV_START); - addBreadcrumbs(model, "Iniciar inscripción", "#"); + addPageMetadata(model, NAV_START, "Iniciar inscripción"); String username = "User-" + getRandomNumber(); String email = "user." + getRandomNumber() + "@example.com"; @@ -155,11 +132,10 @@ public String finish(HttpServletRequest req, @RequestParam(name = "TBK_ORDEN_COMPRA", required = false) String ordenCompra, Model model) throws IOException, InscriptionFinishException { - model.addAttribute(MODEL_NAVIGATION, NAV_FINISH); - addBreadcrumbs(model, "Finalizar inscripción", "#"); + addPageMetadata(model, NAV_FINISH, "Finalizar inscripción"); if (ordenCompra != null) { - model.addAttribute(MODEL_NAVIGATION, NAV_FINISH_RECOVER); + addNavigation(model, NAV_FINISH_RECOVER); model.addAttribute(REQUEST_DATA_JSON, toJson(params)); return VIEW_RECOVER_ERROR; } @@ -171,7 +147,7 @@ public String finish(HttpServletRequest req, model.addAttribute(MODEL_RESPONSE_JSON, toJson(resp)); if (resp.getResponseCode() != AUTHORIZED) { - model.addAttribute(MODEL_NAVIGATION, NAV_FINISH_REJECTED); + addNavigation(model, NAV_FINISH_REJECTED); model.addAttribute(REQUEST_DATA_JSON, toJson(params)); return VIEW_REJECTED_ERROR; } @@ -196,8 +172,7 @@ public String delete(@RequestParam String username, @RequestParam("tbk_user") String tbkUser, Model model) throws IOException, InscriptionDeleteException { - model.addAttribute(MODEL_NAVIGATION, NAV_DELETE); - addBreadcrumbs(model, "Eliminar inscripción", "#"); + addPageMetadata(model, NAV_DELETE, "Eliminar inscripción"); getInscription().delete(tbkUser, username); return VIEW_DELETE; } @@ -214,8 +189,7 @@ public String authorize( @RequestParam("child_commerce_installments2") int installments2, Model model) throws IOException, TransactionAuthorizeException { - model.addAttribute(MODEL_NAVIGATION, NAV_AUTHORIZE); - addBreadcrumbs(model, "Autorizar transacción", "#"); + addPageMetadata(model, NAV_AUTHORIZE, "Autorizar transacción"); String buyOrder = "buyOrder_" + getRandomNumber(); String childBuyOrder1 = "childBuyOrder1_" + getRandomNumber(); @@ -235,8 +209,7 @@ public String authorize( @GetMapping("/status") public String status(@RequestParam("buy_order") String buyOrder, Model model) throws IOException, TransactionStatusException { - model.addAttribute(MODEL_NAVIGATION, NAV_STATUS); - addBreadcrumbs(model, "Consultar estado", "#"); + addPageMetadata(model, NAV_TWO_STEP, "Consultar estado"); var resp = getTransaction().status(buyOrder); model.addAttribute(MODEL_RESPONSE_JSON, toJson(resp)); return VIEW_STATUS; @@ -247,10 +220,9 @@ public String refund(@RequestParam("buy_order") String buyOrder, @RequestParam("child_buy_order") String childBuyOrder, @RequestParam("child_commerce_code") String childCommerceCode, @RequestParam double amount, - Model model) + Model model) throws IOException, TransactionRefundException { - model.addAttribute(MODEL_NAVIGATION, NAV_REFUND); - addBreadcrumbs(model, "Reembolso", "#"); + addPageMetadata(model, NAV_TWO_STEP, "Reembolso"); var resp = getTransaction().refund(buyOrder, childCommerceCode, childBuyOrder, amount); model.addAttribute("buy_order", buyOrder); model.addAttribute(MODEL_RESPONSE_JSON, toJson(resp)); @@ -260,8 +232,7 @@ public String refund(@RequestParam("buy_order") String buyOrder, @GetMapping("/info-bin") public String infoBin(@RequestParam("tbk_user") String tbkUser, Model model) throws IOException, QueryBinException { - model.addAttribute(MODEL_NAVIGATION, NAV_INFO_BIN); - addBreadcrumbs(model, "Consulta servicio de bines", "#"); + addPageMetadata(model, NAV_TWO_STEP, "Consulta servicio de bines"); var requestData = Map.of(TBK_USER, tbkUser); var resp = getBinInfo().queryBin(tbkUser); model.addAttribute(REQUEST_DATA, requestData); @@ -277,6 +248,15 @@ public String handleException(Exception e, Model model) { return VIEW_ERROR; } + private void addPageMetadata(Model model, Map navigation, String label) { + addNavigation(model, navigation); + addBreadcrumbs(model, label, "#"); + } + + private void addNavigation(Model model, Map navigation) { + model.addAttribute(MODEL_NAVIGATION, navigation); + } + private void addBreadcrumbs(Model model, String label, String url) { Map breadcrumbs = new LinkedHashMap<>(); breadcrumbs.put("Inicio", "/");