Skip to main content

Gig::Assignment

Purpose

An Assignment represents a confirmed work order — a specific talent committed to work a specific shift. It is created when both the employer and talent agree (the double handshake completes on Gig::Application).

Assignments belong to the operations phase of the gig lifecycle. They answer the question: "Who is working, when did they start/stop, and how much do we pay them?" While Applications handle recruiting (who wants to work?), Assignments handle execution (attendance, time tracking, verification, payment).

Each Assignment tracks time at two levels:

  • Actual time: immutable timestamps from QR scans — the audit record of what the system observed
  • Billable time: employer-confirmed hours used for payment calculation — defaults to actual, can be adjusted during the settlement window (until 9am the next day)

Model Context

ContextDetails
EntityGig::Assignment (child of Shift)
LayerOperations
Upstream dependenciesGig::Shift (which shift), Gig::Application (which application created this), Talent::Profile (who is working), Gig::QrCode (clock-in/out mechanism)
Downstream dependentsGig::AssignmentAdjustment (billable time changes), Gig::Payment (wage disbursement), Billing (credit consumption)

State Machine

FromToTriggerNotes
(new)confirmedUC-1: Application confirmedCreated by system when Gig::Application reaches confirmed status.
confirmedclocked_inUC-2: Talent scans clock-in QRactual_clock_in recorded. Immutable.
confirmedcancelledUC-5/UC-6: Talent or employer cancelsBefore clock-in. Cancellation reason required.
confirmedno_showUC-7: System detects no clock-inTalent didn't show up. Triggers suspension discipline.
clocked_inclocked_outUC-3: Talent scans clock-out QRactual_clock_out recorded. billable_* fields set from employer form. Settlement window opens.
clocked_incancelledUC-6: Employer rejects mid-shiftEdge case — talent was working but employer cancels. Payment for hours worked is an open question.
clocked_outverifiedUC-4: Billable times lockedEither 9am deadline passes or employer explicitly locks. Payment created. Credits consumed.

Use Cases

IDUse CaseTriggerActor
UC-1Assignment created from confirmed ApplicationBoth parties agreed (double handshake)System
UC-2Talent clocks inTalent scans clock-in QR codeTalent
UC-3Talent clocks outTalent scans clock-out QR code + employer fills time formTalent + Employer
UC-4Assignment verified — billable times locked9am deadline or employer explicit lockSystem / Employer
UC-5Talent cancels assignmentTalent cancels before clock-inTalent
UC-6Employer cancels assignmentEmployer removes worker before or during shiftEmployer
UC-7No-show detectedTalent didn't clock in within the configured windowSystem
UC-8Employer adjusts billable timesEmployer corrects times during settlement windowEmployer
UC-9Admin adjusts billable times on behalf of employerOps enters adjustment from WhatsApp/email requestAdmin
UC-10View assignment detailEmployer reviewing a worker's attendanceEmployer

UC-1: Assignment created from confirmed Application

FieldDetails
ActorSystem
TriggerGig::Application reaches confirmed status

Preconditions:

  • Application status: confirmed (both employer accepted and talent confirmed)
  • Shift is open or active

System Behavior:

  1. System creates Gig::Assignment with:
    • gig_shift_id from the Application's shift
    • gig_application_id linking back to the Application
    • talent_profile_id from the Application
    • status: confirmed
  2. Shift's filled count increases

Business Rules:

  • One Assignment per Application — if the Application is confirmed, exactly one Assignment is created
  • The Assignment inherits the shift and talent references from the Application
  • No manual creation of Assignments — they are always created by the system as a result of Application confirmation

Postconditions:

  • Assignment exists with status: confirmed
  • Talent is expected to show up for the shift

UC-2: Talent clocks in

FieldDetails
ActorTalent::Profile (talent)
TriggerTalent scans clock-in QR code at the shift

Preconditions:

  • Assignment status: confirmed
  • Shift status: active (shift start time has been reached)
  • A valid (non-expired) Gig::QrCode with qr_type: clock_in exists for this shift

System Behavior:

  1. Talent scans the clock-in QR code
  2. System validates the QR UUID and checks expiry
  3. System records actual_clock_in = now() — this timestamp is immutable and never changes
  4. statusclocked_in

Business Rules:

  • actual_clock_in is set once and never modified — it is the system's record of when the QR was scanned
  • A talent can only clock in once per Assignment
  • Clock-in is only allowed when the shift is active
  • If the QR has expired, the talent sees an error and the employer must generate a new one

Postconditions:

  • Assignment status: clocked_in
  • actual_clock_in is set

UC-3: Talent clocks out

FieldDetails
ActorTalent::Profile (talent) + Org::UserProfile (employer)
TriggerTalent scans clock-out QR code, employer fills time confirmation

Preconditions:

  • Assignment status: clocked_in
  • A valid Gig::QrCode with qr_type: clock_out exists for this shift

System Behavior:

  1. Employer generates a clock-out QR code for the shift (see Gig::QrCode spec)
  2. Employer fills in the time confirmation form: billable_clock_in, billable_clock_out, billable_break_minutes
  3. Talent scans the clock-out QR code
  4. System records actual_clock_out = now() — immutable
  5. System sets billable_clock_in, billable_clock_out, billable_break_minutes from the employer's form
  6. If employer skips the form, billable_* defaults to actual times with 0 break minutes
  7. statusclocked_out
  8. The settlement window opens — employer can adjust billable times until 9am the next day

Business Rules:

  • actual_clock_out is set once and never modified
  • billable_* fields are mutable during the settlement window (see UC-8)
  • The time confirmation form is the employer's first opportunity to set billable times — they can adjust later during the settlement window
  • A talent can only clock out once per Assignment

Postconditions:

  • Assignment status: clocked_out
  • actual_clock_out is set
  • billable_* fields are set (from form or defaulted from actuals)
  • Settlement window is open

UC-4: Assignment verified — billable times locked

FieldDetails
ActorSystem (deadline) or Org::UserProfile (explicit lock)
Trigger9am the next day arrives, or employer explicitly locks

Preconditions:

  • Assignment status: clocked_out

System Behavior:

  1. System sets billable_locked_at = now()
  2. statusverified
  3. System calculates wage:
    • total_hours = (billable_clock_out - billable_clock_in) - billable_break_minutes
    • gross_wage = total_hours × shift.hourly_rate
    • net_wage = gross_wage - deductions
  4. System creates Gig::Payment with the calculated wage (status: pending)
  5. System triggers Billing domain to consume gig credits from the employer's entitlement lots (consumed amount = gross wage)

Business Rules:

  • After billable_locked_at is set, no further changes to billable times are allowed (unless admin override)
  • The 9am deadline is system-enforced — a background job runs at 9am and locks all clocked_out Assignments from the previous day
  • Employer can explicitly lock earlier by confirming "these times are final"
  • If the shift had multiple Assignments, each is verified independently — the shift transitions to pending_verificationcompleted when ALL Assignments reach a terminal state

Postconditions:

  • Assignment status: verified
  • billable_locked_at is set
  • Gig::Payment created
  • Billing credits consumed

UC-5: Talent cancels assignment

FieldDetails
ActorTalent::Profile (talent)
TriggerTalent cancels before clock-in

Preconditions:

  • Assignment status: confirmed (not yet clocked in)
  • Shift has not started, OR shift started but talent hasn't clocked in yet

System Behavior:

  1. Talent requests cancellation with a mandatory reason (via Taxonomy::GigStatusReason)
  2. System evaluates cancellation timing (see Overview — Talent Cancellation Policy):
    • >48 hours before shift: no penalty
    • 3–48 hours: enters progressive suspension discipline
    • 0–3 hours: document required (urgent cancellation)
    • After shift start: cancellation blocked (treated as no-show)
  3. statuscancelled, cancelled_at set
  4. The corresponding Gig::Application.statuscancelled
  5. Shift's filled count decreases — position reopens for new applicants if shift is still open
  6. Employer notified

Business Rules:

  • Cancellation is blocked if the talent has already clocked in (clocked_in status)
  • Cancellation reason is mandatory
  • Document required for urgent cancellations (0–3 hours before shift)
  • Suspension discipline follows the progressive tiers: warning → 7d → 30d → 90d with 24-hour appeal window (see Overview — Suspension Policy)

Postconditions:

  • Assignment status: cancelled
  • Application status: cancelled
  • Shift filled count decreased
  • Suspension evaluation triggered if within 48-hour window

UC-6: Employer cancels assignment

FieldDetails
ActorOrg::UserProfile (employer)
TriggerEmployer removes worker before or during shift

Preconditions:

  • Assignment status: confirmed or clocked_in

System Behavior:

  1. Employer selects the Assignment to cancel with a mandatory reason (via Taxonomy::GigStatusReason)
  2. statuscancelled, cancelled_at set
  3. The corresponding Gig::Application.statusrejected
  4. Shift's filled count decreases
  5. Talent notified: "You have been removed from this shift"
  6. Employer credit deduction applies based on timing (see Overview — Employer Cancellation Policy)

Business Rules:

  • Rejection reason is mandatory
  • Progressive credit deduction based on timing: >48h (none) → 24-48h (low) → 12-24h (moderate) → 3-12h (high) → 0-3h (very high) → after start (full)
  • If the Assignment is clocked_in (talent is already working), the employer can still cancel but payment for hours already worked is an open question (see Gig::Payment spec)

Postconditions:

  • Assignment status: cancelled
  • Application status: rejected
  • Credits deducted from employer if applicable

Open Questions:

  • When an employer cancels a clocked_in Assignment, should the talent be paid for hours already worked? If yes, should a partial Payment be created at cancellation time?

UC-7: No-show detected

FieldDetails
ActorSystem
TriggerTalent didn't clock in within the configured window after shift start

Preconditions:

  • Assignment status: confirmed
  • Shift status: active
  • Configured no-show detection window has passed (e.g., shift end time + grace period)

System Behavior:

  1. System detects that the Assignment is still confirmed after the no-show window
  2. statusno_show
  3. No Gig::Payment is created
  4. Talent enters progressive suspension discipline (same tiers as late cancellation)
  5. Employer notified: "Worker did not show up"

Business Rules:

  • No-show detection is system-triggered via a background job
  • The no-show window is configurable (e.g., shift end time + X hours)
  • A no-show counts as an offense in the progressive suspension policy
  • No payment is generated for no-shows

Postconditions:

  • Assignment status: no_show
  • No Payment created
  • Suspension evaluation triggered

UC-8: Employer adjusts billable times

FieldDetails
ActorOrg::UserProfile (employer)
TriggerEmployer corrects times during settlement window

Preconditions:

  • Assignment status: clocked_out
  • billable_locked_at is NULL (settlement window still open)

System Behavior:

  1. Employer modifies one or more of: billable_clock_in, billable_clock_out, billable_break_minutes
  2. Employer provides a reason for the adjustment
  3. System creates an immutable Gig::AssignmentAdjustment record capturing:
    • Which fields changed (with old and new values, as JSON)
    • Who made the change
    • The reason
  4. billable_* fields updated on the Assignment

Business Rules:

  • Adjustments are only allowed while billable_locked_at IS NULL (before verification deadline)
  • Each adjustment creates an immutable audit record — the Assignment's billable_* fields are the current fact, the AssignmentAdjustment records are the event history
  • Reason is mandatory for each adjustment
  • Multiple adjustments are allowed during the settlement window — each creates a separate audit record

Postconditions:

  • Assignment billable_* fields updated
  • Gig::AssignmentAdjustment record created

UC-9: Admin adjusts billable times on behalf of employer

FieldDetails
ActorIdentities::Admin
TriggerOps enters adjustment from employer WhatsApp/email

Preconditions:

  • Assignment status: clocked_out
  • billable_locked_at is NULL (settlement window still open)

System Behavior:

  1. Admin modifies billable_* fields with a reason (e.g., "Per employer WhatsApp request")
  2. System creates Gig::AssignmentAdjustment with adjusted_by_type: Identities::Admin
  3. billable_* fields updated

Business Rules:

  • Same as UC-8, but adjusted_by references an Admin instead of an Employer
  • The reason field captures the communication channel for audit
  • Admin can also adjust AFTER billable_locked_at is set (admin override) — this is the only way to change verified times

Postconditions:

  • Same as UC-8

UC-10: View assignment detail

FieldDetails
ActorOrg::UserProfile (employer)
TriggerEmployer reviewing a worker's attendance

Preconditions:

  • Assignment exists

System Behavior:

  1. Employer views the Assignment detail
  2. System displays:
    • Talent name and profile summary
    • Status
    • Scheduled time (from Gig::Shift.starts_at / ends_at)
    • Actual time (actual_clock_in, actual_clock_out) — if clocked in/out
    • Billable time (billable_clock_in, billable_clock_out, billable_break_minutes) — if set
    • Adjustment history (from Gig::AssignmentAdjustment records)
    • Payment summary (from Gig::Payment) — if verified

Business Rules:

  • The view adapts based on Assignment status:
    • confirmed: shows scheduled time only, with "waiting for clock-in" indicator
    • clocked_in: shows actual clock-in time
    • clocked_out: shows all three time definitions + adjustment form (if settlement window open)
    • verified: read-only summary with payment details
    • cancelled / no_show: shows reason and timestamp

Postconditions:

  • Read-only operation — adjustments are a separate UC

Invariants

  1. An Assignment is always created by the system from a confirmed Application — no manual creation
  2. One Assignment per Application
  3. actual_clock_in and actual_clock_out are immutable once set — they are the system's audit record
  4. billable_* fields can only be modified while billable_locked_at IS NULL (except by admin override)
  5. billable_locked_at is set either by the 9am deadline or explicit employer lock — once set, it triggers Payment creation
  6. Every modification to billable_* fields creates an immutable Gig::AssignmentAdjustment record
  7. Gig::Payment is created only when the Assignment reaches verified status
  8. A clocked_in Assignment cannot be cancelled by the talent — only by the employer
  9. verified, cancelled, and no_show are terminal states
  10. No-show detection only applies to confirmed Assignments that never reached clocked_in

Model Interactions

Related ModelRelationshipInteraction
Gig::ShiftAssignment belongs_to ShiftThe shift provides the scheduled time window and hourly rate.
Gig::ApplicationAssignment belongs_to ApplicationThe Application that created this Assignment via the double handshake.
Talent::ProfileAssignment belongs_to TalentProfileThe worker assigned to this shift.
Gig::QrCodeQrCode scanned to update AssignmentClock-in/out QR codes update actual_clock_in / actual_clock_out on the Assignment.
Gig::AssignmentAdjustmentAssignment has_many AdjustmentsEvery change to billable_* fields creates an immutable audit record.
Gig::PaymentAssignment has_one PaymentCreated when Assignment reaches verified. Tracks wage calculation and disbursement.
BillingAssignment triggers credit consumptionWhen verified, the Billing domain consumes gig credits equal to the billable wage from the employer's entitlement lots.
Taxonomy::GigStatusReasonAssignment references StatusReasonRequired for cancellations and no-shows. Records why the Assignment ended.
Org::UserProfileEmployer manages assignmentsEmployers generate QR codes, fill time forms, adjust billable times, and cancel assignments.

Schema Gaps

Comparing the model spec against the current DBML in jodapp.dbml:

GapImpactSuggested Resolution
No uuid on gig_assignmentsInconsistent with other modelsAdd uuid string [unique, not null]
No gig_application_id on gig_assignmentsCannot trace which Application created this AssignmentAdd gig_application_id bigint [ref: > gig_applications.id, not null]
No status column on gig_assignmentsCannot track the Assignment lifecycleAdd status string [not null] with enum
No actual/billable time columnsCannot track attendance or calculate paymentAdd actual_clock_in, actual_clock_out, billable_clock_in, billable_clock_out, billable_break_minutes, billable_locked_at
No cancelled_at columnCannot track when cancellation happenedAdd cancelled_at timestamp [null]
gig_shift_id typed as integer instead of bigintInconsistent with other FK columnsChange to bigint
Ref type on gig_shift_id is < instead of >Incorrect relationship directionChange to ref: > gig_shifts.id
taxonomy_gig_status_reason_id typed as stringShould be bigint FKChange to bigint [ref: > taxonomy_gig_status_reasons.id]
cancel_reason, cancel_document, rejected_reason columnsReplaced by Taxonomy::GigStatusReason reference + AssignmentAdjustment auditRemove these columns; use taxonomy_gig_status_reason_id instead
note

The updated schema is already captured in jodapp-api/docs/db/gig.dbml. The gaps listed above reference the old schema in jodapp.dbml for migration planning.