Skip to main content

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

ContextDetails
AggregateGig::Shift (root) — Applications, Assignments, and QrCodes are children of a Shift
LayerFulfilment
Upstream dependenciesGig::Job (which job this shift belongs to), Gig::PayRate (resolved hourly rate), Geo::PublicHoliday (public holiday detection for rate resolution)
Downstream dependentsGig::Application (talent apply to shifts), Gig::Assignment (confirmed work orders), Gig::QrCode (clock-in/out codes), Listings::Job (marketplace listing)

State Machine

FromToTriggerNotes
(new)draftUC-1: Shift createdDefault status. Not visible on marketplace.
draftpending_approvalUC-2: Employer publishes (location requires approval)Awaiting approver action.
draftopenUC-2: Employer publishes (no approval required)Immediately visible on marketplace.
pending_approvalopenUC-3: Approver approvesShift becomes visible on marketplace.
pending_approvalcancelledUC-3: Approver rejectsShift will not proceed.
openactiveUC-6: System trigger at starts_atTime-based. Enables clock-in and no-show detection.
opencancelledUC-4: Employer cancels shiftAll Applications cancelled. Credit deduction may apply if Assignments existed.
openexpiredUC-5: Cutoff time reachedNo applicants or no selection by deadline. All pending Applications closed.
activepending_verificationUC-7: All Assignments reach terminal stateAll workers clocked out, cancelled, or no-show. Settlement window begins.
activecancelledUC-4: Employer cancels (edge case — no one has clocked in)Rare. Only if shift is active but no clock-ins have occurred.
pending_verificationcompletedUC-8: All Assignments verified, Payments createdTerminal state. Shift is done.

Use Cases

IDUse CaseTriggerActor
UC-1Create a Shift under a JobEmployer needs workers for a specific date/timeEmployer
UC-2Publish a ShiftEmployer is ready to make the shift visibleEmployer
UC-3Approve or reject a pending ShiftShift requires approval before publishingApprover
UC-4Cancel a ShiftEmployer no longer needs workers for this shiftEmployer
UC-5Shift expires — no applicants or no selectionCutoff time reached without fulfilmentSystem
UC-6Shift becomes activeShift start time reachedSystem
UC-7Shift transitions to pending verificationAll assignments reach terminal stateSystem
UC-8Shift completesAll assignments verified, payments createdSystem
UC-9Edit a draft ShiftEmployer adjusts timing or headcount before publishingEmployer
UC-10View Shift detail with applicants and assignmentsEmployer managing a specific shiftEmployer

UC-1: Create a Shift under a Job

FieldDetails
ActorOrg::UserProfile (employer)
TriggerEmployer needs workers for a specific date/time

Preconditions:

  • Gig::Job exists and status: active
  • Employer has permission to manage this Job

System Behavior:

  1. Employer selects a Job and enters shift details: starts_at, ends_at, headcount
  2. System resolves the hourly rate from Gig::PayRate:
    • Determines day_type from starts_at date (weekday / weekend / public holiday via Geo::PublicHoliday)
    • Determines time_type from starts_at hour (day / night based on company's night cutoff)
    • Looks up matching rate: outlet-specific → company-wide → default (see Gig::PayRate UC-5)
  3. System displays the auto-resolved rate to the employer. Employer can override.
  4. System creates the Shift with status: draft

Business Rules:

  • starts_at must be in the future
  • ends_at must be after starts_at
  • headcount must be a positive integer (default: 1)
  • hourly_rate is auto-resolved but can be overridden by the employer
  • If no Gig::PayRate matches, 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

FieldDetails
ActorOrg::UserProfile (employer)
TriggerEmployer is ready to make the shift visible to talent

Preconditions:

  • Shift exists and status: draft

System Behavior:

  1. Employer publishes the Shift
  2. System calculates the estimated wage for the shift:
    • estimated_wage_cents = headcount × shift_duration_hours × hourly_rate (in cents)
  3. System calls Billing: ReserveEntitlements(reference: Gig::Shift#id, units: estimated_wage_cents)
    • Billing moves credits from availablereserved on 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"
  4. System checks if the employer's location requires approval:
    • If approval required → statuspending_approval
    • If no approval required → statusopen
  5. If open: Listings::Job is 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_approval or open
  • 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

FieldDetails
ActorApprover (HQ or area manager — depends on Org permission design)
TriggerShift requires approval before publishing

Preconditions:

  • Shift exists and status: pending_approval

System Behavior — Approved:

  1. Approver reviews and approves the Shift
  2. statusopen
  3. Listings::Job updated to include this shift

System Behavior — Rejected:

  1. Approver rejects the Shift
  2. statuscancelled
  3. Employer is notified: "Your shift was not approved"

Business Rules:

  • The approval hierarchy depends on Org::UserProfile role/permission design (open question)
  • Rejected shifts cannot be resubmitted — employer must create a new shift

Postconditions:

  • Shift is either open (approved) or cancelled (rejected)

UC-4: Cancel a Shift

FieldDetails
ActorOrg::UserProfile (employer)
TriggerEmployer no longer needs workers for this shift

Preconditions:

  • Shift status is draft, open, or active (with no clock-ins)

System Behavior:

  1. Employer requests cancellation with a mandatory reason (via Taxonomy::GigStatusReason)
  2. All pending and accepted Applications → cancelled
  3. All confirmed Assignments → cancelled
  4. System calls Billing to handle reserved credits:
    • If no Assignments existed: ReleaseHold(reference: Gig::Shift#id) — full reserved amount released back to available
    • If Assignments existed: partial release + partial consumption based on the employer cancellation deduction policy (see below)
  5. statuscancelled
  6. Affected talent are notified

Business Rules:

  • Cancellation reason is mandatory
  • If Assignments existed (talent had confirmed), employer credit deduction applies based on timing:
    • >48 hours 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_in status — 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

FieldDetails
ActorSystem
TriggerCutoff time reached without fulfilment

Preconditions:

  • Shift status: open
  • Cutoff time reached (configured hours before starts_at)

System Behavior:

  1. System checks: does the shift have any confirmed Assignments?
    • If yes: shift does NOT expire (it has confirmed workers)
  2. If no confirmed Assignments:
    • All remaining pending and accepted Applications → expired
    • statusexpired
    • Employer is notified: "Your shift expired without confirmed workers"

Business Rules:

  • Expiration only applies to open shifts
  • 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

FieldDetails
ActorSystem (time-based trigger)
Triggerstarts_at time is reached

Preconditions:

  • Shift status: open
  • At least one confirmed Assignment exists

System Behavior:

  1. System transitions statusactive at starts_at
  2. System begins no-show monitoring for all confirmed Assignments
  3. 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 active and 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

FieldDetails
ActorSystem
TriggerAll Assignments reach a terminal state

Preconditions:

  • Shift status: active
  • All Assignments are in one of: clocked_out, cancelled, no_show

System Behavior:

  1. System detects that no Assignments remain in confirmed or clocked_in status
  2. statuspending_verification
  3. The settlement window is now open for all clocked_out Assignments (employer can adjust billable times until 9am next day)

Business Rules:

  • This transition is automatic — no manual action required
  • The shift stays in pending_verification until all clocked_out Assignments are verified
  • If all Assignments are cancelled or no_show (nobody worked), the shift still enters pending_verification briefly before moving to completed (with no payments)

Postconditions:

  • Shift status: pending_verification
  • Settlement window open for billable time adjustments

UC-8: Shift completes

FieldDetails
ActorSystem
TriggerAll Assignments verified and Payments created

Preconditions:

  • Shift status: pending_verification
  • All Assignments are in terminal state: verified, cancelled, or no_show
  • Gig::Payment created for all verified Assignments

System Behavior:

  1. System confirms all Assignments are in terminal state
  2. statuscompleted

Business Rules:

  • completed is 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

FieldDetails
ActorOrg::UserProfile (employer)
TriggerEmployer adjusts timing or headcount before publishing

Preconditions:

  • Shift exists and status: draft

System Behavior:

  1. Employer modifies: starts_at, ends_at, headcount, hourly_rate
  2. If starts_at changes, system re-resolves the hourly rate from Gig::PayRate (since the day_type or time_type may have changed)
  3. Employer can accept the new rate or override

Business Rules:

  • Only draft shifts are editable — once published, timing and headcount are locked
  • hourly_rate can 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

FieldDetails
ActorOrg::UserProfile (employer)
TriggerEmployer managing a specific shift

Preconditions:

  • Shift exists

System Behavior:

  1. Employer selects a Shift
  2. System displays shift details: date/time, headcount, filled count, hourly rate, status
  3. System displays applicants (ranked) if shift is open
  4. System displays assignments with attendance status if shift is active or beyond
  5. 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 form
    • open: applicant list with ranking + "accept" actions
    • active: assignment list with clock-in status
    • pending_verification: assignment list with billable times + adjustment form
    • completed: read-only summary with payments

Postconditions:

  • Read-only operation — no data changes (actions are separate UCs)
note

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 / HeadcountWhat 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

  1. A Shift must belong to exactly one Gig::Job
  2. starts_at must be before ends_at
  3. starts_at must be in the future at creation time
  4. headcount must be a positive integer
  5. hourly_rate must be a positive decimal — auto-resolved from Gig::PayRate at creation, can be overridden by employer
  6. Only draft shifts are editable — published shifts lock their timing, headcount, and rate
  7. Shift status transitions follow the state machine strictly — no skipping states
  8. A shift with any clocked_in Assignment cannot be cancelled
  9. completed, cancelled, and expired are terminal states
  10. Staffing level (filled vs headcount) is derived from Assignment counts, not stored as a status

Model Interactions

Related ModelRelationshipInteraction
Gig::JobShift belongs_to JobThe Job provides outlet, role, and description. Shifts are added to Jobs over time.
Gig::PayRateRate resolved at creationSystem evaluates PayRate entries based on the Job's outlet + role + the shift's timing. Resolved rate stored on hourly_rate.
Geo::PublicHolidayUsed for rate resolutionSystem checks if starts_at date is a public holiday to determine day_type for rate lookup.
Gig::ApplicationShift has_many ApplicationsTalent apply to shifts. Applications track the selection process.
Gig::AssignmentShift has_many AssignmentsConfirmed work orders. Assignments track attendance and billable times.
Gig::QrCodeShift has_many QrCodesClock-in/out QR codes generated per shift.
Listings::JobShift synced to ListingsWhen a shift becomes open, the marketplace listing is updated to show it. When it leaves open, it's removed.
Org::UserProfileEmployer creates and manages shiftsEmployers create, publish, cancel, and review shifts.
BillingCredit reservation and consumptionAt publish: reserves credits for estimated wage via ReserveEntitlements. At cancellation: releases or partially consumes via ReleaseHold. At verification: consumes actual wage via CompleteShift.
Taxonomy::GigStatusReasonCancellation reasonRequired when employer cancels a shift.

Schema Gaps

GapImpactSuggested Resolution
Current DBML gig_shifts has no headcount columnCannot specify how many workers are neededAdd headcount integer [not null, default: 1]
Current DBML gig_shifts has no hourly_rate columnCannot store the resolved rate for this shiftAdd hourly_rate decimal [not null]
No uuid on gig_shiftsInconsistent with other modelsAdd uuid string [unique, not null]
No created_by / updated_by on gig_shiftsNo audit trailAdd FK references to org_user_profiles or identities_users
No source / source_id columnsCannot track externally sourced shifts (e.g., UKG)Add source string [null], source_id string [null]
No cancelled_at or cancellation_reason_id columnsCannot track when/why a shift was cancelledAdd cancelled_at timestamp, cancellation_reason_id bigint
No expired_at columnCannot distinguish when expiration happenedAdd expired_at timestamp [null]