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
| Table | Rows | Role |
|---|---|---|
kv_membership_plans | 77 | plan catalogue โ no price column |
kv_membership_plan_prices | 136 | the price source of truth |
kv_memberships | 116 (recounted; was 112) | actual subscriptions (who bought what) |
wallet_user_memberships | 280 (recounted; was 257) | membership rows funded via wallet |
wallet_membership_plans | 3 | wallet-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 memorymembership-pricing-canonical-path): never read a price fromkv_membership_plans(it has none) or fromconfig/module_pricing.php(an empty stub). Always joinkv_membership_plansโkv_membership_plan_pricesonplan_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:
- Module on/off โ
kv_module_control(is the vertical live/visible). See
../00-architecture/module-registry.md.
- Tier entitlement โ the merchant's active
kv_membershipstier for thatmodule_keygates
limits/features. WalletModule::getPlatformFeePercentForUser() is membership-aware (higher tier โ lower platform fee).
Business rules
BR-MEM-001โ price =kv_membership_plan_prices.base_pricebyplan_id+billing_cycle; never elsewhere.BR-MEM-002โ a plan is free unless an activekv_membershipsrow exists for(user/company, module_key).BR-MEM-003โ membership purchase is wallet-first (WalletModule::debitโwallet_user_memberships).BR-MEM-004โ GST applies whengst_applicable=1atgst_percent.
Errors (prefix MEM-E)
MEM-E001price shows 0/null โ read fromkv_membership_plansorconfig/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-E003renewal 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.