Flows
Subscription Flow
Plans, subscriptions and billing.
Store Subscription Flow
This document covers the end-to-end subscription lifecycle for stores: auto-provisioning, self-service upgrades via QiCard, admin management, and the plan limits that gate store operations.
Overview
Every store must have an active subscription to perform write operations. The system enforces plan limits via IStoreSubscriptionGuard, which is injected into all store-area services. A store without a valid subscription (expired, canceled, or missing) receives a 403 / 422 error before the business logic executes.
Subscription statuses
| Status | Meaning |
|---|---|
Trialing | Free-trial period; full plan limits apply |
Active | Paid and within the billing period |
PastDue | Payment overdue; write operations blocked |
Canceled | Manually canceled; write operations blocked |
Expired | Period ended without renewal; write operations blocked |
1. Auto-provisioning (trial subscription)
A free_trial subscription is automatically assigned in two scenarios:
- Store self-registration — after the owner completes OTP registration (
AuthService.StoreRegisterAsync),EnsureInitialTrialSubscriptionAsyncis called immediately. - Admin-created store — when an admin directly creates a pre-approved store (
StoreProvisioningService.CreateApprovedStoreWithOwnerAsync),EnsureInitialTrialSubscriptionAsyncis called.
In both cases the method looks up the plan with Code == "free_trial", creates a StoreSubscription with Status = Trialing, and sets PeriodEnd = now + TrialDays (falls back to BillingPeriodDays if TrialDays == 0). Admin approval of a pending store does not create a subscription.
Store self-registers ──► AuthService.StoreRegisterAsync ──► EnsureInitialTrialSubscriptionAsync
│
Admin creates store ──► StoreProvisioningService (pre-approved) ──► EnsureInitialTrialSubscriptionAsync
│
StoreSubscription { Status = Trialing }
2. Store — view subscription and available plans
Main area: GET /api/v1/store
Requires permission: store.subscription.view
| Endpoint | Description |
|---|---|
GET /api/v1/store/subscription | Returns current subscription details (plan, status, period, usage snapshot) |
GET /api/v1/store/subscription/plans | Lists all public plans the store can upgrade to |
3. Store — self-service upgrade flow
Requires permission: store.subscription.upgrade
Step-by-step
- Browse plans — store calls
GET /api/v1/store/subscription/plansto see available plans and pricing. - Create upgrade request — store posts to
POST /api/v1/store/subscription/upgrade-requestswith target plan ID and return URL. The service:- Creates a
StoreSubscriptionUpgradeRequestwithStatus = PendingPayment. - Initiates a QiCard hosted payment session.
- Returns
payment_urlto redirect the user.
- Creates a
- User completes payment — user is redirected to QiCard's hosted page.
- QiCard webhook — on payment completion, QiCard calls
POST /api/v1/payments/qicard/subscription-webhook. The service:- Matches the notification to the upgrade request via
GatewayRequestId. - On success: creates a new
StoreSubscription(statusActive), setsActivatedSubscriptionId, marks upgrade requestActivated. - On failure: marks upgrade request
Failed, storesFailureReason.
- Matches the notification to the upgrade request via
- Store verifies — store can poll or verify via
POST /api/v1/store/subscription/upgrade-requests/{requestId}/verifyafter returning from the payment gateway. - Check status —
GET /api/v1/store/subscription/upgrade-requests/{requestId}shows full request state.
Store ──► POST /store/subscription/upgrade-requests
│
▼
StoreSubscriptionUpgradeRequest { Status = PendingPayment, payment_url }
│
▼
User redirected ──► QiCard payment page
│
▼ (async)
QiCard ──► POST /payments/qicard/subscription-webhook
│
┌───────┴────────┐
Success Failure
│ │
New StoreSubscription UpgradeRequest { Status = Failed }
{ Status = Active }
UpgradeRequest { Status = Activated }
4. Subscription guard — enforced limits
IStoreSubscriptionGuard is called before every write operation in the store area. It resolves the store's most recent active/trialing subscription and evaluates the plan's limits.
| Guard method | Triggered by | Blocks when |
|---|---|---|
EnsureCanWriteAsync | All update/delete/cancel operations | Subscription expired, canceled, or missing |
EnsureCanCreateOrdersAsync | Create order | MaxOrdersPerPeriod quota reached |
EnsureCanUseWarehouseFulfillmentAsync | Create/update order with FulfillmentType = Warehouse | Plan does not allow warehouse fulfillment |
EnsureCanCreateProductAsync | Create product | MaxProducts limit reached |
EnsureCanCreateWarehouseAsync | Create warehouse | MaxWarehouses limit reached |
EnsureCanCreateStoreUserAsync | Create store-scoped user (RBAC) | MaxUsers limit reached |
EnsureCanCreateApiKeyAsync | Create API key | AllowsApiIntegrations = false or MaxApiKeys reached |
EnsureCanManageWebhookAsync | Create webhook / rotate secret | AllowsWebhooks = false or MaxWebhookSubscriptions reached |
EnsureCanUseIntegrationsAsync | (called by API key check) | AllowsApiIntegrations = false |
Quota errors return HTTP 422; access/status errors return HTTP 403.
5. Subscription plan features
A SubscriptionPlan defines both numeric limits and feature flags.
| Field | Type | Meaning |
|---|---|---|
MaxOrdersPerPeriod | int? | Max orders creatable within the subscription period (null = unlimited) |
MaxProducts | int? | Max active products |
MaxWarehouses | int? | Max active store-owned warehouses |
MaxUsers | int? | Max active store users |
MaxApiKeys | int? | Max active API keys |
MaxWebhookSubscriptions | int? | Max webhook subscriptions |
AllowsApiIntegrations | bool | Enables API key creation and integration endpoints |
AllowsWebhooks | bool | Enables webhook management |
AllowsWarehouseFulfillment | bool | Enables warehouse-type order fulfillment |
AllowsAdvancedReports | bool | Reserved for advanced reporting features |
AllowsAiAutomation | bool | Reserved for AI/automation features |
TrialDays | int | Trial duration for auto-provisioned subscriptions |
BillingPeriodDays | int | Default billing period (used as trial fallback) |
MonthlyPriceIqd | decimal | Display price in IQD |
6. Admin — subscription management
Main area: /api/v1/admin
Requires permission: admin.subscriptions.view or admin.subscriptions.manage
| Endpoint | Permission | Description |
|---|---|---|
GET /api/v1/admin/subscription-plans | view | List all plans (optionally including inactive) |
POST /api/v1/admin/subscription-plans | manage | Create a new plan |
PUT /api/v1/admin/subscription-plans/{planId} | manage | Update a plan |
GET /api/v1/admin/stores/{storeId}/subscription | view | View a specific store's current subscription |
POST /api/v1/admin/stores/{storeId}/subscription | manage | Manually assign a plan to a store (bypasses payment) |
POST /api/v1/admin/stores/{storeId}/subscription/cancel | manage | Cancel a store's active subscription |
GET /api/v1/admin/subscription-upgrade-requests | view | Paginated list of all upgrade requests (filterable) |
GET /api/v1/admin/subscription-upgrade-requests/{requestId} | view | View a single upgrade request |
7. New permissions
| Permission | Scope | Description |
|---|---|---|
admin.subscriptions.view | admin | View subscription plans and store subscriptions |
admin.subscriptions.manage | admin | Manage subscription plans and store subscriptions |
store.subscription.view | store | View own subscription status and available plans |
store.subscription.upgrade | store | Request and pay for subscription upgrades |
store.subscription.view is included in both store_owner and store_staff default permission sets. store.subscription.upgrade is included in store_owner only.
8. Payment webhook — /api/v1/payments/qicard/subscription-webhook
This public endpoint (no auth) is called by QiCard on subscription payment events. It is handled by IStoreSubscriptionUpgradeService.TryHandleQiCardWebhookAsync. The handler:
- Parses and validates the QiCard notification JSON.
- Matches
GatewayRequestIdto an existingStoreSubscriptionUpgradeRequest. - On confirmed payment:
- Creates a new
StoreSubscriptionfor the target plan, starting fromnow. - Updates the upgrade request with
Status = Activated,PaidAt,ActivatedAt,ActivatedSubscriptionId.
- Creates a new
- On failed payment:
- Updates the upgrade request with
Status = Failed,FailureReason.
- Updates the upgrade request with
Source: DOCS/flows/store-subscription-flow.md