SPB Git forge

spb/house-ka

Public
18commits 1branches 0releases
1.9 MBsize
maindefault branch
20 days agolast push
Python 67% TypeScript 18.2% CSS 14.4%
5.1 KB · 116 lines python
Raw Blame History
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