Dokumen ini memetakan alur yang benar-benar berjalan pada source saat ini untuk Alert & Case Management, mulai dari alert generation, triage, AUTO_NEGATIVE, deduplication, clustering, case creation, auto-assignment, HOLDING queue, investigator presence, lock, maker-checker, classification, SLA, sampai case closure dan reopen.
Dokumen ini adalah baseline perilaku current, bukan desain target. Rancangan rework yang sudah disepakati—termasuk penghapusan lock, whole-case ownership ketika alert dipilih, dynamic clustering dengan fallback hpan/merchantId, dan action tetap alert-scoped—berada di docs/ALERT_CASE_MANAGEMENT_REWORK_ANALYSIS.md.
| Area | Status | Batas verifikasi |
|---|---|---|
| Service flow dan branching | SOURCE-TRACED |
Ditelusuri dari implementation aktif |
| Endpoint dan scheduler | SOURCE-TRACED |
Ditelusuri dari controller dan annotation |
| Database behavior | SOURCE-TRACED |
Berdasarkan entity, repository, dan Liquibase |
| Runtime behavior | NOT RUNTIME-VERIFIED |
Tidak menjalankan aplikasi atau integration test |
| Production behavior | NOT PRODUCTION-VERIFIED |
Tidak memeriksa data atau konfigurasi environment production |
[!DANGER] Current vs target
Source current masih memiliki single-alert lock dan case bulk lock. Source current juga memindahkan satu alert ke standalone case ketika alert tersebut dipilih dari multi-alert case. Keduanya bukan perilaku target rework.
| Concern | Komponen current | Tanggung jawab |
|---|---|---|
| Alert generation | BaseAlertServiceImpl |
Same-UTRN guard, dedup, triage, alert persistence, case assignment, post-save action |
| Triage | ThresholdPolicy, PriorityService, system parameter ALERT_TRIAGE_ACTIVE |
Menghitung score, priority, dan eligibility AUTO_NEGATIVE |
| Clustering | ClusteringKeyBuilder, T_ALERT_CLUSTERING_SETTING |
Membentuk dynamic clustering key dari field transaksi |
| Case aggregate | AlertCase, CaseServiceImpl |
Assignment, reassign, lifecycle, bulk operation, link/unlink, note, export |
| Distribution | AlertDistributionService |
Memilih group dan investigator untuk case baru |
| Holding queue | HoldingQueueService |
Retry distribution, notify, force-assign |
| Presence | PresenceService, ReAssignmentService |
Status ACTIVE/IDLE/AWAY/OFFLINE/BUSY dan respons saat investigator unavailable |
| Alert action | Issuer/Acquirer alert service | Lock, unlock, direct classification, fraud flag, alert SLA |
| Maker-checker | Issuer/Acquirer ActionRequest dan BulkActionRequest |
Submission, approval, decline, discussion, action execution |
| SLA | AlertSlaLifecycleService, SlaCheckerServiceImpl |
Clock alert dan case, warning, breach, escalation |
| Queue | AlertQueueService, CaseServiceImpl |
Available, assigned, reassignable, active, historical views |
| Audit/notification | AuditTrailService, NotificationService, SSE |
Rekam perubahan dan dorong notification ke investigator/group |
Sebelum membuat alert, service mencari alert non-duplicate dengan utrnno yang sama. Jika ditemukan, alert kedua tidak dibuat dan transaksi hanya ditandai sudah pernah menghasilkan alert.
Same-UTRN guard berbeda dari deduplication:
Jika active dedup configuration menghasilkan key yang sama dengan alert canonical berstatus UNASSIGNED atau IN_REVIEW:
duplicateCount alert canonical dinaikkan;isDuplicated=true, isClassified=true, alertStatus=UNASSIGNED;locked=true dan memiliki lockedAt;Aturan pembentukan key current:
| Step | Current behavior | Contoh |
|---|---|---|
| Configuration scope | active=true dan (applyBoth=true atau alertType sama) |
Setting global ikut Issuer dan Acquirer |
| Field cleanup | null dibuang, lalu trim, exact distinct, dan sort ascending |
merchantId menjadi merchantId |
| Direct lookup | Transaction exact → transaction camelCase → context exact → context camelCase | dest_acc_number mencoba destAccNumber |
rule alias |
ruleId/bindingId dari transaction lalu context |
Rule-triggered dedup |
customer alias |
customerId → hpan → mpan |
Fallback customer identity |
| Destination alias | destAccNumber |
Tujuan transfer |
| Channel alias | transactionType → transactionCode |
Kanal/transaksi |
| Missing value | Tetap menghasilkan field= |
merchantId= |
| Final format | Komponen digabung menggunakan | |
merchantId=M1|terminalId=T1 |
[!DANGER] Empty-value collision
Selama setting tersedia, field yang tidak ditemukan tidak membatalkan dedup key. Ia menjadi
field=. Jika seluruh configured values kosong, transaksi berbeda dapat menghasilkan key kosong-nilai yang sama dan masuk dedup branch.
Field duplicate shadow:
| Field | Value current |
|---|---|
utrnno, ruleId, versionNo |
Disalin dari GenerateAlertContext |
rawRiskValue, triageScore, priority |
Hasil triage transaksi duplicate itu sendiri |
classificationType |
Hasil resolveAlertDecision: dapat SUSPICIOUS atau NEGATIVE |
alertStatus |
Selalu UNASSIGNED |
isDuplicated |
true |
isAutoNegative |
Default false dari BaseAlert |
isClassified |
true |
classifiedDate |
Timestamp generation |
classifiedBy |
Tidak diisi pada builder ini |
isLocked |
true |
lockedAt |
Timestamp generation |
lockedBy |
Tidak diisi |
caseId |
Tidak diisi |
duplicateCount |
0 |
| Assignment/SLA | Tidak diinisialisasi |
[!DANGER] Canonical selection current
Repository lookup hanya memfilter
deduplicationKeydan statusUNASSIGNED/IN_REVIEW; tidak ada predicateisDuplicated=falsedan tidak ada explicit ordering. Karena duplicate shadow sendiri disimpan sebagaiUNASSIGNED, shadow sebelumnya tetap eligible dipilih oleh lookup berikutnya. Pemilihan row juga tidak deterministik ketika lebih dari satu row memenuhi filter.
[!DANGER] Concurrent increment risk
Increment
duplicateCountdilakukan dengan read-modify-save tanpa pessimistic lock atau atomic update pada jalur yang ditelusuri. Dua duplicate concurrent dapat membaca count sama dan salah satu increment berpotensi hilang.
Semua keputusan triage dan dedup dihitung lebih dulu di buildGenerateAlertContext() sebelum same-UTRN guard dan dedup early return dijalankan.
| Context field | Source dan fallback current |
|---|---|
timestamp |
Waktu aplikasi saat generation dimulai |
utrnno |
transDetails.utrnno, default 0L |
rawRiskValue |
transDetails.riskValue, default 0; missing key menulis warning log |
| Threshold | Active ThresholdPolicy sesuai AlertType; tidak ada row menjadi null |
minThreshold |
String minimum policy diparse integer; default 0 |
maxThreshold |
String maximum policy diparse integer; default 0 |
triageScore |
Normalisasi rawRiskValue terhadap maxThreshold |
priority |
CRITICAL untuk STOP_LIST; selain itu hasil AlertTriageEvaluatorService |
decision |
Kombinasi alertStatus dan classificationType |
ruleName, alertName |
Untuk binding RULE, keduanya dari data.ruleName; tipe lain string kosong |
dedupKey |
Hasil active dedup settings dan field resolver |
ruleId |
transDetails.ruleId; nilai <=0 menjadi null |
versionNo |
transDetails.ruleVersionNo; nilai <=0 menjadi null |
Normalization current memakai integer arithmetic:
100;rawRiskValue negatif dapat menghasilkan triageScore negatif;maxThreshold<=0 menghasilkan score 0 tanpa exception;try. Jika maximum invalid, minimum tidak sempat diparse; jika minimum invalid, maximum yang sudah valid tetap dipakai.operatorType |
Match current |
|---|---|
GREATER_THAN |
score > value1 |
LESS_THAN |
score < value1 |
GREATER_THAN_OR_EQUAL |
score >= value1 |
LESS_THAN_OR_EQUAL |
score <= value1 |
EQUAL |
score.equals(value1) |
BETWEEN |
value1 dan value2 wajib non-null; kedua batas inclusive |
Setting diurutkan berdasarkan value1 DESC, bukan berdasarkan enum priority. Jika range overlap, setting pertama yang match menjadi hasil. Untuk operator selain BETWEEN, source mengandalkan value1 tidak null.
isAutoNegative hanya true ketika seluruh precondition kiri terpenuhi dan salah satu kondisi hasil kanan terpenuhi:
ALERT_TRIAGE_ACTIVE
AND thresholdPolicy exists
AND rawRiskValue != 0
AND (priority == LOW OR triageScore < minThreshold)
| Triage toggle | Policy | Raw risk | Priority/score | Decision current |
|---|---|---|---|---|
| Missing parameter | Ada | Non-zero | LOW atau score di bawah minimum | AUTO_NEGATIVE; missing parameter default true |
false |
Apa pun | Apa pun | Apa pun | UNASSIGNED + SUSPICIOUS |
true |
Tidak ada | Non-zero | LOW | UNASSIGNED + SUSPICIOUS |
true |
Ada | 0 |
LOW/score rendah | UNASSIGNED + SUSPICIOUS |
true |
Ada | Non-zero | LOW | AUTO_NEGATIVE + NEGATIVE |
true |
Ada | Non-zero | Bukan LOW, score < minThreshold |
AUTO_NEGATIVE + NEGATIVE |
true |
Ada | Non-zero | Bukan LOW, score >= minThreshold |
UNASSIGNED + SUSPICIOUS |
Ketika policy ada tetapi threshold string invalid, object policy tetap dianggap ada. Hasil decision kemudian memakai nilai threshold yang berhasil diparse atau default 0, ditambah priority hasil evaluator.
| Field | Normal managed alert | AUTO_NEGATIVE |
|---|---|---|
alertStatus |
UNASSIGNED |
AUTO_NEGATIVE |
classificationType |
SUSPICIOUS |
NEGATIVE |
isClassified |
Default false |
true |
classifiedDate |
Tidak diisi | Timestamp generation |
isDuplicated |
false |
false |
isAutoNegative |
false |
true |
isLocked |
false |
false |
caseId |
Diisi oleh clustering flow | Tidak diisi |
| Alert SLA | Dimulai setelah memperoleh assignee | Tidak dimulai |
| Post-save scoring | Dijalankan | Dilewati |
| Scenario | Current result |
|---|---|
ALERT_TRIAGE_ACTIVE=false |
Tidak ada auto-negative; alert mengikuti flow normal |
| Triage aktif, threshold tidak ada | Tidak auto-negative |
Triage aktif, raw risk 0 |
Tidak auto-negative |
Priority LOW |
Auto-negative jika precondition lain terpenuhi |
Score di bawah minThreshold |
Auto-negative jika precondition lain terpenuhi |
STOP_LIST |
Priority dipaksa CRITICAL, lalu mengikuti evaluasi berikutnya |
ALERT_DISTRIBUTION_ACTIVE tidak mengontrol AUTO_NEGATIVE. Toggle tersebut hanya mengontrol auto-distribution case baru.
[!INFO] AUTO_NEGATIVE current
Alert
AUTO_NEGATIVEtetap dipersist sebagai historical evidence dengan classificationNEGATIVE, tetapi tidak memiliki case, assignment, distribution, scoring post-save, atau alert SLA. Ini berbeda dari sekadar membuang event.
ClusteringKeyBuilder membaca active clustering settings sesuai AlertType, mengambil field dari transaction details, mengurutkan field, lalu menghasilkan key berbentuk field=value|field=value.
Pada source current belum ada fallback bawaan hpan untuk Issuer atau merchantId untuk Acquirer. Jika configuration/value tidak menghasilkan key, alert dibuatkan standalone case.
(clusteringKey, alertType, active status).alertCount=1, caseScore=triageScore, slaPriority=alert priority, dan demographic snapshot.caseScore dapat naik, tetapi source current tidak terlihat menaikkan slaPriority case.Closed cases tidak termasuk CaseStatus.ACTIVE, sehingga lookup tidak mengembalikan closed case. Jika key sama muncul kembali setelah closure, service mencoba membuat active case baru.
Setelah managed alert disimpan, service menjalankan side effects berikut:
AUTO_NEGATIVE dan non-duplicate;AUTO_NEGATIVE, case distribution/notification dilewati karena caseId null, tetapi pembuatan initial discussion tetap dicoba.Auto-distribution hanya dijalankan untuk case baru yang masih UNASSIGNED ketika ALERT_DISTRIBUTION_ACTIVE=true. Alert yang bergabung ke case existing tidak memicu redistribusi case.
Supported condition operators adalah EQUALS, NOT_EQUALS, GREATER_THAN, LESS_THAN, IN, dan LIKE. Group tanpa condition dianggap match. Pada tie workload, service menggunakan waktu assignment untuk memilih kandidat yang lebih lama tidak menerima assignment.
Jika group cocok tetapi investigator tidak tersedia, atau tidak ada group/catch-all yang dapat menerima case:
HOLDING;holdingSince diisi satu kali;Fallback configuration jika row database tidak ada:
| Priority | Notify | Force-assign | Critical |
|---|---|---|---|
CRITICAL |
1 minute | 3 minutes | 5 minutes |
HIGH |
2 minutes | 5 minutes | 10 minutes |
MEDIUM |
5 minutes | 15 minutes | 30 minutes |
LOW |
10 minutes | 30 minutes | 60 minutes |
Notification HOLDING normal memiliki cooldown 5 menit; critical notification memiliki cooldown 10 menit.
Presence dihitung ulang setiap 30 detik. Manual BUSY memiliki precedence, kemudian focus, SSE/heartbeat, activity, dan akhirnya ACTIVE.
Saat status berubah ke AWAY atau OFFLINE:
Current behavior sengaja tidak auto-reassign case IN_REVIEW; group hanya diberi notification. Auto-reroute hanya diterapkan pada case ESCALATED dan dapat diblokir oleh alert lock.
Manual case assignment:
IN_REVIEW;[!DANGER] Current split behavior
Memilih satu alert dari multi-alert case memecah hasil clustering dan membuat standalone case baru. Target rework yang telah disepakati akan mengubah pick alert menjadi whole-case ownership, tanpa split.
caseId.assignCase.Reassign menulis previousAssignee, isReassigned, target assignee, dan status IN_REVIEW, kemudian menyinkronkan seluruh alert. Berbeda dari initial/manual assign, source current tidak me-reset SLA deadline pada method reassign.
Current reassignCase() tidak mengubah assignedDate, firstResponseAt, atau case SLA deadline. Karena synchronization memakai state case, member alert menerima owner baru tetapi tidak memperoleh SLA start baru jika SLA sebelumnya sudah dimulai.
Bulk assign dari endpoint Issuer/Acquirer lebih dulu mengelompokkan selected alert berdasarkan caseId.
[!INFO] Scope bulk alert assignment
Jika hanya satu alert dipilih tetapi alert tersebut sudah berada dalam sebuah case, service memanggil
assignCase(). Akibatnya seluruh member alert pada case ikut menerima assignment, walaupun responsesucceededhanya berisi selected alert IDs.
Bulk case operations sengaja tidak all-or-nothing. Case yang sukses tetap commit walaupun case lain gagal karena missing case, closed case, invalid user, atau same-assignee validation.
Controller current tetap mengembalikan response HTTP sukses dengan BulkOperationResult; caller harus membaca failed, bukan hanya HTTP status.
Candidate dipilih dari membership group secara role-agnostic dan tanpa filter presence/workload yang dipakai auto-distribution. Pada method current yang ditelusuri, manual escalation tidak memanggil syncAlertAssignment(), sehingga assignee case dapat berubah tanpa langsung memperbarui projection assignee pada member alert.
Walaupun ownership utama sudah berada di case, source current masih memakai alert lock sebagai syarat editing/action.
case.assignedTo;isLocked, lockedAt, dan lock owner./case/{id}/bulk-lock mengunci seluruh member Issuer dan Acquirer. Current bulk path mengisi isLocked dan lockedAt, tetapi tidak menyamakan lockedBy seperti single-alert lock. Bulk unlock melepas lock semua member.
Current bulk lock/unlock tidak memvalidasi bahwa caller adalah case.assignedTo, tidak mengisi/membersihkan lockedBy, dan tidak menghasilkan audit per alert. Karena berjalan dalam transaction service yang sama, unexpected database exception akan menggagalkan operation tersebut sebagai satu transaction.
Ketika case assignment disinkronkan:
IN_REVIEW jika case assigned, atau UNASSIGNED jika tidak;Classification final adalah POSITIVE dan NEGATIVE. Keduanya:
SUSPICIOUS dan POSTPONED bukan final. POSTPONED membutuhkan durasi dan menyimpan actor/timestamp/until. Temporary classification dapat membuka kembali alert SLA.
Endpoint direct classify pada Issuer/Acquirer juga tersedia. Jalur ini tetap membutuhkan editing lock dan case ownership, tetapi tidak melalui lifecycle ActionRequest approval.
/case/{id}/bulk-classify current:
Jalur ini tidak membuat ActionRequest per alert. Kegagalan update fraud flag pada satu alert dicatat dan loop lanjut ke alert berikutnya.
Issuer dan Acquirer juga memiliki BulkActionRequest. Request menyimpan satu record bulk berisi kumpulan alert IDs, lalu membuat discussion detail per alert.
Untuk action non-assignment, approval loop berada dalam transaction service bulk; exception yang tidak ditangani dari salah satu runAction dapat membatalkan approval transaction. Untuk assignment action, executor memanggil bulk assign/reassign yang memang mengembalikan partial result.
| Area | Direct case bulk classify | Bulk ActionRequest classify |
|---|---|---|
| Entry point | /case/{id}/bulk-classify |
/bulk-*-action-request/add lalu /approved/{id} |
| Target resolution | Seluruh open alerts pada satu case | Explicit alertIds pada bulk record |
| Lock requirement | Tidak terlihat divalidasi | Submission membutuhkan lock dan case ownership |
| Maker-checker | Tidak | Ya |
| Audit/history | Satu case-level bulk audit | Bulk history + discussion per alert |
| Fraud flag failure | Dicatat per alert, loop lanjut | Mengikuti exception behavior RunAction |
| Partial result | Tidak ada BulkOperationResult |
Assignment action dapat membawa partial result |
Current source menjalankan dua clock SLA: satu pada case dan satu pada alert.
slaStartedAt;ON_TRACK, AT_RISK, BREACHED, dan escalation level;POSITIVE/NEGATIVE menyelesaikan clock;Alert checker hanya mengambil ON_TRACK, AT_RISK, dan BREACHED. Transition tidak pernah diturunkan karena evaluator membandingkan ordinal current dan target. WARNING_50 masih memetakan SlaStatus.ON_TRACK; status baru menjadi AT_RISK mulai WARNING_20.
Escalation level bersifat naik; scheduler tidak menurunkan level yang sudah lebih tinggi. Pada case SLA breach, pemilihan assignee lain menggunakan membership group dan tidak memakai workload/presence filter selengkap distribution service.
[!DANGER] Dual SLA current
Case dan alert dapat sama-sama menghasilkan warning/breach notification. Ini adalah current behavior, bukan rekomendasi target.
Setelah final classification, closeCaseIfAllResolved:
| Kombinasi hasil member alert | Case result |
|---|---|
Ada POSITIVE dan NEGATIVE |
CLOSED_MIXED |
Ada POSITIVE, tidak mixed |
CLOSED_FRAUD |
Tidak ada POSITIVE |
CLOSED_FALSE_POSITIVE |
Manual close memakai derivation yang sama, tetapi method current tidak mensyaratkan seluruh alert sudah final.
Reopen mengubah case menjadi REOPENED, menyimpan actor/date, membersihkan close metadata, mengembalikan case SLA ke ON_TRACK, dan menghitung ulang deadline. Pada source yang ditelusuri, reopen case tidak otomatis mengubah classification/status atau reopen SLA seluruh member alert.
alert.caseId ke target case, lalu memperbarui count source/target, audit, dan notification.UNASSIGNED case baru lalu memindahkan alert ke sana.Current link tidak menyinkronkan target case assignment/status/SLA ke alert yang baru dipindahkan. Current unlink juga tidak membersihkan assignment/status/SLA lama pada alert; ia hanya mengganti caseId ke standalone case baru.
Unified queue dibangun dari case, bukan raw alert:
| Queue | Filter utama |
|---|---|
| Available | UNASSIGNED, HOLDING |
| Assigned | assignedTo=username dan IN_REVIEW, ESCALATED, REOPENED |
| Reassignable | Active case statuses |
| Historical | Closed case statuses |
Queue ordering current adalah case_score DESC, created_date ASC. Case search umum menggunakan case_score DESC, created_date DESC, sehingga tie ordering keduanya berbeda. Alert count pada queue tidak menghitung duplicate shadow.
Historical alert provider memasukkan alert RESOLVED dan AUTO_NEGATIVE, sehingga AUTO_NEGATIVE tetap dapat muncul sebagai evidence historis walaupun tidak masuk managed case.
Group notification lebih dulu mengambil group members, membuang username null/blank, menghapus duplicate username menggunakan LinkedHashSet, menerapkan optional excluded username, lalu memanggil notify() untuk setiap penerima.
| Scenario | Alert result | Case result | Assignment/SLA | Next step |
|---|---|---|---|---|
| Same UTRN sudah ada | Tidak membuat alert baru | Tidak berubah | Tidak berubah | Stop |
| Dedup match aktif | Canonical count naik + shadow disimpan | Tidak ada assignment baru | Shadow locked/classified | Historical evidence |
| AUTO_NEGATIVE ON dan eligible | AUTO_NEGATIVE, NEGATIVE |
Tidak ada case | Tidak ada SLA | Historical evidence |
| AUTO_NEGATIVE toggle OFF | UNASSIGNED, SUSPICIOUS |
Masuk clustering/case | Mengikuti distribution | Investigation |
| Clustering key null | Managed alert | Standalone case | Optional auto-assign | Investigation |
| Active matching case unassigned | Alert join case | Count/score naik | Tetap unassigned | Queue/distribution existing tidak dipicu |
| Active matching case assigned | Alert IN_REVIEW |
Existing case | Inherit assignee + alert SLA | Investigation |
| Closed match |