receipter-pro / backend /identity.py
M8ion deploy
Deploy backend
45dc2a1
Raw History Blame Contribute Delete
3.62 kB
"""Identity check (KYC), level 1: the recommended fields from docs/product/account-types.md section 6.
Stored at identity/{uid}. A submission is "submitted" until a person on our side reviews it; nothing
here verifies anything or gates any action yet. ID numbers and uploads are not collected at this level.
"""
from __future__ import annotations
import re
from datetime import datetime, timezone
from backend import accounts
from backend import firestore_client as db
# (key, label, required) in the order they are shown.
FIELDS = {
accounts.ORGANIZATION: [
("legal_name", "Legal name", True),
("trading_name", "Trading name", False),
("registration_number", "Registration number (RC)", True),
("country", "Country of registration", True),
("address", "Registered address", True),
("business_type", "Business type", True),
("contact_name", "Contact person", True),
("contact_role", "Their role", True),
("contact_phone", "Their phone", True),
("website", "Website or social page", True),
],
accounts.INDIVIDUAL: [
("legal_name", "Legal name", True),
("date_of_birth", "Date of birth", True),
("phone", "Phone", True),
("address", "Home or work address", True),
("occupation", "What you send mail for", False),
("website", "Website or social page", False),
],
}
# Filled in by the map picker alongside country and address; kept if present, never required.
PHONE_KEYS = ("phone", "contact_phone")
PLACE_EXTRAS = ("state", "lga", "postal_code")
def _path(uid: str) -> str:
return f"identity/{uid}"
def describe(uid: str) -> dict:
account_type = accounts.account_type_of(uid)
if not account_type:
raise accounts.AccountError("Choose an account type first")
doc = db.get_doc(_path(uid)) or {}
return {
"status": doc.get("status", "not_started"),
"values": doc.get("values", {}),
"submitted_at": doc.get("submitted_at"),
"fields": [{"key": k, "label": l, "required": r} for k, l, r in FIELDS[account_type]],
}
def status_of(uid: str) -> str:
return (db.get_doc(_path(uid)) or {}).get("status", "not_started")
def submit(uid: str, values: dict) -> dict:
account_type = accounts.account_type_of(uid)
if not account_type:
raise accounts.AccountError("Choose an account type first")
clean: dict[str, str] = {}
for key, label, required in FIELDS[account_type]:
value = str(values.get(key) or "").strip()[:300]
if required and not value:
raise accounts.AccountError(f"{label} is needed")
if key in PHONE_KEYS and value and not re.fullmatch(r"\+\d{1,4} \d{6,14}", value):
raise accounts.AccountError(f"{label} should be a calling code and digits only")
if value:
clean[key] = value
if clean.get("country") == "Nigeria":
rc = clean.get("registration_number")
if rc and not re.fullmatch(r"(?i)(RC|BN|IT|LP|LLP)\s?-?\s?\d{4,8}", rc):
raise accounts.AccountError("Registration number should look like RC1234567")
code = str(values.get("postal_code") or "").strip()
if code and not re.fullmatch(r"\d{6}", code):
raise accounts.AccountError("Postal code should be 6 digits")
for key in PLACE_EXTRAS:
value = str(values.get(key) or "").strip()[:300]
if value:
clean[key] = value
db.set_doc(_path(uid), {"status": "submitted", "values": clean, "submitted_at": datetime.now(timezone.utc).isoformat()})
return describe(uid)