File size: 5,166 Bytes
afd56bc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
import os
import stripe
from fastapi import APIRouter, Request, HTTPException
from core.subscription.db import SessionLocal
from core.subscription.models import User
from clerk_backend_api import Clerk

stripe.api_key = os.getenv("STRIPE_SECRET_KEY")
endpoint_secret = os.getenv("STRIPE_WEBHOOK_SECRET")

clerk_secret = os.getenv("CLERK_SECRET_KEY")
clerk = Clerk(bearer_auth=clerk_secret) if clerk_secret else None

router = APIRouter(prefix="/api/webhooks", tags=["webhooks"])


@router.post("/clerk")
async def clerk_webhook(request: Request):
    import json

    payload = await request.body()
    try:
        data = json.loads(payload)
        evt_type = data.get("type")
        if evt_type == "user.created":
            clerk_id = data.get("data", {}).get("id")
            if clerk_id:
                from core.subscription.models import UserUsage

                db = SessionLocal()
                try:
                    user = db.query(User).filter(User.clerk_id == clerk_id).first()
                    if not user:
                        user = User(clerk_id=clerk_id, tier="free")
                        db.add(user)

                    usage = (
                        db.query(UserUsage)
                        .filter(UserUsage.user_id == clerk_id)
                        .first()
                    )
                    if not usage:
                        usage = UserUsage(user_id=clerk_id)
                        db.add(usage)

                    db.commit()
                except Exception:
                    db.rollback()
                finally:
                    db.close()
    except Exception:
        pass
    return {"status": "success"}


@router.post("/stripe")
async def stripe_webhook(request: Request):
    payload = await request.body()
    sig_header = request.headers.get("stripe-signature")

    if not endpoint_secret:
        raise HTTPException(
            status_code=500,
            detail="Trzeba ustawić STRIPE_WEBHOOK_SECRET w zmiennych ENV. Względy bezpieczeństwa!",
        )

    try:
        event = stripe.Webhook.construct_event(payload, sig_header, endpoint_secret)
    except ValueError:
        raise HTTPException(status_code=400, detail="Invalid payload")
    except stripe.error.SignatureVerificationError:
        raise HTTPException(status_code=400, detail="Invalid signature")

    if event["type"] == "checkout.session.completed":
        session = event["data"]["object"]
        clerk_id = session.get("client_reference_id")
        customer_id = session.get("customer")
        subscription_id = session.get("subscription")

        if clerk_id:
            await activate_subscription(clerk_id, customer_id, subscription_id, "pro")

    elif event["type"] == "customer.subscription.deleted":
        subscription = event["data"]["object"]
        await disable_subscription_by_sub_id(subscription.get("id"))

    return {"status": "success"}


async def activate_subscription(
    clerk_id: str, customer_id: str, subscription_id: str, tier: str
):
    # Aktualizacja 2-fazowa - najpierw uderzamy do systemu zew (Clerk), jeżeli się uda zatwierdzamy bazę do stanu aktualnego
    if clerk:
        try:
            clerk.users.update_user(
                clerk_id, public_metadata={"stripe_subscription": tier}
            )
        except Exception as e:
            from core.audit_logger import audit_log

            try:
                audit_log(
                    "ERROR", f"Failed Clerk sub update - DB ROLLBACK triggered: {e}"
                )
            except Exception:
                pass
            return  # Nie uderzamy o bazę jeśli Clerk odrzucił (fail fast)

    db = SessionLocal()
    try:
        user = db.query(User).filter(User.clerk_id == clerk_id).first()
        if not user:
            user = User(clerk_id=clerk_id)
            db.add(user)

        user.tier = tier
        user.stripe_customer_id = customer_id
        user.stripe_subscription_id = subscription_id
        db.commit()
    except Exception as e:
        db.rollback()
        from core.audit_logger import audit_log

        try:
            audit_log("ERROR", f"Failed DB sub update po pomyslnym Clerk: {e}")
        except Exception:
            pass
        # System jest trochę rozjechany: Clerk załapał PRO, baza nie załapała
        # W checker.py od teraz uderzamy Clerk jako główne źródło wiedzy więc check limits zadziała dla PRO
    finally:
        db.close()


async def disable_subscription_by_sub_id(subscription_id: str):
    db = SessionLocal()
    try:
        user = (
            db.query(User)
            .filter(User.stripe_subscription_id == subscription_id)
            .first()
        )
        if user:
            clerk_id = user.clerk_id

            if clerk:
                try:
                    clerk.users.update_user(
                        clerk_id, public_metadata={"stripe_subscription": "free"}
                    )
                except Exception:
                    pass

            user.tier = "free"
            db.commit()
    except Exception:
        db.rollback()
    finally:
        db.close()