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:
19
backend/src/routes/public/client.payment-methods.routes.js
Normal file
19
backend/src/routes/public/client.payment-methods.routes.js
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* App-facing payment catalog endpoint.
|
||||
*
|
||||
* GET /api/client/payment-methods → { groups: [ ... ] }
|
||||
*
|
||||
* Cached + active-filtered (see payment-catalog.service.js::getCatalogForApp).
|
||||
* Authenticated; the customer app already has a JWT by the time it reaches
|
||||
* the payment-method screen.
|
||||
*/
|
||||
|
||||
import { authenticate } from '../../plugins/auth.js'
|
||||
import { getCatalogForApp } from '../../services/payment-catalog.service.js'
|
||||
|
||||
export const clientPaymentMethodsRoutes = (app) => {
|
||||
app.get('/', { preHandler: authenticate }, async (_request, reply) => {
|
||||
const catalog = await getCatalogForApp()
|
||||
return reply.send({ success: true, data: catalog })
|
||||
})
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
readFirstSessionDiscountConfig,
|
||||
} from '../../services/pricing.service.js'
|
||||
import { getXenditConfig } from '../../services/config.service.js'
|
||||
import { findActiveMethodByCode } from '../../services/payment-catalog.service.js'
|
||||
import { UserType, SessionMode } from '../../constants.js'
|
||||
|
||||
const resolveCustomer = async (request, reply) => {
|
||||
@@ -51,8 +52,32 @@ export const clientPaymentRoutes = async (app) => {
|
||||
mode = SessionMode.CHAT,
|
||||
targeted_mitra_id = null,
|
||||
is_extension = false,
|
||||
method = null,
|
||||
} = request.body ?? {}
|
||||
|
||||
// Catalog validation — the customer's pre-pick (set in payment_method_screen.dart)
|
||||
// must reference an active row in `payment_methods`. Casing-tolerant; older app
|
||||
// versions sending lower-case (`qris`) are normalised inside the service.
|
||||
// `method` is optional for backwards compat with pre-Phase-5.x callers.
|
||||
if (method !== null && method !== undefined) {
|
||||
if (typeof method !== 'string' || method.trim().length === 0) {
|
||||
return reply.code(422).send({
|
||||
success: false,
|
||||
error: { code: 'VALIDATION_ERROR', message: 'method must be a non-empty string when provided' },
|
||||
})
|
||||
}
|
||||
const entry = await findActiveMethodByCode(method)
|
||||
if (!entry) {
|
||||
return reply.code(422).send({
|
||||
success: false,
|
||||
error: {
|
||||
code: 'INVALID_PAYMENT_METHOD',
|
||||
message: 'Selected payment method is not available',
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof duration_minutes !== 'number' || duration_minutes <= 0) {
|
||||
return reply.code(422).send({
|
||||
success: false,
|
||||
@@ -116,6 +141,7 @@ export const clientPaymentRoutes = async (app) => {
|
||||
isExtension: Boolean(is_extension),
|
||||
targetedMitraId: targeted_mitra_id || null,
|
||||
mode,
|
||||
preferredPaymentCode: method ? String(method).toUpperCase() : null,
|
||||
})
|
||||
|
||||
return reply.code(201).send({
|
||||
|
||||
Reference in New Issue
Block a user