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.
 
 
 

687 lines
25 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="field">
<label class="label" for="payment_method_select">Payment Method</label>
<div class="control has-icons-left" id="payment_method_container">
<div class="select is-fullwidth is-loading" id="payment_method_loading">
<select disabled>
<option>Loading payment methods...</option>
</select>
</div>
<span class="icon is-small is-left">
<i class="fas fa-credit-card"></i>
</span>
</div>
<div id="payment_method_error" class="notification is-danger is-light is-hidden mt-2">
<span class="icon"><i class="fas fa-exclamation-triangle"></i></span>
<span>Unable to load payment methods. Customer may not have any valid payment methods.</span>
</div>
<p class="help">Select which payment method to use for this payment</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 selected 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-one-third">
<strong>Customer:</strong><br>
<span id="confirmCustomerName">-</span>
</div>
<div class="column is-one-third">
<strong>Amount:</strong><br>
<span id="confirmAmount" class="has-text-weight-bold is-size-4">$0.00</span>
</div>
<div class="column is-one-third">
<strong>Payment Method:</strong><br>
<span id="confirmPaymentMethod" class="tag is-info">-</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;
// Fetch payment methods for this customer
fetchPaymentMethods(customer.id);
}
function showError(message) {
document.getElementById('errorMessage').textContent = message;
document.getElementById('customerError').classList.remove('is-hidden');
}
function fetchPaymentMethods(splynxId) {
// Get the Stripe customer ID from MySQL first
fetch(`/api/stripe-customer-id/${splynxId}`)
.then(response => response.json())
.then(data => {
if (data.success && data.stripe_customer_id) {
// Now fetch payment methods for this Stripe customer
return fetch(`/api/stripe-payment-methods/${data.stripe_customer_id}`);
} else {
throw new Error('Customer does not have a Stripe customer ID');
}
})
.then(response => response.json())
.then(data => {
if (data.success && data.payment_methods) {
displayPaymentMethods(data.payment_methods);
} else {
showPaymentMethodError();
}
})
.catch(error => {
console.error('Error fetching payment methods:', error);
showPaymentMethodError();
});
}
function displayPaymentMethods(paymentMethods) {
const container = document.getElementById('payment_method_container');
if (!paymentMethods || paymentMethods.length === 0) {
showPaymentMethodError();
return;
}
// Create the select element
const selectHtml = `
<div class="select is-fullwidth">
<select id="payment_method_select" name="payment_method" required>
<option value="">Select a payment method</option>
${paymentMethods.map(pm => {
let displayText = '';
if (pm.type === 'card' && pm.card) {
const brand = pm.card.brand || 'Card';
const last4 = pm.card.last4 || '****';
displayText = `${brand.toUpperCase()} ending in ${last4}`;
} else if (pm.type === 'au_becs_debit' && pm.au_becs_debit) {
const last4 = pm.au_becs_debit.last4 || '****';
displayText = `AU Bank Account ending in ${last4}`;
} else {
displayText = pm.type.toUpperCase();
}
return `<option value="${pm.id}">${displayText}</option>`;
}).join('')}
</select>
</div>
`;
container.innerHTML = selectHtml + `
<span class="icon is-small is-left">
<i class="fas fa-credit-card"></i>
</span>
`;
// Hide any error messages
document.getElementById('payment_method_error').classList.add('is-hidden');
}
function showPaymentMethodError() {
const container = document.getElementById('payment_method_container');
// Show a disabled select with error message
container.innerHTML = `
<div class="select is-fullwidth">
<select disabled>
<option>No payment methods available</option>
</select>
</div>
<span class="icon is-small is-left">
<i class="fas fa-exclamation-triangle"></i>
</span>
`;
// Show error notification
document.getElementById('payment_method_error').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');
document.getElementById('payment_method_error').classList.add('is-hidden');
// Clear form
document.getElementById('payment_amount').value = '';
// Reset payment method selector to loading state
const container = document.getElementById('payment_method_container');
container.innerHTML = `
<div class="select is-fullwidth is-loading" id="payment_method_loading">
<select disabled>
<option>Loading payment methods...</option>
</select>
</div>
<span class="icon is-small is-left">
<i class="fas fa-credit-card"></i>
</span>
`;
}
function showConfirmationModal() {
const amount = document.getElementById('payment_amount').value;
const paymentMethodSelect = document.getElementById('payment_method_select');
if (!amount || parseFloat(amount) <= 0) {
alert('Please enter a valid payment amount');
return;
}
if (!paymentMethodSelect || !paymentMethodSelect.value) {
alert('Please select a payment method');
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)}`;
document.getElementById('confirmPaymentMethod').textContent = paymentMethodSelect.options[paymentMethodSelect.selectedIndex].text;
// 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 %}