This commit is contained in:
@@ -655,7 +655,7 @@ func (d *Database) ValidateDeliveryAtomic(commandID int, adminUsername string) (
|
|||||||
log.Printf("📋 [ValidateAtomic] Commande trouvée - status=%s, client=%s, livreur=%s",
|
log.Printf("📋 [ValidateAtomic] Commande trouvée - status=%s, client=%s, livreur=%s",
|
||||||
cmd.Status, cmd.Username, cmd.LivreurAssign)
|
cmd.Status, cmd.Username, cmd.LivreurAssign)
|
||||||
|
|
||||||
validStatuses := []string{"assigned", "en_route", "pending", "livre"}
|
validStatuses := []string{"assigned", "en_route", "arrived", "pending", "livre"}
|
||||||
if !slices.Contains(validStatuses, cmd.Status) {
|
if !slices.Contains(validStatuses, cmd.Status) {
|
||||||
log.Printf("❌ [ValidateAtomic] Statut invalide pour validation: %s", cmd.Status)
|
log.Printf("❌ [ValidateAtomic] Statut invalide pour validation: %s", cmd.Status)
|
||||||
return fmt.Errorf("statut invalide pour validation: %s", cmd.Status)
|
return fmt.Errorf("statut invalide pour validation: %s", cmd.Status)
|
||||||
|
|||||||
@@ -670,7 +670,7 @@ func ValidateDelivery(c *gin.Context) {
|
|||||||
|
|
||||||
currentStatus, _ := command["status"].(string)
|
currentStatus, _ := command["status"].(string)
|
||||||
|
|
||||||
validStatuses := []string{"assigned", "en_route", "pending", "livre"}
|
validStatuses := []string{"assigned", "en_route", "arrived", "pending", "livre"}
|
||||||
if !slices.Contains(validStatuses, currentStatus) {
|
if !slices.Contains(validStatuses, currentStatus) {
|
||||||
failed = append(failed, gin.H{
|
failed = append(failed, gin.H{
|
||||||
"command_id": commandID,
|
"command_id": commandID,
|
||||||
|
|||||||
@@ -12,18 +12,6 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// CONSTANTES DE CONFIGURATION
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
const (
|
|
||||||
// Distance maximale en mètres pour valider une livraison
|
|
||||||
MAX_DELIVERY_VALIDATION_DISTANCE_METERS = 100 // 100 mètres
|
|
||||||
|
|
||||||
// Distance maximale en kilomètres
|
|
||||||
MAX_DELIVERY_VALIDATION_DISTANCE_KM = 0.1 // 100 mètres = 0.1 km
|
|
||||||
)
|
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// 3️⃣ DÉMARRER UNE LIVRAISON (PASSER EN IN_ROUTE)
|
// 3️⃣ DÉMARRER UNE LIVRAISON (PASSER EN IN_ROUTE)
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|||||||
@@ -14,10 +14,13 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
// UpdateDeliveryStatus (handlers/deleviry.go) refuse de valider une livraison
|
// UpdateDeliveryStatus (handlers/deleviry.go) n'impose plus aucune limite de
|
||||||
// (statut "livre") si le livreur se trouve à plus de 350m de la destination
|
// distance entre le livreur et la destination pour valider une livraison
|
||||||
// (contrôle anti-fraude — seuil relevé de 100m à 350m à la demande explicite,
|
// (statut "livre") — la vérification GPS a été volontairement retirée pour ne
|
||||||
// pour tolérer l'imprécision GPS réelle en zone urbaine/immeuble).
|
// pas bloquer le livreur (l'imprécision GPS réelle en zone urbaine/immeuble
|
||||||
|
// provoquait des rejets sur des livraisons pourtant légitimes). Les
|
||||||
|
// coordonnées GPS restent obligatoires et la distance est toujours calculée
|
||||||
|
// et loguée à des fins de suivi, mais elle n'entraîne plus de rejet.
|
||||||
|
|
||||||
const earthRadiusMeters = 6371000.0
|
const earthRadiusMeters = 6371000.0
|
||||||
|
|
||||||
@@ -58,7 +61,7 @@ func deliveryStatusContextJSON(username string, commandID int, body []byte) (*gi
|
|||||||
|
|
||||||
const nantesLat, nantesLon = 47.2184, -1.5536
|
const nantesLat, nantesLon = 47.2184, -1.5536
|
||||||
|
|
||||||
func TestUpdateDeliveryStatus_GPS_WithinThresholdValidatesDelivery(t *testing.T) {
|
func TestUpdateDeliveryStatus_GPS_ValidatesDeliveryAtModerateDistance(t *testing.T) {
|
||||||
cleanupStockTestData(t)
|
cleanupStockTestData(t)
|
||||||
livreur := newTestClient(t, "gps_livreur_within")
|
livreur := newTestClient(t, "gps_livreur_within")
|
||||||
client := newTestClient(t, "gps_client_within")
|
client := newTestClient(t, "gps_client_within")
|
||||||
@@ -66,7 +69,7 @@ func TestUpdateDeliveryStatus_GPS_WithinThresholdValidatesDelivery(t *testing.T)
|
|||||||
cmdID := newTestCommandWithItem(t, client, "en_route", livreur, productID, 1, 10)
|
cmdID := newTestCommandWithItem(t, client, "en_route", livreur, productID, 1, 10)
|
||||||
setCommandDestination(t, cmdID, nantesLat, nantesLon)
|
setCommandDestination(t, cmdID, nantesLat, nantesLon)
|
||||||
|
|
||||||
livreurLat, livreurLon := destinationPointNorthOf(nantesLat, nantesLon, 200) // 200m < 350m
|
livreurLat, livreurLon := destinationPointNorthOf(nantesLat, nantesLon, 200)
|
||||||
body, _ := json.Marshal(map[string]any{"status": "livre", "latitude": livreurLat, "longitude": livreurLon})
|
body, _ := json.Marshal(map[string]any{"status": "livre", "latitude": livreurLat, "longitude": livreurLon})
|
||||||
c, rec := deliveryStatusContextJSON(livreur, cmdID, body)
|
c, rec := deliveryStatusContextJSON(livreur, cmdID, body)
|
||||||
handlers.UpdateDeliveryStatus(c)
|
handlers.UpdateDeliveryStatus(c)
|
||||||
@@ -79,47 +82,27 @@ func TestUpdateDeliveryStatus_GPS_WithinThresholdValidatesDelivery(t *testing.T)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUpdateDeliveryStatus_GPS_BeyondThresholdRejectsValidation(t *testing.T) {
|
// Aucune distance, aussi grande soit-elle, ne doit bloquer la validation : la
|
||||||
|
// vérification GPS a été retirée pour ne jamais empêcher un livreur de
|
||||||
|
// marquer une commande "livre".
|
||||||
|
func TestUpdateDeliveryStatus_GPS_FarBeyondOldThresholdStillValidatesDelivery(t *testing.T) {
|
||||||
cleanupStockTestData(t)
|
cleanupStockTestData(t)
|
||||||
livreur := newTestClient(t, "gps_livreur_beyond")
|
livreur := newTestClient(t, "gps_livreur_far")
|
||||||
client := newTestClient(t, "gps_client_beyond")
|
client := newTestClient(t, "gps_client_far")
|
||||||
productID := newTestProduct(t, "GPSBeyond", 10)
|
productID := newTestProduct(t, "GPSFar", 10)
|
||||||
cmdID := newTestCommandWithItem(t, client, "en_route", livreur, productID, 1, 10)
|
cmdID := newTestCommandWithItem(t, client, "en_route", livreur, productID, 1, 10)
|
||||||
setCommandDestination(t, cmdID, nantesLat, nantesLon)
|
setCommandDestination(t, cmdID, nantesLat, nantesLon)
|
||||||
|
|
||||||
livreurLat, livreurLon := destinationPointNorthOf(nantesLat, nantesLon, 400) // 400m > 350m
|
livreurLat, livreurLon := destinationPointNorthOf(nantesLat, nantesLon, 5000) // 5km : loin de toute ancienne limite
|
||||||
body, _ := json.Marshal(map[string]any{"status": "livre", "latitude": livreurLat, "longitude": livreurLon})
|
|
||||||
c, rec := deliveryStatusContextJSON(livreur, cmdID, body)
|
|
||||||
handlers.UpdateDeliveryStatus(c)
|
|
||||||
|
|
||||||
if rec.Code != http.StatusBadRequest {
|
|
||||||
t.Fatalf("status HTTP: got=%d want=%d body=%s", rec.Code, http.StatusBadRequest, rec.Body.String())
|
|
||||||
}
|
|
||||||
if got := commandStatus(t, cmdID); got != "en_route" {
|
|
||||||
t.Errorf("le statut ne doit pas passer à 'livre' au-delà de 350m: got=%s want=en_route", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Preuve directe du changement demandé : une distance de 150m, qui aurait
|
|
||||||
// échoué sous l'ancien seuil de 100m, doit maintenant réussir sous 350m.
|
|
||||||
func TestUpdateDeliveryStatus_GPS_150Meters_PassesUnderNewThreshold(t *testing.T) {
|
|
||||||
cleanupStockTestData(t)
|
|
||||||
livreur := newTestClient(t, "gps_livreur_150m")
|
|
||||||
client := newTestClient(t, "gps_client_150m")
|
|
||||||
productID := newTestProduct(t, "GPS150m", 10)
|
|
||||||
cmdID := newTestCommandWithItem(t, client, "en_route", livreur, productID, 1, 10)
|
|
||||||
setCommandDestination(t, cmdID, nantesLat, nantesLon)
|
|
||||||
|
|
||||||
livreurLat, livreurLon := destinationPointNorthOf(nantesLat, nantesLon, 150)
|
|
||||||
body, _ := json.Marshal(map[string]any{"status": "livre", "latitude": livreurLat, "longitude": livreurLon})
|
body, _ := json.Marshal(map[string]any{"status": "livre", "latitude": livreurLat, "longitude": livreurLon})
|
||||||
c, rec := deliveryStatusContextJSON(livreur, cmdID, body)
|
c, rec := deliveryStatusContextJSON(livreur, cmdID, body)
|
||||||
handlers.UpdateDeliveryStatus(c)
|
handlers.UpdateDeliveryStatus(c)
|
||||||
|
|
||||||
if rec.Code != http.StatusOK {
|
if rec.Code != http.StatusOK {
|
||||||
t.Fatalf("150m doit être accepté sous le nouveau seuil de 350m: got=%d body=%s", rec.Code, rec.Body.String())
|
t.Fatalf("aucune distance ne doit bloquer la validation: got=%d body=%s", rec.Code, rec.Body.String())
|
||||||
}
|
}
|
||||||
if got := commandStatus(t, cmdID); got != "livre" {
|
if got := commandStatus(t, cmdID); got != "livre" {
|
||||||
t.Errorf("statut après validation à 150m: got=%s want=livre", got)
|
t.Errorf("statut après validation à 5km: got=%s want=livre", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ import (
|
|||||||
// destination périmées) empêchait le livreur d'atteindre 'livre', laissant le
|
// destination périmées) empêchait le livreur d'atteindre 'livre', laissant le
|
||||||
// staff sans recours pour confirmer une commande par ailleurs légitime — voir
|
// staff sans recours pour confirmer une commande par ailleurs légitime — voir
|
||||||
// db.ApproveDeliveryAtomicByStaff. Tout statut non terminal est accepté — y
|
// db.ApproveDeliveryAtomicByStaff. Tout statut non terminal est accepté — y
|
||||||
// compris "arrived", que ValidateDeliveryAtomic (validation admin en masse,
|
// compris "arrived". ValidateDeliveryAtomic (validation admin en masse)
|
||||||
// non modifiée) continue lui de rejeter.
|
// accepte désormais "arrived" aussi, pour la même raison (voir plus bas).
|
||||||
|
|
||||||
func TestApproveDeliveryAtomicByStaff_CreditsPointsExactlyOnApproval(t *testing.T) {
|
func TestApproveDeliveryAtomicByStaff_CreditsPointsExactlyOnApproval(t *testing.T) {
|
||||||
cleanupStockTestData(t)
|
cleanupStockTestData(t)
|
||||||
@@ -183,9 +183,12 @@ func TestApproveDeliveryAtomicByStaff_NoOwnershipCheck_AnyStaffCanConfirmAnyClie
|
|||||||
// Divergence de règle métier volontaire (confirmée) : contrairement aux deux
|
// Divergence de règle métier volontaire (confirmée) : contrairement aux deux
|
||||||
// autres chemins d'approbation (client et staff), qui exigent tous deux le
|
// autres chemins d'approbation (client et staff), qui exigent tous deux le
|
||||||
// statut 'livre', ValidateDeliveryAtomic accepte "pending", "assigned",
|
// statut 'livre', ValidateDeliveryAtomic accepte "pending", "assigned",
|
||||||
// "en_route" ET "livre" — c'est un override admin assumé pour régulariser une
|
// "en_route", "arrived" ET "livre" — c'est un override admin assumé pour
|
||||||
// commande gérée hors flux normal, pas un bug. Les tests suivants documentent
|
// régulariser une commande gérée hors flux normal, pas un bug. "arrived" a été
|
||||||
// ce comportement réel pour qu'une future régression involontaire soit détectée.
|
// ajouté pour que l'admin puisse toujours finaliser une commande arrivée à
|
||||||
|
// destination (le seul bouton de finalisation côté admin passe par ce chemin),
|
||||||
|
// à l'image de ApproveDeliveryAtomicByStaff. Les tests suivants documentent ce
|
||||||
|
// comportement réel pour qu'une future régression involontaire soit détectée.
|
||||||
|
|
||||||
func TestValidateDeliveryAtomic_AcceptsAllDocumentedStatusesAndCreditsPoints(t *testing.T) {
|
func TestValidateDeliveryAtomic_AcceptsAllDocumentedStatusesAndCreditsPoints(t *testing.T) {
|
||||||
cleanupStockTestData(t)
|
cleanupStockTestData(t)
|
||||||
@@ -193,7 +196,7 @@ func TestValidateDeliveryAtomic_AcceptsAllDocumentedStatusesAndCreditsPoints(t *
|
|||||||
{Key: "pool_0", Name: "Pool Test", Categories: []string{"test"}, Tiers: []models.PointsTier{{Min: 0, Max: 0, Points: 6}}},
|
{Key: "pool_0", Name: "Pool Test", Categories: []string{"test"}, Tiers: []models.PointsTier{{Min: 0, Max: 0, Points: 6}}},
|
||||||
})
|
})
|
||||||
|
|
||||||
for _, status := range []string{"pending", "assigned", "en_route", "livre"} {
|
for _, status := range []string{"pending", "assigned", "en_route", "arrived", "livre"} {
|
||||||
t.Run(status, func(t *testing.T) {
|
t.Run(status, func(t *testing.T) {
|
||||||
username := newTestClient(t, "validate_status_"+status)
|
username := newTestClient(t, "validate_status_"+status)
|
||||||
productID := newTestProduct(t, "ValidateStatus"+status, 20)
|
productID := newTestProduct(t, "ValidateStatus"+status, 20)
|
||||||
@@ -218,7 +221,7 @@ func TestValidateDeliveryAtomic_AcceptsAllDocumentedStatusesAndCreditsPoints(t *
|
|||||||
|
|
||||||
func TestValidateDeliveryAtomic_RejectsStatusOutsideAllowedList(t *testing.T) {
|
func TestValidateDeliveryAtomic_RejectsStatusOutsideAllowedList(t *testing.T) {
|
||||||
cleanupStockTestData(t)
|
cleanupStockTestData(t)
|
||||||
for _, status := range []string{"cancelled", "pending_payment", "arrived"} {
|
for _, status := range []string{"cancelled", "pending_payment"} {
|
||||||
t.Run(status, func(t *testing.T) {
|
t.Run(status, func(t *testing.T) {
|
||||||
username := newTestClient(t, "validate_invalid_"+status)
|
username := newTestClient(t, "validate_invalid_"+status)
|
||||||
productID := newTestProduct(t, "ValidateInvalid"+status, 20)
|
productID := newTestProduct(t, "ValidateInvalid"+status, 20)
|
||||||
|
|||||||
Reference in New Issue
Block a user