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
|
||||
|
||||
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
|
||||
|
||||
|
||||
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`
|
||||
- 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
|
||||
|
||||
494
btgo_scraper.py
494
btgo_scraper.py
@@ -98,36 +98,74 @@ 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"),
|
||||
|
||||
# 3. Fallback generic
|
||||
("role", "button", "Accept"),
|
||||
("role", "button", "Accepta"),
|
||||
("role", "button", "OK"),
|
||||
]
|
||||
|
||||
for selector in accept_selectors:
|
||||
for strategy in cookie_strategies:
|
||||
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
|
||||
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.warning(" Nu am gasit buton de accept in GDPR wrapper")
|
||||
logging.info(" Nu exista cookie banner (sau deja acceptat)")
|
||||
return False
|
||||
except:
|
||||
logging.info(" Nu exista GDPR cookie banner (sau deja inchis)")
|
||||
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)
|
||||
})
|
||||
if downloaded:
|
||||
downloaded_files.append(downloaded)
|
||||
|
||||
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:
|
||||
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, <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):
|
||||
"""Extrage soldurile tuturor conturilor"""
|
||||
logging.info("Citire conturi si solduri...")
|
||||
|
||||
Reference in New Issue
Block a user