fixing and creatign mess

This commit is contained in:
IGNY8 VPS (Salman)
2026-01-07 10:19:34 +00:00
parent 0386d4bf33
commit ad1756c349
11 changed files with 1067 additions and 188 deletions

View File

@@ -606,7 +606,7 @@ class BillingViewSet(viewsets.GenericViewSet):
class InvoiceViewSet(AccountModelViewSet):
"""ViewSet for user-facing invoices"""
queryset = Invoice.objects.all().select_related('account')
queryset = Invoice.objects.all().select_related('account', 'subscription', 'subscription__plan')
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess]
pagination_class = CustomPageNumberPagination
@@ -617,6 +617,43 @@ class InvoiceViewSet(AccountModelViewSet):
queryset = queryset.filter(account=self.request.account)
return queryset.order_by('-invoice_date', '-created_at')
def _serialize_invoice(self, invoice):
"""Serialize an invoice with all needed fields"""
# Build subscription data if exists
subscription_data = None
if invoice.subscription:
plan_data = None
if invoice.subscription.plan:
plan_data = {
'id': invoice.subscription.plan.id,
'name': invoice.subscription.plan.name,
'slug': invoice.subscription.plan.slug,
}
subscription_data = {
'id': invoice.subscription.id,
'plan': plan_data,
}
return {
'id': invoice.id,
'invoice_number': invoice.invoice_number,
'status': invoice.status,
'total': str(invoice.total), # Alias for compatibility
'total_amount': str(invoice.total),
'subtotal': str(invoice.subtotal),
'tax_amount': str(invoice.tax),
'currency': invoice.currency,
'invoice_date': invoice.invoice_date.isoformat(),
'due_date': invoice.due_date.isoformat(),
'paid_at': invoice.paid_at.isoformat() if invoice.paid_at else None,
'line_items': invoice.line_items,
'billing_email': invoice.billing_email,
'notes': invoice.notes,
'payment_method': invoice.payment_method,
'subscription': subscription_data,
'created_at': invoice.created_at.isoformat(),
}
def list(self, request):
"""List invoices for current account"""
queryset = self.get_queryset()
@@ -630,25 +667,7 @@ class InvoiceViewSet(AccountModelViewSet):
page = paginator.paginate_queryset(queryset, request)
# Serialize invoice data
results = []
for invoice in (page if page is not None else []):
results.append({
'id': invoice.id,
'invoice_number': invoice.invoice_number,
'status': invoice.status,
'total': str(invoice.total), # Alias for compatibility
'total_amount': str(invoice.total),
'subtotal': str(invoice.subtotal),
'tax_amount': str(invoice.tax),
'currency': invoice.currency,
'invoice_date': invoice.invoice_date.isoformat(),
'due_date': invoice.due_date.isoformat(),
'paid_at': invoice.paid_at.isoformat() if invoice.paid_at else None,
'line_items': invoice.line_items,
'billing_email': invoice.billing_email,
'notes': invoice.notes,
'created_at': invoice.created_at.isoformat(),
})
results = [self._serialize_invoice(invoice) for invoice in (page if page is not None else [])]
return paginated_response(
{'count': paginator.page.paginator.count, 'next': paginator.get_next_link(), 'previous': paginator.get_previous_link(), 'results': results},
@@ -659,24 +678,7 @@ class InvoiceViewSet(AccountModelViewSet):
"""Get invoice detail"""
try:
invoice = self.get_queryset().get(pk=pk)
data = {
'id': invoice.id,
'invoice_number': invoice.invoice_number,
'status': invoice.status,
'total': str(invoice.total), # Alias for compatibility
'total_amount': str(invoice.total),
'subtotal': str(invoice.subtotal),
'tax_amount': str(invoice.tax),
'currency': invoice.currency,
'invoice_date': invoice.invoice_date.isoformat(),
'due_date': invoice.due_date.isoformat(),
'paid_at': invoice.paid_at.isoformat() if invoice.paid_at else None,
'line_items': invoice.line_items,
'billing_email': invoice.billing_email,
'notes': invoice.notes,
'created_at': invoice.created_at.isoformat(),
}
return success_response(data=data, request=request)
return success_response(data=self._serialize_invoice(invoice), request=request)
except Invoice.DoesNotExist:
return error_response(error='Invoice not found', status_code=404, request=request)

View File

@@ -274,10 +274,21 @@ class InvoiceService:
transaction_id: Optional[str] = None
) -> Invoice:
"""
Mark invoice as paid
Mark invoice as paid and record payment details
Args:
invoice: Invoice to mark as paid
payment_method: Payment method used ('stripe', 'paypal', 'bank_transfer', etc.)
transaction_id: External transaction ID (Stripe payment intent, PayPal capture ID, etc.)
"""
invoice.status = 'paid'
invoice.paid_at = timezone.now()
invoice.payment_method = payment_method
# For Stripe payments, store the transaction ID in stripe_invoice_id field
if payment_method == 'stripe' and transaction_id:
invoice.stripe_invoice_id = transaction_id
invoice.save()
return invoice

View File

@@ -105,11 +105,15 @@ class PaymentService:
) -> Payment:
"""
Mark payment as completed and update invoice
For automatic payments (Stripe/PayPal), sets approved_at but leaves approved_by as None
"""
from .invoice_service import InvoiceService
payment.status = 'succeeded'
payment.processed_at = timezone.now()
# For automatic payments, set approved_at to indicate when payment was verified
# approved_by stays None to indicate it was automated, not manual approval
payment.approved_at = timezone.now()
if transaction_id:
payment.transaction_reference = transaction_id

View File

@@ -172,7 +172,7 @@ def _attempt_stripe_renewal(subscription: Subscription, invoice: Invoice) -> boo
payment_method='stripe',
status='processing',
stripe_payment_intent_id=intent.id,
metadata={'renewal': True}
metadata={'renewal': True, 'auto_approved': True}
)
return True
@@ -210,7 +210,7 @@ def _attempt_paypal_renewal(subscription: Subscription, invoice: Invoice) -> boo
payment_method='paypal',
status='processing',
paypal_order_id=subscription.metadata['paypal_subscription_id'],
metadata={'renewal': True}
metadata={'renewal': True, 'auto_approved': True}
)
return True
else:

View File

@@ -183,9 +183,14 @@ class PayPalCreateSubscriptionOrderView(APIView):
request=request
)
# Get plan
# Get plan - support both ID (integer) and slug (string) lookup
try:
plan = Plan.objects.get(id=plan_id, is_active=True)
# Try integer ID first
try:
plan = Plan.objects.get(id=int(plan_id), is_active=True)
except (ValueError, Plan.DoesNotExist):
# Fall back to slug lookup
plan = Plan.objects.get(slug=plan_id, is_active=True)
except Plan.DoesNotExist:
return error_response(
error='Plan not found',
@@ -560,6 +565,7 @@ def _process_credit_purchase(account, package_id: str, capture_result: dict) ->
)
# Create payment record
# For automatic payments, approved_at is set but approved_by is None (automated)
amount = float(capture_result.get('amount', package.price))
currency = capture_result.get('currency', 'USD')
@@ -573,9 +579,11 @@ def _process_credit_purchase(account, package_id: str, capture_result: dict) ->
paypal_order_id=capture_result.get('order_id'),
paypal_capture_id=capture_result.get('capture_id'),
processed_at=timezone.now(),
approved_at=timezone.now(), # Set approved_at for automatic payments
metadata={
'credit_package_id': str(package_id),
'credits_added': package.credits,
'auto_approved': True, # Indicates automated approval
}
)
@@ -642,6 +650,7 @@ def _process_subscription_payment(account, plan_id: str, capture_result: dict) -
)
# Create payment record
# For automatic payments, approved_at is set but approved_by is None (automated)
amount = float(capture_result.get('amount', plan.price))
currency = capture_result.get('currency', 'USD')
@@ -655,12 +664,36 @@ def _process_subscription_payment(account, plan_id: str, capture_result: dict) -
paypal_order_id=capture_result.get('order_id'),
paypal_capture_id=capture_result.get('capture_id'),
processed_at=timezone.now(),
approved_at=timezone.now(), # Set approved_at for automatic payments
metadata={
'plan_id': str(plan_id),
'subscription_type': 'paypal_order',
'auto_approved': True, # Indicates automated approval
}
)
# Update/create AccountPaymentMethod and mark as verified
from ..models import AccountPaymentMethod
# Get country code from account billing info
country_code = account.billing_country if account.billing_country else ''
AccountPaymentMethod.objects.update_or_create(
account=account,
type='paypal',
defaults={
'display_name': 'PayPal',
'is_default': True,
'is_enabled': True,
'is_verified': True, # Mark verified after successful payment
'country_code': country_code, # Set country from account billing info
'metadata': {
'last_payment_at': timezone.now().isoformat(),
'paypal_order_id': capture_result.get('order_id'),
}
}
)
# Set other payment methods as non-default
AccountPaymentMethod.objects.filter(account=account).exclude(type='paypal').update(is_default=False)
# Add subscription credits
if plan.included_credits and plan.included_credits > 0:
CreditService.add_credits(
@@ -674,10 +707,15 @@ def _process_subscription_payment(account, plan_id: str, capture_result: dict) -
}
)
# Update account status
# Update account status AND plan (like Stripe flow)
update_fields = ['updated_at']
if account.status != 'active':
account.status = 'active'
account.save(update_fields=['status', 'updated_at'])
update_fields.append('status')
if account.plan_id != plan.id:
account.plan = plan
update_fields.append('plan')
account.save(update_fields=update_fields)
logger.info(
f"PayPal subscription payment completed for account {account.id}: "
@@ -706,6 +744,10 @@ def _process_generic_payment(account, capture_result: dict) -> dict:
paypal_order_id=capture_result.get('order_id'),
paypal_capture_id=capture_result.get('capture_id'),
processed_at=timezone.now(),
approved_at=timezone.now(), # Set approved_at for automatic payments
metadata={
'auto_approved': True, # Indicates automated approval
}
)
logger.info(f"PayPal generic payment recorded for account {account.id}")

View File

@@ -82,9 +82,14 @@ class StripeCheckoutView(APIView):
request=request
)
# Get plan
# Get plan - support both ID (integer) and slug (string) lookup
try:
plan = Plan.objects.get(id=plan_id, is_active=True)
# Try integer ID first
try:
plan = Plan.objects.get(id=int(plan_id), is_active=True)
except (ValueError, Plan.DoesNotExist):
# Fall back to slug lookup
plan = Plan.objects.get(slug=plan_id, is_active=True)
except Plan.DoesNotExist:
return error_response(
error='Plan not found',
@@ -488,7 +493,13 @@ def _activate_subscription(account, stripe_subscription_id: str, plan_id: str, s
transaction_id=stripe_subscription_id
)
# Create payment record
# Extract payment details from session
# For subscription checkouts, payment_intent may be in session or we need to use subscription invoice
payment_intent_id = session.get('payment_intent')
checkout_session_id = session.get('id')
# Create payment record with proper Stripe identifiers
# For automatic payments, approved_at is set but approved_by is None (automated)
Payment.objects.create(
account=account,
invoice=invoice,
@@ -496,15 +507,41 @@ def _activate_subscription(account, stripe_subscription_id: str, plan_id: str, s
currency=currency,
payment_method='stripe',
status='succeeded',
stripe_payment_intent_id=session.get('payment_intent'),
stripe_payment_intent_id=payment_intent_id or f'sub_{stripe_subscription_id}', # Use subscription ID if no PI
stripe_charge_id=checkout_session_id, # Store checkout session as reference
processed_at=timezone.now(),
approved_at=timezone.now(), # Set approved_at for automatic payments
metadata={
'checkout_session_id': session.get('id'),
'checkout_session_id': checkout_session_id,
'subscription_id': stripe_subscription_id,
'plan_id': str(plan_id),
'payment_intent': payment_intent_id,
'auto_approved': True, # Indicates automated approval
}
)
# Update/create AccountPaymentMethod and mark as verified
from ..models import AccountPaymentMethod
# Get country code from account billing info
country_code = account.billing_country if account.billing_country else ''
AccountPaymentMethod.objects.update_or_create(
account=account,
type='stripe',
defaults={
'display_name': 'Credit/Debit Card (Stripe)',
'is_default': True,
'is_enabled': True,
'is_verified': True, # Mark verified after successful payment
'country_code': country_code, # Set country from account billing info
'metadata': {
'last_payment_at': timezone.now().isoformat(),
'stripe_subscription_id': stripe_subscription_id,
}
}
)
# Set other payment methods as non-default
AccountPaymentMethod.objects.filter(account=account).exclude(type='stripe').update(is_default=False)
# Add initial credits from plan
if plan.included_credits and plan.included_credits > 0:
CreditService.add_credits(
@@ -571,6 +608,7 @@ def _add_purchased_credits(account, credit_package_id: str, credit_amount: str,
)
# Create payment record
# For automatic payments, approved_at is set but approved_by is None (automated)
amount = session.get('amount_total', 0) / 100
currency = session.get('currency', 'usd').upper()
@@ -583,10 +621,12 @@ def _add_purchased_credits(account, credit_package_id: str, credit_amount: str,
status='succeeded',
stripe_payment_intent_id=session.get('payment_intent'),
processed_at=timezone.now(),
approved_at=timezone.now(), # Set approved_at for automatic payments
metadata={
'checkout_session_id': session.get('id'),
'credit_package_id': str(credit_package_id),
'credits_added': credits_to_add,
'auto_approved': True, # Indicates automated approval
}
)