"""
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()