import puppeteer from "puppeteer"; import express from "express"; import "dotenv/config"; const app = express(); const PORT = process.env.PORT || 3000; // Symbol configuration 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", }, }; // Function to scrape price for a given symbol async function getLatestPrice(symbol) { const config = symbols[symbol]; if (!config) { throw new Error(`Invalid symbol: ${symbol}`); } // Launch the browser const browser = await puppeteer.launch({ args: ["--no-sandbox"], timeout: 10000, }); try { // Open a new tab const page = await browser.newPage(); // Visit the page and wait until network connections are completed await page.goto(config.url, { waitUntil: "networkidle2" }); 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; try { console.log(`Fetching price for symbol: ${symbol}`); const priceData = await getLatestPrice(symbol); console.log(`Success: ${symbol} - ${priceData.price}`); res.json(priceData); } catch (error) { console.error(`Error fetching price for ${symbol}:`, error.message); res.status(400).json({ error: error.message, symbol, }); } }); // Health check endpoint app.get("/health", (req, res) => { res.json({ status: "ok", availableSymbols: Object.keys(symbols) }); }); // Root endpoint with usage instructions app.get("/", (req, res) => { res.json({ message: "BPI Stock Price Scraper API", endpoints: { health: "/health", price: "/price/:symbol", }, availableSymbols: Object.keys(symbols), example: `/price/BPIDEST2040`, }); }); // Start the server app.listen(PORT, () => { console.log(`Server running on http://localhost:${PORT}`); console.log(`Available symbols: ${Object.keys(symbols).join(", ")}`); console.log(`Example: http://localhost:${PORT}/price/BPIDEST2040`); });