Skip to content

fix(intra): refresh ongoing regular-cursus levels to catch silent Intra recomputes#21

Open
CheapFuck wants to merge 1 commit into
codam-coding-college:masterfrom
CheapFuck:fix/refresh-ongoing-regular-cursus-levels
Open

fix(intra): refresh ongoing regular-cursus levels to catch silent Intra recomputes#21
CheapFuck wants to merge 1 commit into
codam-coding-college:masterfrom
CheapFuck:fix/refresh-ongoing-regular-cursus-levels

Conversation

@CheapFuck
Copy link
Copy Markdown

What

Extends the existing "force-refetch ongoing cursus_users to catch silent server-side level recomputes" workaround — currently applied only to piscines — to regular cursuses (cursus_id 1 and 21) as well.

Why

Intra recomputes cursus_users.level server-side as a side-effect of project_user / scale_team events, but the recompute does not reliably bump cursus_users.updated_at. Because the incremental sync filters on updated_at, those level changes are silently dropped — the local DB freezes at the value from whichever event last happened to also touch the timestamp.

The author already knew this and worked around it for piscines via the loop at the old src/intra/cursus.ts:L201-L255, with the explicit comment:

// These are not included in the updated_at range because they are not updated directly in the database
// and instead calculated by the API server-side...

…but the same workaround was never extended to common-core. As a result, common-core students can sit at a stale level indefinitely once they hit a "silent" event.

Real-world reproduction

A Codam 2023 cohort student (cursus 21):

  1. Resume retry bumped level to 11.16 — synced OK (event touched updated_at).
  2. ft_containers was later removed from the curriculum — the project_user deletion bumped updated_at, synced OK.
  3. ft_transcendence was validated, taking the level to 12.69 on Intra — updated_at not bumped, sync missed it.
  4. Three more project validations followed — also silent.
  5. Hero stayed stuck at 11.16 while Intra showed 12.69.

How

  1. Extract the piscine force-refetch loop into a reusable refreshOngoingCursusLevels(api, syncDate, cursusIds, opts) helper.
  2. Tighten the "ongoing" filter to begin_at <= syncDate AND (end_at IS NULL OR end_at >= syncDate) so it handles both piscines (end_at always set) and common-core (end_at typically NULL until graduation/alumnization). The old filter end_at: { gte: syncDate } would exclude every common-core student.
  3. Make the "delete missing rows" behaviour opt-in via opts.deleteMissing:
    • true for piscines (preserves existing behaviour — a pisciner can unregister via Apply, and dropping the row is appropriate there).
    • false for REGULAR_CURSUS_IDS. Silently dropping a common-core row because the API briefly omitted one is too risky.
  4. Call the helper for REGULAR_CURSUS_IDS after the existing piscine pass.

Cost

  • One extra /cursus_users?filter[id]=… round per sync, chunked at 100 ids. For Codam, the ongoing common-core population is roughly the size of one cohort × a few years — a handful of API pages, negligible.
  • Logic for piscines is unchanged in behaviour (same query semantics aside from the new end_at IS NULL branch, which can't match piscines anyway).

Notes

  • The previous piscine query included cursus_id: 0 in the in: clause; that ID corresponds to no real cursus and has been dropped in the refactor (it never matched anything).
  • No schema changes, no migration needed.

Tested locally

Verified end-to-end with a focused script that seeds one row, writes a fake-stale level, runs the new refreshOngoingCursusLevels(REGULAR_CURSUS_IDS), and diffs before/after. Refresh correctly pulls the current Intra value.

…ra recomputes

Intra recomputes `cursus_users.level` server-side as a side-effect of
project_user / scale_team events, but the recompute does NOT reliably bump
`cursus_users.updated_at`. The incremental sync uses `filter[updated_at]`,
so those level changes are silently dropped.

The author already worked around this for piscines via a force-refetch loop,
but the same workaround was never extended to common-core (cursus 1 and 21).
Result: a common-core student's level can freeze at the value from the last
event that *did* happen to touch the timestamp, even though Intra's own UI
shows the correct value.

Real-world reproduction (Codam 2023 cohort student):
  - Resume bumped level to 11.16 (synced OK).
  - ft_transcendence was validated, taking the level to 12.69 on Intra.
  - Three more project validations followed.
  - Hero stayed stuck at 11.16 the entire time.

Fix:
  - Extract the piscine force-refetch loop into a reusable
    `refreshOngoingCursusLevels()` helper.
  - Tighten the 'ongoing' filter to `begin_at <= syncDate AND (end_at IS NULL
    OR end_at >= syncDate)` so it handles both piscines (end_at always set)
    and common-core (end_at typically NULL).
  - Make the 'delete missing rows' behaviour opt-in via `deleteMissing`.
    Kept `true` for piscines (a pisciner can unregister via Apply, where
    that semantics make sense). Set to `false` for REGULAR_CURSUS_IDS, since
    silently dropping a common-core row because the API briefly omitted it is
    too risky.
  - Call the helper for REGULAR_CURSUS_IDS in addition to the existing
    piscines pass.
Copy link
Copy Markdown
Member

@FreekBes FreekBes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Only one small change can be made, where cursus_users for the regular cursus that are no longer found in the API can also be deleted from the CodamHero database (see review comment).

Comment thread src/intra/cursus.ts
// We do NOT delete missing rows here: a common-core student dropping out of
// the curriculum is rare, and Intra may temporarily omit a row for unrelated
// reasons; better to leave the existing record in place than to delete it.
await refreshOngoingCursusLevels(api, syncDate, REGULAR_CURSUS_IDS, { deleteMissing: false });
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deleteMissing can be safely set to true here too. When applicants sign up for the Common Core through Apply but then change their mind and unregister, nowadays the cursus_user gets deleted and thus CodamHero should delete it too.

@FreekBes FreekBes added the bug Something isn't working label Jun 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants