Workflow: Posting Lifecycle
Technical reference for the canonical posting-workflow module. Posting workflow owns parent posting creation, tutor applications, coordinator push, parent acceptance, parent withdrawal before scheduling, and coordinator reopen. It hands off to trial-workflow once a trial match exists.
Lowercase status contract
Persisted posting statuses are lowercase only: draft, open, filled, closed, parent_withdrawn.
Address source of truth
Parent posting create/update does not accept freeform address fields anymore. Worker derives postal_code and location_area from the selected child’s effective address (child_profiles.address_data, fallback users.address_data).
Module Contract
| Concern | Implementation | What it does |
|---|---|---|
| Workflow module | workers_nivaa/src/lib/workflows/posting-workflow/ | Owns posting aggregate loading, policy guards, commands, projections, and workflow effects. |
| Commands | commands.ts | Implements createPosting, updatePosting, applyToPosting, reapplyToPosting, withdrawPostingApplication, pushPostingApplications, acceptPostingApplicationAndCreateTrialMatch, parentWithdrawPosting, closePosting, and reopenPosting. |
| Family gate | family-onboarding integration | Blocks posting creation for children who are not ready for posting. |
| Tutor gate | tutor-onboarding integration | Blocks non-approved tutors from applying/reapplying/withdrawing. |
| Address derivation | resolveEffectiveChildPostingLocation() | Derives service area/postal code from child or inherited parent address server-side. |
Tables And Fields Touched
| Entity | Workflow usage |
|---|---|
postings | Created, updated, filled, closed, withdrawn, or reopened. Canonical states: draft, open, filled, closed, parent_withdrawn. |
posting_applications | Tracks tutor application status (Pending, Pushed, Shortlisted, Rejected, Withdrawn). |
student_profiles | Created on demand when acceptance requires a student profile for the new trial match. |
matches | Created at parent acceptance with match_type = 'Trial', status = 'parent_accepted', and trial_phase = 'Date_Pending'. |
trial_date_proposals | Expired/closed when a parent withdraws after a trial match exists but before any trial appointment has been created. |
Detailed Lifecycle
| Step | Trigger | Workflow write | Notifications / fanout | Next state |
|---|---|---|---|---|
| 1. Parent creates draft or live posting | POST /postings | createPosting() inserts a postings row with lowercase status and worker-derived address metadata. | Coordinator-facing notification fanout can fire after creation. | Posting enters draft or open. |
| 2. Parent updates posting | PATCH /postings/:id | updatePosting() mutates editable fields and re-derives address from the linked child. | No special lifecycle fanout by default. | Posting stays editable while policy allows. |
| 3. Tutor applies / re-applies / withdraws | POST /postings/:id/apply, /reapply, /withdraw | Application rows are inserted or status-transitioned on posting_applications. | Coordinator-facing notification/event fanout may fire. | Posting candidate pool changes without altering posting status. |
| 4. Coordinator pushes applications | POST /admin/postings/:id/push | Selected applications become Pushed and coordinator_pushed = 1. | Parent is notified that curated educator choices are ready. | Parent can review blind educator cards. |
| 5. Parent accepts one educator | POST /postings/:id/accept | acceptPostingApplicationAndCreateTrialMatch() creates a trial matches row, marks the selected application Shortlisted, rejects competing non-withdrawn applications, and sets postings.status = 'filled'. | Parent, tutor, and coordinator fanout continues through shared notifications. If initial trial slots are bundled, the route hands off immediately into trial-workflow proposal creation. | Control moves to trial-workflow. |
| 6. Parent withdraws before trial scheduling | POST /postings/:id/withdraw | parentWithdrawPosting() sets postings.status = 'parent_withdrawn'. If a pre-scheduled trial match exists with no trial appointments yet, the match is closed out and pending date proposals are expired. | Tutor/coordinator can see explicit withdrawn state instead of silent disappearance. | Posting is withdrawn but still coordinator-reopenable. |
| 7. Coordinator reopens parent-withdrawn posting | POST /admin/postings/:id/reopen | reopenPosting() sets postings.status = 'open' only when no trial appointment exists. | Normal posting visibility resumes. | Posting re-enters the marketplace. |
| 8. Coordinator closes posting | POST /admin/postings/:id/close | closePosting() sets postings.status = 'closed'. | No acceptance handoff occurs. | Posting leaves the marketplace without creating a new match. |
Withdrawal Boundary
Parent withdraw window
posting=open
-> parent may withdraw
posting=filled + trial match exists + trial_appointment_count=0
-> parent may still withdraw
posting=filled + trial_appointment_count>=1
-> parent withdraw is blocked
-> coordinator reopen is also blockedTrial Handoff
- Posting workflow owns acceptance only up to trial-match creation.
- Once a trial match exists, trial-workflow owns date negotiation, invoice/payment, feedback, and conversion.
- Bundled initial trial slots on the accept route are treated as a handoff shortcut into trial-workflow, not as posting-workflow owning trial negotiation.