docs and billing adn acaoutn 40%

This commit is contained in:
IGNY8 VPS (Salman)
2025-12-04 23:56:38 +00:00
parent 1e3299a089
commit 3a7ea1f4f3
21 changed files with 4994 additions and 24 deletions

View File

@@ -0,0 +1,249 @@
"""
Invoice Service - Handles invoice creation, management, and PDF generation
"""
from decimal import Decimal
from datetime import datetime, timedelta
from typing import Dict, List, Optional
from django.db import transaction
from django.utils import timezone
from ..models import Invoice, CreditPackage
from ....auth.models import Account, Subscription
class InvoiceService:
"""Service for managing invoices"""
@staticmethod
def generate_invoice_number(account: Account) -> str:
"""
Generate unique invoice number
Format: INV-{ACCOUNT_ID}-{YEAR}{MONTH}-{COUNTER}
"""
now = timezone.now()
prefix = f"INV-{account.id}-{now.year}{now.month:02d}"
# Get count of invoices for this account this month
count = Invoice.objects.filter(
account=account,
created_at__year=now.year,
created_at__month=now.month
).count()
return f"{prefix}-{count + 1:04d}"
@staticmethod
@transaction.atomic
def create_subscription_invoice(
subscription: Subscription,
billing_period_start: datetime,
billing_period_end: datetime
) -> Invoice:
"""
Create invoice for subscription billing period
"""
account = subscription.account
plan = subscription.plan
invoice = Invoice.objects.create(
account=account,
subscription=subscription,
invoice_number=InvoiceService.generate_invoice_number(account),
billing_email=account.billing_email or account.users.filter(role='owner').first().email,
status='pending',
currency='USD',
billing_period_start=billing_period_start,
billing_period_end=billing_period_end
)
# Add line item for subscription
invoice.add_line_item(
description=f"{plan.name} Plan - {billing_period_start.strftime('%b %Y')}",
quantity=1,
unit_price=plan.price,
amount=plan.price
)
invoice.calculate_totals()
invoice.save()
return invoice
@staticmethod
@transaction.atomic
def create_credit_package_invoice(
account: Account,
credit_package: CreditPackage
) -> Invoice:
"""
Create invoice for credit package purchase
"""
invoice = Invoice.objects.create(
account=account,
invoice_number=InvoiceService.generate_invoice_number(account),
billing_email=account.billing_email or account.users.filter(role='owner').first().email,
status='pending',
currency='USD'
)
# Add line item for credit package
invoice.add_line_item(
description=f"{credit_package.name} - {credit_package.credits:,} Credits",
quantity=1,
unit_price=credit_package.price,
amount=credit_package.price
)
invoice.calculate_totals()
invoice.save()
return invoice
@staticmethod
@transaction.atomic
def create_custom_invoice(
account: Account,
line_items: List[Dict],
billing_email: Optional[str] = None,
notes: Optional[str] = None,
due_date: Optional[datetime] = None
) -> Invoice:
"""
Create custom invoice with multiple line items
Args:
account: Account to bill
line_items: List of dicts with keys: description, quantity, unit_price
billing_email: Override billing email
notes: Invoice notes
due_date: Payment due date
"""
invoice = Invoice.objects.create(
account=account,
invoice_number=InvoiceService.generate_invoice_number(account),
billing_email=billing_email or account.billing_email or account.users.filter(role='owner').first().email,
status='draft',
currency='USD',
notes=notes,
due_date=due_date or (timezone.now() + timedelta(days=30))
)
# Add all line items
for item in line_items:
invoice.add_line_item(
description=item['description'],
quantity=item.get('quantity', 1),
unit_price=Decimal(str(item['unit_price'])),
amount=Decimal(str(item.get('amount', item['quantity'] * item['unit_price'])))
)
invoice.calculate_totals()
invoice.save()
return invoice
@staticmethod
@transaction.atomic
def mark_paid(
invoice: Invoice,
payment_method: str,
transaction_id: Optional[str] = None
) -> Invoice:
"""
Mark invoice as paid
"""
invoice.status = 'paid'
invoice.paid_at = timezone.now()
invoice.save()
return invoice
@staticmethod
@transaction.atomic
def mark_void(invoice: Invoice, reason: Optional[str] = None) -> Invoice:
"""
Void an invoice
"""
if invoice.status == 'paid':
raise ValueError("Cannot void a paid invoice")
invoice.status = 'void'
if reason:
invoice.notes = f"{invoice.notes}\n\nVoided: {reason}" if invoice.notes else f"Voided: {reason}"
invoice.save()
return invoice
@staticmethod
def generate_pdf(invoice: Invoice) -> bytes:
"""
Generate PDF for invoice
TODO: Implement PDF generation using reportlab or weasyprint
For now, return placeholder
"""
from io import BytesIO
# Placeholder - implement PDF generation
buffer = BytesIO()
# Simple text representation for now
content = f"""
INVOICE #{invoice.invoice_number}
Bill To: {invoice.account.name}
Email: {invoice.billing_email}
Date: {invoice.created_at.strftime('%Y-%m-%d')}
Due Date: {invoice.due_date.strftime('%Y-%m-%d') if invoice.due_date else 'N/A'}
Line Items:
"""
for item in invoice.line_items:
content += f" {item['description']} - ${item['amount']}\n"
content += f"""
Subtotal: ${invoice.subtotal}
Tax: ${invoice.tax_amount}
Total: ${invoice.total_amount}
Status: {invoice.status.upper()}
"""
buffer.write(content.encode('utf-8'))
buffer.seek(0)
return buffer.getvalue()
@staticmethod
def get_account_invoices(
account: Account,
status: Optional[str] = None,
limit: int = 50
) -> List[Invoice]:
"""
Get invoices for an account
"""
queryset = Invoice.objects.filter(account=account)
if status:
queryset = queryset.filter(status=status)
return list(queryset.order_by('-created_at')[:limit])
@staticmethod
def get_upcoming_renewals(days: int = 7) -> List[Subscription]:
"""
Get subscriptions that will renew in the next N days
"""
from django.utils import timezone
from datetime import timedelta
cutoff_date = timezone.now() + timedelta(days=days)
return list(
Subscription.objects.filter(
status='active',
current_period_end__lte=cutoff_date
).select_related('account', 'plan')
)

View File

@@ -0,0 +1,375 @@
"""
Payment Service - Handles payment processing across multiple gateways
"""
from decimal import Decimal
from typing import Optional, Dict, Any
from django.db import transaction
from django.utils import timezone
from ..models import Payment, Invoice, CreditPackage, PaymentMethodConfig, CreditTransaction
from ....auth.models import Account
class PaymentService:
"""Service for processing payments across multiple gateways"""
@staticmethod
@transaction.atomic
def create_stripe_payment(
invoice: Invoice,
stripe_payment_intent_id: str,
stripe_charge_id: Optional[str] = None,
metadata: Optional[Dict] = None
) -> Payment:
"""
Create payment record for Stripe transaction
"""
payment = Payment.objects.create(
account=invoice.account,
invoice=invoice,
amount=invoice.total_amount,
currency=invoice.currency,
payment_method='stripe',
status='pending',
stripe_payment_intent_id=stripe_payment_intent_id,
stripe_charge_id=stripe_charge_id,
metadata=metadata or {}
)
return payment
@staticmethod
@transaction.atomic
def create_paypal_payment(
invoice: Invoice,
paypal_order_id: str,
metadata: Optional[Dict] = None
) -> Payment:
"""
Create payment record for PayPal transaction
"""
payment = Payment.objects.create(
account=invoice.account,
invoice=invoice,
amount=invoice.total_amount,
currency=invoice.currency,
payment_method='paypal',
status='pending',
paypal_order_id=paypal_order_id,
metadata=metadata or {}
)
return payment
@staticmethod
@transaction.atomic
def create_manual_payment(
invoice: Invoice,
payment_method: str, # 'bank_transfer' or 'local_wallet'
transaction_reference: str,
admin_notes: Optional[str] = None,
metadata: Optional[Dict] = None
) -> Payment:
"""
Create manual payment (bank transfer or local wallet)
Requires admin approval
"""
if payment_method not in ['bank_transfer', 'local_wallet', 'manual']:
raise ValueError("Invalid manual payment method")
payment = Payment.objects.create(
account=invoice.account,
invoice=invoice,
amount=invoice.total_amount,
currency=invoice.currency,
payment_method=payment_method,
status='pending_approval',
transaction_reference=transaction_reference,
admin_notes=admin_notes,
metadata=metadata or {}
)
return payment
@staticmethod
@transaction.atomic
def mark_payment_completed(
payment: Payment,
transaction_id: Optional[str] = None
) -> Payment:
"""
Mark payment as completed and update invoice
"""
from .invoice_service import InvoiceService
payment.status = 'completed'
payment.processed_at = timezone.now()
if transaction_id:
payment.transaction_reference = transaction_id
payment.save()
# Update invoice
if payment.invoice:
InvoiceService.mark_paid(
payment.invoice,
payment_method=payment.payment_method,
transaction_id=transaction_id
)
# If payment is for credit package, add credits to account
if payment.metadata.get('credit_package_id'):
PaymentService._add_credits_for_payment(payment)
return payment
@staticmethod
@transaction.atomic
def mark_payment_failed(
payment: Payment,
failure_reason: Optional[str] = None
) -> Payment:
"""
Mark payment as failed
"""
payment.status = 'failed'
payment.failure_reason = failure_reason
payment.processed_at = timezone.now()
payment.save()
return payment
@staticmethod
@transaction.atomic
def approve_manual_payment(
payment: Payment,
approved_by_user_id: int,
admin_notes: Optional[str] = None
) -> Payment:
"""
Approve manual payment (admin action)
"""
if payment.status != 'pending_approval':
raise ValueError("Payment is not pending approval")
payment.status = 'completed'
payment.processed_at = timezone.now()
payment.approved_by_id = approved_by_user_id
if admin_notes:
payment.admin_notes = f"{payment.admin_notes}\n\nApproval notes: {admin_notes}" if payment.admin_notes else admin_notes
payment.save()
# Update invoice
if payment.invoice:
from .invoice_service import InvoiceService
InvoiceService.mark_paid(
payment.invoice,
payment_method=payment.payment_method,
transaction_id=payment.transaction_reference
)
# If payment is for credit package, add credits
if payment.metadata.get('credit_package_id'):
PaymentService._add_credits_for_payment(payment)
return payment
@staticmethod
@transaction.atomic
def reject_manual_payment(
payment: Payment,
rejected_by_user_id: int,
rejection_reason: str
) -> Payment:
"""
Reject manual payment (admin action)
"""
if payment.status != 'pending_approval':
raise ValueError("Payment is not pending approval")
payment.status = 'failed'
payment.failure_reason = rejection_reason
payment.processed_at = timezone.now()
payment.admin_notes = f"{payment.admin_notes}\n\nRejected by user {rejected_by_user_id}: {rejection_reason}" if payment.admin_notes else f"Rejected: {rejection_reason}"
payment.save()
return payment
@staticmethod
def _add_credits_for_payment(payment: Payment) -> None:
"""
Add credits to account after successful payment
"""
credit_package_id = payment.metadata.get('credit_package_id')
if not credit_package_id:
return
try:
credit_package = CreditPackage.objects.get(id=credit_package_id)
except CreditPackage.DoesNotExist:
return
# Create credit transaction
CreditTransaction.objects.create(
account=payment.account,
amount=credit_package.credits,
transaction_type='purchase',
description=f"Purchased {credit_package.name}",
reference_id=str(payment.id),
metadata={
'payment_id': payment.id,
'credit_package_id': credit_package_id,
'invoice_id': payment.invoice_id if payment.invoice else None
}
)
@staticmethod
def get_available_payment_methods(account: Account) -> Dict[str, Any]:
"""
Get available payment methods for account's country
"""
country_code = account.billing_country or 'US'
# Get payment method configurations for country
configs = PaymentMethodConfig.objects.filter(
country_code=country_code,
is_enabled=True
).order_by('sort_order')
# Default methods if no config
if not configs.exists():
return {
'methods': [
{
'type': 'stripe',
'name': 'Credit/Debit Card',
'instructions': 'Pay securely with your credit or debit card'
},
{
'type': 'paypal',
'name': 'PayPal',
'instructions': 'Pay with your PayPal account'
}
],
'stripe': True,
'paypal': True,
'bank_transfer': False,
'local_wallet': False
}
# Build response from configs
methods = []
method_flags = {
'stripe': False,
'paypal': False,
'bank_transfer': False,
'local_wallet': False
}
for config in configs:
method_flags[config.payment_method] = True
method_data = {
'type': config.payment_method,
'name': config.display_name or config.get_payment_method_display(),
'instructions': config.instructions
}
# Add bank details if bank_transfer
if config.payment_method == 'bank_transfer' and config.bank_name:
method_data['bank_details'] = {
'bank_name': config.bank_name,
'account_number': config.account_number,
'routing_number': config.routing_number,
'swift_code': config.swift_code
}
# Add wallet details if local_wallet
if config.payment_method == 'local_wallet' and config.wallet_type:
method_data['wallet_details'] = {
'wallet_type': config.wallet_type,
'wallet_id': config.wallet_id
}
methods.append(method_data)
return {
'methods': methods,
**method_flags
}
@staticmethod
def get_pending_approvals() -> list:
"""
Get all payments pending admin approval
"""
return list(
Payment.objects.filter(
status='pending_approval'
).select_related('account', 'invoice').order_by('-created_at')
)
@staticmethod
def refund_payment(
payment: Payment,
amount: Optional[Decimal] = None,
reason: Optional[str] = None
) -> Payment:
"""
Process refund for a payment
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")
refund_amount = amount or payment.amount
if refund_amount > payment.amount:
raise ValueError("Refund amount cannot exceed payment amount")
# Create refund payment record
refund = Payment.objects.create(
account=payment.account,
invoice=payment.invoice,
amount=-refund_amount, # Negative amount for refund
currency=payment.currency,
payment_method=payment.payment_method,
status='completed',
processed_at=timezone.now(),
metadata={
'refund_for_payment_id': payment.id,
'refund_reason': reason,
'original_amount': str(payment.amount)
}
)
# Update original payment metadata
payment.metadata['refunded'] = True
payment.metadata['refund_payment_id'] = refund.id
payment.metadata['refund_amount'] = str(refund_amount)
payment.save()
return refund
@staticmethod
def get_account_payments(
account: Account,
status: Optional[str] = None,
limit: int = 50
) -> list:
"""
Get payment history for account
"""
queryset = Payment.objects.filter(account=account)
if status:
queryset = queryset.filter(status=status)
return list(
queryset.select_related('invoice')
.order_by('-created_at')[:limit]
)