Skip to main content

Org::Outlet

Purpose

An Outlet represents a physical branch, store, or site where an Org::Company operates and where gig work takes place. Each outlet has its own address, and different outlets of the same company may have different pay rates for the same role.

In the legacy system, outlets are called "locations." The term "Outlet" is used in the new system to avoid confusion with geographic concepts (Geo::Area) and to match how Singapore's retail and F&B industries refer to their branches.

Outlets are a critical dependency for the Gig domain because:

  • A Gig::Job references an Outlet (where the work happens)
  • Org::PayRate can be scoped to a specific Outlet (outlet-level rates vs company-wide defaults)
  • Data shows that 92.5% of company-role combinations have different rates across outlets — rate resolution requires knowing which outlet the shift is for

Outlet Settings

Outlets carry per-outlet settings that control operational behavior. Settings use the is_setting_ prefix to distinguish them from core data fields:

  • is_setting_job_approval_required (boolean, default: false) — when enabled, gig jobs created at this outlet enter pending status and must be approved by an hq_manager or area_manager before they become visible to workers. See JodGig Org Statistics — Job Approval for usage data that informed this design.

Settings are columns directly on the org_outlets table, not a separate settings table. This is appropriate for a small, known set of boolean flags. If the number of settings grows beyond ~10, consider extracting to a separate Org::OutletSetting table.

Model Context

ContextDetails
EntityOrg::Outlet (independent — not an aggregate root with children)
LayerOrganisation Structure
Upstream dependenciesOrg::Company (which company owns this outlet), Geo::Area (geographic location)
Downstream dependentsGig::Job (references which outlet the work is at), Org::PayRate (rates can be scoped to a specific outlet)

State Machine

FromToTriggerNotes
(new)activeUC-1: Outlet createdDefault status on creation
activeinactiveUC-3: Outlet deactivatedNo new Jobs can reference this outlet. Existing Jobs/Shifts are unaffected.
inactiveactiveUC-4: Outlet reactivatedOutlet is available for new Jobs again.

Use Cases

IDUse CaseTriggerActor
UC-1Create an outlet for a companyCompany opens a new branch/storeAdmin (onboarding) or Employer
UC-2Edit an outlet's detailsAddress changes, name updatedAdmin or Employer
UC-3Deactivate an outletBranch closes or is no longer using gig workersAdmin or Employer
UC-4Reactivate an outletBranch resumes gig worker usageAdmin or Employer
UC-5View outlets for a companyEmployer managing their branchesEmployer
UC-6Admin views all outlets across companiesOps reviewing platform structureAdmin

UC-1: Create an outlet for a company

FieldDetails
ActorIdentities::Admin (during onboarding) or Org::Membership with hq_manager role (employer self-serve)
TriggerCompany opens a new branch/store

Preconditions:

  • Org::Company exists and is active

System Behavior:

  1. Actor enters outlet details: name, address, postal code
  2. Actor selects or confirms the Geo::Area for the outlet's location
  3. System creates the Outlet with is_active: true

Business Rules:

  • An Outlet must belong to exactly one Org::Company
  • Name and address are required
  • Multiple outlets can exist at the same address (e.g., two different brands in the same building)
  • Creating an outlet does not create any pay rates — those are configured separately

Postconditions:

  • Outlet exists and is active
  • Pay rates can now be configured for this outlet (via Org::PayRate)
  • Gig::Job can reference this outlet

UC-2: Edit an outlet's details

FieldDetails
ActorIdentities::Admin or Org::Membership (hq_manager)
TriggerAddress changes, name updated

Preconditions:

  • Outlet exists

System Behavior:

  1. Actor modifies outlet fields: name, address, postal code, Geo::Area
  2. System validates inputs

Business Rules:

  • Editable fields: name, address, postal code, address_geo_area_id
  • Immutable fields: org_company_id — an outlet cannot be transferred between companies
  • Edits do not affect existing Gig::Job records or their Shifts
  • Listings::Job entries referencing this outlet will reflect the updated address

Postconditions:

  • Outlet reflects updated values

UC-3: Deactivate an outlet

FieldDetails
ActorIdentities::Admin or Org::Membership (hq_manager)
TriggerBranch closes or is no longer using gig workers

Preconditions:

  • Outlet exists and is_active: true

System Behavior:

  1. Actor deactivates the outlet
  2. System sets is_active: false

Business Rules:

  • Existing Jobs and their Shifts referencing this outlet are NOT affected
  • No new Gig::Job can be created referencing an inactive outlet
  • Org::PayRate entries for this outlet remain in the database but are no longer used for new rate resolution
  • Deactivation is reversible (see UC-4)

Cross-Domain Check — Billing::OutletBudget:

  • Before deactivating, the system checks if an active Billing::OutletBudget exists for this outlet with non-zero balance (units_available > 0 or units_reserved > 0)
  • If non-zero balance exists: the UI displays a warning that credits should be deallocated back to the company pool. Deactivation is not blocked — the admin can proceed — but the warning ensures awareness of stranded credits
  • If units_reserved > 0: reserved credits are held for active shifts and cannot be force-deallocated. They will drain as existing shifts complete or cancel through normal Gig flows
  • After deactivation, the Billing::OutletBudget remains active but no new reservations can occur (since no new jobs can reference an inactive outlet). The admin should deallocate remaining available credits and archive the budget once fully drained
  • See Billing::OutletBudget spec for full details on the outlet budget lifecycle

Postconditions:

  • Outlet is_active: false
  • Outlet no longer available for new Job creation
  • If outlet had a Billing::OutletBudget: budget remains but will not receive new reservations

UC-4: Reactivate an outlet

FieldDetails
ActorIdentities::Admin or Org::Membership (hq_manager)
TriggerBranch resumes gig worker usage

Preconditions:

  • Outlet exists and is_active: false

System Behavior:

  1. Actor reactivates the outlet
  2. System sets is_active: true

Business Rules:

  • Existing Org::PayRate entries become available for rate resolution again
  • No automatic creation of Jobs or Shifts

Postconditions:

  • Outlet is_active: true
  • Available for new Job creation and rate resolution

UC-5: View outlets for a company

FieldDetails
ActorOrg::Membership (employer, any role)
TriggerEmployer managing their branches

Preconditions:

  • Employer belongs to an Org::Company

System Behavior:

  1. Employer navigates to outlet management
  2. System displays all outlets for the company: name, address, status, number of active Jobs

Business Rules:

  • Filterable by status (active, inactive)
  • Shows count of active Jobs per outlet for context

Postconditions:

  • Read-only operation — no data changes

UC-6: Admin views all outlets across companies

FieldDetails
ActorIdentities::Admin
TriggerOps reviewing platform structure

Preconditions:

  • None

System Behavior:

  1. Admin navigates to outlets admin page
  2. System displays all outlets across all companies
  3. Filterable by company, status, area

Business Rules:

  • Admin can see both active and inactive outlets

Postconditions:

  • Read-only operation — no data changes

Invariants

  1. An Outlet must belong to exactly one Org::Company — this cannot change after creation
  2. Outlets are never hard-deleted — only deactivated via is_active
  3. An inactive outlet cannot be referenced by new Gig::Job records
  4. Deactivating an outlet does not affect existing Jobs, Shifts, or Assignments
  5. Name and address are required fields

Model Interactions

Related ModelRelationshipInteraction
Org::CompanyOutlet belongs_to CompanyEvery outlet is owned by one company.
Geo::AreaOutlet belongs_to AreaGeographic area where the outlet is located. Used for "jobs near you" on the marketplace.
Org::PayRatePayRate optionally scoped to OutletPay rates can be company-wide (outlet NULL) or outlet-specific. Outlet-specific rates take precedence.
Gig::JobJob belongs_to OutletEach Job references the outlet where the work happens. This determines which pay rates apply to its Shifts.
Org::MembershipEmployer manages outlets via membershiphq_manager has implicit access to all outlets. area_manager and outlet_manager are scoped via Org::OutletAssignment.
Org::OutletAssignmentAssignment scopes membership to outletLinks an Org::Membership to specific outlets for area_manager and outlet_manager roles.

Schema Gaps

GapImpactSuggested Resolution
No org_outlets table exists in current DBMLOutlet concept is missing from the new system entirelyResolved in DBML. org_outlets table defined with company_id, name, address, geo_area_id, is_active and all other columns.
No is_setting_job_approval_required columnCannot control per-outlet job approval. Legacy uses locations.job_approval_required.Resolved in DBML. is_setting_job_approval_required boolean [not null, default: false] defined on org_outlets.
Legacy credit fields on org_outlets are transitionalavailable_credits, consumed_credits, job_credit_deduction, min_credit_limit, is_credit_negative_validation are synced from JodGig for migration. They do not belong in the new system — all credits live in Billing::EntitlementBalance at the company level.Remove these columns once the legacy migration to Billing::LedgerEntry is complete. Do NOT model outlet-level credit pools in the new system.
Legacy locations table has many fields not neededLegacy has credit tracking, auto-select config, etc. on locationNew org_outlets table should be clean — outlet-specific config can be separate tables if needed