import axios from 'axios' import { Env } from '../lib/env' import { Logger } from '../lib/logger' import { normalizeMidtransStatus, type MidtransStatusResponse, type PaymentStatusResponse, } from '../features/payments/lib/midtrans' // Normalize base URL to ensure a single "/api" segment and no trailing slash function computeApiBase(): string | undefined { const raw = (Env.API_BASE_URL || '').trim() const normalized = raw.replace(/\/+$/g, '') let base = normalized ? (/\/api$/i.test(normalized) ? normalized : `${normalized}/api`) : undefined if (!base) { // Dev fallback: if running locally and backend on default port 8000, use it try { const { hostname } = window.location const isLocal = hostname === 'localhost' || hostname === '127.0.0.1' if (isLocal) { base = 'http://localhost:8000/api' Logger.warn('api.base.fallback', { base }) } } catch { // noop: window not available } } return base } const apiBase = computeApiBase() export const api = axios.create({ baseURL: apiBase }) // Axios interceptors for logging api.interceptors.request.use((config) => { const method = (config.method || 'GET').toUpperCase() const url = `${config.baseURL || ''}${config.url || ''}` Logger.info('api.request', { method, url }) if (Env.LOG_LEVEL === 'debug') Logger.debug('api.request.data', Logger.mask(config.data)) return config }) api.interceptors.response.use( (response) => { const url = response.config?.url || '' Logger.info('api.response', { url, status: response.status }) if (Env.LOG_LEVEL === 'debug') Logger.debug('api.response.data', response.data) return response }, (error) => { const baseURL = error.config?.baseURL || '' const url = error.config?.url || '' const status = error.response?.status const fullUrl = `${baseURL}${url}` Logger.error('api.error', { baseURL, url, fullUrl, status, message: error.message }) throw error } ) export async function getPaymentStatus(orderId: string): Promise { if (apiBase) { const { data } = await api.get(`/payments/${orderId}/status`) // If backend returns Midtrans status response (pass-through), normalize it. if (data && typeof data === 'object' && 'transaction_status' in data) { return normalizeMidtransStatus(data as MidtransStatusResponse) } // Otherwise, assume backend already returns app-level shape. return data as PaymentStatusResponse } // Fallback stub when API base not set return { orderId, status: 'pending' } } export async function postCharge(payload: Record): Promise { if (!apiBase) throw new Error('API base URL not configured (set VITE_API_BASE_URL or use local fallback)') Logger.info('charge.start', { payment_type: payload?.payment_type }) if (Env.LOG_LEVEL === 'debug') Logger.debug('charge.payload', Logger.mask(payload)) const { data } = await api.post('/payments/charge', payload) Logger.info('charge.done', { order_id: data?.order_id, status_code: data?.status_code }) return data } export type RuntimeConfigResponse = { paymentToggles: { bank_transfer: boolean credit_card: boolean gopay: boolean cstore: boolean cpay?: boolean } midtransEnv?: 'production' | 'sandbox' clientKey?: string } export async function getRuntimeConfig(): Promise { if (apiBase) { const { data } = await api.get('/config') Logger.info('config.runtime.loaded') if (Env.LOG_LEVEL === 'debug') Logger.debug('config.runtime.data', data) return data as RuntimeConfigResponse } // Fallback when API base not set: use build-time Env toggles Logger.warn('config.runtime.fallback', { reason: 'API base not set' }) return { paymentToggles: { bank_transfer: Env.ENABLE_BANK_TRANSFER, credit_card: Env.ENABLE_CREDIT_CARD, gopay: Env.ENABLE_GOPAY, cstore: Env.ENABLE_CSTORE, cpay: Env.ENABLE_CPAY, }, midtransEnv: Env.MIDTRANS_ENV, clientKey: Env.MIDTRANS_CLIENT_KEY, } } export type PaymentLinkPayload = { order_id: string nominal: number customer?: { name?: string; phone?: string; email?: string } expire_at?: number allowed_methods?: string[] } export async function getPaymentLinkPayload(token: string): Promise { if (apiBase) { const { data } = await api.get(`/payment-links/${encodeURIComponent(token)}`) Logger.info('paymentlink.resolve', { tokenLen: token.length }) return data as PaymentLinkPayload } // Fallback when API base not set or resolver unavailable // Try a best-effort decode of base64(JSON) payload; if fails, use defaults try { const json = JSON.parse(atob(token)) return { order_id: json.order_id || token, nominal: Number(json.nominal) || 150000, customer: json.customer || {}, expire_at: json.expire_at || Date.now() + 24 * 60 * 60 * 1000 , allowed_methods: json.allowed_methods || undefined, } } catch { return { order_id: token, nominal: 150000, expire_at: Date.now() + 24 * 60 * 60 * 1000 , } } }