Refund Policy added
This commit is contained in:
@@ -239,14 +239,26 @@ public class OrderController {
|
|||||||
return ResponseEntity.status(400).body(Map.of("error", "Cannot cancel an order that is already " + oldStatus));
|
return ResponseEntity.status(400).body(Map.of("error", "Cannot cancel an order that is already " + oldStatus));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ("RITZ_TOKEN".equals(order.getPaymentMethod())) {
|
if ("CANCEL_PENDING".equalsIgnoreCase(oldStatus)) {
|
||||||
tokenService.refund(order.getUserId(), "ORD-" + order.getDisplayOrderId(),
|
return ResponseEntity.status(400).body(Map.of("error", "Cancellation is already pending for this order."));
|
||||||
order.getTotalAmount(), "Order Cancelled");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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");
|
order.setStatus("CANCELLED");
|
||||||
orderRepository.save(order);
|
orderRepository.save(order);
|
||||||
return ResponseEntity.ok(Map.of("success", true, "message", "Order cancelled successfully"));
|
return ResponseEntity.ok(Map.of("success", true, "message", "Order cancelled successfully"));
|
||||||
|
}
|
||||||
}).orElse(ResponseEntity.notFound().build());
|
}).orElse(ResponseEntity.notFound().build());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return ResponseEntity.status(500).body(Map.of("error", e.getMessage()));
|
return ResponseEntity.status(500).body(Map.of("error", e.getMessage()));
|
||||||
|
|||||||
@@ -53,6 +53,9 @@ public class Order {
|
|||||||
@Column(name = "has_feedback", nullable = false)
|
@Column(name = "has_feedback", nullable = false)
|
||||||
private boolean hasFeedback = false;
|
private boolean hasFeedback = false;
|
||||||
|
|
||||||
|
@Column(name = "cancel_requested_at")
|
||||||
|
private LocalDateTime cancelRequestedAt;
|
||||||
|
|
||||||
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, fetch = FetchType.EAGER, orphanRemoval = true)
|
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, fetch = FetchType.EAGER, orphanRemoval = true)
|
||||||
private List<OrderItem> items = new ArrayList<>();
|
private List<OrderItem> items = new ArrayList<>();
|
||||||
|
|
||||||
@@ -114,4 +117,7 @@ public class Order {
|
|||||||
|
|
||||||
public List<OrderItem> getItems() { return items; }
|
public List<OrderItem> getItems() { return items; }
|
||||||
public void setItems(List<OrderItem> items) { this.items = items; }
|
public void setItems(List<OrderItem> items) { this.items = items; }
|
||||||
|
|
||||||
|
public LocalDateTime getCancelRequestedAt() { return cancelRequestedAt; }
|
||||||
|
public void setCancelRequestedAt(LocalDateTime cancelRequestedAt) { this.cancelRequestedAt = cancelRequestedAt; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ public interface OrderRepository extends JpaRepository<Order, Long>, JpaSpecific
|
|||||||
long countByCreatedAtBetween(LocalDateTime start, LocalDateTime end);
|
long countByCreatedAtBetween(LocalDateTime start, LocalDateTime end);
|
||||||
List<Order> findByIsArchivedFalseAndCreatedAtBefore(LocalDateTime timestamp);
|
List<Order> findByIsArchivedFalseAndCreatedAtBefore(LocalDateTime timestamp);
|
||||||
Optional<Order> findByOrderNumber(String orderNumber);
|
Optional<Order> findByOrderNumber(String orderNumber);
|
||||||
|
List<Order> findByStatusAndCancelRequestedAtBefore(String status, LocalDateTime cutoffTime);
|
||||||
|
|
||||||
@Query("SELECT SUM(o.totalAmount) FROM Order o")
|
@Query("SELECT SUM(o.totalAmount) FROM Order o")
|
||||||
BigDecimal getTotalRevenue();
|
BigDecimal getTotalRevenue();
|
||||||
|
|||||||
@@ -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<Order> 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -371,6 +371,7 @@ const Orders: React.FC = () => {
|
|||||||
case 'COMPLETED': return 'bg-emerald-50 text-emerald-600 border-emerald-100';
|
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 'PAID': return 'bg-indigo-50 text-indigo-600 border-indigo-100';
|
||||||
case 'PENDING': return 'bg-amber-50 text-amber-600 border-amber-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';
|
case 'CANCELLED': return 'bg-rose-50 text-rose-600 border-rose-100';
|
||||||
default: return 'bg-slate-50 text-slate-600 border-slate-100';
|
default: return 'bg-slate-50 text-slate-600 border-slate-100';
|
||||||
}
|
}
|
||||||
@@ -411,6 +412,7 @@ const Orders: React.FC = () => {
|
|||||||
<option value="PAID">Paid</option>
|
<option value="PAID">Paid</option>
|
||||||
<option value="COMPLETED">Completed</option>
|
<option value="COMPLETED">Completed</option>
|
||||||
<option value="PENDING">Pending</option>
|
<option value="PENDING">Pending</option>
|
||||||
|
<option value="CANCEL_PENDING">Cancel Pending</option>
|
||||||
<option value="CANCELLED">Cancelled</option>
|
<option value="CANCELLED">Cancelled</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -642,7 +644,7 @@ const Orders: React.FC = () => {
|
|||||||
onClick={() => handleCancelOrder(selectedOrder.id)}
|
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"
|
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"
|
||||||
>
|
>
|
||||||
<XIcon size={14} className="text-rose-600" /> Cancel Order
|
<XIcon size={14} className="text-rose-600" /> {selectedOrder.status.toUpperCase() === 'CANCEL_PENDING' ? 'Approve Cancellation' : 'Cancel Order'}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -355,6 +355,7 @@ const Orders: React.FC = () => {
|
|||||||
case 'COMPLETED': return 'bg-emerald-50 text-emerald-600 border-emerald-100';
|
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 'PAID': return 'bg-indigo-50 text-indigo-600 border-indigo-100';
|
||||||
case 'PENDING': return 'bg-amber-50 text-amber-600 border-amber-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';
|
case 'CANCELLED': return 'bg-rose-50 text-rose-600 border-rose-100';
|
||||||
default: return 'bg-slate-50 text-slate-600 border-slate-100';
|
default: return 'bg-slate-50 text-slate-600 border-slate-100';
|
||||||
}
|
}
|
||||||
@@ -395,6 +396,7 @@ const Orders: React.FC = () => {
|
|||||||
<option value="PAID">Paid</option>
|
<option value="PAID">Paid</option>
|
||||||
<option value="COMPLETED">Completed</option>
|
<option value="COMPLETED">Completed</option>
|
||||||
<option value="PENDING">Pending</option>
|
<option value="PENDING">Pending</option>
|
||||||
|
<option value="CANCEL_PENDING">Cancel Pending</option>
|
||||||
<option value="CANCELLED">Cancelled</option>
|
<option value="CANCELLED">Cancelled</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -626,7 +628,7 @@ const Orders: React.FC = () => {
|
|||||||
onClick={() => handleCancelOrder(selectedOrder.id)}
|
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"
|
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"
|
||||||
>
|
>
|
||||||
<XIcon size={14} className="text-rose-600" /> Cancel Order
|
<XIcon size={14} className="text-rose-600" /> {selectedOrder.status.toUpperCase() === 'CANCEL_PENDING' ? 'Approve Cancellation' : 'Cancel Order'}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ const MyOrdersScreen: React.FC = () => {
|
|||||||
const [pendingItems, setPendingItems] = useState<FoodItem[]>([]);
|
const [pendingItems, setPendingItems] = useState<FoodItem[]>([]);
|
||||||
|
|
||||||
const [isCancelling, setIsCancelling] = useState(false);
|
const [isCancelling, setIsCancelling] = useState(false);
|
||||||
|
const prevOrdersState = React.useRef<Record<number, string>>({});
|
||||||
|
|
||||||
const isOrderExpired = (order: Order) => {
|
const isOrderExpired = (order: Order) => {
|
||||||
if (order.isArchived) return true;
|
if (order.isArchived) return true;
|
||||||
@@ -87,6 +88,14 @@ const MyOrdersScreen: React.FC = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
const newOrdersState: Record<number, string> = {};
|
||||||
|
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);
|
setOrders(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching orders:', error);
|
console.error('Error fetching orders:', error);
|
||||||
@@ -144,7 +153,10 @@ const MyOrdersScreen: React.FC = () => {
|
|||||||
if (data.success) {
|
if (data.success) {
|
||||||
fetchOrders();
|
fetchOrders();
|
||||||
if (selectedOrder?.id === orderId) {
|
if (selectedOrder?.id === orderId) {
|
||||||
setSelectedOrder({ ...selectedOrder, status: 'CANCELLED' });
|
setSelectedOrder({ ...selectedOrder, status: 'CANCEL_PENDING' });
|
||||||
|
}
|
||||||
|
if (data.message) {
|
||||||
|
alert(data.message);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
alert(data.message || 'Failed to cancel order');
|
alert(data.message || 'Failed to cancel order');
|
||||||
@@ -221,7 +233,7 @@ const MyOrdersScreen: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="qr-hint-text">
|
<div className="qr-hint-text">
|
||||||
{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')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -247,7 +259,7 @@ const MyOrdersScreen: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<RefreshCcw size={16} /> Repeat
|
<RefreshCcw size={16} /> Repeat
|
||||||
</button>
|
</button>
|
||||||
{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) && (
|
||||||
<button
|
<button
|
||||||
className="repeat-order-btn mini"
|
className="repeat-order-btn mini"
|
||||||
style={{ flex: 1, backgroundColor: '#fee2e2', color: '#dc2626' }}
|
style={{ flex: 1, backgroundColor: '#fee2e2', color: '#dc2626' }}
|
||||||
@@ -348,6 +360,8 @@ const MyOrdersScreen: React.FC = () => {
|
|||||||
? 'This order has been fulfilled'
|
? 'This order has been fulfilled'
|
||||||
: selectedOrder.status.toUpperCase() === 'CANCELLED'
|
: selectedOrder.status.toUpperCase() === 'CANCELLED'
|
||||||
? 'This order has been cancelled'
|
? 'This order has been cancelled'
|
||||||
|
: selectedOrder.status.toUpperCase() === 'CANCEL_PENDING'
|
||||||
|
? 'Cancellation Processing (5 min delay)'
|
||||||
: 'Show this QR code at the counter'}
|
: 'Show this QR code at the counter'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -386,7 +400,7 @@ const MyOrdersScreen: React.FC = () => {
|
|||||||
<button className="repeat-order-btn" style={{ flex: 1, marginTop: 0 }} onClick={() => handleRepeatOrder(selectedOrder)}>
|
<button className="repeat-order-btn" style={{ flex: 1, marginTop: 0 }} onClick={() => handleRepeatOrder(selectedOrder)}>
|
||||||
<RefreshCcw size={18} /> Repeat
|
<RefreshCcw size={18} /> Repeat
|
||||||
</button>
|
</button>
|
||||||
{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) && (
|
||||||
<button
|
<button
|
||||||
className="repeat-order-btn"
|
className="repeat-order-btn"
|
||||||
style={{ flex: 1, marginTop: 0, backgroundColor: '#fee2e2', color: '#dc2626' }}
|
style={{ flex: 1, marginTop: 0, backgroundColor: '#fee2e2', color: '#dc2626' }}
|
||||||
|
|||||||
Reference in New Issue
Block a user