Sẽ mở trong thẻ mới
PDL AIAI Worker — trợ lý AI trực 24/7: trả lời khách, xử lý yêu cầu, báo cáo tự động

Kỹ sư phần mềm thời AITờ 03-05 / 112

User story và tiêu chí chấp nhận: ngôn ngữ chung giữa bạn, team và AI

Một phiếu đặt bánh chuẩn ở giữa bàn được ba bên cùng đọc, ví cho user story và tiêu chí chấp nhận
Tờ
03-05 / 112
Hạng mục
M03 · Từ ý tưởng đến yêu cầu
Cấp độ
Cơ bản
Thời gian đọc
18 phút
Giai đoạn công trình
0 · Khám phá
Cập nhật
Bản đồ công trìnhTờ này lắp: PRD & spec, Màn hình bếp, Kiểm thử
Bản đồ công trình Mộc OMSBài này: PRD & spec, Kiểm thử, Màn hình bếp. Đã học: Coding agent, Khách hàng, Kênh bán, Sheet & sổ tay. Chưa tới: Repo & CI, Hosting & tên miền, Giám sát lỗi, Giao diện app, API, Database, Dashboard, báo cáo, Trợ lý AI nhận đơn, Đăng nhập, vai trò, Thanh toán VietQR, Thông báo khách, Nhiều tiệm (MộcOS), Kế toán, hoá đơn.Thi công & hạ tầngDòng đơn hàngNghiệp vụ & dịch vụMở rộng MộcOSPRD & specspecCoding agentagentRepo & CIrepo-ciKiểm thửtestsHosting & tên miềnhostingGiám sát lỗimonitoringKhách hàngcustomerKênh bánchannelsGiao diện appapp-uiAPIapiDatabasedatabaseDashboard, báo cáoreportsSheet & sổ taysheetTrợ lý AI nhận đơnai-assistantMàn hình bếpkitchenĐăng nhập, vai tròauthThanh toán VietQRpaymentsThông báo kháchnotificationsNhiều tiệm (MộcOS)tenantsKế toán, hoá đơnintegrations0 Khám phá1 MVP2 Ra mắt3 Thêm AI4 Mở rộng
Bản đồ công trình Mộc OMSBài này: PRD & spec, Kiểm thử, Màn hình bếp. Đã học: Coding agent, Khách hàng, Kênh bán, Sheet & sổ tay. Chưa tới: Repo & CI, Hosting & tên miền, Giám sát lỗi, Giao diện app, API, Database, Dashboard, báo cáo, Trợ lý AI nhận đơn, Đăng nhập, vai trò, Thanh toán VietQR, Thông báo khách, Nhiều tiệm (MộcOS), Kế toán, hoá đơn.Dòng đơn hàngKhách hàngKênh bánGiao diệnAPIDatabaseBáo cáoNghiệp vụ & dịch vụSheet, sổ tayTrợ lý AIMàn hình bếpĐăng nhậpThanh toánThông báoThi công & hạ tầngPRD & specCoding agentRepo & CIKiểm thửHostingGiám sátMở rộng MộcOSNhiều tiệmKế toán0 Khám phá1234

Bài nàyĐã họcChưa tớiĐã thay thế

Bạn sẽ học được gì

Tiêu chí nghiệm thu

Vì sao cần một “ngôn ngữ chung”?

Khi chị Lan nói “làm chức năng tạo đơn”, mỗi người nghe hiểu một kiểu. Hà hình dung form có ô chữ trên bánh. Anh Tuấn hình dung đơn tự hiện ở bếp. Coding agent hình dung… một form với các trường lấy từ bảng orders và nút “Lưu”. Không ai sai, nhưng cũng không ai giống ai.

User story và tiêu chí chấp nhận giống phiếu đặt bánh chuẩn của Bánh Mộc: thay vì để mỗi người tự hiểu “một cái bánh sinh nhật”, phiếu bắt buộc ghi size, vị, chữ, giờ nhận. Ba bên — bạn, người dùng, AI — cùng đọc một tờ phiếu và cùng biết thế nào là “làm đúng”.

Với agent, có thêm một lý do quan trọng: agent cần cách tự kiểm chứng. Nếu bạn chỉ nói “làm chức năng tạo đơn”, tín hiệu duy nhất để agent biết đã xong là “code trông có vẻ hợp lý” — và bạn trở thành người kiểm tra thủ công mọi thứ. Nếu bạn đưa 8 tiêu chí Given–When–Then, agent có thể viết 8 test, chạy chúng, và đưa bạn bằng chứng là chúng đều qua.

User story: ai, muốn gì, để làm gì

Mẫu user story phổ biến nhất:

Là [vai trò], tôi muốn [hành động/khả năng], để [lợi ích/mục đích].

Ví dụ: Là nhân viên bán hàng online, tôi muốn tạo đơn bánh kem đặt trước với đầy đủ chữ, size, vị và giờ nhận, để bếp làm đúng bánh mà tôi không phải gọi lại kiểm tra.

Ba phần đều quan trọng:

  • Vai trò cho biết ai dùng — từ đó suy ra quyền hạn, thiết bị, bối cảnh. “Nhân viên bán hàng online” khác “quản lý chi nhánh”.
  • Hành động cho biết cần làm gì — nhưng chưa nói làm bằng giao diện nào.
  • Lợi ích cho biết vì sao — và là căn cứ để cắt bớt hoặc chọn giải pháp khác. Nếu không viết được vế “để…”, story đó có thể không cần.

User story không phải spec đầy đủ. Nó là lời hứa về một cuộc trò chuyện: story + tiêu chí chấp nhận đủ để mọi người hiểu giống nhau; chi tiết kỹ thuật (giao diện, dữ liệu, API) nằm trong spec tính năng mà bạn sẽ viết ở bài 03-0603-06.

Kiểm tra chất lượng bằng INVEST

INVEST là bộ sáu tiêu chí cho một user story tốt:

Chữ cái Nghĩa Câu hỏi kiểm tra Ví dụ vi phạm ở Bánh Mộc
I — Independent Độc lập Làm được mà không chờ story khác? “Tạo đơn” phụ thuộc “báo cáo doanh thu”
N — Negotiable Thương lượng được Có chừa chỗ cho cách làm khác không? “Tôi muốn một dropdown 5 lựa chọn màu xanh ở góc phải”
V — Valuable Có giá trị Người dùng hoặc doanh nghiệp được gì? “Tôi muốn bảng orders có cột updated_at” (việc kỹ thuật, không phải story)
E — Estimable Ước lượng được Đủ rõ để ước lượng công sức? “Tôi muốn hệ thống thông minh hơn”
S — Small Nhỏ Xong trong vài ngày (hoặc vài phiên agent)? “Tôi muốn quản lý toàn bộ đơn hàng”
T — Testable Kiểm tra được Có thể nói đạt/không đạt? “Tôi muốn form dễ dùng”

Tiêu chí chấp nhận dạng Given–When–Then

Given–When–Then là cấu trúc mô tả một tình huống kiểm tra, xuất phát từ phương pháp BDD (Behavior-Driven Development — phát triển hướng hành vi) và ngôn ngữ Gherkin:

  • Given (cho trước): bối cảnh, trạng thái ban đầu. “Hà đã đăng nhập với vai trò staff tại chi nhánh Phú Nhuận; bây giờ là 10:00 ngày 12/10/2026.”
  • When (khi): hành động của người dùng hoặc sự kiện. “Hà tạo đơn bánh kem với giờ nhận 08:00 ngày 13/10/2026.”
  • Then (thì): kết quả quan sát được. “Hệ thống từ chối và báo ‘Bánh kem tùy chỉnh cần đặt trước ít nhất 24 giờ’.”
  • And (và): nối thêm điều kiện trong cùng một phần.

Bí quyết viết tốt:

  1. Cụ thể đến từng con số. “Giờ nhận sau 22 giờ” thay vì “giờ nhận quá gần”. Con số làm tiêu chí kiểm tra được và lộ ra câu hỏi (“đúng 24 giờ thì sao?”).
  2. Mô tả điều quan sát được, không mô tả cách cài đặt. “Thấy mã đơn dạng BM-261012-xxx” thay vì “hàm createOrder trả về object”.
  3. Một tình huống, một hành vi. Đừng dồn 5 kiểm tra vào một kịch bản.
  4. Ngôn ngữ nghiệp vụ. Viết sao cho Hà đọc hiểu và xác nhận được.

Một lưu ý về từ khóa: Gherkin hỗ trợ từ khóa ở nhiều ngôn ngữ, nhưng giữ Given/When/Then bằng tiếng Anh, nội dung bằng tiếng Việt là cách dễ đọc cho cả người lẫn agent và công cụ.

Bàn thử

Dán user story + kịch bản → kiểm đủ 3 vế ‘Là… tôi muốn… để…’, có Given/When/Then, có con số cụ thể, đếm luồng chính / biên / lỗi; gợi ý câu hỏi săn edge case theo 8 nhóm của bài

Bàn thử cần bật JavaScript.

Kết quả chỉ để tham khảo, không thay cho việc bạn tự duyệt.

Các quy tắc nghiệp vụ dùng trong bài

Trước khi viết story, chị Lan chốt các quy tắc rút ra từ nghiên cứu ở bài 03-0203-02. Các con số là quyết định kinh doanh của Bánh Mộc trong chuỗi bài, bạn sẽ có quy tắc riêng.

Mã Quy tắc
QT-01 Đơn có bánh kem tùy chỉnh bắt buộc có: size, vị, giờ nhận; nếu khách muốn chữ thì chữ trên bánh tối đa 40 ký tự, giữ nguyên dấu tiếng Việt.
QT-02 Bánh kem tùy chỉnh phải đặt trước ít nhất 24 giờ. Nhân viên (staff) không tạo được đơn gấp; quản lý (manager) hoặc chủ (owner) được tạo đơn gấp nếu nhập lý do.
QT-03 Giờ nhận trong khung 07:00–21:00 (giờ mở cửa).
QT-04 Đơn có bánh kem tùy chỉnh chỉ được chuyển sang da_xac_nhan khi đã cọc ít nhất 50% tiền bánh (không tính phí giao), làm tròn lên nghìn đồng.
QT-05 Số điện thoại được chuẩn hóa về dạng bắt đầu bằng 0 (di động 10 chữ số, cố định 11 chữ số đầu 02); mỗi số chỉ thuộc một khách.
QT-06 Giá trên dòng đơn lấy từ danh mục sản phẩm; chỉ manager/owner được sửa đơn giá.
QT-07 Bếp nhóm bánh theo chuyến xe: chuyến 07:30 chở bánh có giờ nhận trước 15:00; chuyến 13:00 chở bánh có giờ nhận từ 15:00.
QT-08 staff và manager chỉ xem đơn của chi nhánh mình; owner xem tất cả. Bếp trung tâm xem bánh kem của mọi chi nhánh.

Bộ user story của Mộc OMS

Dưới đây là 6 user story cho lát 1 của MVP (xem bài 03-0403-04), mỗi story có tiêu chí chấp nhận gồm luồng chính, trường hợp biên và luồng lỗi.

US-01: Tạo đơn bánh kem đặt trước

Là nhân viên bán hàng online (Hà), tôi muốn tạo đơn bánh kem tùy chỉnh với đầy đủ chữ, size, vị, giờ nhận, để bếp làm đúng bánh mà tôi không phải gọi lại kiểm tra.

GHERKIN
Scenario: Tạo đơn bánh kem hợp lệ
  Given Hà đăng nhập với vai trò staff tại chi nhánh Phú Nhuận
  And bây giờ là 10:00 ngày 12/10/2026
  And khách có số điện thoại 0901234567 đã tồn tại
  When Hà tạo đơn gồm 1 "Bánh kem tùy chỉnh 20cm", vị "Chanh dây", chữ "Mừng sinh nhật Mẹ Ánh"
  And giờ nhận 17:00 ngày 13/10/2026, nhận tại chi nhánh, kênh Zalo
  Then đơn được tạo với trạng thái "moi" và mã dạng "BM-261012-xxx"
  And tổng tiền bằng đơn giá trong danh mục nhân số lượng
  And chữ trên bánh được lưu đúng "Mừng sinh nhật Mẹ Ánh", giữ nguyên dấu

Scenario: Thiếu vị bánh
  Given Hà đang tạo đơn có "Bánh kem tùy chỉnh 20cm"
  When Hà bấm "Tạo đơn" mà chưa chọn vị
  Then đơn không được tạo
  And ô "Vị" được đánh dấu lỗi với thông báo "Vui lòng chọn vị bánh"

Scenario: Chữ trên bánh quá dài
  When Hà nhập chữ trên bánh dài 41 ký tự
  Then hệ thống báo "Chữ trên bánh tối đa 40 ký tự" và không cho tạo đơn

Scenario: Nhân viên đặt bánh kem gấp dưới 24 giờ
  Given bây giờ là 10:00 ngày 12/10/2026
  When Hà (staff) chọn giờ nhận 08:00 ngày 13/10/2026
  Then hệ thống báo "Bánh kem tùy chỉnh cần đặt trước ít nhất 24 giờ. Liên hệ quản lý để tạo đơn gấp."

Scenario: Quản lý tạo đơn gấp có lý do
  Given anh Tuấn đăng nhập với vai trò manager
  When anh Tuấn tạo đơn bánh kem nhận sau 6 giờ và nhập lý do "Khách quen, bếp đã nhận"
  Then đơn được tạo và được đánh dấu "Đơn gấp" kèm lý do

Scenario: Bấm "Tạo đơn" hai lần liên tiếp
  When Hà bấm "Tạo đơn" hai lần trong vòng 1 giây
  Then chỉ có đúng một đơn được tạo

US-02: Tìm khách theo số điện thoại

Là nhân viên nhận đơn, tôi muốn tìm khách bằng số điện thoại, để không tạo trùng khách và biết khách đã từng mua gì.

GHERKIN
Scenario: Tìm thấy khách với số có định dạng khác
  Given khách "Trần Văn B" có số 0901234567
  When Hà nhập "+84 901 234 567" vào ô tìm kiếm
  Then hệ thống hiển thị khách "Trần Văn B" và 3 đơn gần nhất

Scenario: Không tìm thấy khách
  When Hà nhập "0912 000 111" và không có khách nào trùng
  Then hệ thống mở form tạo khách mới với số "0912000111" đã điền sẵn

Scenario: Số điện thoại không hợp lệ
  When Hà nhập "09123"
  Then hệ thống báo "Số điện thoại cần 10 chữ số, bắt đầu bằng 0"

US-03: Ghi nhận tiền cọc

Là nhân viên nhận đơn, tôi muốn ghi nhận tiền cọc ngay trên đơn, để chi nhánh nhận bánh biết khách còn phải trả bao nhiêu và cuối ngày đối soát nhanh.

GHERKIN
Scenario: Cọc đủ 50% thì xác nhận được đơn
  Given đơn BM-261012-012 có tiền bánh 600.000đ, nhận tại tiệm (tổng 600.000đ), trạng thái "moi", chưa thanh toán
  When Hà ghi nhận 300.000đ chuyển khoản, mã giao dịch "FT2610121234"
  Then trạng thái thanh toán là "deposit" và số tiền còn lại hiển thị 300.000đ
  And nút "Xác nhận đơn" được bật

Scenario: Cọc chưa đủ 50% với đơn bánh kem
  Given đơn bánh kem tiền bánh 600.000đ, giao tận nơi phí 30.000đ (tổng 630.000đ)
  When Hà ghi nhận cọc 200.000đ và bấm "Xác nhận đơn"
  Then đơn vẫn ở trạng thái "moi"
  And hệ thống báo "Đơn bánh kem cần cọc tối thiểu 300.000đ (50% tiền bánh) trước khi xác nhận"

Scenario: Ghi nhận vượt số tiền còn lại
  Given đơn tổng 600.000đ đã cọc 300.000đ
  When Hà ghi nhận thêm 400.000đ
  Then hệ thống báo "Số tiền vượt quá số còn phải thu (300.000đ)"

US-04: Màn hình bếp theo chuyến xe

Là quản lý bếp trung tâm (anh Tuấn), tôi muốn xem danh sách bánh kem cần làm trong ngày, nhóm theo chuyến xe, để phân công thợ và không bánh nào lỡ chuyến.

GHERKIN
Scenario: Nhóm bánh theo chuyến xe
  Given ngày 13/10/2026 có 3 đơn bánh kem đã xác nhận: nhận 10:00, 14:30 và 17:00
  When anh Tuấn mở màn hình bếp ngày 13/10/2026
  Then nhóm "Chuyến 07:30" có 2 bánh (10:00 và 14:30), sắp theo giờ nhận
  And nhóm "Chuyến 13:00" có 1 bánh (17:00)
  And mỗi bánh hiển thị: mã đơn, chi nhánh nhận, size, vị, chữ trên bánh nguyên văn

Scenario: Đơn chưa xác nhận không hiện ở bếp
  Given đơn bánh kem nhận ngày 13/10 đang ở trạng thái "moi"
  Then đơn đó không xuất hiện trên màn hình bếp

Scenario: Đơn bị hủy sau khi bếp đã thấy
  Given đơn BM-261012-011 đang hiển thị ở bếp
  When quản lý hủy đơn này
  Then lần tải lại kế tiếp, đơn hiển thị gạch ngang với nhãn "ĐÃ HỦY" thay vì biến mất lặng lẽ

US-05: Hủy đơn có lý do

Là quản lý chi nhánh, tôi muốn hủy đơn kèm lý do, để bếp không làm bánh thừa và có dấu vết khi cần tra cứu.

GHERKIN
Scenario: Hủy đơn chưa làm
  Given đơn ở trạng thái "da_xac_nhan" đã cọc 300.000đ
  When quản lý hủy đơn với lý do "Khách đổi ngày, đặt đơn mới"
  Then đơn chuyển "da_huy", lịch sử trạng thái ghi người hủy, thời điểm và lý do
  And hệ thống nhắc "Đơn đã cọc 300.000đ — xử lý hoàn cọc theo quy định của tiệm"

Scenario: Nhân viên không được hủy đơn bếp đang làm
  Given đơn ở trạng thái "dang_lam"
  When Hà (staff) mở đơn
  Then không có nút "Hủy đơn" và có dòng "Liên hệ quản lý để hủy đơn đang làm"

Scenario: Thiếu lý do hủy
  When quản lý bấm "Hủy đơn" mà để trống lý do
  Then hệ thống không hủy và báo "Vui lòng nhập lý do hủy"

US-06: Dashboard đơn hôm nay

Là chủ chuỗi (chị Lan), tôi muốn xem số đơn hôm nay theo trạng thái và chi nhánh, để phát hiện sớm chi nhánh đang quá tải hoặc có đơn bị treo.

GHERKIN
Scenario: Chủ xem toàn chuỗi
  Given hôm nay có 40 đơn ở Phú Nhuận, 35 ở Thảo Điền, 30 ở Gò Vấp
  When chị Lan (owner) mở dashboard
  Then thấy tổng 105 đơn và bảng số đơn theo trạng thái cho từng chi nhánh

Scenario: Quản lý chỉ thấy chi nhánh mình
  When quản lý chi nhánh Gò Vấp mở dashboard
  Then chỉ thấy số liệu của Gò Vấp

Scenario: Đơn treo
  Given có đơn ở trạng thái "moi" đã quá 2 giờ kể từ khi tạo
  Then dashboard hiển thị đơn đó trong mục "Cần xử lý"

Trường hợp biên và luồng lỗi: đi săn có hệ thống

Trường hợp biên (edge case) là tình huống nằm ở rìa của quy tắc: đúng 24 giờ, chuỗi rỗng, 999 đơn trong ngày. Luồng lỗi (error flow) là khi có gì đó hỏng: mất mạng, hai người sửa cùng lúc, người không có quyền cố truy cập. Một bảng câu hỏi gợi ý để đi săn:

Nhóm Câu hỏi Ví dụ ở Mộc OMS
Ranh giới số Đúng bằng ngưỡng thì sao? Ngay dưới, ngay trên? Giờ nhận cách đúng 24 giờ; chữ đúng 40 ký tự; cọc đúng 50%
Thời gian Qua nửa đêm? Múi giờ? Ngày lễ? Đơn tạo lúc 23:59 và 00:01 có mã khác ngày; lưu timestamptz, hiển thị giờ Việt Nam
Định dạng Dữ liệu có dấu, khoảng trắng, ký tự lạ? Chữ trên bánh có emoji (bếp không viết được); số điện thoại có “+84”
Lặp và đồng thời Bấm hai lần? Hai người cùng thao tác? Hai nhân viên tạo đơn cùng giây vẫn có mã khác nhau; hai người sửa cùng đơn
Quyền Người không có quyền thì thấy gì? Staff Gò Vấp mở đường dẫn đơn của Phú Nhuận
Trạng thái Hành động ở trạng thái “sai”? Sửa đơn đã hoan_tat; ghi cọc cho đơn da_huy
Hạ tầng Mất mạng, chậm, lỗi máy chủ? Mất Wi-Fi ở quầy lúc bấm “Tạo đơn” — dữ liệu form không được mất
Dữ liệu cũ Dữ liệu chuyển từ Sheet không chuẩn? Khách cũ không có số điện thoại; số điện thoại trùng giữa hai khách

Bạn không cần viết Given–When–Then cho mọi ô. Hãy chọn những ô có hậu quả lớn (sai tiền, sai bánh, lộ dữ liệu) để viết thành tiêu chí, còn lại đưa vào mục “câu hỏi mở” để quyết định. Ví dụ, khi chị Lan hỏi anh Tuấn về bánh nhận lúc 07:30 (trước khi chuyến sáng tới chi nhánh), anh nói “bánh đó phải đi chuyến 13:00 hôm trước” — một quy tắc QT-07 chưa bao phủ, được ghi vào câu hỏi mở của spec. Kỹ thuật săn edge case được đào sâu ở bài dữ liệu test và edge case08-07.

Từ tiêu chí chấp nhận sang test tự động

Đây là phần khiến tiêu chí chấp nhận trở thành “vũ khí” khi làm việc với agent. Mỗi kịch bản Given–When–Then gần như dịch thẳng được thành một test:

Hình chữ ký Phát lại phiên agent
  1. QT-02: bánh kem tùy chỉnh đặt trước tối thiểu 24 giờ
  2. chấp nhận khi giờ nhận cách hiện tại đúng 24 giờ (ranh giới)
  3. từ chối staff khi giờ nhận chỉ cách 22 giờ
  4. cho manager tạo đơn gấp khi có lý do

Vuốt ngang để xem hết sơ đồ

User story

Tiêu chí chấp nhận
Given–When–Then

Unit test
(quy tắc thuần: 24 giờ, chuẩn hóa SĐT)

Test end-to-end
(luồng trên giao diện)

Kịch bản kiểm thử thủ công / UAT

Chạy tự động mỗi lần thay đổi

Nghiệm thu với người dùng

Vuốt ngang để xem hết sơ đồ

User story

Tiêu chí chấp nhận
Given–When–Then

Unit test
(quy tắc thuần: 24 giờ, chuẩn hóa SĐT)

Test end-to-end
(luồng trên giao diện)

Kịch bản kiểm thử thủ công / UAT

Chạy tự động mỗi lần thay đổi

Nghiệm thu với người dùng

Hình 1 Sơ đồ luồng
Đọc sơ đồ dạng chữ
  1. User story → Tiêu chí chấp nhận Given–When–Then
  2. Tiêu chí chấp nhận Given–When–Then → Unit test (quy tắc thuần: 24 giờ, chuẩn hóa SĐT)
  3. Tiêu chí chấp nhận Given–When–Then → Test end-to-end (luồng trên giao diện)
  4. Tiêu chí chấp nhận Given–When–Then → Kịch bản kiểm thử thủ công / UAT
  5. Unit test (quy tắc thuần: 24 giờ, chuẩn hóa SĐT) → Chạy tự động mỗi lần thay đổi
  6. Test end-to-end (luồng trên giao diện) → Chạy tự động mỗi lần thay đổi
  7. Kịch bản kiểm thử thủ công / UAT → Nghiệm thu với người dùng
  • Quy tắc thuần (tính toán, kiểm tra điều kiện) → unit test chạy nhanh, ví dụ bằng Vitest.
  • Luồng người dùng trên giao diện → test end-to-end, ví dụ bằng Playwright.
  • Những gì khó tự động (chữ có dễ đọc trên máy tính bảng ở bếp không) → kịch bản kiểm thử thủ công cho buổi nghiệm thu.

Ví dụ, kịch bản “Nhân viên đặt bánh kem gấp dưới 24 giờ” thành unit test. Để ý chú thích Given/When/Then được giữ lại trong code — người đọc test có thể đối chiếu ngược về story:

TStests/unit/orders/lead-time.test.ts
// tests/unit/orders/lead-time.test.ts
import { describe, it, expect } from "vitest";
import { checkLeadTime } from "@/features/orders/rules";

describe("QT-02: bánh kem tùy chỉnh đặt trước tối thiểu 24 giờ", () => {
  // Given: bây giờ là 10:00 ngày 12/10/2026 (giờ Việt Nam)
  const now = new Date("2026-10-12T10:00:00+07:00");

  it("chấp nhận khi giờ nhận cách hiện tại đúng 24 giờ (ranh giới)", () => {
    const result = checkLeadTime({
      hasCustomCake: true,
      deliveryAt: new Date("2026-10-13T10:00:00+07:00"),
      now,
      role: "staff",
    });
    expect(result.ok).toBe(true);
  });

  it("từ chối staff khi giờ nhận chỉ cách 22 giờ", () => {
    // When: Hà chọn giờ nhận 08:00 ngày 13/10
    const result = checkLeadTime({
      hasCustomCake: true,
      deliveryAt: new Date("2026-10-13T08:00:00+07:00"),
      now,
      role: "staff",
    });
    // Then: bị từ chối với mã lỗi rõ ràng
    expect(result.ok).toBe(false);
    expect(result.code).toBe("LEAD_TIME_TOO_SHORT");
  });

  it("cho manager tạo đơn gấp khi có lý do", () => {
    const result = checkLeadTime({
      hasCustomCake: true,
      deliveryAt: new Date("2026-10-12T16:00:00+07:00"),
      now,
      role: "manager",
      rushReason: "Khách quen, bếp đã nhận",
    });
    expect(result.ok).toBe(true);
  });
});

Và quy tắc QT-05 về số điện thoại — một hàm nhỏ cùng bộ test dạng bảng:

TSsrc/lib/phone.ts
// src/lib/phone.ts
/** Chuẩn hóa số điện thoại Việt Nam về dạng 0xxxxxxxxx; trả về null nếu không hợp lệ. */
export function normalizePhone(input: string): string | null {
  // Bỏ khoảng trắng, dấu chấm, gạch ngang, ngoặc
  let digits = input.trim().replace(/[\s.\-()]/g, "");
  // +84xxxxxxxxx hoặc 84xxxxxxxxx -> 0xxxxxxxxx
  if (digits.startsWith("+84")) digits = "0" + digits.slice(3);
  else if (digits.startsWith("84")) digits = "0" + digits.slice(2);
  if (!/^\d+$/.test(digits)) return null; // còn ký tự lạ → không hợp lệ
  if (/^0[35789]\d{8}$/.test(digits)) return digits; // di động: 10 số
  if (/^02\d{9}$/.test(digits)) return digits; // cố định: 11 số, đầu 02
  return null; // thiếu số, thừa số, đầu số lạ
}
TStests/unit/customers/phone.test.ts
// tests/unit/customers/phone.test.ts
import { describe, it, expect } from "vitest";
import { normalizePhone } from "@/lib/phone";

describe("QT-05: chuẩn hóa số điện thoại", () => {
  it.each([
    ["0901 234 567", "0901234567"],
    ["+84 901 234 567", "0901234567"],
    ["84901234567", "0901234567"],
    ["090.123.4567", "0901234567"],
  ])("chuẩn hóa '%s' thành '%s'", (input, expected) => {
    expect(normalizePhone(input)).toBe(expected);
  });

  it("trả về null với số không đủ 10 chữ số", () => {
    expect(normalizePhone("09123")).toBeNull();
  });
});

Luồng chính của US-01 trên giao diện, viết bằng Playwright:

TStests/e2e/tao-don-banh-kem.spec.ts
// tests/e2e/tao-don-banh-kem.spec.ts
import { test, expect } from "@playwright/test";

test("US-01: Hà tạo đơn bánh kem hợp lệ", async ({ page }) => {
  // Given: đã đăng nhập vai trò staff (trạng thái đăng nhập chuẩn bị sẵn trong cấu hình test)
  await page.goto("/orders/new");

  // When: nhập thông tin đơn
  await page.getByLabel("Số điện thoại").fill("0901234567");
  await page.getByRole("button", { name: "Tìm khách" }).click();
  await page.getByLabel("Sản phẩm").selectOption({ label: "Bánh kem tùy chỉnh 20cm" });
  await page.getByLabel("Vị").selectOption({ label: "Chanh dây" });
  await page.getByLabel("Chữ trên bánh").fill("Mừng sinh nhật Mẹ Ánh");
  await page.getByLabel("Giờ nhận").fill("2026-10-13T17:00");
  await page.getByRole("button", { name: "Tạo đơn" }).click();

  // Then: thấy mã đơn và chữ trên bánh giữ nguyên dấu
  await expect(page.getByText(/BM-\d{6}-\d{3}/)).toBeVisible();
  await expect(page.getByText("Mừng sinh nhật Mẹ Ánh")).toBeVisible();
});

Áp dụng vào dự án Bánh Mộc

Nhật ký công trình · Bánh Mộc

BM-261013-027

13.10.2026

  • Chị LanChủ chuỗi
  • Anh TuấnBếp trung tâm
  • HàBán hàng online

Chị Lan viết bản nháp 6 story trên trong một buổi tối, rồi làm ba việc trước khi coi là xong.

Đọc to với người dùng. Chị đọc từng kịch bản cho Hà và anh Tuấn nghe. Hà bắt lỗi ngay ở US-02: “Chị ơi, nhiều khách nhắn bằng Zalo không cho số điện thoại, em chỉ có tên Zalo.” Đây là một trường hợp biên quan trọng. Chị quyết định: MVP vẫn bắt buộc số điện thoại (Hà sẽ hỏi khách — vì bếp và người giao đều cần), nhưng ghi vào câu hỏi mở cho giai đoạn trợ lý AI nhận đơn qua Zalo.

Nhờ AI săn trường hợp biên. Chị đưa 6 story cho AI với Prompt 2 bên dưới. AI đề xuất 23 trường hợp. Chị giữ lại 7 trường hợp có hậu quả lớn (ví dụ “chữ trên bánh có emoji”, “hai nhân viên tạo đơn cùng giây”), gạch 10 cái không thực tế với tiệm bánh, và đưa 6 cái vào câu hỏi mở.

Đánh mã để truy vết. Mỗi story có mã (US-01…), mỗi quy tắc có mã (QT-01…). Chị dặn agent đặt mã này vào tên test. Sau này khi test “QT-02” thất bại, ai cũng biết ngay quy tắc nghiệp vụ nào đang bị vi phạm.

Các story được lưu vào docs/specs/user-stories.md và trở thành đầu vào cho spec “Tạo đơn hàng” ở bài tiếp theo.

Prompt mẫu

Prompt 1 — Viết user story và tiêu chí chấp nhận từ mô tả thô

Giao việc cho agent

Bối cảnh: phần mềm quản lý đơn cho chuỗi 3 tiệm bánh. Vai trò: owner, manager, staff.
Quy tắc nghiệp vụ: """[dán bảng QT-01…QT-08]"""
Nhu cầu thô: "Hà cần ghi nhận tiền cọc ngay trên đơn để chi nhánh biết khách còn phải trả bao nhiêu."
Hãy viết:
1. Một user story theo mẫu "Là… tôi muốn… để…".
2. Tiêu chí chấp nhận dạng Given–When–Then (từ khóa tiếng Anh, nội dung tiếng Việt), gồm:
   ít nhất 1 luồng chính, 2 trường hợp biên, 2 luồng lỗi; dùng số cụ thể (tiền VNĐ, giờ, ngày).
3. Tự kiểm tra story theo INVEST, ghi chữ nào chưa đạt và cách sửa.
Không mô tả giao diện chi tiết hay cách cài đặt.

Prompt 2 — Săn trường hợp biên

Giao việc cho agent

Đây là các user story và tiêu chí chấp nhận của tôi:
"""[dán]"""
Đóng vai kiểm thử viên khó tính. Liệt kê các trường hợp biên và luồng lỗi CHƯA được bao phủ, nhóm theo:
ranh giới số, thời gian/múi giờ, định dạng dữ liệu, lặp/đồng thời, phân quyền, trạng thái, hạ tầng, dữ liệu cũ.
Với mỗi trường hợp: mô tả 1 dòng, mức hậu quả (cao/vừa/thấp) với tiệm bánh, và câu hỏi tôi cần quyết định.
Sắp xếp theo mức hậu quả giảm dần.

Prompt 3 — Giao agent viết test từ tiêu chí (trong repo)

Giao việc cho agent

Đọc docs/specs/user-stories.md, phần US-01 và các quy tắc QT-01, QT-02, QT-03.
Nhiệm vụ: CHỈ viết test, chưa viết code tính năng.
- Unit test (Vitest) cho quy tắc thuần trong tests/unit/orders/, tên test bắt đầu bằng mã quy tắc (ví dụ "QT-02: ...").
- Một test E2E (Playwright) cho luồng chính US-01 trong tests/e2e/.
- Giữ chú thích Given/When/Then trong test.
Chạy bộ test, xác nhận các test MỚI đều thất bại vì tính năng chưa tồn tại (không phải vì lỗi cú pháp),
và dán output vào câu trả lời. Không sửa file ngoài thư mục tests/.

Sai lầm thường gặp

  1. Sự cố 01

    Story không có vế “để…”.

    Triệu chứng

    “Là staff, tôi muốn có nút xuất Excel.” *

    Cách tránh

    * bắt buộc vế lợi ích; nếu không viết được, hỏi người dùng hoặc gạch story.

  2. Sự cố 02

    Tiêu chí mơ hồ, không kiểm tra được.

    Triệu chứng

    “Form phải dễ dùng, nhanh.” *

    Cách tránh

    * dùng số và hành vi quan sát được: “Tạo một đơn bánh kem trong dưới 90 giây với khách đã có.”

  3. Sự cố 03

    Chỉ có happy path.

    Triệu chứng

    *

    Cách tránh

    * mỗi story tối thiểu 1 luồng chính, 1–2 trường hợp biên, 1–2 luồng lỗi; dùng bảng câu hỏi săn edge case.

  4. Sự cố 04

    Mô tả cách cài đặt thay vì hành vi.

    Triệu chứng

    “Then gọi API POST /orders trả 201.” *

    Cách tránh

    * viết thứ người dùng thấy; chi tiết kỹ thuật để dành cho spec và plan.

  5. Sự cố 05

    Story quá lớn.

    Triệu chứng

    “Quản lý đơn hàng.” *

    Cách tránh

    * chia theo luồng, vai trò hoặc quy tắc cho tới khi mỗi story vừa vài phiên làm việc với agent.

  6. Sự cố 06

    Viết xong không cho người dùng đọc.

    Triệu chứng

    *

    Cách tránh

    * đọc to từng kịch bản với người dùng thật; họ sẽ phát hiện những trường hợp bạn không ngờ.

Checklist

Phiếu nghiệm thu

Bài tập thực hành

  1. Bài 1

    Cơ bản

    Sửa story xấu

    Viết lại 4 story sau cho đạt INVEST: “Tôi muốn hệ thống quản lý khách hàng”; “Là admin, tôi muốn bảng customers có cột birthday”; “Tôi muốn form đẹp hơn”; “Là staff, tôi muốn xuất Excel”.

    Kết quả mong đợi

    mỗi story có vai trò rõ, vế “để…” có ý nghĩa, đủ nhỏ, kèm ít nhất 2 tiêu chí Given–When–Then.

  2. Bài 2

    Cơ bản

    Ranh giới

    Với quy tắc “đặt trước ít nhất 24 giờ”, viết đủ các kịch bản ranh giới: 23 giờ 59 phút, đúng 24 giờ, 24 giờ 1 phút, qua nửa đêm, giờ nhận ngoài khung 07:00–21:00.

    Kết quả mong đợi

    5 kịch bản, mỗi kịch bản có giờ hiện tại và giờ nhận cụ thể, kết quả rõ ràng.

  3. Bài 3

    Nâng cao

    Từ tiêu chí đến test với agent

    Chọn một story của bạn, viết tiêu chí đầy đủ, rồi dùng Prompt 3 cho agent viết test trước. Đọc từng test và đánh dấu test nào không kiểm tra đúng điều tiêu chí nói.

    Kết quả mong đợi

    test chạy và thất bại đúng lý do; bạn tìm ra ít nhất một chỗ test lệch so với tiêu chí (ví dụ kiểm tra quá lỏng) và yêu cầu agent sửa.

Thuật ngữ trong bài

User story
Mô tả nhu cầu từ góc nhìn người dùng: là ai, muốn gì, để làm gì.
Tiêu chí chấp nhận (acceptance criteria)
Điều kiện cụ thể để coi một story là hoàn thành.
Given–When–Then
Cấu trúc kịch bản: bối cảnh – hành động – kết quả.
Gherkin
Ngôn ngữ viết kịch bản dạng Given–When–Then dùng trong BDD.
BDD
Phát triển hướng hành vi: mô tả hành vi mong muốn trước, dùng làm test.
INVEST
Sáu tiêu chí story tốt: độc lập, thương lượng được, có giá trị, ước lượng được, nhỏ, kiểm tra được.
Happy path
Luồng chính khi mọi thứ diễn ra suôn sẻ.
Trường hợp biên (edge case)
Tình huống ở rìa quy tắc hoặc ít gặp, dễ gây lỗi.
Luồng lỗi (error flow)
Kịch bản khi có sự cố: dữ liệu sai, mất mạng, không có quyền.
Unit test / E2E test
Test một quy tắc nhỏ độc lập / test toàn bộ luồng qua giao diện.

Đọc thêm

Nghiệm thu tờ 03-05

Đọc xong và tự kiểm theo phiếu nghiệm thu, rồi đánh dấu tờ này.

Tác giả
Phú Digital
Đội ngũ PDL Digital — triển khai website, giải pháp AI và digital marketing cho doanh nghiệp.
Tư vấn 1:1

Muốn dựng phần mềm riêng cho doanh nghiệp của bạn?

Kỹ sư PDL cùng bạn chọn bước đầu tiên phù hợp — phản hồi trong giờ làm việc (T2–T7, 8h–18h).

<script src="https://cdn.jsdelivr.net/npm/page-flip/dist/js/page-flip.browser.js"></script>

<div id="container-flipbook" style="display:flex; justify-content:center;">
    <div id="my-flipbook">
       <div class="page"> <img src="https://pdl.vn/wp-content/uploads/2025/05/AI-Google-Search.jpg" /> </div>
       <div class="page"> <img src="https://pdl.vn/wp-content/uploads/2025/05/wearable-tech-advertising.jpg" /> </div>
       <div class="page"> <img src="https://pdl.vn/wp-content/uploads/2025/05/multimodal-embeddings-digital-marketing.jpg" /> </div>
    </div>
</div>
#my-flipbook {
    box-shadow: 0 0 20px rgba(0,0,0,0.2);
    margin: 20px auto;
    background-color: #f0f0f0;
}

.page {
    background-color: white;
    overflow: hidden;
}

.page img {
    width: 100%;
    height: 100%;
    object-fit: cover; /* Giúp ảnh vừa vặn trang sách */
}
// Đảm bảo thư viện đã sẵn sàng
function initFlipbook() {
    const el = document.getElementById("my-flipbook");
    if (!el || !window.St) return; // Nếu chưa thấy thư viện St thì thoát

    const pageFlip = new St.PageFlip(el, {
        width: 550,
        height: 733,
        size: "stretch",
        minWidth: 315,
        maxWidth: 1000,
        minHeight: 420,
        maxHeight: 1350,
        showCover: true,
        maxShadowOpacity: 0.5,
    });

    pageFlip.loadFromHTML(document.querySelectorAll(".page"));
}

// Chạy khởi tạo
if (document.readyState === 'complete') {
    initFlipbook();
} else {
    window.addEventListener('load', initFlipbook);
}
PDL · Tư vấn 1:1

Nhận tư vấn
miễn phí

Để lại thông tin, chuyên viên PDL sẽ liên hệ để hiểu nhu cầu và đề xuất giải pháp phù hợp ngân sách của bạn.

  • Phân tích nhanh hiện trạng website, quảng cáo, kênh bán hàng
  • Đề xuất lộ trình & báo giá minh bạch, rõ từng hạng mục
  • Không ràng buộc: nghe tư vấn xong mới quyết định
Giờ làm việc T2–T7 · 08:00–18:00

Chỉ mất khoảng 30 giây

Bạn cần tư vấn về

🔒 Thông tin được bảo mật, chỉ dùng để PDL liên hệ tư vấn.