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 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 splynx = Splynx(url=SPLYNX_URL, key=SPLYNX_KEY, secret=SPLYNX_SECRET) 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") 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 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_stripe_customer_id(splynx_id): """Get Stripe customer ID from MySQL for a given Splynx customer ID.""" connection = None try: # Connect to MySQL database 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() if result and result['stripe_customer_id']: return result['stripe_customer_id'] else: 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" 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.route('/') @login_required def index(): return render_template('main/index.html') @main_bp.route('/batches') @login_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.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/') @login_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') @login_required def single_payment(): """Display single payment form page.""" return render_template('main/single_payment.html') @main_bp.route('/single-payments') @login_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, 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.Error, SinglePayments.Success, 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 single payment.""" # Get payment information payment = db.session.query( Payments.id, Payments.Splynx_ID, Payments.Stripe_Customer_ID, Payments.Payment_Intent, Payments.PI_FollowUp, Payments.PI_Last_Check, Payments.Payment_Method, Payments.Fee_Tax, Payments.Fee_Stripe, Payments.Fee_Total, Payments.Payment_Amount, Payments.PI_JSON, Payments.PI_FollowUp_JSON, Payments.Error, Payments.Success, Payments.Created)\ .filter(Payments.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('/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']) @login_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') # Validate inputs if not splynx_id or not amount: 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 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}" ) # 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']}" if result.get('needs_fee_update'): payment_record.PI_FollowUp = True 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') @login_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') @login_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']) @login_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/') @login_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']) @login_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']) @login_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-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('/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