efarmz-slackbot-data 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/.clever.json +12 -0
  2. package/.dockerignore +13 -0
  3. package/.env.example +28 -0
  4. package/.github/workflows/deploy-production.yaml +34 -0
  5. package/.prettierrc +6 -0
  6. package/.tasks/F1-bootstrap.md +110 -0
  7. package/.tasks/F2-domain-layer.md +173 -0
  8. package/.tasks/F3-application-layer.md +166 -0
  9. package/.tasks/F4-infrastructure-layer.md +229 -0
  10. package/.tasks/F5-config-main.md +160 -0
  11. package/.tasks/F6-schemas-deployment.md +129 -0
  12. package/CLAUDE.md +163 -0
  13. package/Dockerfile +15 -0
  14. package/PRD.md +119 -0
  15. package/docs/schemas/.gitkeep +0 -0
  16. package/docs/schemas/_guidelines.md +89 -0
  17. package/docs/schemas/efarmz_db.md +759 -0
  18. package/docs/schemas/example.md +16 -0
  19. package/eslint.config.mjs +18 -0
  20. package/package.json +54 -0
  21. package/releaserc.json +15 -0
  22. package/src/.gitkeep +0 -0
  23. package/src/application/agent/.gitkeep +0 -0
  24. package/src/application/agent/AgentContext.test.ts +263 -0
  25. package/src/application/agent/AgentContext.ts +93 -0
  26. package/src/application/agent/AgentLoop.test.ts +275 -0
  27. package/src/application/agent/AgentLoop.ts +101 -0
  28. package/src/application/agent/AgentRunResult.ts +11 -0
  29. package/src/application/agent/LLMMessage.ts +16 -0
  30. package/src/application/agent/tools/RunSqlTool.ts +23 -0
  31. package/src/application/formatting/.gitkeep +0 -0
  32. package/src/application/formatting/CsvRenderer.test.ts +162 -0
  33. package/src/application/formatting/CsvRenderer.ts +34 -0
  34. package/src/application/formatting/MonospaceTableRenderer.test.ts +129 -0
  35. package/src/application/formatting/MonospaceTableRenderer.ts +58 -0
  36. package/src/application/formatting/RenderedResponse.ts +7 -0
  37. package/src/application/formatting/ResponseRenderer.test.ts +159 -0
  38. package/src/application/formatting/ResponseRenderer.ts +39 -0
  39. package/src/application/formatting/ScalarRenderer.test.ts +36 -0
  40. package/src/application/formatting/ScalarRenderer.ts +12 -0
  41. package/src/application/usecases/.gitkeep +0 -0
  42. package/src/application/usecases/AnswerQuestion.test.ts +362 -0
  43. package/src/application/usecases/AnswerQuestion.ts +69 -0
  44. package/src/application/usecases/ParseQuestion.test.ts +39 -0
  45. package/src/application/usecases/ParseQuestion.ts +9 -0
  46. package/src/config/.gitkeep +0 -0
  47. package/src/config/Container.test.ts +35 -0
  48. package/src/config/Container.ts +74 -0
  49. package/src/config/constants.ts +9 -0
  50. package/src/config/env.test.ts +103 -0
  51. package/src/config/env.ts +41 -0
  52. package/src/domain/entities/.gitkeep +0 -0
  53. package/src/domain/entities/Conversation.test.ts +69 -0
  54. package/src/domain/entities/Conversation.ts +26 -0
  55. package/src/domain/entities/ConversationMessage.test.ts +49 -0
  56. package/src/domain/entities/ConversationMessage.ts +18 -0
  57. package/src/domain/entities/index.ts +2 -0
  58. package/src/domain/errors/.gitkeep +0 -0
  59. package/src/domain/errors/AgentLoopExceededError.ts +12 -0
  60. package/src/domain/errors/DomainError.test.ts +106 -0
  61. package/src/domain/errors/DomainError.ts +11 -0
  62. package/src/domain/errors/InvalidSqlError.ts +15 -0
  63. package/src/domain/errors/LLMError.ts +15 -0
  64. package/src/domain/errors/SchemaLoadError.ts +15 -0
  65. package/src/domain/errors/SqlExecutionError.ts +15 -0
  66. package/src/domain/errors/index.ts +15 -0
  67. package/src/domain/ports/.gitkeep +0 -0
  68. package/src/domain/ports/AdminLogger.ts +16 -0
  69. package/src/domain/ports/ConversationRepository.ts +10 -0
  70. package/src/domain/ports/LLMProvider.ts +33 -0
  71. package/src/domain/ports/Logger.ts +8 -0
  72. package/src/domain/ports/SchemaCatalog.ts +5 -0
  73. package/src/domain/ports/SlackMessenger.ts +8 -0
  74. package/src/domain/ports/SqlExecutor.ts +8 -0
  75. package/src/domain/ports/SqlValidator.ts +5 -0
  76. package/src/domain/ports/index.ts +17 -0
  77. package/src/domain/value-objects/.gitkeep +0 -0
  78. package/src/domain/value-objects/LLMProviderName.ts +6 -0
  79. package/src/domain/value-objects/QueryResult.test.ts +51 -0
  80. package/src/domain/value-objects/QueryResult.ts +18 -0
  81. package/src/domain/value-objects/Question.test.ts +59 -0
  82. package/src/domain/value-objects/Question.ts +22 -0
  83. package/src/domain/value-objects/QuestionFlags.test.ts +59 -0
  84. package/src/domain/value-objects/QuestionFlags.ts +18 -0
  85. package/src/domain/value-objects/ResponseRendering.ts +7 -0
  86. package/src/domain/value-objects/SqlQuery.test.ts +40 -0
  87. package/src/domain/value-objects/SqlQuery.ts +12 -0
  88. package/src/domain/value-objects/ThreadId.test.ts +68 -0
  89. package/src/domain/value-objects/ThreadId.ts +27 -0
  90. package/src/domain/value-objects/index.ts +13 -0
  91. package/src/infrastructure/llm/.gitkeep +0 -0
  92. package/src/infrastructure/llm/AnthropicLLMProvider.test.ts +229 -0
  93. package/src/infrastructure/llm/AnthropicLLMProvider.ts +45 -0
  94. package/src/infrastructure/llm/index.ts +4 -0
  95. package/src/infrastructure/llm/mappers/AnthropicMessageMapper.test.ts +173 -0
  96. package/src/infrastructure/llm/mappers/AnthropicMessageMapper.ts +34 -0
  97. package/src/infrastructure/llm/prompts/SystemPromptBuilder.test.ts +41 -0
  98. package/src/infrastructure/llm/prompts/SystemPromptBuilder.ts +31 -0
  99. package/src/infrastructure/llm/prompts/ToolDefinitions.ts +7 -0
  100. package/src/infrastructure/logging/.gitkeep +0 -0
  101. package/src/infrastructure/logging/PinoLogger.test.ts +59 -0
  102. package/src/infrastructure/logging/PinoLogger.ts +28 -0
  103. package/src/infrastructure/logging/index.ts +1 -0
  104. package/src/infrastructure/persistence/.gitkeep +0 -0
  105. package/src/infrastructure/persistence/InMemoryConversationRepository.test.ts +325 -0
  106. package/src/infrastructure/persistence/InMemoryConversationRepository.ts +69 -0
  107. package/src/infrastructure/persistence/PostgresPoolFactory.ts +11 -0
  108. package/src/infrastructure/persistence/PostgresSqlExecutor.test.ts +130 -0
  109. package/src/infrastructure/persistence/PostgresSqlExecutor.ts +34 -0
  110. package/src/infrastructure/persistence/index.ts +3 -0
  111. package/src/infrastructure/schemas/.gitkeep +0 -0
  112. package/src/infrastructure/schemas/FileSystemSchemaCatalog.test.ts +163 -0
  113. package/src/infrastructure/schemas/FileSystemSchemaCatalog.ts +35 -0
  114. package/src/infrastructure/schemas/index.ts +4 -0
  115. package/src/infrastructure/slack/.gitkeep +0 -0
  116. package/src/infrastructure/slack/BoltSlackMessenger.test.ts +59 -0
  117. package/src/infrastructure/slack/BoltSlackMessenger.ts +36 -0
  118. package/src/infrastructure/slack/SlackAdminLogger.test.ts +54 -0
  119. package/src/infrastructure/slack/SlackAdminLogger.ts +27 -0
  120. package/src/infrastructure/slack/SlackApp.ts +9 -0
  121. package/src/infrastructure/slack/handlers/AppMentionHandler.ts +52 -0
  122. package/src/infrastructure/slack/handlers/DirectMessageHandler.ts +65 -0
  123. package/src/infrastructure/slack/index.ts +5 -0
  124. package/src/infrastructure/sql/.gitkeep +0 -0
  125. package/src/infrastructure/sql/RegexSqlValidator.test.ts +242 -0
  126. package/src/infrastructure/sql/RegexSqlValidator.ts +53 -0
  127. package/src/infrastructure/sql/index.ts +1 -0
  128. package/src/main.ts +19 -0
  129. package/tsconfig.json +23 -0
  130. package/vitest.config.ts +15 -0
  131. package/vitest.setup.ts +23 -0
@@ -0,0 +1,759 @@
1
+ ---
2
+ schema: efarmz_db
3
+ sgbd: postgresql
4
+ last_updated: 2026-05-15
5
+ ---
6
+
7
+ > Schéma principal : `efarmz_db`
8
+
9
+ ---
10
+
11
+ ## Conventions globales
12
+
13
+ **Gotchas PostgreSQL** — appliquables à toutes les requêtes sur ce schéma.
14
+
15
+ - ⚠️ `ROUND(double precision, int)` n'existe pas → toujours caster en `::numeric` avant `ROUND(..., 2)`
16
+ - ⚠️ `NULLIF` obligatoire sur tous les dénominateurs (division par zéro sinon)
17
+ - ⚠️ `artid` est `char(n)` (longueur fixe) → les valeurs sont paddées avec des espaces. Pour les patterns qui ciblent la fin de la chaîne (`LIKE '%P'`, regex `$`, `SUBSTRING ... P$`), toujours utiliser `RTRIM(artid)` d'abord.
18
+ - Tie-breaker `w_orderid ASC` dans `ROW_NUMBER() ... ORDER BY dlvdate` (pour les cas où deux commandes même client même jour)
19
+ - Factoriser en CTE si une sous-requête est réutilisée ≥ 2×
20
+ - Casts fréquents : `::date`, `::timestamp`, `::numeric`, `date_trunc('week', ...)`
21
+
22
+ ---
23
+
24
+ ## Tables
25
+
26
+ ### Commandes
27
+
28
+ #### `eorderh` — En-têtes de commandes (historique)
29
+
30
+ | Colonne | Type | Rôle | Description |
31
+ |---|---|---|---|
32
+ | `w_orderid` | `varchar` | PK | ID commande |
33
+ | `c_uuid` | `varchar` | FK → `users.id` | Identifiant client |
34
+ | `dlvdate` | `timestamp` | — | Date de livraison (caster en `::date`) |
35
+ | `status` | `int` | — | Statut de la commande (voir Règles métier / Filtres) |
36
+ | `ordertype` | `int` | — | `1` = one-shot, `2` = abonnement |
37
+ | `s_credate` | `timestamp` | — | Date de création |
38
+ | `totalpaid` | `numeric` | — | Montant total payé |
39
+
40
+ #### `eorderd` — Lignes de commandes (détail picking)
41
+
42
+ | Colonne | Type | Rôle | Description |
43
+ |---|---|---|---|
44
+ | `w_orderid` | `varchar` | FK → `eorderh.w_orderid` | ID commande |
45
+ | `artid` | `char(n)` | FK → `art.artid` | ID produit (alias `product_id` selon contexte) |
46
+ | `qty` | `numeric` | — | Quantité commandée |
47
+ | `unitprice` | `numeric` | — | Prix unitaire de la ligne |
48
+ | `vatpc` | `numeric` | — | Taux de TVA appliqué à la ligne (%) |
49
+ | `boxid` | `varchar` | — | ID box (NULL si pas de box) |
50
+
51
+ #### `lgrefund` — Avoirs clients
52
+
53
+ | Colonne | Type | Rôle | Description |
54
+ |---|---|---|---|
55
+ | `pkid` | `serial` | PK | ID avoir |
56
+ | `artid` | `char(20)` | FK → `art.artid` | Alias `product_id` — produit concerné |
57
+ | `amount` | `double precision` | — | Montant de l'avoir |
58
+ | `date` | `timestamp` | — | Date de l'avoir |
59
+ | `id_product` | `int` | FK → `art` | Produit concerné |
60
+ | `id_cust` | `int` | FK → `users.id` | Client concerné |
61
+ | `id_order` | `int` | — | Commande concernée |
62
+ | `comment` | `text` | — | Commentaire |
63
+ | `refundtype` | `int` | — | `1` = caution, `2` = manquant, `3` = plainte, `9` = commun |
64
+ | `id_order_applied` | `int` | — | Numéro de commande si avoir appliqué |
65
+ | `status` | `int` | — | Statut de l'avoir |
66
+ | `issend` | `int` | — | Envoi effectué |
67
+ | `s_credate` | `timestamp` | — | Date de création |
68
+ | `s_moddate` | `timestamp` | — | Date de modification |
69
+
70
+ ---
71
+
72
+ ### Catalogue
73
+
74
+ #### `art` — Catalogue produits
75
+
76
+ | Colonne | Type | Rôle | Description |
77
+ |---|---|---|---|
78
+ | `artid` | `char(n)` | PK | ID produit |
79
+ | `name1` | `varchar` | — | Nom du produit |
80
+ | `finalprice` | `numeric` | — | Prix non-abonnés |
81
+ | `catalogue1` | `numeric` | — | Prix abonnés |
82
+ | `unit` | `varchar` | — | Unité |
83
+ | `a_zone` | `varchar` | — | Zone picking : `SEC`, `FRAIS`, `UFRAIS` |
84
+ | `a_prodtype` | `varchar` | — | Type : `PRODUIT`, `PANIER`, `ADMIN`, `PLAT` |
85
+ | `ana1` | `varchar` | FK → `anacode.code` | Catégorie 1 |
86
+ | `ana2` | `varchar` | FK → `anacode.code` | Catégorie 2 |
87
+ | `ana4` | `varchar` | FK → `anacode.code` | Marque/brand |
88
+ | `buyprice` | `numeric` | — | Prix d'achat |
89
+ | `discount` | `numeric` | — | Remise fournisseur (%) |
90
+ | `vatid` | `numeric` | — | Taux de TVA (%) |
91
+ | `cp_stock` | `numeric` | — | Stock physique |
92
+ | `cp_res` | `numeric` | — | Stock réservé |
93
+ | `stocktype` | `varchar` | — | Type de stock |
94
+ | `a_gesttype` | `varchar` | — | Type de gestion |
95
+ | `a_okabo` | `int` | — | Disponible en abonnement |
96
+ | `psecotax` | `numeric` | — | Écotaxe |
97
+ | `a_parentid` | `varchar` | FK → `art.artid` | ID produit parent (pour plats multi-portions) |
98
+
99
+ #### `ARTPART` — Composition des meals (ingrédients)
100
+
101
+ | Colonne | Type | Rôle | Description |
102
+ |---|---|---|---|
103
+ | `ARTID` | `char(n)` | FK → `art.artid` | ID meal parent |
104
+ | `ARTPARTID` | `char(n)` | FK → `art.artid` | ID ingrédient |
105
+ | `QTY` | `numeric` | — | Quantité ingrédient |
106
+ | `ORDER` | `int` | — | Numéro de ligne recette |
107
+ | `S_MODDATE` | `timestamp` | — | Date modif (pour dédupliquer via ROW_NUMBER) |
108
+
109
+ #### `anacode` — Référentiel catégories / marques
110
+
111
+ | Colonne | Type | Rôle | Description |
112
+ |---|---|---|---|
113
+ | `code` | `varchar` | PK | FK depuis `art.ana1`, `art.ana2`, `art.ana4` |
114
+ | `name1` | `varchar` | — | Libellé |
115
+
116
+ #### `artfeature` — Association produit / tag
117
+
118
+ | Colonne | Type | Rôle | Description |
119
+ |---|---|---|---|
120
+ | `artid` | `char(n)` | FK → `art.artid` | ID produit |
121
+ | `featureid` | `int` | FK → `feature.featureid` | ID tag |
122
+
123
+ #### `feature` — Tags produits
124
+
125
+ | Colonne | Type | Rôle | Description |
126
+ |---|---|---|---|
127
+ | `featureid` | `int` | PK | ID tag |
128
+ | `name1` | `varchar` | — | Libellé du tag |
129
+
130
+ ---
131
+
132
+ ### Abonnements
133
+
134
+ #### `subscriptions` — Abonnements
135
+
136
+ | Colonne | Type | Rôle | Description |
137
+ |---|---|---|---|
138
+ | `id` | `int` | PK | ID abonnement |
139
+ | `user_id` | `int` | FK → `users.id` | ID client |
140
+ | `next_delivery_date` | `timestamp` | — | Prochaine livraison |
141
+ | `status` | `int` | — | `1` = actif (voir Règles métier / Validité abonnement) |
142
+ | `deleted_at` | `timestamp` | — | NULL si non supprimé |
143
+
144
+ #### `subscription_items` — Produits fixes de l'abonnement
145
+
146
+ | Colonne | Type | Rôle | Description |
147
+ |---|---|---|---|
148
+ | `subscription_id` | `int` | FK → `subscriptions.id` | ID abonnement |
149
+ | `product_id` | `char(n)` | FK → `art.artid` | ID produit |
150
+ | `quantity` | `numeric` | — | Quantité |
151
+
152
+ #### `subscription_add_items` — Produits additionnels ponctuels
153
+
154
+ | Colonne | Type | Rôle | Description |
155
+ |---|---|---|---|
156
+ | `subscription_id` | `int` | FK → `subscriptions.id` | ID abonnement |
157
+ | `product_id` | `char(n)` | FK → `art.artid` | ID produit |
158
+ | `quantity` | `numeric` | — | Quantité |
159
+ | `delivery_date` | `timestamp` | — | Date de livraison concernée |
160
+
161
+ #### `subscription_generation_status` — Statut de génération d'abonnement
162
+
163
+ | Colonne | Type | Rôle | Description |
164
+ |---|---|---|---|
165
+ | `id` | `bigserial` | PK | — |
166
+ | `subscription_id` | `int` | FK → `subscriptions.id` | ID abonnement |
167
+ | `temp_order_ref` | `varchar(255)` | — | Référence commande temporaire |
168
+ | `payment_id` | `varchar(255)` | — | ID paiement |
169
+ | `generation_date` | `date` | — | Date de génération de l'abonnement |
170
+ | `type` | `varchar(255)` | — | `'short'` = produits shop, `'long'` = plats (voir Règles métier / Cutoffs abonnement) |
171
+ | `status` | `varchar(255)` | — | `IS_ON_HOLD` paused · `NO_DELIVERY_WINDOW` · `CARRIER_OFF` · `EFARMZ_OFF` · `AMOUNT_TOO_LOW` · `PAYMENT_SUCCESS` · `NO_ITEMS` · `SUBSCRIPTION_OFF` · `UNEXPECTED_ERROR` · `AMOUNT_TOO_HIGH` · `PAYMENT_FAILED` · `USER_MISSING` · `USER_DELETED` |
172
+ | `payment_link_id` | `varchar(255)` | — | ID lien de paiement |
173
+ | `delivery_date` | `date` | — | Date de livraison de la commande |
174
+ | `created_at` | `timestamp` | — | Date de création |
175
+ | `updated_at` | `timestamp` | — | Date de mise à jour |
176
+
177
+ #### `subscription_missing_product` — Produits manquants lors de la génération
178
+
179
+ | Colonne | Type | Rôle | Description |
180
+ |---|---|---|---|
181
+ | `id` | `bigserial` | PK | — |
182
+ | `user_id` | `int` | FK → `users.id` | Client concerné |
183
+ | `subscription_id` | `int` | FK → `subscriptions.id` | Abonnement concerné |
184
+ | `product_id` | `varchar(255)` | FK → `art.artid` | Produit manquant |
185
+ | `quantity_available` | `int` | — | Quantité disponible au moment de la génération |
186
+ | `quantity_requested` | `int` | — | Quantité demandée |
187
+ | `price` | `double precision` | — | Prix unitaire |
188
+ | `generation_date` | `date` | — | Date de génération de l'abonnement |
189
+ | `delivery_date` | `date` | — | Date de livraison de la commande |
190
+ | `created_at` | `timestamp` | — | Date de création |
191
+ | `updated_at` | `timestamp` | — | Date de mise à jour |
192
+
193
+ #### `subscription_off` — Pauses d'abonnement
194
+
195
+ | Colonne | Type | Rôle | Description |
196
+ |---|---|---|---|
197
+ | `subscription_id` | `int` | FK → `subscriptions.id` | ID abonnement |
198
+ | `start_date` | `timestamp` | — | Date de pause (= `next_delivery_date` du jour) |
199
+
200
+ #### `meals_selected` — Meals choisis par l'abonné
201
+
202
+ | Colonne | Type | Rôle | Description |
203
+ |---|---|---|---|
204
+ | `subscription_id` | `int` | FK → `subscriptions.id` | ID abonnement |
205
+ | `product_id` | `char(n)` | FK → `art.artid` | ID meal kit (contient `%-%`) |
206
+ | `week` | `timestamp` | — | Semaine du meal |
207
+ | `created_at` | `timestamp` | — | Date de création de la sélection |
208
+
209
+ ---
210
+
211
+ ### Livraison
212
+
213
+ #### `delivery_windows` — Fenêtres de livraison
214
+
215
+ | Colonne | Type | Rôle | Description |
216
+ |---|---|---|---|
217
+ | `id` | `bigserial` | PK | — |
218
+ | `user_id` | `varchar(255)` | FK → `users.id` | Client associé |
219
+ | `carrier_id` | `varchar(255)` | FK → `carriers.id` | Transporteur choisi |
220
+ | `postalcode` | `varchar(255)` | — | Code postal de livraison |
221
+ | `city` | `varchar(255)` | — | Ville de livraison |
222
+ | `country` | `varchar(255)` | — | Code pays ISO 2 (défaut `BE`) |
223
+ | `addressline1` | `varchar(255)` | — | Adresse de livraison |
224
+ | `created_at` | `timestamp` | — | Date de création |
225
+ | `updated_at` | `timestamp` | — | Date de mise à jour |
226
+
227
+ #### `delivery_window_order` — Pivot commande ↔ fenêtre de livraison
228
+
229
+ | Colonne | Type | Rôle | Description |
230
+ |---|---|---|---|
231
+ | `delivery_window_id` | `int` | FK → `delivery_windows.id` | Fenêtre de livraison |
232
+ | `w_orderid` | `varchar(255)` | FK → `eorderh.w_orderid` | Commande associée |
233
+
234
+ #### `carriers` — Transporteurs
235
+
236
+ | Colonne | Type | Rôle | Description |
237
+ |---|---|---|---|
238
+ | `id` | `bigint` | PK | — |
239
+ | `name` | `varchar(255)` | — | Nom du transporteur |
240
+ | `type` | `int` | — | `0` = à domicile, `1` = point dépôt |
241
+ | `address1` | `varchar(255)` | — | Adresse du point dépôt |
242
+ | `postcode` | `varchar(255)` | — | Code postal du point dépôt |
243
+ | `city` | `varchar(255)` | — | Ville du point dépôt |
244
+ | `lat` | `varchar(255)` | — | Latitude du point dépôt |
245
+ | `lng` | `varchar(255)` | — | Longitude du point dépôt |
246
+ | `d1` | `boolean` | — | Livre le lundi |
247
+ | `d2` | `boolean` | — | Livre le mardi |
248
+ | `d3` | `boolean` | — | Livre le mercredi |
249
+ | `d4` | `boolean` | — | Livre le jeudi |
250
+ | `d5` | `boolean` | — | Livre le vendredi |
251
+ | `d6` | `boolean` | — | Livre le samedi |
252
+ | `d7` | `boolean` | — | Livre le dimanche |
253
+ | `active` | `boolean` | — | `true` = actif |
254
+
255
+ #### `areas` — Zones de livraison
256
+
257
+ | Colonne | Type | Rôle | Description |
258
+ |---|---|---|---|
259
+ | `id` | `bigserial` | PK | — |
260
+ | `postcode` | `varchar(255)` | — | Code postal |
261
+ | `name` | `varchar(255)` | — | Nom |
262
+ | `commune` | `varchar(255)` | — | Commune |
263
+ | `province` | `varchar(255)` | — | Province |
264
+ | `region` | `varchar(255)` | — | Code région |
265
+ | `region_name` | `varchar(255)` | — | Nom de la région |
266
+ | `country` | `varchar(255)` | — | Code pays ISO 2 (défaut `BE`) |
267
+ | `lat` | `numeric(5,3)` | — | Latitude |
268
+ | `lng` | `numeric(5,3)` | — | Longitude |
269
+
270
+ #### `area_carrier` — Association zone ↔ transporteur
271
+
272
+ | Colonne | Type | Rôle | Description |
273
+ |---|---|---|---|
274
+ | `area_id` | `bigint` | FK → `areas.id` | Zone de livraison |
275
+ | `carrier_id` | `bigint` | FK → `carriers.id` | Transporteur actif sur la zone |
276
+
277
+ ---
278
+
279
+ ### Avis
280
+
281
+ #### `feedback` — En-têtes d'avis
282
+
283
+ | Colonne | Type | Rôle | Description |
284
+ |---|---|---|---|
285
+ | `id` | `bigserial` | PK | — |
286
+ | `user_id` | `int` | FK → `users.id` | Client ayant donné l'avis |
287
+ | `delivered_at` | `date` | — | Date de livraison concernée |
288
+ | `created_at` | `timestamp` | — | Date de création |
289
+ | `updated_at` | `timestamp` | — | Date de mise à jour |
290
+
291
+ #### `feedback_entries` — Détail des avis par plat
292
+
293
+ | Colonne | Type | Rôle | Description |
294
+ |---|---|---|---|
295
+ | `id` | `bigserial` | PK | — |
296
+ | `feedback_id` | `int` | FK → `feedback.id` | En-tête de l'avis |
297
+ | `meal_id` | `text` | FK → `art.artid` | Alias `product_id` — plat évalué |
298
+ | `meal_name` | `text` | — | Nom du plat au moment de l'avis |
299
+ | `meal_image` | `text` | — | URL image du plat |
300
+ | `rating` | `smallint` | — | Note globale de 0 à 10 |
301
+ | `would_order_again` | `boolean` | — | Commanderait à nouveau |
302
+ | `ease` | `smallint` | — | Facilité de préparation (0-10) |
303
+ | `originality` | `smallint` | — | Originalité (0-10) |
304
+ | `quantity` | `smallint` | — | Quantité (0-10) |
305
+ | `taste` | `smallint` | — | Goût (0-10) |
306
+ | `comment` | `text` | — | Commentaire libre |
307
+ | `created_at` | `timestamp` | — | Date de création |
308
+ | `updated_at` | `timestamp` | — | Date de mise à jour |
309
+
310
+ ---
311
+
312
+ ### Clients
313
+
314
+ #### `users` — Clients
315
+
316
+ | Colonne | Type | Rôle | Description |
317
+ |---|---|---|---|
318
+ | `id` | `bigint` | PK | ID client |
319
+ | `first_order_subscription` | `timestamp` | — | Date de première commande abonnement |
320
+
321
+ ---
322
+
323
+ ## Règles métier
324
+
325
+ ### Filtres standards
326
+
327
+ **Commandes valides** — exclure les annulations et remboursements
328
+ ```sql
329
+ status NOT IN (31, 32)
330
+ ```
331
+
332
+ **Jours ouvrés** — lundi à vendredi
333
+ ```sql
334
+ extract(isodow from date) BETWEEN 1 AND 5
335
+ ```
336
+
337
+ **Produits picking (shop)** — exclure les génériques
338
+ ```sql
339
+ eorderd.artid NOT LIKE 'G%'
340
+ ```
341
+
342
+ **Produits shop** — catalogue standard (excluant meals)
343
+ ```sql
344
+ a_prodtype IN ('PRODUIT', 'PANIER', 'ADMIN')
345
+ ```
346
+
347
+ ### Validité abonnement
348
+
349
+ **Abonnement actif valide**
350
+ ```sql
351
+ s.status = 1
352
+ AND s.deleted_at IS NULL
353
+ AND s.next_delivery_date::date > (CURRENT_DATE + interval '1 day')
354
+ AND NOT EXISTS (
355
+ SELECT 1 FROM efarmz_db.subscription_off so
356
+ WHERE so.subscription_id = s.id AND so.start_date = s.next_delivery_date
357
+ )
358
+ ```
359
+
360
+ ### Calculs
361
+
362
+ **Marge produit** — en %
363
+ ```sql
364
+ ((finalprice / NULLIF((1 + (vatid::float / 100)), 0))
365
+ - (buyprice - (buyprice * COALESCE(discount, 0) / 100)))
366
+ / NULLIF(finalprice / NULLIF((1 + (vatid::float / 100)), 0), 0) * 100
367
+ ```
368
+
369
+ **Stock disponible** — stock physique moins réservations
370
+ ```sql
371
+ cp_stock - COALESCE(cp_res, 0)
372
+ ```
373
+
374
+ **Valeur stock** — au coût d'achat net
375
+ ```sql
376
+ (cp_stock - COALESCE(cp_res, 0)) * (buyprice - (buyprice * COALESCE(discount, 0) / 100))
377
+ ```
378
+
379
+ **Rotation stock** — à partir des ventes du mois précédent
380
+ ```sql
381
+ sells_last_month / NULLIF(stock_disponible, 0)
382
+ ```
383
+
384
+ **Montant hors TVA d'une ligne** — à partir de `eorderd`
385
+ ```sql
386
+ ROUND(CAST((eorderd.unitprice - eorderd.unitprice * eorderd.vatpc / 100) AS numeric), 2)
387
+ ```
388
+
389
+ **Seuil panier picking** — montant minimum pour déclarer une commande viable
390
+ ```
391
+ total_add_items + total_items + total_catalogue1 > 40
392
+ ```
393
+ Calculé avec `catalogue1` (prix abonnés × qty) depuis `subscription_items` + `subscription_add_items` + `meals_selected`.
394
+
395
+ ### Cutoffs abonnement
396
+
397
+ Une commande abonnement génère **deux entrées** dans `subscription_generation_status` pour une même `delivery_date` :
398
+
399
+ | `type` | Contenu |
400
+ |---|---|
401
+ | `'long'` | Plats (meals) |
402
+ | `'short'` | Produits du shop |
403
+
404
+ ⚠️ Ne pas agréger sans filtrer sur `type` sous peine de doublon-compter une livraison.
405
+
406
+ ### Frais de port
407
+
408
+ **Identifier les frais de port**
409
+ ```sql
410
+ eorderd.artid = 'COSTPORT'
411
+ ```
412
+
413
+ ⚠️ Toujours vérifier `SUM(price) > 0` pour confirmer qu'ils sont effectivement facturés (ils peuvent être gratuits).
414
+
415
+ ---
416
+
417
+ ## Patterns SQL
418
+
419
+ ### Meals vs Produits simples {#pattern-meals-vs-simples}
420
+
421
+ **Quand** : distinguer un meal kit (à décomposer) d'un produit simple
422
+
423
+ ```sql
424
+ WHERE product_id LIKE '%-%' -- meal kit (2 portions, 3 portions, etc.)
425
+ WHERE product_id NOT LIKE '%-%' -- produit simple (1 ligne picking)
426
+ ```
427
+
428
+ ### ID plat — type & portion {#pattern-plat-id}
429
+
430
+ **Quand** : extraire le type de plat et le nombre de portions
431
+
432
+ ```sql
433
+ -- Type de plat (1ère lettre de artid)
434
+ CASE LEFT(d.artid, 1)
435
+ WHEN 'L' THEN 'Légume'
436
+ WHEN 'V' THEN 'Viande'
437
+ WHEN 'P' THEN 'Poisson'
438
+ ELSE 'Autre'
439
+ END AS plat_type
440
+
441
+ -- Nombre de portions (chiffre avant le P final : V810-2P → 2)
442
+ -- ⚠️ RTRIM obligatoire — artid est char(n)
443
+ SUBSTRING(RTRIM(d.artid) FROM '-(\d+)P$') || ' portion(s)' AS portion
444
+
445
+ -- Filtrer les plats portion (⚠️ RTRIM obligatoire)
446
+ AND RTRIM(d.artid) LIKE '%-%P'
447
+ ```
448
+
449
+ ### Tags produit {#pattern-tags}
450
+
451
+ **Quand** : récupérer les tags (features) associés à un produit
452
+
453
+ ```sql
454
+ SELECT f.name1
455
+ FROM efarmz_db.art
456
+ LEFT JOIN efarmz_db.artfeature af ON af.artid = art.artid
457
+ LEFT JOIN efarmz_db.feature f ON f.featureid = af.featureid
458
+ WHERE art.artid = 'V810'
459
+
460
+ -- Pour les plats : supprimer la portion de l'artid avant le join
461
+ LEFT JOIN efarmz_db.artfeature af
462
+ ON af.artid = REGEXP_REPLACE(RTRIM(d.artid), '-\d+P$', '')
463
+ ```
464
+
465
+ ### Décomposition ARTPART {#pattern-artpart}
466
+
467
+ **Quand** : lister les ingrédients d'un meal kit
468
+
469
+ **Dépend de** : Conventions globales / RTRIM, Conventions globales / deduplication
470
+
471
+ ```sql
472
+ WITH ranked_artpart AS (
473
+ SELECT ap.ARTID AS meal_id, ap.ARTPARTID AS ingredient_id,
474
+ art.NAME1, art.UNIT, art.a_zone, ap.QTY, ap.ORDER,
475
+ ROW_NUMBER() OVER (
476
+ PARTITION BY ap.ARTID, ap.ORDER
477
+ ORDER BY ap.S_MODDATE DESC
478
+ ) AS rn
479
+ FROM efarmz_db.ARTPART ap
480
+ JOIN efarmz_db.ART art ON art.ARTID = ap.ARTPARTID
481
+ WHERE ap.ARTID IN (...)
482
+ )
483
+ SELECT * FROM ranked_artpart WHERE rn = 1
484
+ ```
485
+
486
+ ⚠️ Toujours utiliser `WHERE rn = 1` pour éviter les doublons en cas de mise à jour.
487
+
488
+ ### Alignement N-1 même jour de semaine {#pattern-ly-align}
489
+
490
+ **Quand** : calculer la date homologue de l'année précédente (ou N-2), même jour de la semaine
491
+
492
+ ```sql
493
+ -- N-1
494
+ (
495
+ (delivery_date - interval '1 year')::date
496
+ + (((extract(isodow from delivery_date)::int
497
+ - extract(isodow from (delivery_date - interval '1 year'))::int
498
+ + 10) % 7) - 3) * interval '1 day'
499
+ )::date AS delivery_date_ly
500
+
501
+ -- N-2 (même formule, 2 ans en arrière)
502
+ (
503
+ (delivery_date - interval '2 year')::date
504
+ + (((extract(isodow from delivery_date)::int
505
+ - extract(isodow from (delivery_date - interval '2 year'))::int
506
+ + 10) % 7) - 3) * interval '1 day'
507
+ )::date AS delivery_date_2y
508
+ ```
509
+
510
+ ### Semaine ISO pour les meals {#pattern-iso-week}
511
+
512
+ **Quand** : aligner les sélections de meals avec la semaine de livraison
513
+
514
+ ```sql
515
+ date_trunc('week', ms.week) = date_trunc('week', s.next_delivery_date)
516
+ ```
517
+
518
+ ### Zones picking {#pattern-zones}
519
+
520
+ **Quand** : compter les lignes par zone (SEC, FRAIS, UFRAIS)
521
+
522
+ ```sql
523
+ COUNT(*) FILTER (WHERE a_zone = 'SEC') AS sec_lines,
524
+ COUNT(*) FILTER (WHERE a_zone = 'FRAIS') AS frais_lines,
525
+ COUNT(*) FILTER (WHERE a_zone = 'UFRAIS') AS ufrais_lines
526
+ ```
527
+
528
+ ### Génération dates ouvrées {#pattern-workdays}
529
+
530
+ **Quand** : générer une liste de dates de livraison futures (lun-ven)
531
+
532
+ ```sql
533
+ SELECT gs::date AS delivery_date
534
+ FROM generate_series(
535
+ (current_date + interval '1 day')::date,
536
+ (current_date + interval '13 days')::date,
537
+ interval '1 day'
538
+ ) gs
539
+ WHERE extract(isodow from gs) BETWEEN 1 AND 5
540
+ ```
541
+
542
+ ### Commandes 100% shop {#pattern-shop-only}
543
+
544
+ **Quand** : identifier les commandes contenant exclusivement des produits shop (pas de meals)
545
+
546
+ ```sql
547
+ WHERE NOT EXISTS (
548
+ SELECT 1
549
+ FROM jsonb_array_elements(items::jsonb) AS item
550
+ WHERE item->>'a_prodtype' NOT IN ('PRODUIT', 'PANIER', 'ADMIN')
551
+ )
552
+ ```
553
+
554
+ ### Rang des commandes par client {#pattern-order-rank}
555
+
556
+ **Quand** : numéroter les commandes d'un client (1ère, 2e, 3e…)
557
+
558
+ **Dépend de** : Conventions globales / tie-breaker
559
+
560
+ ```sql
561
+ ROW_NUMBER() OVER (
562
+ PARTITION BY h.c_uuid
563
+ ORDER BY h.dlvdate ASC, h.w_orderid ASC
564
+ ) AS order_rank
565
+ ```
566
+
567
+ ### Timing de sélection des meals {#pattern-meal-timing}
568
+
569
+ **Quand** : mesurer l'avance avec laquelle un abonné sélectionne ses meals
570
+
571
+ ```sql
572
+ CASE
573
+ WHEN date_trunc('week', selection_date) = date_trunc('week', dlvdate)
574
+ THEN 'Semaine même'
575
+ WHEN date_trunc('week', selection_date) = date_trunc('week', dlvdate - interval '1 week')
576
+ THEN '1 semaine en avance'
577
+ WHEN date_trunc('week', selection_date) = date_trunc('week', dlvdate - interval '2 weeks')
578
+ THEN '2 semaines en avance'
579
+ WHEN date_trunc('week', selection_date) = date_trunc('week', dlvdate - interval '3 weeks')
580
+ THEN '3 semaines en avance'
581
+ ELSE '+ de 3 semaines en avance'
582
+ END AS timing
583
+ ```
584
+
585
+ ⚠️ Utiliser `MIN(ms.created_at)` pour la première sélection faite par le client.
586
+
587
+ ### Pourcentage avec total global {#pattern-pct-window}
588
+
589
+ **Quand** : calculer un % par rapport au total de tous les groupes
590
+
591
+ **Dépend de** : Conventions globales / ::numeric avant ROUND
592
+
593
+ ```sql
594
+ ROUND(
595
+ COUNT(*)::numeric
596
+ / NULLIF(SUM(COUNT(*)) OVER ()::numeric, 0) * 100
597
+ , 2) AS pct
598
+ ```
599
+
600
+ ### Ventes mois précédent {#pattern-last-month}
601
+
602
+ **Quand** : agréger les quantités vendues au mois précédent (pour rotation, top produits, etc.)
603
+
604
+ **Dépend de** : Règles métier / Filtres / Commandes valides
605
+
606
+ ```sql
607
+ WITH last_month_sales AS (
608
+ SELECT d.artid, SUM(d.qty) AS qty
609
+ FROM efarmz_db.eorderd d
610
+ INNER JOIN efarmz_db.eorderh h ON h.w_orderid = d.w_orderid
611
+ WHERE h.status NOT IN (31, 32)
612
+ AND date_trunc('month', h.dlvdate) = date_trunc('month', CURRENT_DATE - interval '1 month')
613
+ GROUP BY d.artid
614
+ )
615
+ -- Utiliser cette CTE dans la requête principale
616
+ ```
617
+
618
+ ### Forecast commandes par date {#pattern-forecast}
619
+
620
+ **Quand** : projeter le nombre de commandes attendues sur les 2 prochaines semaines ouvrées (abonnements + one-shot)
621
+
622
+ **Dépend de** : Patterns SQL / Génération dates ouvrées, Patterns SQL / Alignement N-1 même jour de semaine, Règles métier / Validité abonnement, Règles métier / Seuil panier picking
623
+
624
+ ```sql
625
+ WITH base_dates AS (
626
+ SELECT gs::date AS delivery_date
627
+ FROM generate_series(
628
+ (current_date + interval '1 day')::date,
629
+ (date_trunc('week', current_date)::date + interval '1 week' + interval '13 days')::date,
630
+ interval '1 day'
631
+ ) gs
632
+ WHERE extract(isodow from gs) BETWEEN 1 AND 5
633
+ ),
634
+ items AS (
635
+ SELECT i.subscription_id, SUM(i.quantity * a.finalprice) AS total_items
636
+ FROM efarmz_db.subscription_items i
637
+ JOIN efarmz_db.art a ON a.artid = i.product_id
638
+ GROUP BY i.subscription_id
639
+ ),
640
+ add_items AS (
641
+ SELECT ai.subscription_id, ai.delivery_date, SUM(ai.quantity * a.finalprice) AS total_add_items
642
+ FROM efarmz_db.subscription_add_items ai
643
+ JOIN efarmz_db.art a ON a.artid = ai.product_id
644
+ GROUP BY ai.subscription_id, ai.delivery_date
645
+ ),
646
+ meals AS (
647
+ SELECT ms.subscription_id,
648
+ date_trunc('week', ms.week) AS week_start,
649
+ SUM(a.catalogue1) AS total_catalogue1
650
+ FROM efarmz_db.meals_selected ms
651
+ JOIN efarmz_db.art a ON a.artid = ms.product_id
652
+ GROUP BY ms.subscription_id, date_trunc('week', ms.week)
653
+ ),
654
+ -- Abonnements actifs avec panier viable (voir Règles métier / Validité abonnement + Seuil panier picking)
655
+ next_subscriptions AS (
656
+ SELECT s.id, s.next_delivery_date,
657
+ COALESCE(i.total_items, 0) AS total_items,
658
+ COALESCE(ai.total_add_items, 0) AS total_add_items,
659
+ COALESCE(m.total_catalogue1, 0) AS total_catalogue1
660
+ FROM efarmz_db.subscriptions s
661
+ LEFT JOIN items i ON i.subscription_id = s.id
662
+ LEFT JOIN add_items ai ON ai.subscription_id = s.id AND ai.delivery_date = s.next_delivery_date
663
+ LEFT JOIN meals m ON m.subscription_id = s.id AND m.week_start = date_trunc('week', s.next_delivery_date)
664
+ WHERE s.status = 1 AND s.deleted_at IS NULL
665
+ AND s.next_delivery_date::date > (CURRENT_DATE + interval '1 day')
666
+ AND NOT EXISTS (SELECT 1 FROM efarmz_db.subscription_off so
667
+ WHERE so.subscription_id = s.id AND so.start_date = s.next_delivery_date)
668
+ ),
669
+ forecast_subscriptions AS (
670
+ SELECT DATE(next_delivery_date) AS delivery_date, COUNT(DISTINCT id) AS subscription_forecast
671
+ FROM next_subscriptions
672
+ WHERE total_add_items + total_items + total_catalogue1 > 40
673
+ AND extract(isodow from next_delivery_date) NOT IN (6, 7)
674
+ GROUP BY DATE(next_delivery_date)
675
+ ),
676
+ forecast_by_date AS (
677
+ SELECT b.delivery_date, COALESCE(fs.subscription_forecast, 0) AS subscription_forecast
678
+ FROM base_dates b
679
+ LEFT JOIN forecast_subscriptions fs USING (delivery_date)
680
+ ),
681
+ -- Dates homologues N-1 et N-2 (voir #pattern-ly-align)
682
+ aligned AS (
683
+ SELECT f.delivery_date, f.subscription_forecast,
684
+ ((f.delivery_date - interval '1 year')::date
685
+ + (((extract(isodow from f.delivery_date)::int
686
+ - extract(isodow from (f.delivery_date - interval '1 year'))::int + 10) % 7) - 3
687
+ ) * interval '1 day')::date AS delivery_date_ly,
688
+ ((f.delivery_date - interval '2 year')::date
689
+ + (((extract(isodow from f.delivery_date)::int
690
+ - extract(isodow from (f.delivery_date - interval '2 year'))::int + 10) % 7) - 3
691
+ ) * interval '1 day')::date AS delivery_date_2y
692
+ FROM forecast_by_date f
693
+ ),
694
+ -- One-shot N-1 : seulement les commandes passées avec un lead time ≤ au lead time restant actuel
695
+ forecast_oneshot_ly AS (
696
+ SELECT a.delivery_date_ly, COUNT(DISTINCT h.w_orderid) AS count
697
+ FROM efarmz_db.eorderh h
698
+ INNER JOIN aligned a ON DATE(a.delivery_date_ly) = DATE(h.dlvdate)
699
+ WHERE h.status NOT IN (31, 32) AND h.ordertype = 1
700
+ AND h.s_credate::timestamp >= h.dlvdate::timestamp - (DATE(a.delivery_date)::timestamp - now())
701
+ GROUP BY a.delivery_date_ly
702
+ ),
703
+ hist_oneshot AS (
704
+ SELECT dlvdate::date AS delivery_date, COUNT(DISTINCT w_orderid) AS oneshot
705
+ FROM efarmz_db.eorderh WHERE status NOT IN (31, 32) AND ordertype = 1
706
+ GROUP BY dlvdate::date
707
+ ),
708
+ hist_subscription AS (
709
+ SELECT dlvdate::date AS delivery_date, COUNT(DISTINCT w_orderid) AS subscription
710
+ FROM efarmz_db.eorderh WHERE status NOT IN (31, 32) AND ordertype = 2
711
+ GROUP BY dlvdate::date
712
+ )
713
+ SELECT
714
+ TO_CHAR(a.delivery_date, 'YYYY/MM/DD') AS delivery_date,
715
+ COALESCE(h0.oneshot, 0) AS oneshot,
716
+ COALESCE(forecast_oneshot_ly.count, 0) AS oneshot_forecast,
717
+ COALESCE(s0.subscription, 0) AS subscription,
718
+ CASE WHEN a.subscription_forecast > 0
719
+ THEN a.subscription_forecast - COALESCE(s0.subscription, 0) ELSE 0 END AS subscription_forecast,
720
+ COALESCE(h0.oneshot, 0) + COALESCE(forecast_oneshot_ly.count, 0)
721
+ + COALESCE(s0.subscription, 0)
722
+ + CASE WHEN a.subscription_forecast > 0
723
+ THEN a.subscription_forecast - COALESCE(s0.subscription, 0) ELSE 0 END AS total_forecast,
724
+ TO_CHAR(a.delivery_date_ly, 'YYYY/MM/DD') AS delivery_date_ly,
725
+ COALESCE(h1.oneshot + s1.subscription, 0) AS orders_ly,
726
+ TO_CHAR(a.delivery_date_2y, 'YYYY/MM/DD') AS delivery_date_2y,
727
+ COALESCE(h2.oneshot + s2.subscription, 0) AS orders_2y
728
+ FROM aligned a
729
+ LEFT JOIN hist_oneshot h0 ON h0.delivery_date = a.delivery_date
730
+ LEFT JOIN hist_oneshot h1 ON h1.delivery_date = a.delivery_date_ly
731
+ LEFT JOIN hist_oneshot h2 ON h2.delivery_date = a.delivery_date_2y
732
+ LEFT JOIN hist_subscription s0 ON s0.delivery_date = a.delivery_date
733
+ LEFT JOIN hist_subscription s1 ON s1.delivery_date = a.delivery_date_ly
734
+ LEFT JOIN hist_subscription s2 ON s2.delivery_date = a.delivery_date_2y
735
+ LEFT JOIN forecast_oneshot_ly ON forecast_oneshot_ly.delivery_date_ly = a.delivery_date_ly
736
+ ORDER BY a.delivery_date;
737
+ ```
738
+
739
+ ⚠️ Le forecast one-shot (`forecast_oneshot_ly`) filtre les commandes N-1 par lead time restant : `s_credate >= dlvdate - (delivery_date - now())`. Cela évite de compter des commandes N-1 passées très tôt que les clients n'auraient pas encore passées côté J actuel — c'est un prorata temporis, pas un simple comptage brut N-1.
740
+
741
+ ⚠️ `subscription_forecast` représente le *delta* (abonnements prévisionnels moins ceux déjà confirmés dans `eorderh`). Il peut être 0 si tous les abonnements ont déjà généré une commande.
742
+
743
+ ---
744
+
745
+ ## Glossaire
746
+
747
+ | Terme | Alias | Contexte | Notes |
748
+ |---|---|---|---|
749
+ | `artid` | `product_id` | Selon le contexte d'écriture — synonymes dans les queries | Type `char(n)`, toujours `RTRIM` pour patterns de fin |
750
+ | `c_uuid` | `users.id` (jointure via `::bigint`) | `eorderh.c_uuid` ← → `users.id` | Identifiant client unifié |
751
+ | `catalogue1` | — | Prix **abonnés** | Vs `finalprice` (prix non-abonnés) |
752
+ | `finalprice` | — | Prix **non-abonnés** | Vs `catalogue1` (prix abonnés) |
753
+ | Zone picking | `a_zone` | Localisation physique en entrepôt | Valeurs : `SEC`, `FRAIS`, `UFRAIS` |
754
+ | Type produit | `a_prodtype` | Classification en catalogue | Valeurs : `PRODUIT`, `PANIER`, `ADMIN`, `PLAT` |
755
+ | Préfixes artid plats | — | Codes de la 1ère lettre | `L` = Légume, `V` = Viande, `P` = Poisson |
756
+ | Statuts commande | `status` | Filtre standard | `31` = annulé, `32` = remboursé (à exclure) |
757
+ | Statut abonnement | `subscriptions.status` | Activité | `1` = actif |
758
+
759
+ ---