# Paid contributor work

A third panel, at `/work`, where somebody outside the company does small,
well-defined pieces of data work for a fixed price per item.

Built for Linda, but nothing in it is specific to her — it takes any number
of contributors.

---

## The deal

| Task | Pays | Why |
|---|---|---|
| Add a journalist | **250/=** | Heaviest — open a story, read enough to know the beat, check the byline is a real person |
| Add an outlet | **200/=** | |
| Add an influencer | **200/=** | |
| Check our Kiswahili | **150/=** | Lightest — read two lines, answer two questions |

| | |
|---|---|
| A full day | **15,000/=** — about **75 tasks** at the average rate |
| Paid on | Tasks actually finished. No minimum, no threshold. |

**Rates are per type on purpose.** A flat rate has to be wrong for either
the heaviest task or the lightest one, and the person doing them notices
long before we do. Set each in `config/work.php` under
`types.<TYPE>.reward`; the daily target is `WORK_DAILY_TARGET_TZS`.

The "about 75 tasks" figure is derived from the mean rate and is always
shown with "about" — the real number depends on which tasks she picks.

**Stopping early is paid.** Somebody who stops halfway is paid for half,
and the board says exactly that. This is stated on her dashboard in those
words, on purpose — it is the single thing most likely to be misunderstood.

### The rate is frozen at submission time

`reward_amount` is copied onto each row when it is saved and never read
back from config. Raising the rate later does not restate what last week's
work was worth. There is a test for this.

---

## The four tasks

Each had to clear the same bar: **useful to Roote**, **doable in 2–3
minutes by someone who has never seen the codebase**, and **verifiable** —
every submission carries a source URL, so a spot-check is one click.

| Task | What she does | Why it is worth paying for |
|---|---|---|
| **Add a journalist** | Opens a TZ news site, copies a reporter's name, outlet, beat, and the story link | The media list is a PR platform's core asset, and ours is empty |
| **Add an outlet** | Records a paper / TV / radio / online outlet, its type, language and region | Everything else hangs off outlets; regional and Kiswahili ones matter most |
| **Add an influencer** | Finds a TZ creator, platform, handle, followers, niche | The influencer module has nothing in it and brands keep asking |
| **Check our Kiswahili** | Reads one live USSD prompt, says whether it sounds natural, rewrites it | Native-speaker QA on the exact words a panelist reads on their phone |

### Deliberately NOT a task

**Recruiting panelists.** Paying a bounty per signup is how panels fill up
with one person and a drawer of SIM cards, and it would put personal-data
collection in the hands of somebody on piece rates. Recruitment stays with
`PanelRecruiter`.

### The Kiswahili queue is bounded

It is every translated prompt in `FieldQuestionTemplates`, read live rather
than copied — add a question there and it becomes reviewable automatically.
When it runs out the page says so in plain words, because "nothing left"
and "broken" look identical to someone being paid per task.

---

## Creating a contributor

```bash
php artisan work:contributor "Linda Mwakasege" linda@roote.co.tz
```

Prints the three lines to send her: link, email, password. Pass
`--password=` to set one yourself, or re-run to reset it. `--show-tour`
replays the walkthrough.

A contributor has **`agency_id = null`** and **`role = 'CONTRIBUTOR'`**.
That is what keeps them out of the agency workspace — do not create one by
hand and give it an agency.

---

## Isolation — read this before changing anything

`users.agency_id` became nullable to support this. That change required
hardening `BelongsToAgency`'s global scope in the same commit:

> The scope only filtered when the signed-in user **had** an `agency_id`.
> A user with none fell straight through and would have seen **every
> tenant's rows**. It now matches nothing for a signed-in, non-super-admin
> user with no agency. Unauthenticated contexts (jobs, USSD, webhooks) stay
> unscoped as before.

Two tests guard this: `a tenantless user sees no agency scoped rows` and
`a contributor cannot reach the agency workspace`.

The `/work` panel touches exactly one model, `WorkSubmission`, which
deliberately does **not** use `BelongsToAgency` — ownership is `user_id`.

---

## Reviewing and paying

Super admin → **Contributors → Contributor work**. Three tabs:

- **To check** — everything still `PENDING`
- **Owed** — counted and not yet paid; the subheading gives the total
- **Everything**

Filter to one person + "Not yet paid" and the column total is the number to
hand over. Then select the rows → **Mark as paid**.

**Rejecting** requires a reason, and she sees that reason word for word on
her own screen. Rejected work stops counting immediately. Pending work
counts from the moment it is saved — review happens hours later, and a
dashboard showing zero all through someone's first day is both wrong and
demoralising.

---

## Traps already hit

- **A Filament closure parameter must be named `$query`.** `fn (Builder $q)`
  on a tab's `modifyQueryUsing` makes Filament resolve `Builder` from the
  container instead of by name, producing a **model-less** builder. The page
  then dies in `Table::getModel()` with `Cannot use "::class" on value of
  type null`, pointing at the filters form — nowhere near the cause.
  `the super admin review screen renders` exists to catch this.
- **`modalDescription()` after `requiresConfirmation()`** silently cancels
  the confirmation. Order matters.
- **Filament only ships the Tailwind classes it compiled**, so custom Blade
  in these panels uses inline styles for all structural layout.
