paymentservice/templates/payments/portal.html
2026-10-18 22:00:00 +05:30

2144 lines
81 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tradhox Payment Engine - Test & Operations Portal</title>
<meta name="description" content="Operations and Testing Portal for Tradhox Multi-Vendor Payment Engine Microservice">
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&family=Outfit:wght@500;600;700;800&display=swap" rel="stylesheet">
<!-- Cashfree Checkout SDK v3 -->
<script src="https://sdk.cashfree.com/js/v3/cashfree.js"></script>
<style>
:root {
--bg-primary: #07090E;
--bg-surface: #0E131F;
--bg-surface-elevated: #161C2E;
--bg-glass: rgba(18, 24, 40, 0.7);
--border-subtle: rgba(255, 255, 255, 0.08);
--border-accent: rgba(99, 102, 241, 0.3);
--border-cashfree: rgba(236, 72, 153, 0.35);
--text-main: #F3F4F6;
--text-muted: #9CA3AF;
--text-dim: #6B7280;
--primary: #6366F1;
--primary-hover: #4F46E5;
--primary-glow: rgba(99, 102, 241, 0.25);
--cashfree-color: #EC4899;
--cashfree-glow: rgba(236, 72, 153, 0.25);
--success: #10B981;
--success-glow: rgba(16, 185, 129, 0.2);
--warning: #F59E0B;
--warning-glow: rgba(245, 158, 11, 0.2);
--danger: #EF4444;
--danger-glow: rgba(239, 68, 68, 0.2);
--font-body: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
--font-display: 'Outfit', sans-serif;
--font-mono: 'JetBrains Mono', monospace;
--radius-sm: 8px;
--radius-md: 14px;
--radius-lg: 20px;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: var(--bg-primary);
color: var(--text-main);
font-family: var(--font-body);
line-height: 1.5;
min-height: 100vh;
display: flex;
flex-direction: column;
overflow-x: hidden;
background-image:
radial-gradient(circle at 15% 10%, rgba(99, 102, 241, 0.12) 0%, transparent 40%),
radial-gradient(circle at 85% 85%, rgba(139, 92, 246, 0.08) 0%, transparent 45%);
}
/* Top Navigation */
header {
background: var(--bg-glass);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border-bottom: 1px solid var(--border-subtle);
position: sticky;
top: 0;
z-index: 100;
padding: 16px 32px;
display: flex;
justify-content: space-between;
align-items: center;
}
.brand {
display: flex;
align-items: center;
gap: 14px;
}
.brand-logo {
width: 40px;
height: 40px;
background: linear-gradient(135deg, #6366F1, #8B5CF6);
border-radius: var(--radius-sm);
display: flex;
align-items: center;
justify-content: center;
font-family: var(--font-display);
font-weight: 800;
font-size: 20px;
box-shadow: 0 4px 16px var(--primary-glow);
}
.brand-title {
font-family: var(--font-display);
font-size: 20px;
font-weight: 700;
letter-spacing: -0.02em;
}
.brand-subtitle {
font-size: 11px;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.08em;
}
.header-pills {
display: flex;
align-items: center;
gap: 12px;
}
.pill {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 14px;
border-radius: 9999px;
font-size: 12px;
font-weight: 600;
background: var(--bg-surface-elevated);
border: 1px solid var(--border-subtle);
}
.pill-pulse {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--success);
box-shadow: 0 0 10px var(--success);
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { transform: scale(0.95); opacity: 0.8; }
50% { transform: scale(1.2); opacity: 1; }
100% { transform: scale(0.95); opacity: 0.8; }
}
/* Main Container */
main {
flex: 1;
max-width: 1440px;
width: 100%;
margin: 0 auto;
padding: 32px 32px 64px 32px;
display: flex;
flex-direction: column;
gap: 32px;
}
/* Top Level Engine Mode Switcher */
.mode-switcher-container {
display: flex;
gap: 8px;
background: var(--bg-surface);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-md);
padding: 6px;
max-width: 720px;
width: 100%;
margin: 0 auto;
}
.mode-btn {
flex: 1;
background: transparent;
border: none;
color: var(--text-muted);
padding: 12px 20px;
border-radius: var(--radius-sm);
font-size: 14px;
font-weight: 700;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
transition: all 0.25s ease;
font-family: var(--font-display);
}
.mode-btn:hover {
color: var(--text-main);
background: rgba(255, 255, 255, 0.04);
}
.mode-btn.active.mode-orchestrator {
color: #FFFFFF;
background: linear-gradient(135deg, #4F46E5, #6366F1);
box-shadow: 0 4px 16px var(--primary-glow);
}
.mode-btn.active.mode-cashfree {
color: #FFFFFF;
background: linear-gradient(135deg, #DB2777, #EC4899);
box-shadow: 0 4px 16px var(--cashfree-glow);
}
/* Metric Grid */
.metrics-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 16px;
}
.metric-card {
background: var(--bg-surface);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-md);
padding: 20px;
display: flex;
flex-direction: column;
gap: 8px;
transition: transform 0.2s ease, border-color 0.2s ease;
}
.metric-card:hover {
transform: translateY(-2px);
border-color: var(--border-accent);
}
.metric-label {
font-size: 12px;
color: var(--text-muted);
text-transform: uppercase;
font-weight: 600;
letter-spacing: 0.05em;
}
.metric-value {
font-family: var(--font-display);
font-size: 28px;
font-weight: 700;
color: var(--text-main);
}
.metric-trend {
font-size: 12px;
color: var(--success);
display: flex;
align-items: center;
gap: 4px;
}
/* Section Headers */
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.section-title {
font-family: var(--font-display);
font-size: 18px;
font-weight: 700;
display: flex;
align-items: center;
gap: 10px;
}
/* Gateway Circuit Breaker Cards */
.gateways-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
gap: 20px;
}
.gateway-card {
background: var(--bg-surface);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-md);
padding: 24px;
display: flex;
flex-direction: column;
gap: 16px;
position: relative;
overflow: hidden;
}
.gateway-card.active {
border-color: var(--border-accent);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
}
.gateway-card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.gateway-info h3 {
font-size: 16px;
font-weight: 700;
margin-bottom: 4px;
}
.gateway-info p {
font-size: 12px;
color: var(--text-muted);
}
.badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
border-radius: 9999px;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.badge-healthy {
background: var(--success-glow);
color: #34D399;
border: 1px solid rgba(16, 185, 129, 0.3);
}
.badge-degraded {
background: var(--warning-glow);
color: #FBBF24;
border: 1px solid rgba(245, 158, 11, 0.3);
}
.badge-down {
background: var(--danger-glow);
color: #F87171;
border: 1px solid rgba(239, 68, 68, 0.3);
}
.gateway-stats-row {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
background: var(--bg-surface-elevated);
padding: 12px;
border-radius: var(--radius-sm);
}
.gateway-stat-item {
display: flex;
flex-direction: column;
gap: 2px;
}
.gateway-stat-label {
font-size: 11px;
color: var(--text-dim);
}
.gateway-stat-val {
font-size: 13px;
font-weight: 600;
}
.gateway-controls {
display: flex;
align-items: center;
gap: 8px;
}
/* Sandbox Interactive Console Tabs */
.sandbox-container {
background: var(--bg-surface);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-lg);
overflow: hidden;
display: flex;
flex-direction: column;
}
.tab-bar {
display: flex;
gap: 4px;
padding: 12px 20px;
background: var(--bg-surface-elevated);
border-bottom: 1px solid var(--border-subtle);
overflow-x: auto;
}
.tab-btn {
background: transparent;
border: none;
color: var(--text-muted);
padding: 8px 18px;
border-radius: var(--radius-sm);
font-size: 13px;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.2s ease;
white-space: nowrap;
}
.tab-btn:hover {
color: var(--text-main);
background: rgba(255, 255, 255, 0.04);
}
.tab-btn.active {
color: #FFFFFF;
background: var(--primary);
box-shadow: 0 4px 12px var(--primary-glow);
}
.tab-btn.active.tab-cashfree {
background: var(--cashfree-color);
box-shadow: 0 4px 12px var(--cashfree-glow);
}
.tab-content {
padding: 28px;
display: none;
}
.tab-content.active {
display: block;
}
/* Form Layout */
.form-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 20px;
margin-bottom: 24px;
}
.form-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.form-group label {
font-size: 12px;
font-weight: 600;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.04em;
}
.input-wrapper {
position: relative;
display: flex;
align-items: center;
}
.form-control {
width: 100%;
background: var(--bg-surface-elevated);
border: 1px solid var(--border-subtle);
color: var(--text-main);
padding: 10px 14px;
border-radius: var(--radius-sm);
font-size: 14px;
font-family: inherit;
outline: none;
transition: border-color 0.2s;
}
.form-control:focus {
border-color: var(--primary);
box-shadow: 0 0 0 2px var(--primary-glow);
}
.input-addon-btn {
position: absolute;
right: 8px;
background: rgba(255, 255, 255, 0.08);
border: none;
color: var(--text-muted);
padding: 4px 8px;
border-radius: 4px;
font-size: 11px;
cursor: pointer;
}
.input-addon-btn:hover {
color: #FFF;
background: rgba(255, 255, 255, 0.16);
}
/* Buttons */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 10px 20px;
border-radius: var(--radius-sm);
font-size: 13px;
font-weight: 600;
cursor: pointer;
border: 1px solid transparent;
transition: all 0.2s ease;
text-decoration: none;
}
.btn-primary {
background: var(--primary);
color: #FFFFFF;
box-shadow: 0 4px 16px var(--primary-glow);
}
.btn-primary:hover {
background: var(--primary-hover);
transform: translateY(-1px);
}
.btn-cashfree {
background: var(--cashfree-color);
color: #FFFFFF;
box-shadow: 0 4px 16px var(--cashfree-glow);
}
.btn-cashfree:hover {
background: #DB2777;
transform: translateY(-1px);
}
.btn-secondary {
background: var(--bg-surface-elevated);
color: var(--text-main);
border-color: var(--border-subtle);
}
.btn-secondary:hover {
background: rgba(255, 255, 255, 0.08);
}
.btn-danger {
background: rgba(239, 68, 68, 0.15);
color: #F87171;
border-color: rgba(239, 68, 68, 0.3);
}
.btn-danger:hover {
background: rgba(239, 68, 68, 0.25);
}
.btn-success {
background: rgba(16, 185, 129, 0.15);
color: #34D399;
border-color: rgba(16, 185, 129, 0.3);
}
.btn-sm {
padding: 6px 12px;
font-size: 11px;
}
/* Terminal / JSON Code Preview */
.console-box {
background: #04060A;
border: 1px solid var(--border-subtle);
border-radius: var(--radius-sm);
padding: 16px;
font-family: var(--font-mono);
font-size: 12px;
color: #A5B4FC;
max-height: 380px;
overflow-y: auto;
white-space: pre-wrap;
word-break: break-all;
position: relative;
}
.console-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
padding-bottom: 6px;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
font-size: 11px;
color: var(--text-dim);
}
/* Table styling */
.table-container {
overflow-x: auto;
border: 1px solid var(--border-subtle);
border-radius: var(--radius-sm);
}
table {
width: 100%;
border-collapse: collapse;
text-align: left;
font-size: 13px;
}
th {
background: var(--bg-surface-elevated);
padding: 12px 16px;
color: var(--text-muted);
font-weight: 600;
border-bottom: 1px solid var(--border-subtle);
text-transform: uppercase;
font-size: 11px;
letter-spacing: 0.05em;
}
td {
padding: 12px 16px;
border-bottom: 1px solid var(--border-subtle);
color: var(--text-main);
}
tr:last-child td {
border-bottom: none;
}
tr:hover td {
background: rgba(255, 255, 255, 0.02);
}
/* Modal Checkout Overlay */
.checkout-drawer {
margin-top: 24px;
padding: 20px;
border-radius: var(--radius-md);
background: var(--bg-surface-elevated);
border: 1px solid var(--border-accent);
display: none;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
.drawer-title {
font-family: var(--font-display);
font-size: 16px;
font-weight: 700;
margin-bottom: 6px;
color: #FFF;
}
.drawer-actions {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-top: 16px;
}
/* Dedicated QR Container */
.qr-display-box {
background: #0E131F;
border: 1px solid var(--border-cashfree);
border-radius: var(--radius-md);
padding: 24px;
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
margin-top: 20px;
display: none;
}
.qr-image-frame {
background: #FFFFFF;
padding: 16px;
border-radius: var(--radius-sm);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
}
.qr-image-frame img {
width: 220px;
height: 220px;
display: block;
}
/* Notifications / Toast */
#toast-container {
position: fixed;
bottom: 24px;
right: 24px;
z-index: 1000;
display: flex;
flex-direction: column;
gap: 10px;
}
.toast {
padding: 12px 20px;
border-radius: var(--radius-sm);
font-size: 13px;
font-weight: 600;
color: #FFF;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5);
animation: slideIn 0.3s ease;
display: flex;
align-items: center;
gap: 10px;
}
.toast-success { background: #065F46; border: 1px solid #10B981; }
.toast-error { background: #7F1D1D; border: 1px solid #EF4444; }
.toast-info { background: #1E1B4B; border: 1px solid #6366F1; }
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
</style>
</head>
<body>
<!-- Header -->
<header>
<div class="brand">
<div class="brand-logo">T</div>
<div>
<h1 class="brand-title">Tradhox Payment Engine</h1>
<div class="brand-subtitle">Multi-Vendor Orchestration & Failover</div>
</div>
</div>
<div class="header-pills">
<span class="pill"><span class="pill-pulse"></span> Engine Active</span>
<span class="pill">Env: <strong>Sandbox (Beta)</strong></span>
<a href="/api/payments/engine/health/" target="_blank" class="btn btn-secondary btn-sm">Health JSON</a>
</div>
</header>
<main>
<!-- Top-Level Mode Switcher -->
<div class="mode-switcher-container">
<button id="mode-orchestrator-btn" class="mode-btn active mode-orchestrator" onclick="switchEngineMode('orchestrator')">
🌐 Multi-Vendor Orchestrator Engine (Auto-Failover)
</button>
<button id="mode-cashfree-btn" class="mode-btn mode-cashfree" onclick="switchEngineMode('cashfree')">
⚡ Cashfree Dedicated Vendor Studio (Direct Sandbox)
</button>
</div>
<!-- Live Metrics Summary -->
<div class="metrics-grid">
<div class="metric-card">
<span class="metric-label">Total Orders</span>
<span class="metric-value" id="stat-total-orders">--</span>
<span class="metric-trend">Synchronized with PostgreSQL</span>
</div>
<div class="metric-card">
<span class="metric-label">Settled Volume</span>
<span class="metric-value" id="stat-total-volume">--</span>
<span class="metric-trend">INR Gross Collections</span>
</div>
<div class="metric-card">
<span class="metric-label">Success Rate</span>
<span class="metric-value" id="stat-success-rate">--%</span>
<span class="metric-trend">All active gateways</span>
</div>
<div class="metric-card">
<span class="metric-label">Total Refunds</span>
<span class="metric-value" id="stat-total-refunds">--</span>
<span class="metric-trend">Processed & ARN Tracked</span>
</div>
</div>
<!-- ========================================== -->
<!-- SECTION 1: MULTI-VENDOR ORCHESTRATOR MODE -->
<!-- ========================================== -->
<div id="section-orchestrator">
<!-- Gateway Circuit Breaker & Health Manager -->
<div style="margin-bottom: 32px;">
<div class="section-header">
<h2 class="section-title">
<svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
Gateway Routing & Circuit Breakers
</h2>
<button onclick="refreshDashboard()" class="btn btn-secondary btn-sm">↻ Refresh State</button>
</div>
<div class="gateways-grid" id="gateways-container">
<!-- Rendered dynamically by JS -->
</div>
</div>
<!-- Testing Sandbox Tabs -->
<div class="sandbox-container">
<div class="tab-bar">
<button class="tab-btn active" onclick="switchTab('create-order')">1. Create Order & Pay</button>
<button class="tab-btn" onclick="switchTab('webhook-sim')">2. Webhook Simulator</button>
<button class="tab-btn" onclick="switchTab('refunds')">3. Refund Processing</button>
<button class="tab-btn" onclick="switchTab('order-explorer')">4. Orders & Attempt Explorer</button>
<button class="tab-btn" onclick="switchTab('api-console')">5. Live Event Logs</button>
</div>
<!-- Tab 1: Create Order & Checkout -->
<div id="tab-create-order" class="tab-content active">
<h3 style="font-size: 16px; margin-bottom: 16px;">Initiate Payment Session</h3>
<p style="font-size: 13px; color: var(--text-muted); margin-bottom: 24px;">
Submit an order payload. The engine selects the healthiest priority gateway with zero-downtime automatic failover.
</p>
<form id="create-order-form" onsubmit="handleCreateOrder(event)">
<div class="form-grid">
<div class="form-group">
<label>Order ID</label>
<div class="input-wrapper">
<input type="text" id="order-id" class="form-control" required>
<button type="button" class="input-addon-btn" onclick="generateOrderId()">New ID</button>
</div>
</div>
<div class="form-group">
<label>Amount (INR)</label>
<input type="number" step="0.50" min="1" id="order-amount" class="form-control" value="1499.00" required>
</div>
<div class="form-group">
<label>Customer Name</label>
<input type="text" id="customer-name" class="form-control" value="Vignesh Tech" required>
</div>
<div class="form-group">
<label>Customer Phone (10 digits)</label>
<input type="text" id="customer-phone" class="form-control" value="9876543210" required>
</div>
<div class="form-group">
<label>Customer Email</label>
<input type="email" id="customer-email" class="form-control" value="vignesh@tradhox.com" required>
</div>
<div class="form-group">
<label>Order Note</label>
<input type="text" id="order-note" class="form-control" value="Tradhox Platform Purchase">
</div>
</div>
<div style="display: flex; gap: 12px; align-items: center;">
<button type="submit" class="btn btn-primary" id="btn-create-order">
⚡ Initiate Order on Engine
</button>
<span id="order-loading" style="font-size: 12px; color: var(--text-muted); display: none;">Contacting payment gateways...</span>
</div>
</form>
<!-- Checkout Action Drawer -->
<div id="checkout-drawer" class="checkout-drawer">
<div class="drawer-title">✓ Order Initiated Successfully!</div>
<p id="drawer-info" style="font-size: 13px; color: var(--text-muted);"></p>
<div class="drawer-actions">
<button type="button" class="btn btn-primary" id="btn-launch-dropin" onclick="launchCashfreeDropin()">
🚀 Launch Cashfree In-Page Dropin Checkout
</button>
<a href="#" target="_blank" class="btn btn-secondary" id="btn-open-hosted">
↗ Open Cashfree Hosted Page
</a>
<button type="button" class="btn btn-secondary" onclick="simulatePaymentSuccessFromDrawer()">
🧪 Simulate Instant Payment Webhook
</button>
</div>
</div>
</div>
<!-- Tab 2: Webhook Simulator -->
<div id="tab-webhook-sim" class="tab-content">
<h3 style="font-size: 16px; margin-bottom: 16px;">Cashfree Webhook Simulator & Verifier</h3>
<p style="font-size: 13px; color: var(--text-muted); margin-bottom: 24px;">
Simulate incoming webhooks with authentic HMAC-SHA256 signature verification and timestamp anti-replay validation.
</p>
<div class="form-grid">
<div class="form-group">
<label>Target Order ID</label>
<input type="text" id="sim-order-id" class="form-control" placeholder="Select or enter Order ID">
</div>
<div class="form-group">
<label>Event Type</label>
<select id="sim-event-type" class="form-control">
<option value="PAYMENT_SUCCESS_WEBHOOK">PAYMENT_SUCCESS_WEBHOOK</option>
<option value="PAYMENT_FAILED_WEBHOOK">PAYMENT_FAILED_WEBHOOK</option>
<option value="PAYMENT_USER_DROPPED_WEBHOOK">PAYMENT_USER_DROPPED_WEBHOOK</option>
</select>
</div>
</div>
<button type="button" class="btn btn-primary" onclick="dispatchSimulatedWebhook()">
Dispatch Webhook with HMAC-SHA256
</button>
<div style="margin-top: 24px;">
<div class="console-header">
<span>Webhook Verification Result</span>
</div>
<pre class="console-box" id="webhook-sim-output">// Simulator output will display here...</pre>
</div>
</div>
<!-- Tab 3: Refund Console -->
<div id="tab-refunds" class="tab-content">
<h3 style="font-size: 16px; margin-bottom: 16px;">Programmatic Refund Console</h3>
<p style="font-size: 13px; color: var(--text-muted); margin-bottom: 24px;">
Issue full or partial refunds directly against settled orders.
</p>
<div class="form-grid">
<div class="form-group">
<label>Order ID to Refund</label>
<input type="text" id="refund-order-id" class="form-control" placeholder="e.g. TRDX-ORD-...">
</div>
<div class="form-group">
<label>Refund Amount (INR)</label>
<input type="number" step="0.50" id="refund-amount" class="form-control" value="499.00">
</div>
<div class="form-group">
<label>Refund Speed</label>
<select id="refund-speed" class="form-control">
<option value="STANDARD">STANDARD (Banking T+5 to T+7 Days)</option>
<option value="INSTANT">INSTANT (IMPS / UPI Rails)</option>
</select>
</div>
<div class="form-group">
<label>Refund Reason</label>
<input type="text" id="refund-reason" class="form-control" value="Customer return request">
</div>
</div>
<button type="button" class="btn btn-danger" onclick="handleCreateRefund()">
Trigger Refund
</button>
<div style="margin-top: 24px;">
<pre class="console-box" id="refund-output">// Refund response will display here...</pre>
</div>
</div>
<!-- Tab 4: Orders & Attempt Explorer -->
<div id="tab-order-explorer" class="tab-content">
<div class="section-header">
<h3 style="font-size: 16px;">Synchronized PostgreSQL Orders</h3>
<button onclick="refreshDashboard()" class="btn btn-secondary btn-sm">Refresh List</button>
</div>
<div class="table-container">
<table>
<thead>
<tr>
<th>Order ID</th>
<th>Amount</th>
<th>Customer</th>
<th>Active Gateway</th>
<th>Status</th>
<th>Created At</th>
<th>Action</th>
</tr>
</thead>
<tbody id="orders-table-body">
<tr><td colspan="7" style="text-align: center; color: var(--text-dim);">Loading orders...</td></tr>
</tbody>
</table>
</div>
</div>
<!-- Tab 5: Live API & Event Logs -->
<div id="tab-api-console" class="tab-content">
<div class="section-header">
<h3 style="font-size: 16px;">Live Event Stream</h3>
<button onclick="clearConsole()" class="btn btn-secondary btn-sm">Clear Stream</button>
</div>
<pre class="console-box" id="live-console">// Stream initialized. All portal requests, responses, and webhooks log here.
</pre>
</div>
</div>
</div>
<!-- ========================================== -->
<!-- SECTION 2: DEDICATED CASHFREE VENDOR STUDIO -->
<!-- ========================================== -->
<div id="section-cashfree" style="display: none;">
<div class="section-header">
<h2 class="section-title" style="color: var(--cashfree-color);">
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
Cashfree Dedicated Vendor Studio
</h2>
<div style="display: flex; gap: 10px; align-items: center;">
<span class="badge" id="cf-env-badge" style="background: var(--cashfree-glow); color: #F472B6; border: 1px solid var(--border-cashfree);">Mode: Sandbox</span>
<span class="badge" style="background: rgba(16, 185, 129, 0.15); color: #34D399; border: 1px solid rgba(16, 185, 129, 0.3);">
🌐 Auto-Routing: beta.tradhox.com & tradhox.com ➔ Production
</span>
</div>
</div>
<!-- Cashfree Direct Live Config & Environment Switcher -->
<div style="background: rgba(236, 72, 153, 0.08); border: 1px solid var(--border-cashfree); border-radius: var(--radius-md); padding: 18px 24px; margin-bottom: 24px; display: flex; flex-direction: column; gap: 14px;">
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 14px;">
<div style="display: flex; align-items: center; gap: 12px;">
<label style="font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-muted); margin-bottom: 0;">Target Environment:</label>
<select id="cf-studio-env-select" onchange="onCfStudioEnvChange()" class="form-control" style="width: auto; padding: 6px 14px; font-size: 13px; font-weight: 600; background: #0A0D14; border-color: var(--border-cashfree); color: #F472B6; border-radius: var(--radius-sm); cursor: pointer;">
<option value="sandbox" selected>Sandbox Mode (Test Simulator)</option>
<option value="production">Production Mode (api.cashfree.com/pg)</option>
</select>
</div>
<div style="display: flex; gap: 10px; align-items: center;">
<button onclick="pingCashfreeDirectApi()" class="btn btn-cashfree btn-sm" id="btn-ping-cf">⚡ Ping Active Gateway</button>
</div>
</div>
<div style="display: flex; flex-wrap: wrap; gap: 20px; font-size: 12px; background: rgba(0,0,0,0.3); padding: 12px 16px; border-radius: var(--radius-sm); border: 1px solid rgba(255,255,255,0.05);">
<div><span style="color: var(--text-muted); font-size: 11px; text-transform: uppercase; font-weight: 600;">Active App ID:</span> <code id="cf-disp-appid" style="color: #F472B6; font-family: var(--font-mono); font-weight: 600;">TEST1118017027fc875a9485063df8b007108111</code></div>
<div><span style="color: var(--text-muted); font-size: 11px; text-transform: uppercase; font-weight: 600;">Secret Key:</span> <code id="cf-disp-secret" style="color: #F472B6; font-family: var(--font-mono);">cfsk_ma_test_3701...1cf9fb86</code></div>
<div><span style="color: var(--text-muted); font-size: 11px; text-transform: uppercase; font-weight: 600;">API Base URL:</span> <code id="cf-disp-baseurl" style="color: #A5B4FC; font-family: var(--font-mono);">https://sandbox.cashfree.com/pg</code></div>
<div><span style="color: var(--text-muted); font-size: 11px; text-transform: uppercase; font-weight: 600;">Checkout Base:</span> <code id="cf-disp-checkout" style="color: #34D399; font-family: var(--font-mono);">https://payments-test.cashfree.com/order/#</code></div>
<div><span style="color: var(--text-muted); font-size: 11px; text-transform: uppercase; font-weight: 600;">API Version:</span> <code style="color: #F472B6; font-family: var(--font-mono);">2023-08-01</code></div>
</div>
<div style="display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 8px; font-size: 11px; color: var(--text-dim);">
<span>🔒 <strong>Credentials in Memory:</strong> Prod: <code>1370798f...970731</code> (api.cashfree.com) | Sandbox: <code>TEST1118...8111</code> (sandbox.cashfree.com)</span>
<span style="color: #34D399;">✓ Automatic origin-based routing active for beta.tradhox.com & tradhox.com</span>
</div>
</div>
<div class="sandbox-container" style="border-color: var(--border-cashfree);">
<div class="tab-bar">
<button class="tab-btn active tab-cashfree" onclick="switchCashfreeTab('cf-direct-order')">1. Direct Order & Pay</button>
<button class="tab-btn tab-cashfree" onclick="switchCashfreeTab('cf-upi-studio')">2. UPI Studio (Dynamic QR & Collect)</button>
<button class="tab-btn tab-cashfree" onclick="switchCashfreeTab('cf-server-inspector')">3. Live Server Inspector</button>
<button class="tab-btn tab-cashfree" onclick="switchCashfreeTab('cf-webhook-tester')">4. Direct Webhook & Signature</button>
</div>
<!-- CF Tab 1: Direct Order & Pay -->
<div id="tab-cf-direct-order" class="tab-content active">
<h3 style="font-size: 16px; margin-bottom: 8px;">Direct Cashfree API Order Creation</h3>
<p style="font-size: 13px; color: var(--text-muted); margin-bottom: 24px;">
Bypasses multi-vendor orchestration and dispatches raw requests directly to <code>https://sandbox.cashfree.com/pg/orders</code>.
</p>
<form id="cf-direct-order-form" onsubmit="handleDirectCashfreeOrder(event)">
<div class="form-grid">
<div class="form-group">
<label>Order ID</label>
<div class="input-wrapper">
<input type="text" id="cf-direct-order-id" class="form-control" required>
<button type="button" class="input-addon-btn" onclick="generateCfOrderId()">New ID</button>
</div>
</div>
<div class="form-group">
<label>Amount (INR)</label>
<input type="number" step="0.50" min="1" id="cf-direct-amount" class="form-control" value="250.00" required>
</div>
<div class="form-group">
<label>Customer Name</label>
<input type="text" id="cf-direct-cust-name" class="form-control" value="Cashfree Direct Tester" required>
</div>
<div class="form-group">
<label>Customer Phone</label>
<input type="text" id="cf-direct-phone" class="form-control" value="9876543210" required>
</div>
<div class="form-group">
<label>Customer Email</label>
<input type="email" id="cf-direct-email" class="form-control" value="vignesh@tradhox.com" required>
</div>
</div>
<div style="display: flex; gap: 12px; align-items: center;">
<button type="submit" class="btn btn-cashfree" id="btn-cf-direct-create">
⚡ Create Order Directly on Cashfree
</button>
</div>
</form>
<div id="cf-direct-drawer" class="checkout-drawer" style="border-color: var(--border-cashfree);">
<div class="drawer-title" style="color: var(--cashfree-color);">✓ Cashfree Order Generated!</div>
<p id="cf-drawer-info" style="font-size: 13px; color: var(--text-muted);"></p>
<div class="drawer-actions">
<button type="button" class="btn btn-cashfree" onclick="launchDirectCashfreeDropin()">
🚀 Launch In-Page Dropin Modal
</button>
<a href="#" target="_blank" class="btn btn-secondary" id="btn-cf-direct-hosted">
↗ Open Cashfree Hosted Checkout
</a>
<button type="button" class="btn btn-secondary" onclick="switchCashfreeTab('cf-upi-studio')">
📱 Test UPI QR / Collect for this Order
</button>
</div>
</div>
</div>
<!-- CF Tab 2: UPI Studio -->
<div id="tab-cf-upi-studio" class="tab-content">
<h3 style="font-size: 16px; margin-bottom: 8px;">Cashfree UPI Direct Integration Studio</h3>
<p style="font-size: 13px; color: var(--text-muted); margin-bottom: 24px;">
Test Cashfree's direct server-side UPI endpoints: Dynamic QR generation and VPA Collect requests.
</p>
<div class="form-grid">
<div class="form-group" style="grid-column: 1 / -1;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; flex-wrap: wrap; gap: 8px;">
<label style="margin-bottom: 0;">Payment Session ID</label>
<div style="display: flex; gap: 8px; align-items: center;">
<button type="button" class="btn btn-secondary btn-sm" onclick="autoGenerateCashfreeSession(100)" id="btn-quick-session" style="padding: 4px 12px; font-size: 11px; background: rgba(236, 72, 153, 0.15); border-color: var(--border-cashfree); color: #F472B6;">
⚡ Auto-Generate Live Session (₹100)
</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="autoGenerateAndShowQR()" id="btn-quick-qr" style="padding: 4px 12px; font-size: 11px; background: rgba(99, 102, 241, 0.15); border-color: var(--border-accent); color: #A5B4FC;">
⚡ 1-Click: Generate & Show Dynamic QR
</button>
</div>
</div>
<div class="input-wrapper">
<input type="text" id="upi-session-id" class="form-control" placeholder="session_... (or click Auto-Generate Live Session above)">
<button type="button" class="input-addon-btn" onclick="autoGenerateCashfreeSession(100)">⚡ Auto-Generate</button>
</div>
<div style="display: flex; gap: 8px; margin-top: 6px; align-items: center; flex-wrap: wrap;">
<span style="font-size: 11px; color: var(--text-dim);">Quick Amounts:</span>
<button type="button" class="input-addon-btn" style="position: static;" onclick="autoGenerateCashfreeSession(10)">₹10</button>
<button type="button" class="input-addon-btn" style="position: static;" onclick="autoGenerateCashfreeSession(250)">₹250</button>
<button type="button" class="input-addon-btn" style="position: static;" onclick="autoGenerateCashfreeSession(999)">₹999</button>
<span id="session-gen-status" style="font-size: 11px; color: var(--success); margin-left: 8px; display: none;"></span>
</div>
</div>
<div class="form-group">
<label>UPI Mode</label>
<select id="upi-channel-select" class="form-control" onchange="toggleUpiFields()">
<option value="qrcode">Dynamic UPI QR Code (Scannable)</option>
<option value="collect">UPI Collect Request (Push Prompt to VPA)</option>
</select>
</div>
<div class="form-group" id="upi-vpa-group" style="display: none;">
<label>Customer UPI VPA (Test Handle)</label>
<input type="text" id="upi-vpa-input" class="form-control" value="success@upi" placeholder="e.g. success@upi or failure@upi">
</div>
</div>
<div style="display: flex; gap: 12px; align-items: center; flex-wrap: wrap;">
<button type="button" class="btn btn-cashfree" onclick="handleDirectUpiPayment()" id="btn-exec-upi">
Execute Selected UPI Mode
</button>
<button type="button" class="btn btn-secondary" onclick="autoGenerateAndShowQR()">
⚡ 1-Click: Auto-Gen Session & Render QR
</button>
</div>
<!-- Dynamic QR Image Box -->
<div id="qr-display-box" class="qr-display-box">
<h4 style="font-size: 15px; color: #FFF;">Scan Dynamic UPI QR Code</h4>
<p style="font-size: 12px; color: var(--text-muted);">Generated live by Cashfree Sandbox API. Scan with Google Pay, PhonePe, Paytm, or BHIM.</p>
<div class="qr-image-frame">
<img id="qr-code-img" src="" alt="Dynamic UPI QR Code">
</div>
<span id="qr-payment-id" style="font-size: 12px; color: #A5B4FC; font-family: var(--font-mono);"></span>
</div>
<div style="margin-top: 24px;">
<pre class="console-box" id="upi-output">// UPI response data will display here...</pre>
</div>
</div>
<!-- CF Tab 3: Live Server Inspector -->
<div id="tab-cf-server-inspector" class="tab-content">
<h3 style="font-size: 16px; margin-bottom: 8px;">Direct Cashfree Server Inspector</h3>
<p style="font-size: 13px; color: var(--text-muted); margin-bottom: 24px;">
Query Cashfree's live servers directly for raw Order status, Payment attempts, and Settlement records.
</p>
<div class="form-grid">
<div class="form-group" style="grid-column: 1 / -1;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; flex-wrap: wrap; gap: 8px;">
<label style="margin-bottom: 0;">Order ID to Inspect</label>
<div style="display: flex; gap: 8px; align-items: center;">
<button type="button" class="btn btn-secondary btn-sm" onclick="autoGenerateInspectorOrder()" id="btn-inspect-auto-gen" style="padding: 4px 12px; font-size: 11px; background: rgba(236, 72, 153, 0.15); border-color: var(--border-cashfree); color: #F472B6;">
⚡ Auto-Generate New Test Order
</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="useLatestOrderIdForInspector()" style="padding: 4px 12px; font-size: 11px; background: rgba(99, 102, 241, 0.15); border-color: var(--border-accent); color: #A5B4FC;">
↺ Pick Latest Order
</button>
</div>
</div>
<div class="input-wrapper">
<input type="text" id="cf-inspect-order-id" class="form-control" placeholder="e.g. CF-DIR-... or click Auto-Generate New Test Order">
<button type="button" class="input-addon-btn" onclick="autoGenerateInspectorOrder()">⚡ Auto-Generate</button>
</div>
<!-- Recent Orders Quick-Pills -->
<div style="display: flex; gap: 8px; margin-top: 8px; align-items: center; flex-wrap: wrap;" id="inspector-quick-orders-bar">
<span style="font-size: 11px; color: var(--text-dim);">Recent Orders:</span>
<span id="inspector-quick-pills" style="display: flex; gap: 6px; flex-wrap: wrap;"></span>
<span id="inspector-gen-status" style="font-size: 11px; color: var(--success); margin-left: 8px; display: none;"></span>
</div>
</div>
</div>
<div style="display: flex; gap: 12px; flex-wrap: wrap;">
<button type="button" class="btn btn-cashfree" onclick="inspectDirectCashfreeOrder()">
🔍 Fetch Raw Order Object
</button>
<button type="button" class="btn btn-secondary" onclick="inspectDirectCashfreePayments()">
💳 Fetch Bank Payment Attempts
</button>
<button type="button" class="btn btn-secondary" onclick="inspectDirectCashfreeSettlement()">
🏦 Fetch Settlement & UTR
</button>
</div>
<div style="margin-top: 24px;">
<div class="console-header">
<span>Cashfree Live Server Response</span>
</div>
<pre class="console-box" id="cf-inspector-output">// Server inspection data will appear here...</pre>
</div>
</div>
<!-- CF Tab 4: Direct Webhook & Signature Tester -->
<div id="tab-cf-webhook-tester" class="tab-content">
<h3 style="font-size: 16px; margin-bottom: 8px;">Direct Cashfree Webhook & HMAC-SHA256 Signature Tester</h3>
<p style="font-size: 13px; color: var(--text-muted); margin-bottom: 24px;">
Constructs an authentic Cashfree event payload and validates cryptographic signature verification using <code>HMAC-SHA256(timestamp + raw_body, secret_key)</code>.
</p>
<div class="form-grid">
<div class="form-group">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px;">
<label style="margin-bottom: 0;">Target Order ID</label>
<button type="button" class="btn btn-secondary btn-sm" onclick="useLatestOrderIdForWebhook()" style="padding: 2px 8px; font-size: 10px; background: rgba(99, 102, 241, 0.15); border-color: var(--border-accent); color: #A5B4FC;">↺ Latest</button>
</div>
<div class="input-wrapper">
<input type="text" id="cf-sim-order-id" class="form-control" placeholder="e.g. TRDX-ORD-... or CF-DIR-...">
<button type="button" class="input-addon-btn" onclick="autoGenerateInspectorOrder('webhook')">⚡ Auto-Gen</button>
</div>
</div>
<div class="form-group">
<label>Webhook Event Type</label>
<select id="cf-sim-event-type" class="form-control">
<option value="PAYMENT_SUCCESS_WEBHOOK">PAYMENT_SUCCESS_WEBHOOK (Success)</option>
<option value="PAYMENT_FAILED_WEBHOOK">PAYMENT_FAILED_WEBHOOK (Failure)</option>
<option value="PAYMENT_USER_DROPPED_WEBHOOK">PAYMENT_USER_DROPPED_WEBHOOK (User Dropped)</option>
</select>
</div>
</div>
<button type="button" class="btn btn-cashfree" onclick="handleDirectCashfreeWebhookSim()">
🔐 Sign & Dispatch Webhook to Receiver
</button>
<div style="margin-top: 24px;">
<pre class="console-box" id="cf-webhook-sim-output">// Webhook dispatch results will appear here...</pre>
</div>
</div>
</div>
</div>
</main>
<!-- Toast Notifications -->
<div id="toast-container"></div>
<script>
// State management
let currentSessionId = null;
let currentOrderId = null;
let cashfreeInstance = null;
// Initialize Cashfree SDK v3 in sandbox mode
try {
if (typeof Cashfree !== 'undefined') {
cashfreeInstance = Cashfree({ mode: 'sandbox' });
}
} catch (e) {
console.error('Failed to init Cashfree SDK:', e);
}
// Switch between Multi-Vendor Orchestrator and Cashfree Studio
function switchEngineMode(mode) {
const orchestratorSec = document.getElementById('section-orchestrator');
const cashfreeSec = document.getElementById('section-cashfree');
const orchBtn = document.getElementById('mode-orchestrator-btn');
const cfBtn = document.getElementById('mode-cashfree-btn');
if (mode === 'orchestrator') {
orchestratorSec.style.display = 'block';
cashfreeSec.style.display = 'none';
orchBtn.classList.add('active');
cfBtn.classList.remove('active');
showToast('Switched to Multi-Vendor Orchestration Engine', 'info');
} else {
orchestratorSec.style.display = 'none';
cashfreeSec.style.display = 'block';
cfBtn.classList.add('active');
orchBtn.classList.remove('active');
showToast('Switched to Cashfree Dedicated Vendor Studio', 'info');
if (!document.getElementById('cf-direct-order-id').value) {
generateCfOrderId();
}
}
}
function generateOrderId() {
const rand = Math.floor(100000 + Math.random() * 900000);
const id = `TRDX-ORD-${rand}`;
document.getElementById('order-id').value = id;
return id;
}
function generateCfOrderId() {
const rand = Math.floor(100000 + Math.random() * 900000);
const id = `CF-DIR-${rand}`;
document.getElementById('cf-direct-order-id').value = id;
return id;
}
function switchTab(tabId) {
document.querySelectorAll('#section-orchestrator .tab-btn').forEach(btn => btn.classList.remove('active'));
document.querySelectorAll('#section-orchestrator .tab-content').forEach(c => c.classList.remove('active'));
event.currentTarget.classList.add('active');
document.getElementById(`tab-${tabId}`).classList.add('active');
if (tabId === 'order-explorer' || tabId === 'create-order') {
refreshDashboard();
}
}
function switchCashfreeTab(tabId) {
document.querySelectorAll('#section-cashfree .tab-btn').forEach(btn => btn.classList.remove('active'));
document.querySelectorAll('#section-cashfree .tab-content').forEach(c => c.classList.remove('active'));
if (event && event.currentTarget) {
event.currentTarget.classList.add('active');
}
document.getElementById(`tab-${tabId}`).classList.add('active');
}
function toggleUpiFields() {
const channel = document.getElementById('upi-channel-select').value;
const vpaGroup = document.getElementById('upi-vpa-group');
if (channel === 'collect') {
vpaGroup.style.display = 'flex';
} else {
vpaGroup.style.display = 'none';
}
}
function showToast(message, type = 'info') {
const container = document.getElementById('toast-container');
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
toast.textContent = message;
container.appendChild(toast);
setTimeout(() => {
toast.remove();
}, 4000);
}
function logConsole(message, data = null) {
const consoleEl = document.getElementById('live-console');
const ts = new Date().toLocaleTimeString();
let formatted = `[${ts}] ${message}`;
if (data) {
formatted += `\n` + JSON.stringify(data, null, 2);
}
consoleEl.textContent = formatted + `\n\n` + consoleEl.textContent;
}
function clearConsole() {
document.getElementById('live-console').textContent = '// Console cleared.';
}
// Refresh Dashboard Metrics & Gateways
async function refreshDashboard() {
try {
const res = await fetch('/api/payments/engine/stats/');
const data = await res.json();
// Update metrics
if (data.metrics) {
document.getElementById('stat-total-orders').textContent = data.metrics.total_orders;
document.getElementById('stat-total-volume').textContent = `₹${data.metrics.total_volume.toLocaleString('en-IN')}`;
document.getElementById('stat-success-rate').textContent = `${data.metrics.success_rate}%`;
document.getElementById('stat-total-refunds').textContent = data.metrics.total_refunds;
}
renderGateways(data.gateways || []);
renderOrdersTable(data.recent_orders || []);
} catch (err) {
console.error('Failed to refresh dashboard stats:', err);
}
}
function renderGateways(gateways) {
const container = document.getElementById('gateways-container');
container.innerHTML = '';
gateways.forEach(g => {
const badgeClass = g.health_status === 'HEALTHY' ? 'badge-healthy' : (g.health_status === 'DEGRADED' ? 'badge-degraded' : 'badge-down');
const card = document.createElement('div');
card.className = `gateway-card ${g.is_active ? 'active' : ''}`;
card.innerHTML = `
<div class="gateway-card-header">
<div class="gateway-info">
<h3>${g.display_name}</h3>
<p>Identifier: <code>${g.name}</code> | Priority: #${g.priority}</p>
</div>
<span class="badge ${badgeClass}">● ${g.health_status}</span>
</div>
<div class="gateway-stats-row">
<div class="gateway-stat-item">
<span class="gateway-stat-label">Environment</span>
<span class="gateway-stat-val">${g.environment.toUpperCase()}</span>
</div>
<div class="gateway-stat-item">
<span class="gateway-stat-label">Failures</span>
<span class="gateway-stat-val">${g.consecutive_failures}</span>
</div>
<div class="gateway-stat-item">
<span class="gateway-stat-label">Routing Status</span>
<span class="gateway-stat-val" style="color: ${g.is_active ? 'var(--success)' : 'var(--danger)'}">
${g.is_active ? 'Active' : 'Disabled'}
</span>
</div>
</div>
<div class="gateway-controls">
<span style="font-size: 11px; color: var(--text-dim); margin-right: 4px;">Simulate Failover:</span>
<button onclick="toggleGatewayStatus('${g.name}', 'HEALTHY')" class="btn btn-success btn-sm">Healthy</button>
<button onclick="toggleGatewayStatus('${g.name}', 'DEGRADED')" class="btn btn-secondary btn-sm" style="color: #FBBF24;">Degrade</button>
<button onclick="toggleGatewayStatus('${g.name}', 'DOWN')" class="btn btn-danger btn-sm">Trip Down</button>
</div>
`;
container.appendChild(card);
});
}
async function toggleGatewayStatus(gatewayName, targetStatus) {
try {
const res = await fetch('/api/payments/engine/gateway-toggle/', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: gatewayName, health_status: targetStatus })
});
const data = await res.json();
showToast(`Gateway ${gatewayName} set to ${targetStatus}`, 'info');
logConsole(`Circuit breaker toggled for ${gatewayName}: ${targetStatus}`, data);
refreshDashboard();
} catch (e) {
showToast('Error toggling gateway', 'error');
}
}
function renderOrdersTable(orders) {
const tbody = document.getElementById('orders-table-body');
if (!orders || orders.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" style="text-align: center; color: var(--text-dim);">No orders created yet. Use Tab 1 to create one!</td></tr>';
return;
}
tbody.innerHTML = orders.map(o => {
const statusColor = o.status === 'PAID' ? 'var(--success)' : (o.status === 'ACTIVE' ? 'var(--primary)' : 'var(--danger)');
return `
<tr>
<td><strong>${o.order_id}</strong></td>
<td>₹${parseFloat(o.amount).toFixed(2)}</td>
<td>${o.customer_name || o.customer_id}</td>
<td><span class="badge" style="background: rgba(255,255,255,0.06);">${o.active_gateway}</span></td>
<td><strong style="color: ${statusColor}; font-size: 11px;">● ${o.status}</strong></td>
<td style="color: var(--text-muted); font-size: 12px;">${new Date(o.created_at).toLocaleTimeString()}</td>
<td>
<button onclick="selectOrderForSim('${o.order_id}')" class="btn btn-secondary btn-sm">Test Webhook</button>
</td>
</tr>
`;
}).join('');
// Populate Quick-Pills for Live Server Inspector
const pillsContainer = document.getElementById('inspector-quick-pills');
if (pillsContainer) {
pillsContainer.innerHTML = orders.slice(0, 5).map(o =>
`<button type="button" class="input-addon-btn" style="position: static;" onclick="selectOrderForInspection('${o.order_id}')">${o.order_id}</button>`
).join('');
}
}
function selectOrderForSim(orderId) {
document.getElementById('sim-order-id').value = orderId;
document.getElementById('refund-order-id').value = orderId;
document.getElementById('cf-inspect-order-id').value = orderId;
switchTab('webhook-sim');
showToast(`Selected order ${orderId} in Webhook Simulator`, 'info');
}
// Handle Create Order (Orchestrator Mode)
async function handleCreateOrder(e) {
e.preventDefault();
const btn = document.getElementById('btn-create-order');
const loader = document.getElementById('order-loading');
btn.disabled = true;
loader.style.display = 'inline';
const payload = {
order_id: document.getElementById('order-id').value,
amount: document.getElementById('order-amount').value,
currency: 'INR',
customer_id: 'cust_' + Math.floor(Math.random() * 900 + 100),
customer_name: document.getElementById('customer-name').value,
customer_email: document.getElementById('customer-email').value,
customer_phone: document.getElementById('customer-phone').value,
order_note: document.getElementById('order-note').value,
return_url: window.location.origin + '/portal/?order_id={order_id}'
};
try {
logConsole('Dispatching Order Creation to Engine...', payload);
const res = await fetch('/api/payments/orders/create/', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await res.json();
logConsole('Engine Order Creation Response:', data);
if (res.ok && data.success) {
currentOrderId = data.order_id;
currentSessionId = data.payment_session_id;
showToast(`Order ${data.order_id} created on ${data.active_gateway}!`, 'success');
const drawer = document.getElementById('checkout-drawer');
drawer.style.display = 'block';
document.getElementById('drawer-info').textContent =
`Active Gateway: ${data.active_gateway} | Session ID: ${data.payment_session_id || 'N/A'}`;
const hostedBtn = document.getElementById('btn-open-hosted');
if (data.checkout_url) {
hostedBtn.href = data.checkout_url;
hostedBtn.style.display = 'inline-flex';
} else {
hostedBtn.style.display = 'none';
}
document.getElementById('sim-order-id').value = currentOrderId;
document.getElementById('refund-order-id').value = currentOrderId;
document.getElementById('cf-inspect-order-id').value = currentOrderId;
document.getElementById('upi-session-id').value = currentSessionId;
refreshDashboard();
} else {
showToast(`Failed: ${data.error || 'Gateway rejected order'}`, 'error');
}
} catch (err) {
showToast('Network error contacting payment engine', 'error');
logConsole('Error:', err);
} finally {
btn.disabled = false;
loader.style.display = 'none';
}
}
// Launch In-Page Dropin Checkout using Cashfree JS
function launchCashfreeDropin() {
if (!currentSessionId) {
showToast('No active payment session found. Create an order first!', 'error');
return;
}
if (!cashfreeInstance) {
showToast('Cashfree SDK not loaded, opening hosted URL instead', 'info');
window.open(`https://payments-test.cashfree.com/order/#${currentSessionId}`, '_blank');
return;
}
showToast('Launching Cashfree In-Page Drop-in Modal...', 'info');
logConsole('Invoking cashfreeInstance.checkout with session:', currentSessionId);
cashfreeInstance.checkout({
paymentSessionId: currentSessionId,
redirectTarget: '_modal'
}).then((result) => {
logConsole('Cashfree Checkout Result:', result);
if (result.error) {
showToast('Checkout closed or failed: ' + result.error.message, 'error');
}
if (result.paymentDetails) {
showToast('Payment completed in modal! Refreshing status...', 'success');
refreshDashboard();
}
});
}
function simulatePaymentSuccessFromDrawer() {
if (!currentOrderId) {
showToast('Create an order first', 'error');
return;
}
document.getElementById('sim-order-id').value = currentOrderId;
document.getElementById('sim-event-type').value = 'PAYMENT_SUCCESS_WEBHOOK';
dispatchSimulatedWebhook();
}
// Webhook Simulator
async function dispatchSimulatedWebhook() {
const orderId = document.getElementById('sim-order-id').value;
const eventType = document.getElementById('sim-event-type').value;
if (!orderId) {
showToast('Please enter an Order ID', 'error');
return;
}
const out = document.getElementById('webhook-sim-output');
out.textContent = 'Generating HMAC-SHA256 signature and invoking internal webhook...';
try {
const res = await fetch('/api/payments/engine/simulate-webhook/', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ order_id: orderId, event_type: eventType })
});
const data = await res.json();
out.textContent = JSON.stringify(data, null, 2);
logConsole('Simulated Webhook Dispatch Result:', data);
if (res.ok) {
showToast(`Webhook verified! Order status: ${data.order_status}`, 'success');
refreshDashboard();
} else {
showToast(`Error: ${data.error || 'Webhook failed'}`, 'error');
}
} catch (err) {
out.textContent = 'Error: ' + err.message;
showToast('Error sending simulated webhook', 'error');
}
}
// Refund Handler
async function handleCreateRefund() {
const orderId = document.getElementById('refund-order-id').value;
const amount = document.getElementById('refund-amount').value;
const speed = document.getElementById('refund-speed').value;
const reason = document.getElementById('refund-reason').value;
if (!orderId) {
showToast('Please enter an Order ID to refund', 'error');
return;
}
const refundId = `REF-${Math.floor(100000 + Math.random() * 900000)}`;
const out = document.getElementById('refund-output');
out.textContent = 'Triggering refund against active gateway...';
try {
const res = await fetch(`/api/payments/orders/${orderId}/refund/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
refund_id: refundId,
amount: amount,
speed: speed,
reason: reason
})
});
const data = await res.json();
out.textContent = JSON.stringify(data, null, 2);
logConsole('Refund Response:', data);
if (res.ok) {
showToast(`Refund ${refundId} initiated successfully!`, 'success');
refreshDashboard();
} else {
showToast(`Refund failed: ${data.error || 'Error'}`, 'error');
}
} catch (e) {
out.textContent = 'Error: ' + e.message;
showToast('Error processing refund', 'error');
}
}
// ==========================================
// ==========================================
// CASHFREE DEDICATED VENDOR STUDIO HANDLERS
// ==========================================
function getCfStudioEnv() {
const el = document.getElementById('cf-studio-env-select');
return el ? el.value : 'sandbox';
}
function onCfStudioEnvChange() {
const env = getCfStudioEnv();
const badge = document.getElementById('cf-env-badge');
const appIdEl = document.getElementById('cf-disp-appid');
const secretEl = document.getElementById('cf-disp-secret');
const baseEl = document.getElementById('cf-disp-baseurl');
const checkoutEl = document.getElementById('cf-disp-checkout');
if (env === 'production') {
if (badge) {
badge.textContent = 'Mode: Production (Live)';
badge.style.background = 'rgba(239, 68, 68, 0.15)';
badge.style.color = '#F87171';
badge.style.borderColor = 'rgba(239, 68, 68, 0.3)';
}
if (appIdEl) appIdEl.textContent = '1370798f950e30514fa051e93868970731';
if (secretEl) secretEl.textContent = 'cfsk_ma_prod_d441...a626ee00';
if (baseEl) baseEl.textContent = 'https://api.cashfree.com/pg';
if (checkoutEl) checkoutEl.textContent = 'https://payments.cashfree.com/order/#';
showToast('Switched Cashfree Studio to Production Mode (api.cashfree.com)', 'info');
} else {
if (badge) {
badge.textContent = 'Mode: Sandbox';
badge.style.background = 'var(--cashfree-glow)';
badge.style.color = '#F472B6';
badge.style.borderColor = 'var(--border-cashfree)';
}
if (appIdEl) appIdEl.textContent = 'TEST1118017027fc875a9485063df8b007108111';
if (secretEl) secretEl.textContent = 'cfsk_ma_test_3701...1cf9fb86';
if (baseEl) baseEl.textContent = 'https://sandbox.cashfree.com/pg';
if (checkoutEl) checkoutEl.textContent = 'https://payments-test.cashfree.com/order/#';
showToast('Switched Cashfree Studio to Sandbox Mode', 'info');
}
}
async function handleDirectCashfreeOrder(e) {
e.preventDefault();
const btn = document.getElementById('btn-cf-direct-create');
btn.disabled = true;
const env = getCfStudioEnv();
const payload = {
order_id: document.getElementById('cf-direct-order-id').value,
amount: document.getElementById('cf-direct-amount').value,
currency: 'INR',
customer_id: 'cust_cf_' + Math.floor(Math.random() * 900 + 100),
customer_name: document.getElementById('cf-direct-cust-name').value,
customer_email: document.getElementById('cf-direct-email').value,
customer_phone: document.getElementById('cf-direct-phone').value,
order_note: `Direct Cashfree Vendor Order [${env.toUpperCase()}]`,
return_url: window.location.origin + '/portal/?order_id={order_id}',
environment: env
};
try {
logConsole(`Sending Direct Order to Cashfree API [${env.toUpperCase()}]...`, payload);
const res = await fetch('/api/payments/cashfree/orders/create/', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await res.json();
logConsole('Cashfree Direct Response:', data);
if (res.ok && data.success) {
currentOrderId = data.order_id;
currentSessionId = data.payment_session_id;
showToast(`Cashfree order ${data.order_id} created successfully [${env.toUpperCase()}]!`, 'success');
const drawer = document.getElementById('cf-direct-drawer');
drawer.style.display = 'block';
document.getElementById('cf-drawer-info').textContent =
`Order: ${data.gateway_order_id || data.order_id} | Session: ${data.payment_session_id} [${env}]`;
document.getElementById('btn-cf-direct-hosted').href = data.checkout_url || '#';
document.getElementById('upi-session-id').value = data.payment_session_id;
document.getElementById('cf-inspect-order-id').value = data.order_id;
document.getElementById('cf-sim-order-id').value = data.order_id;
refreshDashboard();
} else {
showToast(`Error: ${data.error || 'Cashfree error'}`, 'error');
}
} catch (err) {
showToast('Network error calling Cashfree', 'error');
} finally {
btn.disabled = false;
}
}
function launchDirectCashfreeDropin() {
launchCashfreeDropin();
}
// Ping Cashfree Direct API
async function pingCashfreeDirectApi() {
const btn = document.getElementById('btn-ping-cf');
const origText = btn.textContent;
btn.textContent = 'Pinging...';
btn.disabled = true;
const t0 = performance.now();
try {
const res = await fetch('/api/payments/engine/health/');
const t1 = performance.now();
const latency = Math.round(t1 - t0);
const data = await res.json();
const cfStatus = data.gateways?.find(g => g.name === 'CASHFREE');
if (cfStatus && cfStatus.health_status === 'HEALTHY') {
showToast(`Cashfree Sandbox API reachable (${latency}ms)!`, 'success');
btn.textContent = `● Reachable (${latency}ms)`;
} else {
showToast(`Cashfree ping failed or degraded (${latency}ms)`, 'error');
btn.textContent = `▲ Degraded (${latency}ms)`;
}
} catch (e) {
showToast('Ping failed: ' + e.message, 'error');
btn.textContent = '✕ Unreachable';
} finally {
setTimeout(() => {
btn.textContent = origText;
btn.disabled = false;
}, 3000);
}
}
// Handle Direct Cashfree Webhook Simulation
async function handleDirectCashfreeWebhookSim() {
const orderId = document.getElementById('cf-sim-order-id').value;
const eventType = document.getElementById('cf-sim-event-type').value;
const out = document.getElementById('cf-webhook-sim-output');
if (!orderId) {
showToast('Please enter an Order ID for webhook test', 'error');
return;
}
out.textContent = `Signing and dispatching HMAC-SHA256 Cashfree ${eventType}...`;
try {
const res = await fetch('/api/payments/engine/simulate-webhook/', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
order_id: orderId,
event_type: eventType
})
});
const data = await res.json();
out.textContent = JSON.stringify(data, null, 2);
logConsole('Cashfree Direct Webhook Verification Response:', data);
if (res.ok) {
showToast(`Cashfree Webhook verified! Order status: ${data.order_status}`, 'success');
refreshDashboard();
} else {
showToast(`Error: ${data.error || 'Webhook failed'}`, 'error');
}
} catch (err) {
out.textContent = 'Error: ' + err.message;
showToast('Error dispatching Cashfree webhook', 'error');
}
}
// Auto-Generate Live Cashfree Payment Session
async function autoGenerateCashfreeSession(amount = 100) {
const btn = document.getElementById('btn-quick-session');
const statusEl = document.getElementById('session-gen-status');
const inputEl = document.getElementById('upi-session-id');
const origText = btn ? btn.textContent : '';
if (btn) btn.textContent = 'Generating...';
if (statusEl) {
statusEl.style.display = 'inline';
statusEl.style.color = '#FBBF24';
statusEl.textContent = 'Contacting Cashfree Sandbox...';
}
try {
const env = getCfStudioEnv();
logConsole(`Auto-generating live Cashfree Payment Session for ₹${amount} [${env}]...`);
const res = await fetch('/api/payments/cashfree/quick-session/', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ amount: amount, environment: env })
});
const data = await res.json();
if (res.ok && data.success) {
inputEl.value = data.payment_session_id;
currentSessionId = data.payment_session_id;
currentOrderId = data.order_id;
document.getElementById('cf-inspect-order-id').value = data.order_id;
document.getElementById('cf-sim-order-id').value = data.order_id;
if (statusEl) {
statusEl.style.color = 'var(--success)';
statusEl.textContent = `✓ Generated Session for ${data.order_id} (₹${amount}) [${env}]`;
}
showToast(`Live Session ID generated for order ${data.order_id} [${env}]!`, 'success');
logConsole('Cashfree Quick Session Generated:', data);
refreshDashboard();
return data.payment_session_id;
} else {
showToast(`Session generation failed: ${data.error || 'Error'}`, 'error');
if (statusEl) {
statusEl.style.color = 'var(--danger)';
statusEl.textContent = '✕ Generation failed';
}
return null;
}
} catch (err) {
showToast('Network error generating session', 'error');
if (statusEl) {
statusEl.style.color = 'var(--danger)';
statusEl.textContent = '✕ Network error';
}
return null;
} finally {
if (btn) btn.textContent = origText;
}
}
// 1-Click: Auto-Generate Session & Immediately Show Dynamic QR
async function autoGenerateAndShowQR() {
// Set mode to QR
document.getElementById('upi-channel-select').value = 'qrcode';
toggleUpiFields();
showToast('Generating live session & dynamic QR...', 'info');
const sessionId = await autoGenerateCashfreeSession(100);
if (sessionId) {
await handleDirectUpiPayment();
}
}
// Handle Direct UPI Payment (QR Code or Collect)
async function handleDirectUpiPayment() {
const sessionId = document.getElementById('upi-session-id').value;
const channel = document.getElementById('upi-channel-select').value;
const upiId = document.getElementById('upi-vpa-input').value;
if (!sessionId) {
showToast('Please provide or create a Payment Session ID first', 'error');
return;
}
const out = document.getElementById('upi-output');
const qrBox = document.getElementById('qr-display-box');
out.textContent = 'Calling Cashfree /orders/sessions endpoint...';
try {
const env = getCfStudioEnv();
const res = await fetch('/api/payments/cashfree/pay/', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
payment_session_id: sessionId,
channel: channel,
upi_id: upiId,
environment: env
})
});
const data = await res.json();
out.textContent = JSON.stringify(data, null, 2);
logConsole('Cashfree Direct UPI Pay Response:', data);
if (res.ok) {
showToast(`UPI request executed! Status: ${data.channel || 'OK'}`, 'success');
// Check if QR code base64 returned
const qrBase64 = data.data?.payload?.qrcode;
if (qrBase64) {
document.getElementById('qr-code-img').src = qrBase64;
document.getElementById('qr-payment-id').textContent = `CF Payment ID: ${data.cf_payment_id}`;
qrBox.style.display = 'flex';
showToast('Dynamic UPI QR Code Generated!', 'success');
} else {
qrBox.style.display = 'none';
}
} else {
showToast(`Error: ${data.message || 'Payment request failed'}`, 'error');
qrBox.style.display = 'none';
}
} catch (err) {
out.textContent = 'Error: ' + err.message;
qrBox.style.display = 'none';
}
}
// Auto-generate test order specifically for Live Server Inspector
async function autoGenerateInspectorOrder(target = 'inspector') {
const btn = document.getElementById('btn-inspect-auto-gen');
const statusEl = document.getElementById('inspector-gen-status');
const origText = btn ? btn.textContent : '';
if (btn) btn.textContent = 'Generating...';
if (statusEl) {
statusEl.style.display = 'inline';
statusEl.style.color = '#FBBF24';
statusEl.textContent = 'Creating order on Cashfree...';
}
try {
const env = getCfStudioEnv();
logConsole(`Auto-generating Cashfree test order for inspection [${env}]...`);
const res = await fetch('/api/payments/cashfree/quick-session/', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ amount: 150, environment: env })
});
const data = await res.json();
if (res.ok && data.success) {
currentOrderId = data.order_id;
currentSessionId = data.payment_session_id;
document.getElementById('cf-inspect-order-id').value = data.order_id;
document.getElementById('cf-sim-order-id').value = data.order_id;
document.getElementById('upi-session-id').value = data.payment_session_id;
if (statusEl) {
statusEl.style.color = 'var(--success)';
statusEl.textContent = `✓ Generated ${data.order_id} [${env}]`;
}
showToast(`Generated order ${data.order_id} on Cashfree [${env}]!`, 'success');
refreshDashboard();
if (target === 'inspector') {
await inspectDirectCashfreeOrder();
}
return data.order_id;
} else {
showToast(`Order generation failed: ${data.error || 'Error'}`, 'error');
if (statusEl) statusEl.textContent = '✕ Generation failed';
}
} catch (err) {
showToast('Network error generating order', 'error');
} finally {
if (btn) btn.textContent = origText;
}
}
function useLatestOrderIdForInspector() {
const input = document.getElementById('cf-inspect-order-id');
if (currentOrderId) {
input.value = currentOrderId;
showToast(`Using latest order: ${currentOrderId}`, 'info');
inspectDirectCashfreeOrder();
} else {
const firstRow = document.querySelector('#orders-table-body strong');
if (firstRow && firstRow.textContent) {
input.value = firstRow.textContent;
showToast(`Using latest order: ${firstRow.textContent}`, 'info');
inspectDirectCashfreeOrder();
} else {
showToast('No existing orders found. Generating one now...', 'info');
autoGenerateInspectorOrder('inspector');
}
}
}
function useLatestOrderIdForWebhook() {
const input = document.getElementById('cf-sim-order-id');
if (currentOrderId) {
input.value = currentOrderId;
showToast(`Using latest order: ${currentOrderId}`, 'info');
} else {
const firstRow = document.querySelector('#orders-table-body strong');
if (firstRow && firstRow.textContent) {
input.value = firstRow.textContent;
showToast(`Using latest order: ${firstRow.textContent}`, 'info');
} else {
showToast('No existing orders found. Generating one now...', 'info');
autoGenerateInspectorOrder('webhook');
}
}
}
function selectOrderForInspection(orderId) {
document.getElementById('cf-inspect-order-id').value = orderId;
document.getElementById('cf-sim-order-id').value = orderId;
inspectDirectCashfreeOrder();
}
// Direct Cashfree Server Inspector
async function inspectDirectCashfreeOrder() {
const orderId = document.getElementById('cf-inspect-order-id').value;
if (!orderId) {
showToast('Enter Order ID to inspect', 'error');
return;
}
const env = getCfStudioEnv();
const out = document.getElementById('cf-inspector-output');
out.textContent = `Querying Cashfree API for raw order ${orderId} [${env}]...`;
try {
const res = await fetch(`/api/payments/cashfree/orders/${orderId}/raw/?environment=${env}`);
const data = await res.json();
out.textContent = JSON.stringify(data, null, 2);
logConsole('Raw Cashfree Server Order Object:', data);
} catch (err) {
out.textContent = 'Error: ' + err.message;
}
}
async function inspectDirectCashfreePayments() {
const orderId = document.getElementById('cf-inspect-order-id').value;
if (!orderId) {
showToast('Enter Order ID to inspect', 'error');
return;
}
const env = getCfStudioEnv();
const out = document.getElementById('cf-inspector-output');
out.textContent = `Querying Cashfree /orders/${orderId}/payments directly [${env}]...`;
try {
const res = await fetch(`/api/payments/cashfree/orders/${orderId}/payments/?environment=${env}`);
const data = await res.json();
out.textContent = JSON.stringify(data, null, 2);
logConsole('Direct Cashfree Payments Array:', data);
showToast(`Fetched ${Array.isArray(data) ? data.length : 0} payment attempts from Cashfree servers`, 'info');
} catch (err) {
out.textContent = 'Error: ' + err.message;
}
}
async function inspectDirectCashfreeSettlement() {
const orderId = document.getElementById('cf-inspect-order-id').value;
if (!orderId) {
showToast('Enter Order ID to inspect', 'error');
return;
}
const env = getCfStudioEnv();
const out = document.getElementById('cf-inspector-output');
out.textContent = `Querying Cashfree /orders/${orderId}/settlements directly [${env}]...`;
try {
const res = await fetch(`/api/payments/cashfree/orders/${orderId}/settlements/?environment=${env}`);
const data = await res.json();
out.textContent = JSON.stringify(data, null, 2);
logConsole('Direct Cashfree Settlement Details:', data);
} catch (err) {
out.textContent = 'Error: ' + err.message;
}
}
// Initial load
document.addEventListener('DOMContentLoaded', () => {
generateOrderId();
refreshDashboard();
});
</script>
</body>
</html>