# Kế hoạch Multi-tenant khởi tạo thủ công

## Trạng thái triển khai (23/09/2026)

Đã hoàn thành lớp nền an toàn, mặc định chưa bật trên môi trường hiện tại:

- Migration cho platform database: `database/platform/20260923_platform_core.sql`.
- Mẫu đăng ký database/domain hiện tại: `database/platform/legacy_tenant.example.sql`.
- `TenantContext` dùng chung trong một request.
- `TenantResolver` chuẩn hóa hostname, tra registry bằng prepared statement và chặn domain không tồn tại.
- Factory adapter chọn database tenant trước khi các model cũ chạy.
- Hỗ trợ credential riêng theo tenant bằng AES-256-GCM hoặc credential dùng chung từ môi trường.
- Công cụ mã hóa password tenant: `bin/encrypt-tenant-credential.php` (đọc password ẩn từ STDIN, không nhận password qua tham số command line).
- Công cụ kiểm tra platform registry và tenant database trước khi bật: `bin/tenant-doctor.php`.
- Session đăng nhập và cache được tách theo hostname khi bật multi-tenant.
- Bộ test nền tại `tests/tenant_foundation_test.php`.

Multi-tenant chỉ hoạt động khi `SAAS_MULTI_TENANT_ENABLED=true`. Trước thời điểm đó, ứng dụng tiếp tục dùng nguyên cấu hình database cũ.

Tài liệu này mô tả phương án MVP: khách gửi yêu cầu đăng ký, quản trị viên tự tạo domain và database, sau đó mới kích hoạt tenant. Hệ thống chưa tự động tạo subdomain, DNS, SSL hoặc database.

## 1. Mục tiêu MVP

Luồng mong muốn:

```text
Khách gửi form
    ↓
Giữ chỗ domain
    ↓
Quản trị viên duyệt
    ↓
Tạo domain + database thủ công
    ↓
Chạy migration và seed
    ↓
Kiểm tra tenant
    ↓
Kích hoạt và gửi link cho khách
```

Khách có thể nhận link dự kiến ngay sau khi gửi form, nhưng link chỉ sử dụng được sau khi quản trị viên hoàn thành khởi tạo và kích hoạt.

## 2. Phạm vi

### Có trong MVP

- Form đăng ký trung tâm và domain mong muốn.
- Kiểm tra và giữ chỗ slug trong platform database.
- Danh sách yêu cầu chờ xử lý cho quản trị viên.
- Tenant Resolver chọn đúng database theo hostname.
- Mỗi tenant có database riêng.
- Quản trị viên tạo domain và database thủ công.
- Lệnh nội bộ chạy migration, seed và tạo owner.
- Checklist kiểm tra trước khi kích hoạt.
- Trạng thái tenant và lịch sử xử lý.
- Cô lập session, cache, upload và log theo tenant.

### Chưa làm trong MVP

- Tự động gọi API cPanel hoặc nhà cung cấp DNS.
- Tự động tạo database/MySQL user.
- Tự động cấp custom domain.
- Thanh toán và kích hoạt gói tự động.
- Tự động xóa tenant hết hạn.

## 3. Trạng thái tenant

| Trạng thái | Ý nghĩa | Khách truy cập link |
|---|---|---|
| `pending_review` | Đã nhận form, đang chờ duyệt | Trang “Yêu cầu đang được xét duyệt” |
| `approved` | Đã duyệt, chưa tạo hạ tầng | Trang “Trung tâm đang được thiết lập” |
| `provisioning` | Đang chạy migration/seed | Trang tiến trình thiết lập |
| `ready` | Khởi tạo xong, chờ kiểm tra | Trang “Sắp hoàn tất” |
| `active` | Đã kích hoạt | Trang đăng nhập tenant |
| `failed` | Khởi tạo lỗi | Trang hỗ trợ, không hiện lỗi kỹ thuật |
| `suspended` | Tenant bị tạm ngưng | Trang tạm ngưng dịch vụ |
| `rejected` | Yêu cầu bị từ chối | Trang thông báo yêu cầu không được duyệt |

Không xóa bản ghi tenant khi provisioning lỗi. Giữ lại lịch sử và domain đã được giữ chỗ cho tới khi quản trị viên quyết định giải phóng.

## 4. Form khách hàng

### Thông tin cần nhập

- Tên trung tâm.
- Domain mong muốn.
- Họ tên người phụ trách.
- Email.
- Số điện thoại.
- Quy mô dự kiến hoặc số lượng học viên, nếu cần phục vụ xét duyệt.
- Ghi chú.
- Đồng ý điều khoản sử dụng và chính sách dữ liệu.

Không thu mật khẩu ở form yêu cầu ban đầu. Sau khi tenant được kích hoạt, gửi link một lần để owner tự đặt mật khẩu. Cách này tránh lưu hoặc truyền mật khẩu trong giai đoạn chờ duyệt.

### Domain mong muốn

- Chuẩn hóa lowercase.
- Chỉ nhận chữ ASCII, số và dấu gạch ngang.
- Dài 3–40 ký tự.
- Không bắt đầu hoặc kết thúc bằng dấu gạch ngang.
- Không có hai dấu gạch ngang liên tiếp.
- Chặn slug hệ thống như `www`, `admin`, `api`, `app`, `mail`, `static`, `assets`, `support`, `billing`.
- Kiểm tra trùng bằng AJAX để hỗ trợ UX.
- Bắt buộc có unique constraint ở database để xử lý đăng ký đồng thời.

Sau khi gửi thành công, hiển thị:

```text
Link dự kiến: https://slug.ten-mien-saas.vn
Trạng thái: Đang chờ duyệt
Mã yêu cầu: TENANT-XXXXXXXX
```

## 5. Platform database

### `platform_tenant_tb`

Các trường chính:

- `id`
- `uuid`
- `request_code`
- `name`
- `slug`
- `owner_name`
- `owner_email`
- `owner_phone`
- `estimated_students`
- `database_host`
- `database_port`
- `database_name`
- `database_username`
- `database_password_encrypted`
- `schema_version`
- `status`
- `review_note`
- `provision_error`
- `approved_by`
- `approved_at`
- `activated_by`
- `activated_at`
- `date_created`
- `date_updated`

Ràng buộc:

- Unique `uuid`.
- Unique `request_code`.
- Unique `slug`.
- Không lưu database password dạng rõ. Mã hóa bằng master key nằm trong biến môi trường.

Nếu dùng một MySQL user chung cho tất cả database tenant, có thể không cần lưu username/password từng tenant. Tuy nhiên MVP thủ công nên ưu tiên một user riêng cho mỗi tenant để giảm phạm vi ảnh hưởng khi lộ credential.

### `platform_domain_tb`

- `id`
- `tenant_id`
- `hostname`
- `domain_type`: `subdomain` hoặc `custom`
- `is_primary`
- `dns_status`
- `ssl_status`
- `status`
- `verified_at`
- `date_created`
- `date_updated`

`hostname` phải unique toàn hệ thống.

### `platform_tenant_log_tb`

Ghi audit cho các thao tác:

- Nhận yêu cầu.
- Duyệt hoặc từ chối.
- Lưu cấu hình database.
- Bắt đầu/kết thúc provisioning.
- Kiểm tra tenant.
- Kích hoạt/tạm ngưng.
- Giải phóng domain.

Log lưu `tenant_id`, hành động, người thực hiện, thời gian và metadata đã loại bỏ dữ liệu nhạy cảm.

## 6. Tenant Resolver

Resolver chạy trước adapter nghiệp vụ:

1. Đọc và chuẩn hóa hostname.
2. Tra hostname trong `platform_domain_tb` bằng platform adapter.
3. Lấy tenant và kiểm tra trạng thái.
4. Nếu `active`, giải mã credential và tạo tenant database adapter.
5. Gán adapter vào `GlobalAdapterFeature`.
6. Tạo `TenantContext` cho toàn bộ request.

Quy tắc:

- Domain không tồn tại không được fallback vào database hiện tại.
- Tenant chưa active không được chạy truy vấn nghiệp vụ.
- Tên database chỉ lấy từ registry và phải qua whitelist.
- Session phải chứa tenant UUID và bị từ chối nếu không trùng domain hiện tại.
- Cache, upload và log luôn dùng tenant UUID từ `TenantContext`, không lấy từ request.

## 7. Quy trình quản trị viên tạo tenant

### Bước 1 — Duyệt yêu cầu

- Kiểm tra tên trung tâm, email và số điện thoại.
- Kiểm tra slug không giả mạo thương hiệu hoặc vi phạm danh sách cấm.
- Xác nhận gói dùng thử/gói dịch vụ.
- Chuyển tenant từ `pending_review` sang `approved`.

### Bước 2 — Tạo domain thủ công

Nếu không dùng wildcard domain:

1. Tạo subdomain trong cPanel/DNS.
2. Trỏ document root về cùng thư mục `public_html` của ứng dụng SaaS.
3. Bật HTTPS.
4. Mở link và kiểm tra chứng chỉ.
5. Cập nhật `dns_status = verified`, `ssl_status = active`.

Nếu dùng wildcard `*.ten-mien-saas.vn`, chỉ cần cấu hình wildcard một lần. Khi tạo tenant mới, quản trị viên chỉ thêm hostname vào platform database và kiểm tra link.

### Bước 3 — Tạo database thủ công

1. Tạo database theo quy ước, ví dụ `tenant_000012`.
2. Tạo MySQL user riêng cho tenant.
3. Chỉ cấp quyền cần thiết trên đúng database tenant.
4. Không đặt slug trực tiếp làm database name.
5. Lưu cấu hình kết nối bằng chức năng quản trị có mã hóa.
6. Kiểm tra kết nối đọc/ghi.

Không truyền database password trực tiếp trong command line vì có thể bị lưu vào shell history hoặc process list.

### Bước 4 — Chạy provisioning nội bộ

Lệnh dự kiến:

```bash
php bin/tenant.php provision --tenant=TENANT_UUID
```

Lệnh tự thực hiện:

- Lock tenant để tránh chạy đồng thời.
- Chuyển trạng thái sang `provisioning`.
- Kiểm tra database rỗng hoặc đúng trạng thái retry.
- Chạy schema nền và toàn bộ migration.
- Seed role mặc định.
- Seed CRM stages và CRM sources.
- Seed cấu hình hệ thống cần thiết.
- Ghi tên trung tâm vào `config_tb` ID 1.
- Tạo owner ở trạng thái chờ đặt mật khẩu.
- Tạo activation token một lần, lưu dạng hash.
- Ghi schema version.
- Chuyển tenant sang `ready`.

Provisioning phải idempotent: chạy lại không tạo trùng role, cấu hình hoặc owner.

### Bước 5 — Kiểm tra trước kích hoạt

Checklist bắt buộc:

- Domain mở bằng HTTPS.
- Domain resolve đúng tenant UUID.
- Kết nối đúng database tenant.
- Thông tin tên trung tâm và logo mặc định đúng.
- Owner tồn tại và activation token hợp lệ.
- Các role hệ thống đã được seed.
- CRM stages/sources mặc định đầy đủ.
- Trang đăng nhập tải bình thường.
- Không thấy dữ liệu của tenant mẫu hoặc tenant khác.
- Upload ghi vào đúng thư mục tenant.
- Cache key có đúng tenant prefix.
- Log có tenant UUID.

### Bước 6 — Kích hoạt

- Quản trị viên bấm **Kích hoạt tenant**.
- Backend chỉ cho kích hoạt khi tenant đang `ready`, DNS/SSL hợp lệ và checklist đã hoàn thành.
- Tenant chuyển sang `active`.
- Hệ thống tạo link đặt mật khẩu có thời hạn.
- Gửi link đăng nhập/đặt mật khẩu cho owner.

## 8. Giao diện quản trị platform

### Danh sách tenant

Hiển thị:

- Mã yêu cầu.
- Tên trung tâm.
- Domain.
- Owner.
- Ngày đăng ký.
- Trạng thái.
- Schema version.
- Người duyệt/kích hoạt.
- Thao tác phù hợp theo trạng thái.

### Chi tiết tenant

Các khối:

- Thông tin đăng ký.
- Domain và trạng thái DNS/SSL.
- Database và kiểm tra kết nối; không hiển thị password rõ.
- Tiến trình provisioning.
- Checklist kích hoạt.
- Audit log.
- Nút duyệt, từ chối, chạy provisioning, retry, kích hoạt và tạm ngưng.

Các thao tác nguy hiểm phải có xác nhận và audit log. Không cung cấp nút xóa database trong giao diện MVP.

## 9. Cô lập dữ liệu

### Session

- Lưu `tenant_uuid` cùng member trong session.
- Regenerate session ID sau đăng nhập.
- Domain A không sử dụng được session từ domain B.
- Cookie host-only.

### Cache

```text
tenant:{tenant_uuid}:{cache_key}
```

### Upload

```text
uploads/tenants/{tenant_uuid}/images/
uploads/tenants/{tenant_uuid}/files/
uploads/tenants/{tenant_uuid}/exports/
```

### Cron

Cron platform duyệt danh sách tenant `active`, tạo context mới cho từng tenant rồi chạy job. Adapter phải được giải phóng/thay mới giữa hai tenant.

## 10. Migration dữ liệu hiện tại

Dữ liệu hiện tại trở thành tenant đầu tiên:

1. Backup database và uploads.
2. Tạo tenant legacy trong platform database.
3. Tạo domain mapping cho domain hiện tại.
4. Đăng ký database hiện tại làm database của tenant legacy.
5. Gắn schema version hiện tại.
6. Tạo tenant UUID và chuyển upload sang thư mục tương ứng.
7. Kiểm tra dashboard, lớp, học viên, học phí, CRM, tài chính và thông báo.
8. Bật resolver bắt buộc.

Không cho fallback về database legacy sau khi resolver được bật.

## 11. Kế hoạch triển khai theo giai đoạn

### Giai đoạn A — Chuẩn bị an toàn

- Backup và kiểm tra restore.
- Chuyển database password/API key ra biến môi trường.
- Tạo master encryption key ngoài source.
- Chọn domain SaaS và quy ước database.

### Giai đoạn B — Platform core

- Tạo platform database.
- Tạo tenant/domain/log tables.
- Tạo `PlatformAdapter`, `TenantContext`, `TenantResolver`.
- Tạo trang unknown/pending/suspended tenant.

### Giai đoạn C — Cô lập ứng dụng

- Dynamic tenant adapter.
- Tenant-bound session.
- Prefix cache.
- Tách uploads và logs.
- Chuyển dữ liệu hiện tại thành tenant legacy.

### Giai đoạn D — Provisioning thủ công có công cụ hỗ trợ

- Migration runner.
- Seed runner.
- CLI `tenant.php`.
- Activation token.
- Retry và audit log.

### Giai đoạn E — Form khách hàng và platform admin

- Form yêu cầu tenant.
- AJAX kiểm tra slug.
- Danh sách chờ duyệt.
- Chi tiết tenant và checklist.
- Kích hoạt tenant.

### Giai đoạn F — Kiểm thử và chạy thử

- Tạo hai tenant test với hai database.
- Kiểm thử chéo tenant.
- Kiểm thử lỗi DNS/database/migration.
- Chạy thử quy trình quản trị từ đầu đến cuối.
- Viết runbook backup, restore, suspend và retry.

## 12. Test bắt buộc trước production

- Hai tenant có cùng ID học viên vẫn không đọc chéo dữ liệu.
- Email giống nhau có thể tồn tại ở hai tenant nhưng không đăng nhập nhầm domain.
- Session domain A bị từ chối tại domain B.
- Unknown host không truy cập database legacy.
- Tenant `pending_review`, `approved`, `provisioning`, `ready` không truy cập trang quản lý.
- Chỉ tenant `active` được đăng nhập.
- Upload/export tenant A không tải được từ tenant B.
- Retry provisioning không tạo dữ liệu trùng.
- Hai quản trị viên không thể cùng provision một tenant.
- Kích hoạt bị chặn nếu DNS, SSL, database hoặc checklist chưa đạt.
- Suspend chặn đăng nhập nhưng không xóa dữ liệu.
- Backup và restore được một tenant độc lập.

## 13. Thứ tự bắt đầu code

1. Tách secrets khỏi source.
2. Viết migration platform database.
3. Tạo `TenantContext` và resolver.
4. Chuyển adapter sang tenant-aware.
5. Cô lập session/cache/upload.
6. Chuyển database hiện tại thành tenant legacy.
7. Viết CLI provisioning.
8. Tạo hai tenant test và kiểm tra cô lập.
9. Xây platform admin.
10. Xây form khách hàng.

Form đăng ký là bước cuối của MVP core. Trước đó phải chứng minh được hai domain nối đúng hai database và không có đường đọc chéo dữ liệu.

## 14. Thông tin cần chốt trước khi triển khai

- Domain SaaS chính.
- Có dùng wildcard subdomain hay tạo từng subdomain trong cPanel.
- Hosting có cho tạo nhiều database và MySQL user hay không.
- Quy ước tên database do hosting yêu cầu.
- Tenant có cần quản trị duyệt trước khi giữ slug hay giữ ngay khi gửi form.
- Thời gian giữ slug của yêu cầu chưa xử lý.
- Một email có được sở hữu nhiều tenant hay không.
- Cách gửi activation link: email hệ thống hay quản trị viên gửi thủ công.
