Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,308 @@
package cl.transbank.webpay.example.controllers;

import cl.transbank.common.IntegrationApiKeys;
import cl.transbank.common.IntegrationCommerceCodes;
import cl.transbank.common.IntegrationType;
import cl.transbank.model.MallTransactionCreateDetails;
import cl.transbank.webpay.common.WebpayOptions;
import cl.transbank.webpay.exception.*;
import cl.transbank.webpay.example.models.MallDetailSession;
import cl.transbank.webpay.transaccioncompleta.MallFullTransaction;
import cl.transbank.webpay.transaccioncompleta.model.MallTransactionCommitDetails;
import cl.transbank.webpay.transaccioncompleta.responses.MallFullTransactionInstallmentsDetails;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;

import java.io.IOException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

@Log4j2
@Controller
@RequestMapping("/transaccion-completa-mall-diferido")
public class TransaccionCompletaMallDiferidoController extends BaseController {
private static final String TEMPLATE_FOLDER = "transaccion_completa_mall_diferido";
private static final String BASE_URL = "/transaccion-completa-mall-diferido";
private static final String PRODUCT = "Webpay Transacción Completa Mall Diferido";

private static final String VIEW_INDEX = TEMPLATE_FOLDER + "/index";
private static final String VIEW_CREATE = TEMPLATE_FOLDER + "/create";
private static final String VIEW_INSTALLMENTS = TEMPLATE_FOLDER + "/installments";
private static final String VIEW_COMMIT = TEMPLATE_FOLDER + "/commit";
private static final String VIEW_STATUS = TEMPLATE_FOLDER + "/status";
private static final String VIEW_REFUND = TEMPLATE_FOLDER + "/refund";
private static final String VIEW_CAPTURE = TEMPLATE_FOLDER + "/capture";

private static final String NAV_LABEL_FORM = "Formulario";
private static final String NAV_LABEL_REQUEST = "Petición";
private static final String NAV_LABEL_RESPONSE = "Respuesta";

private static final String ATTR_NAVIGATION = "navigation";
private static final String ATTR_PRODUCT = "product";
private static final String ATTR_BREADCRUMBS = "breadcrumbs";
private static final String ATTR_RESPONSE_DATA = "response_data";
private static final String ATTR_RESPONSE_DATA_JSON = "response_data_json";
private static final String ATTR_REQUEST_TOKEN = "request_token";
private static final String ATTR_AMOUNT = "amount";
private static final String ATTR_ERROR = "error";
private static final String ATTR_ID_QUERY_INSTALLMENTS = "id_query_installments";

private static final String SESSION_DETAILS = "transaccion_completa_mall_diferido_details";
private static final String NAV_KEY_REQUEST = "request";
private static final String NAV_KEY_RESPONSE = "response";
private static final String NAV_KEY_FORM = "form";

private static final SecureRandom SECURE_RANDOM = new SecureRandom();

private static final Map<String, String> NAV_INDEX = createNav(NAV_KEY_FORM);
private static final Map<String, String> NAV_CREATE = createNav(NAV_KEY_REQUEST, NAV_KEY_RESPONSE, NAV_KEY_FORM);
private static final Map<String, String> NAV_INSTALLMENTS = createNav(NAV_KEY_REQUEST, NAV_KEY_RESPONSE, NAV_KEY_FORM);
private static final Map<String, String> NAV_COMMIT = createNav(NAV_KEY_REQUEST, NAV_KEY_RESPONSE, NAV_KEY_FORM);
private static final Map<String, String> NAV_STATUS = createNav(NAV_KEY_REQUEST, NAV_KEY_RESPONSE);
private static final Map<String, String> NAV_CAPTURE = NAV_STATUS;
private static final Map<String, String> NAV_REFUND = NAV_STATUS;
private final MallFullTransaction tx;

private static Map<String, String> createNav(String... keys) {
Map<String, String> nav = new LinkedHashMap<>();
for (String key : keys) {
String label = switch (key) {
case NAV_KEY_REQUEST -> NAV_LABEL_REQUEST;
case NAV_KEY_RESPONSE -> NAV_LABEL_RESPONSE;
case NAV_KEY_FORM -> NAV_LABEL_FORM;
default -> null;
};
if (label != null) {
nav.put(key, label);
}
}
return nav;
}


public TransaccionCompletaMallDiferidoController() {
this.tx = new MallFullTransaction(
new WebpayOptions(
IntegrationCommerceCodes.TRANSACCION_COMPLETA_MALL_DEFERRED,
IntegrationApiKeys.WEBPAY,
IntegrationType.TEST
)
);
}

private void addProductAndBreadcrumbs(Model model, String label, String url) {
var breadcrumbs = new LinkedHashMap<String, String>();
breadcrumbs.put("Inicio", "/");
breadcrumbs.put(PRODUCT, BASE_URL);
if (label != null) {
breadcrumbs.put(label, url);
}
model.addAttribute(ATTR_PRODUCT, PRODUCT);
model.addAttribute(ATTR_BREADCRUMBS, breadcrumbs);
}

@GetMapping("")
public String index(Model model) {
model.addAttribute(ATTR_NAVIGATION, NAV_INDEX);
addProductAndBreadcrumbs(model, null, null);
return VIEW_INDEX;
}

@PostMapping("/create")
public String create(
HttpServletRequest req,
@RequestParam("number") String number,
@RequestParam("expiry") String expiry,
@RequestParam("cvc") String cvc,
Model model
) throws TransactionCreateException, IOException {
model.addAttribute(ATTR_NAVIGATION, NAV_CREATE);
addProductAndBreadcrumbs(model, "Crear transacción", BASE_URL + "/create");

String cardNumber = number.replaceAll("\\s+", "");
String[] expiryParts = expiry.split("/");
String month = expiryParts.length > 0 ? expiryParts[0] : "";
String year = expiryParts.length > 1 ? expiryParts[1] : "";
String cardExpiry = year + "/" + month;

String buyOrder = "O-" + getRandomNumber();
String sessionId = "S-" + getRandomNumber();

var sessionDetails = buildSessionDetails(
IntegrationCommerceCodes.TRANSACCION_COMPLETA_MALL_DEFERRED_CHILD1,
IntegrationCommerceCodes.TRANSACCION_COMPLETA_MALL_DEFERRED_CHILD2
);

var details = MallTransactionCreateDetails.build()
.add(sessionDetails.get(0).getAmount(), sessionDetails.get(0).getCommerceCode(), sessionDetails.get(0).getBuyOrder())
.add(sessionDetails.get(1).getAmount(), sessionDetails.get(1).getCommerceCode(), sessionDetails.get(1).getBuyOrder());

var resp = tx.create(buyOrder, sessionId, cardNumber, cardExpiry, details, Short.parseShort(cvc));
req.getSession().setAttribute(SESSION_DETAILS, sessionDetails);

model.addAttribute(ATTR_RESPONSE_DATA, resp);
model.addAttribute(ATTR_RESPONSE_DATA_JSON, toJson(resp));

return VIEW_CREATE;
}

@PostMapping("/installments")
public String installments(
HttpServletRequest req,
@RequestParam("token") String token,
@RequestParam("installments_number") byte installmentsNumber,
Model model
) throws TransactionInstallmentException, IOException {
model.addAttribute(ATTR_NAVIGATION, NAV_INSTALLMENTS);
addProductAndBreadcrumbs(model, "Consulta de cuotas", BASE_URL + "/installments");

List<MallDetailSession> sessionDetails = getSessionDetails(req);
if (sessionDetails.isEmpty()) {
model.addAttribute(ATTR_ERROR, "Debes crear la transacción antes de consultar cuotas.");
return VIEW_ERROR;
}

var details = MallFullTransactionInstallmentsDetails.build()
.add(sessionDetails.get(0).getCommerceCode(), sessionDetails.get(0).getBuyOrder(), installmentsNumber)
.add(sessionDetails.get(1).getCommerceCode(), sessionDetails.get(1).getBuyOrder(), installmentsNumber);

var resp = tx.installments(token, details);
Long idQueryInstallments = null;
if (resp != null && resp.getResponseList() != null && !resp.getResponseList().isEmpty()) {
idQueryInstallments = resp.getResponseList().get(0).getIdQueryInstallments();
}

model.addAttribute(ATTR_REQUEST_TOKEN, token);
model.addAttribute(ATTR_RESPONSE_DATA, resp);
model.addAttribute(ATTR_RESPONSE_DATA_JSON, toJson(resp));
model.addAttribute(ATTR_ID_QUERY_INSTALLMENTS, idQueryInstallments);

return VIEW_INSTALLMENTS;
}

@GetMapping("/commit")
public String commit(
HttpServletRequest req,
@RequestParam("token") String token,
@RequestParam(value = "idQueryInstallments", required = false) Long idQueryInstallments,
@RequestParam(value = "deferredPeriodIndex", required = false) Byte deferredPeriodIndex,
@RequestParam(value = "gracePeriod", required = false) Boolean gracePeriod,
Model model
) throws TransactionCommitException, IOException {
model.addAttribute(ATTR_NAVIGATION, NAV_COMMIT);
addProductAndBreadcrumbs(model, "Confirmar transacción", BASE_URL + "/commit");

List<MallDetailSession> sessionDetails = getSessionDetails(req);
if (sessionDetails.isEmpty()) {
model.addAttribute(ATTR_ERROR, "Debes crear la transacción antes de confirmar.");
return VIEW_ERROR;
}

boolean safeGracePeriod = gracePeriod != null ? gracePeriod : Boolean.FALSE;

var details = MallTransactionCommitDetails.build()
.add(sessionDetails.get(0).getCommerceCode(), sessionDetails.get(0).getBuyOrder(), idQueryInstallments, deferredPeriodIndex, safeGracePeriod)
.add(sessionDetails.get(1).getCommerceCode(), sessionDetails.get(1).getBuyOrder(), idQueryInstallments, deferredPeriodIndex, safeGracePeriod);

var resp = tx.commit(token, details);

model.addAttribute(ATTR_REQUEST_TOKEN, token);
model.addAttribute(ATTR_RESPONSE_DATA, resp);
model.addAttribute(ATTR_RESPONSE_DATA_JSON, toJson(resp));

return VIEW_COMMIT;
}

@GetMapping("/capture")
public String capture(
@RequestParam("token") String token,
@RequestParam("child_commerce_code") String commerceCode,
@RequestParam("child_buy_order") String buyOrder,
@RequestParam("authorization_code") String authorizationCode,
@RequestParam("amount") double amount,
Model model
) throws TransactionCaptureException, IOException {
model.addAttribute(ATTR_NAVIGATION, NAV_CAPTURE);
addProductAndBreadcrumbs(model, "Capturar", BASE_URL + "/capture");

var resp = tx.capture(token, commerceCode, buyOrder, authorizationCode, amount);
model.addAttribute(ATTR_AMOUNT, amount);
model.addAttribute(ATTR_REQUEST_TOKEN, token);
model.addAttribute(ATTR_RESPONSE_DATA, resp);
model.addAttribute(ATTR_RESPONSE_DATA_JSON, toJson(resp));
model.addAttribute("child_buy_order", buyOrder);
model.addAttribute("child_commerce_code", commerceCode);

return VIEW_CAPTURE;
}

@GetMapping("/status")
public String status(
@RequestParam("token") String token,
Model model
) throws TransactionStatusException, IOException {
model.addAttribute(ATTR_NAVIGATION, NAV_STATUS);
addProductAndBreadcrumbs(model, "Estado de transacción", BASE_URL + "/status");

var resp = tx.status(token);
model.addAttribute(ATTR_RESPONSE_DATA, resp);
model.addAttribute(ATTR_RESPONSE_DATA_JSON, toJson(resp));

return VIEW_STATUS;
}

@GetMapping("/refund")
public String refund(
@RequestParam("token") String token,
@RequestParam("buy_order") String buyOrder,
@RequestParam("commerce_code") String commerceCode,
@RequestParam("amount") double amount,
Model model
) throws TransactionRefundException, IOException {
model.addAttribute(ATTR_NAVIGATION, NAV_REFUND);
addProductAndBreadcrumbs(model, "Reembolsar", BASE_URL + "/refund");

var resp = tx.refund(token, buyOrder, commerceCode, amount);
model.addAttribute(ATTR_REQUEST_TOKEN, token);
model.addAttribute(ATTR_RESPONSE_DATA, resp);
model.addAttribute(ATTR_RESPONSE_DATA_JSON, toJson(resp));

return VIEW_REFUND;
}

private List<MallDetailSession> buildSessionDetails(String commerceCode1, String commerceCode2) {
List<MallDetailSession> details = new ArrayList<>();
details.add(new MallDetailSession(
1000.0 + SECURE_RANDOM.nextInt(1001),
commerceCode1,
"O-" + getRandomNumber()
));
details.add(new MallDetailSession(
1000.0 + SECURE_RANDOM.nextInt(1001),
commerceCode2,
"O-" + getRandomNumber()
));
return details;
}

@SuppressWarnings("unchecked")
private List<MallDetailSession> getSessionDetails(HttpServletRequest req) {
Object value = req.getSession().getAttribute(SESSION_DETAILS);
if (value instanceof List<?>) {
return (List<MallDetailSession>) value;
}
return new ArrayList<>();
}

@ExceptionHandler(Exception.class)
public String handleException(Exception e, Model model) {
model.addAttribute(ATTR_ERROR, e.getMessage());
return VIEW_ERROR;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<div th:replace="~{layout :: layout(~{::content})}">
<div th:fragment="content">
<h1>Transacción Completa Mall Diferido - Capturar transacción</h1>
<p class="mb-32">
En este paso debemos capturar la transacción para realmente capturar el dinero que habia sido previamente reservado al hacer la transacción.
</p>

<h2 id="request">Paso 1: Petición</h2>
<p class="mb-32">
Para capturar una transacción, necesitaremos el Token, la Orden de compra, el Código de autorización y el monto a capturar.
</p>

<pre class="mb-16"><code class="language-java">
import cl.transbank.common.IntegrationApiKeys;
import cl.transbank.common.IntegrationCommerceCodes;
import cl.transbank.common.IntegrationType;
import cl.transbank.webpay.common.WebpayOptions;
import cl.transbank.webpay.transaccioncompleta.MallFullTransaction;

MallFullTransaction tx = new MallFullTransaction(
new WebpayOptions(
IntegrationCommerceCodes.TRANSACCION_COMPLETA_MALL_DEFERRED,
IntegrationApiKeys.WEBPAY,
IntegrationType.TEST
)
);

var resp = tx.capture(token, childCommerceCode, childBuyOrder, authorizationCode, amount);
</code></pre>

<h2 id="response">Paso 2: Respuesta</h2>
<p class="mb-32">Una vez capturada la transacción, recibirás los siguientes datos de respuesta:</p>

<pre class="mb-16"><code class="language-json" th:text="${response_data_json}"></code></pre>

<h2 id="form">Otras utilidades</h2>
<p class="mb-16">
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.
</p>

<p>Después de capturar la transacción, podrás realizar otras operaciones útiles:</p>
<ul class="bullet-list">
<li><span class="fw-700">Reembolsar:</span> Puedes reversar o anular el pago según ciertas condiciones comerciales.</li>
<li><span class="fw-700">Consultar Estado:</span> Hasta 7 días después de realizada la transacción, podrás consultar el estado de la transacción.</li>
</ul>

<form th:action="@{/transaccion-completa-mall-diferido/refund}" method="GET" data-loading-form="true">
<div class="tbk-card">
<div class="input-container mb-32">
<label for="amount" class="tbk-label">Monto a reembolsar</label>
<input type="text" id="amount" name="amount" class="tbk-input-text" th:value="${amount}" />
<input type="hidden" name="token" th:value="${request_token}" />
<input type="hidden" name="buy_order" th:value="${child_buy_order}" />
<input type="hidden" name="commerce_code" th:value="${child_commerce_code}" />
</div>

<div class="tbk-card-footer">
<button type="submit" class="tbk-button primary loading-button" data-loading-button="true">
<span class="loading-spinner" aria-hidden="true"></span>
<span>REEMBOLSAR</span>
</button>
</div>
</div>
</form>

<a th:href="@{/transaccion-completa-mall-diferido/status(token=${request_token})}" class="tbk-button primary mb-32">CONSULTAR ESTADO</a>
</div>
</div>
Loading
Loading