Domain Model
Cả hai entity được định nghĩa trong
@nx/coredưới schema PostgreSQLoutreachvà được re-export vào package này. Không có khoá ngoại giữa chúng - mỗi entity là một bảng thu thập độc lập.
1. ERD đầy đủ
2. Entities
Inquiry
| Thuộc tính | Giá trị |
|---|---|
| Bảng | outreach.Inquiry |
| Nguồn | packages/core/src/models/schemas/outreach/inquiry/schema.ts |
| Soft-delete | có (deletedAt) |
| Cột Owner ID | - (toàn cục, không phân tách theo merchant) |
| Thuộc tính ẩn | deletedAt |
Trường:
| Trường | Kiểu | Bắt buộc | Mặc định | Mô tả |
|---|---|---|---|---|
id | text | ✓ | Snowflake | Khóa chính |
type | text | ✓ | 000_CONSULT | Kênh nguồn - xem enum |
status | text | ✓ | NEW | Trạng thái trong pipeline - xem enum |
firstName | text | ✓ | - | Tên liên hệ |
lastName | text | - | Họ liên hệ | |
email | text | ✓ | - | Email liên hệ (có index) |
phone | text | ✓ | - | SĐT liên hệ |
businessName | text | - | Doanh nghiệp của lead | |
businessType | text | - | Danh mục doanh nghiệp | |
locationCount | text | - | Đánh giá lead | |
estimatedRevenue | text | - | Đánh giá lead | |
subject | text | - | Chủ đề tin nhắn | |
message | text | - | Nội dung dạng văn bản tự do | |
assignedTo | text | - | id của admin/user sở hữu | |
repliedAt | timestamptz | - | Thời điểm trả lời đầu tiên | |
repliedBy | text | - | id của user trả lời | |
convertedAt | timestamptz | - | Thời điểm chuyển đổi | |
lostReason | text | - | Lý do mất deal | |
note | text | - | Ghi chú nội bộ | |
createdAt | timestamptz | ✓ | now() | - |
Enum type (InquiryTypes):
| Giá trị | Mô tả |
|---|---|
000_CONSULT | Tư vấn chung / nguồn không rõ (mặc định) |
100_CONTACT | Form liên hệ (/contact) |
200_SALES | Yêu cầu bán hàng (/contact-sales) |
300_DEMO | Yêu cầu demo (/demo) |
400_PARTNER | Đăng ký đối tác (/partners) |
Enum status (InquiryStatuses, IGNIS Statuses):
| Giá trị | Mô tả |
|---|---|
NEW | Vừa gửi |
PROCESSING | Admin đã tiếp nhận |
COMPLETED | Đã chuyển đổi |
CLOSED | Đã xử lý nhưng không chuyển đổi |
CANCELLED | Spam / không hợp lệ / bị bỏ dở |
Index & constraint:
| Tên | Cột | Loại |
|---|---|---|
PK_Inquiry | id | Khóa chính |
IDX_Inquiry_type | type | Btree |
IDX_Inquiry_status | status | Btree |
IDX_Inquiry_email | email | Btree |
Quan hệ: không có.
Subscriber
| Thuộc tính | Giá trị |
|---|---|
| Bảng | outreach.Subscriber |
| Nguồn | packages/core/src/models/schemas/outreach/subscriber/schema.ts |
| Soft-delete | có (deletedAt) |
| Cột Owner ID | - (toàn cục) |
| Thuộc tính ẩn | deletedAt, unsubscribeToken (không bao giờ được model API trả về) |
Trường:
| Trường | Kiểu | Bắt buộc | Mặc định | Mô tả |
|---|---|---|---|---|
id | text | ✓ | Snowflake | Khóa chính |
email | text | ✓ | - | Duy nhất (partial, với dòng chưa xoá) |
locale | text | ✓ | vi | Ngôn ngữ newsletter (vi / en) |
topics | jsonb (string[]) | ✓ | ['all'] | Tuỳ chọn chủ đề |
status | text | ✓ | ACTIVATED | Xem enum |
unsubscribeToken | text | ✓ | Snowflake ($defaultFn) | Token cho link huỷ đăng ký; ẩn khỏi API |
subscribedAt | timestamptz | ✓ | now() | Thời điểm đăng ký đầu tiên/gần nhất |
unsubscribedAt | timestamptz | - | Được set khi huỷ đăng ký, xoá khi đăng ký lại | |
createdAt | timestamptz | ✓ | now() | - |
Enum status (SubscriberStatuses, IGNIS Statuses):
| Giá trị | Mô tả |
|---|---|
ACTIVATED | Đăng ký đang hoạt động |
DEACTIVATED | Đã huỷ đăng ký |
ARCHIVED | Đã được admin lưu trữ (trạng thái hợp lệ, không do luồng service tạo ra) |
Index & constraint:
| Tên | Cột | Loại |
|---|---|---|
PK_Subscriber | id | Khóa chính |
UPI_Subscriber_email | email (where deletedAt IS NULL) | Unique partial index |
IDX_Subscriber_status | status | Btree |
IDX_Subscriber_unsubscribeToken | unsubscribeToken | Btree |
Quan hệ: không có.
3. Bất biến giữa các entity
| Bất biến | Cách thực thi |
|---|---|
Mỗi email chỉ có một Subscriber còn hiệu lực | Unique partial index UPI_Subscriber_email (where deletedAt IS NULL) + SubscriberService.subscribe() tìm-rồi-kích-hoạt-lại |
unsubscribeToken đủ độc nhất để không đoán được | Snowflake $defaultFn (64-bit, worker 10) - xem ADR-0001 |
Đăng ký lại sẽ xoá unsubscribedAt | SubscriberService.subscribe() đặt unsubscribedAt: null khi kích hoạt lại |
4. Hành vi Soft-delete
| Hành vi | Chi tiết |
|---|---|
| Đọc mặc định | deletedAt IS NULL (model defaultFilter) |
| Hard-delete | Không bao giờ theo mặc định; CRUD của admin deleteById thực hiện soft-delete |
| Khôi phục | Không mở ra qua API |
| Tính duy nhất của email | Chỉ áp dụng trên các dòng chưa xoá (partial unique index) |