diff --git a/CLAUDE.md b/CLAUDE.md index 4eee4de..c60af23 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,11 +4,11 @@ This file provides guidance to Claude Code when working with code in this reposi ## Project Overview -BTGO Scraper - Playwright automation for extracting account balances and transaction CSVs from Banca Transilvania George (btgo.ro). +BTGO Scraper - Playwright automation for extracting account balances and transaction CSVs from Banca Transilvania George (go.bancatransilvania.ro). **Security Context**: Authorized personal banking automation tool for educational purposes. -**⚠️ CRITICAL**: Docker/headless mode is **BLOCKED by WAF**. ONLY works locally with `HEADLESS=false`. +**CRITICAL**: Docker/headless mode is **BLOCKED by WAF**. ONLY works locally with `HEADLESS=false`. ## Coding Guidelines @@ -54,30 +54,54 @@ login() → handle_2fa_wait() → read_accounts() → download_transactions() - Each card: `h4` (name), `span.text-grayscale-label` (IBAN), `strong.sold` (balance) - Balance format: `"7,223.26 RON"` → parse to float + currency -#### 4. Transaction Download Modal (lines ~312-420) +#### 4. Transaction Download (lines ~529-732) -**State Machine:** +**Flow (2024+ version):** ``` -Account 1: Expand card → Click "Tranzacții" → Download → Back → Modal -Account 2+: Select from modal → [ALREADY on page] → Download → Back +Account 1: Expand card -> Click tranzactii icon -> Select "CSV" -> "Genereaza" -> Download from fba-document-item +Account 2+: Click #selectAccountBtn -> Select by heading name -> "Genereaza" -> Download ``` -**Critical**: After modal selection, you're ALREADY on transactions page. Don't expand/click again. +**Key methods:** +- `_download_first_account()`: Handles first account (expand + select CSV format) +- `_download_subsequent_account()`: Handles accounts 2+ (dropdown selection) +- `_wait_and_download()`: Waits for fba-document-item and downloads -**Modal selectors:** -- Modal: `.modal-content` -- Account buttons: `#accountC14RONCRT{last_10_iban_digits}` -- Example: IBAN `...0637236701` → `#accountC14RONCRT0637236701` +**Account selection strategies (in order):** +1. `get_by_role("heading", name=account_name)` +2. `locator("fba-account-details").filter(has_text=account_name)` +3. `get_by_text(account_name, exact=True)` ### Key Selectors -- Login: `get_by_placeholder("ID de logare")`, `get_by_placeholder("Parola")` -- Post-login: `#accountsBtn`, `goapp.bancatransilvania.ro` domain -- Accounts: `fba-account-details-card`, `.collapse-account-btn`, `.account-transactions-btn` -- Modal: `.modal-content`, `#accountC14RONCRT{iban_digits}` -- CSV: `get_by_role("button", name="CSV")` +**Cookie consent:** +- New (2024+): `get_by_role("button", name="Accept toate")` +- One-time consent: `get_by_text("Am inteles")` -**Update selectors:** `playwright codegen https://btgo.ro --target python` +**Login page:** +- URL: `https://go.bancatransilvania.ro/` +- Login link: `get_by_role("link", name="Login")` +- Username: `get_by_placeholder("ID logare")` (intelligent fallback in `_find_username_field`) +- Password: `get_by_placeholder("Parola")` or `input[type='password']` +- Submit: `get_by_role("button", name="Autentifica-te")` (intelligent fallback in `_find_submit_button`) + +**Post-login:** +- 2FA success indicator: `#accountsBtn` visible and enabled +- Domain: `goapp.bancatransilvania.ro` + +**Accounts:** +- Cards: `fba-account-details-card` +- Expand icon: `.mx-auto .mat-icon svg`, `.collapse-account-btn` +- Transactions button: `fba-account-buttons svg`, `.account-transactions-btn` + +**Transaction download:** +- Account selector: `#selectAccountBtn svg` +- Account in dropdown: `get_by_role("heading", name=account_name)` +- CSV format: `get_by_text("CSV", exact=True)` +- Generate button: `get_by_role("button", name="Genereaza")` +- Download item: `fba-document-item svg`, `fba-document-item path` + +**Update selectors:** `playwright codegen https://go.bancatransilvania.ro --target python` ## Docker Limitation @@ -97,17 +121,19 @@ Account 2+: Select from modal → [ALREADY on page] → Download → Back - **Fix**: Run locally with `HEADLESS=false` ### Transaction Download Timeout -- Modal not detected: Check `.modal-content` selector -- Account ID mismatch: Verify IBAN mapping `#accountC14RONCRT{last_10_digits}` -- For idx > 1: Already on page, don't expand/click +- Check `fba-document-item` selector (wait for document generation) +- Verify `#selectAccountBtn` for account dropdown +- Account selection: verify heading name matches exactly ### 2FA Timeout - Increase `TIMEOUT_2FA_SECONDS` in `.env` - Verify URL redirect to `goapp.bancatransilvania.ro` +- Check for "Am inteles" consent dialog blocking -### "Nu exista card la pozitia X" -- Trying to access cards in modal context -- First account needs expand, subsequent accounts don't +### Account Selection Failed +- Account name might have changed - verify exact match +- Try running `playwright codegen` to see current UI structure +- Check if dropdown opened (`#selectAccountBtn`) ## Exit Codes diff --git a/README.md b/README.md index c2529ad..48e044c 100644 --- a/README.md +++ b/README.md @@ -325,12 +325,8 @@ deployment\windows\scripts\menu.ps1 - Mărește timeout: `TIMEOUT_2FA_SECONDS=180` în `.env` - Verifică notificări activate pe telefon -### Selectors nu funcționează -Site-ul s-a schimbat. Re-generează selectors: -```bash -.venv\Scripts\activate -playwright codegen https://btgo.ro --target python -``` +### Selectors nu functioneaza +Site-ul s-a schimbat. Urmeaza pasii din sectiunea **Inregistrare Manuala cu Playwright** de mai jos. ### Notificări Email nu funcționează - Pentru Gmail: folosește App Password, nu parola normală @@ -343,12 +339,116 @@ playwright codegen https://btgo.ro --target python - Chat ID pentru grupuri trebuie să fie **negativ** (ex: `-1001234567890`) - Asigură-te că botul este în grup +## Inregistrare Manuala cu Playwright (Codegen) + +Cand site-ul BT George isi schimba interfata, trebuie sa reinregistrezi fluxul manual. + +### 1. Porneste Playwright Codegen + +```powershell +# Activeaza venv-ul +.venv\Scripts\Activate.ps1 + +# Porneste codegen +playwright codegen https://go.bancatransilvania.ro --target python +``` + +Se deschid 2 ferestre: +- **Browser** - aici faci actiunile manual +- **Playwright Inspector** - aici vezi codul Python generat + +### 2. Inregistreaza Fluxul + +1. Accept cookies ("Accept toate") +2. Click pe "Login" - se deschide popup +3. Completeaza username si parola +4. Click "Autentifica-te" +5. Asteapta 2FA pe telefon +6. Dupa login, click pe "Conturi" +7. **Pentru primul cont:** + - Expand card (click pe sageata) + - Click pe butonul "Tranzactii" (iconita cu grafic) + - Click pe "CSV" pentru format + - Click pe "Genereaza" + - Click pe documentul generat pentru download +8. **Pentru conturile urmatoare:** + - Click pe `#selectAccountBtn` (dropdown conturi) + - Selecteaza contul din lista + - Click pe "Genereaza" + - Download fisierul + +### 3. Salveaza si Analizeaza Scriptul + +- In Inspector: **Copy** sau **File > Save** +- Compara cu `btgo_scraper.py` si actualizeaza selectorii modificati +- Selectori cheie de verificat: + - Cookie consent: `get_by_role("button", name="...")` + - Username field: `get_by_placeholder("...")` + - Submit button: `get_by_role("button", name="...")` + - Account selector: `#selectAccountBtn` + - Download item: `fba-document-item` + +## Testare Manuala a Scraperului + +### Testare Pas cu Pas + +```powershell +# 1. Activeaza venv +.venv\Scripts\Activate.ps1 + +# 2. Ruleaza scraper-ul +python btgo_scraper.py +``` + +### Verificare Output + +Dupa rulare, verifica: + +```powershell +# Fisiere generate +dir data\ + +# Trebuie sa vezi: +# - solduri_*.csv (solduri toate conturile) +# - solduri_*.json (metadata + solduri) +# - tranzactii_*.csv (cate un fisier per cont) +# - dashboard_*.png (screenshot final) + +# Verifica log-ul pentru erori +type logs\scraper_*.log | Select-String -Pattern "EROARE|ERROR|Exception" +``` + +### Testare Doar Solduri (fara download tranzactii) + +```powershell +# Seteaza variabila temporar +$env:BALANCES_ONLY = "true" +python btgo_scraper.py +``` + +### Debug - Screenshot-uri + +Daca ceva nu merge, verifica screenshot-urile din `data/`: +- `debug_login_popup_*.png` - starea paginii de login +- `debug_dropdown_*.png` - dropdown-ul de selectare conturi (daca esueaza) +- `error_*.png` - screenshot la eroare + +### Testare Notificari (fara scraping) + +```powershell +# Test Telegram +python test_telegram.py + +# Trimite ultimele fisiere manual +python send_notifications.py +``` + ## Securitate -**⚠️ IMPORTANT:** -- NU comite `.env` în git (deja în `.gitignore`) -- NU partaja screenshots/logs - conțin date sensibile -- Șterge fișierele vechi periodic: +**IMPORTANT:** +- NU comite `.env` in git (deja in `.gitignore`) +- NU partaja screenshots/logs - contin date sensibile +- Sterge fisierele vechi periodic: ```bash # Windows diff --git a/btgo_scraper.py b/btgo_scraper.py index 8dd9390..9273e5a 100644 --- a/btgo_scraper.py +++ b/btgo_scraper.py @@ -98,37 +98,75 @@ class BTGoScraper: page: Pagina Playwright pe care sa verifice """ try: - # Verifica daca exista gdprcookie-wrapper - gdpr_wrapper = page.locator(".gdprcookie-wrapper") - if gdpr_wrapper.is_visible(timeout=3000): - logging.info(" GDPR cookie banner detectat") + # Strategii pentru cookie consent (in ordinea probabilitatii) + cookie_strategies = [ + # 1. Noul buton BT (2024+) + ("role", "button", "Accept toate"), + ("role", "button", "Accepta toate"), - # Incearca diverse butoane de accept (in ordinea probabilitatii) - accept_selectors = [ - ".gdprcookie-wrapper button:has-text('Accept')", - ".gdprcookie-wrapper button:has-text('Accepta')", - ".gdprcookie-wrapper button:has-text('Sunt de acord')", - ".gdprcookie-wrapper button:has-text('OK')", - ".gdprcookie-wrapper .gdprcookie-buttons button:first-child", - ".gdprcookie-wrapper button", - ] + # 2. Vechiul GDPR wrapper + ("css", ".gdprcookie-wrapper button:has-text('Accept')"), + ("css", ".gdprcookie-wrapper button:has-text('Sunt de acord')"), + ("css", ".gdprcookie-wrapper button"), - for selector in accept_selectors: - try: - accept_btn = page.locator(selector).first - if accept_btn.is_visible(timeout=1000): - accept_btn.click() - logging.info(f" [OK] Cookies acceptate (selector: {selector})") - time.sleep(1) # Asteapta sa dispara banner-ul - return True - except: - continue + # 3. Fallback generic + ("role", "button", "Accept"), + ("role", "button", "Accepta"), + ("role", "button", "OK"), + ] - logging.warning(" Nu am gasit buton de accept in GDPR wrapper") - return False - except: - logging.info(" Nu exista GDPR cookie banner (sau deja inchis)") + for strategy in cookie_strategies: + try: + if strategy[0] == "role": + btn = page.get_by_role(strategy[1], name=strategy[2]) + else: + btn = page.locator(strategy[2]).first + + if btn.is_visible(timeout=2000): + btn.click() + logging.info(f" [OK] Cookies acceptate ({strategy})") + time.sleep(1) + return True + except: + continue + + logging.info(" Nu exista cookie banner (sau deja acceptat)") return False + except: + logging.info(" Nu exista cookie banner (sau deja acceptat)") + return False + + def _dismiss_one_time_consent(self, page): + """ + Inchide dialoguri one-time (ex: 'Am inteles') daca apar + + Args: + page: Pagina Playwright pe care sa verifice + """ + consent_buttons = [ + ("text", "Am înțeles"), + ("text", "Am inteles"), + ("role", "button", "Am înțeles"), + ("role", "button", "OK"), + ("role", "button", "Continua"), + ] + + for strategy in consent_buttons: + try: + if strategy[0] == "text": + btn = page.get_by_text(strategy[1], exact=True) + elif strategy[0] == "role": + btn = page.get_by_role(strategy[1], name=strategy[2]) + + if btn.is_visible(timeout=2000): + btn.click() + logging.info(f" [OK] Consent inchis ({strategy})") + time.sleep(1) + return True + except: + continue + + return False def _find_username_field(self, page): """ @@ -466,10 +504,14 @@ class BTGoScraper: if accounts_btn.is_visible(timeout=1000): # Verifică că este și clickable (enabled) if accounts_btn.is_enabled(): - logging.info("✓ Autentificare 2FA reusita! (Buton conturi activ)") + logging.info("[OK] Autentificare 2FA reusita! (Buton conturi activ)") time.sleep(2) # Asteapta ca pagina sa se stabilizeze complet # Update page reference la login_page pentru restul operatiilor self.page = self.login_page + + # Inchide dialoguri one-time (ex: "Am inteles") daca apar + self._dismiss_one_time_consent(self.page) + return True except: pass @@ -485,30 +527,19 @@ class BTGoScraper: raise TimeoutError(f"Timeout 2FA dupa {timeout} secunde. Verifica ca ai aprobat pe telefon!") def download_transactions(self, accounts): - """Descarca CSV-uri cu tranzactiile pentru fiecare cont""" + """ + Descarca CSV-uri cu tranzactiile pentru fiecare cont. + + Flux nou (2024+): + 1. Primul cont: expand card -> click tranzactii -> select CSV -> Genereaza -> download + 2. Conturile urmatoare: #selectAccountBtn -> select cont by heading -> Genereaza -> download + """ logging.info("=" * 60) logging.info("Descarcare tranzactii pentru toate conturile...") logging.info("=" * 60) downloaded_files = [] - # IMPORTANT: Collapse toate conturile mai intai - logging.info("Collapse toate conturile...") - all_expanded = self.page.locator(".mat-icon.rotate-90").all() - for expanded_icon in all_expanded: - try: - expanded_icon.click() - time.sleep(0.3) - except: - pass - - time.sleep(1) - logging.info("✓ Toate conturile sunt collapse") - - # Re-gaseste toate cardurile de conturi - all_cards = self.page.locator("fba-account-details-card").all() - logging.info(f"Gasit {len(all_cards)} carduri de conturi") - for idx, account in enumerate(accounts, 1): try: nume_cont = account['nume_cont'] @@ -517,118 +548,19 @@ class BTGoScraper: self._update_progress(f"Descarc tranzactii ({idx}/{len(accounts)})...") logging.info(f"[{idx}/{len(accounts)}] Descarcare tranzactii pentru: {nume_cont}") - # Doar pentru PRIMUL cont trebuie expand + click Tranzacții - # Pentru restul, suntem deja pe pagina de tranzacții (din selectarea din modal) if idx == 1: - # Primul cont - expand și click Tranzacții - if idx - 1 >= len(all_cards): - logging.error(f" ✗ Nu exista card la pozitia {idx-1}") - continue - - card = all_cards[idx - 1] - - # Expand contul (click pe săgeată) - expand_button = card.locator(".collapse-account-btn").first - expand_button.click() - time.sleep(2) # Așteaptă expandare - logging.info(f" Contul expandat") - - # Click pe butonul Tranzacții - try: - transactions_button = card.locator(".account-transactions-btn").first - transactions_button.click() - time.sleep(3) # Așteaptă încărcarea paginii cu tranzacții - logging.info(f" Click pe buton Tranzactii - pagina se incarca...") - except Exception as e: - logging.error(f" ✗ Nu am gasit butonul Tranzactii: {e}") - try: - expand_button.click() - time.sleep(0.5) - except: - pass - continue + # PRIMUL CONT: expand -> click tranzactii -> select CSV + downloaded = self._download_first_account(account) else: - # Conturile 2-5: suntem deja pe pagina de tranzacții (din modal) - logging.info(f" Deja pe pagina tranzactii (selectat din modal)") - time.sleep(2) # Așteaptă stabilizare pagină + # CONTURILE URMATOARE: selecteaza din dropdown -> Genereaza -> download + downloaded = self._download_subsequent_account(account) - # Așteaptă să apară butonul CSV (indica că pagina s-a încărcat) - try: - self.page.wait_for_selector('button:has-text("CSV")', timeout=5000) - logging.info(f" Buton CSV detectat") - except: - logging.warning(f" Timeout asteptand butonul CSV") - - # Click pe butonul CSV și așteaptă download - try: - with self.page.expect_download(timeout=15000) as download_info: - csv_button = self.page.get_by_role("button", name="CSV") - csv_button.click() - logging.info(f" Click pe butonul CSV - astept download...") - - download = download_info.value - - # Salvează fișierul cu un nume descriptiv - timestamp = datetime.now().strftime('%Y-%m-%d_%H-%M-%S') - nume_safe = nume_cont.replace(' ', '_').replace('/', '_') - filename = f"tranzactii_{nume_safe}_{timestamp}.csv" - save_path = Path(self.config.OUTPUT_DIR) / filename - - download.save_as(save_path) - logging.info(f" ✓ Salvat: {save_path}") - - downloaded_files.append({ - 'cont': nume_cont, - 'iban': iban, - 'fisier': str(save_path) - }) - - except Exception as e: - logging.error(f" ✗ Eroare la descarcarea CSV: {e}") - - # Navighează înapoi la lista de conturi - try: - # Click pe butonul back/close (săgeată stânga sau X) - back_button = self.page.locator('button[aria-label="Back"], .back-button, #selectAccountBtn').first - back_button.click() - time.sleep(1.5) - logging.info(f" Navigat inapoi - verific modal...") - except Exception as e: - logging.warning(f" Nu am putut naviga inapoi: {e}") - time.sleep(1) - - # Verifică dacă a apărut modal de selectare cont - try: - modal_visible = self.page.locator('.modal-content').is_visible(timeout=2000) - if modal_visible and idx < len(accounts): - logging.info(f" Modal detectat - selectez contul urmator...") - - # Calculează ID-ul contului următor - next_account = accounts[idx] # idx este 0-indexed pentru next - next_iban = next_account['iban'] - next_iban_digits = ''.join(filter(str.isdigit, next_iban))[-10:] - next_account_id = f"accountC14RONCRT{next_iban_digits}" - - # Click pe contul următor din modal - modal_account = self.page.locator(f'#{next_account_id}').first - modal_account.click() - time.sleep(2) - logging.info(f" ✓ Selectat cont din modal: {next_account['nume_cont']}") - else: - # Nu e modal - e ultima iteratie sau nu a aparut modal - logging.info(f" Nu e modal - continuam normal") - except Exception as e: - logging.warning(f" Eroare verificare modal: {e}") - - # Re-găsește cardurile (pentru flow normal fără modal) - try: - all_cards = self.page.locator("fba-account-details-card").all() - except: - pass + if downloaded: + downloaded_files.append(downloaded) except Exception as e: - logging.error(f" ✗ Eroare la descarcarea tranzactiilor pentru {nume_cont}: {e}") - # Încearcă să navighezi înapoi + logging.error(f" [EROARE] Eroare la descarcarea tranzactiilor pentru {nume_cont}: {e}") + # Incearca sa revii la o stare stabila try: self.page.keyboard.press("Escape") time.sleep(1) @@ -637,11 +569,279 @@ class BTGoScraper: continue logging.info("=" * 60) - logging.info(f"✓ Descarcate {len(downloaded_files)}/{len(accounts)} fisiere CSV cu tranzactii") + logging.info(f"[OK] Descarcate {len(downloaded_files)}/{len(accounts)} fisiere CSV cu tranzactii") logging.info("=" * 60) return downloaded_files + def _download_first_account(self, account): + """ + Descarca tranzactii pentru primul cont. + Flow: expand card (daca nu e deja) -> click buton tranzactii -> select CSV -> Genereaza -> download + """ + nume_cont = account['nume_cont'] + iban = account['iban'] + + try: + # Gaseste primul card + first_card = self.page.locator("fba-account-details-card").first + time.sleep(1) + + # Verifica daca butonul de tranzactii e DEJA vizibil (cont expandat) + transactions_btn = first_card.locator("fba-account-buttons svg, fba-account-buttons, .account-transactions-btn").first + is_already_expanded = False + + try: + is_already_expanded = transactions_btn.is_visible(timeout=2000) + except: + pass + + if is_already_expanded: + logging.info(" Contul deja expandat - skip expand click") + else: + # Click pe expand icon (sageata din card) + logging.info(" Contul collapsed - expandez...") + expand_icon = first_card.locator(".mx-auto .mat-icon svg, .collapse-account-btn").first + expand_icon.click() + time.sleep(2) + logging.info(" Contul expandat") + + # Click pe butonul de tranzactii (NU pe delete/inchide cont!) + # Butonul corect are: clasa .account-transactions-btn, SVG cu documentChartList,

cu "Tranzactii" + transactions_btn = None + + # Strategie 1 (PRINCIPALA): container cu clasa account-transactions-btn + try: + btn = first_card.locator(".account-transactions-btn").first + if btn.is_visible(timeout=2000): + transactions_btn = btn + logging.info(" Buton tranzactii gasit prin .account-transactions-btn") + except: + pass + + # Strategie 2: SVG cu data-mat-icon-name="documentChartList" + if not transactions_btn: + try: + btn = first_card.locator("mat-icon[data-mat-icon-name='documentChartList']").first + if btn.is_visible(timeout=1000): + transactions_btn = btn + logging.info(" Buton tranzactii gasit prin documentChartList icon") + except: + pass + + # Strategie 3: element care contine

cu text "Tranzactii" + if not transactions_btn: + try: + btn = first_card.locator("div:has(p:text('Tranzacții')), div:has(p:text('Tranzactii'))").first + if btn.is_visible(timeout=1000): + transactions_btn = btn + logging.info(" Buton tranzactii gasit prin

text") + except: + pass + + # Strategie 4: click direct pe textul "Tranzactii" din card + if not transactions_btn: + try: + btn = first_card.get_by_text("Tranzacții", exact=True) + if btn.is_visible(timeout=1000): + transactions_btn = btn + logging.info(" Buton tranzactii gasit prin get_by_text") + except: + pass + + if not transactions_btn: + raise Exception("Nu am gasit butonul de tranzactii!") + + transactions_btn.click() + time.sleep(3) + logging.info(" Pagina tranzactii se incarca...") + + # Selecteaza format CSV (click pe text "CSV") + csv_option = self.page.get_by_text("CSV", exact=True) + csv_option.click() + time.sleep(1) + logging.info(" Format CSV selectat") + + # Click pe butonul Genereaza + generate_btn = self.page.get_by_role("button", name="Generează") + generate_btn.click() + time.sleep(2) + logging.info(" Click Genereaza - astept generare...") + + # Asteapta si descarca fisierul + return self._wait_and_download(nume_cont, iban) + + except Exception as e: + logging.error(f" [EROARE] Download primul cont: {e}") + return None + + def _download_subsequent_account(self, account): + """ + Descarca tranzactii pentru conturile 2+. + Flow: click #selectAccountBtn -> select cont by heading -> Genereaza -> download + """ + nume_cont = account['nume_cont'] + iban = account['iban'] + + try: + # Click pe butonul de selectare cont (#selectAccountBtn) + select_btn = self.page.locator("#selectAccountBtn svg, #selectAccountBtn").first + select_btn.click() + time.sleep(2) + logging.info(" Dropdown conturi deschis") + + # Debug: listeaza toate heading-urile vizibile din dropdown + try: + headings = self.page.locator("fba-account-details h4, .account-name, h4").all() + visible_names = [] + for h in headings[:10]: # Max 10 + try: + if h.is_visible(timeout=500): + visible_names.append(h.inner_text().strip()) + except: + pass + if visible_names: + logging.info(f" Conturi in dropdown: {visible_names}") + except: + pass + + # Selecteaza contul dupa nume - strategii multiple + account_selected = False + + # Strategie 1: heading cu numele exact + try: + heading = self.page.get_by_role("heading", name=nume_cont, exact=True) + if heading.is_visible(timeout=2000): + heading.click() + account_selected = True + logging.info(f" Cont selectat prin heading exact: {nume_cont}") + except Exception as e: + logging.debug(f" Heading exact failed: {e}") + + # Strategie 2: heading cu numele partial (fara exact match) + if not account_selected: + try: + heading = self.page.get_by_role("heading", name=nume_cont) + if heading.is_visible(timeout=2000): + heading.click() + account_selected = True + logging.info(f" Cont selectat prin heading partial: {nume_cont}") + except Exception as e: + logging.debug(f" Heading partial failed: {e}") + + # Strategie 3: fba-account-details cu has_text + if not account_selected: + try: + account_item = self.page.locator("fba-account-details").filter(has_text=nume_cont).first + if account_item.is_visible(timeout=2000): + account_item.click() + account_selected = True + logging.info(f" Cont selectat prin fba-account-details: {nume_cont}") + except Exception as e: + logging.debug(f" fba-account-details failed: {e}") + + # Strategie 4: locator h4 care contine textul + if not account_selected: + try: + h4_elem = self.page.locator(f"h4:has-text('{nume_cont}')").first + if h4_elem.is_visible(timeout=2000): + h4_elem.click() + account_selected = True + logging.info(f" Cont selectat prin h4:has-text: {nume_cont}") + except Exception as e: + logging.debug(f" h4:has-text failed: {e}") + + # Strategie 5: orice element cu textul contului + if not account_selected: + try: + text_elem = self.page.get_by_text(nume_cont, exact=True) + if text_elem.is_visible(timeout=2000): + text_elem.click() + account_selected = True + logging.info(f" Cont selectat prin text exact: {nume_cont}") + except Exception as e: + logging.debug(f" text exact failed: {e}") + + # Strategie 6: text partial match + if not account_selected: + try: + text_elem = self.page.get_by_text(nume_cont) + if text_elem.is_visible(timeout=2000): + text_elem.click() + account_selected = True + logging.info(f" Cont selectat prin text partial: {nume_cont}") + except Exception as e: + logging.debug(f" text partial failed: {e}") + + if not account_selected: + logging.error(f" [EROARE] Nu am putut selecta contul: {nume_cont}") + # Screenshot pentru debug + try: + debug_path = Path(self.config.OUTPUT_DIR) / f"debug_dropdown_{nume_cont.replace(' ', '_')}_{datetime.now().strftime('%H%M%S')}.png" + self.page.screenshot(path=str(debug_path)) + logging.error(f" Screenshot debug salvat: {debug_path}") + except: + pass + return None + + time.sleep(2) + + # Click pe butonul Genereaza (CSV deja selectat de la primul cont) + generate_btn = self.page.get_by_role("button", name="Generează") + generate_btn.click() + time.sleep(2) + logging.info(" Click Genereaza - astept generare...") + + # Asteapta si descarca fisierul + return self._wait_and_download(nume_cont, iban) + + except Exception as e: + logging.error(f" [EROARE] Download cont ulterior {nume_cont}: {e}") + return None + + def _wait_and_download(self, nume_cont, iban, timeout=20000): + """ + Asteapta generarea fisierului si il descarca. + + Args: + nume_cont: Numele contului (pentru filename) + iban: IBAN-ul contului + timeout: Timeout pentru download (ms) + + Returns: + Dict cu informatii despre fisierul descarcat sau None + """ + try: + # Asteapta sa apara fba-document-item (indica ca fisierul e gata) + self.page.wait_for_selector("fba-document-item", timeout=timeout) + logging.info(" Document generat - descarc...") + + # Click pe document item pentru a descarca + with self.page.expect_download(timeout=timeout) as download_info: + download_btn = self.page.locator("fba-document-item svg, fba-document-item path").first + download_btn.click() + + download = download_info.value + + # Salveaza fisierul cu nume descriptiv + timestamp = datetime.now().strftime('%Y-%m-%d_%H-%M-%S') + nume_safe = nume_cont.replace(' ', '_').replace('/', '_').replace('\\', '_') + filename = f"tranzactii_{nume_safe}_{timestamp}.csv" + save_path = Path(self.config.OUTPUT_DIR) / filename + + download.save_as(save_path) + logging.info(f" [OK] Salvat: {save_path}") + + return { + 'cont': nume_cont, + 'iban': iban, + 'fisier': str(save_path) + } + + except Exception as e: + logging.error(f" [EROARE] Download fisier: {e}") + return None + def read_accounts(self): """Extrage soldurile tuturor conturilor""" logging.info("Citire conturi si solduri...")