feat(subscriptions): migrate subscription management to merchant level and seed tiers

Move subscription create/edit from store detail (broken endpoint) to merchant
detail page with proper modal UI. Seed 4 subscription tiers (Essential,
Professional, Business, Enterprise) in init_production.py. Also includes
cross-module dependency declarations, store domain platform_id migration,
platform context middleware, CMS route fixes, and migration backups.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-09 21:04:04 +01:00
parent 7feacd5af8
commit 68493dc6cb
97 changed files with 13286 additions and 77 deletions

View File

@@ -194,6 +194,167 @@
</div>
</div>
<!-- Subscription Card -->
<div class="px-4 py-3 mb-6 bg-white rounded-lg shadow-md dark:bg-gray-800" x-show="subscription">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">
Subscription
</h3>
</div>
<!-- Tier and Status -->
<div class="flex flex-wrap items-center gap-4 mb-4">
<div class="flex items-center gap-2">
<span class="text-sm text-gray-600 dark:text-gray-400">Tier:</span>
<span class="px-2.5 py-0.5 text-sm font-medium rounded-full"
:class="{
'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300': subscription?.tier === 'essential',
'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300': subscription?.tier === 'professional',
'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300': subscription?.tier === 'business',
'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300': subscription?.tier === 'enterprise'
}"
x-text="subscription?.tier ? subscription.tier.charAt(0).toUpperCase() + subscription.tier.slice(1) : '-'">
</span>
</div>
<div class="flex items-center gap-2">
<span class="text-sm text-gray-600 dark:text-gray-400">Status:</span>
<span class="px-2.5 py-0.5 text-sm font-medium rounded-full"
:class="{
'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300': subscription?.status === 'active',
'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300': subscription?.status === 'trial',
'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300': subscription?.status === 'past_due',
'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300': subscription?.status === 'cancelled' || subscription?.status === 'expired'
}"
x-text="subscription?.status ? subscription.status.replace('_', ' ').charAt(0).toUpperCase() + subscription.status.slice(1) : '-'">
</span>
</div>
<template x-if="subscription?.is_annual">
<span class="px-2.5 py-0.5 text-xs font-medium text-purple-800 bg-purple-100 rounded-full dark:bg-purple-900 dark:text-purple-300">
Annual
</span>
</template>
</div>
<!-- Period Info -->
<div class="flex flex-wrap gap-4 mb-4 text-sm">
<div>
<span class="text-gray-600 dark:text-gray-400">Period:</span>
<span class="ml-1 text-gray-700 dark:text-gray-300" x-text="formatDate(subscription?.period_start)"></span>
<span class="text-gray-400">&rarr;</span>
<span class="text-gray-700 dark:text-gray-300" x-text="formatDate(subscription?.period_end)"></span>
</div>
<template x-if="subscription?.trial_ends_at">
<div>
<span class="text-gray-600 dark:text-gray-400">Trial ends:</span>
<span class="ml-1 text-gray-700 dark:text-gray-300" x-text="formatDate(subscription?.trial_ends_at)"></span>
</div>
</template>
</div>
<!-- Usage Meters -->
<div class="grid gap-4 md:grid-cols-3">
<template x-for="metric in usageMetrics" :key="metric.name">
<div class="p-3 bg-gray-50 rounded-lg dark:bg-gray-700">
<div class="flex items-center justify-between mb-2">
<span class="text-xs font-medium text-gray-600 dark:text-gray-400 uppercase" x-text="metric.name"></span>
</div>
<div class="flex items-baseline gap-1">
<span class="text-xl font-bold text-gray-700 dark:text-gray-200" x-text="metric.current"></span>
<span class="text-sm text-gray-500 dark:text-gray-400">
/ <span x-text="metric.is_unlimited ? '∞' : metric.limit"></span>
</span>
</div>
<div class="mt-2 h-1.5 bg-gray-200 rounded-full dark:bg-gray-600" x-show="!metric.is_unlimited">
<div class="h-1.5 rounded-full transition-all"
:class="getUsageBarColor(metric.current, metric.limit)"
:style="`width: ${Math.min(100, metric.percentage || 0)}%`">
</div>
</div>
</div>
</template>
<template x-if="usageMetrics.length === 0">
<div class="p-3 bg-gray-50 rounded-lg dark:bg-gray-700 md:col-span-3">
<p class="text-sm text-gray-500 dark:text-gray-400 text-center">No usage data available</p>
</div>
</template>
</div>
</div>
<!-- No Subscription Notice -->
<div class="px-4 py-3 mb-6 bg-yellow-50 border border-yellow-200 rounded-lg dark:bg-yellow-900/20 dark:border-yellow-800" x-show="!subscription && !loading && platformId">
<div class="flex items-center gap-3">
<span x-html="$icon('exclamation', 'w-5 h-5 text-yellow-600 dark:text-yellow-400')"></span>
<div>
<p class="text-sm font-medium text-yellow-800 dark:text-yellow-200">No Subscription Found</p>
<p class="text-sm text-yellow-700 dark:text-yellow-300">This merchant doesn't have a subscription yet.</p>
</div>
<button
@click="openCreateSubscriptionModal()"
class="ml-auto px-3 py-1.5 text-sm font-medium text-white bg-yellow-600 rounded-lg hover:bg-yellow-700">
Create Subscription
</button>
</div>
</div>
<!-- Create Subscription Modal -->
<div x-show="showCreateSubscriptionModal" class="fixed inset-0 z-50 flex items-center justify-center" x-cloak>
<div class="fixed inset-0 bg-black bg-opacity-50" @click="showCreateSubscriptionModal = false"></div>
<div class="relative z-10 w-full max-w-md p-6 bg-white rounded-lg shadow-xl dark:bg-gray-800">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">Create Subscription</h3>
<!-- Tier Selector -->
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Subscription Tier</label>
<select x-model="createForm.tier_code"
class="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 focus:outline-none focus:ring-2 focus:ring-purple-500">
<template x-for="tier in tiers" :key="tier.code">
<option :value="tier.code" x-text="tier.name + ' — ' + formatTierPrice(tier)"></option>
</template>
</select>
</div>
<!-- Status -->
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Status</label>
<select x-model="createForm.status"
class="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 focus:outline-none focus:ring-2 focus:ring-purple-500">
<option value="trial">Trial</option>
<option value="active">Active</option>
</select>
</div>
<!-- Trial Days (shown only when status=trial) -->
<div class="mb-4" x-show="createForm.status === 'trial'">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Trial Days</label>
<input type="number" x-model="createForm.trial_days" min="1" max="90"
class="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 focus:outline-none focus:ring-2 focus:ring-purple-500">
</div>
<!-- Annual Billing -->
<div class="mb-6">
<label class="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
<input type="checkbox" x-model="createForm.is_annual"
class="rounded border-gray-300 text-purple-600 focus:ring-purple-500 dark:border-gray-600 dark:bg-gray-700">
Annual billing
</label>
</div>
<!-- Actions -->
<div class="flex justify-end gap-3">
<button @click="showCreateSubscriptionModal = false"
class="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600">
Cancel
</button>
<button @click="createSubscription()"
:disabled="creatingSubscription"
class="px-4 py-2 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700 disabled:opacity-50 disabled:cursor-not-allowed">
<span x-show="!creatingSubscription">Create</span>
<span x-show="creatingSubscription">Creating...</span>
</button>
</div>
</div>
</div>
<!-- Stores Section -->
<div class="px-4 py-3 mb-8 bg-white rounded-lg shadow-md dark:bg-gray-800" x-show="merchant?.stores && merchant?.stores.length > 0">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">

View File

@@ -112,12 +112,12 @@
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">
Subscription
</h3>
<button
@click="showSubscriptionModal = true"
<a
:href="'/admin/merchants/' + store?.merchant_id"
class="flex items-center px-3 py-1.5 text-sm font-medium text-purple-600 hover:text-purple-700 dark:text-purple-400 dark:hover:text-purple-300">
<span x-html="$icon('edit', 'w-4 h-4 mr-1')"></span>
Edit
</button>
<span x-html="$icon('external-link', 'w-4 h-4 mr-1')"></span>
Manage on Merchant Page
</a>
</div>
<!-- Tier and Status -->
@@ -206,11 +206,11 @@
<p class="text-sm font-medium text-yellow-800 dark:text-yellow-200">No Subscription Found</p>
<p class="text-sm text-yellow-700 dark:text-yellow-300">This store doesn't have a subscription yet.</p>
</div>
<button
@click="createSubscription()"
<a
:href="'/admin/merchants/' + store?.merchant_id"
class="ml-auto px-3 py-1.5 text-sm font-medium text-white bg-yellow-600 rounded-lg hover:bg-yellow-700">
Create Subscription
</button>
Manage on Merchant Page
</a>
</div>
</div>