PRD: Người đăng ký bản tin
| Module | Khách hàng (CORE-09) | PRD ID | PRD-SUB-001 |
| Status | Shipped | Owner | Customer squad |
| Date | 2026-04-03 | Version | v1.0 |
| Packages | @nx/outreach · @nx/core · apps/bo | URD | SUB |
TL;DR
Cho phép khách truy cập trang công khai đăng ký bản tin bằng email, kèm topics và locale, rồi hủy đăng ký chỉ với một cú nhấp qua một token duy nhất - và cung cấp cho back-office một góc nhìn thống kê có xác thực, duy nhất về độ tăng trưởng của danh sách (tổng số, mới-trong-tháng, đếm theo trạng thái). Kết quả: một danh sách gửi thư sạch, email duy nhất toàn cục với đăng ký/đăng ký lại idempotent và một bảng theo dõi tình trạng, thay cho một form đăng ký mờ mịt không có khả năng quan sát cho admin.
1. Context & Problem
Trang marketing công khai Overture thu thập đăng ký bản tin, nhưng back-office (apps/bo) không có cách nào để đọc danh sách hay xem nó đang tăng trưởng ra sao. Khách truy cập có thể đăng ký, nhưng admin không thể trả lời những câu hỏi cơ bản - có bao nhiêu người đăng ký, bao nhiêu người tham gia trong tháng này, bao nhiêu người đã rời đi. Việc đăng ký cũng cần hành xử an toàn khi lặp lại: cùng một email gửi hai lần không được tạo bản trùng, và một địa chỉ đã rời đi trước đó phải có thể tham gia lại mà không cần dọn dẹp thủ công. Thiếu sự đảm bảo email duy nhất, một hành động hủy đăng ký một-lần-nhấp, và một lần đọc thống kê có xác thực, danh sách trở nên mờ mịt và dễ lỗi - một rào cản cho tầng tương-tác-marketing mà Khách hàng hướng tới.
Tính năng này xây dựng vòng đời người đăng ký (đăng ký, hủy đăng ký, đăng ký lại) trên service @nx/outreach và phơi bày một aggregate thống kê chỉ-đọc cho back-office.
2. Goals & Non-Goals
Goals
- Đăng ký bằng email kèm topics và locale tùy chọn, giữ email duy nhất toàn cục trong số những người đăng ký.
- Đăng ký idempotent: một email đang hoạt động trả về người đăng ký hiện có; một email đã bị vô hiệu hóa sẽ được kích hoạt lại.
- Hủy đăng ký một-lần-nhấp qua một liên kết token duy nhất, không yêu cầu xác thực.
- Một endpoint thống kê có xác thực cho back-office - tổng số, mới-trong-tháng, và đếm theo nhóm trạng thái activated/deactivated.
Non-Goals
- Engine chiến dịch Email / SMS - Dự kiến (xem URD §7).
- Phân khúc và nhắm mục tiêu khách hàng.
- Phân tích giá trị vòng đời của người đăng ký.
- Gửi email / vận chuyển SMTP (thuộc phạm vi ngoài module này).
3. Success Metrics
| Metric | Target / signal |
|---|---|
| Tính toàn vẹn của danh sách | Không có email trùng trong số người đăng ký; mỗi người đăng ký có một token hủy đăng ký duy nhất |
| Tính idempotent của đăng ký | Đăng ký lặp cùng một email không tạo row mới; row đã vô hiệu hóa được kích hoạt lại sạch sẽ |
| Độ tin cậy của hủy đăng ký | Liên kết token một-lần-nhấp vô hiệu hóa ngay ở lần gọi đầu; token không hợp lệ trả về 404 |
| Khả năng quan sát của admin | Góc nhìn thống kê back-office trả về tổng số, mới-trong-tháng, và đếm theo trạng thái khi yêu cầu |
4. Personas & Use Cases
| Persona | Mục tiêu trong tính năng này |
|---|---|
| Site Visitor | Tham gia bản tin bằng email; rời đi chỉ với một cú nhấp |
| Marketing | Xem kích thước danh sách, tăng trưởng theo tháng, và số lượng rời đi trong back-office |
| Owner | Xác nhận danh sách gửi thư khỏe mạnh và đang tăng trưởng |
Core scenarios: khách truy cập đăng ký bằng email (topics + locale) → một email lặp lại hoặc đã từng rời đi được xử lý idempotent → khách truy cập hủy đăng ký qua một liên kết token → marketing mở back-office và đọc thống kê người đăng ký.
5. User Stories
- Là một site visitor, tôi muốn đăng ký bằng email, topics, và locale của mình, để tôi nhận được bản tin.
- Là một site visitor, tôi muốn việc đăng ký hai lần là an toàn, để tôi không bao giờ tạo đăng ký trùng hay lỗi.
- Là một khách đã hủy đăng ký trước đó, tôi muốn đăng ký lại và được kích hoạt lại, để tôi có thể tham gia lại mà không cần trợ giúp.
- Là một site visitor, tôi muốn hủy đăng ký chỉ với một cú nhấp qua một liên kết token, để việc rời đi không tốn công.
- Là marketing, tôi muốn một góc nhìn thống kê có xác thực về danh sách, để tôi có thể theo dõi tăng trưởng và số lượng rời đi.
6. Functional Requirements
| # | Requirement | URD ref |
|---|---|---|
| FR-1 | Đăng ký bằng email (POST /subscribe, không xác thực) với locale (vi/en) tùy chọn và topics[] tùy chọn; trả về id của người đăng ký | URD-SUB-001 |
| FR-2 | Email duy nhất toàn cục trong số người đăng ký; token hủy đăng ký duy nhất toàn cục | URD-SUB-002 |
| FR-3 | Đăng ký là idempotent - email đang hoạt động trả về người đăng ký hiện có; email đã vô hiệu hóa được kích hoạt lại (subscribedAt đặt lại, unsubscribedAt xóa) | URD-SUB-004 |
| FR-4 | Hủy đăng ký bằng token (GET /unsubscribe?token=…, không xác thực) vô hiệu hóa người đăng ký; token không hợp lệ trả về 404 | URD-SUB-003 |
| FR-5 | Endpoint thống kê (GET /statistics, xác thực JWT hoặc Basic) trả về total, monthlyNew, và byStatus { activated, deactivated } | URD-SUB-005 |
| FR-6 | Thống kê là một aggregate SQL duy nhất trên các row chưa xóa: COUNT(*) FILTER (…) cho mới-trong-tháng (subscribedAt >= startOfMonth) và đếm theo trạng thái | URD-SUB-005 |
Toàn văn requirement và tiêu chí chấp nhận nằm trong URD Khách hàng. PRD này tham chiếu chúng thay vì lặp lại.
7. Non-Functional Requirements
| Area | Requirement |
|---|---|
| Tính toàn vẹn dữ liệu | Email và token hủy đăng ký duy nhất toàn cục; đăng ký không bao giờ tạo bản trùng; mọi row dùng soft-delete |
| Tính idempotent | Đăng ký lặp lại là an toàn (trả về hiện có hoặc kích hoạt lại); các chuyển trạng thái là tất định |
| Tenancy & authz | Đăng ký/hủy đăng ký công khai không yêu cầu xác thực; thống kê yêu cầu strategy JWT hoặc BASIC; permission khai báo qua crudPermissions(Subscriber.AUTHORIZATION_SUBJECT, …) |
| Hiệu năng / quy mô | Thống kê tính bằng một aggregate SQL duy nhất (COUNT(*) FILTER), không đếm từng row |
| i18n | Locale ghi nhận theo từng người đăng ký (vi/en); nhãn hiển thị cho người dùng là song ngữ ({ en, vi }) |
8. UX & Flows
Màn hình chính: form đăng ký bản tin công khai trên trang Overture, và màn hình danh sách người đăng ký + góc nhìn thống kê trong back-office apps/bo.
9. Data & Domain
| Entity | Role |
|---|---|
Subscriber | Mục trong danh sách gửi thư - email (duy nhất), status (activated/deactivated), locale, topics[], subscribedAt, unsubscribedAt, unsubscribeToken duy nhất |
| Aggregate thống kê | Một projection chỉ-đọc trên Subscriber: đếm total, monthlyNew, và byStatus |
Chỉ ở mức khái niệm - schema và bất biến đầy đủ nằm trong Outreach domain model.
10. Dependencies & Assumptions
Phụ thuộc vào
@nx/outreach- chứa service, repository, và route của người đăng ký.@nx/core- cung cấp modelSubscriber, schema, vàcrudPermissions.apps/bo- bề mặt back-office đọc endpoint thống kê.- Trang công khai Overture - nguồn các đăng ký bản tin.
Giả định
- Trang công khai có thể gọi các route đăng ký/hủy đăng ký không xác thực.
- Người dùng back-office xác thực qua
JWThoặcBASICđể đọc thống kê.
11. Risks & Open Questions
| Risk / câu hỏi | Giảm thiểu / trạng thái |
|---|---|
| Đăng ký trùng do gửi lặp lại | Đăng ký là idempotent; email duy nhất toàn cục |
| Đoán / phát lại token hủy đăng ký | Token duy nhất toàn cục; token không hợp lệ trả về 404; hủy đăng ký idempotent trên row đã vô hiệu hóa |
| Chưa có engine chiến dịch để hành động trên danh sách | Ngoài phạm vi; ghi nhận là Dự kiến trong URD |
| Chi phí thống kê khi danh sách lớn dần | Aggregate SQL duy nhất; xem lại caching nếu back-office poll nhiều |
12. Release Plan & Launch Criteria
| Aspect | Plan |
|---|---|
| Phase | P2 - feature Khách hàng SUB trong danh mục feature URD |
| Rollout | Mọi deployment; không có feature flag |
| Migration | Không (entity mới; không backfill dữ liệu) |
| Launch criteria | Đăng ký/hủy đăng ký/đăng ký lại được kiểm chứng idempotent từ đầu đến cuối; endpoint thống kê trả về đúng tổng số, mới-trong-tháng, và đếm theo trạng thái; màn hình danh sách back-office render được dữ liệu |
| Monitoring | Tăng trưởng người đăng ký (mới-trong-tháng), tỷ lệ rời đi (số deactivated), tỷ lệ lỗi hủy đăng ký |
13. FAQ
Điều gì xảy ra nếu cùng một email đăng ký hai lần? Đăng ký là idempotent - một email đang hoạt động trả về người đăng ký hiện có, và một email đã vô hiệu hóa trước đó được kích hoạt lại. Không có row trùng nào được tạo.
Hủy đăng ký có cần đăng nhập không? Không - hủy đăng ký là một liên kết công khai một-lần-nhấp mang một token duy nhất. Ngược lại, thống kê yêu cầu xác thực JWT hoặc Basic.
Endpoint thống kê trả về gì? total người đăng ký, monthlyNew (tham gia kể từ đầu tháng hiện tại), và đếm byStatus chia thành activated và deactivated.
Tính năng này có gửi email không? Không - nó quản lý danh sách người đăng ký và thống kê của nó. Engine chiến dịch/gửi là Dự kiến và nằm ngoài module này.
Người đăng ký đã xóa có được đếm không? Không - thống kê chỉ aggregate trên các row chưa xóa (nhận biết soft-delete).
References
- URD: Khách hàng - Người đăng ký bản tin
- Module: Khách hàng - tổng quan + truy vết
- Developer: @nx/outreach · Outreach domain model