diff --git a/apps/edr-freight-api/py/create_contracts.py b/apps/edr-freight-api/py/create_contracts.py index 086ecf993..956c81cbb 100644 --- a/apps/edr-freight-api/py/create_contracts.py +++ b/apps/edr-freight-api/py/create_contracts.py @@ -53,9 +53,13 @@ CUSTOMER_PASSWORD = os.getenv("CUSTOMER_PASSWORD", "") ADMIN_EMAIL = os.getenv("ADMIN_EMAIL", "") ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD", "") -# Phone the sign-OTP is sent to and read back from Postgres. Independent of the -# logged-in user — the sign endpoint keys the OTP purely on this number. +# Phone the sign-OTP is sent to and read back from Postgres. Resolved at runtime +# from the customer's own IAM profile (GET /api/me → phoneNumber). OTP_PHONE is an +# optional override / fallback used only when the customer has no phone on file. +# The sign endpoint keys the OTP purely on this number, so it just has to be the +# same value for "send" and "read". OTP_PHONE = os.getenv("OTP_PHONE", "") +OTP_PHONE_FALLBACK = os.getenv("OTP_PHONE_FALLBACK", "251900000000") # DB connection used ONLY to read the plaintext sign-OTP from freight.otp_verifications. DB_HOST = os.getenv("DB_HOST", "localhost") @@ -202,6 +206,25 @@ def login(email: str, password: str, who: str) -> Client: # --------------------------------------------------------------------------- # # OTP — send + read from Postgres # --------------------------------------------------------------------------- # +def resolve_otp_phone(customer: Client) -> str: + """Phone the sign-OTP is sent to. Prefer the customer's own IAM profile phone + (GET /api/me → phoneNumber); fall back to OTP_PHONE, then OTP_PHONE_FALLBACK. + The value only has to be consistent between send + DB read.""" + phone = "" + try: + me = customer.get("/me") or {} + phone = (me.get("phoneNumber") or "").strip() + except Exception: + pass + phone = phone or OTP_PHONE or OTP_PHONE_FALLBACK + if not phone: + raise RuntimeError( + "Could not resolve an OTP phone (customer has none, and neither " + "OTP_PHONE nor OTP_PHONE_FALLBACK is set)." + ) + return phone + + def send_otp(customer: Client, phone: str) -> None: # POST /api/otp/send is @Public — no token needed, but sending one is harmless. customer.post_json("/otp/send", {"phone": phone}) @@ -321,7 +344,7 @@ def waafi_file_tuple(field_name: str) -> tuple: def drive_flow( - flow: Flow, idx: int, customer: Client, admin: Client, ref: RefData + flow: Flow, idx: int, customer: Client, admin: Client, ref: RefData, otp_phone: str ) -> dict[str, Any]: result: dict[str, Any] = {"flow": flow.label, "n": idx, "status": None} @@ -349,9 +372,9 @@ def drive_flow( admin.post_json(f"/contracts/{cid}/contract/generate") # S6 — customer sign (needs OTP) → SIGNED_CUSTOMER - send_otp(customer, OTP_PHONE) + send_otp(customer, otp_phone) time.sleep(1.0) # let the OTP row land - otp = read_otp_from_db(OTP_PHONE) + otp = read_otp_from_db(otp_phone) customer.post_json( f"/contracts/{cid}/contract/sign", { @@ -360,7 +383,7 @@ def drive_flow( "signerDisplayName": "Seed Customer", "consentText": "I agree.", "otp": otp, - "otpPhone": OTP_PHONE, + "otpPhone": otp_phone, }, ) @@ -429,7 +452,6 @@ def require_config() -> None: ("CUSTOMER_PASSWORD", CUSTOMER_PASSWORD), ("ADMIN_EMAIL", ADMIN_EMAIL), ("ADMIN_PASSWORD", ADMIN_PASSWORD), - ("OTP_PHONE", OTP_PHONE), ] if not val ] @@ -453,6 +475,9 @@ def main() -> None: customer = login(CUSTOMER_EMAIL, CUSTOMER_PASSWORD, "customer") admin = login(ADMIN_EMAIL, ADMIN_PASSWORD, "admin") + otp_phone = resolve_otp_phone(customer) + print(f"OTP phone: {otp_phone}") + print("Loading reference data...") ref = load_ref_data(admin) @@ -461,7 +486,7 @@ def main() -> None: for n in range(1, CONTRACTS_PER_FLOW + 1): tag = f"[{flow.label} #{n}]" try: - res = drive_flow(flow, n, customer, admin, ref) + res = drive_flow(flow, n, customer, admin, ref, otp_phone) print(f" OK {tag} {res['reference']} -> {res['status']}") results.append(res) except Exception as exc: # noqa: BLE001 — report and continue