You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
554 lines
20 KiB
554 lines
20 KiB
{% extends "base.html" %}
|
|
|
|
{% block title %}Single Payment - Plutus{% endblock %}
|
|
|
|
{% block content %}
|
|
<nav class="breadcrumb" aria-label="breadcrumbs">
|
|
<ul>
|
|
<li><a href="{{ url_for('main.index') }}">Dashboard</a></li>
|
|
<li class="is-active"><a href="#" aria-current="page">Single Payment</a></li>
|
|
</ul>
|
|
</nav>
|
|
|
|
<div class="level">
|
|
<div class="level-left">
|
|
<div>
|
|
<h1 class="title">Single Payment Processing</h1>
|
|
<p class="subtitle">Process individual customer payments through Stripe</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Single Payment Form -->
|
|
<div class="box">
|
|
<!-- Step 1: Enter Splynx ID -->
|
|
<div id="step1" class="payment-step">
|
|
<h2 class="title is-4">
|
|
<span class="icon"><i class="fas fa-search"></i></span>
|
|
Customer Lookup
|
|
</h2>
|
|
|
|
<div class="field">
|
|
<label class="label" for="lookup_splynx_id">Splynx Customer ID</label>
|
|
<div class="control">
|
|
<input class="input" type="number" id="lookup_splynx_id" placeholder="Enter customer ID" required>
|
|
</div>
|
|
<p class="help">Enter the Splynx customer ID to fetch customer details</p>
|
|
</div>
|
|
|
|
<!-- Loading State -->
|
|
<div id="loading" class="has-text-centered py-5 is-hidden">
|
|
<div class="spinner"></div>
|
|
<p class="mt-3">Fetching customer details...</p>
|
|
</div>
|
|
|
|
<!-- Error State -->
|
|
<div id="customerError" class="notification is-danger is-hidden">
|
|
<span class="icon"><i class="fas fa-exclamation-triangle"></i></span>
|
|
<span id="errorMessage">Customer not found or error occurred</span>
|
|
</div>
|
|
|
|
<div class="field is-grouped">
|
|
<div class="control">
|
|
<button class="button is-primary" id="nextBtn" onclick="fetchCustomerDetails()">
|
|
<span class="icon"><i class="fas fa-arrow-right"></i></span>
|
|
<span>Next</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Step 2: Confirm Customer & Enter Amount -->
|
|
<div id="step2" class="payment-step is-hidden">
|
|
<h2 class="title is-4">
|
|
<span class="icon"><i class="fas fa-user-check"></i></span>
|
|
Confirm Customer & Payment Details
|
|
</h2>
|
|
|
|
<div class="box has-background-light mb-5">
|
|
<h3 class="subtitle is-5">Customer Information</h3>
|
|
<div id="customerDetails">
|
|
<!-- Customer details will be populated here -->
|
|
</div>
|
|
</div>
|
|
|
|
<form id="paymentForm">
|
|
<input type="hidden" id="confirmed_splynx_id" name="splynx_id">
|
|
|
|
<div class="field">
|
|
<label class="label" for="payment_amount">Payment Amount (AUD)</label>
|
|
<div class="control has-icons-left">
|
|
<input class="input is-large" type="number" step="0.01" min="0.01" max="10000"
|
|
id="payment_amount" name="amount" placeholder="0.00" required>
|
|
<span class="icon is-small is-left">
|
|
<i class="fas fa-dollar-sign"></i>
|
|
</span>
|
|
</div>
|
|
<p class="help">Enter the amount to charge (maximum $10,000)</p>
|
|
</div>
|
|
|
|
<div class="notification is-info is-light">
|
|
<span class="icon"><i class="fas fa-info-circle"></i></span>
|
|
This payment will be processed immediately using the customer's default Stripe payment method.
|
|
</div>
|
|
</form>
|
|
|
|
<div class="field is-grouped">
|
|
<div class="control">
|
|
<button class="button is-light" id="backBtn" onclick="goBackToStep1()">
|
|
<span class="icon"><i class="fas fa-arrow-left"></i></span>
|
|
<span>Back</span>
|
|
</button>
|
|
</div>
|
|
<div class="control">
|
|
<button class="button is-warning" id="processBtn" onclick="showConfirmationModal()">
|
|
<span class="icon"><i class="fas fa-credit-card"></i></span>
|
|
<span>Process Payment</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Payment Confirmation Modal -->
|
|
<div class="modal" id="confirmationModal">
|
|
<div class="modal-background" onclick="hideModal('confirmationModal')"></div>
|
|
<div class="modal-card">
|
|
<header class="modal-card-head has-background-warning">
|
|
<p class="modal-card-title">
|
|
<span class="icon"><i class="fas fa-exclamation-triangle"></i></span>
|
|
Confirm Payment Processing
|
|
</p>
|
|
<button class="delete" aria-label="close" onclick="hideModal('confirmationModal')"></button>
|
|
</header>
|
|
<section class="modal-card-body">
|
|
<div class="content">
|
|
<p class="is-size-5 has-text-weight-semibold">Are you sure you want to process this payment?</p>
|
|
|
|
<div class="box has-background-light">
|
|
<div class="columns">
|
|
<div class="column is-half">
|
|
<strong>Customer:</strong><br>
|
|
<span id="confirmCustomerName">-</span>
|
|
</div>
|
|
<div class="column is-half">
|
|
<strong>Amount:</strong><br>
|
|
<span id="confirmAmount" class="has-text-weight-bold is-size-4">$0.00</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="notification is-warning is-light">
|
|
<span class="icon"><i class="fas fa-exclamation-triangle"></i></span>
|
|
<strong>Warning:</strong> This action cannot be undone. The payment will be charged immediately.
|
|
</div>
|
|
</div>
|
|
</section>
|
|
<footer class="modal-card-foot">
|
|
<button class="button is-danger" id="confirmPaymentBtn" onclick="processPayment()">
|
|
<span class="icon"><i class="fas fa-credit-card"></i></span>
|
|
<span>Confirm & Process Payment</span>
|
|
</button>
|
|
<button class="button" onclick="hideModal('confirmationModal')">Cancel</button>
|
|
</footer>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Success Modal -->
|
|
<div class="modal" id="successModal">
|
|
<div class="modal-background"></div>
|
|
<div class="modal-card">
|
|
<header class="modal-card-head has-background-success">
|
|
<p class="modal-card-title has-text-white">
|
|
<span class="icon"><i class="fas fa-check-circle"></i></span>
|
|
Payment Successful
|
|
</p>
|
|
</header>
|
|
<section class="modal-card-body">
|
|
<div class="has-text-centered py-4">
|
|
<span class="icon is-large has-text-success mb-4">
|
|
<i class="fas fa-check-circle fa-3x"></i>
|
|
</span>
|
|
<h3 class="title is-4">Payment Processed Successfully!</h3>
|
|
<div id="successMessage" class="content">
|
|
<!-- Success details will be populated here -->
|
|
</div>
|
|
</div>
|
|
</section>
|
|
<footer class="modal-card-foot is-justify-content-center">
|
|
<button class="button is-primary" onclick="closeSuccessModal()">
|
|
<span class="icon"><i class="fas fa-check"></i></span>
|
|
<span>Close</span>
|
|
</button>
|
|
</footer>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Fee Update Modal (Orange) -->
|
|
<div class="modal" id="feeUpdateModal">
|
|
<div class="modal-background"></div>
|
|
<div class="modal-card">
|
|
<header class="modal-card-head has-background-warning">
|
|
<p class="modal-card-title has-text-dark">
|
|
<span class="icon"><i class="fas fa-clock"></i></span>
|
|
Direct Debit Processing
|
|
</p>
|
|
</header>
|
|
<section class="modal-card-body">
|
|
<div class="has-text-centered py-4">
|
|
<span class="icon is-large has-text-warning mb-4">
|
|
<i class="fas fa-clock fa-3x"></i>
|
|
</span>
|
|
<h3 class="title is-4">Direct Debit is still being processed</h3>
|
|
<div class="content">
|
|
<p>Your Direct Debit payment is currently being processed by the bank. This can take a few minutes to complete.</p>
|
|
<p><strong>Please check back later or click the button below to view payment details.</strong></p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
<footer class="modal-card-foot is-justify-content-center">
|
|
<button class="button is-warning" id="viewPaymentDetailsBtn" onclick="viewPaymentDetails()">
|
|
<span class="icon"><i class="fas fa-eye"></i></span>
|
|
<span>View Payment Details</span>
|
|
</button>
|
|
</footer>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Error Modal -->
|
|
<div class="modal" id="errorModal">
|
|
<div class="modal-background" onclick="hideModal('errorModal')"></div>
|
|
<div class="modal-card">
|
|
<header class="modal-card-head has-background-danger">
|
|
<p class="modal-card-title has-text-white">
|
|
<span class="icon"><i class="fas fa-exclamation-circle"></i></span>
|
|
Payment Failed
|
|
</p>
|
|
<button class="delete" aria-label="close" onclick="hideModal('errorModal')"></button>
|
|
</header>
|
|
<section class="modal-card-body">
|
|
<div class="has-text-centered py-4">
|
|
<span class="icon is-large has-text-danger mb-4">
|
|
<i class="fas fa-exclamation-circle fa-3x"></i>
|
|
</span>
|
|
<h3 class="title is-4">Payment Processing Failed</h3>
|
|
<div id="errorDetails" class="content">
|
|
<!-- Error details will be populated here -->
|
|
</div>
|
|
</div>
|
|
</section>
|
|
<footer class="modal-card-foot is-justify-content-center">
|
|
<button class="button is-danger" onclick="hideModal('errorModal')">
|
|
<span class="icon"><i class="fas fa-times"></i></span>
|
|
<span>Close</span>
|
|
</button>
|
|
</footer>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
/* Loading spinner */
|
|
.spinner {
|
|
display: inline-block;
|
|
width: 40px;
|
|
height: 40px;
|
|
border: 4px solid rgba(212, 175, 55, 0.3);
|
|
border-radius: 50%;
|
|
border-top-color: var(--plutus-gold);
|
|
animation: spin 1s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes spin {
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
/* Step transitions */
|
|
.payment-step {
|
|
transition: opacity 0.3s ease, transform 0.3s ease;
|
|
}
|
|
|
|
.payment-step.is-hidden {
|
|
display: none;
|
|
}
|
|
|
|
/* Enhanced form styling */
|
|
.input.is-large {
|
|
font-size: 1.5rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* Modal enhancements */
|
|
.modal-card-head.has-background-warning {
|
|
color: var(--plutus-charcoal);
|
|
}
|
|
|
|
.modal-card-head.has-background-success {
|
|
color: white;
|
|
}
|
|
|
|
.modal-card-head.has-background-danger {
|
|
color: white;
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
let currentCustomerData = null;
|
|
let currentPaymentId = null;
|
|
|
|
function fetchCustomerDetails() {
|
|
const splynxIdElement = document.getElementById('lookup_splynx_id');
|
|
const splynxId = splynxIdElement ? splynxIdElement.value : '';
|
|
|
|
// Clear previous errors
|
|
document.getElementById('customerError').classList.add('is-hidden');
|
|
|
|
if (!splynxId || splynxId.trim() === '' || splynxId.trim() === '0') {
|
|
showError('Please enter a valid Splynx Customer ID');
|
|
return;
|
|
}
|
|
|
|
// Show loading state
|
|
document.getElementById('loading').classList.remove('is-hidden');
|
|
document.getElementById('nextBtn').disabled = true;
|
|
|
|
const apiUrl = `/api/splynx/${splynxId.trim()}`;
|
|
|
|
// Make API call
|
|
fetch(apiUrl)
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
// Hide loading
|
|
document.getElementById('loading').classList.add('is-hidden');
|
|
document.getElementById('nextBtn').disabled = false;
|
|
|
|
if (data && data.id) {
|
|
currentCustomerData = data;
|
|
displayCustomerDetails(data);
|
|
goToStep2();
|
|
} else {
|
|
showError('Customer not found or invalid data received');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error fetching customer:', error);
|
|
document.getElementById('loading').classList.add('is-hidden');
|
|
document.getElementById('nextBtn').disabled = false;
|
|
showError(`Failed to fetch customer details: ${error.message}`);
|
|
});
|
|
}
|
|
|
|
function displayCustomerDetails(customer) {
|
|
const detailsHtml = `
|
|
<div class="columns is-multiline">
|
|
<div class="column is-half">
|
|
<strong>Name:</strong><br>
|
|
<span>${customer.name || 'N/A'}</span>
|
|
</div>
|
|
<div class="column is-half">
|
|
<strong>Customer ID:</strong><br>
|
|
<span class="tag is-info">${customer.id}</span>
|
|
</div>
|
|
<div class="column is-half">
|
|
<strong>Status:</strong><br>
|
|
${customer.status === 'active'
|
|
? '<span class="tag is-success">Active</span>'
|
|
: `<span class="tag is-warning">${customer.status || 'Unknown'}</span>`
|
|
}
|
|
</div>
|
|
<div class="column is-half">
|
|
<strong>Email:</strong><br>
|
|
<span>${customer.email || 'N/A'}</span>
|
|
</div>
|
|
<div class="column is-full">
|
|
<strong>Address:</strong><br>
|
|
<span>${customer.street_1 || ''} ${customer.street_2 || ''}<br>
|
|
${customer.city || ''} ${customer.zip_code || ''}</span>
|
|
</div>
|
|
<div class="column is-half">
|
|
<strong>Phone:</strong><br>
|
|
<span>${customer.phone || 'N/A'}</span>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
document.getElementById('customerDetails').innerHTML = detailsHtml;
|
|
document.getElementById('confirmed_splynx_id').value = customer.id;
|
|
}
|
|
|
|
function showError(message) {
|
|
document.getElementById('errorMessage').textContent = message;
|
|
document.getElementById('customerError').classList.remove('is-hidden');
|
|
}
|
|
|
|
function goToStep2() {
|
|
// Hide step 1, show step 2
|
|
document.getElementById('step1').classList.add('is-hidden');
|
|
document.getElementById('step2').classList.remove('is-hidden');
|
|
|
|
// Focus on amount input
|
|
document.getElementById('payment_amount').focus();
|
|
}
|
|
|
|
function goBackToStep1() {
|
|
// Show step 1, hide step 2
|
|
document.getElementById('step1').classList.remove('is-hidden');
|
|
document.getElementById('step2').classList.add('is-hidden');
|
|
|
|
// Clear any errors
|
|
document.getElementById('customerError').classList.add('is-hidden');
|
|
|
|
// Clear form
|
|
document.getElementById('payment_amount').value = '';
|
|
}
|
|
|
|
function showConfirmationModal() {
|
|
const amount = document.getElementById('payment_amount').value;
|
|
|
|
if (!amount || parseFloat(amount) <= 0) {
|
|
alert('Please enter a valid payment amount');
|
|
return;
|
|
}
|
|
|
|
if (!currentCustomerData) {
|
|
alert('Customer data not found. Please restart the process.');
|
|
return;
|
|
}
|
|
|
|
// Update confirmation modal content
|
|
document.getElementById('confirmCustomerName').textContent = currentCustomerData.name || 'Unknown';
|
|
document.getElementById('confirmAmount').textContent = `$${parseFloat(amount).toFixed(2)}`;
|
|
|
|
// Show modal
|
|
document.getElementById('confirmationModal').classList.add('is-active');
|
|
}
|
|
|
|
function processPayment() {
|
|
const form = document.getElementById('paymentForm');
|
|
const formData = new FormData(form);
|
|
|
|
// Disable confirm button and show loading
|
|
const confirmBtn = document.getElementById('confirmPaymentBtn');
|
|
const originalText = confirmBtn.innerHTML;
|
|
confirmBtn.disabled = true;
|
|
confirmBtn.innerHTML = '<span class="icon"><i class="fas fa-spinner fa-spin"></i></span><span>Processing...</span>';
|
|
|
|
// Submit the payment
|
|
fetch('/single-payment/process', {
|
|
method: 'POST',
|
|
body: formData
|
|
})
|
|
.then(response => {
|
|
return response.json().then(data => {
|
|
return { status: response.status, data: data };
|
|
});
|
|
})
|
|
.then(result => {
|
|
// Hide confirmation modal
|
|
hideModal('confirmationModal');
|
|
|
|
const { status, data } = result;
|
|
|
|
// Check if payment was successful
|
|
if (status === 200 && data.success && data.payment_success) {
|
|
showSuccessModal(data);
|
|
} else if (status === 422 && data.fee_update) {
|
|
// Direct Debit needs fee update - show orange modal
|
|
showFeeUpdateModal(data);
|
|
} else {
|
|
// Payment failed or had an error - show the specific error
|
|
let errorMessage;
|
|
|
|
if (status === 422) {
|
|
// Payment processing failed (business logic error)
|
|
errorMessage = `Payment Failed: ${data.stripe_error || data.error || 'Unknown error'}`;
|
|
} else if (status >= 400) {
|
|
// Other HTTP errors
|
|
errorMessage = data.error || 'Payment processing failed';
|
|
} else {
|
|
// Unexpected status
|
|
errorMessage = 'Payment processing failed. Please try again.';
|
|
}
|
|
|
|
showErrorModal(errorMessage);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error processing payment:', error);
|
|
hideModal('confirmationModal');
|
|
showErrorModal('Payment processing failed. Please try again.');
|
|
})
|
|
.finally(() => {
|
|
// Re-enable button
|
|
confirmBtn.disabled = false;
|
|
confirmBtn.innerHTML = originalText;
|
|
});
|
|
}
|
|
|
|
function showSuccessModal(data) {
|
|
const successHtml = `
|
|
<p><strong>Payment ID:</strong> ${data.payment_id}</p>
|
|
<p><strong>Payment Intent:</strong> ${data.payment_intent || 'N/A'}</p>
|
|
<p><strong>Amount:</strong> $${parseFloat(data.amount).toFixed(2)}</p>
|
|
<p><strong>Customer:</strong> ${data.customer_name}</p>
|
|
`;
|
|
|
|
document.getElementById('successMessage').innerHTML = successHtml;
|
|
document.getElementById('successModal').classList.add('is-active');
|
|
}
|
|
|
|
function showErrorModal(errorMessage) {
|
|
document.getElementById('errorDetails').innerHTML = `<p>${errorMessage}</p>`;
|
|
document.getElementById('errorModal').classList.add('is-active');
|
|
}
|
|
|
|
function hideModal(modalId) {
|
|
document.getElementById(modalId).classList.remove('is-active');
|
|
}
|
|
|
|
function showFeeUpdateModal(data) {
|
|
currentPaymentId = data.payment_id;
|
|
document.getElementById('feeUpdateModal').classList.add('is-active');
|
|
}
|
|
|
|
function viewPaymentDetails() {
|
|
if (currentPaymentId) {
|
|
// Redirect to the single payment detail page
|
|
window.location.href = `/single-payment/detail/${currentPaymentId}`;
|
|
}
|
|
}
|
|
|
|
function closeSuccessModal() {
|
|
hideModal('successModal');
|
|
// Reset form to step 1
|
|
goBackToStep1();
|
|
document.getElementById('lookup_splynx_id').value = '';
|
|
currentCustomerData = null;
|
|
}
|
|
|
|
// Close modals on escape key
|
|
document.addEventListener('keydown', function(event) {
|
|
if (event.key === 'Escape') {
|
|
const activeModals = document.querySelectorAll('.modal.is-active');
|
|
activeModals.forEach(modal => modal.classList.remove('is-active'));
|
|
}
|
|
});
|
|
|
|
// Enter key navigation
|
|
document.getElementById('lookup_splynx_id').addEventListener('keypress', function(event) {
|
|
if (event.key === 'Enter') {
|
|
fetchCustomerDetails();
|
|
}
|
|
});
|
|
|
|
document.getElementById('payment_amount').addEventListener('keypress', function(event) {
|
|
if (event.key === 'Enter') {
|
|
showConfirmationModal();
|
|
}
|
|
});
|
|
</script>
|
|
{% endblock %}
|