fix(billing): complete billing module — fix tier change, platform support, merchant portal

- Fix admin tier change: resolve tier_code→tier_id in update_subscription(),
  delegate to billing_service.change_tier() for Stripe-connected subs
- Add platform support to admin tiers page: platform column, filter dropdown,
  platform selector in create/edit modal, platform_name in tier API response
- Filter used platforms in create subscription modal on merchant detail page
- Enrich merchant portal API responses with tier code, tier_name, platform_name
- Add eager-load of platform relationship in get_merchant_subscription()
- Remove stale store_name/store_code references from merchant templates
- Add merchant tier change endpoint (POST /change-tier) and tier selector UI
  replacing broken requestUpgrade() button
- Fix subscription detail link to use platform_id instead of sub.id

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-10 20:49:48 +01:00
parent 0b37274140
commit d1fe3584ff
54 changed files with 222 additions and 52 deletions

View File

@@ -71,6 +71,13 @@
<input type="checkbox" x-model="includeInactive" @change="loadTiers()" class="mr-2 rounded border-gray-300 dark:border-gray-600 dark:bg-gray-700">
Show inactive tiers
</label>
<select x-model="filterPlatformId" @change="loadTiers()"
class="px-3 py-1.5 text-sm border border-gray-300 rounded-lg dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300">
<option value="">All Platforms</option>
<template x-for="p in platforms" :key="p.id">
<option :value="p.id" x-text="p.name"></option>
</template>
</select>
</div>
<button
@@ -87,6 +94,7 @@
<table class="w-full whitespace-nowrap">
{% call table_header_custom() %}
<th class="px-4 py-3">#</th>
<th class="px-4 py-3">Platform</th>
{{ th_sortable('code', 'Code', 'sortBy', 'sortOrder') }}
{{ th_sortable('name', 'Name', 'sortBy', 'sortOrder') }}
<th class="px-4 py-3 text-right">Monthly</th>
@@ -98,7 +106,7 @@
<tbody class="bg-white divide-y dark:divide-gray-700 dark:bg-gray-800">
<template x-if="loading">
<tr>
<td colspan="8" class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
<td colspan="9" 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>
@@ -106,7 +114,7 @@
</template>
<template x-if="!loading && tiers.length === 0">
<tr>
<td colspan="8" class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
<td colspan="9" class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
No subscription tiers found.
</td>
</tr>
@@ -114,6 +122,7 @@
<template x-for="(tier, index) in tiers" :key="tier.id">
<tr class="text-gray-700 dark:text-gray-400" :class="{ 'opacity-50': !tier.is_active }">
<td class="px-4 py-3 text-sm" x-text="tier.display_order"></td>
<td class="px-4 py-3 text-sm" x-text="tier.platform_name || 'Global'"></td>
<td class="px-4 py-3">
<span class="px-2 py-1 text-xs font-medium rounded-full"
:class="{
@@ -186,6 +195,21 @@
>
</div>
<!-- Platform -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Platform</label>
<select
x-model="formData.platform_id"
:disabled="editingTier"
class="w-full px-3 py-2 border border-gray-300 rounded-lg dark:border-gray-600 dark:bg-gray-700 dark:text-white disabled:opacity-50"
>
<option :value="null">Global (all platforms)</option>
<template x-for="p in platforms" :key="p.id">
<option :value="p.id" x-text="p.name"></option>
</template>
</select>
</div>
<!-- Name -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Name</label>