Koovira Dev Docs
30-core/membership.md

Membership & Access Control

Status: ๐ŸŸก Partially implemented (plans priced; recurring/renewal billing not running) ยท Last validated: 2026-09-06

Plans, tiers, feature gating and per-module membership. Validated against classs/KVMembershipEngine.php, the per-module *MembershipModule.php classes, and the live schema.


Architecture

  • KVMembershipEngine (classs/KVMembershipEngine.php) โ€” the central engine.
  • Per-module membership modules wrap it with vertical-specific plan logic:

ClassifiedsMembershipModule, FoodMembershipModule, HotelMembershipModule, JobsMembershipModule, JobSeekerMembershipModule, FreelanceMembershipModule, ServicesMembershipModule, RealEstateMembershipModule, RideMembershipModule, FitnessMembershipModule, HopinMembershipModule, KvFitnessMembershipPlans.

Data model (2026-09-06) โ€” the price is NOT on the plan

TableRowsRole
kv_membership_plans77plan catalogue โ€” no price column
kv_membership_plan_prices136the price source of truth
kv_memberships116 (recounted; was 112)actual subscriptions (who bought what)
wallet_user_memberships280 (recounted; was 257)membership rows funded via wallet
wallet_membership_plans3wallet-tier plans

kv_membership_plans columns: rowId, module_key, plan_code, tier_name enum(free,silver,gold,enterprise), plan_name, description, is_active, sort_order. There is no price column here.

kv_membership_plan_prices columns: plan_id (โ†’ plan), module_key, plan_code, billing_cycle enum(monthly,yearly), base_price decimal, currency, validity_days, gst_applicable, gst_percent, is_active.

Landmine (validated in memory membership-pricing-canonical-path): never read a price from kv_membership_plans (it has none) or from config/module_pricing.php (an empty stub). Always join kv_membership_plans โ†’ kv_membership_plan_prices on plan_id (+ billing_cycle).

Lifecycle

free (default) โ†’ purchase (silver/gold/enterprise) โ†’ active (kv_memberships row, validity from validity_days) โ†’ expiring โ†’ expired / cancelled. Purchase is wallet-funded (โ†’ wallet_user_memberships, WalletModule::debit).

Gap (validated): kv_billing_subscriptions / kv_billing_recurring_cycles = 0 rows โ€” there is no recurring/renewal billing running; annual plans do not auto-renew today.

Feature gating

Two independent gates:

  1. Module on/off โ€” kv_module_control (is the vertical live/visible). See

../00-architecture/module-registry.md.

  1. Tier entitlement โ€” the merchant's active kv_memberships tier for that module_key gates

limits/features. WalletModule::getPlatformFeePercentForUser() is membership-aware (higher tier โ†’ lower platform fee).

Business rules

  • BR-MEM-001 โ€” price = kv_membership_plan_prices.base_price by plan_id + billing_cycle; never elsewhere.
  • BR-MEM-002 โ€” a plan is free unless an active kv_memberships row exists for (user/company, module_key).
  • BR-MEM-003 โ€” membership purchase is wallet-first (WalletModule::debit โ†’ wallet_user_memberships).
  • BR-MEM-004 โ€” GST applies when gst_applicable=1 at gst_percent.

Errors (prefix MEM-E)

  • MEM-E001 price shows 0/null โ€” read from kv_membership_plans or config/module_pricing.php

instead of kv_membership_plan_prices.

  • MEM-E002 "priced but 0 sold" โ€” plan active but no purchase path/landing (e.g. Real Estate,

Website Builder); inspect kv_memberships/wallet_user_memberships for that module = 0.

  • MEM-E003 renewal never happens โ€” recurring billing not running (kv_billing_subscriptions=0).

Change-impact

Gating touches every module's limits and the platform fee %. Re-test: price resolution via the prices table, tier gate enforcement, wallet debit on purchase, and that a free user hits the right limits.