Some checks failed
build-and-release / build (push) Failing after 0s
- Inject Bearer token from sessionStorage into all API requests - Add typed API helpers for invoices, credits, discounts, ledger, orders, pricing, tax - Overhaul invoice, credit, pricing, tax admin routes with full CRUD UI - Remove committed log files Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
252 lines
9.9 KiB
TypeScript
252 lines
9.9 KiB
TypeScript
import { Show, createSignal, onMount } from "solid-js";
|
|
|
|
type PaymentGatewayConfig = {
|
|
provider: string;
|
|
mode: 'sandbox' | 'live';
|
|
enabled: boolean;
|
|
baseUrl: string;
|
|
callbackUrl: string;
|
|
webhookUrl: string;
|
|
merchantId: string;
|
|
apiKey: string;
|
|
secretKey: string;
|
|
clientId: string;
|
|
clientSecret: string;
|
|
};
|
|
|
|
const DEFAULT_CONFIG: PaymentGatewayConfig = {
|
|
provider: 'PayU',
|
|
mode: 'sandbox',
|
|
enabled: true,
|
|
baseUrl: 'https://test.payu.in',
|
|
callbackUrl: '',
|
|
webhookUrl: '',
|
|
merchantId: '',
|
|
apiKey: '',
|
|
secretKey: '',
|
|
clientId: '',
|
|
clientSecret: '',
|
|
};
|
|
|
|
const READ_ENDPOINTS = [
|
|
'/api/admin/payment-gateway-config',
|
|
];
|
|
|
|
const WRITE_ENDPOINTS = [
|
|
'/api/admin/payment-gateway-config',
|
|
];
|
|
|
|
function authHeaders() {
|
|
const token = typeof sessionStorage !== 'undefined'
|
|
? (sessionStorage.getItem('nxtgauge_admin_access_token') || '')
|
|
: '';
|
|
return {
|
|
'Content-Type': 'application/json',
|
|
Accept: 'application/json',
|
|
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
};
|
|
}
|
|
|
|
function normalizePayload(payload: any): PaymentGatewayConfig {
|
|
const src = payload?.config || payload?.data || payload || {};
|
|
return {
|
|
provider: String(src.provider || DEFAULT_CONFIG.provider),
|
|
mode: String(src.mode || DEFAULT_CONFIG.mode).toLowerCase() === 'live' ? 'live' : 'sandbox',
|
|
enabled: src.enabled !== false,
|
|
baseUrl: String(src.baseUrl || src.base_url || ''),
|
|
callbackUrl: String(src.callbackUrl || src.callback_url || ''),
|
|
webhookUrl: String(src.webhookUrl || src.webhook_url || ''),
|
|
merchantId: String(src.merchantId || src.merchant_id || ''),
|
|
apiKey: String(src.apiKey || src.api_key || ''),
|
|
secretKey: String(src.secretKey || src.secret_key || ''),
|
|
clientId: String(src.clientId || src.client_id || ''),
|
|
clientSecret: String(src.clientSecret || src.client_secret || ''),
|
|
};
|
|
}
|
|
|
|
export default function PaymentGatewayManagementPage() {
|
|
const [loading, setLoading] = createSignal(false);
|
|
const [saving, setSaving] = createSignal(false);
|
|
const [error, setError] = createSignal('');
|
|
const [success, setSuccess] = createSignal('');
|
|
const [showSecret, setShowSecret] = createSignal(false);
|
|
const [showClientSecret, setShowClientSecret] = createSignal(false);
|
|
const [cfg, setCfg] = createSignal<PaymentGatewayConfig>(DEFAULT_CONFIG);
|
|
|
|
const load = async () => {
|
|
setLoading(true);
|
|
setError('');
|
|
try {
|
|
let loaded = false;
|
|
for (const endpoint of READ_ENDPOINTS) {
|
|
const res = await fetch(endpoint, { method: 'GET', headers: authHeaders(), credentials: 'include' }).catch(() => null);
|
|
if (!res || !res.ok) continue;
|
|
const payload = await res.json().catch(() => ({}));
|
|
setCfg(normalizePayload(payload));
|
|
loaded = true;
|
|
break;
|
|
}
|
|
if (!loaded) setCfg(DEFAULT_CONFIG);
|
|
} catch (e: any) {
|
|
setError(e?.message || 'Failed to load payment gateway configuration.');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
onMount(() => void load());
|
|
|
|
const setField = <K extends keyof PaymentGatewayConfig>(key: K, value: PaymentGatewayConfig[K]) => {
|
|
setCfg((prev) => ({ ...prev, [key]: value }));
|
|
};
|
|
|
|
const save = async (e: Event) => {
|
|
e.preventDefault();
|
|
setSaving(true);
|
|
setError('');
|
|
setSuccess('');
|
|
try {
|
|
const payload = {
|
|
provider: cfg().provider.trim(),
|
|
mode: cfg().mode,
|
|
enabled: cfg().enabled,
|
|
base_url: cfg().baseUrl.trim(),
|
|
callback_url: cfg().callbackUrl.trim(),
|
|
webhook_url: cfg().webhookUrl.trim(),
|
|
merchant_id: cfg().merchantId.trim(),
|
|
api_key: cfg().apiKey.trim(),
|
|
secret_key: cfg().secretKey.trim(),
|
|
client_id: cfg().clientId.trim(),
|
|
client_secret: cfg().clientSecret.trim(),
|
|
};
|
|
|
|
let saved = false;
|
|
for (const endpoint of WRITE_ENDPOINTS) {
|
|
const methods: Array<'PUT' | 'PATCH' | 'POST'> = ['PUT', 'PATCH', 'POST'];
|
|
for (const method of methods) {
|
|
const res = await fetch(endpoint, {
|
|
method,
|
|
headers: authHeaders(),
|
|
credentials: 'include',
|
|
body: JSON.stringify(payload),
|
|
}).catch(() => null);
|
|
if (!res || !res.ok) continue;
|
|
saved = true;
|
|
break;
|
|
}
|
|
if (saved) break;
|
|
}
|
|
|
|
if (!saved) throw new Error('Could not save configuration. Please verify backend endpoint wiring.');
|
|
setSuccess('Payment gateway configuration saved successfully.');
|
|
await load();
|
|
} catch (e: any) {
|
|
setError(e?.message || 'Failed to save payment gateway configuration.');
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
const inputCls = 'w-full rounded-lg border border-gray-200 px-3 py-2 text-sm outline-none focus:border-[#FF5E13] focus:ring-1 focus:ring-[#FF5E13]';
|
|
const labelCls = 'mb-1.5 block text-sm font-medium text-gray-700';
|
|
|
|
return (
|
|
<div class="w-full space-y-6 pb-8">
|
|
<div style="margin-bottom:1.5rem">
|
|
<h1 class="text-[28px] font-bold leading-tight text-[#111827]">Payment Gateway Management</h1>
|
|
<p class="mt-1 text-[14px] text-[#6B7280]">Manage PayU credentials, mode, and callback URLs for platform payments.</p>
|
|
</div>
|
|
|
|
<Show when={error()}>
|
|
<div class="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{error()}</div>
|
|
</Show>
|
|
<Show when={success()}>
|
|
<div class="rounded-lg border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-700">{success()}</div>
|
|
</Show>
|
|
|
|
<section class="rounded-xl border border-gray-200 bg-white shadow-sm p-6">
|
|
<Show when={loading()}>
|
|
<p class="text-sm text-gray-500">Loading configuration...</p>
|
|
</Show>
|
|
|
|
<form onSubmit={save} class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
<div>
|
|
<label class={labelCls}>Provider</label>
|
|
<input class={inputCls} value={cfg().provider} onInput={(e) => setField('provider', e.currentTarget.value)} placeholder="PayU" />
|
|
</div>
|
|
<div>
|
|
<label class={labelCls}>Mode</label>
|
|
<select class={inputCls} value={cfg().mode} onChange={(e) => setField('mode', e.currentTarget.value === 'live' ? 'live' : 'sandbox')}>
|
|
<option value="sandbox">Sandbox (Test)</option>
|
|
<option value="live">Live (Production)</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class={labelCls}>Base API URL</label>
|
|
<input class={inputCls} value={cfg().baseUrl} onInput={(e) => setField('baseUrl', e.currentTarget.value)} placeholder="https://test.payu.in" />
|
|
</div>
|
|
<div>
|
|
<label class={labelCls}>Merchant Key</label>
|
|
<input class={inputCls} value={cfg().apiKey} onInput={(e) => setField('apiKey', e.currentTarget.value)} placeholder="PayU merchant key" />
|
|
</div>
|
|
<div>
|
|
<label class={labelCls}>Merchant Salt</label>
|
|
<div class="flex gap-2">
|
|
<input
|
|
type={showSecret() ? 'text' : 'password'}
|
|
class={inputCls}
|
|
value={cfg().secretKey}
|
|
onInput={(e) => setField('secretKey', e.currentTarget.value)}
|
|
placeholder="PayU merchant salt"
|
|
/>
|
|
<button type="button" class="rounded-lg border border-gray-200 px-3 py-2 text-xs font-semibold text-gray-700 hover:bg-gray-50" onClick={() => setShowSecret((v) => !v)}>
|
|
{showSecret() ? 'Hide' : 'Show'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label class={labelCls}>Client ID (optional)</label>
|
|
<input class={inputCls} value={cfg().clientId} onInput={(e) => setField('clientId', e.currentTarget.value)} placeholder="PayU OAuth client id" />
|
|
</div>
|
|
<div>
|
|
<label class={labelCls}>Client Secret (optional)</label>
|
|
<div class="flex gap-2">
|
|
<input
|
|
type={showClientSecret() ? 'text' : 'password'}
|
|
class={inputCls}
|
|
value={cfg().clientSecret}
|
|
onInput={(e) => setField('clientSecret', e.currentTarget.value)}
|
|
placeholder="PayU OAuth client secret"
|
|
/>
|
|
<button type="button" class="rounded-lg border border-gray-200 px-3 py-2 text-xs font-semibold text-gray-700 hover:bg-gray-50" onClick={() => setShowClientSecret((v) => !v)}>
|
|
{showClientSecret() ? 'Hide' : 'Show'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label class={labelCls}>Callback URL (surl/furl)</label>
|
|
<input class={inputCls} value={cfg().callbackUrl} onInput={(e) => setField('callbackUrl', e.currentTarget.value)} placeholder="https://yourapp.com/api/payments/payu-return" />
|
|
</div>
|
|
<div>
|
|
<label class={labelCls}>Webhook URL</label>
|
|
<input class={inputCls} value={cfg().webhookUrl} onInput={(e) => setField('webhookUrl', e.currentTarget.value)} placeholder="https://yourapp.com/api/payments/payu/webhook" />
|
|
</div>
|
|
<div class="sm:col-span-2">
|
|
<label class="inline-flex items-center gap-2 text-sm text-gray-700">
|
|
<input type="checkbox" checked={cfg().enabled} onChange={(e) => setField('enabled', e.currentTarget.checked)} />
|
|
Enable payment gateway
|
|
</label>
|
|
</div>
|
|
<div class="sm:col-span-2 flex items-center justify-end gap-2 border-t border-gray-100 pt-4">
|
|
<button type="button" class="rounded-lg border border-gray-200 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50" onClick={() => void load()}>
|
|
Reload
|
|
</button>
|
|
<button type="submit" class="rounded-lg bg-[#0D0D2A] px-4 py-2 text-sm font-semibold text-white hover:bg-[#17173f]" disabled={saving()}>
|
|
{saving() ? 'Saving...' : 'Save Configuration'}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</section>
|
|
</div>
|
|
);
|
|
}
|