import json
import pymysql
from flask import Blueprint, render_template, request, jsonify, flash, redirect, url_for, current_app
from flask_login import login_required, current_user
from sqlalchemy import func, case
from datetime import datetime
from app import db
from typing import Dict, Any, List
from models import PaymentBatch, Payments, SinglePayments, PaymentPlans, Logs, Users
from splynx import Splynx, SPLYNX_URL, SPLYNX_KEY, SPLYNX_SECRET
from stripe_payment_processor import StripePaymentProcessor
from config import Config
from services import log_activity
from permissions import admin_required, finance_required, helpdesk_required
from notification_service import NotificationService
import re
import time
splynx = Splynx(url=SPLYNX_URL, key=SPLYNX_KEY, secret=SPLYNX_SECRET)
def create_customer_friendly_message(payment_data: dict, error_details: str) -> str:
"""
Create a customer-friendly ticket message for failed payments.
Args:
payment_data: Dictionary containing payment information
error_details: Raw error details
Returns:
str: HTML formatted customer-friendly message
"""
try:
# Extract payment details
amount = abs(payment_data.get('amount', 0))
splynx_id = payment_data.get('splynx_id', 'Unknown')
# Parse PI_JSON for payment method details if available
pi_json = payment_data.get('pi_json')
payment_method_type = "unknown"
last4 = "****"
if pi_json:
try:
import json
parsed_json = json.loads(pi_json)
payment_method_type = parsed_json.get('payment_method_type', 'unknown')
# Get last 4 digits from various possible locations in JSON
if 'payment_method_details' in parsed_json:
pm_details = parsed_json['payment_method_details']
if payment_method_type == 'card' and 'card' in pm_details:
last4 = pm_details['card'].get('last4', '****')
elif payment_method_type == 'au_becs_debit' and 'au_becs_debit' in pm_details:
last4 = pm_details['au_becs_debit'].get('last4', '****')
elif 'last4' in parsed_json:
last4 = parsed_json.get('last4', '****')
except:
pass
# Format payment method for display
if payment_method_type == 'au_becs_debit':
payment_method_display = f"Bank Account ending in {last4}"
elif payment_method_type == 'card':
payment_method_display = f"Card ending in {last4}"
else:
payment_method_display = "Payment method"
# Get current datetime
from datetime import datetime
current_time = datetime.now().strftime("%d/%m/%Y at %I:%M %p")
# Get customer-friendly error explanation
error_classification = classify_payment_error(error_details, pi_json)
if error_classification:
error_message = error_classification['message']
else:
error_message = "An error occurred during payment processing"
# Create customer-friendly HTML message
customer_message = f"""
Your payment attempt was unsuccessful.
Payment Details:
• Amount: ${amount:.2f} AUD
• Date/Time: {current_time}
• {payment_method_display}
Issue: {error_message}
Please contact us if you need assistance with your payment.
"""
return customer_message.strip()
except Exception as e:
# Fallback message if there's any error creating the friendly message
return f"""
Your payment attempt was unsuccessful. Please contact us for assistance.
"""
def classify_payment_error(error_text, json_data=None):
"""
Classify payment errors into user-friendly categories.
Args:
error_text (str): The error text from the Error field
json_data (str): Optional JSON data containing additional error details
Returns:
dict: Error classification with type, title, message, suggestion, and icon
"""
if not error_text:
return None
# Parse JSON data if provided
parsed_json = None
if json_data:
try:
parsed_json = json.loads(json_data)
except:
pass
# Extract decline code from JSON if available
decline_code = None
if parsed_json:
decline_code = parsed_json.get('decline_code')
if not decline_code and 'error' in parsed_json:
error_obj = parsed_json['error']
if isinstance(error_obj, dict):
decline_code = error_obj.get('decline_code')
# Convert to lowercase for easier matching
error_lower = error_text.lower()
# Insufficient Funds
if (decline_code in ['insufficient_funds', 'card_declined'] and 'insufficient' in error_lower) or \
'insufficient funds' in error_lower or 'insufficient_funds' in error_lower:
return {
'type': 'insufficient-funds',
'title': 'Insufficient Funds',
'message': 'Customer does not have sufficient funds in their account',
'suggestion': 'Customer should check their account balance or try a different payment method',
'icon': 'fa-credit-card'
}
# Incorrect Card Information
if decline_code in ['incorrect_number', 'incorrect_cvc', 'incorrect_zip', 'expired_card', 'invalid_expiry_month', 'invalid_expiry_year'] or \
any(phrase in error_lower for phrase in ['incorrect', 'invalid', 'expired', 'wrong', 'bad']):
return {
'type': 'incorrect-card',
'title': 'Incorrect Card Information',
'message': 'Card information is incorrect, invalid, or expired',
'suggestion': 'Customer should verify their card details or use a different card',
'icon': 'fa-exclamation-triangle'
}
# Bank Account Closed
if decline_code in ['call_issuer', 'pickup_card', 'restricted_card', 'security_violation'] or \
any(phrase in error_lower for phrase in ['closed']):
return {
'type': 'bank-contact',
'title': 'Bank Account Closed',
'message': 'The customer bank account has been closed.',
'suggestion': 'Customer should call the phone number on the back of their card',
'icon': 'fa-phone'
}
# Bank Account Not Found
if decline_code in ['call_issuer', 'pickup_card', 'restricted_card', 'security_violation'] or \
any(phrase in error_lower for phrase in ['located']):
return {
'type': 'bank-contact',
'title': 'Account Not Located',
'message': 'The customer bank account could not be located.',
'suggestion': 'Customer should call the phone number on the back of their card',
'icon': 'fa-phone'
}
# Bank Contact Required
if decline_code in ['call_issuer', 'pickup_card', 'restricted_card', 'security_violation'] or \
any(phrase in error_lower for phrase in ['call', 'contact', 'bank', 'issuer', 'restricted', 'blocked']):
return {
'type': 'bank-contact',
'title': 'Bank Contact Required',
'message': 'Customer needs to contact their bank or card issuer',
'suggestion': 'Customer should call the phone number on the back of their card',
'icon': 'fa-phone'
}
# Processing Errors
if decline_code in ['processing_error', 'try_again_later'] or \
'error_type' in error_lower and any(phrase in error_lower for phrase in ['processing', 'temporary', 'try again', 'timeout']):
return {
'type': 'processing-error',
'title': 'Processing Error',
'message': 'Temporary payment processing issue occurred',
'suggestion': 'Please try the payment again in a few minutes',
'icon': 'fa-sync-alt'
}
# Network Errors
if 'network' in error_lower or 'connection' in error_lower or 'timeout' in error_lower:
return {
'type': 'network-error',
'title': 'Network Error',
'message': 'Network connection issue during payment processing',
'suggestion': 'Please check your connection and try again',
'icon': 'fa-wifi'
}
# General Decline (catch-all for other card declines)
if decline_code in ['generic_decline', 'do_not_honor', 'card_not_supported', 'currency_not_supported'] or \
any(phrase in error_lower for phrase in ['declined', 'decline', 'not supported', 'do not honor']):
return {
'type': 'general-decline',
'title': 'Payment Declined',
'message': 'Payment was declined by the card issuer',
'suggestion': 'Customer should try a different payment method or contact their bank',
'icon': 'fa-ban'
}
# Default for unclassified errors
return {
'type': 'general-decline',
'title': 'Payment Error',
'message': 'An error occurred during payment processing',
'suggestion': 'Please try again or contact support if the issue persists',
'icon': 'fa-exclamation-circle'
}
def get_error_alert_data(payment):
"""
Get error alert data for template rendering.
Args:
payment: Payment object with Error and PI_JSON fields
Returns:
dict: Error alert data or None if no error
"""
if not payment.Error:
return None
# Use PI_JSON if available, otherwise try PI_FollowUp_JSON
json_data = payment.PI_JSON or payment.PI_FollowUp_JSON
error_classification = classify_payment_error(payment.Error, json_data)
if error_classification:
error_classification['raw_error'] = payment.Error
return error_classification
def processPaymentResult(pay_id, result, key):
"""Process payment result and update database record."""
from datetime import datetime
if key == "pay":
payment = db.session.query(Payments).filter(Payments.id == pay_id).first()
elif key == "singlepay":
payment = db.session.query(SinglePayments).filter(SinglePayments.id == pay_id).first()
try:
if result.get('error') and not result.get('needs_fee_update'):
payment.Error = f"Error Type: {result['error_type']}\nError: {result['error']}"
payment.Success = result['success']
payment.PI_JSON = json.dumps(result)
else:
if result.get('needs_fee_update'):
payment.PI_FollowUp = True
payment.Payment_Intent = result['payment_intent_id']
payment.Success = result['success']
if result['success'] and Config.PROCESS_LIVE and key == "singlepay":
# Only update Splynx for successful single payments in live mode
find_pay_splynx_invoices(payment.Splynx_ID)
add_payment_splynx(
splynx_id=payment.Splynx_ID,
pi_id=result['payment_intent_id'],
pay_id=payment.id,
amount=payment.Payment_Amount
)
if result.get('payment_method_type') == "card":
payment.Payment_Method = result['estimated_fee_details']['card_display_brand']
elif result.get('payment_method_type') == "au_becs_debit":
payment.Payment_Method = result['payment_method_type']
if payment.PI_JSON:
combined = {**json.loads(payment.PI_JSON), **result}
payment.PI_JSON = json.dumps(combined)
else:
payment.PI_JSON = json.dumps(result)
if result.get('fee_details'):
payment.Fee_Total = result['fee_details']['total_fee']
for fee_type in result['fee_details']['fee_breakdown']:
if fee_type['type'] == "tax":
payment.Fee_Tax = fee_type['amount']
elif fee_type['type'] == "stripe_fee":
payment.Fee_Stripe = fee_type['amount']
except Exception as e:
print(f"processPaymentResult error: {e}\n{json.dumps(result)}")
payment.PI_FollowUp = True
def find_pay_splynx_invoices(splynx_id: int, splynx_pay_id: int, invoice_ids: List[int]) -> List[int]:
"""Mark Splynx invoices as paid for the given customer ID."""
#result = splynx.get(url=f"/api/2.0/admin/finance/invoices?main_attributes[customer_id]={splynx_id}&main_attributes[status]=not_paid")
print(f"\n\nInvoice IDs to Pay: {invoice_ids} of type {type(invoice_ids)}\n")
#params = {
# 'main_attributes': {
# 'customer_id': splynx_id,
# 'status': ['IN', ['not_paid', 'pending']]
# },
#}
#query_string = splynx.build_splynx_query_params(params)
#result = splynx.get(url=f"/api/2.0/admin/finance/invoices?{query_string}")
invoice_pay = {
"status": "paid",
"payment_id": splynx_pay_id,
"date_payment": datetime.now().strftime("%Y-%m-%d")
}
#for pay in result:
for invoice in invoice_ids:
res = splynx.put(url=f"/api/2.0/admin/finance/invoices/{invoice}", params=invoice_pay)
return res
def find_set_pending_splynx_invoices(splynx_id: int, invoice_list: List):
"""Mark Splynx invoices as pending for the given customer ID."""
#params = {
# 'main_attributes': {
# 'customer_id': splynx_id,
# 'status': 'not_paid'
# },
#}
#query_string = splynx.build_splynx_query_params(params)
#result = splynx.get(url=f"/api/2.0/admin/finance/invoices?{query_string}")
invoice_pending = {
"status": "pending"
}
updated_invoices = []
for invoice in invoice_list:
res = splynx.put(url=f"/api/2.0/admin/finance/invoices/{invoice}", params=invoice_pending)
if res:
updated_invoices.append(res)
return updated_invoices
def add_payment_splynx(splynx_id, pi_id, pay_id, amount, invoice_id):
"""Add a payment record to Splynx."""
from datetime import datetime
stripe_pay = {
"customer_id": splynx_id,
"amount": amount,
"date": str(datetime.now().strftime('%Y-%m-%d')),
"field_1": pi_id,
"field_2": f"Single Payment_ID: {pay_id}",
"invoice_id": invoice_id
}
res = splynx.post(url="/api/2.0/admin/finance/payments", params=stripe_pay)
if res:
return res['id']
else:
return False
def get_customer_data_for_notification(splynx_id):
"""Get customer data from Splynx for notifications."""
try:
customer_data = splynx.Customer(splynx_id)
if customer_data != 'unknown':
return customer_data
else:
return {'name': 'Unknown Customer'}
except:
return {'name': 'Unknown Customer'}
def search_stripe_customer_by_email(email):
"""Search for a Stripe customer by email address."""
try:
import stripe
# Use appropriate API key based on config
if Config.PROCESS_LIVE:
stripe.api_key = "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"
else:
stripe.api_key = "sk_test_51Rsi9gPfYyg6zE1S4ZpaPI1ehpbsHRLsGhysYXKwAWCZ7w6KYgVXy4pV095Nd8tyjUw9AkBhqfxqsIiiWJg5fexI00Dw36vnvx"
customers = stripe.Customer.search(query=f"email:'{email}'")
if customers.get('data') and len(customers['data']) > 0:
# Return the most recent customer if multiple exist
return customers['data'][-1]['id']
else:
return None
except Exception as e:
print(f"Error searching Stripe customer by email {email}: {e}")
return None
def create_stripe_customer(customer_data, splynx_id):
"""Create a new Stripe customer with the provided data."""
try:
import stripe
# Use appropriate API key based on config
if Config.PROCESS_LIVE:
stripe.api_key = "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"
else:
stripe.api_key = "sk_test_51Rsi9gPfYyg6zE1S4ZpaPI1ehpbsHRLsGhysYXKwAWCZ7w6KYgVXy4pV095Nd8tyjUw9AkBhqfxqsIiiWJg5fexI00Dw36vnvx"
customer = stripe.Customer.create(
name=customer_data.get('name', ''),
description=customer_data.get('name', ''),
email=customer_data.get('billing_email', ''),
metadata={
'splynx_id': str(splynx_id)
}
)
return customer.id
except Exception as e:
print(f"Error creating Stripe customer for Splynx ID {splynx_id}: {e}")
return None
def update_splynx_customer_stripe_id(splynx_id, stripe_customer_id):
"""Update Splynx customer with Stripe customer ID."""
try:
params = {
'additional_attributes': {
'stripe_customer_id': stripe_customer_id
}
}
update_result = splynx.put(url=f"/api/2.0/admin/customers/customer/{splynx_id}", params=params)
return update_result is not None
except Exception as e:
print(f"Error updating Splynx customer {splynx_id} with Stripe ID {stripe_customer_id}: {e}")
return False
def get_stripe_customer_id(splynx_id):
"""
Get Stripe customer ID for a given Splynx customer ID.
Enhanced logic:
1. First check MySQL database for existing Stripe customer ID
2. If not found, check Splynx additional_attributes for stripe_customer_id
3. If not valid, search Stripe by customer email
4. If still not found, create new Stripe customer
5. Store the Stripe customer ID back to Splynx
"""
connection = None
try:
# Step 1: Check MySQL database first (existing logic)
connection = pymysql.connect(
host=Config.MYSQL_CONFIG['host'],
database=Config.MYSQL_CONFIG['database'],
user=Config.MYSQL_CONFIG['user'],
password=Config.MYSQL_CONFIG['password'],
port=Config.MYSQL_CONFIG['port'],
autocommit=False,
cursorclass=pymysql.cursors.DictCursor
)
query = """
SELECT
cb.customer_id,
cb.deposit,
cb.payment_method,
pad.field_1 AS stripe_customer_id
FROM customer_billing cb
LEFT OUTER JOIN payment_account_data pad ON cb.customer_id = pad.customer_id
WHERE cb.customer_id = %s
ORDER BY cb.payment_method ASC
LIMIT 1
"""
with connection.cursor() as cursor:
cursor.execute(query, (splynx_id,))
result = cursor.fetchone()
print(f"MYSQL: {result}")
if result and result['stripe_customer_id']:
print(f"Found Stripe customer ID in MySQL: {result['stripe_customer_id']}")
return result['stripe_customer_id']
# Step 2: MySQL lookup failed, get customer from Splynx
print(f"No Stripe customer ID found in MySQL for Splynx ID {splynx_id}, checking Splynx...")
cust = splynx.Customer(splynx_id)
if not cust or cust == 'unknown':
print(f"Customer {splynx_id} not found in Splynx")
return None
# Step 3: Check Splynx additional_attributes for stripe_customer_id
additional_attrs = cust.get('additional_attributes', {})
existing_stripe_id = additional_attrs.get('stripe_customer_id', '')
if existing_stripe_id and existing_stripe_id.startswith('cus_'):
print(f"Found valid Stripe customer ID in Splynx: {existing_stripe_id}")
return existing_stripe_id
# Step 4: Search Stripe by customer email
customer_email = cust.get('billing_email', '')
if customer_email:
print(f"Searching Stripe for customer with email: {customer_email}")
stripe_customer_id = search_stripe_customer_by_email(customer_email)
if stripe_customer_id:
print(f"Found existing Stripe customer: {stripe_customer_id}")
# Store the ID back to Splynx
if update_splynx_customer_stripe_id(splynx_id, stripe_customer_id):
print(f"Updated Splynx customer {splynx_id} with Stripe ID {stripe_customer_id}")
else:
print(f"Failed to update Splynx customer {splynx_id} with Stripe ID")
return stripe_customer_id
# Step 5: Create new Stripe customer if none found
print(f"No existing Stripe customer found, creating new customer for Splynx ID {splynx_id}")
stripe_customer_id = create_stripe_customer(cust, splynx_id)
if stripe_customer_id:
print(f"Created new Stripe customer: {stripe_customer_id}")
# Store the new ID back to Splynx
if update_splynx_customer_stripe_id(splynx_id, stripe_customer_id):
print(f"Updated Splynx customer {splynx_id} with new Stripe ID {stripe_customer_id}")
else:
print(f"Failed to update Splynx customer {splynx_id} with new Stripe ID")
return stripe_customer_id
else:
print(f"Failed to create Stripe customer for Splynx ID {splynx_id}")
return None
except pymysql.Error as e:
print(f"MySQL Error in get_stripe_customer_id: {e}")
return None
except Exception as e:
print(f"Unexpected Error in get_stripe_customer_id: {e}")
return None
finally:
if connection:
connection.close()
def get_stripe_payment_methods(stripe_customer_id):
"""Get payment methods for a Stripe customer."""
try:
# Initialize Stripe processor
if Config.PROCESS_LIVE:
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"
payment_methods = processor.get_payment_methods(stripe_customer_id)
return payment_methods
except Exception as e:
print(f"Error fetching payment methods: {e}")
return []
main_bp = Blueprint('main', __name__)
@main_bp.app_template_filter('format_json')
def format_json_filter(json_string):
"""Format JSON string with proper indentation."""
if not json_string:
return ''
try:
# Parse the JSON string and format it with indentation
parsed = json.loads(json_string)
return json.dumps(parsed, indent=2, ensure_ascii=False)
except (json.JSONDecodeError, TypeError):
# If it's not valid JSON, return as-is
return json_string
@main_bp.app_template_filter('currency')
def currency_filter(value):
"""Format number as currency with digit grouping."""
if value is None:
return '$0.00'
try:
# Convert to float if it's not already
num_value = float(value)
# Format with comma separators and 2 decimal places
return f"${num_value:,.2f}"
except (ValueError, TypeError):
return '$0.00'
@main_bp.app_template_filter('error_alert')
def error_alert_filter(payment):
"""Get error alert data for a payment."""
return get_error_alert_data(payment)
@main_bp.route('/')
@login_required
def index():
return render_template('main/index.html')
@main_bp.route('/batches')
@finance_required
def batch_list():
"""Display list of all payment batches with summary information."""
# Query all batches with summary statistics
batches = db.session.query(
PaymentBatch.id,
PaymentBatch.Created,
func.count(Payments.id).label('payment_count'),
func.sum(Payments.Payment_Amount).label('total_amount'),
func.sum(Payments.Fee_Stripe).label('total_fees'),
func.sum(case((Payments.Success == True, 1), else_=0)).label('successful_count'),
func.sum(case((Payments.PI_FollowUp == True, 1), else_=0)).label('pending_count'),
func.sum(case((Payments.Success == False, 1), else_=0)).label('failed_count'),
func.sum(case((Payments.Error.isnot(None), 1), else_=0)).label('error_count')
).outerjoin(Payments, PaymentBatch.id == Payments.PaymentBatch_ID)\
.group_by(PaymentBatch.id, PaymentBatch.Created)\
.order_by(PaymentBatch.Created.desc()).all()
return render_template('main/batch_list.html', batches=batches)
@main_bp.route('/batch/')
@finance_required
def batch_detail(batch_id):
"""Display detailed view of a specific payment batch."""
# Get batch information
batch = PaymentBatch.query.get_or_404(batch_id)
# Get summary statistics for this batch
summary = db.session.query(
func.count(Payments.id).label('payment_count'),
func.sum(Payments.Payment_Amount).label('total_amount'),
func.sum(Payments.Fee_Stripe).label('total_fees'),
func.sum(case((Payments.Success == True, 1), else_=0)).label('successful_count'),
func.sum(case((Payments.Success == False, 1), else_=0)).label('failed_count'),
func.sum(case((Payments.Error.isnot(None), 1), else_=0)).label('error_count')
).filter(Payments.PaymentBatch_ID == batch_id).first()
# Get all payments for this batch ordered by Splynx_ID
payments = Payments.query.filter_by(PaymentBatch_ID=batch_id)\
.order_by(Payments.Splynx_ID.asc()).all()
return render_template('main/batch_detail.html',
batch=batch,
summary=summary,
payments=payments)
@main_bp.route('/single-payment')
@helpdesk_required
def single_payment():
"""Display single payment form page."""
return render_template('main/single_payment.html')
@main_bp.route('/single-payments')
@helpdesk_required
def single_payments_list():
"""Display list of all single payments with summary information."""
# Query all single payments with user information
from models import Users
payments = db.session.query(
SinglePayments.id,
SinglePayments.Splynx_ID,
SinglePayments.Stripe_Customer_ID,
SinglePayments.Payment_Intent,
SinglePayments.Payment_Method,
SinglePayments.Payment_Amount,
SinglePayments.Fee_Stripe,
SinglePayments.Fee_Total,
SinglePayments.Success,
SinglePayments.Error,
SinglePayments.PI_JSON,
SinglePayments.Created,
SinglePayments.PI_FollowUp,
SinglePayments.Refund,
SinglePayments.Refund_FollowUp,
Users.FullName.label('processed_by')
).outerjoin(Users, SinglePayments.Who == Users.id)\
.order_by(SinglePayments.Created.desc()).all()
# Calculate summary statistics
total_payments = len(payments)
successful_payments = sum(1 for p in payments if p.Success == True)
failed_payments = sum(1 for p in payments if p.Success == False)
pending_payments = sum(1 for p in payments if p.Success == None)
total_amount = sum(p.Payment_Amount or 0 for p in payments if p.Success == True)
total_fees = sum(p.Fee_Stripe or 0 for p in payments if p.Success == True)
summary = {
'total_payments': total_payments,
'successful_payments': successful_payments,
'failed_payments': failed_payments,
'pending_payments': pending_payments,
'total_amount': total_amount,
'total_fees': total_fees,
'success_rate': (successful_payments / total_payments * 100) if total_payments > 0 else 0
}
return render_template('main/single_payments_list.html', payments=payments, summary=summary)
@main_bp.route('/single-payment/detail/')
@login_required
def single_payment_detail(payment_id):
"""Display detailed view of a specific single payment."""
# Get payment information
from models import Users
payment = db.session.query(
SinglePayments.id,
SinglePayments.Splynx_ID,
SinglePayments.Stripe_Customer_ID,
SinglePayments.Payment_Intent,
SinglePayments.PI_FollowUp,
SinglePayments.PI_Last_Check,
SinglePayments.Payment_Method,
SinglePayments.Fee_Tax,
SinglePayments.Fee_Stripe,
SinglePayments.Fee_Total,
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)\
.filter(SinglePayments.id == payment_id).first()
if not payment:
flash('Payment not found.', 'error')
return redirect(url_for('main.single_payments_list'))
return render_template('main/single_payment_detail.html', payment=payment)
@main_bp.route('/payment/detail/')
@login_required
def payment_detail(payment_id):
"""Display detailed view of a specific batch payment."""
# Get payment information with all fields needed for the detail view
payment = db.session.query(Payments).filter(Payments.id == payment_id).first()
if not payment:
current_app.logger.warning(f"Payment ID {payment_id} not found in Payments table")
flash(f'Payment #{payment_id} not found in batch payments.', 'error')
return redirect(url_for('main.batch_list'))
# Log the payment detail view access
log_activity(
user_id=current_user.id,
action="view_payment_detail",
entity_type="payment",
entity_id=payment_id,
details=f"Viewed batch payment detail for payment ID {payment_id}"
)
# Batch payments don't store individual user info, so set to Batch Processor
payment.processed_by = 'Batch Processor'
# Render the batch payment detail template
current_app.logger.info(f"Rendering batch payment detail for payment {payment_id} (batch {payment.PaymentBatch_ID})")
return render_template('main/batch_payment_detail.html', payment=payment)
@main_bp.route('/single-payment/check-intent/', methods=['POST'])
@login_required
def check_payment_intent(payment_id):
"""Check the status of a payment intent and update the record."""
from datetime import datetime
try:
# Get the payment record
payment = SinglePayments.query.get_or_404(payment_id)
if not payment.Payment_Intent:
return jsonify({'success': False, 'error': 'No payment intent found'}), 400
# Initialize Stripe processor
if Config.PROCESS_LIVE:
api_key = "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"
else:
api_key = "sk_test_51Rsi9gPfYyg6zE1S4ZpaPI1ehpbsHRLsGhysYXKwAWCZ7w6KYgVXy4pV095Nd8tyjUw9AkBhqfxqsIiiWJg5fexI00Dw36vnvx"
processor = StripePaymentProcessor(api_key=api_key, enable_logging=True)
# Check payment intent status
intent_result = processor.check_payment_intent(payment.Payment_Intent)
print(json.dumps(intent_result, indent=2))
if intent_result['status'] == "succeeded":
payment.PI_FollowUp_JSON = json.dumps(intent_result)
payment.PI_FollowUp = False
payment.PI_Last_Check = datetime.now()
processPaymentResult(pay_id=payment.id, result=intent_result, key="singlepay")
else:
payment.PI_FollowUp_JSON = json.dumps(intent_result)
payment.PI_Last_Check = datetime.now()
db.session.commit()
return jsonify({
'success': True,
'status': intent_result['status'],
'payment_succeeded': intent_result['status'] == "succeeded",
'message': f'Payment intent status: {intent_result["status"]}'
})
except Exception as e:
db.session.rollback()
print(f"Check payment intent error: {e}")
return jsonify({'success': False, 'error': 'Failed to check payment intent'}), 500
@main_bp.route('/api/splynx/invoices/', methods=['GET'])
@helpdesk_required
def get_customer_invoices(splynx_id):
"""Fetch unpaid invoices for a customer from Splynx."""
try:
params = {
'main_attributes': {
'customer_id': splynx_id,
'status': 'not_paid'
}
}
query_string = splynx.build_splynx_query_params(params)
invoices = splynx.get(url=f"/api/2.0/admin/finance/invoices?{query_string}")
# Format invoice data for frontend
formatted_invoices = []
for inv in invoices:
formatted_invoices.append({
'id': inv['id'],
'number': inv.get('number', 'N/A'),
'date': inv.get('date', 'N/A'),
'total': float(inv.get('total', 0)),
'status': inv.get('status', 'unknown'),
'description': inv.get('memo', 'No description')
})
return jsonify({'success': True, 'invoices': formatted_invoices})
except Exception as e:
print(f"Error fetching invoices: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@main_bp.route('/single-payment/process', methods=['POST'])
@helpdesk_required
def process_single_payment():
"""Process a single payment using Stripe."""
try:
# Get form data
splynx_id = request.form.get('splynx_id')
amount = request.form.get('amount')
payment_method = request.form.get('payment_method')
invoice_ids = request.form.get('invoice_ids', [])
invoice_list = invoice_ids.split(",") if invoice_ids else [0]
# Validate inputs
if not splynx_id or not amount or not payment_method:
return jsonify({'success': False, 'error': 'Missing required fields'}), 400
try:
splynx_id = int(splynx_id)
amount = float(amount)
except (ValueError, TypeError):
return jsonify({'success': False, 'error': 'Invalid input format'}), 400
if amount <= 0:
return jsonify({'success': False, 'error': 'Amount must be greater than 0'}), 400
# Get customer details from Splynx
customer_data = splynx.Customer(splynx_id)
if not customer_data:
return jsonify({'success': False, 'error': 'Customer not found in Splynx'}), 404
# Get Stripe customer ID from MySQL
stripe_customer_id = get_stripe_customer_id(splynx_id)
if not stripe_customer_id:
return jsonify({'success': False, 'error': 'Customer does not have a valid Stripe payment method'}), 400
# Create payment record in database
payment_record = SinglePayments(
Splynx_ID=splynx_id,
Stripe_Customer_ID=stripe_customer_id,
Payment_Amount=amount,
Who=current_user.id,
Invoices_to_Pay=invoice_ids
)
db.session.add(payment_record)
db.session.commit() # Commit to get the payment ID
# Initialize Stripe processor
if Config.PROCESS_LIVE:
print("LIVE Payment")
api_key = "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"
else:
print("SANDBOX Payment")
api_key = "sk_test_51Rsi9gPfYyg6zE1S4ZpaPI1ehpbsHRLsGhysYXKwAWCZ7w6KYgVXy4pV095Nd8tyjUw9AkBhqfxqsIiiWJg5fexI00Dw36vnvx"
# Use test customer for sandbox
#import random
#test_customers = ['cus_SoNAgAbkbFo8ZY', 'cus_SoMyDihTxRsa7U', 'cus_SoQedaG3q2ecKG', 'cus_SoMVPWxdYstYbr']
#stripe_customer_id = random.choice(test_customers)
processor = StripePaymentProcessor(api_key=api_key, enable_logging=True)
print(f"stripe_customer_id: {stripe_customer_id}")
# Process payment with specified payment method
result = processor.process_payment(
customer_id=stripe_customer_id,
amount=amount,
currency="aud",
description=f"Single Payment - Splynx ID: {splynx_id} - Payment ID: {payment_record.id}",
stripe_pm=payment_method
)
# Update payment record with results
payment_record.Success = result.get('success', False)
payment_record.Payment_Intent = result.get('payment_intent_id')
payment_record.PI_JSON = json.dumps(result)
if result.get('error') and not result.get('needs_fee_update'):
payment_record.Error = f"Error Type: {result.get('error_type', 'Unknown')}\nError: {result['error']}"
# Send notification and create ticket for failed single payments
try:
# Initialize notification service
notification_service = NotificationService()
# Get customer information
customer_data = get_customer_data_for_notification(splynx_id)
# Prepare payment data for notification
payment_data = {
'payment_id': payment_record.id,
'splynx_id': splynx_id,
'amount': amount,
'error': payment_record.Error,
'payment_method': payment_method,
'customer_name': customer_data.get('name', 'Unknown Customer'),
'payment_type': 'single',
'stripe_customer_id': stripe_customer_id,
'payment_intent': result.get('payment_intent_id')
}
# Send notification and create ticket (only in live mode)
#if Config.PROCESS_LIVE:
# Send email notification
email_sent = notification_service.send_payment_failure_notification(payment_data)
# Create Splynx ticket
ticket_result = splynx.create_ticket(
customer_id=splynx_id,
subject=f"Payment Failure - ${amount:.2f}",
type_id=1,
group_up=7,
status_id=1,
priority="medium"
)
internal_message=f"""
Single payment processing has failed for customer {customer_data.get('name', 'Unknown')} (ID: {splynx_id}).
Payment Details:
- Payment ID: {payment_record.id} (single payment)
- Amount: ${amount:.2f} AUD
- Payment Method: {payment_method}
- Stripe Customer: {stripe_customer_id}
- Payment Intent: {result.get('payment_intent_id', 'N/A')}
- Processed by: {current_user.FullName}
Error Information:
{payment_record.Error}
This ticket was automatically created by the Plutus Payment System.
"""
# Create customer-friendly message
payment_data_for_msg = {
'amount': amount,
'splynx_id': splynx_id,
'pi_json': result.get('pi_json') or json.dumps(result)
}
cust_message = create_customer_friendly_message(payment_data_for_msg, result.get('error', 'Unknown error'))
# Add Internal Note
add_internal_note = splynx.add_ticket_message(
ticket_id=ticket_result['ticket_id'],
message=internal_message,
is_admin=False,
hide_for_customer=True,
message_type="note"
)
# Customer Message
add_message = splynx.add_ticket_message(
ticket_id=ticket_result['ticket_id'],
message=cust_message,
is_admin=False,
hide_for_customer=False,
message_type="message"
)
print(f"Notification sent: {email_sent}, Ticket created: {ticket_result.get('success', False)}")
except Exception as e:
print(f"Error sending notification for failed single payment: {e}")
if result.get('needs_fee_update'):
payment_record.PI_FollowUp = True
# Mark invoices as pending when PI_FollowUp is set
#if Config.PROCESS_LIVE:
try:
pending_invoices = find_set_pending_splynx_invoices(splynx_id, invoice_list)
if invoice_list[0] == 0:
payment_record.Invoices_to_Pay = ','.join(pending_invoices)
except Exception as e:
print(f"⚠️ Error setting invoices to pending: {e}")
if result.get('payment_method_type') == "card":
payment_record.Payment_Method = result.get('estimated_fee_details', {}).get('card_display_brand', 'card')
elif result.get('payment_method_type') == "au_becs_debit":
payment_record.Payment_Method = result['payment_method_type']
if result.get('fee_details') and result.get('fee_details').get('fee_breakdown'):
payment_record.Fee_Total = result['fee_details']['total_fee']
for fee_type in result['fee_details']['fee_breakdown']:
if fee_type['type'] == "tax":
payment_record.Fee_Tax = fee_type['amount']
elif fee_type['type'] == "stripe_fee":
payment_record.Fee_Stripe = fee_type['amount']
# Commit the updated payment record
db.session.commit()
# Check if payment was actually successful
if result.get('success'):
# Payment succeeded - update Splynx if in live mode
#if Config.PROCESS_LIVE:
try:
# Add payment record to Splynx
splynx_payment_id = add_payment_splynx(
splynx_id=splynx_id,
pi_id=result.get('payment_intent_id'),
pay_id=payment_record.id,
amount=amount,
invoice_id=invoice_list[0]
)
if splynx_payment_id:
print(f"✅ Splynx payment record created: {splynx_payment_id}")
# Mark invoices as paid in Splynx
find_pay_splynx_invoices(splynx_id, splynx_payment_id, invoice_list)
else:
print("⚠️ Failed to create Splynx payment record")
except Exception as splynx_error:
print(f"❌ Error updating Splynx: {splynx_error}")
# Continue processing even if Splynx update fails
# Log successful payment
log_activity(
current_user.id,
"PAYMENT_SUCCESS",
"SinglePayment",
payment_record.id,
details=f"Single payment successful: ${amount:,.2f} for customer {splynx_id} ({customer_data.get('name', 'Unknown')})"
)
# Payment succeeded
return jsonify({
'success': True,
'payment_success': True,
'payment_id': payment_record.id,
'payment_intent': result.get('payment_intent_id'),
'amount': amount,
'customer_name': customer_data.get('name'),
'message': f'Payment processed successfully for {customer_data.get("name")}'
})
else:
# Payment failed - log the failure
log_activity(
current_user.id,
"PAYMENT_FAILED",
"SinglePayment",
payment_record.id,
details=f"Single payment failed: ${amount:,.2f} for customer {splynx_id} ({customer_data.get('name', 'Unknown')}) - {result.get('error', 'Unknown error')}"
)
# Payment failed - return the specific error
if result.get('needs_fee_update'):
fee_update = True
else:
fee_update = False
return jsonify({
'success': False,
'payment_success': False,
'fee_update': fee_update,
'payment_id': payment_record.id,
'error': result.get('error', 'Payment failed'),
'error_type': result.get('error_type', 'unknown_error'),
'stripe_error': result.get('error', 'Unknown payment error'),
'customer_name': customer_data.get('name')
}), 422 # 422 Unprocessable Entity for business logic failures
except Exception as e:
db.session.rollback()
print(f"Single payment processing error: {e}")
return jsonify({'success': False, 'error': 'Payment processing failed. Please try again.'}), 500
@main_bp.route('/payment-plans')
@finance_required
def payment_plans_list():
"""Display list of all payment plans with summary information."""
from models import Users
# Query all payment plans with user information
plans = db.session.query(
PaymentPlans.id,
PaymentPlans.Splynx_ID,
PaymentPlans.Amount,
PaymentPlans.Frequency,
PaymentPlans.Start_Date,
PaymentPlans.Stripe_Payment_Method,
PaymentPlans.Enabled,
PaymentPlans.Created,
Users.FullName.label('created_by')
).outerjoin(Users, PaymentPlans.Who == Users.id)\
.order_by(PaymentPlans.Created.desc()).all()
# Calculate summary statistics
total_plans = len(plans)
active_plans = sum(1 for p in plans if p.Enabled == True)
inactive_plans = sum(1 for p in plans if p.Enabled == False)
total_recurring_amount = sum(p.Amount or 0 for p in plans if p.Enabled == True)
summary = {
'total_plans': total_plans,
'active_plans': active_plans,
'inactive_plans': inactive_plans,
'total_recurring_amount': total_recurring_amount
}
return render_template('main/payment_plans_list.html', plans=plans, summary=summary)
@main_bp.route('/payment-plans/create')
@finance_required
def payment_plans_create():
"""Display payment plan creation form."""
return render_template('main/payment_plans_form.html', edit_mode=False)
@main_bp.route('/payment-plans/create', methods=['POST'])
@finance_required
def payment_plans_create_post():
"""Handle payment plan creation."""
try:
# Get form data
splynx_id = request.form.get('splynx_id')
amount = request.form.get('amount')
frequency = request.form.get('frequency')
start_date = request.form.get('start_date')
stripe_payment_method = request.form.get('stripe_payment_method')
# Validate inputs
if not all([splynx_id, amount, frequency, start_date, stripe_payment_method]):
flash('All fields are required.', 'error')
return redirect(url_for('main.payment_plans_create'))
try:
splynx_id = int(splynx_id)
amount = float(amount)
from datetime import datetime
start_date = datetime.strptime(start_date, '%Y-%m-%d')
except (ValueError, TypeError):
flash('Invalid input format.', 'error')
return redirect(url_for('main.payment_plans_create'))
if amount <= 0:
flash('Amount must be greater than 0.', 'error')
return redirect(url_for('main.payment_plans_create'))
# Validate customer exists in Splynx
customer_data = splynx.Customer(splynx_id)
if not customer_data:
flash('Customer not found in Splynx.', 'error')
return redirect(url_for('main.payment_plans_create'))
# Create payment plan record
payment_plan = PaymentPlans(
Splynx_ID=splynx_id,
Amount=amount,
Frequency=frequency,
Start_Date=start_date,
Stripe_Payment_Method=stripe_payment_method,
Who=current_user.id
)
db.session.add(payment_plan)
db.session.commit()
# Log payment plan creation
log_activity(
current_user.id,
"PAYPLAN_CREATED",
"PaymentPlan",
payment_plan.id,
details=f"Payment plan created: ${amount:,.2f} {frequency} for customer {splynx_id} ({customer_data.get('name', 'Unknown')})"
)
flash(f'Payment plan created successfully for {customer_data.get("name", "customer")}.', 'success')
return redirect(url_for('main.payment_plans_detail', plan_id=payment_plan.id))
except Exception as e:
db.session.rollback()
print(f"Payment plan creation error: {e}")
flash('Failed to create payment plan. Please try again.', 'error')
return redirect(url_for('main.payment_plans_create'))
@main_bp.route('/payment-plans/edit/')
@finance_required
def payment_plans_edit(plan_id):
"""Display payment plan edit form."""
plan = PaymentPlans.query.get_or_404(plan_id)
return render_template('main/payment_plans_form.html', plan=plan, edit_mode=True)
@main_bp.route('/payment-plans/edit/', methods=['POST'])
@finance_required
def payment_plans_edit_post(plan_id):
"""Handle payment plan updates."""
try:
plan = PaymentPlans.query.get_or_404(plan_id)
# Get form data
amount = request.form.get('amount')
frequency = request.form.get('frequency')
start_date = request.form.get('start_date')
stripe_payment_method = request.form.get('stripe_payment_method')
# Validate inputs
if not all([amount, frequency, start_date, stripe_payment_method]):
flash('All fields are required.', 'error')
return redirect(url_for('main.payment_plans_edit', plan_id=plan_id))
try:
amount = float(amount)
from datetime import datetime
start_date = datetime.strptime(start_date, '%Y-%m-%d')
except (ValueError, TypeError):
flash('Invalid input format.', 'error')
return redirect(url_for('main.payment_plans_edit', plan_id=plan_id))
if amount <= 0:
flash('Amount must be greater than 0.', 'error')
return redirect(url_for('main.payment_plans_edit', plan_id=plan_id))
# Update payment plan
plan.Amount = amount
plan.Frequency = frequency
plan.Start_Date = start_date
plan.Stripe_Payment_Method = stripe_payment_method
db.session.commit()
# Log payment plan update
log_activity(
current_user.id,
"PAYPLAN_UPDATED",
"PaymentPlan",
plan.id,
details=f"Payment plan updated: ${amount:,.2f} {frequency} starting {start_date.strftime('%Y-%m-%d')}"
)
flash('Payment plan updated successfully.', 'success')
return redirect(url_for('main.payment_plans_detail', plan_id=plan.id))
except Exception as e:
db.session.rollback()
print(f"Payment plan update error: {e}")
flash('Failed to update payment plan. Please try again.', 'error')
return redirect(url_for('main.payment_plans_edit', plan_id=plan_id))
@main_bp.route('/payment-plans/delete/', methods=['POST'])
@finance_required
def payment_plans_delete(plan_id):
"""Handle payment plan deletion (soft delete)."""
try:
plan = PaymentPlans.query.get_or_404(plan_id)
# Soft delete by setting Enabled to False
plan.Enabled = False
db.session.commit()
flash('Payment plan has been disabled.', 'success')
return redirect(url_for('main.payment_plans_list'))
except Exception as e:
db.session.rollback()
print(f"Payment plan deletion error: {e}")
flash('Failed to disable payment plan. Please try again.', 'error')
return redirect(url_for('main.payment_plans_detail', plan_id=plan_id))
@main_bp.route('/payment-plans/toggle/', methods=['POST'])
@login_required
def payment_plans_toggle(plan_id):
"""Toggle payment plan enabled status."""
try:
plan = PaymentPlans.query.get_or_404(plan_id)
# Toggle enabled status
plan.Enabled = not plan.Enabled
db.session.commit()
# Log payment plan toggle
action = "PAYPLAN_ENABLED" if plan.Enabled else "PAYPLAN_DISABLED"
log_activity(
current_user.id,
action,
"PaymentPlan",
plan.id,
details=f"Payment plan {'enabled' if plan.Enabled else 'disabled'}: ${plan.Amount:,.2f} {plan.Frequency}"
)
status = "enabled" if plan.Enabled else "disabled"
flash(f'Payment plan has been {status}.', 'success')
return redirect(url_for('main.payment_plans_detail', plan_id=plan_id))
except Exception as e:
db.session.rollback()
print(f"Payment plan toggle error: {e}")
flash('Failed to update payment plan status. Please try again.', 'error')
return redirect(url_for('main.payment_plans_detail', plan_id=plan_id))
@main_bp.route('/payment-plans/detail/')
@login_required
def payment_plans_detail(plan_id):
"""Display detailed view of a specific payment plan."""
from models import Users
# Get payment plan with user information
plan = db.session.query(
PaymentPlans.id,
PaymentPlans.Splynx_ID,
PaymentPlans.Amount,
PaymentPlans.Frequency,
PaymentPlans.Start_Date,
PaymentPlans.Stripe_Payment_Method,
PaymentPlans.Enabled,
PaymentPlans.Created,
Users.FullName.label('created_by')
).outerjoin(Users, PaymentPlans.Who == Users.id)\
.filter(PaymentPlans.id == plan_id).first()
if not plan:
flash('Payment plan not found.', 'error')
return redirect(url_for('main.payment_plans_list'))
# Get associated single payments
associated_payments = db.session.query(
Payments.id,
Payments.Payment_Amount,
Payments.Success,
Payments.Error,
Payments.Created,
Payments.Payment_Intent)\
.filter(Payments.PaymentPlan_ID == plan_id)\
.order_by(Payments.Created.desc()).all()
return render_template('main/payment_plans_detail.html',
plan=plan,
associated_payments=associated_payments)
@main_bp.route('/api/stripe-customer-id/')
@login_required
def api_stripe_customer_id(splynx_id):
"""Get Stripe customer ID for a Splynx customer."""
print(f"\n\nSplynx/Stripe finding customer\n\n")
try:
stripe_customer_id = get_stripe_customer_id(splynx_id)
if stripe_customer_id:
return jsonify({'success': True, 'stripe_customer_id': stripe_customer_id})
else:
return jsonify({'success': False, 'error': 'Customer does not have a Stripe customer ID'}), 404
except Exception as e:
print(f"Error fetching Stripe customer ID: {e}")
return jsonify({'success': False, 'error': 'Failed to fetch Stripe customer ID'}), 500
@main_bp.route('/api/stripe-payment-methods/')
@login_required
def api_stripe_payment_methods(stripe_customer_id):
"""Get Stripe payment methods for a customer."""
try:
payment_methods = get_stripe_payment_methods(stripe_customer_id)
return jsonify({'success': True, 'payment_methods': payment_methods})
except Exception as e:
print(f"Error fetching payment methods: {e}")
return jsonify({'success': False, 'error': 'Failed to fetch payment methods'}), 500
@main_bp.route('/payment/check-intent/', methods=['POST'])
@login_required
def check_batch_payment_intent(payment_id):
"""Check the status of a batch payment intent and update the record."""
from datetime import datetime
try:
# Get the payment record from Payments table (batch payments)
payment = Payments.query.get_or_404(payment_id)
if not payment.Payment_Intent:
return jsonify({'success': False, 'error': 'No payment intent found'}), 400
# Initialize Stripe processor with correct API key
if Config.PROCESS_LIVE:
api_key = Config.STRIPE_LIVE_API_KEY
else:
api_key = Config.STRIPE_TEST_API_KEY
processor = StripePaymentProcessor(api_key=api_key, enable_logging=True)
# Check payment intent status
intent_result = processor.check_payment_intent(payment.Payment_Intent)
if intent_result['status'] == "succeeded":
payment.PI_FollowUp_JSON = json.dumps(intent_result)
payment.PI_FollowUp = False
payment.PI_Last_Check = datetime.now()
payment.Success = True
if intent_result.get('charge_id'):
payment.Stripe_Charge_ID = intent_result.get('charge_id')
processPaymentResult(pay_id=payment.id, result=intent_result, key="pay")
elif intent_result['status'] == "failed":
payment.PI_FollowUp_JSON = json.dumps(intent_result)
payment.PI_FollowUp = False
payment.PI_Last_Check = datetime.now()
payment.Success = False
else:
# Still pending
payment.PI_FollowUp_JSON = json.dumps(intent_result)
payment.PI_Last_Check = datetime.now()
db.session.commit()
# Log the intent check activity
log_activity(
user_id=current_user.id,
action="check_payment_intent",
entity_type="payment",
entity_id=payment_id,
details=f"Checked payment intent {payment.Payment_Intent}, status: {intent_result['status']}"
)
return jsonify({
'success': True,
'status': intent_result['status'],
'payment_succeeded': intent_result['status'] == "succeeded",
'message': f'Payment intent status: {intent_result["status"]}'
})
except Exception as e:
db.session.rollback()
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/', 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 based on refund status
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)
# Handle refund status - check if it's succeeded or pending
if refund.status == "succeeded":
payment.Refund = True
refund_status_message = "succeeded"
elif refund.status == "pending":
payment.Refund_FollowUp = True
refund_status_message = "pending"
else:
# For any other status, just store the refund data but don't mark as complete
refund_status_message = refund.status
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}, status: {refund_status_message}"
)
return jsonify({
'success': True,
'pending': refund.status == "pending",
'refund_id': refund.id,
'refund_status': refund.status,
'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('/single-payment/check-refund/', methods=['POST'])
@login_required
def check_single_payment_refund_status(payment_id):
"""Check the status of a pending 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
if not payment.Stripe_Refund_ID:
return jsonify({'success': False, 'error': 'No refund ID found for this payment'}), 400
# Initialize Stripe
import stripe
if Config.PROCESS_LIVE:
stripe.api_key = Config.STRIPE_LIVE_API_KEY
else:
stripe.api_key = Config.STRIPE_TEST_API_KEY
# Get refund details from Stripe
refund = stripe.Refund.retrieve(payment.Stripe_Refund_ID)
# Update payment record based on refund status
if refund.status == "succeeded":
payment.Refund = True
payment.Refund_FollowUp = False
refund_completed = True
elif refund.status == "pending":
# Still pending, no change needed
refund_completed = False
elif refund.status in ["failed", "canceled"]:
# Refund failed, update status
payment.Refund_FollowUp = False
refund_completed = False
else:
refund_completed = False
# Update the Refund_JSON with latest data
payment.Refund_JSON = json.dumps(refund, default=str)
db.session.commit()
# Log the refund status check
log_activity(
user_id=current_user.id,
action="check_refund_status",
entity_type="single_payment",
entity_id=payment_id,
details=f"Checked refund status for single payment ID {payment_id}, status: {refund.status}"
)
return jsonify({
'success': True,
'refund_completed': refund_completed,
'status': refund.status,
'refund_id': refund.id,
'amount_refunded': f"${refund.amount/100:.2f}"
})
except stripe.StripeError as e:
return jsonify({'success': False, 'error': f'Stripe error: {str(e)}'}), 500
except Exception as e:
print(f"Error checking refund status: {e}")
return jsonify({'success': False, 'error': 'Internal server error'}), 500
@main_bp.route('/payment/check-refund/', methods=['POST'])
@login_required
def check_batch_payment_refund_status(payment_id):
"""Check the status of a pending refund for a batch payment."""
try:
# Get the payment record from Payments table (batch payments)
payment = Payments.query.get_or_404(payment_id)
if not payment.Stripe_Refund_ID:
return jsonify({'success': False, 'error': 'No refund ID found for this payment'}), 400
# Initialize Stripe
import stripe
if Config.PROCESS_LIVE:
stripe.api_key = Config.STRIPE_LIVE_API_KEY
else:
stripe.api_key = Config.STRIPE_TEST_API_KEY
# Get refund details from Stripe
refund = stripe.Refund.retrieve(payment.Stripe_Refund_ID)
# Update payment record based on refund status
if refund.status == "succeeded":
payment.Refund = True
payment.Refund_FollowUp = False
refund_completed = True
elif refund.status == "pending":
# Still pending, no change needed
refund_completed = False
elif refund.status in ["failed", "canceled"]:
# Refund failed, update status
payment.Refund_FollowUp = False
refund_completed = False
else:
refund_completed = False
# Update the Refund_JSON with latest data
payment.Refund_JSON = json.dumps(refund, default=str)
db.session.commit()
# Log the refund status check
log_activity(
user_id=current_user.id,
action="check_refund_status",
entity_type="payment",
entity_id=payment_id,
details=f"Checked refund status for batch payment ID {payment_id}, status: {refund.status}"
)
return jsonify({
'success': True,
'refund_completed': refund_completed,
'status': refund.status,
'refund_id': refund.id,
'amount_refunded': f"${refund.amount/100:.2f}"
})
except stripe.StripeError as e:
return jsonify({'success': False, 'error': f'Stripe error: {str(e)}'}), 500
except Exception as e:
print(f"Error checking batch refund status: {e}")
return jsonify({'success': False, 'error': 'Internal server error'}), 500
@main_bp.route('/logs')
@finance_required
def logs_list():
"""Display system logs with filtering and pagination."""
# Get filter parameters
page = request.args.get('page', 1, type=int)
search = request.args.get('search', '')
user_filter = request.args.get('user', '', type=int)
action_filter = request.args.get('action', '')
entity_type_filter = request.args.get('entity_type', '')
date_from = request.args.get('date_from', '')
date_to = request.args.get('date_to', '')
# Build query with joins and filters
from models import Users
query = db.session.query(
Logs.id,
Logs.User_ID,
Logs.Log_Entry,
Logs.Added,
Logs.Action,
Logs.Entity_Type,
Logs.Entity_ID,
Logs.IP_Address,
Users.FullName.label('user_name')
).outerjoin(Users, Logs.User_ID == Users.id)
# Apply filters
if search:
search_filter = f"%{search}%"
query = query.filter(db.or_(
Logs.Log_Entry.like(search_filter),
Logs.Action.like(search_filter),
Logs.Entity_Type.like(search_filter),
Users.FullName.like(search_filter),
Logs.IP_Address.like(search_filter)
))
if user_filter:
query = query.filter(Logs.User_ID == user_filter)
if action_filter:
query = query.filter(Logs.Action == action_filter)
if entity_type_filter:
query = query.filter(Logs.Entity_Type == entity_type_filter)
if date_from:
try:
from datetime import datetime
date_from_obj = datetime.strptime(date_from, '%Y-%m-%d')
query = query.filter(Logs.Added >= date_from_obj)
except ValueError:
pass
if date_to:
try:
from datetime import datetime
date_to_obj = datetime.strptime(date_to, '%Y-%m-%d')
# Add one day to include the entire date_to day
date_to_obj = date_to_obj.replace(hour=23, minute=59, second=59)
query = query.filter(Logs.Added <= date_to_obj)
except ValueError:
pass
# Order by most recent first
query = query.order_by(Logs.Added.desc())
# Paginate results
per_page = 50 # Show 50 logs per page
pagination = query.paginate(
page=page,
per_page=per_page,
error_out=False
)
logs = pagination.items
# Get unique users for filter dropdown
users = db.session.query(Users.id, Users.FullName).filter(
Users.id.in_(
db.session.query(Logs.User_ID).distinct()
)
).order_by(Users.FullName).all()
# Get unique actions for filter dropdown
actions = db.session.query(Logs.Action).filter(
Logs.Action.isnot(None)
).distinct().order_by(Logs.Action).all()
actions = [action[0] for action in actions if action[0]]
# Get unique entity types for filter dropdown
entity_types = db.session.query(Logs.Entity_Type).filter(
Logs.Entity_Type.isnot(None)
).distinct().order_by(Logs.Entity_Type).all()
entity_types = [entity_type[0] for entity_type in entity_types if entity_type[0]]
# Log this page access
log_activity(
user_id=current_user.id,
action="view_logs",
entity_type="logs",
details=f"Viewed system logs page {page} with filters: search={search}, user={user_filter}, action={action_filter}",
ip_address=request.remote_addr
)
return render_template(
'main/logs_list.html',
logs=logs,
pagination=pagination,
users=users,
actions=actions,
entity_types=entity_types
)
@main_bp.route('/logs/detail/')
@finance_required
def log_detail(log_id):
"""Get detailed information for a specific log entry."""
log = db.session.query(Logs).filter(Logs.id == log_id).first()
if not log:
return jsonify({'success': False, 'error': 'Log entry not found'}), 404
# Get user name if available
from models import Users
user = db.session.query(Users).filter(Users.id == log.User_ID).first() if log.User_ID else None
log_data = {
'id': log.id,
'User_ID': log.User_ID,
'user_name': user.FullName if user else None,
'Log_Entry': log.Log_Entry,
'timestamp': log.Added.strftime('%Y-%m-%d %H:%M:%S') if log.Added else None,
'Action': log.Action,
'Entity_Type': log.Entity_Type,
'Entity_ID': log.Entity_ID,
'IP_Address': log.IP_Address
}
# Log this detail view access
log_activity(
user_id=current_user.id,
action="view_log_detail",
entity_type="log",
entity_id=log_id,
details=f"Viewed details for log entry {log_id}",
ip_address=request.remote_addr
)
return jsonify({'success': True, 'log': log_data})
@main_bp.route('/logs/export')
@finance_required
def export_logs():
"""Export logs as CSV file with current filters applied."""
# Get filter parameters (same as logs_list)
search = request.args.get('search', '')
user_filter = request.args.get('user', '', type=int)
action_filter = request.args.get('action', '')
entity_type_filter = request.args.get('entity_type', '')
date_from = request.args.get('date_from', '')
date_to = request.args.get('date_to', '')
# Build query with same filters as logs_list
from models import Users
query = db.session.query(
Logs.id,
Logs.User_ID,
Logs.Log_Entry,
Logs.Added,
Logs.Action,
Logs.Entity_Type,
Logs.Entity_ID,
Logs.IP_Address,
Users.FullName.label('user_name')
).outerjoin(Users, Logs.User_ID == Users.id)
# Apply same filters as in logs_list
if search:
search_filter = f"%{search}%"
query = query.filter(db.or_(
Logs.Log_Entry.like(search_filter),
Logs.Action.like(search_filter),
Logs.Entity_Type.like(search_filter),
Users.FullName.like(search_filter),
Logs.IP_Address.like(search_filter)
))
if user_filter:
query = query.filter(Logs.User_ID == user_filter)
if action_filter:
query = query.filter(Logs.Action == action_filter)
if entity_type_filter:
query = query.filter(Logs.Entity_Type == entity_type_filter)
if date_from:
try:
from datetime import datetime
date_from_obj = datetime.strptime(date_from, '%Y-%m-%d')
query = query.filter(Logs.Added >= date_from_obj)
except ValueError:
pass
if date_to:
try:
from datetime import datetime
date_to_obj = datetime.strptime(date_to, '%Y-%m-%d')
date_to_obj = date_to_obj.replace(hour=23, minute=59, second=59)
query = query.filter(Logs.Added <= date_to_obj)
except ValueError:
pass
# Order by most recent first
logs = query.order_by(Logs.Added.desc()).limit(10000).all() # Limit to 10k records for export
# Generate CSV
import csv
from io import StringIO
from flask import Response
output = StringIO()
writer = csv.writer(output)
# Write headers
writer.writerow([
'ID', 'Timestamp', 'User ID', 'User Name', 'Action',
'Entity Type', 'Entity ID', 'Details', 'IP Address'
])
# Write data
for log in logs:
writer.writerow([
log.id,
log.Added.strftime('%Y-%m-%d %H:%M:%S') if log.Added else '',
log.User_ID or '',
log.user_name or '',
log.Action or '',
log.Entity_Type or '',
log.Entity_ID or '',
(log.Log_Entry or '').replace('\n', ' ').replace('\r', ' '), # Clean newlines for CSV
log.IP_Address or ''
])
# Log the export activity
log_activity(
user_id=current_user.id,
action="export_logs",
entity_type="logs",
details=f"Exported {len(logs)} log entries with filters applied",
ip_address=request.remote_addr
)
# Create response
from datetime import datetime
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
filename = f"system_logs_{timestamp}.csv"
response = Response(
output.getvalue(),
mimetype='text/csv',
headers={'Content-Disposition': f'attachment; filename={filename}'}
)
return response
@main_bp.route('/payment/refund/', methods=['POST'])
@login_required
def process_payment_refund(payment_id):
"""Process a refund for a batch payment."""
from datetime import datetime
import stripe
try:
# Get the payment record from Payments table (batch payments)
payment = Payments.query.get_or_404(payment_id)
# Validate payment can be refunded
if not payment.Success:
return jsonify({'success': False, 'error': 'Cannot refund an unsuccessful payment'}), 400
if payment.Refund:
return jsonify({'success': False, 'error': 'Payment has already been refunded'}), 400
#if not payment.Stripe_Charge_ID:
# return jsonify({'success': False, 'error': 'No Stripe charge ID found for this payment'}), 400
# Get refund reason from request
data = request.get_json()
reason = data.get('reason', 'requested_by_customer')
# Initialize Stripe with correct API key
if Config.PROCESS_LIVE:
stripe.api_key = Config.STRIPE_LIVE_API_KEY
else:
stripe.api_key = Config.STRIPE_TEST_API_KEY
# Create refund parameters
#refund_params = {
# 'charge': payment.Stripe_Charge_ID,
# 'reason': reason
#}
# Process the refund with Stripe
#refund = stripe.Refund.create(**refund_params)
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
}
)
print(f"refund: {refund}")
time.sleep(3)
refunds = stripe.Refund.list(payment_intent=payment.Payment_Intent)
#refund = stripe.Refund.retrieve(payment_intent=payment.Payment_Intent)
#for refund in refunds.data:
if refunds.count == 1:
refund = refunds.data[-1]
else:
# Log the refund activity
log_activity(
user_id=current_user.id,
action="process_refund",
entity_type="refund_error",
entity_id=payment_id,
details=f"Error in refund count ({refunds.count} for Pay Intent: {payment.Payment_Intent})"
)
if refund['status'] in ["succeeded", "pending"]:
# Update payment record with refund information
payment.Stripe_Refund_ID = refund.id
payment.Stripe_Refund_Created = datetime.fromtimestamp(refund.created)
payment.Refund_JSON = json.dumps(refund)
if refund['status'] == "succeeded":
payment.Refund = True
elif refund['status'] == "pending":
payment.Refund_FollowUp = True
db.session.commit()
# Log the refund activity
log_activity(
user_id=current_user.id,
action="process_refund",
entity_type="payment",
entity_id=payment_id,
details=f"Processed refund {refund.id} for payment {payment_id}, amount: ${refund.amount/100:.2f}"
)
if refund['status'] == "succeeded":
return jsonify({
'success': True,
'pending': False,
'refund_id': refund.id,
'amount_refunded': f"${refund.amount/100:.2f}",
'status': refund.status,
'message': 'Refund processed successfully'
})
elif refund['status'] == "pending":
return jsonify({
'success': True,
'pending': True,
'refund_id': refund.id,
'amount_refunded': f"${refund.amount/100:.2f}",
'status': refund.status,
'message': 'Refund is being processed. Refund should occur within the next few days.'
})
else:
# Refund failed
payment.Refund = False
payment.Refund_JSON = json.dumps(refund)
db.session.commit()
return jsonify({
'success': False,
'error': f'Refund failed with status: {refund.status}'
}), 400
except stripe.error.InvalidRequestError as e:
# Handle Stripe-specific errors
error_msg = str(e)
if "has already been refunded" in error_msg:
# Mark as refunded in our database even if Stripe says it's already refunded
payment.Refund = True
payment.Stripe_Refund_Created = datetime.now()
db.session.commit()
return jsonify({'success': False, 'error': 'Payment has already been refunded in Stripe'}), 400
else:
return jsonify({'success': False, 'error': f'Stripe error: {error_msg}'}), 400
except Exception as e:
db.session.rollback()
print(f"Process payment refund error: {e}")
return jsonify({'success': False, 'error': 'Failed to process refund'}), 500
@main_bp.route('/api/splynx/')
@login_required
def api_splynx_customer(id):
"""
Get Splynx customer information by ID
Security: Restricted to operational and financial staff who need customer data access
"""
try:
log_activity(current_user.id, "API_ACCESS", "SplynxCustomer", id,
details=f"Accessed Splynx customer API for customer {id}")
print(f"Splynx Customer API: {id}")
res = splynx.Customer(id)
if res:
log_activity(current_user.id, "API_SUCCESS", "SplynxCustomer", id,
details=f"Successfully retrieved Splynx customer {id}")
return res
else:
log_activity(current_user.id, "API_NOT_FOUND", "SplynxCustomer", id,
details=f"Splynx customer {id} not found")
return {"error": "Customer not found"}, 404
except Exception as e:
log_activity(current_user.id, "API_ERROR", "SplynxCustomer", id,
details=f"Splynx customer API error: {str(e)}")
return {"error": "Internal server error"}, 500
# ============ Payment Method Management Routes ============
@main_bp.route('/single-payments/add-payment-method')
@login_required
def add_payment_method():
"""
Display the payment method addition form.
"""
log_activity(
user_id=current_user.id,
action="view_add_payment_method",
entity_type="payment_method",
details="Accessed add payment method page",
ip_address=request.remote_addr
)
return render_template('main/add_payment_method.html')
@main_bp.route('/api/create-setup-intent', methods=['POST'])
@login_required
def create_setup_intent():
"""
Create a Stripe Setup Intent for collecting payment method details.
"""
try:
data = request.get_json()
stripe_customer_id = data.get('stripe_customer_id')
#stripe_customer_id = 'cus_SoQqMGLmCjiBDZ'
payment_method_types = data.get('payment_method_types', ['card', 'au_becs_debit'])
if not stripe_customer_id:
return jsonify({
'success': False,
'error': 'stripe_customer_id is required'
}), 400
# Initialize Stripe processor
config = Config()
processor = StripePaymentProcessor(api_key=config.STRIPE_SECRET_KEY)
# Create setup intent
result = processor.create_setup_intent(
customer_id=stripe_customer_id,
payment_method_types=payment_method_types
)
print(f"main result: {result}")
if result['success']:
# Add the publishable key for frontend
result['stripe_publishable_key'] = config.STRIPE_PUBLISHABLE_KEY
log_activity(
user_id=current_user.id,
action="create_setup_intent",
entity_type="setup_intent",
entity_id=0,
details=f"Created setup intent for customer {stripe_customer_id} using Setup Intent: {result['setup_intent_id']}",
ip_address=request.remote_addr
)
else:
log_activity(
user_id=current_user.id,
action="create_setup_intent_failed",
entity_type="setup_intent",
details=f"Failed to create setup intent for customer {stripe_customer_id}: {result.get('error')}",
ip_address=request.remote_addr
)
return jsonify(result)
except Exception as e:
log_activity(
user_id=current_user.id,
action="create_setup_intent_error",
entity_type="setup_intent",
details=f"Setup intent creation error: {str(e)}",
ip_address=request.remote_addr
)
return jsonify({
'success': False,
'error': f'Setup intent creation failed: {str(e)}'
}), 500
@main_bp.route('/api/finalize-payment-method', methods=['POST'])
@login_required
def finalize_payment_method():
"""
Finalize payment method setup after Stripe confirmation.
"""
#try:
data = request.get_json()
setup_intent_id = data.get('setup_intent_id')
stripe_customer_id = data.get('stripe_customer_id')
#stripe_customer_id = "cus_SoQqMGLmCjiBDZ"
set_as_default = data.get('set_as_default', False)
splynx_id = data.get('splynx_id')
if not all([setup_intent_id, stripe_customer_id]):
return jsonify({
'success': False,
'error': 'setup_intent_id and stripe_customer_id are required'
}), 400
# Initialize Stripe processor
config = Config()
processor = StripePaymentProcessor(api_key=config.STRIPE_SECRET_KEY)
# Check setup intent status
setup_result = processor.get_setup_intent_status(setup_intent_id)
print(f"setup_result: {setup_result}")
if not setup_result['success']:
return jsonify({
'success': False,
'error': f'Setup intent check failed: {setup_result.get("error")}'
}), 400
if setup_result['status'] != 'succeeded':
return jsonify({
'success': False,
'error': f'Setup intent not succeeded. Status: {setup_result["status"]}'
}), 400
payment_method = setup_result.get('payment_method')
print(f"payment_method: {payment_method}")
if not payment_method:
return jsonify({
'success': False,
'error': 'No payment method found in setup intent'
}), 400
# Attach payment method to customer (if not already attached)
attach_result = processor.attach_payment_method(
payment_method['id'],
stripe_customer_id
)
print(f"attach_result: {attach_result}")
if not attach_result['success']:
return jsonify({
'success': False,
'error': f'Failed to attach payment method: {attach_result.get("error")}'
}), 500
# Set as default if requested
if set_as_default:
default_result = processor.set_default_payment_method(
stripe_customer_id,
payment_method['id']
)
if not default_result['success']:
# Log warning but don't fail the request
log_activity(
user_id=current_user.id,
action="set_default_payment_method_failed",
entity_type="payment_method",
entity_id=None,
details=f"Failed to set as default: {default_result.get('error')}",
ip_address=request.remote_addr
)
# Log successful addition
log_activity(
user_id=current_user.id,
action="add_payment_method",
entity_type="payment_method",
entity_id=payment_method['id'],
details=f"Added {payment_method['type']} payment method for customer {stripe_customer_id} (Splynx ID: {splynx_id}). Set as default: {set_as_default}",
ip_address=request.remote_addr
)
return jsonify({
'success': True,
'payment_method': payment_method,
'is_default': set_as_default,
'setup_intent_id': setup_intent_id,
'customer_id': stripe_customer_id
})
#except Exception as e:
# log_activity(
# user_id=current_user.id,
# action="finalize_payment_method_error",
# entity_type="payment_method",
# details=f"Payment method finalization error: {str(e)}",
# ip_address=request.remote_addr
# )
# return jsonify({
# 'success': False,
# 'error': f'Payment method finalization failed: {str(e)}'
# }), 500
@main_bp.route('/api/get-payment-methods', methods=['POST'])
@login_required
def get_payment_methods_api():
"""
Get payment methods for a Stripe customer.
"""
try:
data = request.get_json()
stripe_customer_id = data.get('stripe_customer_id')
if not stripe_customer_id:
return jsonify({
'success': False,
'error': 'stripe_customer_id is required'
}), 400
# Initialize Stripe processor
config = Config()
processor = StripePaymentProcessor(api_key=config.STRIPE_SECRET_KEY)
# Get payment methods
payment_methods = processor.get_payment_methods(stripe_customer_id)
log_activity(
user_id=current_user.id,
action="get_payment_methods",
entity_type="payment_method",
details=f"Retrieved {len(payment_methods)} payment methods for customer {stripe_customer_id}",
ip_address=request.remote_addr
)
return jsonify(payment_methods)
except Exception as e:
log_activity(
user_id=current_user.id,
action="get_payment_methods_error",
entity_type="payment_method",
details=f"Get payment methods error: {str(e)}",
ip_address=request.remote_addr
)
return jsonify({
'success': False,
'error': f'Failed to get payment methods: {str(e)}'
}), 500
@main_bp.route('/test')
@login_required
def test():
payment_data = {
'payment_id': 111,
'splynx_id': 31,
'amount': 11.11,
'error': 'payment_record.Error',
'payment_method': 'payment_method',
'customer_name': 'Alan',
'payment_type': 'single',
'stripe_customer_id': 'cus_31',
'payment_intent': 'pi_'
}
# Send notification and create ticket (only in live mode)
#if Config.PROCESS_LIVE:
# Send email notification
#email_sent = notification_service.send_payment_failure_notification(payment_data)
# Create Splynx ticket
ticket_result = splynx.create_ticket(
customer_id=31,
subject=f"Single Payment Failure - Customer 31 - $11.11",
message=f"""
Single payment processing has failed for customer Alan (ID: 31).
Payment Details:
- Payment ID: 12345 (single payment)
- Amount: $11.11 AUD
- Payment Method: pm_
- Stripe Customer: cus_31
- Payment Intent: pi_
- Processed by: Me
Error Information:
Some error
This ticket was automatically created by the Plutus Payment System.
""",
priority="medium"
)
print(f"Ticket created: {ticket_result.get('success', False)}")
return ticket_result