# Changelog

## [Unreleased] — 2026-02-26

### Fixed

#### Jobs — Missing `failed()` method
All four queued jobs were missing the `failed(\Throwable $exception)` method required by the
project spec. Without it, permanently-failed jobs would not log an error at the job level
(only the global `NotifyFailedJob` listener fires, which requires `FAILED_JOBS_NOTIFY_EMAIL`
to be configured). Added `failed()` to:
- `SendNotificationJob` — also marks the notification as `status = 'failed'`
- `DeliverWebhookJob`
- `ProcessSlackCommandJob`
- `ProcessSlackModalSubmitJob`

#### SendNotificationJob — database-only notifications never marked as `sent`
Notifications delivered only to the `database` channel (no email, Slack, or push configured)
were left in `status = 'queued'` forever after the job ran, because neither
`EmailNotificationSender` nor `SlackNotificationSender` set the status for that channel.
Added a final `refresh + update` at the end of `handle()`: if the notification is still
`queued` after all channel senders finish, it is promoted to `sent`.

Updated two tests (`ReminderEngineTest`, `EmailNotificationDeliveryTest`) that were asserting
the intermediate `queued` state instead of the correct final `sent` state.

#### TaskTemplate — missing `HasFactory` trait
`App\Models\TaskTemplate` used `HasUuids` but not `HasFactory`, which would cause a runtime
error if any test or seeder called `TaskTemplate::factory()`.
Added `use HasFactory, HasUuids;` and created a proper `TaskTemplateFactory` with
`workspace_id`, `name`, `description`, `template_data`, `created_by`, `is_shared` states
(including a `shared()` state for convenience).

#### SendGrid inbound-email signature verification missing (security)
`InboundEmailController` verified Mailgun webhook signatures but accepted SendGrid webhooks
unconditionally. Added `verifySendGridSignature()` using ECDSA with the
`X-Twilio-Email-Event-Webhook-Signature` / `X-Twilio-Email-Event-Webhook-Timestamp` headers.
Added `services.sendgrid.webhook_public_key` config key and documented
`SENDGRID_WEBHOOK_PUBLIC_KEY` in `.env.example`.

#### TaskController.index() — no pagination (all tasks returned at once)
`GET /api/v1/tasks` was calling `->get()` and returning an unbounded collection, violating
the cursor-pagination requirement for all list endpoints. Changed to `cursorPaginate()` with
a configurable `per_page` (default 50, max 100) and a consistent
`{data, meta:{per_page, next_cursor, prev_cursor, has_more}}` envelope.

#### SlackService.withRetry() — blocking `sleep()` in queue worker
The Slack rate-limit retry path called `sleep($retryAfter)` (up to 60 s) inside a queue
worker, blocking the worker thread for the entire sleep duration. Changed to throw a
`\RuntimeException` instead, letting the queue framework's `backoff()` mechanism handle the
delay on the next attempt.

#### Deactivated users could still authenticate (security)
- **API login** (`Api\V1\AuthController@login`): added `status === 'active'` check before
  issuing a Sanctum token; returns HTTP 422 with a clear message if deactivated.
- **Web login** (`Auth\AuthController@login`): merged `['status' => 'active']` into the
  `Auth::attempt()` credentials array so Laravel's built-in guard rejects inactive accounts.
- **EnsureWorkspace middleware**: added a `status !== 'active'` guard that returns HTTP 403
  (JSON) or logs the user out and redirects to login (web) for sessions that became invalid
  after a user was deactivated.
- **API protected routes**: added `ensure.workspace` to the `auth:sanctum` middleware group
  so that deactivated users with existing tokens are blocked on every API call, not just at
  login time.

#### api/user route returned raw Eloquent model
The scaffolding `GET /api/user` route returned `$request->user()` directly, bypassing
`$hidden` attributes and the `UserResource` transformation. Changed to return
`new UserResource($request->user())`.

#### Api\V1\AuthController — raw models in login and me responses
`login()` returned `'user' => $user` (raw model); `me()` returned
`'user' => $request->user()->load('workspace')` (raw model). Both now use `UserResource`.

#### UserResource — missing `workspace` and `workspace_id` fields
`UserResource::toArray()` did not include `workspace_id` or the conditionally-loaded
`workspace` relation. Tests that called `GET /api/v1/auth/me` and asserted
`user.workspace.id` were failing. Added both fields.

#### deploy.sh — missing `storage:link`
The deployment script did not run `php artisan storage:link`, which is required for local
disk attachments to be publicly accessible. Added the step before `config:cache`.

#### .env.example — undocumented environment variables
Added documentation for:
- `SENDGRID_WEBHOOK_PUBLIC_KEY` — EC public key for SendGrid inbound webhook verification
- `FAILED_JOBS_NOTIFY_EMAIL` — email address notified when a queued job permanently fails

### Added

- `database/factories/TaskTemplateFactory.php` — full factory for `TaskTemplate` with
  default state and `shared()` state.
- `config/services.php` — `sendgrid.webhook_public_key` config key.

---

## Issues Requiring Manual Attention

1. **Slack app configuration** — The Slack OAuth flow, slash commands, and interactive
   messages require a properly configured Slack app with the correct redirect URIs, slash
   command endpoints, and event subscriptions pointing to the production URL. These cannot
   be automated.

2. **Push notification stub** — `PushNotificationSender` logs payloads but does not send
   real push notifications. Integration with Firebase FCM or Apple APNs must be implemented
   separately.

3. **SendGrid webhook key** — `SENDGRID_WEBHOOK_PUBLIC_KEY` must be set in `.env` when
   `INBOUND_EMAIL_PROVIDER=sendgrid`. Obtain the EC public key from the SendGrid Event
   Webhook settings.

4. **Queue workers** — Ensure supervisord (or equivalent) is configured with the four
   queues (`default`, `notifications`, `slack`, `email`) per `supervisord.conf.example`.

5. **Slack OAuth new-user flow** — New Slack users from an already-connected team cannot
   sign up via OAuth; they must be added via `slack:sync-users` or invited directly.
   Consider improving UX with a clearer error message or automatic workspace joining.

---

## Recommended Future Improvements

- **Task visibility with supervisor scope** — The `scopeVisibleTo` query scope does not
  grant supervisors visibility into direct reports' private tasks. If that is desired
  behaviour, extend the scope.
- **API logout for deactivated accounts** — A deactivated user's Sanctum token persists
  after being blocked; adding token cleanup to `EnsureWorkspace` (JSON path) would be a
  small quality-of-life improvement.
- **SendGrid CC parsing** — The SendGrid payload normaliser does not extract CC addresses
  from the `headers` blob; add CC parsing parity with the Mailgun normaliser.
- **Test coverage for new features** — Add dedicated tests for: deactivated-user blocking
  (web + API), SendGrid signature verification, `TaskTemplate::factory()` usage, and the
  paginated task index endpoint.
