generated from vignesh/searchengine
All checks were successful
Deploy Beta (NATIVE) / deploy (push) Successful in 36s
2144 lines
81 KiB
HTML
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>
|