# Premium Module

Premium membership for Student Bazaar. This document describes the **Phase 1
implementation** as built — a *manual, admin-activated* subscription workflow.
The approved data model background lives in
`docs/02_Architecture/Database/07_Administration.md`; the business model is
described in `docs/01_Project/Vision.md` (§9) and `docs/01_Project/Scope.md`.

---

## Purpose

Premium is one of the two revenue streams in the platform's hybrid model
(the other is advertisements). A premium member is a student with an **active,
non-expired subscription**. Premium is designed to eventually grant unlimited
messaging, an ad-free experience, a seller badge and listing visibility perks.

Because **Student Bazaar never processes payments**, Phase 1 does not integrate a
payment gateway. Instead, students pay out-of-band (UPI / bank transfer / cash)
and submit a request; a Super Admin verifies the payment and activates the
subscription. The architecture is intentionally shaped so that a payment gateway
can later replace the manual approval step **without changing the `Subscription`
model** (see *Design decisions*).

---

## Scope (Phase 1)

Implemented:

* A **manual activation workflow**: student submits a `PremiumRequest`
  describing an out-of-band payment; a Super Admin approves or rejects it.
* Approval creates an **active `Subscription`** whose duration matches the plan.
* A single source of truth for entitlement — `User.is_premium_active()` /
  `is_user_premium(user)` — resolved live from the subscription table.
* Student-facing premium overview (benefits, pricing, FAQ, payment
  instructions), a request form, and a request-status/success experience.
* Super Admin review console: request list, request detail, approve/reject.
* Premium status card on the user profile.

Deferred (kept ready, intentionally **not** surfaced in Phase 1):

* **All premium feature unlocks.** No chat, advertisement, featured-listing,
  boost, badge or analytics behaviour is gated on premium yet. Phase 2 will
  begin unlocking features by calling `current_user.is_premium_active()`.
* ~~Automated payment (a payment gateway replacing manual approval).~~ — now
  implemented, see *Online payments (Razorpay)* below. Manual approval is kept as
  a fallback.
* Auto-expiry notifications and a scheduled expiry job (expiry already takes
  effect live — see *Subscription lifecycle*).

---

## Online payments (Razorpay)

Students can now upgrade instantly online via Razorpay, in addition to the manual
out-of-band workflow (which remains a fallback for offline payers, comps and
disputes). The gateway is isolated so the Premium system never depends on it
directly and a second provider can be added without touching Premium logic:

```
Premium Service  →  Payment Service  →  Razorpay Service (SDK)
```

* **Module:** `app/payments/` — `models.py` (`Payment`), `services.py`
  (provider-agnostic), `razorpay_service.py` (the only place the SDK is used),
  `routes.py` (create-order / verify) and `webhook.py`.
* **Dedicated `payments` table** (see
  `docs/02_Architecture/Database/07_Administration.md`) records every attempt.
  The `Subscription` model is unchanged, honouring the Phase 1 design intent.
* **Server-side pricing:** the order amount is computed from the admin-configured
  `PremiumPlan.price` — never from the browser.
* **Activation reuse:** a verified capture calls
  `premium.services.activate_premium_from_payment`, which wraps the existing
  `activate_premium` plus the same "Premium Activated" notification and activity
  entry used by manual approval. No subscription logic is duplicated.
* **Security:** premium is activated only after a verified Razorpay signature
  (Checkout callback) or a signature-verified webhook. Webhooks are the
  authoritative status source and are processed idempotently, so duplicate events
  never double-activate. The webhook endpoint is CSRF-exempt (authenticated by its
  signature); the JSON endpoints are login-required and CSRF-protected. A refund
  webhook revokes premium.
* **Configuration:** `RAZORPAY_KEY_ID`, `RAZORPAY_KEY_SECRET`,
  `RAZORPAY_WEBHOOK_SECRET`, `PREMIUM_CURRENCY` (all environment-driven). The
  online upgrade button appears only when keys are configured
  (`PAYMENTS_ENABLED`); otherwise the manual flow is the sole path.

### Administration, reporting & user visibility (Increment 2)

Read-only observability layered on top of the Increment 1 processing (which is
unchanged):

* **Admin Payments dashboard** (`/admin/payments`) — KPI cards (total revenue,
  revenue this month, successful/failed/pending counts, active premium subscribers,
  success rate); a searchable/filterable payment table (status, provider, plan,
  amount range, date range; search by user name/email/order/payment id); a
  per-payment details **drawer** with a derived lifecycle **timeline** and
  copy-to-clipboard for gateway ids; and **revenue analytics** charts (revenue over
  time, status distribution, revenue by plan, provider breakdown incl. an "Offline"
  bucket from manual approvals, premium-conversion trend) reusing the shared
  Chart.js helper. CSV export respects the active filters.
* **Refunds are external.** Refunds are performed in the Razorpay Dashboard and
  only *synchronised* into CampusCart via the `refund.processed` webhook (status →
  `REFUNDED`, premium revoked, user notified). They are intentionally **not**
  surfaced as an admin metric, chart segment or filter — the dashboard reflects the
  features CampusCart provides. Refunded payments remain visible as history records
  with their synced status.
* **All admin querying** lives in `app/payments/admin_services.py` in a fixed,
  small number of grouped aggregation queries (no N+1). The timeline is **derived**
  from the payment's status + timestamps — no duplicate event storage.
* **User payment history** (`/app/payments/history`) — current subscription summary
  (plan, start, expiry, remaining days, status) plus the user's own payments, with a
  printable **receipt** (`/app/payments/receipt/<id>`). Both enforce ownership so a
  user can only ever see their own payments; no secrets/signatures are exposed.
* **Provider-agnostic:** the provider breakdown and labels are data-driven, so a
  future gateway appears automatically.

---

## Manual activation workflow

```
Student                         Super Admin
  │                                  │
  │ pays out-of-band (UPI/bank/cash) │
  │ submits PremiumRequest (PENDING) │
  ├─────────────────────────────────▶ reviews request + verifies payment
  │                                  │
  │                     ┌── approve ─┤  → PremiumRequest = APPROVED
  │                     │            │  → new Subscription = ACTIVE
  │  premium activated ◀┘            │     (start = now, end = now + plan)
  │                                  │
  │                     ┌── reject ──┤  → PremiumRequest = REJECTED
  │  no subscription   ◀┘            │     (nothing granted)
```

The `PremiumRequest` row is retained either way as an auditable record of the
decision (who reviewed it and when).

---

## Architecture

Standard module layout — routes stay thin, all business rules live in the
service layer.

```
app/premium/
    __init__.py        # premium_bp blueprint, registered at /app/premium
    services.py        # subscription lifecycle + entitlement + admin queries
    forms.py           # PremiumRequestForm (Flask-WTF, CSRF)
    routes.py          # request handlers only (student + admin)

app/models/subscription.py   # Subscription, PremiumRequest
app/templates/premium/
    index.html              # premium overview / pricing / FAQ
    request.html            # request form + payment instructions
    request_success.html    # success / already-under-review status page
    admin_requests.html     # admin review list
    admin_request_detail.html # single-request detail + approve/reject
```

Admin routes live inside the premium blueprint (guarded by `admin_required`)
rather than the `/admin` console, keeping the whole feature cohesive for Phase 1.

---

## Database schema

Two tables. `PremiumRequest` records an **intent + payment claim to verify**;
`Subscription` records a **granted entitlement**. See *Design decisions* for why
they are separate.

### `premium_requests` (`PremiumRequest`)

| Column                  | Type                                   | Notes                                   |
| ----------------------- | -------------------------------------- | --------------------------------------- |
| id                      | BIGINT (PK)                            | Surrogate key                           |
| public_id               | CHAR(36) UNIQUE                        | Used in URLs                            |
| user_id                 | BIGINT FK → users.id                   | Requesting student (indexed)            |
| plan_name               | VARCHAR(50)                            | `monthly` / `yearly`                    |
| payment_method          | VARCHAR(30)                            | `upi` / `bank_transfer` / `cash`        |
| transaction_reference   | VARCHAR(120) NULL                      | Required except for cash                |
| remarks                 | TEXT NULL                              | Free-text note for the reviewer         |
| status                  | ENUM(`pending`,`approved`,`rejected`)  | Default `pending` (indexed)             |
| requested_at            | DATETIME                               | Submission time (indexed)               |
| reviewed_at             | DATETIME NULL                          | When approved/rejected                  |
| reviewed_by             | BIGINT FK → users.id NULL              | The reviewing Super Admin (audit)       |

### `subscriptions` (`Subscription`)

| Column        | Type                                     | Notes                              |
| ------------- | ---------------------------------------- | ---------------------------------- |
| id            | BIGINT (PK)                              | Surrogate key                      |
| public_id     | CHAR(36) UNIQUE                          | Used in URLs                       |
| user_id       | BIGINT FK → users.id                     | Subscriber (indexed)               |
| plan_name     | VARCHAR(50)                              | `monthly` / `yearly`               |
| status        | ENUM(`active`,`expired`,`cancelled`)     | Default `active` (indexed)         |
| start_date    | DATETIME                                 | Activation time                    |
| end_date      | DATETIME                                 | Expiry time (indexed)              |
| activated_by  | BIGINT FK → users.id NULL                | The approving Super Admin (audit)  |
| created_at    | DATETIME                                 | From `TimestampMixin`              |
| updated_at    | DATETIME                                 | From `TimestampMixin`              |

Migration: `migrations/versions/d7e9b1f4a3c2_add_premium_subscription_tables.py`.

### Entity relationship

```
users ──1:N──▶ premium_requests   (user_id;   reviewed_by → users, audit)
users ──1:N──▶ subscriptions      (user_id; activated_by → users, audit)
```

A user has at most **one `ACTIVE` subscription** at a time; historical rows
(`EXPIRED` / `CANCELLED`) are kept so the subscription history is never lost.
A user may accumulate many `premium_requests` over time (one open `PENDING` at a
time — enforced in the service layer).

### Status enums (`app/models/enums.py`)

* `SubscriptionStatus` — `ACTIVE`, `EXPIRED`, `CANCELLED`.
* `PremiumRequestStatus` — `PENDING`, `APPROVED`, `REJECTED`.

Supporting enum members already present in the schema and reused here:
`NotificationType.PREMIUM` and `RelatedEntityType.SUBSCRIPTION`.

---

## Service layer (`app/premium/services.py`)

Entitlement:

* `get_active_subscription(user)` — the user's active, non-expired subscription
  or `None`; the **single source of truth** (`status == ACTIVE` **and**
  `end_date > now`).
* `is_user_premium(user)` — boolean wrapper over the above.

Requests & lifecycle:

* `get_pending_request(user)` — the user's open request, if any.
* `create_premium_request(user, plan_name, payment_method, reference, remarks)`
  — validates the plan, blocks a second concurrent pending request, creates a
  `PENDING` row.
* `approve_premium_request(request, admin)` — pending-only; marks the request
  `APPROVED`, records the reviewer, **cancels any existing active subscription**
  (preserving the single-active invariant + history) and creates a new `ACTIVE`
  subscription whose `end_date = now + plan.duration_days`.
* `reject_premium_request(request, admin)` — pending-only; marks the request
  `REJECTED`; grants nothing.

Admin queries:

* `list_premium_requests(status=None)` — all requests, newest first, optional
  status filter.
* `get_request_or_404(public_id)` — single-request fetch.
* `get_user_requests(user)` / `get_latest_subscription(user)` — read-only
  helpers for the profile card and status pages.

Catalog & constants: `PLAN_CATALOG` (plan label, **display-only** price,
`duration_days`, tagline), `PAYMENT_METHODS`, `CURRENCY_SYMBOL`, `plan_choices()`.
Errors: `PremiumError` and the specific `DuplicateRequestError`,
`InvalidPlanError`, `RequestNotPendingError`.

---

## User helper (`User.is_premium_active()`)

`app/models/auth.py` adds:

```python
def is_premium_active(self) -> bool:
    from app.premium.services import is_user_premium
    return is_user_premium(self)
```

It delegates to the service (lazy import to avoid a circular import at
model-load time) so premium is resolved in exactly one place. **This is the hook
Phase 2 will use** to gate premium features, e.g.
`if current_user.is_premium_active(): ...`.

---

## Routes

All under the `premium` blueprint, prefix `/app/premium`.

| Method | Path                                     | Endpoint                       | Purpose                                              |
| ------ | ---------------------------------------- | ------------------------------ | ---------------------------------------------------- |
| GET    | `/`                                      | `premium.index`                | Overview: benefits, pricing, how-it-works, FAQ.      |
| GET/POST | `/request`                             | `premium.request_premium`      | Submit a manual activation request (PRG on success). |
| GET    | `/request/success`                       | `premium.request_success`      | Success / "already under review" status page.        |
| GET    | `/admin/requests`                        | `premium.admin_requests`       | Super Admin review list.                             |
| GET    | `/admin/requests/<public_id>`            | `premium.admin_request_detail` | Full request detail + approve/reject.                |
| POST   | `/admin/requests/<public_id>/approve`    | `premium.approve_request`      | Approve → activate subscription.                     |
| POST   | `/admin/requests/<public_id>/reject`     | `premium.reject_request`       | Reject → grant nothing.                              |

Student routes require authentication. Admin routes additionally require
`admin_required` (Super Admin only; `403` otherwise). The request form redirects
to the success page after submit; visiting the form with a pending request
redirects to the same page in its "already under review" mode.

---

## Admin approval workflow

1. The Super Admin opens **Premium requests** (`admin_requests`) — a list of all
   requests, newest first, with status badges.
2. Opening a row shows the **request detail**: student, plan, payment method,
   transaction reference, remarks, request date and current status.
3. **Approve** (pending-only) verifies the payment out-of-band, marks the
   request `APPROVED`, records the admin + timestamp, and creates an `ACTIVE`
   subscription for the plan's duration.
4. **Reject** (pending-only) marks the request `REJECTED` and grants nothing.
5. Approve/reject actions are CSRF-protected POSTs behind a confirmation dialog.

---

## Subscription lifecycle

```
PENDING request ──approve──▶ ACTIVE subscription ──(end_date passes)──▶ effectively expired
                                     │
                                     └──(new approval)──▶ old one CANCELLED, new one ACTIVE
```

* **Live expiry.** `get_active_subscription` requires `end_date > now`, so an
  expired subscription loses its benefits **automatically** at read time — no
  background job is required for correctness. A future scheduled job can flip
  `status` to `EXPIRED` and send notifications, but is not needed for entitlement.
* **Single active per user.** Approving a new request while one is active
  cancels the old subscription first; history is retained.
* **History preserved.** Subscriptions and requests are never deleted.

---

## Design decisions

**Why `PremiumRequest` and `Subscription` are separate tables.**
They model two different things with different lifecycles:

* A `PremiumRequest` is an **intent + a payment claim to verify** — it can be
  pending, approved or rejected, and many can exist per user over time.
* A `Subscription` is a **granted entitlement** — it exists only after a payment
  is confirmed, and drives `is_premium_active()`.

Keeping them separate means:

* The **entitlement check stays trivial and fast** — it reads one table
  (`subscriptions`) and never has to reason about payment/review state.
* **Audit and history are clean** — rejected requests never pollute the
  subscription table, and the request table is a complete record of every
  decision.
* **Rejections and re-submissions** are naturally represented without mutating
  any granted entitlement.

**Future compatibility with payment gateways.**
The manual approval step is the *only* place that turns a verified payment into a
subscription. When a payment gateway is added (a future phase), the gateway's
success callback simply calls the **same** `approve_premium_request` path (or an
equivalent `activate_subscription(user, plan)` service) — the `Subscription`
model, `is_user_premium`, `User.is_premium_active()` and every future feature
gate remain **unchanged**. In other words, automated payment confirmation
replaces manual admin confirmation without touching the entitlement core. The
`PremiumRequest` table can then also carry a real gateway transaction id in its
existing `transaction_reference` column.

---

## Future expansion

* **Phase 2 — Feature unlocks (chat & ads).** Gate unlimited messaging on
  `current_user.is_premium_active()` (bridging into the existing
  `Conversation.is_premium_conversation` path) and suppress advertisements for
  premium users at the `inject_globals` context processor and the two
  `app/main/routes.py` ad fetches.
* **Phase 3 — Seller perks.** Premium seller badge, featured listings
  (`listings.is_featured`) and listing boosts in marketplace sort — respecting
  the "premium must not affect marketplace fairness" constraint.
* **Phase 4 — Lifecycle automation.** Scheduled expiry job flipping `ACTIVE →
  EXPIRED`, plus premium notifications (activated / nearing expiry / expired)
  using `NotificationType.PREMIUM`.
* **Phase 5 — Automated payments.** Payment-gateway integration replacing manual
  approval as described above; advanced seller analytics.

---

## What Phase 1 deliberately does **not** do

* No premium feature is unlocked anywhere yet (chat, ads, featured listings,
  boosts, badges, analytics are untouched).
* No payment is processed in-app.
* No change to the chat, marketplace, advertisement or notification modules.
