Gig::Shift
Purpose
A Shift is a specific time window within a Job that needs workers — for example, "Tuesday April 8, 9am–6pm, 3 workers needed." Shifts are the primary lifecycle entity in the Gig domain. All status tracking, applications, assignments, attendance, and payment happen at the Shift level, not the Job level.
Employers add Shifts to long-lived Jobs whenever they need manpower. Each Shift specifies when the work happens, how many workers are needed (headcount), and what the hourly rate is (auto-resolved from Gig::PayRate based on the shift's timing).
Model Context
| Context | Details |
|---|---|
| Aggregate | Gig::Shift (root) — Applications, Assignments, and QrCodes are children of a Shift |
| Layer | Fulfilment |
| Upstream dependencies | Gig::Job (which job this shift belongs to), Gig::PayRate (resolved hourly rate), Geo::PublicHoliday (public holiday detection for rate resolution) |
| Downstream dependents | Gig::Application (talent apply to shifts), Gig::Assignment (confirmed work orders), Gig::QrCode (clock-in/out codes), Listings::Job (marketplace listing) |
State Machine
| From | To | Trigger | Notes |
|---|---|---|---|
| (new) | draft | UC-1: Shift created | Default status. Not visible on marketplace. |
draft | pending_approval | UC-2: Employer publishes (location requires approval) | Awaiting approver action. |
draft | open | UC-2: Employer publishes (no approval required) | Immediately visible on marketplace. |
pending_approval | open | UC-3: Approver approves | Shift becomes visible on marketplace. |
pending_approval | cancelled | UC-3: Approver rejects | Shift will not proceed. |
open | active | UC-6: System trigger at starts_at | Time-based. Enables clock-in and no-show detection. |
open | cancelled | UC-4: Employer cancels shift | All Applications cancelled. Credit deduction may apply if Assignments existed. |
open | expired | UC-5: Cutoff time reached | No applicants or no selection by deadline. All pending Applications closed. |
active | pending_verification | UC-7: All Assignments reach terminal state | All workers clocked out, cancelled, or no-show. Settlement window begins. |
active | cancelled | UC-4: Employer cancels (edge case — no one has clocked in) | Rare. Only if shift is active but no clock-ins have occurred. |
pending_verification | completed | UC-8: All Assignments verified, Payments created | Terminal state. Shift is done. |
Use Cases
| ID | Use Case | Trigger | Actor |
|---|---|---|---|
| UC-1 | Create a Shift under a Job | Employer needs workers for a specific date/time | Employer |
| UC-2 | Publish a Shift | Employer is ready to make the shift visible | Employer |
| UC-3 | Approve or reject a pending Shift | Shift requires approval before publishing | Approver |
| UC-4 | Cancel a Shift | Employer no longer needs workers for this shift | Employer |
| UC-5 | Shift expires — no applicants or no selection | Cutoff time reached without fulfilment | System |
| UC-6 | Shift becomes active | Shift start time reached | System |
| UC-7 | Shift transitions to pending verification | All assignments reach terminal state | System |
| UC-8 | Shift completes | All assignments verified, payments created | System |
| UC-9 | Edit a draft Shift | Employer adjusts timing or headcount before publishing | Employer |
| UC-10 | View Shift detail with applicants and assignments | Employer managing a specific shift | Employer |
UC-1: Create a Shift under a Job
| Field | Details |
|---|---|
| Actor | Org::UserProfile (employer) |
| Trigger | Employer needs workers for a specific date/time |
Preconditions:
Gig::Jobexists andstatus: active- Employer has permission to manage this Job
System Behavior:
- Employer selects a Job and enters shift details:
starts_at,ends_at,headcount - System resolves the hourly rate from
Gig::PayRate:- Determines
day_typefromstarts_atdate (weekday / weekend / public holiday viaGeo::PublicHoliday) - Determines
time_typefromstarts_athour (day / night based on company's night cutoff) - Looks up matching rate: outlet-specific → company-wide → default (see
Gig::PayRateUC-5)
- Determines
- System displays the auto-resolved rate to the employer. Employer can override.
- System creates the Shift with
status: draft
Business Rules:
starts_atmust be in the futureends_atmust be afterstarts_atheadcountmust be a positive integer (default: 1)hourly_rateis auto-resolved but can be overridden by the employer- If no
Gig::PayRatematches, the system blocks creation with: "No pay rate configured for this role. Please contact your manager or ops."
Postconditions:
- Shift exists with
status: draft - Not visible on marketplace until published
UC-2: Publish a Shift
| Field | Details |
|---|---|
| Actor | Org::UserProfile (employer) |
| Trigger | Employer is ready to make the shift visible to talent |
Preconditions:
- Shift exists and
status: draft
System Behavior:
- Employer publishes the Shift
- System calculates the estimated wage for the shift:
estimated_wage_cents = headcount × shift_duration_hours × hourly_rate(in cents)
- System calls Billing:
ReserveEntitlements(reference: Gig::Shift#id, units: estimated_wage_cents)- Billing moves credits from
available→reservedon the employer's gig entitlement balance - Billing creates a hold projection keyed to this Shift (may span multiple FIFO lots)
- If the employer does not have enough available credits, publishing is blocked with an error: "Insufficient gig credits to post this shift"
- Billing moves credits from
- System checks if the employer's location requires approval:
- If approval required →
status→pending_approval - If no approval required →
status→open
- If approval required →
- If
open:Listings::Jobis updated to include this shift in the marketplace
Business Rules:
- Publishing validates all required fields are present (starts_at, ends_at, headcount, hourly_rate)
- Credit reservation happens at publish time to prevent overspend — the employer cannot post shifts they cannot pay for
- The reserved amount is an estimate based on full headcount × full shift duration. Actual consumption happens at Assignment verification (may be less if fewer workers, shorter hours, or cancellations)
- If the shift is cancelled, the reserved credits are released back to
available(see UC-4)
Postconditions:
- Shift is either
pending_approvaloropen - Billing credits reserved for the estimated wage
- If
open, talent can see and apply to this shift on the marketplace
UC-3: Approve or reject a pending Shift
| Field | Details |
|---|---|
| Actor | Approver (HQ or area manager — depends on Org permission design) |
| Trigger | Shift requires approval before publishing |
Preconditions:
- Shift exists and
status: pending_approval
System Behavior — Approved:
- Approver reviews and approves the Shift
status→openListings::Jobupdated to include this shift
System Behavior — Rejected:
- Approver rejects the Shift
status→cancelled- Employer is notified: "Your shift was not approved"
Business Rules:
- The approval hierarchy depends on
Org::UserProfilerole/permission design (open question) - Rejected shifts cannot be resubmitted — employer must create a new shift
Postconditions:
- Shift is either
open(approved) orcancelled(rejected)
UC-4: Cancel a Shift
| Field | Details |
|---|---|
| Actor | Org::UserProfile (employer) |
| Trigger | Employer no longer needs workers for this shift |
Preconditions:
- Shift
statusisdraft,open, oractive(with no clock-ins)
System Behavior:
- Employer requests cancellation with a mandatory reason (via
Taxonomy::GigStatusReason) - All
pendingandacceptedApplications →cancelled - All
confirmedAssignments →cancelled - System calls Billing to handle reserved credits:
- If no Assignments existed:
ReleaseHold(reference: Gig::Shift#id)— full reserved amount released back toavailable - If Assignments existed: partial release + partial consumption based on the employer cancellation deduction policy (see below)
- If no Assignments existed:
status→cancelled- Affected talent are notified
Business Rules:
- Cancellation reason is mandatory
- If Assignments existed (talent had confirmed), employer credit deduction applies based on timing:
>48hours before shift: no deduction — full reserved amount released- 24–48 hours: progressive deduction (low) — partial consumed, remainder released
- 12–24 hours: progressive deduction (moderate)
- 3–12 hours: progressive deduction (high)
- 0–3 hours: progressive deduction (very high)
- After shift start: full shift value consumed from reserved
- Cannot cancel a shift where any Assignment has
clocked_instatus — those workers are already working - Shifts from external sources (e.g., UKG) cannot be cancelled from Jod
Postconditions:
- Shift
status: cancelled - All Applications and Assignments cancelled
- Billing: reserved credits either released (no penalty) or partially consumed (deduction applied)
UC-5: Shift expires — no applicants or no selection
| Field | Details |
|---|---|
| Actor | System |
| Trigger | Cutoff time reached without fulfilment |
Preconditions:
- Shift
status: open - Cutoff time reached (configured hours before
starts_at)
System Behavior:
- System checks: does the shift have any
confirmedAssignments?- If yes: shift does NOT expire (it has confirmed workers)
- If no confirmed Assignments:
- All remaining
pendingandacceptedApplications →expired status→expired- Employer is notified: "Your shift expired without confirmed workers"
- All remaining
Business Rules:
- Expiration only applies to
openshifts - A shift with at least one confirmed Assignment does not expire — it proceeds to
active - The cutoff time is configurable (system-wide or per-company)
Postconditions:
- Shift
status: expired - All non-confirmed Applications closed
UC-6: Shift becomes active
| Field | Details |
|---|---|
| Actor | System (time-based trigger) |
| Trigger | starts_at time is reached |
Preconditions:
- Shift
status: open - At least one
confirmedAssignment exists
System Behavior:
- System transitions
status→activeatstarts_at - System begins no-show monitoring for all
confirmedAssignments - Clock-in is now allowed for this shift's Assignments
Business Rules:
- Transition is time-based, not event-based — the shift becomes active because it's time, regardless of clock-in activity
- This enables no-show detection: if a shift is
activeand an Assignment hasn't clocked in within the configured window, it can be flagged
Postconditions:
- Shift
status: active - Workers can clock in
UC-7: Shift transitions to pending verification
| Field | Details |
|---|---|
| Actor | System |
| Trigger | All Assignments reach a terminal state |
Preconditions:
- Shift
status: active - All Assignments are in one of:
clocked_out,cancelled,no_show
System Behavior:
- System detects that no Assignments remain in
confirmedorclocked_instatus status→pending_verification- The settlement window is now open for all
clocked_outAssignments (employer can adjust billable times until 9am next day)
Business Rules:
- This transition is automatic — no manual action required
- The shift stays in
pending_verificationuntil allclocked_outAssignments areverified - If all Assignments are
cancelledorno_show(nobody worked), the shift still enterspending_verificationbriefly before moving tocompleted(with no payments)
Postconditions:
- Shift
status: pending_verification - Settlement window open for billable time adjustments
UC-8: Shift completes
| Field | Details |
|---|---|
| Actor | System |
| Trigger | All Assignments verified and Payments created |
Preconditions:
- Shift
status: pending_verification - All Assignments are in terminal state:
verified,cancelled, orno_show Gig::Paymentcreated for allverifiedAssignments
System Behavior:
- System confirms all Assignments are in terminal state
status→completed
Business Rules:
completedis a terminal state — no further transitions- The Job remains
active— employer can add new Shifts
Postconditions:
- Shift
status: completed - All Payments created and Billing credits consumed
UC-9: Edit a draft Shift
| Field | Details |
|---|---|
| Actor | Org::UserProfile (employer) |
| Trigger | Employer adjusts timing or headcount before publishing |
Preconditions:
- Shift exists and
status: draft
System Behavior:
- Employer modifies:
starts_at,ends_at,headcount,hourly_rate - If
starts_atchanges, system re-resolves the hourly rate fromGig::PayRate(since the day_type or time_type may have changed) - Employer can accept the new rate or override
Business Rules:
- Only
draftshifts are editable — once published, timing and headcount are locked hourly_ratecan be re-resolved or manually overridden
Postconditions:
- Shift reflects updated values
- Rate may have been re-resolved based on new timing
Open Questions:
- Should published (
open) shifts allow headcount changes? Increasing headcount is safe, but decreasing could affect confirmed Assignments.
UC-10: View Shift detail with applicants and assignments
| Field | Details |
|---|---|
| Actor | Org::UserProfile (employer) |
| Trigger | Employer managing a specific shift |
Preconditions:
- Shift exists
System Behavior:
- Employer selects a Shift
- System displays shift details: date/time, headcount, filled count, hourly rate, status
- System displays applicants (ranked) if shift is
open - System displays assignments with attendance status if shift is
activeor beyond - System displays rate source: "Rate auto-resolved from company pay rate" or "Rate manually set by [employer name]"
Business Rules:
- The view adapts based on shift status:
draft: edit formopen: applicant list with ranking + "accept" actionsactive: assignment list with clock-in statuspending_verification: assignment list with billable times + adjustment formcompleted: read-only summary with payments
Postconditions:
- Read-only operation — no data changes (actions are separate UCs)
Clock-in and clock-out are Assignment-level concerns, not Shift-level. QR code generation, scanning, and time recording are covered in the Gig::Assignment and Gig::QrCode model specs. The Shift reacts to clock-in/out indirectly — when all Assignments reach a terminal state, the Shift transitions to pending_verification (UC-7).
Derived State: Staffing Level
Staffing level is not a status — it is derived by counting confirmed Assignments against headcount.
filled_count = shift.assignments.where(status: [:confirmed, :clocked_in, :clocked_out, :verified]).count
remaining = shift.headcount - filled_count
| Filled / Headcount | What the employer sees |
|---|---|
| 0 / 3 | "3 positions open" |
| 1 / 3 | "1 confirmed, 2 positions open" |
| 3 / 3 | "Fully staffed" |
The shift stops accepting new Applications when filled_count >= headcount or the application cutoff time passes.
Invariants
- A Shift must belong to exactly one
Gig::Job starts_atmust be beforeends_atstarts_atmust be in the future at creation timeheadcountmust be a positive integerhourly_ratemust be a positive decimal — auto-resolved fromGig::PayRateat creation, can be overridden by employer- Only
draftshifts are editable — published shifts lock their timing, headcount, and rate - Shift status transitions follow the state machine strictly — no skipping states
- A shift with any
clocked_inAssignment cannot be cancelled completed,cancelled, andexpiredare terminal states- Staffing level (filled vs headcount) is derived from Assignment counts, not stored as a status
Model Interactions
| Related Model | Relationship | Interaction |
|---|---|---|
Gig::Job | Shift belongs_to Job | The Job provides outlet, role, and description. Shifts are added to Jobs over time. |
Gig::PayRate | Rate resolved at creation | System evaluates PayRate entries based on the Job's outlet + role + the shift's timing. Resolved rate stored on hourly_rate. |
Geo::PublicHoliday | Used for rate resolution | System checks if starts_at date is a public holiday to determine day_type for rate lookup. |
Gig::Application | Shift has_many Applications | Talent apply to shifts. Applications track the selection process. |
Gig::Assignment | Shift has_many Assignments | Confirmed work orders. Assignments track attendance and billable times. |
Gig::QrCode | Shift has_many QrCodes | Clock-in/out QR codes generated per shift. |
Listings::Job | Shift synced to Listings | When a shift becomes open, the marketplace listing is updated to show it. When it leaves open, it's removed. |
Org::UserProfile | Employer creates and manages shifts | Employers create, publish, cancel, and review shifts. |
Billing | Credit reservation and consumption | At publish: reserves credits for estimated wage via ReserveEntitlements. At cancellation: releases or partially consumes via ReleaseHold. At verification: consumes actual wage via CompleteShift. |
Taxonomy::GigStatusReason | Cancellation reason | Required when employer cancels a shift. |
Schema Gaps
| Gap | Impact | Suggested Resolution |
|---|---|---|
Current DBML gig_shifts has no headcount column | Cannot specify how many workers are needed | Add headcount integer [not null, default: 1] |
Current DBML gig_shifts has no hourly_rate column | Cannot store the resolved rate for this shift | Add hourly_rate decimal [not null] |
No uuid on gig_shifts | Inconsistent with other models | Add uuid string [unique, not null] |
No created_by / updated_by on gig_shifts | No audit trail | Add FK references to org_user_profiles or identities_users |
No source / source_id columns | Cannot track externally sourced shifts (e.g., UKG) | Add source string [null], source_id string [null] |
No cancelled_at or cancellation_reason_id columns | Cannot track when/why a shift was cancelled | Add cancelled_at timestamp, cancellation_reason_id bigint |
No expired_at column | Cannot distinguish when expiration happened | Add expired_at timestamp [null] |