# Admin / Studio Premium — Frontend Integration Covers two flows: 1. Configuring a channel's premium plans (Update Channel Premium) 2. Listing premium subscriptions Both flows are server-rendered through `PicTv.Api` minimal-API endpoints. All examples assume the standard auth cookie/header used by the existing studio/admin clients. --- ## 1. Update Channel Premium ### Endpoints | Method | Path | Purpose | |---|---|---| | `GET` | `/studio/v1/channels/{id}/premium` | Read current premium config for the channel (existing plans, prices, currency, active flag). | | `PUT` | `/studio/v1/channels/{id}/premium` | Create or update the channel's premium plans on Stripe + DB. | > Note on the route prefix: this lives under `/studio/v1/...`, not `/admin/v1/...`. It is gated by `CreatorAuthorizationFilter`, so the caller must be the channel's creator. If a back-office admin needs to edit any channel's plans, a parallel `/admin/v1/channels/{id}/premium` route + handler is required — it does not exist today. ### `GET` response shape (`StudioPremiumProductDto`) ```json { "id": "prd_…", "entityType": 1, "entityId": "ch_…", "isActive": true, "premiumPlans": [ { "id": "pln_…", "name": "Channel - Monthly", "price": 9.99, "currency": "brl", "type": 3 }, { "id": "pln_…", "name": "Channel - SixMonth", "price": 49.99, "currency": "brl", "type": 5 }, { "id": "pln_…", "name": "Channel - Yearly", "price": 89.99, "currency": "brl", "type": 4 } ] } ``` `type` is `PremiumPlanType`: - `0` Trial - `1` Daily - `2` Weekly - `3` Monthly - `4` Yearly - `5` SixMonth ### `PUT` request body (`UpdateChannelPremiumCommand`) ```json { "isPremium": true, "monthly": true, "sixMonth": true, "yearly": true, "monthlyPrice": 9.99, "sixMonthPrice": 49.99, "yearlyPrice": 89.99, "trialDays": 7, "currency": "brl" } ``` Field semantics: | Field | Type | Notes | |---|---|---| | `isPremium` | bool | Master switch. The PUT no-ops if `false` (and no plan flag is on). | | `monthly`, `sixMonth`, `yearly` | bool | Per-plan toggles. At least one must be `true` for the request to do anything. | | `monthlyPrice`, `sixMonthPrice`, `yearlyPrice` | decimal | Amount in the chosen currency (e.g. `9.99` BRL = 999 cents Stripe-side). | | `trialDays` | int? | Optional, applied to **all** prices created in this request. Stored on the Stripe Price's `recurring.trial_period_days`. | | `currency` | string | ISO 4217 lowercase, default `brl`. | ### Behavior the UI must reflect 1. **Plans are created on first save, then become immutable Stripe Prices.** Once a plan exists for a channel + type, the backend will not recreate it. The UI should treat existing plans as read-only for `price` and `trialDays` after creation — only the toggle and currency for *new* plans matter. 2. **Editing a price when active subs exist throws.** The handler currently rejects with `"There are active subscriptions for the {monthly|six-month|yearly} plan. You cannot update this plan!"` — surface this as an inline error on the price field, not a toast, so the creator knows which row is the problem. 3. **Trial is per-Price, not per-Subscription.** Changing `trialDays` after a plan exists is silently ignored (Stripe Prices are immutable). Disable the trial field after first save, or label it "Trial — set at creation time only". 4. **No partial success rollback.** If the monthly plan creates but the yearly fails (e.g. Stripe error), the monthly is already persisted. Re-submitting is safe — the handler skips plans that already exist. ### Error responses | HTTP | When | Body | |---|---|---| | `404` | Channel not found for the caller | `NotFoundResponse` | | `400` | Stripe failure, validation, or "active subscriptions exist" | `BadRequestResponse` with `message` | ### Suggested form layout ``` [ ] Enable premium for this channel (isPremium) Currency: [BRL ▼] (currency) Trial period (days, applied to all plans): [ 7 ] (trialDays) [ ] Monthly Price: [ 9.99 ] (monthly + monthlyPrice) [ ] Six months Price: [ 49.99 ] (sixMonth + sixMonthPrice) [ ] Yearly Price: [ 89.99 ] (yearly + yearlyPrice) [ Save ] ``` On mount: `GET /studio/v1/channels/{id}/premium` → prefill toggles, prices, disable already-created rows' price/trial inputs. --- ## 2. List Premium Subscriptions ### Endpoint | Method | Path | Auth | |---|---|---| | `GET` | `/admin/v1/premium/subscriptions` | `UserAuthorizationFilter` | ### Query parameters | Param | Type | Required | Notes | |---|---|---|---| | `page` | int | yes | 1-based. | | `pageSize` | int | no | Defaults to `10`. | | `channelId` | string | no | When provided, filters to that channel and forces `entityType = Channel`. | | `status` | `PremiumSubscriptionStatus` | no | Numeric enum. See table below. | `PremiumSubscriptionStatus` values: - `0` Pending - `1` Active - `2` Expired - `3` Canceled - `4` Cancelling - `5` Retry - `6` Refunded - `7` Trial - `8` GrantAccess > **Important caveat about the current handler.** Despite the route being under `/admin/v1/...`, the handler filters by `x.UserId == _applicationContext.User.Id` — i.e. it only returns subscriptions belonging to the **calling user**, not all users in the system. If the admin UI needs a system-wide list, the handler needs to be changed (drop the `UserId` filter, or accept an explicit `userId` query param). Document this with backend before shipping the admin screen. ### Response shape (`Pagination`) ```json { "items": [ { "id": "sub_…", "entityType": 1, "entityId": "ch_…", "status": 1, "startDate": "2026-01-15T10:00:00Z", "endDate": null, "activePeriodStart": "2026-05-15T10:00:00Z", "activePeriodEnd": "2026-06-15T10:00:00Z", "nextBillingDate": "2026-06-15T10:00:00Z", "canceledAt": null, "planType": 3, "entity": { "id": "ch_…", "name": "Channel Name", "image": "https://…" } } ], "totalCount": 42 } ``` `entity` is populated by the backend for channel subscriptions (`entityType = 1`). Other entity types return `entity = null` today. ### Suggested table layout | Channel | Plan | Status | Period | Next billing | Actions | |---|---|---|---|---|---| | `entity.image` + `entity.name` | label of `planType` | colored chip from `status` | `activePeriodStart` → `activePeriodEnd` | `nextBillingDate` | (cancel, refund — separate endpoints) | ### Filters bar - **Status** dropdown — multi-state, defaults "All". - **Channel** typeahead → sets `channelId`. - **Page size** — 10 / 25 / 50. ### Related endpoints (not covered in depth here) | Method | Path | Purpose | |---|---|---| | `POST` | `/admin/v1/premium/grant-access` | `GrantStudioPremiumAccessCommand { userId }` — grant free access. | | `PUT` | `/admin/v1/premium/remove-access` | `RevokeStudioPremiumAccessCommand` — revoke a granted access. | | `POST` | `/client/v1/premium/subscriptions/cancel` | Cancel at period end (called from the user-facing app, not admin). | --- ## Open items / TODO before shipping - [ ] Confirm whether `/admin/v1/premium/subscriptions` should drop the `UserId` filter for true admin scope. - [ ] Decide on price-update strategy when active subscriptions exist (grandfather vs migrate vs block — currently blocks). See `docs/channel-tiered-subscriptions.md` and the `UpdateChannelPremiumCommandHandler` review. - [ ] No dedicated `admin/v1/channels/{id}/premium` route exists; if back-office admins need to edit any channel's premium config (not just creators editing their own), add it.