Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
85 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
15a1556
Merge pull request #17 from TransbankDevelopers/feat/transaction-comp…
victormendoza96 Feb 20, 2026
49cf76f
feat: add TransaccionCompletaDiferidaController for deferred transact…
victormendoza96 Feb 20, 2026
e6fc12b
feat: add form for Transacción Completa Diferida with card input hand…
victormendoza96 Feb 20, 2026
69759c6
feat: add create transaction form for Transacción Completa Diferida w…
victormendoza96 Feb 20, 2026
f7b3d5d
feat: add confirmation step for Transacción Completa Diferida with re…
victormendoza96 Feb 20, 2026
59e3958
feat: add installments consultation step for Transacción Completa Dif…
victormendoza96 Feb 20, 2026
ab5500d
feat: add capture transaction step for Transacción Completa Diferida …
victormendoza96 Feb 20, 2026
ca46e58
feat: add refund step for Transacción Completa Diferida with request …
victormendoza96 Feb 20, 2026
85bf34f
feat: add status page for Transacción Completa Diferida with request …
victormendoza96 Feb 20, 2026
319662d
feat: refactor navigation label creation in Transacción Completa Dife…
victormendoza96 Feb 20, 2026
78e9163
Merge pull request #18 from TransbankDevelopers/feat/transaction-comp…
victormendoza96 Feb 23, 2026
058621f
feat: improve amount formatting in authorization forms and enhance er…
victormendoza96 Mar 12, 2026
70f0e17
feat: enhance DOUBLE_SERIALIZER to handle NaN and infinite values
victormendoza96 Mar 12, 2026
fad5bf0
Merge pull request #19 from TransbankDevelopers/fix/amount-format
victormendoza96 Mar 16, 2026
8f5a541
feat: add TransaccionCompletaMallController for handling mall transac…
victormendoza96 Mar 30, 2026
5bef76b
feat: add loading button styles and functionality for form submissions
victormendoza96 Mar 30, 2026
c0a189b
feat: create MallDetailSession class for handling mall transaction de…
victormendoza96 Mar 30, 2026
f4de6ec
feat: remove error logging in exception handler of TransaccionComplet…
victormendoza96 Mar 30, 2026
da5d5d3
feat: add form loading script to layout for improved user experience
victormendoza96 Mar 30, 2026
60cb21f
feat: refine text and formatting in transaction form for clarity and …
victormendoza96 Mar 30, 2026
c6b6a8a
feat: add transaction form for mall payments with card details and va…
victormendoza96 Mar 30, 2026
2e37190
feat: add create transaction page for mall payments with detailed ste…
victormendoza96 Mar 30, 2026
463bbe9
feat: add installments consultation page for mall transactions with f…
victormendoza96 Mar 30, 2026
10765ee
feat: add confirmation page for mall transactions with request and re…
victormendoza96 Mar 30, 2026
66c7cc2
feat: add refund page for mall transactions with request and response…
victormendoza96 Mar 30, 2026
661e477
feat: add status page for mall transactions with request and response…
victormendoza96 Mar 30, 2026
33b3209
feat: remove unnecessary default case in navigation switch statement
victormendoza96 Mar 30, 2026
bfce824
feat: simplify navigation label creation using switch expression
victormendoza96 Mar 30, 2026
c8b7700
feat: update transaction commit details to use optional parameters
victormendoza96 Mar 30, 2026
d2fc83d
feat: add MallFullTransaction instance variable to TransaccionComplet…
victormendoza96 Mar 31, 2026
6021978
feat: add CSRF token input to mall transaction forms and fix label fo…
victormendoza96 Mar 31, 2026
4354dd3
feat: improve formatting and readability of the transaction form HTML
victormendoza96 Mar 31, 2026
8e777a4
feat: fix label formatting for optional fields in installments and tr…
victormendoza96 Mar 31, 2026
54d0206
Merge pull request #20 from TransbankDevelopers/feat/tx-complete-mall
victormendoza96 Mar 31, 2026
0633144
feat: implement TransaccionCompletaMallDiferidoController for mall de…
victormendoza96 Mar 31, 2026
ae8a367
feat: add transaction form for Mall Completa Diferido with card detai…
victormendoza96 Mar 31, 2026
3a4128e
feat: add create transaction form for Mall Completa Diferido with opt…
victormendoza96 Mar 31, 2026
d8e7714
feat: add installments consultation form for Mall Completa Diferido w…
victormendoza96 Mar 31, 2026
36aee46
feat: add refund process template for Mall Completa Diferido transact…
victormendoza96 Mar 31, 2026
bee83e7
feat: add capture transaction template for Mall Completa Diferido wit…
victormendoza96 Mar 31, 2026
580b5b6
feat: add confirmation template for Mall Completa Diferido transactio…
victormendoza96 Mar 31, 2026
b4225aa
feat: add status template for Mall Completa Diferido transactions
victormendoza96 Mar 31, 2026
18877b1
feat: improve formatting and readability of installments consultation…
victormendoza96 Mar 31, 2026
d6684b8
fix: correct wording in transaction form description for Mall Complet…
victormendoza96 Apr 1, 2026
7ce0bc0
feat: enhance readability and formatting of refund template for Mall …
victormendoza96 Apr 1, 2026
43518b3
feat: improve formatting and readability of installments template for…
victormendoza96 Apr 1, 2026
dd1f12a
Merge pull request #21 from TransbankDevelopers/TBK-178-feat/tx-compl…
victormendoza96 Apr 1, 2026
8c3bf36
feat: add PatpassComercioController and update application properties…
victormendoza96 Apr 22, 2026
927bfe7
feat: add start.html template for Patpass Comercio transaction initia…
victormendoza96 Apr 22, 2026
f868052
feat: add commit.html template for Patpass Comercio registration conf…
victormendoza96 Apr 22, 2026
490d091
feat: add voucher.html template for Patpass Comercio voucher display
victormendoza96 Apr 22, 2026
065f375
feat: improve error handling by providing user-friendly error messages
victormendoza96 Apr 23, 2026
9cd82e5
feat: remove server.forward-headers-strategy property from applicatio…
victormendoza96 Apr 23, 2026
a06ff56
Merge pull request #22 from TransbankDevelopers/TBK-183-feat/patpass-…
victormendoza96 Apr 28, 2026
2447df5
feat: add .env to .gitignore to prevent environment file from being t…
victormendoza96 May 12, 2026
183e4a4
feat: add .env.example file and update README with environment variab…
victormendoza96 May 12, 2026
c347620
feat: add OneClick Mall promotions configuration to application prope…
victormendoza96 May 12, 2026
2fbf6ac
feat: implement PromotionsOneclickMallController for handling Oneclic…
victormendoza96 May 12, 2026
6e39db9
feat: add start.html template for Webpay Oneclick Mall inscription pr…
victormendoza96 May 12, 2026
7388d08
feat: add finish.html template for Webpay Oneclick Mall enrollment co…
victormendoza96 May 12, 2026
f6965ab
feat: add info_bin.html template for Webpay Oneclick Mall BIN consult…
victormendoza96 May 12, 2026
4497e87
feat: add authorize.html template for Webpay Oneclick Mall payment au…
victormendoza96 May 12, 2026
6d25fd5
feat: add delete, refund, and status templates for Webpay Oneclick Ma…
victormendoza96 May 12, 2026
070f735
style: improve formatting and readability of info_bin.html content
victormendoza96 May 12, 2026
70900b1
feat: update transbank-sdk-java dependency version to 6.1.0
victormendoza96 May 13, 2026
3d9654d
feat: update labels in authorize and capture templates for clarity
victormendoza96 May 13, 2026
53a8538
feat: refactor navigation maps in PromotionsOneclickMallController fo…
victormendoza96 May 13, 2026
f8a87b3
Merge pull request #23 from TransbankDevelopers/TBK-225-feat/info-bin
victormendoza96 May 14, 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
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ Test.java
build
gradle
.DS_Store
.env
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
<dependency>
<groupId>com.github.transbankdevelopers</groupId>
<artifactId>transbank-sdk-java</artifactId>
<version>6.0.0</version>
<version>6.1.0</version>
</dependency>

<dependency>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
package cl.transbank.webpay.example.controllers;

import cl.transbank.exception.TransbankException;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonPrimitive;
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 {
private static final String GENERIC_ERROR_MESSAGE =
"Ocurrió un error inesperado al procesar la operación.";

protected static final String VIEW_ERROR = "error/error_page";
protected static final String VIEW_ABORTED_ERROR = "error/webpay/aborted";
Expand All @@ -13,11 +21,42 @@ public abstract class BaseController {
protected static final String VIEW_RECOVER_ERROR = "error/oneclick/recover";
protected static final String VIEW_REJECTED_ERROR = "error/oneclick/rejected";

private static final JsonSerializer<Double> DOUBLE_SERIALIZER = (value, type, ctx) -> {
if (value.isNaN() || value.isInfinite()) {
return new JsonPrimitive(value);
}
return new JsonPrimitive(new BigDecimal(value.toString()).stripTrailingZeros().toPlainString());
};

public String toJson(Object obj) {
return (new GsonBuilder().setPrettyPrinting().create()).toJson(obj);
return new GsonBuilder()
.setPrettyPrinting()
.registerTypeAdapter(Double.class, DOUBLE_SERIALIZER)
.registerTypeAdapter(double.class, DOUBLE_SERIALIZER)
.create()
.toJson(obj);
}

protected String getRandomNumber() {
return String.valueOf(new Random().nextInt(Integer.MAX_VALUE));
}

protected String getDisplayableErrorMessage(Exception e) {
Throwable current = e;
while (current != null) {
if (current instanceof TransbankException && current.getMessage() != null && !current.getMessage().isBlank()) {
return current.getMessage();
}
current = current.getCause();
}
return GENERIC_ERROR_MESSAGE;
}

protected static Map<String, String> navigation(String... entries) {
Map<String, String> navigation = new LinkedHashMap<>();
for (int i = 0; i < entries.length; i += 2) {
navigation.put(entries[i], entries[i + 1]);
}
return navigation;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ public String refund(@RequestParam("buy_order") String buyOrder,
@ExceptionHandler(Exception.class)
public String handleException(Exception e, Model model) {
log.error("Error inesperado", e);
model.addAttribute("error", e.getMessage());
model.addAttribute("error", getDisplayableErrorMessage(e));
return VIEW_ERROR;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ public String capture(
@ExceptionHandler(Exception.class)
public String handleException(Exception e, Model model) {
log.error("Error inesperado", e);
model.addAttribute("error", e.getMessage());
model.addAttribute("error", getDisplayableErrorMessage(e));
return VIEW_ERROR;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
package cl.transbank.webpay.example.controllers;

import cl.transbank.common.IntegrationApiKeys;
import cl.transbank.common.IntegrationCommerceCodes;
import cl.transbank.patpass.PatpassComercio;
import cl.transbank.patpass.responses.PatpassComercioInscriptionStartResponse;
import cl.transbank.patpass.responses.PatpassComercioTransactionStatusResponse;
import cl.transbank.webpay.exception.InscriptionStartException;
import cl.transbank.webpay.exception.TransactionStatusException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import lombok.extern.log4j.Log4j2;
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.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;

@Log4j2
@Controller
@RequestMapping("/patpass-comercio")
public class PatpassComercioController extends BaseController {
private static final String TEMPLATE_FOLDER = "patpass_comercio";
private static final String BASE_URL = "/patpass-comercio";
private static final String PRODUCT = "Patpass Comercio";
private static final String SESSION_TOKEN_KEY = "patpass_j_token";
private static final String DEFAULT_VOUCHER_URL =
"https://pagoautomaticocontarjetasint.transbank.cl/nuevo-ic-rest/tokenVoucherLogin";

private static final String COMMIT_PATH = "/commit";
private static final String VOUCHER_PATH = "/voucher";
private static final String NAVIGATION_ATTR = "navigation";
private static final String VOUCHER_URL = "voucherUrl";
private static final String ERROR_ATTR = "error";
private static final String VIEW_START = TEMPLATE_FOLDER + "/start";
private static final String VIEW_COMMIT = TEMPLATE_FOLDER + COMMIT_PATH;
private static final String VIEW_VOUCHER = TEMPLATE_FOLDER + VOUCHER_PATH;

private static final Map<String, String> NAV_START;
private static final Map<String, String> NAV_COMMIT;
private static final Map<String, String> NAV_VOUCHER;

static {
NAV_START = new LinkedHashMap<>();
NAV_START.put("request", "Petición");
NAV_START.put("response", "Respuesta");
NAV_START.put("form", "Formulario");
NAV_START.put("example", "Ejemplo");

NAV_COMMIT = new LinkedHashMap<>();
NAV_COMMIT.put("data", "Datos recibidos");
NAV_COMMIT.put("request", "Petición");
NAV_COMMIT.put("response", "Respuesta");
NAV_COMMIT.put("form", "Formulario");

NAV_VOUCHER = new LinkedHashMap<>();
NAV_VOUCHER.put("form", "voucher");
}

private final PatpassComercio.Inscription inscription;

public PatpassComercioController() {
this.inscription = PatpassComercio.Inscription.buildForIntegration(
IntegrationCommerceCodes.PATPASS_COMERCIO,
IntegrationApiKeys.PATPASS_COMERCIO
);
}

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

@GetMapping({"", "/"})
public String start(HttpServletRequest request, Model model)
throws IOException, InscriptionStartException {
model.addAttribute(NAVIGATION_ATTR, NAV_START);
addBreadcrumbs(model, null, null);

String returnUrl = request.getRequestURL().toString().replaceAll("/?$", "") + COMMIT_PATH;
String finalUrl = request.getRequestURL().toString().replaceAll("/?$", "") + VOUCHER_PATH;

Map<String, Object> requestData = new LinkedHashMap<>();
requestData.put("serviceId", "Service-" + getRandomNumber());
requestData.put("maxAmount", 100);
requestData.put("returnUrl", returnUrl);
requestData.put("finalUrl", finalUrl);
requestData.put("name", "Isaac");
requestData.put("lastName", "Newton");
requestData.put("secondLastName", "Gonzales");
requestData.put("rut", "11111111-1");
requestData.put("phone", "123456734");
requestData.put("cellPhone", "123456723");
requestData.put("patpassName", "Membresia de cable");
requestData.put("personEmail", "developer@continuum.cl");
requestData.put("commerceEmail", "developer@continuum.cl");
requestData.put("address", "Satelite 101");
requestData.put("city", "Santiago");


PatpassComercioInscriptionStartResponse resp = this.inscription.start(
requestData.get("returnUrl").toString(),
requestData.get("name").toString(),
requestData.get("lastName").toString(),
requestData.get("secondLastName").toString(),
requestData.get("rut").toString(),
requestData.get("serviceId").toString(),
requestData.get("finalUrl").toString(),
Double.valueOf(requestData.get("maxAmount").toString()),
requestData.get("phone").toString(),
requestData.get("cellPhone").toString(),
requestData.get("patpassName").toString(),
requestData.get("personEmail").toString(),
requestData.get("commerceEmail").toString(),
requestData.get("address").toString(),
requestData.get("city").toString()
);

model.addAttribute("request_data", requestData);
model.addAttribute("response_data", resp);
model.addAttribute("response_data_json", toJson(resp));
return VIEW_START;
}

@PostMapping("/commit")
public String commitPost(@RequestParam(name = "j_token", required = false) String jTokenLower,
@RequestParam(name = "J_TOKEN", required = false) String jTokenUpper,
@RequestParam(name = "token", required = false) String token,
HttpServletRequest request,
Model model) {
String jToken = firstNonBlank(jTokenLower, jTokenUpper, token);
if (isBlank(jToken)) {
model.addAttribute(ERROR_ATTR, "No se recibió el token de inscripción (J_TOKEN).");
return VIEW_ERROR;
}

request.getSession().setAttribute(SESSION_TOKEN_KEY, jToken);
return "redirect:" + BASE_URL + COMMIT_PATH;
}

@GetMapping(COMMIT_PATH)
public String commit(@RequestParam(name = "j_token", required = false) String jTokenLower,
@RequestParam(name = "J_TOKEN", required = false) String jTokenUpper,
@RequestParam(name = "token", required = false) String token,
HttpServletRequest request,
Model model)
throws IOException, TransactionStatusException {
String jToken = getIncomingToken(request.getSession(), jTokenLower, jTokenUpper, token);
if (isBlank(jToken)) {
model.addAttribute(ERROR_ATTR, "No se encontró el token de inscripción (J_TOKEN).");
return VIEW_ERROR;
}

model.addAttribute(NAVIGATION_ATTR, NAV_COMMIT);
addBreadcrumbs(model, "Confirmar registro", BASE_URL + COMMIT_PATH);

PatpassComercioTransactionStatusResponse resp = inscription.status(jToken);
request.getSession().setAttribute(SESSION_TOKEN_KEY, jToken);

model.addAttribute("token", jToken);
model.addAttribute("response_data", resp);
model.addAttribute("response_data_json", toJson(resp));
model.addAttribute("response_payload_json", toJson(Map.of(
"authorized", resp.isAuthorized(),
VOUCHER_URL, resp.getVoucherUrl()
)));
model.addAttribute(VOUCHER_URL,
isBlank(resp.getVoucherUrl()) ? DEFAULT_VOUCHER_URL : resp.getVoucherUrl());

return VIEW_COMMIT;
}

@PostMapping(VOUCHER_PATH)
public String voucherPost(@RequestParam(name = "j_token", required = false) String jTokenLower,
@RequestParam(name = "J_TOKEN", required = false) String jTokenUpper,
@RequestParam(name = "tokenComercio", required = false) String tokenComercio,
@RequestParam(name = "token", required = false) String token,
HttpServletRequest request,
Model model) {
String jToken = firstNonBlank(jTokenLower, jTokenUpper, tokenComercio, token);
if (isBlank(jToken)) {
model.addAttribute(ERROR_ATTR, "No se recibió el token de inscripción (J_TOKEN).");
return VIEW_ERROR;
}

request.getSession().setAttribute(SESSION_TOKEN_KEY, jToken);
return "redirect:" + BASE_URL + VOUCHER_PATH;
}

@GetMapping(VOUCHER_PATH)
public String voucher(@RequestParam(name = "j_token", required = false) String jTokenLower,
@RequestParam(name = "J_TOKEN", required = false) String jTokenUpper,
@RequestParam(name = "tokenComercio", required = false) String tokenComercio,
@RequestParam(name = "token", required = false) String token,
HttpServletRequest request,
Model model) {
String jToken = getIncomingToken(request.getSession(), jTokenLower, jTokenUpper, tokenComercio, token);
if (isBlank(jToken)) {
model.addAttribute(ERROR_ATTR, "No se encontró el token de inscripción (J_TOKEN).");
return VIEW_ERROR;
}

model.addAttribute(NAVIGATION_ATTR, NAV_VOUCHER);
addBreadcrumbs(model, "Voucher", BASE_URL + VOUCHER_PATH);
model.addAttribute("token", jToken);
model.addAttribute(VOUCHER_URL, DEFAULT_VOUCHER_URL);

return VIEW_VOUCHER;
}

private String getIncomingToken(HttpSession session, String... values) {
String token = firstNonBlank(values);
if (!isBlank(token)) {
return token;
}
Object sessionToken = session.getAttribute(SESSION_TOKEN_KEY);
return sessionToken == null ? null : sessionToken.toString();
}

private String firstNonBlank(String... values) {
for (String value : values) {
if (!isBlank(value)) {
return value;
}
}
return null;
}

private boolean isBlank(String value) {
return value == null || value.isBlank();
}

@ExceptionHandler(Exception.class)
public String handleException(Exception e, Model model) {
log.error("Error inesperado", e);
model.addAttribute(ERROR_ATTR, getDisplayableErrorMessage(e));
return VIEW_ERROR;
}
}
Loading
Loading