""" PNA Assistant — Stripe Webhook Handler POST /webhook → Stripe sends checkout.session.completed events here → We generate a PNA-PRO-XXXXX code and email it to the customer Run standalone: uvicorn webhook:app --host 0.0.0.0 --port 8080 In Docker: separate service in docker-compose.yml """ import hashlib import hmac import json import logging import os import random import string from datetime import datetime, timezone from pathlib import Path import boto3 from botocore.exceptions import ClientError from fastapi import FastAPI, HTTPException, Request from fastapi.responses import JSONResponse # ─── Config ────────────────────────────────────────────────────────────────── STRIPE_WEBHOOK_SECRET = os.getenv("STRIPE_WEBHOOK_SECRET", "") AWS_REGION = os.getenv("AWS_REGION", "eu-west-2") SES_FROM_EMAIL = os.getenv("SES_FROM_EMAIL", "lincoln@clinyqai.com") # Where we persist issued codes (append-only JSONL) CODES_LOG = Path("/app/codes.jsonl") logging.basicConfig(level=logging.INFO) log = logging.getLogger("pna-webhook") app = FastAPI(title="PNA Assistant Webhook", docs_url=None, redoc_url=None) # ─── Helpers ───────────────────────────────────────────────────────────────── def _generate_code(tier: str = "PRO") -> str: """Generate a unique activation code e.g. PNA-PRO-A7K2M9""" suffix = "".join(random.choices(string.ascii_uppercase + string.digits, k=6)) return f"PNA-{tier}-{suffix}" def _verify_stripe_signature(payload: bytes, sig_header: str, secret: str) -> bool: """ Validate Stripe-Signature header. Stripe uses HMAC-SHA256 with the raw request body. """ if not secret: log.warning("STRIPE_WEBHOOK_SECRET not set — skipping signature verification") return True # dev mode only; set the secret in production try: parts = {k: v for part in sig_header.split(",") for k, v in [part.split("=", 1)]} timestamp = parts.get("t", "") v1_sig = parts.get("v1", "") signed_payload = f"{timestamp}.".encode() + payload expected = hmac.new( secret.encode(), signed_payload, hashlib.sha256, ).hexdigest() return hmac.compare_digest(expected, v1_sig) except Exception as exc: log.error("Signature verification error: %s", exc) return False def _log_code(email: str, code: str, plan: str, session_id: str) -> None: """Append issued code to local JSONL log for admin audit trail.""" CODES_LOG.parent.mkdir(parents=True, exist_ok=True) record = { "ts": datetime.now(timezone.utc).isoformat(), "email": email, "code": code, "plan": plan, "session_id": session_id, } with CODES_LOG.open("a") as f: f.write(json.dumps(record) + "\n") log.info("Code issued: %s → %s (%s)", email, code, plan) def _send_activation_email(email: str, code: str, plan: str) -> bool: """ Send activation code to customer via Amazon SES. Returns True if sent, False on error. """ label = "Pro" if plan == "PRO" else "Institution" price = "£9.99/month" if plan == "PRO" else "£99/month" body_html = f"""
Professional Nurse Advocate AI — {label} Plan
Thank you for subscribing to PNA Assistant {label} ({price}).
Your activation code is:
How to activate:
Your {label} plan includes:
If you have any questions, reply to this email or contact
lincoln@clinyqai.com.
This tool is for educational purposes only and does not replace clinical supervision
or your employer's formal governance processes.
Contains public sector information licensed under the
Open Government Licence v3.0
— NHS England.