fixing and creatign mess
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user