feat(#597): implement milestone appeal system in contract and UI#956
Open
jerrymusaga wants to merge 1 commit into
Open
feat(#597): implement milestone appeal system in contract and UI#956jerrymusaga wants to merge 1 commit into
jerrymusaga wants to merge 1 commit into
Conversation
…ract, backend, and UI Contract (course_milestone/src/lib.rs): - Add Appealed and FinalRejected variants to MilestoneStatus enum - Add AppealData struct (reason + submitted_at) stored at MilestoneAppeal key - Add appeal_milestone(): learner transitions Rejected → Appealed, stores reason, emits event - Add resolve_appeal(): admin transitions Appealed → Approved (mints LRN) or FinalRejected - Add get_appeal() query function Backend: - Migration 021: extend status CHECK (+ appealed, final_rejected), add appeal_reason / appeal_submitted_at columns, extend audit decision CHECK (+ appeal_approved, appeal_rejected) - milestone-store.ts: add MilestoneReportStatus / MilestoneAuditDecision union types, appeal fields to MilestoneReport, submitAppeal() method (in-memory + Postgres) - milestone-appeal.controller.ts: appealMilestone (POST /api/milestones/:id/appeal — scholar auth, rejected→appealed, notifies SECOND_TIER_VALIDATOR_ADDRESS) and resolveAppeal (POST /api/admin/milestones/:id/resolve-appeal — admin, calls callVerifyMilestone on approve or emitRejectionEvent on final reject) - milestone-appeal.routes.ts: factory router wired with jwtService for scholar auth - index.ts: register createMilestoneAppealRouter Frontend: - types/milestone.ts: MilestoneReportStatus union type, appeal fields on report - MilestoneSubmitPanel: rejected state shows Appeal button + inline reason form; appealed state shows "Under Review" banner; final_rejected shows final decision panel - ScholarMilestones: STATUS_LABELS/STATUS_COLORS maps, coloured status badge, appeal timestamp in submission card
|
@jerrymusaga Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #597
Adds a formal appeal path for scholars when their milestone is rejected, covering the full stack: Soroban contract state machine, backend API endpoints, DB migration, and frontend UI.
Contract (
contracts/course_milestone/src/lib.rs)MilestoneStatusvariants:AppealedandFinalRejectedAppealDatastruct (reason + submitted_at) stored underDataKey::MilestoneAppealappeal_milestone(learner, course_id, milestone_id, reason)— learner-signed; transitionsRejected → Appealed, stores appeal data, emits eventresolve_appeal(admin, learner, course_id, milestone_id, approved)— admin-signed; transitionsAppealed → Approved(orFinalRejected)get_appeal()read-only queryDatabase (
server/src/db/migrations/021_milestone_appeals.sql)milestone_reports.statusCHECK to includeappealedandfinal_rejectedappeal_reason TEXTandappeal_submitted_at TIMESTAMPTZcolumnsmilestone_audit_log.decisionCHECK to includeappeal_approvedandappeal_rejectedstatus = 'appealed'for admin queue queriesBackend
milestone-appeal.controller.tsappealMilestone—POST /api/milestones/:id/appeal: requires scholar JWT + ownership check, validatesrejectedstatus, callsmilestoneStore.submitAppeal(), notifiesSECOND_TIER_VALIDATOR_ADDRESS(env var) via in-app + push notificationresolveAppeal—POST /api/admin/milestones/:id/resolve-appeal: requires admin auth, validatesappealedstatus; on approve callscallVerifyMilestoneto mint LRN and updates toapproved; on reject callsemitRejectionEventand updates tofinal_rejected; adds audit entry, notifies scholarmilestone-appeal.routes.ts: factory router usingjwtServicefor scholar authmilestone-store.ts: newMilestoneReportStatus/MilestoneAuditDecisionunion types,appeal_reason/appeal_submitted_atfields,submitAppeal()method (Postgres + in-memory)index.ts: registerscreateMilestoneAppealRouterFrontend
src/types/milestone.ts:MilestoneReportStatusunion type, appeal fields onSubmittedMilestoneReportMilestoneSubmitPanel.tsx: new states —rejected(shows "File an Appeal" button + inline reason form),appealed(pulsing warning banner),final_rejected(closed final-decision card)ScholarMilestones.tsx:STATUS_LABELS/STATUS_COLORSmaps, coloured status badge, appeal timestamp shown when status isappealedTest plan
npm run migrate— confirm021_milestone_appealsapplies cleanlypendingreport → expect 409appealed, second-tier validator receives notificationappealedreport → expect 409/admin/milestones/:id— confirmappealedstatus is visiblePOST /api/admin/milestones/:id/resolve-appealwith{ approved: true }→ status →approved, LRN minted, scholar notifiedPOST /api/admin/milestones/:id/resolve-appealwith{ approved: false, reason: "..." }→ status →final_rejected, scholar notifiedappealedstatus shows pulsing yellow banner;final_rejectedshows closed red cardnpm run migrate:rollback— confirms undo script drops columns and restores CHECK constraints cleanlyNotes
appeal_milestone()on-chain requires learner wallet auth; the backend records the DB state only. Callers who want full on-chain provenance can invoke the contract from the frontend wallet before or after calling this endpoint.SECOND_TIER_VALIDATOR_ADDRESSenv var controls who gets notified; if unset, no notification is sent (no error).