Phase 5.x payment catalog + customer-app splash/register polish
Payment catalog (Phase 5.x — see requirement/phase5-payment-catalog-plan.md):
- New tables payment_method_groups + payment_methods with seed (3 groups,
10 methods; GoPay seeded inactive pending Xendit channel confirmation).
- payment-catalog.service.js with two-layer cache (60s in-process + 1h
Valkey) and config:invalidate pub/sub fanout. Mutator API + casing-
tolerant findActiveMethodByCode for downstream validation.
- App-facing GET /api/client/payment-methods returns pre-grouped JSON,
active-only, empty groups dropped server-side.
- POST /api/client/payment-requests now validates `method` against the
catalog (INVALID_PAYMENT_METHOD 422) and stamps
product_metadata.preferred_payment_code (upper-cased).
- Control-center /internal/payment-{groups,methods}{,/:id,/reorder}
endpoints (full CRUD + idempotent reorder). New Payment Catalog page
wired into the CC nav.
- Customer app renders the catalog as collapsible groups (first expanded)
via paymentCatalogProvider; QRIS-only hardcoded fallback on 5xx so
checkout never hard-fails. Replaces the hardcoded _PayMethod enum.
- 10 brand SVGs (~63KB) bundled in client_app/assets/payment_icons/ from
github.com/hafidznoor/idn-finlogos. Xendit's per-channel media-asset
pages were planned but found decommissioned during implementation —
switched to idn-finlogos with the standard "channels-we-accept"
trademark posture. See assets/payment_icons/README.md for the workflow
to add new methods.
- 16 vitest cases covering the service + cache; full backend suite green
(162/162).
Customer-app splash + register polish:
- Splash rewritten per figma S1: warm vertical gradient, two ImageFiltered
radial orbs, 96×96 rounded-square logo tile, "HaloBestie" + "kamu gak
harus ngerasain ini sendirian." Self-driving navigation via context.go
after a 2.5s post-frame timer (native Android splash burns ~1-1.5s
before Flutter paints — 1s timer yielded near-zero visible duration).
Router early-returns null for isSplash so it never moves us off /splash
on its own.
- 3-page onboarding carousel removed: user clarified the new splash
REPLACES that carousel. Dropped /onboarding route, OnboardingScreen,
onboardingDoneProvider + gating, dead splash_{1,2,3}.png + the
splash_chat_hebat.png Flutter asset. Phase 4 /onboarding/* subroutes
untouched; Android-native launch_background drawable left alone.
- Register screen (login-by-phone) polished: circular pink back button +
72×72 logo badge (same brandLogoBg pink as splash, Transform.scale 1.4
to fill the tile). Step-dots indicator removed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1145,6 +1145,118 @@ const migrate = async () => {
|
||||
WHERE payment_request_id IS NOT NULL
|
||||
`
|
||||
|
||||
// --- Phase 5.x: Payment catalog (groups + methods) ----------------------
|
||||
//
|
||||
// DB-backed payment method catalog edited from control center, cached in
|
||||
// Valkey by payment-catalog.service.js. Replaces the hardcoded _PayMethod
|
||||
// enum in client_app/lib/features/payment/screens/payment_method_screen.dart.
|
||||
// See requirement/phase5-payment-catalog-plan.md.
|
||||
//
|
||||
// `payment_code` stores the Xendit channel code (OVO, DANA, QRIS, BCA_VA,
|
||||
// …) verbatim — no mapping layer. UPPER-CASE by convention; the service
|
||||
// layer normalises incoming app-submitted codes to upper before lookup.
|
||||
|
||||
await sql`
|
||||
CREATE TABLE IF NOT EXISTS payment_method_groups (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name TEXT NOT NULL,
|
||||
display_order INTEGER NOT NULL DEFAULT 0,
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
)
|
||||
`
|
||||
|
||||
await sql`
|
||||
CREATE INDEX IF NOT EXISTS idx_payment_method_groups_order
|
||||
ON payment_method_groups (display_order)
|
||||
`
|
||||
|
||||
await sql`
|
||||
CREATE TABLE IF NOT EXISTS payment_methods (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
group_id UUID NOT NULL REFERENCES payment_method_groups(id) ON DELETE RESTRICT,
|
||||
display_name TEXT NOT NULL,
|
||||
payment_code TEXT NOT NULL UNIQUE,
|
||||
display_order INTEGER NOT NULL DEFAULT 0,
|
||||
icon TEXT,
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
)
|
||||
`
|
||||
|
||||
await sql`
|
||||
CREATE INDEX IF NOT EXISTS idx_payment_methods_group_order
|
||||
ON payment_methods (group_id, display_order)
|
||||
`
|
||||
|
||||
// Seed: only when the groups table is empty. Once seeded, operators edit via
|
||||
// CC; we never re-seed (avoid clobbering custom orderings).
|
||||
const [{ n: groupCount }] = await sql`
|
||||
SELECT COUNT(*)::int AS n FROM payment_method_groups
|
||||
`
|
||||
if (groupCount === 0) {
|
||||
const PAYMENT_CATALOG_SEED = [
|
||||
{
|
||||
name: 'Paling Cepat',
|
||||
order: 0,
|
||||
methods: [
|
||||
{ code: 'QRIS', display: 'QRIS', icon: 'qris', active: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'E-Wallet',
|
||||
order: 1,
|
||||
methods: [
|
||||
{ code: 'OVO', display: 'OVO', icon: 'ovo', active: true },
|
||||
{ code: 'DANA', display: 'DANA', icon: 'dana', active: true },
|
||||
{ code: 'SHOPEEPAY', display: 'ShopeePay', icon: 'shopeepay', active: true },
|
||||
// Xendit Invoice API doesn't expose GoPay (Gojek/GoPay relationship).
|
||||
// Seeded as inactive so it surfaces in CC but is hidden from the app
|
||||
// until we either confirm a Xendit channel or remove it entirely.
|
||||
{ code: 'GOPAY', display: 'GoPay', icon: 'gopay', active: false },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Virtual Account',
|
||||
order: 2,
|
||||
methods: [
|
||||
{ code: 'BCA_VA', display: 'BCA Virtual Account', icon: 'bca', active: true },
|
||||
{ code: 'MANDIRI_VA', display: 'Mandiri Virtual Account', icon: 'mandiri', active: true },
|
||||
{ code: 'BNI_VA', display: 'BNI Virtual Account', icon: 'bni', active: true },
|
||||
{ code: 'BRI_VA', display: 'BRI Virtual Account', icon: 'bri', active: true },
|
||||
{ code: 'PERMATA_VA', display: 'Permata Virtual Account', icon: 'permata', active: true },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
for (const group of PAYMENT_CATALOG_SEED) {
|
||||
const [{ id: groupId }] = await sql`
|
||||
INSERT INTO payment_method_groups (name, display_order, is_active)
|
||||
VALUES (${group.name}, ${group.order}, true)
|
||||
RETURNING id
|
||||
`
|
||||
let methodOrder = 0
|
||||
for (const m of group.methods) {
|
||||
await sql`
|
||||
INSERT INTO payment_methods (
|
||||
group_id, display_name, payment_code, display_order, icon, is_active
|
||||
)
|
||||
VALUES (
|
||||
${groupId},
|
||||
${m.display},
|
||||
${m.code},
|
||||
${methodOrder++},
|
||||
${m.icon},
|
||||
${m.active}
|
||||
)
|
||||
ON CONFLICT (payment_code) DO NOTHING
|
||||
`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Migration complete.')
|
||||
await sql.end()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user