# Signal Trade preparation records in Node.js

Use Node.js 20 or later with native `fetch`. Keep the API key on your server.
`prep:read` reads stock preparation jobs; `prep:manage` also creates and updates
them. These scopes are separate from stock, appraisal and CRM scopes.

The supplier directory is managed in the Signal Trade portal. There is no V1
supplier directory endpoint. If you already have a same-business active supplier
ID, pass `supplierId`; otherwise omit it and optionally record a manual `supplier`
label. A linked job retains the directory name recorded at assignment.

```js
const baseUrl = 'https://signalautomotive.uk';
const key = process.env.SIGNAL_TRADE_PREPARATION_KEY;

async function preparationRequest(path, method = 'GET', body) {
  const response = await fetch(new URL(path, baseUrl), {
    method,
    headers: {
      Authorization: `Bearer ${key}`,
      ...(body === undefined ? {} : { 'Content-Type': 'application/json' }),
    },
    ...(body === undefined ? {} : { body: JSON.stringify(body) }),
  });
  const result = await response.json();
  if (!response.ok) {
    const error = new Error(result.error || `Preparation request failed (${response.status})`);
    error.status = response.status;
    error.code = result.code;
    error.current = result.current;
    throw error;
  }
  return result;
}

// Save this exact body durably before sending it. Reuse it on an uncertain
// response; do not generate another createId merely because the response failed.
const createBody = {
  createId: crypto.randomUUID(),
  vehicleId: 'STOCK_VEHICLE_ID',
  category: 'mechanical',
  title: 'Investigate coolant leak',
  severity: 'urgent',
  estimatePence: 18500,
  blocksRetail: true,
};
const created = await preparationRequest('/api/v1/preparation', 'POST', createBody);

// Load the current version before an edit. Preserve this exact body for retries.
const detail = await preparationRequest(`/api/v1/preparation/${created.job.id}`);
const updateBody = {
  operationId: crypto.randomUUID(),
  expectedUpdatedAt: detail.job.updatedAt,
  status: 'complete',
  actualPence: 18500,
};
const completed = await preparationRequest(`/api/v1/preparation/${detail.job.id}`, 'PATCH', updateBody);

// A separate new job preserves the completed source and its cost history.
const reworkBody = {
  action: 'rework',
  createId: crypto.randomUUID(),
  reworkOfJobId: completed.job.id,
  expectedUpdatedAt: completed.job.updatedAt,
  reworkReason: 'Fault returned after the recorded repair',
};
const rework = await preparationRequest('/api/v1/preparation', 'POST', reworkBody);
console.log(rework.job.id, rework.job.actualPence); // New actual is null.
```

Amounts are whole pence, bounded from 0 to 100,000,000. `null` is unknown;
`0` is an explicitly recorded zero. A new rework job starts open with an unknown
actual cost and no copied estimate or supplier assignment unless explicitly
provided. It retains the source severity and use/retail restrictions. Completing
a job does not release a vehicle safety hold or certify roadworthiness.

Retry the exact request after a transport failure or HTTP 503. HTTP 409 requires
reconciliation: `JOB_CHANGED` or `SOURCE_CHANGED` identifies a changed record;
`SUPPLIER_CHANGED` or `SUPPLIER_INACTIVE` identifies a changed assignment; an
operation ID reused with different details returns `OPERATION_REUSED`. Once a
successful request has been reconciled, use a new operation ID for a new edit.
An exact replay returns the current job with `replayed: true` and the immutable
original `operation` receipt, so its `resultUpdatedAt` may predate the current job.

Job detail returns `job`, `events` and `timing`. The events record factual lifecycle
transitions and supplier-name snapshots. Timing uses recorded steps only; legacy
dates cannot establish an unknown start. Elapsed start-to-completion time includes
waiting and is not measured labour. `prep:read` redacts costs, private notes,
rework reasons and event reasons. Treat `financeRedacted: true` as an access
restriction, not as evidence of a zero cost.

For synchronization, first capture `pollStartedAt` from
`GET /api/v1/preparation/events?limit=1`, finish every job-list page, then drain
events from that checkpoint. Keep `updated_since` fixed through event cursor
pages and advance it only after all changed jobs are saved. The compact event
feed and the per-job lifecycle timeline serve different purposes. See the
[public API guide](https://signalautomotive.uk/motor-trade/api) and
[OpenAPI contract](https://signalautomotive.uk/openapi/signal-trade-v1.yaml).
