triangle-api 0.1.4__tar.gz → 0.1.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {triangle_api-0.1.4 → triangle_api-0.1.5}/PKG-INFO +1 -1
- {triangle_api-0.1.4 → triangle_api-0.1.5}/setup.py +1 -1
- triangle_api-0.1.5/triangle_api/__init__.py +4 -0
- triangle_api-0.1.5/triangle_api/client.py +173 -0
- {triangle_api-0.1.4 → triangle_api-0.1.5}/triangle_api.egg-info/PKG-INFO +1 -1
- triangle_api-0.1.4/triangle_api/__init__.py +0 -4
- triangle_api-0.1.4/triangle_api/client.py +0 -232
- {triangle_api-0.1.4 → triangle_api-0.1.5}/README.md +0 -0
- {triangle_api-0.1.4 → triangle_api-0.1.5}/setup.cfg +0 -0
- {triangle_api-0.1.4 → triangle_api-0.1.5}/triangle_api/exceptions.py +0 -0
- {triangle_api-0.1.4 → triangle_api-0.1.5}/triangle_api.egg-info/SOURCES.txt +0 -0
- {triangle_api-0.1.4 → triangle_api-0.1.5}/triangle_api.egg-info/dependency_links.txt +0 -0
- {triangle_api-0.1.4 → triangle_api-0.1.5}/triangle_api.egg-info/requires.txt +0 -0
- {triangle_api-0.1.4 → triangle_api-0.1.5}/triangle_api.egg-info/top_level.txt +0 -0
|
@@ -5,7 +5,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
|
|
|
5
5
|
|
|
6
6
|
setup(
|
|
7
7
|
name="triangle-api", # Nome que aparecerá no PyPI
|
|
8
|
-
version="0.1.
|
|
8
|
+
version="0.1.5",
|
|
9
9
|
author="Diogo Bastos",
|
|
10
10
|
author_email="seu-email@exemplo.com", # Opcional
|
|
11
11
|
description="Unofficial Python wrapper for Canadian Tire Triangle Mastercard API",
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
from playwright.sync_api import sync_playwright
|
|
2
|
+
import os
|
|
3
|
+
import time
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
class TriangleClient:
|
|
9
|
+
def __init__(self, username=None, password=None, session_path="auth_state.json"):
|
|
10
|
+
self.username = username
|
|
11
|
+
self.password = password
|
|
12
|
+
self.session_path = session_path
|
|
13
|
+
self.account_data = None
|
|
14
|
+
|
|
15
|
+
def _get_csrf(self, context):
|
|
16
|
+
"""Auxiliar para extrair o token CSRF dos cookies."""
|
|
17
|
+
cookies = context.cookies()
|
|
18
|
+
return next((c['value'] for c in cookies if c['name'] == 'csrftoken'), "")
|
|
19
|
+
|
|
20
|
+
def _fetch_account_data(self, page, csrf_token):
|
|
21
|
+
"""Faz a chamada manual do retrieveAccount com CSRF."""
|
|
22
|
+
js_get_account = """
|
|
23
|
+
async (csrf) => {
|
|
24
|
+
const r = await fetch('/dash/v1/account/retrieveAccount', {
|
|
25
|
+
method: 'POST',
|
|
26
|
+
headers: {
|
|
27
|
+
'Content-Type': 'application/json',
|
|
28
|
+
'CSRF-Token': csrf,
|
|
29
|
+
'csrfToken': csrf
|
|
30
|
+
},
|
|
31
|
+
body: JSON.stringify({})
|
|
32
|
+
});
|
|
33
|
+
return r.ok ? await r.json() : null;
|
|
34
|
+
}
|
|
35
|
+
"""
|
|
36
|
+
return page.evaluate(js_get_account, csrf_token)
|
|
37
|
+
|
|
38
|
+
def login(self, headless=True):
|
|
39
|
+
with sync_playwright() as p:
|
|
40
|
+
browser = p.chromium.launch(headless=headless)
|
|
41
|
+
has_session = os.path.exists(self.session_path)
|
|
42
|
+
storage = self.session_path if has_session else None
|
|
43
|
+
|
|
44
|
+
context = browser.new_context(
|
|
45
|
+
viewport={'width': 1280, 'height': 800},
|
|
46
|
+
storage_state=storage,
|
|
47
|
+
user_agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
|
48
|
+
)
|
|
49
|
+
page = context.new_page()
|
|
50
|
+
|
|
51
|
+
print("[API] Acessando o portal...")
|
|
52
|
+
page.goto("https://www.ctfs.com/content/dash/en/private/Summary.html", timeout=60000)
|
|
53
|
+
|
|
54
|
+
# Se cair no login, faz o processo de autenticação
|
|
55
|
+
if "login" in page.url.lower():
|
|
56
|
+
if not self.username or not self.password:
|
|
57
|
+
browser.close()
|
|
58
|
+
return "NEEDS_CREDENTIALS"
|
|
59
|
+
|
|
60
|
+
print("[API] Sessão expirada. Autenticando...")
|
|
61
|
+
try:
|
|
62
|
+
# Lida com cookies
|
|
63
|
+
if page.is_visible("#onetrust-accept-btn-handler"):
|
|
64
|
+
page.click("#onetrust-accept-btn-handler", timeout=5000)
|
|
65
|
+
|
|
66
|
+
page.wait_for_selector("#username", timeout=30000)
|
|
67
|
+
page.fill("#username", self.username)
|
|
68
|
+
page.fill("#password", self.password)
|
|
69
|
+
page.focus("#password")
|
|
70
|
+
page.keyboard.press("Enter")
|
|
71
|
+
|
|
72
|
+
# Espera sair do login
|
|
73
|
+
page.wait_for_function("() => !window.location.href.includes('login')", timeout=45000)
|
|
74
|
+
|
|
75
|
+
if "challenge" in page.url or "otp" in page.url:
|
|
76
|
+
sms_code = input("\n[MFA] Digite o código SMS: ")
|
|
77
|
+
page.get_by_role("textbox").first.fill(sms_code)
|
|
78
|
+
page.keyboard.press("Enter")
|
|
79
|
+
page.wait_for_url("**/Summary.html", timeout=45000)
|
|
80
|
+
except Exception as e:
|
|
81
|
+
print(f"[ERRO] Falha no login: {e}")
|
|
82
|
+
browser.close()
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
# CAPTURA DE DADOS (Saldo)
|
|
86
|
+
print("[API] Capturando dados financeiros...")
|
|
87
|
+
try:
|
|
88
|
+
page.wait_for_load_state("domcontentloaded")
|
|
89
|
+
csrf = self._get_csrf(context)
|
|
90
|
+
self.account_data = self._fetch_account_data(page, csrf)
|
|
91
|
+
|
|
92
|
+
if self.account_data:
|
|
93
|
+
context.storage_state(path=self.session_path)
|
|
94
|
+
print("[API] Dados capturados com sucesso.")
|
|
95
|
+
except Exception as e:
|
|
96
|
+
print(f"[AVISO] Erro na captura de saldo: {e}")
|
|
97
|
+
|
|
98
|
+
browser.close()
|
|
99
|
+
return self.account_data
|
|
100
|
+
|
|
101
|
+
def get_transactions(self, start_date=None, headless=True):
|
|
102
|
+
today = datetime.today()
|
|
103
|
+
if not start_date:
|
|
104
|
+
start_date = today.replace(day=1).strftime('%Y-%m-%d')
|
|
105
|
+
|
|
106
|
+
with sync_playwright() as p:
|
|
107
|
+
browser = p.chromium.launch(headless=headless)
|
|
108
|
+
context = browser.new_context(storage_state=self.session_path)
|
|
109
|
+
page = context.new_page()
|
|
110
|
+
|
|
111
|
+
print("[API] Sincronizando para transações...")
|
|
112
|
+
page.goto("https://www.ctfs.com/content/dash/en/private/Summary.html", wait_until="domcontentloaded")
|
|
113
|
+
|
|
114
|
+
csrf = self._get_csrf(context)
|
|
115
|
+
|
|
116
|
+
# Se o account_data sumiu da memória, pegamos de novo agora que temos o CSRF
|
|
117
|
+
if not self.account_data:
|
|
118
|
+
self.account_data = self._fetch_account_data(page, csrf)
|
|
119
|
+
|
|
120
|
+
if not self.account_data:
|
|
121
|
+
browser.close()
|
|
122
|
+
return []
|
|
123
|
+
|
|
124
|
+
js_tx_script = """
|
|
125
|
+
async (params) => {
|
|
126
|
+
const call = async (cat, stmt = null) => {
|
|
127
|
+
const body = { category: cat, transientReference: params.transient };
|
|
128
|
+
if (stmt) body.statementDate = stmt;
|
|
129
|
+
const r = await fetch('/dash/v1/account/retrieveTransactions', {
|
|
130
|
+
method: 'POST',
|
|
131
|
+
headers: { 'Content-Type': 'application/json', 'CSRF-Token': params.csrf, 'csrfToken': params.csrf },
|
|
132
|
+
body: JSON.stringify(body)
|
|
133
|
+
});
|
|
134
|
+
return r.ok ? await r.json() : { transactions: [] };
|
|
135
|
+
};
|
|
136
|
+
const auth = await call('AUTHORIZED');
|
|
137
|
+
const post = await call('POSTED');
|
|
138
|
+
const stmt = params.lastStmt ? await call('STATEMENTED', params.lastStmt) : { transactions: [] };
|
|
139
|
+
return [...(auth.transactions || []), ...(post.transactions || []), ...(stmt.transactions || [])];
|
|
140
|
+
}
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
raw_txs = page.evaluate(js_tx_script, {
|
|
144
|
+
"transient": self.account_data.get("transientReference"),
|
|
145
|
+
"lastStmt": self.account_data.get("lastStatementDate"),
|
|
146
|
+
"csrf": csrf
|
|
147
|
+
})
|
|
148
|
+
browser.close()
|
|
149
|
+
|
|
150
|
+
# Filtro e Deduplicação
|
|
151
|
+
unique_txs = {}
|
|
152
|
+
for t in raw_txs:
|
|
153
|
+
d = t.get("tranDate", "")
|
|
154
|
+
if d >= start_date:
|
|
155
|
+
key = f"{d}|{t.get('merchant')}|{t.get('amount')}"
|
|
156
|
+
unique_txs[key] = t
|
|
157
|
+
return list(unique_txs.values())
|
|
158
|
+
|
|
159
|
+
def get_normalized_transactions(self, start_date=None, headless=True):
|
|
160
|
+
txs = self.get_transactions(start_date=start_date, headless=headless)
|
|
161
|
+
normalized = []
|
|
162
|
+
for t in txs:
|
|
163
|
+
date, payee, amount = t.get("tranDate"), t.get("merchant", "Unknown"), float(t.get("amount", 0.0))
|
|
164
|
+
ext_id = hashlib.md5(f"{date}|{payee}|{amount}".encode()).hexdigest()
|
|
165
|
+
normalized.append({
|
|
166
|
+
"date": date, "payee": payee, "amount": amount, "currency": "cad",
|
|
167
|
+
"external_id": ext_id, "status": "cleared" if t.get("category") in ["POSTED", "STATEMENTED"] else "uncleared",
|
|
168
|
+
"notes": f"Type: {t.get('type')} | Imported via Triangle API"
|
|
169
|
+
})
|
|
170
|
+
return sorted(normalized, key=lambda x: x["date"], reverse=True)
|
|
171
|
+
|
|
172
|
+
def get_balance(self):
|
|
173
|
+
return self.account_data.get("currentBalanceAmt", 0.0) if self.account_data else "N/A"
|
|
@@ -1,232 +0,0 @@
|
|
|
1
|
-
from playwright.sync_api import sync_playwright
|
|
2
|
-
import os
|
|
3
|
-
import time
|
|
4
|
-
import hashlib
|
|
5
|
-
from datetime import datetime
|
|
6
|
-
|
|
7
|
-
class TriangleClient:
|
|
8
|
-
def __init__(self, username=None, password=None, session_path="auth_state.json"):
|
|
9
|
-
self.username = username
|
|
10
|
-
self.password = password
|
|
11
|
-
self.session_path = session_path
|
|
12
|
-
self.account_data = None
|
|
13
|
-
|
|
14
|
-
def login(self, headless=True):
|
|
15
|
-
with sync_playwright() as p:
|
|
16
|
-
# 1. Configuração do Navegador
|
|
17
|
-
browser = p.chromium.launch(headless=headless)
|
|
18
|
-
|
|
19
|
-
# Verifica se existe sessão salva para tentar restaurar
|
|
20
|
-
has_session = os.path.exists(self.session_path)
|
|
21
|
-
storage = self.session_path if has_session else None
|
|
22
|
-
|
|
23
|
-
context = browser.new_context(
|
|
24
|
-
viewport={'width': 1280, 'height': 800},
|
|
25
|
-
storage_state=storage,
|
|
26
|
-
user_agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
|
27
|
-
)
|
|
28
|
-
page = context.new_page()
|
|
29
|
-
|
|
30
|
-
# 2. Definição da URL inicial
|
|
31
|
-
if has_session:
|
|
32
|
-
print("[API] Tentando restaurar sessão anterior...")
|
|
33
|
-
target_url = "https://www.ctfs.com/content/dash/en/private/Summary.html"
|
|
34
|
-
else:
|
|
35
|
-
print("[API] Iniciando novo login...")
|
|
36
|
-
target_url = "https://www.ctfs.com/content/dashpublic/en/login.html"
|
|
37
|
-
|
|
38
|
-
page.goto(target_url, wait_until="load", timeout=60000)
|
|
39
|
-
|
|
40
|
-
# 3. Lidar com Banner de Cookies (OneTrust)
|
|
41
|
-
# Esse banner bloqueia a interação com os campos de texto se não for fechado
|
|
42
|
-
try:
|
|
43
|
-
cookie_btn = page.locator("#onetrust-accept-btn-handler, text='Accept All'")
|
|
44
|
-
if cookie_btn.is_visible(timeout=8000):
|
|
45
|
-
cookie_btn.click()
|
|
46
|
-
print("[API] Banner de cookies aceito.")
|
|
47
|
-
time.sleep(1) # Pausa para animação de fechamento
|
|
48
|
-
except:
|
|
49
|
-
pass
|
|
50
|
-
|
|
51
|
-
# 4. Fluxo de Autenticação (Caso a sessão tenha expirado ou não exista)
|
|
52
|
-
if "login" in page.url.lower() or not has_session:
|
|
53
|
-
if not self.username or not self.password:
|
|
54
|
-
print("[API] Credenciais necessárias para continuar.")
|
|
55
|
-
browser.close()
|
|
56
|
-
return "NEEDS_CREDENTIALS"
|
|
57
|
-
|
|
58
|
-
print("[API] Tela de login detectada. Preenchendo credenciais...")
|
|
59
|
-
try:
|
|
60
|
-
page.wait_for_selector("#username", timeout=30000)
|
|
61
|
-
page.fill("#username", self.username)
|
|
62
|
-
page.fill("#password", self.password)
|
|
63
|
-
|
|
64
|
-
# Submissão via Teclado (Enter) é mais resiliente contra widgets de chat
|
|
65
|
-
page.focus("#password")
|
|
66
|
-
page.keyboard.press("Enter")
|
|
67
|
-
|
|
68
|
-
# Clique de segurança via JavaScript caso o Enter falhe
|
|
69
|
-
page.evaluate("document.querySelector('#signin-form')?.click()")
|
|
70
|
-
|
|
71
|
-
# Espera sair da tela de login (mudar URL ou detectar MFA)
|
|
72
|
-
print("[API] Formulário enviado. Aguardando resposta do banco...")
|
|
73
|
-
page.wait_for_function("() => !window.location.href.includes('login')", timeout=45000)
|
|
74
|
-
|
|
75
|
-
# --- Verificação de MFA (SMS/OTP) ---
|
|
76
|
-
if "challenge" in page.url or "otp" in page.url or page.is_visible("input[aria-describedby*='otp']"):
|
|
77
|
-
print("\n[MFA] Verificação de segurança detectada.")
|
|
78
|
-
sms_code = input("Digite o código SMS recebido no celular: ")
|
|
79
|
-
page.get_by_role("textbox").first.fill(sms_code)
|
|
80
|
-
page.keyboard.press("Enter")
|
|
81
|
-
page.wait_for_url("**/Summary.html", timeout=60000)
|
|
82
|
-
|
|
83
|
-
print("[API] Login realizado com sucesso!")
|
|
84
|
-
|
|
85
|
-
except Exception as e:
|
|
86
|
-
print(f"[ERRO] Falha no processo de login: {e}")
|
|
87
|
-
page.screenshot(path="debug_login_error.png")
|
|
88
|
-
browser.close()
|
|
89
|
-
return None
|
|
90
|
-
|
|
91
|
-
# 5. CAPTURA FINAL DE DADOS (Saldo e transientReference)
|
|
92
|
-
print("[API] Capturando dados financeiros (retrieveAccount)...")
|
|
93
|
-
try:
|
|
94
|
-
# Esperamos a página carregar o básico
|
|
95
|
-
page.wait_for_load_state("domcontentloaded")
|
|
96
|
-
|
|
97
|
-
# Em vez de 'esperar' uma resposta, nós 'pedimos' os dados
|
|
98
|
-
# Isso evita o erro de "Protocol error"
|
|
99
|
-
js_get_account = """
|
|
100
|
-
async () => {
|
|
101
|
-
const r = await fetch('/dash/v1/account/retrieveAccount', {
|
|
102
|
-
method: 'POST',
|
|
103
|
-
headers: { 'Content-Type': 'application/json' },
|
|
104
|
-
body: JSON.stringify({})
|
|
105
|
-
});
|
|
106
|
-
return r.ok ? await r.json() : null;
|
|
107
|
-
}
|
|
108
|
-
"""
|
|
109
|
-
|
|
110
|
-
# Executa o fetch dentro do contexto do navegador
|
|
111
|
-
self.account_data = page.evaluate(js_get_account)
|
|
112
|
-
|
|
113
|
-
if self.account_data:
|
|
114
|
-
# Agora que temos os dados, salvamos a sessão
|
|
115
|
-
context.storage_state(path=self.session_path)
|
|
116
|
-
print("[API] Dados financeiros capturados com sucesso.")
|
|
117
|
-
else:
|
|
118
|
-
print("[AVISO] O servidor retornou vazio para o saldo.")
|
|
119
|
-
|
|
120
|
-
except Exception as e:
|
|
121
|
-
print(f"[ERRO] Falha ao capturar saldo via JS: {e}")
|
|
122
|
-
|
|
123
|
-
browser.close()
|
|
124
|
-
return self.account_data
|
|
125
|
-
|
|
126
|
-
def get_normalized_transactions(self, start_date=None, headless=True):
|
|
127
|
-
"""
|
|
128
|
-
Versão ultra-robusta para evitar erros de ordenação (NoneType).
|
|
129
|
-
"""
|
|
130
|
-
# 1. Busca as transações brutas
|
|
131
|
-
raw_txs = self.get_transactions(start_date=start_date, headless=headless)
|
|
132
|
-
|
|
133
|
-
normalized = []
|
|
134
|
-
seen_ids = set()
|
|
135
|
-
|
|
136
|
-
for tx in raw_txs:
|
|
137
|
-
# Pegamos a data. Se não existir, ignoramos o item.
|
|
138
|
-
t_date = tx.get("tranDate")
|
|
139
|
-
if not t_date:
|
|
140
|
-
continue
|
|
141
|
-
|
|
142
|
-
payee = tx.get("merchant", "Unknown Merchant")
|
|
143
|
-
amount = float(tx.get("amount", 0.0))
|
|
144
|
-
|
|
145
|
-
# Gerar ID Único para deduplicação
|
|
146
|
-
identifier = f"{t_date}|{payee}|{amount}"
|
|
147
|
-
if identifier in seen_ids:
|
|
148
|
-
continue
|
|
149
|
-
seen_ids.add(identifier)
|
|
150
|
-
|
|
151
|
-
# MD5 para o Lunch Money
|
|
152
|
-
ext_id = hashlib.md5(identifier.encode("utf-8")).hexdigest()
|
|
153
|
-
|
|
154
|
-
# Certifique-se de que o dicionário normalized tenha a chave 'notes'
|
|
155
|
-
normalized.append({
|
|
156
|
-
"date": t_date,
|
|
157
|
-
"payee": payee,
|
|
158
|
-
"amount": amount,
|
|
159
|
-
"currency": "cad",
|
|
160
|
-
"external_id": ext_id,
|
|
161
|
-
"type": tx.get("type", "PURCHASE"),
|
|
162
|
-
"status": "cleared" if tx.get("category") in ["POSTED", "STATEMENTED"] else "uncleared",
|
|
163
|
-
"notes": f"Type: {tx.get('type')} | Imported via Triangle API"
|
|
164
|
-
})
|
|
165
|
-
|
|
166
|
-
# Ordenar garantindo que não existam Nones na chave de ordenação
|
|
167
|
-
return sorted(normalized, key=lambda x: x.get("date", ""), reverse=True)
|
|
168
|
-
|
|
169
|
-
def get_transactions(self, start_date=None, headless=True):
|
|
170
|
-
today = datetime.today()
|
|
171
|
-
if not start_date:
|
|
172
|
-
start_date = today.replace(day=1).strftime('%Y-%m-%d')
|
|
173
|
-
end_date = today.strftime('%Y-%m-%d')
|
|
174
|
-
|
|
175
|
-
print(f"[API] Buscando transações de {start_date} até {end_date}...")
|
|
176
|
-
|
|
177
|
-
with sync_playwright() as p:
|
|
178
|
-
browser = p.chromium.launch(headless=headless)
|
|
179
|
-
storage = self.session_path if os.path.exists(self.session_path) else None
|
|
180
|
-
context = browser.new_context(storage_state=storage)
|
|
181
|
-
page = context.new_page()
|
|
182
|
-
|
|
183
|
-
try:
|
|
184
|
-
print("[API] Sincronizando sessão para histórico...")
|
|
185
|
-
# Navega e pesca o retrieveAccount da janela atual
|
|
186
|
-
with page.expect_response("**/retrieveAccount", timeout=30000) as resp_acc:
|
|
187
|
-
page.goto("https://www.ctfs.com/content/dash/en/private/Summary.html", wait_until="domcontentloaded")
|
|
188
|
-
|
|
189
|
-
acc_info = resp_acc.value.json()
|
|
190
|
-
transient = acc_info.get("transientReference")
|
|
191
|
-
acc_id = acc_info.get("accountId")
|
|
192
|
-
last_stmt = acc_info.get("lastStatementDate")
|
|
193
|
-
|
|
194
|
-
cookies = context.cookies()
|
|
195
|
-
csrf = next((c['value'] for c in cookies if c['name'] == 'csrftoken'), "")
|
|
196
|
-
|
|
197
|
-
# Script de busca tripla
|
|
198
|
-
js_script = """
|
|
199
|
-
async (params) => {
|
|
200
|
-
const call = async (cat, stmt = null) => {
|
|
201
|
-
const body = { category: cat, transientReference: params.transient, accountId: params.accId };
|
|
202
|
-
if (stmt) body.statementDate = stmt;
|
|
203
|
-
const r = await fetch('/dash/v1/account/retrieveTransactions', {
|
|
204
|
-
method: 'POST',
|
|
205
|
-
headers: { 'Content-Type': 'application/json', 'CSRF-Token': params.csrf, 'csrfToken': params.csrf },
|
|
206
|
-
body: JSON.stringify(body)
|
|
207
|
-
});
|
|
208
|
-
return r.ok ? await r.json() : { transactions: [] };
|
|
209
|
-
};
|
|
210
|
-
const auth = await call('AUTHORIZED');
|
|
211
|
-
const post = await call('POSTED');
|
|
212
|
-
const stmt = params.lastStmt ? await call('STATEMENTED', params.lastStmt) : { transactions: [] };
|
|
213
|
-
return [...(auth.transactions || []), ...(post.transactions || []), ...(stmt.transactions || [])];
|
|
214
|
-
}
|
|
215
|
-
"""
|
|
216
|
-
|
|
217
|
-
raw_txs = page.evaluate(js_script, {
|
|
218
|
-
"transient": transient, "accId": acc_id, "lastStmt": last_stmt, "csrf": csrf
|
|
219
|
-
})
|
|
220
|
-
|
|
221
|
-
# Filtro de data bruto
|
|
222
|
-
filtered = [t for t in raw_txs if t.get("tranDate", "") >= start_date]
|
|
223
|
-
browser.close()
|
|
224
|
-
return filtered
|
|
225
|
-
|
|
226
|
-
except Exception as e:
|
|
227
|
-
print(f"[ERRO] Falha na captura: {e}")
|
|
228
|
-
browser.close()
|
|
229
|
-
return []
|
|
230
|
-
|
|
231
|
-
def get_balance(self):
|
|
232
|
-
return self.account_data.get("currentBalanceAmt", 0.0) if self.account_data else "N/A"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|