Entity Relationship Diagrams

Logical data model for Raisd campus portals. Attributes and relationships mirror Zod contracts in student-portal and admissions entities from SDD-04. Split by domain so each diagram stays readable. CMS and LMS tables share one ERD with distinct colours.

PK / FK annotated Cardinality explicit Students map via programme version LMS tables · CAP + F01–F37 Logical — not a migrated Postgres schema
CMS — campus SoR Identity, enrolment, curriculum, finance, admissions, immigration, published results. Existing Cyberjaya CMS remains SoR this phase (ADR-1).
LMS — learning surface Weeks, materials, submissions, progress, quizzes, live class, forums. Portal API role surfaces; not a fifth portal product.
Shared bridge Module offering, teaching assignment, class schedule/session, module assessment, attendance, assessment result — consumed by both CMS and LMS journeys.
Proposed LMS Research-backed CAP / F01–F37 tables not yet in PortalRecordGraph. Marked proposed — not Live.
Student ↔ programme mapping. Students are mapped to a programme version, not only to a programme. Path: StudentProfile → ProgrammeEnrolment → programmeVersionId → ProgrammeVersion → Programme. Keep ProgrammeVersion: enrolment stays on the curriculum/rules under which the student enrolled (e.g. BBA 129 vs 122 credits) — never resolve from “current” programme config. See domain map, student record, and architecture.
Campus where a programme is offered. Prefer ProgrammeCampusOffering (programme version × campus), not “faculty belongs to one campus ⇒ programme belongs to that campus”. The same academic programme may be delivered at Botswana, Uganda, Sierra Leone, etc. Faculty remains organisational. Enrolment still stores ProgrammeEnrolment.campusId for the student’s study campus. See curriculum and architecture.
Canonical vs legacy. Do not mirror r_student / r_stdprogram / … Legacy IDs live in LegacyEntityMap; messy translation lives in the CAP-53 anti-corruption layer. Details: LUCT CMS / Portal compatibility.
CS-01–CS-13 (1 October 2026). Schema v3 campus policy layer is in the Demo contract (PORTAL_RECORD_SCHEMA_VERSION = 3). University-default rows (campusId null) plus campus overrides drive active-student / valid-semester membership, credit classification, CGPA repeat rules, document numbering and adjustment enablement. Per-campus Live confirmation remains readiness work — not a Schema blocker. CS decision page.
0. Domain mapCMS + LMS aggregates 1. InstitutionCampus, staff, policy · CMS 2. CurriculumProgramme, modules · CMS 3. Delivery & LMS coreOfferings, weeks, materials 4. Student recordPerson, enrolment · CMS 5. Results & LMS activitySubmissions, attendance, grades 6. FinanceInvoices, corrections · CMS 7. AdmissionsSDD-04 · CMS 8. Immigration & formsCases, graduation · CMS 9. Integration & auditStatus, views · CMS 10. LMS tablesCatalogue · proposed CAP gaps 11. ConfirmationsSchema · campus Live policy

0. Domain map

High-level aggregates and ownership boundaries. Blue = CMS · Green = LMS · Amber = shared bridge.

Aggregate hubs Shared

ProgrammeEnrolment and Campus are the primary CMS FK hubs. ModuleOffering is the LMS delivery hub hanging off curriculum + term.

erDiagram
  CAMPUS ||--o{ FACULTY : hosts
  FACULTY ||--o{ PROGRAMME : organises
  PROGRAMME ||--o{ PROGRAMME_VERSION : versions
  PROGRAMME_VERSION ||--o{ PROGRAMME_CAMPUS_OFFERING : offered_at
  CAMPUS ||--o{ PROGRAMME_CAMPUS_OFFERING : delivers
  PROGRAMME_VERSION ||--o{ PROGRAMME_ENROLMENT : enrols_into
  STUDENT_PROFILE ||--o{ PROGRAMME_ENROLMENT : has
  CAMPUS ||--o{ PROGRAMME_ENROLMENT : studies_at
  PROGRAMME_ENROLMENT ||--|| FINANCE_ACCOUNT : bills
  PROGRAMME_ENROLMENT ||--o{ STUDY_PERIOD : spans
  STUDY_PERIOD ||--o{ MODULE_REGISTRATION : registers
  MODULE_OFFERING ||--o{ MODULE_REGISTRATION : offered_as
  PROGRAMME_VERSION ||--o{ CURRICULUM_MODULE : contains
  MODULE ||--o{ CURRICULUM_MODULE : listed_in
  MODULE ||--o{ MODULE_OFFERING : delivered_as
  MODULE_OFFERING ||--o{ MODULE_WEEK : weeks
  MODULE_WEEK ||--o{ MODULE_LEARNING_ITEM : items
  MODULE_OFFERING ||--o{ MODULE_ASSESSMENT : assesses
  MODULE_REGISTRATION ||--o{ ASSIGNMENT_SUBMISSION : submits
  MODULE_REGISTRATION ||--o{ LEARNING_PROGRESS : progress

  CAMPUS {
    string id PK
    string code
    string name
  }
  STUDENT_PROFILE {
    string id PK
    string studentNumber
  }
  PROGRAMME_ENROLMENT {
    string id PK
    string status
  }
  PROGRAMME_CAMPUS_OFFERING {
    string id PK
    string programmeVersionId FK
    string campusId FK
  }
  FINANCE_ACCOUNT {
    string id PK
    string currencyCode
  }
  MODULE_OFFERING {
    string id PK
    string sectionCode
  }
  MODULE_WEEK {
    string id PK
    int weekNumber
  }
  MODULE_LEARNING_ITEM {
    string id PK
    string kind
  }
  ASSIGNMENT_SUBMISSION {
    string id PK
    string fileName
  }
          
Yes — we map the student on programme version. ProgrammeEnrolment.programmeVersionId pins curriculum/fee/rules. Do not determine requirements dynamically from the programme’s current config.
Campus offering is via ProgrammeCampusOffering. Faculty organises programmes organisationally; multi-campus delivery uses ProgrammeVersion × Campus offerings (same pattern as ModuleOffering).
Messaging / community read models (Conversation, Meetup, CommunityGroup) exist in portal contracts but are session-scoped demos today and are omitted from the durable ERD set. Proposed durable LMS forum tables live under §10 LMS tables.

1. Institution

Campus organisation, staff, and campus-scoped configuration. Faculty is organisational (staff, programme ownership for admin). Where a programme version is delivered is modelled by ProgrammeCampusOffering, not by assuming one faculty ⇒ one campus for academic offering.

Campus · Faculty · Staff · Config · Policy CMS

Prefer effective-dated campus policy tables for operational differences — not scattered if (campus === …) in domain code. 1:1 finance profile; N immigration / graduation / tax configs; university-default policy rows use campusId null.

erDiagram
  CAMPUS ||--o{ FACULTY : has
  FACULTY ||--o{ STAFF_MEMBER : employs
  PERSON ||--o| STAFF_MEMBER : owns
  PERSON ||--|| USER_ACCOUNT : may_login
  USER_ACCOUNT ||--o{ USER_ROLE_GRANT : grants
  ROLE ||--o{ USER_ROLE_GRANT : to_account
  ROLE ||--o{ ROLE_PERMISSION : grants
  PERMISSION ||--o{ ROLE_PERMISSION : used_by
  CAMPUS ||--|| CAMPUS_FINANCE_PROFILE : configures
  CAMPUS ||--o{ CAMPUS_SERVICE_TAX_PROFILE : taxes
  CAMPUS ||--o{ CAMPUS_IMMIGRATION_CONFIG : immigration
  CAMPUS ||--o{ CAMPUS_GRADUATION_CONFIG : graduation
  CAMPUS ||--o| CAMPUS_PRIVACY_SECURITY : privacy
  CAMPUS ||--o{ ANNOUNCEMENT : publishes
  CAMPUS ||--o{ UNIVERSITY_POLICY : policies
  CAMPUS ||--o{ CAMPUS_SERVICE_PAGE : services
  CAMPUS ||--o{ REVIEW_QUESTIONNAIRE : reviews
  CAMPUS ||--o{ CAMPUS_STATUS_MAPPING : status_map
  CAMPUS ||--o{ CAMPUS_ACADEMIC_STATUS_POLICY : academic_status
  CAMPUS ||--o{ CAMPUS_CREDIT_POLICY : credit
  CAMPUS ||--o{ CAMPUS_CGPA_POLICY : cgpa
  CAMPUS ||--o{ CAMPUS_DOCUMENT_NUMBER_POLICY : document_numbers
  CAMPUS ||--o{ CAMPUS_ADJUSTMENT_POLICY : adjustments

  CAMPUS {
    string id PK
    string code
    string name
    string countryCode
    string timeZone
  }
  FACULTY {
    string id PK
    string campusId FK
    string code
    string name
  }
  STAFF_MEMBER {
    string id PK
    string personId FK
    string facultyId FK
    string staffNumber
    string displayName
    string academicTitle
    string emailAddress
  }
  PERSON {
    string id PK
    string displayName
    string personType
    string studentProfileId FK
    string staffMemberId FK
  }
  USER_ACCOUNT {
    string id PK
    string personId FK
    string loginName
    string status
    datetime disabledAt
  }
  ROLE {
    string id PK
    string code
    string name
  }
  PERMISSION {
    string id PK
    string code
    string name
  }
  USER_ROLE_GRANT {
    string id PK
    string userAccountId FK
    string roleId FK
    datetime grantedAt
  }
  CAMPUS_FINANCE_PROFILE {
    string id PK
    string campusId FK
    string currencyCode
    string bankDetails
    int processingDays
  }
  CAMPUS_SERVICE_TAX_PROFILE {
    string id PK
    string campusId FK
    string jurisdiction
    string registration
    int rateBasisPoints
    date effectiveFrom
    date effectiveTo
  }
  CAMPUS_IMMIGRATION_CONFIG {
    string id PK
    string campusId FK
    string programmeVersionId FK
    string feeItemId FK
  }
  CAMPUS_GRADUATION_CONFIG {
    string id PK
    string campusId FK
    string programmeVersionId FK
    string feeItemId FK
  }
  ANNOUNCEMENT {
    string id PK
    string campusId FK
    string title
    string department
    datetime publishedAt
    boolean isPinned
  }
  REVIEW_QUESTIONNAIRE {
    string id PK
    string campusId FK
    string version
  }
  CAMPUS_STATUS_MAPPING {
    string id PK
    string campusId FK
    string sourceSystem
    string sourceStatus
    string canonicalStatus
    string target
    date effectiveFrom
    date effectiveTo
  }
  CAMPUS_ACADEMIC_STATUS_POLICY {
    string id PK
    string campusId FK
    string academicStatus
    boolean countsAsValidStudyPeriod
    boolean countsAsActiveStudent
    date effectiveFrom
    date effectiveTo
  }
  CAMPUS_CREDIT_POLICY {
    string id PK
    string campusId FK
    string outcome
    boolean countsAsAttempted
    boolean countsAsEarned
    boolean countsInGpa
    date effectiveFrom
    date effectiveTo
  }
  CAMPUS_CGPA_POLICY {
    string id PK
    string campusId FK
    string repeatRule
    date effectiveFrom
    date effectiveTo
  }
  CAMPUS_DOCUMENT_NUMBER_POLICY {
    string id PK
    string campusId FK
    string documentType
    string prefix
    string sequencePattern
    date effectiveFrom
    date effectiveTo
  }
  CAMPUS_ADJUSTMENT_POLICY {
    string id PK
    string campusId FK
    string category
    boolean enabled
    string approvalRequirement
    date effectiveFrom
    date effectiveTo
  }
          
Faculty still links programmes for organisational ownership (Programme.facultyId). Multi-campus academic delivery uses ProgrammeCampusOffering. Person owns at most one StudentProfile and/or StaffMember; login lives on UserAccount (disable/lock does not delete history). Role and permission codes are demonstration seeds until the real RBAC matrix is confirmed. Campus policy tables (campus_*_policies / campus_status_mappings) store university defaults with campusId null; a campus row overrides the default while both are effective on the reference date.

2. Curriculum

Programmes, versions, campus offerings, module catalogue, prerequisites, grading, and fee plans.

Programme · ProgrammeCampusOffering · Module · FeePlan CMS

Programme = academic identity · ProgrammeVersion = curriculum/rules · ProgrammeCampusOffering = where that version is delivered.

erDiagram
  FACULTY ||--o{ PROGRAMME : organises
  PROGRAMME ||--o{ PROGRAMME_VERSION : versions
  PROGRAMME_VERSION ||--o{ PROGRAMME_CAMPUS_OFFERING : offered_at
  CAMPUS ||--o{ PROGRAMME_CAMPUS_OFFERING : at_campus
  PROGRAMME_VERSION ||--o{ CURRICULUM_MODULE : includes
  MODULE ||--o{ CURRICULUM_MODULE : appears_in
  CURRICULUM_MODULE ||--o{ MODULE_PREREQUISITE : requires
  MODULE ||--o{ MODULE_PREREQUISITE : is_prereq_of
  CAMPUS ||--o{ GRADING_SCHEME : defines
  GRADING_SCHEME ||--o{ GRADE_BAND : bands
  PROGRAMME_VERSION ||--o| FEE_PLAN : priced_by
  FEE_PLAN ||--o{ FEE_ITEM : lines
  MODULE ||--o{ FEE_ITEM : tuition_for
  FUNDING_SCHEME ||--o{ STUDENT_FUNDING_AWARD : awards

  PROGRAMME {
    string id PK
    string facultyId FK
    string code
    string title
    string awardLevel
  }
  PROGRAMME_VERSION {
    string id PK
    string programmeId FK
    string versionLabel
    date effectiveFrom
    date effectiveTo
    int totalSemesterCount
    float totalCreditsRequired
    int minimumSemesterCredits
    int maximumSemesterCredits
    float minimumGraduationCgpa
    float minimumAttendancePercent
  }
  PROGRAMME_CAMPUS_OFFERING {
    string id PK
    string programmeVersionId FK
    string campusId FK
    string localProgrammeCode
    date effectiveFrom
    date effectiveTo
    string status
  }
  MODULE {
    string id PK
    string code
    string title
    string description
    float defaultCredits
  }
  CURRICULUM_MODULE {
    string id PK
    string programmeVersionId FK
    string moduleId FK
    int recommendedSemesterNumber
    float credits
    string requirementType
  }
  MODULE_PREREQUISITE {
    string id PK
    string curriculumModuleId FK
    string prerequisiteModuleId FK
    string minimumGrade
  }
  GRADING_SCHEME {
    string id PK
    string campusId FK
    string name
    float maximumPoints
  }
  GRADE_BAND {
    string id PK
    string gradingSchemeId FK
    string label
    float minimumScore
    float points
    string outcome
  }
  FEE_PLAN {
    string id PK
    string programmeVersionId FK
    string currencyCode
  }
  FEE_ITEM {
    string id PK
    string feePlanId FK
    string moduleId FK
    string category
    string label
    int amountMinor
    string serviceTaxTreatment
    string serviceTaxExemptionCode
  }
  FUNDING_SCHEME {
    string id PK
    string name
    string kind
    boolean renewable
  }
          
FeeItem.category: tuition | registration | books | resource | international-services | repeat-administration | other. Tuition items must reference a Module; non-tuition must not.
Offered at which campus? Query ProgrammeCampusOffering for the version — not Faculty alone. Student study location remains on ProgrammeEnrolment.campusId.

3. Delivery & LMS core

Academic terms and class offerings are the shared bridge. Weeks, learning items and assessments are the LMS core (CAP-17–19, 27). Keep AcademicTerm (calendar e.g. 2026-03) distinct from StudyPeriod.programmeSemesterNumber (student’s semester in the programme).

Term · Offering · Schedule · Materials LMS Shared

Amber entities: offering / schedule / session / teaching / assessment outline. Green entities: weeks and learning items (materials surface).

erDiagram
  CAMPUS ||--o{ ACADEMIC_TERM : runs
  MODULE ||--o{ MODULE_OFFERING : offered_as
  ACADEMIC_TERM ||--o{ MODULE_OFFERING : in_term
  CAMPUS ||--o{ MODULE_OFFERING : at_campus
  MODULE_OFFERING ||--o{ TEACHING_ASSIGNMENT : taught_by
  STAFF_MEMBER ||--o{ TEACHING_ASSIGNMENT : assigned
  MODULE_OFFERING ||--o{ CLASS_SCHEDULE : scheduled
  CLASS_SCHEDULE ||--o{ CLASS_SESSION : instances
  MODULE_OFFERING ||--o{ MODULE_WEEK : weeks
  MODULE_WEEK ||--o{ MODULE_LEARNING_ITEM : items
  MODULE_OFFERING ||--o{ MODULE_ASSESSMENT : assesses
  MODULE_ASSESSMENT ||--o{ MODULE_LEARNING_ITEM : may_link

  ACADEMIC_TERM {
    string id PK
    string campusId FK
    string code
    string title
    int sequenceNumber
    datetime startsAt
    datetime endsAt
    datetime resultPublishedAt
    string status
  }
  MODULE_OFFERING {
    string id PK
    string moduleId FK
    string academicTermId FK
    string campusId FK
    string sectionCode
    string deliveryMode
  }
  TEACHING_ASSIGNMENT {
    string id PK
    string moduleOfferingId FK
    string staffMemberId FK
    string role
    int displayOrder
  }
  CLASS_SCHEDULE {
    string id PK
    string moduleOfferingId FK
    int dayOfWeek
    string startsAtLocal
    string endsAtLocal
    string location
    date effectiveFrom
    date effectiveTo
  }
  CLASS_SESSION {
    string id PK
    string classScheduleId FK
    datetime startsAt
    datetime endsAt
    string status
  }
  MODULE_WEEK {
    string id PK
    string moduleOfferingId FK
    int weekNumber
    date startsOn
    string title
  }
  MODULE_LEARNING_ITEM {
    string id PK
    string moduleWeekId FK
    string moduleAssessmentId FK
    string kind
    string title
    datetime availableFrom
    string fileName
    string mimeType
    int sizeBytes
  }
  MODULE_ASSESSMENT {
    string id PK
    string moduleOfferingId FK
    string title
    string kind
    float weightPercent
    datetime availableFrom
    datetime dueAt
    datetime scheduledStartsAt
    datetime scheduledEndsAt
  }
          
LMS kinds on ModuleLearningItem. Contract: lecture-note | assignment | quiz | test | midterm | final. Route-facing Materials also project resource/quiz/exam views (module-materials.ts). Full LMS catalogue including proposed CAP-24/25/26 tables: §10.

4. Student record & enrolment

Person ownership, external identifiers, visa pass, programme enrolment, and study periods. Academic placement is always through a programme version on ProgrammeEnrolment. Study-period status is the timeline position; academicStatus is the Registry status used for active-student and credit reporting.

Person · identifiers · enrolment · study period CMS

ProgrammeEnrolment links student ↔ programme version ↔ campus. Effective-dated status events keep enrolment, period and registration history.

erDiagram
  PERSON ||--o| STUDENT_PROFILE : owns
  STUDENT_PROFILE ||--o{ STUDENT_IDENTIFIER : identifiers
  STUDENT_IDENTIFIER ||--o| STUDENT_IDENTIFIER : superseded_by
  STUDENT_PROFILE ||--o{ EMERGENCY_CONTACT : contacts
  STUDENT_PROFILE ||--o{ ACADEMIC_QUALIFICATION : quals
  STUDENT_PROFILE ||--o{ ENGLISH_PROFICIENCY : english
  STUDENT_PROFILE ||--o{ STUDENT_DOCUMENT : documents
  STUDENT_PROFILE ||--o| STUDENT_RESUME : resume
  STUDENT_RESUME ||--o{ RESUME_WORK_EXPERIENCE : work
  STUDENT_RESUME ||--o{ RESUME_EDUCATION : education
  STUDENT_RESUME ||--o{ RESUME_SKILL : skills
  STUDENT_RESUME ||--o{ RESUME_RECOGNITION : awards
  STUDENT_PROFILE ||--o{ PORTFOLIO_PROJECT : portfolio
  STUDENT_PROFILE ||--o{ PROGRAMME_ENROLMENT : enrolments
  STUDENT_PROFILE ||--o{ STUDENT_VISA_PASS : passes
  STUDENT_IDENTIFIER ||--o{ STUDENT_VISA_PASS : passport_used
  PROGRAMME_ENROLMENT ||--o{ STUDENT_VISA_PASS : for_enrolment
  PROGRAMME_VERSION ||--o{ PROGRAMME_ENROLMENT : of_version
  CAMPUS ||--o{ PROGRAMME_ENROLMENT : at
  PROGRAMME_ENROLMENT ||--o{ STUDY_PERIOD : periods
  PROGRAMME_ENROLMENT ||--o{ PROGRAMME_ENROLMENT_STATUS_EVENT : history
  ACADEMIC_TERM ||--o{ STUDY_PERIOD : term
  STUDY_PERIOD ||--o{ STUDY_PERIOD_STATUS_EVENT : history
  STUDY_PERIOD ||--o| CREDIT_LOAD_OVERRIDE : override
  STAFF_MEMBER ||--o{ CREDIT_LOAD_OVERRIDE : approves
  STUDENT_PROFILE ||--o{ STUDENT_ACCOUNT_ISSUE : issues

  PERSON {
    string id PK
    string displayName
    string personType
  }
  STUDENT_PROFILE {
    string id PK
    string personId FK
    string studentNumber
    string displayName
    string nationality
    string emailAddress
    date passportExpiresOn
  }
  STUDENT_IDENTIFIER {
    string id PK
    string studentProfileId FK
    string type
    string countryCode
    string value
    boolean isPrimary
    string supersededByIdentifierId FK
  }
  PROGRAMME_ENROLMENT {
    string id PK
    string studentProfileId FK
    string programmeVersionId FK
    string campusId FK
    datetime enrolledAt
    date expectedCompletionOn
    string status
  }
  PROGRAMME_ENROLMENT_STATUS_EVENT {
    string id PK
    string programmeEnrolmentId FK
    string status
    datetime effectiveFrom
    datetime effectiveTo
    string source
  }
  STUDY_PERIOD {
    string id PK
    string programmeEnrolmentId FK
    string academicTermId FK
    int programmeSemesterNumber
    string status
    string academicStatus
    datetime statusChangedAt
    string statusReason
    datetime registrationOpensAt
    datetime registrationClosesAt
  }
  STUDY_PERIOD_STATUS_EVENT {
    string id PK
    string studyPeriodId FK
    string status
    datetime effectiveFrom
    datetime effectiveTo
    string source
  }
  STUDENT_VISA_PASS {
    string id PK
    string studentProfileId FK
    string programmeEnrolmentId FK
    string passportIdentifierId FK
    string passType
    string passportNumber
    string passNumber
    string status
  }
  CREDIT_LOAD_OVERRIDE {
    string id PK
    string studyPeriodId FK
    int approvedMaximumCredits
    string approvedByStaffMemberId FK
    string status
  }
  STUDENT_ACCOUNT_ISSUE {
    string id PK
    string studentProfileId FK
    string kind
    string status
    int amountMinor
    datetime dueAt
  }
          
Programme version, not programme alone. Path: StudentProfile → ProgrammeEnrolment → programmeVersionId → ProgrammeVersion → Programme. Study periods, module registrations, finance account, visa pass, and most journey records hang off that enrolment. academicStatus ∈ {active, repeat, outstanding, enrolled, credit-transfer} counts as a valid semester for EMGS / active-student reporting (current_active_students view). Identifier history (passport, NRIC, student number, EMGS reference) lives on StudentIdentifier; the visa pass links the passport it was issued against. Statuses are layered (identity / enrolment / study period / registration / immigration / finance) — see active student definitions.

5. Registration, LMS activity & results

Module registration is the junction for LMS attendance, submissions and progress, and for CMS published results. Attempts are never overwritten. TermResult is a calculated snapshot; published history is versioned via ResultPublication.

ModuleRegistration lifecycle LMS CMS Shared

Green: learning progress, assignment submission. Amber: attendance, assessment result. Blue: module/term results, transfers, publications.

erDiagram
  STUDY_PERIOD ||--o{ MODULE_REGISTRATION : registers
  MODULE_OFFERING ||--o{ MODULE_REGISTRATION : into
  MODULE_REGISTRATION ||--o{ MODULE_REGISTRATION_STATUS_EVENT : history
  MODULE_REGISTRATION ||--o{ ATTENDANCE : attendance
  CLASS_SESSION ||--o{ ATTENDANCE : session
  MODULE_REGISTRATION ||--o{ LEARNING_PROGRESS : progress
  MODULE_LEARNING_ITEM ||--o{ LEARNING_PROGRESS : item
  MODULE_REGISTRATION ||--o{ ASSIGNMENT_SUBMISSION : submits
  MODULE_LEARNING_ITEM ||--o{ ASSIGNMENT_SUBMISSION : for_item
  MODULE_REGISTRATION ||--o{ ASSESSMENT_RESULT : scored
  MODULE_ASSESSMENT ||--o{ ASSESSMENT_RESULT : of
  MODULE_REGISTRATION ||--|| MODULE_RESULT : final
  GRADE_BAND ||--o{ MODULE_RESULT : band
  STUDY_PERIOD ||--|| TERM_RESULT : term_gpa
  PROGRAMME_ENROLMENT ||--o{ STUDENT_CREDIT_TRANSFER : transfers
  STUDY_PERIOD ||--o{ STUDENT_CREDIT_TRANSFER : recorded_in
  CURRICULUM_MODULE ||--o{ STUDENT_CREDIT_TRANSFER : target
  PROGRAMME_ENROLMENT ||--o{ CURRICULUM_SUBSTITUTION : substitutions
  CURRICULUM_MODULE ||--o{ CURRICULUM_SUBSTITUTION : replaced
  MODULE ||--o{ CURRICULUM_SUBSTITUTION : substitute
  PROGRAMME_VERSION ||--o{ MODULE_EQUIVALENCY : equivalencies
  MODULE ||--o{ MODULE_EQUIVALENCY : maps
  STUDY_PERIOD ||--o{ RESULT_PUBLICATION : publications
  RESULT_PUBLICATION ||--o| RESULT_PUBLICATION : supersedes
  MODULE_REGISTRATION ||--o{ LECTURER_REVIEW : reviewed
  TEACHING_ASSIGNMENT ||--o{ LECTURER_REVIEW : by
  STUDENT_PROFILE ||--o{ LECTURER_REVIEW : from

  MODULE_REGISTRATION {
    string id PK
    string studyPeriodId FK
    string moduleOfferingId FK
    int attemptNumber
    string registrationType
    string status
    datetime registeredAt
  }
  MODULE_REGISTRATION_STATUS_EVENT {
    string id PK
    string moduleRegistrationId FK
    string status
    datetime effectiveFrom
    datetime effectiveTo
  }
  ATTENDANCE {
    string id PK
    string moduleRegistrationId FK
    string classSessionId FK
    string status
    datetime recordedAt
  }
  LEARNING_PROGRESS {
    string id PK
    string moduleRegistrationId FK
    string moduleLearningItemId FK
    string status
    datetime completedAt
  }
  ASSIGNMENT_SUBMISSION {
    string id PK
    string moduleRegistrationId FK
    string moduleLearningItemId FK
    string fileName
    string mimeType
    int sizeBytes
    datetime submittedAt
    datetime deletedAt
  }
  ASSESSMENT_RESULT {
    string id PK
    string moduleRegistrationId FK
    string moduleAssessmentId FK
    float scorePercent
    float weightedScore
    datetime publishedAt
  }
  MODULE_RESULT {
    string id PK
    string moduleRegistrationId FK
    string gradeBandId FK
    string grade
    string outcome
    float creditsAttempted
    float creditsEarned
    float finalPoints
    boolean includedInCgpa
    datetime publishedAt
  }
  TERM_RESULT {
    string id PK
    string studyPeriodId FK
    float gpa
    float cgpa
    float termCreditsAttempted
    float termCreditsEarned
    float termGpaCredits
    float termPoints
    float transcriptCreditsAttempted
    float cumulativeCreditsEarned
    float cumulativeGpaCredits
    float cumulativePoints
    datetime publishedAt
  }
  STUDENT_CREDIT_TRANSFER {
    string id PK
    string programmeEnrolmentId FK
    string studyPeriodId FK
    string targetCurriculumModuleId FK
    float creditsGranted
    string sourceInstitution
    datetime approvedAt
  }
  CURRICULUM_SUBSTITUTION {
    string id PK
    string programmeEnrolmentId FK
    string replacedCurriculumModuleId FK
    string substituteModuleId FK
  }
  MODULE_EQUIVALENCY {
    string id PK
    string programmeVersionId FK
    string moduleId FK
    string equivalentModuleId FK
  }
  LECTURER_REVIEW {
    string id PK
    string studentProfileId FK
    string moduleRegistrationId FK
    string teachingAssignmentId FK
  }
          
Attempts never overwrite. First fail + repeat keep both ModuleRegistration rows (attemptNumber, registrationType). Latest attempt per module counts toward CGPA; failed attempts still count in term GPA.
Credits earned ≠ CGPA. Pass · creditsEarned 3 · points 0 · includedInCgpa = false is valid. Term totals must equal the values derived from results + credit transfers (student_credit_summary view mirrors the TypeScript derivation).
A curriculum module is satisfied by at most one of: a published pass, a credit transfer, or an approved substitution whose substitute module has a pass.
LMS activity vs CMS results. AssignmentSubmission / LearningProgress are LMS metadata (Demo: no durable file bytes yet — SDD-10 Q5). ModuleResult / TermResult remain CMS-published academic outcomes. Attendance is shared: CMS SoR this phase; LMS student/lecturer surfaces read via Portal API.

6. Finance

One finance account per enrolment; payments allocate to invoices (N:M). Reversals, refunds, credit notes and voids are first-class corrections — not in-place rewrites. Result visibility uses rule-based ResultAccessPolicy (balance, funding, holds).

Account · Invoice · Payment · Corrections · Funding CMS

Payment proof (uploaded file) is distinct from ledger Payment / Allocation. Adjustments carry an explicit category.

erDiagram
  PROGRAMME_ENROLMENT ||--|| FINANCE_ACCOUNT : has
  FINANCE_ACCOUNT ||--o{ INVOICE : invoices
  STUDY_PERIOD ||--o{ INVOICE : may_bill
  INVOICE ||--o{ INVOICE_LINE : lines
  FEE_ITEM ||--o{ INVOICE_LINE : from
  INVOICE ||--o| INVOICE_SERVICE_TAX : tax
  CAMPUS_SERVICE_TAX_PROFILE ||--o{ INVOICE_SERVICE_TAX : profile
  INVOICE ||--o| INVOICE_VOID : voided_by
  INVOICE ||--o{ CREDIT_NOTE : credited
  FINANCE_ACCOUNT ||--o{ PAYMENT : payments
  PAYMENT ||--o{ PAYMENT_ALLOCATION : allocates
  INVOICE ||--o{ PAYMENT_ALLOCATION : receives
  PAYMENT ||--o| PAYMENT_REVERSAL : reversed
  FINANCE_ACCOUNT ||--o{ REFUND : refunds
  PAYMENT ||--o{ REFUND : source
  PROGRAMME_ENROLMENT ||--o{ EDUCATION_SERVICE_TAX_EXEMPTION : exemptions
  FUNDING_SCHEME ||--o{ STUDENT_FUNDING_AWARD : grants
  PROGRAMME_ENROLMENT ||--o{ STUDENT_FUNDING_AWARD : receives
  STUDENT_FUNDING_AWARD ||--o{ STUDENT_FUNDING_BENEFIT : benefits
  FINANCE_ACCOUNT ||--o{ FINANCIAL_ADJUSTMENT : adjustments
  INVOICE ||--o{ FINANCIAL_ADJUSTMENT : may_adjust
  STUDENT_FUNDING_BENEFIT ||--o{ FINANCIAL_ADJUSTMENT : from_benefit

  FINANCE_ACCOUNT {
    string id PK
    string programmeEnrolmentId FK
    string currencyCode
    string status
  }
  INVOICE {
    string id PK
    string financeAccountId FK
    string studyPeriodId FK
    string invoiceNumber
    int totalMinor
    string status
  }
  CREDIT_NOTE {
    string id PK
    string invoiceId FK
    string creditNoteNumber
    int amountMinor
    int serviceTaxAmountMinor
    datetime issuedAt
  }
  INVOICE_VOID {
    string id PK
    string invoiceId FK
    string replacementInvoiceId FK
    datetime voidedAt
    string reason
  }
  PAYMENT {
    string id PK
    string financeAccountId FK
    string reference
    datetime paidAt
    int amountMinor
    string method
    string status
  }
  PAYMENT_REVERSAL {
    string id PK
    string paymentId FK
    datetime reversedAt
    string reason
  }
  REFUND {
    string id PK
    string financeAccountId FK
    string sourcePaymentId FK
    int amountMinor
    datetime paidAt
    string method
  }
  PAYMENT_ALLOCATION {
    string id PK
    string paymentId FK
    string invoiceId FK
    int amountMinor
  }
  FINANCIAL_ADJUSTMENT {
    string id PK
    string financeAccountId FK
    string invoiceId FK
    string studentFundingBenefitId FK
    string kind
    string category
    int amountMinor
  }
          
Invoice numbers use INV-###### (INV-000000 reserved); credit notes use CN-######. A reversed payment has exactly one reversal and no allocations; a void invoice has exactly one void record. Account balance = billed − credit notes + adjustments − paid + refunds (student_outstanding_balances view). Historical SST stays on InvoiceServiceTax — never recalculated from today's rate.

7. Admissions (SDD-04 target)

Logical CMS entities for applicant journey — not yet exposed as PortalApi methods.

Applicant → Application → Offer → Enrolment CMS

Evidence bytes live on the evidence volume; metadata in CMS / planned Postgres.

erDiagram
  APPLICANT_ACCOUNT ||--o{ APPLICATION : submits
  CAMPUS ||--o{ APPLICATION : campus
  PROGRAMME ||--o{ APPLICATION : programme
  APPLICATION ||--o{ EVIDENCE_OBJECT : evidence
  APPLICATION ||--o{ DECLARATION : declarations
  APPLICATION ||--o| OFFER : may_receive
  OFFER ||--o| FIRST_ENROLMENT : on_accept
  FIRST_ENROLMENT ||--|| STUDENT_PROFILE : creates
  FIRST_ENROLMENT ||--|| PROGRAMME_ENROLMENT : seeds

  APPLICANT_ACCOUNT {
    string id PK
    string identitySubject
    string email
    string contactNumber
  }
  APPLICATION {
    string id PK
    string applicantAccountId FK
    string programmeId FK
    string campusId FK
    string intake
    string status
  }
  EVIDENCE_OBJECT {
    string id PK
    string applicationId FK
    string evidenceType
    string fileId
    string checksum
    datetime uploadedAt
  }
  DECLARATION {
    string id PK
    string applicationId FK
    string policyVersion
    datetime acceptedAt
  }
  OFFER {
    string id PK
    string applicationId FK
    string conditions
    date expiresOn
    string decision
  }
  FIRST_ENROLMENT {
    string id PK
    string offerId FK
    string studentId
    string programmeId
    date startOn
  }
          
Application status path (logical): draft → submitted → under-review → offered / rejected / withdrawn → enrolled (after offer accept).

8. Immigration, online forms & graduation

Case management and form submissions tied to enrolment / invoices.

ImmigrationCase · OnlineForm · GraduationRecord CMS

Optional 1:1 links to form submission, visa pass, and invoice.

erDiagram
  STUDENT_PROFILE ||--o{ IMMIGRATION_CASE : cases
  PROGRAMME_ENROLMENT ||--o{ IMMIGRATION_CASE : for
  CAMPUS ||--o{ IMMIGRATION_CASE : at
  IMMIGRATION_CASE ||--o{ IMMIGRATION_DOCUMENT_CHECK : checks
  IMMIGRATION_CASE ||--o{ IMMIGRATION_CASE_EVENT : events
  ONLINE_FORM_SUBMISSION ||--o| IMMIGRATION_CASE : may_drive
  STUDENT_VISA_PASS ||--o| IMMIGRATION_CASE : may_attach
  INVOICE ||--o| IMMIGRATION_CASE : may_bill
  CAMPUS ||--o{ ONLINE_FORM_DEFINITION : forms
  ONLINE_FORM_DEFINITION ||--o{ ONLINE_FORM_SUBMISSION : submissions
  STUDENT_PROFILE ||--o{ ONLINE_FORM_SUBMISSION : submits
  PROGRAMME_ENROLMENT ||--o{ ONLINE_FORM_SUBMISSION : context
  ONLINE_FORM_SUBMISSION ||--o{ ONLINE_FORM_MESSAGE : messages
  STUDENT_PROFILE ||--o{ GRADUATION_RECORD : graduation
  PROGRAMME_ENROLMENT ||--o{ GRADUATION_RECORD : for
  CAMPUS ||--o{ GRADUATION_RECORD : at
  ONLINE_FORM_SUBMISSION ||--o| GRADUATION_RECORD : clearance
  ONLINE_FORM_SUBMISSION ||--o| GRADUATION_RECORD : convocation
  INVOICE ||--o| GRADUATION_RECORD : fee

  IMMIGRATION_CASE {
    string id PK
    string studentProfileId FK
    string programmeEnrolmentId FK
    string campusId FK
    string caseType
    string status
    string formSubmissionId FK
    string visaPassId FK
    string invoiceId FK
  }
  IMMIGRATION_DOCUMENT_CHECK {
    string id PK
    string immigrationCaseId FK
    string label
    string status
  }
  IMMIGRATION_CASE_EVENT {
    string id PK
    string immigrationCaseId FK
    string label
    datetime occurredAt
  }
  ONLINE_FORM_DEFINITION {
    string id PK
    string campusId FK
    string version
    string eligibility
  }
  ONLINE_FORM_SUBMISSION {
    string id PK
    string formDefinitionId FK
    string studentProfileId FK
    string programmeEnrolmentId FK
    int revision
    string status
  }
  ONLINE_FORM_MESSAGE {
    string id PK
    string submissionId FK
    int sequence
    string sender
    string text
  }
  GRADUATION_RECORD {
    string id PK
    string studentProfileId FK
    string programmeEnrolmentId FK
    string campusId FK
    string clearanceSubmissionId FK
    string convocationSubmissionId FK
    string invoiceId FK
  }
          

9. Integration, audit & data quality

Canonical IDs are not legacy CMS IDs. Mapping, DQ, idempotency, reporting views and append-only audit live with the portal/integration layer — see architecture.

Status history · campus policy · reporting views · AuditEvent · LegacyEntityMap CMS

Effective-dated status events and campus policy are first-class collections. Postgres reporting views mirror the TypeScript projection helpers and resolve academic-status membership through campus_academic_status_policies.

erDiagram
  PROGRAMME_ENROLMENT ||--o{ PROGRAMME_ENROLMENT_STATUS_EVENT : history
  STUDY_PERIOD ||--o{ STUDY_PERIOD_STATUS_EVENT : history
  MODULE_REGISTRATION ||--o{ MODULE_REGISTRATION_STATUS_EVENT : history
  CAMPUS ||--o{ CAMPUS_ACADEMIC_STATUS_POLICY : policy
  CAMPUS_ACADEMIC_STATUS_POLICY ||--o{ CURRENT_ACTIVE_STUDENT : filters
  CAMPUS_ACADEMIC_STATUS_POLICY ||--o{ LATEST_VALID_STUDY_PERIOD : filters
  PROGRAMME_ENROLMENT ||--o| LATEST_VALID_STUDY_PERIOD : reports
  PROGRAMME_ENROLMENT ||--o| STUDENT_CREDIT_SUMMARY : reports
  FINANCE_ACCOUNT ||--o| STUDENT_OUTSTANDING_BALANCE : reports
  STUDENT_PROFILE ||--o| CURRENT_ACTIVE_STUDENT : may_appear
  LEGACY_ENTITY_MAP }o--|| STUDENT_PROFILE : may_map
  DATA_QUALITY_ISSUE }o--o| STUDENT_PROFILE : may_flag

  PROGRAMME_ENROLMENT_STATUS_EVENT {
    string id PK
    string programmeEnrolmentId FK
    string status
    datetime effectiveFrom
    datetime effectiveTo
    string changedByUserAccountId FK
    string reason
    string source
  }
  CAMPUS_ACADEMIC_STATUS_POLICY {
    string id PK
    string campusId FK
    string academicStatus
    boolean countsAsValidStudyPeriod
    boolean countsAsActiveStudent
    date effectiveFrom
    date effectiveTo
  }
  LATEST_VALID_STUDY_PERIOD {
    string programmeEnrolmentId FK
    string studyPeriodId FK
    string academicStatus
    int programmeSemesterNumber
  }
  CURRENT_ACTIVE_STUDENT {
    string studentProfileId FK
    string studentNumber
    string programmeEnrolmentId FK
    string studyPeriodId FK
    string academicStatus
  }
  STUDENT_CREDIT_SUMMARY {
    string programmeEnrolmentId FK
    float creditsRequired
    float creditsEarned
    float transferredCredits
    float cgpa
    float remainingCredits
  }
  STUDENT_OUTSTANDING_BALANCE {
    string financeAccountId FK
    string programmeEnrolmentId FK
    int balanceMinor
    int openInvoiceBalanceMinor
    string balanceState
  }
  LEGACY_ENTITY_MAP {
    string id PK
    string canonicalEntityType
    string canonicalEntityId
    string sourceSystem
    string legacyTable
    string legacyId
  }
  AUDIT_EVENT {
    bigint id PK
    datetime occurredAt
    bigint revision
    int schemaVersion
    string actorStudentProfileId
    string actorUserAccountId
    string collection
    string recordId
    string action
    string before
    string after
  }
  DATA_QUALITY_ISSUE {
    string id PK
    string entityType
    string entityId
    string issueType
    string severity
    datetime detectedAt
  }
  INTEGRATION_OPERATION {
    string id PK
    string idempotencyKey
    string operationType
    string status
    datetime startedAt
  }
          
portal_meta.schema_version tracks the record contract (PORTAL_RECORD_SCHEMA_VERSION, currently 3 with campus policy collections). A mismatch reseeds record tables and keeps student_portal_state / audit_events. Every graph save appends insert/update/delete rows to audit_events. Demo tables still carry no physical FK constraints — db-admin infers edges from *_id column names (including policy campus_id → campuses). Reporting views resolve academic-status membership via campus_academic_status_policies.

10. LMS database tables

Logical LMS tables for Raisd learning surfaces (CAP + F01–F37). Demo contract tables already project as snake_case collections in Neon; proposed tables close Moodle-like gaps (quizzes, live class, forums, feedback, QA) and are not Live. See lms-schema.md and LMS hub.

In-contract LMS & shared tables LMS Shared

Postgres projection names match PortalRecordGraph collections (PORTAL_RECORD_SCHEMA_VERSION = 3.

Table Domain Owns / purpose CAP Status
module_offerings Shared Module × term × campus section; LMS delivery root 11–14 Demo
teaching_assignments Shared Lead / co-lecturer / tutor on an offering 40, 44 Demo
class_schedules Shared Recurring timetable slots 11 Demo
class_sessions Shared Dated session instances from a schedule 11, 12 Demo
attendances Shared present | absent per registration × session (CMS SoR this phase) 12 Demo
module_assessments Shared Weighted assessment outline (quiz / assignment / exam kinds) 19, 24 Demo
assessment_results Shared Published component scores against an assessment 13, 28 Demo
module_weeks LMS Teaching week / topic container on an offering 23, 27 Demo
module_learning_items LMS Resources, briefs, quizzes, exams with release time 17–19, 24, 27 Partial / Demo
learning_progress LMS Student item status: not-started | viewed | completed | locked 27, 32 Partial
assignment_submissions LMS File metadata for submit / replace / withdraw (bytes need Live store) 20 Demo (metadata)
lecturer_reviews LMS Student CAP-30 course evaluation of a teaching assignment 30 Demo (session)

Proposed LMS tables (CAP gaps) Proposed

Research-backed extensions for a complete LMS (BASE-44). Do not treat as implemented. Join keys follow existing hubs: module_offering_id, module_registration_id, module_learning_item_id, module_assessment_id.

erDiagram
  MODULE_OFFERING ||--o{ LIVE_CLASS_SESSION : hosts
  CLASS_SESSION ||--o| LIVE_CLASS_SESSION : may_link
  MODULE_REGISTRATION ||--o{ LIVE_CLASS_ATTENDANCE : joins
  LIVE_CLASS_SESSION ||--o{ LIVE_CLASS_ATTENDANCE : attendance

  MODULE_ASSESSMENT ||--o{ QUIZ_ATTEMPT : attempts
  MODULE_REGISTRATION ||--o{ QUIZ_ATTEMPT : sits
  QUIZ_ATTEMPT ||--o{ QUIZ_ATTEMPT_ANSWER : answers
  MODULE_ASSESSMENT ||--o{ QUIZ_QUESTION : questions

  MODULE_ASSESSMENT ||--o{ ASSESSMENT_FEEDBACK : feedback
  MODULE_REGISTRATION ||--o{ ASSESSMENT_FEEDBACK : receives
  STAFF_MEMBER ||--o{ ASSESSMENT_FEEDBACK : writes

  MODULE_OFFERING ||--o{ MODULE_FORUM : forums
  MODULE_FORUM ||--o{ MODULE_FORUM_POST : posts
  MODULE_FORUM_POST ||--o| MODULE_FORUM_POST : replies_to
  STUDENT_PROFILE ||--o{ MODULE_FORUM_POST : authors
  STAFF_MEMBER ||--o{ MODULE_FORUM_POST : may_moderate

  MODULE_LEARNING_ITEM ||--o| LEARNING_RELEASE_RULE : release
  MODULE_LEARNING_ITEM ||--o{ MATERIAL_QA_REVIEW : qa
  STAFF_MEMBER ||--o{ MATERIAL_QA_REVIEW : reviews
  MODULE_LEARNING_ITEM ||--o{ COPYRIGHT_PERMISSION : rights

  LIVE_CLASS_SESSION {
    string id PK
    string moduleOfferingId FK
    string classSessionId FK
    string meetingUrl
    string provider
    datetime startsAt
    datetime endsAt
    string status
  }
  LIVE_CLASS_ATTENDANCE {
    string id PK
    string liveClassSessionId FK
    string moduleRegistrationId FK
    datetime joinedAt
    datetime leftAt
  }
  QUIZ_ATTEMPT {
    string id PK
    string moduleAssessmentId FK
    string moduleRegistrationId FK
    int attemptNumber
    datetime startedAt
    datetime submittedAt
    float scorePercent
    string integrityStatus
  }
  QUIZ_QUESTION {
    string id PK
    string moduleAssessmentId FK
    int displayOrder
    string questionType
    string prompt
    float points
  }
  QUIZ_ATTEMPT_ANSWER {
    string id PK
    string quizAttemptId FK
    string quizQuestionId FK
    string response
    boolean isCorrect
    float pointsAwarded
  }
  ASSESSMENT_FEEDBACK {
    string id PK
    string moduleAssessmentId FK
    string moduleRegistrationId FK
    string staffMemberId FK
    string body
    datetime publishedAt
  }
  MODULE_FORUM {
    string id PK
    string moduleOfferingId FK
    string title
    string visibility
  }
  MODULE_FORUM_POST {
    string id PK
    string moduleForumId FK
    string authorStudentProfileId FK
    string authorStaffMemberId FK
    string parentPostId FK
    string body
    datetime postedAt
  }
  LEARNING_RELEASE_RULE {
    string id PK
    string moduleLearningItemId FK
    string ruleType
    datetime availableFrom
    string prerequisiteItemId FK
  }
  MATERIAL_QA_REVIEW {
    string id PK
    string moduleLearningItemId FK
    string reviewerStaffMemberId FK
    string decision
    datetime reviewedAt
  }
  COPYRIGHT_PERMISSION {
    string id PK
    string moduleLearningItemId FK
    string rightsHolder
    string licence
    date expiresOn
  }
          
CAP mapping. Quizzes / integrity → CAP-24, 29 · Live class → CAP-26 · Forums → CAP-25 · Feedback → CAP-28 · Release / completion → CAP-27 · Material QA → CAP-43 · Copyright → CAP-42. Meeting tool for live class remains TBC (SDD-10 Q11).
Proposed ≠ Live. Do not invent Portal API methods or Neon tables for these until product signs off and the Zod graph / Schema version is bumped. Session-only messaging demos are not these durable forum tables.

Logical DDL sketch (LMS core) LMS

Illustrative column shapes for the in-contract LMS set. Live Neon Demo uses id + record jsonb per collection — this sketch is the logical relational target, not a migration script.

-- LMS core (logical). Shared bridge tables omitted here; see §3 / §5.

module_weeks (
  id PK, module_offering_id FK, week_number, starts_on, title
)
module_learning_items (
  id PK, module_week_id FK, module_assessment_id FK NULL,
  kind, title, available_from, file_name, mime_type, size_bytes
)
learning_progress (
  id PK, module_registration_id FK, module_learning_item_id FK,
  status, completed_at NULL
)
assignment_submissions (
  id PK, module_registration_id FK, module_learning_item_id FK,
  file_name, mime_type, size_bytes, submitted_at, deleted_at NULL
)

-- Proposed (not in PortalRecordGraph yet)
quiz_attempts, quiz_questions, quiz_attempt_answers
assessment_feedback
live_class_sessions, live_class_attendances
module_forums, module_forum_posts
learning_release_rules, material_qa_reviews, copyright_permissions

Attribute quick reference — hub entities

Most referenced keys CMS

EntityPrimary keyImportant foreign keysStatus / enums
Campus id — countryCode, timeZone
StudentProfile id — maritalStatus, passport fields
Faculty id campusId — organisational home Not the sole definition of programme campus offering
Programme id facultyId — organisational owner certificate | diploma | bachelor | master | doctorate
ProgrammeCampusOffering id programmeVersionId, campusId Where a version is delivered; optional localProgrammeCode
ProgrammeEnrolment id studentProfileId, programmeVersionId (maps student to a version), campusId active | completed | withdrawn | deferred
ProgrammeVersion id programmeId Credits, semester count, CGPA/attendance mins; curriculum + fee plan attach here
StudyPeriod id programmeEnrolmentId, academicTermId completed | current | planned | deferred
ModuleRegistration id studyPeriodId, moduleOfferingId registered | completed | withdrawn | pending-confirmation
FinanceAccount id programmeEnrolmentId (1:1) active | closed
Invoice id financeAccountId, studyPeriodId? invoiceNumber INV-######
ModuleOffering Shared id moduleId, academicTermId, campusId online | physical | hybrid
ModuleWeek LMS id moduleOfferingId weekNumber, startsOn, title
ModuleLearningItem LMS id moduleWeekId, moduleAssessmentId? lecture-note | assignment | quiz | test | midterm | final
AssignmentSubmission LMS id moduleRegistrationId, moduleLearningItemId metadata; file bytes Live-only (Q5)
LearningProgress LMS id moduleRegistrationId, moduleLearningItemId not-started | viewed | completed | locked
Attendance Shared id moduleRegistrationId, classSessionId present | absent