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.
| Collection | Portée | Ce qu'est un enregistrement |
|---|---|---|
cooling | cooling:read | Un produit chaud ramené à la température de conservation au froid. |
freezing | freezing:read | Un produit descendu jusqu'à la congélation. |
reheating | reheating:read | Un produit froid remonté à la température de service. |
transport | transport:read | Un 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.
| Champ | Type | Signification |
|---|---|---|
timestamp | string, obligatoire | L'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. |
product | string, obligatoire | Ce qui a été refroidi, sous forme de texte. |
beginDate | string, obligatoire | Quand le cycle a commencé, en millisecondes depuis l'epoch sous forme de chaîne de caractères. |
beginTemperature | number, obligatoire | La température de départ. |
endDate | string | null | Quand il s'est terminé. null tant qu'il ne l'est pas. |
endTemperature | number | null | La température d'arrivée. null en même temps qu'endDate. |
reminderId | string | null | Le rappel que l'application a attaché au cycle, quand il y en avait un. |
userId | string | null | Le 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.
| Champ | Type | Signification |
|---|---|---|
timestamp | string, obligatoire | L'horodatage propre à l'enregistrement. |
product | string, obligatoire | Ce qui a été congelé. |
beginDate | string, obligatoire | Quand l'opération a commencé. |
beginTemperature | number, obligatoire | La température de départ. |
endDate | string | null | Quand elle s'est terminée, ou null. |
endTemperature | number | null | La température d'arrivée, ou null. |
reminderId | string | null | Comme ci-dessus. |
userId | string | null | Qui 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.
| Champ | Type | Signification |
|---|---|---|
timestamp | string, obligatoire | L'horodatage propre à l'enregistrement. |
product | string, obligatoire | Ce qui a été remis en température. |
beginDate | string, obligatoire | Quand il est entré. |
beginTemperature | number, obligatoire | La température de départ. |
endDate | string | null | Quand il est sorti, ou null. |
endTemperature | number | null | La température atteinte, ou null. |
reminderId | string | null | Comme ci-dessus. |
userId | string | null | Qui 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é.
| Champ | Type | Signification |
|---|---|---|
timestamp | string, obligatoire | L'horodatage propre à l'enregistrement. |
product | string, obligatoire | Ce qui a été transporté, sous forme de texte. |
departureLocation | string, obligatoire | D'où il est parti, sous forme de texte. |
departureTime | string, obligatoire | Quand il est parti, en millisecondes depuis l'epoch sous forme de chaîne de caractères. |
departureTemperature | number, obligatoire | La température au départ. |
arrivalLocation | string | null | Où il est arrivé. null tant qu'il ne l'est pas. |
arrivalTime | string | null | Quand il est arrivé, ou null. |
arrivalTemperature | number | null | La température à l'arrivée, ou null. |
reminderId | string | null | Comme ci-dessus. |
userId | string | null | Qui 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.