445 lines
17 KiB
Python
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())
|