chore: update custom-rules & update style & script data

This commit is contained in:
2026-01-21 21:44:02 +01:00
parent f46a5d9652
commit c3ac31917a
11 changed files with 862 additions and 603 deletions
-30
View File
@@ -196,33 +196,3 @@
group: "{{ docker_group }}" group: "{{ docker_group }}"
mode: "0640" mode: "0640"
ignore_errors: yes ignore_errors: yes
- name: Build Docker project
ansible.builtin.command:
cmd: docker compose -f docker-compose-prod.yml build
chdir: "{{ docker_dir }}/docker"
become: yes
become_user: "{{ docker_user }}"
tags: build
- name: Start Docker project
ansible.builtin.command:
cmd: docker compose -f docker-compose-prod.yml up -d
chdir: "{{ docker_dir }}/docker"
become: yes
become_user: "{{ docker_user }}"
tags: start
- name: Show Docker containers
ansible.builtin.command:
cmd: docker compose ps
chdir: "{{ docker_dir }}/docker"
become: yes
become_user: "{{ docker_user }}"
register: docker_ps
tags: start
- name: Display Docker containers
debug:
var: docker_ps.stdout_lines
tags: start
+8
View File
@@ -1,3 +1,11 @@
SecRuleRemoveById 932235
SecRuleRemoveById 911100
SecRule REQUEST_URI "@streq /api/v2/admin/protected/products" \
"id:399002,phase:2,nolog,pass,\
ctl:ruleRemoveById=920120,\
ctl:ruleRemoveById=920121"
SecRule IP:BANNED "@eq 1" \ SecRule IP:BANNED "@eq 1" \
"id:100000,phase:1,deny,status:403,log,\ "id:100000,phase:1,deny,status:403,log,\
msg:'IP is banned'" msg:'IP is banned'"
+1 -1
View File
@@ -5,7 +5,7 @@
// ✅ loginUser et registerUser retournent AuthResponse // ✅ loginUser et registerUser retournent AuthResponse
// ✅ sessionStorage (pas localStorage) // ✅ sessionStorage (pas localStorage)
const API_URL = "http://localhost:8080/api/v1"; const API_URL = "/api/v1";
import type { import type {
ConfirmReceptionResponse, ConfirmReceptionResponse,
CheckoutCartResponse, CheckoutCartResponse,
+1 -55
View File
@@ -15,7 +15,7 @@ import type {
DeliveryPersonDetails, DeliveryPersonDetails,
DeliveryPersonStats, DeliveryPersonStats,
} from "./api_admin_types"; } from "./api_admin_types";
const API_URL = "http://localhost:8080/api/v2"; const API_URL = "/api/v2";
// ============================================ // ============================================
// 🔐 TYPES - ADMIN // 🔐 TYPES - ADMIN
@@ -825,59 +825,6 @@ export const getCommandByID = async (commandId: number) => {
} }
}; };
/**
* ✅ Récupérer les items d'une commande
*/
export const getCommandItems = async (commandId: number) => {
const token = sessionStorage.getItem("admin_token");
if (!token) {
throw new Error("Token admin non trouvé");
}
try {
console.log("🔍 [GET_COMMAND_ITEMS] Appel:", commandId);
// Utiliser l'endpoint cabine qui est accessible avec le token admin
const response = await fetch(
`http://localhost:8080/api/v1/cabine/commands/${commandId}/items`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
},
);
const data = await response.json();
if (!response.ok) {
console.error("❌ [GET_COMMAND_ITEMS] Erreur API:", data);
return {
success: false,
items: [],
};
}
console.log("✅ [GET_COMMAND_ITEMS] Réponse:", data);
return {
success: true,
items: data.items || [],
count: data.count || 0,
command_info: data.command_info,
client_info: data.client_info,
};
} catch (error) {
console.error("❌ [GET_COMMAND_ITEMS] Erreur fetch:", error);
return {
success: false,
items: [],
};
}
};
/** /**
* ✅ Mettre à jour le statut d'une commande * ✅ Mettre à jour le statut d'une commande
*/ */
@@ -2653,7 +2600,6 @@ export default {
// Commands // Commands
getAllCommands, getAllCommands,
getCommandByID, getCommandByID,
getCommandItems,
getCommandCount, getCommandCount,
getCommandCountCompleted, getCommandCountCompleted,
getCommandCountInRoute, getCommandCountInRoute,
+1 -1
View File
@@ -5,7 +5,7 @@
// ✅ Utilise /api/v1/cabine/* endpoints uniquement // ✅ Utilise /api/v1/cabine/* endpoints uniquement
// ✅ sessionStorage pour la persistance // ✅ sessionStorage pour la persistance
const API_URL = "http://localhost:8080/api/v1/cabine"; const API_URL = "/api/v1/cabine";
import type { import type {
DeliveryPerson, DeliveryPerson,
DeliveryPersonsStats, DeliveryPersonsStats,
+1 -1
View File
@@ -4,7 +4,7 @@
// ✅ Fonctions helper pour le dashboard livreur // ✅ Fonctions helper pour le dashboard livreur
// ✅ Utilise /api/v1/livreur/* endpoints // ✅ Utilise /api/v1/livreur/* endpoints
const API_URL = "http://localhost:8080/api/v1/livreur"; const API_URL = "/api/v1/livreur";
// ============================================ // ============================================
// 🔐 TYPES - LIVREUR // 🔐 TYPES - LIVREUR
@@ -17,6 +17,7 @@ import "./AdminAlerts.css";
import Sidebar from "../../components/Sidebar"; import Sidebar from "../../components/Sidebar";
import { isAdminAuthenticated, deleteAlertAdmin } from "../../api/api_admin"; import { isAdminAuthenticated, deleteAlertAdmin } from "../../api/api_admin";
import { getAllAlerts, getAlertDetails } from "../../api/api_cabine"; import { getAllAlerts, getAlertDetails } from "../../api/api_cabine";
import Toast from "../../components/Toast";
interface Alert { interface Alert {
id: number; id: number;
@@ -32,6 +33,12 @@ interface AlertStats {
resolved: number; resolved: number;
} }
interface ToastState {
show: boolean;
message: string;
type: "success" | "error" | "warning" | "info";
}
function AdminAlerts() { function AdminAlerts() {
const navigate = useNavigate(); const navigate = useNavigate();
@@ -54,6 +61,26 @@ function AdminAlerts() {
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [alertToDelete, setAlertToDelete] = useState<number | null>(null); const [alertToDelete, setAlertToDelete] = useState<number | null>(null);
// État pour le Toast
const [toast, setToast] = useState<ToastState>({
show: false,
message: "",
type: "success",
});
// Fonction pour afficher un toast
const showToast = (
message: string,
type: "success" | "error" | "warning" | "info" = "success",
) => {
setToast({ show: true, message, type });
};
// Fonction pour fermer le toast
const handleCloseToast = () => {
setToast({ ...toast, show: false });
};
// ============================================ // ============================================
// 🔐 VÉRIFICATION AUTHENTIFICATION // 🔐 VÉRIFICATION AUTHENTIFICATION
// ============================================ // ============================================
@@ -249,11 +276,14 @@ function AdminAlerts() {
setSelectedAlert(result.alert as Alert); setSelectedAlert(result.alert as Alert);
setShowDetailsModal(true); setShowDetailsModal(true);
} else { } else {
alert("Impossible de récupérer les détails"); showToast(
"Impossible de récupérer les détails de l'alerte",
"error",
);
} }
} catch (error) { } catch (error) {
console.error("❌ Erreur récupération détails:", error); console.error("❌ Erreur récupération détails:", error);
alert("Erreur lors de la récupération des détails"); showToast("Erreur lors de la récupération des détails", "error");
} }
}; };
@@ -281,15 +311,21 @@ function AdminAlerts() {
setShowDeleteConfirm(false); setShowDeleteConfirm(false);
setAlertToDelete(null); setAlertToDelete(null);
// Message de succès // Afficher le toast de succès
alert("Alerte supprimée avec succès"); showToast(
`Alerte #${alertToDelete} supprimée avec succès`,
"success",
);
} else { } else {
console.error("❌ [DELETE_ALERT] Erreur:", result.error); console.error("❌ [DELETE_ALERT] Erreur:", result.error);
alert(result.error || "Erreur lors de la suppression"); showToast(
result.error || "Erreur lors de la suppression",
"error",
);
} }
} catch (error) { } catch (error) {
console.error("❌ [DELETE_ALERT] Erreur:", error); console.error("❌ [DELETE_ALERT] Erreur:", error);
alert("Erreur lors de la suppression de l'alerte"); showToast("Erreur lors de la suppression de l'alerte", "error");
} }
}; };
@@ -673,6 +709,16 @@ function AdminAlerts() {
</div> </div>
</div> </div>
)} )}
{/* Toast Notifications */}
{toast.show && (
<Toast
message={toast.message}
type={toast.type}
duration={3000}
onClose={handleCloseToast}
/>
)}
</div> </div>
</> </>
); );
@@ -20,11 +20,10 @@ import AdminLayout from "../../components/AdminLayout";
import "./AdminOrders.css"; import "./AdminOrders.css";
import { import {
getAllCommands, getAllCommands,
getCommandItems,
getDeliverymanLocationForCommand, getDeliverymanLocationForCommand,
isAdminAuthenticated, isAdminAuthenticated,
} from "../../api/api_admin"; } from "../../api/api_admin";
import { getCommandItems } from "../../api/api_cabine";
import type { DeliverymanLocationResponse } from "../../api/api_admin_types"; import type { DeliverymanLocationResponse } from "../../api/api_admin_types";
// Interface correspondant au backend // Interface correspondant au backend
@@ -24,13 +24,13 @@ import SidebarCabine from "../../components/SidebarCabine";
import "./CabineOrders.css"; import "./CabineOrders.css";
import { import {
getAllCommands, getAllCommands,
getCommandItems,
getDeliverymanLocationForCommand, getDeliverymanLocationForCommand,
} from "../../api/api_admin"; } from "../../api/api_admin";
import { import {
deleteCommand, deleteCommand,
isCabineAuthenticated, // ⭐ AJOUT isCabineAuthenticated,
getCommandItems,
} from "../../api/api_cabine"; } from "../../api/api_cabine";
import type { DeliverymanLocationResponse } from "../../api/api_admin_types"; import type { DeliverymanLocationResponse } from "../../api/api_admin_types";
@@ -36,7 +36,11 @@
align-items: center; align-items: center;
gap: 1rem; gap: 1rem;
padding: 1rem 1.5rem; padding: 1rem 1.5rem;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.03) 0%, rgba(255, 255, 255, 0.01) 100%); background: linear-gradient(
135deg,
rgba(255, 255, 255, 0.03) 0%,
rgba(255, 255, 255, 0.01) 100%
);
border: 1px solid rgba(255, 255, 255, 0.08); border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 12px; border-radius: 12px;
backdrop-filter: blur(8px); backdrop-filter: blur(8px);
@@ -78,7 +82,11 @@
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
padding: 0.75rem 1.5rem; padding: 0.75rem 1.5rem;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.03) 0%, rgba(255, 255, 255, 0.01) 100%); background: linear-gradient(
135deg,
rgba(255, 255, 255, 0.03) 0%,
rgba(255, 255, 255, 0.01) 100%
);
border: 1px solid rgba(255, 255, 255, 0.08); border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 10px; border-radius: 10px;
color: #888; color: #888;
@@ -90,7 +98,11 @@
} }
.filter-btn:hover { .filter-btn:hover {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.05) 0%, rgba(255, 255, 255, 0.02) 100%); background: linear-gradient(
135deg,
rgba(255, 255, 255, 0.05) 0%,
rgba(255, 255, 255, 0.02) 100%
);
border-color: rgba(59, 130, 246, 0.3); border-color: rgba(59, 130, 246, 0.3);
color: white; color: white;
transform: translateY(-2px); transform: translateY(-2px);
@@ -141,7 +153,11 @@
.user-card { .user-card {
position: relative; position: relative;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.03) 0%, rgba(255, 255, 255, 0.01) 100%); background: linear-gradient(
135deg,
rgba(255, 255, 255, 0.03) 0%,
rgba(255, 255, 255, 0.01) 100%
);
border: 1px solid rgba(255, 255, 255, 0.08); border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 16px; border-radius: 16px;
padding: 1.5rem; padding: 1.5rem;
@@ -153,19 +169,28 @@
} }
.user-card::before { .user-card::before {
content: ''; content: "";
position: absolute; position: absolute;
top: 0; top: 0;
left: -100%; left: -100%;
width: 100%; width: 100%;
height: 100%; height: 100%;
background: linear-gradient(90deg, transparent, rgba(59, 130, 246, 0.1), transparent); background: linear-gradient(
90deg,
transparent,
rgba(59, 130, 246, 0.1),
transparent
);
transition: left 0.6s cubic-bezier(0.4, 0, 0.2, 1); transition: left 0.6s cubic-bezier(0.4, 0, 0.2, 1);
pointer-events: none; pointer-events: none;
} }
.user-card:hover { .user-card:hover {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.05) 0%, rgba(255, 255, 255, 0.02) 100%); background: linear-gradient(
135deg,
rgba(255, 255, 255, 0.05) 0%,
rgba(255, 255, 255, 0.02) 100%
);
border-color: rgba(59, 130, 246, 0.3); border-color: rgba(59, 130, 246, 0.3);
box-shadow: 0 12px 32px rgba(59, 130, 246, 0.2); box-shadow: 0 12px 32px rgba(59, 130, 246, 0.2);
transform: translateY(-2px); transform: translateY(-2px);
@@ -197,7 +222,11 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: linear-gradient(135deg, rgba(59, 130, 246, 0.2), rgba(37, 99, 235, 0.1)); background: linear-gradient(
135deg,
rgba(59, 130, 246, 0.2),
rgba(37, 99, 235, 0.1)
);
color: #3b82f6; color: #3b82f6;
box-shadow: 0 8px 24px rgba(59, 130, 246, 0.3); box-shadow: 0 8px 24px rgba(59, 130, 246, 0.3);
transition: transform 0.3s ease; transition: transform 0.3s ease;
@@ -316,14 +345,20 @@
/* ============================================ */ /* ============================================ */
.edit-modal-content { .edit-modal-content {
background: linear-gradient(135deg, rgba(20, 20, 20, 0.98) 0%, rgba(15, 15, 15, 0.95) 100%); background: linear-gradient(
135deg,
rgba(20, 20, 20, 0.98) 0%,
rgba(15, 15, 15, 0.95) 100%
);
border: 1px solid rgba(59, 130, 246, 0.3); border: 1px solid rgba(59, 130, 246, 0.3);
border-radius: 24px; border-radius: 24px;
width: 100%; width: 100%;
max-width: 500px; max-width: 500px;
max-height: 90vh; max-height: 90vh;
overflow-y: auto; overflow-y: auto;
box-shadow: 0 24px 48px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(59, 130, 246, 0.2); box-shadow:
0 24px 48px rgba(0, 0, 0, 0.5),
0 0 0 1px rgba(59, 130, 246, 0.2);
animation: slideUp 0.3s ease; animation: slideUp 0.3s ease;
} }
@@ -333,7 +368,11 @@
align-items: center; align-items: center;
padding: 2rem; padding: 2rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1); border-bottom: 1px solid rgba(255, 255, 255, 0.1);
background: linear-gradient(135deg, rgba(59, 130, 246, 0.1) 0%, transparent 100%); background: linear-gradient(
135deg,
rgba(59, 130, 246, 0.1) 0%,
transparent 100%
);
} }
.modal-header-title { .modal-header-title {
@@ -563,7 +602,8 @@
} }
@keyframes pulse { @keyframes pulse {
0%, 100% { 0%,
100% {
opacity: 0.6; opacity: 0.6;
} }
50% { 50% {
@@ -639,7 +679,7 @@
@media (max-width: 480px) { @media (max-width: 480px) {
.filter-btn { .filter-btn {
padding: 0.6rem 1rem; padding: 10px 13px;
font-size: 0.85rem; font-size: 0.85rem;
} }
@@ -709,16 +749,27 @@
} }
.client-card { .client-card {
background: linear-gradient(135deg, rgba(16, 185, 129, 0.05), rgba(16, 185, 129, 0.02)); background: linear-gradient(
border-left: 3px solid #10b981; 135deg,
rgba(255, 255, 255, 0.03) 0%,
rgba(255, 255, 255, 0.01) 100%
);
} }
.user-avatar.client { .user-avatar.client {
background: linear-gradient(135deg, #10b981, #059669); background: linear-gradient(
135deg,
rgba(59, 130, 246, 0.2),
rgba(37, 99, 235, 0.1)
);
} }
.user-role-badge.green { .user-role-badge.green {
background: linear-gradient(135deg, rgba(16, 185, 129, 0.2), rgba(16, 185, 129, 0.1)); background: linear-gradient(
135deg,
rgba(16, 185, 129, 0.2),
rgba(16, 185, 129, 0.1)
);
color: #10b981; color: #10b981;
border: 1px solid rgba(16, 185, 129, 0.3); border: 1px solid rgba(16, 185, 129, 0.3);
} }
Executable
+239
View File
@@ -0,0 +1,239 @@
#!/bin/bash
# ============================================
# SCRIPT DE SIMULATION RÉALISTE - E-COMMERCE
# Version avec validation GPS + Simulation déplacement livreur
# ============================================
BASE_URL="https://d437671454fb1f66-90-50-148-138.serveousercontent.com"
ADMIN_TOKEN=""
CABINE_TOKEN=""
LIVREUR_TOKEN=""
NUM_PRODUCTS=13
# Couleurs pour l'affichage
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
MAGENTA='\033[0;35m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
declare -A PRODUCT_DETAILS
declare -a PRODUCT_IDS
# Compteurs de tests
TOTAL_TESTS=0
PASSED_TESTS=0
FAILED_TESTS=0
# Fonctions d'affichage
print_section() {
echo ""
echo -e "${BLUE}================================================${NC}"
echo -e "${BLUE}$1${NC}"
echo -e "${BLUE}================================================${NC}"
}
print_test() {
echo ""
echo -e "${YELLOW}$1${NC}"
((TOTAL_TESTS++))
}
print_success() {
echo -e "${GREEN}$1${NC}"
((PASSED_TESTS++))
}
print_error() {
echo -e "${RED}$1${NC}"
((FAILED_TESTS++))
}
print_info() {
echo -e "${MAGENTA} $1${NC}"
}
print_warning() {
echo -e "${YELLOW}$1${NC}"
}
# Fonction pour extraire le token
extract_token() {
local response="$1"
local token=$(echo "$response" | jq -r '.token // .access_token // empty' 2>/dev/null)
echo "$token"
}
# Fonction pour extraire un ID
extract_id() {
local response="$1"
local field="${2:-id}"
local id=$(echo "$response" | jq -r ".${field} // .product.id // .product_id // empty" 2>/dev/null)
echo "$id"
}
# Fonction pour vérifier les erreurs
check_error() {
local response="$1"
if echo "$response" | jq -e '.error' >/dev/null 2>&1; then
return 0
else
return 1
fi
}
# Fonction pour vérifier le succès
check_success() {
local response="$1"
if echo "$response" | jq -e '.id // .token // .success // .command_id' >/dev/null 2>&1; then
return 0
else
return 1
fi
}
# ============================================
# 0. VÉRIFICATION PRÉALABLE
# ============================================
print_section "0. VÉRIFICATION PRÉALABLE"
print_test "Vérification du serveur"
HEALTH_CHECK=$(curl -s -X GET "$BASE_URL/api/v1/health")
if [ -n "$HEALTH_CHECK" ]; then
print_success "Serveur accessible"
else
print_error "Serveur non accessible"
exit 1
fi
# ============================================
# 1. AUTHENTIFICATION ADMIN
# ============================================
print_section "0. AUTHENTIFICATION CLIENT"
CLIENT_TOKEN=$(extract_token "$CLIENT_LOGIN")
print_test "Création client"
CLIENT_REGISTER=$(curl -s -X POST "$BASE_URL/api/v1/auth/register" \
-H "Content-Type: application/json" \
-d "{\"username\":\"salut\",\"password\":\"salut1234_\",\"role\":\"client\",\"nom\": \"Dupont\",\"prenom\": \"Jean\",\"telephone\": \"+33612345678\"}")
CLIENT_TOKEN=$(extract_token "$CLIENT_REGISTER")
if [ -n "$CLIENT_TOKEN" ]; then
print_success "Client créé et authentifié"
else
print_error "Échec authentification client"
exit 1
fi
# ============================================
# 1. AUTHENTIFICATION ADMIN
# ============================================
print_section "1. AUTHENTIFICATION ADMIN"
ADMIN_TOKEN=$(extract_token "$ADMIN_LOGIN")
print_test "Création admin"
UNIQUE_ADMIN="admin_$(date +%s)"
ADMIN_REGISTER=$(curl -s -X POST "$BASE_URL/api/v2/admin/auth/register" \
-H "Content-Type: application/json" \
-d "{\"username\":\"$UNIQUE_ADMIN\",\"password\":\"AdminPass123!\",\"role\":\"admin\"}")
ADMIN_TOKEN=$(extract_token "$ADMIN_REGISTER")
if [ -n "$ADMIN_TOKEN" ]; then
print_success "Admin créé et authentifié"
else
print_error "Échec authentification admin"
exit 1
fi
print_test "Création livreur"
UNIQUE_LIVREUR="livreur_$(date +%s)"
ADMIN_REGISTER=$(curl -s -X POST "$BASE_URL/api/v2/admin/auth/register" \
-H "Content-Type: application/json" \
-d "{\"username\":\"$UNIQUE_LIVREUR\",\"password\":\"AdminPass123!\",\"role\":\"livreur\"}")
LIVREUR_TOKEN=$(extract_token "$ADMIN_REGISTER")
if [ -n "$LIVREUR_TOKEN" ]; then
print_success "Admin créé et authentifié"
else
print_error "Échec authentification admin"
exit 1
fi
print_test "Création cabine"
UNIQUE_CABINE="cabine_$(date +%s)"
ADMIN_REGISTER=$(curl -s -X POST "$BASE_URL/api/v2/admin/auth/register" \
-H "Content-Type: application/json" \
-d "{\"username\":\"$UNIQUE_CABINE\",\"password\":\"AdminPass123!\",\"role\":\"cabine\"}")
CABINE_TOKEN=$(extract_token "$ADMIN_REGISTER")
if [ -n "$CABINE_TOKEN" ]; then
print_success "Admin créé et authentifié"
else
print_error "Échec authentification admin"
exit 1
fi
# ============================================
# 2. CRÉATION MASSIVE DE PRODUITS
# ============================================
print_section "2. CRÉATION MASSIVE DE PRODUITS"
print_info "Création de $NUM_PRODUCTS produits..."
PREDEFINED_PRODUCTS=(
'{"name":"Zipette Borealis 1", "category":"zipette&co", "description":"Plante luminescente aux teintes bleutées inspirée des aurores boréales", "price":24.99, "stock":35.00}'
'{"name":"Zipette Titan 1", "category":"zipette&co", "description":"Plante géante fictive atteignant jusqu'\''à 2 mètres en intérieur", "price":19.99, "stock":50.00}'
'{"name":"Zipette Borealis 2", "category":"zipette&co", "description":"Plante luminescente aux teintes bleutées inspirée des aurores boréales", "price":24.99, "stock":35.00}'
'{"name":"Zipette Titan 2", "category":"zipette&co", "description":"Plante géante fictive atteignant jusqu'\''à 2 mètres en intérieur", "price":19.99, "stock":50.00}'
'{"name":"Zipette Borealis 3", "category":"zipette&co", "description":"Plante luminescente aux teintes bleutées inspirée des aurores boréales", "price":24.99, "stock":35.00}'
'{"name":"Zipette Titan 3", "category":"zipette&co", "description":"Plante géante fictive atteignant jusqu'\''à 2 mètres en intérieur", "price":19.99, "stock":50.00}'
'{"name":"Titan 4", "category":"gros&semi", "description":"Plante géante fictive atteignant jusqu'\''à 2 mètres en intérieur", "price":19.99, "stock":50.00}'
'{"name":"Borealis 4", "category":"gros&semi", "description":"Plante luminescente aux teintes bleutées inspirée des aurores boréales", "price":24.99, "stock":35.00}'
'{"name":"Titan 5", "category":"gros&semi", "description":"Plante géante fictive atteignant jusqu'\''à 2 mètres en intérieur", "price":19.99, "stock":50.00}'
'{"name":"Kush 1", "category":"weed&hash", "description":"Variété fictive aux arômes terreux et notes citronnées, culture indoor facile", "price":14.99, "stock":80.00}'
'{"name":"Haze 1", "category":"weed&hash", "description":"Plante fictive à dominance sativa, réputée pour son parfum épicé", "price":16.99, "stock":60.00}'
'{"name":"Hash Gold 1", "category":"weed&hash", "description":"Résine fictive premium à la texture souple et aux notes florales", "price":9.99, "stock":120.00}'
'{"name":"Hash Black 1", "category":"weed&hash", "description":"Résine fictive sombre, goût intense et épicé", "price":11.99, "stock":90.00}'
'{"name":"Kush 2", "category":"weed&hash", "description":"Variété fictive compacte, floraison rapide et parfum boisé", "price":14.99, "stock":75.00}'
)
for ((i=0; i<NUM_PRODUCTS && i<${#PREDEFINED_PRODUCTS[@]}; i++)); do
PRODUCT_JSON=${PREDEFINED_PRODUCTS[$i]}
PRODUCT_NAME=$(echo "$PRODUCT_JSON" | jq -r '.name')
PRODUCT_CATEGORY=$(echo "$PRODUCT_JSON" | jq -r '.category')
PRODUCT_DESC=$(echo "$PRODUCT_JSON" | jq -r '.description')
PRODUCT_PRICE=$(echo "$PRODUCT_JSON" | jq -r '.price')
PRODUCT_STOCK=$(echo "$PRODUCT_JSON" | jq -r '.stock')
print_test "Création produit $((i+1))/$NUM_PRODUCTS: $PRODUCT_NAME"
PRODUCT_RESPONSE=$(curl -s -X POST "$BASE_URL/api/v2/admin/protected/products" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-F "name=$PRODUCT_NAME" \
-F "category=$PRODUCT_CATEGORY" \
-F "description=$PRODUCT_DESC" \
-F "stock=$PRODUCT_STOCK" \
-F "prices[0][quantity]=1" \
-F "prices[0][price]=$PRODUCT_PRICE" \
-F "prices[1][quantity]=3" \
-F "prices[1][price]=$(echo "scale=2; $PRODUCT_PRICE * 3 * 0.9" | bc)" \
-F "prices[2][quantity]=5" \
-F "prices[2][price]=$(echo "scale=2; $PRODUCT_PRICE * 5 * 0.85" | bc)")
PRODUCT_ID=$(extract_id "$PRODUCT_RESPONSE")
if [ -n "$PRODUCT_ID" ]; then
PRODUCT_IDS+=("$PRODUCT_ID")
PRODUCT_DETAILS["$PRODUCT_ID"]="$PRODUCT_JSON"
print_success "Produit créé (ID: $PRODUCT_ID) - $PRODUCT_NAME - ${PRODUCT_PRICE}"
else
print_warning "Échec création produit $PRODUCT_NAME"
echo "Response: $PRODUCT_RESPONSE"
fi
sleep 0.5
done
print_info "Total produits créés: ${#PRODUCT_IDS[@]}"