feat(billing): add missing payment methods and configurations

- Added migration to include global payment method configurations for Stripe and PayPal (both disabled).
- Ensured existing payment methods like bank transfer and manual payment are correctly configured.
- Added database constraints and indexes for improved data integrity in billing models.
- Introduced foreign key relationship between CreditTransaction and Payment models.
- Added webhook configuration fields to PaymentMethodConfig for future payment gateway integrations.
- Updated SignUpFormUnified component to handle payment method selection based on user country and plan.
- Implemented PaymentHistory component to display user's payment history with status indicators.
This commit is contained in:
IGNY8 VPS (Salman)
2025-12-09 06:14:44 +00:00
parent 72d0b6b0fd
commit 4d13a57068
36 changed files with 4159 additions and 253 deletions

View File

@@ -0,0 +1,84 @@
# Generated migration to add subscription FK to Invoice and fix Payment status default
from django.db import migrations, models
import django.db.models.deletion
def populate_subscription_from_metadata(apps, schema_editor):
"""Populate subscription FK from metadata for existing invoices"""
# Use raw SQL to avoid model field issues during migration
from django.db import connection
with connection.cursor() as cursor:
# Get invoices with subscription_id in metadata
cursor.execute("""
SELECT id, metadata
FROM igny8_invoices
WHERE metadata::text LIKE '%subscription_id%'
""")
updated_count = 0
for invoice_id, metadata in cursor.fetchall():
if metadata and isinstance(metadata, dict) and 'subscription_id' in metadata:
try:
sub_id = int(metadata['subscription_id'])
# Check if subscription exists
cursor.execute(
"SELECT id FROM igny8_subscriptions WHERE id = %s",
[sub_id]
)
if cursor.fetchone():
# Update invoice with subscription FK
cursor.execute(
"UPDATE igny8_invoices SET subscription_id = %s WHERE id = %s",
[sub_id, invoice_id]
)
updated_count += 1
except (ValueError, KeyError) as e:
print(f"Could not populate subscription for invoice {invoice_id}: {e}")
print(f"Populated subscription FK for {updated_count} invoices")
class Migration(migrations.Migration):
dependencies = [
('billing', '0007_simplify_payment_statuses'),
('igny8_core_auth', '0011_remove_subscription_payment_method'),
]
operations = [
# Add subscription FK to Invoice
migrations.AddField(
model_name='invoice',
name='subscription',
field=models.ForeignKey(
blank=True,
help_text='Subscription this invoice is for (if subscription-based)',
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name='invoices',
to='igny8_core_auth.subscription'
),
),
# Populate data
migrations.RunPython(
populate_subscription_from_metadata,
reverse_code=migrations.RunPython.noop
),
# Fix Payment status default
migrations.AlterField(
model_name='payment',
name='status',
field=models.CharField(
choices=[
('pending_approval', 'Pending Approval'),
('succeeded', 'Succeeded'),
('failed', 'Failed'),
('refunded', 'Refunded')
],
db_index=True,
default='pending_approval',
max_length=20
),
),
]

View File

@@ -0,0 +1,80 @@
# Migration to add missing payment method configurations
from django.db import migrations
def add_missing_payment_methods(apps, schema_editor):
"""Add stripe and paypal global configs (disabled) and ensure all configs exist"""
PaymentMethodConfig = apps.get_model('billing', 'PaymentMethodConfig')
# Add global Stripe (disabled - waiting for integration)
PaymentMethodConfig.objects.get_or_create(
country_code='*',
payment_method='stripe',
defaults={
'is_enabled': False,
'display_name': 'Credit/Debit Card (Stripe)',
'instructions': 'Stripe payment integration coming soon.',
'sort_order': 1
}
)
# Add global PayPal (disabled - waiting for integration)
PaymentMethodConfig.objects.get_or_create(
country_code='*',
payment_method='paypal',
defaults={
'is_enabled': False,
'display_name': 'PayPal',
'instructions': 'PayPal payment integration coming soon.',
'sort_order': 2
}
)
# Ensure global bank_transfer exists with good instructions
PaymentMethodConfig.objects.get_or_create(
country_code='*',
payment_method='bank_transfer',
defaults={
'is_enabled': True,
'display_name': 'Bank Transfer',
'instructions': 'Bank transfer details will be provided after registration.',
'sort_order': 3
}
)
# Add manual payment as global option
PaymentMethodConfig.objects.get_or_create(
country_code='*',
payment_method='manual',
defaults={
'is_enabled': True,
'display_name': 'Manual Payment (Contact Support)',
'instructions': 'Contact support@igny8.com for manual payment arrangements.',
'sort_order': 10
}
)
print("Added/updated payment method configurations")
def remove_added_payment_methods(apps, schema_editor):
"""Reverse migration"""
PaymentMethodConfig = apps.get_model('billing', 'PaymentMethodConfig')
PaymentMethodConfig.objects.filter(
country_code='*',
payment_method__in=['stripe', 'paypal', 'manual']
).delete()
class Migration(migrations.Migration):
dependencies = [
('billing', '0008_add_invoice_subscription_fk'),
]
operations = [
migrations.RunPython(
add_missing_payment_methods,
reverse_code=remove_added_payment_methods
),
]

View File

@@ -0,0 +1,58 @@
# Migration to add database constraints and indexes for data integrity
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('billing', '0009_add_missing_payment_methods'),
('igny8_core_auth', '0011_remove_subscription_payment_method'),
]
operations = [
# Add DB index on invoice_number for fast lookups
migrations.AlterField(
model_name='invoice',
name='invoice_number',
field=models.CharField(db_index=True, max_length=50, unique=True),
),
# Add index on Payment.status for filtering
migrations.AlterField(
model_name='payment',
name='status',
field=models.CharField(
choices=[
('pending_approval', 'Pending Approval'),
('succeeded', 'Succeeded'),
('failed', 'Failed'),
('refunded', 'Refunded')
],
db_index=True,
default='pending_approval',
max_length=20
),
),
# Add partial unique index on AccountPaymentMethod for single default per account
# This prevents multiple is_default=True per account
migrations.RunSQL(
sql="""
CREATE UNIQUE INDEX billing_account_payment_method_single_default
ON igny8_account_payment_methods (tenant_id)
WHERE is_default = true AND is_enabled = true;
""",
reverse_sql="""
DROP INDEX IF EXISTS billing_account_payment_method_single_default;
"""
),
# Add composite index on Payment for common queries
migrations.AddIndex(
model_name='payment',
index=models.Index(
fields=['account', 'status', '-created_at'],
name='payment_account_status_created_idx'
),
),
]

View File

@@ -0,0 +1,25 @@
# Generated migration to add payment constraints
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('billing', '0010_add_database_constraints'),
]
operations = [
# Add composite unique constraint on manual_reference + tenant_id
# This prevents duplicate payment submissions with same reference for an account
migrations.RunSQL(
sql="""
CREATE UNIQUE INDEX billing_payment_manual_ref_account_unique
ON igny8_payments(tenant_id, manual_reference)
WHERE manual_reference != '' AND manual_reference IS NOT NULL;
""",
reverse_sql="""
DROP INDEX IF EXISTS billing_payment_manual_ref_account_unique;
"""
),
]

View File

@@ -0,0 +1,44 @@
# Generated migration to add Payment FK to CreditTransaction
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('billing', '0011_add_manual_reference_constraint'),
]
operations = [
# Add payment FK field
migrations.AddField(
model_name='credittransaction',
name='payment',
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name='credit_transactions',
to='billing.payment',
help_text='Payment that triggered this credit transaction'
),
),
# Migrate existing reference_id data to payment FK
migrations.RunSQL(
sql="""
UPDATE igny8_credit_transactions ct
SET payment_id = (
SELECT id FROM igny8_payments p
WHERE p.id::text = ct.reference_id
AND ct.reference_id ~ '^[0-9]+$'
LIMIT 1
)
WHERE ct.reference_id IS NOT NULL
AND ct.reference_id != ''
AND ct.reference_id ~ '^[0-9]+$';
""",
reverse_sql=migrations.RunSQL.noop
),
]

View File

@@ -0,0 +1,49 @@
# Generated migration to add webhook fields to PaymentMethodConfig
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('billing', '0012_add_payment_fk_to_credit_transaction'),
]
operations = [
# Add webhook configuration fields
migrations.AddField(
model_name='paymentmethodconfig',
name='webhook_url',
field=models.URLField(
blank=True,
help_text='Webhook URL for payment gateway callbacks (Stripe/PayPal)'
),
),
migrations.AddField(
model_name='paymentmethodconfig',
name='webhook_secret',
field=models.CharField(
max_length=255,
blank=True,
help_text='Webhook secret for signature verification'
),
),
migrations.AddField(
model_name='paymentmethodconfig',
name='api_key',
field=models.CharField(
max_length=255,
blank=True,
help_text='API key for payment gateway integration'
),
),
migrations.AddField(
model_name='paymentmethodconfig',
name='api_secret',
field=models.CharField(
max_length=255,
blank=True,
help_text='API secret for payment gateway integration'
),
),
]