Skip to content

Shared Rules — CRM Ticket Workflow

1. Thuật ngữ chuẩn

Thuật ngữNghĩa trong code hiện tạiGhi chú
Ticketcrm.ticketWork item trung tâm cho chăm sóc/tư vấn/follow-up
Resultticket.result_idKết quả xử lý của ticket hiện tại; có thể quyết định sinh ticket mới
Statusticket.status_idTrạng thái lifecycle như new, assigned, completed, canceled
Assigneeticket.assignee_idNgười trực tiếp xử lý ticket
In charge / curatorticket.in_charge_idNgười theo dõi/phụ trách rộng hơn assignee
Parent ticketparent_ticket_idTicket trước đó trong chain
Root ticketroot_ticket_idGốc chuỗi ticket
Sourcesource_idNguồn sinh ticket như hotline, CRM staff, chain result, scheduler
Ticket distributeticket_distributePointer nhớ last-assignee cho auto-distribution
Incallincall_call, incall_call_log, incall_extensionRuntime hotline/call center gắn với ticket

2. Source Matrix

SourceÝ nghĩa business hiện trong FEOrigin thực tế
ticket_source_1Trực page, phát triển cộng đồng, phát triển thị trường, hotline, ADS, HQ SaleChủ yếu external lead/hotline surface
ticket_source_2Telesales, chăm sóc khách hàngCRM staff tạo trực tiếp
ticket_source_3Tạo bởi kết quả của ticket trước đóchangeStatusTicket/duplicate chain
ticket_source_4Lịch hẹn xác nhận, công việc hoàn thành, đơn hàng hoàn thành, thanh toán thành công, phát sinh đơn hàngScheduler consolidate_ticket_4
ticket_source_5Tạo từ lịch hẹn tư vấnAppointment-driven
ticket_source_6Tạo từ đánh giá công việcUpstream evaluation flow
ticket_source_7Tạo từ nhãn liên hệ sai sốContact label/wrong-number cleanup
ticket_source_8Sinh nhật hôm nay và thực thu tối thiểu 1 triệuScheduler consolidate_ticket_8

sourceDescriptions đang nằm trực tiếp ở FE (Tickets.tsx:128-158), nên nếu business đổi taxonomy mà quên sửa code thì tooltip sẽ drift ngay.

3. Status Matrix

Status familyÝ nghĩa
ticket_status_newTicket mới, thường chưa có assignee
ticket_status_assignedTicket đã có assignee và đang được xử lý
ticket_status_completedTicket hiện tại hoàn tất; có thể kéo theo ticket kế tiếp
ticket_status_canceledTicket dừng / hủy

Điểm đáng chú ý:

  • FE có auto-transition new -> assigned nếu save với assigneeId.
  • FE cũng có nhánh assigned -> new nếu bỏ assignee rồi save.
  • Kết quả (result_id) thường được chốt cùng lúc với complete, nhưng rule sinh ticket mới không nằm ở FE mà nằm ở backend duplicate flow.

4. Actor Matrix

ActorVai trò
Telesales / CSKH staffXử lý ticket được giao, follow-up, call
Telesales / CSKH leaderThấy rộng hơn, edit rộng hơn, assignment surface
Call center / hotline operatorRuntime call và log cuộc gọi
Admin / BODScope rộng, không bị bó bởi setSpecialDataByRole()
Scheduler / systemTạo source 4, source 8, nhắc lịch, auto-distribute

5. Action / Scheduler Matrix

NameTypeVai trò
assignMultipleTicketHasura actionBulk assign ticket theo branch và target role
changeStatusTicketHasura actionValidate transition và xử lý duplicate/new ticket chain
callcenterCallDataHasura action + REST endpointGhi nhận trạng thái cuộc gọi inbound/outbound
callcenterCallOutEventHasura action + REST endpointGhi event cuộc gọi đi
secureIncallHasura actionContract bảo mật khi khởi tạo call
consolidate_ticket_4CronSinh source 4 tickets lúc 18:30 hằng ngày
consolidate_ticket_8CronSinh source 8 tickets lúc 18:20 hằng ngày
distribute_ticketCronAuto-assign ticket lúc 21:00 hằng ngày
remind_ticket_todayCronNhắc ticket due/overdue lúc 01:00
remind_ticket_tomorrowCronNhắc ticket có appointment ngày mai lúc 09:30

6. Invariants quan trọng

SR-001: changeStatusTicket là lifecycle engine thật

  • FE có thể gọi mutation create/update trước, nhưng transition hợp lệ cuối cùng vẫn bị chặn ở ticket.ValidateNewStatus(...).
  • Nhánh TicketCompleted gọi ticket.Duplicate(...) thay vì update trực tiếp (change_status_ticket.go:44-57).

SR-002: Result không phải lúc nào cũng kết thúc chuỗi ticket

FE có whitelist resultNotGenerateNewTicket() gồm:

  • cancel_consultant_appointment
  • cancel_service
  • wrong_number
  • wrong_information
  • completed

Nếu complete trả new_ticket_id và result không nằm trong whitelist này, UI sẽ chuyển thẳng sang ticket mới (TicketCreate.tsx:174-181, TicketCreate.tsx:305-319).

SR-003: Auto-distribution chỉ nhớ last assignee ở scheduler path

  • assignMultipleTicket tính assignee theo branch/role nhưng không ghi ticket_distribute.
  • distribute_ticket vừa update ticket vừa insert ticket_distribute cho assignee cuối mỗi target (distribute_ticket.go:132-147).

Hệ quả:

  • manual assign và auto-distribute không chia sẻ fairness state hoàn toàn,
  • audit logic cần tách rõ ticket được giao bởi action hay bởi cron.

SR-004: Visibility của ticket đang phụ thuộc mạnh vào FE builder

TicketBuildWhere.setSpecialDataByRole() đáng ra phải thêm:

  • ticket unassigned theo belong_to,
  • ticket đang assign cho current user,
  • ticket current user là in_charge,
  • ticket theo target_id với assigned roles.

Nhưng do _or.concat(...) không reassign, hai nhánh assignee/in-charge bị mất (useTicketBuilder.ts:143-187).

SR-005: Call log và ticket có thể được nối sau khi ticket vừa tạo

Nếu newCallObj tồn tại trong localStorage, TicketCreate sẽ update incall_call.ticket_id ngay sau khi create/update ticket thành công (TicketCreate.tsx:210-220).

7. Boundary Checklist

Khi phân tích bug hoặc change request trong vùng này, luôn hỏi:

  1. Ticket đến từ source nào: manual, chain result, hay scheduler?
  2. Case này đang đi qua create/update form hay đi thẳng qua changeStatusTicket?
  3. Assignment xảy ra bởi bulk action hay cron distribute_ticket?
  4. Ticket có đang gắn appointment hoặc call log không?
  5. Visibility issue nằm ở Hasura permission hay nằm ở TicketBuildWhere phía FE?

8. Rủi ro / Findings

IDFinding
SR-F01Source taxonomy hiện sống một phần trong tooltip FE, một phần trong scheduler/backend; rất dễ drift.
SR-F02Result semantics và status semantics đang bị trộn trong save flow, nên QA nếu chỉ test status matrix sẽ thiếu chain-generation behavior.
SR-F03Assignment fairness/history không đồng nhất vì manual path và cron path không dùng cùng persistence rule.
SR-F04Ticket visibility cho non-leader CRM roles có dấu hiệu sai thật do bug _or.concat(...).