project initialization
Some checks failed
System Monitoring / Health Checks (push) Has been cancelled
System Monitoring / Performance Monitoring (push) Has been cancelled
System Monitoring / Database Monitoring (push) Has been cancelled
System Monitoring / Cache Monitoring (push) Has been cancelled
System Monitoring / Log Monitoring (push) Has been cancelled
System Monitoring / Resource Monitoring (push) Has been cancelled
System Monitoring / Uptime Monitoring (push) Has been cancelled
System Monitoring / Backup Monitoring (push) Has been cancelled
System Monitoring / Security Monitoring (push) Has been cancelled
System Monitoring / Monitoring Dashboard (push) Has been cancelled
System Monitoring / Alerting (push) Has been cancelled
Security Scanning / Dependency Scanning (push) Has been cancelled
Security Scanning / Code Security Scanning (push) Has been cancelled
Security Scanning / Secrets Scanning (push) Has been cancelled
Security Scanning / Container Security Scanning (push) Has been cancelled
Security Scanning / Compliance Checking (push) Has been cancelled
Security Scanning / Security Dashboard (push) Has been cancelled
Security Scanning / Security Remediation (push) Has been cancelled

This commit is contained in:
2025-10-05 02:37:33 +08:00
parent 2cbb6d5fa1
commit b3fff546e9
226 changed files with 97805 additions and 35 deletions

View File

@@ -0,0 +1,996 @@
"""
Payment service for multi-tenant SaaS platform.
Handles payment processing, refunds, disputes, and Malaysian market
integration with Stripe, Midtrans, and local payment methods.
"""
import json
import uuid
from datetime import datetime, timedelta, timezone
from typing import Dict, List, Optional, Any, Tuple, Union
from decimal import Decimal
from django.conf import settings
from django.contrib.auth import get_user_model
from django.core.cache import cache
from django.core.mail import send_mail
from django.db import transaction
from django.utils import timezone
from django.db.models import Q, Sum, Count
from logging import getLogger
from ..models.payment import PaymentTransaction, PaymentMethodToken, Dispute
from ..models.tenant import Tenant
from ..models.subscription import Subscription
from ..exceptions import ValidationError, AuthenticationError, BusinessLogicError
User = get_user_model()
logger = getLogger(__name__)
class PaymentService:
"""
Service for managing payment operations including:
- Payment processing with multiple providers
- Payment method management
- Refunds and disputes
- Malaysian tax compliance (SST)
- Local payment methods integration
- Payment reporting and analytics
"""
def __init__(self):
self.payment_cache_prefix = 'payment:'
self.refund_cache_prefix = 'refund:'
self.cache_timeout = getattr(settings, 'PAYMENT_CACHE_TIMEOUT', 1800) # 30 minutes
# Malaysian SST rate
self.sst_rate = Decimal(getattr(settings, 'SST_RATE', '0.06')) # 6%
# Payment provider configurations
self.providers = {
'stripe': {
'enabled': getattr(settings, 'STRIPE_ENABLED', True),
'api_key': getattr(settings, 'STRIPE_API_KEY', ''),
'webhook_secret': getattr(settings, 'STRIPE_WEBHOOK_SECRET', ''),
},
'midtrans': {
'enabled': getattr(settings, 'MIDTRANS_ENABLED', True),
'server_key': getattr(settings, 'MIDTRANS_SERVER_KEY', ''),
'client_key': getattr(settings, 'MIDTRANS_CLIENT_KEY', ''),
},
'fpx': {
'enabled': getattr(settings, 'FPX_ENABLED', True),
'merchant_id': getattr(settings, 'FPX_MERCHANT_ID', ''),
},
'touch_n_go': {
'enabled': getattr(settings, 'TOUCH_N_GO_ENABLED', True),
'merchant_id': getattr(settings, 'TOUCH_N_GO_MERCHANT_ID', ''),
},
'grabpay': {
'enabled': getattr(settings, 'GRABPAY_ENABLED', True),
'merchant_id': getattr(settings, 'GRABPAY_MERCHANT_ID', ''),
},
}
# Malaysian payment methods
self.malaysian_payment_methods = [
'credit_card', 'debit_card', 'fpx', 'online_banking',
'touch_n_go', 'grabpay', 'boost', 'shopee_pay'
]
@transaction.atomic
def process_payment(
self,
tenant_id: str,
amount: Decimal,
currency: str = 'MYR',
payment_method: str = 'stripe',
payment_method_token: Optional[str] = None,
description: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
customer_email: Optional[str] = None,
subscription_id: Optional[str] = None
) -> PaymentTransaction:
"""
Process a payment transaction.
Args:
tenant_id: Tenant ID
amount: Payment amount
currency: Currency code (default: MYR)
payment_method: Payment method
payment_method_token: Payment method token
description: Payment description
metadata: Additional metadata
customer_email: Customer email
subscription_id: Associated subscription ID
Returns:
Created PaymentTransaction instance
Raises:
ValidationError: If validation fails
BusinessLogicError: If payment processing fails
"""
# Validate inputs
if amount <= 0:
raise ValidationError("Amount must be positive")
if currency != 'MYR':
raise ValidationError("Only MYR currency is supported")
if payment_method not in self.providers or not self.providers[payment_method]['enabled']:
raise ValidationError(f"Payment method {payment_method} not supported")
try:
tenant = Tenant.objects.get(id=tenant_id)
except Tenant.DoesNotExist:
raise AuthenticationError("Tenant not found")
# Calculate SST if applicable
sst_amount = amount * self.sst_rate
total_amount = amount + sst_amount
# Create payment transaction
transaction_id = self._generate_transaction_id()
payment_transaction = PaymentTransaction.objects.create(
tenant=tenant,
transaction_id=transaction_id,
amount=amount,
currency=currency,
sst_amount=sst_amount,
total_amount=total_amount,
payment_method=payment_method,
payment_method_token=payment_method_token,
status='pending',
description=description or f'Payment for {tenant.name}',
metadata=metadata or {},
customer_email=customer_email or tenant.email,
subscription_id=subscription_id,
)
# Process payment with provider
try:
provider_result = self._process_with_provider(payment_transaction)
if provider_result['success']:
payment_transaction.status = 'completed'
payment_transaction.provider_transaction_id = provider_result['transaction_id']
payment_transaction.provider_response = provider_result['response']
payment_transaction.completed_at = timezone.now()
# Update associated subscription if applicable
if subscription_id:
self._update_subscription_payment(subscription_id, payment_transaction)
# Send payment confirmation
self._send_payment_confirmation(payment_transaction)
logger.info(f"Payment processed successfully: {transaction_id}")
else:
payment_transaction.status = 'failed'
payment_transaction.failure_reason = provider_result['error']
payment_transaction.failed_at = timezone.now()
logger.error(f"Payment failed: {transaction_id}, error: {provider_result['error']}")
except Exception as e:
payment_transaction.status = 'failed'
payment_transaction.failure_reason = str(e)
payment_transaction.failed_at = timezone.now()
logger.error(f"Payment processing error: {transaction_id}, error: {str(e)}")
payment_transaction.save()
return payment_transaction
def process_refund(
self,
transaction_id: str,
amount: Optional[Decimal] = None,
reason: str = 'customer_request',
metadata: Optional[Dict[str, Any]] = None
) -> PaymentTransaction:
"""
Process a refund for a payment transaction.
Args:
transaction_id: Original transaction ID
amount: Refund amount (None for full refund)
reason: Refund reason
metadata: Additional metadata
Returns:
Created refund PaymentTransaction instance
Raises:
ValidationError: If validation fails
BusinessLogicError: If refund cannot be processed
"""
try:
original_transaction = PaymentTransaction.objects.get(transaction_id=transaction_id)
except PaymentTransaction.DoesNotExist:
raise AuthenticationError("Original transaction not found")
if original_transaction.status != 'completed':
raise BusinessLogicError("Cannot refund non-completed transaction")
if original_transaction.refund_status == 'fully_refunded':
raise BusinessLogicError("Transaction already fully refunded")
# Calculate refund amount
if amount is None:
# Full refund
refund_amount = original_transaction.total_amount
sst_refund_amount = original_transaction.sst_amount
else:
# Partial refund
if amount <= 0:
raise ValidationError("Refund amount must be positive")
if amount > original_transaction.total_amount:
raise ValidationError("Refund amount cannot exceed original amount")
# Calculate proportional SST refund
sst_refund_amount = (amount / original_transaction.total_amount) * original_transaction.sst_amount
refund_amount = amount
# Check if partial refund is possible
if original_transaction.refund_status == 'partially_refunded':
already_refunded = PaymentTransaction.objects.filter(
original_transaction=original_transaction,
transaction_type='refund',
status='completed'
).aggregate(total=Sum('amount'))['total'] or Decimal('0')
if (already_refunded + refund_amount) > original_transaction.total_amount:
raise BusinessLogicError("Total refunds would exceed original amount")
# Create refund transaction
refund_transaction_id = self._generate_transaction_id()
refund_transaction = PaymentTransaction.objects.create(
tenant=original_transaction.tenant,
transaction_id=refund_transaction_id,
transaction_type='refund',
amount=refund_amount,
currency=original_transaction.currency,
sst_amount=sst_refund_amount,
total_amount=refund_amount,
payment_method=original_transaction.payment_method,
original_transaction=original_transaction,
status='pending',
description=f'Refund for {original_transaction.description}',
metadata=metadata or {},
customer_email=original_transaction.customer_email,
refund_reason=reason,
)
# Process refund with provider
try:
refund_result = self._process_refund_with_provider(original_transaction, refund_transaction)
if refund_result['success']:
refund_transaction.status = 'completed'
refund_transaction.provider_transaction_id = refund_result['transaction_id']
refund_transaction.provider_response = refund_result['response']
refund_transaction.completed_at = timezone.now()
# Update original transaction refund status
if amount is None or (original_transaction.total_amount - refund_amount) <= 0:
original_transaction.refund_status = 'fully_refunded'
else:
original_transaction.refund_status = 'partially_refunded'
original_transaction.save()
# Send refund confirmation
self._send_refund_confirmation(refund_transaction)
logger.info(f"Refund processed successfully: {refund_transaction_id}")
else:
refund_transaction.status = 'failed'
refund_transaction.failure_reason = refund_result['error']
refund_transaction.failed_at = timezone.now()
logger.error(f"Refund failed: {refund_transaction_id}, error: {refund_result['error']}")
except Exception as e:
refund_transaction.status = 'failed'
refund_transaction.failure_reason = str(e)
refund_transaction.failed_at = timezone.now()
logger.error(f"Refund processing error: {refund_transaction_id}, error: {str(e)}")
refund_transaction.save()
return refund_transaction
def create_dispute(
self,
transaction_id: str,
reason: str,
description: str,
amount: Optional[Decimal] = None,
evidence: Optional[Dict[str, Any]] = None
) -> Dispute:
"""
Create a payment dispute.
Args:
transaction_id: Transaction ID
reason: Dispute reason
description: Dispute description
amount: Disputed amount (None for full amount)
evidence: Dispute evidence
Returns:
Created Dispute instance
Raises:
ValidationError: If validation fails
BusinessLogicError: If dispute cannot be created
"""
try:
transaction = PaymentTransaction.objects.get(transaction_id=transaction_id)
except PaymentTransaction.DoesNotExist:
raise AuthenticationError("Transaction not found")
if transaction.status != 'completed':
raise BusinessLogicError("Cannot dispute non-completed transaction")
if transaction.transaction_type == 'refund':
raise BusinessLogicError("Cannot dispute refund transactions")
# Check if dispute already exists
if Dispute.objects.filter(transaction=transaction, status__in=['pending', 'under_review']).exists():
raise BusinessLogicError("Dispute already exists for this transaction")
# Calculate dispute amount
dispute_amount = amount or transaction.total_amount
if dispute_amount <= 0:
raise ValidationError("Dispute amount must be positive")
if dispute_amount > transaction.total_amount:
raise ValidationError("Dispute amount cannot exceed transaction amount")
# Create dispute
dispute = Dispute.objects.create(
transaction=transaction,
reason=reason,
description=description,
amount=dispute_amount,
evidence=evidence or {},
status='pending',
created_by=getattr(transaction, 'created_by', None),
)
# Update transaction status
transaction.dispute_status = 'under_review'
transaction.save()
# Process dispute with provider
try:
dispute_result = self._process_dispute_with_provider(transaction, dispute)
if dispute_result['success']:
dispute.provider_dispute_id = dispute_result['dispute_id']
dispute.provider_response = dispute_result['response']
dispute.status = 'under_review'
logger.info(f"Dispute created successfully: {dispute.id}")
else:
dispute.status = 'failed'
dispute.failure_reason = dispute_result['error']
transaction.dispute_status = 'no_dispute'
transaction.save()
logger.error(f"Dispute creation failed: {dispute.id}, error: {dispute_result['error']}")
except Exception as e:
dispute.status = 'failed'
dispute.failure_reason = str(e)
transaction.dispute_status = 'no_dispute'
transaction.save()
logger.error(f"Dispute processing error: {dispute.id}, error: {str(e)}")
dispute.save()
return dispute
def add_payment_method(
self,
tenant_id: str,
payment_method_type: str,
payment_method_data: Dict[str, Any],
is_default: bool = False,
nickname: Optional[str] = None
) -> PaymentMethodToken:
"""
Add a payment method for a tenant.
Args:
tenant_id: Tenant ID
payment_method_type: Payment method type
payment_method_data: Payment method data
is_default: Whether to set as default
nickname: Payment method nickname
Returns:
Created PaymentMethodToken instance
Raises:
ValidationError: If validation fails
BusinessLogicError: If payment method cannot be added
"""
try:
tenant = Tenant.objects.get(id=tenant_id)
except Tenant.DoesNotExist:
raise AuthenticationError("Tenant not found")
if payment_method_type not in self.providers:
raise ValidationError(f"Unsupported payment method type: {payment_method_type}")
# Tokenize payment method with provider
try:
token_result = self._tokenize_payment_method(payment_method_type, payment_method_data)
if not token_result['success']:
raise BusinessLogicError(f"Tokenization failed: {token_result['error']}")
except Exception as e:
raise BusinessLogicError(f"Payment method tokenization failed: {str(e)}")
# Create payment method token
payment_method = PaymentMethodToken.objects.create(
tenant=tenant,
payment_method_type=payment_method_type,
token=token_result['token'],
last_four=token_result.get('last_four', ''),
expiry_month=token_result.get('expiry_month'),
expiry_year=token_result.get('expiry_year'),
card_type=token_result.get('card_type'),
is_default=is_default,
nickname=nickname or f"{payment_method_type.title()} Card",
provider_response=token_result.get('response', {}),
)
# If set as default, update other payment methods
if is_default:
PaymentMethodToken.objects.filter(
tenant=tenant,
is_default=True
).exclude(id=payment_method.id).update(is_default=False)
logger.info(f"Added payment method for tenant {tenant_id}")
return payment_method
def remove_payment_method(self, payment_method_id: str, tenant_id: str) -> bool:
"""
Remove a payment method.
Args:
payment_method_id: Payment method ID
tenant_id: Tenant ID
Returns:
True if removal successful
Raises:
AuthenticationError: If payment method not found
BusinessLogicError: If payment method cannot be removed
"""
try:
payment_method = PaymentMethodToken.objects.get(
id=payment_method_id,
tenant_id=tenant_id,
is_active=True
)
except PaymentMethodToken.DoesNotExist:
raise AuthenticationError("Payment method not found")
# Check if payment method is used in active subscriptions
active_subscriptions = Subscription.objects.filter(
tenant_id=tenant_id,
payment_method_token=payment_method.token,
status__in=['active', 'trial']
).exists()
if active_subscriptions:
raise BusinessLogicError("Cannot remove payment method used in active subscriptions")
# Deactivate payment method
payment_method.is_active = False
payment_method.deactivated_at = timezone.now()
payment_method.save()
# If this was the default, set another as default
if payment_method.is_default:
remaining_methods = PaymentMethodToken.objects.filter(
tenant_id=tenant_id,
is_active=True
).first()
if remaining_methods:
remaining_methods.is_default = True
remaining_methods.save()
logger.info(f"Removed payment method {payment_method_id}")
return True
def get_payment_methods(self, tenant_id: str) -> List[PaymentMethodToken]:
"""
Get active payment methods for a tenant.
Args:
tenant_id: Tenant ID
Returns:
List of PaymentMethodToken instances
"""
return list(PaymentMethodToken.objects.filter(
tenant_id=tenant_id,
is_active=True
).order_by('-is_default', '-created_at'))
def get_payment_history(
self,
tenant_id: str,
start_date: Optional[datetime] = None,
end_date: Optional[datetime] = None,
status: Optional[str] = None,
transaction_type: Optional[str] = None,
limit: int = 50,
offset: int = 0
) -> Tuple[List[PaymentTransaction], int]:
"""
Get payment history for a tenant.
Args:
tenant_id: Tenant ID
start_date: Start date filter
end_date: End date filter
status: Status filter
transaction_type: Transaction type filter
limit: Result limit
offset: Result offset
Returns:
Tuple of (transactions list, total count)
"""
try:
tenant = Tenant.objects.get(id=tenant_id)
except Tenant.DoesNotExist:
raise AuthenticationError("Tenant not found")
queryset = PaymentTransaction.objects.filter(tenant=tenant)
if start_date:
queryset = queryset.filter(created_at__gte=start_date)
if end_date:
queryset = queryset.filter(created_at__lte=end_date)
if status:
queryset = queryset.filter(status=status)
if transaction_type:
queryset = queryset.filter(transaction_type=transaction_type)
total_count = queryset.count()
transactions = queryset[offset:offset + limit]
return list(transactions), total_count
def get_payment_statistics(
self,
tenant_id: str,
start_date: Optional[datetime] = None,
end_date: Optional[datetime] = None
) -> Dict[str, Any]:
"""
Get payment statistics for a tenant.
Args:
tenant_id: Tenant ID
start_date: Start date filter
end_date: End date filter
Returns:
Dictionary with payment statistics
"""
cache_key = f"{self.payment_cache_prefix}{tenant_id}:stats"
stats = cache.get(cache_key)
if stats is None:
try:
tenant = Tenant.objects.get(id=tenant_id)
except Tenant.DoesNotExist:
return {}
queryset = PaymentTransaction.objects.filter(tenant=tenant)
if start_date:
queryset = queryset.filter(created_at__gte=start_date)
if end_date:
queryset = queryset.filter(created_at__lte=end_date)
# Calculate statistics
total_payments = queryset.filter(
transaction_type='payment',
status='completed'
).aggregate(
total_amount=Sum('total_amount'),
total_sst=Sum('sst_amount'),
count=Count('id')
)
total_refunds = queryset.filter(
transaction_type='refund',
status='completed'
).aggregate(
total_amount=Sum('total_amount'),
total_sst=Sum('sst_amount'),
count=Count('id')
)
active_disputes = Dispute.objects.filter(
transaction__tenant=tenant,
status__in=['pending', 'under_review']
).count()
payment_methods = PaymentMethodToken.objects.filter(
tenant=tenant,
is_active=True
).count()
stats = {
'tenant_id': str(tenant.id),
'total_payments_amount': float(total_payments['total_amount'] or 0),
'total_payments_sst': float(total_payments['total_sst'] or 0),
'total_payments_count': total_payments['count'] or 0,
'total_refunds_amount': float(total_refunds['total_amount'] or 0),
'total_refunds_sst': float(total_refunds['total_sst'] or 0),
'total_refunds_count': total_refunds['count'] or 0,
'net_amount': float((total_payments['total_amount'] or 0) - (total_refunds['total_amount'] or 0)),
'active_disputes': active_disputes,
'active_payment_methods': payment_methods,
'period_start': start_date.isoformat() if start_date else None,
'period_end': end_date.isoformat() if end_date else None,
}
cache.set(cache_key, stats, timeout=self.cache_timeout)
return stats
def retry_failed_payment(self, transaction_id: str) -> PaymentTransaction:
"""
Retry a failed payment transaction.
Args:
transaction_id: Transaction ID
Returns:
Updated PaymentTransaction instance
Raises:
AuthenticationError: If transaction not found
BusinessLogicError: If retry not allowed
"""
try:
transaction = PaymentTransaction.objects.get(transaction_id=transaction_id)
except PaymentTransaction.DoesNotExist:
raise AuthenticationError("Transaction not found")
if transaction.status != 'failed':
raise BusinessLogicError("Can only retry failed transactions")
# Check retry limit
retry_count = PaymentTransaction.objects.filter(
original_transaction=transaction,
transaction_type='retry'
).count()
if retry_count >= 3:
raise BusinessLogicError("Maximum retry attempts exceeded")
# Create retry transaction
retry_transaction = PaymentTransaction.objects.create(
tenant=transaction.tenant,
transaction_id=self._generate_transaction_id(),
transaction_type='retry',
amount=transaction.amount,
currency=transaction.currency,
sst_amount=transaction.sst_amount,
total_amount=transaction.total_amount,
payment_method=transaction.payment_method,
payment_method_token=transaction.payment_method_token,
original_transaction=transaction,
status='pending',
description=f'Retry of {transaction.description}',
metadata=transaction.metadata.copy(),
customer_email=transaction.customer_email,
subscription_id=transaction.subscription_id,
)
# Process payment
try:
provider_result = self._process_with_provider(retry_transaction)
if provider_result['success']:
retry_transaction.status = 'completed'
retry_transaction.provider_transaction_id = provider_result['transaction_id']
retry_transaction.provider_response = provider_result['response']
retry_transaction.completed_at = timezone.now()
# Update original transaction
transaction.retry_status = 'retried_successfully'
transaction.save()
# Update associated subscription
if transaction.subscription_id:
self._update_subscription_payment(transaction.subscription_id, retry_transaction)
# Send payment confirmation
self._send_payment_confirmation(retry_transaction)
logger.info(f"Payment retry successful: {retry_transaction.transaction_id}")
else:
retry_transaction.status = 'failed'
retry_transaction.failure_reason = provider_result['error']
retry_transaction.failed_at = timezone.now()
# Update original transaction
transaction.retry_status = 'retry_failed'
transaction.save()
logger.error(f"Payment retry failed: {retry_transaction.transaction_id}")
except Exception as e:
retry_transaction.status = 'failed'
retry_transaction.failure_reason = str(e)
retry_transaction.failed_at = timezone.now()
transaction.retry_status = 'retry_failed'
transaction.save()
logger.error(f"Payment retry error: {retry_transaction.transaction_id}")
retry_transaction.save()
return retry_transaction
# Helper methods
def _generate_transaction_id(self) -> str:
"""Generate unique transaction ID."""
return f"TRX{uuid.uuid4().hex[:16].upper()}"
def _process_with_provider(self, transaction: PaymentTransaction) -> Dict[str, Any]:
"""Process payment with appropriate provider."""
provider = transaction.payment_method
if provider == 'stripe':
return self._process_stripe_payment(transaction)
elif provider == 'midtrans':
return self._process_midtrans_payment(transaction)
elif provider in ['fpx', 'touch_n_go', 'grabpay']:
return self._process_local_payment(transaction)
else:
return {'success': False, 'error': f'Unsupported provider: {provider}'}
def _process_stripe_payment(self, transaction: PaymentTransaction) -> Dict[str, Any]:
"""Process payment with Stripe."""
# This would integrate with Stripe Python SDK
# For now, simulate successful payment
return {
'success': True,
'transaction_id': f"ch_{uuid.uuid4().hex[:16]}",
'response': {'status': 'succeeded'}
}
def _process_midtrans_payment(self, transaction: PaymentTransaction) -> Dict[str, Any]:
"""Process payment with Midtrans."""
# This would integrate with Midtrans Python SDK
# For now, simulate successful payment
return {
'success': True,
'transaction_id': f"MIDTRANS-{uuid.uuid4().hex[:12].upper()}",
'response': {'status_code': '200', 'transaction_status': 'settlement'}
}
def _process_local_payment(self, transaction: PaymentTransaction) -> Dict[str, Any]:
"""Process payment with local Malaysian providers."""
# This would integrate with FPX, Touch 'n Go, GrabPay APIs
# For now, simulate successful payment
return {
'success': True,
'transaction_id': f"LOCAL-{uuid.uuid4().hex[:12].upper()}",
'response': {'status': 'success'}
}
def _process_refund_with_provider(
self,
original_transaction: PaymentTransaction,
refund_transaction: PaymentTransaction
) -> Dict[str, Any]:
"""Process refund with provider."""
provider = original_transaction.payment_method
if provider == 'stripe':
return self._process_stripe_refund(original_transaction, refund_transaction)
elif provider == 'midtrans':
return self._process_midtrans_refund(original_transaction, refund_transaction)
else:
return {'success': False, 'error': f'Refunds not supported for provider: {provider}'}
def _process_stripe_refund(
self,
original_transaction: PaymentTransaction,
refund_transaction: PaymentTransaction
) -> Dict[str, Any]:
"""Process refund with Stripe."""
# This would integrate with Stripe Python SDK
return {
'success': True,
'transaction_id': f"re_{uuid.uuid4().hex[:16]}",
'response': {'status': 'succeeded'}
}
def _process_midtrans_refund(
self,
original_transaction: PaymentTransaction,
refund_transaction: PaymentTransaction
) -> Dict[str, Any]:
"""Process refund with Midtrans."""
# This would integrate with Midtrans Python SDK
return {
'success': True,
'transaction_id': f"MIDTRANS-REF-{uuid.uuid4().hex[:8].upper()}",
'response': {'status_code': '200'}
}
def _process_dispute_with_provider(
self,
transaction: PaymentTransaction,
dispute: Dispute
) -> Dict[str, Any]:
"""Process dispute with provider."""
provider = transaction.payment_method
if provider == 'stripe':
return self._process_stripe_dispute(transaction, dispute)
elif provider == 'midtrans':
return self._process_midtrans_dispute(transaction, dispute)
else:
return {'success': False, 'error': f'Disputes not supported for provider: {provider}'}
def _process_stripe_dispute(
self,
transaction: PaymentTransaction,
dispute: Dispute
) -> Dict[str, Any]:
"""Process dispute with Stripe."""
# This would integrate with Stripe Python SDK
return {
'success': True,
'dispute_id': f"dp_{uuid.uuid4().hex[:16]}",
'response': {'status': 'needs_response'}
}
def _process_midtrans_dispute(
self,
transaction: PaymentTransaction,
dispute: Dispute
) -> Dict[str, Any]:
"""Process dispute with Midtrans."""
# This would integrate with Midtrans Python SDK
return {
'success': True,
'dispute_id': f"MIDTRANS-DISP-{uuid.uuid4().hex[:8].upper()}",
'response': {'status': 'pending'}
}
def _tokenize_payment_method(self, payment_method_type: str, payment_method_data: Dict[str, Any]) -> Dict[str, Any]:
"""Tokenize payment method with provider."""
if payment_method_type == 'stripe':
return self._tokenize_stripe_payment_method(payment_method_data)
elif payment_method_type == 'midtrans':
return self._tokenize_midtrans_payment_method(payment_method_data)
else:
return {'success': False, 'error': f'Tokenization not supported for: {payment_method_type}'}
def _tokenize_stripe_payment_method(self, payment_method_data: Dict[str, Any]) -> Dict[str, Any]:
"""Tokenize payment method with Stripe."""
# This would integrate with Stripe Python SDK
return {
'success': True,
'token': f"pm_{uuid.uuid4().hex[:16]}",
'last_four': payment_method_data.get('number', '')[-4:],
'expiry_month': payment_method_data.get('exp_month'),
'expiry_year': payment_method_data.get('exp_year'),
'card_type': payment_method_data.get('brand', 'visa'),
'response': {'id': f"pm_{uuid.uuid4().hex[:16]}"}
}
def _tokenize_midtrans_payment_method(self, payment_method_data: Dict[str, Any]) -> Dict[str, Any]:
"""Tokenize payment method with Midtrans."""
# This would integrate with Midtrans Python SDK
return {
'success': True,
'token': f"MIDTRANS-TOKEN-{uuid.uuid4().hex[:12].upper()}",
'last_four': payment_method_data.get('number', '')[-4:],
'response': {'token_id': f"MIDTRANS-TOKEN-{uuid.uuid4().hex[:12].upper()}"}
}
def _update_subscription_payment(self, subscription_id: str, payment_transaction: PaymentTransaction):
"""Update subscription with payment information."""
try:
subscription = Subscription.objects.get(id=subscription_id)
subscription.last_payment_at = payment_transaction.completed_at
subscription.last_payment_amount = payment_transaction.total_amount
subscription.save()
except Subscription.DoesNotExist:
logger.warning(f"Subscription {subscription_id} not found for payment update")
def _send_payment_confirmation(self, transaction: PaymentTransaction):
"""Send payment confirmation email."""
subject = f"Payment Confirmation - {transaction.transaction_id}"
message = f"""
Dear {transaction.tenant.name},
Your payment has been processed successfully!
Payment Details:
- Transaction ID: {transaction.transaction_id}
- Amount: RM{transaction.total_amount:.2f}
- SST (6%): RM{transaction.sst_amount:.2f}
- Payment Method: {transaction.payment_method.title()}
- Date: {transaction.completed_at.strftime('%Y-%m-%d %H:%M:%S')}
Thank you for your payment!
Best regards,
The {settings.APP_NAME} Team
"""
try:
send_mail(
subject=subject,
message=message,
from_email=getattr(settings, 'DEFAULT_FROM_EMAIL', 'noreply@example.com'),
recipient_list=[transaction.customer_email],
fail_silently=False,
)
except Exception as e:
logger.error(f"Failed to send payment confirmation: {str(e)}")
def _send_refund_confirmation(self, transaction: PaymentTransaction):
"""Send refund confirmation email."""
subject = f"Refund Confirmation - {transaction.transaction_id}"
message = f"""
Dear {transaction.tenant.name},
Your refund has been processed successfully!
Refund Details:
- Refund Transaction ID: {transaction.transaction_id}
- Original Transaction ID: {transaction.original_transaction.transaction_id}
- Refund Amount: RM{transaction.total_amount:.2f}
- SST Refunded: RM{transaction.sst_amount:.2f}
- Refund Reason: {transaction.refund_reason}
- Date: {transaction.completed_at.strftime('%Y-%m-%d %H:%M:%S')}
The refund should appear in your account within 5-7 business days.
Best regards,
The {settings.APP_NAME} Team
"""
try:
send_mail(
subject=subject,
message=message,
from_email=getattr(settings, 'DEFAULT_FROM_EMAIL', 'noreply@example.com'),
recipient_list=[transaction.customer_email],
fail_silently=False,
)
except Exception as e:
logger.error(f"Failed to send refund confirmation: {str(e)}")
# Global payment service instance
payment_service = PaymentService()