Compare commits
2
Commits
f3dfb7b2ae
...
f904a37964
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f904a37964 | ||
|
|
c69dc70680 |
@@ -375,6 +375,16 @@ func ClaimMyReward(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sans produit éligible pour ce pool (ex: catégories de la récompense mal
|
||||||
|
// alignées avec celles du pool), on refuse avant de consommer un point —
|
||||||
|
// sinon points_redeemed serait incrémenté sans qu'aucun produit ne soit
|
||||||
|
// jamais ajouté au panier (récompense perdue silencieusement).
|
||||||
|
if len(itemsToAdd) == 0 {
|
||||||
|
log.Printf("❌ [CLAIM] Aucun produit éligible pour %s (pool=%s)", username, req.PoolKey)
|
||||||
|
c.JSON(http.StatusConflict, gin.H{"error": "Récompense momentanément indisponible, contactez le support"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
remaining, added, err := database.ClaimPoolRewardAndAddToBasket(username, req.PoolKey, reward.Threshold, itemsToAdd)
|
remaining, added, err := database.ClaimPoolRewardAndAddToBasket(username, req.PoolKey, reward.Threshold, itemsToAdd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if strings.Contains(err.Error(), "pas de récompense disponible") {
|
if strings.Contains(err.Error(), "pas de récompense disponible") {
|
||||||
|
|||||||
@@ -224,6 +224,51 @@ func TestClaimMyReward_HTTPFlow_FailsAtomicallyWhenProductMissing(t *testing.T)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Si les catégories configurées sur la récompense ne correspondent à aucune
|
||||||
|
// catégorie du pool réclamé (erreur de configuration admin : pool assigné à
|
||||||
|
// "test", récompense configurée sur "other"), la liste de produits éligibles
|
||||||
|
// est vide et la réclamation doit échouer avant de consommer un point —
|
||||||
|
// sinon points_redeemed serait incrémenté sans qu'aucun produit ne soit
|
||||||
|
// jamais ajouté au panier (régression couverte : la récompense était
|
||||||
|
// auparavant "consommée" silencieusement sans rien livrer).
|
||||||
|
func TestClaimMyReward_HTTPFlow_RejectsWhenNoEligibleItemsForPoolCategories(t *testing.T) {
|
||||||
|
cleanupStockTestData(t)
|
||||||
|
username := newTestClient(t, "reward_http_no_eligible")
|
||||||
|
newTestProduct(t, "RewardHTTPNoEligible", 5)
|
||||||
|
|
||||||
|
configureRewardSettings(t, &models.PointsReward{
|
||||||
|
Threshold: 20,
|
||||||
|
CategoryConfigs: []models.RewardCategoryConfig{
|
||||||
|
{Category: "other", Type: "free_product", AllProducts: true, Quantity: 1},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
setClientPoolPoints(t, username, "pool_0", 25)
|
||||||
|
|
||||||
|
body, _ := json.Marshal(map[string]string{"pool_key": "pool_0"})
|
||||||
|
c, rec := claimRewardContext(username, body)
|
||||||
|
handlers.ClaimMyReward(c)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusConflict {
|
||||||
|
t.Fatalf("status HTTP: got=%d want=%d body=%s", rec.Code, http.StatusConflict, rec.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
points, redeemed, err := testDB.GetClientPointsAndRewards(username)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetClientPointsAndRewards: %v", err)
|
||||||
|
}
|
||||||
|
if redeemed["pool_0"] != 0 {
|
||||||
|
t.Errorf("la récompense ne doit PAS être consommée si aucun produit n'est éligible: got redeemed=%d want=0", redeemed["pool_0"])
|
||||||
|
}
|
||||||
|
if points["pool_0"] != 25 {
|
||||||
|
t.Errorf("les points accumulés ne doivent pas être touchés: got=%d want=25", points["pool_0"])
|
||||||
|
}
|
||||||
|
|
||||||
|
rows := basketRewardItems(t, username)
|
||||||
|
if len(rows) != 0 {
|
||||||
|
t.Errorf("aucun produit récompense ne doit être ajouté au panier: %+v", rows)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Une même catégorie peut avoir les deux types de récompense actifs en
|
// Une même catégorie peut avoir les deux types de récompense actifs en
|
||||||
// parallèle (un lot de produits offerts + un lot de produits à -50%), chacun
|
// parallèle (un lot de produits offerts + un lot de produits à -50%), chacun
|
||||||
// avec sa propre sélection de produits et sa propre quantité. Un seul claim
|
// avec sa propre sélection de produits et sa propre quantité. Un seul claim
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ BOT2_USERNAME=rezDJDFJSFUltraFast_bot
|
|||||||
BOT2_WEBHOOK_SECRET=591aVEu1kj3YUVCNWAOU2xGdFNCVWqElzXGi
|
BOT2_WEBHOOK_SECRET=591aVEu1kj3YUVCNWAOU2xGdFNCVWqElzXGi
|
||||||
|
|
||||||
# URL publique de la gateway (pour setWebhook Telegram)
|
# URL publique de la gateway (pour setWebhook Telegram)
|
||||||
GATEWAY_URL=https://demo-uber.club
|
GATEWAY_URL=https://uber-demo.club
|
||||||
|
|
||||||
# JWT
|
# JWT
|
||||||
JWT_SECRET=IxGF36s14J0ZNeQCF2Of0APc4kpNd5PlsJ
|
JWT_SECRET=IxGF36s14J0ZNeQCF2Of0APc4kpNd5PlsJ
|
||||||
|
|||||||
@@ -1440,6 +1440,60 @@ export const cancelCommand = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ✅ UPDATE OWN COMMAND ADDRESS - Corriger l'adresse de sa propre commande
|
||||||
|
* PUT /api/v1/commands/:id/address
|
||||||
|
*/
|
||||||
|
export const updateOwnCommandAddress = async (
|
||||||
|
commandId: number,
|
||||||
|
deliveryAddress: string,
|
||||||
|
): Promise<{ success: boolean; message: string }> => {
|
||||||
|
const token = sessionStorage.getItem("token");
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Session invalide",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${API_URL}/commands/${commandId}/address`,
|
||||||
|
{
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ delivery_address: deliveryAddress }),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = await safeJson(response);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: data.error || "Erreur lors de la mise à jour de l'adresse",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: data.message || "Adresse mise à jour",
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message:
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Erreur lors de la mise à jour de l'adresse",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ✅ GET MY CANCELLATION HISTORY - Historique des annulations
|
* ✅ GET MY CANCELLATION HISTORY - Historique des annulations
|
||||||
* GET /api/v1/my-cancellation-history
|
* GET /api/v1/my-cancellation-history
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
getOrderETA,
|
getOrderETA,
|
||||||
confirmReception,
|
confirmReception,
|
||||||
cancelCommand,
|
cancelCommand,
|
||||||
|
updateOwnCommandAddress,
|
||||||
isUserAuthenticated,
|
isUserAuthenticated,
|
||||||
getPublicSettings,
|
getPublicSettings,
|
||||||
} from "../../api/api";
|
} from "../../api/api";
|
||||||
@@ -287,6 +288,12 @@ function SuiviLivraison() {
|
|||||||
useState<CancelCommandResponse | null>(null);
|
useState<CancelCommandResponse | null>(null);
|
||||||
const [poolNames, setPoolNames] = useState<string[]>([]);
|
const [poolNames, setPoolNames] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const [editingAddressOrder, setEditingAddressOrder] = useState<
|
||||||
|
number | null
|
||||||
|
>(null);
|
||||||
|
const [newAddress, setNewAddress] = useState("");
|
||||||
|
const [editAddressLoading, setEditAddressLoading] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getPublicSettings().then((s) => setPoolNames(s.pool_names ?? []));
|
getPublicSettings().then((s) => setPoolNames(s.pool_names ?? []));
|
||||||
}, []);
|
}, []);
|
||||||
@@ -574,6 +581,53 @@ function SuiviLivraison() {
|
|||||||
handleCancelOrder(true);
|
handleCancelOrder(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openEditAddressDialog = (orderId: number) => {
|
||||||
|
if (!isUserAuthenticated()) {
|
||||||
|
navigate("/login/client", { replace: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const order = orders.find((o) => o.id === orderId);
|
||||||
|
setNewAddress(order ? getDeliveryAddress(order) : "");
|
||||||
|
setEditingAddressOrder(orderId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeEditAddressDialog = () => {
|
||||||
|
setEditingAddressOrder(null);
|
||||||
|
setNewAddress("");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateAddress = async () => {
|
||||||
|
if (!editingAddressOrder || !newAddress.trim()) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setEditAddressLoading(true);
|
||||||
|
const response = await updateOwnCommandAddress(
|
||||||
|
editingAddressOrder,
|
||||||
|
newAddress.trim(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
showToast("Adresse mise à jour", "success");
|
||||||
|
closeEditAddressDialog();
|
||||||
|
loadOrders();
|
||||||
|
} else {
|
||||||
|
showToast(
|
||||||
|
response.message || "Erreur lors de la mise à jour",
|
||||||
|
"error",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error: unknown) {
|
||||||
|
showToast(
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Erreur lors de la mise à jour de l'adresse",
|
||||||
|
"error",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setEditAddressLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (loading && orders.length === 0) {
|
if (loading && orders.length === 0) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -1143,6 +1197,27 @@ function SuiviLivraison() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="action-buttons">
|
<div className="action-buttons">
|
||||||
|
{(statusLow ===
|
||||||
|
"pending" ||
|
||||||
|
statusLow ===
|
||||||
|
"assigned") && (
|
||||||
|
<button
|
||||||
|
className="btn-secondary"
|
||||||
|
onClick={() =>
|
||||||
|
openEditAddressDialog(
|
||||||
|
order.id,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={
|
||||||
|
faMapMarkerAlt
|
||||||
|
}
|
||||||
|
/>{" "}
|
||||||
|
Modifier
|
||||||
|
l'adresse
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
className="btn-cancel-order"
|
className="btn-cancel-order"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
@@ -1263,6 +1338,73 @@ function SuiviLivraison() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Dialog de modification d'adresse */}
|
||||||
|
{editingAddressOrder !== null && (
|
||||||
|
<div
|
||||||
|
className="confirm-dialog-overlay"
|
||||||
|
onClick={closeEditAddressDialog}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="confirm-dialog"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div className="confirm-dialog-header">
|
||||||
|
<h3>
|
||||||
|
<FontAwesomeIcon icon={faMapMarkerAlt} />{" "}
|
||||||
|
Modifier l'adresse de livraison
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className="confirm-dialog-body">
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="new-address">
|
||||||
|
Nouvelle adresse de livraison
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="new-address"
|
||||||
|
value={newAddress}
|
||||||
|
onChange={(e) =>
|
||||||
|
setNewAddress(e.target.value)
|
||||||
|
}
|
||||||
|
placeholder="Adresse complète"
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="confirm-dialog-actions">
|
||||||
|
<button
|
||||||
|
className="btn-secondary"
|
||||||
|
onClick={closeEditAddressDialog}
|
||||||
|
disabled={editAddressLoading}
|
||||||
|
>
|
||||||
|
Retour
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn-confirm"
|
||||||
|
onClick={handleUpdateAddress}
|
||||||
|
disabled={
|
||||||
|
editAddressLoading || !newAddress.trim()
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{editAddressLoading ? (
|
||||||
|
<>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faClock}
|
||||||
|
spin
|
||||||
|
/>{" "}
|
||||||
|
Enregistrement...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<FontAwesomeIcon icon={faCheck} />{" "}
|
||||||
|
Enregistrer
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Dialog d'annulation */}
|
{/* Dialog d'annulation */}
|
||||||
{showCancelDialog && (
|
{showCancelDialog && (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -457,6 +457,29 @@ export const respondToAddressProposal = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const updateOwnCommandAddress = async (
|
||||||
|
commandId: number,
|
||||||
|
deliveryAddress: string,
|
||||||
|
): Promise<{ success: boolean; message: string }> => {
|
||||||
|
try {
|
||||||
|
const { data } = await apiClient.put(
|
||||||
|
`${V1}/commands/${commandId}/address`,
|
||||||
|
{ delivery_address: deliveryAddress },
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: data.message || "Adresse mise à jour",
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message:
|
||||||
|
error.response?.data?.error ||
|
||||||
|
"Erreur lors de la mise à jour de l'adresse",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const getOrderTracking = async (
|
export const getOrderTracking = async (
|
||||||
commandId: number,
|
commandId: number,
|
||||||
): Promise<TrackingResponse> => {
|
): Promise<TrackingResponse> => {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
confirmReception,
|
confirmReception,
|
||||||
cancelCommand,
|
cancelCommand,
|
||||||
respondToAddressProposal,
|
respondToAddressProposal,
|
||||||
|
updateOwnCommandAddress,
|
||||||
formatOrderDate,
|
formatOrderDate,
|
||||||
formatPrice,
|
formatPrice,
|
||||||
calculateOrderTotal,
|
calculateOrderTotal,
|
||||||
@@ -74,6 +75,11 @@ export default function OrderTrackingScreen() {
|
|||||||
useState<CancelCommandResponse | null>(null);
|
useState<CancelCommandResponse | null>(null);
|
||||||
const [penaltyOrderId, setPenaltyOrderId] = useState<number | null>(null);
|
const [penaltyOrderId, setPenaltyOrderId] = useState<number | null>(null);
|
||||||
const [penaltiesEnabled, setPenaltiesEnabled] = useState(false);
|
const [penaltiesEnabled, setPenaltiesEnabled] = useState(false);
|
||||||
|
const [editingAddressId, setEditingAddressId] = useState<number | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
const [newAddress, setNewAddress] = useState("");
|
||||||
|
const [editAddressLoading, setEditAddressLoading] = useState(false);
|
||||||
const [toastMsg, setToastMsg] = useState("");
|
const [toastMsg, setToastMsg] = useState("");
|
||||||
const [toastType, setToastType] = useState<
|
const [toastType, setToastType] = useState<
|
||||||
"success" | "error" | "warning" | "info"
|
"success" | "error" | "warning" | "info"
|
||||||
@@ -187,6 +193,29 @@ export default function OrderTrackingScreen() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleUpdateAddress = async (orderId: number) => {
|
||||||
|
if (!newAddress.trim()) return;
|
||||||
|
setEditAddressLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await updateOwnCommandAddress(
|
||||||
|
orderId,
|
||||||
|
newAddress.trim(),
|
||||||
|
);
|
||||||
|
if (res.success) {
|
||||||
|
showToast("Adresse mise à jour", "success");
|
||||||
|
setEditingAddressId(null);
|
||||||
|
setNewAddress("");
|
||||||
|
fetchOrders();
|
||||||
|
} else {
|
||||||
|
showToast(res.message || "Erreur", "error");
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
showToast("Erreur lors de la mise à jour de l'adresse", "error");
|
||||||
|
} finally {
|
||||||
|
setEditAddressLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const styles = useMemo(
|
const styles = useMemo(
|
||||||
() =>
|
() =>
|
||||||
StyleSheet.create({
|
StyleSheet.create({
|
||||||
@@ -407,6 +436,10 @@ export default function OrderTrackingScreen() {
|
|||||||
"assigned",
|
"assigned",
|
||||||
"en_route",
|
"en_route",
|
||||||
].includes(order.status);
|
].includes(order.status);
|
||||||
|
const canEditAddress = [
|
||||||
|
"pending",
|
||||||
|
"assigned",
|
||||||
|
].includes(order.status);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
@@ -636,6 +669,23 @@ export default function OrderTrackingScreen() {
|
|||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{canEditAddress && (
|
||||||
|
<Button
|
||||||
|
title="Modifier l'adresse"
|
||||||
|
onPress={() => {
|
||||||
|
setNewAddress(
|
||||||
|
order.delivery_address ||
|
||||||
|
order.adresse ||
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
setEditingAddressId(
|
||||||
|
order.id,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{canCancel && (
|
{canCancel && (
|
||||||
<Button
|
<Button
|
||||||
title="Annuler"
|
title="Annuler"
|
||||||
@@ -689,6 +739,52 @@ export default function OrderTrackingScreen() {
|
|||||||
</View>
|
</View>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
visible={editingAddressId !== null}
|
||||||
|
onClose={() => {
|
||||||
|
setEditingAddressId(null);
|
||||||
|
setNewAddress("");
|
||||||
|
}}
|
||||||
|
title="Modifier l'adresse de livraison"
|
||||||
|
icon="location-outline"
|
||||||
|
iconColor={colors.accent}
|
||||||
|
>
|
||||||
|
<View style={styles.modalBody}>
|
||||||
|
<Text style={styles.modalText}>
|
||||||
|
Nouvelle adresse de livraison :
|
||||||
|
</Text>
|
||||||
|
<RNTextInput
|
||||||
|
style={styles.cancelInput}
|
||||||
|
placeholder="Adresse complète"
|
||||||
|
placeholderTextColor={colors.textMuted}
|
||||||
|
value={newAddress}
|
||||||
|
onChangeText={setNewAddress}
|
||||||
|
multiline
|
||||||
|
/>
|
||||||
|
<View style={styles.modalActions}>
|
||||||
|
<Button
|
||||||
|
title="Retour"
|
||||||
|
onPress={() => {
|
||||||
|
setEditingAddressId(null);
|
||||||
|
setNewAddress("");
|
||||||
|
}}
|
||||||
|
variant="outline"
|
||||||
|
size="md"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
title="Enregistrer"
|
||||||
|
onPress={() =>
|
||||||
|
editingAddressId &&
|
||||||
|
handleUpdateAddress(editingAddressId)
|
||||||
|
}
|
||||||
|
loading={editAddressLoading}
|
||||||
|
variant="success"
|
||||||
|
size="md"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
visible={cancellingId !== null}
|
visible={cancellingId !== null}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user