diff --git a/app/templates/admin/subscription-tiers.html b/app/templates/admin/subscription-tiers.html
index 038ac915..120f018e 100644
--- a/app/templates/admin/subscription-tiers.html
+++ b/app/templates/admin/subscription-tiers.html
@@ -144,6 +144,9 @@
+
@@ -332,6 +335,143 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Loading features...
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ No features available.
+
+
+
+
+
+
+
+ features selected
+
+
+
+
+
+
+
+
+
+
+
{% endblock %}
{% block extra_scripts %}
diff --git a/docs/features/subscription-billing.md b/docs/features/subscription-billing.md
index a291cb68..745e609f 100644
--- a/docs/features/subscription-billing.md
+++ b/docs/features/subscription-billing.md
@@ -137,6 +137,26 @@ tier.features = [
]
```
+### Admin Tier Management
+
+Administrators can manage subscription tiers at `/admin/subscription-tiers`:
+
+**Capabilities:**
+- View all tiers with stats (total, active, public, MRR)
+- Create new tiers with custom pricing and limits
+- Edit tier properties (name, pricing, limits, Stripe IDs)
+- Activate/deactivate tiers
+- Assign features to tiers via slide-over panel
+
+**Feature Assignment:**
+1. Click the puzzle-piece icon on any tier row
+2. Features are displayed grouped by category
+3. Use checkboxes to select/deselect features
+4. Use "Select all" / "Deselect all" per category
+5. Click "Save Features" to update
+
+See [Feature Gating System](../implementation/feature-gating-system.md#admin-tier-management-ui) for technical details.
+
## Limit Enforcement
Limits are enforced at the service layer:
diff --git a/docs/guides/subscription-tier-management.md b/docs/guides/subscription-tier-management.md
new file mode 100644
index 00000000..a11d967f
--- /dev/null
+++ b/docs/guides/subscription-tier-management.md
@@ -0,0 +1,135 @@
+# Subscription Tier Management
+
+This guide explains how to manage subscription tiers and assign features to them in the admin panel.
+
+## Accessing Tier Management
+
+Navigate to **Admin → Billing & Subscriptions → Subscription Tiers** or go directly to `/admin/subscription-tiers`.
+
+## Dashboard Overview
+
+The tier management page displays:
+
+### Stats Cards
+- **Total Tiers**: Number of configured subscription tiers
+- **Active Tiers**: Tiers currently available for subscription
+- **Public Tiers**: Tiers visible to vendors (excludes enterprise/custom)
+- **Est. MRR**: Estimated Monthly Recurring Revenue
+
+### Tier Table
+
+Each tier shows:
+
+| Column | Description |
+|--------|-------------|
+| # | Display order (affects pricing page order) |
+| Code | Unique identifier (e.g., `essential`, `professional`) |
+| Name | Display name shown to vendors |
+| Monthly | Monthly price in EUR |
+| Annual | Annual price in EUR (or `-` if not set) |
+| Orders/Mo | Monthly order limit (or `Unlimited`) |
+| Products | Product limit (or `Unlimited`) |
+| Team | Team member limit (or `Unlimited`) |
+| Features | Number of features assigned |
+| Status | Active, Private, or Inactive |
+| Actions | Edit Features, Edit, Activate/Deactivate |
+
+## Managing Tiers
+
+### Creating a New Tier
+
+1. Click **Create Tier** button
+2. Fill in the tier details:
+ - **Code**: Unique lowercase identifier (cannot be changed after creation)
+ - **Name**: Display name for the tier
+ - **Monthly Price**: Price in cents (e.g., 4900 for €49.00)
+ - **Annual Price**: Optional annual price in cents
+ - **Order Limit**: Leave empty for unlimited
+ - **Product Limit**: Leave empty for unlimited
+ - **Team Members**: Leave empty for unlimited
+ - **Display Order**: Controls sort order on pricing pages
+ - **Active**: Whether tier is available
+ - **Public**: Whether tier is visible to vendors
+3. Click **Create**
+
+### Editing a Tier
+
+1. Click the **pencil icon** on the tier row
+2. Modify the tier properties
+3. Click **Update**
+
+Note: The tier code cannot be changed after creation.
+
+### Activating/Deactivating Tiers
+
+- Click the **check-circle icon** to activate an inactive tier
+- Click the **x-circle icon** to deactivate an active tier
+
+Deactivating a tier:
+- Does not affect existing subscriptions
+- Hides the tier from new subscription selection
+- Can be reactivated at any time
+
+## Managing Features
+
+### Assigning Features to a Tier
+
+1. Click the **puzzle-piece icon** on the tier row
+2. A slide-over panel opens showing all available features
+3. Features are grouped by category:
+ - Analytics
+ - Product Management
+ - Order Management
+ - Marketing
+ - Support
+ - Integration
+ - Branding
+ - Team
+
+4. Check/uncheck features to include in the tier
+5. Use **Select all** or **Deselect all** per category for bulk actions
+6. The footer shows the total number of selected features
+7. Click **Save Features** to apply changes
+
+### Feature Categories
+
+| Category | Example Features |
+|----------|------------------|
+| Analytics | Basic Analytics, Analytics Dashboard, Custom Reports |
+| Product Management | Bulk Edit, Variants, Bundles, Inventory Alerts |
+| Order Management | Order Automation, Advanced Fulfillment, Multi-Warehouse |
+| Marketing | Discount Codes, Abandoned Cart, Email Marketing, Loyalty |
+| Support | Email Support, Priority Support, Phone Support, Dedicated Manager |
+| Integration | Basic API, Advanced API, Webhooks, Custom Integrations |
+| Branding | Theme Customization, Custom Domain, White Label |
+| Team | Team Management, Role Permissions, Audit Logs |
+
+## Best Practices
+
+### Tier Pricing Strategy
+
+1. **Essential**: Entry-level with basic features and limits
+2. **Professional**: Mid-tier with increased limits and key integrations
+3. **Business**: Full-featured for growing businesses
+4. **Enterprise**: Custom pricing with unlimited everything
+
+### Feature Assignment Tips
+
+- Start with fewer features in lower tiers
+- Ensure each upgrade tier adds meaningful value
+- Keep support features as upgrade incentives
+- API access typically belongs in Business+ tiers
+
+### Stripe Integration
+
+For each tier, you can optionally configure:
+- **Stripe Product ID**: Link to Stripe product
+- **Stripe Monthly Price ID**: Link to monthly price
+- **Stripe Annual Price ID**: Link to annual price
+
+These are required for automated billing via Stripe Checkout.
+
+## Related Documentation
+
+- [Subscription & Billing System](../features/subscription-billing.md) - Complete billing documentation
+- [Feature Gating System](../implementation/feature-gating-system.md) - Technical feature gating details
diff --git a/docs/implementation/feature-gating-system.md b/docs/implementation/feature-gating-system.md
index 1737e20b..29259f6f 100644
--- a/docs/implementation/feature-gating-system.md
+++ b/docs/implementation/feature-gating-system.md
@@ -338,6 +338,65 @@ Located at `/admin/features`:
- Search by name/code
- View tier requirements
+## Admin Tier Management UI
+
+Located at `/admin/subscription-tiers`:
+
+### Overview
+
+The subscription tiers admin page provides full CRUD functionality for managing subscription tiers and their feature assignments.
+
+### Features
+
+1. **Stats Cards**: Display total tiers, active tiers, public tiers, and estimated MRR
+2. **Tier Table**: Sortable list of all tiers with:
+ - Display order
+ - Code (colored badge by tier)
+ - Name
+ - Monthly/Annual pricing
+ - Limits (orders, products, team members)
+ - Feature count
+ - Status (Active/Private/Inactive)
+ - Actions (Edit Features, Edit, Activate/Deactivate)
+
+3. **Create/Edit Modal**: Form with all tier fields:
+ - Code and Name
+ - Monthly and Annual pricing (in cents)
+ - Order, Product, and Team member limits
+ - Display order
+ - Stripe IDs (optional)
+ - Description
+ - Active/Public toggles
+
+4. **Feature Assignment Slide-over Panel**:
+ - Opens when clicking the puzzle-piece icon
+ - Shows all features grouped by category
+ - Checkbox selection with Select all/Deselect all per category
+ - Feature count in footer
+ - Save to update tier's feature assignments
+
+### Files
+
+| File | Purpose |
+|------|---------|
+| `app/templates/admin/subscription-tiers.html` | Page template |
+| `static/admin/js/subscription-tiers.js` | Alpine.js component |
+| `app/routes/admin_pages.py` | Route registration |
+
+### API Endpoints Used
+
+| Action | Method | Endpoint |
+|--------|--------|----------|
+| Load tiers | GET | `/api/v1/admin/subscriptions/tiers` |
+| Load stats | GET | `/api/v1/admin/subscriptions/stats` |
+| Create tier | POST | `/api/v1/admin/subscriptions/tiers` |
+| Update tier | PATCH | `/api/v1/admin/subscriptions/tiers/{code}` |
+| Delete tier | DELETE | `/api/v1/admin/subscriptions/tiers/{code}` |
+| Load features | GET | `/api/v1/admin/features` |
+| Load categories | GET | `/api/v1/admin/features/categories` |
+| Get tier features | GET | `/api/v1/admin/features/tiers/{code}/features` |
+| Update tier features | PUT | `/api/v1/admin/features/tiers/{code}/features` |
+
## Migration
The features are seeded via Alembic migration:
diff --git a/mkdocs.yml b/mkdocs.yml
index 2e84912c..bb483f5e 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -211,6 +211,7 @@ nav:
- User Management: guides/user-management.md
- Product Management: guides/product-management.md
- Inventory Management: guides/inventory-management.md
+ - Subscription Tier Management: guides/subscription-tier-management.md
- Shop Setup: guides/shop-setup.md
- CSV Import: guides/csv-import.md
- Marketplace Integration: guides/marketplace-integration.md
diff --git a/static/admin/js/subscription-tiers.js b/static/admin/js/subscription-tiers.js
index d4e4ed8a..547237ab 100644
--- a/static/admin/js/subscription-tiers.js
+++ b/static/admin/js/subscription-tiers.js
@@ -1,7 +1,7 @@
// static/admin/js/subscription-tiers.js
// noqa: JS-003 - Uses ...baseData which is data() with safety check
-const tiersLog = window.LogConfig?.loggers?.subscriptionTiers || console;
+const tiersLog = window.LogConfig?.loggers?.subscriptionTiers || window.LogConfig?.createLogger?.('subscriptionTiers') || console;
function adminSubscriptionTiers() {
// Get base data with safety check for standalone usage
@@ -23,6 +23,16 @@ function adminSubscriptionTiers() {
stats: null,
includeInactive: false,
+ // Feature management
+ features: [],
+ categories: [],
+ featuresGrouped: {},
+ selectedFeatures: [],
+ selectedTierForFeatures: null,
+ showFeaturePanel: false,
+ loadingFeatures: false,
+ savingFeatures: false,
+
// Sorting
sortBy: 'display_order',
sortOrder: 'asc',
@@ -56,8 +66,18 @@ function adminSubscriptionTiers() {
window._adminSubscriptionTiersInitialized = true;
tiersLog.info('=== SUBSCRIPTION TIERS PAGE INITIALIZING ===');
- await this.loadTiers();
- await this.loadStats();
+ try {
+ await Promise.all([
+ this.loadTiers(),
+ this.loadStats(),
+ this.loadFeatures(),
+ this.loadCategories()
+ ]);
+ tiersLog.info('=== SUBSCRIPTION TIERS PAGE INITIALIZED ===');
+ } catch (error) {
+ tiersLog.error('Failed to initialize subscription tiers page:', error);
+ this.error = 'Failed to load page data. Please refresh.';
+ }
},
async refresh() {
@@ -207,6 +227,129 @@ function adminSubscriptionTiers() {
style: 'currency',
currency: 'EUR'
}).format(cents / 100);
+ },
+
+ // ==================== FEATURE MANAGEMENT ====================
+
+ async loadFeatures() {
+ try {
+ const data = await apiClient.get('/admin/features');
+ this.features = data.features || [];
+ tiersLog.info(`Loaded ${this.features.length} features`);
+ } catch (error) {
+ tiersLog.error('Failed to load features:', error);
+ }
+ },
+
+ async loadCategories() {
+ try {
+ const data = await apiClient.get('/admin/features/categories');
+ this.categories = data.categories || [];
+ tiersLog.info(`Loaded ${this.categories.length} categories`);
+ } catch (error) {
+ tiersLog.error('Failed to load categories:', error);
+ }
+ },
+
+ groupFeaturesByCategory() {
+ this.featuresGrouped = {};
+ for (const category of this.categories) {
+ this.featuresGrouped[category] = this.features.filter(f => f.category === category);
+ }
+ },
+
+ async openFeaturePanel(tier) {
+ tiersLog.info('Opening feature panel for tier:', tier.code);
+ this.selectedTierForFeatures = tier;
+ this.loadingFeatures = true;
+ this.showFeaturePanel = true;
+
+ try {
+ // Load tier's current features
+ const data = await apiClient.get(`/admin/features/tiers/${tier.code}/features`);
+ if (data.features) {
+ this.selectedFeatures = data.features.map(f => f.code);
+ } else {
+ this.selectedFeatures = tier.features || [];
+ }
+ } catch (error) {
+ tiersLog.error('Failed to load tier features:', error);
+ this.selectedFeatures = tier.features || [];
+ } finally {
+ this.groupFeaturesByCategory();
+ this.loadingFeatures = false;
+ }
+ },
+
+ closeFeaturePanel() {
+ this.showFeaturePanel = false;
+ this.selectedTierForFeatures = null;
+ this.selectedFeatures = [];
+ this.featuresGrouped = {};
+ },
+
+ toggleFeature(featureCode) {
+ const index = this.selectedFeatures.indexOf(featureCode);
+ if (index === -1) {
+ this.selectedFeatures.push(featureCode);
+ } else {
+ this.selectedFeatures.splice(index, 1);
+ }
+ },
+
+ isFeatureSelected(featureCode) {
+ return this.selectedFeatures.includes(featureCode);
+ },
+
+ async saveFeatures() {
+ if (!this.selectedTierForFeatures) return;
+
+ tiersLog.info('Saving features for tier:', this.selectedTierForFeatures.code);
+ this.savingFeatures = true;
+
+ try {
+ await apiClient.put(
+ `/admin/features/tiers/${this.selectedTierForFeatures.code}/features`,
+ { feature_codes: this.selectedFeatures }
+ );
+
+ this.successMessage = `Features updated for ${this.selectedTierForFeatures.name}`;
+ this.closeFeaturePanel();
+ await this.loadTiers();
+ } catch (error) {
+ tiersLog.error('Failed to save features:', error);
+ this.error = error.message || 'Failed to save features';
+ } finally {
+ this.savingFeatures = false;
+ }
+ },
+
+ selectAllInCategory(category) {
+ const categoryFeatures = this.featuresGrouped[category] || [];
+ for (const feature of categoryFeatures) {
+ if (!this.selectedFeatures.includes(feature.code)) {
+ this.selectedFeatures.push(feature.code);
+ }
+ }
+ },
+
+ deselectAllInCategory(category) {
+ const categoryFeatures = this.featuresGrouped[category] || [];
+ const codes = categoryFeatures.map(f => f.code);
+ this.selectedFeatures = this.selectedFeatures.filter(c => !codes.includes(c));
+ },
+
+ allSelectedInCategory(category) {
+ const categoryFeatures = this.featuresGrouped[category] || [];
+ if (categoryFeatures.length === 0) return false;
+ return categoryFeatures.every(f => this.selectedFeatures.includes(f.code));
+ },
+
+ formatCategoryName(category) {
+ return category
+ .split('_')
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
+ .join(' ');
}
};
}
|