feat(billing): migrate frontend templates to feature provider system

Replace hardcoded subscription fields (orders_limit, products_limit,
team_members_limit) across 5 frontend pages with dynamic feature
provider APIs. Add admin convenience endpoint for store subscription
lookup. Remove legacy stubs (StoreSubscription, FeatureCode, Feature,
TIER_LIMITS, FeatureInfo, FeatureUpgradeInfo) and schema aliases.

Pages updated:
- Admin subscriptions: dynamic feature overrides editor
- Admin tiers: correct feature catalog/limits API URLs
- Store billing: usage metrics from /store/billing/usage
- Merchant subscription detail: tier.feature_limits rendering
- Admin store detail: new GET /admin/subscriptions/store/{id} endpoint

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-07 15:18:16 +01:00
parent 922616c9e3
commit 1db7e8a087
19 changed files with 2508 additions and 1205 deletions

View File

@@ -91,9 +91,6 @@
{{ th_sortable('name', 'Name', 'sortBy', 'sortOrder') }}
<th class="px-4 py-3 text-right">Monthly</th>
<th class="px-4 py-3 text-right">Annual</th>
<th class="px-4 py-3 text-center">Orders/Mo</th>
<th class="px-4 py-3 text-center">Products</th>
<th class="px-4 py-3 text-center">Team</th>
<th class="px-4 py-3 text-center">Features</th>
<th class="px-4 py-3 text-center">Status</th>
<th class="px-4 py-3 text-right">Actions</th>
@@ -101,7 +98,7 @@
<tbody class="bg-white divide-y dark:divide-gray-700 dark:bg-gray-800">
<template x-if="loading">
<tr>
<td colspan="11" class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
<td colspan="8" class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
<span x-html="$icon('refresh', 'inline w-6 h-6 animate-spin mr-2')"></span>
Loading tiers...
</td>
@@ -109,7 +106,7 @@
</template>
<template x-if="!loading && tiers.length === 0">
<tr>
<td colspan="11" class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
<td colspan="8" class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
No subscription tiers found.
</td>
</tr>
@@ -131,11 +128,8 @@
<td class="px-4 py-3 text-sm font-medium text-gray-900 dark:text-gray-100" x-text="tier.name"></td>
<td class="px-4 py-3 text-sm text-right font-mono" x-text="formatCurrency(tier.price_monthly_cents)"></td>
<td class="px-4 py-3 text-sm text-right font-mono" x-text="tier.price_annual_cents ? formatCurrency(tier.price_annual_cents) : '-'"></td>
<td class="px-4 py-3 text-sm text-center" x-text="tier.orders_per_month || 'Unlimited'"></td>
<td class="px-4 py-3 text-sm text-center" x-text="tier.products_limit || 'Unlimited'"></td>
<td class="px-4 py-3 text-sm text-center" x-text="tier.team_members || 'Unlimited'"></td>
<td class="px-4 py-3 text-sm text-center">
<span class="px-2 py-1 text-xs bg-gray-100 dark:bg-gray-700 rounded" x-text="(tier.features || []).length"></span>
<span class="px-2 py-1 text-xs bg-gray-100 dark:bg-gray-700 rounded" x-text="(tier.feature_codes || tier.features || []).length"></span>
</td>
<td class="px-4 py-3 text-center">
<span x-show="tier.is_active && tier.is_public" class="px-2 py-1 text-xs font-medium text-green-700 bg-green-100 rounded-full dark:bg-green-900 dark:text-green-200">Active</span>
@@ -225,39 +219,6 @@
>
</div>
<!-- Orders per Month -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Orders/Month (empty = unlimited)</label>
<input
type="number"
x-model.number="formData.orders_per_month"
class="w-full px-3 py-2 border border-gray-300 rounded-lg dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="e.g., 100"
>
</div>
<!-- Products Limit -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Products Limit (empty = unlimited)</label>
<input
type="number"
x-model.number="formData.products_limit"
class="w-full px-3 py-2 border border-gray-300 rounded-lg dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="e.g., 200"
>
</div>
<!-- Team Members -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Team Members (empty = unlimited)</label>
<input
type="number"
x-model.number="formData.team_members"
class="w-full px-3 py-2 border border-gray-300 rounded-lg dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="e.g., 3"
>
</div>
<!-- Display Order -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Display Order</label>
@@ -310,7 +271,7 @@
</label>
<label class="flex items-center">
<input type="checkbox" x-model="formData.is_public" class="mr-2 rounded border-gray-300 dark:border-gray-600">
<span class="text-sm text-gray-700 dark:text-gray-300">Public (visible to vendors)</span>
<span class="text-sm text-gray-700 dark:text-gray-300">Public (visible to stores)</span>
</label>
</div>
</div>
@@ -420,18 +381,32 @@
<!-- Features List -->
<div class="divide-y divide-gray-100 dark:divide-gray-700">
<template x-for="feature in featuresGrouped[category]" :key="feature.code">
<label class="flex items-start px-4 py-3 hover:bg-gray-50 dark:hover:bg-gray-700/30 cursor-pointer">
<input
type="checkbox"
:checked="isFeatureSelected(feature.code)"
@change="toggleFeature(feature.code)"
class="mt-0.5 rounded border-gray-300 dark:border-gray-600 text-purple-600 focus:ring-purple-500"
>
<div class="ml-3">
<div class="text-sm font-medium text-gray-900 dark:text-white" x-text="feature.name"></div>
<div class="text-xs text-gray-500 dark:text-gray-400" x-text="feature.description"></div>
</div>
</label>
<div class="flex items-start px-4 py-3 hover:bg-gray-50 dark:hover:bg-gray-700/30">
<label class="flex items-start cursor-pointer flex-1">
<input
type="checkbox"
:checked="isFeatureSelected(feature.code)"
@change="toggleFeature(feature.code)"
class="mt-0.5 rounded border-gray-300 dark:border-gray-600 text-purple-600 focus:ring-purple-500"
>
<div class="ml-3">
<div class="text-sm font-medium text-gray-900 dark:text-white" x-text="feature.name_key || feature.name"></div>
<div class="text-xs text-gray-500 dark:text-gray-400" x-text="feature.description_key || feature.description"></div>
</div>
</label>
<template x-if="feature.feature_type === 'quantitative' && isFeatureSelected(feature.code)">
<div class="ml-2 flex-shrink-0">
<input
type="number"
:value="getFeatureLimitValue(feature.code)"
@input="setFeatureLimitValue(feature.code, $event.target.value)"
placeholder="Unlimited"
class="w-24 px-2 py-1 text-sm border border-gray-300 rounded dark:border-gray-600 dark:bg-gray-700 dark:text-white"
min="0"
>
</div>
</template>
</div>
</template>
</div>
</div>