Two complaints, one cause.
A Student who had finished a Course and came back to review it found that every “Go to next lesson” landed on the Course-complete screen. The handler checked “is Progress at 100%?”, and on a completed Course the answer is always yes. Reviewing a Course meant going through the finish screen once per Lesson.
A Student who had not solved an Active Lesson had no forward button at all. Word Match, Sentence Selection and Grouping only offered “Next lesson” after a correct Check. The way out was “Back to course”, which lands on the overview, where the Student has to find their place again.
Underneath both, every Lesson type answered “where does forward go?” for itself, from @is_last, @next_lesson and a Progress percentage.
The Next Step
The item gave that answer a name, the Next Step, and one home: Forgia.Courses.NextStep, a pure function over the Course’s Chapters, the Student’s Completions and the current Lesson.
flowchart TD
S["NextStep.resolve(chapters, completed, lesson)"] --> J{"this Completion
completes the Course?"}
J -->|"yes"| F[":finish
Course-complete screen"]
J -->|"no"| N{"a next Lesson?"}
N -->|"same Chapter"| NX[":next
Próxima aula →"]
N -->|"opens a Chapter"| NC[":next_chapter
Próximo capítulo: … →"]
N -->|"none"| C{"Course complete?"}
C -->|"yes"| CC[":course_complete
Ver conclusão do curso"]
C -->|"no"| P{"another Lesson pending?"}
P -->|"yes"| PE[":pending
Ir para a aula pendente →"]
P -->|"no, only this one"| NO[":none
nothing: the Check is the way on"]
style F fill:#dce8dc,stroke:#5f7a5f
style CC fill:#dce8dc,stroke:#5f7a5f
Two things the old code could not express fall straight out of the shape.
“Completes the Course” is an event, not a state. The finish screen appears only when this Completion is the one that took the Course to 100%, which is the rule finish_quiz already followed. On a completed Course the Next Step keeps going forward, and a secondary link offers the completion screen for anyone who wants the certificate again. The Instructor asked for both while reviewing, so both are there:

The overview is never an answer. A Course’s last Lesson with something skipped earlier used to send the Student to the overview without a word. The backlog had a separate Card for exactly that (Honest Finish Course Button), which this item absorbed. Now it says “Ir para a aula pendente” and goes to the first Lesson without a Completion. “Voltar ao curso” still exists, but only in the topbar, where it means “back”, a different action from “forward”.
Leaving without solving
An unsolved Active Lesson now always shows its Next Step as a secondary button beside Check:

How the step is taken matters as much as where it leads. A Passive Lesson’s forward button completes the Lesson (that is its self-declared Completion), so it stays a button that bubbles complete_and_next to the parent LiveView. The parent records the Completion and re-resolves the step on the server from the Completions as they now stand, rather than trusting the page. An Active Lesson’s forward control is a plain <.link navigate>: its Completion belongs to a correct Check, and leaving unsolved must record nothing. A test walks every Active type in the registry, clicks the secondary link and asserts zero Completions and zero Challenge Attempts.

The fifth outcome, :none, exists for one case: the current Lesson is the last, unsolved, and the only thing missing in the whole Course. A button pointing back at the page you are on would be a lie, so nothing renders, and the Check is the way on.
A criterion that was wrong
One acceptance criterion read “never offer an unpublished Lesson as a destination”. It rested on an assumption that turned out to be false: a Lesson’s status is not read anywhere on the Student side. The Lesson list, Progress and the Certificate all count draft Lessons today. Filtering drafts out of the Next Step alone would have made it disagree with Progress. A draft Lesson would still count towards 100% and never be offered, so the forward button could never complete the Course.
The criterion was replaced with “the Next Step’s destinations are exactly the Lessons the rest of the page counts and lists”, with the reason in the item’s Conversation. Whether Students should see draft Lessons at all is a real question. It is a different one, and it is noted rather than turned into a Card until it proves to be real.
Worth doing differently
That criterion got written because the refinement reasoned from the domain model (“unpublished means unseen”) instead of from the code. Next time a criterion asserts a filter, the refinement can grep for the filter before writing it down. One grep -rn "status" lib/forgia_web/live/catalog_live would have caught it in the interview rather than in implementation.