diff --git a/backend/igny8_core/auth/migrations/0016_alter_plan_annual_discount_percent.py b/backend/igny8_core/auth/migrations/0016_alter_plan_annual_discount_percent.py new file mode 100644 index 00000000..22ddf318 --- /dev/null +++ b/backend/igny8_core/auth/migrations/0016_alter_plan_annual_discount_percent.py @@ -0,0 +1,19 @@ +# Generated by Django 5.2.9 on 2025-12-13 20:31 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('igny8_core_auth', '0015_add_plan_original_price'), + ] + + operations = [ + migrations.AlterField( + model_name='plan', + name='annual_discount_percent', + field=models.IntegerField(default=15, help_text='Annual subscription discount percentage (default 15%)', validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100)]), + ), + ] diff --git a/backend/igny8_core/auth/models.py b/backend/igny8_core/auth/models.py index e70016b7..5fb8ff40 100644 --- a/backend/igny8_core/auth/models.py +++ b/backend/igny8_core/auth/models.py @@ -185,10 +185,8 @@ class Plan(models.Model): help_text="Original price (before discount) - shows as crossed out price. Leave empty if no discount." ) billing_cycle = models.CharField(max_length=20, choices=BILLING_CYCLE_CHOICES, default='monthly') - annual_discount_percent = models.DecimalField( - max_digits=5, - decimal_places=2, - default=15.00, + annual_discount_percent = models.IntegerField( + default=15, validators=[MinValueValidator(0), MaxValueValidator(100)], help_text="Annual subscription discount percentage (default 15%)" ) diff --git a/backend/igny8_core/modules/billing/migrations/0016_remove_payment_payment_account_status_created_idx_and_more.py b/backend/igny8_core/modules/billing/migrations/0016_remove_payment_payment_account_status_created_idx_and_more.py new file mode 100644 index 00000000..0ba9cdf2 --- /dev/null +++ b/backend/igny8_core/modules/billing/migrations/0016_remove_payment_payment_account_status_created_idx_and_more.py @@ -0,0 +1,53 @@ +# Generated by Django 5.2.9 on 2025-12-13 20:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('billing', '0015_planlimitusage'), + ] + + operations = [ + migrations.RemoveIndex( + model_name='payment', + name='payment_account_status_created_idx', + ), + migrations.RemoveField( + model_name='invoice', + name='billing_email', + ), + migrations.RemoveField( + model_name='invoice', + name='billing_period_end', + ), + migrations.RemoveField( + model_name='invoice', + name='billing_period_start', + ), + migrations.RemoveField( + model_name='payment', + name='transaction_reference', + ), + migrations.AlterField( + model_name='accountpaymentmethod', + name='type', + field=models.CharField(choices=[('stripe', 'Stripe (Credit/Debit Card)'), ('paypal', 'PayPal'), ('bank_transfer', 'Bank Transfer (Manual)'), ('local_wallet', 'Local Wallet (Manual)'), ('manual', 'Manual Payment')], db_index=True, max_length=50), + ), + migrations.AlterField( + model_name='credittransaction', + name='reference_id', + field=models.CharField(blank=True, help_text='DEPRECATED: Use payment FK. Legacy reference (e.g., payment id, invoice id)', max_length=255), + ), + migrations.AlterField( + model_name='paymentmethodconfig', + name='payment_method', + field=models.CharField(choices=[('stripe', 'Stripe (Credit/Debit Card)'), ('paypal', 'PayPal'), ('bank_transfer', 'Bank Transfer (Manual)'), ('local_wallet', 'Local Wallet (Manual)'), ('manual', 'Manual Payment')], max_length=50), + ), + migrations.AlterField( + model_name='paymentmethodconfig', + name='webhook_url', + field=models.URLField(blank=True, help_text='Webhook URL for payment gateway callbacks'), + ), + ] diff --git a/frontend/src/components/auth/SignUpFormUnified.tsx b/frontend/src/components/auth/SignUpFormUnified.tsx index 80d3df38..49fc55b9 100644 --- a/frontend/src/components/auth/SignUpFormUnified.tsx +++ b/frontend/src/components/auth/SignUpFormUnified.tsx @@ -251,6 +251,12 @@ export default function SignUpFormUnified({ }; const extractFeatures = (plan: Plan): string[] => { + // Use features from plan's JSON field if available, otherwise build from limits + if (plan.features && plan.features.length > 0) { + return plan.features; + } + + // Fallback to building from plan limits const features: string[] = []; features.push(`${plan.max_sites} ${plan.max_sites === 1 ? 'Site' : 'Sites'}`); features.push(`${plan.max_users} ${plan.max_users === 1 ? 'User' : 'Users'}`); @@ -628,9 +634,9 @@ export default function SignUpFormUnified({ - {/* Features - 3 rows x 2 columns = 6 features */} + {/* Features - All features in 2 columns */}
- {features.slice(0, 6).map((feature, idx) => ( + {features.map((feature, idx) => (
{feature} diff --git a/frontend/src/components/ui/pricing-table/PricingTable.tsx b/frontend/src/components/ui/pricing-table/PricingTable.tsx index ebeef4d0..6ed59bb6 100644 --- a/frontend/src/components/ui/pricing-table/PricingTable.tsx +++ b/frontend/src/components/ui/pricing-table/PricingTable.tsx @@ -2,6 +2,7 @@ import { useState } from 'react'; export interface PricingPlan { id?: number; + slug?: string; name: string; price: string | number; // Current displayed price (will be calculated based on period) monthlyPrice?: string | number; // Base monthly price (used for annual discount calculation) @@ -100,40 +101,40 @@ export default function PricingTable({ )} {showToggle && (
-
- - - -
- {billingPeriod === 'annually' && ( -
- +
+
+ + + +
+ {billingPeriod === 'annually' && ( + - Save {plans[0]?.annualDiscountPercent || 15}% with annual billing + Save {Math.round(plans[0]?.annualDiscountPercent || 15)}% with annual billing -
- )} + )} +
)} -
+
{plans.map((plan, index) => { const isHighlighted = plan.highlighted || false; // Use explicit highlighted prop const displayPrice = getDisplayPrice(plan); @@ -157,7 +158,7 @@ export default function PricingTable({ return (
{plan.buttonText || (plan.price === 0 || plan.monthlyPrice === 0 ? 'Start Free' : 'Choose Plan')} diff --git a/frontend/src/marketing/pages/Pricing.tsx b/frontend/src/marketing/pages/Pricing.tsx index dbb99e50..88d1c7fe 100644 --- a/frontend/src/marketing/pages/Pricing.tsx +++ b/frontend/src/marketing/pages/Pricing.tsx @@ -19,6 +19,7 @@ interface Plan { name: string; slug?: string; price: number | string; + original_price?: number | string; annual_discount_percent?: number; is_featured?: boolean; max_sites?: number; @@ -69,9 +70,11 @@ const convertToPricingPlan = (plan: Plan): PricingPlan => { return { id: plan.id, + slug: plan.slug, name: plan.name, monthlyPrice: monthlyPrice, price: monthlyPrice, + originalPrice: plan.original_price ? (typeof plan.original_price === 'number' ? plan.original_price : parseFloat(String(plan.original_price))) : undefined, annualDiscountPercent: plan.annual_discount_percent || 15, period: '/month', description: description, @@ -206,7 +209,7 @@ const Pricing: React.FC = () => {