from flask import Blueprint, render_template, request, jsonify, flash, redirect, url_for from flask_login import login_required, current_user from sqlalchemy import func, case import json import pymysql from app import db 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): """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") 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" } for pay in result: res = splynx.put(url=f"/api/2.0/admin/finance/invoices/{pay['id']}", params=invoice_pay) return res def find_set_pending_splynx_invoices(splynx_id): """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 result: res = splynx.put(url=f"/api/2.0/admin/finance/invoices/{invoice['id']}", params=invoice_pending) if res: updated_invoices.append(res) return updated_invoices def add_payment_splynx(splynx_id, pi_id, pay_id, amount): """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}" } 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: flash('Payment not found.', '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}" ) return render_template('main/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('/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') # 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 ) 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: find_set_pending_splynx_invoices(splynx_id) 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'): 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: # Mark invoices as paid in Splynx find_pay_splynx_invoices(splynx_id) # 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 ) if splynx_payment_id: print(f"✅ Splynx payment record created: {splynx_payment_id}") 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