# ----------------------------------------------------------------------------- # Immo-Ka — Agrégateur de maisons à vendre (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # mortgage/providers/rbc.py : RBC — API JSON publique publicrates (celle que # consomme leur propre page de taux, 1 requête par code de taux). # ⚠️ Les produits variables sont publiés en ÉCART vs prime (ex. -0.500) : # reconstitués avec le code prime, jamais interprétés comme des taux. # ----------------------------------------------------------------------------- from __future__ import annotations import json from .base import RateProvider API = ("https://apps.royalbank.com/apps/app-services/public-rates/api/" "publicrates?code={code}") PAGE_URL = "https://www.rbcroyalbank.com/mortgages/mortgage-rates.html" PRIME_CODE = "0006470002" # code -> (rate_type, terme mois, kind, insured_status, nom, code APR|None) CODE_MAP: dict[str, tuple] = { "0006340010": ("fixed", 6, "posted", "unknown", "Fixe 6 mois convertible", None), "0006340013": ("fixed", 12, "posted", "unknown", "Fixe fermé 1 an", None), "0006340016": ("fixed", 24, "posted", "unknown", "Fixe fermé 2 ans", None), "0006340019": ("fixed", 36, "posted", "unknown", "Fixe fermé 3 ans", None), "0006340022": ("fixed", 48, "posted", "unknown", "Fixe fermé 4 ans", None), "0006340025": ("fixed", 60, "posted", "unknown", "Fixe fermé 5 ans", None), "0006340042": ("fixed", 84, "posted", "unknown", "Fixe fermé 7 ans", None), "0006340030": ("fixed", 120, "posted", "unknown", "Fixe fermé 10 ans", None), "0386080056": ("fixed", 12, "special", "unknown", "Fixe fermé 1 an (offre spéciale)", "0386140074"), "0386080057": ("fixed", 24, "special", "unknown", "Fixe fermé 2 ans (offre spéciale)", "0386140075"), "0386080058": ("fixed", 36, "special", "unknown", "Fixe fermé 3 ans (offre spéciale)", "0386140076"), "0386080059": ("fixed", 48, "special", "unknown", "Fixe fermé 4 ans (offre spéciale)", "0386140077"), "0386080060": ("fixed", 60, "special", "unknown", "Fixe fermé 5 ans (offre spéciale)", "0386140078"), "0386080061": ("fixed", 84, "special", "unknown", "Fixe fermé 7 ans (offre spéciale)", "0386140079"), "0385940006": ("fixed", 60, "special", "insured", "Fixe fermé 5 ans (ratio élevé)", "0386090006"), } # codes variables : ÉCART vs prime -> taux = prime + valeur SPREAD_MAP: dict[str, tuple] = { "0236440012": ("variable", 60, "posted", "unknown", "Variable fermé 5 ans", "0166950009"), "0386080070": ("variable", 60, "special", "unknown", "Variable fermé 5 ans (offre spéciale)", "0386140088"), "0385940016": ("variable", 60, "special", "insured", "Variable fermé 5 ans (ratio élevé)", "0386090016"), } def _value(responses: dict, code: str) -> float | None: resp = responses.get(code) if not isinstance(resp, dict): return None content = resp.get("result_content") or {} try: return float(content.get("Value")) except (TypeError, ValueError): return None class RbcProvider(RateProvider): provider_id = "rbc" institution = "RBC Banque Royale" source_url = PAGE_URL request_delay = 0.6 def _codes(self) -> list[str]: codes = [PRIME_CODE] + list(CODE_MAP) + list(SPREAD_MAP) codes += [apr for *_, apr in CODE_MAP.values() if apr] codes += [apr for *_, apr in SPREAD_MAP.values() if apr] return codes def fetch(self) -> list[dict]: responses: dict[str, dict] = {} for code in self._codes(): try: responses[code] = json.loads(self.get(API.format(code=code)).text) except Exception: # noqa: BLE001 — un code raté n'annule pas le reste continue return self.parse_codes(responses) def parse(self, payload: str) -> list[dict]: """Pour les tests fixtures : payload = JSON {code: réponse API}.""" data = json.loads(payload) return self.parse_codes(data.get("responses") or data) def parse_codes(self, responses: dict) -> list[dict]: out: list[dict] = [] prime = _value(responses, PRIME_CODE) for code, (rtype, term, kind, insured, name, apr_code) in CODE_MAP.items(): rate = _value(responses, code) if rate is None or rate <= 0: continue apr = _value(responses, apr_code) if apr_code else None out.append(self.make_product( rate=rate, rate_type=rtype, term_months=term, kind=kind, product_name=name, apr=apr, insured_status=insured, raw={"code": code, "value": rate})) if prime is not None: for code, (rtype, term, kind, insured, name, apr_code) in SPREAD_MAP.items(): spread = _value(responses, code) if spread is None or abs(spread) > 3: continue # un écart vs prime > 3 pts : structure changée apr = _value(responses, apr_code) if apr_code else None out.append(self.make_product( rate=round(prime + spread, 3), rate_type=rtype, term_months=term, kind=kind, product_name=name, apr=apr, insured_status=insured, conditions=f"Prime RBC {prime} % {spread:+} pt", raw={"code": code, "spread": spread, "prime": prime})) out.append(self.make_product( rate=prime, rate_type="other", term_months=12, kind="posted", product_name="Taux préférentiel RBC", purpose="unknown", raw={"code": PRIME_CODE, "value": prime})) return out