Skip to content

Epic 48 — Admin Off-Platform Job Ingestion & Claim

New. Depends on Epic 39 (Email Delivery — not built yet; the invite email needs it). Decision (user, 2026-06-16): Admins can create job postings on behalf of other users by scraping an external page. If a producer user already exists with the scraped email, that user owns the job. Otherwise, email the address that a job was created on their behalf and invite them to create a free account to claim the posting and unlock system features.

✅ SHIPPED 2026-06-19. Epic 39 unblocked this. Decision (user, 2026-06-19): unclaimed jobs are NOT hidden — Job.producer is fully nullable and unclaimed imports are OPEN + swipeable by talents (shown with the scraped company/contact name, or nothing). Backend: services/jobImport/scrape.ts parseJobPosting (JSON-LD JobPosting + OG + email regex, zero deps — no cheerio) + scrapeJobUrl; Job.producerId nullable + pendingOwnerEmail/claimToken @unique/claimedAt (migration 20260619100000); GraphQL Job.producer/producerId nullable; the 4 non-null-producer spots guarded (tryFormMatch skips producer-less jobs — talent likes are held as JobInteractions and become warm leads on claim); createJobForEmail (owned → JOB_CREATED_ON_BEHALF notification/email; unknown email → unclaimed + token + jobClaimInvite email), claimJob(token) (race-safe, makes a ProducerProfile if needed), listClaimableJobs; GraphQL scrapeJobPosting/adminCreateJobForEmail (admin) + claimJob/claimableJobs (auth); JOB_CREATED_ON_BEHALF added to all 4 notification places + registry + authz-matrix. Frontend: null-producer fallback across DiscoverPage/TalentJobsPage/JobDetailPage/PublicJobPage/flier pages (producer?.companyName ?? contactPersonName ?? '', producer chrome only when present); AdminJobImportPage (paste URL → scrape → preview/edit → create-for-email, reached from an "Import a job" button on /admin/jobs); ClaimPage (/claim?token=) + ClaimBanner (post-login email-matched catch-all on the feed); routes/pageTitles/i18n (claim.* en/pt/es). Tests: scrape fixtures, owned-vs-unclaimed branching, claim/token validation, ClaimPage flow. BE 1468 / FE app 580 / DS 44 green; typecheck + lint + authz-matrix (399) clean. Deploy: run the Prisma migration. Completes the 2026-06-16 realignment wave (Epics 40–48).

Why: Seeds the marketplace with real off-platform postings and a growth loop (claim-to-onboard). The building blocks exist — admin auth (requireAdmin), createJob (sets producerId), user-lookup-by-email, createNotification — but there's no scraper, no admin create-for-user path, no unclaimed-job/claim model, and no email (Epic 39).

Architecture: An admin-only scrape utility fetches a URL and extracts job fields + a contact email (native fetch + a lightweight HTML/OG parser — add cheerio or parse OG/JSON-LD). adminCreateJobForEmail either assigns ownership to the matching producer's ProducerProfile, or creates an unclaimed job: Job.producerId nullable + pendingOwnerEmail + claimToken + claimedAt, with an invite email (Epic 39) + a new JOB_CREATED_ON_BEHALF notification. On register/login with that email (or via the signed claim link), the user claims the job and becomes its producer owner.

BE-JOBIMPORT-001 — Admin scrape/parse utility

Files:

  • Create: castyou-backend/src/services/jobImport/scrape.ts — admin-only fetch + parse (title, description, location, contact email, …) from a URL via OG/JSON-LD + cheerio; returns a draft job + detected email. Add cheerio to package.json.

Acceptance criteria: Given a sample posting URL/HTML, returns structured draft fields + the detected contact email (unit-tested against fixtures, no live network in tests).

BE-JOBIMPORT-002 — Create-on-behalf + unclaimed ownership

Files:

  • Edit: castyou-backend/prisma/schema.prismaJob.producerId nullable + pendingOwnerEmail String?, claimToken String? @unique, claimedAt DateTime?. Requires prisma migrate.
  • Create/Edit: admin resolver adminCreateJobForEmail(draft, sourceUrl) (requireAdmin) — if a producer exists for the email, set owner; else create unclaimed (signed claimToken), enqueue invite email (Epic 39), and fire JOB_CREATED_ON_BEHALF (new NotificationType in models/mongo/Notification.ts + NotificationTypes).

Acceptance criteria: Existing-producer email → owned job; unknown email → unclaimed job + invite email queued + claim token minted; admin-only.

BE-JOBIMPORT-003 — Claim flow

Files:

  • Create/Edit: claimJob(token) mutation (auth) + register/login hook — assigns the caller's ProducerProfile (creating one if needed) as the job's owner, clears pendingOwnerEmail/claimToken, sets claimedAt; rejects expired/used tokens.

Acceptance criteria: A user with the matching email claims the job via the signed link and becomes its owner; reused/forged tokens are rejected.

FE-JOBIMPORT-001 — Admin import page

Files:

  • Create: castyou-frontend/apps/app/src/pages/admin/AdminJobImportPage.tsx — paste URL → preview scraped fields → edit → "Create for email"; DS + i18n + pageTitles.

Acceptance criteria: Admin pastes a URL, reviews/edits parsed fields, and creates the on-behalf job; shows whether it was owned or unclaimed+invited.

FE-JOBIMPORT-002 — Claim banner/page

Files:

  • Create: a claim landing (from the email link) + a post-login "claim this job" banner for invited users.

Acceptance criteria: Invited user lands on a claim page, authenticates/registers (free), and claims the posting.

TEST-JOBIMPORT-001 — Ingestion + claim coverage

Acceptance criteria: Scrape parsing (fixtures), owned-vs-unclaimed branching, the email/notification trigger, and the claim/token validation are tested.