Browse Source

major rewrite

master
Alan Woodman 3 weeks ago
parent
commit
60fe478d73
  1. 22
      .gitignore
  2. 392
      LOGGING.md
  3. 0
      archive/delete_splynx_payments.py
  4. 0
      archive/payments_fixup.py
  5. 113
      archive/payments_fixup_find_PIs.py
  6. 0
      archive/payments_fixup_find_customers.py
  7. 0
      archive/payments_fixup_find_customers_v2.py
  8. 95
      archive/pending_fixup.py
  9. 0
      archive/query_mysql - Copy.py
  10. 437
      archive/query_mysql.py
  11. 1095
      archive/query_mysql.py.backup
  12. 95
      archive/set_pending.py
  13. 0
      archive/test_logging.py
  14. 9
      cli/__init__.py
  15. 178
      cli/payment_cli.py
  16. 8
      config.py
  17. 2
      models.py
  18. 10
      orchestration/__init__.py
  19. 261
      orchestration/payment_orchestrator.py
  20. 18
      payment_processors/__init__.py
  21. 198
      payment_processors/base_processor.py
  22. 306
      payment_processors/batch_processor.py
  23. 310
      payment_processors/followup_processor.py
  24. 137
      payment_processors/notification_handler.py
  25. 292
      payment_processors/payment_plan_processor.py
  26. 26
      payment_services/__init__.py
  27. 141
      payment_services/customer_service.py
  28. 452
      payment_services/payment_service.py
  29. 52
      pending_fixup.py
  30. 532
      query_mysql - Copy.py
  31. 105
      query_mysql_new.py
  32. 14
      repositories/__init__.py
  33. 482
      repositories/payment_repository.py
  34. 314
      repositories/splynx_repository.py
  35. 84
      stripe_payment_processor.py
  36. 42
      test.py

22
.gitignore

@ -17,7 +17,6 @@ eggs/
.eggs/
lib/
lib64/
lib64
parts/
sdist/
var/
@ -115,12 +114,6 @@ ENV/
env.bak/
venv.bak/
# Virtual environment directories (if created in project root)
bin/
include/
pyvenv.cfg
*.log
# Spyder project settings
.spyderproject
.spyproject
@ -145,4 +138,17 @@ dmypy.json
# Cython debug symbols
cython_debug/
*.csv
venv
__pycache__
Include
Lib
Scripts
pyvenv.cfg
pyodbc.pyi
payment_processing.log
*.md
!README.md

392
LOGGING.md

@ -1,392 +0,0 @@
# Plutus Payment System - Logging Best Practices
## Overview
This document outlines the enhanced logging system implemented in the Plutus Payment Processing System. The logging infrastructure provides comprehensive monitoring, security event tracking, performance analysis, and automated log management.
## Logging Architecture
### Core Components
1. **Enhanced Logging Configuration** (`logging_config.py`)
- Structured logging with correlation IDs
- Multiple specialized logger types
- Automatic log formatting and rotation
2. **Middleware System** (`middleware.py`)
- Request/response logging
- Performance monitoring
- Security event detection
- Database query tracking
3. **Analytics Dashboard** (`blueprints/analytics.py`)
- Real-time system health monitoring
- Performance metrics visualization
- Security event analysis
- Log search and filtering
4. **Log Retention System** (`log_retention.py`)
- Automated cleanup and archiving
- Configurable retention policies
- Disk space management
## Logger Types
### StructuredLogger
General-purpose logger with correlation ID support and structured data.
```python
from logging_config import get_logger
logger = get_logger('module_name')
logger.info("Payment processed successfully",
payment_id=12345,
amount=89.95,
customer_id="cus_123")
```
### SecurityLogger
Specialized logger for security events and threats.
```python
from logging_config import security_logger
security_logger.log_login_attempt("username", success=False, ip_address="192.168.1.1")
security_logger.log_payment_fraud_alert(payment_id=123, customer_id="cus_456",
reason="Unusual amount pattern", amount=5000.0)
```
### PerformanceLogger
Dedicated logger for performance monitoring and optimization.
```python
from logging_config import performance_logger
performance_logger.log_request_time("POST /payments", "POST", 1250.5, 200, user_id=1)
performance_logger.log_stripe_api_call("create_payment", 850.2, True)
```
## Log Files Structure
### File Organization
```
logs/
├── plutus_detailed.log # Comprehensive application logs
├── performance.log # Performance metrics and slow operations
├── security.log # Security events and threats
├── payment_processing.log # Payment-specific operations
├── archive/ # Archived logs by month
│ ├── 202409/
│ └── 202410/
└── *.log.gz # Compressed rotated logs
```
### Log Formats
#### Standard Format
```
2024-09-02 14:30:15,123 - [corr-abc123] - plutus.payments - INFO - Payment processed successfully {"payment_id": 12345, "amount": 89.95}
```
#### Security Format
```
2024-09-02 14:30:15,123 - SECURITY - [corr-abc123] - WARNING - LOGIN_FAILED for user: testuser {"ip_address": "192.168.1.1", "user_agent": "Mozilla/5.0..."}
```
#### Performance Format
```
2024-09-02 14:30:15,123 - PERF - [corr-abc123] - REQUEST: POST /payments - 1250.50ms - 200 {"user_id": 1, "endpoint": "/payments"}
```
## Correlation IDs
### Purpose
Correlation IDs track requests across the entire system, making it easy to trace a single operation through multiple components.
### Usage
```python
from logging_config import log_context, set_correlation_id
# Automatic correlation ID
with log_context():
logger.info("Processing payment") # Will include auto-generated correlation ID
# Custom correlation ID
with log_context("req-12345"):
logger.info("Processing payment") # Will include "req-12345"
# Manual setting
correlation_id = set_correlation_id("custom-id")
logger.info("Payment processed")
```
## Performance Monitoring
### Automatic Monitoring
The system automatically tracks:
- HTTP request response times
- Database query performance
- Stripe API call latencies
- Slow operations (>1 second requests, >100ms queries)
### Manual Performance Logging
```python
from logging_config import log_performance
@log_performance("payment_processing")
def process_payment(payment_data):
# Function implementation
pass
# Or manually
start_time = time.time()
result = some_operation()
duration_ms = (time.time() - start_time) * 1000
performance_logger.log_request_time("operation_name", "GET", duration_ms, 200)
```
## Security Event Monitoring
### Automatic Detection
The middleware automatically detects and logs:
- SQL injection attempts
- Cross-site scripting (XSS) attempts
- Failed authentication attempts
- Suspicious user agents
- Access to admin endpoints
- Brute force attack patterns
### Manual Security Logging
```python
from logging_config import security_logger
# Log permission violations
security_logger.log_permission_denied("username", "delete_payment", "payment/123", "192.168.1.1")
# Log fraud alerts
security_logger.log_payment_fraud_alert(payment_id=123, customer_id="cus_456",
reason="Multiple failed attempts", amount=1000.0)
```
## Log Retention and Management
### Retention Policies
Default retention periods:
- Application logs: 30 days
- Performance logs: 14 days
- Security logs: 90 days
- Payment processing logs: 60 days
### Automated Cleanup
- Runs daily at 2:00 AM
- Compresses logs older than configured threshold
- Archives important logs before deletion
- Monitors disk space usage
### Manual Management
```python
from log_retention import retention_manager
# Get statistics
stats = retention_manager.get_log_statistics()
# Manual cleanup
cleanup_stats = retention_manager.cleanup_logs()
# Emergency cleanup (when disk space is low)
emergency_stats = retention_manager.emergency_cleanup(target_size_mb=500)
```
## Analytics Dashboard
### Access
Navigate to `/analytics/dashboard` (requires Finance+ permissions)
### Features
- **System Health**: Real-time health score and key metrics
- **Performance Monitoring**: Response times, slow requests, database performance
- **Payment Analytics**: Success rates, error analysis, trends
- **Security Events**: Failed logins, suspicious activity, fraud alerts
- **Log Search**: Full-text search with filtering and pagination
### API Endpoints
- `GET /analytics/api/system-health` - Current system health metrics
- `GET /analytics/api/performance-metrics` - Performance analysis data
- `GET /analytics/api/payment-analytics` - Payment processing statistics
- `GET /analytics/api/security-events` - Security event summary
- `GET /analytics/api/logs/search` - Search system logs
## Best Practices
### For Developers
1. **Use Structured Logging**
```python
# Good
logger.info("Payment processed", payment_id=123, amount=89.95, status="success")
# Avoid
logger.info(f"Payment {payment_id} processed for ${amount} - status: {status}")
```
2. **Include Context**
```python
# Include relevant context in all log messages
logger.info("Payment failed",
payment_id=payment.id,
customer_id=payment.customer_id,
error_code=error.code,
error_message=str(error))
```
3. **Use Appropriate Log Levels**
- `DEBUG`: Detailed diagnostic information
- `INFO`: General information about system operation
- `WARNING`: Something unexpected happened but system continues
- `ERROR`: Serious problem that prevented function completion
- `CRITICAL`: Very serious error that may abort the program
4. **Security-Sensitive Data**
```python
# Never log sensitive data
logger.info("Payment processed",
payment_id=123,
amount=89.95,
card_last4="1234") # OK - only last 4 digits
# Avoid logging full card numbers, CVV, passwords, etc.
```
### For Operations
1. **Monitor Key Metrics**
- System health score (target: >90%)
- Payment success rate (target: >95%)
- Error rate (target: <5%)
- Average response time (target: <1000ms)
2. **Set Up Alerts**
- Health score drops below 75%
- Payment success rate drops below 90%
- Multiple security events in short timeframe
- Disk space usage exceeds 80%
3. **Regular Review**
- Weekly review of security events
- Monthly analysis of performance trends
- Quarterly review of retention policies
- Annual security audit of logged events
### For Security
1. **Monitor for Patterns**
- Multiple failed logins from same IP
- Unusual payment amounts or frequencies
- Access attempts to admin endpoints
- SQL injection or XSS attempts
2. **Incident Response**
- Use correlation IDs to trace incident across systems
- Export relevant logs for forensic analysis
- Coordinate with development team using structured log data
## Configuration
### Environment Variables
```bash
# Optional: Override default log retention
LOG_RETENTION_DAYS=30
LOG_CLEANUP_TIME=02:00
LOG_MAX_FILE_SIZE_MB=100
LOG_ARCHIVE_COMPRESS=true
```
### Programmatic Configuration
```python
# Custom retention configuration
custom_config = {
'retention_policies': {
'security.log': {'days': 180, 'compress_after_days': 7},
'performance.log': {'days': 7, 'compress_after_days': 1},
'default': {'days': 30, 'compress_after_days': 7}
},
'cleanup_schedule': '03:00',
'max_file_size_mb': 50
}
retention_manager = LogRetentionManager(custom_config)
```
## Troubleshooting
### Common Issues
1. **Logs Not Appearing**
- Check logs directory permissions
- Verify logger configuration in app initialization
- Check disk space availability
2. **High Disk Usage**
- Run manual cleanup: `python log_retention.py`
- Reduce retention periods for non-critical logs
- Enable compression for all log types
3. **Performance Impact**
- Disable DEBUG level logging in production
- Reduce log verbosity for high-frequency operations
- Use async logging for high-throughput scenarios
4. **Missing Correlation IDs**
- Ensure middleware is properly initialized
- Check that log context is being used in threaded operations
- Verify correlation ID propagation in external API calls
### Log Analysis Commands
```bash
# Search for specific payment
grep "payment_id.*12345" logs/plutus_detailed.log
# Find all errors in last hour
grep "$(date -d '1 hour ago' '+%Y-%m-%d %H')" logs/plutus_detailed.log | grep ERROR
# Count security events by type
grep "SECURITY" logs/security.log | cut -d'-' -f5 | sort | uniq -c
# Monitor real-time logs
tail -f logs/plutus_detailed.log
# Analyze correlation ID flow
grep "corr-abc123" logs/*.log | sort
```
## Support and Maintenance
### Log File Monitoring
Set up monitoring for:
- Log file growth rates
- Error frequency patterns
- Security event trends
- System performance degradation
### Regular Maintenance
- Weekly: Review disk space and cleanup if needed
- Monthly: Analyze performance trends and optimize slow queries
- Quarterly: Review retention policies and adjust as needed
- Annually: Audit security events and update detection rules
### Contact Information
For logging system issues or questions:
- Development Team: Review code in `logging_config.py`, `middleware.py`
- Operations Team: Monitor analytics dashboard and system health
- Security Team: Review security logs and event patterns
## Version History
- **v1.0** (Phase 8): Initial enhanced logging implementation
- **v1.1** (Phase 9): Analytics dashboard and retention system
- **v1.2**: Correlation ID improvements and performance optimization
---
This logging system provides comprehensive visibility into the Plutus Payment System while maintaining security, performance, and operational efficiency. Regular review and maintenance of the logging infrastructure ensures continued reliability and usefulness for system monitoring and troubleshooting.

0
delete_splynx_payments.py → archive/delete_splynx_payments.py

0
payments_fixup.py → archive/payments_fixup.py

113
archive/payments_fixup_find_PIs.py

@ -0,0 +1,113 @@
import pymysql
import sys
import json
import random
import threading
import logging
import stripe
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime
from typing import List, Dict, Union, Any
from stripe_payment_processor import StripePaymentProcessor
from config import Config
from app import create_app, db
from models import Payments, PaymentBatch, SinglePayments, PaymentPlans
from splynx import Splynx, SPLYNX_URL, SPLYNX_KEY, SPLYNX_SECRET
from services import (
log_script_start, log_script_completion, log_batch_created,
log_payment_intent_followup
)
from notification_service import NotificationService
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('payment_processing.log'),
logging.StreamHandler(sys.stdout)
]
)
logger = logging.getLogger(__name__)
# Initialize Splynx API
splynx = Splynx(url=SPLYNX_URL, key=SPLYNX_KEY, secret=SPLYNX_SECRET)
# Import constants from config
PAYMENT_METHOD_DIRECT_DEBIT = Config.PAYMENT_METHOD_DIRECT_DEBIT
PAYMENT_METHOD_CARD = Config.PAYMENT_METHOD_CARD
PAYMENT_METHOD_PAYMENT_PLAN = Config.PAYMENT_METHOD_PAYMENT_PLAN
PROCESS_LIVE = Config.PROCESS_LIVE
# Get Stripe API key from config
if PROCESS_LIVE:
api_key = Config.STRIPE_LIVE_API_KEY
else:
api_key = Config.STRIPE_TEST_API_KEY
#test_stripe_customers = ['cus_SoQqMGLmCjiBDZ', 'cus_SoQptxwe8hczGz', 'cus_SoQjeNXkKOdORI', 'cus_SoQiDcSrNRxbPF', 'cus_SoQedaG3q2ecKG', 'cus_SoQeTkzMA7AaLR', 'cus_SoQeijBTETQcGb', 'cus_SoQe259iKMgz7o', 'cus_SoQejTstdXEDTO', 'cus_SoQeQH2ORWBOWX', 'cus_SoQevtyWxqXtpC', 'cus_SoQekOFUHugf26', 'cus_SoPq6Zh0MCUR9W', 'cus_SoPovwUPJmvugz', 'cus_SoPnvGfejhpSR5', 'cus_SoNAgAbkbFo8ZY', 'cus_SoMyDihTxRsa7U', 'cus_SoMVPWxdYstYbr', 'cus_SoMVQ6Xj2dIrCR', 'cus_SoMVmBn1xipFEB', 'cus_SoMVNvZ2Iawb7Y', 'cus_SoMVZupj6wRy5e', 'cus_SoMVqjH7zkc5Qe', 'cus_SoMVkzj0ZUK0Ai', 'cus_SoMVFq3BUD3Njw', 'cus_SoLcrRrvoy9dJ4', 'cus_SoLcqHN1k0WD8j', 'cus_SoLcLtYDZGG32V', 'cus_SoLcG23ilNeMYt', 'cus_SoLcFhtUVzqumj', 'cus_SoLcPgMnuogINl', 'cus_SoLccGTY9mMV7T', 'cus_SoLRxqvJxuKFes', 'cus_SoKs7cjdcvW1oO']
stripe.api_key = api_key
def get_latest_payment_intent(customer_id):
"""Get the most recently created Payment Intent for a customer"""
payment_intents = stripe.PaymentIntent.list(
customer=customer_id,
limit=1 # Only get the most recent one
)
if payment_intents.data:
return payment_intents.data[0]
return None
if __name__ == "__main__":
## Payment Method:
## 2 - Direct Debit (Automatic)
## 3 - Card Payment (Automatic)
## 9 - Payment Plan
### Running Mode
## batch = Monthly Direct Debit/Credit Cards
## payintent = Check outstanding Payment Intents and update
## payplan = Check for Payment Plans to run
## refund = Check outstanding Refunds and update
start_time = datetime.now()
# Create Flask application context
app = create_app()
api_key = Config.STRIPE_LIVE_API_KEY
with app.app_context():
customers = (
db.session.query(Payments)
.filter(Payments.PaymentBatch_ID.in_((108,109)))
.all()
)
for pay in customers:
#print(pay)
pi = get_latest_payment_intent(pay.Stripe_Customer_ID)
#print(json.dumps(pi,indent=2))
if str(pay.id) in pi['description']:
print("MATCH")
pm = stripe.Customer.list_payment_methods(
customer=pay.Stripe_Customer_ID
)
#print(json.dumps(pm,indent=2))
#print(f"card: {pm['data'][0].get('card').get('brand')}")
pm2 = pm['data'][0]
if hasattr(pm2, 'card'):
print(f"card: {pm['data'][0].get('card').get('brand')}")
pay.Payment_Method = pm['data'][0].get('card').get('brand')
elif hasattr(pm2, 'au_becs_debit'):
print("au_becs_debit")
pay.Payment_Method = "au_becs_debit"
pay.Payment_Intent = pi['id']
pay.Stripe_Payment_Method = pi['payment_method']
pay.PI_FollowUp = True
db.session.commit()

0
payments_fixup_find_customers.py → archive/payments_fixup_find_customers.py

0
payments_fixup_find_customers_v2.py → archive/payments_fixup_find_customers_v2.py

95
archive/pending_fixup.py

@ -0,0 +1,95 @@
import json
import stripe
from datetime import datetime
from typing import List, Dict, Union, Any
from app import create_app, db
from models import Payments, PaymentBatch, SinglePayments, PaymentPlans
from splynx import Splynx, SPLYNX_URL, SPLYNX_KEY, SPLYNX_SECRET
from services import log_activity
from config import Config
from sqlalchemy import and_
splynx = Splynx(url=SPLYNX_URL, key=SPLYNX_KEY, secret=SPLYNX_SECRET)
api_key = Config.STRIPE_LIVE_API_KEY
stripe.api_key = api_key
def find_pay_splynx_invoices(splynx_id: int, result: dict) -> List[Dict[str, Any]]:
#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"
}
for pay in result:
#res = splynx.put(url=f"/api/2.0/admin/finance/invoices/{pay['id']}", params=invoice_pay)
print(f"Invoice: {pay['id']} marked as paid - {pay['status']}")
def add_payment_splynx(splynx_id: int, pi_id: str, pay_id: int, amount: float, invoice_id: int) -> Union[int, bool]:
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_id
}
res = splynx.post(url="/api/2.0/admin/finance/payments", params=stripe_pay)
if res:
return res['id']
else:
return False
if __name__ == "__main__":
app = create_app()
i = 1
#cust_bill = splynx.get(url=f"/api/2.0/admin/customers/customer-billing/31")
#print(json.dumps(cust_bill,indent=2))
with app.app_context():
custs = db.session.query(PaymentBatch,Payments)\
.join(Payments, Payments.PaymentBatch_ID == PaymentBatch.id)\
.filter(and_(PaymentBatch.id.in_((109,109)), Payments.Success == True))\
.all()
print(len(custs))
for cust in custs:
try:
cust_bill = splynx.get(url=f"/api/2.0/admin/customers/customer-billing/{cust.Payments.Splynx_ID}")
print(f"{i}/{len(custs)}")
print(f"SplynxID: {cust.Payments.Splynx_ID} - ${float(cust_bill['deposit'])} - ${cust.Payments.Payment_Amount} - {cust.Payments.Payment_Intent}")
#print(json.dumps(cust_bill,indent=2))
if float(cust_bill['deposit']) < 0:
params = {
"main_attributes": {
"customer_id": cust.Payments.Splynx_ID,
"status": ["IN", ["pending", "not_paid"]],
"date_created": ["!=", "2026-01-05"]
}
}
#result = splynx.get(url=f"/api/2.0/admin/finance/invoices?main_attributes[customer_id]={cust.Payments.Splynx_ID}&main_attributes[status]=pending")
result = splynx.get(url=f"/api/2.0/admin/finance/invoices?{splynx.build_splynx_query_params(params)}")
#print(json.dumps(result,indent=2))
if len(result) > 0:
print(f"\t{cust.Payments.Splynx_ID} - Has unpaid invoices ({len(result)})")
for res in result:
print(f"\tInvoiceID: {res['id']} - {res['status']}")
#print(json.dumps(res,indent=2))
res = add_payment_splynx(
splynx_id=cust.Payments.Splynx_ID,
pi_id=cust.Payments.Payment_Intent,
pay_id=cust.Payments.PaymentBatch_ID,
amount=cust.Payments.Payment_Amount,
invoice_id=result[0]['id']
)
#if res:
# print("\tPayment added")
# find_pay_splynx_invoices(splynx_id=cust.Payments.Splynx_ID, result=result)
#else:
# print("\tAdding payment failed")
except:
print("Fuck")
i += 1

0
query_mysql.py → archive/query_mysql - Copy.py

437
query_mysql-bak.py → archive/query_mysql.py

@ -3,7 +3,8 @@
External script to query MySQL database (Splynx) for customer billing data.
This script runs independently of the Flask application.
Usage: python query_mysql.py
Usage: python query_mysql.py [mode] [live]
Modes: batch, payintent, payplan, refund
"""
import pymysql
@ -24,6 +25,7 @@ from services import (
log_script_start, log_script_completion, log_batch_created,
log_payment_intent_followup
)
from notification_service import NotificationService
# Configure logging
logging.basicConfig(
@ -50,11 +52,108 @@ if PROCESS_LIVE:
api_key = Config.STRIPE_LIVE_API_KEY
else:
api_key = Config.STRIPE_TEST_API_KEY
test_stripe_customers = ['cus_SoQqMGLmCjiBDZ', 'cus_SoQptxwe8hczGz', 'cus_SoQjeNXkKOdORI', 'cus_SoQiDcSrNRxbPF', 'cus_SoQedaG3q2ecKG', 'cus_SoQeTkzMA7AaLR', 'cus_SoQeijBTETQcGb', 'cus_SoQe259iKMgz7o', 'cus_SoQejTstdXEDTO', 'cus_SoQeQH2ORWBOWX', 'cus_SoQevtyWxqXtpC', 'cus_SoQekOFUHugf26', 'cus_SoPq6Zh0MCUR9W', 'cus_SoPovwUPJmvugz', 'cus_SoPnvGfejhpSR5', 'cus_SoNAgAbkbFo8ZY', 'cus_SoMyDihTxRsa7U', 'cus_SoMVPWxdYstYbr', 'cus_SoMVQ6Xj2dIrCR', 'cus_SoMVmBn1xipFEB', 'cus_SoMVNvZ2Iawb7Y', 'cus_SoMVZupj6wRy5e', 'cus_SoMVqjH7zkc5Qe', 'cus_SoMVkzj0ZUK0Ai', 'cus_SoMVFq3BUD3Njw', 'cus_SoLcrRrvoy9dJ4', 'cus_SoLcqHN1k0WD8j', 'cus_SoLcLtYDZGG32V', 'cus_SoLcG23ilNeMYt', 'cus_SoLcFhtUVzqumj', 'cus_SoLcPgMnuogINl', 'cus_SoLccGTY9mMV7T', 'cus_SoLRxqvJxuKFes', 'cus_SoKs7cjdcvW1oO']
#test_stripe_customers = ['cus_SoQqMGLmCjiBDZ', 'cus_SoQptxwe8hczGz', 'cus_SoQjeNXkKOdORI', 'cus_SoQiDcSrNRxbPF', 'cus_SoQedaG3q2ecKG', 'cus_SoQeTkzMA7AaLR', 'cus_SoQeijBTETQcGb', 'cus_SoQe259iKMgz7o', 'cus_SoQejTstdXEDTO', 'cus_SoQeQH2ORWBOWX', 'cus_SoQevtyWxqXtpC', 'cus_SoQekOFUHugf26', 'cus_SoPq6Zh0MCUR9W', 'cus_SoPovwUPJmvugz', 'cus_SoPnvGfejhpSR5', 'cus_SoNAgAbkbFo8ZY', 'cus_SoMyDihTxRsa7U', 'cus_SoMVPWxdYstYbr', 'cus_SoMVQ6Xj2dIrCR', 'cus_SoMVmBn1xipFEB', 'cus_SoMVNvZ2Iawb7Y', 'cus_SoMVZupj6wRy5e', 'cus_SoMVqjH7zkc5Qe', 'cus_SoMVkzj0ZUK0Ai', 'cus_SoMVFq3BUD3Njw', 'cus_SoLcrRrvoy9dJ4', 'cus_SoLcqHN1k0WD8j', 'cus_SoLcLtYDZGG32V', 'cus_SoLcG23ilNeMYt', 'cus_SoLcFhtUVzqumj', 'cus_SoLcPgMnuogINl', 'cus_SoLccGTY9mMV7T', 'cus_SoLRxqvJxuKFes', 'cus_SoKs7cjdcvW1oO']
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:
# 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')
payment_method_type = "unknown"
last4 = "****"
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
# 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"""
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html>
<body>
<div>Your payment attempt was unsuccessful.</div>
<div><br></div>
<div><strong>Payment Details:</strong></div>
<div> Amount: ${amount:.2f} AUD</div>
<div> Date/Time: {current_time}</div>
<div> {payment_method_display}</div>
<div><br></div>
<div><strong>Issue:</strong> {error_message}</div>
<div><br></div>
<div>Please contact us if you need assistance with your payment.</div>
</body>
</html>
"""
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"""
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html>
<body>
<div>Your payment attempt was unsuccessful. Please contact us for assistance.</div>
</body>
</html>
"""
def find_pay_splynx_invoices(splynx_id: int) -> List[Dict[str, Any]]:
result = splynx.get(url=f"/api/2.0/admin/finance/invoices?main_attributes[customer_id]={splynx_id}&main_attributes[status]=not_paid")
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"
@ -81,6 +180,56 @@ def find_set_pending_splynx_invoices(splynx_id: int) -> List[Dict[str, Any]]:
updated_invoices.append(res)
return updated_invoices
def find_set_pending_splynx_invoices_to_unpaid(splynx_id: int) -> List[Dict[str, Any]]:
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_id: int, payintent: str) -> Dict[str, Any]:
"""Delete Splynx payment records for a given customer and payment intent."""
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_id: int, pi_id: str, pay_id: int, amount: float) -> Union[int, bool]:
stripe_pay = {
"customer_id": splynx_id,
@ -255,10 +404,8 @@ def addInitialPayments(customers, batch_id):
# Prepare all payments first
for cust in customers:
if PROCESS_LIVE:
stripe_customer_id = cust['stripe_customer_id']
else:
stripe_customer_id = test_stripe_customers[random.randint(1, len(test_stripe_customers)-1)]
add_payer = Payments(
PaymentBatch_ID = batch_id,
Splynx_ID = cust['customer_id'],
@ -300,14 +447,36 @@ def processPaymentResult(pay_id, result, key):
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:
#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)
# Send notification and create ticket for failed payments
handle_failed_payment_notification(
payment_record=payment,
error_details=payment.Error,
payment_type=key
)
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.PI_JSON = json.dumps(result)
# Send notification and create ticket for failed payments
handle_failed_payment_notification(
payment_record=payment,
error_details=payment.Error,
payment_type=key
)
else:
print("Payment successful!")
if result.get('needs_fee_update'):
payment.PI_FollowUp = True
# Mark invoices as pending when PI_FollowUp is set
if PROCESS_LIVE:
find_set_pending_splynx_invoices(payment.Splynx_ID)
payment.Payment_Intent = result['payment_intent_id']
payment.Success = result['success']
if result['success'] and PROCESS_LIVE:
@ -334,11 +503,11 @@ def processPaymentResult(pay_id, result, key):
payment.Fee_Tax = fee_type['amount']
elif fee_type['type'] == "stripe_fee":
payment.Fee_Stripe = fee_type['amount']
except Exception as e:
logger.error(f"processPaymentResult: {e}\nResult: {json.dumps(result)}")
payment.PI_FollowUp = True
if PROCESS_LIVE:
find_set_pending_splynx_invoices(payment.Splynx_ID)
#except Exception as e:
# logger.error(f"processPaymentResult: {e}\nResult: {json.dumps(result)}")
# payment.PI_FollowUp = True
# if PROCESS_LIVE:
# find_set_pending_splynx_invoices(payment.Splynx_ID)
def _update_payment():
return True # Just need to trigger commit, payment is already modified
@ -564,14 +733,20 @@ def process_payintent_mode(processor):
try:
intent_result = processor.check_payment_intent(pi.Payment_Intent)
logger.debug(f"Intent result: {json.dumps(intent_result, indent=2)}")
print(f"\n\npayintent result: {json.dumps(intent_result, indent=2)}\n\n")
if intent_result['status'] == "succeeded":
pi.PI_FollowUp_JSON = json.dumps(intent_result)
pi.PI_FollowUp = False
pi.PI_Last_Check = datetime.now()
pi.Success = True
# Mark invoices as paid when payment intent succeeds
#if PROCESS_LIVE:
# find_pay_splynx_invoices(pi.Splynx_ID)
#if intent_result.get('charge_id').startswith('ch_'):
# pi.Stripe_Charge_ID = intent_result.get('charge_id')
print("\nProcess payment results coz it succeeded")
processPaymentResult(pay_id=pi.id, result=intent_result, key=key)
succeeded_count += 1
elif intent_result['status'] == "failed":
@ -605,6 +780,231 @@ def process_payintent_mode(processor):
return succeeded_count, failed_count
def process_refund_followup_mode(processor):
"""Handle refund follow-up processing for pending refunds."""
to_check = {
"pay": db.session.query(Payments).filter(Payments.Refund_FollowUp == True).all(),
"singlepay": db.session.query(SinglePayments).filter(SinglePayments.Refund_FollowUp == True).all(),
}
total_pending = 0
completed_count = 0
failed_count = 0
still_pending = 0
for key, value in to_check.items():
logger.debug(f"Processing refund follow-up for {len(value)} {key} items")
total_pending += len(value)
for refund_record in value:
try:
if not refund_record.Stripe_Refund_ID:
logger.error(f"No Stripe refund ID found for {key} record {refund_record.id}")
failed_count += 1
continue
print(f"refund_record.Stripe_Refund_ID: {refund_record.Stripe_Refund_ID}")
refund_result = processor.check_refund_status(refund_record.Stripe_Refund_ID)
logger.debug(f"Refund result: {json.dumps(refund_result, indent=2)}")
# Check if the API call was successful
if not refund_result.get('success', False):
logger.error(f"Failed to check refund status: {refund_result.get('error', 'Unknown error')}")
failed_count += 1
continue
if refund_result['status'] == "succeeded":
# Refund completed successfully
refund_record.Refund = True
refund_record.Refund_FollowUp = False
refund_record.Refund_JSON = json.dumps(refund_result)
# Delete associated Splynx payment record if in live mode
if PROCESS_LIVE and refund_record.Payment_Intent:
delete_result = delete_splynx_invoices(
splynx_id=refund_record.Splynx_ID,
payintent=refund_record.Payment_Intent
)
if delete_result.get('success'):
logger.info(f"Deleted Splynx payment for refund completion: customer {refund_record.Splynx_ID}")
else:
logger.warning(f"Failed to delete Splynx payment: {delete_result.get('error')}")
completed_count += 1
logger.info(f"✅ Refund completed: {refund_record.Stripe_Refund_ID}")
elif refund_result['status'] in ["failed", "canceled"]:
# Refund failed
refund_record.Refund_FollowUp = False
refund_record.Refund_JSON = json.dumps(refund_result)
failed_count += 1
logger.warning(f"❌ Refund failed: {refund_record.Stripe_Refund_ID} - {refund_result['status']}")
elif refund_result['status'] == "pending":
# Still pending - update JSON but keep follow-up flag
refund_record.Refund_JSON = json.dumps(refund_result)
still_pending += 1
logger.info(f"⏳ Refund still pending: {refund_record.Stripe_Refund_ID}")
else:
# Unknown status
refund_record.Refund_JSON = json.dumps(refund_result)
still_pending += 1
logger.warning(f"⚠️ Unknown refund status: {refund_record.Stripe_Refund_ID} - {refund_result['status']}")
db.session.commit()
except Exception as e:
logger.error(f"Error processing refund {refund_record.Stripe_Refund_ID}: {e}")
failed_count += 1
# Log refund follow-up results
if total_pending > 0:
logger.info(f"Refund follow-up completed: {completed_count} completed, {failed_count} failed, {still_pending} still pending")
# Log the activity for tracking
from services import log_activity
try:
log_activity(
user_id=1, # System user
action="refund_followup",
entity_type="script",
entity_id=None,
details=f"Processed {total_pending} pending refunds: {completed_count} completed, {failed_count} failed, {still_pending} still pending"
)
except Exception as log_error:
logger.error(f"Failed to log refund follow-up activity: {log_error}")
else:
logger.info("No refunds requiring follow-up")
return completed_count, failed_count
def handle_failed_payment_notification(payment_record, error_details: str, payment_type: str = "batch"):
"""
Handle notification and ticket creation for failed payments.
Args:
payment_record: Database payment record (Payments or SinglePayments)
error_details: Error message details
payment_type: Type of payment ("batch" or "single")
"""
try:
# Initialize notification service
notification_service = NotificationService()
# Get customer information from Splynx
try:
customer_data = splynx.Customer(payment_record.Splynx_ID)
customer_name = customer_data.get('name', 'Unknown Customer') if customer_data != 'unknown' else 'Unknown Customer'
except:
customer_name = 'Unknown Customer'
# Prepare payment data for notification
payment_data = {
'payment_id': payment_record.id,
'splynx_id': payment_record.Splynx_ID,
'amount': abs(payment_record.Payment_Amount),
'error': error_details,
'payment_method': payment_record.Payment_Method or 'Unknown',
'customer_name': customer_name,
'payment_type': payment_type,
'stripe_customer_id': payment_record.Stripe_Customer_ID,
'payment_intent': payment_record.Payment_Intent
}
# Revert pending invoices back to "not_paid" (only in live mode)
if PROCESS_LIVE:
updated_invoices = find_set_pending_splynx_invoices_to_unpaid(splynx_id=payment_record.Splynx_ID)
if updated_invoices:
logger.info(f"✅ Payment failure pending invoices reverted back to not_paid Splynx ID: {payment_record.Splynx_ID} - PayID: {payment_record.id}")
else:
logger.error(f"❌ Failed to send payment failure email for payment {payment_record.id}")
# Send email notification (only in live mode)
if PROCESS_LIVE:
email_sent = notification_service.send_payment_failure_notification(payment_data)
if email_sent:
logger.info(f"✅ Payment failure email sent for payment {payment_record.id}")
else:
logger.error(f"❌ Failed to send payment failure email for payment {payment_record.id}")
# Create Splynx ticket (only in live mode)
if PROCESS_LIVE:
ticket_subject = f"Payment Failure - Customer {payment_record.Splynx_ID} - ${abs(payment_record.Payment_Amount):.2f}"
internal_message=f"""
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html>
<body>
<div>Payment processing has failed for customer {customer_name} (ID: {payment_record.Splynx_ID}).</div>
<div><br></div>
<div><strong>Payment Details:</strong></div>
<ul>
<li>Payment ID: {payment_record.id} ({payment_type}</li>
<li>Amount: ${abs(payment_record.Payment_Amount):.2f} AUD</li>
<li>Payment Method: {payment_record.Payment_Method or 'Unknown'}</li>
<li>Stripe Customer: {payment_record.Stripe_Customer_ID}</li>
<li>Payment Intent: {payment_record.Payment_Intent or 'N/A'}</li>
</ul>
<div><br></div>
<div><strong>Error Information:</strong></div>
<div>{error_details}</div>
<div><br></div>
<div>This ticket was automatically created by the Plutus Payment System.</div>
</body>
</html>
"""
# Create customer-friendly message
payment_data_for_msg = {
'amount': payment_data['amount'],
'splynx_id': payment_data['splynx_id'],
'pi_json': payment_record.PI_JSON
}
customer_message = create_customer_friendly_message(payment_data_for_msg, error_details)
ticket_result = splynx.create_ticket(
customer_id = payment_record.Splynx_ID,
subject = ticket_subject,
priority = 'medium',
type_id = 1,
group_id = 7,
status_id = 1,
)
#splynx.create_ticket(
# customer_id=payment_record.Splynx_ID,
# subject=ticket_subject,
# message=internal_message,
# priority="medium"
#)
if ticket_result.get('success'):
logger.info(f"✅ Splynx ticket created: #{ticket_result['ticket_id']} for payment {payment_record.id}")
# Optionally store ticket ID in payment record for tracking
# This would require adding a Splynx_Ticket_ID field to the models
## Adds internal note
add_message = splynx.add_ticket_message(
ticket_id=ticket_result['ticket_id'],
message=internal_message,
is_admin=False,
hide_for_customer=True,
message_type="note"
)
#result['ticket_id']
add_message = splynx.add_ticket_message(
ticket_id=ticket_result['ticket_id'],
message=customer_message,
is_admin=False,
hide_for_customer=False,
message_type="message"
)
else:
logger.error(f"❌ Failed to create Splynx ticket for payment {payment_record.id}: {ticket_result.get('error')}")
except Exception as e:
logger.error(f"Error handling failed payment notification for payment {payment_record.id}: {e}")
if __name__ == "__main__":
## Payment Method:
## 2 - Direct Debit (Automatic)
@ -615,6 +1015,7 @@ if __name__ == "__main__":
## batch = Monthly Direct Debit/Credit Cards
## payintent = Check outstanding Payment Intents and update
## payplan = Check for Payment Plans to run
## refund = Check outstanding Refunds and update
start_time = datetime.now()
success_count = 0
@ -630,9 +1031,11 @@ if __name__ == "__main__":
running_mode = "payintent"
elif sys.argv[1] == "payplan":
running_mode = "payplan"
elif sys.argv[1] == "refund":
running_mode = "refund"
else:
logger.error(f"Invalid running mode: {sys.argv[1]}")
logger.info("Valid modes: batch, payintent, payplan")
logger.info("Valid modes: batch, payintent, payplan, refund")
sys.exit(1)
try:
if sys.argv[2] == "live":
@ -645,6 +1048,10 @@ if __name__ == "__main__":
# Create Flask application context
app = create_app()
if PROCESS_LIVE:
api_key = Config.STRIPE_LIVE_API_KEY
else:
api_key = Config.STRIPE_TEST_API_KEY
print(f"api_key: {api_key}")
processor = StripePaymentProcessor(api_key=api_key, enable_logging=True)
@ -663,6 +1070,8 @@ if __name__ == "__main__":
execute_payment_batches(processor, batch_ids)
elif running_mode == "payintent":
success_count, failed_count = process_payintent_mode(processor)
elif running_mode == "refund":
success_count, failed_count = process_refund_followup_mode(processor)
except Exception as e:
logger.error(f"Script execution failed: {e}")
errors.append(str(e))

1095
archive/query_mysql.py.backup

File diff suppressed because it is too large

95
archive/set_pending.py

@ -0,0 +1,95 @@
import pymysql
import sys
import json
import random
import threading
import logging
import stripe
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime
from typing import List, Dict, Union, Any
from stripe_payment_processor import StripePaymentProcessor
from config import Config
from sqlalchemy import and_
from app import create_app, db
from models import Payments, PaymentBatch, SinglePayments, PaymentPlans
from splynx import Splynx, SPLYNX_URL, SPLYNX_KEY, SPLYNX_SECRET
from services import (
log_script_start, log_script_completion, log_batch_created,
log_payment_intent_followup
)
from notification_service import NotificationService
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('payment_processing.log'),
logging.StreamHandler(sys.stdout)
]
)
logger = logging.getLogger(__name__)
# Initialize Splynx API
splynx = Splynx(url=SPLYNX_URL, key=SPLYNX_KEY, secret=SPLYNX_SECRET)
def find_set_pending_splynx_invoices(splynx, splynx_id: int) -> 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)
return updated_invoices
if __name__ == "__main__":
## Payment Method:
## 2 - Direct Debit (Automatic)
## 3 - Card Payment (Automatic)
## 9 - Payment Plan
### Running Mode
## batch = Monthly Direct Debit/Credit Cards
## payintent = Check outstanding Payment Intents and update
## payplan = Check for Payment Plans to run
## refund = Check outstanding Refunds and update
start_time = datetime.now()
# Create Flask application context
app = create_app()
with app.app_context():
customers = (
db.session.query(Payments)
.filter(and_(Payments.PaymentBatch_ID.in_((108,109)), Payments.PI_FollowUp == True))
.all()
)
for pay in customers:
print(pay.Splynx_ID)
find_set_pending_splynx_invoices(splynx, pay.Splynx_ID)

0
test_logging.py → archive/test_logging.py

9
cli/__init__.py

@ -0,0 +1,9 @@
"""
CLI Module
This module contains command-line interface components for payment processing.
"""
from .payment_cli import main
__all__ = ['main']

178
cli/payment_cli.py

@ -0,0 +1,178 @@
#!/usr/bin/env python3
"""
Payment Processing CLI
Command-line interface for running payment processing in different modes.
Usage: python payment_cli.py [mode] [live]
Modes: batch, payintent, payplan, refund
"""
import sys
import logging
import argparse
from config import Config
from app import create_app
from orchestration import PaymentOrchestrator
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('payment_processing.log'),
logging.StreamHandler(sys.stdout)
]
)
logger = logging.getLogger(__name__)
def parse_arguments():
"""
Parse command-line arguments.
Returns:
Namespace with parsed arguments
"""
parser = argparse.ArgumentParser(
description='Payment Processing System - Process payments, payment plans, and follow-ups',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python payment_cli.py batch test # Process batch payments in test mode
python payment_cli.py batch live # Process batch payments in live mode
python payment_cli.py payintent # Check payment intents (test mode)
python payment_cli.py payplan live # Process payment plans in live mode
python payment_cli.py refund # Check refund statuses (test mode)
Modes:
batch - Process Direct Debit and Card payments in batches
payplan - Process recurring payment plans due today
payintent - Follow up on pending payment intents
refund - Follow up on pending refunds
Environment:
test - Use test Stripe API keys (default)
live - Use live Stripe API keys and update Splynx
"""
)
parser.add_argument(
'mode',
choices=['batch', 'payintent', 'payplan', 'refund'],
help='Processing mode to run'
)
parser.add_argument(
'environment',
nargs='?',
choices=['test', 'live'],
default='test',
help='Environment to run in (default: test)'
)
parser.add_argument(
'-v', '--verbose',
action='store_true',
help='Enable verbose logging'
)
parser.add_argument(
'--dry-run',
action='store_true',
help='Run in dry-run mode (no changes to Splynx)'
)
return parser.parse_args()
def main():
"""
Main entry point for the payment processing CLI.
"""
args = parse_arguments()
# Set logging level
if args.verbose:
logging.getLogger().setLevel(logging.DEBUG)
logger.debug("Verbose logging enabled")
# Determine if running in live mode
process_live = (args.environment == 'live') and not args.dry_run
if args.dry_run:
logger.info("🔍 DRY-RUN MODE: No changes will be made to Splynx")
# Log execution details
env_display = "LIVE" if process_live else "TEST"
logger.info("=" * 80)
logger.info(f"Payment Processing System - Mode: {args.mode.upper()} - Environment: {env_display}")
logger.info("=" * 80)
try:
# Create Flask application
app = create_app()
# Create orchestrator
orchestrator = PaymentOrchestrator(
app=app,
config=Config,
process_live=process_live
)
# Run processing
result = orchestrator.run(args.mode)
# Display results
if result.get('success'):
logger.info("=" * 80)
logger.info("PROCESSING COMPLETED SUCCESSFULLY")
logger.info("=" * 80)
# Display mode-specific results
if args.mode == "batch":
logger.info(f"Batch IDs: {result.get('batch_ids', [])}")
logger.info(f"Successful: {result.get('success_count', 0)}")
logger.info(f"Failed: {result.get('failed_count', 0)}")
elif args.mode == "payplan":
logger.info(f"Batch ID: {result.get('batch_id')}")
logger.info(f"Successful: {result.get('success_count', 0)}")
logger.info(f"Failed: {result.get('failed_count', 0)}")
logger.info(f"Total Amount: ${result.get('total_amount', 0):.2f}")
elif args.mode == "payintent":
logger.info(f"Total Checked: {result.get('total_pending', 0)}")
logger.info(f"Succeeded: {result.get('succeeded', 0)}")
logger.info(f"Failed: {result.get('failed', 0)}")
logger.info(f"Still Pending: {result.get('still_pending', 0)}")
elif args.mode == "refund":
logger.info(f"Total Checked: {result.get('total_pending', 0)}")
logger.info(f"Completed: {result.get('completed', 0)}")
logger.info(f"Failed: {result.get('failed', 0)}")
logger.info(f"Still Pending: {result.get('still_pending', 0)}")
logger.info(f"Duration: {result.get('duration', 0):.1f}s")
logger.info("=" * 80)
sys.exit(0)
else:
logger.error("=" * 80)
logger.error("PROCESSING FAILED")
logger.error("=" * 80)
logger.error(f"Error: {result.get('error', 'Unknown error')}")
logger.error("=" * 80)
sys.exit(1)
except KeyboardInterrupt:
logger.warning("\n\nProcessing interrupted by user")
sys.exit(130)
except Exception as e:
logger.error("=" * 80)
logger.error("FATAL ERROR")
logger.error("=" * 80)
logger.error(f"Error: {e}", exc_info=True)
logger.error("=" * 80)
sys.exit(1)
if __name__ == "__main__":
main()

8
config.py

@ -5,7 +5,9 @@ class Config:
SECRET_KEY = os.environ.get('SECRET_KEY') or 'plutus-dev-secret-key-change-in-production'
# PostgreSQL database configuration (Flask-SQLAlchemy)
SQLALCHEMY_DATABASE_URI = 'postgresql://flask:FR0u9312rad$swib13125@192.168.20.53/plutus'
#SQLALCHEMY_DATABASE_URI = 'postgresql://flask:FR0u9312rad$swib13125@192.168.20.53/plutus'
SQLALCHEMY_DATABASE_URI = 'postgresql://postgres:strong_password@10.0.1.15/plutus'
SQLALCHEMY_TRACK_MODIFICATIONS = False
# MySQL database configuration (read-only)
@ -28,8 +30,10 @@ class Config:
# Process live on Sandbox
# False = Sandbox - Default
PROCESS_LIVE = True
PROCESS_LIVE = False
# Mode: batch, payintent, payplan, refund
MODE = 'payintent'
# Threading configuration
MAX_PAYMENT_THREADS = 15 # Number of concurrent payment processing threads

2
models.py

@ -60,6 +60,7 @@ class Payments(db.Model):
Stripe_Refund_Created = db.Column(db.DateTime, nullable=True)
Created = db.Column(db.DateTime, nullable=False, default=datetime.now())
PaymentPlan_ID = db.Column(db.Integer, db.ForeignKey('PaymentPlans.id'), nullable=True)
Invoices_to_Pay = db.Column(db.String())
class SinglePayments(db.Model):
__tablename__ = 'SinglePayments'
@ -87,6 +88,7 @@ class SinglePayments(db.Model):
Stripe_Refund_Created = db.Column(db.DateTime, nullable=True)
Created = db.Column(db.DateTime, nullable=False, default=datetime.now())
Who = db.Column(db.Integer, db.ForeignKey('Users.id'), nullable=False)
Invoices_to_Pay = db.Column(db.String())
class Logs(db.Model):

10
orchestration/__init__.py

@ -0,0 +1,10 @@
"""
Orchestration Module
This module contains the payment orchestrator that coordinates
different payment processing modes.
"""
from .payment_orchestrator import PaymentOrchestrator
__all__ = ['PaymentOrchestrator']

261
orchestration/payment_orchestrator.py

@ -0,0 +1,261 @@
"""
Payment Orchestrator Module
This module coordinates the execution of different payment processing modes.
"""
import logging
from datetime import datetime
from typing import Dict, Any, Optional
from flask import Flask
logger = logging.getLogger(__name__)
class PaymentOrchestrator:
"""
Orchestrator for payment processing operations.
This class coordinates the execution of different payment processing modes
(batch, payment plan, payment intent follow-up, refund follow-up).
"""
VALID_MODES = ["batch", "payplan", "payintent", "refund"]
def __init__(self, app: Flask, config, process_live: bool = False):
"""
Initialize the payment orchestrator.
Args:
app: Flask application instance
config: Configuration object
process_live: Whether to process in live mode
"""
self.app = app
self.config = config
self.process_live = process_live
self.logger = logging.getLogger(self.__class__.__name__)
# Initialize components (deferred to run())
self.stripe_processor = None
self.splynx_repo = None
self.payment_repo = None
def run(self, mode: str) -> Dict[str, Any]:
"""
Run payment processing for the specified mode.
Args:
mode: Processing mode ("batch", "payplan", "payintent", "refund")
Returns:
Dictionary with processing results
"""
if mode not in self.VALID_MODES:
error_msg = f"Invalid mode '{mode}'. Valid modes: {', '.join(self.VALID_MODES)}"
self.logger.error(error_msg)
return {
'success': False,
'error': error_msg,
'mode': mode
}
start_time = datetime.now()
# Log script start
try:
from services import log_script_start
environment = "live" if self.process_live else "sandbox"
log_script_start("query_mysql_new.py", mode, environment)
except Exception as e:
self.logger.warning(f"Failed to log script start: {e}")
# Initialize components within Flask app context
with self.app.app_context():
self._initialize_components()
try:
# Execute the appropriate processing mode
if mode == "batch":
result = self._process_batch()
elif mode == "payplan":
result = self._process_payment_plans()
elif mode == "payintent":
result = self._process_payment_intents()
elif mode == "refund":
result = self._process_refunds()
# Calculate duration
end_time = datetime.now()
duration = (end_time - start_time).total_seconds()
result['duration'] = duration
# Log script completion
self._log_completion(mode, result, duration)
return result
except Exception as e:
self.logger.error(f"Error during {mode} processing: {e}", exc_info=True)
duration = (datetime.now() - start_time).total_seconds()
return {
'success': False,
'error': str(e),
'mode': mode,
'duration': duration
}
def _initialize_components(self):
"""Initialize all required components within Flask app context."""
from app import db
from stripe_payment_processor import StripePaymentProcessor
from splynx import Splynx, SPLYNX_URL, SPLYNX_KEY, SPLYNX_SECRET
from repositories import PaymentRepository, SplynxRepository
# Initialize Stripe processor
api_key = self.config.STRIPE_LIVE_API_KEY if self.process_live else self.config.STRIPE_TEST_API_KEY
self.stripe_processor = StripePaymentProcessor(api_key=api_key, enable_logging=True)
# Initialize Splynx
splynx_client = Splynx(url=SPLYNX_URL, key=SPLYNX_KEY, secret=SPLYNX_SECRET)
self.splynx_repo = SplynxRepository(splynx_client)
# Initialize payment repository
self.payment_repo = PaymentRepository(db.session)
self.logger.info("Components initialized successfully")
def _process_batch(self) -> Dict[str, Any]:
"""
Process batch payments (Direct Debit and Card).
Returns:
Dictionary with processing results
"""
from app import db
from payment_processors import BatchPaymentProcessor
self.config.MODE = 'batch'
processor = BatchPaymentProcessor(
db_session_factory=db.session,
stripe_processor=self.stripe_processor,
splynx_repository=self.splynx_repo,
payment_repository=self.payment_repo,
config=self.config,
process_live=self.process_live
)
result = processor.process()
result['mode'] = self.config.MODE
return result
def _process_payment_plans(self) -> Dict[str, Any]:
"""
Process payment plans.
Returns:
Dictionary with processing results
"""
from app import db
from payment_processors import PaymentPlanProcessor
self.config.MODE = 'payplan'
processor = PaymentPlanProcessor(
db_session_factory=db.session,
stripe_processor=self.stripe_processor,
splynx_repository=self.splynx_repo,
payment_repository=self.payment_repo,
config=self.config,
process_live=self.process_live
)
result = processor.process()
result['mode'] = self.config.MODE
return result
def _process_payment_intents(self) -> Dict[str, Any]:
"""
Process payment intent follow-ups.
Returns:
Dictionary with processing results
"""
from app import db
from payment_processors import FollowUpProcessor
self.config.MODE = 'payintent'
processor = FollowUpProcessor(
db_session_factory=db.session,
stripe_processor=self.stripe_processor,
splynx_repository=self.splynx_repo,
payment_repository=self.payment_repo,
config=self.config,
process_live=self.process_live
)
result = processor.process_payment_intents()
result['mode'] = self.config.MODE
return result
def _process_refunds(self) -> Dict[str, Any]:
"""
Process refund follow-ups.
Returns:
Dictionary with processing results
"""
from app import db
from payment_processors import FollowUpProcessor
self.config.MODE = 'refund'
processor = FollowUpProcessor(
db_session_factory=db.session,
stripe_processor=self.stripe_processor,
splynx_repository=self.splynx_repo,
payment_repository=self.payment_repo,
config=self.config,
process_live=self.process_live
)
result = processor.process_refunds()
result['mode'] = self.config.MODE
return result
def _log_completion(self, mode: str, result: Dict[str, Any], duration: float):
"""
Log script completion using services module.
Args:
mode: Processing mode
result: Processing result dictionary
duration: Execution duration in seconds
"""
try:
from services import log_script_completion
success_count = result.get('success_count', result.get('succeeded', result.get('completed', 0)))
failed_count = result.get('failed_count', result.get('failed', 0))
total_amount = result.get('total_amount', 0.0)
batch_ids = result.get('batch_ids') or ([result.get('batch_id')] if result.get('batch_id') else None)
errors = [result.get('error')] if result.get('error') else None
log_script_completion(
script_name="query_mysql.py",
mode=mode,
success_count=success_count,
failed_count=failed_count,
total_amount=total_amount,
batch_ids=batch_ids,
duration_seconds=duration,
errors=errors
)
self.logger.info(f"Script completed in {duration:.1f}s: {success_count} successful, {failed_count} failed")
except Exception as e:
self.logger.warning(f"Failed to log script completion: {e}")

18
payment_processors/__init__.py

@ -0,0 +1,18 @@
"""
Payment Processors Module
This module contains payment processor classes that handle different
payment processing modes (batch, payment plan, follow-ups).
"""
from .base_processor import BasePaymentProcessor
from .batch_processor import BatchPaymentProcessor
from .payment_plan_processor import PaymentPlanProcessor
from .followup_processor import FollowUpProcessor
__all__ = [
'BasePaymentProcessor',
'BatchPaymentProcessor',
'PaymentPlanProcessor',
'FollowUpProcessor'
]

198
payment_processors/base_processor.py

@ -0,0 +1,198 @@
"""
Base Payment Processor Module
This module provides the abstract base class for all payment processors.
It defines the common interface and shared functionality.
"""
import logging
import json
from abc import ABC, abstractmethod
from typing import Dict, Any, Optional
from sqlalchemy.orm import scoped_session
logger = logging.getLogger(__name__)
class BasePaymentProcessor(ABC):
"""
Abstract base class for payment processors.
This class defines the common interface and shared functionality
for all payment processing modes.
"""
def __init__(self, db_session_factory, stripe_processor, splynx_repository,
payment_repository, config, process_live: bool = False):
"""
Initialize the base payment processor.
Args:
db_session_factory: Factory for creating database sessions (for thread safety)
stripe_processor: StripePaymentProcessor instance
splynx_repository: SplynxRepository instance
payment_repository: PaymentRepository instance
config: Configuration object
process_live: Whether to process in live mode
"""
self.db_session_factory = db_session_factory
self.stripe_processor = stripe_processor
self.splynx_repo = splynx_repository
self.payment_repo = payment_repository
self.config = config
self.process_live = process_live
self.logger = logging.getLogger(self.__class__.__name__)
def get_db_session(self):
"""
Get a database session for the current thread.
Returns:
Database session
"""
if isinstance(self.db_session_factory, scoped_session):
return self.db_session_factory()
else:
return self.db_session_factory
@abstractmethod
def process(self) -> Dict[str, Any]:
"""
Process payments for this processor's mode.
This method must be implemented by all subclasses.
Returns:
Dictionary with processing results (success_count, failed_count, etc.)
"""
pass
def handle_database_operation(self, operation_func, operation_name: str) -> Any:
"""
Execute a database operation with consistent error handling.
Args:
operation_func: Function that performs the database operation
operation_name: Description of the operation for logging
Returns:
Result of operation_func or None if failed
"""
try:
result = operation_func()
self.payment_repo.commit()
self.logger.debug(f"{operation_name} completed successfully")
return result
except Exception as e:
self.payment_repo.rollback()
self.logger.error(f"{operation_name} failed: {e}")
return None
def log_processing_start(self, mode: str, details: str = ""):
"""
Log the start of payment processing.
Args:
mode: Processing mode name
details: Additional details to log
"""
env = "LIVE" if self.process_live else "SANDBOX"
self.logger.info(f"Starting {mode} processing ({env})")
if details:
self.logger.info(details)
def log_processing_complete(self, mode: str, success_count: int, failed_count: int,
duration: float, additional_info: Dict[str, Any] = None):
"""
Log the completion of payment processing.
Args:
mode: Processing mode name
success_count: Number of successful operations
failed_count: Number of failed operations
duration: Processing duration in seconds
additional_info: Additional information to log
"""
self.logger.info(f"{mode} processing completed in {duration:.1f}s")
self.logger.info(f"Results: {success_count} successful, {failed_count} failed")
if additional_info:
for key, value in additional_info.items():
self.logger.info(f"{key}: {value}")
def create_payment_data(self, payment_record, payment_id: int, description: str = None) -> Dict[str, Any]:
"""
Create standardized payment data dictionary for Stripe processing.
Args:
payment_record: Database payment record
payment_id: Payment ID
description: Payment description override
Returns:
Dictionary with payment data for Stripe API
"""
if not description:
description = f"Payment ID: {payment_id} - Splynx ID: {payment_record.Splynx_ID}"
return {
'payment_id': payment_id,
'customer_id': payment_record.Stripe_Customer_ID,
'amount': payment_record.Payment_Amount,
'currency': "aud",
'description': description,
'stripe_pm': payment_record.Stripe_Payment_Method
}
def handle_payment_result(self, payment_id: int, result: Dict[str, Any],
payment_type: str = "pay") -> bool:
"""
Handle payment processing result and update database.
Args:
payment_id: Payment record ID
result: Payment processing result from Stripe
payment_type: "pay" or "singlepay"
Returns:
True if successful, False otherwise
"""
from payment_services.payment_service import processPaymentResult
#print(f"\n\nhandle_payment_result - result: {json.dumps(result,indent=2)}\n\n")
cust_stripe_details = self.stripe_processor.get_customer_info(customer_id=result.get('customer_id'))
#print(f"\ncust_stripe_details ({result.get('customer_id')}) - result: {json.dumps(cust_stripe_details,indent=2)}\n\n")
try:
# Get notification handler
notification_handler = self._get_notification_handler()
# Process the result
processPaymentResult(
db=self.payment_repo.db,
splynx=self.splynx_repo.splynx,
notification_handler=notification_handler,
pay_id=payment_id,
result=result,
key=payment_type,
process_live=self.process_live,
cust_stripe_details=cust_stripe_details,
mode=self.config.MODE
)
return True
except Exception as e:
self.logger.error(f"Error handling payment result for payment {payment_id}: {e}")
return False
def _get_notification_handler(self):
"""
Get the notification handler function for failed payments.
Returns:
Notification handler function or None
"""
from .notification_handler import handle_failed_payment_notification
#return handle_failed_payment_notification if self.process_live else None
return handle_failed_payment_notification

306
payment_processors/batch_processor.py

@ -0,0 +1,306 @@
"""
Batch Payment Processor Module
This module handles batch processing of Direct Debit and Card payments.
"""
import sys
import json
import logging
import threading
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime
from typing import Dict, Any, List
from .base_processor import BasePaymentProcessor
logger = logging.getLogger(__name__)
class BatchPaymentProcessor(BasePaymentProcessor):
"""
Processor for batch payment mode.
This class handles the batch processing of Direct Debit and Card payments,
including multi-threaded payment execution.
"""
def __init__(self, *args, **kwargs):
"""Initialize the batch payment processor."""
super().__init__(*args, **kwargs)
self.db_lock = threading.Lock()
def process(self) -> Dict[str, Any]:
"""
Process batch payments for Direct Debit and Card payment methods.
Returns:
Dictionary with processing results
"""
start_time = datetime.now()
self.log_processing_start("Batch")
print(f"Batch Processor Mode: {self.process_live}")
# Create batches for both payment methods
if self.process_live:
print("LIVE MODE")
batch_ids = self._create_payment_batches()
else:
print("The Sandbox")
batch_ids = self._create_payment_batches()
#sys.exit()
if not batch_ids:
self.logger.warning("No batches created for processing")
return {
'success': False,
'batch_ids': [],
'success_count': 0,
'failed_count': 0,
'duration': (datetime.now() - start_time).total_seconds()
}
# Execute payments for all batches
success_count, failed_count = self._execute_payment_batches(batch_ids)
# Calculate duration and log completion
duration = (datetime.now() - start_time).total_seconds()
self.log_processing_complete(
"Batch",
success_count,
failed_count,
duration,
{'batch_ids': batch_ids}
)
return {
'success': True,
'batch_ids': batch_ids,
'success_count': success_count,
'failed_count': failed_count,
'duration': duration
}
def _create_payment_batches(self) -> List[int]:
"""
Create payment batches for Direct Debit and Card payments.
Returns:
List of batch IDs
"""
batch_ids = []
payment_methods = [
self.config.PAYMENT_METHOD_CARD,
self.config.PAYMENT_METHOD_DIRECT_DEBIT
]
payment_method_names = {
self.config.PAYMENT_METHOD_CARD: "Card Payment",
self.config.PAYMENT_METHOD_DIRECT_DEBIT: "Direct Debit"
}
#for pm in payment_methods:
# Create batch
batch_id = self.payment_repo.create_payment_batch()
if batch_id is None:
self.logger.error(f"Failed to create batch for payment method {payment_methods}")
#continue
# Query customers for this payment method
if self.process_live:
customers = self.payment_repo.query_mysql_customers(
mysql_config=self.config.MYSQL_CONFIG,
payment_methods=payment_methods,
deposit_threshold=self.config.DEPOSIT_THRESHOLD,
limit=self.config.DEFAULT_QUERY_LIMIT
)
invoices = self.payment_repo.query_mysql_customer_invoices(
mysql_config=self.config.MYSQL_CONFIG,
limit=self.config.DEFAULT_QUERY_LIMIT
)
else:
print("Collecting test customer data")
#customers = self.payment_repo.get_test_customer_data(payment_method=payment_methods)
customers = self.payment_repo.query_mysql_customers(
mysql_config=self.config.MYSQL_CONFIG,
payment_methods=payment_methods,
deposit_threshold=self.config.DEPOSIT_THRESHOLD,
limit=self.config.DEFAULT_QUERY_LIMIT
)
invoices = self.payment_repo.query_mysql_customer_invoices(
mysql_config=self.config.MYSQL_CONFIG,
limit=self.config.DEFAULT_QUERY_LIMIT
)
if customers:
customer_count = len(customers)
result = self.payment_repo.add_payments_to_batch(customers, batch_id, invoices)
if result['added'] > 0:
batch_ids.append(batch_id)
#self.logger.info(f"Created batch {batch_id} for {payment_method_names[pm]} with {customer_count} customers")
self.logger.info(f"Created batch {batch_id} with {customer_count} customers")
# Log batch creation using services
try:
from services import log_batch_created
#log_batch_created(batch_id, payment_method_names[pm], customer_count)
log_batch_created(batch_id, str(payment_methods), customer_count)
except Exception as e:
self.logger.warning(f"Failed to log batch creation: {e}")
else:
self.logger.warning(f"No payments added to batch {batch_id}")
else:
#self.logger.info(f"No customers found for {payment_method_names[pm]}")
self.logger.info(f"No customers found for {str(payment_methods)}")
sys.exit()
return batch_ids
def _execute_payment_batches(self, batch_ids: List[int]) -> tuple:
"""
Execute payments for all provided batch IDs using thread pool.
Args:
batch_ids: List of batch IDs to process
Returns:
Tuple of (success_count, failed_count)
"""
if not batch_ids:
self.logger.warning("No valid batches to process")
return (0, 0)
max_threads = self.config.MAX_PAYMENT_THREADS
total_success = 0
total_failed = 0
for batch_id in batch_ids:
self.logger.info(f"Processing batch {batch_id}")
# Get payments for this batch
payments = self.payment_repo.get_payments_by_batch(batch_id)
if not payments:
self.logger.info(f"No payments found for batch {batch_id}")
continue
self.logger.info(f"Processing {len(payments)} payments using {max_threads} threads")
success_count, failed_count = self._process_payments_threaded(
payments,
max_threads
)
total_success += success_count
total_failed += failed_count
self.logger.info(f"Batch {batch_id} completed: {success_count}/{len(payments)} successful")
return (total_success, total_failed)
def _process_payments_threaded(self, payments: List[Any], max_threads: int) -> tuple:
"""
Process payments using thread pool with immediate commits.
Args:
payments: List of payment records
max_threads: Maximum number of concurrent threads
Returns:
Tuple of (success_count, failed_count)
"""
processed_count = 0
failed_count = 0
# Process payments in chunks to avoid timeout issues
chunk_size = max_threads * 2
for i in range(0, len(payments), chunk_size):
chunk = payments[i:i + chunk_size]
self.logger.info(f"Processing chunk {i//chunk_size + 1}: payments {i+1}-{min(i+chunk_size, len(payments))}")
# Prepare payment tasks for this chunk
payment_tasks = []
for pay in chunk:
payment_data = self.create_payment_data(pay, pay.id)
payment_tasks.append(payment_data)
# Process chunk with ThreadPoolExecutor
with ThreadPoolExecutor(max_workers=max_threads) as executor:
future_to_payment = {
executor.submit(self._process_single_payment, task): task
for task in payment_tasks
}
# Process results as they complete
for future in as_completed(future_to_payment):
try:
result = future.result(timeout=60)
if result['success'] and result['result']:
# Immediately commit each successful payment
self._update_payment_result(result['payment_id'], result['result'])
processed_count += 1
self.logger.info(f"Payment {result['payment_id']} processed ({processed_count}/{len(payments)})")
else:
failed_count += 1
self.logger.warning(f"Payment {result['payment_id']} failed ({failed_count} failures)")
except Exception as e:
payment_data = future_to_payment[future]
failed_count += 1
self.logger.error(f"Thread exception for payment {payment_data['payment_id']}: {e}")
self.logger.info(f"Chunk completed: {processed_count} processed, {failed_count} failed")
return (processed_count, failed_count)
def _process_single_payment(self, payment_data: Dict[str, Any]) -> Dict[str, Any]:
"""
Process a single payment (thread-safe).
Args:
payment_data: Dictionary containing payment information
Returns:
Dictionary with payment result and metadata
"""
try:
# Process payment with Stripe
print(f"\n\nBatch Processor - processPaymentResult - Mode: {self.config.MODE}")
result = self.stripe_processor.process_payment(
customer_id=payment_data['customer_id'],
amount=payment_data['amount'],
currency=payment_data['currency'],
description=payment_data['description'],
stripe_pm=payment_data['stripe_pm']
)
return {
'payment_id': payment_data['payment_id'],
'result': result,
'success': True
}
except Exception as e:
self.logger.error(f"Payment processing failed for payment ID {payment_data['payment_id']}: {e}")
return {
'payment_id': payment_data['payment_id'],
'result': None,
'success': False,
'error': str(e)
}
def _update_payment_result(self, payment_id: int, result: Dict[str, Any]):
"""
Thread-safe update of payment result to database.
Args:
payment_id: Payment ID
result: Payment processing result
"""
with self.db_lock:
try:
if result:
self.handle_payment_result(payment_id, result, payment_type="pay")
self.logger.debug(f"Payment {payment_id} result committed to database")
else:
self.logger.warning(f"No result to commit for payment {payment_id}")
except Exception as e:
self.logger.error(f"Failed to update payment {payment_id}: {e}")

310
payment_processors/followup_processor.py

@ -0,0 +1,310 @@
"""
Follow-Up Processor Module
This module handles follow-up processing for payment intents and refunds.
"""
import json
import logging
from datetime import datetime
from typing import Dict, Any
from .base_processor import BasePaymentProcessor
logger = logging.getLogger(__name__)
class FollowUpProcessor(BasePaymentProcessor):
"""
Processor for payment intent and refund follow-up modes.
This class handles checking the status of pending payment intents
and refunds, updating the database accordingly.
"""
def process(self) -> Dict[str, Any]:
"""
Process follow-up (payment intents by default).
This method is required by BasePaymentProcessor abstract class.
Use process_payment_intents() or process_refunds() for specific modes.
Returns:
Dictionary with processing results
"""
return self.process_payment_intents()
def process_payment_intents(self) -> Dict[str, Any]:
"""
Process follow-up for pending payment intents.
Returns:
Dictionary with processing results
"""
start_time = datetime.now()
self.log_processing_start("Payment Intent Follow-up")
# Get pending payment intents
pending_intents = self.payment_repo.get_pending_payment_intents(payment_type="both")
total_pending = sum(len(v) for v in pending_intents.values())
if total_pending == 0:
self.logger.info("No payment intents requiring follow-up")
return {
'success': True,
'total_pending': 0,
'succeeded': 0,
'failed': 0,
'still_pending': 0,
'duration': (datetime.now() - start_time).total_seconds()
}
# Process payment intents
succeeded_count = 0
failed_count = 0
still_pending = 0
for payment_type, payments in pending_intents.items():
self.logger.info(f"Processing {len(payments)} {payment_type} payment intents")
for payment in payments:
result = self._check_payment_intent(payment, payment_type)
if result == "succeeded":
succeeded_count += 1
elif result == "failed":
failed_count += 1
elif result == "pending":
still_pending += 1
# Calculate duration and log completion
duration = (datetime.now() - start_time).total_seconds()
self.log_processing_complete(
"Payment Intent Follow-up",
succeeded_count,
failed_count,
duration,
{'still_pending': still_pending, 'total_checked': total_pending}
)
# Log using services
try:
from services import log_payment_intent_followup
log_payment_intent_followup(total_pending, succeeded_count, failed_count, still_pending)
except Exception as e:
self.logger.warning(f"Failed to log payment intent follow-up: {e}")
return {
'success': True,
'total_pending': total_pending,
'succeeded': succeeded_count,
'failed': failed_count,
'still_pending': still_pending,
'duration': duration
}
def process_refunds(self) -> Dict[str, Any]:
"""
Process follow-up for pending refunds.
Returns:
Dictionary with processing results
"""
start_time = datetime.now()
self.log_processing_start("Refund Follow-up")
# Get pending refunds
pending_refunds = self.payment_repo.get_pending_refunds(payment_type="both")
total_pending = sum(len(v) for v in pending_refunds.values())
if total_pending == 0:
self.logger.info("No refunds requiring follow-up")
return {
'success': True,
'total_pending': 0,
'completed': 0,
'failed': 0,
'still_pending': 0,
'duration': (datetime.now() - start_time).total_seconds()
}
# Process refunds
completed_count = 0
failed_count = 0
still_pending = 0
for payment_type, refunds in pending_refunds.items():
self.logger.info(f"Processing {len(refunds)} {payment_type} refunds")
for refund in refunds:
result = self._check_refund(refund, payment_type)
if result == "succeeded":
completed_count += 1
elif result == "failed":
failed_count += 1
elif result == "pending":
still_pending += 1
# Calculate duration and log completion
duration = (datetime.now() - start_time).total_seconds()
self.log_processing_complete(
"Refund Follow-up",
completed_count,
failed_count,
duration,
{'still_pending': still_pending, 'total_checked': total_pending}
)
# Log using services
try:
from services import log_activity
log_activity(
user_id=1, # System user
action="refund_followup",
entity_type="script",
entity_id=None,
details=f"Processed {total_pending} pending refunds: {completed_count} completed, {failed_count} failed, {still_pending} still pending"
)
except Exception as e:
self.logger.warning(f"Failed to log refund follow-up activity: {e}")
return {
'success': True,
'total_pending': total_pending,
'completed': completed_count,
'failed': failed_count,
'still_pending': still_pending,
'duration': duration
}
def _check_payment_intent(self, payment, payment_type: str) -> str:
"""
Check the status of a payment intent and update the database.
Args:
payment: Payment record
payment_type: "pay" or "singlepay"
Returns:
Status string: "succeeded", "failed", or "pending"
"""
try:
intent_result = self.stripe_processor.check_payment_intent(payment.Payment_Intent)
self.logger.debug(f"Payment intent {payment.Payment_Intent}: {intent_result['status']}")
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
# Process payment result to update Splynx
self.handle_payment_result(payment.id, intent_result, payment_type)
self.payment_repo.commit()
self.logger.info(f"SUCCESS: Payment intent {payment.Payment_Intent} succeeded")
return "succeeded"
elif intent_result['status'] == "failed":
payment.PI_FollowUp_JSON = json.dumps(intent_result)
payment.PI_FollowUp = False
payment.PI_Last_Check = datetime.now()
# Process payment result to update Splynx
self.handle_payment_result(payment.id, intent_result, payment_type)
self.payment_repo.commit()
self.logger.warning(f"ERROR: Payment intent {payment.Payment_Intent} failed")
return "failed"
else:
# Still pending or requires action
payment.PI_FollowUp_JSON = json.dumps(intent_result)
payment.PI_Last_Check = datetime.now()
if intent_result.get('failure_reason'):
# Has a failure reason, mark as failed
self.handle_payment_result(payment.id, intent_result, payment_type)
payment.PI_FollowUp = False
payment.Error = json.dumps(intent_result)
self.payment_repo.commit()
self.logger.warning(f"ERROR: Payment intent {payment.Payment_Intent} failed with reason")
return "failed"
else:
# Still pending
self.payment_repo.commit()
self.logger.info(f"PENDING: Payment intent {payment.Payment_Intent} still pending")
return "pending"
except Exception as e:
self.logger.error(f"Error checking payment intent {payment.Payment_Intent}: {e}")
return "failed"
def _check_refund(self, refund_record, payment_type: str) -> str:
"""
Check the status of a refund and update the database.
Args:
refund_record: Payment record with refund
payment_type: "pay" or "singlepay"
Returns:
Status string: "succeeded", "failed", or "pending"
"""
try:
if not refund_record.Stripe_Refund_ID:
self.logger.error(f"No Stripe refund ID found for {payment_type} record {refund_record.id}")
return "failed"
refund_result = self.stripe_processor.check_refund_status(refund_record.Stripe_Refund_ID)
self.logger.debug(f"Refund {refund_record.Stripe_Refund_ID}: {refund_result.get('status')}")
# Check if the API call was successful
if not refund_result.get('success', False):
self.logger.error(f"Failed to check refund status: {refund_result.get('error', 'Unknown error')}")
return "failed"
if refund_result['status'] == "succeeded":
# Refund completed successfully
refund_record.Refund = True
refund_record.Refund_FollowUp = False
refund_record.Refund_JSON = json.dumps(refund_result)
# Delete associated Splynx payment record if in live mode
if self.process_live and refund_record.Payment_Intent:
delete_result = self.splynx_repo.delete_payment(
customer_id=refund_record.Splynx_ID,
payment_intent_id=refund_record.Payment_Intent
)
if delete_result.get('success'):
self.logger.info(f"Deleted Splynx payment for refund completion: customer {refund_record.Splynx_ID}")
else:
self.logger.warning(f"Failed to delete Splynx payment: {delete_result.get('error')}")
self.payment_repo.commit()
self.logger.info(f"SUCCESS: Refund completed: {refund_record.Stripe_Refund_ID}")
return "succeeded"
elif refund_result['status'] in ["failed", "canceled"]:
# Refund failed
refund_record.Refund_FollowUp = False
refund_record.Refund_JSON = json.dumps(refund_result)
self.payment_repo.commit()
self.logger.warning(f"ERROR: Refund failed: {refund_record.Stripe_Refund_ID} - {refund_result['status']}")
return "failed"
elif refund_result['status'] == "pending":
# Still pending
refund_record.Refund_JSON = json.dumps(refund_result)
self.payment_repo.commit()
self.logger.info(f"PENDING: Refund still pending: {refund_record.Stripe_Refund_ID}")
return "pending"
else:
# Unknown status
refund_record.Refund_JSON = json.dumps(refund_result)
self.payment_repo.commit()
self.logger.warning(f"WARNING: Unknown refund status: {refund_record.Stripe_Refund_ID} - {refund_result['status']}")
return "pending"
except Exception as e:
self.logger.error(f"Error processing refund {refund_record.Stripe_Refund_ID}: {e}")
return "failed"

137
payment_processors/notification_handler.py

@ -0,0 +1,137 @@
"""
Notification Handler Module
This module handles failed payment notifications and ticket creation.
"""
import logging
from payment_services.payment_service import create_customer_friendly_message
from notification_service import NotificationService
logger = logging.getLogger(__name__)
def handle_failed_payment_notification(payment_record, error_details: str, cust_stripe_details: dict, payment_type: str = "batch"):
"""
Handle notification and ticket creation for failed payments.
Args:
payment_record: Database payment record (Payments or SinglePayments)
error_details: Error message details
payment_type: Type of payment ("batch" or "single")
"""
# Import here to avoid circular dependencies
from splynx import Splynx, SPLYNX_URL, SPLYNX_KEY, SPLYNX_SECRET
from config import Config
try:
# Initialize services
splynx = Splynx(url=SPLYNX_URL, key=SPLYNX_KEY, secret=SPLYNX_SECRET)
notification_service = NotificationService()
# Get customer information from Splynx
try:
customer_data = splynx.Customer(payment_record.Splynx_ID)
customer_name = customer_data.get('name', 'Unknown Customer') if customer_data != 'unknown' else 'Unknown Customer'
except:
customer_name = 'Unknown Customer'
# Prepare payment data for notification
payment_data = {
'payment_id': payment_record.id,
'splynx_id': payment_record.Splynx_ID,
'amount': abs(payment_record.Payment_Amount),
'error': error_details,
'payment_method': payment_record.Payment_Method or 'Unknown',
'customer_name': customer_name,
'payment_type': payment_type,
'stripe_customer_id': payment_record.Stripe_Customer_ID,
'payment_intent': payment_record.Payment_Intent
}
# Revert pending invoices back to "not_paid" (only in live mode)
from payment_services.payment_service import find_set_pending_splynx_invoices_to_unpaid
updated_invoices = find_set_pending_splynx_invoices_to_unpaid(splynx, payment_record.Splynx_ID)
if updated_invoices:
logger.info(f"Payment failure: pending invoices reverted to not_paid for Splynx ID {payment_record.Splynx_ID}")
else:
logger.warning(f"No pending invoices to revert for Splynx ID {payment_record.Splynx_ID}")
# Send email notification
email_sent = notification_service.send_payment_failure_notification(payment_data)
if email_sent:
logger.info(f"Payment failure email sent for payment {payment_record.id}")
else:
logger.error(f"Failed to send payment failure email for payment {payment_record.id}")
# Create Splynx ticket
ticket_subject = f"Payment Failure - Customer {payment_record.Splynx_ID} - ${abs(payment_record.Payment_Amount):.2f}"
internal_message = f"""
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html>
<body>
<div>Payment processing has failed for customer {customer_name} (ID: {payment_record.Splynx_ID}).</div>
<div><br></div>
<div><strong>Payment Details:</strong></div>
<ul>
<li>Payment ID: {payment_record.id} ({payment_type})</li>
<li>Amount: ${abs(payment_record.Payment_Amount):.2f} AUD</li>
<li>Payment Method: {payment_record.Payment_Method or 'Unknown'}</li>
<li>Stripe Customer: {payment_record.Stripe_Customer_ID}</li>
<li>Payment Intent: {payment_record.Payment_Intent or 'N/A'}</li>
</ul>
<div><br></div>
<div><strong>Error Information:</strong></div>
<div>{error_details}</div>
<div><br></div>
<div>This ticket was automatically created by the Plutus Payment System.</div>
</body>
</html>
"""
# Create customer-friendly message
payment_data_for_msg = {
'amount': payment_data['amount'],
'splynx_id': payment_data['splynx_id'],
'pi_json': payment_record.PI_JSON,
'stripe_customer_id': payment_record.Stripe_Customer_ID,
'cust_stripe_details': cust_stripe_details
}
customer_message = create_customer_friendly_message(payment_data_for_msg, error_details)
ticket_result = splynx.create_ticket(
customer_id=payment_record.Splynx_ID,
subject=ticket_subject,
priority='medium',
type_id=1,
group_id=7,
status_id=1,
)
if ticket_result.get('success'):
logger.info(f"Splynx ticket created: #{ticket_result['ticket_id']} for payment {payment_record.id}")
# 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"
)
# Add customer-visible message
splynx.add_ticket_message(
ticket_id=ticket_result['ticket_id'],
message=customer_message,
is_admin=False,
hide_for_customer=False,
message_type="message"
)
else:
logger.error(f"Failed to create Splynx ticket for payment {payment_record.id}: {ticket_result.get('error')}")
except Exception as e:
logger.error(f"Error handling failed payment notification for payment {payment_record.id}: {e}")

292
payment_processors/payment_plan_processor.py

@ -0,0 +1,292 @@
"""
Payment Plan Processor Module
This module handles processing of recurring payment plans.
"""
import logging
import threading
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime
from typing import Dict, Any, List
from .base_processor import BasePaymentProcessor
logger = logging.getLogger(__name__)
class PaymentPlanProcessor(BasePaymentProcessor):
"""
Processor for payment plan mode.
This class handles processing of recurring payment plans that are due
based on their schedule (weekly/fortnightly).
"""
def __init__(self, *args, **kwargs):
"""Initialize the payment plan processor."""
super().__init__(*args, **kwargs)
self.db_lock = threading.Lock()
def process(self) -> Dict[str, Any]:
"""
Process payment plans that are due today.
Returns:
Dictionary with processing results
"""
start_time = datetime.now()
self.log_processing_start("Payment Plan")
# Query for due payment plans
due_plans = self._get_due_payment_plans()
if not due_plans:
self.logger.info("No payment plans due for processing today")
return {
'success': True,
'batch_id': None,
'success_count': 0,
'failed_count': 0,
'total_amount': 0.0,
'duration': (datetime.now() - start_time).total_seconds()
}
# Create batch for payment plans
batch_id = self.payment_repo.create_payment_batch()
if batch_id is None:
self.logger.error("Failed to create batch for payment plan processing")
return {
'success': False,
'batch_id': None,
'success_count': 0,
'failed_count': 0,
'total_amount': 0.0,
'duration': (datetime.now() - start_time).total_seconds()
}
# Add payments to batch
total_amount = sum(abs(plan.get('deposit', 0)) for plan in due_plans)
result = self.payment_repo.add_payments_to_batch(due_plans, batch_id)
if result['added'] == 0:
self.logger.error("Failed to add payment plans to batch")
return {
'success': False,
'batch_id': batch_id,
'success_count': 0,
'failed_count': 0,
'total_amount': 0.0,
'duration': (datetime.now() - start_time).total_seconds()
}
self.logger.info(f"Created payment plan batch {batch_id} with {result['added']} plans (${total_amount:,.2f} total)")
# Log batch creation
try:
from services import log_batch_created
log_batch_created(batch_id, "Payment Plan", result['added'])
except Exception as e:
self.logger.warning(f"Failed to log batch creation: {e}")
# Execute payments
payments = self.payment_repo.get_payments_by_batch(batch_id)
success_count, failed_count = self._process_payments_threaded(
payments,
self.config.MAX_PAYMENT_THREADS
)
# Calculate duration and log completion
duration = (datetime.now() - start_time).total_seconds()
self.log_processing_complete(
"Payment Plan",
success_count,
failed_count,
duration,
{
'batch_id': batch_id,
'total_amount': f"${total_amount:,.2f}"
}
)
return {
'success': True,
'batch_id': batch_id,
'success_count': success_count,
'failed_count': failed_count,
'total_amount': total_amount,
'duration': duration
}
def _get_due_payment_plans(self) -> List[Dict[str, Any]]:
"""
Get payment plans that are due for processing today.
Returns:
List of payment plan dictionaries
"""
active_plans = self.payment_repo.get_active_payment_plans()
due_plans = []
for plan in active_plans:
if plan.Start_Date and self._is_payment_day(
start_date_string=str(plan.Start_Date.strftime('%Y-%m-%d')),
schedule=plan.Frequency
):
payment_data = {
"customer_id": plan.Splynx_ID,
"stripe_customer_id": plan.Stripe_Customer_ID,
"deposit": plan.Amount * -1,
"stripe_pm": plan.Stripe_Payment_Method,
"paymentplan_id": plan.id
}
due_plans.append(payment_data)
self.logger.info(f"Found {len(due_plans)} payment plans due today")
return due_plans
def _is_payment_day(self, start_date_string: str, schedule: str, date_format: str = "%Y-%m-%d") -> bool:
"""
Check if today is a payment day based on start date and frequency.
Args:
start_date_string: The first payment date
schedule: Payment frequency ("Weekly" or "Fortnightly")
date_format: Format of the date string
Returns:
True if today is a payment day, False otherwise
"""
try:
if not start_date_string or not schedule:
self.logger.error("Missing required parameters for payment day calculation")
return False
if schedule == "Weekly":
num_days = 7
elif schedule == "Fortnightly":
num_days = 14
else:
self.logger.error(f"Unsupported payment schedule '{schedule}'")
return False
start_date = datetime.strptime(start_date_string, date_format)
today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
start_date = start_date.replace(hour=0, minute=0, second=0, microsecond=0)
# Calculate days since start date
days_since_start = (today - start_date).days
# Check if it's a multiple of the payment frequency
return days_since_start >= 0 and days_since_start % num_days == 0
except ValueError as e:
self.logger.error(f"Error parsing date '{start_date_string}' with format '{date_format}': {e}")
return False
except Exception as e:
self.logger.error(f"Unexpected error in is_payment_day: {e}")
return False
def _process_payments_threaded(self, payments: List[Any], max_threads: int) -> tuple:
"""
Process payments using thread pool.
Args:
payments: List of payment records
max_threads: Maximum number of concurrent threads
Returns:
Tuple of (success_count, failed_count)
"""
processed_count = 0
failed_count = 0
if not payments:
return (0, 0)
self.logger.info(f"Processing {len(payments)} payment plan payments using {max_threads} threads")
# Prepare payment tasks
payment_tasks = []
for pay in payments:
payment_data = self.create_payment_data(pay, pay.id)
payment_tasks.append(payment_data)
# Process with ThreadPoolExecutor
with ThreadPoolExecutor(max_workers=max_threads) as executor:
future_to_payment = {
executor.submit(self._process_single_payment, task): task
for task in payment_tasks
}
# Process results as they complete
for future in as_completed(future_to_payment):
try:
result = future.result(timeout=60)
if result['success'] and result['result']:
# Immediately commit each successful payment
self._update_payment_result(result['payment_id'], result['result'])
processed_count += 1
self.logger.info(f"Payment {result['payment_id']} processed ({processed_count}/{len(payments)})")
else:
failed_count += 1
self.logger.warning(f"Payment {result['payment_id']} failed ({failed_count} failures)")
except Exception as e:
payment_data = future_to_payment[future]
failed_count += 1
self.logger.error(f"Thread exception for payment {payment_data['payment_id']}: {e}")
return (processed_count, failed_count)
def _process_single_payment(self, payment_data: Dict[str, Any]) -> Dict[str, Any]:
"""
Process a single payment (thread-safe).
Args:
payment_data: Dictionary containing payment information
Returns:
Dictionary with payment result and metadata
"""
try:
# Process payment with Stripe
result = self.stripe_processor.process_payment(
customer_id=payment_data['customer_id'],
amount=payment_data['amount'],
currency=payment_data['currency'],
description=payment_data['description'],
stripe_pm=payment_data['stripe_pm']
)
return {
'payment_id': payment_data['payment_id'],
'result': result,
'success': True
}
except Exception as e:
self.logger.error(f"Payment processing failed for payment ID {payment_data['payment_id']}: {e}")
return {
'payment_id': payment_data['payment_id'],
'result': None,
'success': False,
'error': str(e)
}
def _update_payment_result(self, payment_id: int, result: Dict[str, Any]):
"""
Thread-safe update of payment result to database.
Args:
payment_id: Payment ID
result: Payment processing result
"""
with self.db_lock:
try:
if result:
self.handle_payment_result(payment_id, result, payment_type="pay")
self.logger.debug(f"Payment {payment_id} result committed to database")
else:
self.logger.warning(f"No result to commit for payment {payment_id}")
except Exception as e:
self.logger.error(f"Failed to update payment {payment_id}: {e}")

26
payment_services/__init__.py

@ -0,0 +1,26 @@
"""
Services module for shared business logic.
This module contains reusable service functions that are shared between
the query_mysql.py script and the Flask application.
"""
from .payment_service import (
processPaymentResult,
find_pay_splynx_invoices,
find_set_pending_splynx_invoices,
find_set_pending_splynx_invoices_to_unpaid,
add_payment_splynx,
delete_splynx_invoices,
create_customer_friendly_message
)
__all__ = [
'processPaymentResult',
'find_pay_splynx_invoices',
'find_set_pending_splynx_invoices',
'find_set_pending_splynx_invoices_to_unpaid',
'add_payment_splynx',
'delete_splynx_invoices',
'create_customer_friendly_message'
]

141
payment_services/customer_service.py

@ -0,0 +1,141 @@
"""
Customer Service Module
This module contains customer-related functions including:
- Stripe customer ID lookup and creation
- MySQL customer billing queries
- Customer data retrieval
"""
import logging
import pymysql
from typing import Optional, Dict, Any
logger = logging.getLogger(__name__)
def get_stripe_customer_id_from_mysql(splynx_id: int, mysql_config: dict) -> Optional[str]:
"""
Query MySQL database to get Stripe customer ID for a Splynx customer.
Args:
splynx_id: Customer ID in Splynx
mysql_config: MySQL connection configuration dictionary
Returns:
Stripe customer ID if found, None otherwise
"""
connection = None
try:
connection = pymysql.connect(
host=mysql_config['host'],
database=mysql_config['database'],
user=mysql_config['user'],
password=mysql_config['password'],
port=mysql_config['port'],
autocommit=False,
cursorclass=pymysql.cursors.DictCursor
)
query = """
SELECT field_1 AS stripe_customer_id
FROM payment_account_data
WHERE customer_id = %s
LIMIT 1
"""
with connection.cursor() as cursor:
cursor.execute(query, (splynx_id,))
result = cursor.fetchone()
if result and result.get('stripe_customer_id'):
logger.info(f"Found Stripe customer ID for Splynx ID {splynx_id}: {result['stripe_customer_id']}")
return result['stripe_customer_id']
else:
logger.warning(f"No Stripe customer ID found for Splynx ID {splynx_id}")
return None
except pymysql.Error as e:
logger.error(f"MySQL Error while fetching Stripe customer ID: {e}")
return None
except Exception as e:
logger.error(f"Unexpected error while fetching Stripe customer ID: {e}")
return None
finally:
if connection:
connection.close()
def get_customer_from_splynx(splynx, splynx_id: int) -> Optional[Dict[str, Any]]:
"""
Get customer information from Splynx API.
Args:
splynx: Splynx API client instance
splynx_id: Customer ID in Splynx
Returns:
Customer data dictionary if found, None otherwise
"""
try:
customer_data = splynx.Customer(splynx_id)
if customer_data != 'unknown':
return customer_data
else:
logger.warning(f"Customer {splynx_id} not found in Splynx")
return None
except Exception as e:
logger.error(f"Error fetching customer {splynx_id} from Splynx: {e}")
return None
def get_stripe_customer_id_from_splynx(splynx, splynx_id: int) -> Optional[str]:
"""
Get Stripe customer ID from Splynx additional attributes.
Args:
splynx: Splynx API client instance
splynx_id: Customer ID in Splynx
Returns:
Stripe customer ID if found, None otherwise
"""
try:
customer_data = get_customer_from_splynx(splynx, splynx_id)
if customer_data:
# Check for Stripe customer ID in additional attributes
additional_attrs = customer_data.get('additional_attributes', {})
stripe_id = additional_attrs.get('stripe_customer_id')
if stripe_id:
logger.info(f"Found Stripe customer ID in Splynx attributes for customer {splynx_id}")
return stripe_id
return None
except Exception as e:
logger.error(f"Error fetching Stripe ID from Splynx for customer {splynx_id}: {e}")
return None
def create_stripe_customer(stripe_processor, email: str, name: str, splynx_id: int) -> Optional[str]:
"""
Create a new Stripe customer.
Args:
stripe_processor: StripePaymentProcessor instance
email: Customer email
name: Customer name
splynx_id: Customer ID in Splynx (for metadata)
Returns:
Stripe customer ID if created successfully, None otherwise
"""
try:
# This would use the Stripe API via stripe_processor
# For now, just log that this would need implementation
logger.info(f"Creating new Stripe customer for {email} (Splynx ID: {splynx_id})")
# Implementation would go here using stripe_processor
# customer = stripe_processor.create_customer(email=email, name=name, metadata={'splynx_id': splynx_id})
# return customer.id
return None
except Exception as e:
logger.error(f"Error creating Stripe customer: {e}")
return None

452
payment_services/payment_service.py

@ -0,0 +1,452 @@
"""
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"""
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html>
<body>
<div>Your payment attempt was unsuccessful.</div>
<div><br></div>
<div><strong>Payment Details:</strong></div>
<div> Amount: ${amount:.2f} AUD</div>
<div> Date/Time: {current_time}</div>
<div> {payment_method_display}</div>
<div><br></div>
<div><strong>Issue:</strong> {error_message}</div>
<div><br></div>
<div>Please contact us if you need assistance with your payment.</div>
</body>
</html>
"""
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"""
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html>
<body>
<div>Your payment attempt was unsuccessful. Please contact us for assistance.</div>
</body>
</html>
"""
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()

52
pending_fixup.py

@ -1,52 +0,0 @@
import json
import stripe
from typing import List, Dict, Union, Any
from app import create_app, db
from models import Payments, PaymentBatch, SinglePayments, PaymentPlans
from splynx import Splynx, SPLYNX_URL, SPLYNX_KEY, SPLYNX_SECRET
from services import log_activity
from config import Config
from sqlalchemy import and_
splynx = Splynx(url=SPLYNX_URL, key=SPLYNX_KEY, secret=SPLYNX_SECRET)
api_key = Config.STRIPE_LIVE_API_KEY
stripe.api_key = api_key
def find_pay_splynx_invoices(splynx_id: int, result: dict) -> List[Dict[str, Any]]:
#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"
}
for pay in result:
res = splynx.put(url=f"/api/2.0/admin/finance/invoices/{pay['id']}", params=invoice_pay)
if __name__ == "__main__":
app = create_app()
i = 1
#cust_bill = splynx.get(url=f"/api/2.0/admin/customers/customer-billing/31")
#print(json.dumps(cust_bill,indent=2))
with app.app_context():
custs = db.session.query(PaymentBatch,Payments)\
.join(Payments, Payments.PaymentBatch_ID == PaymentBatch.id)\
.filter(and_(PaymentBatch.id.in_((102,103)), Payments.Success == True))\
.all()
print(len(custs))
for cust in custs:
cust_bill = splynx.get(url=f"/api/2.0/admin/customers/customer-billing/{cust.Payments.Splynx_ID}")
print(f"{i}/{len(custs)}")
if cust_bill['deposit'] == 0:
result = splynx.get(url=f"/api/2.0/admin/finance/invoices?main_attributes[customer_id]={cust.Payments.Splynx_ID}&main_attributes[status]=pending")
#print(json.dumps(result,indent=2))
if len(result) > 0:
print(f"\t{cust.Payments.Splynx_ID} - Has unpaid invoices")
find_pay_splynx_invoices(splynx_id=cust.Payments.Splynx_ID, result=result)
i += 1

532
query_mysql - Copy.py

@ -1,532 +0,0 @@
#!/usr/bin/env python3
"""
External script to query MySQL database (Splynx) for customer billing data.
This script runs independently of the Flask application.
Usage: python query_mysql.py
"""
import pymysql
import sys
import json
import random
import threading
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime, timedelta
from stripe_payment_processor import StripePaymentProcessor
from config import Config
from app import create_app, db
from models import Logs, Payments, PaymentBatch, SinglePayments, PaymentPlans
from splynx import Splynx, SPLYNX_URL, SPLYNX_KEY, SPLYNX_SECRET
# Initialize Splynx API
splynx = Splynx(url=SPLYNX_URL, key=SPLYNX_KEY, secret=SPLYNX_SECRET)
# Import constants from config
PAYMENT_METHOD_DIRECT_DEBIT = Config.PAYMENT_METHOD_DIRECT_DEBIT
PAYMENT_METHOD_CARD = Config.PAYMENT_METHOD_CARD
PAYMENT_METHOD_PAYMENT_PLAN = Config.PAYMENT_METHOD_PAYMENT_PLAN
PROCESS_LIVE = Config.PROCESS_LIVE
if PROCESS_LIVE:
api_key = "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"
else:
api_key = "sk_test_51Rsi9gPfYyg6zE1S4ZpaPI1ehpbsHRLsGhysYXKwAWCZ7w6KYgVXy4pV095Nd8tyjUw9AkBhqfxqsIiiWJg5fexI00Dw36vnvx"
test_stripe_customers = ['cus_SoQqMGLmCjiBDZ', 'cus_SoQptxwe8hczGz', 'cus_SoQjeNXkKOdORI', 'cus_SoQiDcSrNRxbPF', 'cus_SoQedaG3q2ecKG', 'cus_SoQeTkzMA7AaLR', 'cus_SoQeijBTETQcGb', 'cus_SoQe259iKMgz7o', 'cus_SoQejTstdXEDTO', 'cus_SoQeQH2ORWBOWX', 'cus_SoQevtyWxqXtpC', 'cus_SoQekOFUHugf26', 'cus_SoPq6Zh0MCUR9W', 'cus_SoPovwUPJmvugz', 'cus_SoPnvGfejhpSR5', 'cus_SoNAgAbkbFo8ZY', 'cus_SoMyDihTxRsa7U', 'cus_SoMVPWxdYstYbr', 'cus_SoMVQ6Xj2dIrCR', 'cus_SoMVmBn1xipFEB', 'cus_SoMVNvZ2Iawb7Y', 'cus_SoMVZupj6wRy5e', 'cus_SoMVqjH7zkc5Qe', 'cus_SoMVkzj0ZUK0Ai', 'cus_SoMVFq3BUD3Njw', 'cus_SoLcrRrvoy9dJ4', 'cus_SoLcqHN1k0WD8j', 'cus_SoLcLtYDZGG32V', 'cus_SoLcG23ilNeMYt', 'cus_SoLcFhtUVzqumj', 'cus_SoLcPgMnuogINl', 'cus_SoLccGTY9mMV7T', 'cus_SoLRxqvJxuKFes', 'cus_SoKs7cjdcvW1oO']
def find_pay_splynx_invoices(splynx_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)
#print(json.dumps(res,indent=2))
return res
def add_payment_splynx(splynx_id, pi_id, pay_id, amount):
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}"
}
res = splynx.post(url="/api/2.0/admin/finance/payments", params=stripe_pay)
if res:
return res['id']
else:
return False
def handle_database_operation(operation_func, operation_name):
"""
Reusable function to handle database operations with consistent error handling.
Args:
operation_func: Function that performs the database operation
operation_name: String description of the operation for error messages
Returns:
Result of operation_func or None if failed
"""
try:
result = operation_func()
db.session.commit()
return result
except Exception as e:
db.session.rollback()
print(f"{operation_name} failed: {e}")
return None
def is_payment_day(start_date_string, payplan_schedule, date_format="%Y-%m-%d"):
"""
Check if today is a fortnightly payment day based on a start date.
Args:
start_date_string (str): The first payment date
date_format (str): Format of the date string
Returns:
bool: True if today is a payment day, False otherwise
"""
try:
if payplan_schedule == "Weekly":
num_days = 7
elif payplan_schedule == "Fortnightly":
num_days = 14
start_date = datetime.strptime(start_date_string, date_format)
today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
start_date = start_date.replace(hour=0, minute=0, second=0, microsecond=0)
# Calculate days since start date
days_since_start = (today - start_date).days
# Check if it's a multiple of 14 days (fortnight)
return days_since_start >= 0 and days_since_start % num_days == 0
except ValueError as e:
print(f"Error parsing date: {e}")
return False
def query_payplan_customers():
"""Query customer billing data from MySQL database and find Payment Plan customers."""
to_return = []
customers = db.session.query(PaymentPlans).filter(PaymentPlans.Enabled == True).all()
for cust in customers:
if is_payment_day(start_date_string=str(cust.Start_Date.strftime('%Y-%m-%d')), payplan_schedule=cust.Frequency):
blah = {
"customer_id": cust.Splynx_ID,
"stripe_customer_id": cust.Stripe_Customer_ID,
"deposit": cust.Amount*-1,
"stripe_pm": cust.Stripe_Payment_Method,
"paymentplan_id": cust.id
}
to_return.append(blah)
return to_return
def query_splynx_customers(pm):
"""Query customer billing data from MySQL database."""
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 # Return results as dictionaries
)
print("✅ Connected to MySQL database successfully")
print(f"Database: {Config.MYSQL_CONFIG['database']} on {Config.MYSQL_CONFIG['host']}")
print("-" * 80)
## Payment Method:
## 2 - Direct Debit (Automatic)
## 3 - Card Payment (Automatic)
## 9 - Payment Plan
# Execute the query
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.payment_method = %s
AND cb.deposit < %s
ORDER BY cb.payment_method ASC
LIMIT %s
"""
with connection.cursor() as cursor:
cursor.execute(query, (pm, Config.DEPOSIT_THRESHOLD, Config.DEFAULT_QUERY_LIMIT))
#cursor.execute(query, (Config.DEFAULT_QUERY_LIMIT))
results = cursor.fetchall()
if results:
print(f"📊 Found {len(results)} rows:")
return results
else:
print("ℹ️ No rows found matching the criteria")
return False
except pymysql.Error as e:
print(f"❌ MySQL Error: {e}")
sys.exit(1)
except Exception as e:
print(f"❌ Unexpected Error: {e}")
sys.exit(1)
finally:
if connection:
connection.close()
print("\n🔒 MySQL connection closed")
def addInitialPayments(customers, batch_id):
added = {"added": 0, "failed": 0}
payments_to_add = []
# Prepare all payments first
for cust in customers:
if PROCESS_LIVE:
stripe_customer_id = cust['stripe_customer_id']
else:
#stripe_customer_id = cust['stripe_customer_id']
stripe_customer_id = test_stripe_customers[random.randint(1, len(test_stripe_customers)-1)]
add_payer = Payments(
PaymentBatch_ID = batch_id,
Splynx_ID = cust['customer_id'],
Stripe_Customer_ID = stripe_customer_id,
Payment_Amount = float(cust['deposit'])*-1,
Stripe_Payment_Method = cust.get('stripe_pm', None),
PaymentPlan_ID = cust.get('paymentplan_id', None)
)
payments_to_add.append(add_payer)
db.session.add(add_payer)
# Atomic commit for entire batch
try:
db.session.commit()
added["added"] = len(payments_to_add)
print(f"✅ Successfully added {len(payments_to_add)} payments to batch {batch_id}")
except Exception as e:
db.session.rollback()
added["failed"] = len(payments_to_add)
print(f"❌ addInitialPayments failed for entire batch {batch_id}: {e}")
print(f"Plutus DB: {json.dumps(added,indent=2)}\n")
def addPaymentBatch():
"""Create a new payment batch and return its ID."""
add_batch = PaymentBatch()
try:
db.session.add(add_batch)
db.session.commit()
return add_batch.id
except Exception as e:
db.session.rollback()
print(f"❌ addPaymentBatch failed: {e}")
return None
def processPaymentResult(pay_id, result, key):
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 PROCESS_LIVE:
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: {e}\n{json.dumps(result)}")
payment.PI_FollowUp = True
def _update_payment():
return True # Just need to trigger commit, payment is already modified
handle_database_operation(_update_payment, "processPaymentResult")
# Thread lock for database operations
db_lock = threading.Lock()
def process_single_payment(processor, payment_data):
"""
Thread-safe function to process a single payment.
Args:
processor: StripePaymentProcessor instance
payment_data: Dict containing payment information
Returns:
Dict with payment result and metadata
"""
try:
# Process payment with Stripe (thread-safe)
result = processor.process_payment(
customer_id=payment_data['customer_id'],
amount=payment_data['amount'],
currency=payment_data['currency'],
description=payment_data['description'],
stripe_pm=payment_data['stripe_pm']
)
# Return result with payment ID for database update
return {
'payment_id': payment_data['payment_id'],
'result': result,
'success': True
}
except Exception as e:
print(f"❌ Payment processing failed for payment ID {payment_data['payment_id']}: {e}")
return {
'payment_id': payment_data['payment_id'],
'result': None,
'success': False,
'error': str(e)
}
def update_single_payment_result(payment_id, result):
"""
Thread-safe immediate update of single payment result to database.
Commits immediately to ensure data safety.
Args:
payment_id: ID of the payment to update
result: Payment processing result
"""
with db_lock:
try:
if result:
processPaymentResult(pay_id=payment_id, result=result, key="pay")
print(f"✅ Payment {payment_id} result committed to database")
else:
print(f"⚠️ No result to commit for payment {payment_id}")
except Exception as e:
print(f"❌ Failed to update payment {payment_id}: {e}")
def process_batch_mode(processor):
"""Handle batch processing for Direct Debit and Card payments."""
to_run_batches = []
payment_methods = [PAYMENT_METHOD_DIRECT_DEBIT, PAYMENT_METHOD_CARD]
for pm in payment_methods:
batch_id = addPaymentBatch()
if batch_id is not None:
to_run_batches.append(batch_id)
customers = query_splynx_customers(pm)
addInitialPayments(customers=customers, batch_id=batch_id)
else:
print(f"❌ Failed to create batch for payment method {pm}")
return to_run_batches
def process_payplan_mode(processor):
"""Handle payment plan processing."""
to_run_batches = []
batch_id = addPaymentBatch()
if batch_id is not None:
to_run_batches.append(batch_id)
customers = query_payplan_customers()
addInitialPayments(customers=customers, batch_id=batch_id)
else:
print(f"❌ Failed to create batch for payment plan processing")
return to_run_batches
def execute_payment_batches(processor, batch_ids):
"""Execute payments for all provided batch IDs using safe threading with immediate commits."""
if not batch_ids:
print("⚠️ No valid batches to process")
return
max_threads = Config.MAX_PAYMENT_THREADS
for batch in batch_ids:
if batch is None:
print("⚠️ Skipping None batch ID")
continue
cust_pay = db.session.query(Payments).filter(Payments.PaymentBatch_ID == batch).all()
if not cust_pay:
print(f"ℹ️ No payments found for batch {batch}")
continue
print(f"🔄 Processing {len(cust_pay)} payments in batch {batch} using {max_threads} threads")
print(f"📊 Safety Mode: Each payment will be committed immediately to database")
# Process payments in smaller chunks to avoid timeout issues
processed_count = 0
failed_count = 0
# Process payments in chunks
chunk_size = max_threads * 2 # Process 2x thread count at a time
for i in range(0, len(cust_pay), chunk_size):
chunk = cust_pay[i:i + chunk_size]
print(f"🔄 Processing chunk {i//chunk_size + 1}: payments {i+1}-{min(i+chunk_size, len(cust_pay))}")
# Prepare payment data for this chunk
payment_tasks = []
for pay in chunk:
if PROCESS_LIVE:
customer_id = pay.Stripe_Customer_ID
else:
customer_id = pay.Stripe_Customer_ID
#customer_id = test_stripe_customers[random.randint(1, len(test_stripe_customers)-1)]
payment_data = {
'payment_id': pay.id,
'customer_id': customer_id,
'amount': pay.Payment_Amount,
'currency': "aud",
'description': f"Payment ID: {pay.id} - Splynx ID: {pay.Splynx_ID}",
'stripe_pm': pay.Stripe_Payment_Method
}
print(f"payment_data: {json.dumps(payment_data,indent=2)}")
payment_tasks.append(payment_data)
# Process this chunk with ThreadPoolExecutor
with ThreadPoolExecutor(max_workers=max_threads) as executor:
# Submit tasks for this chunk
future_to_payment = {
executor.submit(process_single_payment, processor, task): task
for task in payment_tasks
}
# Process results as they complete (NO TIMEOUT on as_completed)
for future in as_completed(future_to_payment):
try:
result = future.result(timeout=60) # Individual payment timeout
if result['success'] and result['result']:
# IMMEDIATELY commit each successful payment to database
update_single_payment_result(result['payment_id'], result['result'])
processed_count += 1
print(f"✅ Payment {result['payment_id']} processed and committed ({processed_count}/{len(cust_pay)})")
else:
failed_count += 1
print(f"❌ Payment {result['payment_id']} failed ({failed_count} failures total)")
except Exception as e:
payment_data = future_to_payment[future]
failed_count += 1
print(f"❌ Thread exception for payment {payment_data['payment_id']}: {e}")
print(f"📊 Chunk completed: {processed_count} processed, {failed_count} failed")
print(f"✅ Batch {batch} completed: {processed_count}/{len(cust_pay)} payments processed successfully")
def process_payintent_mode(processor):
"""Handle payment intent follow-up processing."""
to_check = {
"pay": db.session.query(Payments).filter(Payments.PI_FollowUp == True).all(),
"singlepay": db.session.query(SinglePayments).filter(SinglePayments.PI_FollowUp == True).all(),
}
#pis = db.session.query(Payments).filter(Payments.PI_FollowUp == True).all()
#to_check.append(pis)
#pis = db.session.query(SinglePayments).filter(SinglePayments.PI_FollowUp == True).all()
#to_check.append(pis)
for key, value in to_check.items():
print(value)
for pi in value:
intent_result = processor.check_payment_intent(pi.Payment_Intent)
print(json.dumps(intent_result, indent=2))
if intent_result['status'] == "succeeded":
pi.PI_FollowUp_JSON = json.dumps(intent_result)
pi.PI_FollowUp = False
pi.PI_Last_Check = datetime.now()
processPaymentResult(pay_id=pi.id, result=intent_result, key=key)
else:
pi.PI_FollowUp_JSON = json.dumps(intent_result)
pi.PI_Last_Check = datetime.now()
db.session.commit()
if __name__ == "__main__":
## Payment Method:
## 2 - Direct Debit (Automatic)
## 3 - Card Payment (Automatic)
## 9 - Payment Plan
### Running Mode
## batch = Monthly Direct Debit/Credit Cards
## payintent = Check outstanding Payment Intents and update
## payplan = Check for Payment Plans to run
try:
if sys.argv[1] == "batch":
running_mode = "batch"
elif sys.argv[1] == "payintent":
running_mode = "payintent"
elif sys.argv[1] == "payplan":
running_mode = "payplan"
else:
print(f"❌ Invalid running mode: {sys.argv[1]}")
print("Valid modes: batch, payintent, payplan")
sys.exit(1)
try:
if sys.argv[2] == "live":
PROCESS_LIVE = True
except:
print("Processing payments against Sandbox")
except IndexError:
print("ℹ️ No running mode specified, defaulting to 'payintent'")
running_mode = "payintent"
# Create Flask application context
app = create_app()
processor = StripePaymentProcessor(api_key=api_key, enable_logging=True)
with app.app_context():
if running_mode == "batch":
batch_ids = process_batch_mode(processor)
execute_payment_batches(processor, batch_ids)
elif running_mode == "payplan":
batch_ids = process_payplan_mode(processor)
execute_payment_batches(processor, batch_ids)
elif running_mode == "payintent":
process_payintent_mode(processor)

105
query_mysql_new.py

@ -0,0 +1,105 @@
#!/usr/bin/env python3
"""
Payment Processing Script (New Architecture)
This script provides backward-compatible CLI for the refactored payment processing system.
It maintains the same command-line interface as the original query_mysql.py while
using the new modular architecture under the hood.
Usage: python query_mysql.py [mode] [live]
Modes: batch, payintent, payplan, refund
The new architecture provides:
- Separation of concerns with dedicated modules
- Thread-safe database operations
- Improved error handling and logging
- Better testability and maintainability
"""
import sys
import logging
from config import Config
from app import create_app
from orchestration import PaymentOrchestrator
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('payment_processing.log'),
logging.StreamHandler(sys.stdout)
]
)
logger = logging.getLogger(__name__)
def main():
"""
Main entry point - maintains backward compatibility with original CLI.
"""
# Parse command-line arguments (original format)
running_mode = "payintent" # Default mode
process_live = False
try:
if len(sys.argv) > 1:
mode_arg = sys.argv[1].lower()
if mode_arg in ["batch", "payintent", "payplan", "refund"]:
running_mode = mode_arg
else:
logger.error(f"Invalid running mode: {sys.argv[1]}")
logger.info("Valid modes: batch, payintent, payplan, refund")
sys.exit(1)
if len(sys.argv) > 2:
if sys.argv[2].lower() == "live":
process_live = True
else:
logger.warning(f"Unknown environment argument: {sys.argv[2]}. Using test mode.")
except IndexError:
logger.info("No running mode specified, defaulting to 'payintent'")
# Log execution details
env_display = "LIVE" if process_live else "SANDBOX"
logger.info("=" * 80)
logger.info(f"Payment Processing System (Refactored)")
logger.info(f"Mode: {running_mode.upper()} - Environment: {env_display}")
logger.info("=" * 80)
try:
# Create Flask application
app = create_app()
# Create and run orchestrator
orchestrator = PaymentOrchestrator(
app=app,
config=Config,
process_live=process_live
)
result = orchestrator.run(running_mode)
# Log results
if result.get('success'):
logger.info("=" * 80)
logger.info("SUCCESS: Processing completed successfully")
logger.info("=" * 80)
sys.exit(0)
else:
logger.error("=" * 80)
logger.error(f"FAILED: Processing failed: {result.get('error', 'Unknown error')}")
logger.error("=" * 80)
sys.exit(1)
except KeyboardInterrupt:
logger.warning("\n\nWARNING: Processing interrupted by user")
sys.exit(130)
except Exception as e:
logger.error(f"Fatal error: {e}", exc_info=True)
sys.exit(1)
if __name__ == "__main__":
main()

14
repositories/__init__.py

@ -0,0 +1,14 @@
"""
Repositories module for data access layer.
This module contains repository classes that handle database operations
for payments, customers, and external API interactions.
"""
from .payment_repository import PaymentRepository
from .splynx_repository import SplynxRepository
__all__ = [
'PaymentRepository',
'SplynxRepository'
]

482
repositories/payment_repository.py

@ -0,0 +1,482 @@
"""
Payment Repository Module
This module provides a repository pattern for payment-related database operations.
It abstracts database access and provides a clean interface for CRUD operations.
"""
import logging
import pymysql
import json
from datetime import datetime
from typing import List, Dict, Any, Optional, Union
from sqlalchemy.orm import Session
from collections import defaultdict
logger = logging.getLogger(__name__)
class PaymentRepository:
"""
Repository for payment-related database operations.
This class provides methods for creating, reading, updating, and deleting
payment records in both the Payments and SinglePayments tables.
"""
def __init__(self, db_session: Session):
"""
Initialize the payment repository.
Args:
db_session: SQLAlchemy database session
"""
self.db = db_session
def create_payment_batch(self) -> Optional[int]:
"""
Create a new payment batch record.
Returns:
Batch ID if successful, None otherwise
"""
from models import PaymentBatch
try:
batch = PaymentBatch()
self.db.add(batch)
self.db.commit()
logger.info(f"Created payment batch with ID {batch.id}")
return batch.id
except Exception as e:
self.db.rollback()
logger.error(f"Failed to create payment batch: {e}")
return None
def add_payments_to_batch(self, customers: List[Dict[str, Any]], batch_id: int, invoices: Dict) -> Dict[str, int]:
"""
Add multiple payments to a batch atomically.
Args:
customers: List of customer payment data dictionaries
batch_id: Batch ID to associate payments with
Returns:
Dictionary with 'added' and 'failed' counts
"""
from models import Payments
result = {"added": 0, "failed": 0}
payments_to_add = []
try:
for cust in customers:
cust_id_str = str(cust['customer_id'])
cust_invoices = invoices.get(cust_id_str) # Returns None if no invoices
payment = Payments(
PaymentBatch_ID=batch_id,
Splynx_ID=cust['customer_id'],
Stripe_Customer_ID=cust['stripe_customer_id'],
Payment_Amount=float(cust['deposit']) * -1,
Stripe_Payment_Method=cust.get('stripe_pm', None),
PaymentPlan_ID=cust.get('paymentplan_id', None),
Invoices_to_Pay=cust_invoices
)
payments_to_add.append(payment)
self.db.add(payment)
self.db.commit()
result["added"] = len(payments_to_add)
logger.info(f"Successfully added {len(payments_to_add)} payments to batch {batch_id}")
except Exception as e:
self.db.rollback()
result["failed"] = len(payments_to_add)
logger.error(f"Failed to add payments to batch {batch_id}: {e}")
return result
def get_payments_by_batch(self, batch_id: int) -> List[Any]:
"""
Get all payments for a given batch.
Args:
batch_id: Batch ID
Returns:
List of Payment objects
"""
from models import Payments
try:
payments = self.db.query(Payments).filter(Payments.PaymentBatch_ID == batch_id).all()
logger.debug(f"Found {len(payments)} payments for batch {batch_id}")
return payments
except Exception as e:
logger.error(f"Error fetching payments for batch {batch_id}: {e}")
return []
def get_payment_by_id(self, payment_id: int, payment_type: str = "pay") -> Optional[Any]:
"""
Get a payment record by ID.
Args:
payment_id: Payment record ID
payment_type: "pay" for Payments, "singlepay" for SinglePayments
Returns:
Payment object if found, None otherwise
"""
from models import Payments, SinglePayments
try:
if payment_type == "pay":
return self.db.query(Payments).filter(Payments.id == payment_id).first()
elif payment_type == "singlepay":
return self.db.query(SinglePayments).filter(SinglePayments.id == payment_id).first()
else:
logger.error(f"Invalid payment type: {payment_type}")
return None
except Exception as e:
logger.error(f"Error fetching payment {payment_id}: {e}")
return None
def get_pending_payment_intents(self, payment_type: str = "both") -> Dict[str, List[Any]]:
"""
Get all payments with pending payment intents requiring follow-up.
Args:
payment_type: "pay", "singlepay", or "both"
Returns:
Dictionary with payment lists by type
"""
from models import Payments, SinglePayments
result = {}
try:
if payment_type in ["pay", "both"]:
result["pay"] = self.db.query(Payments).filter(Payments.PI_FollowUp == True).all()
if payment_type in ["singlepay", "both"]:
result["singlepay"] = self.db.query(SinglePayments).filter(SinglePayments.PI_FollowUp == True).all()
total = sum(len(v) for v in result.values())
logger.info(f"Found {total} pending payment intents requiring follow-up")
return result
except Exception as e:
logger.error(f"Error fetching pending payment intents: {e}")
return {}
def get_pending_refunds(self, payment_type: str = "both") -> Dict[str, List[Any]]:
"""
Get all payments with pending refunds requiring follow-up.
Args:
payment_type: "pay", "singlepay", or "both"
Returns:
Dictionary with payment lists by type
"""
from models import Payments, SinglePayments
result = {}
try:
if payment_type in ["pay", "both"]:
result["pay"] = self.db.query(Payments).filter(Payments.Refund_FollowUp == True).all()
if payment_type in ["singlepay", "both"]:
result["singlepay"] = self.db.query(SinglePayments).filter(SinglePayments.Refund_FollowUp == True).all()
total = sum(len(v) for v in result.values())
logger.info(f"Found {total} pending refunds requiring follow-up")
return result
except Exception as e:
logger.error(f"Error fetching pending refunds: {e}")
return {}
def get_active_payment_plans(self) -> List[Any]:
"""
Get all active payment plans.
Returns:
List of PaymentPlans objects
"""
from models import PaymentPlans
try:
plans = self.db.query(PaymentPlans).filter(PaymentPlans.Enabled == True).all()
logger.info(f"Found {len(plans)} active payment plans")
return plans
except Exception as e:
logger.error(f"Error fetching active payment plans: {e}")
return []
def get_test_customer_data(self, payment_method: List) -> Union[List[Dict[str, Any]], bool]:
customers = [
{
"customer_id": 1222024,
"stripe_customer_id": "cus_SoNAgAbkbFo8ZY",
"deposit": -10,
"stripe_pm": "pm_1RskQIPfYyg6zE1Spbtf4pIa",
"paymentplan_id": None
},
{
"customer_id": 1222025,
"stripe_customer_id": "cus_SoMVPWxdYstYbr",
"deposit": -10,
"stripe_pm": "pm_1SmTzDPfYyg6zE1SZnZogJAz",
"paymentplan_id": None
},
{
"customer_id": 1222026,
"stripe_customer_id": "cus_SoMVQ6Xj2dIrCR",
"deposit": -10,
"stripe_pm": "pm_1RsjmdPfYyg6zE1SOp2vBKNT",
"paymentplan_id": None
},
{
"customer_id": 1222027,
"stripe_customer_id": "cus_SoMVqjH7zkc5Qe",
"deposit": -10,
"stripe_pm": "pm_1RsjmFPfYyg6zE1SS1DmqdvB",
"paymentplan_id": None
},
{
"customer_id": 1222028,
"stripe_customer_id": "cus_SoQiDcSrNRxbPF",
"deposit": -10,
"stripe_pm": "pm_1SmTblPfYyg6zE1SWkzAFekG",
"paymentplan_id": None
},
]
return customers
def query_mysql_customer_invoices(self, mysql_config: dict, limit: int = 10) -> Dict:
"""
Query customer invoice data from external MySQL database.
Args:
mysql_config: MySQL connection configuration
limit: Maximum number of results
Returns:
List of customer dictionaries or False if no results/error
"""
invoices_dict = {}
connection = None
try:
connection = pymysql.connect(
host=mysql_config['host'],
database=mysql_config['database'],
user=mysql_config['user'],
password=mysql_config['password'],
port=mysql_config['port'],
autocommit=False,
cursorclass=pymysql.cursors.DictCursor
)
logger.info(f"Connected to MySQL database: {mysql_config['database']} on {mysql_config['host']}")
query = """
SELECT
i.id,
i.customer_id
FROM invoices i
WHERE i.status = 'not_paid'
ORDER BY i.customer_id ASC
LIMIT %s
"""
with connection.cursor() as cursor:
cursor.execute(query, (limit))
results = cursor.fetchall()
if results:
logger.info(f"Found {len(results)} customer invoices")
# Group invoice IDs by customer_id
invoices_dict = defaultdict(list)
for res in results:
#print(res['id'], res['customer_id'])
invoices_dict[res['customer_id']].append(str(res['id']))
#print(f"\nCustomer Invoice Data: {json.dumps(invoices_dict)}\n")
print(f"Cust 31: {invoices_dict[31]}")
# Convert to comma-separated strings
invoices_dict = {
str(cust_id): ",".join(inv_ids)
for cust_id, inv_ids in invoices_dict.items()
}
return invoices_dict
else:
logger.info(f"No customer invoices found")
return False
except pymysql.Error as e:
logger.error(f"MySQL Error: {e}")
return False
except Exception as e:
logger.error(f"Unexpected Error querying MySQL: {e}")
return False
finally:
if connection:
connection.close()
logger.debug("MySQL connection closed")
def query_mysql_customers(self, mysql_config: dict, payment_methods: List, deposit_threshold: float = 0, limit: int = 10) -> Union[List[Dict[str, Any]], bool]:
"""
Query customer billing data from external MySQL database.
Args:
mysql_config: MySQL connection configuration
payment_method: Payment method ID (2=Direct Debit, 3=Card, 9=Payment Plan)
deposit_threshold: Maximum deposit threshold
limit: Maximum number of results
Returns:
List of customer dictionaries or False if no results/error
"""
connection = None
try:
connection = pymysql.connect(
host=mysql_config['host'],
database=mysql_config['database'],
user=mysql_config['user'],
password=mysql_config['password'],
port=mysql_config['port'],
autocommit=False,
cursorclass=pymysql.cursors.DictCursor
)
logger.info(f"Connected to MySQL database: {mysql_config['database']} on {mysql_config['host']}")
placeholders = ','.join(['%s'] * len(payment_methods))
query = f"""
SELECT DISTINCT
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.payment_method IN ({placeholders})
AND cb.deposit < %s
AND pad.field_1 IS NOT NULL
AND NOT EXISTS (
SELECT 1
FROM invoices i
WHERE i.customer_id = cb.customer_id
AND i.status = 'pending'
)
GROUP BY cb.customer_id, cb.deposit, cb.payment_method, pad.field_1
ORDER BY cb.payment_method ASC, cb.customer_id ASC
LIMIT %s
"""
###### Multi-month billing runs
#query = """
#SELECT DISTINCT
# 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.payment_method = %s
#AND cb.deposit < %s
#AND pad.field_1 IS NOT NULL
#AND NOT EXISTS (
# SELECT 1
# FROM invoices i
# WHERE i.customer_id = cb.customer_id
# AND i.status = 'pending'
#)
#AND EXISTS (
# SELECT 1
# FROM invoices i
# WHERE i.customer_id = cb.customer_id
# AND i.date_till = '2026-01-20'
#)
#GROUP BY cb.customer_id, cb.deposit, cb.payment_method, pad.field_1
#ORDER BY cb.payment_method ASC, cb.customer_id ASC
#LIMIT %s
#"""
## Need to fix "date_till"
#####################
with connection.cursor() as cursor:
cursor.execute(query, (*payment_methods, deposit_threshold, limit))
results = cursor.fetchall()
if results:
logger.info(f"Found {len(results)} customers for payment method {payment_methods}")
return results
else:
logger.info(f"No customers found for payment method {payment_methods}")
return False
except pymysql.Error as e:
logger.error(f"MySQL Error: {e}")
return False
except Exception as e:
logger.error(f"Unexpected Error querying MySQL: {e}")
return False
finally:
if connection:
connection.close()
logger.debug("MySQL connection closed")
def update_payment(self, payment_id: int, updates: Dict[str, Any], payment_type: str = "pay") -> bool:
"""
Update a payment record with given fields.
Args:
payment_id: Payment ID
updates: Dictionary of field names and values to update
payment_type: "pay" or "singlepay"
Returns:
True if successful, False otherwise
"""
try:
payment = self.get_payment_by_id(payment_id, payment_type)
if not payment:
logger.error(f"Payment {payment_id} not found")
return False
for key, value in updates.items():
if hasattr(payment, key):
setattr(payment, key, value)
else:
logger.warning(f"Payment object has no attribute '{key}'")
self.db.commit()
logger.debug(f"Updated payment {payment_id} with {len(updates)} fields")
return True
except Exception as e:
self.db.rollback()
logger.error(f"Failed to update payment {payment_id}: {e}")
return False
def commit(self):
"""Commit the current database session."""
try:
self.db.commit()
except Exception as e:
self.db.rollback()
logger.error(f"Failed to commit database session: {e}")
raise
def rollback(self):
"""Rollback the current database session."""
try:
self.db.rollback()
except Exception as e:
logger.error(f"Failed to rollback database session: {e}")
raise

314
repositories/splynx_repository.py

@ -0,0 +1,314 @@
"""
Splynx Repository Module
This module provides a repository wrapper around the Splynx API client.
It adds additional error handling, retry logic, and commonly used operations.
"""
import logging
import time
from typing import Dict, Any, Optional, List
logger = logging.getLogger(__name__)
class SplynxRepository:
"""
Repository for Splynx API operations.
This class wraps the Splynx API client and provides higher-level
operations with consistent error handling and retry logic.
"""
def __init__(self, splynx_client, max_retries: int = 3, retry_delay: float = 1.0):
"""
Initialize the Splynx repository.
Args:
splynx_client: Splynx API client instance
max_retries: Maximum number of retry attempts for transient failures
retry_delay: Delay in seconds between retries
"""
self.splynx = splynx_client
self.max_retries = max_retries
self.retry_delay = retry_delay
def _retry_operation(self, operation_func, operation_name: str):
"""
Execute an operation with retry logic for transient failures.
Args:
operation_func: Function to execute
operation_name: Description for logging
Returns:
Result of operation_func or None if all retries failed
"""
last_error = None
for attempt in range(self.max_retries):
try:
return operation_func()
except Exception as e:
last_error = e
if attempt < self.max_retries - 1:
logger.warning(f"{operation_name} failed (attempt {attempt + 1}/{self.max_retries}): {e}")
time.sleep(self.retry_delay)
else:
logger.error(f"{operation_name} failed after {self.max_retries} attempts: {e}")
return None
def get_customer(self, customer_id: int) -> Optional[Dict[str, Any]]:
"""
Get customer information from Splynx.
Args:
customer_id: Customer ID in Splynx
Returns:
Customer data dictionary or None if not found
"""
def _get():
customer_data = self.splynx.Customer(customer_id)
return customer_data if customer_data != 'unknown' else None
return self._retry_operation(_get, f"Get customer {customer_id}")
def get_customer_name(self, customer_id: int) -> str:
"""
Get customer name from Splynx.
Args:
customer_id: Customer ID in Splynx
Returns:
Customer name or "Unknown Customer" if not found
"""
customer_data = self.get_customer(customer_id)
if customer_data:
return customer_data.get('name', 'Unknown Customer')
return 'Unknown Customer'
def mark_invoices_paid(self, customer_id: int) -> List[Dict[str, Any]]:
"""
Mark all unpaid/pending invoices as paid for a customer.
Args:
customer_id: Customer ID in Splynx
Returns:
List of updated invoice dictionaries
"""
def _mark():
result = self.splynx.get(
url=f"/api/2.0/admin/finance/invoices?main_attributes[customer_id]={customer_id}&main_attributes[status]=not_paid&main_attributes[status]=pending"
)
invoice_pay = {"status": "paid"}
updated_invoices = []
for invoice in result:
res = self.splynx.put(url=f"/api/2.0/admin/finance/invoices/{invoice['id']}", params=invoice_pay)
if res:
updated_invoices.append(res)
logger.info(f"Marked {len(updated_invoices)} invoices as paid for customer {customer_id}")
return updated_invoices
return self._retry_operation(_mark, f"Mark invoices paid for customer {customer_id}") or []
def mark_invoices_pending(self, customer_id: int) -> List[Dict[str, Any]]:
"""
Mark all unpaid invoices as pending for a customer.
Args:
customer_id: Customer ID in Splynx
Returns:
List of updated invoice dictionaries
"""
def _mark():
result = self.splynx.get(
url=f"/api/2.0/admin/finance/invoices?main_attributes[customer_id]={customer_id}&main_attributes[status]=not_paid"
)
invoice_pay = {"status": "pending"}
updated_invoices = []
for invoice in result:
res = self.splynx.put(url=f"/api/2.0/admin/finance/invoices/{invoice['id']}", params=invoice_pay)
if res:
updated_invoices.append(res)
logger.info(f"Marked {len(updated_invoices)} invoices as pending for customer {customer_id}")
return updated_invoices
return self._retry_operation(_mark, f"Mark invoices pending for customer {customer_id}") or []
def revert_invoices_to_unpaid(self, customer_id: int) -> List[Dict[str, Any]]:
"""
Revert pending invoices back to unpaid status for a customer.
Args:
customer_id: Customer ID in Splynx
Returns:
List of updated invoice dictionaries
"""
def _revert():
result = self.splynx.get(
url=f"/api/2.0/admin/finance/invoices?main_attributes[customer_id]={customer_id}&main_attributes[status]=pending"
)
invoice_pay = {"status": "not_paid"}
updated_invoices = []
for invoice in result:
res = self.splynx.put(url=f"/api/2.0/admin/finance/invoices/{invoice['id']}", params=invoice_pay)
if res:
updated_invoices.append(res)
logger.info(f"Reverted {len(updated_invoices)} invoices to unpaid for customer {customer_id}")
return updated_invoices
return self._retry_operation(_revert, f"Revert invoices to unpaid for customer {customer_id}") or []
def add_payment(self, customer_id: int, amount: float, payment_intent_id: str, payment_id: int) -> Optional[int]:
"""
Add a payment record to Splynx.
Args:
customer_id: Customer ID in Splynx
amount: Payment amount
payment_intent_id: Stripe payment intent ID
payment_id: Internal payment ID
Returns:
Splynx payment ID if successful, None otherwise
"""
def _add():
from datetime import datetime
stripe_pay = {
"customer_id": customer_id,
"amount": amount,
"date": str(datetime.now().strftime('%Y-%m-%d')),
"field_1": payment_intent_id,
"field_2": f"Payment_ID (Batch): {payment_id}"
}
res = self.splynx.post(url="/api/2.0/admin/finance/payments", params=stripe_pay)
if res:
logger.info(f"Added payment to Splynx for customer {customer_id}: payment ID {res['id']}")
return res['id']
else:
logger.error(f"Failed to add payment to Splynx for customer {customer_id}")
return None
return self._retry_operation(_add, f"Add payment to Splynx for customer {customer_id}")
def delete_payment(self, customer_id: int, payment_intent_id: str) -> Dict[str, Any]:
"""
Delete a Splynx payment record by customer ID and payment intent.
Args:
customer_id: Customer ID in Splynx
payment_intent_id: Stripe payment intent ID
Returns:
Dictionary with success status and details
"""
try:
params = {
'main_attributes': {
'customer_id': customer_id,
'field_1': payment_intent_id
},
}
query_string = self.splynx.build_splynx_query_params(params)
result = self.splynx.get(url=f"/api/2.0/admin/finance/payments?{query_string}")
if not result:
logger.warning(f"No Splynx payment found for customer {customer_id}, payment intent {payment_intent_id}")
return {'success': False, 'error': 'No payment found to delete'}
logger.info(f"Found {len(result)} Splynx payment(s) to delete for customer {customer_id}")
delete_success = self.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: {customer_id}")
return {
'success': True,
'deleted_payment_id': result[0]['id'],
'customer_id': customer_id,
'payment_intent': payment_intent_id
}
else:
logger.error(f"Failed to delete Splynx Payment ID: {result[0]['id']} for customer: {customer_id}")
return {'success': False, 'error': 'Delete operation failed'}
except Exception as e:
logger.error(f"Error deleting Splynx payment for customer {customer_id}: {e}")
return {'success': False, 'error': str(e)}
def create_ticket(self, customer_id: int, subject: str, priority: str = "medium",
type_id: int = 1, group_id: int = 7, status_id: int = 1) -> Dict[str, Any]:
"""
Create a support ticket in Splynx.
Args:
customer_id: Customer ID in Splynx
subject: Ticket subject
priority: Ticket priority (low, medium, high)
type_id: Ticket type ID
group_id: Ticket group ID
status_id: Ticket status ID
Returns:
Dictionary with success status and ticket ID
"""
def _create():
return self.splynx.create_ticket(
customer_id=customer_id,
subject=subject,
priority=priority,
type_id=type_id,
group_id=group_id,
status_id=status_id
)
result = self._retry_operation(_create, f"Create ticket for customer {customer_id}")
if result and result.get('success'):
logger.info(f"Created ticket #{result['ticket_id']} for customer {customer_id}")
return result or {'success': False, 'error': 'Failed to create ticket'}
def add_ticket_message(self, ticket_id: int, message: str, is_admin: bool = False,
hide_for_customer: bool = False, message_type: str = "message") -> bool:
"""
Add a message to an existing ticket.
Args:
ticket_id: Ticket ID
message: Message content (HTML)
is_admin: Whether message is from admin
hide_for_customer: Whether to hide message from customer
message_type: Message type ("message" or "note")
Returns:
True if successful, False otherwise
"""
def _add():
return self.splynx.add_ticket_message(
ticket_id=ticket_id,
message=message,
is_admin=is_admin,
hide_for_customer=hide_for_customer,
message_type=message_type
)
result = self._retry_operation(_add, f"Add message to ticket {ticket_id}")
if result:
logger.info(f"Added {message_type} to ticket #{ticket_id}")
return True
return False

84
stripe_payment_processor.py

@ -224,7 +224,7 @@ class StripePaymentProcessor:
# Retrieve customer
customer = stripe.Customer.retrieve(customer_id)
print(f"customer: {json.dumps(customer,indent=2)}")
print(f"SPP- customer: {json.dumps(customer,indent=2)}")
if not customer:
response['error'] = f'Customer {customer_id} not found'
@ -252,7 +252,7 @@ class StripePaymentProcessor:
payment_method = stripe.PaymentMethod.retrieve(default_payment_method)
payment_method_type = payment_method.type
print(f"payment_method: {json.dumps(payment_method,indent=2)}")
#print(f"SPP - payment_method: {json.dumps(payment_method,indent=2)}")
response.update({
@ -296,8 +296,9 @@ class StripePaymentProcessor:
self._log('info', "Added BECS mandate data for offline acceptance")
# Create and confirm Payment Intent
#print(f"\nSPP - creating PI\n")
payment_intent = stripe.PaymentIntent.create(**payment_intent_params)
#print(f"\nSPP - payment_intent: {json.dumps(payment_intent,indent=2)}\n")
# Add payment intent details
response.update({
'payment_intent_id': payment_intent.id,
@ -306,7 +307,7 @@ class StripePaymentProcessor:
if payment_intent.status == 'succeeded':
response['success'] = True
self._log('info', f" Payment successful: {payment_intent.id}")
self._log('info', f"SUCCESS: Payment successful: {payment_intent.id}")
# Get actual fee details for successful payments
try:
@ -342,7 +343,7 @@ class StripePaymentProcessor:
self._log('warning', f"Failed to get actual fees: {str(e)} - marked for later update")
elif payment_intent.status == 'processing' and wait_for_completion:
# Payment is processing - wait for completion
self._log('info', f"💭 Payment is processing, waiting for completion...")
self._log('info', f"INFO: Payment is processing, waiting for completion...")
# Use the polling method to wait for completion
polling_result = self.wait_for_payment_completion(payment_intent.id, customer_id=customer_id, max_wait_seconds=30)
@ -354,7 +355,7 @@ class StripePaymentProcessor:
if polling_result['status'] == 'succeeded':
response['success'] = True
self._log('info', f" Payment completed successfully after polling")
self._log('info', f"SUCCESS: Payment completed successfully after polling")
else:
response['success'] = False
response['error'] = f'Payment completed with status: {polling_result["status"]}'
@ -391,7 +392,7 @@ class StripePaymentProcessor:
response['error'] = f'Payment not completed. Status: {payment_intent.status}'
response['error_type'] = 'payment_incomplete'
self._log('warning', f"⚠️ Payment incomplete: {payment_intent.id} - {payment_intent.status}")
self._log('warning', f"WARNING: Payment incomplete: {payment_intent.id} - {payment_intent.status}")
# Calculate processing time
processing_time = (datetime.now() - transaction_start).total_seconds()
@ -402,15 +403,27 @@ class StripePaymentProcessor:
except stripe.CardError as e:
# Card-specific error (declined, etc.)
processing_time = (datetime.now() - transaction_start).total_seconds()
#print(f"stripe.CardError: {str(e)}\n{e.user_message}\n{e.request_id}\n{e.code}")
#print(json.dumps(e, indent=2))
#print(f"SPP - stripe.CardError: {str(e)}\n{e.user_message}\n{e.request_id}\n{e.code}\n\n")
pi = stripe.PaymentIntent.list(customer=customer_id, limit=1)
if pi:
payment_intent_error = pi['data'][0]['id']
payment_intent_details = pi
print(f"\nSPP - payment_intent_error: {payment_intent_error}")
else:
payment_intent_error = "not found"
payment_intent_details = None
response.update({
'payment_intent_id': payment_intent_error,
'payment_intent_details': payment_intent_details,
'error': f'Card declined: {e.user_message}',
'error_type': 'card_declined',
'decline_code': e.code,
'processing_time_seconds': round(processing_time, 2)
})
self._log('error', f"❌ Card declined for {customer_id}: {e.user_message}")
self._log('error', f"ERROR: Card declined for {customer_id}: {e.user_message}")
return response
except stripe.InvalidRequestError as e:
@ -421,7 +434,7 @@ class StripePaymentProcessor:
'error_type': 'invalid_request',
'processing_time_seconds': round(processing_time, 2)
})
self._log('error', f" Invalid request for {customer_id}: {str(e)}")
self._log('error', f"ERROR: Invalid request for {customer_id}: {str(e)}")
return response
except stripe.AuthenticationError as e:
@ -432,7 +445,7 @@ class StripePaymentProcessor:
'error_type': 'authentication_error',
'processing_time_seconds': round(processing_time, 2)
})
self._log('error', f" Authentication failed: {str(e)}")
self._log('error', f"ERROR: Authentication failed: {str(e)}")
return response
except stripe.APIConnectionError as e:
@ -443,7 +456,7 @@ class StripePaymentProcessor:
'error_type': 'network_error',
'processing_time_seconds': round(processing_time, 2)
})
self._log('error', f" Network error: {str(e)}")
self._log('error', f"ERROR: Network error: {str(e)}")
return response
except stripe.StripeError as e:
@ -454,7 +467,7 @@ class StripePaymentProcessor:
'error_type': 'stripe_error',
'processing_time_seconds': round(processing_time, 2)
})
self._log('error', f" Stripe error: {str(e)}")
self._log('error', f"ERROR: Stripe error: {str(e)}")
return response
except Exception as e:
@ -465,7 +478,7 @@ class StripePaymentProcessor:
'error_type': 'unexpected_error',
'processing_time_seconds': round(processing_time, 2)
})
self._log('error', f" Unexpected error for {customer_id}: {str(e)}")
self._log('error', f"ERROR: Unexpected error for {customer_id}: {str(e)}")
return response
def get_customer_info(self, customer_id: str) -> Dict[str, Any]:
@ -497,14 +510,19 @@ class StripePaymentProcessor:
limit=10
)
#print(f"\nSPP - payment_methods: {json.dumps(payment_methods,indent=2)}\n")
for pm in payment_methods.data:
#print(f"\nSPP - pm: {json.dumps(pm,indent=2)}\n")
pm_info = {
'id': pm.id,
'type': pm.type,
'created': pm.created
}
if pm.card:
if pm_info['type'] == "card":
print("CARD TIME!")
pm_info['card'] = {
'brand': pm.card.brand,
'last4': pm.card.last4,
@ -512,12 +530,18 @@ class StripePaymentProcessor:
'exp_month': pm.card.exp_month,
'exp_year': pm.card.exp_year
}
elif pm.au_becs_debit:
elif pm_info['type'] == "au_becs_debit":
print("AU BECS DEBIT TIME!")
pm_info['au_becs_debit'] = {
'bsb_number': pm.au_becs_debit.bsb_number,
'last4': pm.au_becs_debit.last4
}
#print(pm.get('au_becs_debit', 'BECS_Nope'))
#print(pm.get('card', 'Card_Nope'))
#print(f"pm_info: {json.dumps(pm_info,indent=2)}")
customer_info['payment_methods'].append(pm_info)
return customer_info
@ -895,7 +919,7 @@ class StripePaymentProcessor:
if hasattr(refund, 'pending_reason') and refund.pending_reason:
response['pending_reason'] = refund.pending_reason
self._log('info', f" Refund status check successful: {refund_id} - {refund.status}")
self._log('info', f"SUCCESS: Refund status check successful: {refund_id} - {refund.status}")
return response
@ -1045,9 +1069,9 @@ class StripePaymentProcessor:
}
if current_status == 'succeeded':
self._log('info', f" Payment completed successfully after {elapsed_time:.1f}s ({attempts} attempts)")
self._log('info', f"SUCCESS: Payment completed successfully after {elapsed_time:.1f}s ({attempts} attempts)")
else:
self._log('warning', f" Payment completed with status '{current_status}' after {elapsed_time:.1f}s")
self._log('warning', f"ERROR: Payment completed with status '{current_status}' after {elapsed_time:.1f}s")
current_result['pi_status'] = current_status
return current_result
@ -1086,9 +1110,9 @@ class StripePaymentProcessor:
# If payment is still processing after timeout and we have customer_id, mark for later review
if final_status == 'processing' and customer_id:
final_result['needs_fee_update'] = [customer_id, payment_intent_id]
self._log('warning', f" Payment still processing after timeout - marked for later review")
self._log('warning', f"TIMEOUT: Payment still processing after timeout - marked for later review")
self._log('warning', f" Polling timed out after {max_wait_seconds}s. Final status: {final_status}")
self._log('warning', f"TIMEOUT: Polling timed out after {max_wait_seconds}s. Final status: {final_status}")
else:
# Error on final check
final_result['polling_info'] = {
@ -1105,7 +1129,7 @@ class StripePaymentProcessor:
# If we have customer_id and this might be a processing payment, mark for later
if customer_id:
final_result['needs_fee_update'] = [customer_id, payment_intent_id]
self._log('warning', f" Final check failed - marked for later review")
self._log('warning', f"TIMEOUT: Final check failed - marked for later review")
current_result['pi_status'] = final_status
return final_result
@ -1162,7 +1186,7 @@ class StripePaymentProcessor:
'timestamp': datetime.now().isoformat()
}
self._log('info', f" Setup intent created: {setup_intent.id}")
self._log('info', f"SUCCESS: Setup intent created: {setup_intent.id}")
return response
except stripe.StripeError as e:
@ -1240,7 +1264,7 @@ class StripePaymentProcessor:
}
response['payment_method'] = pm_details
self._log('info', f" Setup intent succeeded with payment method: {payment_method.id}")
self._log('info', f"SUCCESS: Setup intent succeeded with payment method: {payment_method.id}")
elif setup_intent.status in ['requires_payment_method', 'requires_confirmation']:
response['next_action'] = 'Setup still requires user action'
@ -1311,7 +1335,7 @@ class StripePaymentProcessor:
payment_method_id,
customer=customer_id
)
self._log('info', f" Payment method attached successfully")
self._log('info', f"SUCCESS: Payment method attached successfully")
except stripe.InvalidRequestError as e:
if 'already attached' in str(e).lower():
# Already attached, just retrieve it
@ -1498,21 +1522,21 @@ class StripePaymentProcessor:
if has_actual_fees and is_complete:
update_result['needs_further_updates'] = False
update_result['note'] = 'Payment complete with actual fees'
self._log('info', f" Payment {payment_intent_id} now complete with actual fees")
self._log('info', f"SUCCESS: Payment {payment_intent_id} now complete with actual fees")
elif is_complete and not has_actual_fees:
update_result['needs_further_updates'] = False
update_result['note'] = 'Payment complete but actual fees not available'
self._log('info', f" Payment {payment_intent_id} complete but no actual fees")
self._log('info', f"SUCCESS: Payment {payment_intent_id} complete but no actual fees")
elif has_actual_fees and not is_complete:
update_result['needs_further_updates'] = True
update_result['needs_fee_update'] = [customer_id, payment_intent_id] # Keep tracking
update_result['note'] = 'Has actual fees but payment still processing'
self._log('info', f" Payment {payment_intent_id} has fees but still processing")
self._log('info', f"PENDING: Payment {payment_intent_id} has fees but still processing")
else:
update_result['needs_further_updates'] = True
update_result['needs_fee_update'] = [customer_id, payment_intent_id] # Keep tracking
update_result['note'] = 'Payment still processing without actual fees'
self._log('info', f" Payment {payment_intent_id} still needs updates")
self._log('info', f"PENDING: Payment {payment_intent_id} still needs updates")
return update_result

42
test.py

@ -1,6 +1,7 @@
import json
import stripe
from typing import List, Dict, Union, Any
from datetime import datetime
from app import create_app, db
from models import Payments, PaymentBatch, SinglePayments, PaymentPlans
from splynx import Splynx, SPLYNX_URL, SPLYNX_KEY, SPLYNX_SECRET
@ -11,13 +12,44 @@ from config import Config
splynx = Splynx(url=SPLYNX_URL, key=SPLYNX_KEY, secret=SPLYNX_SECRET)
api_key = Config.STRIPE_LIVE_API_KEY
#api_key = Config.STRIPE_LIVE_API_KEY
api_key = Config.STRIPE_TEST_API_KEY
stripe.api_key = api_key
def attach_becs_payment_to_customer():
payment_method = stripe.PaymentMethod.create(
type='au_becs_debit',
au_becs_debit={
'bsb_number': '000000', # 6-digit BSB
'account_number': '333333335' # Account number
},
billing_details={
'name': 'AU BECS Decline',
'email': 'customer@example.com'
}
)
if __name__ == "__main__":
splynx_id = 1218789
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")
# Attach to customer
stripe.PaymentMethod.attach(
payment_method.id,
customer='cus_SoMVPWxdYstYbr'
)
print(f"Payment method {payment_method.id} attached to customer")
print(json.dumps(result,indent=2))
if __name__ == "__main__":
#splynx_id = 1218789
#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")
#
#print(json.dumps(result,indent=2))
#pi = stripe.PaymentIntent.list(customer="cus_SoQiDcSrNRxbPF", limit=1)
#
#print(json.dumps(pi,indent=2))
print(datetime.now())
splynx_date = datetime.now().strftime("%Y-%m-%d")
print(splynx_date)
print(type(splynx_date))
Loading…
Cancel
Save