Skip to main content

Org::Company

Purpose

An Org::Company represents an organisation that uses the Jod Platform. It is the root entity that owns all operational resources on the platform: team members, job postings (Careers and Gig), outlets, campaigns, and a billing account.

Every company that posts jobs, manages workers, or purchases credits on Jod is an Org::Company. If an organisation does not use the platform, it is not an Org::Company — even if it pays invoices or signs service agreements on behalf of a company that does.

This distinction is important. In the real world, the company that uses a service and the company that pays for it are often different. For example, Studio M Hotel Singapore posts jobs on Jod, but Republic Iconic Hotel Pte Ltd pays the invoices. Only Studio M is an Org::Company. Republic Iconic Hotel exists as a Billing::BillToProfile on Studio M's billing account — a billing-domain concept, not an org-domain concept.

Key Concepts

Operational Identity

Org::Company answers one question: "Who uses Jod?"

A company qualifies as an Org::Company when it:

  • Has users who log in to the platform
  • Posts job listings (Careers or Gig)
  • Manages workers and shifts
  • Consumes entitlements (placement credits, gig credits)

A company does not qualify as an Org::Company just because it:

  • Pays invoices on behalf of another company
  • Signs service agreements with Jod
  • Appears on commercial documents as the bill-to party

These commercial roles are handled by Billing::BillToProfile, a model in the Billing domain that captures who appears on invoices and agreements. Each Billing::Account can have multiple bill-to profiles. See the Invoice model spec for details.

Why Jod Does Not Model Corporate Structure

Real-world corporate relationships form a graph, not a tree:

RelationshipExampleWhy it breaks a tree
Joint VentureCompany A owns 60%, Company B owns 40%Two parents — a tree only allows one
FranchiseHanbaobao Pte Ltd (brand owner) and XYZ Food Services (franchisee)They are independent entities, not parent-child
ManagementHotel owned by Entity A, managed by Entity B, Entity B posts jobs on JodNeither is the "parent" of the other
RestructuringCompany splits, merges, or changes ownershipThe graph changes outside Jod's knowledge

Org::Company supports only a tree (one parent via parent_company_id). This is intentional — not a limitation. A tree can answer "who manages whom on the platform" — a question that always has one answer. A graph of corporate ownership, franchise, and management relationships does not have one answer. It changes frequently without Jod knowing, and it is not Jod's responsibility to maintain.

The principle: model what you can keep true, not what would be nice to know. Jod can verify "who uses our platform" — they log in, they post jobs, this is something Jod can observe. Jod cannot verify "who owns whom in the corporate world" — that changes through mergers, acquisitions, company restructuring, and government filings that happen outside Jod's control.

A company's publicly known name is not always its registered legal name:

Display NameLegal NameRegistration No (UEN)
McDonaldsHanbaobao Pte Ltd199401341H
GoGoX SingaporeGOGOVAN SINGAPORE PTE. LTD.201409568C

These are the same legal entity with two names. The name field stores the display name (shown in the platform UI and job listings). The legal_name field stores the registered legal name (used in commercial documents via Billing::BillToProfile).

This is a naming problem — one entity, two labels. It is different from the Studio M / Republic Iconic Hotel case, where two different registration numbers mean two separate legal entities. The test is simple:

  • Same registration number = same entity, different names. Use name and legal_name on one Org::Company.
  • Different registration numbers = different entities. The one that uses Jod is an Org::Company. The one that only pays is a Billing::BillToProfile.

Slug Generation

Each company has a slug — a URL-friendly version of the company's name, generated using Rails' parameterize method. For example:

nameslug
McDonaldsmcdonalds
NTUC Fairprice Co-operative Ltd Supermarkets (South)ntuc-fairprice-co-operative-ltd-supermarkets-south
Studio M Hotel Singaporestudio-m-hotel-singapore

The slug is derived from name (the display name), not from legal_name. This is intentional — URLs should use the name that people search for and recognise ("mcdonalds"), not the registered legal name ("hanbaobao-pte-ltd").

Where the slug is used:

  • Company landing page: /companies/{slug} — the public-facing company profile page where job seekers view the company and its active job listings
  • Listings domain: Listings::Job references company_slug to build job detail page URLs and for SEO
  • Sitemap generation: company URLs are included in the sitemap for search engine indexing

Business rules:

  • slug is unique across all companies (enforced by database unique index)
  • slug is generated automatically at company creation time — it is not user-editable
  • If name changes, the slug should be regenerated. However, this breaks existing URLs. Consider keeping the old slug as a redirect, or making slug immutable after creation.
  • Within the same parent company, (company_id, slug) is unique on outlets — outlet slugs are scoped to their company

Operational Hierarchy (Parent and Child Companies)

Org::Company supports a single level of nesting through parent_company_id:

  • A root company has parent_company_id = null. It is the top-level entity.
  • A child company has parent_company_id pointing to a root company.

This hierarchy controls who can manage whom on the platform:

  • Root company users can view and manage all child companies
  • Child company users can only manage their own company
  • Only root company users can create new child companies

This hierarchy is not corporate ownership. It represents who manages whom on the Jod Platform. Republic Iconic Hotel (which pays Studio M's invoices) does not appear in this hierarchy — it does not use Jod.

Enterprise Divisions

Large enterprise clients often have internal divisions that are not separate legal entities. These divisions need to operate independently on the platform: each managing their own outlets, budgets, users, and jobs.

For example, NTUC Fairprice Co-Operative Limited is a single legal entity (UEN S83CS0191L) with regional divisions like Supermarkets (South), Supermarkets (Central East), and Finest. Each division manages different outlets across Singapore and has its own budget for gig worker hiring.

These divisions are modeled as child Org::Company records under a root company:

Org::Company: NTUC Fairprice Co-Operative Limited (root, UEN S83CS0191L)
├── Org::Company: NTUC Fairprice Supermarkets (South) ← child, same UEN
│ ├── Billing::Account (own credits, own invoices)
│ ├── Outlets: FairPrice Bukit Ho Swee, FairPrice Redhill, ...
│ └── Users: area managers, location managers
├── Org::Company: NTUC Fairprice (Finest) ← child, same UEN
│ ├── Billing::Account (own credits, own invoices)
│ ├── Outlets: FairPrice Finest Waterway Point, ...
│ └── Users: area managers, location managers
└── ... more divisions

Each division:

  • Has its own Billing::Account with its own credit balance and entitlement history
  • Has its own Billing::Agreement with negotiated terms (terms may be identical across divisions)
  • Receives its own invoices (billed via Billing::BillToProfile, e.g., "NTUC Fairprice Co-Operative Limited (South)")
  • Owns its own jobs, outlets, and user assignments
  • Can have credits transferred to or from other divisions by Jod admin (admin adjustment between Billing::Account records)

Child companies share their parent's registration_no. The uniqueness constraint on registration_no is enforced only for root companies (via a partial unique index), allowing divisions to share the parent legal entity's UEN.

Simple vs Enterprise: Same Model, Different Data

The system uses the same model for both simple and enterprise companies. There is no feature flag or is_enterprise column. The difference is only in the data — not in the code:

AspectSimple Company (majority)Enterprise Company
StructureOne Org::Company (root, no children)Root Org::Company + child Org::Company records
Who is operational?The root company itselfThe child companies (divisions)
Billing::AccountActive, on the rootActive on each child; unused on the root
Jobs, outlets, usersBelong to the root companyBelong to child companies
registration_noUnique per countryRoot is unique; children share parent's value

A simple company is just an enterprise company that has no children. The same code works for both cases — no if/else branching is needed.

The Enterprise Root Company

For enterprise clients, the root Org::Company serves as the parent record that holds the legal entity information and groups all divisions together:

  • It stores the legal entity's UEN and legal name
  • It groups the operational divisions (child companies) under one parent
  • It may have management-level users, though in practice these accounts are rarely used

The root company's Billing::Account is created automatically (same as all companies) but is unused — zero credit balance, no invoices, no active agreements. All billing activity happens on the child companies' accounts.

Billing::Agreement records are created per division account, not on the root account. This keeps invoice generation simple: each division's invoices are generated from its own account without needing to look up the root. If the legal entity renegotiates terms, Jod admin updates each division's agreement individually.

Model Context

ContextDetails
EntityOrg::Company (aggregate root — owns Memberships, Outlets, and all operational resources)
LayerOrganisation Structure
Upstream dependenciesGeo::Country (country of registration), Geo::Area (office location)
Downstream dependentsOrg::Membership (team members), Org::Outlet (branches), Billing::Account (1:1), Careers::Job, Gig::TempJob

State Machine

FromToTriggerNotes
(new)activeUC-1a: Employer self-serve sign-upSMB employer creates company during sign-up. Active immediately.
(new)disabledUC-1b or UC-2: Admin creates companyAdmin onboarding or division creation. Requires explicit enable (UC-4).
disabledactiveUC-4: Admin enables the companyCompany can now post jobs and use the platform.
activedisabledUC-5: Admin disables the companyCompany cannot post new jobs. Existing jobs and data are preserved.
activedeletedUC-7: Employer soft-deletesOnly child companies. Root companies cannot be deleted.
disableddeletedUC-7: Employer soft-deletesOnly child companies.

State mapping to database columns:

  • disabled = is_enabled: false, is_deleted: false
  • active = is_enabled: true, is_deleted: false
  • deleted = is_deleted: true, is_deleted_at: <timestamp>

Use Cases

IDUse CaseTriggerActor
UC-1aEmployer signs up and creates a companySMB employer registers on the websiteEmployer (new)
UC-1bAdmin onboards a root companyEnterprise or special client is onboarded by opsAdmin
UC-2Create a child company (division)Enterprise client adds an operational divisionAdmin or Employer (root)
UC-3Edit company detailsCompany information changes (address, name, etc.)Admin or Employer
UC-4Enable a companyCompany is ready to use the platformAdmin
UC-5Disable a companyCompany pauses or stops using the platformAdmin
UC-6View companiesEmployer manages their company and divisionsEmployer
UC-7Delete a child companyDivision no longer uses the platformEmployer (root)
UC-8View company on marketplaceJob seeker views the company profilePublic

UC-1a: Employer signs up and creates a company

FieldDetails
ActorIdentities::User who does not yet have an Org::Membership
TriggerEmployer logs in and the system detects they have no Membership — shows the company creation form

Preconditions:

  • Identities::User already exists (created earlier via the sign-up/registration endpoint)
  • The user has no existing Org::Membership
  • No existing Org::Company with the same registration_no and country_id where parent_company_id is null
  • No existing Org::Company with the same name
  • No existing Org::Company with the same email

System Behavior:

  1. User logs in. System detects they have no Org::Membership and shows the company creation form.
  2. Employer fills in: company name, registration number, address, postal code, email, and their title
  3. System creates Org::Company with parent_company_id = null and is_enabled = true (active immediately)
  4. System creates Org::Membership linking the user to the company with role hq_manager, is_owner: true, is_default: true
  5. System generates UUID and slug for the company
  6. System determines the Geo::Area based on the address
  7. System automatically provisions a Billing::Account for the company

Business Rules:

  • This is the primary path for SMB companies joining the platform
  • Identities::User is created separately via the registration endpoint — this use case only handles Org::Company + Org::Membership creation
  • Company and Membership are created together in one transaction
  • Company starts as active — the employer can post jobs immediately
  • A root company has no parent (parent_company_id = null)
  • registration_no must be unique among root companies within the same country_id
  • email and name must be unique across all companies
  • The employer becomes the first user of this company via Org::Membership (role: hq_manager, is_owner: true)
  • Creating a company automatically creates a Billing::Account (see Billing Use Cases)

Postconditions:

  • Root company exists with is_enabled = true
  • Billing::Account exists for the company
  • Org::Membership exists linking the employer to the company
  • Employer can immediately post jobs and use the platform

UC-1b: Admin onboards a root company

FieldDetails
ActorIdentities::Admin (during onboarding)
TriggerEnterprise or special client is onboarded by ops

Preconditions:

  • No existing Org::Company with the same registration_no and country_id where parent_company_id is null
  • No existing Org::Company with the same name
  • No existing Org::Company with the same email

System Behavior:

  1. Admin enters company details: name, legal name, registration number, address, postal code, email
  2. System determines the Geo::Area based on the address
  3. System generates a UUID and a URL-friendly slug
  4. System creates the company with parent_company_id = null and is_enabled = false
  5. System automatically provisions a Billing::Account for the company

Business Rules:

  • This path is used for enterprise clients and special onboarding cases managed by Jod ops
  • Company starts as disabled — admin must explicitly enable it (UC-4)
  • A root company has no parent (parent_company_id = null)
  • registration_no must be unique among root companies within the same country_id
  • email and name must be unique across all companies
  • Creating a company automatically creates a Billing::Account (see Billing Use Cases)
  • For simple companies, the root company IS the operational entity — its Billing::Account will be active once enabled
  • For enterprise clients, the root company groups divisions together — its Billing::Account will remain unused while division accounts are active

Postconditions:

  • Root company exists with is_enabled = false
  • Billing::Account exists for the company
  • Admin can now create users, outlets, and child companies

UC-2: Create a child company (division)

FieldDetails
ActorIdentities::Admin or Org::Membership (employer from root company)
TriggerEnterprise client adds an operational division

Preconditions:

  • Actor belongs to a root company (parent_company_id = null) or is an admin
  • No existing Org::Company with the same name
  • No existing Org::Company with the same email

System Behavior:

  1. Actor enters division details: name, address, postal code, email
  2. System sets parent_company_id to the root company's ID
  3. System copies the root company's registration_no and country_id to the child
  4. System determines the Geo::Area, generates UUID and slug
  5. System creates the child company with is_enabled = false
  6. System automatically provisions a Billing::Account for the child company

Business Rules:

  • Only users from a root company (or admins) can create child companies
  • Only one level of nesting is allowed: a child company cannot create its own children
  • Child companies inherit the parent's registration_no — this is not editable on the child
  • Each child company gets its own Billing::Account with its own credit balance and entitlement history
  • Billing::Agreement must be created separately on the child's account for invoice generation

Postconditions:

  • Child company exists with parent_company_id set to the root company
  • Child company shares the parent's registration_no
  • Child company has its own Billing::Account
  • Root company users can now manage this division

Open Questions:

  • Should child companies be allowed to have a different country_id than the parent? (Future: multi-country expansion)

UC-3: Edit company details

FieldDetails
ActorIdentities::Admin or Org::Membership
TriggerCompany information changes

Preconditions:

  • Company exists and is not deleted
  • If actor is an employer: they belong to this company (if root), or to the parent company (if child)

System Behavior:

  1. Actor modifies company fields
  2. System validates uniqueness constraints
  3. System updates the company record

Business Rules:

  • Editable fields: name, legal_name, primary_address, postal_code, email, website_url, description, logo fields
  • Immutable fields: registration_no, country_id, parent_company_id — these define the company's identity and cannot change after creation
  • Root company users can edit both the root company and its child companies
  • Child company users can only edit their own company
  • Changing name or email must not conflict with existing companies

Postconditions:

  • Company reflects updated values

UC-4: Enable a company

FieldDetails
ActorIdentities::Admin
TriggerCompany is ready to use the platform

Preconditions:

  • Company exists with is_enabled = false
  • Company is not deleted

System Behavior:

  1. Admin enables the company
  2. System sets is_enabled = true

Business Rules:

  • Only admins can enable companies (not employers)
  • Enabling allows the company's users to post jobs and use platform features

Postconditions:

  • Company is_enabled = true
  • Company is fully operational on the platform

UC-5: Disable a company

FieldDetails
ActorIdentities::Admin
TriggerCompany pauses or stops using the platform

Preconditions:

  • Company exists with is_enabled = true

System Behavior:

  1. Admin disables the company
  2. System sets is_enabled = false

Business Rules:

  • Only admins can disable companies
  • Disabling does not delete data — jobs, users, billing history are all preserved
  • Existing active jobs and shifts are not automatically cancelled
  • The company cannot create new jobs while disabled

Postconditions:

  • Company is_enabled = false
  • Company cannot post new jobs

Open Questions:

  • Should disabling a parent company also disable all its child companies?
  • Should existing job postings be automatically closed when a company is disabled?

UC-6: View companies

FieldDetails
ActorOrg::Membership (employer)
TriggerEmployer manages their company and divisions

Preconditions:

  • Actor belongs to an Org::Company

System Behavior:

  1. Employer navigates to company management
  2. System returns companies based on the actor's position:
    • Root company user: their own company and all child companies (divisions)
    • Child company user: only their own company

Business Rules:

  • Root company users see the full group (own company + all children)
  • Child company users see only their own company
  • Deleted companies are excluded

Postconditions:

  • Read-only operation — no data changes

UC-7: Delete a child company

FieldDetails
ActorOrg::Membership (employer belonging to the root company)
TriggerDivision no longer uses the platform

Preconditions:

  • Target company is a child company (parent_company_id is not null)
  • Actor belongs to the parent (root) company

System Behavior:

  1. Actor requests deletion of a child company
  2. System performs a soft delete: sets is_deleted = true and is_deleted_at = now

Business Rules:

  • Root companies cannot be deleted — only disabled (UC-5)
  • Only child companies can be soft-deleted
  • Only users from the parent company can delete a child
  • Soft delete preserves all data for audit and billing history
  • Deleted companies do not appear in normal queries
  • Active jobs under the deleted company remain until completed — they are not cancelled

Postconditions:

  • Company is_deleted = true
  • Company no longer visible in company listings
  • Historical data (jobs, invoices, ledger entries) preserved

UC-8: View company on marketplace

FieldDetails
ActorPublic (job seeker)
TriggerJob seeker views the company profile

Preconditions:

  • Company exists, is enabled, and is not deleted

System Behavior:

  1. Job seeker navigates to the company's marketplace page (via slug)
  2. System displays public information: name, description, logo, location, active job listings

Business Rules:

  • Only enabled, non-deleted companies are visible on the marketplace
  • The display name is shown (not legal_name or registration_no)
  • Job listings shown are filtered to open/active jobs only

Postconditions:

  • Read-only operation — no data changes

Invariants

  1. An Org::Company represents either an organisation that uses the Jod Platform, or a parent record that groups enterprise divisions — never create one for an entity that only pays or signs agreements
  2. registration_no is unique within the same country_id for root companies only (enforced via partial unique index where parent_company_id IS NULL)
  3. Child companies inherit and share their parent's registration_no — this value is immutable on both root and child
  4. name and email are globally unique across all companies (root and child)
  5. parent_company_id supports only one level of nesting — a child company cannot have children of its own
  6. A root company (parent_company_id = null) cannot be soft-deleted — it can only be disabled
  7. Companies are never hard-deleted — deactivation uses is_enabled, removal uses is_deleted
  8. Every Org::Company has exactly one Billing::Account, created automatically at company creation
  9. country_id and registration_no are immutable after creation — they define the company's legal identity
  10. The parent-child relationship represents platform management hierarchy, not corporate ownership or billing responsibility

Model Interactions

Related ModelRelationshipInteraction
Org::Company (self)Company optionally belongs_to parentSingle-level operational hierarchy. Root company users manage child companies on the platform.
Org::MembershipCompany has_many MembershipsTeam members. Each membership links an Identities::User to a specific company with a role.
Org::OutletCompany has_many OutletsPhysical branches where work takes place. Each outlet belongs to exactly one company.
Org::PayRatePayRate scoped to CompanyPay rates for gig roles. Can be company-wide defaults or scoped to a specific outlet.
Geo::CountryCompany belongs_to CountryCountry of registration. Root companies must have unique registration_no within the same country.
Geo::AreaCompany belongs_to AreaGeographic area where the company office is located.
Billing::AccountCompany has_one AccountCreated automatically when company is created. Holds entitlement balances, invoices, and agreements. Unused for enterprise root companies.
Billing::BillToProfileAccount has_many BillToProfilesBill-to identities for commercial documents. May reference a different legal entity than the company itself.
Billing::AgreementAccount has_many AgreementsCreated per-division account (not on root). Captures negotiated terms for invoice pricing.
Careers::JobCompany has_many JobsFull-time and part-time career job postings.
Careers::JobApplicationCompany has_many JobApplicationsApplications received for the company's career jobs.
Gig::TempJobCompany has_many TempJobsTemporary gig job postings. Linked via remote_id during legacy migration.

Schema Gaps

GapImpactSuggested Resolution
No legal_name columnCannot distinguish display name from registered legal name (e.g., "McDonalds" vs "Hanbaobao Pte Ltd"). Billing::BillToProfile needs the legal name for commercial documents.Resolved in DBML. Add legal_name string column (nullable — defaults to name when not set)
registration_no has a full unique index (with country_id)Enterprise divisions (child companies) share their parent's UEN and cannot be created with the current constraintResolved in DBML. Replace with a partial unique index: UNIQUE(registration_no, country_id) WHERE parent_company_id IS NULL
created_by / updated_by are plain bigintCannot distinguish if the actor was an Identities::Admin or Identities::User. No FK constraint.Resolved in DBML. Replace with polymorphic columns: creator_type/creator_id and updater_type/updater_id. Rails belongs_to :creator, polymorphic: true. Values: 'Identities::Admin' or 'Identities::User'.
is_enabled + is_deleted used instead of a single status columnTwo booleans create a 2x2 matrix where one combination (enabled + deleted) is meaningless. Queries require checking both columns (WHERE is_enabled = true AND is_deleted = false).Consider replacing with a single status enum (active, disabled, deleted). If other models also use is_deleted for soft-delete consistency, keep the current approach but document the valid combinations.