Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
ebd6125
feat: add TransaccionCompletaController for full transaction handling
victormendoza96 Feb 18, 2026
8b03e38
feat: update styles for card components and improve CSS formatting
victormendoza96 Feb 18, 2026
3a77a73
feat: add card js
victormendoza96 Feb 18, 2026
ed0670b
feat: add create transaction page with request and response handling
victormendoza96 Feb 18, 2026
52ae836
feat: add transaction complete form with card details input
victormendoza96 Feb 18, 2026
96d6ba1
feat: add transaction confirmation page with request and response det…
victormendoza96 Feb 18, 2026
054aaf2
feat: add installments consultation page with request and response ha…
victormendoza96 Feb 18, 2026
000c13b
feat: add refund transaction page with request and response handling
victormendoza96 Feb 18, 2026
61189dc
feat: add transaction status page with request and response handling
victormendoza96 Feb 18, 2026
4daaf6b
feat: update navigation labels for transaction confirmation and refund
victormendoza96 Feb 18, 2026
7460e2b
feat: replace Random with ThreadLocalRandom for amount generation
victormendoza96 Feb 19, 2026
d4e7a6e
feat: replace ThreadLocalRandom with SecureRandom for amount generation
victormendoza96 Feb 19, 2026
8026794
feat: ensure amount is a double for transaction creation
victormendoza96 Feb 19, 2026
f6dc683
feat: define a constant instead of duplicating string
victormendoza96 Feb 19, 2026
a2b26dc
feat: refactor navigation maps and session attribute handling in Tran…
victormendoza96 Feb 19, 2026
ff2cf2a
feat: add default case to navigation map switch statement in Transacc…
victormendoza96 Feb 19, 2026
6ee86a6
feat: update default case in navigation map switch statement to an em…
victormendoza96 Feb 19, 2026
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,215 @@
package cl.transbank.webpay.example.controllers;

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.exception.*;
import cl.transbank.webpay.transaccioncompleta.FullTransaction;
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.util.LinkedHashMap;
import java.util.Map;
import java.security.SecureRandom;

@Log4j2
@Controller
@RequestMapping("/transaccion-completa")
public class TransaccionCompletaController extends BaseController {
private static final String TEMPLATE_FOLDER = "transaccion_completa";
private static final String BASE_URL = "/transaccion-completa";
private static final String PRODUCT = "Webpay Transacción Completa";

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 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 SESSION_AMOUNT = "transaccion_completa_amount";
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_REFUND = NAV_STATUS;

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

private final FullTransaction tx;

public TransaccionCompletaController() {
this.tx = new FullTransaction(
new WebpayOptions(
IntegrationCommerceCodes.TRANSACCION_COMPLETA,
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();
double amount = 1000.0 + SECURE_RANDOM.nextInt(1001);

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

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

return VIEW_CREATE;
}

@PostMapping("/installments")
public String installments(
@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");

var resp = tx.installments(token, installmentsNumber);
model.addAttribute(ATTR_REQUEST_TOKEN, token);
model.addAttribute(ATTR_RESPONSE_DATA, resp);
model.addAttribute(ATTR_RESPONSE_DATA_JSON, toJson(resp));

return VIEW_INSTALLMENTS;
}

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

Byte deferredPeriodIndex = null;
Boolean gracePeriod = Boolean.FALSE;

var resp = tx.commit(token, idQueryInstallments, deferredPeriodIndex, gracePeriod);
Object amount = req.getSession().getAttribute(SESSION_AMOUNT);
req.getSession().removeAttribute(SESSION_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));

return VIEW_COMMIT;
}

@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("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, amount);
model.addAttribute(ATTR_REQUEST_TOKEN, token);
model.addAttribute(ATTR_RESPONSE_DATA, resp);
model.addAttribute(ATTR_RESPONSE_DATA_JSON, toJson(resp));

return VIEW_REFUND;
}

@ExceptionHandler(Exception.class)
public String handleException(Exception e, Model model) {
log.error("Error inesperado", e);
model.addAttribute(ATTR_ERROR, e.getMessage());
return VIEW_ERROR;
}
}
63 changes: 57 additions & 6 deletions src/main/resources/static/css/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@
--tbk-red: #f6f7f9;
--tbk-table-header: rgb(35 39 47/0.95);
--shadow-elevation: inset 0 0 0 1px hsla(0, 0%, 100%, 0.08);
--white-elevation: 0 0 0 1px hsla(0, 0%, 100%, 0.15),
0px 0.8px 2px rgba(0, 0, 0, 0.032), 0px 2.7px 6.7px rgba(0, 0, 0, 0.048),
0px 12px 30px rgba(0, 0, 0, 0.08);
--white-elevation:
0 0 0 1px hsla(0, 0%, 100%, 0.15), 0px 0.8px 2px rgba(0, 0, 0, 0.032),
0px 2.7px 6.7px rgba(0, 0, 0, 0.048), 0px 12px 30px rgba(0, 0, 0, 0.08);
--tbk-white-red: #ffb9b93b;
}

Expand Down Expand Up @@ -269,6 +269,10 @@ code {
margin-bottom: 16px;
}

.mr-16 {
margin-right: 16px;
}

.tbk-link {
color: var(--tbk-red);
text-decoration: none;
Expand Down Expand Up @@ -651,8 +655,10 @@ code {
display: flex;
flex-direction: column;
border-radius: 4px;
box-shadow: 0px 1px 2px 0px rgba(14, 21, 32, 0.18),
0px 0px 2px 0px rgba(14, 21, 32, 0.12), 0px 0px 2px 0px rgba(0, 0, 0, 0.04);
box-shadow:
0px 1px 2px 0px rgba(14, 21, 32, 0.18),
0px 0px 2px 0px rgba(14, 21, 32, 0.12),
0px 0px 2px 0px rgba(0, 0, 0, 0.04);
color: var(--tbk-black);
top: 104px;
.dark & {
Expand Down Expand Up @@ -807,7 +813,9 @@ code {
line-height: 1.75rem;
font-weight: 700;
color: var(--tbk-grey);
transition: opacity 0.5s ease, transform 0.5s ease;
transition:
opacity 0.5s ease,
transform 0.5s ease;
padding-left: 24px;
}

Expand Down Expand Up @@ -1112,6 +1120,49 @@ code {
}
}

/* tx completa */

.card-inputs-container {
display: flex;
flex-direction: column;
gap: 20px;
margin-bottom: 50px;
margin-top: 70px;
}

.card-split-inputs {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 31px;
}

.card-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
padding-top: 1.5rem;
}

.card-border {
border: 1px solid #c9d0e4;
border-radius: 1px;
margin-bottom: 30px;
width: 100%;
}

.card-footer {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}

.card-wrapper {
z-index: 0;
}

@media (max-width: 1439.98px) {
.body-container,
.body-container.no-nav {
Expand Down
Loading