Rescrie complet fluxul de descarcare tranzactii pentru noul UI BT George
Modificari principale: - Noul flux download: expand -> tranzactii -> CSV -> Genereaza -> download - Detectie inteligenta buton Tranzactii (evita butonul Delete) - Verificare daca primul cont e deja expandat inainte de click - Selectie conturi cu 6 strategii fallback + debug logging - Handler pentru cookie consent "Accept toate" si "Am inteles" - Screenshot automat la erori de selectie cont Documentatie: - README: sectiuni noi pentru inregistrare Playwright si testare manuala - CLAUDE.md: selectori actualizati pentru noul UI 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
72
CLAUDE.md
72
CLAUDE.md
@@ -4,11 +4,11 @@ This file provides guidance to Claude Code when working with code in this reposi
|
|||||||
|
|
||||||
## Project Overview
|
## 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.
|
**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
|
## 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)
|
- Each card: `h4` (name), `span.text-grayscale-label` (IBAN), `strong.sold` (balance)
|
||||||
- Balance format: `"7,223.26 RON"` → parse to float + currency
|
- 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 1: Expand card -> Click tranzactii icon -> Select "CSV" -> "Genereaza" -> Download from fba-document-item
|
||||||
Account 2+: Select from modal → [ALREADY on page] → Download → Back
|
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:**
|
**Account selection strategies (in order):**
|
||||||
- Modal: `.modal-content`
|
1. `get_by_role("heading", name=account_name)`
|
||||||
- Account buttons: `#accountC14RONCRT{last_10_iban_digits}`
|
2. `locator("fba-account-details").filter(has_text=account_name)`
|
||||||
- Example: IBAN `...0637236701` → `#accountC14RONCRT0637236701`
|
3. `get_by_text(account_name, exact=True)`
|
||||||
|
|
||||||
### Key Selectors
|
### Key Selectors
|
||||||
|
|
||||||
- Login: `get_by_placeholder("ID de logare")`, `get_by_placeholder("Parola")`
|
**Cookie consent:**
|
||||||
- Post-login: `#accountsBtn`, `goapp.bancatransilvania.ro` domain
|
- New (2024+): `get_by_role("button", name="Accept toate")`
|
||||||
- Accounts: `fba-account-details-card`, `.collapse-account-btn`, `.account-transactions-btn`
|
- One-time consent: `get_by_text("Am inteles")`
|
||||||
- Modal: `.modal-content`, `#accountC14RONCRT{iban_digits}`
|
|
||||||
- CSV: `get_by_role("button", name="CSV")`
|
|
||||||
|
|
||||||
**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
|
## Docker Limitation
|
||||||
|
|
||||||
@@ -97,17 +121,19 @@ Account 2+: Select from modal → [ALREADY on page] → Download → Back
|
|||||||
- **Fix**: Run locally with `HEADLESS=false`
|
- **Fix**: Run locally with `HEADLESS=false`
|
||||||
|
|
||||||
### Transaction Download Timeout
|
### Transaction Download Timeout
|
||||||
- Modal not detected: Check `.modal-content` selector
|
- Check `fba-document-item` selector (wait for document generation)
|
||||||
- Account ID mismatch: Verify IBAN mapping `#accountC14RONCRT{last_10_digits}`
|
- Verify `#selectAccountBtn` for account dropdown
|
||||||
- For idx > 1: Already on page, don't expand/click
|
- Account selection: verify heading name matches exactly
|
||||||
|
|
||||||
### 2FA Timeout
|
### 2FA Timeout
|
||||||
- Increase `TIMEOUT_2FA_SECONDS` in `.env`
|
- Increase `TIMEOUT_2FA_SECONDS` in `.env`
|
||||||
- Verify URL redirect to `goapp.bancatransilvania.ro`
|
- Verify URL redirect to `goapp.bancatransilvania.ro`
|
||||||
|
- Check for "Am inteles" consent dialog blocking
|
||||||
|
|
||||||
### "Nu exista card la pozitia X"
|
### Account Selection Failed
|
||||||
- Trying to access cards in modal context
|
- Account name might have changed - verify exact match
|
||||||
- First account needs expand, subsequent accounts don't
|
- Try running `playwright codegen` to see current UI structure
|
||||||
|
- Check if dropdown opened (`#selectAccountBtn`)
|
||||||
|
|
||||||
## Exit Codes
|
## Exit Codes
|
||||||
|
|
||||||
|
|||||||
120
README.md
120
README.md
@@ -325,12 +325,8 @@ deployment\windows\scripts\menu.ps1
|
|||||||
- Mărește timeout: `TIMEOUT_2FA_SECONDS=180` în `.env`
|
- Mărește timeout: `TIMEOUT_2FA_SECONDS=180` în `.env`
|
||||||
- Verifică notificări activate pe telefon
|
- Verifică notificări activate pe telefon
|
||||||
|
|
||||||
### Selectors nu funcționează
|
### Selectors nu functioneaza
|
||||||
Site-ul s-a schimbat. Re-generează selectors:
|
Site-ul s-a schimbat. Urmeaza pasii din sectiunea **Inregistrare Manuala cu Playwright** de mai jos.
|
||||||
```bash
|
|
||||||
.venv\Scripts\activate
|
|
||||||
playwright codegen https://btgo.ro --target python
|
|
||||||
```
|
|
||||||
|
|
||||||
### Notificări Email nu funcționează
|
### Notificări Email nu funcționează
|
||||||
- Pentru Gmail: folosește App Password, nu parola normală
|
- 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`)
|
- Chat ID pentru grupuri trebuie să fie **negativ** (ex: `-1001234567890`)
|
||||||
- Asigură-te că botul este în grup
|
- 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
|
## Securitate
|
||||||
|
|
||||||
**⚠️ IMPORTANT:**
|
**IMPORTANT:**
|
||||||
- NU comite `.env` în git (deja în `.gitignore`)
|
- NU comite `.env` in git (deja in `.gitignore`)
|
||||||
- NU partaja screenshots/logs - conțin date sensibile
|
- NU partaja screenshots/logs - contin date sensibile
|
||||||
- Șterge fișierele vechi periodic:
|
- Sterge fisierele vechi periodic:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Windows
|
# Windows
|
||||||
|
|||||||
508
btgo_scraper.py
508
btgo_scraper.py
@@ -98,37 +98,75 @@ class BTGoScraper:
|
|||||||
page: Pagina Playwright pe care sa verifice
|
page: Pagina Playwright pe care sa verifice
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Verifica daca exista gdprcookie-wrapper
|
# Strategii pentru cookie consent (in ordinea probabilitatii)
|
||||||
gdpr_wrapper = page.locator(".gdprcookie-wrapper")
|
cookie_strategies = [
|
||||||
if gdpr_wrapper.is_visible(timeout=3000):
|
# 1. Noul buton BT (2024+)
|
||||||
logging.info(" GDPR cookie banner detectat")
|
("role", "button", "Accept toate"),
|
||||||
|
("role", "button", "Accepta toate"),
|
||||||
|
|
||||||
# Incearca diverse butoane de accept (in ordinea probabilitatii)
|
# 2. Vechiul GDPR wrapper
|
||||||
accept_selectors = [
|
("css", ".gdprcookie-wrapper button:has-text('Accept')"),
|
||||||
".gdprcookie-wrapper button:has-text('Accept')",
|
("css", ".gdprcookie-wrapper button:has-text('Sunt de acord')"),
|
||||||
".gdprcookie-wrapper button:has-text('Accepta')",
|
("css", ".gdprcookie-wrapper button"),
|
||||||
".gdprcookie-wrapper button:has-text('Sunt de acord')",
|
|
||||||
".gdprcookie-wrapper button:has-text('OK')",
|
|
||||||
".gdprcookie-wrapper .gdprcookie-buttons button:first-child",
|
|
||||||
".gdprcookie-wrapper button",
|
|
||||||
]
|
|
||||||
|
|
||||||
for selector in accept_selectors:
|
# 3. Fallback generic
|
||||||
try:
|
("role", "button", "Accept"),
|
||||||
accept_btn = page.locator(selector).first
|
("role", "button", "Accepta"),
|
||||||
if accept_btn.is_visible(timeout=1000):
|
("role", "button", "OK"),
|
||||||
accept_btn.click()
|
]
|
||||||
logging.info(f" [OK] Cookies acceptate (selector: {selector})")
|
|
||||||
time.sleep(1) # Asteapta sa dispara banner-ul
|
|
||||||
return True
|
|
||||||
except:
|
|
||||||
continue
|
|
||||||
|
|
||||||
logging.warning(" Nu am gasit buton de accept in GDPR wrapper")
|
for strategy in cookie_strategies:
|
||||||
return False
|
try:
|
||||||
except:
|
if strategy[0] == "role":
|
||||||
logging.info(" Nu exista GDPR cookie banner (sau deja inchis)")
|
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
|
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):
|
def _find_username_field(self, page):
|
||||||
"""
|
"""
|
||||||
@@ -466,10 +504,14 @@ class BTGoScraper:
|
|||||||
if accounts_btn.is_visible(timeout=1000):
|
if accounts_btn.is_visible(timeout=1000):
|
||||||
# Verifică că este și clickable (enabled)
|
# Verifică că este și clickable (enabled)
|
||||||
if accounts_btn.is_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
|
time.sleep(2) # Asteapta ca pagina sa se stabilizeze complet
|
||||||
# Update page reference la login_page pentru restul operatiilor
|
# Update page reference la login_page pentru restul operatiilor
|
||||||
self.page = self.login_page
|
self.page = self.login_page
|
||||||
|
|
||||||
|
# Inchide dialoguri one-time (ex: "Am inteles") daca apar
|
||||||
|
self._dismiss_one_time_consent(self.page)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
@@ -485,30 +527,19 @@ class BTGoScraper:
|
|||||||
raise TimeoutError(f"Timeout 2FA dupa {timeout} secunde. Verifica ca ai aprobat pe telefon!")
|
raise TimeoutError(f"Timeout 2FA dupa {timeout} secunde. Verifica ca ai aprobat pe telefon!")
|
||||||
|
|
||||||
def download_transactions(self, accounts):
|
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("=" * 60)
|
||||||
logging.info("Descarcare tranzactii pentru toate conturile...")
|
logging.info("Descarcare tranzactii pentru toate conturile...")
|
||||||
logging.info("=" * 60)
|
logging.info("=" * 60)
|
||||||
|
|
||||||
downloaded_files = []
|
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):
|
for idx, account in enumerate(accounts, 1):
|
||||||
try:
|
try:
|
||||||
nume_cont = account['nume_cont']
|
nume_cont = account['nume_cont']
|
||||||
@@ -517,118 +548,19 @@ class BTGoScraper:
|
|||||||
self._update_progress(f"Descarc tranzactii ({idx}/{len(accounts)})...")
|
self._update_progress(f"Descarc tranzactii ({idx}/{len(accounts)})...")
|
||||||
logging.info(f"[{idx}/{len(accounts)}] Descarcare tranzactii pentru: {nume_cont}")
|
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:
|
if idx == 1:
|
||||||
# Primul cont - expand și click Tranzacții
|
# PRIMUL CONT: expand -> click tranzactii -> select CSV
|
||||||
if idx - 1 >= len(all_cards):
|
downloaded = self._download_first_account(account)
|
||||||
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
|
|
||||||
else:
|
else:
|
||||||
# Conturile 2-5: suntem deja pe pagina de tranzacții (din modal)
|
# CONTURILE URMATOARE: selecteaza din dropdown -> Genereaza -> download
|
||||||
logging.info(f" Deja pe pagina tranzactii (selectat din modal)")
|
downloaded = self._download_subsequent_account(account)
|
||||||
time.sleep(2) # Așteaptă stabilizare pagină
|
|
||||||
|
|
||||||
# Așteaptă să apară butonul CSV (indica că pagina s-a încărcat)
|
if downloaded:
|
||||||
try:
|
downloaded_files.append(downloaded)
|
||||||
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
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f" ✗ Eroare la descarcarea tranzactiilor pentru {nume_cont}: {e}")
|
logging.error(f" [EROARE] Eroare la descarcarea tranzactiilor pentru {nume_cont}: {e}")
|
||||||
# Încearcă să navighezi înapoi
|
# Incearca sa revii la o stare stabila
|
||||||
try:
|
try:
|
||||||
self.page.keyboard.press("Escape")
|
self.page.keyboard.press("Escape")
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
@@ -637,11 +569,279 @@ class BTGoScraper:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
logging.info("=" * 60)
|
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)
|
logging.info("=" * 60)
|
||||||
|
|
||||||
return downloaded_files
|
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, <p> 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 <p> 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 <p> 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):
|
def read_accounts(self):
|
||||||
"""Extrage soldurile tuturor conturilor"""
|
"""Extrage soldurile tuturor conturilor"""
|
||||||
logging.info("Citire conturi si solduri...")
|
logging.info("Citire conturi si solduri...")
|
||||||
|
|||||||
Reference in New Issue
Block a user