Skip to content

Instantly share code, notes, and snippets.

@rahulvramesh
Created June 27, 2025 03:32
Show Gist options
  • Save rahulvramesh/1dfa840db2a6ffdb4284219a901e9301 to your computer and use it in GitHub Desktop.
Save rahulvramesh/1dfa840db2a6ffdb4284219a901e9301 to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tipaload Per-Load Payment Test</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
.container {
background: white;
padding: 20px;
margin: 10px 0;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.section {
margin-bottom: 30px;
border-left: 4px solid #007bff;
padding-left: 15px;
}
button {
background: #007bff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
margin: 5px;
}
button:hover {
background: #0056b3;
}
button:disabled {
background: #ccc;
cursor: not-allowed;
}
.success {
background: #d4edda;
color: #155724;
padding: 10px;
border-radius: 4px;
margin: 10px 0;
}
.error {
background: #f8d7da;
color: #721c24;
padding: 10px;
border-radius: 4px;
margin: 10px 0;
}
.info {
background: #d1ecf1;
color: #0c5460;
padding: 10px;
border-radius: 4px;
margin: 10px 0;
}
pre {
background: #f8f9fa;
padding: 10px;
border-radius: 4px;
overflow-x: auto;
max-height: 300px;
}
input, select, textarea {
width: 100%;
padding: 8px;
margin: 5px 0;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
}
.form-row {
display: flex;
gap: 10px;
margin: 10px 0;
}
.form-row > div {
flex: 1;
}
.bid-item {
border: 1px solid #ddd;
padding: 15px;
margin: 10px 0;
border-radius: 4px;
background: #f9f9f9;
}
.bid-item.selected {
border-color: #007bff;
background: #e7f3ff;
}
.allocation-item {
border: 1px solid #ccc;
padding: 10px;
margin: 5px 0;
border-radius: 4px;
background: #f0f0f0;
}
.status-badge {
display: inline-block;
padding: 3px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: bold;
}
.status-pending { background: #ffeaa7; color: #2d3436; }
.status-approved { background: #55a3ff; color: white; }
.status-processing { background: #fdcb6e; color: #2d3436; }
.status-paid { background: #00b894; color: white; }
.status-failed { background: #e17055; color: white; }
.status-completed { background: #6c5ce7; color: white; }
.workflow-step {
border: 2px solid #ddd;
padding: 15px;
margin: 10px 0;
border-radius: 8px;
}
.workflow-step.active {
border-color: #007bff;
background: #e7f3ff;
}
.workflow-step.completed {
border-color: #28a745;
background: #d4edda;
}
</style>
</head>
<body>
<h1>🚛 Tipaload Per-Load Payment System Test</h1>
<!-- Authentication Section -->
<div class="container section">
<h2>1. Authentication</h2>
<div class="form-row">
<div>
<label>Username:</label>
<input type="text" id="username" value="+618129220033">
</div>
<div>
<label>Password:</label>
<input type="password" id="password" value="tipaload">
</div>
</div>
<button onclick="login()">Login</button>
<div id="authStatus"></div>
</div>
<!-- Job Management Section -->
<div class="container section">
<h2>2. Job Management</h2>
<button onclick="createJob()">Create New Job</button>
<button onclick="updateJobToBidding()">Update Job to Bidding</button>
<button onclick="getJobs()">Refresh Jobs</button>
<div id="jobStatus"></div>
<div id="jobsList"></div>
</div>
<!-- Bid Management Section -->
<div class="container section">
<h2>3. Bid Management</h2>
<div class="form-row">
<div>
<label>Job ID for Bidding:</label>
<input type="text" id="bidJobId" placeholder="Select a job first">
</div>
</div>
<button onclick="createBid()">Create Bid</button>
<button onclick="getBidsForJob()">Get Bids for Job</button>
<div id="bidStatus"></div>
<div id="bidsList"></div>
</div>
<!-- NEW: Bid Approval Section -->
<div class="container section">
<h2>4. Bid Approval (New API)</h2>
<div class="info">
<strong>Per-Load Payment System:</strong> No payment required during bid approval. Payment will be collected after each load completion.
</div>
<button onclick="approveBids()">Approve Selected Bids</button>
<div id="bidApprovalStatus"></div>
</div>
<!-- Load Workflow Section -->
<div class="container section">
<h2>5. Load Workflow (Per-Load Payment)</h2>
<div class="form-row">
<div>
<label>Allocation ID:</label>
<input type="text" id="workflowAllocationId" placeholder="Select allocation">
</div>
</div>
<div class="workflow-step" id="step1">
<h4>Step 1: Start Trip</h4>
<button onclick="startTrip()">Start Trip</button>
</div>
<div class="workflow-step" id="step2">
<h4>Step 2: Complete Load & Create Docket</h4>
<div class="form-row">
<div>
<label>Quantity:</label>
<input type="number" id="loadQuantity" value="1" step="0.1">
</div>
<div>
<label>Signature (Base64):</label>
<input type="text" id="signatureBase64" value="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==">
</div>
</div>
<button onclick="createDocket()">Complete Load & Create Docket</button>
</div>
<div class="workflow-step" id="step3">
<h4>Step 3: Payment (If Required)</h4>
<div class="info">Payment will be automatically required if this is not the first load or if payment failed previously.</div>
<button onclick="checkCanStartNextLoad()">Check Payment Status</button>
<button onclick="retryPayment()" disabled id="retryPaymentBtn">Retry Failed Payment</button>
</div>
<div class="workflow-step" id="step4">
<h4>Step 4: Next Load</h4>
<button onclick="startNextLoad()">Start Next Load</button>
</div>
<div id="workflowStatus"></div>
</div>
<!-- System State Section -->
<div class="container section">
<h2>6. System State</h2>
<button onclick="getAllocations()">Refresh Allocations</button>
<button onclick="getPaymentStatus()">Check Payment Status</button>
<div id="allocationsStatus"></div>
<div id="allocationsList"></div>
</div>
<!-- Debug Section -->
<div class="container section">
<h2>7. Debug & Logs</h2>
<button onclick="clearLogs()">Clear Logs</button>
<pre id="debugLogs"></pre>
</div>
<script>
// Global state
let authToken = '';
let currentJobId = '';
let currentBids = [];
let selectedBidIds = [];
let currentAllocations = [];
let currentAllocationId = '';
const API_BASE = 'http://100.84.162.116:8080';
// Utility functions
function log(message) {
const timestamp = new Date().toLocaleTimeString();
const logElement = document.getElementById('debugLogs');
logElement.textContent += `[${timestamp}] ${message}\n`;
logElement.scrollTop = logElement.scrollHeight;
console.log(message);
}
function clearLogs() {
document.getElementById('debugLogs').textContent = '';
}
function showStatus(elementId, message, type = 'info') {
const element = document.getElementById(elementId);
element.innerHTML = `<div class="${type}">${message}</div>`;
}
function showError(elementId, error) {
showStatus(elementId, `Error: ${error.message || error}`, 'error');
log(`ERROR: ${error.message || error}`);
}
function showSuccess(elementId, message) {
showStatus(elementId, message, 'success');
log(`SUCCESS: ${message}`);
}
// API helper
async function apiCall(endpoint, method = 'GET', data = null) {
const options = {
method,
headers: {
'Content-Type': 'application/json',
}
};
if (authToken) {
options.headers['Authorization'] = `Bearer ${authToken}`;
}
if (data) {
options.body = JSON.stringify(data);
}
log(`API Call: ${method} ${endpoint}`);
if (data) log(`Request data: ${JSON.stringify(data, null, 2)}`);
const response = await fetch(`${API_BASE}${endpoint}`, options);
const result = await response.text();
log(`Response status: ${response.status}`);
log(`Response: ${result}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${result}`);
}
try {
return JSON.parse(result);
} catch {
return result;
}
}
// Authentication
async function login() {
try {
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
const result = await apiCall('/auth/signin', 'POST', { username, password });
// Handle different possible response formats
authToken = result.access_token || result.accessToken || result.token || result;
if (!authToken) {
throw new Error('No access token received from server');
}
// Safely handle token display
const tokenDisplay = typeof authToken === 'string' && authToken.length > 20
? `${authToken.substring(0, 20)}...`
: 'Token received';
showSuccess('authStatus', `Logged in successfully! Token: ${tokenDisplay}`);
log(`Auth token set: ${authToken}`);
log(`Full login response: ${JSON.stringify(result, null, 2)}`);
} catch (error) {
showError('authStatus', error);
}
}
// Job Management
async function createJob() {
try {
const jobData = {
companyID: "682411097457a8bd66c0cc8f",
type: "cart_and_dispose"
};
const result = await apiCall('/jobs', 'POST', jobData);
currentJobId = result.id;
document.getElementById('bidJobId').value = currentJobId;
showSuccess('jobStatus', `Job created! ID: ${result.id}`);
log(`Job created with ID: ${result.id}`);
await getJobs();
} catch (error) {
showError('jobStatus', error);
}
}
async function updateJobToBidding() {
try {
if (!currentJobId) {
throw new Error('No job selected. Create a job first.');
}
const updateData = {
status: "bidding",
type: "cart_and_dispose",
unitRate: "load",
loadNum: 3,
vehicleNum: 1,
tonneNum: 0,
vehicleTypeIDs: ["rigid_truck_3_bogie"],
wasteTypeID: "venm",
materialTypeIDs: ["clay", "sand"],
jobDate: "2025-01-15T09:00:00.000Z",
additionalInfo: "Construction waste removal - 3 loads required",
mobileNumber: "0412345678",
uhfNumber: "CH40",
betweenTrucksDuration: 1800,
minimumHireDuration: 7200,
loadTruckDuration: 2700,
minimumTravelDuration: 1800,
entrySiteDirection: "front",
exitSiteDirection: "front",
jobLocations: [{
type: "Point",
locationType: "pickup",
address: "123 Construction St, Sydney NSW 2000",
coordinates: [-33.8688,151.2093]
}],
startLocation: {
type: "Point",
locationType: "pickup",
address: "123 Construction St, Sydney NSW 2000",
coordinates: [-33.8688,151.2093]
},
vehicleNums: {
rigid_truck_3_bogie: 1
},
suggestedCosts: {
rigid_truck_3_bogie: 50000
}
};
const result = await apiCall(`/jobs/${currentJobId}`, 'PATCH', updateData);
showSuccess('jobStatus', `Job updated to bidding status!`);
await getJobs();
} catch (error) {
showError('jobStatus', error);
}
}
async function getJobs() {
try {
const result = await apiCall('/jobs');
const jobsHtml = result.data.map(job => `
<div class="bid-item" onclick="selectJob('${job.id}')" style="cursor: pointer;">
<strong>Job ${job.id}</strong><br>
Status: <span class="status-badge status-${job.status}">${job.status}</span><br>
Type: ${job.type}<br>
${job.loadNum ? `Loads: ${job.loadNum}` : ''}<br>
${job.additionalInfo ? `Info: ${job.additionalInfo}` : ''}
</div>
`).join('');
document.getElementById('jobsList').innerHTML = jobsHtml;
showSuccess('jobStatus', `Found ${result.data.length} jobs`);
} catch (error) {
showError('jobStatus', error);
}
}
function selectJob(jobId) {
currentJobId = jobId;
document.getElementById('bidJobId').value = jobId;
showSuccess('jobStatus', `Selected job: ${jobId}`);
// Auto-load bids for this job
getBidsForJob();
}
// Bid Management
async function createBid() {
try {
const jobId = document.getElementById('bidJobId').value;
if (!jobId) {
throw new Error('Please enter a job ID');
}
const bidData = {
companyID: "682411097457a8bd66c0cc8f",
jobID: jobId,
cost: 150000,
costs: {
load_service: 50000
},
amountBidded: 3,
allocations: [{
jobType: "cart_and_dispose",
unitRate: "load",
amount: 3,
vehicleID: "682775308a74a0be5fcc1597",
jobID: jobId,
companyID: "682411097457a8bd66c0cc8f",
bidID: ""
}],
comments: "Ready to handle 3 loads of construction waste disposal with rigid truck"
};
const result = await apiCall('/bids', 'POST', bidData);
showSuccess('bidStatus', `Bid created! ID: ${result.id}`);
await getBidsForJob();
} catch (error) {
showError('bidStatus', error);
}
}
async function getBidsForJob() {
try {
const jobId = document.getElementById('bidJobId').value;
if (!jobId) {
throw new Error('Please enter a job ID');
}
const result = await apiCall(`/bids/find_bids_by_job?jobID=${jobId}`);
currentBids = result.data;
const bidsHtml = currentBids.map(bid => `
<div class="bid-item ${selectedBidIds.includes(bid.id) ? 'selected' : ''}"
onclick="toggleBidSelection('${bid.id}')">
<input type="checkbox" ${selectedBidIds.includes(bid.id) ? 'checked' : ''}
onchange="toggleBidSelection('${bid.id}')">
<strong>Bid ${bid.id}</strong><br>
Status: <span class="status-badge status-${bid.status}">${bid.status}</span><br>
Company: ${bid.company.businessName}<br>
Amount Bidded: ${bid.amountBidded}<br>
Amount Approved: ${bid.amountApproved || 0}<br>
Comments: ${bid.comments}<br>
<div style="margin-top: 10px;">
<strong>Allocations:</strong>
${bid.allocations.map(alloc => `
<div class="allocation-item" onclick="selectAllocation('${alloc.id}', event)">
ID: ${alloc.id}<br>
Status: <span class="status-badge status-${alloc.status}">${alloc.status}</span><br>
Amount: ${alloc.amount}, Approved: ${alloc.amountApproved || 0}<br>
Vehicle: ${alloc.vehicle.rego}
${alloc.loadNumber ? `<br>Load #: ${alloc.loadNumber}` : ''}
${alloc.loadPaymentStatus ? `<br>Payment: <span class="status-badge status-${alloc.loadPaymentStatus}">${alloc.loadPaymentStatus}</span>` : ''}
</div>
`).join('')}
</div>
</div>
`).join('');
document.getElementById('bidsList').innerHTML = bidsHtml;
showSuccess('bidStatus', `Found ${currentBids.length} bids for job ${jobId}`);
} catch (error) {
showError('bidStatus', error);
}
}
function toggleBidSelection(bidId) {
if (selectedBidIds.includes(bidId)) {
selectedBidIds = selectedBidIds.filter(id => id !== bidId);
} else {
selectedBidIds.push(bidId);
}
getBidsForJob(); // Refresh to show selection
}
function selectAllocation(allocationId, event) {
event.stopPropagation();
currentAllocationId = allocationId;
document.getElementById('workflowAllocationId').value = allocationId;
showSuccess('workflowStatus', `Selected allocation: ${allocationId}`);
}
// NEW: Bid Approval with New API
async function approveBids() {
try {
if (selectedBidIds.length === 0) {
throw new Error('Please select at least one bid to approve');
}
const approvalData = {
bidIds: selectedBidIds
};
const result = await apiCall('/bids/approve_by_bid_ids', 'POST', approvalData);
showSuccess('bidApprovalStatus', `${result} - Selected ${selectedBidIds.length} bids`);
// Refresh bids to show updated status
await getBidsForJob();
await getAllocations();
// Clear selection
selectedBidIds = [];
} catch (error) {
showError('bidApprovalStatus', error);
}
}
// Load Workflow
async function startTrip() {
try {
const allocationId = document.getElementById('workflowAllocationId').value;
if (!allocationId) {
throw new Error('Please select an allocation');
}
const result = await apiCall(`/allocations/${allocationId}/start_trip`, 'POST');
showSuccess('workflowStatus', 'Trip started successfully!');
document.getElementById('step1').classList.add('completed');
document.getElementById('step2').classList.add('active');
await getAllocations();
} catch (error) {
showError('workflowStatus', error);
}
}
async function createDocket() {
try {
const allocationId = document.getElementById('workflowAllocationId').value;
const quantity = document.getElementById('loadQuantity').value;
const signature = document.getElementById('signatureBase64').value;
if (!allocationId) {
throw new Error('Please select an allocation');
}
const docketData = {
signedBase64: signature,
quantity: quantity,
cancelByShipper: false
};
const result = await apiCall(`/allocations/${allocationId}/create_docket`, 'POST', docketData);
let message = 'Docket created successfully!';
if (result.loadPaymentStatus === 'processing') {
message += ` Payment required for Load #${result.loadNumber}. Payment Intent: ${result.loadPaymentIntentId}`;
document.getElementById('step3').classList.add('active');
document.getElementById('retryPaymentBtn').disabled = false;
}
showSuccess('workflowStatus', message);
document.getElementById('step2').classList.add('completed');
await getAllocations();
} catch (error) {
showError('workflowStatus', error);
}
}
async function checkCanStartNextLoad() {
try {
const allocationId = document.getElementById('workflowAllocationId').value;
if (!allocationId) {
throw new Error('Please select an allocation');
}
const result = await apiCall(`/allocations/${allocationId}/can-start-next-load`);
if (result.canProceed) {
showSuccess('workflowStatus', 'Payment complete! Can start next load.');
document.getElementById('step3').classList.add('completed');
document.getElementById('step4').classList.add('active');
} else {
showStatus('workflowStatus', `${result.message}. Payment Intent: ${result.paymentIntentId}`, 'info');
document.getElementById('retryPaymentBtn').disabled = false;
}
} catch (error) {
showError('workflowStatus', error);
}
}
async function retryPayment() {
try {
const allocationId = document.getElementById('workflowAllocationId').value;
if (!allocationId) {
throw new Error('Please select an allocation');
}
const result = await apiCall(`/allocations/${allocationId}/retry-load-payment`, 'POST');
showSuccess('workflowStatus', `Payment retry initiated. New Payment Intent: ${result.intentID}`);
// Simulate payment success after a delay (for testing)
setTimeout(async () => {
await simulatePaymentSuccess(allocationId);
}, 3000);
} catch (error) {
showError('workflowStatus', error);
}
}
async function simulatePaymentSuccess(allocationId) {
try {
// In a real scenario, this would be handled by Stripe webhooks
showSuccess('workflowStatus', '💳 Payment simulation: Payment completed successfully!');
document.getElementById('step3').classList.add('completed');
document.getElementById('step4').classList.add('active');
await getAllocations();
} catch (error) {
showError('workflowStatus', error);
}
}
async function startNextLoad() {
try {
const allocationId = document.getElementById('workflowAllocationId').value;
if (!allocationId) {
throw new Error('Please select an allocation');
}
// Check if we can start next load
const canStart = await apiCall(`/allocations/${allocationId}/can-start-next-load`);
if (!canStart.canProceed) {
throw new Error(canStart.message);
}
showSuccess('workflowStatus', 'Next load can be started! Reset workflow for next load.');
// Reset workflow steps
document.querySelectorAll('.workflow-step').forEach(step => {
step.classList.remove('active', 'completed');
});
document.getElementById('step1').classList.add('active');
await getAllocations();
} catch (error) {
showError('workflowStatus', error);
}
}
// System State
async function getAllocations() {
try {
const result = await apiCall('/allocations');
currentAllocations = result.data;
const allocationsHtml = currentAllocations.map(alloc => `
<div class="allocation-item" onclick="selectAllocation('${alloc.id}', event)">
<strong>Allocation ${alloc.id}</strong><br>
Status: <span class="status-badge status-${alloc.status}">${alloc.status}</span><br>
Vehicle: ${alloc.vehicle.rego}<br>
Amount: ${alloc.amount}, Approved: ${alloc.amountApproved || 0}, Completed: ${alloc.amountCompleted || 0}<br>
${alloc.loadNumber ? `Load #: ${alloc.loadNumber}<br>` : ''}
${alloc.loadPaymentStatus ? `Payment Status: <span class="status-badge status-${alloc.loadPaymentStatus}">${alloc.loadPaymentStatus}</span><br>` : ''}
${alloc.loadPaymentIntentId ? `Payment Intent: ${alloc.loadPaymentIntentId}<br>` : ''}
${alloc.tripStartDate ? `Trip Started: ${new Date(alloc.tripStartDate).toLocaleString()}<br>` : ''}
${alloc.tripCompleteDate ? `Trip Completed: ${new Date(alloc.tripCompleteDate).toLocaleString()}<br>` : ''}
</div>
`).join('');
document.getElementById('allocationsList').innerHTML = allocationsHtml;
showSuccess('allocationsStatus', `Found ${currentAllocations.length} allocations`);
} catch (error) {
showError('allocationsStatus', error);
}
}
async function getPaymentStatus() {
try {
if (!currentAllocationId) {
throw new Error('Please select an allocation first');
}
const result = await apiCall(`/allocations/${currentAllocationId}`);
const paymentInfo = {
loadNumber: result.loadNumber,
loadPaymentStatus: result.loadPaymentStatus,
loadPaymentIntentId: result.loadPaymentIntentId,
loadInvoiceId: result.loadInvoiceId
};
showSuccess('allocationsStatus', `Payment Status: ${JSON.stringify(paymentInfo, null, 2)}`);
} catch (error) {
showError('allocationsStatus', error);
}
}
// Initialize
document.addEventListener('DOMContentLoaded', function() {
log('Frontend test page loaded');
showStatus('authStatus', 'Please login to start testing', 'info');
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment