From 405a9596916a7d4fe3f31183788f5cbf8b6662f1 Mon Sep 17 00:00:00 2001 From: Lino Silva Date: Tue, 12 May 2026 22:59:12 +0100 Subject: [PATCH] feat: Deco proteste --- .gitignore | 2 +- index.mjs | 137 ++++++++++++++++++++++++++++++++-------------- package-lock.json | 78 -------------------------- 3 files changed, 96 insertions(+), 121 deletions(-) diff --git a/.gitignore b/.gitignore index ceaea36..ff61950 100644 --- a/.gitignore +++ b/.gitignore @@ -129,4 +129,4 @@ dist .yarn/build-state.yml .yarn/install-state.gz .pnp.* - +.DS_Store diff --git a/index.mjs b/index.mjs index 6c0268c..bea8330 100644 --- a/index.mjs +++ b/index.mjs @@ -10,10 +10,17 @@ const symbols = { BPIDEST2040: { url: "https://www.bancobpi.pt/particulares/poupar-investir/ppr/bpi-destino-ppr-2040", name: "BPI Destino PPR 2040", + type: "bpi", }, BPIDEST2050: { url: "https://www.bancobpi.pt/particulares/poupar-investir/ppr/bpi-destino-ppr-2050", name: "BPI Destino PPR 2050", + type: "bpi", + }, + IE00BK5BQT80: { + url: "https://www.deco.proteste.pt/investe/investimentos/fundos/vanguard-ftse-all-world-ucits-etf-usd-acc", + name: "Vanguard FTSE All-World UCITS ETF USD Acc", + type: "deco", }, }; @@ -38,53 +45,99 @@ async function getLatestPrice(symbol) { // Visit the page and wait until network connections are completed await page.goto(config.url, { waitUntil: "networkidle2" }); - // Interact with the DOM to retrieve the price and date - const [price, date] = await page.evaluate(() => { - const spanTags = document.getElementsByTagName("span"); - const priceSearchText = "ÚLTIMA COTAÇÃO:"; - const dateSearchText = "DATA COTAÇÃO:"; - let priceElementFound; - let dateElementFound; - - for (let i = 0; i < spanTags.length; i++) { - if (spanTags[i].textContent.trim() == priceSearchText) { - priceElementFound = spanTags[i]; - if (priceElementFound && dateElementFound) break; - } - if (spanTags[i].textContent.trim() == dateSearchText) { - dateElementFound = spanTags[i]; - if (priceElementFound && dateElementFound) break; - } - } - - if (!priceElementFound || !dateElementFound) { - throw new Error("Could not find price or date elements"); - } - - return [ - priceElementFound.nextSibling.innerHTML, - dateElementFound.nextSibling.innerHTML, - ]; - }); - - const marketPrice = parseFloat( - price.replace("€", "").trim().replace(",", "."), - ); - - return { - symbol, - name: config.name, - currency: "EUR", - price: marketPrice, - date: date.trim(), - timestamp: new Date().toISOString(), - }; + switch (config.type) { + case "bpi": + return await scrapeBPI(page, symbol, config); + case "deco": + return await scrapeDeco(page, symbol, config); + default: + throw new Error(`Unsupported type: ${config.type}`); + } } finally { // Always close the browser await browser.close(); } } +async function scrapeDeco(page, symbol, config) { + // Interact with the DOM to retrieve the price and date + const [price, date] = await page.evaluate(() => { + const priceElement = document.querySelector( + ".product-points-data__current-value", + ); + const dateElement = document.querySelector( + ".product-points-data__current-value + span", + ); + + if (!priceElement || !dateElement) { + throw new Error("Could not find price or date elements"); + } + + const price = priceElement.textContent + .replace("€", "") + .replace("EUR", "") + .replace(",", ".") + .trim(); + const date = dateElement.textContent.trim(); + + return [price, date]; + }); + + const marketPrice = parseFloat(price); + return { + symbol, + name: config.name, + currency: "EUR", + price: marketPrice, + date, + timestamp: new Date().toISOString(), + }; +} + +async function scrapeBPI(page, symbol, config) { + // Interact with the DOM to retrieve the price and date + const [price, date] = await page.evaluate(() => { + const spanTags = document.getElementsByTagName("span"); + const priceSearchText = "ÚLTIMA COTAÇÃO:"; + const dateSearchText = "DATA COTAÇÃO:"; + let priceElementFound; + let dateElementFound; + + for (let i = 0; i < spanTags.length; i++) { + if (spanTags[i].textContent.trim() == priceSearchText) { + priceElementFound = spanTags[i]; + if (priceElementFound && dateElementFound) break; + } + if (spanTags[i].textContent.trim() == dateSearchText) { + dateElementFound = spanTags[i]; + if (priceElementFound && dateElementFound) break; + } + } + + if (!priceElementFound || !dateElementFound) { + throw new Error("Could not find price or date elements"); + } + + return [ + priceElementFound.nextSibling.innerHTML, + dateElementFound.nextSibling.innerHTML, + ]; + }); + + const marketPrice = parseFloat( + price.replace("€", "").trim().replace(",", "."), + ); + + return { + symbol, + name: config.name, + currency: "EUR", + price: marketPrice, + date: date.trim(), + timestamp: new Date().toISOString(), + }; +} + // API endpoint to get latest price for a symbol app.get("/price/:symbol", async (req, res) => { const { symbol } = req.params; @@ -92,7 +145,7 @@ app.get("/price/:symbol", async (req, res) => { try { console.log(`Fetching price for symbol: ${symbol}`); const priceData = await getLatestPrice(symbol); - console.log(`Success: ${symbol} - €${priceData.price}`); + console.log(`Success: ${symbol} - ${priceData.price}`); res.json(priceData); } catch (error) { console.error(`Error fetching price for ${symbol}:`, error.message); diff --git a/package-lock.json b/package-lock.json index b7f90ac..ac6cfbb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,6 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "axios": "^1.7.7", "dotenv": "^16.4.5", "express": "^5.2.1", "puppeteer": "^23.5.3" @@ -432,19 +431,6 @@ "node": ">=4" } }, - "node_modules/asynckit": { - "version": "0.4.0", - "license": "MIT" - }, - "node_modules/axios": { - "version": "1.7.7", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, "node_modules/b4a": { "version": "1.6.7", "license": "Apache-2.0" @@ -676,16 +662,6 @@ "version": "1.1.4", "license": "MIT" }, - "node_modules/combined-stream": { - "version": "1.0.8", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/concat-map": { "version": "0.0.1", "dev": true, @@ -809,13 +785,6 @@ "node": ">= 14" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -1299,36 +1268,6 @@ "dev": true, "license": "ISC" }, - "node_modules/follow-redirects": { - "version": "1.15.9", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/form-data": { - "version": "4.0.1", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -1808,23 +1747,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/mime-db": { - "version": "1.52.0", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/minimatch": { "version": "3.1.2", "dev": true,