# Cleaning

> The cleaning plan, the tasks actually done and the photographs proving it. Three collections, and what a missing record does not prove.

Source: https://developers.backresto.com/en/docs/cleaning

Cleaning in the app is a plan and its proof. The restaurant writes down what
has to be cleaned, where and how often; staff tick the tasks off as they do
them; some of those tasks demand a photograph. Three collections, one per
layer, and the join between them is where most of the work is.

| Collection | Scope | What a record is |
| --- | --- | --- |
| `cleaning-tasks` | `cleaning-tasks:read` | A line of the plan: what must be cleaned, where, how often, whether it needs a photograph. |
| `cleaning-task-records` | `cleaning-task-records:read` | A task actually done: when, and by whom. |
| `cleaning-task-pictures` | `cleaning-task-pictures:read` | The photograph proving it was. Carries a file. |

All three share the [snapshot envelope](https://developers.backresto.com/en/docs/collections): `id`, `deleted`,
`capturedAt`, `receivedAt`, `sequence` and the rest sit beside the `data`
described below.

## `cleaning-tasks`

The plan itself. These change rarely (a restaurant writes its plan once and
edits it when the kitchen changes), so this is the collection you walk least
often and cache longest.

| Field | Type | Meaning |
| --- | --- | --- |
| `index` | integer, **required** | The task's position in the plan, as the app orders it. |
| `name` | string, **required** | What has to be cleaned, in the restaurant's words. |
| `description` | string \| null | Longer instructions, when someone wrote them. |
| `areaId` | string \| null | The [area](https://developers.backresto.com/en/docs/facilities) it belongs to. `null` on a restaurant that does not split itself into areas. |
| `recurrence` | integer, **required** | How often the task comes round, as a plain integer. |
| `requirePhotoProof` | boolean \| null | `true` when the task demands a photograph. `null` means the app never set it. |
| `isDeleted` | boolean, **required** | The app's own soft delete. Read the note below. |

```json
{
  "id": "c9a4f7e2-1b83-4d05-a6f1-90e3b7c4d218",
  "collection": "cleaning-tasks",
  "deleted": false,
  "capturedAt": "1789601204773",
  "receivedAt": "1789601399042",
  "sequence": "1",
  "data": {
    "index": 3,
    "name": "Nettoyage de la trancheuse",
    "description": "Démonter la lame, dégraisser, désinfecter.",
    "areaId": "7d2c0b61-4e8a-49f3-9c27-5a1b8e60d3f4",
    "recurrence": 1,
    "requirePhotoProof": true,
    "isDeleted": false
  }
}
```

**`recurrence` carries no unit.** It is an integer and the record does not say
what it counts. Show it as the app shows it rather than rendering "every 1
days" from a guess.

**`isDeleted` is not the envelope's `deleted`.** A task the restaurant removed
from its plan comes back with `isDeleted: true` and `deleted: false`: it is
still a live record, describing a line that no longer applies. Filter on both,
and keep the deleted ones around. Old records still point at them.

## `cleaning-task-records`

A task done. Small on purpose: which task, when, who.

| Field | Type | Meaning |
| --- | --- | --- |
| `timestamp` | string, **required** | When the task was done, epoch milliseconds as a string. |
| `cleaningTaskId` | string, **required** | The `cleaning-tasks` record it satisfies. |
| `userId` | string \| null | The [staff member](https://developers.backresto.com/en/docs/facilities) who did it, when the app recorded one. |

```json
{
  "id": "5f8b3d16-2c90-4a77-b4e8-31d0a9c65f7b",
  "collection": "cleaning-task-records",
  "deleted": false,
  "capturedAt": "1789775012558",
  "receivedAt": "1789775204910",
  "sequence": "2",
  "data": {
    "timestamp": "1789774800000",
    "cleaningTaskId": "c9a4f7e2-1b83-4d05-a6f1-90e3b7c4d218",
    "userId": "2774953d-8d9b-4a68-8ec4-1209edd90777"
  }
}
```

`cleaningTaskId` is required, so every record has a task. The reverse does not
hold: a task can sit in the plan with no record against it for weeks, and that
is the normal state of a weekly task on a Tuesday.

## `cleaning-task-pictures`

The photograph. It carries a file, which makes it the one collection here you
cannot consume from the records endpoint alone.

| Field | Type | Meaning |
| --- | --- | --- |
| `timestamp` | string, **required** | When the picture was taken, epoch milliseconds as a string. |
| `cleaningTaskRecordId` | string \| null | The `cleaning-task-records` record it proves. Nullable. See below. |
| `part` | number \| null | Undocumented, and a decimal rather than a counter. Carry it through rather than interpreting it, the same caveat as on [traceability labels](https://developers.backresto.com/en/docs/traceability-labels). |
| `groupId` | string \| null | Ties those pictures together. Records sharing a `groupId` belong to the same proof. |
| `asset` | object, **required** | Names the file: `objectKey` and `status` always, plus `contentType`, `byteLength` and `sha256` once it is uploaded. |

```json
{
  "id": "2d71c8a9-6e34-4b0f-8517-ac93e25d0b46",
  "collection": "cleaning-task-pictures",
  "deleted": false,
  "capturedAt": "1789775118330",
  "receivedAt": "1789775301774",
  "sequence": "3",
  "data": {
    "timestamp": "1789774860000",
    "cleaningTaskRecordId": "5f8b3d16-2c90-4a77-b4e8-31d0a9c65f7b",
    "part": 1,
    "groupId": "a03e5c88-71bd-4f92-b6d4-2e8f10c73a5b",
    "asset": {
      "objectKey": "cleaning-task-pictures/2d71c8a9/1.jpg",
      "status": "uploaded",
      "contentType": "image/jpeg",
      "byteLength": 812043,
      "sha256": "9f2a…"
    }
  }
}
```

The bytes are a second call, against the record's own id:

```http
GET /v1/restaurants/{restaurantId}/collections/cleaning-task-pictures/records/{recordId}/assets
```

It answers with a list of signed URLs, each valid for **fifteen minutes**, each
carrying its own authorisation so the download needs no header. Fetch the bytes
now, store the bytes rather than the link, and call the endpoint again when you
need a fresh URL. The rules (what appears, what `sha256` is for, why
`contentType` is worth reading) are on
[the collections page](https://developers.backresto.com/en/docs/collections), and they are the same here.

**`asset.status` tells you whether there are bytes to fetch.** It is `pending`
or `uploaded`. A `pending` asset is a photograph the device has announced and
not yet finished sending: the record is real, the file is not, and the assets
endpoint answers without it. Read the status before you treat an empty asset
list as a photograph that was never taken.

**`cleaningTaskRecordId` is nullable.** A picture can exist without pointing at
a record, so a join that assumes it is always there drops photographs on the
floor. Count them separately rather than discarding them silently.

## Joining the three

The shape you almost certainly want is one row per task done, with its plan
entry and its photographs:

- `cleaning-task-records.cleaningTaskId` → `cleaning-tasks.id`
- `cleaning-task-pictures.cleaningTaskRecordId` → `cleaning-task-records.id`

Three walks, joined on your side. There is no endpoint that does it for you,
and there is no filter by task or by date. You walk each collection whole and
reconcile, as [the envelope page](https://developers.backresto.com/en/docs/collections) describes.

Report on `capturedAt` and sync on `receivedAt`: a tablet in a cold room
uploads when it finds signal, and a picture taken at 09:00 can arrive at
14:00, after the record it belongs to, or before it.

## What a missing record does not mean

**A plan entry with no matching record is not proof the task was skipped.** It
may have been done on a device that has not uploaded yet, or done before
capture was switched on for that restaurant. The same goes for a task marked
`requirePhotoProof: true` with no picture against it: the photograph may be
mid-upload, and an asset that has not finished uploading is absent rather than
broken.

So do not print a cleaning completion rate from this API and call it
compliance. What you can say honestly is what arrived, and when. See
[the completeness note](https://developers.backresto.com/en/docs/support) before you put a number in front of
anyone.
