""" Payment Service Module This module contains shared payment processing functions used by both query_mysql.py and the Flask application. These functions handle: - Payment result processing - Splynx invoice status updates - Payment record creation - Customer-friendly error messages """ import json import logging from datetime import datetime from typing import List, Dict, Any, Union logger = logging.getLogger(__name__) 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 """ print("\n\ncreate_customer_friendly_message\n\n") try: # Import classify_payment_error from main.py from blueprints.main import classify_payment_error # 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') cust_stripe_details = payment_data.get('cust_stripe_details') payment_method_type = "unknown" last4 = "****" #cust_stripe_details = self.stripe_processor.get_customer_info(customer_id=payment_data.get('stripe_customer_id')) print(f"\npayment_data:\n{json.dumps(payment_data,indent=2)}\n\n") #if pi_json: # try: # 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 if cust_stripe_details: try: if cust_stripe_details['payment_methods'][0]['type'] == "card": last4 = cust_stripe_details['payment_methods'][0]['card']['last4'] payment_method_type = "card" elif cust_stripe_details['payment_methods'][0]['type'] == "au_becs_debit": last4 = cust_stripe_details['payment_methods'][0]['au_becs_debit']['last4'] payment_method_type = "au_becs_debit" 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 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 logger.error(f"Error creating customer-friendly message: {e}") return f"""
Your payment attempt was unsuccessful. Please contact us for assistance.
""" def find_pay_splynx_invoices(splynx, splynx_id: int, splynx_pay_id: int, invoice_list: List) -> List[Dict[str, Any]]: """ Mark Splynx invoices as paid for a given customer. Args: splynx: Splynx API client instance splynx_id: Customer ID in Splynx Returns: List of updated invoice dictionaries """ remove_first_invoice = invoice_list.pop(0) #result = splynx.get( # url=f"/api/2.0/admin/finance/invoices?main_attributes[customer_id]={splynx_id}&main_attributes[status]=not_paid&main_attributes[status]=pending" #) invoice_pay = { "status": "paid", "payment_id": splynx_pay_id, "date_payment": datetime.now().strftime("%Y-%m-%d") } updated_invoices = [] #for pay in result: # res = splynx.put(url=f"/api/2.0/admin/finance/invoices/{pay['id']}", params=invoice_pay) # if res: # updated_invoices.append(res) for invoice in invoice_list: res = splynx.put(url=f"/api/2.0/admin/finance/invoices/{invoice}", params=invoice_pay) if res: updated_invoices.append(res) return updated_invoices def find_set_pending_splynx_invoices(splynx, splynx_id: int, invoice_list: List) -> List[Dict[str, Any]]: """ Mark Splynx invoices as pending for a given customer. Args: splynx: Splynx API client instance splynx_id: Customer ID in Splynx Returns: List of updated invoice dictionaries """ #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": "pending"} updated_invoices = [] #for pay in result: # res = splynx.put(url=f"/api/2.0/admin/finance/invoices/{pay['id']}", params=invoice_pay) # if res: # updated_invoices.append(res) for invoice in invoice_list: res = splynx.put(url=f"/api/2.0/admin/finance/invoices/{invoice}", params=invoice_pay) if res: updated_invoices.append(res) return updated_invoices def find_set_pending_splynx_invoices_to_unpaid(splynx, splynx_id: int) -> List[Dict[str, Any]]: """ Revert pending Splynx invoices back to unpaid status for a given customer. Args: splynx: Splynx API client instance splynx_id: Customer ID in Splynx Returns: List of updated invoice dictionaries """ result = splynx.get( url=f"/api/2.0/admin/finance/invoices?main_attributes[customer_id]={splynx_id}&main_attributes[status]=pending" ) invoice_pay = {"status": "not_paid"} updated_invoices = [] for pay in result: res = splynx.put(url=f"/api/2.0/admin/finance/invoices/{pay['id']}", params=invoice_pay) if res: updated_invoices.append(res) return updated_invoices def delete_splynx_invoices(splynx, splynx_id: int, payintent: str) -> Dict[str, Any]: """ Delete Splynx payment records for a given customer and payment intent. Args: splynx: Splynx API client instance splynx_id: Customer ID in Splynx payintent: Stripe payment intent ID Returns: Dictionary with success status and details """ try: params = { 'main_attributes': { 'customer_id': splynx_id, 'field_1': payintent }, } query_string = splynx.build_splynx_query_params(params) result = splynx.get(url=f"/api/2.0/admin/finance/payments?{query_string}") if not result: logger.warning(f"No Splynx payment found for customer {splynx_id}, payment intent {payintent}") return {'success': False, 'error': 'No payment found to delete'} logger.info(f"Found {len(result)} Splynx payment(s) to delete for customer {splynx_id}") delete_success = splynx.delete(url=f"/api/2.0/admin/finance/payments/{result[0]['id']}") if delete_success: logger.info(f"Successfully deleted Splynx Payment ID: {result[0]['id']} for customer: {splynx_id}") return { 'success': True, 'deleted_payment_id': result[0]['id'], 'customer_id': splynx_id, 'payment_intent': payintent } else: logger.error(f"Failed to delete Splynx Payment ID: {result[0]['id']} for customer: {splynx_id}") return {'success': False, 'error': 'Delete operation failed'} except Exception as e: logger.error(f"Error deleting Splynx payment for customer {splynx_id}: {e}") return {'success': False, 'error': str(e)} def add_payment_splynx(splynx, splynx_id: int, pi_id: str, pay_id: int, amount: float, invoice: int) -> Union[int, bool]: """ Add a payment record to Splynx. Args: splynx: Splynx API client instance splynx_id: Customer ID in Splynx pi_id: Stripe payment intent ID pay_id: Internal payment ID amount: Payment amount Returns: Splynx payment ID if successful, False otherwise """ stripe_pay = { "customer_id": splynx_id, "amount": amount, "date": str(datetime.now().strftime('%Y-%m-%d')), "field_1": pi_id, "field_2": f"Payment_ID (Batch): {pay_id}", "invoice_id": invoice } res = splynx.post(url="/api/2.0/admin/finance/payments", params=stripe_pay) if res: return res['id'] else: return False def processPaymentResult(db, splynx, notification_handler, pay_id: int, result: dict, key: str, cust_stripe_details: dict, mode: str, process_live: bool = False): """ Process payment result and update database record. This function is shared between query_mysql.py and the Flask application. It handles both successful and failed payments, updates the database, and triggers notifications for failures. Args: db: Database session/connection splynx: Splynx API client instance notification_handler: Function to handle failed payment notifications pay_id: Payment record ID result: Payment processing result dictionary from StripePaymentProcessor key: "pay" for Payments table, "singlepay" for SinglePayments table process_live: Whether to update live Splynx records Returns: None (updates database in place) """ from models import Payments, SinglePayments # Fetch payment record print(f"\n\nPayment Service - processPaymentResult - Mode: {mode}") if key == "pay": payment = db.query(Payments).filter(Payments.id == pay_id).first() elif key == "singlepay": payment = db.query(SinglePayments).filter(SinglePayments.id == pay_id).first() else: logger.error(f"Invalid key '{key}' provided to processPaymentResult") return if not payment: logger.error(f"Payment record {pay_id} not found for key '{key}'") return try: # Handle errors if result.get('error') and not result.get('needs_fee_update'): print("\n\tPayment Error!\n") #print(f"result: {json.dumps(result,indent=2)}") payment.Error = f"Error Type: {result['error_type']}\nError: {result['error']}" payment.Success = result['success'] payment.Payment_Intent = result['payment_intent_id'] payment.PI_JSON = json.dumps(result) # Update payment method 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'] # Send notification and create ticket for failed payments #print(f"\n\nNotification Handler: {notification_handler}\n\n") if notification_handler: print("\n\tNotification time!\n") notification_handler( payment_record=payment, error_details=payment.Error, payment_type=key, cust_stripe_details=cust_stripe_details ) elif result.get('failure_details'): payment.Error = f"Error Type: {result.get('failure_details').get('decline_code')}\nError: {result['failure_reason']}" payment.Success = result['success'] payment.Payment_Intent = result['payment_intent_id'] payment.PI_JSON = json.dumps(result) # Update payment method 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'] # Send notification and create ticket for failed payments if notification_handler: print("\n\tNotification time!\n") notification_handler( payment_record=payment, error_details=payment.Error, payment_type=key, cust_stripe_details=cust_stripe_details ) else: # Handle successful or pending payments logger.info("Payment successful!") invoice_list = payment.Invoices_to_Pay.split(",") if payment.Invoices_to_Pay else [0] if result.get('needs_fee_update'): payment.PI_FollowUp = True # Mark invoices as pending when PI_FollowUp is set #if process_live: if invoice_list[0] != 0 and mode != 'payintent': find_set_pending_splynx_invoices(splynx, payment.Splynx_ID, invoice_list) payment.Payment_Intent = result['payment_intent_id'] payment.Success = result['success'] if result['success']: #if result['success'] and process_live: splynx_pay_id = add_payment_splynx( splynx=splynx, splynx_id=payment.Splynx_ID, pi_id=result['payment_intent_id'], pay_id=payment.id, amount=payment.Payment_Amount, invoice=invoice_list[0] ) if len(invoice_list) > 1: find_pay_splynx_invoices(splynx, payment.Splynx_ID, splynx_pay_id, invoice_list) # Update payment method 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'] # Update PI_JSON if payment.PI_JSON: combined = {**json.loads(payment.PI_JSON), **result} payment.PI_JSON = json.dumps(combined) else: payment.PI_JSON = json.dumps(result) # Update fee details 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'] # Commit changes db.commit() logger.info(f"Successfully processed payment result for payment {pay_id}") except Exception as e: db.rollback() logger.error(f"Error in processPaymentResult for payment {pay_id}: {e}") # Set PI_FollowUp flag on error to retry later try: payment.PI_FollowUp = True #if process_live: #find_set_pending_splynx_invoices(splynx, payment.Splynx_ID) find_set_pending_splynx_invoices(splynx, payment.Splynx_ID, invoice_list) db.commit() except Exception as rollback_error: logger.error(f"Failed to set PI_FollowUp flag after error: {rollback_error}") db.rollback()