-
Notifications
You must be signed in to change notification settings - Fork 0
Feat/tx complete mall #20
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
8f5a541
feat: add TransaccionCompletaMallController for handling mall transac…
victormendoza96 5bef76b
feat: add loading button styles and functionality for form submissions
victormendoza96 c0a189b
feat: create MallDetailSession class for handling mall transaction de…
victormendoza96 f4de6ec
feat: remove error logging in exception handler of TransaccionComplet…
victormendoza96 da5d5d3
feat: add form loading script to layout for improved user experience
victormendoza96 60cb21f
feat: refine text and formatting in transaction form for clarity and …
victormendoza96 c6b6a8a
feat: add transaction form for mall payments with card details and va…
victormendoza96 2e37190
feat: add create transaction page for mall payments with detailed ste…
victormendoza96 463bbe9
feat: add installments consultation page for mall transactions with f…
victormendoza96 10765ee
feat: add confirmation page for mall transactions with request and re…
victormendoza96 66c7cc2
feat: add refund page for mall transactions with request and response…
victormendoza96 661e477
feat: add status page for mall transactions with request and response…
victormendoza96 33b3209
feat: remove unnecessary default case in navigation switch statement
victormendoza96 bfce824
feat: simplify navigation label creation using switch expression
victormendoza96 c8b7700
feat: update transaction commit details to use optional parameters
victormendoza96 d2fc83d
feat: add MallFullTransaction instance variable to TransaccionComplet…
victormendoza96 6021978
feat: add CSRF token input to mall transaction forms and fix label fo…
victormendoza96 4354dd3
feat: improve formatting and readability of the transaction form HTML
victormendoza96 8e777a4
feat: fix label formatting for optional fields in installments and tr…
victormendoza96 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
282 changes: 282 additions & 0 deletions
282
src/main/java/cl/transbank/webpay/example/controllers/TransaccionCompletaMallController.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,282 @@ | ||
| 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.transaccioncompleta.MallFullTransaction; | ||
| import cl.transbank.webpay.transaccioncompleta.model.MallTransactionCommitDetails; | ||
| import cl.transbank.webpay.transaccioncompleta.responses.MallFullTransactionInstallmentsDetails; | ||
| import cl.transbank.webpay.example.models.MallDetailSession; | ||
| 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") | ||
| public class TransaccionCompletaMallController extends BaseController { | ||
| private static final String TEMPLATE_FOLDER = "transaccion_completa_mall"; | ||
| private static final String BASE_URL = "/transaccion-completa-mall"; | ||
| private static final String PRODUCT = "Webpay Transacción Completa Mall"; | ||
|
|
||
| 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_ERROR = "error"; | ||
| private static final String ATTR_ID_QUERY_INSTALLMENTS = "id_query_installments"; | ||
|
|
||
| private static final String SESSION_DETAILS = "transaccion_completa_mall_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_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 TransaccionCompletaMallController() { | ||
| this.tx = new MallFullTransaction( | ||
| new WebpayOptions( | ||
| IntegrationCommerceCodes.TRANSACCION_COMPLETA_MALL, | ||
| 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_CHILD1, | ||
| IntegrationCommerceCodes.TRANSACCION_COMPLETA_MALL_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("/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; | ||
| } | ||
| } | ||
29 changes: 29 additions & 0 deletions
29
src/main/java/cl/transbank/webpay/example/models/MallDetailSession.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| package cl.transbank.webpay.example.models; | ||
|
|
||
| import java.io.Serializable; | ||
|
|
||
| public class MallDetailSession implements Serializable { | ||
| private static final long serialVersionUID = 1L; | ||
|
|
||
| private final double amount; | ||
| private final String commerceCode; | ||
| private final String buyOrder; | ||
|
|
||
| public MallDetailSession(double amount, String commerceCode, String buyOrder) { | ||
| this.amount = amount; | ||
| this.commerceCode = commerceCode; | ||
| this.buyOrder = buyOrder; | ||
| } | ||
|
|
||
| public double getAmount() { | ||
| return amount; | ||
| } | ||
|
|
||
| public String getCommerceCode() { | ||
| return commerceCode; | ||
| } | ||
|
|
||
| public String getBuyOrder() { | ||
| return buyOrder; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| document.addEventListener('DOMContentLoaded', function () { | ||
| const forms = document.querySelectorAll('[data-loading-form="true"]'); | ||
| const buttons = document.querySelectorAll('[data-loading-button="true"]'); | ||
|
|
||
| forms.forEach(function (form) { | ||
| form.addEventListener('submit', function () { | ||
| buttons.forEach(function (btn) { | ||
| btn.disabled = true; | ||
| btn.classList.add('is-disabled'); | ||
| }); | ||
|
|
||
| const btn = form.querySelector('[data-loading-button="true"]'); | ||
| if (btn) { | ||
| btn.classList.add('is-loading'); | ||
| } | ||
| }); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.