Python 67%
TypeScript 18.2%
CSS 14.4%
1# -----------------------------------------------------------------------------2# Immo-Ka — Agrégateur de maisons à vendre (province de Québec)3# Auteur : Simon-Pierre Boucher — contact@spboucher.ai4# mortgage/providers/td.py : TD Canada Trust — API JSON publique getRates5# (POST ratesType=resl, celle que consomme leur page de taux). Chaque code6# MTG{F|V}{mois}{C|O} porte deux tableaux highRatio / nonHighRatio de la7# forme [affiché, escompte, spécial, APR, drapeau]. Le « affiché » des8# produits variables est le TD Mortgage Prime. ⚠️ Pour les termes sans offre9# spéciale réelle, TD publie un « spécial » reconstruit (escompte négatif)10# dont l'APR correspond en fait au taux AFFICHÉ : ces lignes incohérentes11# (APR < spécial) sont écartées — jamais interprétées.12# -----------------------------------------------------------------------------13from __future__ import annotations1415import json16import re1718from .base import RateProvider1920API_URL = "https://psservice.td.com/ca/en/carate/getRates"21PAGE_URL = ("https://www.td.com/ca/en/personal-banking/products/mortgages/"22 "mortgage-rates")2324CODE_RX = re.compile(r"^MTG([FV])(\d{3})([CO])$")2526IDX_POSTED, IDX_SPECIAL, IDX_APR = 0, 2, 3272829def _num(v) -> float | None:30 try:31 return float(str(v).strip())32 except (TypeError, ValueError):33 return None343536def _label(months: int) -> str:37 if months < 12:38 return f"{months} mois"39 years = months // 1240 return f"{years} an" if years == 1 else f"{years} ans"414243class TdProvider(RateProvider):44 provider_id = "td"45 institution = "TD Canada Trust"46 source_url = PAGE_URL47 request_delay = 1.04849 def fetch(self) -> list[dict]:50 wait_kw = {"timeout": self.timeout,51 "headers": {"Content-Type": "application/json"}}52 resp = self.session.post(API_URL, json={"ratesType": "resl"}, **wait_kw)53 resp.raise_for_status()54 return self.parse(resp.text)5556 def parse(self, payload: str) -> list[dict]:57 data = json.loads(payload)58 if not isinstance(data, dict):59 raise ValueError("réponse TD inattendue (structure changée)")60 for wrapper in ("rates", "data", "result"):61 if wrapper in data and isinstance(data[wrapper], dict):62 data = data[wrapper]63 break64 out: list[dict] = []65 prime: float | None = None66 for code, tables in data.items():67 m = CODE_RX.match(str(code))68 if not m or not isinstance(tables, dict):69 continue # FLT* (FlexLine/HELOC) et codes inconnus : ignorés70 rtype = "fixed" if m.group(1) == "F" else "variable"71 term = int(m.group(2))72 openness = "fermé" if m.group(3) == "C" else "ouvert"73 base = ("Fixe" if rtype == "fixed" else "Variable")74 non_hr = tables.get("nonHighRatio") or []75 high_r = tables.get("highRatio") or []76 posted = _num(non_hr[IDX_POSTED]) if len(non_hr) > IDX_POSTED else None77 if posted and posted > 0:78 if rtype == "variable":79 # l'« affiché » des codes MTGV est le TD Mortgage Prime,80 # pas le taux du produit : jamais émis comme taux variable81 prime = posted82 else:83 out.append(self.make_product(84 rate=posted, rate_type=rtype, term_months=term,85 kind="posted",86 product_name=f"{base} {openness} {_label(term)}",87 raw={"code": code, "row": non_hr}))88 for arr, insured, suffix in ((non_hr, "uninsured", ""),89 (high_r, "insured", ", ratio élevé")):90 if len(arr) <= IDX_APR:91 continue92 special = _num(arr[IDX_SPECIAL])93 if special is None or special <= 0:94 continue95 if posted is not None and special == posted and rtype == "fixed" \96 and _num(arr[1]) in (0, None):97 continue # pas de spécial publié pour ce produit98 apr = _num(arr[IDX_APR])99 if apr is not None and apr < special - 0.02:100 continue # « spécial » reconstruit : APR = celui du taux affiché101 out.append(self.make_product(102 rate=special, rate_type=rtype, term_months=term,103 kind="special", apr=apr,104 product_name=f"{base} {openness} {_label(term)} "105 f"(offre spéciale{suffix})",106 insured_status=insured,107 conditions="Base = TD Mortgage Prime"108 if rtype == "variable" else None,109 raw={"code": code, "row": arr}))110 if prime and prime > 0:111 out.append(self.make_product(112 rate=prime, rate_type="other", term_months=12, kind="posted",113 product_name="TD Mortgage Prime", purpose="unknown",114 raw={"field": "MTGV*[0]", "value": prime}))115 return out116