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%
4.3 KB · 105 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/scotiabank.py : Banque Scotia — API JSON publique5#   dmtsms.scotiabank.com (celle que consomme leur propre page de taux).6#   nonspecialmortgage = taux affichés ; varmortgage = slots promotionnels7#   (le mapping slot -> produit vient de la page ; on ne garde que les slots8#   dont l'identité est connue, avec confiance réduite).9# -----------------------------------------------------------------------------10from __future__ import annotations1112import json1314from .base import RateProvider1516POSTED_URL = "https://dmtsms.scotiabank.com/api/rates/daily/nonspecialmortgage"17PROMO_URL = "https://dmtsms.scotiabank.com/api/rates/daily/varmortgage"18PAGE_URL = ("https://www.scotiabank.com/ca/en/personal/rates-prices/"19            "mortgages-rates.html")2021# PRODUCT -> (rate_type, insured_status, nom, purpose)22POSTED_MAP = {23    "N.H.A. RESIDENTIAL, ETC": ("fixed", "insured", "Fixe affiché (assuré LNH)"),24    "CONVENTIONAL RESIDENTIAL, ETC": ("fixed", "uninsured", "Fixe affiché (conventionnel)"),25    "ULTIMATE VARIABLE RATE": ("variable", "unknown", "Ultimate Variable"),26    "OPEN": ("fixed", "unknown", "Fixe ouvert"),27    "FLEXIBLE": ("fixed", "unknown", "Flexible"),28}2930# Slots promotionnels dont l'identité est documentée sur la page Scotia31# (2026-08). Confiance réduite : si Scotia réordonne ses slots, la validation32# et la comparaison croisée limitent les dégâts.33PROMO_SLOTS = {34    "MORTGAGE PROMOTIONAL 1": ("variable", 60, "Flex Value fermé 5 ans (variable)"),35    "MORTGAGE PROMOTIONAL 2": ("variable", 36, "Ultimate Variable 3 ans"),36    "MORTGAGE PROMOTIONAL 3": ("variable", 60, "Flex Value ouvert 5 ans (variable)"),37}383940def _term_months(t: dict) -> int | None:41    try:42        v = int(t["TERM_VALUE"])43    except (KeyError, ValueError, TypeError):44        return None45    unit = (t.get("TERM_UNIT") or "").upper()46    if unit == "Y":47        return v * 1248    if unit == "M":49        return v50    return None515253class ScotiabankProvider(RateProvider):54    provider_id = "scotiabank"55    institution = "Banque Scotia"56    source_url = PAGE_URL57    request_delay = 1.05859    def fetch(self) -> list[dict]:60        posted = self.get(POSTED_URL).text61        try:62            promo = self.get(PROMO_URL).text63        except Exception:  # noqa: BLE001 — promos facultatives64            promo = ""65        return self.parse(posted) + (self.parse_promos(promo) if promo else [])6667    def parse(self, payload: str) -> list[dict]:68        data = json.loads(payload)69        out: list[dict] = []70        for prod in data.get("data") or []:71            name = (prod.get("PRODUCT") or "").strip()72            if name not in POSTED_MAP:73                continue  # produits chalet/cap/right-rate : hors périmètre74            rtype, insured, label = POSTED_MAP[name]75            for t in prod.get("TERMS") or []:76                term = _term_months(t)77                rate = t.get("RATE")78                if term is None or not isinstance(rate, (int, float)) or rate <= 0:79                    continue80                out.append(self.make_product(81                    rate=float(rate), rate_type=rtype, term_months=term,82                    kind="posted", product_name=label,83                    insured_status=insured,84                    conditions=name,85                    raw={"product": name, "term": t}))86        return out8788    def parse_promos(self, payload: str) -> list[dict]:89        data = json.loads(payload)90        out: list[dict] = []91        for prod in data.get("data") or []:92            name = (prod.get("PRODUCT") or "").strip()93            if name not in PROMO_SLOTS:94                continue95            rate = prod.get("RATE")96            if not isinstance(rate, (int, float)) or rate <= 0:97                continue  # slot inutilisé98            rtype, term, label = PROMO_SLOTS[name]99            out.append(self.make_product(100                rate=float(rate), rate_type=rtype, term_months=term,101                kind="special", product_name=label, confidence=0.8,102                conditions=f"Slot promotionnel Scotia ({name})",103                raw={"product": name, "rate": rate}))104        return out105