v1.0.11 - Plugin BCV Exchange Rate para iDempiere v10
This commit is contained in:
@@ -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 + "'}";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user