diff --git a/.env.docker.dev b/.env.docker.dev
index 8428360..fc580e6 100644
--- a/.env.docker.dev
+++ b/.env.docker.dev
@@ -21,11 +21,11 @@ ADMIN_INTERNAL_URL=http://host.docker.internal:3002
DASHBOARD_ASSET_PREFIX=http://localhost:3001/dashboard
ADMIN_ASSET_PREFIX=http://localhost:3002/admin
CARPLACE_ASSET_PREFIX=http://localhost:3004/carplace
-JWT_SECRET=placeholder
+JWT_SECRET=JYNKxfyYaZbqT6NN8W4pXu0zOUvpunrDPdtC0I6OZPzq0B5RRI1Ybub00
JWT_EXPIRY=8h
RENTER_JWT_EXPIRY=7d
ADMIN_SEED_EMAIL=rentaldrivego@gmail.com
-ADMIN_SEED_PASSWORD=placeholder
+ADMIN_SEED_PASSWORD=Qwerty00!@#$%
ADMIN_SEED_FIRST_NAME=Platform
ADMIN_SEED_LAST_NAME=Admin
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=
@@ -34,17 +34,17 @@ NODE_ENV=development
CORS_ORIGINS=http://localhost:3000,http://localhost:3001,http://localhost:3002,http://localhost:4000,http://127.0.0.1:3000,http://127.0.0.1:3001,http://127.0.0.1:3002,http://127.0.0.1:4000
-# Email — Resend (primary) with SMTP fallback
-# Get your API key at https://resend.com/api-keys
+# Email — Gmail SMTP for local Docker development
+EMAIL_PROVIDER=gmail
RESEND_API_KEY=re_PLACEHOLDER
EMAIL_FROM=noreply@rentaldrivego.ma
EMAIL_FROM_NAME=RentalDriveGo
-# SMTP fallback (only used if Resend fails or is unconfigured)
+# Use a Gmail app password, not your normal Google account password.
MAIL_HOST=smtp.gmail.com
MAIL_PORT=587
MAIL_SCHEME=smtp
MAIL_USERNAME=rentaldrivego@gmail.com
-MAIL_PASSWORD=placeholder
+MAIL_PASSWORD=kfahihfzbcvkczew
MAIL_FROM_ADDRESS=rentaldrivego@gmail.com
MAIL_FROM_NAME=RentalDriveGo
MAIL_REPLY_TO_ADDRESS=rentaldrivego@gmail.com
@@ -55,4 +55,18 @@ MAIL_REPLY_TO_NAME=RentalDriveGo
# STRIPE_API_KEY must be a Stripe secret/restricted key (sk_ or rk_).
# STRIPE_WEBHOOK_SECRET must be a Stripe webhook signing secret (whsec_).
STRIPE_API_KEY=sk_test_51TvTsb9SpDRZn9yJyBAlUSXcTp9zpwQfhJYNKxfyYaZbqT6NN8W4pXu0zOUvpunrDPdtC0I6OZPzq0B5RRI1Ybub00OcYvj28K
-STRIPE_WEBHOOK_SECRET=whsec_c5e0a6b2dd5e2f6ac804b428fe46f04e3af9b55c562f4124de20c52866f3c211
\ No newline at end of file
+STRIPE_WEBHOOK_SECRET=whsec_c5e0a6b2dd5e2f6ac804b428fe46f04e3af9b55c562f4124de20c52866f3c211
+
+# Manual subscription payments for local development
+MANUAL_SUBSCRIPTION_PAYMENTS_ENABLED=true
+MANUAL_PAYMENT_EVIDENCE_UPLOAD_ENABLED=true
+PAYMENT_EVIDENCE_SCANNER_MODE=stub-clean
+BANK_TRANSFER_ENABLED=true
+BANK_TRANSFER_ACCOUNT_NAME=RentalDriveGo SARL
+BANK_TRANSFER_BANK_NAME=Local Development Bank
+BANK_TRANSFER_ACCOUNT_REFERENCE=DEV-MA64-0000-0000-0000
+BANK_TRANSFER_DUE_DAYS=7
+CHECK_PAYMENT_ENABLED=true
+CHECK_PAYMENT_PAYEE=RentalDriveGo SARL
+CHECK_PAYMENT_DELIVERY_ADDRESS=Local development billing desk
+CHECK_PAYMENT_DUE_DAYS=14
diff --git a/.env.docker.production.example b/.env.docker.production.example
index 168fcb1..b823937 100644
--- a/.env.docker.production.example
+++ b/.env.docker.production.example
@@ -75,12 +75,13 @@ EMAIL_FROM_NAME=RentalDriveGo
# Option A — Resend
#RESEND_API_KEY=C8qPDuFwsv5l@KsGhL/V
# Option B — SMTP (Gmail)
-# SMTP fallback (only used if Resend fails or is unconfigured)
+# EMAIL_PROVIDER=gmail selects Gmail SMTP and skips Resend.
+EMAIL_PROVIDER=gmail
MAIL_HOST=smtp.gmail.com
MAIL_PORT=587
MAIL_SCHEME=smtp
MAIL_USERNAME=rentaldrivego@gmail.com
-MAIL_PASSWORD=placeholder
+MAIL_PASSWORD=your-16-character-gmail-app-password
MAIL_FROM_ADDRESS=rentaldrivego@gmail.com
MAIL_FROM_NAME=RentalDriveGo
MAIL_REPLY_TO_ADDRESS=rentaldrivego@gmail.com
diff --git a/.env.example b/.env.example
index b0b0427..606b890 100644
--- a/.env.example
+++ b/.env.example
@@ -110,12 +110,14 @@ ADMIN_SEED_PASSWORD=PMPS5k0D7rUeJOk0NkhI5bRtoGjkUqjK
ADMIN_SEED_FIRST_NAME=Super
ADMIN_SEED_LAST_NAME=Admin
-# SMTP fallback (only used if Resend fails or is unconfigured)
+# Email provider: auto, resend, smtp, or gmail.
+# Use a Gmail app password for Gmail SMTP, not your normal Google account password.
+EMAIL_PROVIDER=gmail
MAIL_HOST=smtp.gmail.com
MAIL_PORT=587
MAIL_SCHEME=smtp
MAIL_USERNAME=rentaldrivego@gmail.com
-MAIL_PASSWORD=kfahihfzbcvkczew
+MAIL_PASSWORD=your-16-character-gmail-app-password
MAIL_FROM_ADDRESS=rentaldrivego@gmail.com
MAIL_FROM_NAME=RentalDriveGo
MAIL_REPLY_TO_ADDRESS=rentaldrivego@gmail.com
diff --git a/MANUAL_SUBSCRIPTION_PAYMENTS.md b/MANUAL_SUBSCRIPTION_PAYMENTS.md
new file mode 100644
index 0000000..61eb961
--- /dev/null
+++ b/MANUAL_SUBSCRIPTION_PAYMENTS.md
@@ -0,0 +1,23 @@
+# Manual subscription payments
+
+This project implements bank-transfer and check collection as first-class subscription billing paths. Evidence submission is explicitly **not** settlement: only a fresh-2FA finance confirmation of cleared funds creates a successful offline payment attempt, pays the canonical billing invoice, and activates or renews the subscription.
+
+## Safe rollout
+
+1. Apply the `20260809190000_manual_subscription_payments` migration and deploy with all new flags disabled.
+2. Configure a private persistent, encrypted-at-rest `FILE_STORAGE_ROOT`; set both private-storage confirmation flags only after verifying the mounted storage; configure a fail-closed scanner executable, bank/check payer instructions, billing contacts, valid IANA timezones, and finance collections owners.
+3. Enable `MANUAL_PAYMENT_EVIDENCE_UPLOAD_ENABLED`, then `MANUAL_SUBSCRIPTION_PAYMENTS_ENABLED` and the desired manual method flags.
+4. Enable `SUBSCRIPTION_COLLECTIONS_NOTIFICATIONS_ENABLED`; verify the AR/EN/FR snapshots, outbox delivery, 48-hour call task, and escalation queue.
+5. Enable `SUBSCRIPTION_AUTOMATIC_SUSPENSION_ENABLED` only after observing a full reminder/grace cycle. Suspension starts after 30 calendar grace days and rechecks payment, overrides, and configuration atomically.
+
+Manual methods remain hidden unless their instructions and evidence-scanning pipeline are ready. The non-production `PAYMENT_EVIDENCE_SCANNER_MODE=stub-clean` option exists only for local tests; production requires `PAYMENT_EVIDENCE_SCANNER_PATH`.
+
+## Operational invariants
+
+- Subscription billing has one canonical `BillingInvoice`; legacy `SubscriptionInvoice` rows are compatibility links only.
+- A scheduled renewal is reused by Stripe checkout rather than duplicated.
+- Manual confirmation requires the full current MAD balance, matching collection method, clean same-invoice evidence, a unique normalized external reference, a unique idempotency key, a clearance timestamp, and a funds-verified attestation.
+- Initial purchases activate from confirmation time. Renewals extend from the original expiration, including late confirmations after suspension.
+- Evidence is stored privately with no public URL, quarantined before scanning, served only through authorized routes, and immutable after submission.
+- Collections reminders are recipient-specific and localized using contact preference, linked employee preference, then company default. Only Arabic, English, and French can be enabled.
+- Payment resolution cancels open call tasks and suppresses queued collections reminders.
diff --git a/apps/Archive.zip b/apps/Archive.zip
deleted file mode 100644
index 2ced688..0000000
Binary files a/apps/Archive.zip and /dev/null differ
diff --git a/apps/admin/next.config.js b/apps/admin/next.config.js
index c3666a9..5b1352d 100644
--- a/apps/admin/next.config.js
+++ b/apps/admin/next.config.js
@@ -10,7 +10,7 @@ const ADMIN_BASE_PATH = '/admin'
// absolute chunk URLs so /admin pages load their JS/CSS and HMR directly from
// port 3002, bypassing the proxy (which can't upgrade WebSocket connections).
const assetPrefix = normalizeAssetPrefix(process.env.ADMIN_ASSET_PREFIX, ADMIN_BASE_PATH)
-const securityHeaders = buildSecurityHeaders({ assetSources: [assetPrefix] })
+const securityHeaders = buildSecurityHeaders({ assetSources: [assetPrefix], frameSources: ['blob:'] })
const nextConfig = {
basePath: ADMIN_BASE_PATH,
diff --git a/apps/admin/src/app/dashboard/admin-users/page.tsx b/apps/admin/src/app/dashboard/admin-users/page.tsx
index 45f320e..63079e4 100644
--- a/apps/admin/src/app/dashboard/admin-users/page.tsx
+++ b/apps/admin/src/app/dashboard/admin-users/page.tsx
@@ -9,6 +9,7 @@ interface AdminUser {
lastName: string
email: string
role: string
+ preferredLocale: 'ar' | 'en' | 'fr'
isActive: boolean
createdAt: string
permissions?: { id: string; resource: string; actions: string[] }[]
@@ -21,6 +22,7 @@ const EMPTY_FORM = {
email: '',
password: '',
role: 'SUPPORT',
+ preferredLocale: 'en',
isActive: true,
}
@@ -68,6 +70,7 @@ export default function AdminUsersPage() {
email: admin.email,
password: '',
role: admin.role,
+ preferredLocale: admin.preferredLocale,
isActive: admin.isActive,
})
setError(null)
@@ -92,6 +95,7 @@ export default function AdminUsersPage() {
lastName: form.lastName,
email: form.email,
role: form.role,
+ preferredLocale: form.preferredLocale,
isActive: form.isActive,
...(form.password ? { password: form.password } : {}),
}
@@ -273,6 +277,18 @@ export default function AdminUsersPage() {
{ROLES.map((r) => )}
+
+
+
+