Compare commits

..

3 Commits

Author SHA1 Message Date
a28d5d1de5 fix(i18n): convert remaining $t() to server-side _() and fix store dashboard language
Some checks failed
CI / pytest (push) Waiting to run
CI / ruff (push) Successful in 13s
CI / dependency-scanning (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / validate (push) Has been cancelled
- Convert storefront enrollment $t() calls to server-side _() to silence
  dev-toolbar warnings (welcome bonus + join button)
- Fix store base template I18n.init() to use current_language (from middleware)
  instead of dashboard_language (hardcoded store config) so language changes
  take effect immediately
- Switch admin loyalty routes to use get_admin_context() for proper i18n support
- Switch store loyalty routes to use core get_store_context() from page_context
- Pass program object to storefront enrollment context for server-side rendering
- Add LANG-011 architecture rule: enforce $t()/_() over I18n.t() in templates
- Fix duplicate file_pattern key in LANG-004 rule (YAML validation error)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 17:00:42 +01:00
502473eee4 feat(seed): add WizaMart merchant with OMS trial and wizamart.com custom domain
Adds WizaMart S.à r.l. as a demo merchant with:
- OMS platform subscription (essential tier, 30-day trial)
- Custom domain wizamart.com linked to OMS platform
- Idempotent: safe to run multiple times

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 14:33:31 +01:00
183f55c7b3 docs(deployment): add runbooks for store subdomains, custom domains, and new platforms
- Update origin cert config: wildcards for omsflow.lu, rewardflow.lu, hostwizard.lu
- Add wildcard Caddy blocks to production Caddyfile example
- Replace "Future" section with actionable runbooks:
  - Add a Store Subdomain (self-service, no infra changes)
  - Add a Custom Store Domain (Cloudflare + Caddy + DB)
  - Add a New Platform Domain (full setup)
- Document wizard.lu exception (no wildcard due to git.wizard.lu DNS-only)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 12:31:56 +01:00
9 changed files with 279 additions and 106 deletions

View File

@@ -111,11 +111,9 @@ language_rules:
function languageSelector(currentLang, enabledLanguages) { ... } function languageSelector(currentLang, enabledLanguages) { ... }
window.languageSelector = languageSelector; window.languageSelector = languageSelector;
pattern: pattern:
file_pattern: "static/shop/js/shop-layout.js" file_patterns:
required_patterns: - "static/shop/js/shop-layout.js"
- "function languageSelector" - "static/vendor/js/init-alpine.js"
- "window.languageSelector"
file_pattern: "static/vendor/js/init-alpine.js"
required_patterns: required_patterns:
- "function languageSelector" - "function languageSelector"
- "window.languageSelector" - "window.languageSelector"
@@ -247,3 +245,26 @@ language_rules:
pattern: pattern:
file_pattern: "static/locales/*.json" file_pattern: "static/locales/*.json"
check: "valid_json" check: "valid_json"
- id: "LANG-011"
name: "Use $t() not I18n.t() in HTML templates"
severity: "error"
description: |
In HTML templates, never use I18n.t() directly. It evaluates once
and does NOT re-evaluate when translations finish loading async.
WRONG (non-reactive, shows raw key then updates):
<span x-text="I18n.t('module.key')"></span>
RIGHT (reactive, updates when translations load):
<span x-text="$t('module.key')"></span>
BEST (server-side, zero flash):
<span>{{ _('module.key') }}</span>
Note: I18n.t() is fine in .js files where it's called inside
async callbacks after I18n.init() has completed.
pattern:
file_pattern: "**/*.html"
anti_patterns:
- "I18n.t("

View File

@@ -13,6 +13,7 @@ from fastapi.responses import HTMLResponse
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.api.deps import get_db, require_menu_access from app.api.deps import get_db, require_menu_access
from app.modules.core.utils.page_context import get_admin_context
from app.modules.enums import FrontendType from app.modules.enums import FrontendType
from app.modules.tenancy.models import User from app.modules.tenancy.models import User
from app.templates_config import templates from app.templates_config import templates
@@ -43,10 +44,7 @@ async def admin_loyalty_programs(
""" """
return templates.TemplateResponse( return templates.TemplateResponse(
"loyalty/admin/programs.html", "loyalty/admin/programs.html",
{ get_admin_context(request, db, current_user),
"request": request,
"user": current_user,
},
) )
@@ -62,10 +60,7 @@ async def admin_loyalty_analytics(
""" """
return templates.TemplateResponse( return templates.TemplateResponse(
"loyalty/admin/analytics.html", "loyalty/admin/analytics.html",
{ get_admin_context(request, db, current_user),
"request": request,
"user": current_user,
},
) )
@@ -91,11 +86,7 @@ async def admin_loyalty_merchant_detail(
""" """
return templates.TemplateResponse( return templates.TemplateResponse(
"loyalty/admin/merchant-detail.html", "loyalty/admin/merchant-detail.html",
{ get_admin_context(request, db, current_user, merchant_id=merchant_id),
"request": request,
"user": current_user,
"merchant_id": merchant_id,
},
) )
@@ -116,11 +107,7 @@ async def admin_loyalty_program_edit(
""" """
return templates.TemplateResponse( return templates.TemplateResponse(
"loyalty/admin/program-edit.html", "loyalty/admin/program-edit.html",
{ get_admin_context(request, db, current_user, merchant_id=merchant_id),
"request": request,
"user": current_user,
"merchant_id": merchant_id,
},
) )
@@ -141,9 +128,5 @@ async def admin_loyalty_merchant_settings(
""" """
return templates.TemplateResponse( return templates.TemplateResponse(
"loyalty/admin/merchant-settings.html", "loyalty/admin/merchant-settings.html",
{ get_admin_context(request, db, current_user, merchant_id=merchant_id),
"request": request,
"user": current_user,
"merchant_id": merchant_id,
},
) )

View File

@@ -24,10 +24,8 @@ from app.api.deps import (
get_db, get_db,
get_resolved_store_code, get_resolved_store_code,
) )
from app.modules.core.services.platform_settings_service import ( from app.modules.core.utils.page_context import get_store_context
platform_settings_service, from app.modules.tenancy.models import User
)
from app.modules.tenancy.models import Store, User
from app.templates_config import templates from app.templates_config import templates
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -42,49 +40,6 @@ ROUTE_CONFIG = {
} }
# ============================================================================
# HELPER: Build Store Context
# ============================================================================
def get_store_context(
request: Request,
db: Session,
current_user: User,
store_code: str,
**extra_context,
) -> dict:
"""Build template context for store loyalty pages."""
# Load store from database
store = db.query(Store).filter(Store.subdomain == store_code).first()
# Get platform defaults
platform_config = platform_settings_service.get_storefront_config(db)
# Resolve with store override
storefront_locale = platform_config["locale"]
storefront_currency = platform_config["currency"]
if store and store.storefront_locale:
storefront_locale = store.storefront_locale
context = {
"request": request,
"user": current_user,
"store": store,
"store_code": store_code,
"storefront_locale": storefront_locale,
"storefront_currency": storefront_currency,
"dashboard_language": store.dashboard_language if store else "en",
}
# Add any extra context
if extra_context:
context.update(extra_context)
return context
# ============================================================================ # ============================================================================
# LOYALTY ROOT (Redirect to Terminal) # LOYALTY ROOT (Redirect to Terminal)
# ============================================================================ # ============================================================================

View File

@@ -17,6 +17,7 @@ from sqlalchemy.orm import Session
from app.api.deps import get_current_customer_from_cookie_or_header, get_db from app.api.deps import get_current_customer_from_cookie_or_header, get_db
from app.modules.core.utils.page_context import get_storefront_context from app.modules.core.utils.page_context import get_storefront_context
from app.modules.customers.models import Customer from app.modules.customers.models import Customer
from app.modules.loyalty.services import program_service
from app.templates_config import templates from app.templates_config import templates
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -114,7 +115,10 @@ async def loyalty_self_enrollment(
}, },
) )
context = get_storefront_context(request, db=db) store = request.state.store
program = program_service.get_active_program_by_store(db, store.id) if store else None
context = get_storefront_context(request, db=db, program=program)
return templates.TemplateResponse( return templates.TemplateResponse(
"loyalty/storefront/enroll.html", "loyalty/storefront/enroll.html",
context, context,

View File

@@ -16,7 +16,7 @@
<img src="{{ store.logo_url }}" alt="{{ store.name }}" class="h-16 w-auto mx-auto mb-4"> <img src="{{ store.logo_url }}" alt="{{ store.name }}" class="h-16 w-auto mx-auto mb-4">
{% endif %} {% endif %}
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">{{ _('loyalty.enrollment.title') }}</h1> <h1 class="text-3xl font-bold text-gray-900 dark:text-white">{{ _('loyalty.enrollment.title') }}</h1>
<p class="mt-2 text-gray-600 dark:text-gray-400" x-text="I18n.t('loyalty.enrollment.subtitle', {points: program?.points_per_euro || 1})"></p> <p class="mt-2 text-gray-600 dark:text-gray-400">{{ _('loyalty.enrollment.subtitle', points=program.points_per_euro if program else 1) }}</p>
</div> </div>
<!-- Loading --> <!-- Loading -->
@@ -38,7 +38,7 @@
class="p-4 text-center text-white" class="p-4 text-center text-white"
:style="'background-color: ' + (program?.card_color || 'var(--color-primary)')"> :style="'background-color: ' + (program?.card_color || 'var(--color-primary)')">
<span x-html="$icon('gift', 'w-6 h-6 inline mr-2')"></span> <span x-html="$icon('gift', 'w-6 h-6 inline mr-2')"></span>
<span class="font-semibold" x-text="I18n.t('loyalty.enrollment.welcome_bonus', {points: program?.welcome_bonus_points})"></span> <span class="font-semibold">{{ _('loyalty.enrollment.welcome_bonus', points=program.welcome_bonus_points if program else 0) }}</span>
</div> </div>
<form @submit.prevent="submitEnrollment" class="p-6 space-y-4"> <form @submit.prevent="submitEnrollment" class="p-6 space-y-4">
@@ -119,7 +119,7 @@
class="w-full py-3 px-4 text-white font-semibold rounded-lg transition-colors disabled:opacity-50" class="w-full py-3 px-4 text-white font-semibold rounded-lg transition-colors disabled:opacity-50"
:style="'background-color: ' + (program?.card_color || 'var(--color-primary)')"> :style="'background-color: ' + (program?.card_color || 'var(--color-primary)')">
<span x-show="enrolling" x-html="$icon('spinner', 'w-5 h-5 inline animate-spin mr-2')"></span> <span x-show="enrolling" x-html="$icon('spinner', 'w-5 h-5 inline animate-spin mr-2')"></span>
<span x-text="enrolling ? I18n.t('loyalty.enrollment.form.joining') : I18n.t('loyalty.enrollment.form.join_button', {points: program?.welcome_bonus_points || 0})"></span> <span x-text="enrolling ? '{{ _('loyalty.enrollment.form.joining') }}' : '{{ _('loyalty.enrollment.form.join_button', points=program.welcome_bonus_points if program else 0) }}'"></span>
</button> </button>
</form> </form>

View File

@@ -84,7 +84,7 @@
// Wrapped in DOMContentLoaded so deferred i18n.js has loaded // Wrapped in DOMContentLoaded so deferred i18n.js has loaded
document.addEventListener('DOMContentLoaded', async function() { document.addEventListener('DOMContentLoaded', async function() {
const modules = {% block i18n_modules %}[]{% endblock %}; const modules = {% block i18n_modules %}[]{% endblock %};
await I18n.init('{{ dashboard_language | default("en") }}', modules); await I18n.init('{{ current_language | default("en") }}', modules);
}); });
</script> </script>

View File

@@ -758,7 +758,7 @@ Then restart Gitea:
cd ~/gitea && docker compose up -d gitea cd ~/gitea && docker compose up -d gitea
``` ```
### Future: Multi-Tenant Store Routing ### Multi-Tenant Store Routing
Stores on each platform use two routing modes: Stores on each platform use two routing modes:
@@ -769,49 +769,204 @@ Both modes are handled by the `StoreContextMiddleware` which reads the `Host` he
#### Wildcard Subdomains (for store subdomains) #### Wildcard Subdomains (for store subdomains)
When stores start using subdomains like `acme.omsflow.lu`, add wildcard blocks: Each non-main platform uses a wildcard Caddy block so any store subdomain (e.g. `acme.omsflow.lu`, `parcelproxy.hostwizard.lu`) is automatically routed without per-store Caddy changes.
The wildcard blocks use the same origin cert as the platform root domain. The origin cert must include `*.<platform_domain>` — see [Step 21.4](#214-generate-origin-certificates) for how to generate it.
```caddy ```caddy
*.omsflow.lu { *.omsflow.lu {
tls /etc/caddy/certs/omsflow.lu/cert.pem /etc/caddy/certs/omsflow.lu/key.pem
reverse_proxy localhost:8001 reverse_proxy localhost:8001
} }
*.rewardflow.lu { *.rewardflow.lu {
tls /etc/caddy/certs/rewardflow.lu/cert.pem /etc/caddy/certs/rewardflow.lu/key.pem
reverse_proxy localhost:8001 reverse_proxy localhost:8001
} }
*.wizard.lu { *.hostwizard.lu {
tls /etc/caddy/certs/hostwizard.lu/cert.pem /etc/caddy/certs/hostwizard.lu/key.pem
reverse_proxy localhost:8001 reverse_proxy localhost:8001
} }
``` ```
!!! warning "Wildcard SSL requires DNS challenge" !!! warning "No wildcard for wizard.lu"
Let's Encrypt cannot issue wildcard certificates via HTTP challenge. Wildcard certs require a **DNS challenge**, which means installing a Caddy DNS provider plugin (e.g. `caddy-dns/cloudflare`) and configuring API credentials for your DNS provider. See [Caddy DNS challenge docs](https://caddyserver.com/docs/automatic-https#dns-challenge). `wizard.lu` cannot use a wildcard block because `git.wizard.lu` is DNS-only (grey cloud in Cloudflare) and uses a Let's Encrypt cert. A wildcard origin cert would conflict. For wizard.lu subdomains, add each one explicitly (same pattern as `api.wizard.lu`, `flower.wizard.lu`).
#### Custom Store Domains (for premium stores) **Cloudflare DNS**: Add a wildcard DNS record for each platform:
When premium stores bring their own domains (e.g. `acme.lu`), use Caddy's **on-demand TLS**: - `*.omsflow.lu` → A `91.99.65.229` (proxied, orange cloud)
- `*.rewardflow.lu` → A `91.99.65.229` (proxied, orange cloud)
- `*.hostwizard.lu` → A `91.99.65.229` (proxied, orange cloud)
With this in place, adding a new store subdomain only requires a database entry (via admin UI) — no DNS or Caddy changes needed.
#### Runbook: Add a Store Subdomain
When a merchant creates a store with subdomain `acme` on the OMS platform:
1. **Database**: Create the store via admin UI — set `subdomain = "acme"` and link to the platform. The `StoreContextMiddleware` will resolve `acme.omsflow.lu` automatically.
2. **DNS**: Already covered by the wildcard `*.omsflow.lu` record in Cloudflare.
3. **Caddy**: Already covered by the `*.omsflow.lu` block.
4. **SSL**: Already covered by the wildcard origin cert.
5. **Verify**: `curl -I https://acme.omsflow.lu/`
No infrastructure changes needed — it's fully self-service.
#### Runbook: Add a Custom Store Domain
When a premium store brings their own domain (e.g. `wizamart.com`), the domain must be added to **your** Cloudflare account. This ensures you control SSL, WAF, and DNS — critical since you are responsible for the infrastructure.
**Step 1: Add domain to Cloudflare**
1. In [Cloudflare Dashboard](https://dash.cloudflare.com), click **Add a site** > enter `wizamart.com`
2. Select the Free plan
3. Cloudflare assigns NS records — give these to the store owner to update at their registrar
4. Wait for NS propagation (can take up to 24h, usually minutes)
**Step 2: Configure DNS in Cloudflare**
Add A records (proxied, orange cloud):
| Type | Name | Content | Proxy |
|------|------|---------|-------|
| A | `wizamart.com` | `91.99.65.229` | Proxied |
| A | `www` | `91.99.65.229` | Proxied |
**Step 3: Generate origin certificate**
1. In Cloudflare: **SSL/TLS** > **Origin Server** > **Create Certificate**
2. Hostnames: `wizamart.com, www.wizamart.com`
3. Validity: 15 years
4. Download cert and key (key shown only once!)
**Step 4: Install cert on server**
```bash
sudo mkdir -p /etc/caddy/certs/wizamart.com
sudo nano /etc/caddy/certs/wizamart.com/cert.pem # paste certificate
sudo nano /etc/caddy/certs/wizamart.com/key.pem # paste private key
sudo chown -R caddy:caddy /etc/caddy/certs/wizamart.com
sudo chmod 600 /etc/caddy/certs/wizamart.com/key.pem
```
**Step 5: Add to Caddyfile**
```bash
sudo nano /etc/caddy/Caddyfile
```
Add:
```caddy ```caddy
https:// { # ─── Custom store domain: wizamart.com ────────────────────────
tls { wizamart.com {
on_demand tls /etc/caddy/certs/wizamart.com/cert.pem /etc/caddy/certs/wizamart.com/key.pem
} reverse_proxy localhost:8001
}
www.wizamart.com {
tls /etc/caddy/certs/wizamart.com/cert.pem /etc/caddy/certs/wizamart.com/key.pem
redir https://wizamart.com{uri} permanent
}
```
**Step 6: Reload Caddy**
```bash
sudo systemctl reload caddy
sudo systemctl status caddy
```
**Step 7: Configure Cloudflare settings**
In Cloudflare dashboard for `wizamart.com`:
| Setting | Location | Value |
|---|---|---|
| SSL mode | SSL/TLS > Overview | Full (Strict) |
| Always Use HTTPS | SSL/TLS > Edge Certificates | On |
| Bot Fight Mode | Security > Settings | On |
**Step 8: Register domain in database**
Via admin UI: create a `StoreDomain` record linking `wizamart.com` to the store and platform.
**Step 9: Verify**
```bash
curl -I https://wizamart.com/
```
The `StoreContextMiddleware` will detect `wizamart.com` as a custom domain, look it up in the `store_domains` table, and route to the correct store.
#### Runbook: Add a New Platform Domain
When adding an entirely new platform (e.g. `newplatform.lu`):
**Step 1: Cloudflare**
1. Add `newplatform.lu` as a site in Cloudflare
2. Configure NS at registrar
3. Add DNS records (all proxied):
- `newplatform.lu` → A `91.99.65.229`
- `www.newplatform.lu` → A `91.99.65.229`
- `*.newplatform.lu` → A `91.99.65.229` (for store subdomains)
**Step 2: Generate origin certificate**
In Cloudflare: **SSL/TLS** > **Origin Server** > **Create Certificate**
Hostnames: `newplatform.lu, www.newplatform.lu, *.newplatform.lu`
**Step 3: Install cert and update Caddyfile**
```bash
sudo mkdir -p /etc/caddy/certs/newplatform.lu
sudo nano /etc/caddy/certs/newplatform.lu/cert.pem
sudo nano /etc/caddy/certs/newplatform.lu/key.pem
sudo chown -R caddy:caddy /etc/caddy/certs/newplatform.lu
sudo chmod 600 /etc/caddy/certs/newplatform.lu/key.pem
```
Add to Caddyfile:
```caddy
# ─── Platform: NewPlatform (newplatform.lu) ───────────────────
newplatform.lu {
tls /etc/caddy/certs/newplatform.lu/cert.pem /etc/caddy/certs/newplatform.lu/key.pem
reverse_proxy localhost:8001
}
www.newplatform.lu {
tls /etc/caddy/certs/newplatform.lu/cert.pem /etc/caddy/certs/newplatform.lu/key.pem
redir https://newplatform.lu{uri} permanent
}
*.newplatform.lu {
tls /etc/caddy/certs/newplatform.lu/cert.pem /etc/caddy/certs/newplatform.lu/key.pem
reverse_proxy localhost:8001 reverse_proxy localhost:8001
} }
``` ```
On-demand TLS auto-provisions SSL certificates when a new domain connects. Add an `ask` endpoint to validate that the domain is registered in the `store_domains` table, preventing abuse: ```bash
sudo systemctl reload caddy
```caddy
tls {
on_demand
ask http://localhost:8001/api/v1/internal/verify-domain
}
``` ```
!!! note "Not needed yet" **Step 4: Cloudflare settings**
Wildcard subdomains and custom domains are future work. The current Caddyfile handles all platform root domains and service subdomains.
Same as other platforms — see [Step 21.6](#216-cloudflare-settings-per-domain).
**Step 5: Database**
Create the platform record via `init_production.py` or admin UI with `domain = "newplatform.lu"`.
**Step 6: Verify**
```bash
curl -I https://newplatform.lu/
curl -I https://teststore.newplatform.lu/
```
## Step 15: Gitea Actions Runner ## Step 15: Gitea Actions Runner
@@ -2007,15 +2162,15 @@ Disable DNSSEC at the registrar before switching NS — re-enable later via Clou
Cloudflare Origin Certificates (free, 15-year validity) avoid ACME challenge issues when traffic is proxied: Cloudflare Origin Certificates (free, 15-year validity) avoid ACME challenge issues when traffic is proxied:
1. In Cloudflare: **SSL/TLS** > **Origin Server** > **Create Certificate** 1. In Cloudflare: **SSL/TLS** > **Origin Server** > **Create Certificate**
2. Generate for each domain with **specific subdomains** (not wildcards): 2. Generate for each domain:
- `wizard.lu`: `wizard.lu, api.wizard.lu, www.wizard.lu, flower.wizard.lu, grafana.wizard.lu` - `wizard.lu`: `wizard.lu, api.wizard.lu, www.wizard.lu, flower.wizard.lu, grafana.wizard.lu` (**specific subdomains, no wildcard**)
- `omsflow.lu`: `omsflow.lu, www.omsflow.lu` - `omsflow.lu`: `omsflow.lu, www.omsflow.lu, *.omsflow.lu` (wildcard for store subdomains)
- `rewardflow.lu`: `rewardflow.lu, www.rewardflow.lu` - `rewardflow.lu`: `rewardflow.lu, www.rewardflow.lu, *.rewardflow.lu` (wildcard for store subdomains)
- `hostwizard.lu`: `hostwizard.lu, www.hostwizard.lu` - `hostwizard.lu`: `hostwizard.lu, www.hostwizard.lu, *.hostwizard.lu` (wildcard for store subdomains)
3. Download the certificate and private key (private key is shown only once) 3. Download the certificate and private key (private key is shown only once)
!!! warning "Do NOT use wildcard origin certs for wizard.lu" !!! warning "Do NOT use wildcard origin certs for wizard.lu"
A `*.wizard.lu` wildcard cert will match `git.wizard.lu`, which needs a Let's Encrypt cert (DNS-only, not proxied through Cloudflare). Use specific subdomains instead. A `*.wizard.lu` wildcard cert will match `git.wizard.lu`, which is DNS-only (grey cloud) and uses a Let's Encrypt cert. A wildcard origin cert would conflict. Use specific subdomains instead. For new wizard.lu subdomains, add them explicitly to this cert and to the Caddyfile.
Install on the server: Install on the server:
@@ -2090,6 +2245,22 @@ www.hostwizard.lu {
redir https://hostwizard.lu{uri} permanent redir https://hostwizard.lu{uri} permanent
} }
# ─── Store subdomains (wildcard — all except wizard.lu) ──────
*.omsflow.lu {
tls /etc/caddy/certs/omsflow.lu/cert.pem /etc/caddy/certs/omsflow.lu/key.pem
reverse_proxy localhost:8001
}
*.rewardflow.lu {
tls /etc/caddy/certs/rewardflow.lu/cert.pem /etc/caddy/certs/rewardflow.lu/key.pem
reverse_proxy localhost:8001
}
*.hostwizard.lu {
tls /etc/caddy/certs/hostwizard.lu/cert.pem /etc/caddy/certs/hostwizard.lu/key.pem
reverse_proxy localhost:8001
}
# ─── Services (wizard.lu origin cert) ─────────────────────── # ─── Services (wizard.lu origin cert) ───────────────────────
api.wizard.lu { api.wizard.lu {
tls /etc/caddy/certs/wizard.lu/cert.pem /etc/caddy/certs/wizard.lu/key.pem tls /etc/caddy/certs/wizard.lu/cert.pem /etc/caddy/certs/wizard.lu/key.pem

View File

@@ -153,6 +153,19 @@ DEMO_COMPANIES = [
"business_address": "12 Rue du Web, Differdange, L-4501, Luxembourg", "business_address": "12 Rue du Web, Differdange, L-4501, Luxembourg",
"tax_number": "LU45678901", "tax_number": "LU45678901",
}, },
{
"name": "WizaMart S.à r.l.",
"description": "Online marketplace for everyday essentials and home goods",
"owner_email": "alex.owner@wizamart.com",
"owner_password": "password123", # noqa: SEC001
"owner_first_name": "Alex",
"owner_last_name": "Dupont",
"contact_email": "info@wizamart.com",
"contact_phone": "+352 567 890 123",
"website": "https://www.wizamart.com",
"business_address": "88 Route d'Arlon, Strassen, L-8008, Luxembourg",
"tax_number": "LU56789012",
},
] ]
# Demo store configurations (linked to merchants by index) # Demo store configurations (linked to merchants by index)
@@ -245,6 +258,17 @@ DEMO_STORES = [
"theme_preset": "modern", "theme_preset": "modern",
"custom_domain": None, "custom_domain": None,
}, },
# WizaMart store (OMS platform, custom domain)
{
"merchant_index": 4, # WizaMart
"store_code": "WIZAMART",
"name": "WizaMart",
"subdomain": "wizamart",
"description": "Online marketplace for everyday essentials and home goods",
"theme_preset": "modern",
"custom_domain": "wizamart.com",
"custom_domain_platform": "oms",
},
] ]
# Demo subscriptions (linked to merchants by index) # Demo subscriptions (linked to merchants by index)
@@ -258,6 +282,8 @@ DEMO_SUBSCRIPTIONS = [
{"merchant_index": 2, "platform_code": "oms", "tier_code": "business", "trial_days": 0}, {"merchant_index": 2, "platform_code": "oms", "tier_code": "business", "trial_days": 0},
# LuxWeb Agency: Hosting (professional, active) # LuxWeb Agency: Hosting (professional, active)
{"merchant_index": 3, "platform_code": "hosting", "tier_code": "professional", "trial_days": 0}, {"merchant_index": 3, "platform_code": "hosting", "tier_code": "professional", "trial_days": 0},
# WizaMart: OMS (essential, trial)
{"merchant_index": 4, "platform_code": "oms", "tier_code": "essential", "trial_days": 30},
] ]
# Demo team members (linked to merchants by index, assigned to stores by store_code) # Demo team members (linked to merchants by index, assigned to stores by store_code)

View File

@@ -3758,6 +3758,19 @@ class ArchitectureValidator:
context=line.strip()[:80], context=line.strip()[:80],
suggestion="Use single quotes: x-data='func({{ data|tojson }})'", suggestion="Use single quotes: x-data='func({{ data|tojson }})'",
) )
# LANG-011: I18n.t() in templates (should use $t() or server-side _())
if "I18n.t(" in line:
self._add_violation(
rule_id="LANG-011",
rule_name="Use $t() not I18n.t() in HTML templates",
severity=Severity.ERROR,
file_path=file_path,
line_number=i,
message="I18n.t() is non-reactive in templates. Use $t() (reactive) or _() (server-side)",
context=line.strip()[:80],
suggestion="Replace I18n.t('key') with $t('key') or {{ _('key') }}",
)
except Exception: except Exception:
pass # Skip files that can't be read pass # Skip files that can't be read