feat: outbound webhooks for revoke/hold/resume — only grant and payment fired before

Found while re-reading the subscription lifecycle routes: payment.paid
and subscription.granted_by_admin fire a webhook, but revoke (which has
existed the whole night) and the two new hold/resume routes did not.
Inconsistent for anyone actually wiring this into a CRM/support tool —
they'd see a subscription get granted but never find out it was later
paused, resumed, or cut off entirely, since only the "gains access"
side of the lifecycle was ever reported outward.

Three new events, same shape and delivery as the existing ones:
subscription.revoked, subscription.held, subscription.resumed. Added
right where the DB/xray state change already happens in each route, so
they're conditioned on the action actually succeeding (a hold attempt
on an already-held/expired subscription 400s before ever reaching the
webhooks.send call).

Verification: webhooks.py itself is unchanged — this only adds new call
sites with new event-name strings, so re-verified the exact thing the
original webhook feature proved: stood up a real local HTTP server,
fired all three new events through the actual webhooks.send(), and had
the receiver independently recompute the HMAC from its own copy of the
secret and compare against the X-Signature header it actually got, for
all three — not just trusting that the sender computed something.
Checked the JSON envelope and data payload match what each route sends
byte for byte. api.py itself still can't be imported locally, same wall
as always; the new lines were checked by reading the subscription-row
shape they pull from (tg_id/node/plan are all real columns already
confirmed present in every prior test this session) plus the standard
py_compile + pyflakes pass, clean across the whole repo.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Savsis? 2026-09-14 01:26:26 +05:00
parent 71c98c5032
commit f6e4e52b65
2 changed files with 12 additions and 2 deletions

10
api.py
View file

@ -887,6 +887,9 @@ def admin_revoke_subscription(uuid: str, request: Request):
if node:
xray_manager.remove_client_from_node(node, uuid)
db.revoke_subscription(uuid)
webhooks.send("subscription.revoked", {
"tg_id": sub["tg_id"], "node": sub["node"], "plan": sub["plan"], "subscription_uuid": uuid,
})
return {"ok": True}
@ -901,6 +904,9 @@ def admin_hold_subscription(uuid: str, request: Request):
node = db.get_node(sub["node"])
if node:
xray_manager.remove_client_from_node(node, uuid)
webhooks.send("subscription.held", {
"tg_id": sub["tg_id"], "node": sub["node"], "plan": sub["plan"], "subscription_uuid": uuid,
})
return {"ok": True}
@ -913,6 +919,10 @@ def admin_resume_subscription(uuid: str, request: Request):
node = db.get_node(resumed["node"])
if node:
xray_manager.add_client_to_node(node, uuid, email=uuid)
webhooks.send("subscription.resumed", {
"tg_id": resumed["tg_id"], "node": resumed["node"], "plan": resumed["plan"],
"subscription_uuid": uuid, "expires_at": resumed["expires_at"],
})
return {"ok": True, "expires_at": resumed["expires_at"]}