Midtrans-Middleware/docs/ui-architecture.md

41 KiB
Raw Permalink Blame History

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)

// 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:

// 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.

// 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.

/* 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.
// 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:

// 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.

// 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.

/* 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; }
// 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_numberva_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.

// 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.

// 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:
// 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:

// 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:

// 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.

// 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):

// 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: