Files
igny8/backend/test_payment_workflow.py
2025-12-09 00:11:35 +00:00

445 lines
17 KiB
Python

#!/usr/bin/env python3
"""
End-to-End Payment Workflow Test Script
Tests the complete manual payment approval flow
"""
import os
import sys
import django
# Setup Django
sys.path.insert(0, os.path.dirname(__file__))
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'igny8_core.settings')
django.setup()
from django.contrib.auth import get_user_model
from django.db import transaction
from django.utils import timezone
from decimal import Decimal
from datetime import timedelta
from igny8_core.auth.models import Account, Subscription, Plan
from igny8_core.business.billing.models import (
Invoice, Payment, AccountPaymentMethod, CreditTransaction
)
from igny8_core.business.billing.services.invoice_service import InvoiceService
User = get_user_model()
class Colors:
HEADER = '\033[95m'
OKBLUE = '\033[94m'
OKCYAN = '\033[96m'
OKGREEN = '\033[92m'
WARNING = '\033[93m'
FAIL = '\033[91m'
ENDC = '\033[0m'
BOLD = '\033[1m'
def print_header(text):
print(f"\n{Colors.HEADER}{Colors.BOLD}{'='*60}{Colors.ENDC}")
print(f"{Colors.HEADER}{Colors.BOLD}{text:^60}{Colors.ENDC}")
print(f"{Colors.HEADER}{Colors.BOLD}{'='*60}{Colors.ENDC}\n")
def print_success(text):
print(f"{Colors.OKGREEN}{text}{Colors.ENDC}")
def print_error(text):
print(f"{Colors.FAIL}{text}{Colors.ENDC}")
def print_info(text):
print(f"{Colors.OKCYAN}{text}{Colors.ENDC}")
def cleanup_test_data():
"""Remove test data from previous runs"""
print_header("CLEANUP TEST DATA")
# Delete test accounts
test_emails = [
'workflow_test_free@example.com',
'workflow_test_paid@example.com'
]
for email in test_emails:
try:
user = User.objects.filter(email=email).first()
if user:
# Delete associated account (cascade will handle related objects)
account = Account.objects.filter(owner=user).first()
if account:
account.delete()
print_success(f"Deleted account for {email}")
user.delete()
print_success(f"Deleted user {email}")
except Exception as e:
print_error(f"Error cleaning up {email}: {e}")
def test_free_trial_signup():
"""Test free trial user registration"""
print_header("TEST 1: FREE TRIAL SIGNUP")
try:
# Get free plan
free_plan = Plan.objects.get(slug='free')
print_info(f"Free Plan: {free_plan.name} - {free_plan.included_credits} credits")
# Create user
with transaction.atomic():
user = User.objects.create_user(
username='workflow_test_free',
email='workflow_test_free@example.com',
password='TestPass123!',
first_name='Free',
last_name='Trial'
)
print_success(f"Created user: {user.email}")
# Create account
account = Account.objects.create(
name=f"{user.first_name}'s Account",
slug=f'free-trial-{timezone.now().timestamp()}',
owner=user,
plan=free_plan,
status='trial',
credits=free_plan.included_credits
)
print_success(f"Created account: {account.name} (ID: {account.id})")
# Create credit transaction
CreditTransaction.objects.create(
account=account,
transaction_type='plan_allocation',
amount=free_plan.included_credits,
balance_after=account.credits,
description=f'Initial credits from {free_plan.name} plan'
)
print_success(f"Allocated {free_plan.included_credits} credits")
# Verify
account.refresh_from_db()
assert account.status == 'trial', "Status should be 'trial'"
assert account.credits == 1000, "Credits should be 1000"
assert account.plan.slug == 'free', "Plan should be 'free'"
# Check no subscription or invoice created
sub_count = Subscription.objects.filter(account=account).count()
invoice_count = Invoice.objects.filter(account=account).count()
assert sub_count == 0, "Free trial should not have subscription"
assert invoice_count == 0, "Free trial should not have invoice"
print_success("No subscription created (correct for free trial)")
print_success("No invoice created (correct for free trial)")
print_success("\nFREE TRIAL TEST PASSED ✓")
return account
except Exception as e:
print_error(f"Free trial test failed: {e}")
raise
def test_paid_signup():
"""Test paid user registration with manual payment"""
print_header("TEST 2: PAID SIGNUP WORKFLOW")
try:
# Get starter plan
starter_plan = Plan.objects.get(slug='starter')
print_info(f"Starter Plan: {starter_plan.name} - ${starter_plan.price} - {starter_plan.included_credits} credits")
# Step 1: Create user with billing info
print_info("\nStep 1: User Registration")
with transaction.atomic():
user = User.objects.create_user(
username='workflow_test_paid',
email='workflow_test_paid@example.com',
password='TestPass123!',
first_name='Paid',
last_name='User'
)
print_success(f"Created user: {user.email}")
# Create account with billing info
account = Account.objects.create(
name=f"{user.first_name}'s Account",
slug=f'paid-user-{timezone.now().timestamp()}',
owner=user,
plan=starter_plan,
status='pending_payment',
credits=0, # No credits until payment approved
billing_email='billing@example.com',
billing_address_line1='123 Main Street',
billing_city='Karachi',
billing_country='PK'
)
print_success(f"Created account: {account.name} (ID: {account.id})")
print_info(f" Status: {account.status}")
print_info(f" Credits: {account.credits}")
# Create subscription
subscription = Subscription.objects.create(
account=account,
plan=starter_plan,
status='pending_payment',
current_period_start=timezone.now(),
current_period_end=timezone.now() + timedelta(days=30)
)
print_success(f"Created subscription (ID: {subscription.id})")
print_info(f" Status: {subscription.status}")
# Create invoice
invoice_service = InvoiceService()
invoice = invoice_service.create_subscription_invoice(
subscription=subscription,
billing_period_start=subscription.current_period_start,
billing_period_end=subscription.current_period_end
)
print_success(f"Created invoice: {invoice.invoice_number}")
print_info(f" Status: {invoice.status}")
print_info(f" Total: ${invoice.total}")
print_info(f" Has billing snapshot: {'billing_snapshot' in invoice.metadata}")
# Create payment method
payment_method = AccountPaymentMethod.objects.create(
account=account,
type='bank_transfer',
is_default=True
)
print_success(f"Created payment method: {payment_method.type}")
# Step 2: User submits payment confirmation
print_info("\nStep 2: User Payment Confirmation")
payment = Payment.objects.create(
invoice=invoice,
account=account,
amount=invoice.total,
currency=invoice.currency,
payment_method='bank_transfer',
status='pending_approval',
manual_reference='BT-TEST-20251208-001',
manual_notes='Test payment via ABC Bank'
)
print_success(f"Created payment (ID: {payment.id})")
print_info(f" Status: {payment.status}")
print_info(f" Reference: {payment.manual_reference}")
# Verify pending state
account.refresh_from_db()
subscription.refresh_from_db()
invoice.refresh_from_db()
assert account.status == 'pending_payment', "Account should be pending_payment"
assert account.credits == 0, "Credits should be 0 before approval"
assert subscription.status == 'pending_payment', "Subscription should be pending_payment"
assert invoice.status == 'pending', "Invoice should be pending"
assert payment.status == 'pending_approval', "Payment should be pending_approval"
print_success("\nPending state verified ✓")
# Step 3: Admin approves payment
print_info("\nStep 3: Admin Payment Approval")
# Create admin user for approval
admin_user = User.objects.filter(is_superuser=True).first()
if not admin_user:
admin_user = User.objects.create_superuser(
username='test_admin',
email='test_admin@example.com',
password='admin123',
first_name='Test',
last_name='Admin'
)
print_info(f"Created admin user: {admin_user.email}")
with transaction.atomic():
# Update payment
payment.status = 'succeeded'
payment.approved_by = admin_user
payment.approved_at = timezone.now()
payment.admin_notes = 'Verified in bank statement'
payment.save()
print_success("Payment approved")
# Update invoice
invoice.status = 'paid'
invoice.paid_at = timezone.now()
invoice.save()
print_success("Invoice marked as paid")
# Update subscription
subscription.status = 'active'
subscription.save()
print_success("Subscription activated")
# Update account and add credits
account.status = 'active'
account.credits = starter_plan.included_credits
account.save()
print_success(f"Account activated with {starter_plan.included_credits} credits")
# Log credit transaction
credit_txn = CreditTransaction.objects.create(
account=account,
transaction_type='plan_allocation',
amount=starter_plan.included_credits,
balance_after=account.credits,
description=f'Credits from approved payment (Invoice: {invoice.invoice_number})'
)
print_success("Credit transaction logged")
# Final verification
print_info("\nStep 4: Final Verification")
account.refresh_from_db()
subscription.refresh_from_db()
invoice.refresh_from_db()
payment.refresh_from_db()
assert account.status == 'active', "Account should be active"
assert account.credits == 1000, "Credits should be 1000"
assert subscription.status == 'active', "Subscription should be active"
assert invoice.status == 'paid', "Invoice should be paid"
assert payment.status == 'succeeded', "Payment should be succeeded"
assert payment.approved_by == admin_user, "Payment should have approved_by"
print_success(f"Account: {account.status}")
print_success(f"Credits: {account.credits}")
print_success(f"Subscription: {subscription.status}")
print_success(f"Invoice: {invoice.status}")
print_success(f"Payment: {payment.status}")
print_success(f"Approved by: {payment.approved_by.email}")
# Check credit transaction
txn = CreditTransaction.objects.filter(account=account).latest('created_at')
print_success(f"Credit Transaction: {txn.transaction_type} | {txn.amount} credits ✓")
print_success("\nPAID SIGNUP TEST PASSED ✓")
return account
except Exception as e:
print_error(f"Paid signup test failed: {e}")
raise
def test_payment_rejection():
"""Test payment rejection flow"""
print_header("TEST 3: PAYMENT REJECTION")
try:
# Use the paid account from previous test
account = Account.objects.get(owner__email='workflow_test_paid@example.com')
# Create a second invoice for testing rejection
print_info("Creating second invoice for rejection test")
subscription = Subscription.objects.get(account=account)
invoice_service = InvoiceService()
with transaction.atomic():
invoice2 = invoice_service.create_subscription_invoice(
subscription=subscription,
billing_period_start=subscription.current_period_start + timedelta(days=30),
billing_period_end=subscription.current_period_end + timedelta(days=30)
)
print_success(f"Created invoice: {invoice2.invoice_number}")
# Submit payment
payment2 = Payment.objects.create(
invoice=invoice2,
account=account,
amount=invoice2.total,
currency=invoice2.currency,
payment_method='bank_transfer',
status='pending_approval',
manual_reference='BT-INVALID-REF',
manual_notes='Test invalid payment reference'
)
print_success(f"Created payment (ID: {payment2.id})")
# Admin rejects payment
print_info("\nRejecting payment...")
admin_user = User.objects.filter(is_superuser=True).first()
with transaction.atomic():
payment2.status = 'failed'
payment2.approved_by = admin_user
payment2.approved_at = timezone.now()
payment2.admin_notes = 'Reference number not found in bank statement'
payment2.save()
print_success("Payment rejected")
# Verify rejection
payment2.refresh_from_db()
invoice2.refresh_from_db()
assert payment2.status == 'failed', "Payment should be failed"
assert invoice2.status == 'pending', "Invoice should remain pending"
print_success(f"Payment status: {payment2.status}")
print_success(f"Invoice status: {invoice2.status}")
print_success(f"Rejection reason: {payment2.admin_notes}")
print_success("\nPAYMENT REJECTION TEST PASSED ✓")
except Exception as e:
print_error(f"Payment rejection test failed: {e}")
raise
def print_summary():
"""Print test summary"""
print_header("TEST SUMMARY")
# Count accounts by status
from django.db.models import Count
status_counts = Account.objects.values('status').annotate(count=Count('id'))
print_info("Account Status Distribution:")
for item in status_counts:
print(f" {item['status']:20} {item['count']} account(s)")
# Count payments by status
payment_counts = Payment.objects.values('status').annotate(count=Count('id'))
print_info("\nPayment Status Distribution:")
for item in payment_counts:
print(f" {item['status']:20} {item['count']} payment(s)")
# Count subscriptions by status
sub_counts = Subscription.objects.values('status').annotate(count=Count('id'))
print_info("\nSubscription Status Distribution:")
for item in sub_counts:
print(f" {item['status']:20} {item['count']} subscription(s)")
print()
def main():
"""Run all tests"""
print_header("PAYMENT WORKFLOW E2E TEST SUITE")
print(f"{Colors.BOLD}Date: {timezone.now().strftime('%Y-%m-%d %H:%M:%S')}{Colors.ENDC}\n")
try:
# Cleanup
cleanup_test_data()
# Run tests
test_free_trial_signup()
test_paid_signup()
test_payment_rejection()
# Summary
print_summary()
# Final success
print_header("ALL TESTS PASSED ✓")
print(f"{Colors.OKGREEN}{Colors.BOLD}The payment workflow is functioning correctly!{Colors.ENDC}\n")
return 0
except Exception as e:
print_header("TESTS FAILED ✗")
print(f"{Colors.FAIL}{Colors.BOLD}Error: {e}{Colors.ENDC}\n")
import traceback
traceback.print_exc()
return 1
if __name__ == '__main__':
sys.exit(main())