Refroidissement et procédés alimentaires

Refroidissement, congélation, remise en température et transport — quatre collections qui enregistrent une transition de température, et à quoi ressemble une transition inachevée.

Un plat chaud qui refroidit lentement passe des heures dans la plage où les bactéries se multiplient : la règle est de le descendre vite et de pouvoir le prouver. Quatre collections enregistrent ce genre de transition. Trois d'entre elles sont une transition dans le temps — descendre, descendre encore, remonter — et la quatrième est la même idée dans l'espace.

CollectionPortéeCe qu'est un enregistrement
coolingcooling:readUn produit chaud ramené à la température de conservation au froid.
freezingfreezing:readUn produit descendu jusqu'à la congélation.
reheatingreheating:readUn produit froid remonté à la température de service.
transporttransport:readUn produit déplacé d'un lieu à un autre, avec la température à chaque extrémité.

Toutes les quatre partagent l'enveloppe d'instantané : id, deleted, capturedAt, receivedAt, sequence et le reste entourent le data décrit ci-dessous.

cooling

Un cycle : un produit, quand il a commencé et à quelle température, quand il s'est terminé et à quelle température.

ChampTypeSignification
timestampstring, obligatoireL'horodatage propre à l'enregistrement, en millisecondes depuis l'epoch sous forme de chaîne de caractères. Pas l'horloge du cycle — ce sont les deux dates ci-dessous.
productstring, obligatoireCe qui a été refroidi, sous forme de texte.
beginDatestring, obligatoireQuand le cycle a commencé, en millisecondes depuis l'epoch sous forme de chaîne de caractères.
beginTemperaturenumber, obligatoireLa température de départ.
endDatestring | nullQuand il s'est terminé. null tant qu'il ne l'est pas.
endTemperaturenumber | nullLa température d'arrivée. null en même temps qu'endDate.
reminderIdstring | nullLe rappel que l'application a attaché au cycle, quand il y en avait un.
userIdstring | nullLe membre du personnel qui l'a enregistré, quand l'application en a enregistré un.
{
  "id": "0e5d1a7c-9f24-4b13-8d60-c1a7f0b25e94",
  "collection": "cooling",
  "deleted": false,
  "capturedAt": "1789779612441",
  "receivedAt": "1789779910228",
  "sequence": "1",
  "data": {
    "timestamp": "1789772400000",
    "product": "Blanquette de veau",
    "beginDate": "1789772400000",
    "beginTemperature": 63.5,
    "endDate": "1789779600000",
    "endTemperature": 8.2,
    "userId": "2774953d-8d9b-4a68-8ec4-1209edd90777"
  }
}

product est un nom, pas une référence. Il n'y a pas de productId sur ces enregistrements : vous ne pouvez pas les joindre à products sans comparer des chaînes de caractères — et ces chaînes sont saisies pendant le service, dans la langue du restaurant.

reminderId est opaque. Aucune collection de cette API ne le résout. Transportez-le s'il vous aide à grouper des enregistrements ; n'espérez pas pouvoir le consulter.

freezing

La même forme, pour un produit descendu jusqu'à la congélation plutôt qu'à la température du réfrigérateur. Champ pour champ, c'est cooling.

ChampTypeSignification
timestampstring, obligatoireL'horodatage propre à l'enregistrement.
productstring, obligatoireCe qui a été congelé.
beginDatestring, obligatoireQuand l'opération a commencé.
beginTemperaturenumber, obligatoireLa température de départ.
endDatestring | nullQuand elle s'est terminée, ou null.
endTemperaturenumber | nullLa température d'arrivée, ou null.
reminderIdstring | nullComme ci-dessus.
userIdstring | nullQui l'a enregistrée.

Rien dans l'enregistrement ne dit laquelle des trois c'est : c'est la collection d'où il vient, et le champ collection de l'enveloppe la reprend. Un pipeline qui fusionne les trois dans une seule table doit conserver cette colonne.

reheating

Un produit froid remonté. Encore la même forme, lue dans l'autre sens : beginTemperature est la basse et endTemperature la haute.

ChampTypeSignification
timestampstring, obligatoireL'horodatage propre à l'enregistrement.
productstring, obligatoireCe qui a été remis en température.
beginDatestring, obligatoireQuand il est entré.
beginTemperaturenumber, obligatoireLa température de départ.
endDatestring | nullQuand il est sorti, ou null.
endTemperaturenumber | nullLa température atteinte, ou null.
reminderIdstring | nullComme ci-dessus.
userIdstring | nullQui l'a enregistré.

Une opération sans fin ressemble à ceci :

{
  "id": "b4c8e21d-3a76-4f58-9c02-77ad5e1b8f30",
  "collection": "reheating",
  "deleted": false,
  "capturedAt": "1789783221907",
  "receivedAt": "1789783402115",
  "sequence": "2",
  "data": {
    "timestamp": "1789783200000",
    "product": "Gratin dauphinois",
    "beginDate": "1789783200000",
    "beginTemperature": 4.1,
    "endDate": null,
    "endTemperature": null,
    "userId": "2774953d-8d9b-4a68-8ec4-1209edd90777"
  }
}

transport

La même transition, dans l'espace plutôt que dans le temps : d'où le produit est parti et où il est arrivé, avec une température à chaque extrémité.

ChampTypeSignification
timestampstring, obligatoireL'horodatage propre à l'enregistrement.
productstring, obligatoireCe qui a été transporté, sous forme de texte.
departureLocationstring, obligatoireD'où il est parti, sous forme de texte.
departureTimestring, obligatoireQuand il est parti, en millisecondes depuis l'epoch sous forme de chaîne de caractères.
departureTemperaturenumber, obligatoireLa température au départ.
arrivalLocationstring | nullOù il est arrivé. null tant qu'il ne l'est pas.
arrivalTimestring | nullQuand il est arrivé, ou null.
arrivalTemperaturenumber | nullLa température à l'arrivée, ou null.
reminderIdstring | nullComme ci-dessus.
userIdstring | nullQui l'a enregistré.
{
  "id": "f31b6a08-5c47-4de9-b1a5-6e0c9d247b82",
  "collection": "transport",
  "deleted": false,
  "capturedAt": "1789786944610",
  "receivedAt": "1789787101883",
  "sequence": "3",
  "data": {
    "timestamp": "1789785000000",
    "product": "Plats préparés — service traiteur",
    "departureLocation": "Cuisine centrale",
    "departureTime": "1789785000000",
    "departureTemperature": 3.4,
    "arrivalLocation": "Salle Montparnasse",
    "arrivalTime": "1789786800000",
    "arrivalTemperature": 5.9,
    "userId": "2774953d-8d9b-4a68-8ec4-1209edd90777"
  }
}

Les deux lieux sont du texte libre, ni des zones ni des adresses. C'est ce que la personne a saisi, et le côté arrivée est exactement aussi facultatif que la température d'arrivée : un enregistrement peut ne porter qu'un départ et rien d'autre.

Travailler avec ces enregistrements

Une fin à null, c'est deux choses différentes. endDate et endTemperature — ainsi qu'arrivalTime, arrivalTemperature et arrivalLocation sur transport — acceptent null, et l'enregistrement ne vous dit pas pourquoi. Ce peut être une opération encore en cours au moment où vous la lisez, ou une opération que personne n'a jamais close. Les deux sont identiques.

Cela compte dès l'instant où vous calculez une durée. Les dates sont des chaînes de caractères : endDate - beginDate se soustrait très bien sur un enregistrement clos et, sur un enregistrement ouvert, convertit null en zéro et vous rend un grand nombre négatif plutôt qu'une erreur. Rien ne lève d'exception ; votre moyenne est simplement fausse. Filtrez d'abord sur les enregistrements clos, comptez les ouverts à part, et décidez vous-même à partir de quel âge un enregistrement ouvert cesse d'être en cours pour devenir abandonné — rien dans cette API ne tranche à votre place.

Calculez sur beginDate et endDate, pas sur timestamp. Les dates sont l'horloge de l'opération. timestamp est celle de l'enregistrement, et ce n'est pas avec elle qu'on fabrique une durée.

Ordonnez sur capturedAt, pas sur receivedAt. Une tablette dans une chambre froide téléverse quand elle trouve du réseau : un cycle commencé à 09:00 peut arriver à 14:00. La page de l'enveloppe raconte toute l'histoire.

L'absence ne prouve rien. Un produit sans enregistrement de refroidissement peut n'avoir jamais été refroidi, ou dormir sur un appareil qui n'a pas encore téléversé. Ne transformez pas un décompte en chiffre de conformité — voyez la mise en garde.

Dernière mise à jour 2026-09-20.