Merge pull request #44 from R0m1k3/1.1

Fix client order validation errors for production
This commit is contained in:
LogiFlow authored and GitHub committed 2025-08-12 16:51:48 +02:00
commit ee54efa654
5 files changed
+62 -108

No files matched your search

+43 -88
View File
@@ -1,106 +1,61 @@
# Fix Urgent - Erreurs de Validation Zod # Fix Validation Errors - Commandes Client Production
## ❌ Problème Identifié ## Problème Identifié
**Erreur 500 :** ZodError lors de PUT /api/deliveries/:id
### Erreurs Specifiques **Erreurs de validation dans le formulaire de commande client :**
1. **blAmount** : Expected string, received number 1. `"Expected number, received string"` pour le champ **Fournisseur** (`supplierId`)
2. **validatedAt** : Expected date, received string 2. `"Expected number, received string"` pour le champ **Acompte** (`deposit`)
## 🔍 Analyse du Problème ## Cause du Problème
### Schema Drizzle (shared/schema.ts) Le formulaire frontend envoie des valeurs string, mais le schema Zod attend des numbers :
```typescript
// Dans deliveries table:
blAmount: decimal("bl_amount", { precision: 10, scale: 2 }) // → Zod attend string
invoiceAmount: decimal("invoice_amount", { precision: 10, scale: 2 }) // → Zod attend string
validatedAt: timestamp("validated_at") // → Zod attend Date object
```
### Données Envoyées depuis Frontend
```javascript ```javascript
// ❌ Problématique // AVANT (Problématique)
{ supplierId: z.number().int().positive().optional().default(1)
blAmount: 3397.86, // number → should be string deposit: z.number().optional().default(0)
invoiceAmount: null, // OK
validatedAt: '2025-08-12T11:42:39.679Z' // string → should be Date // APRÈS (Corrigé)
} supplierId: z.coerce.number().int().positive().optional().default(1)
deposit: z.coerce.number().optional().default(0)
``` ```
## ✅ Solutions Appliquées ## Solution Appliquée
### 1. **Transformation côté Serveur** (server/routes.ts) **Fichier modifié :** `shared/schema.ts`
```typescript
// Avant validation Zod, transformer les types
const transformedData = { ...req.body };
// Convertir montants number → string ✅ Ajouté `z.coerce.number()` pour forcer la conversion automatique string → number
if (transformedData.blAmount !== undefined && transformedData.blAmount !== null) { ✅ Appliqué aussi pour `quantity` et `groupId` par précaution
transformedData.blAmount = transformedData.blAmount.toString(); ✅ Corrigé `customerEmail` pour accepter chaîne vide avec `.or(z.literal(""))`
}
if (transformedData.invoiceAmount !== undefined && transformedData.invoiceAmount !== null) {
transformedData.invoiceAmount = transformedData.invoiceAmount.toString();
}
// Convertir validatedAt string → Date ## Schema Corrigé
if (transformedData.validatedAt && typeof transformedData.validatedAt === 'string') {
transformedData.validatedAt = new Date(transformedData.validatedAt);
}
const data = insertDeliverySchema.partial().parse(transformedData);
```
### 2. **Correction côté Frontend** (ReconciliationModal.tsx)
```typescript
// Avant
validatedAt: new Date() // ❌ Date object → string lors JSON.stringify
// Après
validatedAt: new Date().toISOString() // ✅ string → convertie en Date côté serveur
```
### 3. **Montants toujours en string** (ReconciliationModal.tsx)
```typescript
// Déjà corrigé précédemment
blAmount: formData.blAmount ? formData.blAmount.toString() : null,
invoiceAmount: formData.invoiceAmount ? formData.invoiceAmount.toString() : null,
```
## 🧪 Test de Validation
### Payload Attendu Maintenant
```javascript ```javascript
// ✅ Correct après transformation export const insertCustomerOrderFrontendSchema = z.object({
{ customerName: z.string().min(1, "Customer name is required"),
blNumber: 'LD2250800571', contactNumber: z.string().min(1, "Contact number is required"),
blAmount: '3397.86', // string ✅ productName: z.string().min(1, "Product name is required"),
invoiceReference: null, productDescription: z.string().optional(),
invoiceAmount: null, quantity: z.coerce.number().int().positive().default(1), // ✅ CORRIGÉ
reconciled: true, groupId: z.coerce.number().int().positive(), // ✅ CORRIGÉ
validatedAt: new Date('2025-08-12T11:42:39.679Z') // Date object ✅ isPickup: z.boolean().default(false),
} notes: z.string().optional(),
orderTaker: z.string().optional(),
gencode: z.string().optional().default(""),
supplierId: z.coerce.number().int().positive().optional().default(1), // ✅ CORRIGÉ
deposit: z.coerce.number().optional().default(0), // ✅ CORRIGÉ
isPromotionalPrice: z.boolean().default(false),
customerEmail: z.string().email().optional().or(z.literal("")), // ✅ CORRIGÉ
productReference: z.string().optional(),
});
``` ```
### Tests à Effectuer ## Test
1. **Enregistrer données BL** : blAmount doit passer en string
2. **Valider rapprochement** : validatedAt doit passer en Date
3. **Enregistrer montant facture** : invoiceAmount doit passer en string
4. **Vérifier console** : Plus d'erreurs ZodError 500
## 🔄 Flux de Données Complet Après cette correction, les employés devraient pouvoir créer des commandes client sans erreur de validation en production.
``` ## Déploiement
Frontend Form → JSON.stringify → HTTP Request → Server Transform → Zod Validation → Database
↓ ↓ ↓ ↓ ↓ ↓
{number} → "3397.86" → "3397.86" → "3397.86" → ✅ → DECIMAL
{Date} → "2025-..." → "2025-..." → Date(...) → ✅ → TIMESTAMP
```
## 📊 Status 1. ✅ Schema corrigé dans `shared/schema.ts`
2. 🔄 **Redémarrer le serveur production**
- ✅ Transformation automatique côté serveur 3. 🧪 **Tester création commande client avec employé**
- ✅ Frontend envoie validatedAt en ISO string
- ✅ Montants forcés en string côté client
- 🔄 **À tester en production**
**L'erreur 500 ZodError devrait être résolue !**
Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

+13 -14
View File
@@ -26,8 +26,7 @@ import { insertCustomerOrderFrontendSchema, type CustomerOrderWithRelations, typ
import { useStore } from "@/components/Layout"; import { useStore } from "@/components/Layout";
const customerOrderFormSchema = insertCustomerOrderFrontendSchema.extend({ const customerOrderFormSchema = insertCustomerOrderFrontendSchema.extend({
deposit: z.number().optional().default(0), groupId: z.coerce.number().int().positive().optional(),
groupId: z.number().int().positive().optional(),
createdBy: z.string().optional(), createdBy: z.string().optional(),
}); });
@@ -72,14 +71,14 @@ export function CustomerOrderForm({
defaultValues: { defaultValues: {
orderTaker: order?.orderTaker || user?.name || "", orderTaker: order?.orderTaker || user?.name || "",
customerName: order?.customerName || "", customerName: order?.customerName || "",
customerPhone: order?.customerPhone || "", contactNumber: order?.customerPhone || "",
productDesignation: order?.productDesignation || "", productName: order?.productDesignation || "",
productReference: order?.productReference || "", productReference: order?.productReference || "",
gencode: order?.gencode || "", gencode: order?.gencode || "",
quantity: order?.quantity || 1, quantity: order?.quantity || 1,
supplierId: order?.supplierId || undefined, supplierId: order?.supplierId || undefined,
status: "En attente de Commande", // Statut fixe status: "En attente de Commande", // Statut fixe
deposit: order?.deposit ? order.deposit.toString() : "0", deposit: order?.deposit || 0,
isPromotionalPrice: order?.isPromotionalPrice || false, isPromotionalPrice: order?.isPromotionalPrice || false,
customerNotified: order?.customerNotified || false, customerNotified: order?.customerNotified || false,
groupId: order?.groupId || (user?.role === 'admin' && selectedStoreId ? selectedStoreId : user?.userGroups?.[0]?.groupId) || undefined, // Respect admin store selection groupId: order?.groupId || (user?.role === 'admin' && selectedStoreId ? selectedStoreId : user?.userGroups?.[0]?.groupId) || undefined, // Respect admin store selection
@@ -119,17 +118,17 @@ export function CustomerOrderForm({
// Prepare data with proper types for frontend schema // Prepare data with proper types for frontend schema
const submitData = { const submitData = {
customerName: data.customerName.trim(), customerName: data.customerName.trim(),
contactNumber: (data as any).customerPhone || (data as any).contactNumber || '', contactNumber: data.contactNumber || '',
productName: (data as any).productDesignation || (data as any).productName || '', productName: data.productName || '',
quantity: data.quantity, quantity: data.quantity,
groupId: typeof groupId === 'number' ? groupId : parseInt(groupId.toString()), groupId: typeof groupId === 'number' ? groupId : parseInt(groupId.toString()),
isPickup: false, isPickup: false,
notes: (data as any).notes, notes: data.notes,
deposit: data.deposit || 0, deposit: data.deposit || 0,
isPromotionalPrice: (data as any).isPromotionalPrice || false, isPromotionalPrice: data.isPromotionalPrice || false,
customerEmail: (data as any).customerEmail, customerEmail: data.customerEmail,
gencode: (data as any).gencode || '', gencode: data.gencode || '',
supplierId: (data as any).supplierId || 1, supplierId: data.supplierId || 1,
}; };
console.log("🔍 Frontend submit data:", submitData); console.log("🔍 Frontend submit data:", submitData);
onSubmit(submitData); onSubmit(submitData);
@@ -193,7 +192,7 @@ export function CustomerOrderForm({
<FormField <FormField
control={form.control} control={form.control}
name="customerPhone" name="contactNumber"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>N° de téléphone</FormLabel> <FormLabel>N° de téléphone</FormLabel>
@@ -236,7 +235,7 @@ export function CustomerOrderForm({
<FormField <FormField
control={form.control} control={form.control}
name="productDesignation" name="productName"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Désignation du produit</FormLabel> <FormLabel>Désignation du produit</FormLabel>
+1 -1
View File
@@ -2,4 +2,4 @@
# https://curl.se/docs/http-cookies.html # https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk. # This file was generated by libcurl! Edit at your own risk.
#HttpOnly_localhost FALSE / FALSE 1755095028 connect.sid s%3AxwnR6osGUOsL-I6MHjHctBuJf4K1HUWh.jqsaHkMTtzflaI4dg%2BmJBmTrVplUTDTG45KCUiL%2FVfw #HttpOnly_localhost FALSE / FALSE 1755096543 connect.sid s%3AdW4IaK_l9KAwScxqJTml_UK7Lh2MI3U-.9wChJ5YVxYxvwztaxrxvedzNdY2gvauFeMasHVP8jsk
+5 -5
View File
@@ -456,17 +456,17 @@ export const insertCustomerOrderFrontendSchema = z.object({
contactNumber: z.string().min(1, "Contact number is required"), // Maps to customerPhone contactNumber: z.string().min(1, "Contact number is required"), // Maps to customerPhone
productName: z.string().min(1, "Product name is required"), // Maps to productDesignation productName: z.string().min(1, "Product name is required"), // Maps to productDesignation
productDescription: z.string().optional(), // Optional description productDescription: z.string().optional(), // Optional description
quantity: z.number().int().positive().default(1), quantity: z.coerce.number().int().positive().default(1),
groupId: z.number().int().positive(), groupId: z.coerce.number().int().positive(),
isPickup: z.boolean().default(false), isPickup: z.boolean().default(false),
notes: z.string().optional(), notes: z.string().optional(),
// Optional fields with defaults // Optional fields with defaults
orderTaker: z.string().optional(), // Will be set to creator name if not provided orderTaker: z.string().optional(), // Will be set to creator name if not provided
gencode: z.string().optional().default(""), // Will be empty if not provided gencode: z.string().optional().default(""), // Will be empty if not provided
supplierId: z.number().int().positive().optional().default(1), // Default supplier ID supplierId: z.coerce.number().int().positive().optional().default(1), // Default supplier ID
deposit: z.number().optional().default(0), deposit: z.coerce.number().optional().default(0),
isPromotionalPrice: z.boolean().default(false), isPromotionalPrice: z.boolean().default(false),
customerEmail: z.string().email().optional(), customerEmail: z.string().email().optional().or(z.literal("")),
productReference: z.string().optional(), productReference: z.string().optional(),
}); });