Browse Source

New Features

master
Alan Woodman 4 months ago
parent
commit
aefc130f15
  1. 87
      blueprints/main.py
  2. 3
      stripe_payment_processor.py
  3. 11
      templates/main/single_payment.html
  4. 198
      templates/main/single_payment_detail.html
  5. 5
      templates/main/single_payments_list.html

87
blueprints/main.py

@ -277,11 +277,11 @@ def get_stripe_payment_methods(stripe_customer_id):
api_key = "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"
else:
api_key = "sk_test_51Rsi9gPfYyg6zE1S4ZpaPI1ehpbsHRLsGhysYXKwAWCZ7w6KYgVXy4pV095Nd8tyjUw9AkBhqfxqsIiiWJg5fexI00Dw36vnvx"
print(api_key)
processor = StripePaymentProcessor(api_key=api_key, enable_logging=False)
# Get payment methods from Stripe
stripe_customer_id = "cus_SoMyDihTxRsa7U"
#stripe_customer_id = "cus_SoMyDihTxRsa7U"
payment_methods = processor.get_payment_methods(stripe_customer_id)
return payment_methods
@ -446,8 +446,12 @@ def single_payment_detail(payment_id):
SinglePayments.Payment_Amount,
SinglePayments.PI_JSON,
SinglePayments.PI_FollowUp_JSON,
SinglePayments.Refund_JSON,
SinglePayments.Error,
SinglePayments.Success,
SinglePayments.Refund,
SinglePayments.Stripe_Refund_ID,
SinglePayments.Stripe_Refund_Created,
SinglePayments.Created,
Users.FullName.label('processed_by')
).outerjoin(Users, SinglePayments.Who == Users.id)\
@ -1050,6 +1054,85 @@ def check_batch_payment_intent(payment_id):
print(f"Check batch payment intent error: {e}")
return jsonify({'success': False, 'error': 'Failed to check payment intent'}), 500
@main_bp.route('/single-payment/refund/<int:payment_id>', methods=['POST'])
@login_required
def process_single_payment_refund(payment_id):
"""Process refund for a single payment."""
try:
# Get the payment record
payment = db.session.query(SinglePayments).filter(SinglePayments.id == payment_id).first()
if not payment:
return jsonify({'success': False, 'error': 'Payment not found'}), 404
# Check if payment can be refunded
if payment.Success != True:
return jsonify({'success': False, 'error': 'Cannot refund unsuccessful payment'}), 400
if payment.Refund == True:
return jsonify({'success': False, 'error': 'Payment has already been refunded'}), 400
if not payment.Payment_Intent:
return jsonify({'success': False, 'error': 'No payment intent found for this payment'}), 400
# Get refund reason from request
data = request.get_json()
reason = data.get('reason', 'requested_by_customer') if data else 'requested_by_customer'
# Initialize Stripe
if Config.PROCESS_LIVE:
api_key = "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"
else:
api_key = "sk_test_51Rsi9gPfYyg6zE1S4ZpaPI1ehpbsHRLsGhysYXKwAWCZ7w6KYgVXy4pV095Nd8tyjUw9AkBhqfxqsIiiWJg5fexI00Dw36vnvx"
import stripe
stripe.api_key = api_key
# Process refund through Stripe
refund = stripe.Refund.create(
payment_intent=payment.Payment_Intent,
reason=reason,
metadata={
'splynx_customer_id': str(payment.Splynx_ID),
'payment_id': str(payment_id),
'processed_by': current_user.FullName
}
)
# Update payment record
payment.Refund = True
payment.Refund_JSON = json.dumps(refund, default=str)
payment.Stripe_Refund_ID = refund.id
# Convert timestamp to datetime
if hasattr(refund, 'created') and refund.created:
from datetime import datetime
payment.Stripe_Refund_Created = datetime.fromtimestamp(refund.created)
db.session.commit()
# Log the refund activity
log_activity(
user_id=current_user.id,
action="process_refund",
entity_type="single_payment",
entity_id=payment_id,
details=f"Processed refund for single payment ID {payment_id}, amount ${payment.Payment_Amount}, reason: {reason}"
)
return jsonify({
'success': True,
'refund_id': refund.id,
'amount_refunded': f"${payment.Payment_Amount:.2f}",
'reason': reason
})
except stripe.StripeError as e:
return jsonify({'success': False, 'error': f'Stripe error: {str(e)}'}), 500
except Exception as e:
print(f"Error processing single payment refund: {e}")
return jsonify({'success': False, 'error': 'Internal server error'}), 500
@main_bp.route('/payment/refund/<int:payment_id>', methods=['POST'])
@login_required
def process_payment_refund(payment_id):

3
stripe_payment_processor.py

@ -553,7 +553,7 @@ class StripePaymentProcessor:
customer=customer_id,
limit=10
)
print(json.dumps(payment_methods,indent=2))
methods_list = []
for pm in payment_methods.data:
@ -580,6 +580,7 @@ class StripePaymentProcessor:
methods_list.append(pm_info)
self._log('info', f"Found {len(methods_list)} payment methods")
print(methods_list)
return methods_list
except stripe.StripeError as e:

11
templates/main/single_payment.html

@ -452,10 +452,13 @@ function displayPaymentMethods(paymentMethods) {
<option value="">Select a payment method</option>
${paymentMethods.map(pm => {
let displayText = '';
if (pm.type === 'card') {
displayText = `${pm.display_brand.toUpperCase()} ending in ${pm.last4}`;
} else if (pm.type === 'au_becs_debit') {
displayText = `AU Bank Account ending in ${pm.last4}`;
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();
}

198
templates/main/single_payment_detail.html

@ -31,7 +31,11 @@
<div class="level">
<div class="level-left">
<div class="level-item">
{% if payment.Success == True %}
{% if payment.Refund == True %}
<span class="icon is-large" style="color: #9370db;">
<i class="fas fa-undo fa-2x"></i>
</span>
{% elif payment.Success == True %}
<span class="icon is-large has-text-success">
<i class="fas fa-check-circle fa-2x"></i>
</span>
@ -47,7 +51,13 @@
</div>
<div class="level-item">
<div>
{% if payment.Success == True %}
{% if payment.Refund == True %}
<h2 class="title is-4 mb-2" style="color: #9370db;">Payment Refunded</h2>
<p class="has-text-grey">This payment has been refunded to the customer.</p>
{% if payment.Stripe_Refund_Created %}
<p class="has-text-grey is-size-7">Refunded: {{ payment.Stripe_Refund_Created.strftime('%Y-%m-%d %H:%M:%S') }}</p>
{% endif %}
{% elif payment.Success == True %}
<h2 class="title is-4 has-text-success mb-2">Payment Successful</h2>
<p class="has-text-grey">This payment has been completed successfully.</p>
{% elif payment.Success == False %}
@ -61,12 +71,24 @@
</div>
</div>
<div class="level-right">
{% if payment.PI_FollowUp %}
<button class="button is-warning" id="checkIntentBtn" onclick="checkPaymentIntent()">
<span class="icon"><i class="fas fa-sync-alt"></i></span>
<span>Force Check Status</span>
</button>
{% endif %}
<div class="field is-grouped">
{% if payment.Refund != True and payment.Success == True %}
<div class="control">
<button class="button" style="border-color: #9370db; color: #9370db;" id="refundBtn" onclick="showRefundModal()">
<span class="icon"><i class="fas fa-undo"></i></span>
<span>Process Refund</span>
</button>
</div>
{% endif %}
{% if payment.PI_FollowUp %}
<div class="control">
<button class="button is-warning" id="checkIntentBtn" onclick="checkPaymentIntent()">
<span class="icon"><i class="fas fa-sync-alt"></i></span>
<span>Force Check Status</span>
</button>
</div>
{% endif %}
</div>
</div>
</div>
</div>
@ -165,21 +187,58 @@
{% endif %}
</div>
{% endif %}
{% if payment.Refund == True %}
<div class="notification is-light" style="background-color: #f8f4ff; border-color: #9370db;">
<span class="icon" style="color: #9370db;"><i class="fas fa-undo"></i></span>
<strong style="color: #9370db;">Refund Processed:</strong> This payment has been refunded.
{% if payment.Stripe_Refund_Created %}
<br><small>Refunded: {{ payment.Stripe_Refund_Created.strftime('%Y-%m-%d %H:%M:%S') }}</small>
{% endif %}
{% if payment.Stripe_Refund_ID %}
<br><small>Refund ID: <code>{{ payment.Stripe_Refund_ID }}</code></small>
{% endif %}
</div>
{% endif %}
</div>
</div>
</div>
<!-- Error Information -->
{% if payment.Error %}
{% set error_alert = payment | error_alert %}
<div class="box">
<h3 class="title is-5 has-text-danger">
<span class="icon"><i class="fas fa-exclamation-triangle"></i></span>
Error Information
Payment Error Details
</h3>
{% if error_alert %}
<div class="error-alert {{ error_alert.type }}">
<div class="error-alert-header">
<span class="icon"><i class="fas {{ error_alert.icon }}"></i></span>
<span class="error-title">{{ error_alert.title }}</span>
</div>
<div class="error-alert-body">
<p class="error-message">{{ error_alert.message }}</p>
<p class="error-suggestion"><strong>Suggested Action:</strong> {{ error_alert.suggestion }}</p>
<details class="error-details">
<summary>View Technical Details</summary>
<pre>{{ error_alert.raw_error }}</pre>
</details>
</div>
</div>
{% else %}
<!-- Fallback for unclassified errors -->
<div class="notification is-danger is-light">
<pre>{{ payment.Error }}</pre>
<h5 class="title is-6">Payment Error</h5>
<p>An error occurred during payment processing.</p>
<details class="mt-3">
<summary class="has-text-grey">Technical Details</summary>
<pre class="mt-2">{{ payment.Error }}</pre>
</details>
</div>
{% endif %}
</div>
{% endif %}
@ -230,6 +289,76 @@
</div>
</div>
{% endif %}
{% if payment.Refund_JSON %}
<div class="column">
<div class="box">
<h3 class="title is-5">
<span class="icon" style="color: #9370db;"><i class="fas fa-undo"></i></span>
Refund JSON
</h3>
<div class="field is-grouped">
<div class="control">
<button class="button is-small" style="border-color: #9370db; color: #9370db;" onclick="copyFormattedJSON('refund-json-content')">
<span class="icon"><i class="fas fa-copy"></i></span>
<span>Copy JSON</span>
</button>
</div>
</div>
<pre class="is-size-7"><code>{{ payment.Refund_JSON | format_json }}</code></pre>
<div id="refund-json-content" style="display: none;">{{ payment.Refund_JSON | format_json }}</div>
</div>
</div>
{% endif %}
</div>
<!-- Refund Modal -->
<div class="modal" id="refundModal">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head" style="background-color: #9370db;">
<p class="modal-card-title has-text-white">
<span class="icon"><i class="fas fa-undo"></i></span>
Process Refund
</p>
<button class="delete" aria-label="close" onclick="hideModal('refundModal')"></button>
</header>
<section class="modal-card-body">
<div class="has-text-centered py-4">
<span class="icon is-large mb-4" style="color: #9370db;">
<i class="fas fa-undo fa-3x"></i>
</span>
<div class="content">
<h4 class="title is-5">Confirm Refund</h4>
<p>Are you sure you want to process a refund for this payment?</p>
<p><strong>Payment Amount:</strong> {{ payment.Payment_Amount | currency }}</p>
<p><strong>Customer:</strong> {{ payment.Splynx_ID }}</p>
<div class="field">
<label class="label">Refund Reason</label>
<div class="control">
<div class="select is-fullwidth">
<select id="refundReason">
<option value="requested_by_customer">Requested by Customer</option>
<option value="duplicate">Duplicate Payment</option>
<option value="fraudulent">Fraudulent</option>
</select>
</div>
</div>
</div>
</div>
</div>
</section>
<footer class="modal-card-foot is-justify-content-center">
<button class="button" style="border-color: #9370db; color: #9370db;" onclick="processRefund()" id="processRefundBtn">
<span class="icon"><i class="fas fa-undo"></i></span>
<span>Process Refund</span>
</button>
<button class="button" onclick="hideModal('refundModal')">Cancel</button>
</footer>
</div>
</div>
<!-- Success Modal -->
@ -292,6 +421,55 @@
</div>
<script>
function showRefundModal() {
document.getElementById('refundModal').classList.add('is-active');
}
function processRefund() {
const btn = document.getElementById('processRefundBtn');
const originalText = btn.innerHTML;
const reason = document.getElementById('refundReason').value;
// Disable button and show loading
btn.disabled = true;
btn.innerHTML = '<span class="icon"><i class="fas fa-spinner fa-spin"></i></span><span>Processing...</span>';
// Make API call to process refund
fetch(`/single-payment/refund/{{ payment.id }}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
reason: reason
})
})
.then(response => response.json())
.then(data => {
hideModal('refundModal');
if (data.success) {
showSuccessModal(`
<h4 class="title is-5">Refund Processed Successfully!</h4>
<p>The refund has been processed and sent to Stripe.</p>
<p><strong>Refund ID:</strong> ${data.refund_id}</p>
<p><strong>Amount:</strong> ${data.amount_refunded}</p>
`);
} else {
showErrorModal(data.error || 'Failed to process refund');
}
})
.catch(error => {
hideModal('refundModal');
console.error('Error processing refund:', error);
showErrorModal('Failed to process refund. Please try again.');
})
.finally(() => {
// Re-enable button
btn.disabled = false;
btn.innerHTML = originalText;
});
}
function checkPaymentIntent() {
const btn = document.getElementById('checkIntentBtn');
const originalText = btn.innerHTML;

5
templates/main/single_payments_list.html

@ -128,7 +128,10 @@
<tr class="{{ row_class }}">
<td>
<strong>#{{ payment.id }}</strong>
<a href="{{ url_for('main.single_payment_detail', payment_id=payment.id) }}"
class="has-text-weight-semibold has-text-primary">
#{{ payment.id }}
</a>
</td>
<td>
<span class="is-size-7">{{ payment.Created.strftime('%Y-%m-%d') }}</span><br>

Loading…
Cancel
Save