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.
 
 
 

748 lines
27 KiB

{% extends "base.html" %}
{% block title %}Add Payment Method - Plutus{% endblock %}
{% block content %}
<nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li><a href="{{ url_for('main.index') }}">Dashboard</a></li>
<li><a href="{{ url_for('main.single_payments_list') }}">Single Payments</a></li>
<li class="is-active"><a href="#" aria-current="page">Add Payment Method</a></li>
</ul>
</nav>
<div class="level">
<div class="level-left">
<div>
<h1 class="title">Add Payment Method</h1>
<p class="subtitle">Add credit cards or BECS Direct Debit to customer accounts</p>
</div>
</div>
</div>
<!-- Add Payment Method 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 & Select Payment Method Type -->
<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 & Select Payment Method Type
</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>
<!-- Current Payment Methods -->
<div class="box has-background-info-light mb-5">
<h3 class="subtitle is-5">
<span class="icon"><i class="fas fa-credit-card"></i></span>
Current Payment Methods
</h3>
<div id="currentPaymentMethods">
<div class="has-text-centered py-4">
<div class="spinner"></div>
<p class="mt-3">Loading current payment methods...</p>
</div>
</div>
</div>
<div class="field">
<label class="label">Payment Method Type</label>
<div class="control">
<div class="columns">
<div class="column">
<div class="box payment-type-card" onclick="selectPaymentType('card')" id="cardOption">
<div class="has-text-centered">
<span class="icon is-large has-text-info">
<i class="fas fa-credit-card fa-2x"></i>
</span>
<h4 class="title is-5 mt-3">Credit/Debit Card</h4>
<p class="content">Add a new credit or debit card</p>
<ul class="content is-small">
<li>Domestic cards: 1.7% + $0.30</li>
<li>International cards: 3.5% + $0.30</li>
</ul>
</div>
</div>
</div>
<div class="column">
<div class="box payment-type-card" onclick="selectPaymentType('au_becs_debit')" id="becsOption">
<div class="has-text-centered">
<span class="icon is-large has-text-success">
<i class="fas fa-university fa-2x"></i>
</span>
<h4 class="title is-5 mt-3">BECS Direct Debit</h4>
<p class="content">Add Australian bank account</p>
<ul class="content is-small">
<li>1.0% + $0.30 (capped at $3.50)</li>
<li>Lower fees than cards</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="field is-grouped">
<div class="control">
<button class="button is-light" 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-primary" id="continueBtn" onclick="setupPaymentMethod()" disabled>
<span class="icon"><i class="fas fa-arrow-right"></i></span>
<span>Continue</span>
</button>
</div>
</div>
</div>
<!-- Step 3: Payment Method Collection -->
<div id="step3" class="payment-step is-hidden">
<h2 class="title is-4">
<span class="icon"><i class="fas fa-plus-circle"></i></span>
Add Payment Method
</h2>
<div class="box has-background-light mb-5">
<h3 class="subtitle is-5">Selected Type</h3>
<div id="selectedTypeDisplay">
<!-- Selected payment method type will be shown here -->
</div>
</div>
<!-- Stripe Elements Container -->
<div class="field">
<label class="label" id="paymentElementLabel">Payment Details</label>
<div class="control">
<div id="payment-element" class="stripe-element">
<!-- Stripe Elements will be mounted here -->
</div>
</div>
<div id="payment-element-errors" role="alert" class="help is-danger is-hidden"></div>
</div>
<!-- BECS Mandate Agreement (only shown for BECS) -->
<div id="becsMandate" class="notification is-info is-hidden">
<div class="content">
<h4>Direct Debit Request Service Agreement</h4>
<p>By providing your bank account details and confirming this payment, you acknowledge that:</p>
<ul>
<li>You have read and agree to the Direct Debit Request Service Agreement</li>
<li>You authorize debits to your account according to the arrangement outlined</li>
<li>This authorization will remain in effect until cancelled by you</li>
</ul>
<label class="checkbox">
<input type="checkbox" id="becsAgreement">
I agree to the Direct Debit Request Service Agreement
</label>
</div>
</div>
<!-- Set as Default Option -->
<div class="field">
<div class="control">
<label class="checkbox">
<input type="checkbox" id="setAsDefault">
Set as default payment method for this customer
</label>
</div>
</div>
<div class="field is-grouped">
<div class="control">
<button class="button is-light" onclick="goBackToStep2()">
<span class="icon"><i class="fas fa-arrow-left"></i></span>
<span>Back</span>
</button>
</div>
<div class="control">
<button class="button is-primary" id="savePaymentMethodBtn" onclick="savePaymentMethod()">
<span class="icon"><i class="fas fa-save"></i></span>
<span>Save Payment Method</span>
</button>
</div>
</div>
<!-- Processing State -->
<div id="processingPayment" class="notification is-info is-hidden">
<div class="has-text-centered py-4">
<div class="spinner"></div>
<p class="mt-3">Processing payment method...</p>
</div>
</div>
</div>
<!-- Step 4: Success -->
<div id="step4" class="payment-step is-hidden">
<h2 class="title is-4 has-text-success">
<span class="icon"><i class="fas fa-check-circle"></i></span>
Payment Method Added Successfully
</h2>
<div class="box has-background-success-light">
<div id="successDetails">
<!-- Success details will be populated here -->
</div>
</div>
<div class="field is-grouped">
<div class="control">
<button class="button is-success" onclick="addAnotherPaymentMethod()">
<span class="icon"><i class="fas fa-plus"></i></span>
<span>Add Another Payment Method</span>
</button>
</div>
<div class="control">
<a class="button is-info" href="{{ url_for('main.single_payments_list') }}">
<span class="icon"><i class="fas fa-list"></i></span>
<span>View Payments</span>
</a>
</div>
</div>
</div>
</div>
<!-- Include Stripe.js -->
<script src="https://js.stripe.com/v3/"></script>
<script>
// Global variables
let currentCustomer = null;
let selectedPaymentType = null;
let stripe = null;
let elements = null;
let paymentElement = null;
let setupIntentClientSecret = null;
// Initialize Stripe (we'll get the publishable key from the backend)
document.addEventListener('DOMContentLoaded', function() {
// We'll initialize Stripe when we need it in step 3
});
function fetchCustomerDetails() {
const splynxId = document.getElementById('lookup_splynx_id').value;
if (!splynxId) {
showError('Please enter a Splynx ID');
return;
}
showLoading(true);
hideError();
// Use existing API endpoint to get Stripe customer ID
fetch(`/api/stripe-customer-id/${splynxId}`)
.then(response => response.json())
.then(data => {
if (data.success) {
// Get Splynx customer details
return fetch(`/api/splynx/${splynxId}`)
.then(response => response.json())
.then(splynxData => {
// Combine the data
const combinedData = {
success: true,
splynx_id: splynxId,
stripe_customer_id: data.stripe_customer_id,
customer_name: `${splynxData.name || ''} ${splynxData.lastname || ''}`.trim(),
customer_email: splynxData.email || '',
splynx_data: splynxData
};
currentCustomer = combinedData;
displayCustomerDetails(combinedData);
loadCurrentPaymentMethods(data.stripe_customer_id);
showStep(2);
showLoading(false);
});
} else {
showLoading(false);
showError(data.error || 'Customer not found or no Stripe customer ID available');
}
})
.catch(error => {
showLoading(false);
showError('Error fetching customer details: ' + error.message);
});
}
function displayCustomerDetails(customer) {
const html = `
<div class="columns">
<div class="column">
<strong>Name:</strong> ${customer.customer_name || 'N/A'}<br>
<strong>Email:</strong> ${customer.customer_email || 'N/A'}<br>
<strong>Splynx ID:</strong> ${customer.splynx_id}
</div>
<div class="column">
<strong>Stripe Customer ID:</strong> ${customer.stripe_customer_id}<br>
<strong>Status:</strong> <span class="tag is-success">Active</span>
</div>
</div>
`;
document.getElementById('customerDetails').innerHTML = html;
}
function loadCurrentPaymentMethods(stripeCustomerId) {
fetch(`/api/stripe-payment-methods/${stripeCustomerId}`)
.then(response => response.json())
.then(data => {
if (data.success) {
displayCurrentPaymentMethods(data.payment_methods);
} else {
document.getElementById('currentPaymentMethods').innerHTML =
'<p class="has-text-danger">Error loading payment methods: ' + (data.error || 'Unknown error') + '</p>';
}
})
.catch(error => {
document.getElementById('currentPaymentMethods').innerHTML =
'<p class="has-text-danger">Error loading payment methods: ' + error.message + '</p>';
});
}
function displayCurrentPaymentMethods(methods) {
const container = document.getElementById('currentPaymentMethods');
if (!methods || methods.length === 0) {
container.innerHTML = '<p class="has-text-grey">No payment methods found for this customer.</p>';
return;
}
let html = '<div class="columns is-multiline">';
methods.forEach(method => {
let methodInfo = '';
let icon = '';
if (method.type === 'card' && method.card) {
icon = 'fas fa-credit-card';
methodInfo = `${method.card.brand.toUpperCase()} ending in ${method.card.last4}`;
} else if (method.type === 'au_becs_debit' && method.au_becs_debit) {
icon = 'fas fa-university';
methodInfo = `Bank account ${method.au_becs_debit.bsb_number} ending in ${method.au_becs_debit.last4}`;
} else {
icon = 'fas fa-question-circle';
methodInfo = `${method.type} payment method`;
}
html += `
<div class="column is-half">
<div class="box is-small">
<div class="media">
<div class="media-left">
<span class="icon has-text-info">
<i class="${icon}"></i>
</span>
</div>
<div class="media-content">
<p class="is-size-6">${methodInfo}</p>
<p class="is-size-7 has-text-grey">Added: ${new Date(method.created * 1000).toLocaleDateString()}</p>
</div>
</div>
</div>
</div>
`;
});
html += '</div>';
container.innerHTML = html;
}
function selectPaymentType(type) {
selectedPaymentType = type;
// Update UI
document.querySelectorAll('.payment-type-card').forEach(card => {
card.classList.remove('has-background-primary-light', 'has-border-primary');
});
const selectedCard = document.getElementById(type === 'card' ? 'cardOption' : 'becsOption');
selectedCard.classList.add('has-background-primary-light', 'has-border-primary');
document.getElementById('continueBtn').disabled = false;
}
function setupPaymentMethod() {
if (!selectedPaymentType) {
showError('Please select a payment method type');
return;
}
// Update selected type display
const typeDisplay = selectedPaymentType === 'card' ?
'<span class="icon"><i class="fas fa-credit-card"></i></span> Credit/Debit Card' :
'<span class="icon"><i class="fas fa-university"></i></span> BECS Direct Debit';
document.getElementById('selectedTypeDisplay').innerHTML = typeDisplay;
// Show/hide BECS mandate
if (selectedPaymentType === 'au_becs_debit') {
document.getElementById('becsMandate').classList.remove('is-hidden');
document.getElementById('paymentElementLabel').textContent = 'Bank Account Details';
} else {
document.getElementById('becsMandate').classList.add('is-hidden');
document.getElementById('paymentElementLabel').textContent = 'Card Details';
}
showStep(3);
initializeStripeElements();
}
function initializeStripeElements() {
console.log('Initializing Stripe elements for payment type:', selectedPaymentType);
console.log('Customer:', currentCustomer);
// Get Stripe publishable key and create setup intent
fetch('/api/create-setup-intent', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
stripe_customer_id: currentCustomer.stripe_customer_id,
payment_method_types: [selectedPaymentType]
})
})
.then(response => {
console.log('Setup intent response status:', response.status);
return response.json();
})
.then(data => {
console.log('Setup intent response data:', data);
if (data.success) {
setupIntentClientSecret = data.client_secret;
console.log('Client secret received:', data.client_secret.substring(0, 20) + '...');
initializeStripe(data.stripe_publishable_key, data.client_secret);
} else {
console.error('Setup intent failed:', data.error);
showError('Failed to initialize payment method setup: ' + data.error);
}
})
.catch(error => {
console.error('Setup intent error:', error);
showError('Error initializing setup: ' + error.message);
});
}
function initializeStripe(publishableKey, clientSecret) {
console.log('Initializing Stripe with publishable key:', publishableKey.substring(0, 12) + '...');
console.log('Selected payment type:', selectedPaymentType);
stripe = Stripe(publishableKey);
// Create elements with specific appearance and payment method options
const appearance = {
theme: 'stripe',
variables: {
colorPrimary: '#3273dc',
colorBackground: '#ffffff',
colorText: '#30313d',
colorDanger: '#df1b41',
fontFamily: 'Ideal Sans, system-ui, sans-serif',
spacingUnit: '2px',
borderRadius: '4px'
}
};
elements = stripe.elements({
clientSecret: clientSecret,
appearance: appearance
});
// Configure payment element with specific payment method types
const paymentElementOptions = {
layout: 'tabs',
paymentMethodOrder: selectedPaymentType === 'card'
? ['card']
: ['au_becs_debit'],
fields: {
billingDetails: {
name: 'auto',
email: 'auto'
}
}
};
console.log('Creating payment element with options:', paymentElementOptions);
paymentElement = elements.create('payment', paymentElementOptions);
console.log('Mounting payment element to #payment-element');
const mountElement = document.getElementById('payment-element');
if (mountElement) {
console.log('Mount element found:', mountElement);
try {
paymentElement.mount('#payment-element');
console.log('Payment element mounted successfully');
} catch (error) {
console.error('Error mounting payment element:', error);
showError('Error mounting payment form: ' + error.message);
}
} else {
console.error('Mount element #payment-element not found');
showError('Payment form container not found');
}
paymentElement.on('change', function(event) {
console.log('Payment element change event:', event);
const errorElement = document.getElementById('payment-element-errors');
if (event.error) {
errorElement.textContent = event.error.message;
errorElement.classList.remove('is-hidden');
} else {
errorElement.textContent = '';
errorElement.classList.add('is-hidden');
}
});
paymentElement.on('ready', function() {
console.log('Payment element is ready and visible');
});
paymentElement.on('loaderror', function(event) {
console.error('Payment element load error:', event);
showError('Error loading payment form: ' + event.error.message);
});
}
function savePaymentMethod() {
// Validate BECS agreement if needed
if (selectedPaymentType === 'au_becs_debit') {
const becsAgreement = document.getElementById('becsAgreement');
if (!becsAgreement.checked) {
showError('Please agree to the Direct Debit Request Service Agreement');
return;
}
}
const saveBtn = document.getElementById('savePaymentMethodBtn');
saveBtn.disabled = true;
saveBtn.innerHTML = '<span class="icon"><i class="fas fa-spinner fa-spin"></i></span><span>Processing...</span>';
document.getElementById('processingPayment').classList.remove('is-hidden');
// Confirm the setup intent
stripe.confirmSetup({
elements,
confirmParams: {
return_url: window.location.origin + '/single-payments/add-payment-method/success',
},
redirect: 'if_required'
}).then(function(result) {
if (result.error) {
// Handle error
showError('Payment method setup failed: ' + result.error.message);
saveBtn.disabled = false;
saveBtn.innerHTML = '<span class="icon"><i class="fas fa-save"></i></span><span>Save Payment Method</span>';
document.getElementById('processingPayment').classList.add('is-hidden');
} else {
// Setup succeeded
handleSetupSuccess(result.setupIntent);
}
});
}
function handleSetupSuccess(setupIntent) {
const setAsDefault = document.getElementById('setAsDefault').checked;
// Attach payment method and optionally set as default
fetch('/api/finalize-payment-method', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
setup_intent_id: setupIntent.id,
stripe_customer_id: currentCustomer.stripe_customer_id,
set_as_default: setAsDefault,
splynx_id: currentCustomer.splynx_id
})
})
.then(response => response.json())
.then(data => {
document.getElementById('processingPayment').classList.add('is-hidden');
if (data.success) {
displaySuccess(data);
showStep(4);
} else {
showError('Failed to finalize payment method: ' + data.error);
const saveBtn = document.getElementById('savePaymentMethodBtn');
saveBtn.disabled = false;
saveBtn.innerHTML = '<span class="icon"><i class="fas fa-save"></i></span><span>Save Payment Method</span>';
}
})
.catch(error => {
document.getElementById('processingPayment').classList.add('is-hidden');
showError('Error finalizing setup: ' + error.message);
const saveBtn = document.getElementById('savePaymentMethodBtn');
saveBtn.disabled = false;
saveBtn.innerHTML = '<span class="icon"><i class="fas fa-save"></i></span><span>Save Payment Method</span>';
});
}
function displaySuccess(data) {
const paymentMethod = data.payment_method;
let methodInfo = '';
if (paymentMethod.type === 'card' && paymentMethod.card) {
methodInfo = `${paymentMethod.card.brand.toUpperCase()} ending in ${paymentMethod.card.last4}`;
} else if (paymentMethod.type === 'au_becs_debit' && paymentMethod.au_becs_debit) {
methodInfo = `Bank account ${paymentMethod.au_becs_debit.bsb_number} ending in ${paymentMethod.au_becs_debit.last4}`;
}
const html = `
<div class="content">
<h4>Payment Method Added</h4>
<p><strong>Type:</strong> ${methodInfo}</p>
<p><strong>Customer:</strong> ${currentCustomer.customer_name} (${currentCustomer.customer_email})</p>
<p><strong>Set as Default:</strong> ${data.is_default ? 'Yes' : 'No'}</p>
<p><strong>Payment Method ID:</strong> <code>${paymentMethod.id}</code></p>
</div>
`;
document.getElementById('successDetails').innerHTML = html;
}
// Utility functions
function showStep(step) {
document.querySelectorAll('.payment-step').forEach(div => div.classList.add('is-hidden'));
document.getElementById('step' + step).classList.remove('is-hidden');
}
function goBackToStep1() {
showStep(1);
selectedPaymentType = null;
}
function goBackToStep2() {
showStep(2);
}
function addAnotherPaymentMethod() {
// Reset form
document.getElementById('lookup_splynx_id').value = '';
currentCustomer = null;
selectedPaymentType = null;
setupIntentClientSecret = null;
showStep(1);
}
function showLoading(show) {
const loading = document.getElementById('loading');
const nextBtn = document.getElementById('nextBtn');
if (show) {
loading.classList.remove('is-hidden');
nextBtn.disabled = true;
} else {
loading.classList.add('is-hidden');
nextBtn.disabled = false;
}
}
function showError(message) {
document.getElementById('errorMessage').textContent = message;
document.getElementById('customerError').classList.remove('is-hidden');
}
function hideError() {
document.getElementById('customerError').classList.add('is-hidden');
}
// Allow Enter key to trigger next button in step 1
document.getElementById('lookup_splynx_id').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
fetchCustomerDetails();
}
});
</script>
<style>
.payment-type-card {
cursor: pointer;
border: 2px solid transparent;
transition: all 0.3s ease;
}
.payment-type-card:hover {
border-color: #dbdbdb;
}
.has-border-primary {
border-color: #3273dc !important;
}
#payment-element {
border: 1px solid #dbdbdb;
border-radius: 4px;
padding: 12px;
background-color: white;
min-height: 60px;
}
.stripe-element {
border: 1px solid #dbdbdb;
border-radius: 4px;
padding: 12px;
background-color: white;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #3273dc;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
{% endblock %}