From 624bfb41999bb2fcf2b76bfac77006e6daf39bbd Mon Sep 17 00:00:00 2001 From: Manan Puri Date: Sat, 6 Jun 2026 23:44:39 +0530 Subject: [PATCH 1/8] docs: add subscription router examples and webhook testing tips --- backend/app/routers/subscribe.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/backend/app/routers/subscribe.py b/backend/app/routers/subscribe.py index 266007a4..9ef29d02 100644 --- a/backend/app/routers/subscribe.py +++ b/backend/app/routers/subscribe.py @@ -12,6 +12,33 @@ router = APIRouter(tags=["subscribe"]) +""" +Subscription API Usage Examples +================================ + +1. SUBSCRIBE (Add email to digest): + POST /subscribe/ + Body: {"email": "user@example.com"} + + Response: {"message": "You're subscribed!", "email": "user@example.com"} + +2. UNSUBSCRIBE (Remove email): + POST /subscribe/unsubscribe + Body: {"email": "user@example.com", "token": "abc123"} + + Response: {"message": "You've been unsubscribed.", "email": "user@example.com"} + +3. UNSUBSCRIBE via GET (One-click email link): + GET /subscribe/unsubscribe?email=user@example.com&token=abc123 + + Response: {"message": "You've been unsubscribed."} + +4. WEBHOOK TESTING TIP: + Use ngrok to test locally: + - Install: pip install ngrok + - Run: ngrok http 8000 + - Use generated URL for webhook callbacks +""" @router.post("/", response_model=SubscribeResponse) def subscribe(body: SubscribeRequest, db: Session = Depends(get_db)): From 9e1975ef6bd7ec4e51ea03e082e12a7841182bf5 Mon Sep 17 00:00:00 2001 From: Manan Puri Date: Sun, 14 Jun 2026 20:01:45 +0530 Subject: [PATCH 2/8] fix: format subscribe.py with black formatter --- backend/app/routers/subscribe.py | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/app/routers/subscribe.py b/backend/app/routers/subscribe.py index 9ef29d02..075011ce 100644 --- a/backend/app/routers/subscribe.py +++ b/backend/app/routers/subscribe.py @@ -40,6 +40,7 @@ - Use generated URL for webhook callbacks """ + @router.post("/", response_model=SubscribeResponse) def subscribe(body: SubscribeRequest, db: Session = Depends(get_db)): """Subscribe an email address to the weekly digest. From 5432e65f8186f26849519f9206ec5f78a36d86a2 Mon Sep 17 00:00:00 2001 From: Manan Puri Date: Wed, 17 Jun 2026 10:26:55 +0530 Subject: [PATCH 3/8] fix: resolve flake8 lint errors in subscribe.py --- backend/app/routers/subscribe.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/backend/app/routers/subscribe.py b/backend/app/routers/subscribe.py index 075011ce..fe05dd79 100644 --- a/backend/app/routers/subscribe.py +++ b/backend/app/routers/subscribe.py @@ -11,28 +11,20 @@ from ..models import DigestSubscription router = APIRouter(tags=["subscribe"]) - """ Subscription API Usage Examples ================================ - 1. SUBSCRIBE (Add email to digest): POST /subscribe/ Body: {"email": "user@example.com"} - Response: {"message": "You're subscribed!", "email": "user@example.com"} - 2. UNSUBSCRIBE (Remove email): POST /subscribe/unsubscribe Body: {"email": "user@example.com", "token": "abc123"} - Response: {"message": "You've been unsubscribed.", "email": "user@example.com"} - 3. UNSUBSCRIBE via GET (One-click email link): GET /subscribe/unsubscribe?email=user@example.com&token=abc123 - Response: {"message": "You've been unsubscribed."} - 4. WEBHOOK TESTING TIP: Use ngrok to test locally: - Install: pip install ngrok From da56368b3976fadeff0ba080856f3eb7e725f7fd Mon Sep 17 00:00:00 2001 From: Manan Puri Date: Wed, 17 Jun 2026 14:04:42 +0530 Subject: [PATCH 4/8] fix: sort imports with isort --- backend/app/routers/subscribe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/app/routers/subscribe.py b/backend/app/routers/subscribe.py index fe05dd79..3649036b 100644 --- a/backend/app/routers/subscribe.py +++ b/backend/app/routers/subscribe.py @@ -6,9 +6,9 @@ from sqlalchemy.orm import Session from ..database import get_db +from ..models import DigestSubscription from ..schemas import SubscribeRequest, SubscribeResponse, UnsubscribeRequest from ..services.email_service import _generate_token -from ..models import DigestSubscription router = APIRouter(tags=["subscribe"]) """ From 94550d3490ecfcfe218ee23f3c1ac62741290638 Mon Sep 17 00:00:00 2001 From: Manan Puri Date: Thu, 18 Jun 2026 21:07:09 +0530 Subject: [PATCH 5/8] docs: add detailed endpoint documentation with examples and webhook lifecycle details --- backend/app/routers/subscribe.py | 117 +++++++++++++++++++++++++------ 1 file changed, 96 insertions(+), 21 deletions(-) diff --git a/backend/app/routers/subscribe.py b/backend/app/routers/subscribe.py index 3649036b..5e3362b8 100644 --- a/backend/app/routers/subscribe.py +++ b/backend/app/routers/subscribe.py @@ -11,32 +11,41 @@ from ..services.email_service import _generate_token router = APIRouter(tags=["subscribe"]) -""" -Subscription API Usage Examples -================================ -1. SUBSCRIBE (Add email to digest): - POST /subscribe/ - Body: {"email": "user@example.com"} - Response: {"message": "You're subscribed!", "email": "user@example.com"} -2. UNSUBSCRIBE (Remove email): - POST /subscribe/unsubscribe - Body: {"email": "user@example.com", "token": "abc123"} - Response: {"message": "You've been unsubscribed.", "email": "user@example.com"} -3. UNSUBSCRIBE via GET (One-click email link): - GET /subscribe/unsubscribe?email=user@example.com&token=abc123 - Response: {"message": "You've been unsubscribed."} -4. WEBHOOK TESTING TIP: - Use ngrok to test locally: - - Install: pip install ngrok - - Run: ngrok http 8000 - - Use generated URL for webhook callbacks -""" @router.post("/", response_model=SubscribeResponse) def subscribe(body: SubscribeRequest, db: Session = Depends(get_db)): """Subscribe an email address to the weekly digest. + Endpoint: POST /subscribe/ + + Request Body: + email (str): The email address to subscribe. + + Example Request: + POST /subscribe/ + { + "email": "user@example.com" + } + + Example Response (200 OK): + { + "message": "You're subscribed! You'll receive your first digest next Sunday.", + "email": "user@example.com" + } + + Example Response (409 Conflict - already subscribed): + { + "detail": "This email is already subscribed to the weekly digest." + } + + Subscription Lifecycle: + - New email -> creates an active subscription record. + - Previously unsubscribed email -> re-activates the existing + record and issues a new unsubscribe_token (old token becomes + invalid for security reasons). + - Already active email -> returns 409 Conflict. + If the email was previously subscribed but unsubscribed, this re-activates the subscription rather than creating a duplicate. """ @@ -77,7 +86,37 @@ def subscribe(body: SubscribeRequest, db: Session = Depends(get_db)): def unsubscribe(body: UnsubscribeRequest, db: Session = Depends(get_db)): """Unsubscribe an email address from the weekly digest. + Endpoint: POST /subscribe/unsubscribe + + Request Body: + email (str): The subscribed email address. + token (str): The unsubscribe_token issued at subscription time. + + Example Request: + POST /subscribe/unsubscribe + { + "email": "user@example.com", + "token": "abc123" + } + + Example Response (200 OK): + { + "message": "You've been unsubscribed from the weekly digest.", + "email": "user@example.com" + } + + Example Response (404 Not Found): + { + "detail": "Subscription not found or already inactive." + } + + Example Response (403 Forbidden - wrong token): + { + "detail": "Invalid unsubscribe token." + } + Requires both the email and its unsubscribe token for verification. + This prevents anyone from unsubscribing an email they don't own. """ email = body.email.strip().lower() @@ -112,7 +151,43 @@ def unsubscribe_via_get( token: str = Query(...), db: Session = Depends(get_db), ): - """GET-based unsubscribe for one-click links in email.""" + """GET-based unsubscribe for one-click links in email. + + Endpoint: GET /subscribe/unsubscribe + + Query Parameters: + email (str): The subscribed email address. + token (str): The unsubscribe_token issued at subscription time. + + Example Request: + GET /subscribe/unsubscribe?email=user@example.com&token=abc123 + + Example Response (200 OK): + { + "message": "You've been unsubscribed from the weekly digest." + } + + Example Response (already inactive): + { + "message": "Subscription not found or already inactive." + } + + Example Response (invalid token): + { + "message": "Invalid unsubscribe link." + } + + Why This Endpoint Exists (Webhook/Email Callback Use Case): + Email clients allow one-click unsubscribe links to be plain + GET requests (no JSON body needed). This makes the endpoint + usable directly inside an email template, e.g.: + + https://yourapp.com/subscribe/unsubscribe?email={{email}}&token={{token}} + + This is the same pattern used for webhook-style callbacks + where an external service (like an email provider) needs to + hit a URL directly without constructing a POST request body. + """ sub = ( db.query(DigestSubscription) .filter( From 2ff15b53fa37e9a9a2cb41a849072d68bb2e7a4f Mon Sep 17 00:00:00 2001 From: Manan Puri Date: Fri, 19 Jun 2026 22:17:47 +0530 Subject: [PATCH 6/8] docs: add end-to-end subscription integration guide --- docs/SUBSCRIPTION_GUIDE.md | 119 +++++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 docs/SUBSCRIPTION_GUIDE.md diff --git a/docs/SUBSCRIPTION_GUIDE.md b/docs/SUBSCRIPTION_GUIDE.md new file mode 100644 index 00000000..078be6c8 --- /dev/null +++ b/docs/SUBSCRIPTION_GUIDE.md @@ -0,0 +1,119 @@ +# Subscription Integration Guide + +This guide walks through the complete end-to-end flow for integrating +with the weekly digest subscription API. + +--- + +## End-to-End Flow + +### Step 1: Subscribe + +Send a POST request to subscribe an email address: + + POST /subscribe/ + Content-Type: application/json + + { + "email": "user@example.com" + } + +Response (200 OK): + + { + "message": "You're subscribed! You'll receive your first digest next Sunday.", + "email": "user@example.com" + } + +Response (409 Conflict - already subscribed): + + { + "detail": "This email is already subscribed to the weekly digest." + } + +--- + +### Step 2: Email/Webhook Delivery + +Once subscribed, the system sends a weekly digest email every Sunday. +The email contains an unsubscribe link: + + https://yourapp.com/subscribe/unsubscribe?email=user@example.com&token=abc123 + +The token is a unique unsubscribe_token generated at subscription time. + +Webhook Callback Testing (Local Development): + + pip install ngrok + ngrok http 8000 + +Use the generated URL for webhook testing: + + https://abc123.ngrok.io/subscribe/unsubscribe?email=user@example.com&token=abc123 + +--- + +### Step 3: Unsubscribe + +Option A - One-Click Link (GET): +User clicks the unsubscribe link in the email: + + GET /subscribe/unsubscribe?email=user@example.com&token=abc123 + +Response (200 OK): + + { + "message": "You've been unsubscribed from the weekly digest." + } + +Option B - API Call (POST): + + POST /subscribe/unsubscribe + Content-Type: application/json + + { + "email": "user@example.com", + "token": "abc123" + } + +Response (200 OK): + + { + "message": "You've been unsubscribed from the weekly digest.", + "email": "user@example.com" + } + +--- + +## Subscription Lifecycle Summary + + New Email + | + v + POST /subscribe/ -> Active Subscription Created + | + v + Weekly Digest Email Sent (every Sunday) + | + v + User Clicks Unsubscribe Link + | + v + GET /subscribe/unsubscribe -> Subscription Deactivated + | + v + Re-subscribe Anytime + | + v + POST /subscribe/ -> Subscription Re-activated (new token issued) + +--- + +## Error Reference + +| Status Code | Meaning | +|-------------|------------------------------| +| 200 | Success | +| 409 | Email already subscribed | +| 404 | Subscription not found | +| 403 | Invalid unsubscribe token | \ No newline at end of file From f54966d638f1ba9dec0ddad5d89995be7101d005 Mon Sep 17 00:00:00 2001 From: Manan Puri Date: Tue, 23 Jun 2026 14:07:15 +0530 Subject: [PATCH 7/8] revert: restore pull_request_template.md to original --- .github/pull_request_template.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index b4b76f13..e15d304c 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -20,6 +20,7 @@ Fixes # - [ ] I have not introduced duplicate issues or features - [ ] My PR title follows the format: `feat/fix/docs/test: short description` - [ ] I have added tests for new features (Level 2 and 3 issues) +- [ ] I have updated `docs/CHANGELOG.md` for user-facing changes or fixes - [ ] No hardcoded secrets or API keys in my code - [ ] This PR is linked to a GSSoC 2026 issue From aa227ca27535aa12107b41c89152b7a1f400f8b7 Mon Sep 17 00:00:00 2001 From: Manan Puri Date: Mon, 27 Jul 2026 21:59:43 +0530 Subject: [PATCH 8/8] text error tracking file added --- assets/.ipynb_checkpoints/icon-checkpoint.svg | 14 +++++++ .../logo-dark-checkpoint.svg | 31 ++++++++++++++ backend/tests/test_error_tracking.py | 40 +++++++++++++++++++ 3 files changed, 85 insertions(+) create mode 100644 assets/.ipynb_checkpoints/icon-checkpoint.svg create mode 100644 assets/.ipynb_checkpoints/logo-dark-checkpoint.svg create mode 100644 backend/tests/test_error_tracking.py diff --git a/assets/.ipynb_checkpoints/icon-checkpoint.svg b/assets/.ipynb_checkpoints/icon-checkpoint.svg new file mode 100644 index 00000000..21636a4d --- /dev/null +++ b/assets/.ipynb_checkpoints/icon-checkpoint.svg @@ -0,0 +1,14 @@ + + QyverixAI icon + QyverixAI hexagon icon mark with search lens + + + + + + + + + + + diff --git a/assets/.ipynb_checkpoints/logo-dark-checkpoint.svg b/assets/.ipynb_checkpoints/logo-dark-checkpoint.svg new file mode 100644 index 00000000..71c0468a --- /dev/null +++ b/assets/.ipynb_checkpoints/logo-dark-checkpoint.svg @@ -0,0 +1,31 @@ + + QyverixAI + QyverixAI — AI Developer Assistant, dark background version + + + + + + + + + + + + + + + + + + + + + QyverixAI + diff --git a/backend/tests/test_error_tracking.py b/backend/tests/test_error_tracking.py new file mode 100644 index 00000000..390550f1 --- /dev/null +++ b/backend/tests/test_error_tracking.py @@ -0,0 +1,40 @@ +from unittest.mock import MagicMock, patch + +from app.services.error_tracking import init_error_tracking + + +def test_error_tracking_disabled_when_no_dsn(): + """Verify init_error_tracking returns False when sentry_dsn is not set.""" + with patch("app.services.error_tracking.settings") as mock_settings: + mock_settings.sentry_dsn = None + + result = init_error_tracking() + + assert result is False + + +def test_error_tracking_enabled_when_dsn_present(): + """Verify init_error_tracking returns True when sentry_dsn is set.""" + with patch("app.services.error_tracking.settings") as mock_settings: + mock_settings.sentry_dsn = "https://fake@sentry.io/123" + mock_settings.sentry_traces_sample_rate = 1.0 + + with patch("sentry_sdk.init") as mock_sentry_init: + mock_sentry_init.return_value = None + + result = init_error_tracking() + + assert result is True + + +def test_error_tracking_returns_false_on_init_failure(): + """Verify init_error_tracking returns False when sentry init raises exception.""" + with patch("app.services.error_tracking.settings") as mock_settings: + mock_settings.sentry_dsn = "https://fake@sentry.io/123" + mock_settings.sentry_traces_sample_rate = 1.0 + + with patch("sentry_sdk.init", side_effect=Exception("Sentry failed")): + + result = init_error_tracking() + + assert result is False \ No newline at end of file