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:
@@ -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
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -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
|
||||
),
|
||||
]
|
||||
@@ -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'
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -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;
|
||||
"""
|
||||
),
|
||||
]
|
||||
@@ -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
|
||||
),
|
||||
]
|
||||
@@ -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'
|
||||
),
|
||||
),
|
||||
]
|
||||
Reference in New Issue
Block a user