billing admin account 1
This commit is contained in:
@@ -222,7 +222,7 @@ class UsageAnalyticsViewSet(viewsets.ViewSet):
|
||||
'period_days': days,
|
||||
'start_date': start_date.isoformat(),
|
||||
'end_date': timezone.now().isoformat(),
|
||||
'current_balance': account.credit_balance,
|
||||
'current_balance': account.credits,
|
||||
'usage_by_type': list(usage_by_type),
|
||||
'purchases_by_type': list(purchases_by_type),
|
||||
'daily_usage': daily_usage,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
Billing Models for Credit System
|
||||
"""
|
||||
from decimal import Decimal
|
||||
from django.db import models
|
||||
from django.core.validators import MinValueValidator
|
||||
from django.conf import settings
|
||||
@@ -22,6 +23,11 @@ class CreditTransaction(AccountBaseModel):
|
||||
balance_after = models.IntegerField(help_text="Credit balance after this transaction")
|
||||
description = models.CharField(max_length=255)
|
||||
metadata = models.JSONField(default=dict, help_text="Additional context (AI call details, etc.)")
|
||||
reference_id = models.CharField(
|
||||
max_length=255,
|
||||
blank=True,
|
||||
help_text="Optional reference (e.g., payment id, invoice id)"
|
||||
)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
@@ -183,9 +189,10 @@ class Invoice(AccountBaseModel):
|
||||
)
|
||||
|
||||
# Amounts
|
||||
subtotal = models.DecimalField(max_digits=10, decimal_places=2)
|
||||
subtotal = models.DecimalField(max_digits=10, decimal_places=2, default=0)
|
||||
tax = models.DecimalField(max_digits=10, decimal_places=2, default=0)
|
||||
total = models.DecimalField(max_digits=10, decimal_places=2)
|
||||
total = models.DecimalField(max_digits=10, decimal_places=2, default=0)
|
||||
currency = models.CharField(max_length=3, default='USD')
|
||||
|
||||
# Status
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending', db_index=True)
|
||||
@@ -201,6 +208,9 @@ class Invoice(AccountBaseModel):
|
||||
# Payment integration
|
||||
stripe_invoice_id = models.CharField(max_length=255, null=True, blank=True)
|
||||
payment_method = models.CharField(max_length=50, null=True, blank=True)
|
||||
billing_email = models.EmailField(null=True, blank=True)
|
||||
billing_period_start = models.DateTimeField(null=True, blank=True)
|
||||
billing_period_end = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
# Metadata
|
||||
notes = models.TextField(blank=True)
|
||||
@@ -222,6 +232,45 @@ class Invoice(AccountBaseModel):
|
||||
def __str__(self):
|
||||
return f"Invoice {self.invoice_number} - {self.account.name if self.account else 'No Account'}"
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Helpers to keep service code working with legacy field names
|
||||
# ------------------------------------------------------------------
|
||||
@property
|
||||
def subtotal_amount(self):
|
||||
return self.subtotal
|
||||
|
||||
@property
|
||||
def tax_amount(self):
|
||||
return self.tax
|
||||
|
||||
@property
|
||||
def total_amount(self):
|
||||
return self.total
|
||||
|
||||
def add_line_item(self, description: str, quantity: int, unit_price: Decimal, amount: Decimal = None):
|
||||
"""Append a line item and keep JSON shape consistent."""
|
||||
items = list(self.line_items or [])
|
||||
qty = quantity or 1
|
||||
amt = Decimal(amount) if amount is not None else Decimal(unit_price) * qty
|
||||
items.append({
|
||||
'description': description,
|
||||
'quantity': qty,
|
||||
'unit_price': str(unit_price),
|
||||
'amount': str(amt),
|
||||
})
|
||||
self.line_items = items
|
||||
|
||||
def calculate_totals(self):
|
||||
"""Recompute subtotal, tax, and total from line_items."""
|
||||
subtotal = Decimal('0')
|
||||
for item in self.line_items or []:
|
||||
try:
|
||||
subtotal += Decimal(str(item.get('amount') or 0))
|
||||
except Exception:
|
||||
pass
|
||||
self.subtotal = subtotal
|
||||
self.total = subtotal + (self.tax or Decimal('0'))
|
||||
|
||||
|
||||
class Payment(AccountBaseModel):
|
||||
"""
|
||||
@@ -230,8 +279,10 @@ class Payment(AccountBaseModel):
|
||||
"""
|
||||
STATUS_CHOICES = [
|
||||
('pending', 'Pending'),
|
||||
('pending_approval', 'Pending Approval'),
|
||||
('processing', 'Processing'),
|
||||
('succeeded', 'Succeeded'),
|
||||
('completed', 'Completed'), # Legacy alias for succeeded
|
||||
('failed', 'Failed'),
|
||||
('refunded', 'Refunded'),
|
||||
('cancelled', 'Cancelled'),
|
||||
@@ -272,6 +323,8 @@ class Payment(AccountBaseModel):
|
||||
help_text="Bank transfer reference, wallet transaction ID, etc."
|
||||
)
|
||||
manual_notes = models.TextField(blank=True, help_text="Admin notes for manual payments")
|
||||
transaction_reference = models.CharField(max_length=255, blank=True)
|
||||
admin_notes = models.TextField(blank=True, help_text="Internal notes on approval/rejection")
|
||||
approved_by = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
null=True,
|
||||
|
||||
@@ -52,6 +52,8 @@ class InvoiceService:
|
||||
billing_email=account.billing_email or account.users.filter(role='owner').first().email,
|
||||
status='pending',
|
||||
currency='USD',
|
||||
invoice_date=timezone.now().date(),
|
||||
due_date=billing_period_end.date(),
|
||||
billing_period_start=billing_period_start,
|
||||
billing_period_end=billing_period_end
|
||||
)
|
||||
@@ -83,7 +85,13 @@ class InvoiceService:
|
||||
invoice_number=InvoiceService.generate_invoice_number(account),
|
||||
billing_email=account.billing_email or account.users.filter(role='owner').first().email,
|
||||
status='pending',
|
||||
currency='USD'
|
||||
currency='USD',
|
||||
invoice_date=timezone.now().date(),
|
||||
due_date=timezone.now().date(),
|
||||
metadata={
|
||||
'credit_package_id': credit_package.id,
|
||||
'credit_amount': credit_package.credits,
|
||||
},
|
||||
)
|
||||
|
||||
# Add line item for credit package
|
||||
@@ -125,6 +133,7 @@ class InvoiceService:
|
||||
status='draft',
|
||||
currency='USD',
|
||||
notes=notes,
|
||||
invoice_date=timezone.now().date(),
|
||||
due_date=due_date or (timezone.now() + timedelta(days=30))
|
||||
)
|
||||
|
||||
|
||||
@@ -77,6 +77,12 @@ class PaymentService:
|
||||
if payment_method not in ['bank_transfer', 'local_wallet', 'manual']:
|
||||
raise ValueError("Invalid manual payment method")
|
||||
|
||||
meta = metadata or {}
|
||||
# propagate credit package metadata from invoice if present
|
||||
if invoice.metadata.get('credit_package_id'):
|
||||
meta.setdefault('credit_package_id', invoice.metadata.get('credit_package_id'))
|
||||
meta.setdefault('credit_amount', invoice.metadata.get('credit_amount'))
|
||||
|
||||
payment = Payment.objects.create(
|
||||
account=invoice.account,
|
||||
invoice=invoice,
|
||||
@@ -86,7 +92,7 @@ class PaymentService:
|
||||
status='pending_approval',
|
||||
transaction_reference=transaction_reference,
|
||||
admin_notes=admin_notes,
|
||||
metadata=metadata or {}
|
||||
metadata=meta
|
||||
)
|
||||
|
||||
return payment
|
||||
@@ -102,7 +108,7 @@ class PaymentService:
|
||||
"""
|
||||
from .invoice_service import InvoiceService
|
||||
|
||||
payment.status = 'completed'
|
||||
payment.status = 'succeeded'
|
||||
payment.processed_at = timezone.now()
|
||||
|
||||
if transaction_id:
|
||||
@@ -153,9 +159,10 @@ class PaymentService:
|
||||
if payment.status != 'pending_approval':
|
||||
raise ValueError("Payment is not pending approval")
|
||||
|
||||
payment.status = 'completed'
|
||||
payment.status = 'succeeded'
|
||||
payment.processed_at = timezone.now()
|
||||
payment.approved_by_id = approved_by_user_id
|
||||
payment.approved_at = timezone.now()
|
||||
|
||||
if admin_notes:
|
||||
payment.admin_notes = f"{payment.admin_notes}\n\nApproval notes: {admin_notes}" if payment.admin_notes else admin_notes
|
||||
@@ -212,6 +219,11 @@ class PaymentService:
|
||||
except CreditPackage.DoesNotExist:
|
||||
return
|
||||
|
||||
# Update account balance
|
||||
account: Account = payment.account
|
||||
account.credits = (account.credits or 0) + credit_package.credits
|
||||
account.save(update_fields=['credits', 'updated_at'])
|
||||
|
||||
# Create credit transaction
|
||||
CreditTransaction.objects.create(
|
||||
account=payment.account,
|
||||
@@ -244,14 +256,20 @@ class PaymentService:
|
||||
return {
|
||||
'methods': [
|
||||
{
|
||||
'id': 'stripe-default',
|
||||
'type': 'stripe',
|
||||
'name': 'Credit/Debit Card',
|
||||
'instructions': 'Pay securely with your credit or debit card'
|
||||
'display_name': 'Credit/Debit Card',
|
||||
'instructions': 'Pay securely with your credit or debit card',
|
||||
'is_enabled': True,
|
||||
},
|
||||
{
|
||||
'id': 'paypal-default',
|
||||
'type': 'paypal',
|
||||
'name': 'PayPal',
|
||||
'instructions': 'Pay with your PayPal account'
|
||||
'display_name': 'PayPal',
|
||||
'instructions': 'Pay with your PayPal account',
|
||||
'is_enabled': True,
|
||||
}
|
||||
],
|
||||
'stripe': True,
|
||||
@@ -272,9 +290,12 @@ class PaymentService:
|
||||
for config in configs:
|
||||
method_flags[config.payment_method] = True
|
||||
method_data = {
|
||||
'id': f"{config.country_code}-{config.payment_method}-{config.id}",
|
||||
'type': config.payment_method,
|
||||
'name': config.display_name or config.get_payment_method_display(),
|
||||
'instructions': config.instructions
|
||||
'display_name': config.display_name or config.get_payment_method_display(),
|
||||
'instructions': config.instructions,
|
||||
'is_enabled': True,
|
||||
}
|
||||
|
||||
# Add bank details if bank_transfer
|
||||
@@ -323,8 +344,8 @@ class PaymentService:
|
||||
TODO: Implement actual refund logic for Stripe/PayPal
|
||||
For now, just mark as refunded
|
||||
"""
|
||||
if payment.status != 'completed':
|
||||
raise ValueError("Can only refund completed payments")
|
||||
if payment.status not in ['completed', 'succeeded']:
|
||||
raise ValueError("Can only refund succeeded/complete payments")
|
||||
|
||||
refund_amount = amount or payment.amount
|
||||
|
||||
@@ -338,8 +359,9 @@ class PaymentService:
|
||||
amount=-refund_amount, # Negative amount for refund
|
||||
currency=payment.currency,
|
||||
payment_method=payment.payment_method,
|
||||
status='completed',
|
||||
status='refunded',
|
||||
processed_at=timezone.now(),
|
||||
refunded_at=timezone.now(),
|
||||
metadata={
|
||||
'refund_for_payment_id': payment.id,
|
||||
'refund_reason': reason,
|
||||
@@ -348,10 +370,16 @@ class PaymentService:
|
||||
)
|
||||
|
||||
# Update original payment metadata
|
||||
payment.metadata['refunded'] = True
|
||||
payment.metadata['refund_payment_id'] = refund.id
|
||||
payment.metadata['refund_amount'] = str(refund_amount)
|
||||
payment.save()
|
||||
meta = payment.metadata or {}
|
||||
meta['refunded'] = True
|
||||
meta['refund_payment_id'] = refund.id
|
||||
meta['refund_amount'] = str(refund_amount)
|
||||
if reason:
|
||||
meta['refund_reason'] = reason
|
||||
payment.metadata = meta
|
||||
payment.status = 'refunded'
|
||||
payment.refunded_at = timezone.now()
|
||||
payment.save(update_fields=['metadata', 'status', 'refunded_at', 'updated_at'])
|
||||
|
||||
return refund
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ from rest_framework.response import Response
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.db import models
|
||||
|
||||
from .models import Invoice, Payment, CreditPackage, PaymentMethodConfig, CreditTransaction
|
||||
from .services.invoice_service import InvoiceService
|
||||
@@ -126,16 +127,21 @@ class PaymentViewSet(viewsets.ViewSet):
|
||||
"""Get available payment methods for current account"""
|
||||
account = request.user.account
|
||||
methods = PaymentService.get_available_payment_methods(account)
|
||||
method_list = methods.pop('methods', [])
|
||||
|
||||
return Response(methods)
|
||||
return Response({
|
||||
'results': method_list,
|
||||
'count': len(method_list),
|
||||
**methods
|
||||
})
|
||||
|
||||
@action(detail=False, methods=['post'])
|
||||
@action(detail=False, methods=['post'], url_path='manual')
|
||||
def create_manual_payment(self, request):
|
||||
"""Submit manual payment for approval"""
|
||||
account = request.user.account
|
||||
invoice_id = request.data.get('invoice_id')
|
||||
payment_method = request.data.get('payment_method') # 'bank_transfer' or 'local_wallet'
|
||||
transaction_reference = request.data.get('transaction_reference')
|
||||
transaction_reference = request.data.get('transaction_reference') or request.data.get('reference')
|
||||
notes = request.data.get('notes')
|
||||
|
||||
if not all([invoice_id, payment_method, transaction_reference]):
|
||||
@@ -261,22 +267,32 @@ class CreditTransactionViewSet(viewsets.ViewSet):
|
||||
for txn in transactions
|
||||
],
|
||||
'count': transactions.count(),
|
||||
'current_balance': account.credit_balance
|
||||
'current_balance': account.credits
|
||||
})
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def balance(self, request):
|
||||
"""Get current credit balance"""
|
||||
account = request.user.account
|
||||
|
||||
# Get subscription details
|
||||
active_subscription = account.subscriptions.filter(status='active').first()
|
||||
from django.utils import timezone
|
||||
from datetime import timedelta
|
||||
now = timezone.now()
|
||||
month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
||||
used_this_month = abs(
|
||||
CreditTransaction.objects.filter(
|
||||
account=account,
|
||||
created_at__gte=month_start,
|
||||
amount__lt=0
|
||||
).aggregate(total=models.Sum('amount'))['total'] or 0
|
||||
)
|
||||
plan = getattr(account, 'plan', None)
|
||||
included = plan.included_credits if plan else 0
|
||||
|
||||
return Response({
|
||||
'balance': account.credit_balance,
|
||||
'subscription_plan': active_subscription.plan.name if active_subscription else 'None',
|
||||
'monthly_credits': active_subscription.plan.monthly_credits if active_subscription else 0,
|
||||
'subscription_status': active_subscription.status if active_subscription else None
|
||||
'credits': account.credits,
|
||||
'plan_credits_per_month': included,
|
||||
'credits_used_this_month': used_this_month,
|
||||
'credits_remaining': max(account.credits, 0),
|
||||
})
|
||||
|
||||
|
||||
@@ -284,15 +300,95 @@ class AdminBillingViewSet(viewsets.ViewSet):
|
||||
"""Admin billing management"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def pending_payments(self, request):
|
||||
"""List payments pending approval"""
|
||||
# Check admin permission
|
||||
if not request.user.is_staff:
|
||||
def _require_admin(self, request):
|
||||
if not request.user.is_staff and not getattr(request.user, 'is_superuser', False):
|
||||
return Response(
|
||||
{'error': 'Admin access required'},
|
||||
status=status.HTTP_403_FORBIDDEN
|
||||
)
|
||||
return None
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def invoices(self, request):
|
||||
"""List invoices across all accounts (admin)"""
|
||||
error = self._require_admin(request)
|
||||
if error:
|
||||
return error
|
||||
|
||||
status_filter = request.query_params.get('status')
|
||||
account_id = request.query_params.get('account_id')
|
||||
qs = Invoice.objects.all().select_related('account').order_by('-created_at')
|
||||
if status_filter:
|
||||
qs = qs.filter(status=status_filter)
|
||||
if account_id:
|
||||
qs = qs.filter(account_id=account_id)
|
||||
|
||||
invoices = qs[:200]
|
||||
return Response({
|
||||
'results': [
|
||||
{
|
||||
'id': inv.id,
|
||||
'invoice_number': inv.invoice_number,
|
||||
'status': inv.status,
|
||||
'total_amount': str(getattr(inv, 'total_amount', inv.total)),
|
||||
'subtotal': str(inv.subtotal),
|
||||
'tax_amount': str(getattr(inv, 'tax_amount', inv.tax)),
|
||||
'currency': inv.currency,
|
||||
'created_at': inv.created_at.isoformat(),
|
||||
'paid_at': inv.paid_at.isoformat() if inv.paid_at else None,
|
||||
'due_date': inv.due_date.isoformat() if inv.due_date else None,
|
||||
'line_items': inv.line_items,
|
||||
'account_name': inv.account.name if inv.account else None,
|
||||
}
|
||||
for inv in invoices
|
||||
],
|
||||
'count': qs.count(),
|
||||
})
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def payments(self, request):
|
||||
"""List payments across all accounts (admin)"""
|
||||
error = self._require_admin(request)
|
||||
if error:
|
||||
return error
|
||||
|
||||
status_filter = request.query_params.get('status')
|
||||
account_id = request.query_params.get('account_id')
|
||||
payment_method = request.query_params.get('payment_method')
|
||||
qs = Payment.objects.all().select_related('account', 'invoice').order_by('-created_at')
|
||||
if status_filter:
|
||||
qs = qs.filter(status=status_filter)
|
||||
if account_id:
|
||||
qs = qs.filter(account_id=account_id)
|
||||
if payment_method:
|
||||
qs = qs.filter(payment_method=payment_method)
|
||||
|
||||
payments = qs[:200]
|
||||
return Response({
|
||||
'results': [
|
||||
{
|
||||
'id': pay.id,
|
||||
'account_name': pay.account.name if pay.account else None,
|
||||
'amount': str(pay.amount),
|
||||
'currency': pay.currency,
|
||||
'status': pay.status,
|
||||
'payment_method': pay.payment_method,
|
||||
'created_at': pay.created_at.isoformat(),
|
||||
'invoice_id': pay.invoice_id,
|
||||
'invoice_number': pay.invoice.invoice_number if pay.invoice else None,
|
||||
'transaction_reference': pay.transaction_reference,
|
||||
}
|
||||
for pay in payments
|
||||
],
|
||||
'count': qs.count(),
|
||||
})
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def pending_payments(self, request):
|
||||
"""List payments pending approval"""
|
||||
error = self._require_admin(request)
|
||||
if error:
|
||||
return error
|
||||
|
||||
payments = PaymentService.get_pending_approvals()
|
||||
|
||||
@@ -317,11 +413,9 @@ class AdminBillingViewSet(viewsets.ViewSet):
|
||||
@action(detail=True, methods=['post'])
|
||||
def approve_payment(self, request, pk=None):
|
||||
"""Approve a manual payment"""
|
||||
if not request.user.is_staff:
|
||||
return Response(
|
||||
{'error': 'Admin access required'},
|
||||
status=status.HTTP_403_FORBIDDEN
|
||||
)
|
||||
error = self._require_admin(request)
|
||||
if error:
|
||||
return error
|
||||
|
||||
payment = get_object_or_404(Payment, id=pk)
|
||||
admin_notes = request.data.get('notes')
|
||||
@@ -347,11 +441,9 @@ class AdminBillingViewSet(viewsets.ViewSet):
|
||||
@action(detail=True, methods=['post'])
|
||||
def reject_payment(self, request, pk=None):
|
||||
"""Reject a manual payment"""
|
||||
if not request.user.is_staff:
|
||||
return Response(
|
||||
{'error': 'Admin access required'},
|
||||
status=status.HTTP_403_FORBIDDEN
|
||||
)
|
||||
error = self._require_admin(request)
|
||||
if error:
|
||||
return error
|
||||
|
||||
payment = get_object_or_404(Payment, id=pk)
|
||||
rejection_reason = request.data.get('reason', 'No reason provided')
|
||||
@@ -377,11 +469,9 @@ class AdminBillingViewSet(viewsets.ViewSet):
|
||||
@action(detail=False, methods=['get'])
|
||||
def stats(self, request):
|
||||
"""System billing stats"""
|
||||
if not request.user.is_staff:
|
||||
return Response(
|
||||
{'error': 'Admin access required'},
|
||||
status=status.HTTP_403_FORBIDDEN
|
||||
)
|
||||
error = self._require_admin(request)
|
||||
if error:
|
||||
return error
|
||||
|
||||
from django.db.models import Sum, Count
|
||||
from ...auth.models import Account
|
||||
@@ -407,12 +497,12 @@ class AdminBillingViewSet(viewsets.ViewSet):
|
||||
|
||||
# Revenue stats
|
||||
total_revenue = Payment.objects.filter(
|
||||
status='completed',
|
||||
status__in=['completed', 'succeeded'],
|
||||
amount__gt=0
|
||||
).aggregate(total=Sum('amount'))['total'] or 0
|
||||
|
||||
revenue_this_month = Payment.objects.filter(
|
||||
status='completed',
|
||||
status__in=['completed', 'succeeded'],
|
||||
processed_at__gte=this_month_start,
|
||||
amount__gt=0
|
||||
).aggregate(total=Sum('amount'))['total'] or 0
|
||||
@@ -430,9 +520,7 @@ class AdminBillingViewSet(viewsets.ViewSet):
|
||||
).aggregate(total=Sum('amount'))['total'] or 0)
|
||||
|
||||
# Payment/Invoice stats
|
||||
pending_approvals = Payment.objects.filter(
|
||||
status='pending_approval'
|
||||
).count()
|
||||
pending_approvals = Payment.objects.filter(status='pending_approval').count()
|
||||
|
||||
invoices_pending = Invoice.objects.filter(status='pending').count()
|
||||
invoices_overdue = Invoice.objects.filter(
|
||||
@@ -442,7 +530,7 @@ class AdminBillingViewSet(viewsets.ViewSet):
|
||||
|
||||
# Recent activity
|
||||
recent_payments = Payment.objects.filter(
|
||||
status='completed'
|
||||
status__in=['completed', 'succeeded']
|
||||
).order_by('-processed_at')[:5]
|
||||
|
||||
recent_activity = [
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
# Generated by Django 5.2.8 on 2025-12-05 07:59
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('billing', '0004_add_invoice_payment_models'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='credittransaction',
|
||||
name='reference_id',
|
||||
field=models.CharField(blank=True, help_text='Optional reference (e.g., payment id, invoice id)', max_length=255),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='invoice',
|
||||
name='billing_email',
|
||||
field=models.EmailField(blank=True, max_length=254, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='invoice',
|
||||
name='billing_period_end',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='invoice',
|
||||
name='billing_period_start',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='invoice',
|
||||
name='currency',
|
||||
field=models.CharField(default='USD', max_length=3),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='payment',
|
||||
name='admin_notes',
|
||||
field=models.TextField(blank=True, help_text='Internal notes on approval/rejection'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='payment',
|
||||
name='transaction_reference',
|
||||
field=models.CharField(blank=True, max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='invoice',
|
||||
name='subtotal',
|
||||
field=models.DecimalField(decimal_places=2, default=0, max_digits=10),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='invoice',
|
||||
name='total',
|
||||
field=models.DecimalField(decimal_places=2, default=0, max_digits=10),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='payment',
|
||||
name='status',
|
||||
field=models.CharField(choices=[('pending', 'Pending'), ('pending_approval', 'Pending Approval'), ('processing', 'Processing'), ('succeeded', 'Succeeded'), ('completed', 'Completed'), ('failed', 'Failed'), ('refunded', 'Refunded'), ('cancelled', 'Cancelled')], db_index=True, default='pending', max_length=20),
|
||||
),
|
||||
]
|
||||
Reference in New Issue
Block a user