STripe Paymen and PK payemtns and many othe rbacekd and froentened issues
This commit is contained in:
@@ -578,8 +578,10 @@ class CreditPackage(models.Model):
|
||||
|
||||
class PaymentMethodConfig(models.Model):
|
||||
"""
|
||||
Configure payment methods availability per country
|
||||
Allows enabling/disabling manual payments by region
|
||||
Configure payment methods availability per country.
|
||||
|
||||
For online payments (stripe, paypal): Credentials stored in IntegrationProvider.
|
||||
For manual payments (bank_transfer, local_wallet): Bank/wallet details stored here.
|
||||
"""
|
||||
# Use centralized choices
|
||||
PAYMENT_METHOD_CHOICES = PAYMENT_METHOD_CHOICES
|
||||
@@ -587,7 +589,7 @@ class PaymentMethodConfig(models.Model):
|
||||
country_code = models.CharField(
|
||||
max_length=2,
|
||||
db_index=True,
|
||||
help_text="ISO 2-letter country code (e.g., US, GB, IN)"
|
||||
help_text="ISO 2-letter country code (e.g., US, GB, PK) or '*' for global"
|
||||
)
|
||||
payment_method = models.CharField(max_length=50, choices=PAYMENT_METHOD_CHOICES)
|
||||
is_enabled = models.BooleanField(default=True)
|
||||
@@ -596,21 +598,17 @@ class PaymentMethodConfig(models.Model):
|
||||
display_name = models.CharField(max_length=100, blank=True)
|
||||
instructions = models.TextField(blank=True, help_text="Payment instructions for users")
|
||||
|
||||
# Manual payment details (for bank_transfer/local_wallet)
|
||||
# Manual payment details (for bank_transfer only)
|
||||
bank_name = models.CharField(max_length=255, blank=True)
|
||||
account_number = models.CharField(max_length=255, blank=True)
|
||||
routing_number = models.CharField(max_length=255, blank=True)
|
||||
swift_code = models.CharField(max_length=255, blank=True)
|
||||
account_title = models.CharField(max_length=255, blank=True, help_text="Account holder name")
|
||||
routing_number = models.CharField(max_length=255, blank=True, help_text="Routing/Sort code")
|
||||
swift_code = models.CharField(max_length=255, blank=True, help_text="SWIFT/BIC code for international")
|
||||
iban = models.CharField(max_length=255, blank=True, help_text="IBAN for international transfers")
|
||||
|
||||
# Additional fields for local wallets
|
||||
wallet_type = models.CharField(max_length=100, blank=True, help_text="E.g., PayTM, PhonePe, etc.")
|
||||
wallet_id = models.CharField(max_length=255, blank=True)
|
||||
|
||||
# Webhook configuration (Stripe/PayPal)
|
||||
webhook_url = models.URLField(blank=True, help_text="Webhook URL for payment gateway callbacks")
|
||||
webhook_secret = models.CharField(max_length=255, blank=True, help_text="Webhook secret for signature verification")
|
||||
api_key = models.CharField(max_length=255, blank=True, help_text="API key for payment gateway integration")
|
||||
api_secret = models.CharField(max_length=255, blank=True, help_text="API secret for payment gateway integration")
|
||||
wallet_type = models.CharField(max_length=100, blank=True, help_text="E.g., JazzCash, EasyPaisa, etc.")
|
||||
wallet_id = models.CharField(max_length=255, blank=True, help_text="Mobile number or wallet ID")
|
||||
|
||||
# Order/priority
|
||||
sort_order = models.IntegerField(default=0)
|
||||
|
||||
@@ -14,6 +14,40 @@ from ....auth.models import Account, Subscription
|
||||
class InvoiceService:
|
||||
"""Service for managing invoices"""
|
||||
|
||||
@staticmethod
|
||||
def get_pending_invoice(subscription: Subscription) -> Optional[Invoice]:
|
||||
"""
|
||||
Get pending invoice for a subscription.
|
||||
Used to find existing invoice during payment processing instead of creating duplicates.
|
||||
"""
|
||||
return Invoice.objects.filter(
|
||||
subscription=subscription,
|
||||
status='pending'
|
||||
).order_by('-created_at').first()
|
||||
|
||||
@staticmethod
|
||||
def get_or_create_subscription_invoice(
|
||||
subscription: Subscription,
|
||||
billing_period_start: datetime,
|
||||
billing_period_end: datetime
|
||||
) -> tuple[Invoice, bool]:
|
||||
"""
|
||||
Get existing pending invoice or create new one.
|
||||
Returns tuple of (invoice, created) where created is True if new invoice was created.
|
||||
"""
|
||||
# First try to find existing pending invoice for this subscription
|
||||
existing = InvoiceService.get_pending_invoice(subscription)
|
||||
if existing:
|
||||
return existing, False
|
||||
|
||||
# Create new invoice if none exists
|
||||
invoice = InvoiceService.create_subscription_invoice(
|
||||
subscription=subscription,
|
||||
billing_period_start=billing_period_start,
|
||||
billing_period_end=billing_period_end
|
||||
)
|
||||
return invoice, True
|
||||
|
||||
@staticmethod
|
||||
def generate_invoice_number(account: Account) -> str:
|
||||
"""
|
||||
@@ -52,6 +86,10 @@ class InvoiceService:
|
||||
) -> Invoice:
|
||||
"""
|
||||
Create invoice for subscription billing period
|
||||
|
||||
Currency logic:
|
||||
- USD for online payments (stripe, paypal)
|
||||
- Local currency (PKR) only for bank_transfer in applicable countries
|
||||
"""
|
||||
account = subscription.account
|
||||
plan = subscription.plan
|
||||
@@ -74,12 +112,22 @@ class InvoiceService:
|
||||
invoice_date = timezone.now().date()
|
||||
due_date = invoice_date + timedelta(days=INVOICE_DUE_DATE_OFFSET)
|
||||
|
||||
# Get currency based on billing country
|
||||
# Determine currency based on payment method:
|
||||
# - Online payments (stripe, paypal): Always USD
|
||||
# - Manual payments (bank_transfer, local_wallet): Local currency for applicable countries
|
||||
from igny8_core.business.billing.utils.currency import get_currency_for_country, convert_usd_to_local
|
||||
currency = get_currency_for_country(account.billing_country)
|
||||
|
||||
# Convert plan price to local currency
|
||||
local_price = convert_usd_to_local(float(plan.price), account.billing_country)
|
||||
payment_method = account.payment_method
|
||||
online_payment_methods = ['stripe', 'paypal']
|
||||
|
||||
if payment_method in online_payment_methods:
|
||||
# Online payments are always in USD
|
||||
currency = 'USD'
|
||||
local_price = float(plan.price)
|
||||
else:
|
||||
# Manual payments use local currency for applicable countries
|
||||
currency = get_currency_for_country(account.billing_country)
|
||||
local_price = convert_usd_to_local(float(plan.price), account.billing_country)
|
||||
|
||||
invoice = Invoice.objects.create(
|
||||
account=account,
|
||||
@@ -95,7 +143,8 @@ class InvoiceService:
|
||||
'billing_period_end': billing_period_end.isoformat(),
|
||||
'subscription_id': subscription.id, # Keep in metadata for backward compatibility
|
||||
'usd_price': str(plan.price), # Store original USD price
|
||||
'exchange_rate': str(local_price / float(plan.price) if plan.price > 0 else 1.0)
|
||||
'exchange_rate': str(local_price / float(plan.price) if plan.price > 0 else 1.0),
|
||||
'payment_method': payment_method
|
||||
}
|
||||
)
|
||||
|
||||
@@ -120,16 +169,28 @@ class InvoiceService:
|
||||
) -> Invoice:
|
||||
"""
|
||||
Create invoice for credit package purchase
|
||||
|
||||
Currency logic:
|
||||
- USD for online payments (stripe, paypal)
|
||||
- Local currency (PKR) only for bank_transfer in applicable countries
|
||||
"""
|
||||
from igny8_core.business.billing.config import INVOICE_DUE_DATE_OFFSET
|
||||
invoice_date = timezone.now().date()
|
||||
|
||||
# Get currency based on billing country
|
||||
# Determine currency based on payment method
|
||||
from igny8_core.business.billing.utils.currency import get_currency_for_country, convert_usd_to_local
|
||||
currency = get_currency_for_country(account.billing_country)
|
||||
|
||||
# Convert credit package price to local currency
|
||||
local_price = convert_usd_to_local(float(credit_package.price), account.billing_country)
|
||||
payment_method = account.payment_method
|
||||
online_payment_methods = ['stripe', 'paypal']
|
||||
|
||||
if payment_method in online_payment_methods:
|
||||
# Online payments are always in USD
|
||||
currency = 'USD'
|
||||
local_price = float(credit_package.price)
|
||||
else:
|
||||
# Manual payments use local currency for applicable countries
|
||||
currency = get_currency_for_country(account.billing_country)
|
||||
local_price = convert_usd_to_local(float(credit_package.price), account.billing_country)
|
||||
|
||||
invoice = Invoice.objects.create(
|
||||
account=account,
|
||||
@@ -143,7 +204,8 @@ class InvoiceService:
|
||||
'credit_package_id': credit_package.id,
|
||||
'credit_amount': credit_package.credits,
|
||||
'usd_price': str(credit_package.price), # Store original USD price
|
||||
'exchange_rate': str(local_price / float(credit_package.price) if credit_package.price > 0 else 1.0)
|
||||
'exchange_rate': str(local_price / float(credit_package.price) if credit_package.price > 0 else 1.0),
|
||||
'payment_method': payment_method
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -627,10 +627,9 @@ def _process_subscription_payment(account, plan_id: str, capture_result: dict) -
|
||||
}
|
||||
)
|
||||
|
||||
# Create invoice
|
||||
invoice = InvoiceService.create_subscription_invoice(
|
||||
account=account,
|
||||
plan=plan,
|
||||
# Get existing pending invoice or create new one (avoids duplicates)
|
||||
invoice, invoice_created = InvoiceService.get_or_create_subscription_invoice(
|
||||
subscription=subscription,
|
||||
billing_period_start=now,
|
||||
billing_period_end=period_end,
|
||||
)
|
||||
|
||||
@@ -10,7 +10,7 @@ Endpoints:
|
||||
"""
|
||||
import stripe
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone as dt_timezone, timedelta
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
@@ -402,6 +402,7 @@ def _activate_subscription(account, stripe_subscription_id: str, plan_id: str, s
|
||||
# Get subscription details from Stripe
|
||||
try:
|
||||
stripe_sub = stripe.Subscription.retrieve(stripe_subscription_id)
|
||||
logger.info(f"Retrieved Stripe subscription: {stripe_subscription_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to retrieve Stripe subscription {stripe_subscription_id}: {e}")
|
||||
return
|
||||
@@ -413,6 +414,40 @@ def _activate_subscription(account, stripe_subscription_id: str, plan_id: str, s
|
||||
logger.error(f"Plan {plan_id} not found for subscription activation")
|
||||
return
|
||||
|
||||
# Extract period dates from Stripe subscription
|
||||
# Note: In newer Stripe API, current_period_start/end are on subscription items, not the subscription
|
||||
try:
|
||||
# Convert to dict for easier access
|
||||
sub_dict = stripe_sub.to_dict_recursive() if hasattr(stripe_sub, 'to_dict_recursive') else dict(stripe_sub)
|
||||
|
||||
# Try to get period from subscription items first (new API)
|
||||
items = sub_dict.get('items', {}).get('data', [])
|
||||
if items:
|
||||
first_item = items[0]
|
||||
period_start = first_item.get('current_period_start')
|
||||
period_end = first_item.get('current_period_end')
|
||||
else:
|
||||
# Fallback to subscription level (old API)
|
||||
period_start = sub_dict.get('current_period_start')
|
||||
period_end = sub_dict.get('current_period_end')
|
||||
|
||||
# If still not found, use billing_cycle_anchor or start_date
|
||||
if not period_start:
|
||||
period_start = sub_dict.get('billing_cycle_anchor') or sub_dict.get('start_date') or sub_dict.get('created')
|
||||
if not period_end:
|
||||
# Default to 30 days from start
|
||||
period_end = period_start + (30 * 24 * 60 * 60) if period_start else None
|
||||
|
||||
cancel_at_end = sub_dict.get('cancel_at_period_end', False)
|
||||
|
||||
logger.info(f"Extracted period: start={period_start}, end={period_end}, cancel_at_end={cancel_at_end}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to extract period dates from Stripe subscription: {e}")
|
||||
# Use current time as fallback
|
||||
period_start = timezone.now().timestamp()
|
||||
period_end = (timezone.now() + timedelta(days=30)).timestamp()
|
||||
cancel_at_end = False
|
||||
|
||||
with transaction.atomic():
|
||||
# Create or update subscription
|
||||
subscription, created = Subscription.objects.update_or_create(
|
||||
@@ -422,27 +457,29 @@ def _activate_subscription(account, stripe_subscription_id: str, plan_id: str, s
|
||||
'stripe_subscription_id': stripe_subscription_id,
|
||||
'status': 'active',
|
||||
'current_period_start': datetime.fromtimestamp(
|
||||
stripe_sub.current_period_start,
|
||||
tz=timezone.utc
|
||||
period_start,
|
||||
tz=dt_timezone.utc
|
||||
),
|
||||
'current_period_end': datetime.fromtimestamp(
|
||||
stripe_sub.current_period_end,
|
||||
tz=timezone.utc
|
||||
period_end,
|
||||
tz=dt_timezone.utc
|
||||
),
|
||||
'cancel_at_period_end': stripe_sub.cancel_at_period_end,
|
||||
'cancel_at_period_end': cancel_at_end,
|
||||
}
|
||||
)
|
||||
|
||||
# Create invoice record
|
||||
# Get existing pending invoice or create new one (avoids duplicates)
|
||||
amount = session.get('amount_total', 0) / 100 # Convert from cents
|
||||
currency = session.get('currency', 'usd').upper()
|
||||
|
||||
invoice = InvoiceService.create_subscription_invoice(
|
||||
account=account,
|
||||
plan=plan,
|
||||
invoice, invoice_created = InvoiceService.get_or_create_subscription_invoice(
|
||||
subscription=subscription,
|
||||
billing_period_start=subscription.current_period_start,
|
||||
billing_period_end=subscription.current_period_end,
|
||||
)
|
||||
|
||||
if not invoice_created:
|
||||
logger.info(f"Found existing pending invoice {invoice.invoice_number} for subscription")
|
||||
|
||||
# Mark invoice as paid
|
||||
InvoiceService.mark_paid(
|
||||
@@ -481,10 +518,15 @@ def _activate_subscription(account, stripe_subscription_id: str, plan_id: str, s
|
||||
}
|
||||
)
|
||||
|
||||
# Update account status if needed
|
||||
# Update account status and plan
|
||||
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"Subscription activated for account {account.id}: "
|
||||
@@ -659,15 +701,33 @@ def _handle_subscription_updated(subscription_data: dict):
|
||||
stripe_status = subscription_data.get('status')
|
||||
new_status = status_map.get(stripe_status, 'active')
|
||||
|
||||
# Update period dates
|
||||
subscription.current_period_start = datetime.fromtimestamp(
|
||||
subscription_data.get('current_period_start'),
|
||||
tz=timezone.utc
|
||||
)
|
||||
subscription.current_period_end = datetime.fromtimestamp(
|
||||
subscription_data.get('current_period_end'),
|
||||
tz=timezone.utc
|
||||
)
|
||||
# Extract period dates - check subscription items first (new API), then subscription level
|
||||
items = subscription_data.get('items', {}).get('data', [])
|
||||
if items:
|
||||
first_item = items[0]
|
||||
period_start = first_item.get('current_period_start')
|
||||
period_end = first_item.get('current_period_end')
|
||||
else:
|
||||
period_start = subscription_data.get('current_period_start')
|
||||
period_end = subscription_data.get('current_period_end')
|
||||
|
||||
# Fallback to billing_cycle_anchor if period dates not found
|
||||
if not period_start:
|
||||
period_start = subscription_data.get('billing_cycle_anchor') or subscription_data.get('start_date')
|
||||
if not period_end and period_start:
|
||||
period_end = period_start + (30 * 24 * 60 * 60) # Default 30 days
|
||||
|
||||
# Only update period dates if we have valid values
|
||||
if period_start:
|
||||
subscription.current_period_start = datetime.fromtimestamp(
|
||||
period_start,
|
||||
tz=dt_timezone.utc
|
||||
)
|
||||
if period_end:
|
||||
subscription.current_period_end = datetime.fromtimestamp(
|
||||
period_end,
|
||||
tz=dt_timezone.utc
|
||||
)
|
||||
subscription.cancel_at_period_end = subscription_data.get('cancel_at_period_end', False)
|
||||
subscription.status = new_status
|
||||
subscription.save()
|
||||
|
||||
Reference in New Issue
Block a user