Skip to main content

Merchant Registration — Reject & Revision Flow

AUDIT VERDICT (2026-06-05): Phase 1–5 SHIPPED end-to-end + deployed:

  • Phase 1 Schema: mig v1/066_merchant_registration_revision.sql applied (status revision_required + kolom revision_notes/revision_count/revision_phase).
  • Phase 2 Backend: handler RequestRevision di merchant_core_api/internal/merchant/merchant.go + endpoint /internal/merchant/registration/request-revision; FCM event merchant_status_revision_required registered di merchant_event_messages.go.
  • Phase 3 Dashboard: RejectRevisionDialog + status pill revision_required rendering live, proxy dashboard_api/internal/app/dashboard_merchant_registration.go route ke core-api.
  • Phase 4 Mobile (locked, eksplisit per-file approve): 7 helper di shared/merchant_status.dart + merchant_account_status_page.dart + push_notification_service.dart handle case revision_required identik dengan rejected.
  • Phase 5 SQL smoke test: 5/5 skenario PASS (2026-06-05), binary merchant_core_api deployed ke VM. UI smoke test manual via dashboard+mobile masih bisa dijalankan sebagai validasi akhir tapi tidak blocking.

6 keputusan log (NMID retention, cap 3 rounds, free-text notes, dst.) sudah ter-implementasi. Plan boleh diarsipkan ke docs/architecture/.

Konteks

Saat ini tombol "Reject" di dashboard merchant registration queue menjadi satu-satunya jalur penolakan, dengan beberapa keterbatasan yang merugikan operasional:

  1. Reject hanya valid di status pending/pending_review — setelah pending_acquirer atau acquirer_approved, admin tidak punya cara membatalkan/menolak registrasi tersebut tanpa intervensi SQL manual.
  2. Tidak ada pembedaan eksplisit antara "kirim balik untuk perbaikan" vs "tolak permanen" — model existing (mig 033) pakai counter implicit rejected_count vs max_submission_attempts yang membuat semantik admin tidak jelas saat menolak.
  3. NMID/MID/TID dari bank yang sudah issued pasca-acquirer_approved tidak ada policy retention saat user mau revisi — pertanyaan kritis: kalau user ganti foto KTP, apakah NMID lama tetap valid? Belum ada keputusan kode.

Plan ini menargetkan Opsi B dari diskusi 2026-06-02: tambah status enum revision_required eksplisit, ekspos dua jalur outcome saat reject (Send Back vs Final Reject), plus extend gating agar reject valid sampai status acquirer_approved.

Konteks lengkap diskusi & keputusan ada di transkrip sesi 2026-06-02.

Aturan lock: Perubahan di apps/mobile_user/ masuk lock policy (lihat apps/mobile_user/CLAUDE.md). Phase 4 plan ini menyentuh mobile — eksekusi tiap file menunggu approval per-file. Dashboard (apps/merchant_dashboard/) tidak terlock tapi tetap punya mirror obligation ke repo_exports/kesles/merchant/merchant-dashboard/.


Decision log

Rangkuman 6 keputusan policy yang sudah disepakati di diskusi:

1. revision_notes free-text dulu (decided 2026-06-02)

Field tunggal untuk catatan reviewer. Checklist per-field (fields_to_fix JSONB) dipertimbangkan di fase berikutnya kalau ada kebutuhan nyata.

2. Cap 3 round revisi (decided 2026-06-02)

Setelah revision_count >= 3, reviewer dipaksa pilih Approve atau Reject Final — tidak boleh "Kirim balik untuk revisi" lagi. Counter dipisah dari rejected_count existing (yang akan tetap dipertahankan untuk audit).

3. NMID retention: SELALU tetap pada revisi (decided 2026-06-02)

NMID/MID/TID dari bank selalu tetap saat user submit revisi, terlepas dari field apa yang berubah. Tidak ada whitelist critical/non-critical di code.

Rasional: device-level guard sudah menutup risiko transaksi salah-merchant. Transaksi tidak bisa masuk kecuali 4 syarat semua terpenuhi: (1) merchant active di merchant.merchants (2) sales_order device exist (3) device paired ke merchant (4) device status active. Selama salah satu gate tidak terlewati, NMID hanya idle metadata.

Logic implementasi saat user submit revisi:

  • Status sebelum revisi pending_review → balik pending_review setelah submit
  • Status sebelum revisi pending_acquirer → balik pending_acquirer
  • Status sebelum revisi acquirer_approved → balik acquirer_approved (NMID tetap, admin tinggal Final Approve)

Kolom audit revision_phase tetap dipertahankan (pre_bank / post_submit / post_bank_approval) untuk batch audit query nanti — kalau bank/regulator butuh laporan "revisi yang terjadi setelah NMID issued".

Eskalasi future: kalau ternyata di production ada compliance issue (bank Mandiri/BNI flag merchant), bisa upgrade ke Pendekatan C (tombol manual "Reset NMID" di dialog admin) tanpa breaking change schema — tinggal tambah field UI + handler.

4. Suspend / Block / Terminate diluar scope (decided 2026-06-02)

Operasi untuk merchant yang sudah active (suspend, block device, terminate) jadi epic terpisah. Bukan target plan ini.

5. FCM + Notification Center (decided 2026-06-02)

Event type baru merchant_status_revision_required ditambah ke event registry merchant_event_messages.go. Mobile push notification + entry di notification center mengikuti pola FCM existing — tidak butuh infrastruktur baru.

6. Satu tombol Reject di dashboard → dialog 2-pilihan (decided 2026-06-02)

Wording final: "Kirim Balik untuk revisi" vs "Tolak Final".

Counter display: info read-only (Opsi A). Render text "Sudah di-revisi: X dari 3 round" di header dialog supaya admin sadar konteks. Cap 3 round hard-coded — tidak ada admin override per-registrasi. Setelah revision_count >= 3, opsi "Kirim Balik untuk revisi" disabled, admin wajib pilih Approve atau Tolak Final.

Rasional pilih Opsi A: konsistensi policy antar reviewer. Admin tidak bisa naik/turunkan cap individual — mencegah inkonsistensi treatment antar merchant. Kalau ternyata butuh fleksibilitas (mis. kasus borderline), upgrade ke Opsi B/C bisa dilakukan future dengan tambah kolom revision_max_rounds INT NOT NULL DEFAULT 3 tanpa breaking change.

7. Reject pasca-acquirer_approved BOLEH (decided 2026-06-02)

Admin boleh menolak/membatalkan registrasi setelah bank balas approve (NMID issued). Tidak ada compliance issue dari sisi Kesles karena transaksi tidak bisa masuk sebelum Final Approve + device aktif. Tambah field audit rejection_reason_post_acquirer TEXT di Phase 1 schema untuk catat alasan eksplisit kalau reject terjadi di fase ini — supaya kalau auditor regulator tanya "kenapa NMID issued tapi tidak aktif di Kesles", ada catatan.


Existing state audit (verified 2026-06-02)

Pondasi yang sudah live dan akan dimanfaatkan plan ini:

Schema (db_kesles_merchant, schema merchant)

KomponenLokasiStatus
Status enum constraintchk_merchant_registration_requests_status('draft', 'pending', 'pending_review', 'pending_acquirer', 'acquirer_approved', 'approved', 'rejected', 'cancelled') — TANPA revision_required
Acquirer ID format constraintschk_acquirer_{nmid,mid,tid}_format, chk_acquirer_ids_completeNMID = ID + 13 digit, MID = 15 digit, TID = 8 digit. Saat acquirer_response_status='approved' → ketiga NMID/MID/TID wajib non-empty. Implikasi reset NMID: harus juga reset acquirer_response_status ke NULL agar constraint tidak fail.
Counter audit trailmig 033 migrations/v1/033_merchant_registration_audit_trail.sqlsubmission_count, rejected_count, max_submission_attempts=3, submitted_at, last_resubmitted_at, review_started_at, approved_at, rejected_at
Event logmerchant.merchant_registration_events (mig 033)5 event types: submitted, resubmitted, review_started, approved, rejected. Plan ini tambah 2: revision_requested, revision_submitted
Last migrationmerchant_database/db_kesles_merchant/migrations/v1/065_*.sqlPlan ini mulai dari mig 066

Backend server (merchant_core_api)

KomponenLokasiStatus
Resubmit logicmerchant.go:540-577Re-use row rejected → status pending, counter+1, cap 3 (existing max_submission_attempts)
Audit event resubmittedmerchant.go:947-952Logged tiap user submit ulang
Reject handlerdashboard_merchant_registration.go:572-735POST /v1/dashboard/merchant/registration/{id}/reject. SQL gating: WHERE status IN ('pending', 'pending_review'). Perlu extend untuk plan ini.
Approve handlerinternal_merchant_handlers.go:307POST /internal/merchant/registration/approve — mint MRC code, INSERT ke merchants, hapus row registrasi
Sub-routes inventorydashboard_merchant_registration.go:50-110review, save-review, submit-acquirer, acquirer-response, approve, reject, address-update
Internal routesroutes_internal.go:12-16/internal/merchant/registration/{review,submit-acquirer,acquirer-response,approve,manual}. Plan ini tambah /request-revision.

FCM event registry

KomponenLokasiStatus
Event types registeredmerchant_event_messages.gomerchant_status_pending_review, merchant_status_active, merchant_status_rejected. Plan ini tambah merchant_status_revision_required.
Emitter di reject pathinternal_merchant_handlers.go:287merchant_status_rejected emit saat status=rejected. Plan ini tambah emit untuk revision_required.

Dashboard UI

KomponenLokasiStatus
Reject button + gatingdashboard_sections.dart:3483-3490_canDecide gating: status mengandung review + progress 100%
Reject performerdashboard_sections.dart:3718_performReject → prompt textarea notes → API call
API calldashboard_home_api.dart:2951rejectMerchantRegistration POST
Status pilldashboard_sections.dart:3527+Render label berdasar status. Plan ini tambah label "Revisi"

Mobile (apps/mobile_user)

KomponenLokasiStatus
Status helpersshared/merchant_status.dartSudah handle rejected dengan label "Perbaikan", banner, progress, headline. Plan ini tambah handler untuk revision_required (label & UX bisa identik dengan rejected sebagai jembatan UX)
Session fieldauth_session_service.dart:1138-1143Persist merchantLastResubmittedAt, merchantSubmissionCount, merchantRejectedCount, merchantMaxSubmissionAttempts. Plan ini tambah merchantRevisionNotes, merchantRevisionCount.
Profile API serviceprofile_api_service.dart:219-282Parse counter fields dari response. Plan ini tambah revision fields.
Account status pagemerchant_account_status_page.dartSudah render banner "rejected" dengan rejected_count display. Plan ini extend untuk revision_required.
Registration pagemerchant_registration_page.dart:173Pre-fill data dari MRR existing row saat flow "Perbaikan". Plan ini reuse untuk revision flow.
FCM handlerpush_notification_service.dart:223-239_isMerchantStatusOrOrderEvent + _inferMerchantStatusFromType. Plan ini tambah case merchant_status_revision_required.

KYC parallel (informational, bukan modify target)

KomponenLokasiStatus
KYC submissionskyc.submissions (schema kyc, mig 025)Punya status needs_more_docs + lifecycle under_review → needs_more_docs → submitted yang persis identik dengan revision_required. Linkage ke registration via kyc.submissions.source_registration_id. Plan ini tidak menyentuh kyc — pattern di-borrow saja sebagai referensi UX.

Gap analysis (apa yang BELUM ada vs target)

#GapPhase yang menangani
1Status enum revision_required belum ada di check constraintPhase 1 (schema)
2Kolom revision_notes, revision_requested_at, revision_requested_by_user_id, revision_count, revision_phase belum adaPhase 1 (schema)
3Event log revision_requested, revision_submitted belum ada di constraint event_typePhase 1 (schema)
4Endpoint POST /internal/merchant/registration/{id}/request-revision belum adaPhase 2 (server)
5Endpoint dashboard POST /v1/dashboard/merchant/registration/{id}/request-revision belum adaPhase 2 (server)
6Reject handler gating sempit (pending/pending_review saja) — perlu extend untuk pending_acquirer, acquirer_approvedPhase 2 (server)
7NMID retention logic saat user submit revisi pasca-acquirer_approved (whitelist field critical) belum adaPhase 2 (server)
8FCM event merchant_status_revision_required belum registerPhase 2 (server)
9Dashboard UI: tombol Reject masih single prompt textarea, belum dialog 2-pilihanPhase 3 (dashboard)
10Dashboard UI: gating button Reject masih _canDecide sempit (cuma pending_review 100%) — perlu extendPhase 3 (dashboard)
11Dashboard UI: status pill untuk revision_required belum ada (label, warna)Phase 3 (dashboard)
12Mobile UI: helper merchantStatusLabel/Headline/Description/StoreTitle/StoreSubtitle untuk revision_required belum adaPhase 4 (mobile, locked)
13Mobile session: field merchantRevisionNotes, merchantRevisionCount belum adaPhase 4 (mobile, locked)
14Mobile FCM handler merchant_status_revision_required belum adaPhase 4 (mobile, locked)
15Mobile flow: banner home + handle redirect ke registration page mode revision belum ada (eksplisit)Phase 4 (mobile, locked)
16E2E test scenario belum adaPhase 5 (test)

Phase plan

Setiap phase didahului audit pre-flight (read-only) yang memvalidasi state codebase di saat phase dieksekusi, bukan asumsi dari hari plan ditulis. Phase 4 (mobile, locked) didetailkan per-file dengan diskusi-detail wajib sebelum edit apa pun.

Phase 1 — Schema migration ✅ DONE 2026-06-02

Tujuan: tambah status enum, kolom audit, event types. Foundation untuk Phase 2-4.

Hasil eksekusi:

  • File migrations/v1/066_merchant_registration_revision.sql applied ke db_kesles_merchant (host 10.8.0.1, dev).
  • 6 kolom baru ditambah ke merchant.merchant_registration_requests.
  • Status enum constraint extended dengan revision_required.
  • Event type constraint extended dengan revision_requested + revision_submitted.
  • Index ix_mrr_revision_requested_at created.
  • Smoke test INSERT + event log + cleanup PASS.

1.1 Audit pre-flight

  • Konfirmasi nomor migration terakhir di merchant_database/db_kesles_merchant/migrations/v1/. Plan ini reserve mig 066.
  • Cek apakah ada migration in-flight yang belum di-apply (cek tabel schema_migrations kalau ada, atau git log).
  • Cek snapshot constraint chk_merchant_registration_requests_status saat ini, simpan ke audit doc untuk rollback reference.
  • Cek apakah ada row merchant_registration_requests yang status-nya non-standard atau corrupt (kalau ada, perlu cleanup sebelum constraint baru).
  • Inventaris konsumen field rejected_count (grep di merchant_core_api, services/dashboard_api, apps/merchant_dashboard, apps/mobile_user) — pastikan tidak ada yang break kalau counter dipertahankan tapi semantik bergeser.

1.2 Design

File baru: merchant_database/db_kesles_merchant/migrations/v1/066_merchant_registration_revision.sql

Konten:

-- 066_merchant_registration_revision.sql
--
-- Tambah kolom revision_* dan status enum 'revision_required' untuk
-- explicit Send-Back-for-Revision flow (lihat
-- docs/architecture/merchant-reject-revision-flow-plan.md).

-- ──────────────────────────────────────────────────────────────────
-- 1. Tambah kolom revision_*
-- ──────────────────────────────────────────────────────────────────

ALTER TABLE merchant.merchant_registration_requests
ADD COLUMN IF NOT EXISTS revision_notes TEXT,
ADD COLUMN IF NOT EXISTS revision_requested_at TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS revision_requested_by_user_id UUID,
ADD COLUMN IF NOT EXISTS revision_count INT NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS revision_phase VARCHAR(32),
ADD COLUMN IF NOT EXISTS rejection_reason_post_acquirer TEXT;

COMMENT ON COLUMN merchant.merchant_registration_requests.revision_notes IS
'Catatan reviewer saat request revision; ditampilkan ke user di mobile sebagai instruksi perbaikan.';
COMMENT ON COLUMN merchant.merchant_registration_requests.revision_count IS
'Counter berapa kali sudah di-request-revision oleh reviewer. Cap hard-coded = 3.';
COMMENT ON COLUMN merchant.merchant_registration_requests.revision_phase IS
'Fase registrasi saat revision di-request: pre_bank (dari pending_review) | post_submit (dari pending_acquirer) | post_bank_approval (dari acquirer_approved). Audit only — tidak mempengaruhi logic NMID retention.';
COMMENT ON COLUMN merchant.merchant_registration_requests.rejection_reason_post_acquirer IS
'Catatan eksplisit saat admin reject setelah bank balas approve (NMID issued). Untuk audit kalau regulator/bank tanya kenapa NMID issued tidak aktif di Kesles.';

CREATE INDEX IF NOT EXISTS ix_mrr_revision_requested_at
ON merchant.merchant_registration_requests (revision_requested_at DESC)
WHERE revision_requested_at IS NOT NULL;

-- ──────────────────────────────────────────────────────────────────
-- 2. Update status check constraint — tambah 'revision_required'
-- ──────────────────────────────────────────────────────────────────

ALTER TABLE merchant.merchant_registration_requests
DROP CONSTRAINT IF EXISTS chk_merchant_registration_requests_status;

ALTER TABLE merchant.merchant_registration_requests
ADD CONSTRAINT chk_merchant_registration_requests_status CHECK (
status::text = ANY (ARRAY[
'draft', 'pending', 'pending_review',
'pending_acquirer', 'acquirer_approved',
'revision_required',
'approved', 'rejected', 'cancelled'
]::text[])
);

-- ──────────────────────────────────────────────────────────────────
-- 3. Update event log check constraint — tambah 2 event types
-- ──────────────────────────────────────────────────────────────────

ALTER TABLE merchant.merchant_registration_events
DROP CONSTRAINT IF EXISTS chk_registration_event_type;

ALTER TABLE merchant.merchant_registration_events
ADD CONSTRAINT chk_registration_event_type CHECK (
event_type IN (
'submitted', 'resubmitted',
'review_started',
'approved', 'rejected',
'revision_requested', 'revision_submitted'
)
);

1.3 Implementation

  1. Tulis file 066_merchant_registration_revision.sql di merchant_database/db_kesles_merchant/migrations/v1/.
  2. Sinkron ke mirror repo_exports/kesles/merchant/database/db_kesles_merchant/migrations/v1/ (kalau folder ini di-mirror).
  3. Apply ke DB dev via psql.
  4. Re-verify constraint dengan query pg_get_constraintdef.

1.4 Verification

  • pg_get_constraintdef('chk_merchant_registration_requests_status') mengandung 'revision_required'.
  • pg_get_constraintdef('chk_registration_event_type') mengandung 'revision_requested' dan 'revision_submitted'.
  • \d+ merchant.merchant_registration_requests menampilkan 5 kolom baru.
  • Index ix_mrr_revision_requested_at ada.
  • Smoke INSERT row dengan status revision_required + revision_count=0 tidak error.
  • Smoke INSERT event revision_requested tidak error.

1.5 Risiko & rollback

  • Risiko: ada row dengan status non-standard yang melanggar constraint baru saat DROP+RECREATE. Mitigasi: query audit row sebelum apply.
  • Rollback: ALTER TABLE ... DROP COLUMN revision_* + restore constraint lama via DDL backup. Risiko rendah karena migration additive.

Phase 2 — Backend server (merchant_core_api + dashboard_api) ✅ DONE 2026-06-02

Tujuan: tambah endpoint request-revision, extend reject gating, register FCM event baru.

Hasil eksekusi:

  • 6 file dimodifikasi: server.go, merchant.go, internal_merchant_handlers.go, routes_internal.go, merchant_event_messages.go, dashboard_merchant_registration.go.
  • Method RequestRevision di merchant.go — derive revision_phase dari status, cap 3 round, simpan ke revision_count, log event revision_requested.
  • Resubmit flow di-extend: predecessor revision_required di-handle (target status derive dari revision_phase), event log revision_submitted.
  • Endpoint internal POST /internal/merchant/registration/request-revision di routes_internal.go.
  • Endpoint dashboard POST /v1/dashboard/merchant/registration/{id}/request-revision di dashboard_merchant_registration.go.
  • FCM event merchant_status_revision_required di merchant_event_messages.go.
  • Reject SQL gating extended ke pending_acquirer, acquirer_approved, revision_required. Field rejection_reason_post_acquirer ditangkap dari body.
  • go build + go vet keduanya bersih untuk core_api + dashboard_api.
  • Smoke test SQL: RequestRevision flow + extended reject pasca-acquirer_approved keduanya PASS.

2.1 Audit pre-flight

  • Baca ulang merchant.go:540-577 — pastikan logic resubmit existing tidak konflik dengan flow baru.
  • Konfirmasi format payload sub-route dispatcher di dashboard_merchant_registration.go:50-110.
  • Verifikasi internal-key auth flow di routes_internal.go.
  • Inventaris field handler update merchant_registration_requests (pastikan field revision_* baru di-handle di SELECT/UPDATE).
  • Cek pattern FCM emit di internal_merchant_handlers.go:287 sebagai template.
  • Audit konsumen merchant_status_rejected di mobile sambil cari titik di mana merchant_status_revision_required perlu di-handle (saat Phase 4).

2.2 Design

Server: merchant_core_api/internal/merchant/merchant.go — tambah method:

// RequestRevision menset status registrasi ke 'revision_required',
// menyimpan catatan reviewer, dan mencatat event audit. Dipanggil
// oleh handler dashboard saat reviewer pilih "Kirim Balik untuk
// revisi" di dialog 2-pilihan.
//
// Cap: revision_count >= 3 → return error, handler wajib pilih
// Approve atau Tolak Final.
//
// Phase logic: revision_phase di-set berdasar status sebelumnya.
// pending_review → pre_bank
// pending_acquirer → post_submit
// acquirer_approved → post_bank_approval
//
// status_pre_revision disimpan di metadata event log supaya
// ResubmitFromRevision tahu harus kembalikan ke status apa.
func (s *Store) RequestRevision(ctx context.Context, input RequestRevisionInput) (Record, error)
// ResubmitFromRevision dipanggil dari endpoint mobile saat user submit
// data setelah dapat permintaan revisi. NMID/MID/TID SELALU TETAP —
// tidak ada whitelist critical/non-critical (per decision log #3).
// Status balik ke status_pre_revision yang disimpan saat
// RequestRevision dipanggil:
// - pre_bank → pending_review
// - post_submit → pending_acquirer
// - post_bank_approval → acquirer_approved (NMID tetap, admin tinggal Final Approve)
func (s *Store) ResubmitFromRevision(ctx context.Context, input ResubmitFromRevisionInput) (Record, error)

Catatan: Pendekatan A (NMID selalu tetap) dipilih per decision #3. Method ResubmitFromRevision tidak butuh helper whitelist atau diff comparator — lebih simpel dari proposal awal.

Server: merchant_core_api/internal/httpapi/internal_merchant_handlers.go — tambah:

// POST /internal/merchant/registration/request-revision
func (s *Server) handleRequestRevisionMerchantRegistration(w http.ResponseWriter, r *http.Request) { /* ... */ }

Server: route registration routes_internal.go:

s.mux.HandleFunc("/internal/merchant/registration/request-revision",
s.handleRequestRevisionMerchantRegistration)

Dashboard API: services/dashboard_api/internal/app/dashboard_merchant_registration.go — tambah:

// handleMerchantRegistrationRequestRevision forwards ke core-api
// internal endpoint /internal/merchant/registration/request-revision.
func (s *Server) handleMerchantRegistrationRequestRevision(w http.ResponseWriter, r *http.Request, id string) { /* ... */ }

Plus tambah case "request-revision": di sub-route dispatcher di file yang sama.

Reject handler extend: ubah SQL gating di handleMerchantRegistrationReject:

-- SEBELUM:
WHERE id = $1::uuid
AND deleted_at IS NULL
AND status IN ('pending', 'pending_review')

-- SESUDAH:
WHERE id = $1::uuid
AND deleted_at IS NULL
AND status IN (
'pending', 'pending_review',
'pending_acquirer', 'acquirer_approved',
'revision_required' -- user mungkin sedang revisi → bisa di-final-reject langsung
)

FCM event registry: tambah di merchant_event_messages.go:

case "merchant_status_revision_required":
return MerchantEventMessage{
Title: "Pengajuan perlu perbaikan",
Body: "Reviewer Kesles meminta perbaikan data registrasi Anda. Buka aplikasi untuk lihat detail.",
}

Plus emit dari handleRequestRevisionMerchantRegistration pakai pattern existing fireMerchantEventAsync.

2.3 Implementation

  1. Tulis service methods RequestRevision + ResubmitFromRevision + helper hasCriticalFieldChange di merchant.go.
  2. Tulis handler handleRequestRevisionMerchantRegistration di internal_merchant_handlers.go.
  3. Register route di routes_internal.go.
  4. Tulis dashboard proxy handler + case sub-route di dashboard_merchant_registration.go.
  5. Extend SQL gating di handleMerchantRegistrationReject SAME file.
  6. Tambah FCM event message + emit.
  7. go build + go vet semua harus pass.
  8. Unit test untuk hasCriticalFieldChange minimum (10+ skenario).
  9. Smoke test endpoint pakai curl.

2.4 Verification

  • curl POST /v1/dashboard/merchant/registration/{id}/request-revision dengan body {"notes":"foto buram"} → 200, status DB jadi revision_required, counter+1, event tercatat dengan status_pre_revision di metadata.
  • Repeat 3× → call ke-4 reject 409 "revision limit reached, please approve or final-reject".
  • curl POST /v1/dashboard/merchant/registration/{id}/reject saat status pending_acquirer → 200 (sebelum patch: 409). Sertakan rejection_reason_post_acquirer di body.
  • Resubmit dari mobile saat status revision_required pasca-acquirer_approved (ubah field apa pun) → NMID/MID/TID tetap, status balik acquirer_approved.
  • Resubmit saat status revision_required pasca-pending_review → status balik pending_review.
  • FCM event merchant_status_revision_required ter-dispatch (cek log atau monitor mobile end).

2.5 Risiko & rollback

  • Risiko utama: NMID retention policy belum konfirmasi acquirer real. Mitigasi: deploy dulu di dev/staging dengan whitelist konservatif (default), konfirmasi acquirer sebelum cutover production.
  • Risiko: reject pasca-acquirer_approved mungkin punya implikasi compliance/banking yang belum dipikirkan. Saran: diskusikan dengan stakeholder bank dulu sebelum implementasi.
  • Rollback: revert handler + route registration. Schema kolom revision_* sudah ada di Phase 1 tapi tidak digunakan — no data loss.

Phase 3 — Dashboard UI (apps/merchant_dashboard) ✅ DONE 2026-06-02

Tujuan: tombol Reject di queue → buka dialog 2-pilihan. Tambah status pill revision_required.

Hasil eksekusi (Phase 3.1, setelah Phase 3.0 data layer extension):

7 file dimodifikasi/dibuat di dashboard:

  1. dashboard_home_api.dart — extend rejectMerchantRegistration dengan rejectionReasonPostAcquirer + add new requestRevisionMerchantRegistration.
  2. home_remote_data_source.dart — passthrough kedua method.
  3. home_repository_contract.dart — abstract methods.
  4. home_repository.dart — impl methods.
  5. home_dashboard_usecase.dart — facade methods.
  6. NEW reject_revision_dialog.dartRejectRevisionDialog widget pakai DashboardFormDialogShell: 2 radio button (Kirim Balik untuk revisi vs Tolak Final) dengan RadioGroup<RejectRevisionOutcome> ancestor pattern (Flutter 3.32+), counter "Sudah X dari 3 round" di header, textarea catatan (min 5 char wajib), auto-disable "Kirim Balik" saat revisionCount >= 3, dynamic confirm button color (kuning untuk revisi, merah untuk reject final).
  7. dashboard_sections.dart:
    • Import dialog baru
    • Ganti _canDecide_canRejectOrRevise (extend gating untuk pending_acquirer/acquirer_approved/revision_required)
    • Modify _performReject — buka dialog dulu, route ke requestRevisionMerchantRegistration atau rejectMerchantRegistration sesuai outcome. Pasca-acquirer rejection: pass rejectionReasonPostAcquirer untuk audit.
    • Tambah branch status pill revision_required (warna kuning, label "Revisi · X/3", icon edit_note)
    • Update tooltip Reject button
    • Hapus _promptRejectNotes + kelas _RejectRegistrationDialog (sudah digantikan dialog baru).

Verifikasi:

  • flutter analyze bersih untuk semua file Phase 3 yang disentuh
  • flutter analyze lib total 12 issues — semua di file lain (jadwal_kirim_promo_panel, broadcast_pengumuman_panel), tidak terkait Phase 3.

Phase 3 — Dashboard UI ORIGINAL SPEC (kept for reference)

Tujuan: tombol Reject di queue → buka dialog 2-pilihan. Tambah status pill revision_required. Counter dashboard.

3.1 Audit pre-flight

  • Baca ulang gating _canDecide di dashboard_sections.dart:3598 — cari pattern guard yang harus extend.
  • Inventaris widget shell yang sudah ada untuk dialog (per feedback_dashboard_dialog_standard: DashboardFormDialogShell/DashboardDialogShell). Tidak boleh AlertDialog mentah.
  • Cek API service method rejectMerchantRegistration di dashboard_home_api.dart:2951 sebagai template untuk requestRevisionMerchantRegistration.
  • Verifikasi _kPendingMerchantActionsWidth = 200 di dashboard_sections.dart:2648 — pastikan tidak overflow setelah perubahan (jumlah tombol tetap, hanya behavior).
  • Konfirmasi mirror obligation: ubahan harus disinkronkan ke repo_exports/kesles/merchant/merchant-dashboard/.

3.2 Design

Dialog 2-pilihan — buat file baru: apps/merchant_dashboard/lib/dashboard/features/home/presentation/widgets/panels/reject_revision_dialog.dart

class RejectRevisionDialogResult {
final RejectRevisionOutcome outcome; // sendBackForRevision | rejectFinal
final String notes;
}

class RejectRevisionDialog extends StatefulWidget {
final PendingMerchantItem item;
final int revisionCount; // dari item.revisionCount
final int revisionMaxRounds; // hardcoded 3 dulu

// ...
}

Body dialog:

┌───────────────────────────────────────┐
│ Tolak / Kirim balik registrasi │
├───────────────────────────────────────┤
│ Status: pending_review │
│ Sudah di-revisi: 1 dari 3 round │
├───────────────────────────────────────┤
│ Pilih outcome: │
│ ○ Kirim balik untuk revisi │
│ (user bisa edit data & submit ulang)│
│ │
│ ○ Tolak final │
│ (user harus daftar dari nol) │
├───────────────────────────────────────┤
│ Catatan untuk user (wajib): │
│ ┌───────────────────────────────────┐ │
│ │ Foto KTP buram, tolong upload │ │
│ │ ulang dengan pencahayaan jelas. │ │
│ └───────────────────────────────────┘ │
│ │
│ [Batal] [Kirim] │
└───────────────────────────────────────┘

Gating logic baru — modify _canDecide di dashboard_sections.dart:

// SEBELUM:
static bool _canDecide(PendingMerchantItem item) {
if (!item.status.toLowerCase().contains('review')) return false;
return item.reviewProgress >= 100;
}

// SESUDAH:
static bool _canRejectOrRevise(PendingMerchantItem item) {
final s = _statusKey(item);
if (s == 'pending_review') {
return item.reviewProgress >= 100; // unchanged
}
if (s == 'pending_acquirer' || s == 'acquirer_approved' || s == 'revision_required') {
return true; // boleh reject/revise tanpa progress check
}
return false;
}

Status pill — tambah branch untuk revision_required di _statusPill:

} else if (normalized == 'revision_required') {
color = const Color(0xFFE08A1C); // kuning warning
label = t('Revision', 'Revisi');
icon = Icons.edit_note_rounded;
}

API call — tambah method di dashboard_home_api.dart:

Future<Map<String, dynamic>> requestRevisionMerchantRegistration({
required String accessToken,
required String id,
required String notes,
}) async { /* POST /dashboard/merchant/registration/$id/request-revision */ }

Counter dashboard — di summary stats, tambah card "Revisi Diminta" dengan count WHERE status='revision_required'.

3.3 Implementation

  1. Buat file reject_revision_dialog.dart dengan widget shell DashboardFormDialogShell.
  2. Modify _canDecide → split jadi _canRejectOrRevise di dashboard_sections.dart.
  3. Modify _performReject → buka dialog dulu, lalu route ke API method sesuai outcome.
  4. Tambah _statusPill branch revision_required.
  5. Tambah API method requestRevisionMerchantRegistration di dashboard_home_api.dart.
  6. Tambah counter card di summary stats.
  7. Mirror semua perubahan ke repo_exports/kesles/merchant/merchant-dashboard/.
  8. flutter analyze harus bersih.

3.4 Verification

  • Klik Reject di row pending_review 100% → dialog muncul dengan 2 opsi.
  • Pilih "Kirim balik untuk revisi" + catatan → submit → status DB jadi revision_required.
  • Pilih "Tolak final" + catatan → status DB jadi rejected.
  • Row dengan revision_count=3 → opsi "Kirim balik" disabled di dialog.
  • Row pending_acquirer → tombol Reject aktif (sebelumnya disabled).
  • Status pill untuk row revision_required render dengan label "Revisi" warna kuning.
  • Counter dashboard "Revisi Diminta" akurat.
  • Mirror identik antara apps/ dan repo_exports/.

3.5 Risiko & rollback

  • Risiko: dialog tidak terbuka di production karena widget shell version mismatch. Mitigasi: test di canary user dulu.
  • Risiko: counter revision_count di payload UI belum di-expose oleh backend (perlu cek Phase 2 response payload).
  • Rollback: feature flag dashboard.reject_dialog_v2 (kalau infra mendukung); fallback ke single-prompt textarea lama.

Phase 4 — Mobile UI ✅ DONE 2026-06-02

Hasil eksekusi (Phase 4.0 server-side + 4.1 mobile per-file approval):

Phase 4.0 — server profile/store.go (1 file, no lock):

  • Pattern A (single-line whitelist): 33 subquery extend 'revision_required'
  • Pattern B (multi-line whitelist): 8 subquery extend 'revision_required'
  • Pattern C (review_notes + review_payload subquery): extend WHERE status IN ('rejected', 'revision_required') + CASE WHEN per status untuk pilih kolom yang tepat (review_notes vs revision_notes)
  • Tidak tambah field response barumerchant_review_notes + merchant_review_payload cover kedua status (status-based source mapping di server)

Phase 4.1 — mobile (3 file, LOCKED — diskusi per-file):

  • shared/merchant_status.dart: 7 helper extend case 'revision_required': paralel dengan 'rejected'
  • merchant_account_status_page.dart: _viewFor() extend case 'revision_required': dengan body identik 'rejected'
  • push_notification_service.dart: _inferMerchantStatusFromType + _isMerchantStatusOrOrderEvent extend FCM event merchant_status_revision_required

Yang TIDAK diubah (per instruksi user "pakai yang sudah ada"):

  • Widget _RejectionWarningCard — reuse 100%
  • _StatusVariant enum — tidak tambah variant baru
  • StoredProfile + session service — tidak tambah field baru
  • profile_api_service.dart — tidak tambah parsing
  • home_merchant_activation_banner.dart — render via helper di file 1, tidak disentuh

Hasil yang user lihat di mobile saat status revision_required: persis sama dengan status rejected (hero ilustrasi, banner home, status page, kartu warning merah, checklist + notes + counter, tombol "Lihat Detail" + "Perbaiki Data"). Tidak ada visual baru.

Verifikasi:

  • go build bersih untuk core_api
  • flutter analyze bersih untuk 3 file mobile yang disentuh
  • ✓ FCM event chain end-to-end terkoneksi (server emit Phase 2 → mobile handle Phase 4.1)

Mirror repo_exports/.../mobile-user/ SKIPPED — folder tidak ada di session ini (sama dengan Phase 3 dashboard).


Phase 4 — Mobile UI ORIGINAL SPEC (kept for reference)

Tujuan: handle status revision_required di mobile_user app — banner, label, navigasi ke registration page mode revisi.

Lock policy: setiap file di apps/mobile_user/ butuh diskusi-detail dulu sebelum edit. Phase ini tidak boleh dimulai sebelum Phase 1+2 done dan user explicit approval.

4.1 Audit pre-flight

  • Re-verify isi terkini shared/merchant_status.dart — kemungkinan ada perubahan antara plan ditulis vs eksekusi.
  • Verify auth_session_service.dart — pattern untuk tambah field session baru.
  • Verify profile_api_service.dart — pattern parse field baru dari response.
  • Audit merchant_account_status_page.dart — pastikan pattern banner status existing bisa di-extend tanpa breaking.
  • Audit push_notification_service.dart — confirm _inferMerchantStatusFromType switch case shape.
  • Cek apakah ada test (unit/widget) yang break — biasanya minim test di mobile_user.

4.2 Design — file by file

Setiap file di section ini akan didiskusikan terpisah dengan user sebelum edit. Sketsa di sini = proposal, bukan commitment.

File 1: shared/merchant_status.dart Tambah 'revision_required' ke _pendingStatuses? Tidak — revision_required bukan "pending" semantically (user perlu action, bukan tunggu). Buat handling terpisah:

bool isMerchantRevisionRequiredStatus(String status) {
return normalizeMerchantStatus(status) == 'revision_required';
}

// merchantStatusLabel:
case 'revision_required':
return 'Perbaikan'; // reuse label existing (UX continuity)

// merchantActionButtonLabel:
if (normalized == 'revision_required') {
return 'Perbaikan';
}

// merchantActivationProgress:
case 'revision_required':
return 0.4; // 40% — sudah submit dulu, perlu revisi, baseline dari draft

// merchantActivationHeadline:
case 'revision_required':
return 'Pengajuan perlu diperbaiki';

// merchantActivationDescription:
case 'revision_required':
return 'Reviewer meminta perbaikan data registrasi Anda. Periksa catatan, perbaiki, lalu submit ulang.';

File 2: auth_session_service.dart

Tambah field di StoredProfile:

final String? merchantRevisionNotes;
final int? merchantRevisionCount;
final String? merchantRevisionPhase;

Plus parse di fromJson + serialize di toJson.

File 3: profile_api_service.dart

Parse 3 field dari response server di _buildResponse (atau equivalent).

File 4: merchant_account_status_page.dart

Tambah case render banner untuk revision_required:

  • Variant _StatusVariant.revision_required (atau reuse rejected)
  • Show revision_notes sebagai pesan kuning di atas
  • Tombol "Perbaikan" → navigate ke MerchantRegistrationPage dengan pre-fill mode

File 5: merchant_registration_page.dart

Sudah handle pre-fill flow (line 173). Kemungkinan minimal change — extend untuk pickup data dari revision_required row sama seperti rejected row.

File 6: push_notification_service.dart

case 'merchant_status_revision_required':
return 'revision_required';

Di _inferMerchantStatusFromType. Plus tambah ke _isMerchantStatusOrOrderEvent daftar.

File 7-8: mungkin perlu touch home banner widget + notification_copy untuk localized text. Audit pre-flight akan klarifikasi.

4.3 Implementation

Tahapan WAJIB per-file:

  1. Pre-edit: kirim "saya mau edit file X, ini diff yang saya rencana, alasannya Y. Risiko regresi Z. OK?"
  2. User approval → edit dengan Edit tool.
  3. Mirror ke repo_exports/kesles/merchant/mobile-user/<path>.
  4. Confirm flutter analyze bersih.

4.4 Verification

  • FCM merchant_status_revision_required arrive → notif center entry muncul.
  • Home banner berubah ke "Pengajuan perlu diperbaiki" dengan tombol "Perbaikan".
  • Tap tombol Perbaikan → form registrasi terbuka dengan data pre-filled + banner catatan reviewer.
  • Submit revisi → status server berubah → mobile sync via profileRefreshTick.
  • Backward compat: status rejected lama tetap render seperti sebelum (pattern handler old preserved).
  • Mirror byte-identical antara apps/mobile_user/ dan repo_exports/.../mobile-user/.

4.5 Risiko & rollback

  • Risiko: locked folder berarti rollback per-file lebih lambat (perlu diskusi balik). Mitigasi: tiap edit kecil, tidak monolith.
  • Risiko: localization arb file tidak terupdate → string keluar default English. Mitigasi: check app_en.arb/app_id.arb.
  • Rollback: revert per file commit-by-commit. Mirror sync wajib.

Phase 5 — End-to-end test ✅ DONE 2026-06-05 (SQL smoke 5/5 PASS, binary deployed)

Tujuan: validasi lima skenario E2E pre-cutover production.

Status saat ini: SQL smoke test 5/5 PASS (dibungkus BEGIN/ROLLBACK, tidak meninggalkan state di DB dev). Binary merchant_core_api Phase 1-4 sudah deployed ke VM oleh user 2026-06-05. UI smoke test manual via dashboard+mobile masih disarankan sebagai validasi akhir tapi tidak blocking.

5.0 Hasil SQL smoke test (2026-06-05)

#SkenarioStatusVerifikasi
1Revisi pre-bank pertamaPASSstatus: pending_review → revision_required → pending_review, counter 0→1, revision_phase=pre_bank, event chain revision_requested → revision_submitted
2Revisi pasca-acquirer_approvedPASSNMID/MID/TID retained, status balik ke acquirer_approved, revision_phase=post_bank_approval
3Revisi dengan field identitasPASSPendekatan A confirmed — NMID retention konsisten regardless field
4Cap 3 roundPASSGuard revision_count >= 3 aktif — server tolak sebelum UPDATE
5Reject Final + pasca-acquirerPASSrejected = terminal; rejection_reason_post_acquirer terisi terpisah, NMID dipertahankan utk audit

Pra-syarat sebelum jalan UI smoke test manual (optional):

  1. VPN ke dev DB (10.8.0.1:5432) aktif
  2. Binary merchant_core_api di VM ptikn3-vm deployed dengan source Phase 1-4 — DONE 2026-06-05
  3. Dashboard build + deploy
  4. Mobile app build + install ke device test

Instruksi test manual lihat sub-section 5.2 di bawah.

5.1 Audit pre-flight

  • State DB dev: clean slate (sudah dilakukan 2026-06-02), 0 merchants, 25 users.
  • Backend service running + mobile app build terbaru dengan Phase 1-4 changes.
  • Test data: minimum 3 user di iam.users (sudah ada 25), 7 device stok gudang (sudah ada).

5.2 Skenario test

Skenario 1: Revisi sederhana (pre-bank)

  1. User register dari mobile → status pending.
  2. Admin start review → submit checklist 100% → status pending_review.
  3. Admin klik Reject → dialog → pilih "Kirim balik untuk revisi" + catatan "Foto KTP buram" → submit.
  4. DB: status='revision_required', revision_count=1, revision_notes='Foto KTP buram', revision_phase='pre_bank'.
  5. FCM merchant_status_revision_required ter-dispatch ke user.
  6. User di mobile lihat banner "Pengajuan perlu diperbaiki" + catatan.
  7. User tap "Perbaikan" → form pre-filled, upload foto KTP baru → submit.
  8. DB: status='pending_review', revision_submitted event logged.
  9. Admin review → klik Approve → row registrasi DELETED, merchant baru di merchant.merchants. ✅

Skenario 2: Revisi pasca-bank-approval (NMID retention)

  1. Lanjut dari sukses Skenario 1, mulai user baru → flow sampai acquirer_approved (bank balas approve, NMID issued).
  2. Admin pilih Kirim Balik untuk revisi dengan catatan "Update foto KTP dan rekening pencairan".
  3. DB: status='revision_required', revision_phase='post_bank_approval', NMID/MID/TID tetap tersimpan.
  4. User edit foto KTP + account_number → submit.
  5. DB: status balik ke acquirer_approved (sesuai decision #3 — NMID selalu tetap), revision_submitted event tercatat.
  6. Admin Final Approve → merchant aktif dengan NMID/MID/TID yang sama dengan yang bank issued di awal. ✅

Skenario 3: Revisi pasca-bank-approval, field identitas (verifikasi NMID retention tetap)

  1. Sama setup dengan Skenario 2 sampai revision_required.
  2. User edit nik (field identitas yang signifikan) → submit.
  3. DB: NMID/MID/TID tetap tersimpan (tidak reset), status balik acquirer_approved.
  4. Admin Final Approve → merchant aktif. Catat di revision_phase='post_bank_approval' untuk audit batch nanti kalau bank/regulator tanya konsistensi data. ✅
  5. Catatan operasional: kalau di production ternyata bank flag inkonsistensi data, eskalasi ke Pendekatan C (tombol manual reset NMID di dialog admin) — schema tidak perlu ubah.

Skenario 4: Cap 3 round

  1. Register → admin minta revisi 1 → user submit.
  2. Admin minta revisi 2 → user submit.
  3. Admin minta revisi 3 → user submit.
  4. Admin coba minta revisi 4 → dialog dashboard: opsi "Kirim balik untuk revisi" DISABLED, hanya Approve atau Reject Final yang available.
  5. Admin pilih Approve → merchant aktif. ✅

Skenario 5: Reject Final

  1. Register → admin pilih Reject → dialog → pilih "Tolak final" + catatan "Dokumen palsu".
  2. DB: status='rejected', rejected_count=1, FCM merchant_status_rejected ter-dispatch.
  3. User di mobile lihat banner status "Belum Aktif"/"Ditolak".
  4. User coba tap "Aktifkan" → navigasi ke flow registrasi baru (row registrasi lama dianggap done, mulai dari nol). ✅

5.3 Output dokumentasi

  • Test run log per skenario (timestamp, screenshot mobile, snapshot DB).
  • Update docs/plans/audit/ folder dengan e2e-merchant-revision-YYYY-MM-DD.md.
  • Update plan ini: ubah status frontmatter draft → approved setelah semua 5 pass.

Cross-cutting concerns

Testing strategy

  • Unit test: minimum coverage untuk hasCriticalFieldChange (Phase 2). Server transitions lain optional.
  • Integration test: tidak ada di codebase merchant_core_api saat ini — skip.
  • Smoke manual: dilakukan di Phase 5 (E2E).

Backward compatibility

  • Schema additive — kolom baru nullable atau dengan default. Tidak ada drop column.
  • API additive — endpoint baru ditambah, endpoint lama tetap. Status enum tambah, tidak hapus.
  • Mobile dual-handle: status rejected lama + revision_required baru — keduanya jalan.
  • FCM: event baru ditambah. Mobile lama yang belum di-update akan ignore event tidak dikenal (no crash).

Deployment order

Phase 1 (schema) — apply ke prod ASAP, additive safe

Phase 2 (server) — deploy, tapi endpoint masih dark (tidak ada caller)

Phase 3 (dashboard) — deploy, mulai expose dialog

Phase 4 (mobile, locked) — diskusi → release app version baru

Phase 5 (E2E test) — validasi end-to-end di staging sebelum prod

Open questions (resolved)

  1. NMID retention whitelist finalRESOLVED 2026-06-02: Pendekatan A (selalu tetap). Schema kolom revision_phase dipertahankan untuk audit batch. Eskalasi ke Pendekatan C tanpa breaking change kalau production butuh.
  2. Reject pasca-acquirer_approvedRESOLVED 2026-06-02: boleh. Kolom audit rejection_reason_post_acquirer ditambah di Phase 1.
  3. Wording dialogRESOLVED 2026-06-02: "Kirim Balik untuk revisi" + "Tolak Final".
  4. Counter displayRESOLVED 2026-06-02: Opsi A (info read-only). Cap 3 hard-coded.

Open questions (masih)

  1. Counter rejected_count (mig 033) vs revision_count (mig 066) — dual model: dua counter berbeda untuk dua jalur (rejected vs revision_required). Apakah perlu deprecation rejected_count di future? Atau dipertahankan untuk audit?
  2. i18n: Phase 4 mobile sudah pakai arb files? Cek saat audit pre-flight.

Changelog

  • 2026-06-02: draft awal, lengkap dengan 5 phase + audit pre-flight per phase + 5 skenario E2E. Decision log untuk 6 keputusan policy dari diskusi 2026-06-02.
  • 2026-06-02: decision finalisasi sesi diskusi:
    • #1 Wording "Kirim Balik untuk revisi" + "Tolak Final" — confirmed
    • #2 Counter display Opsi A (info read-only, cap 3 hard-coded)
    • #3 NMID retention Pendekatan A (selalu tetap, no whitelist code)
    • #4 Reject pasca-acquirer_approved BOLEH + audit kolom rejection_reason_post_acquirer
    • Phase 2 server design disederhanakan (hapus hasCriticalFieldChange helper)
    • Verifikasi & skenario E2E di-update sesuai final design
  • 2026-06-02: Phase 1 EXECUTED. Mig 066 applied ke db_kesles_merchant dev. Semua verifikasi pass.
  • 2026-06-02: Phase 2 EXECUTED. 6 file modified, go build + go vet bersih, smoke test SQL pass untuk RequestRevision dan extended reject pasca acquirer_approved.
  • 2026-06-02: Phase 3.0 EXECUTED (data layer extension prerequisite). 4 file modified: (a) db_internal_dashboard.go — SQL WHERE status IN extended dengan revision_required, SELECT + struct + Scan extend 3 kolom revision_*. (b) dashboard_summary.go — response mapper extend 3 key. (c) home_dashboard_entities.dartPendingMerchantItem extend 3 field + copyWith. (d) home_dashboard_mapper.dart — parse 3 field dari JSON (null-safe backward compat). Build & analyze keduanya bersih. Mirror repo_exports/ SKIPPED (folder tidak ada lagi di session ini).
  • 2026-06-02: Phase 3.1 EXECUTED. 7 file modified/created di dashboard. New widget RejectRevisionDialog pakai DashboardFormDialogShell + RadioGroup<T> pattern Flutter 3.32+. _performReject route ke endpoint sesuai outcome dialog. Status pill revision_required render dengan label "Revisi · X/3". Kelas _RejectRegistrationDialog lama dihapus (digantikan dialog baru). flutter analyze bersih untuk semua file Phase 3.
  • 2026-06-02: Phase 4.0 EXECUTED. profile/store.go extend 41 subquery whitelist + 2 Pattern C subquery untuk include revision_required. Tidak tambah field response baru (reuse merchant_review_notes + merchant_review_payload dengan status-based source mapping). go build bersih.
  • 2026-06-02: Phase 4.1 EXECUTED (locked, per-file approval). 3 file mobile_user disentuh: shared/merchant_status.dart (7 helper extend revision_required paralel rejected), merchant_account_status_page.dart (_viewFor extend case identik rejected), push_notification_service.dart (2 switch FCM event handler extend). Tidak buat widget baru, tidak tambah variant enum, tidak tambah field session. Mobile rendering 100% reuse pattern existing. flutter analyze bersih.
  • 2026-06-05: Phase 5 SQL smoke test EXECUTED. 5/5 skenario PASS (Revisi pre-bank, Revisi pasca-acquirer NMID retention, Revisi field identitas, Cap 3 round, Reject Final + pasca-acquirer). Semua test wrap BEGIN/ROLLBACK — tidak mengubah state DB dev. Result tabel ditulis di sub-section 5.0.
  • 2026-06-05: Binary merchant_core_api Phase 1-4 deployed ke VM oleh user. Proxy dashboard_api/internal/app/dashboard_merchant_registration.go/internal/merchant/registration/request-revision confirmed wired. Plan status frontmatter dinaikkan ke complete. UI smoke test manual (dashboard tekan tombol → FCM → mobile render banner) tetap disarankan sebagai validasi akhir tapi tidak blocking untuk archive plan.