Skip to content

STORY-F-019: Industry profiles seed (labour_hire + construction) + ETS cache

Epic: Compliance Seed Priority: Must Have Story Points: 2 Status: Not Started Assigned To: Unassigned Created: 2026-04-17 Sprint: 4


User Story

As an Org Admin (future) and as a developer (now), I want two real industry profiles seeded (labour_hire + construction) referencing credential_types from F-015, with an ETS cache for hot-path reads, so that Phase 0's deliverable is genuinely multi-industry-ready (ADR per brainstorm-05) and subsequent Scout+Verify sprints have real industry config to reason over.


Description

Background

Brainstorm-05 + ADR (multi-industry design) commits to industries-as-configuration, not code. Ten profiles total (labour_hire, construction, mining, logistics, defence, retail, white_collar, traffic, civil, engineering) eventually; Phase 0 lands the two most relevant to ASG: labour_hire (meta-industry, typically combined) + construction.

This completes the Phase 0 compliance foundation. The remaining 8 profiles land as each relevant Migration Phase picks them up (e.g. mining + defence come when ASG starts serving those verticals).

Scope

In scope:

  • Migration creating compliance.industry_profiles table per data.md spec:
  • id UUID, slug VARCHAR UNIQUE, display_name, description, required_credential_types JSONB ([credential_type_ids]), recommended_credential_types JSONB, enabled_modules JSONB (["safety","fatigue","clearance"]), onboarding_steps JSONB, compliance_rules JSONB, terminology JSONB, dashboard_config JSONB, report_templates JSONB, metadata JSONB, timestamps, soft delete — platform-wide reference data (no org_id per data.md reference-data pattern)
  • compliance.org_industry_profiles pivot — (org_id, industry_profile_id) composite PK, activated_at, custom_overrides JSONB
  • Seed file priv/repo/seeds/compliance_industry_profiles.exs:
  • labour_hire — slug: labour_hire, required_creds: [right_to_work_*, first_aid_provide, wwcc], recommended: [white_card], enabled_modules: [] (meta — no module-specific enablement), compliance_rules: {"consent_required": true, "national_police_check": true}, terminology: {"worker": "candidate"} (labour hire norm), onboarding_steps: ["right_to_work_verify", "national_police_check", "first_aid_verify"]
  • construction — slug: construction, required_creds: [white_card, first_aid_provide], recommended: [height_safety, working_at_heights, confined_space, ewp, telehandler, forklift], enabled_modules: ["safety", "assets"], compliance_rules: {"swms_required": true, "site_induction_required": true, "ppe_hi_vis_required": true}, terminology: {"worker": "operative", "shift": "deployment"}, onboarding_steps: ["right_to_work_verify", "white_card_verify", "site_induction", "ppe_issuance"], dashboard_config: {"widgets": ["active_worksites_map", "credential_expiry_heatmap"]}
  • FinnestCompliance.IndustryProfile Ecto schema (reference-data allowlist)
  • FinnestCompliance.OrgIndustryProfile Ecto schema
  • FinnestCompliance.IndustryProfiles context: get_by_slug/1, list/0, activate_for_org/2, deactivate_for_org/2, profiles_for_org/1, merged_requirements/1 (union of required_credentials across an org's active profiles per brainstorm-05 "composable profiles")
  • FinnestCompliance.IndustryProfileCache ETS GenServer — mirrors CredentialCache pattern from F-015; refreshes on industry_profile_updated event
  • Integration: FinnestCompliance.check/2 (skeleton from F-015) extended to read active industry profiles for the worker's org and determine which credentials are required — still returns :compliant placeholder for now; real gated-write logic lands in Scout+Verify sprints
  • Architecture test: IndustryProfile is reference-data (no org_id); OrgIndustryProfile has composite PK including org_id (expected exception to standard id UUID PK pattern — document in allowlist)

Out of scope:

  • Remaining 8 industry profiles (mining, logistics, defence, retail, traffic, civil, engineering, white_collar) — land as relevant phases pick them up
  • Full dashboard widget implementation (Scout+Verify sprints for analytics work)
  • Industry-adaptive UI theming (brainstorm-09 vision — Migration Phase 2 or 3)
  • FinnestCompliance.check/2 real gating logic (Scout+Verify sprints)
  • Admin UI for industry profile management (ties to F-020 admin console work, but not in Phase 0)

Technical Notes

  • Composable profiles per brainstorm-05: an org can activate both labour_hire + construction; required_credentials = UNION. This is implemented in merged_requirements/1.
  • Seed idempotent: upsert by slug; safe to re-run
  • required_credential_types stored as array of credential_type UUIDs — resolve at seed time by looking up F-015 seeds
  • Architecture test industry_profile_composability_test.exs: activate labour_hire + construction → merged_requirements contains UNION (no duplicates)
  • Terminology JSONB — when rendering UI for a given org, look up active profiles' terminology and merge (later profile overrides earlier) → pass as locale-like context. Phase 0 only scaffolds; no UI uses it yet.
  • Module namespace: modules live under FinnestCompliance.* (flat — not Finnest.Compliance.*). Same pattern as F-015 per Sprint 3 D4 resolution (Boundary library constraint).

Dependencies

  • Blocked by: STORY-F-015 (credential_types seed — profiles reference credential IDs)

Acceptance Criteria

  • Migration creates compliance.industry_profiles + compliance.org_industry_profiles; reversible
  • Seed inserts 2 profiles (labour_hire, construction); idempotent on re-run
  • FinnestCompliance.IndustryProfiles.get_by_slug("construction") returns construction profile with correct required/recommended credentials
  • IndustryProfiles.activate_for_org(org, profile) creates pivot row; emits industry_profile_activated event
  • IndustryProfiles.merged_requirements(org) with both profiles active returns UNION of credentials (no duplicates)
  • IndustryProfileCache ETS loads on boot; cache hit <1ms
  • Each seeded profile references valid credential_type UUIDs (FK integrity via runtime lookup)
  • Architecture test: reference-data allowlist applies; queries don't require tenant context
  • FinnestCompliance.check/2 skeleton extends to use profile data (returns :compliant placeholder still, but reads from real profiles)
  • mix format, credo --strict, dialyzer, mix boundary all green

Testing Requirements

  • Unit: seed runs cleanly; repeated seed no-op
  • Unit: context functions (get_by_slug, activate_for_org, merged_requirements) happy + error paths
  • Integration: activate both profiles for an org → merged_requirements returns UNION; deactivate one → re-query returns only the other's credentials
  • Cache: populate + invalidate + re-populate test

References