add Basic tier, monthly/yearly billing, and supplier detail redesign

- New Basic tier (€39/mo or €349/yr): verified directory listing with
  enquiry form, contact sidebar, services checklist, social links, no leads
- Monthly + yearly billing for all paid tiers; yearly defaults selected
  in signup wizard with CSS-only price toggle (no JS state)
- Redesigned supplier_detail.html: navy hero with court-grid pattern,
  two-column body+sidebar for Basic+, tier-adaptive CTA strips
- Supplier enquiry form: HTMX-powered, rate-limited 5/24h, email relayed
  via worker task; supplier_enquiries table tracks all submissions
- New supplier columns: services_offered, contact_role, linkedin_url,
  instagram_url, youtube_url (migration 0012)
- _lead_tier_required decorator restricts lead feed to growth/pro;
  Basic users see overview + listing tabs only
- Admin: basic tier in dropdown, new fields in form/detail + enquiry count
- setup_paddle.py: adds 4 new products with yearly interval support
- Webhook handler strips _monthly/_yearly suffixes, Basic gets 0 credits
  and is_verified=1; existing growth/pro webhooks unchanged
- Sort order: pro > growth > basic > free
- 572 tests pass (+2 new for basic tier + yearly webhook variants)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Deeman
2026-02-19 15:03:21 +01:00
parent 07c7e61049
commit 536eefffdb
22 changed files with 1592 additions and 350 deletions

View File

@@ -296,6 +296,55 @@ class TestSupplierSubscriptionActivated:
assert sup[0][0] == 1 # highlight
assert sup[0][1] == 1 # is_verified
async def test_basic_plan_sets_tier_zero_credits_and_verified(
self, client, db, supplier, paddle_products, test_user,
):
"""Basic plan: tier='basic', 0 credits, is_verified=1, no ledger entry."""
payload = make_supplier_activation_payload(
items=[],
supplier_id=supplier["id"],
plan="supplier_basic",
user_id=test_user["id"],
)
resp = await _post_webhook(client, payload)
assert resp.status_code == 200
row = await db.execute_fetchall(
"SELECT tier, credit_balance, monthly_credits, is_verified FROM suppliers WHERE id = ?",
(supplier["id"],),
)
assert row[0][0] == "basic"
assert row[0][1] == 0
assert row[0][2] == 0
assert row[0][3] == 1
# No credit ledger entry for Basic (0 credits)
ledger = await db.execute_fetchall(
"SELECT id FROM credit_ledger WHERE supplier_id = ?",
(supplier["id"],),
)
assert len(ledger) == 0
async def test_yearly_plan_derives_correct_tier(
self, client, db, supplier, paddle_products, test_user,
):
"""Yearly plan key suffix is stripped; tier derives correctly."""
payload = make_supplier_activation_payload(
items=[("pri_growth", 1)],
supplier_id=supplier["id"],
plan="supplier_growth_yearly", # yearly variant
user_id=test_user["id"],
)
resp = await _post_webhook(client, payload)
assert resp.status_code == 200
row = await db.execute_fetchall(
"SELECT tier, credit_balance FROM suppliers WHERE id = ?",
(supplier["id"],),
)
assert row[0][0] == "growth"
assert row[0][1] == 30
async def test_no_supplier_id_is_noop(
self, client, db, supplier, paddle_products, test_user,
):