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) && ( - {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) && (