md-platform

admin-premium-frontend.md
View raw Back to list

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)

{
  "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:

PUT request body (UpdateChannelPremiumCommand)

{
  "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:

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<PremiumSubscriptionDto>)

{
  "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 activePeriodStartactivePeriodEnd nextBillingDate (cancel, refund — separate endpoints)

Filters bar

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