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::Jobreferences an Outlet (where the work happens) Org::PayRatecan 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 enterpendingstatus and must be approved by anhq_managerorarea_managerbefore 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
| Context | Details |
|---|---|
| Entity | Org::Outlet (independent — not an aggregate root with children) |
| Layer | Organisation Structure |
| Upstream dependencies | Org::Company (which company owns this outlet), Geo::Area (geographic location) |
| Downstream dependents | Gig::Job (references which outlet the work is at), Org::PayRate (rates can be scoped to a specific outlet) |
State Machine
| From | To | Trigger | Notes |
|---|---|---|---|
| (new) | active | UC-1: Outlet created | Default status on creation |
active | inactive | UC-3: Outlet deactivated | No new Jobs can reference this outlet. Existing Jobs/Shifts are unaffected. |
inactive | active | UC-4: Outlet reactivated | Outlet is available for new Jobs again. |
Use Cases
| ID | Use Case | Trigger | Actor |
|---|---|---|---|
| UC-1 | Create an outlet for a company | Company opens a new branch/store | Admin (onboarding) or Employer |
| UC-2 | Edit an outlet's details | Address changes, name updated | Admin or Employer |
| UC-3 | Deactivate an outlet | Branch closes or is no longer using gig workers | Admin or Employer |
| UC-4 | Reactivate an outlet | Branch resumes gig worker usage | Admin or Employer |
| UC-5 | View outlets for a company | Employer managing their branches | Employer |
| UC-6 | Admin views all outlets across companies | Ops reviewing platform structure | Admin |
UC-1: Create an outlet for a company
| Field | Details |
|---|---|
| Actor | Identities::Admin (during onboarding) or Org::Membership with hq_manager role (employer self-serve) |
| Trigger | Company opens a new branch/store |
Preconditions:
Org::Companyexists and is active
System Behavior:
- Actor enters outlet details: name, address, postal code
- Actor selects or confirms the
Geo::Areafor the outlet's location - 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::Jobcan reference this outlet
UC-2: Edit an outlet's details
| Field | Details |
|---|---|
| Actor | Identities::Admin or Org::Membership (hq_manager) |
| Trigger | Address changes, name updated |
Preconditions:
- Outlet exists
System Behavior:
- Actor modifies outlet fields: name, address, postal code,
Geo::Area - 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::Jobrecords or their Shifts Listings::Jobentries referencing this outlet will reflect the updated address
Postconditions:
- Outlet reflects updated values
UC-3: Deactivate an outlet
| Field | Details |
|---|---|
| Actor | Identities::Admin or Org::Membership (hq_manager) |
| Trigger | Branch closes or is no longer using gig workers |
Preconditions:
- Outlet exists and
is_active: true
System Behavior:
- Actor deactivates the outlet
- System sets
is_active: false
Business Rules:
- Existing Jobs and their Shifts referencing this outlet are NOT affected
- No new
Gig::Jobcan be created referencing an inactive outlet Org::PayRateentries 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::OutletBudgetexists for this outlet with non-zero balance (units_available > 0orunits_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::OutletBudgetremains 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::OutletBudgetspec 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
| Field | Details |
|---|---|
| Actor | Identities::Admin or Org::Membership (hq_manager) |
| Trigger | Branch resumes gig worker usage |
Preconditions:
- Outlet exists and
is_active: false
System Behavior:
- Actor reactivates the outlet
- System sets
is_active: true
Business Rules:
- Existing
Org::PayRateentries 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
| Field | Details |
|---|---|
| Actor | Org::Membership (employer, any role) |
| Trigger | Employer managing their branches |
Preconditions:
- Employer belongs to an
Org::Company
System Behavior:
- Employer navigates to outlet management
- 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
| Field | Details |
|---|---|
| Actor | Identities::Admin |
| Trigger | Ops reviewing platform structure |
Preconditions:
- None
System Behavior:
- Admin navigates to outlets admin page
- System displays all outlets across all companies
- Filterable by company, status, area
Business Rules:
- Admin can see both active and inactive outlets
Postconditions:
- Read-only operation — no data changes
Invariants
- An Outlet must belong to exactly one
Org::Company— this cannot change after creation - Outlets are never hard-deleted — only deactivated via
is_active - An inactive outlet cannot be referenced by new
Gig::Jobrecords - Deactivating an outlet does not affect existing Jobs, Shifts, or Assignments
- Name and address are required fields
Model Interactions
| Related Model | Relationship | Interaction |
|---|---|---|
Org::Company | Outlet belongs_to Company | Every outlet is owned by one company. |
Geo::Area | Outlet belongs_to Area | Geographic area where the outlet is located. Used for "jobs near you" on the marketplace. |
Org::PayRate | PayRate optionally scoped to Outlet | Pay rates can be company-wide (outlet NULL) or outlet-specific. Outlet-specific rates take precedence. |
Gig::Job | Job belongs_to Outlet | Each Job references the outlet where the work happens. This determines which pay rates apply to its Shifts. |
Org::Membership | Employer manages outlets via membership | hq_manager has implicit access to all outlets. area_manager and outlet_manager are scoped via Org::OutletAssignment. |
Org::OutletAssignment | Assignment scopes membership to outlet | Links an Org::Membership to specific outlets for area_manager and outlet_manager roles. |
Schema Gaps
| Gap | Impact | Suggested Resolution |
|---|---|---|
No org_outlets table exists in current DBML | Outlet concept is missing from the new system entirely | Resolved 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 column | Cannot 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 transitional | available_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 needed | Legacy has credit tracking, auto-select config, etc. on location | New org_outlets table should be clean — outlet-specific config can be separate tables if needed |