Mục đích: Cho phép DN Owner (doanh nghiệp PCCC nhập khẩu / sản xuất tier 1) tự đăng ký tài khoản tenant trên app.pccctrace.vn. Người dùng khai thông tin doanh nghiệp, tạo mật khẩu, upload Giấy đăng ký kinh doanh (ĐKKD), xác thực quyền sở hữu email qua OTP, sau đó hồ sơ chuyển sang trạng thái pending_kyc chờ Platform Admin duyệt thủ công (SLA 24h — FR-18).
Trong phạm vi (In scope)
Form đăng ký 1 trang: tên DN, MST, email, SĐT, mật khẩu, upload ĐKKD.
Xác thực email bằng OTP 6 số qua modal (OTP chỉ để verify email, không dùng để đăng nhập).
Tạo tenant (trạng thái pending_kyc) + user DN Owner khi OTP hợp lệ.
Điều hướng sang DN-002 (KYC Pending) sau khi gửi hồ sơ thành công.
Ngoài phạm vi (Out of scope)
Duyệt KYC (thuộc Platform Admin — FR-18, web platform).
Tự động đối soát MST qua API Tổng cục Thuế (FR-1b — Phase 2).
Đăng nhập (DN-003), mời thành viên, quên mật khẩu.
Chỉnh sửa thông tin DN sau khi duyệt (DN-007 — Tenant Settings).
Bối cảnh hệ thống
Đây là bước đầu của hành trình onboarding J1 (DN-001 → DN-019, mục tiêu time-to-first-print < 7 ngày). Hệ thống multi-tenant, dữ liệu cô lập bằng Postgres RLS theo tenant_id (FR-58a). Quy mô mục tiêu nhỏ (~30 DN) — ưu tiên giải pháp đơn giản, không over-engineer.
2.User story & Acceptance Criteria
Là chủ doanh nghiệp PCCC (DN Owner), tôi muốn đăng ký tài khoản doanh nghiệp bằng MST + email và nộp hồ sơ ĐKKD ngay trên web, để doanh nghiệp của tôi được duyệt KYC và bắt đầu sử dụng PCCCTrace trong vòng 24 giờ.
Acceptance Criteria
ID
Kịch bản (Given / When / Then)
AC-01
Given người dùng chưa có tài khoản, When nhập đầy đủ thông tin hợp lệ + upload ≥1 file ĐKKD và bấm "Gửi hồ sơ đăng ký", Then hệ thống gửi OTP 6 số tới email đã khai và hiển thị modal nhập OTP.
AC-02
Given modal OTP đang mở, When nhập đúng mã OTP còn hiệu lực và bấm "Xác nhận & gửi hồ sơ", Then hệ thống tạo tenant (status pending_kyc) + user DN Owner, lưu hồ sơ ĐKKD, và chuyển hướng sang DN-002.
AC-03
Given MST đã tồn tại trong hệ thống, When submit form, Then hiển thị lỗi "Tài khoản đã tồn tại, vui lòng đăng nhập" kèm link sang DN-003, không gửi OTP.
AC-04
Given modal OTP đang mở, When nhập sai OTP 5 lần liên tiếp, Then khoá xác thực tạm thời (FR-7), hiển thị thông báo khoá và thời gian thử lại.
AC-05
Given bất kỳ field nào không hợp lệ (xem mục 5), When submit, Then hiển thị lỗi inline dưới từng field, focus vào field lỗi đầu tiên, không gọi API.
AC-06
Given người dùng chưa xác nhận OTP, Thenkhông có tenant/user nào được tạo chính thức (chỉ tồn tại bản ghi đăng ký tạm — xem BR-06).
AC-07
Given countdown OTP đã hết, When bấm "Gửi lại mã", Then OTP cũ bị vô hiệu, OTP mới được gửi, countdown reset (tối đa 5 OTP/giờ/email — FR-7).
3.Luồng nghiệp vụ
Điều kiện vào / ra
Mục
Chi tiết
Entry points
Link "Đăng ký" từ landing page; link "Đăng ký" trên DN-003 (Login); URL trực tiếp /signup.
Precondition
Người dùng chưa đăng nhập. Nếu đã có session hợp lệ → redirect về dashboard.
Layout: card căn giữa, max-width 680px, nền --neutral. Tham chiếu trực tiếp prototype DN-001 — dùng đúng design tokens trong tokens.css.
ID
Thành phần
Loại
Bắt buộc
Thuộc tính / hành vi
SU-00
Header card (H1 + lead + badge SLA)
Static
—
H1 "Đăng ký tài khoản doanh nghiệp" · lead: "Đăng ký một lần, quản lý truy xuất nguồn gốc tem PCCC cho toàn doanh nghiệp." · badge xanh lá "⏱ Duyệt hồ sơ trong 24 giờ làm việc" — cam kết SLA duyệt KYC (FR-18; 24h làm việc không tính T7/CN — chốt 03/07/2026, chi tiết tại spec DN-002 BR-02).
SU-01
Tên doanh nghiệp
Text input, icon 🏢
Có
maxlength=200 · placeholder "VD: Công ty TNHH PCCC Hà Nội" · trim khoảng trắng đầu/cuối khi submit.
SU-02
Mã số thuế (MST)
Text input, icon #
Có
maxlength=14 · inputmode="numeric" · chỉ nhận chữ số và dấu "-" cho MST chi nhánh, chuẩn hoá bỏ gạch trước khi lưu (OQ-03 ✅) · placeholder "10 hoặc 13 chữ số".
SU-03
Email
Email input, icon ✉
Có
type="email" · autocomplete="username" · lowercase khi submit · là login identifier duy nhất (FR-1a).
SU-04
Số điện thoại
Tel input, icon 📞
Có
type="tel" · nhận số VN 10 số (di động 0xx / cố định 02x) · bỏ khoảng trắng khi validate.
SU-05
Mật khẩu
Password input, icon 🔒 + toggle 👁 + thanh độ mạnh
Có
autocomplete="new-password" · tối thiểu 8 ký tự · toggle mắt chuyển type=password ⇄ text · thanh độ mạnh 3 mức hiện khi bắt đầu nhập, ẩn khi trống (rule tại mục 5.1).
SU-06
Xác nhận mật khẩu
Password input, icon 🔒 + toggle 👁
Có
Phải khớp SU-05 · validate on blur + on submit.
SU-07
Upload Giấy ĐKKD
File drop-zone (kéo thả / click)
Có
Định dạng PDF, JPG, PNG · ≤10 MB/file · tối đa 5 file, tối thiểu 1 file · counter "n/5" · sau khi chọn hiển thị danh sách file kèm nút xoá từng file.
SU-08
Checkbox consent
Checkbox + links
Có
Checkbox bắt buộc tick trước khi submit: "Tôi đồng ý với Điều khoản sử dụng và Chính sách bảo mật của PCCCTrace" · link mở tab mới (/terms, /privacy) · server lưu consent_at timestamp làm bằng chứng đồng ý (FR-75 — OQ-05 ✅).
SU-09
Nút "Gửi hồ sơ đăng ký ›"
Button primary, full-width, h=48px
—
Click → client validate → gọi API gửi OTP → mở modal OTP · disable + spinner khi đang gọi API (chống double-submit).
Validate 2 lớp: client (on blur + on submit, hiển thị inline dưới field, viền đỏ --error) và server (nguồn chân lý cuối cùng). Thông báo lỗi 100% tiếng Việt.
Field
Rule
Thông báo lỗi
SU-01
Rỗng (sau trim)
Vui lòng nhập tên doanh nghiệp
SU-01
>200 ký tự
Tên doanh nghiệp tối đa 200 ký tự
SU-02
Rỗng
Vui lòng nhập mã số thuế
SU-02
Không đúng định dạng 10 số hoặc 13 số (dạng chi nhánh ##########-###)
Email đã được sử dụng, vui lòng đăng nhập hoặc dùng email khác
SU-04
Rỗng / không phải SĐT Việt Nam hợp lệ
Vui lòng nhập số điện thoại hợp lệ
SU-05
<8 ký tự
Mật khẩu tối thiểu 8 ký tự
SU-06
Không khớp SU-05
Mật khẩu xác nhận không khớp
SU-07
Chưa có file nào
Vui lòng tải lên Giấy đăng ký kinh doanh
SU-07
File >10MB
File "{tên file}" vượt quá 10 MB
SU-07
Sai định dạng
Chỉ chấp nhận PDF, JPG, PNG
SU-07
Quá 5 file
Tối đa 5 file
SU-08
Chưa tick checkbox đồng ý
Vui lòng đồng ý Điều khoản sử dụng và Chính sách bảo mật để tiếp tục
OTP
Sai mã
Mã OTP không đúng, còn {n} lần thử
OTP
Hết hạn
Mã OTP đã hết hạn, vui lòng bấm "Gửi lại mã"
OTP
Sai 5 lần → khoá
Bạn đã nhập sai quá 5 lần. Vui lòng thử lại sau 30 phút
OTP
Quá 5 lần gửi/giờ
Bạn đã yêu cầu mã quá nhiều lần. Vui lòng thử lại sau {phút} phút
5.1 · Chỉ báo độ mạnh mật khẩu (cơ bản)
Thanh 3 vạch + nhãn chữ hiển thị dưới field Mật khẩu (SU-05), cập nhật realtime theo mỗi ký tự nhập; ẩn khi field trống. Chỉ mang tính gợi ý — không chặn submit (điều kiện bắt buộc vẫn là ≥8 ký tự theo mục 5).
Mức
Hiển thị
Điều kiện
Yếu
1 vạch đỏ + nhãn đỏ
Có ký tự nhưng chưa đạt mức Trung bình (<8 ký tự, hoặc chỉ toàn chữ / toàn số).
Trung bình
2 vạch vàng + nhãn vàng
≥8 ký tự và có cả chữ lẫn số.
Mạnh
3 vạch xanh + nhãn xanh
≥10 ký tự và có chữ + số và có chữ hoa hoặc ký tự đặc biệt.
6.Business rules
ID
Rule
Nguồn
BR-01
MST là định danh duy nhất của tenant — mỗi MST chỉ đăng ký được 1 tenant. Email là login identifier unique toàn hệ thống (lưu như thuộc tính, không phải primary key).
FR-1a
BR-02
tenant_id và user_id được tạo khi OTP verified là bất biến — không thay đổi kể cả khi FR-1b (tự động hoá KYC) triển khai sau này.
FR-1a/1b
BR-03
Tenant mới luôn ở trạng thái pending_kyc. Chỉ Platform Admin mới chuyển được sang active/rejected (qua platform.pccctrace.vn — FR-18). Trước khi được duyệt, user chỉ truy cập được DN-002.
FR-1a, FR-18
BR-04
OTP email tại màn hình này chỉ dùng để xác thực quyền sở hữu email, không phải cơ chế đăng nhập. Đăng nhập dùng email + mật khẩu (DN-003).
Prototype
BR-05
Giới hạn OTP: 6 chữ số · hiệu lực 5 phút · cooldown gửi lại 60 giây (OQ-06 ✅) · tối đa 5 lần gửi/giờ/email · sai 5 lần → khoá tạm 30 phút.
FR-7
BR-06
Trước khi OTP verified, dữ liệu form + file lưu ở bản ghi đăng ký tạm (TTL đề xuất 24h). Không tạo tenant/user chính thức; bản ghi tạm quá hạn bị dọn tự động, file mồ côi trong MinIO bị xoá theo.
BA đề xuất
BR-07
Mật khẩu hash bcrypt cost 12 — không bao giờ lưu/log plaintext.
DN-003 spec
BR-08
Tạo tenant thành công phải ghi audit log: tenant_id, actor_user_id, IP, timestamp, action tenant.signup.
FR-2 pattern
BR-09
Sau khi tạo tenant, gửi email xác nhận "Đã nhận hồ sơ — sẽ duyệt trong 24 giờ làm việc" tới email đăng ký.
Journey J1
7.Modal OTP xác thực email
Hiện đè lên form (overlay rgba(17,24,39,0.55)) sau khi bấm SU-09 và server đã gửi OTP thành công.
ID
Thành phần
Hành vi
OTP-01
6 ô input mã (56px, 1 ký tự/ô)
Chỉ nhận chữ số · auto-focus ô đầu · nhập xong tự nhảy ô kế · Backspace lùi ô trước · hỗ trợ paste cả mã 6 số (tự phân bổ vào 6 ô) · inputmode="numeric", autocomplete="one-time-code".
OTP-02
Countdown "Gửi lại mã sau 60 giây"
60s là cooldown gửi lại, không phải hạn của mã (OQ-06 ✅). Về 0 → OTP-03 enable. Mã OTP hiệu lực 5 phút kể từ lúc gửi — hết hạn server trả OTP_EXPIRED, người dùng bấm "Gửi lại mã". OTP-04 luôn enable khi đủ 6 số.
OTP-03
Link "Gửi lại mã"
Disabled khi countdown còn chạy · click → vô hiệu OTP cũ, gửi mã mới, reset countdown · tôn trọng rate limit BR-05.
OTP-04
Nút "Xác nhận & gửi hồ sơ"
Enable khi đủ 6 số · click → verify OTP; đúng → tạo tenant + redirect DN-002; sai → lỗi inline + rung nhẹ các ô, clear mã.
OTP-05
Nút "Huỷ"
Đóng modal, quay lại form (dữ liệu giữ nguyên). OTP đã gửi vẫn còn hiệu lực trong thời hạn.
Lưu ý cho dev
Trong prototype, nút "Xác nhận & gửi hồ sơ" là thẻ <a href> điều hướng thẳng — khi implement phải là button gọi API verify, chỉ redirect khi server trả thành công.
8.API contract (đề xuất — backend chốt cuối)
8.1 · Gửi hồ sơ & yêu cầu OTP
// Tạo bản ghi đăng ký tạm + gửi OTP. Files upload multipart cùng request (đơn giản, phù hợp scale 30 DN).
POST /api/v1/auth/signup
Content-Type: multipart/form-data
company_name : string(≤200)
tax_code : string(10|13 digits)
email : string(email, lowercase)
phone : string(VN phone)
password : string(≥8)
consent : boolean — bắt buộc true; server lưu consent_at (FR-75)
documents[] : file × 1–5 (pdf|jpg|jpeg|png, ≤10MB/file)
// 200 OK
{ "registration_id": "reg_8f2k...", "otp_expires_in": 300, "resend_cooldown": 60 }
// Lỗi
409 TAX_CODE_EXISTS → "Tài khoản đã tồn tại, vui lòng đăng nhập"
409 EMAIL_EXISTS → "Email đã được sử dụng..."
422 VALIDATION_ERROR → { "errors": { "tax_code": "..." } }
413 FILE_TOO_LARGE / 415 FILE_TYPE_INVALID
429 RATE_LIMITED → { "retry_after_seconds": 1800 }
8.2 · Xác thực OTP → tạo tenant
POST /api/v1/auth/signup/verify
{ "registration_id": "reg_8f2k...", "otp": "387265" }
// 200 OK — tenant + user đã tạo, trạng thái pending_kyc
{ "tenant_id": "tn_01H...", "user_id": "us_01H...", "kyc_status": "pending_kyc" }
// Lỗi
400 OTP_INVALID → { "attempts_left": 3 }
400 OTP_EXPIRED
423 OTP_LOCKED → { "retry_after_seconds": 1800 }
410 REGISTRATION_EXPIRED // bản ghi tạm quá TTL — yêu cầu đăng ký lại
Ghi chú lưu trữ
File ĐKKD lưu MinIO theo key kyc/{registration_id}/{filename}; khi tenant được tạo, gắn ownership theo tenant_id. OTP hash trước khi lưu (không lưu plaintext), so sánh constant-time.
9.Trạng thái màn hình (UI states)
State
Mô tả hiển thị
Empty / Default
Form pristine: mọi field trống chỉ có placeholder, counter file "0/5", thanh độ mạnh mật khẩu ẩn, không có lỗi, nút submit enable.
Loading (skeleton)
Toàn bộ card thay bằng skeleton shimmer khớp layout thật (title, lead, 2 field đơn, 2 hàng grid đôi, khối upload, consent, nút submit) — dùng khi trang đang tải dữ liệu.
Error
Banner đỏ đầu card "Không thể gửi hồ sơ đăng ký. Vui lòng kiểm tra lại các trường được đánh dấu bên dưới." + field lỗi viền đỏ kèm message inline (kịch bản mẫu AC-03: MST đã tồn tại → lỗi tại MST, Email, file ĐKKD); focus field lỗi đầu tiên.
Uploading
Mỗi file có progress; chưa upload xong thì disable nút submit.
Xem demo trong prototypedn-001-signup-mockup.html có khối "State & Spec" ở góc phải dưới: preview trực tiếp 3 trạng thái Loading / Empty / Error (bấm lại nút đang active để về default) + link mở file spec này. Khối này chỉ phục vụ review prototype — không build vào production.
10.Yêu cầu phi chức năng
Bảo mật: HTTPS bắt buộc; bcrypt cost 12 (BR-07); OTP hash + constant-time compare; rate limit theo BR-05; không log mật khẩu/OTP; CSRF protection cho form.
Hiệu năng: Trang tải <2s trên mạng 3G nhanh; upload 5 file 10MB không block UI (upload song song hoặc tuần tự có progress).
Responsive: Mobile ≤560px chuyển grid 2 cột → 1 cột (đã có trong prototype); modal OTP dùng được trên mobile.
Accessibility: Label gắn for/id đúng field; lỗi đọc được bởi screen reader (aria-describedby + aria-invalid); toggle mắt có aria-label; focus ring rõ (đã có --focus-ring).
Ngôn ngữ: 100% tiếng Việt, kể cả thông báo lỗi từ server.
Đơn giản hoá: Quy mô 30 DN — không cần queue/CDN/captcha phức tạp ở MVP; rate limit đơn giản theo email + IP là đủ.
11.Edge cases
ID
Tình huống
Hành vi mong đợi
EC-01
Đăng ký tạm đã tồn tại cho cùng email/MST (user quay lại sau khi bỏ dở)
Ghi đè bản ghi tạm cũ (chưa verify) bằng bản mới; OTP cũ vô hiệu.
EC-02
MST đã có tenant nhưng bị rejected KYC trước đó
Cho đăng ký lại — hồ sơ mới vào lại queue duyệt (OQ-07 ✅); hiển thị ghi chú "Hồ sơ trước đã bị từ chối".
EC-03
Refresh trang khi modal OTP đang mở
Form mất dữ liệu (chấp nhận ở MVP); bản ghi tạm vẫn hợp lệ — nếu đăng ký lại cùng email thì đi theo EC-01.
EC-04
2 người cùng submit MST giống nhau gần như đồng thời
Ràng buộc unique ở DB; người sau nhận 409 TAX_CODE_EXISTS.
EC-05
File đúng đuôi nhưng sai nội dung (đổi tên .exe → .pdf)
Server validate MIME/magic bytes, từ chối với 415.
EC-06
Email nhập đúng định dạng nhưng không tồn tại (không nhận được OTP)
User không thể verify → bản ghi tạm hết TTL tự dọn. Hint trong modal: "Kiểm tra cả hộp thư Spam".
EC-07
Kéo thả nhiều hơn 5 file cùng lúc
Nhận 5 file đầu, toast "Tối đa 5 file".
EC-08
Unicode/emoji trong tên DN
Cho phép ký tự tiếng Việt có dấu; escape đầy đủ khi render (chống XSS).
12.Câu hỏi mở — đã chốt (03/07/2026)
✅ Đã chốt toàn bộ 8 câu hỏi theo đề xuất của BA — 03/07/2026
Spec lấy prototype dn-001-signup-mockup.html làm chuẩn. Cột "Quyết định" dưới đây là quyết định chính thức, đã lan truyền vào các mục 4 / 5 / 6 / 7 / 8 / 11 của spec, prototype và tài liệu liên quan (07-1 screen inventory, 07-3 UX spec).
ID
Câu hỏi
Quyết định (theo đề xuất BA) ✅
OQ-01
Screen inventory mô tả flow 2 bước (OTP trước → form KYC sau); prototype gộp thành 1 form + OTP modal cuối. Chốt theo hướng nào?
Theo prototype (1 form + OTP cuối) — ít bước hơn, phù hợp mục tiêu onboarding nhanh.
OQ-02
SĐT: inventory ghi optional, prototype đánh dấu bắt buộc (*).
Bắt buộc — cần kênh liên hệ khi duyệt KYC thủ công.
OQ-03
Định dạng MST: inventory ghi "10/13 ký tự", prototype ghi "10 hoặc 14 chữ số". MST chi nhánh VN là 13 số, thường viết ##########-### (14 ký tự gồm gạch).
Nhận 10 số hoặc 13 số; cho phép nhập kèm dấu "-", chuẩn hoá bỏ gạch trước khi lưu.
OQ-04
Inventory có thêm field địa chỉ trụ sở, ngành nghề, người đại diện pháp luật — prototype không có.
Bỏ khỏi DN-001 (giảm friction); thu thập sau tại DN-007 Tenant Settings sau khi được duyệt.
OQ-05
Consent: inventory yêu cầu checkbox đồng ý DPA + Privacy (FR-75); prototype chỉ có dòng text implicit.
Dùng checkbox bắt buộc + lưu consent_at timestamp làm bằng chứng đồng ý (đáp ứng FR-75).
OQ-06
Prototype ghi OTP "hết hạn sau 60 giây" — 60s là quá ngắn cho email đến hộp thư.
60s là cooldown "Gửi lại mã"; hiệu lực mã thực tế nên là 5 phút.
OQ-07
DN bị reject KYC có được tự đăng ký lại không, hay phải qua support?
Cho tự đăng ký lại, hồ sơ mới vào lại queue duyệt (đơn giản nhất cho MVP).
OQ-08
URL "Điều khoản sử dụng" / "Chính sách bảo mật" là gì? (prototype đang để #)
Cần trang tĩnh /terms và /privacy trước khi launch.
13.Lịch sử thay đổi
Version
Ngày
Người viết
Nội dung
v1.2
03/07/2026
BA
Chốt 8 câu hỏi mở (mục 12) theo đề xuất BA: (1) giữ flow 1 form + OTP modal cuối; (2) SĐT bắt buộc; (3) MST 10/13 số, nhận dấu "-", chuẩn hoá bỏ gạch — SU-02 + mục 5; (4) không thêm field phụ, thu thập tại DN-007; (5) consent thành checkbox bắt buộc + lưu consent_at — SU-08, mục 5, API 8.1; (6) OTP hiệu lực 5 phút / cooldown 60s — OTP-02, BR-05, API 8.1; (7) reject KYC được tự đăng ký lại — EC-02; (8) cần trang tĩnh /terms + /privacy trước launch. Đồng bộ prototype + 07-1 inventory + 07-3 UX spec.
v1.1
03/07/2026
BA
Đồng bộ theo prototype cập nhật: (1) lead copy mới + badge SLA "Duyệt hồ sơ trong vòng 24 giờ" (SU-00); (2) thêm chỉ báo độ mạnh mật khẩu 3 mức — mục 5.1, cập nhật SU-05; (3) chi tiết hoá 3 UI states Loading (skeleton) / Empty / Error theo thiết kế mới + ghi chú khối demo "UI State" trong prototype.
v1.0
02/07/2026
BA
Bản đầu tiên — viết từ prototype dn-001-signup-mockup.html + PRD FR-1a/FR-7/FR-18/FR-75 + screen inventory 07-1.