v1.0.11 - Plugin BCV Exchange Rate para iDempiere v10

This commit is contained in:
2026-07-03 15:00:31 -04:00
commit e9c19b4b3b
16 changed files with 1700 additions and 0 deletions
@@ -0,0 +1,276 @@
package com.venezuela.bcvrate.service;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
public class BCVApiService {
private static final Logger log = Logger.getLogger(BCVApiService.class.getName());
private static final String BASE_URL = "https://bcv.today/api/v1";
private static final String BCV_WEBSITE = "https://www.bcv.org.ve/";
private static final int CONNECTION_TIMEOUT = 15000;
private static final int READ_TIMEOUT = 30000;
private static final int MAX_DAYS_BACK = 10;
public BCVRateResponse getRateForDateWithFallback(String requestedDate) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Calendar cal = Calendar.getInstance();
try {
cal.setTime(sdf.parse(requestedDate));
} catch (Exception e) {
log.log(Level.WARNING, "Error parseando fecha: " + requestedDate, e);
return null;
}
for (int i = 0; i <= MAX_DAYS_BACK; i++) {
String dateToQuery = sdf.format(cal.getTime());
BCVRateResponse rate = getRateFromApi(dateToQuery);
if (rate != null && rate.isValid()) {
log.info("Tasa encontrada (API) consultando " + dateToQuery
+ " para fecha solicitada " + requestedDate
+ ": USD=" + rate.getDollar()
+ " effective=" + rate.getDate());
return new BCVRateResponse(rate.getDollar(), requestedDate, rate.getDate());
}
cal.add(Calendar.DAY_OF_MONTH, -1);
}
log.info("API no tiene tasa para " + requestedDate + ", intentando scraping BCV...");
BCVRateResponse websiteRate = getRateFromBCVWebsite();
if (websiteRate != null && websiteRate.isValid()) {
log.info("Tasa obtenida del sitio web BCV: USD=" + websiteRate.getDollar()
+ " effective=" + websiteRate.getDate());
return new BCVRateResponse(websiteRate.getDollar(), requestedDate, websiteRate.getDate());
}
log.info("No se encontro tasa en los ultimos " + MAX_DAYS_BACK + " dias para: " + requestedDate);
return null;
}
public BCVRateResponse getRateForDate(String date) {
return getRateFromApi(date);
}
public BCVRateResponse getLatestRate() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
for (int i = 1; i <= 10; i++) {
Calendar cal = Calendar.getInstance();
cal.add(Calendar.DAY_OF_MONTH, i);
String futureDate = sdf.format(cal.getTime());
BCVRateResponse rate = getRateFromApi(futureDate);
if (rate != null && rate.isValid()) {
log.info("Tasa encontrada (API futura) para " + futureDate
+ ": USD=" + rate.getDollar() + " effective=" + rate.getDate());
return rate;
}
}
log.info("API no tiene tasa futura, intentando scraping BCV...");
try {
BCVRateResponse websiteRate = getRateFromBCVWebsite();
if (websiteRate != null && websiteRate.isValid()) {
log.info("Tasa obtenida del sitio web BCV: USD=" + websiteRate.getDollar()
+ " effective=" + websiteRate.getDate());
return websiteRate;
}
log.info("Scraping BCV no devolvio tasa valida");
} catch (Exception e) {
log.log(Level.WARNING, "Scraping BCV fallo", e);
}
log.info("Fallback a API actual...");
BCVRateResponse apiRate = getCurrentRate();
return apiRate;
}
private BCVRateResponse getRateFromApi(String date) {
String urlStr = BASE_URL + "/history/" + date + ".json";
try {
String response = httpGet(urlStr);
if (response == null) return null;
BigDecimal dollar = extractUSD(response);
String effectiveDate = extractField(response, "effective_date");
if (effectiveDate == null) effectiveDate = extractField(response, "date");
if (dollar != null && effectiveDate != null) {
return new BCVRateResponse(dollar, effectiveDate);
}
return null;
} catch (Exception e) {
log.log(Level.WARNING, "Error consultando fecha: " + date, e);
return null;
}
}
public BCVRateResponse getCurrentRate() {
String urlStr = BASE_URL + "/rate.json";
try {
String response = httpGet(urlStr);
if (response == null) return null;
BigDecimal dollar = extractUSD(response);
String effectiveDate = extractField(response, "effective_date");
if (effectiveDate == null) effectiveDate = extractField(response, "date");
if (dollar != null && effectiveDate != null) {
log.info("Tasa actual (API): USD=" + dollar + " effective=" + effectiveDate);
return new BCVRateResponse(dollar, effectiveDate);
}
return null;
} catch (Exception e) {
log.log(Level.WARNING, "Error consultando tasa actual", e);
return null;
}
}
private BCVRateResponse getRateFromBCVWebsite() {
try {
log.info("Iniciando scraping de BCV website...");
String html = httpGetHtml(BCV_WEBSITE);
if (html == null) {
log.warning("No se pudo obtener pagina BCV (httpGetHtml returned null)");
return null;
}
log.info("Pagina BCV obtenida, longitud: " + html.length());
Matcher rateMatcher = Pattern.compile(
"USD</span>.*?strong-tb\">\\s*([\\d.,]+)\\s*</strong>", Pattern.DOTALL).matcher(html);
if (!rateMatcher.find()) {
log.warning("No se encontro tasa USD en pagina BCV (regex no match)");
return null;
}
String rateStr = rateMatcher.group(1).replace(",", ".");
log.info("Tasa encontrada en HTML: " + rateStr);
BigDecimal dollar = new BigDecimal(rateStr).setScale(4, RoundingMode.HALF_UP);
Matcher dateMatcher = Pattern.compile(
"content=\"(\\d{4}-\\d{2}-\\d{2})T").matcher(html);
String effectiveDate = null;
if (dateMatcher.find()) {
effectiveDate = dateMatcher.group(1);
}
if (dollar.compareTo(BigDecimal.ZERO) > 0) {
if (effectiveDate == null) {
effectiveDate = new SimpleDateFormat("yyyy-MM-dd").format(new java.util.Date());
}
log.info("Scraping BCV exitoso: USD=" + dollar + " effective=" + effectiveDate);
return new BCVRateResponse(dollar, effectiveDate);
}
return null;
} catch (Exception e) {
log.log(Level.WARNING, "Error scraping BCV website: " + e.getMessage(), e);
return null;
}
}
private BigDecimal extractUSD(String json) {
Matcher m = Pattern.compile("\"USD\"\\s*:\\s*([0-9.]+)").matcher(json);
if (m.find()) {
return new BigDecimal(m.group(1)).setScale(4, RoundingMode.HALF_UP);
}
return null;
}
private String extractField(String json, String field) {
Matcher m = Pattern.compile("\"" + field + "\"\\s*:\\s*\"([^\"]+)\"").matcher(json);
if (m.find()) {
return m.group(1);
}
return null;
}
private String httpGet(String urlStr) throws Exception {
URL url = new URL(urlStr);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("Accept", "application/json");
conn.setConnectTimeout(CONNECTION_TIMEOUT);
conn.setReadTimeout(READ_TIMEOUT);
int responseCode = conn.getResponseCode();
if (responseCode != 200) {
conn.disconnect();
return null;
}
StringBuilder sb = new StringBuilder();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(conn.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
} finally {
conn.disconnect();
}
return sb.toString();
}
private String httpGetHtml(String urlStr) throws Exception {
URL url = new URL(urlStr);
TrustManager[] trustAllCerts = new TrustManager[]{
new X509TrustManager() {
public X509Certificate[] getAcceptedIssuers() { return null; }
public void checkClientTrusted(X509Certificate[] certs, String authType) {}
public void checkServerTrusted(X509Certificate[] certs, String authType) {}
}
};
SSLContext sc = SSLContext.getInstance("TLS");
sc.init(null, trustAllCerts, new SecureRandom());
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
conn.setSSLSocketFactory(sc.getSocketFactory());
conn.setHostnameVerifier((hostname, session) -> true);
conn.setRequestMethod("GET");
conn.setRequestProperty("Accept", "text/html");
conn.setConnectTimeout(CONNECTION_TIMEOUT);
conn.setReadTimeout(READ_TIMEOUT);
int responseCode = conn.getResponseCode();
if (responseCode != 200) {
conn.disconnect();
return null;
}
StringBuilder sb = new StringBuilder();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(conn.getInputStream(), "UTF-8"))) {
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
} finally {
conn.disconnect();
}
return sb.toString();
}
}
@@ -0,0 +1,107 @@
package com.venezuela.bcvrate.service;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
/**
* Modelo para la respuesta del historial de tasas de la API BCV.
* Endpoint: GET /rates/history?start_date={start}&end_date={end}
* Respuesta: {"start_date": "...", "end_date": "...", "rates": [...]}
*/
public class BCVHistoryResponse {
private String start_date;
private String end_date;
private List<RateEntry> rates;
public BCVHistoryResponse() {
this.rates = new ArrayList<>();
}
public String getStart_date() {
return start_date;
}
public void setStart_date(String start_date) {
this.start_date = start_date;
}
public String getEnd_date() {
return end_date;
}
public void setEnd_date(String end_date) {
this.end_date = end_date;
}
public List<RateEntry> getRates() {
return rates;
}
public void setRates(List<RateEntry> rates) {
this.rates = rates;
}
/**
* Obtiene la tasa más reciente del historial (primer elemento de la lista).
* La API retorna las tasas ordenadas de más reciente a más antigua.
* @return la tasa más reciente, o null si no hay tasas
*/
public RateEntry getMostRecentRate() {
if (rates != null && !rates.isEmpty()) {
return rates.get(0);
}
return null;
}
/**
* Modelo interno para cada entrada de tasa en el historial.
*/
public static class RateEntry {
private BigDecimal dollar;
private String date;
public RateEntry() {
}
public RateEntry(BigDecimal dollar, String date) {
this.dollar = dollar;
this.date = date;
}
public BigDecimal getDollar() {
return dollar;
}
public void setDollar(BigDecimal dollar) {
this.dollar = dollar;
}
public String getDate() {
return date;
}
public void setDate(String date) {
this.date = date;
}
public boolean isValid() {
return dollar != null
&& dollar.compareTo(BigDecimal.ZERO) > 0
&& date != null
&& !date.isEmpty();
}
@Override
public String toString() {
return "RateEntry{dollar=" + dollar + ", date='" + date + "'}";
}
}
@Override
public String toString() {
return "BCVHistoryResponse{start_date='" + start_date + "', end_date='" + end_date
+ "', rates_count=" + (rates != null ? rates.size() : 0) + "}";
}
}
@@ -0,0 +1,62 @@
package com.venezuela.bcvrate.service;
import java.math.BigDecimal;
public class BCVRateResponse {
private BigDecimal dollar;
private String date;
private String effectiveDate;
public BCVRateResponse() {
}
public BCVRateResponse(BigDecimal dollar, String date) {
this.dollar = dollar;
this.date = date;
this.effectiveDate = date;
}
public BCVRateResponse(BigDecimal dollar, String requestedDate, String effectiveDate) {
this.dollar = dollar;
this.date = requestedDate;
this.effectiveDate = effectiveDate;
}
public BigDecimal getDollar() {
return dollar;
}
public void setDollar(BigDecimal dollar) {
this.dollar = dollar;
}
public String getDate() {
return date;
}
public void setDate(String date) {
this.date = date;
}
public String getEffectiveDate() {
return effectiveDate;
}
public void setEffectiveDate(String effectiveDate) {
this.effectiveDate = effectiveDate;
}
public boolean isValid() {
return dollar != null
&& dollar.compareTo(BigDecimal.ZERO) > 0
&& date != null
&& !date.isEmpty();
}
@Override
public String toString() {
return "BCVRateResponse{dollar=" + dollar + ", date='" + date
+ "', effectiveDate='" + effectiveDate + "'}";
}
}