150 lines
4.9 KiB
TypeScript
150 lines
4.9 KiB
TypeScript
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<PaymentStatusResponse> {
|
|
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<string, any>): Promise<any> {
|
|
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<RuntimeConfigResponse> {
|
|
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<PaymentLinkPayload> {
|
|
if (apiBase) {
|
|
const { data } = await api.get(`/payment-links/${encodeURIComponent(token)}`)
|
|
Logger.info('paymentlink.resolve', { tokenLen: token.length })
|
|
return data as PaymentLinkPayload
|
|
}
|
|
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
|
|
,
|
|
}
|
|
}
|
|
} |