Project: Okul Database · Hub: Okul Database — Guides
Key Relationships & Entity Map
Top-Level Entity Hierarchy
┌─────────────────┐
│ schools │ ← Core entity
└────────┬────────┘
│ FK in many tables
┌──────────────────┼────────────────────┐
│ │ │
┌──────┴──────┐ ┌──────┴──────┐ ┌───────┴────────┐
│ customers │ │ leads │ │ school_filters │
│ (B2B firm) │ │ (D2C form) │ │ (denorm cache) │
└──────┬──────┘ └──────┬──────┘ └────────────────┘
│ │
┌──────┴──────┐ ┌──────┴──────┐
│ customer │ │ users │
│ agreements │ │ (parents) │
└──────┬──────┘ └─────────────┘
│
┌──────┴──────┐
│ payments │
└─────────────┘
B2B Revenue Flow
customers (firm)
│
├──< customer_users M2M: firm's portal users
│ └──< customer_user_schools per-school access grants
│
├──< customer_agreements one contract per school per period
│ ├── agreement_type billing model (fixed/subscription/arge)
│ ├── products JSON: product list
│ │
│ ├──< payments installments or one-off
│ │ └──< payment_transactions gateway log per attempt
│ │
│ └──< customer_agreements_renew_discounts renewal discounts
│
└──< customer_scores health score (demand/payment/responsiveness...)
D2C Lead Flow
users (parent/student)
│
└──< school_offers submitted inquiry form
└──> leads one lead per school per offer
├── stage_id Pipedrive stage (see ref-lead-stages)
├── approved_status_id internal qualification status
├── school_answered_deal_status school's outcome
├── customer_viewed_at when school saw the lead
└──> pipedrive_deals CRM sync (has lag)
School Entity Hub
schools
├── school_filters ← 1:1, denormalized cache (USE THIS FOR QUERIES)
├── school_statistics ← 1:1, analytics copy
├── campuses ← 1:N, physical locations
├── school_details ← 1:N, EAV extended attributes
├── school_fees ← 1:N, tuition fees by fee_type
├── school_features ← 1:N, template-based features
├── school_facilities ← M:N via pivot, with facilities table
├── school_services ← M:N via pivot, with services table
├── school_shifts ← M:N via pivot, with shifts table
├── school_learning_languages ← M:N via pivot
├── school_comments ← 1:N, user reviews (requires approved_at)
├── school_announcements ← 1:N, school announcements
├── school_interactions ← 1:N, CRM contact log (is_latest flag)
├── school_daily_logs ← 1:N, daily engagement metrics
├── school_badges ← 1:N, ranking badges
├── school_product_prices← 1:N, school-specific pricing
├── school_inbound_call_logs ← 1:N, call tracking
├── school_name_changes ← 1:N, historical renames
├── leads ← 1:N via leads.school_id
├── discounts ← 1:N, fee discounts
└── media ← via media_type_id=2, row_id=school_id
Location Hierarchy
locations (self-referential)
parent_id = NULL → Province (81 total)
parent_id = prov → District (hundreds)
schools.city_id → locations (province)
schools.county_id → locations (district)
campuses.location_id → locations (any level)
school_filters.city_name / county_name → already denormalized, no join needed
Education Hierarchy
universities
└──< faculty_programs ──< faculty_program_info (EAV)
└──< faculty_program_winners (admission scores)
colleges
├──< campuses (physical locations)
└── college_schools (M:N) ←→ schools
User ↔ Customer Mapping
users.customer_id IS NULL → Pure D2C user
users.customer_id = X → User is linked to customer X
customer_users (M2M bridge):
customer_users.user_id → users.id
customer_users.customer_id → customers.id
customer_users.role_id → configs (type='customer_user_role')
customer_user_schools (access control):
customer_user_id → customer_users.id
school_id → schools.id
(a user can only see schools they're explicitly granted access to)
Media Polymorphic
media
media_type_id → media_types.id (which entity type)
media_role_id → media_roles.id (what purpose)
row_id → the entity's ID
Example: School profile photo
media_type_id = 2 (School)
media_role_id = 3 (profile)
row_id = school_id
See ref-media-roles for full type/role mapping.
configs Lookup Pattern
Many tables store lookup IDs that resolve against configs:
leads.approved_status_id → configs WHERE type='offer_approved_status'
leads.school_answered_deal_status → configs WHERE type='school_answered_deal_status'
leads.deal_cause_of_reason → configs WHERE type='deal_cause_of_reason'
school_interactions.reason_id → configs WHERE type='interaction_reason'
school_interactions.result_id → configs WHERE type='interaction_result'
school_interactions.type_id → configs WHERE type='interaction_type'
customer_users.role_id → configs WHERE type='customer_user_role'
customer_users.title_id → configs WHERE type='customer_user_title'
Join pattern:
JOIN configs c ON c.type = '<type_name>'
AND c.item_id = <the_id_column>
AND c.deleted_at IS NULL
See ref-configs for all type names.
Common Multi-Table Joins
School + Location + Type (use school_filters instead)
-- If you MUST join schools:
SELECT s.name, st.type, l.name as city
FROM schools s
JOIN schools_types st ON st.id = s.school_type_id
JOIN locations l ON l.id = s.city_id
WHERE s.deleted_at IS NULL
-- BUT: school_filters has all of this pre-joinedLead + School + Customer
SELECT l.id, l.created_at, s.name as school,
c.firm_name, sf.city_name
FROM leads l
JOIN schools s ON s.id = l.school_id
JOIN customers c ON c.id = l.customer_id
JOIN school_filters sf ON sf.school_id = l.school_id AND sf.deleted_at IS NULL
WHERE l.deleted_at IS NULL AND (l.is_test = 0 OR l.is_test IS NULL)Payment + Agreement + Customer
SELECT p.paid_at, p.amount, p.status,
ca.agreement_date, ca.agreement_type,
c.firm_name
FROM payments p
JOIN customer_agreements ca ON ca.id = p.customer_agreement_id
JOIN customers c ON c.id = ca.customer_id
WHERE p.deleted_at IS NULL AND p.status = 'success'Related
- all-model-relationships — Complete 771-relationship list from models
- guide-data-scientist-quickstart — Quick start guide
- guide-common-gotchas — Common mistakes