|
|
@ -9,9 +9,141 @@ from splynx import Splynx, SPLYNX_URL, SPLYNX_KEY, SPLYNX_SECRET |
|
|
from stripe_payment_processor import StripePaymentProcessor |
|
|
from stripe_payment_processor import StripePaymentProcessor |
|
|
from config import Config |
|
|
from config import Config |
|
|
from services import log_activity |
|
|
from services import log_activity |
|
|
|
|
|
import re |
|
|
|
|
|
|
|
|
splynx = Splynx(url=SPLYNX_URL, key=SPLYNX_KEY, secret=SPLYNX_SECRET) |
|
|
splynx = Splynx(url=SPLYNX_URL, key=SPLYNX_KEY, secret=SPLYNX_SECRET) |
|
|
|
|
|
|
|
|
|
|
|
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 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): |
|
|
def processPaymentResult(pay_id, result, key): |
|
|
"""Process payment result and update database record.""" |
|
|
"""Process payment result and update database record.""" |
|
|
from datetime import datetime |
|
|
from datetime import datetime |
|
|
@ -185,6 +317,11 @@ def currency_filter(value): |
|
|
except (ValueError, TypeError): |
|
|
except (ValueError, TypeError): |
|
|
return '$0.00' |
|
|
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('/') |
|
|
@main_bp.route('/') |
|
|
@login_required |
|
|
@login_required |
|
|
def index(): |
|
|
def index(): |
|
|
@ -401,9 +538,10 @@ def process_single_payment(): |
|
|
# Get form data |
|
|
# Get form data |
|
|
splynx_id = request.form.get('splynx_id') |
|
|
splynx_id = request.form.get('splynx_id') |
|
|
amount = request.form.get('amount') |
|
|
amount = request.form.get('amount') |
|
|
|
|
|
payment_method = request.form.get('payment_method') |
|
|
|
|
|
|
|
|
# Validate inputs |
|
|
# Validate inputs |
|
|
if not splynx_id or not amount: |
|
|
if not splynx_id or not amount or not payment_method: |
|
|
return jsonify({'success': False, 'error': 'Missing required fields'}), 400 |
|
|
return jsonify({'success': False, 'error': 'Missing required fields'}), 400 |
|
|
|
|
|
|
|
|
try: |
|
|
try: |
|
|
@ -449,12 +587,13 @@ def process_single_payment(): |
|
|
|
|
|
|
|
|
processor = StripePaymentProcessor(api_key=api_key, enable_logging=True) |
|
|
processor = StripePaymentProcessor(api_key=api_key, enable_logging=True) |
|
|
print(f"stripe_customer_id: {stripe_customer_id}") |
|
|
print(f"stripe_customer_id: {stripe_customer_id}") |
|
|
# Process payment |
|
|
# Process payment with specified payment method |
|
|
result = processor.process_payment( |
|
|
result = processor.process_payment( |
|
|
customer_id=stripe_customer_id, |
|
|
customer_id=stripe_customer_id, |
|
|
amount=amount, |
|
|
amount=amount, |
|
|
currency="aud", |
|
|
currency="aud", |
|
|
description=f"Single Payment - Splynx ID: {splynx_id} - Payment ID: {payment_record.id}" |
|
|
description=f"Single Payment - Splynx ID: {splynx_id} - Payment ID: {payment_record.id}", |
|
|
|
|
|
stripe_pm=payment_method |
|
|
) |
|
|
) |
|
|
|
|
|
|
|
|
# Update payment record with results |
|
|
# Update payment record with results |
|
|
@ -821,6 +960,20 @@ def payment_plans_detail(plan_id): |
|
|
plan=plan, |
|
|
plan=plan, |
|
|
associated_payments=associated_payments) |
|
|
associated_payments=associated_payments) |
|
|
|
|
|
|
|
|
|
|
|
@main_bp.route('/api/stripe-customer-id/<int:splynx_id>') |
|
|
|
|
|
@login_required |
|
|
|
|
|
def api_stripe_customer_id(splynx_id): |
|
|
|
|
|
"""Get Stripe customer ID for a Splynx customer.""" |
|
|
|
|
|
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/<stripe_customer_id>') |
|
|
@main_bp.route('/api/stripe-payment-methods/<stripe_customer_id>') |
|
|
@login_required |
|
|
@login_required |
|
|
def api_stripe_payment_methods(stripe_customer_id): |
|
|
def api_stripe_payment_methods(stripe_customer_id): |
|
|
|