Midtrans-Middleware/docs/ui-architecture.md

1015 lines
41 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Midtrans CIFO Frontend Architecture Document
## Change Log
| Date | Version | Description | Author |
|------------|---------|----------------------------------------|--------|
| 2025-11-07 | v1 | Initial frontend architecture scaffolding | Architect (Winston) |
## 1. Template and Framework Selection
Context & Decision
- Basis dokumen: `docs/prd.md` (UI multi-metode pembayaran: VA, QRIS, Kartu) dan belum ada dokumen arsitektur utama (`docs/architecture.md`).
- Lingkungan proyek: Laragon (PHP) untuk backend; frontend akan diatur sebagai aplikasi SPA terpisah yang berkomunikasi via REST.
- Starter pilihan: Vite + React + TypeScript.
Rationale
- Vite + React memberikan setup cepat, build cepat, dan ekosistem luas. TypeScript membantu kualitas kode meski QA dilakukan manual (type-safety, DX lebih baik).
- React Router memadai untuk halaman Checkout, Status, dan Riwayat; TanStack Query menangani data fetching, status, cache, dan retry/polling untuk status transaksi.
- Tailwind CSS mempercepat UI dengan gaya konsisten, aksesibilitas lebih terukur, dan footprint tooling rendah.
- Tidak memilih Next.js karena kebutuhan SSR tidak kritikal untuk halaman checkout sederhana, dan menghindari kompleksitas deploy.
Konsekuensi
- Manual QA only: tidak ada pipeline test otomatis; struktur proyek dan standar coding harus tegas untuk menghindari regressi.
- Build/deploy dilakukan manual; dokumentasi command harus jelas (dev/build). Observability di UI via logging ringan & event tracking internal.
## 2. Frontend Tech Stack (Draft untuk Validasi)
| Category | Technology | Version | Purpose | Rationale |
|---------------------|-------------------------|---------|------------------------------------------------|---------------------------------------------------------------------------|
| Framework | React | ^18 | SPA untuk checkout dan status transaksi | Ekosistem luas, composable UI, dukungan community yang kuat |
| UI Library | Tailwind CSS | ^3 | Styling utilitas cepat | Cepat, konsisten, mudah diadopsi, cocok untuk MVP |
| State Management | TanStack Query | ^5 | Server state (fetch/poll/retry/cache) | Ideal untuk status transaksi & polling; minimal boilerplate |
| Routing | React Router | ^6 | Navigasi halaman | Pola rute sederhana, proteksi rute mudah |
| Build Tool | Vite | ^5 | Dev/build tooling | Startup cepat, konfigurasi sederhana |
| Styling | Tailwind + CSS Modules | ^3 | Styling kombinasi utilitas + modular | Fleksibel; module untuk komponen kompleks |
| Testing | Manual QA | N/A | Pengujian manual | Sesuai kebijakan proyek (tanpa CI/CD); template test dapat ditambahkan |
| Component Library | Headless UI / Shadcn | latest | Komponen aksesibel minimal | Mempercepat pembuatan form/overlay; tetap fleksibel |
| Form Handling | React Hook Form | ^7 | Validasi & kontrol form | Ringan, ergonomis, integrasi baik dengan TS |
| Animation | Framer Motion | ^11 | Mikro-animasi & transisi | Peningkatan UX tanpa kompleksitas berlebih |
| Dev Tools | ESLint + Prettier | latest | Konsistensi & kualitas kode | Membantu kualitas saat QA manual; cegah kesalahan umum |
Rationale Tambahan
- TanStack Query mempermudah implementasi polling status transaksi (fallback ketika webhook belum tiba) dan retry terukur sesuai PRD.
- React Hook Form memudahkan validasi input kartu (tokenisasi) dan interaksi form minimal.
- Headless UI/Shadcn memberi komponen aksesibel untuk dialog/menus tanpa mengikat ke desain tertentu.
Elicit Options (19)
1. Proceed ke bagian berikutnya (Project Structure)
2. Risk & Challenge Analysis untuk pilihan stack
3. Structural Analysis (komponen vs halaman vs services)
4. Devils Advocate (tantangan stack ini untuk manual QA)
5. Multi-Persona (PO/SM/Dev/QA) feedback cepat
6. Creative Exploration (alternatif: Vue/Nuxt atau Angular)
7. 2025 Techniques (SSR-lite/Partial Hydration pertimbangan)
8. Process Control (tandai asumsi dan batasan eksplisit)
9. Reflective Review (cek keselarasan dengan PRD Epics 13)
Select 19 or just type your question/feedback:
## 7. Routing
Tujuan: mendefinisikan peta rute, alur navigasi Checkout → Status → Riwayat, serta praktik React Router v6 (lazy loading, error boundaries, guard ringan).
### 7.1 Route Map
- `/checkout` — halaman utama untuk memilih metode (VA, QRIS/GoPay, Card, Cstore) dan memulai charge.
- `/payments/:orderId/status` — halaman status transaksi realtime (polling/sse) untuk semua metode.
- `/history` — riwayat transaksi (read-only, filter/sort sederhana).
- Opsional (Card 3DS): `/checkout/card/challenge` — halaman penanganan challenge bila diperlukan; atau gunakan `redirect_url` eksternal.
- Fallback: `*` — NotFound.
### 7.2 Konfigurasi React Router (v6)
```tsx
// src/app/router.tsx
import { createBrowserRouter, RouterProvider, Navigate } from 'react-router-dom';
import { AppLayout } from './AppLayout';
import { CheckoutPage } from '../pages/CheckoutPage';
import { PaymentStatusPage } from '../pages/PaymentStatusPage';
import { PaymentHistoryPage } from '../pages/PaymentHistoryPage';
import { NotFoundPage } from '../pages/NotFoundPage';
const router = createBrowserRouter([
{
path: '/',
element: <AppLayout />,
errorElement: <div role="alert">Terjadi kesalahan. Coba muat ulang.</div>,
children: [
{ index: true, element: <Navigate to="/checkout" replace /> },
{ path: 'checkout', element: <CheckoutPage /> },
{ path: 'payments/:orderId/status', element: <PaymentStatusPage /> },
{ path: 'history', element: <PaymentHistoryPage /> },
{ path: '*', element: <NotFoundPage /> },
],
},
]);
export function AppRouter() {
return <RouterProvider router={router} />;
}
```
Catatan:
- Gunakan lazy loading untuk halaman berat (mis. `PaymentHistoryPage`) guna mempercepat initial load checkout.
- Tambahkan `<ScrollRestoration />` di layout bila perlu.
### 7.3 Alur Navigasi
- Dari `/checkout`, setelah `charge` sukses (menerima `order_id`), arahkan ke `/payments/:orderId/status`.
- Simpan `payment_type` di `location.state` atau query (contoh: `?type=gopay`) bila diperlukan untuk UI hint.
- Terminal state:
- `settlement`: tampilkan success dan CTA kembali ke `/history`.
- `expire`/`cancel`/`deny`: tampilkan informasi dan CTA ulang di `/checkout`.
- Card (3DS): bila response berisi `redirect_url`, buka di jendela/tab baru atau navigasi ke `/checkout/card/challenge` yang menangani proses lalu kembali ke `/payments/:orderId/status`.
Contoh helper navigasi:
```ts
// src/features/payments/lib/navigation.ts
import { useNavigate } from 'react-router-dom';
export function usePaymentNavigation() {
const navigate = useNavigate();
return {
toStatus(orderId: string, paymentType?: string) {
navigate(`/payments/${orderId}/status`, {
state: paymentType ? { paymentType } : undefined,
replace: true,
});
},
toHistory() {
navigate('/history');
},
toCheckout() {
navigate('/checkout');
},
};
}
```
### 7.4 Guards & Recovery
- Validasi akses langsung ke `/payments/:orderId/status`:
- Query `getPaymentStatus(orderId)` — jika 404/invalid, tampilkan NotFound atau ajak kembali ke `/checkout`.
- Lindungi `/history` dengan batasan sederhana (misal hanya read-only; tidak memerlukan auth jika PRD tidak mensyaratkan).
- Toleransi refresh: halaman status dapat dibuka ulang langsung selama `order_id` valid.
### 7.5 Status Updates (Polling / Push)
- Default: polling 3 detik menggunakan TanStack Query seperti di bagian State Management.
- Opsi 2025: SSE/WebSocket untuk push status agar realtime dan hemat jaringan; fallback ke polling saat koneksi tidak stabil.
### 7.6 Aksesibilitas & Analytics
- Announce perubahan status dengan ARIA live region (mis. `role="status"`, `aria-live="polite"`).
- Catat event navigasi (route change) beserta `X-Correlation-Id` untuk tracing ringan di UI.
---
Elicit Options (Routing → Next)
1. Lanjut ke Styling (Tailwind arsitektur dan utility patterns)
2. Tambah Testing (rencana manual QA dan template kasus uji)
3. Environment Config (env vars & build/dev commands)
4. Developer Standards (lint, format, commit hygiene)
5. Error UX polish (banner, retry, deep link handling)
6. Performance (lazy routes, code-splitting, prefetch)
## 8. Styling
Tujuan: menetapkan arsitektur styling yang konsisten dan dapat dipelihara menggunakan Tailwind CSS (utilitas) dan CSS Modules (gaya terisolasi), termasuk varian komponen, responsif, dark mode, dan a11y visual.
### 8.1 Setup Tailwind (config + globals)
Konfigurasi dasar Tailwind dengan dark mode berbasis class dan plugin formulir/typografi.
```ts
// tailwind.config.ts
import type { Config } from 'tailwindcss';
export default {
content: ['./index.html', './src/**/*.{ts,tsx}'],
darkMode: 'class',
theme: {
extend: {
colors: {
brand: {
50: '#fef2f2',
100: '#fee2e2',
200: '#fecaca',
300: '#fca5a5',
400: '#f87171',
500: '#ef4444',
600: '#dc2626', // merah utama
700: '#b91c1c',
800: '#991b1b',
900: '#7f1d1d',
},
},
boxShadow: {
focus: '0 0 0 3px rgba(220,38,38,0.45)',
},
},
},
plugins: [require('@tailwindcss/forms'), require('@tailwindcss/typography')],
} satisfies Config;
```
Global stylesheet minimal: reset, font, dan token CSS (opsional) untuk warna.
```css
/* src/styles/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--radius: 8px;
}
@layer base {
html, body, #root { height: 100%; }
body { @apply antialiased bg-white text-black; }
.dark body { @apply bg-black text-white; }
a { @apply text-brand-600 hover:text-brand-700; }
}
@layer utilities {
.focus-ring { @apply outline-none ring-2 ring-brand-600 ring-offset-2 ring-offset-white dark:ring-offset-black; }
.card { @apply rounded-[var(--radius)] border border-black/10 bg-white shadow-sm dark:bg-black dark:border-white/20; }
}
```
### 8.2 Pola Utilitas & Komposisi
- Gunakan utilitas untuk layout: `flex`, `grid`, `gap`, `container`, `max-w-*`, `space-*`.
- Terapkan skala spacing/typography konsisten: `text-sm/base/lg`, `p-2/3/4`, `gap-2/3/4`.
- Komposisi class dinamis via `clsx`/`cn` helper.
```ts
// src/lib/cn.ts
import { clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: Array<string | Record<string, boolean> | undefined>) {
return twMerge(clsx(inputs));
}
```
Contoh penggunaan:
```tsx
// src/components/Section.tsx
import { cn } from '../lib/cn';
export function Section(props: { title: string; className?: string; children: React.ReactNode }) {
return (
<section className={cn('mb-6', props.className)}>
<h2 className="text-lg font-semibold mb-2">{props.title}</h2>
{props.children}
</section>
);
}
```
### 8.3 Varian Komponen dengan CVA (opsional)
Gunakan `class-variance-authority` untuk varian yang dapat dikontrol tanpa membengkakkan class string.
```ts
// src/components/ui/button.tsx
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '../../lib/cn';
const buttonVariants = cva(
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:focus-ring disabled:opacity-50 disabled:pointer-events-none',
{
variants: {
variant: {
primary: 'bg-brand-600 text-white hover:bg-brand-700',
secondary: 'bg-white text-black hover:bg-white/90 dark:bg-black dark:text-white dark:hover:bg-black/90',
outline: 'border border-black text-black hover:bg-black/5 dark:border-white dark:text-white dark:hover:bg-white/10',
},
size: { sm: 'h-8 px-3', md: 'h-10 px-4', lg: 'h-11 px-6' },
},
defaultVariants: { variant: 'primary', size: 'md' },
}
);
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {}
export function Button({ className, variant, size, ...props }: ButtonProps) {
return <button className={cn(buttonVariants({ variant, size }), className)} {...props} />;
}
```
### 8.4 CSS Modules untuk Gaya Terkapsulasi
Gunakan CSS Modules untuk komponen kompleks yang membutuhkan selector stateful atau animasi khusus sambil memanfaatkan `@apply` Tailwind.
```css
/* src/components/alert/Alert.module.css */
.root { @apply card p-4 flex gap-3; }
.icon { @apply text-brand-600; }
.title { @apply font-semibold; }
.desc { @apply text-sm text-slate-600 dark:text-slate-300; }
```
```tsx
// src/components/alert/Alert.tsx
import s from './Alert.module.css';
export function Alert({ title, children }: { title: string; children?: React.ReactNode }) {
return (
<div className={s.root} role="status" aria-live="polite">
<div className={s.icon}></div>
<div>
<div className={s.title}>{title}</div>
{children ? <div className={s.desc}>{children}</div> : null}
</div>
</div>
);
}
```
### 8.5 Responsif & Dark Mode
- Breakpoint standar: `sm`, `md`, `lg`, `xl`, `2xl`; gunakan progressif (mobile-first) dan hindari override berlebihan.
- Dark mode via class `dark` di tag `html`/`body`; toggling dilakukan oleh provider tema UI (lihat State Management bagian UI-store).
- Gambar/ikon: pilih versi gelap/terang atau gunakan `opacity`/`mix-blend` untuk adaptasi.
### 8.6 Aksesibilitas Visual
- Fokus terlihat: gunakan `.focus-ring` dan `focus-visible` agar keyboard navigation jelas.
- Kontras warna memenuhi WCAG AA; warna brand 600+ untuk tombol utama, teks tetap terbaca di dark mode.
- ARIA untuk komponen interaktif (dialog, toast, status) sesuai rekomendasi Headless UI/Shadcn.
### 8.7 Performansi & Maintainability
- Hindari class string sangat panjang—ekstrak ke komponen kecil atau helper varian.
- Gunakan `prettier-plugin-tailwindcss` untuk pengurutan class otomatis (opsional, non-blocking).
- Pastikan purge/content paths tepat agar ukuran CSS hasil build tetap kecil.
---
Elicit Options (Styling → Next)
1. Testing (kasus uji manual dan template skenario)
2. Environment Config (env vars, build/dev command)
3. Developer Standards (lint/format/commit hygiene)
4. Error UX polish (banner, retry, deep link)
5. Performance (code-splitting, prefetch assets)
6. Theming tokens (CSS vars dan skala desain)
## 6. API Integration
Tujuan: mendefinisikan batas frontendbackend, kontrak endpoint REST, tipe respons yang relevan untuk UI, dan layanan frontend untuk VA (Bank Transfer), Card (3DS), E-Wallet/QRIS (GoPay), serta Over the Counter (cstore). Sesuai kebijakan keamanan, semua `charge` dilakukan di backend (Server Key).
### 6.1 Backend Endpoints (Kontrak)
- `POST /api/payments/charge`
- Body (umum):
- `payment_type`: `'bank_transfer' | 'permata' | 'echannel' | 'credit_card' | 'gopay' | 'cstore'`
- `transaction_details`: `{ gross_amount: number, order_id?: string }`
- Parameter metode:
- Bank Transfer (VA): `{ bank_transfer: { bank: 'bca' | 'bni' | 'bri' | 'cimb' } }`
- Permata VA: `payment_type: 'permata'` (tanpa `bank_transfer` body)
- Mandiri Bill Payment (E-Channel): `payment_type: 'echannel'`, `echannel: { bill_info1?: string, bill_info2?: string }`
- Card: `{ credit_card: { token_id: string, authentication: true } }` (token diambil di frontend via 3DS script)
- E-Wallet/QRIS: `{ gopay: { enable_callback?: boolean, callback_url?: string } }`
- Cstore: `{ cstore: { store: 'alfamart' | 'indomaret', message?: string, ... } }`
- Response (disederhanakan untuk UI):
- Bank Transfer: `{ order_id, payment_type:'bank_transfer', bank, va_number, status:'pending', expiry_time }` (backend menormalkan `va_numbers` menjadi satu `va_number`)
- Permata VA: `{ order_id, payment_type:'permata', va_number, status:'pending', expiry_time }` (backend menormalkan `permata_va_number``va_number`)
- Mandiri E-Channel: `{ order_id, payment_type:'echannel', bill_key, biller_code, status:'pending', expiry_time }`
- Card: `{ order_id, payment_type:'credit_card', status:'pending' | 'challenge' | 'capture' | 'settlement', redirect_url? }`
- QRIS/GoPay: `{ order_id, payment_type:'gopay', qr_url, deeplink_url?, expiry_time, status:'pending' }`
- Cstore: `{ order_id, payment_type:'cstore', payment_code, store, status:'pending', expiry_time }`
- `GET /api/payments/:orderId/status`
- Response: `{ order_id, payment_type, status: 'pending' | 'settlement' | 'deny' | 'cancel' | 'expire', transaction_time?, gross_amount?, fraud_status? }`
- `POST /api/payments/:orderId/cancel` (opsional; sebelum settlement)
- `POST /api/payments/:orderId/refund` (opsional; admin flow)
- `POST /api/payments/webhook` (backend only) — UI tidak memanggil; status dipush ke DB, lalu UI membaca via endpoint `status`.
Catatan keamanan:
- Server Key hanya di backend; frontend memakai Client Key untuk 3DS script.
- Backend memvalidasi `order_id` untuk idempotensi; frontend boleh mengirim atau backend dapat membuatkan.
- Header standar: `X-Correlation-Id` untuk tracing.
### 6.2 Frontend Services (contracts)
`services/apiClient.ts` — klien HTTP berbasis Axios (atau fetch) dengan base URL dari `VITE_API_BASE_URL`.
```ts
// src/features/payments/services/apiClient.ts
import axios from 'axios';
const apiBase = import.meta.env.VITE_API_BASE_URL;
export const apiClient = axios.create({ baseURL: apiBase });
apiClient.interceptors.request.use((config) => {
config.headers = {
...config.headers,
'Content-Type': 'application/json',
Accept: 'application/json',
'X-Correlation-Id': crypto?.randomUUID?.() ?? String(Date.now()),
};
return config;
});
```
`services/paymentsService.ts` — fungsi-fungsi terpisah per metode.
```ts
// src/features/payments/services/paymentsService.ts
import { apiClient } from './apiClient';
export type PaymentMethod = 'bank_transfer' | 'credit_card' | 'gopay' | 'cstore';
export type PaymentStatus = 'pending' | 'challenge' | 'capture' | 'settlement' | 'deny' | 'cancel' | 'expire';
export async function chargeVA(bank: 'bca' | 'bni' | 'bri' | 'cimb') {
const res = await apiClient.post('/payments/charge', {
payment_type: 'bank_transfer',
bank_transfer: { bank },
});
return res.data as {
order_id: string;
payment_type: 'bank_transfer';
bank: string;
va_number: string;
status: PaymentStatus;
expiry_time?: string;
};
}
export async function chargePermataVA() {
const res = await apiClient.post('/payments/charge', {
payment_type: 'permata',
});
return res.data as {
order_id: string;
payment_type: 'permata';
va_number: string; // normalized from permata_va_number
status: PaymentStatus;
expiry_time?: string;
};
}
export async function chargeMandiriEChannel(billInfo?: { bill_info1?: string; bill_info2?: string }) {
const res = await apiClient.post('/payments/charge', {
payment_type: 'echannel',
echannel: billInfo ?? {},
});
return res.data as {
order_id: string;
payment_type: 'echannel';
bill_key: string;
biller_code: string;
status: PaymentStatus;
expiry_time?: string;
};
}
export async function chargeCard(tokenId: string, authentication = true) {
const res = await apiClient.post('/payments/charge', {
payment_type: 'credit_card',
credit_card: { token_id: tokenId, authentication },
});
return res.data as {
order_id: string;
payment_type: 'credit_card';
status: PaymentStatus;
redirect_url?: string;
};
}
export async function chargeQRIS(options?: { enable_callback?: boolean; callback_url?: string }) {
const res = await apiClient.post('/payments/charge', {
payment_type: 'gopay',
gopay: options ?? {},
});
return res.data as {
order_id: string;
payment_type: 'gopay';
qr_url: string;
deeplink_url?: string;
status: PaymentStatus;
expiry_time?: string;
};
}
export async function chargeCstore(store: 'alfamart' | 'indomaret', message?: string) {
const res = await apiClient.post('/payments/charge', {
payment_type: 'cstore',
cstore: { store, message },
});
return res.data as {
order_id: string;
payment_type: 'cstore';
payment_code: string;
store: string;
status: PaymentStatus;
expiry_time?: string;
};
}
export async function getPaymentStatus(orderId: string) {
const res = await apiClient.get(`/payments/${orderId}/status`);
return res.data as {
order_id: string;
payment_type: PaymentMethod;
status: PaymentStatus;
transaction_time?: string;
gross_amount?: number;
fraud_status?: string;
};
}
```
### 6.3 Env Vars & Script Loader (Card 3DS)
- Frontend `.env.example`:
- `VITE_API_BASE_URL=https://localhost:8000/api`
- `VITE_MIDTRANS_CLIENT_KEY=YOUR_CLIENT_KEY`
- `VITE_MIDTRANS_ENV=sandbox` (atau `production`)
- Komponen loader script 3DS:
```tsx
// src/components/ScriptLoader.tsx
import { useEffect } from 'react';
export function Midtrans3DSLoader() {
useEffect(() => {
const script = document.createElement('script');
script.id = 'midtrans-script';
script.type = 'text/javascript';
script.src = 'https://api.midtrans.com/v2/assets/js/midtrans-new-3ds.min.js';
script.setAttribute('data-environment', import.meta.env.VITE_MIDTRANS_ENV);
script.setAttribute('data-client-key', import.meta.env.VITE_MIDTRANS_CLIENT_KEY);
document.body.appendChild(script);
return () => {
document.body.removeChild(script);
};
}, []);
return null;
}
```
Penggunaan: Render `Midtrans3DSLoader` di `CheckoutPage` atau `App` saat flow kartu diperlukan.
### 6.4 Error Contract & Mapping
- Standar error dari backend:
- 400: `VALIDATION_ERROR` (contoh: kartu gagal Luhn, parameter salah)
- 401/403: `AUTH_ERROR`
- 409: `ORDER_CONFLICT` (duplikasi `order_id`)
- 5xx: `SERVER_ERROR`
- UI menampilkan pesan ramah di `ErrorBanner` dan menyediakan CTA `Retry`, `Back`, `Contact Support` sesuai konteks.
- Logging ringan di `lib/logger.ts` mencatat `X-Correlation-Id` untuk pelacakan.
### 6.5 Alur per Metode (Ringkas)
- VA:
- `chargeVA(bank)` → tampilkan `va_number` + instruksi; arahkan ke `PaymentStatus`; polling hingga `settlement/expire`.
- Card (3DS):
- Muat 3DS script → `MidtransNew3ds.getCardToken(...)` → kirim `token_id` ke backend via `chargeCard(token)` → arahkan ke `PaymentStatus`; status bisa `challenge` sebelum `settlement`.
- QRIS/GoPay:
- `chargeQRIS()` → tampilkan `qr_url` (atau deeplink untuk mobile) + countdown; arahkan ke `PaymentStatus`; polling status.
- Cstore:
- `chargeCstore(store)` → tampilkan `payment_code` + instruksi; arahkan ke `PaymentStatus`; tunggu konfirmasi.
### 6.6 Boundary & Keamanan
- Tidak ada Server Key di frontend; semua request ke Midtrans dari backend.
- Client Key hanya untuk 3DS tokenization; token_id berlaku satu transaksi.
- Hindari menampilkan data sensitif; UI menyimpan minimal (status & indikator UX).
### 6.7 Schema Compatibility Notes (Midtrans Core API)
Tujuan: memastikan kontrak UI selaras dengan skema resmi Midtrans dan menjelaskan pemetaan/normalisasi yang dilakukan backend agar UI tetap sederhana.
- Bank Transfer (VA)
- Request resmi: `payment_type: "bank_transfer"`, `transaction_details.{order_id,gross_amount}`, `bank_transfer.{bank}` untuk BCA/BNI/BRI/CIMB [0].
- Response variasi: `va_numbers: [{ bank, va_number }]` (multi-bank), disederhanakan backend menjadi satu `va_number` untuk UI.
- Permata: gunakan `payment_type: "permata"` (bukan `bank_transfer`); response field `permata_va_number` dinormalisasi menjadi `va_number` [0].
- Mandiri Bill Payment (E-Channel): gunakan `payment_type: "echannel"` dengan `echannel.{bill_info1,bill_info2}`; response berisi `bill_key` dan `biller_code` [0].
- Card (3DS)
- Frontend mengambil `token_id` via `midtrans-new-3ds` script (Client Key) [1].
- Backend Charge: `payment_type: "credit_card"`, `credit_card.{ token_id, authentication }` [1].
- Response dapat mengandung `redirect_url` (untuk challenge) dan `transaction_status` seperti `pending`, `challenge`, `capture`/`settlement`; UI menambahkan dukungan `capture` [1].
- E-Wallet/QRIS (GoPay)
- Request: `payment_type: "gopay"`; optional `gopay.{ enable_callback, callback_url }` [2].
- Response: `actions` (QR URL untuk desktop, deeplink untuk mobile) dan `transaction_status: pending`; backend menormalkan ke `qr_url` dan `deeplink_url` untuk konsumsi UI [2].
- Over the Counter (cstore)
- Request: `payment_type: "cstore"`, `cstore.{ store, message, alfamart_free_text_1..3? }` [3].
- Response: `payment_code`, `store`, `transaction_status: pending` [3].
- Field umum yang dapat di-pass-through untuk debugging: `status_code`, `status_message`, `transaction_id`, `order_id`, `gross_amount`, `payment_type`, `transaction_time`, `fraud_status`.
Catatan implementasi:
- Backend bertindak sebagai adapter yang menormalisasi variasi field antar metode/bank agar UI konsumsi konsisten.
- Jika diperlukan strictness penuh, UI dapat memakai nama field resmi (`transaction_status`, `va_numbers`, `actions`) dan merender sesuai variasi.
---
Elicit Options (19)
1. Proceed ke Routing (navigasi Checkout → Status → Riwayat)
2. Risk & Challenge (kontrak API dan dampak QA manual)
3. Structural Analysis (abstraksi services dan tipe respons)
4. Devils Advocate (apakah satu endpoint `charge` terlalu generik?)
5. Multi-Persona (PO/SM/Dev/QA sudut pandang)
6. Creative Exploration (layer adapter untuk future payment methods)
7. 2025 Techniques (SSE/WebSocket untuk status push)
8. Process Control (idempotensi & correlation id praktik)
9. Reflective Review (keselarasan dengan PRD epics & Midtrans docs)
Select 19 or just type your question/feedback:
## 5. State Management
Tujuan: memisahkan dengan jelas server-state (status transaksi, hasil charge) dan UI-state (pilihan metode, dialog), menerapkan polling yang aman, dan konsisten dengan PRD (webhook + fallback polling).
### 5.1 Server-State dengan TanStack Query
- Query Client Defaults (di `providers/QueryProvider.tsx`):
- `refetchOnWindowFocus: false` (hindari refetch tak terduga saat QA manual).
- `retry(failCount, error)`: maksimal 3 kali untuk error jaringan/transient; mapping error 4xx jadi non-retry.
- `staleTime`: 35 detik untuk status transaksi; 0 untuk operasi lain.
- Query Keys Standar:
- `['payments','status', orderId]` untuk status transaksi.
- `['payments','details', orderId]` untuk detail transaksi.
- Mutations: `['payments','charge', method]` untuk pembuatan transaksi.
- Invalidation & Lifecycle:
- Setelah `charge` sukses, arahkan ke `PaymentStatus` dan mulai polling `status`.
- Hentikan polling saat mencapai terminal state (`settlement`, `deny`, `cancel`, `expire`).
- Invalidate status jika webhook backend mengubah status (opsional: gunakan indikator versi/timestamp dari backend).
Contoh hook status dengan polling kondisional:
```ts
// features/payments/hooks/usePaymentStatus.ts
import { useQuery } from '@tanstack/react-query';
import { apiClient } from '../services/apiClient';
const TERMINAL = new Set(['settlement', 'deny', 'cancel', 'expire']);
export function usePaymentStatus(orderId: string) {
return useQuery({
queryKey: ['payments', 'status', orderId],
queryFn: async () => {
const res = await apiClient.get(`/payments/${orderId}/status`);
return res.data; // { status: 'pending' | 'settlement' | ... }
},
refetchInterval: (data) => {
const status = data?.status;
return status && TERMINAL.has(status) ? false : 3000; // 3s polling
},
retry: (failCount, error: any) => {
const is4xx = error?.response?.status && String(error.response.status).startsWith('4');
if (is4xx) return false; // user error, jangan retry
return failCount < 3;
},
refetchOnWindowFocus: false,
});
}
```
Catatan:
- Status mengikuti pola Midtrans (contoh umum: `pending`, `settlement`, `deny`, `cancel`, `expire`).
- Untuk kartu, status dapat melalui tahapan otentikasi (3DS) sebelum menjadi `settlement`.
### 5.2 UI-State dengan Store Tipis
Ruang lingkup UI-state: pilihan metode, visibilitas modal/dialog, notifikasi/UI flags (misal QR countdown). Simpan terpisah dari server-state.
Contoh store sederhana berbasis Context + Reducer:
```ts
// features/payments/store/uiStore.ts
import React, { createContext, useContext, useReducer } from 'react';
type PaymentMethod = 'VA' | 'QRIS' | 'Card' | 'Cstore' | null;
type UIState = {
selectedMethod: PaymentMethod;
isModalOpen: boolean;
toast: { type: 'info' | 'error' | 'success'; message: string } | null;
};
type Action =
| { type: 'SET_METHOD'; method: PaymentMethod }
| { type: 'OPEN_MODAL' }
| { type: 'CLOSE_MODAL' }
| { type: 'SHOW_TOAST'; toast: UIState['toast'] }
| { type: 'CLEAR_TOAST' };
const initialState: UIState = {
selectedMethod: null,
isModalOpen: false,
toast: null,
};
function reducer(state: UIState, action: Action): UIState {
switch (action.type) {
case 'SET_METHOD':
return { ...state, selectedMethod: action.method };
case 'OPEN_MODAL':
return { ...state, isModalOpen: true };
case 'CLOSE_MODAL':
return { ...state, isModalOpen: false };
case 'SHOW_TOAST':
return { ...state, toast: action.toast };
case 'CLEAR_TOAST':
return { ...state, toast: null };
default:
return state;
}
}
const UIContext = createContext<{ state: UIState; dispatch: React.Dispatch<Action> } | undefined>(
undefined,
);
export function UIProvider({ children }: { children: React.ReactNode }) {
const [state, dispatch] = useReducer(reducer, initialState);
return <UIContext.Provider value={{ state, dispatch }}>{children}</UIContext.Provider>;
}
export function useUI() {
const ctx = useContext(UIContext);
if (!ctx) throw new Error('useUI must be used within UIProvider');
return ctx;
}
```
Prinsip:
- UI-state tidak menyimpan data transaksi dari server; gunakan Query untuk itu.
- Reset UI-state saat berganti rute (contoh: dari Checkout ke PaymentStatus).
- Hindari global state berlebihan; scope-kan per feature bila perlu.
### 5.3 Error Handling & Logging
- Mapping error backend ke pesan user-friendly di `features/payments/components/ErrorBanner.tsx`.
- Log teknis (correlation id, status) di `lib/logger.ts`; hindari menyimpan data sensitif.
- Tampilkan CTA standar: `Retry`, `Back`, atau `Contact Support` sesuai konteks.
### 5.4 Kontrak API & Mutations
- Semua `charge` dipanggil via backend (`/api/payments/charge`) dengan `payment_type` dan parameter metode (VA/QRIS/Card/Cstore). Server Key tetap di backend.
- Untuk kartu: frontend mengambil `token_id` via script 3DS, mengirim ke backend untuk `charge` dengan `authentication: true` bila 3DS diaktifkan.
- Mutations mengembalikan `order_id` atau detail awal; UI lalu menavigasi ke `PaymentStatus` dan memulai polling.
---
Elicit Options (19)
1. Proceed ke API Integration (kontrak endpoints & services)
2. Risk & Challenge (ketahanan polling dan dampak UX)
3. Structural Analysis (batas UI-state vs server-state)
4. Devils Advocate (apakah store terpisah perlu diganti Context-only?)
5. Multi-Persona (PO/SM/Dev/QA sudut pandang)
6. Creative Exploration (push vs polling: SSE/WebSocket pros/cons)
7. 2025 Techniques (background sync & offline cache relevansi)
8. Process Control (standar error mapping & logging ringan)
9. Reflective Review (keselarasan dengan PRD status & rekonsiliasi)
Select 19 or just type your question/feedback:
## 4. Component Standards
Tujuan: menyelaraskan pola pembuatan komponen agar konsisten, mudah dirawat, dan dapat ditinjau dengan QA manual.
### 4.1 Component Template (TypeScript + A11y + Tailwind)
Gunakan fungsi komponen dengan props bertipe jelas, aksesibilitas dasar, dan kelas Tailwind terstandar.
```tsx
// components/Button.tsx
import React from 'react';
export type ButtonProps = {
variant?: 'primary' | 'secondary' | 'danger';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
children: React.ReactNode;
'aria-label'?: string;
};
export const Button: React.FC<ButtonProps> = ({
variant = 'primary',
size = 'md',
disabled = false,
onClick,
children,
...aria
}) => {
const base =
'inline-flex items-center justify-center rounded-md font-medium focus:outline-none focus:ring-2 focus:ring-offset-2 transition-colors';
const sizes: Record<NonNullable<ButtonProps['size']>, string> = {
sm: 'px-2.5 py-1.5 text-xs',
md: 'px-3 py-2 text-sm',
lg: 'px-4 py-2.5 text-base',
};
const variants: Record<NonNullable<ButtonProps['variant']>, string> = {
primary:
'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500 disabled:bg-blue-400',
secondary:
'bg-gray-100 text-gray-900 hover:bg-gray-200 focus:ring-gray-400 disabled:bg-gray-200',
danger:
'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500 disabled:bg-red-400',
};
return (
<button
type="button"
className={`${base} ${sizes[size]} ${variants[variant]}`}
disabled={disabled}
onClick={onClick}
{...aria}
>
{children}
</button>
);
};
export default Button;
```
Contoh komponen formulir ringan (gunakan React Hook Form di page yang memakai form):
```tsx
// components/FormField.tsx
import React from 'react';
export type FormFieldProps = {
label: string;
name: string;
type?: 'text' | 'number';
value: string | number;
onChange: (value: string) => void;
error?: string;
required?: boolean;
};
export const FormField: React.FC<FormFieldProps> = ({
label,
name,
type = 'text',
value,
onChange,
error,
required,
}) => {
const id = `field-${name}`;
return (
<div className="space-y-1">
<label htmlFor={id} className="text-sm font-medium text-gray-700">
{label} {required && <span className="text-red-600">*</span>}
</label>
<input
id={id}
name={name}
type={type}
className="block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-blue-500"
value={String(value)}
onChange={(e) => onChange(e.target.value)}
aria-invalid={Boolean(error)}
aria-describedby={error ? `${id}-error` : undefined}
/>
{error && (
<p id={`${id}-error`} className="text-xs text-red-600">
{error}
</p>
)}
</div>
);
};
```
Prinsip umum:
- Props selalu bertipe eksplisit; hindari `any`.
- Aksesibilitas: gunakan `aria-*`, `htmlFor`, dan label jelas.
- Tailwind untuk layout/warna; gunakan CSS Modules untuk gaya spesifik komponen bila perlu.
- Komponen presentasional tidak mengakses API langsung; logika data di layer `services/` atau hooks.
### 4.2 Penamaan & Konvensi
- Components: PascalCase untuk nama file dan komponen (`PaymentSummary.tsx`).
- Hooks: `useCamelCase` untuk nama hook (`usePaymentStatus.ts`). Hanya named exports.
- Services: file camelCase berakhiran `Service` bila berkelas atau fungsi di satu file (`paymentsService.ts`). Named exports.
- Stores (state): `paymentsStore.ts`, `uiStore.ts` dengan named exports.
- Types: nama tipe PascalCase (`Payment`, `PaymentStatus`). File `types/payment.ts` (lowercase singular).
- Pages: PascalCase folder, file `index.tsx` per page; komponen utama bernama sama dengan folder.
- Routes: `routes.tsx` sebagai pusat deklarasi rute.
- CSS Modules: `Component.module.css` untuk gaya spesifik; global di `styles/`.
- Icons/assets: kebab-case (`credit-card.svg`), impor dari `assets/icons`.
- Test (opsional): `Component.test.tsx` berdekatan dengan file sumber; karena QA manual, gunakan hanya untuk unit kecil yang berisiko.
Anti-pattern yang dihindari:
- Komponen melakukan fetch langsung ke Midtrans; gunakan `services/paymentsService` atau hooks.
- State global tanpa kontrol (gunakan TanStack Query untuk server-state, store terpisah untuk UI-state).
- Default export untuk hooks/services (prefer named export untuk tree-shaking dan konsistensi).
Rasional:
- Template tipe + a11y mempercepat review QA manual dan mengurangi regresi UI.
- Konvensi penamaan konsisten mempermudah navigasi struktur feature-first yang selaras dengan PRD.
- Pemisahan presentational vs data logic mendukung keterujian manual dan perawatan.
Elicit Options (19)
1. Proceed ke State Management (server-state vs UI-state)
2. Risk & Challenge (konsekuensi konvensi terhadap kecepatan dev)
3. Structural Analysis (komponen presentasional vs container)
4. Devils Advocate (apakah Tailwind + CSS Modules berlebih?)
5. Multi-Persona (PO/SM/Dev/QA sudut pandang)
6. Creative Exploration (atomic design vs feature-first)
7. 2025 Techniques (server components/partial hydration relevansi)
8. Process Control (standarisasi lint/format tanpa CI)
9. Reflective Review (cek keselarasan dengan PRD Epics 13)
Select 19 or just type your question/feedback:
## 3. Project Structure
Struktur direktori yang diusulkan (Vite + React + TS) untuk memenuhi PRD (Checkout, VA/QRIS/Kartu, Status, Riwayat), TanStack Query (polling/retry), dan Tailwind CSS.
```
frontend/
├── index.html
├── package.json
├── tsconfig.json
├── vite.config.ts
├── .env.example
├── tailwind.config.js
├── postcss.config.js
├── src/
│ ├── main.tsx
│ ├── app/
│ │ ├── App.tsx
│ │ ├── routes.tsx
│ │ └── providers/
│ │ ├── QueryProvider.tsx
│ │ └── ThemeProvider.tsx
│ ├── pages/
│ │ ├── Checkout/
│ │ │ ├── CheckoutPage.tsx
│ │ │ └── components/
│ │ │ ├── MethodSelector.tsx
│ │ │ └── SummaryPanel.tsx
│ │ ├── PaymentStatus/
│ │ │ └── PaymentStatusPage.tsx
│ │ └── TransactionHistory/
│ │ └── TransactionHistoryPage.tsx
│ ├── features/
│ │ └── payments/
│ │ ├── VA/
│ │ │ ├── VAInstructions.tsx
│ │ │ └── VAInfoCard.tsx
│ │ ├── QRIS/
│ │ │ ├── QRISCanvas.tsx
│ │ │ └── QRISCountdown.tsx
│ │ ├── Card/
│ │ │ ├── CardForm.tsx
│ │ │ └── ThreeDSFlow.tsx
│ │ ├── components/
│ │ │ ├── ErrorBanner.tsx
│ │ │ ├── LoadingSkeleton.tsx
│ │ │ └── StatusBadge.tsx
│ │ ├── hooks/
│ │ │ ├── usePaymentStatus.ts
│ │ │ └── useCopyToClipboard.ts
│ │ ├── services/
│ │ │ ├── apiClient.ts
│ │ │ └── paymentsService.ts
│ │ ├── types/
│ │ │ └── payment.ts
│ │ └── store/
│ │ └── uiStore.ts
│ ├── components/
│ │ ├── Button.tsx
│ │ ├── Modal.tsx
│ │ └── FormField.tsx
│ ├── lib/
│ │ ├── logger.ts
│ │ └── accessibility.ts
│ ├── styles/
│ │ ├── index.css
│ │ └── themes.css
│ ├── assets/
│ │ └── icons/
│ ├── utils/
│ │ ├── formatting.ts
│ │ └── validation.ts
│ └── config/
│ └── env.ts
└── README.md
```
Rationale
- Memetakan PRD Epics ke struktur: `pages/Checkout` (pemilihan metode & create transaksi), `pages/PaymentStatus` (status real-time), `pages/TransactionHistory` (riwayat pengguna).
- `features/payments` mengelompokkan komponen spesifik per-metode (VA/QRIS/Kartu), hooks (status/polling/copy), services (API client + payment service) agar mudah di-maintain.
- TanStack Query di `providers/QueryProvider.tsx` untuk cache/polling/retry terpusat; `services/apiClient.ts` untuk interceptors (auth, error mapping) dan konfigurasi `VITE_API_BASE_URL`.
- Tailwind + CSS Modules: utilitas cepat + modularitas untuk komponen kompleks; `styles/themes.css` untuk variabel global.
- Observability ringan di `lib/logger.ts` (correlation id pada event UI) dan komponen `ErrorBanner/StatusBadge` konsisten.
- `.env.example` mendokumentasikan variabel `VITE_...` (frontend) terpisah dari secret backend; `config/env.ts` membaca dan memvalidasi.
Opsi Elicitasi (19)
1. Proceed ke Component Standards (template dan konvensi)
2. Structural Analysis (cek keseimbangan pages vs features)
3. Risk & Challenge (kompleksitas maintainability dan QA manual)
4. Devils Advocate (apakah struktur terlalu granular?)
5. Multi-Persona (PO/SM/Dev/QA sudut pandang)
6. Creative Exploration (varian struktur: feature-first vs route-first)
7. 2025 Techniques (module federation/partial hydration relevansi)
8. Process Control (tandai asumsi & batasan eksplisit)
9. Reflective Review (keselarasan dengan PRD & arsitektur backend)
Select 19 or just type your question/feedback: