Four phases shipped together. Each is a logical deploy unit on its own;
keeping the diff atomic so the rename runbook + migrations stay aligned.
Phase 1 — restore HP's formal review workflow
- Prisma: FeedbackItem, ReviewSession, ReviewSessionItem + enums
- New ApprovalType (NONE | SIMPLE | FORMAL) on PipelineStageDefinition
and PipelineStageTemplate. Stage row UI branches per type.
- feedback-service + review-session-service ported from HP (no ColorProbe)
- annotation-service auto-creates a FeedbackItem; revision-service
carries forward unresolved action items into the new revision.
- API: /api/reviews/*, /api/stages/[id]/feedback, /api/feedback/[id]
- Hooks: use-feedback, use-review-sessions
- UI: feedback-checklist, feedback-item-card, feedback-progress-bar,
create-session-dialog, session-builder, session-presenter,
session-summary, plus a new stage-review-panel
- Pages: /reviews list + detail, deliverable annotation review page
- Pipeline editor gets the approvalType select; sidebar gets Reviews
Phase 2 — full Dow Jones → L'Oréal rebrand + slug rename
- URL slug /dow-prod-tracker → /loreal-prod-tracker (next.config,
base path, redirects)
- docker-compose name + DB → loreal_prod_tracker; server path
/opt/loreal-prod-tracker; apache template renamed
- All visible strings → L'Oréal; sidebar bg #002B5C → black
- docs/RENAME_RUNBOOK.md describes the one-shot server migration
- Internal modules dow-excel-service/dow-import + OMG webhook domain
dowjones.com deliberately preserved (orthogonal to the rebrand)
Phase 3 — external /api/v1 for projects + deliverables
- API-key auth already in middleware; finished idempotency support
via new IdempotencyRecord model + src/lib/api/idempotency.ts
- Default-pipeline fallback in createProject when no template id given
- POST/GET /api/v1/projects + POST /api/v1/projects/[id]/deliverables
- docs/EXTERNAL_API.md with curl examples
Phase 4 — Box bidirectional integration
- JWT app-auth via jose (no extra deps). Config mounted as a docker
compose secret; deploy.sh stubs an empty {} so compose can start
before the operator drops the real JSON.
- Outbound: pushDeliverableToBox auto-fires on !APPROVED → APPROVED
in deliverable-status-service; "Send to client (Box)" manual button
on the approval stage row. Folder naming
{omgJobNumber}_{slug}_v{round}. 3-attempt exp backoff. BoxPushLog
audit.
- Inbound: /api/webhooks/box receives Box's signed events, matches by
OMG # + slug, creates a new Revision, routes to assignee or notifies
project owner. BoxInboundLog audit + two new NotificationType
values (BOX_UNMATCHED_FILE, NEW_FILE_AWAITING_REVIEWER).
- Naming-convention logic isolated in external-delivery-service so an
OMG-API transport can swap in later without touching matchers.
- Admin /settings/box page surfaces config status + recent activity.
Three Prisma migrations to apply on next deploy:
20260512000000_restore_review_workflow
20260512100000_idempotency_records
20260512200000_box_integration
URL rename is a one-shot — see docs/RENAME_RUNBOOK.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
145 lines
4.4 KiB
Markdown
145 lines
4.4 KiB
Markdown
# L'Oréal Studio Tracker — External API (v1)
|
|
|
|
Server-to-server API for creating projects and deliverables from external
|
|
systems (e.g. OMG, brief intake forms, integration scripts).
|
|
|
|
Base URL: `https://optical-dev.oliver.solutions/loreal-prod-tracker/api/v1`
|
|
|
|
## Auth
|
|
|
|
Send the shared API key on every request:
|
|
|
|
```
|
|
x-api-key: <API_KEY>
|
|
```
|
|
|
|
The key matches `process.env.API_KEY` on the server. Requests with a
|
|
missing or wrong key get `401 Unauthorized`.
|
|
|
|
Optionally specify which organization the request applies to:
|
|
|
|
```
|
|
x-org-id: <organization-id>
|
|
```
|
|
|
|
If omitted, requests apply to the first organization in the database
|
|
(useful in single-org deployments).
|
|
|
|
## Idempotency
|
|
|
|
POST endpoints honour `Idempotency-Key`. A retried request with the same
|
|
key + same payload returns the cached response without re-executing:
|
|
|
|
```
|
|
idempotency-key: 7f3c8e2a-...-deterministic-uuid
|
|
```
|
|
|
|
The same key with a *different* payload returns `409 Conflict`. Records
|
|
expire after 24 hours.
|
|
|
|
## Endpoints
|
|
|
|
### `GET /api/v1/projects`
|
|
|
|
List all projects visible to the caller. Returns the same shape as the
|
|
in-app `/api/projects`.
|
|
|
|
### `POST /api/v1/projects`
|
|
|
|
Create a project. If `pipelineTemplateId` is omitted, the org's default
|
|
pipeline template is auto-applied. Body matches `createProjectSchema`
|
|
(see `src/lib/validators/project.ts`).
|
|
|
|
Required fields:
|
|
- `projectCode` (string, unique per org)
|
|
- `name` (string)
|
|
|
|
Recommended fields:
|
|
- `omgJobNumber` (string) — enables Box-folder matching downstream
|
|
- `clientTeamId` (string) — drives team visibility
|
|
- `requestorUserId` (string) — project owner
|
|
- `priority` (`LOW` | `MEDIUM` | `HIGH` | `URGENT`)
|
|
- `status` (`PIPELINE` | `ACTIVE` | …)
|
|
- `startDate`, `dueDate` (ISO 8601)
|
|
|
|
Returns `201` + the created project.
|
|
|
|
```bash
|
|
curl -X POST \
|
|
-H "x-api-key: $API_KEY" \
|
|
-H "idempotency-key: $(uuidgen)" \
|
|
-H "content-type: application/json" \
|
|
-d '{
|
|
"projectCode": "API-001",
|
|
"name": "Test integration project",
|
|
"omgJobNumber": "12345",
|
|
"priority": "MEDIUM"
|
|
}' \
|
|
https://optical-dev.oliver.solutions/loreal-prod-tracker/api/v1/projects
|
|
```
|
|
|
|
### `GET /api/v1/projects/{projectId}`
|
|
|
|
Read-back. Same shape as in-app `/api/projects/{id}` — includes
|
|
deliverables, stages, attachments, requestor user, client team.
|
|
|
|
### `POST /api/v1/projects/{projectId}/deliverables`
|
|
|
|
Create a deliverable on the given project. Pipeline stages auto-applied
|
|
from the project's template (`Deliverable.pipelineTemplate` cascades to
|
|
`DeliverableStage` rows). Body matches `createDeliverableSchema`.
|
|
|
|
Required fields:
|
|
- `name` (string)
|
|
|
|
Common fields:
|
|
- `priority`, `dueDate`, `notes`, `externalId` (idempotency for upstream
|
|
webhook callers — distinct from the `Idempotency-Key` header)
|
|
|
|
Returns `201` + the created deliverable with its stage chain populated.
|
|
|
|
```bash
|
|
curl -X POST \
|
|
-H "x-api-key: $API_KEY" \
|
|
-H "idempotency-key: $(uuidgen)" \
|
|
-H "content-type: application/json" \
|
|
-d '{
|
|
"name": "Hero banner",
|
|
"priority": "HIGH",
|
|
"dueDate": "2026-06-30T12:00:00Z"
|
|
}' \
|
|
https://optical-dev.oliver.solutions/loreal-prod-tracker/api/v1/projects/$PROJECT_ID/deliverables
|
|
```
|
|
|
|
## Error shapes
|
|
|
|
```json
|
|
{ "error": "Project code \"X\" already exists" }
|
|
```
|
|
|
|
All errors return JSON with an `error` string. Status codes:
|
|
- `400` — validation error (zod issues joined into one message)
|
|
- `401` — missing/wrong API key
|
|
- `404` — project not visible (RBAC) or not found
|
|
- `409` — Idempotency-Key reused with a different payload
|
|
- `500` — server error (check server logs)
|
|
|
|
## What's NOT here
|
|
|
|
- Update/delete endpoints — out of scope for v1. External callers create;
|
|
in-app users update.
|
|
- Deliverable read-back — `GET /api/v1/projects/{id}` includes
|
|
deliverables in its response. A dedicated deliverable read endpoint can
|
|
be added if external callers need it.
|
|
- Bulk endpoints — call POST per project/deliverable.
|
|
|
|
## Internal notes
|
|
|
|
- Auth path: `src/middleware.ts` short-circuits API-key requests before
|
|
the session check. `src/lib/api-utils.ts` `getAuthSession()` resolves
|
|
the key into a synthetic ADMIN session for the chosen org.
|
|
- Idempotency: `src/lib/api/idempotency.ts` + `IdempotencyRecord` prisma
|
|
model. 24-hour TTL — sweep via cron (see `scripts/`).
|
|
- Default pipeline fallback: `src/lib/services/project-service.ts`
|
|
`createProject()` — looks up the org's `PipelineTemplate.isDefault` if
|
|
no `pipelineTemplateId` is supplied.
|