refactor: complete module-driven architecture migration

This commit completes the migration to a fully module-driven architecture:

## Models Migration
- Moved all domain models from models/database/ to their respective modules:
  - tenancy: User, Admin, Vendor, Company, Platform, VendorDomain, etc.
  - cms: MediaFile, VendorTheme
  - messaging: Email, VendorEmailSettings, VendorEmailTemplate
  - core: AdminMenuConfig
- models/database/ now only contains Base and TimestampMixin (infrastructure)

## Schemas Migration
- Moved all domain schemas from models/schema/ to their respective modules:
  - tenancy: company, vendor, admin, team, vendor_domain
  - cms: media, image, vendor_theme
  - messaging: email
- models/schema/ now only contains base.py and auth.py (infrastructure)

## Routes Migration
- Moved admin routes from app/api/v1/admin/ to modules:
  - menu_config.py -> core module
  - modules.py -> tenancy module
  - module_config.py -> tenancy module
- app/api/v1/admin/ now only aggregates auto-discovered module routes

## Menu System
- Implemented module-driven menu system with MenuDiscoveryService
- Extended FrontendType enum: PLATFORM, ADMIN, VENDOR, STOREFRONT
- Added MenuItemDefinition and MenuSectionDefinition dataclasses
- Each module now defines its own menu items in definition.py
- MenuService integrates with MenuDiscoveryService for template rendering

## Documentation
- Updated docs/architecture/models-structure.md
- Updated docs/architecture/menu-management.md
- Updated architecture validation rules for new exceptions

## Architecture Validation
- Updated MOD-019 rule to allow base.py in models/schema/
- Created core module exceptions.py and schemas/ directory
- All validation errors resolved (only warnings remain)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-01 21:02:56 +01:00
parent 09d7d282c6
commit d7a0ff8818
307 changed files with 5536 additions and 3826 deletions

View File

@@ -6,8 +6,8 @@ Defines the billing module including its features, menu items,
route configurations, and scheduled tasks.
"""
from app.modules.base import ModuleDefinition, ScheduledTask
from models.database.admin_menu_config import FrontendType
from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition, ScheduledTask
from app.modules.enums import FrontendType
def _get_admin_router():
@@ -53,6 +53,72 @@ billing_module = ModuleDefinition(
"invoices", # Vendor invoice history
],
},
# New module-driven menu definitions
menus={
FrontendType.ADMIN: [
MenuSectionDefinition(
id="billing",
label_key="billing.menu.billing_subscriptions",
icon="credit-card",
order=50,
items=[
MenuItemDefinition(
id="subscription-tiers",
label_key="billing.menu.subscription_tiers",
icon="tag",
route="/admin/subscription-tiers",
order=10,
),
MenuItemDefinition(
id="subscriptions",
label_key="billing.menu.vendor_subscriptions",
icon="credit-card",
route="/admin/subscriptions",
order=20,
),
MenuItemDefinition(
id="billing-history",
label_key="billing.menu.billing_history",
icon="document-text",
route="/admin/billing-history",
order=30,
),
],
),
],
FrontendType.VENDOR: [
MenuSectionDefinition(
id="sales",
label_key="billing.menu.sales_orders",
icon="currency-euro",
order=20,
items=[
MenuItemDefinition(
id="invoices",
label_key="billing.menu.invoices",
icon="currency-euro",
route="/vendor/{vendor_code}/invoices",
order=30,
),
],
),
MenuSectionDefinition(
id="account",
label_key="billing.menu.account_settings",
icon="credit-card",
order=900,
items=[
MenuItemDefinition(
id="billing",
label_key="billing.menu.billing",
icon="credit-card",
route="/vendor/{vendor_code}/billing",
order=30,
),
],
),
],
},
is_core=False, # Billing can be disabled (e.g., internal platforms)
# =========================================================================
# Self-Contained Module Configuration

View File

@@ -89,7 +89,17 @@
"payment_method_updated": "Zahlungsmethode aktualisiert",
"subscription_cancelled": "Abonnement gekündigt",
"error_loading": "Fehler beim Laden der Abrechnungsinformationen",
"error_updating": "Fehler beim Aktualisieren des Abonnements"
"error_updating": "Fehler beim Aktualisieren des Abonnements",
"failed_to_load_billing_data": "Failed to load billing data",
"failed_to_create_checkout_session": "Failed to create checkout session",
"failed_to_open_payment_portal": "Failed to open payment portal",
"subscription_cancelled_you_have_access_u": "Subscription cancelled. You have access until the end of your billing period.",
"failed_to_cancel_subscription": "Failed to cancel subscription",
"subscription_reactivated": "Subscription reactivated!",
"failed_to_reactivate_subscription": "Failed to reactivate subscription",
"failed_to_purchase_addon": "Failed to purchase add-on",
"addon_cancelled_successfully": "Add-on cancelled successfully",
"failed_to_cancel_addon": "Failed to cancel add-on"
},
"limits": {
"orders_exceeded": "Monatliches Bestelllimit erreicht. Upgrade für mehr.",

View File

@@ -89,7 +89,17 @@
"payment_method_updated": "Payment method updated",
"subscription_cancelled": "Subscription cancelled",
"error_loading": "Error loading billing information",
"error_updating": "Error updating subscription"
"error_updating": "Error updating subscription",
"failed_to_load_billing_data": "Failed to load billing data",
"failed_to_create_checkout_session": "Failed to create checkout session",
"failed_to_open_payment_portal": "Failed to open payment portal",
"subscription_cancelled_you_have_access_u": "Subscription cancelled. You have access until the end of your billing period.",
"failed_to_cancel_subscription": "Failed to cancel subscription",
"subscription_reactivated": "Subscription reactivated!",
"failed_to_reactivate_subscription": "Failed to reactivate subscription",
"failed_to_purchase_addon": "Failed to purchase add-on",
"addon_cancelled_successfully": "Add-on cancelled successfully",
"failed_to_cancel_addon": "Failed to cancel add-on"
},
"limits": {
"orders_exceeded": "Monthly order limit reached. Upgrade to continue.",

View File

@@ -89,7 +89,17 @@
"payment_method_updated": "Moyen de paiement mis à jour",
"subscription_cancelled": "Abonnement annulé",
"error_loading": "Erreur lors du chargement des informations de facturation",
"error_updating": "Erreur lors de la mise à jour de l'abonnement"
"error_updating": "Erreur lors de la mise à jour de l'abonnement",
"failed_to_load_billing_data": "Failed to load billing data",
"failed_to_create_checkout_session": "Failed to create checkout session",
"failed_to_open_payment_portal": "Failed to open payment portal",
"subscription_cancelled_you_have_access_u": "Subscription cancelled. You have access until the end of your billing period.",
"failed_to_cancel_subscription": "Failed to cancel subscription",
"subscription_reactivated": "Subscription reactivated!",
"failed_to_reactivate_subscription": "Failed to reactivate subscription",
"failed_to_purchase_addon": "Failed to purchase add-on",
"addon_cancelled_successfully": "Add-on cancelled successfully",
"failed_to_cancel_addon": "Failed to cancel add-on"
},
"limits": {
"orders_exceeded": "Limite mensuelle de commandes atteinte. Passez à un niveau supérieur.",

View File

@@ -89,7 +89,17 @@
"payment_method_updated": "Zuelungsmethod aktualiséiert",
"subscription_cancelled": "Abonnement gekënnegt",
"error_loading": "Feeler beim Lueden vun de Rechnungsinformatiounen",
"error_updating": "Feeler beim Aktualiséieren vum Abonnement"
"error_updating": "Feeler beim Aktualiséieren vum Abonnement",
"failed_to_load_billing_data": "Failed to load billing data",
"failed_to_create_checkout_session": "Failed to create checkout session",
"failed_to_open_payment_portal": "Failed to open payment portal",
"subscription_cancelled_you_have_access_u": "Subscription cancelled. You have access until the end of your billing period.",
"failed_to_cancel_subscription": "Failed to cancel subscription",
"subscription_reactivated": "Subscription reactivated!",
"failed_to_reactivate_subscription": "Failed to reactivate subscription",
"failed_to_purchase_addon": "Failed to purchase add-on",
"addon_cancelled_successfully": "Add-on cancelled successfully",
"failed_to_cancel_addon": "Failed to cancel add-on"
},
"limits": {
"orders_exceeded": "Monatlech Bestellungslimit erreecht. Upgrade fir méi.",

View File

@@ -18,7 +18,7 @@ from sqlalchemy.orm import Session
from app.api.deps import get_current_admin_api, require_module_access
from app.core.database import get_db
from app.modules.billing.services import admin_subscription_service, subscription_service
from models.database.user import User
from app.modules.tenancy.models import User
from app.modules.billing.schemas import (
BillingHistoryListResponse,
BillingHistoryWithVendor,

View File

@@ -20,7 +20,7 @@ from app.api.deps import get_current_vendor_api, require_module_access
from app.core.config import settings
from app.core.database import get_db
from app.modules.billing.services import billing_service, subscription_service
from models.database.user import User
from app.modules.tenancy.models import User
logger = logging.getLogger(__name__)

View File

@@ -15,8 +15,8 @@ from sqlalchemy.orm import Session
from app.api.deps import get_db, require_menu_access
from app.modules.core.utils.page_context import get_admin_context
from app.templates_config import templates
from models.database.admin_menu_config import FrontendType
from models.database.user import User
from app.modules.enums import FrontendType
from app.modules.tenancy.models import User
router = APIRouter()

View File

@@ -14,7 +14,7 @@ from sqlalchemy.orm import Session
from app.api.deps import get_current_vendor_from_cookie_or_header, get_db
from app.modules.core.utils.page_context import get_vendor_context
from app.templates_config import templates
from models.database.user import User
from app.modules.tenancy.models import User
router = APIRouter()

View File

@@ -28,7 +28,7 @@ from app.modules.billing.models import (
VendorSubscription,
)
from app.modules.catalog.models import Product
from models.database.vendor import Vendor, VendorUser
from app.modules.tenancy.models import Vendor, VendorUser
logger = logging.getLogger(__name__)

View File

@@ -23,7 +23,7 @@ from app.modules.billing.models import (
VendorAddOn,
VendorSubscription,
)
from models.database.vendor import Vendor
from app.modules.tenancy.models import Vendor
logger = logging.getLogger(__name__)

View File

@@ -22,7 +22,7 @@ from app.modules.billing.models import (
SubscriptionStatus,
VendorSubscription,
)
from models.database.vendor import Vendor, VendorUser
from app.modules.tenancy.models import Vendor, VendorUser
logger = logging.getLogger(__name__)

View File

@@ -27,7 +27,7 @@ from app.modules.billing.models import (
SubscriptionTier,
VendorSubscription,
)
from models.database.vendor import Vendor
from app.modules.tenancy.models import Vendor
logger = logging.getLogger(__name__)
@@ -308,7 +308,7 @@ class StripeService:
customer_id = subscription.stripe_customer_id
else:
# Get vendor owner email
from models.database.vendor import VendorUser
from app.modules.tenancy.models import VendorUser
owner = (
db.query(VendorUser)

View File

@@ -46,7 +46,7 @@ from app.modules.billing.schemas import (
UsageSummary,
)
from app.modules.catalog.models import Product
from models.database.vendor import Vendor, VendorUser
from app.modules.tenancy.models import Vendor, VendorUser
logger = logging.getLogger(__name__)

View File

@@ -29,6 +29,9 @@ function vendorBilling() {
// Initialize
async init() {
// Load i18n translations
await I18n.loadModule('billing');
// Guard against multiple initialization
if (window._vendorBillingInitialized) return;
window._vendorBillingInitialized = true;
@@ -81,7 +84,7 @@ function vendorBilling() {
} catch (error) {
billingLog.error('Error loading billing data:', error);
Utils.showToast('Failed to load billing data', 'error');
Utils.showToast(I18n.t('billing.messages.failed_to_load_billing_data'), 'error');
} finally {
this.loading = false;
}
@@ -101,7 +104,7 @@ function vendorBilling() {
}
} catch (error) {
billingLog.error('Error creating checkout:', error);
Utils.showToast('Failed to create checkout session', 'error');
Utils.showToast(I18n.t('billing.messages.failed_to_create_checkout_session'), 'error');
}
},
@@ -113,7 +116,7 @@ function vendorBilling() {
}
} catch (error) {
billingLog.error('Error opening portal:', error);
Utils.showToast('Failed to open payment portal', 'error');
Utils.showToast(I18n.t('billing.messages.failed_to_open_payment_portal'), 'error');
}
},
@@ -125,24 +128,24 @@ function vendorBilling() {
});
this.showCancelModal = false;
Utils.showToast('Subscription cancelled. You have access until the end of your billing period.', 'success');
Utils.showToast(I18n.t('billing.messages.subscription_cancelled_you_have_access_u'), 'success');
await this.loadData();
} catch (error) {
billingLog.error('Error cancelling subscription:', error);
Utils.showToast('Failed to cancel subscription', 'error');
Utils.showToast(I18n.t('billing.messages.failed_to_cancel_subscription'), 'error');
}
},
async reactivate() {
try {
await apiClient.post('/vendor/billing/reactivate', {});
Utils.showToast('Subscription reactivated!', 'success');
Utils.showToast(I18n.t('billing.messages.subscription_reactivated'), 'success');
await this.loadData();
} catch (error) {
billingLog.error('Error reactivating subscription:', error);
Utils.showToast('Failed to reactivate subscription', 'error');
Utils.showToast(I18n.t('billing.messages.failed_to_reactivate_subscription'), 'error');
}
},
@@ -159,7 +162,7 @@ function vendorBilling() {
}
} catch (error) {
billingLog.error('Error purchasing addon:', error);
Utils.showToast('Failed to purchase add-on', 'error');
Utils.showToast(I18n.t('billing.messages.failed_to_purchase_addon'), 'error');
} finally {
this.purchasingAddon = null;
}
@@ -172,11 +175,11 @@ function vendorBilling() {
try {
await apiClient.delete(`/vendor/billing/addons/${addon.id}`);
Utils.showToast('Add-on cancelled successfully', 'success');
Utils.showToast(I18n.t('billing.messages.addon_cancelled_successfully'), 'success');
await this.loadData();
} catch (error) {
billingLog.error('Error cancelling addon:', error);
Utils.showToast('Failed to cancel add-on', 'error');
Utils.showToast(I18n.t('billing.messages.failed_to_cancel_addon'), 'error');
}
},

View File

@@ -2,7 +2,7 @@
{# Standalone Pricing Page #}
{% extends "public/base.html" %}
{% block title %}{{ _("platform.pricing.title") }} - Wizamart{% endblock %}
{% block title %}{{ _("cms.platform.pricing.title") }} - Wizamart{% endblock %}
{% block content %}
<div x-data="{ annual: false }" class="py-16 lg:py-24">
@@ -10,15 +10,15 @@
{# Header #}
<div class="text-center mb-12">
<h1 class="text-4xl md:text-5xl font-bold text-gray-900 dark:text-white mb-4">
{{ _("platform.pricing.title") }}
{{ _("cms.platform.pricing.title") }}
</h1>
<p class="text-xl text-gray-600 dark:text-gray-400 max-w-2xl mx-auto">
{{ _("platform.pricing.trial_note", trial_days=trial_days) }}
{{ _("cms.platform.pricing.trial_note", trial_days=trial_days) }}
</p>
{# Billing Toggle #}
<div class="flex items-center justify-center mt-8 space-x-4">
<span class="text-gray-700 dark:text-gray-300" :class="{ 'font-semibold': !annual }">{{ _("platform.pricing.monthly") }}</span>
<span class="text-gray-700 dark:text-gray-300" :class="{ 'font-semibold': !annual }">{{ _("cms.platform.pricing.monthly") }}</span>
<button @click="annual = !annual"
class="relative w-14 h-7 rounded-full transition-colors"
:class="annual ? 'bg-indigo-600' : 'bg-gray-300 dark:bg-gray-600'">
@@ -26,8 +26,8 @@
:class="annual ? 'translate-x-7' : ''"></span>
</button>
<span class="text-gray-700 dark:text-gray-300" :class="{ 'font-semibold': annual }">
{{ _("platform.pricing.annual") }}
<span class="text-green-600 text-sm font-medium ml-1">{{ _("platform.pricing.save_months") }}</span>
{{ _("cms.platform.pricing.annual") }}
<span class="text-green-600 text-sm font-medium ml-1">{{ _("cms.platform.pricing.save_months") }}</span>
</span>
</div>
</div>
@@ -40,7 +40,7 @@
{% if tier.is_popular %}
<div class="absolute -top-3 left-1/2 -translate-x-1/2">
<span class="bg-indigo-600 text-white text-xs font-bold px-3 py-1 rounded-full">{{ _("platform.pricing.recommended") }}</span>
<span class="bg-indigo-600 text-white text-xs font-bold px-3 py-1 rounded-full">{{ _("cms.platform.pricing.recommended") }}</span>
</div>
{% endif %}
@@ -50,17 +50,17 @@
<template x-if="!annual">
<div>
<span class="text-4xl font-extrabold text-gray-900 dark:text-white">{{ tier.price_monthly }}</span>
<span class="text-gray-500">{{ _("platform.pricing.per_month") }}</span>
<span class="text-gray-500">{{ _("cms.platform.pricing.per_month") }}</span>
</div>
</template>
<template x-if="annual">
<div>
{% if tier.price_annual %}
<span class="text-4xl font-extrabold text-gray-900 dark:text-white">{{ (tier.price_annual / 12)|round(0)|int }}</span>
<span class="text-gray-500">{{ _("platform.pricing.per_month") }}</span>
<div class="text-sm text-gray-500">{{ tier.price_annual }}{{ _("platform.pricing.per_year") }}</div>
<span class="text-gray-500">{{ _("cms.platform.pricing.per_month") }}</span>
<div class="text-sm text-gray-500">{{ tier.price_annual }}{{ _("cms.platform.pricing.per_year") }}</div>
{% else %}
<span class="text-2xl font-bold text-gray-900 dark:text-white">{{ _("platform.pricing.custom") }}</span>
<span class="text-2xl font-bold text-gray-900 dark:text-white">{{ _("cms.platform.pricing.custom") }}</span>
{% endif %}
</div>
</template>
@@ -71,37 +71,37 @@
<svg class="w-4 h-4 text-green-500 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/>
</svg>
{% if tier.orders_per_month %}{{ _("platform.pricing.orders_per_month", count=tier.orders_per_month) }}{% else %}{{ _("platform.pricing.unlimited_orders") }}{% endif %}
{% if tier.orders_per_month %}{{ _("cms.platform.pricing.orders_per_month", count=tier.orders_per_month) }}{% else %}{{ _("cms.platform.pricing.unlimited_orders") }}{% endif %}
</li>
<li class="flex items-center text-gray-700 dark:text-gray-300">
<svg class="w-4 h-4 text-green-500 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/>
</svg>
{% if tier.products_limit %}{{ _("platform.pricing.products_limit", count=tier.products_limit) }}{% else %}{{ _("platform.pricing.unlimited_products") }}{% endif %}
{% if tier.products_limit %}{{ _("cms.platform.pricing.products_limit", count=tier.products_limit) }}{% else %}{{ _("cms.platform.pricing.unlimited_products") }}{% endif %}
</li>
<li class="flex items-center text-gray-700 dark:text-gray-300">
<svg class="w-4 h-4 text-green-500 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/>
</svg>
{% if tier.team_members %}{{ _("platform.pricing.team_members", count=tier.team_members) }}{% else %}{{ _("platform.pricing.unlimited_team") }}{% endif %}
{% if tier.team_members %}{{ _("cms.platform.pricing.team_members", count=tier.team_members) }}{% else %}{{ _("cms.platform.pricing.unlimited_team") }}{% endif %}
</li>
<li class="flex items-center text-gray-700 dark:text-gray-300">
<svg class="w-4 h-4 text-green-500 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/>
</svg>
{{ _("platform.pricing.letzshop_sync") }}
{{ _("cms.platform.pricing.letzshop_sync") }}
</li>
</ul>
{% if tier.is_enterprise %}
<a href="/contact" class="block w-full py-3 bg-gray-200 dark:bg-gray-700 text-gray-900 dark:text-white font-semibold rounded-xl text-center hover:bg-gray-300 transition-colors">
{{ _("platform.pricing.contact_sales") }}
{{ _("cms.platform.pricing.contact_sales") }}
</a>
{% else %}
<a :href="'/signup?tier={{ tier.code }}&annual=' + annual"
class="block w-full py-3 font-semibold rounded-xl text-center transition-colors
{% if tier.is_popular %}bg-indigo-600 hover:bg-indigo-700 text-white{% else %}bg-indigo-100 text-indigo-700 hover:bg-indigo-200{% endif %}">
{{ _("platform.pricing.start_trial") }}
{{ _("cms.platform.pricing.start_trial") }}
</a>
{% endif %}
</div>
@@ -111,7 +111,7 @@
{# Back to Home #}
<div class="text-center mt-12">
<a href="/" class="text-indigo-600 dark:text-indigo-400 hover:underline">
&larr; {{ _("platform.pricing.back_home") }}
&larr; {{ _("cms.platform.pricing.back_home") }}
</a>
</div>
</div>

View File

@@ -2,7 +2,7 @@
{# Signup Success Page #}
{% extends "public/base.html" %}
{% block title %}{{ _("platform.success.title") }}{% endblock %}
{% block title %}{{ _("cms.platform.success.title") }}{% endblock %}
{% block content %}
<div class="min-h-screen py-16 bg-gray-50 dark:bg-gray-900 flex items-center justify-center">
@@ -17,23 +17,23 @@
{# Welcome Message #}
<h1 class="text-4xl font-bold text-gray-900 dark:text-white mb-4">
{{ _("platform.success.title") }}
{{ _("cms.platform.success.title") }}
</h1>
<p class="text-xl text-gray-600 dark:text-gray-400 mb-8">
{{ _("platform.success.subtitle", trial_days=trial_days) }}
{{ _("cms.platform.success.subtitle", trial_days=trial_days) }}
</p>
{# Next Steps #}
<div class="bg-white dark:bg-gray-800 rounded-2xl p-8 border border-gray-200 dark:border-gray-700 text-left mb-8">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">{{ _("platform.success.what_next") }}</h2>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">{{ _("cms.platform.success.what_next") }}</h2>
<ul class="space-y-4">
<li class="flex items-start">
<div class="w-6 h-6 bg-indigo-100 dark:bg-indigo-900/30 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5">
<span class="text-indigo-600 dark:text-indigo-400 text-sm font-bold">1</span>
</div>
<span class="ml-3 text-gray-700 dark:text-gray-300">
<strong>{{ _("platform.success.step_connect") }}</strong> {{ _("platform.success.step_connect_desc") }}
<strong>{{ _("cms.platform.success.step_connect") }}</strong> {{ _("cms.platform.success.step_connect_desc") }}
</span>
</li>
<li class="flex items-start">
@@ -41,7 +41,7 @@
<span class="text-indigo-600 dark:text-indigo-400 text-sm font-bold">2</span>
</div>
<span class="ml-3 text-gray-700 dark:text-gray-300">
<strong>{{ _("platform.success.step_invoicing") }}</strong> {{ _("platform.success.step_invoicing_desc") }}
<strong>{{ _("cms.platform.success.step_invoicing") }}</strong> {{ _("cms.platform.success.step_invoicing_desc") }}
</span>
</li>
<li class="flex items-start">
@@ -49,7 +49,7 @@
<span class="text-indigo-600 dark:text-indigo-400 text-sm font-bold">3</span>
</div>
<span class="ml-3 text-gray-700 dark:text-gray-300">
<strong>{{ _("platform.success.step_products") }}</strong> {{ _("platform.success.step_products_desc") }}
<strong>{{ _("cms.platform.success.step_products") }}</strong> {{ _("cms.platform.success.step_products_desc") }}
</span>
</li>
</ul>
@@ -59,7 +59,7 @@
{% if vendor_code %}
<a href="/vendor/{{ vendor_code }}/dashboard"
class="inline-flex items-center px-8 py-4 bg-indigo-600 hover:bg-indigo-700 text-white font-semibold rounded-xl shadow-lg transition-all hover:scale-105">
{{ _("platform.success.go_to_dashboard") }}
{{ _("cms.platform.success.go_to_dashboard") }}
<svg class="w-5 h-5 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"/>
</svg>
@@ -67,14 +67,14 @@
{% else %}
<a href="/admin/login"
class="inline-flex items-center px-8 py-4 bg-indigo-600 hover:bg-indigo-700 text-white font-semibold rounded-xl shadow-lg transition-all">
{{ _("platform.success.login_dashboard") }}
{{ _("cms.platform.success.login_dashboard") }}
</a>
{% endif %}
{# Support Link #}
<p class="mt-8 text-gray-500 dark:text-gray-400">
{{ _("platform.success.need_help") }}
<a href="/contact" class="text-indigo-600 dark:text-indigo-400 hover:underline">{{ _("platform.success.contact_support") }}</a>
{{ _("cms.platform.success.need_help") }}
<a href="/contact" class="text-indigo-600 dark:text-indigo-400 hover:underline">{{ _("cms.platform.success.contact_support") }}</a>
</p>
</div>
</div>