Compare commits
2
Commits
| 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)
|
||||
if err != nil {
|
||||
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
|
||||
// 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
|
||||
|
||||
@@ -13,7 +13,7 @@ BOT2_USERNAME=rezDJDFJSFUltraFast_bot
|
||||
BOT2_WEBHOOK_SECRET=591aVEu1kj3YUVCNWAOU2xGdFNCVWqElzXGi
|
||||
|
||||
# URL publique de la gateway (pour setWebhook Telegram)
|
||||
GATEWAY_URL=https://demo-uber.club
|
||||
GATEWAY_URL=https://uber-demo.club
|
||||
|
||||
# JWT
|
||||
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 /api/v1/my-cancellation-history
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
getOrderETA,
|
||||
confirmReception,
|
||||
cancelCommand,
|
||||
updateOwnCommandAddress,
|
||||
isUserAuthenticated,
|
||||
getPublicSettings,
|
||||
} from "../../api/api";
|
||||
@@ -287,6 +288,12 @@ function SuiviLivraison() {
|
||||
useState<CancelCommandResponse | null>(null);
|
||||
const [poolNames, setPoolNames] = useState<string[]>([]);
|
||||
|
||||
const [editingAddressOrder, setEditingAddressOrder] = useState<
|
||||
number | null
|
||||
>(null);
|
||||
const [newAddress, setNewAddress] = useState("");
|
||||
const [editAddressLoading, setEditAddressLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
getPublicSettings().then((s) => setPoolNames(s.pool_names ?? []));
|
||||
}, []);
|
||||
@@ -574,6 +581,53 @@ function SuiviLivraison() {
|
||||
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) {
|
||||
return (
|
||||
<>
|
||||
@@ -1143,6 +1197,27 @@ function SuiviLivraison() {
|
||||
</div>
|
||||
) : (
|
||||
<div className="action-buttons">
|
||||
{(statusLow ===
|
||||
"pending" ||
|
||||
statusLow ===
|
||||
"assigned") && (
|
||||
<button
|
||||
className="btn-secondary"
|
||||
onClick={() =>
|
||||
openEditAddressDialog(
|
||||
order.id,
|
||||
)
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={
|
||||
faMapMarkerAlt
|
||||
}
|
||||
/>{" "}
|
||||
Modifier
|
||||
l'adresse
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="btn-cancel-order"
|
||||
onClick={() =>
|
||||
@@ -1263,6 +1338,73 @@ function SuiviLivraison() {
|
||||
</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 */}
|
||||
{showCancelDialog && (
|
||||
<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 (
|
||||
commandId: number,
|
||||
): Promise<TrackingResponse> => {
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
confirmReception,
|
||||
cancelCommand,
|
||||
respondToAddressProposal,
|
||||
updateOwnCommandAddress,
|
||||
formatOrderDate,
|
||||
formatPrice,
|
||||
calculateOrderTotal,
|
||||
@@ -74,6 +75,11 @@ export default function OrderTrackingScreen() {
|
||||
useState<CancelCommandResponse | null>(null);
|
||||
const [penaltyOrderId, setPenaltyOrderId] = useState<number | null>(null);
|
||||
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 [toastType, setToastType] = useState<
|
||||
"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(
|
||||
() =>
|
||||
StyleSheet.create({
|
||||
@@ -407,6 +436,10 @@ export default function OrderTrackingScreen() {
|
||||
"assigned",
|
||||
"en_route",
|
||||
].includes(order.status);
|
||||
const canEditAddress = [
|
||||
"pending",
|
||||
"assigned",
|
||||
].includes(order.status);
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
@@ -636,6 +669,23 @@ export default function OrderTrackingScreen() {
|
||||
size="sm"
|
||||
/>
|
||||
)}
|
||||
{canEditAddress && (
|
||||
<Button
|
||||
title="Modifier l'adresse"
|
||||
onPress={() => {
|
||||
setNewAddress(
|
||||
order.delivery_address ||
|
||||
order.adresse ||
|
||||
"",
|
||||
);
|
||||
setEditingAddressId(
|
||||
order.id,
|
||||
);
|
||||
}}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
/>
|
||||
)}
|
||||
{canCancel && (
|
||||
<Button
|
||||
title="Annuler"
|
||||
@@ -689,6 +739,52 @@ export default function OrderTrackingScreen() {
|
||||
</View>
|
||||
</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
|
||||
visible={cancellingId !== null}
|
||||
onClose={() => {
|
||||
|
||||
Reference in New Issue
Block a user