Skip to content

feat: Implement viewer service module with auto-enrolment tracking - #52

Open
aimansharief wants to merge 31 commits into
v1.0.3from
viewer-service
Open

feat: Implement viewer service module with auto-enrolment tracking#52
aimansharief wants to merge 31 commits into
v1.0.3from
viewer-service

Conversation

@aimansharief

Copy link
Copy Markdown
Collaborator

Description

Fixes # (issue)

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Refactor / Technical Debt
  • Test case addition/update

Microservice(s) Affected

  • userorg-service
  • lms-service
  • notification-service
  • lern-service (Unified/Common)
  • core / shared-utils

How Has This Been Tested?

  • Unit Tests: Ran mvn test for the affected module.
  • Integration Tests: Verified API endpoints locally.
  • Code Coverage: Generated JaCoCo report and met threshold.

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published in downstream modules

Viewer module (actors + thin controllers) wired into the lern monolith:
- View lifecycle (view/start|update|end|read), assessment submit/read, summary,
  and the recursive nested rollup in ViewerAggregatorActor.
- LP auto-enrolment: structural detection via trackablenodes, per-learner
  optionality, level-gated progressive enrol, cert + durable user_skills at completion.

Adapters make legacy content/state + activity/agg thin translators to the viewer,
with deployment_mode-driven transport (monolith: in-JVM ask/tell; distributed: HTTP).

Correctness fixes in this pass:
- writeAllNodeEnrolments matches rows on collectionid AND this LP's contextid,
  so independent same-course enrolments are no longer clobbered (design §4).
- Level completion derived from persisted course-enrolment status (recompute-safe).
- Optionality computed once via an in-JVM memo (no DB column).
- creditSkills writes only when there are new skills.
- aggregate() failures are caught and logged (fire-and-forget safety).

Config: viewer_enabled, deployment_mode, viewer_service_base_url, viewer_ask_timeout_ms,
hierarchy_relations_cache_ttl. Migrations: viewer.cql (prod in-place) +
viewer-test-keyspace.cql (fresh keyspace for testing).
Untrack viewer.cql (prod migration) and viewer-test-keyspace.cql and gitignore them;
they remain on disk but are not versioned.
- CourseEnrolmentActor: drop enrolTrackableDescendants/readCollectionHierarchy/
  collectTrackable (the eager pre-enrol the level-gated walker replaces) + now-unused
  isViewerEnabled/cassandraOperation/jsonMapper fields. Root enrol only; systemEnroll
  (the LP auto-enrol seam) kept.
- ProgressionPolicy: remove unused hasNestedTrackable/isAssessment/computeAchievedSkills
  (+ orphaned privates); keep levelOf/coursesOfLevel/orderedLevels/computeOptionalNodes.
- ViewerRequestKeys: canonical collectionId/contextId/contentId only (no legacy
  courseId/batchId fallback — callers resolve). Fix triggerAggregation to send contextId.
- ContentConsumptionActor: drop redundant batchId key (viewer reads contextId).
Enable the viewer to run as its own deployable service (deployment_mode=distributed)
alongside the monolith path (viewer-actors in lern-service, unchanged):
- build/viewer/Dockerfile (Play dist viewer-service-1.0-SNAPSHOT).
- build-local.sh / docker-build.sh: add 'viewer' service (profile=viewer,
  module=modules/viewer/service, dist=viewer-service-1.0-SNAPSHOT-dist.zip).
- deploy.yml: gated Build/Push Viewer Service steps (VIEWER_SERVICE_BUILD var) ->
  lern-viewer-service image.
- pr-checks.yml: viewer-build job (compile+test both viewer submodules) wired into
  sonar aggregation.

Verified: mvn -P viewer reactor (19 modules) + play2:dist produce the dist zip.
feat (viewer) : Implement viewer service module with auto-enrolment tracking
…contextid under viewer_enabled

Gated remap (Util.toCollectionColumns — no-op when viewer disabled) so the legacy enrol path works
against the generalised (viewer) schema. Applied in CourseBatchDaoImpl (all methods),
UserCoursesDaoImpl (read/update/updateV2/listEnrolments/getBatchParticipants), and
CourseEnrolmentActor.upsertEnrollment (insert row).
…index

Point the courseBatch ES read/write at the generalised field names (course-batch-1):
- CourseBatchUtil.esCourseMapping writes collectionId/contextId (batch create/update indexing)
- CourseBatchUtil.validateCourseBatch reads collectionId
- BaseEnrolmentActor.getBatches filters by contextId
- CourseEnrolmentActor.addBatchDetails keys batch docs by contextId
- CourseBatchController.search translates client courseId/batchId filters to collectionId/contextId
…abled

Non-viewer deployments keep courseId/batchId (old course-batch index); viewer deployments
use collectionId/contextId (course-batch-1). Gated via Util.VIEWER_ENABLED in:
- CourseBatchUtil.esCourseMapping (write) + validateCourseBatch (read)
- BaseEnrolmentActor.getBatches (search filter) + CourseEnrolmentActor.addBatchDetails (doc key)
- CourseBatchController.search (client filter translation)
Adds Util import to BaseEnrolmentActor.scala and CourseBatchController.java.
When Util.VIEWER_ENABLED, translate stored viewer fields (collectionId/contextId) back to the API contract (courseId/batchId). Changes added in SearchHandlerActor and CourseEnrolmentActor to remap keys so clients and downstream logic (participants, status) remain unchanged. Also adjust batchMap construction to use JsonKey.BATCH_ID. Status enrichment from dates is preserved.
…xtId

Adds collectionid=collectionId and contextid=contextId to the Cassandra
column->field mapping so viewer-renamed columns resolve to the camelCase
field names used by ES sync, batch search, and the enrolment list. Inert on
non-viewer clusters (columns absent).
Viewer-renamed columns read back as collectionId/contextId; remap them to
the API contract courseId/batchId at the enrolment read point so the list
flow (course/batch details join) resolves. Gated on viewer_enabled.
listEnrolments returns null (not empty) when a user has no enrolments;
move the viewer collectionId/contextId->courseId/batchId remap inside the
existing isNotEmpty guard to avoid NPE.
Logs cache decision + hit/miss, raw read count and row keys, post-remap
courseId/batchId, active-filter count, content-search request/result,
course-filter kept count, batch join, and final returned count -- to
pinpoint where /user/courses/list drops enrolments.
Logs confirmed the read returns lowercase collectionid/contextid when the
column-mapping properties aren't deployed, so the camelCase-only remap left
courseId/batchId null and the list dropped every enrolment. Handle both
camelCase and lowercase forms.
Add idempotent CourseBatchUtil.toEsCollectionFields and call it in
syncCourseBatchForeground (covers cert add/remove) and
BackgroundJobManager.updateCourseBatchInfoToEs, so every courseBatch ES
write emits camelCase collectionId/contextId under viewer. Prevents a
secondary writer from overwriting the create doc with mismatched field
names (the 'batch shows then disappears' bug).
…d rename

Revert the viewer collection-identity generalisation across shared lern
code and the viewer module to the canonical courseId/batchId (courseid/
batchid columns, course_id/batch_id for assessment_aggregator). Removes
Util.VIEWER_ENABLED/toCollectionColumns, CourseBatchUtil.toEsCollectionFields,
the esCourseMapping/SearchHandlerActor/CourseBatchController remaps, the
enrolment read remaps, and the assessment column gating. Viewer actors +
controllers now speak courseId/batchId. user_activity_agg.context_id and the
isViewerEnabled delegation toggle are unchanged. Adds a clean courseid/batchid
test-keyspace CQL (viewer*.cql untouched).
1) writeAllNodeEnrolments read row.get("courseid")/"batchid" but createResponse
   returns camelCase courseId/batchId, so nodeId was null and the root enrolment
   was never updated -> course completion never landed on user_enrolments. Read
   camelCase (lowercase fallback).
2) Stamp user_enrolments.lastcontentaccesstime/lastreadcontentid/lastreadcontentstatus
   on every view op (start/update/end/assess) so summary/list reflects real access.
- summaryDownload CSV read lowercase keys (courseid/completionpercentage/completedon)
  but createResponse returns camelCase, so those columns were blank. Map csv headers
  to the camelCase result keys.
- writeAllNodeEnrolments: createResponse rows are always camelCase, so drop the
  unnecessary lowercase orElse fallback and read courseId/batchId directly.
- summaryDownload (format=csv) writes CSV to a temp file and uploads via the
  generic CloudStorageUtil (StorageServiceFactory), returning result.url per the
  design. Provider/container/prefix all config-driven (sunbird_cloud_service_provider,
  sunbird_content_cloud_storage_container, viewer_summary_upload_path); no new dep.
- CloudStorageUtil: pass cloud_storage_region to StorageConfig (AWS S3 outside
  us-east-1); no-op when blank so Azure/existing behaviour is unchanged. Add
  JsonKey.CLOUD_STORAGE_REGION + config default.
- viewer/service pom: add azure/aws/gcp/oci profiles so the standalone viewer dist
  bundles the CSP runtime jar.
Critical:
- ViewConsumptionActor.touchEnrolmentAccess: read enrolment first, skip if
  absent (updateRecordV2 ifExists is a no-op -> would upsert phantom rows).
- ViewerAggregatorActor: merge contentstatus into the existing map instead of
  replacing, so nodes outside the root-keyed read aren't clobbered.
- ContentConsumptionActor.delegateAssessmentsToViewer: apply the legacy
  batch-validity guard (invalid/completed batches -> BATCH_NOT_EXISTS /
  NOT_A_ON_GOING_BATCH); only valid ongoing batches dispatch.

Important:
- ViewerSummaryActor.summaryDelete: read camelCase courseId/batchId.
- ActorStartModule: bind aggregator with pekko.actor.viewer-dispatcher.
- Delete dead RequestValidator.java and filters/LoggingFilter.java.
- ViewController/ViewSummaryController: validate mandatory fields before
  dispatch (400 with offending field instead of NPE/500).
- ContentConsumptionActor.updateConsumption: build finalContentList only in
  the legacy branch (dead work on the viewer-on path).

Perf:
- ViewerAggregatorActor.advanceLp: read enrolments once into a
  (courseId,batchId)->status snapshot; collapses the LP path's O(courses)
  status queries into one. Safe: runs after writeAllNodeEnrolments commits.
…tentstatus merge)

The C1 fix and the courseId/batchId revert had left the suite red (tests were
not re-run): touchEnrolmentAccess's getRecordByIdentifier was unstubbed, and the
aggregator guard tests sent stale collectionId/contextId keys. Restore green +
pin the two fixes.

- ViewConsumptionActorTest: stub the enrolment read in every write-op test;
  add touchEnrolmentAccess cases — no stamp when the enrolment is absent (no
  updateRecordV2 -> phantom-row guard), stamp exactly once when present.
- ViewerAggregatorActor: extract the contentstatus merge into a pure
  companion mergeContentStatus (updateRecordV2 replaces the whole column, so a
  root-keyed rollup must merge, not clobber).
- ViewerAggregatorActorTest: fix guard-test request keys; add mergeContentStatus
  cases (preserve existing leaves, fresh wins on conflict, null-tolerant).

19/19 green.
…urseId/batchId cleanup

- CourseEnrolmentActor: remove systemEnroll (and its onReceive route); LP progression
  now uses the standard `enrol` op. No parallel enrol method.
- ViewerAggregatorActor: internalEnrol dispatches `enrol` with requestId="system"
  (recorded as addedBy via context REQUEST_ID); re-enrol safety stays the caller's
  (advanceLp only enrols courses absent from the snapshot). Skill-derivation stub
  comments point at the framework last-category contract (design §6), not se_skills.
- CourseBatchManagementActor: nested-trackable child batches inherit the LP root
  batch's cert_templates (strip removed) -> each child course issues the LP cert.
- ActivityAggregatorActor: viewer-dispatch payload uses courseId/batchId (was the
  leftover collectionId/contextId, which the viewer never reads).
- ContentConsumptionActor: drop the leftover collectionId alias from content-state
  read (courseId is canonical); update CourseConsumptionActorTest expectation.
…trap

- Consistent fixed-phrase loggers (stage=view/rollup/lp/summary, key=val) at each
  state transition: view start/update/end/access, rollup start/LP-detect/nodes/
  node-complete->cert, lp optionality/level-opened/enrol/complete, summary ops.
- CourseEnrolmentActor: log an enrol confirmation ("enrol: enrolled ...").
- Wire the enrol->LP bootstrap (design Step 3): fireLpBootstrap after a user enrol
  fires the viewer rollup once (monolith in-JVM tell to viewer-aggregator-actor;
  distributed HTTP /v1/view/agg), so advanceLp opens the first LP course at enrol
  time. Skips system-lp child enrols and no-ops when viewer disabled. enroll's
  core logic unchanged.
- Fix stale "system-enrol requested" log wording in ViewerAggregatorActor.
ViewerAggregatorActor: course ancestors are not published, so derive a course's ancestor chain from one of its leaf nodes (use getLeafNodes(...).headOption -> getAncestors; fallback to empty list). Updated ProgressionPolicySpec with two tests to cover leaf ancestor chains and courses directly under root to ensure levelOf picks the correct level in both cases.
@aimansharief aimansharief changed the title Implement viewer service module with auto-enrolment tracking feat: Implement viewer service module with auto-enrolment tracking Aug 11, 2026
- LpPolicyUtil: cached /v3/search per LP + long-TTL framework->last-category-code
  cache; policy/courseMeta/isAssessment/questionSets/skillsOfQuestions (reads
  <category>, not se_<category>Ids)
- ViewerAggregatorActor: delegate policy/assessment/skill stubs; pre-assessment
  = first-level assessment course; Adaptive w/o pre-assessment warns+exits;
  PriorLearning waives prior-completed (any-batch status=2); skillsFromAssessment
  from assessment_aggregator best attempt (fully-correct). Engine & creditSkills
  unchanged
- ProgressionPolicy.computeOptionalNodes: +priorCompleted
- cache TTL config keys
…ts fix

- ViewerAggregatorActor: extract LP progression to injectable LpProgressionEngine
  (SRP/DIP) + EnrolDispatcher trait (OCP, monolith/HTTP); actor 445->288 lines
- A: precompute course->level map once; completedCourses Set; LpMeta.categoryCode
  (drop framework re-resolve)
- LpPolicyUtil.searchByIds: mapper-built JSON (escaping)
- CourseBatchManagementActor.batchExists: reuse readById, narrow catch so a
  transient DB error isn't misread as absent (no duplicate child batch)
- Adaptive LP with no pre-assessment now halts (opens nothing) instead of opening
  the first course
- Tests: LpProgressionEngineSpec (Strict/waiver/Adaptive-halt) + levelByCourse
…rse cert toggle

- Bridge to the LP root only on a course's not-complete->complete transition
  (writeAllNodeEnrolments returns the transitioned set; gate the bridge on it).
- Resolve the parent LP from the authoritative course_batch (by batchid), picking
  the row whose courseid differs from the completed course; extracted to a pure
  resolveParentLp for testability. Standalone (colon-free) batch -> no trigger.
- course_certificate_enabled toggle (default true) gating per-course certs; LP cert
  unaffected. Root progress/status + LP cert on LP completion in the engine.
- Tests: resolveParentLp (standalone/child/self-only/tiebreak) + parseCourseCertEnabled.
Update ViewerAggregatorActor so completedNow collects nodes whose status == 2 after the write pass (not only nodes that transitioned this pass). This makes LP bridging/advance run for already-complete courses (idempotent), while certificate issuance remains gated to the actual transition (status != 2 -> 2) to avoid duplicate cert events. Clarify related comments to reflect the changed semantics.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant