diff --git a/backend/src/main/java/com/rit/canteen/sales/controller/OrderController.java b/backend/src/main/java/com/rit/canteen/sales/controller/OrderController.java
index 8745f63b..89f0771d 100644
--- a/backend/src/main/java/com/rit/canteen/sales/controller/OrderController.java
+++ b/backend/src/main/java/com/rit/canteen/sales/controller/OrderController.java
@@ -239,14 +239,26 @@ public class OrderController {
return ResponseEntity.status(400).body(Map.of("error", "Cannot cancel an order that is already " + oldStatus));
}
- if ("RITZ_TOKEN".equals(order.getPaymentMethod())) {
- tokenService.refund(order.getUserId(), "ORD-" + order.getDisplayOrderId(),
- order.getTotalAmount(), "Order Cancelled");
+ if ("CANCEL_PENDING".equalsIgnoreCase(oldStatus)) {
+ return ResponseEntity.status(400).body(Map.of("error", "Cancellation is already pending for this order."));
+ }
+
+ if (!isStaff()) {
+ // Customer cancellation -> CANCEL_PENDING (delayed)
+ order.setStatus("CANCEL_PENDING");
+ order.setCancelRequestedAt(LocalDateTime.now());
+ orderRepository.save(order);
+ return ResponseEntity.ok(Map.of("success", true, "message", "Cancellation requested. It will be processed in 5 minutes unless the order is already being prepared."));
+ } else {
+ // Staff manual cancellation -> instant CANCELLED
+ if ("RITZ_TOKEN".equals(order.getPaymentMethod())) {
+ tokenService.refund(order.getUserId(), "ORD-" + order.getDisplayOrderId(),
+ order.getTotalAmount(), "Order Cancelled by Staff");
+ }
+ order.setStatus("CANCELLED");
+ orderRepository.save(order);
+ return ResponseEntity.ok(Map.of("success", true, "message", "Order cancelled successfully"));
}
-
- order.setStatus("CANCELLED");
- orderRepository.save(order);
- return ResponseEntity.ok(Map.of("success", true, "message", "Order cancelled successfully"));
}).orElse(ResponseEntity.notFound().build());
} catch (Exception e) {
return ResponseEntity.status(500).body(Map.of("error", e.getMessage()));
diff --git a/backend/src/main/java/com/rit/canteen/sales/model/Order.java b/backend/src/main/java/com/rit/canteen/sales/model/Order.java
index c480829d..4f742a6f 100644
--- a/backend/src/main/java/com/rit/canteen/sales/model/Order.java
+++ b/backend/src/main/java/com/rit/canteen/sales/model/Order.java
@@ -53,6 +53,9 @@ public class Order {
@Column(name = "has_feedback", nullable = false)
private boolean hasFeedback = false;
+ @Column(name = "cancel_requested_at")
+ private LocalDateTime cancelRequestedAt;
+
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, fetch = FetchType.EAGER, orphanRemoval = true)
private List items = new ArrayList<>();
@@ -114,4 +117,7 @@ public class Order {
public List getItems() { return items; }
public void setItems(List items) { this.items = items; }
+
+ public LocalDateTime getCancelRequestedAt() { return cancelRequestedAt; }
+ public void setCancelRequestedAt(LocalDateTime cancelRequestedAt) { this.cancelRequestedAt = cancelRequestedAt; }
}
diff --git a/backend/src/main/java/com/rit/canteen/sales/repository/OrderRepository.java b/backend/src/main/java/com/rit/canteen/sales/repository/OrderRepository.java
index 40530ac0..2c2eaa34 100644
--- a/backend/src/main/java/com/rit/canteen/sales/repository/OrderRepository.java
+++ b/backend/src/main/java/com/rit/canteen/sales/repository/OrderRepository.java
@@ -20,6 +20,7 @@ public interface OrderRepository extends JpaRepository, JpaSpecific
long countByCreatedAtBetween(LocalDateTime start, LocalDateTime end);
List findByIsArchivedFalseAndCreatedAtBefore(LocalDateTime timestamp);
Optional findByOrderNumber(String orderNumber);
+ List findByStatusAndCancelRequestedAtBefore(String status, LocalDateTime cutoffTime);
@Query("SELECT SUM(o.totalAmount) FROM Order o")
BigDecimal getTotalRevenue();
diff --git a/backend/src/main/java/com/rit/canteen/sales/service/OrderCancellationProcessor.java b/backend/src/main/java/com/rit/canteen/sales/service/OrderCancellationProcessor.java
new file mode 100644
index 00000000..75b2539a
--- /dev/null
+++ b/backend/src/main/java/com/rit/canteen/sales/service/OrderCancellationProcessor.java
@@ -0,0 +1,56 @@
+package com.rit.canteen.sales.service;
+
+import com.rit.canteen.sales.model.Order;
+import com.rit.canteen.sales.repository.OrderRepository;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Service
+public class OrderCancellationProcessor {
+
+ @Autowired
+ private OrderRepository orderRepository;
+
+ @Autowired
+ private TokenService tokenService;
+
+ // Run every minute
+ @Scheduled(fixedRate = 60000)
+ @Transactional
+ public void processPendingCancellations() {
+ // Find orders that were requested to cancel more than 5 minutes ago
+ LocalDateTime cutoffTime = LocalDateTime.now().minusMinutes(5);
+ List pendingOrders = orderRepository.findByStatusAndCancelRequestedAtBefore("CANCEL_PENDING", cutoffTime);
+
+ for (Order order : pendingOrders) {
+ try {
+ // Double check status just in case
+ if (!"CANCEL_PENDING".equals(order.getStatus())) {
+ continue;
+ }
+
+ // Refund if token used
+ if ("RITZ_TOKEN".equals(order.getPaymentMethod())) {
+ tokenService.refund(
+ order.getUserId(),
+ "ORD-" + order.getDisplayOrderId(),
+ order.getTotalAmount(),
+ "Auto-processed delayed cancellation"
+ );
+ }
+
+ order.setStatus("CANCELLED");
+ orderRepository.save(order);
+
+ System.out.println("[CANCELLATION PROCESSOR] Successfully cancelled order " + order.getOrderNumber());
+ } catch (Exception e) {
+ System.err.println("[CANCELLATION PROCESSOR] Error cancelling order " + order.getOrderNumber() + ": " + e.getMessage());
+ }
+ }
+ }
+}
diff --git a/counter-frontend/src/pages/Orders.tsx b/counter-frontend/src/pages/Orders.tsx
index ef0bf048..e34c7e8d 100644
--- a/counter-frontend/src/pages/Orders.tsx
+++ b/counter-frontend/src/pages/Orders.tsx
@@ -371,6 +371,7 @@ const Orders: React.FC = () => {
case 'COMPLETED': return 'bg-emerald-50 text-emerald-600 border-emerald-100';
case 'PAID': return 'bg-indigo-50 text-indigo-600 border-indigo-100';
case 'PENDING': return 'bg-amber-50 text-amber-600 border-amber-100';
+ case 'CANCEL_PENDING': return 'bg-orange-50 text-orange-600 border-orange-200 animate-pulse';
case 'CANCELLED': return 'bg-rose-50 text-rose-600 border-rose-100';
default: return 'bg-slate-50 text-slate-600 border-slate-100';
}
@@ -411,6 +412,7 @@ const Orders: React.FC = () => {
+
@@ -642,7 +644,7 @@ const Orders: React.FC = () => {
onClick={() => handleCancelOrder(selectedOrder.id)}
className="w-full text-left px-4 py-3 text-xs font-bold text-rose-600 hover:bg-rose-50 flex items-center gap-3 transition-colors cursor-pointer"
>
- Cancel Order
+ {selectedOrder.status.toUpperCase() === 'CANCEL_PENDING' ? 'Approve Cancellation' : 'Cancel Order'}
)}
diff --git a/frontend/src/pages/Orders.tsx b/frontend/src/pages/Orders.tsx
index 7a19bd90..30e469b5 100644
--- a/frontend/src/pages/Orders.tsx
+++ b/frontend/src/pages/Orders.tsx
@@ -355,6 +355,7 @@ const Orders: React.FC = () => {
case 'COMPLETED': return 'bg-emerald-50 text-emerald-600 border-emerald-100';
case 'PAID': return 'bg-indigo-50 text-indigo-600 border-indigo-100';
case 'PENDING': return 'bg-amber-50 text-amber-600 border-amber-100';
+ case 'CANCEL_PENDING': return 'bg-orange-50 text-orange-600 border-orange-200 animate-pulse';
case 'CANCELLED': return 'bg-rose-50 text-rose-600 border-rose-100';
default: return 'bg-slate-50 text-slate-600 border-slate-100';
}
@@ -395,6 +396,7 @@ const Orders: React.FC = () => {
+
@@ -626,7 +628,7 @@ const Orders: React.FC = () => {
onClick={() => handleCancelOrder(selectedOrder.id)}
className="w-full text-left px-4 py-3 text-xs font-bold text-rose-600 hover:bg-rose-50 flex items-center gap-3 transition-colors cursor-pointer"
>
- Cancel Order
+ {selectedOrder.status.toUpperCase() === 'CANCEL_PENDING' ? 'Approve Cancellation' : 'Cancel Order'}
)}
diff --git a/ordering_site/src/pages/MyOrdersScreen.tsx b/ordering_site/src/pages/MyOrdersScreen.tsx
index 2a5689a8..3e357c87 100644
--- a/ordering_site/src/pages/MyOrdersScreen.tsx
+++ b/ordering_site/src/pages/MyOrdersScreen.tsx
@@ -43,6 +43,7 @@ const MyOrdersScreen: React.FC = () => {
const [pendingItems, setPendingItems] = useState([]);
const [isCancelling, setIsCancelling] = useState(false);
+ const prevOrdersState = React.useRef>({});
const isOrderExpired = (order: Order) => {
if (order.isArchived) return true;
@@ -87,6 +88,14 @@ const MyOrdersScreen: React.FC = () => {
}
});
const data = await response.json();
+ const newOrdersState: Record = {};
+ data.forEach((o: Order) => {
+ newOrdersState[o.id] = o.status;
+ if (prevOrdersState.current[o.id] === 'CANCEL_PENDING' && (o.status === 'COMPLETED' || o.status === 'DELIVERED')) {
+ alert('Order #' + (o.displayOrderId || o.id) + ' has been delivered/completed, so cannot cancel order.');
+ }
+ });
+ prevOrdersState.current = newOrdersState;
setOrders(data);
} catch (error) {
console.error('Error fetching orders:', error);
@@ -144,7 +153,10 @@ const MyOrdersScreen: React.FC = () => {
if (data.success) {
fetchOrders();
if (selectedOrder?.id === orderId) {
- setSelectedOrder({ ...selectedOrder, status: 'CANCELLED' });
+ setSelectedOrder({ ...selectedOrder, status: 'CANCEL_PENDING' });
+ }
+ if (data.message) {
+ alert(data.message);
}
} else {
alert(data.message || 'Failed to cancel order');
@@ -221,7 +233,7 @@ const MyOrdersScreen: React.FC = () => {
)}
- {isOrderExpired(latestOrder) ? 'QR Expired' : (latestOrder.status.toUpperCase() === 'COMPLETED' ? 'Order Fulfilled' : latestOrder.status.toUpperCase() === 'CANCELLED' ? 'Order Cancelled' : 'Tap to enlarge QR')}
+ {isOrderExpired(latestOrder) ? 'QR Expired' : (latestOrder.status.toUpperCase() === 'COMPLETED' ? 'Order Fulfilled' : latestOrder.status.toUpperCase() === 'CANCELLED' ? 'Order Cancelled' : latestOrder.status.toUpperCase() === 'CANCEL_PENDING' ? 'Cancellation Processing...' : 'Tap to enlarge QR')}
@@ -247,7 +259,7 @@ const MyOrdersScreen: React.FC = () => {
>
Repeat
- {latestOrder.status.toUpperCase() !== 'COMPLETED' && latestOrder.status.toUpperCase() !== 'CANCELLED' && !isOrderExpired(latestOrder) && (
+ {latestOrder.status.toUpperCase() !== 'COMPLETED' && latestOrder.status.toUpperCase() !== 'CANCELLED' && latestOrder.status.toUpperCase() !== 'CANCEL_PENDING' && !isOrderExpired(latestOrder) && (
@@ -386,7 +400,7 @@ const MyOrdersScreen: React.FC = () => {
- {selectedOrder.status.toUpperCase() !== 'COMPLETED' && selectedOrder.status.toUpperCase() !== 'CANCELLED' && !isOrderExpired(selectedOrder) && (
+ {selectedOrder.status.toUpperCase() !== 'COMPLETED' && selectedOrder.status.toUpperCase() !== 'CANCELLED' && selectedOrder.status.toUpperCase() !== 'CANCEL_PENDING' && !isOrderExpired(selectedOrder) && (