Midtrans-Middleware/src/services/api.ts

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
,
}
}
}