refactor
This commit is contained in:
366
final-tenancy-accounts-payments/PRICING-TO-PAID-SIGNUP-GAP.md
Normal file
366
final-tenancy-accounts-payments/PRICING-TO-PAID-SIGNUP-GAP.md
Normal file
@@ -0,0 +1,366 @@
|
||||
# CRITICAL GAP: Pricing Page to Paid Plans Signup
|
||||
## Issue Not Covered in Previous Documentation
|
||||
|
||||
**Discovered:** Marketing pricing page analysis
|
||||
**Severity:** HIGH - Payment flow is broken
|
||||
|
||||
---
|
||||
|
||||
## Problem Identified
|
||||
|
||||
### Current State (Broken)
|
||||
|
||||
**Pricing Page:** [`frontend/src/marketing/pages/Pricing.tsx:307-316`](frontend/src/marketing/pages/Pricing.tsx:307)
|
||||
|
||||
ALL plan cards (Starter $89, Growth $139, Scale $229) have identical buttons:
|
||||
```tsx
|
||||
<a href="https://app.igny8.com/signup">
|
||||
Start free trial
|
||||
</a>
|
||||
```
|
||||
|
||||
**This means:**
|
||||
- ❌ User clicks "Start free trial" on Growth ($139/month)
|
||||
- ❌ Goes to https://app.igny8.com/signup
|
||||
- ❌ Gets FREE TRIAL with free-trial plan (0 payment)
|
||||
- ❌ NO WAY to actually sign up for paid plans from pricing page
|
||||
|
||||
### What's Missing
|
||||
**There is NO paid plan signup flow at all.**
|
||||
|
||||
---
|
||||
|
||||
## Required Solution
|
||||
|
||||
### Option A: Query Parameter Routing (RECOMMENDED)
|
||||
|
||||
**Pricing page buttons:**
|
||||
```tsx
|
||||
// Starter
|
||||
<a href="https://app.igny8.com/signup?plan=starter">
|
||||
Get Started - $89/mo
|
||||
</a>
|
||||
|
||||
// Growth
|
||||
<a href="https://app.igny8.com/signup?plan=growth">
|
||||
Get Started - $139/mo
|
||||
</a>
|
||||
|
||||
// Scale
|
||||
<a href="https://app.igny8.com/signup?plan=scale">
|
||||
Get Started - $229/mo
|
||||
</a>
|
||||
|
||||
// Free trial stays same
|
||||
<a href="https://app.igny8.com/signup">
|
||||
Start Free Trial
|
||||
</a>
|
||||
```
|
||||
|
||||
**App signup page logic:**
|
||||
```tsx
|
||||
// In SignUpForm.tsx
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const planSlug = searchParams.get('plan');
|
||||
|
||||
if (planSlug) {
|
||||
// Paid plan signup - show payment form
|
||||
navigate('/payment', { state: { planSlug } });
|
||||
} else {
|
||||
// Free trial - current simple form
|
||||
// Continue with free trial registration
|
||||
}
|
||||
```
|
||||
|
||||
**Backend:**
|
||||
```python
|
||||
# RegisterSerializer checks plan query/body
|
||||
plan_slug = request.data.get('plan_slug') or request.GET.get('plan')
|
||||
|
||||
if plan_slug in ['starter', 'growth', 'scale']:
|
||||
# Paid plan - requires payment
|
||||
plan = Plan.objects.get(slug=plan_slug)
|
||||
account.status = 'pending_payment'
|
||||
# Create Subscription with status='pending_payment'
|
||||
# Wait for payment confirmation
|
||||
else:
|
||||
# Free trial
|
||||
plan = Plan.objects.get(slug='free-trial')
|
||||
account.status = 'trial'
|
||||
# Immediate access
|
||||
```
|
||||
|
||||
### Option B: Separate Payment Route
|
||||
|
||||
**Pricing page:**
|
||||
```tsx
|
||||
// Paid plans go to /payment
|
||||
<a href="https://app.igny8.com/payment?plan=starter">
|
||||
Get Started - $89/mo
|
||||
</a>
|
||||
|
||||
// Free trial stays /signup
|
||||
<a href="https://app.igny8.com/signup">
|
||||
Start Free Trial
|
||||
</a>
|
||||
```
|
||||
|
||||
**Create new route:**
|
||||
- `/signup` - Free trial only (current implementation)
|
||||
- `/payment` - Paid plans with payment form
|
||||
|
||||
---
|
||||
|
||||
## Implementation Required
|
||||
|
||||
### 1. Update Pricing Page CTAs
|
||||
|
||||
**File:** [`frontend/src/marketing/pages/Pricing.tsx:307`](frontend/src/marketing/pages/Pricing.tsx:307)
|
||||
|
||||
Add plan data to tiers:
|
||||
```tsx
|
||||
const tiers = [
|
||||
{
|
||||
name: "Starter",
|
||||
slug: "starter", // NEW
|
||||
price: "$89",
|
||||
// ... rest
|
||||
},
|
||||
// ...
|
||||
];
|
||||
```
|
||||
|
||||
Update CTA button logic:
|
||||
```tsx
|
||||
<a
|
||||
href={`https://app.igny8.com/signup?plan=${tier.slug}`}
|
||||
className={...}
|
||||
>
|
||||
{tier.price === "Free" ? "Start free trial" : `Get ${tier.name} - ${tier.price}/mo`}
|
||||
</a>
|
||||
```
|
||||
|
||||
### 2. Create Payment Flow Page
|
||||
|
||||
**File:** `frontend/src/pages/Payment.tsx` (NEW)
|
||||
|
||||
```tsx
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
export default function Payment() {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const [selectedPlan, setSelectedPlan] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(location.search);
|
||||
const planSlug = params.get('plan');
|
||||
|
||||
if (!planSlug) {
|
||||
// No plan selected, redirect to pricing
|
||||
navigate('/pricing');
|
||||
return;
|
||||
}
|
||||
|
||||
// Load plan details from API
|
||||
fetch(`/api/v1/auth/plans/?slug=${planSlug}`)
|
||||
.then(res => res.json())
|
||||
.then(data => setSelectedPlan(data.results[0]));
|
||||
}, [location]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Complete Your Subscription</h1>
|
||||
{selectedPlan && (
|
||||
<>
|
||||
<h2>{selectedPlan.name} - ${selectedPlan.price}/{selectedPlan.billing_cycle}</h2>
|
||||
|
||||
{/* Payment method selection */}
|
||||
<div>
|
||||
<h3>Select Payment Method</h3>
|
||||
<button>Credit Card (Stripe) - Coming Soon</button>
|
||||
<button>PayPal - Coming Soon</button>
|
||||
<button>Bank Transfer</button>
|
||||
</div>
|
||||
|
||||
{/* If bank transfer selected, show form */}
|
||||
<form onSubmit={handleBankTransferSubmit}>
|
||||
<input name="email" placeholder="Your email" required />
|
||||
<input name="account_name" placeholder="Account name" required />
|
||||
<button>Submit - We'll send payment details</button>
|
||||
</form>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Update Backend Registration
|
||||
|
||||
**File:** [`backend/igny8_core/auth/serializers.py:276`](backend/igny8_core/auth/serializers.py:276)
|
||||
|
||||
Add plan_slug handling:
|
||||
```python
|
||||
def create(self, validated_data):
|
||||
from django.db import transaction
|
||||
from igny8_core.business.billing.models import CreditTransaction
|
||||
|
||||
with transaction.atomic():
|
||||
# Check for plan_slug in request
|
||||
plan_slug = validated_data.get('plan_slug')
|
||||
|
||||
if plan_slug in ['starter', 'growth', 'scale']:
|
||||
# PAID PLAN - requires payment
|
||||
plan = Plan.objects.get(slug=plan_slug, is_active=True)
|
||||
account_status = 'pending_payment'
|
||||
initial_credits = 0 # No credits until payment
|
||||
# Do NOT create CreditTransaction yet
|
||||
else:
|
||||
# FREE TRIAL - immediate access
|
||||
try:
|
||||
plan = Plan.objects.get(slug='free-trial', is_active=True)
|
||||
except Plan.DoesNotExist:
|
||||
plan = Plan.objects.get(slug='free', is_active=True)
|
||||
account_status = 'trial'
|
||||
initial_credits = plan.get_effective_credits_per_month()
|
||||
|
||||
# ... create user and account ...
|
||||
|
||||
account = Account.objects.create(
|
||||
name=account_name,
|
||||
slug=slug,
|
||||
owner=user,
|
||||
plan=plan,
|
||||
credits=initial_credits,
|
||||
status=account_status
|
||||
)
|
||||
|
||||
# Only log credits for trial (paid accounts get credits after payment)
|
||||
if account_status == 'trial' and initial_credits > 0:
|
||||
CreditTransaction.objects.create(
|
||||
account=account,
|
||||
transaction_type='subscription',
|
||||
amount=initial_credits,
|
||||
balance_after=initial_credits,
|
||||
description=f'Free trial credits from {plan.name}',
|
||||
metadata={'registration': True, 'trial': True}
|
||||
)
|
||||
|
||||
# ... rest of code ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Current Pricing Page Button Behavior
|
||||
|
||||
**All buttons currently do this:**
|
||||
```
|
||||
igny8.com/pricing
|
||||
├─ Starter card → "Start free trial" → https://app.igny8.com/signup
|
||||
├─ Growth card → "Start free trial" → https://app.igny8.com/signup
|
||||
└─ Scale card → "Start free trial" → https://app.igny8.com/signup
|
||||
```
|
||||
|
||||
**Result:** NO WAY to sign up for paid plans.
|
||||
|
||||
---
|
||||
|
||||
## Recommended Implementation
|
||||
|
||||
### Marketing Site (igny8.com)
|
||||
```tsx
|
||||
// Pricing.tsx - Update tier CTAs
|
||||
|
||||
{tier.price === "Free" ? (
|
||||
<a href="https://app.igny8.com/signup">
|
||||
Start Free Trial
|
||||
</a>
|
||||
) : (
|
||||
<a href={`https://app.igny8.com/signup?plan=${tier.slug}`}>
|
||||
Get {tier.name} - {tier.price}/mo
|
||||
</a>
|
||||
)}
|
||||
```
|
||||
|
||||
### App Site (app.igny8.com)
|
||||
```tsx
|
||||
// SignUpForm.tsx - Check for plan parameter
|
||||
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const planSlug = params.get('plan');
|
||||
|
||||
if (planSlug && ['starter', 'growth', 'scale'].includes(planSlug)) {
|
||||
// Redirect to payment page
|
||||
navigate(`/payment?plan=${planSlug}`);
|
||||
}
|
||||
// Otherwise continue with free trial signup
|
||||
}, []);
|
||||
```
|
||||
|
||||
### Payment Page (NEW)
|
||||
- Route: `/payment?plan=starter`
|
||||
- Shows: Plan details, payment method selection
|
||||
- Options: Bank Transfer (active), Stripe (coming soon), PayPal (coming soon)
|
||||
- Flow: Collect info → Create pending account → Send payment instructions
|
||||
|
||||
---
|
||||
|
||||
## Update to Requirements
|
||||
|
||||
### Add to FINAL-IMPLEMENTATION-REQUIREMENTS.md
|
||||
|
||||
**New Section: E. Paid Plans Signup Flow**
|
||||
|
||||
```markdown
|
||||
### CRITICAL ISSUE E: No Paid Plan Signup Path
|
||||
|
||||
#### Problem
|
||||
Marketing pricing page shows paid plans ($89, $139, $229) but all buttons go to free trial signup.
|
||||
No way for users to actually subscribe to paid plans.
|
||||
|
||||
#### Fix
|
||||
1. Pricing page buttons must differentiate:
|
||||
- Free trial: /signup (no params)
|
||||
- Paid plans: /signup?plan=starter (with plan slug)
|
||||
|
||||
2. Signup page must detect plan parameter:
|
||||
- If plan=paid → Redirect to /payment
|
||||
- If no plan → Free trial signup
|
||||
|
||||
3. Create /payment page:
|
||||
- Show selected plan details
|
||||
- Payment method selection (bank transfer active, others coming soon)
|
||||
- Collect user info + payment details
|
||||
- Create account with status='pending_payment'
|
||||
- Send payment instructions
|
||||
|
||||
4. Backend must differentiate:
|
||||
- Free trial: immediate credits and access
|
||||
- Paid plans: 0 credits, pending_payment status, wait for confirmation
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Files That Need Updates
|
||||
|
||||
### Frontend
|
||||
1. `frontend/src/marketing/pages/Pricing.tsx:307` - Add plan slug to CTAs
|
||||
2. `frontend/src/components/auth/SignUpForm.tsx` - Detect plan param, redirect to payment
|
||||
3. `frontend/src/pages/Payment.tsx` - NEW FILE - Payment flow page
|
||||
4. `frontend/src/App.tsx` - Add /payment route
|
||||
|
||||
### Backend
|
||||
5. `backend/igny8_core/auth/serializers.py:276` - Handle plan_slug for paid plans
|
||||
6. `backend/igny8_core/auth/views.py:978` - Expose plan_slug in RegisterSerializer
|
||||
|
||||
---
|
||||
|
||||
## This Was Missing From All Previous Documentation
|
||||
|
||||
✅ Free trial flow - COVERED
|
||||
❌ Paid plan subscription flow - **NOT COVERED**
|
||||
|
||||
**This is a critical gap that needs to be added to the implementation plan.**
|
||||
Reference in New Issue
Block a user