resolve OTP phone from customer profile with fallback option

This commit is contained in:
Marshal
2026-07-05 19:08:49 +00:00
parent 3e2299960a
commit 048dd05292

View File

@@ -53,9 +53,13 @@ CUSTOMER_PASSWORD = os.getenv("CUSTOMER_PASSWORD", "")
ADMIN_EMAIL = os.getenv("ADMIN_EMAIL", "") ADMIN_EMAIL = os.getenv("ADMIN_EMAIL", "")
ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD", "") ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD", "")
# Phone the sign-OTP is sent to and read back from Postgres. Independent of the # Phone the sign-OTP is sent to and read back from Postgres. Resolved at runtime
# logged-in user — the sign endpoint keys the OTP purely on this number. # 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 = 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 connection used ONLY to read the plaintext sign-OTP from freight.otp_verifications.
DB_HOST = os.getenv("DB_HOST", "localhost") 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 # 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: def send_otp(customer: Client, phone: str) -> None:
# POST /api/otp/send is @Public — no token needed, but sending one is harmless. # POST /api/otp/send is @Public — no token needed, but sending one is harmless.
customer.post_json("/otp/send", {"phone": phone}) customer.post_json("/otp/send", {"phone": phone})
@@ -321,7 +344,7 @@ def waafi_file_tuple(field_name: str) -> tuple:
def drive_flow( 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]: ) -> dict[str, Any]:
result: dict[str, Any] = {"flow": flow.label, "n": idx, "status": None} 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") admin.post_json(f"/contracts/{cid}/contract/generate")
# S6 — customer sign (needs OTP) → SIGNED_CUSTOMER # 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 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( customer.post_json(
f"/contracts/{cid}/contract/sign", f"/contracts/{cid}/contract/sign",
{ {
@@ -360,7 +383,7 @@ def drive_flow(
"signerDisplayName": "Seed Customer", "signerDisplayName": "Seed Customer",
"consentText": "I agree.", "consentText": "I agree.",
"otp": otp, "otp": otp,
"otpPhone": OTP_PHONE, "otpPhone": otp_phone,
}, },
) )
@@ -429,7 +452,6 @@ def require_config() -> None:
("CUSTOMER_PASSWORD", CUSTOMER_PASSWORD), ("CUSTOMER_PASSWORD", CUSTOMER_PASSWORD),
("ADMIN_EMAIL", ADMIN_EMAIL), ("ADMIN_EMAIL", ADMIN_EMAIL),
("ADMIN_PASSWORD", ADMIN_PASSWORD), ("ADMIN_PASSWORD", ADMIN_PASSWORD),
("OTP_PHONE", OTP_PHONE),
] ]
if not val if not val
] ]
@@ -453,6 +475,9 @@ def main() -> None:
customer = login(CUSTOMER_EMAIL, CUSTOMER_PASSWORD, "customer") customer = login(CUSTOMER_EMAIL, CUSTOMER_PASSWORD, "customer")
admin = login(ADMIN_EMAIL, ADMIN_PASSWORD, "admin") admin = login(ADMIN_EMAIL, ADMIN_PASSWORD, "admin")
otp_phone = resolve_otp_phone(customer)
print(f"OTP phone: {otp_phone}")
print("Loading reference data...") print("Loading reference data...")
ref = load_ref_data(admin) ref = load_ref_data(admin)
@@ -461,7 +486,7 @@ def main() -> None:
for n in range(1, CONTRACTS_PER_FLOW + 1): for n in range(1, CONTRACTS_PER_FLOW + 1):
tag = f"[{flow.label} #{n}]" tag = f"[{flow.label} #{n}]"
try: 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']}") print(f" OK {tag} {res['reference']} -> {res['status']}")
results.append(res) results.append(res)
except Exception as exc: # noqa: BLE001 — report and continue except Exception as exc: # noqa: BLE001 — report and continue