15 KiB
Got you. Let’s lock the design first, then walk a very concrete implementation plan.
1. Design baseline
1.1 What is actually enforced vs just marketing
| Area | Enforced in backend | Marketing only / derived text |
|---|---|---|
| Sites per account | Yes, via Plan.max_sites and Site.get_max_sectors_limit() for industries/sectors |
Text on pricing page rephrases this, no extra logic |
| Users per account | Yes, via per plan user limit if present in Plan model (or enforced through Account → User count) | Pricing shows same number, no separate enforcement |
| Credits per month | Yes, Plan.included_credits / Plan.get_effective_credits_per_month() → Account.credits on signup and renewal |
Words per month etc derived from credits only |
| Payment method | Yes, new payment_method on Account and Subscription (stripe, paypal, bank_transfer) |
UI labels like “Bank transfer” or “Coming soon” |
| Keywords, clusters, ideas | Not enforced for now, only advisory copy | Keep in pricing copy, treat as soft guidance |
| Words per month | Not enforced directly | Approx = credits * X words, shown only on frontend |
| Daily content tasks | Not enforced directly | Approx, shown only on frontend |
| Images per month | Optional: either map to credits or leave as copy | Pricing text only for now |
Decision: backend hard limits are Sites, (Industries/Sectors), Users, Credits. Everything else is just marketing descriptions driven from those hard limits.
2. Target user flows (one view)
2.1 High level text diagram
Pricing page (marketing)
→ Choose plan (Starter / Growth / Scale)
→ Click "Start" → App Register (with plan_id preselected)
Register
→ POST /api/v1/auth/register/ with plan_id + payment_method=bank_transfer
→ Create User + Account(plan) + Subscription(payment_method, status='pending_payment')
→ Seed Account.credits = plan.get_effective_credits_per_month()
→ User can log in and use app while account.status in ['trial', 'active', 'pending_payment'] if you allow pre-payment use
App usage (monthly)
→ Login → JWT → AccountContextMiddleware sets request.account
→ User works in Planner / Writer
→ Every AI call:
- CreditService.check_credits()
- AICore runs model
- CreditService.deduct_credits_for_operation()
Renewal
→ Admin confirms bank transfer for next period
→ Or Stripe webhook in future
→ Subscription period dates updated
→ Account.credits reset to plan monthly credits
→ Account.status set to 'active'
Expiry / non payment
→ Subscription.status becomes 'past_due'/'canceled'
→ Account.status set to 'suspended'
→ Login / AI calls blocked with clear message
3. Phase A – Plans and pricing alignment
A1. Make Plan the single source of truth for limits
Implementation
-
In
Planmodel ensure we have at least:slug,name,price,billing_cycle,included_credits,max_sites,max_users,max_industries(or current field names). -
Add helper:
Plan.to_public_dict()that returns exactly what pricing needs:name,slug,price,billing_cyclemax_sites,max_users,included_credits- optional:
tagline,recommended,order_index(pure marketing fields stored on Plan to avoid hardcoded frontend).
-
Update
/api/v1/auth/plans/to return only these fields plus computed soft fields:approx_words_per_month = included_credits * WORDS_PER_CREDIT(constant, eg 120).approx_images_per_month = included_credits / CREDITS_PER_IMAGE(if you want).
Verification
-
Hit
GET /api/v1/auth/plans/and confirm:- Each plan matches DB row and has only canonical hard limit fields plus derived estimates.
- No legacy fields leaking like “keywords_limit” unless they truly exist.
-
Update pricing page to build cards from this response instead of static JSON.
A2. Simplify pricing UI to reflect hard limits
Implementation
-
On
igny8.com/pricing:-
Show for each plan:
- Sites, users, AI credits per month (clear).
- A line like “Up to ~120K AI generated words per month” using API derived field.
- Keep keywords/clusters/ideas as descriptive bullet points, but not used anywhere in backend.
-
-
Change CTA text from “Start free trial” to “Get started” or keep label but functionally treat as plan signup.
Verification
- Disable JS and verify pricing still renders correct values from API.
- Check for each plan: numbers on page match
Planentries and included_credits.
4. Phase B – Signup, subscription and payment methods
B1. Extend models for payment_method and external_payment_id
Implementation
-
Add migrations as per audit:
Account.payment_method(choices: stripe, paypal, bank_transfer, default stripe).Subscription.payment_methodsame choices.Subscription.external_payment_idnullable.- Make
stripe_subscription_idnullable and drop unique if necessary.
-
Backfill existing rows: set
payment_method='stripe'wherestripe_subscription_idis not null.
Verification
-
Run migrations in staging:
- Confirm existing subscriptions untouched but now have
payment_method='stripe'. - New columns visible in Django admin.
- Confirm existing subscriptions untouched but now have
B2. Wire pricing CTA to register with plan
Implementation
-
Pricing card button:
- On click, route to
app.igny8.com/register?plan=starteretc.
- On click, route to
-
Register page in app:
- On mount, call
/api/v1/auth/plans/and validate thatplanquery param is a validslug. - Show selected plan summary in the form.
- On mount, call
-
Registration form posts:
-
POST /api/v1/auth/register/with body:{ "email": "...", "password": "...", "account_name": "...", "plan_slug": "starter", "payment_method": "bank_transfer" } -
Backend resolves
plan_slugtoPlan, sets on Account and Subscription, setspayment_method, setsSubscription.status='pending_payment'andAccount.status='pending_payment'ortrial, and seedsAccount.credits = plan.get_effective_credits_per_month().
-
-
Wrap creation in a database transaction as per audit.
Verification
-
From pricing go through full register flow:
- Check DB: new User, Account with correct Plan, Subscription linked,
payment_method='bank_transfer', credits set to plan value. - Decode JWT after login and verify
account_idmatches created account and account.status is as expected.
- Check DB: new User, Account with correct Plan, Subscription linked,
B3. Bank transfer confirmation flow
Implementation
-
Create admin endpoint
POST /api/v1/billing/confirm-bank-transfer/:-
Guarded by
IsAdminOrOwneror separateIsStaffAdmin. -
Accepts
subscription_id,external_payment_id, amount, payer details, optional proof URL. -
Inside a transaction:
- Validate subscription belongs to account.
- Set
Subscription.payment_method='bank_transfer',external_payment_id=...,status='active', and newcurrent_period_startandcurrent_period_end. - Set
Account.status='active'. - Reset
Account.credits = plan.get_effective_credits_per_month()and create aCreditTransactionlog entry.
-
Verification
-
In staging, create an account via bank transfer path, then hit admin endpoint:
- Confirm status flips from pending to active.
- Credits reset correctly.
- User can login and use AI features without seeing payment required errors.
5. Phase C – Auth, tenancy and API key correctness
C1. Central account+plan validation
Implementation
-
Extract helper
validate_account_and_plan(account)fromAccountContextMiddleware.-
Checks:
accountexists and not deleted.account.status in allowed set(trial, active, pending_payment if you allow usage).account.planexists and plan active.
-
-
Use this helper:
- In middleware for JWT/session requests.
- In
APIKeyAuthentication.authenticate()for API key requests.
Verification
-
Test matrix:
- Account active → both JWT and API key get access.
- Account suspended or plan inactive → both JWT and API key requests receive consistent 402/403.
C2. Throttling and tenant scoping
Implementation
-
Update
DebugScopedRateThrottle:- Keyed by
(scope, account.id)instead of user only. - Only bypass when
DEBUG=Trueor a special flag is set.
- Keyed by
Verification
-
Use a script to hit AI endpoints many times:
- Same account should start getting 429 after configured limit.
- Different accounts are throttled independently.
6. Phase D – Credits as single limiting mechanism
D1. Standardize credit operations
Implementation
-
In
CreditServicedefine per operation costs:CLUSTER_COST_PER_KEYWORD,IDEA_COST_PER_CLUSTER,CONTENT_COST_PER_1K_TOKENS,IMAGE_COST_PER_IMAGE, etc.
-
For each AI entry point in Planner and Writer:
- Before calling any AI engine, call
CreditService.check_credits(account, operation_type, units)and fail fast with a friendly error if credits insufficient. - After success, call
deduct_credits_for_operation.
- Before calling any AI engine, call
-
Ensure
AIEngine.executenever calls external API without passing through CreditService.
Verification
-
With a test account:
- Set credits small (for example 10 credits).
- Run a sequence of operations until credits exactly hit zero.
- Verify next AI attempt fails with “not enough credits” and no external API call is made.
D2. Monthly credits reset on renewal
Implementation
-
In renewal handler (Stripe webhook or bank transfer confirm):
-
Always:
- Set
Account.credits = plan.get_effective_credits_per_month()as new monthly allowance (no rollover), or apply your chosen policy.
- Set
-
Verification
-
Manipulate subscription dates to simulate new period, run renewal handler:
- Compare previous credits to new credits and confirm reset logic is correct.
7. Phase E – End to end user flows and QA checklists
For each flow from Final_Flow_Tenancy plus the new marketing connection, create a short verification table. Example:
E1. Flow P – Pricing to first login
| Step | Action | Expected backend state |
|---|---|---|
| P1 | Open pricing page | GET /auth/plans/ called, numbers match Plan rows |
| P2 | Click “Start” on Growth | Browser navigates to /register?plan=growth |
| P3 | Submit registration | User, Account(plan=growth), Subscription(payment_method='bank_transfer', status='pending_payment'), Account.credits set |
| P4 | Login | JWT issued, request.account set to new account |
| P5 | Visit Sites page | Only default site or none, request.account matches account.id |
E2. Flow C – Credits and AI usage
| Step | Action | Expected result |
|---|---|---|
| C1 | Note current Account.credits | Shows value N from Plan |
| C2 | Run auto cluster for small batch | Credits drop by expected amount, clusters created |
| C3 | Generate content draft | Credits drop again, Content row created |
| C4 | Keep triggering until credits zero | Last allowed request succeeds, next request returns “insufficient credits” without external call |
E3. Flow R – Renewal
| Step | Action | Expected result |
|---|---|---|
| R1 | Admin confirms next month bank transfer | Subscription dates advanced, status active, Account.credits reset to monthly quota |
| R2 | User logs in and runs AI | Requests succeed and credits decrement from new monthly total |
E4. Flow X – Expiry
| Step | Action | Expected result |
|---|---|---|
| X1 | Set Subscription.status='past_due' and Account.status='suspended' | Login shows “Active subscription required”, AI endpoints blocked |
| X2 | Confirm payment again | Account.status back to active, login and AI work |
8. Phase F – Tests and CI
Wrap everything with tests so this stays correct.
-
Unit tests for:
- Plan API response shape vs Plan model.
- Registration transaction: no partial user or account on failure.
- validate_account_and_plan for different statuses.
- CreditService check and deduct behavior for each operation.
-
Integration tests for:
- Pricing → register → login → run AI until credits exhausted.
- Bank transfer confirmation and renewal.
- API key access for active vs suspended accounts.
Once these phases are implemented and the flow tables all pass in staging, you have a fully connected, credit based, monthly subscription system where marketing, signup, tenancy, billing, and AI usage all match the single Final_Flow_Tenancy spec plus the two audits.