BillBot Code added
This commit is contained in:
@@ -50,10 +50,12 @@ public class SecurityConfig {
|
|||||||
.requestMatchers("/api/stock/stream").permitAll()
|
.requestMatchers("/api/stock/stream").permitAll()
|
||||||
|
|
||||||
// ── PUBLIC: Terminal hardware order lookup (auth via X-API-KEY header, not JWT) ──
|
// ── PUBLIC: Terminal hardware order lookup (auth via X-API-KEY header, not JWT) ──
|
||||||
.requestMatchers(HttpMethod.GET, "/api/terminals/orders/**").permitAll()
|
.requestMatchers("/api/terminals/orders/**").permitAll()
|
||||||
.requestMatchers(HttpMethod.GET, "/api/terminals/validate").permitAll()
|
.requestMatchers(HttpMethod.GET, "/api/terminals/validate").permitAll()
|
||||||
|
.requestMatchers(HttpMethod.GET, "/api/terminals/pair").permitAll()
|
||||||
.requestMatchers(HttpMethod.POST, "/api/terminals/pair").permitAll()
|
.requestMatchers(HttpMethod.POST, "/api/terminals/pair").permitAll()
|
||||||
.requestMatchers(HttpMethod.POST, "/api/terminals/*/verify-pin").permitAll()
|
.requestMatchers(HttpMethod.POST, "/api/terminals/*/verify-pin").permitAll()
|
||||||
|
.requestMatchers(HttpMethod.GET, "/api/terminals/counters").permitAll()
|
||||||
|
|
||||||
// ── PUBLIC: Device log ingestion (ESP32 Bill-Bot devices, no JWT) ──
|
// ── PUBLIC: Device log ingestion (ESP32 Bill-Bot devices, no JWT) ──
|
||||||
.requestMatchers(HttpMethod.POST, "/api/device-logs").permitAll()
|
.requestMatchers(HttpMethod.POST, "/api/device-logs").permitAll()
|
||||||
|
|||||||
@@ -7,11 +7,15 @@ import com.rit.canteen.sales.service.DevicePairingService;
|
|||||||
import com.rit.canteen.sales.service.TerminalService;
|
import com.rit.canteen.sales.service.TerminalService;
|
||||||
import com.rit.canteen.sales.repository.TerminalRepository;
|
import com.rit.canteen.sales.repository.TerminalRepository;
|
||||||
import com.rit.canteen.sales.repository.OrderRepository;
|
import com.rit.canteen.sales.repository.OrderRepository;
|
||||||
|
import com.rit.canteen.sales.repository.StallRepository;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@@ -32,6 +36,9 @@ public class TerminalController {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private OrderRepository orderRepository;
|
private OrderRepository orderRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private StallRepository stallRepository;
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
public List<TerminalDTO> getAllTerminals() {
|
public List<TerminalDTO> getAllTerminals() {
|
||||||
return terminalService.getAllTerminals().stream()
|
return terminalService.getAllTerminals().stream()
|
||||||
@@ -75,10 +82,15 @@ public class TerminalController {
|
|||||||
// Order Lookup (ESP32 uses X-API-KEY)
|
// Order Lookup (ESP32 uses X-API-KEY)
|
||||||
// ──────────────────────────────────────────────────────────────
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@GetMapping("/orders/{orderNumber}")
|
@GetMapping("/counters")
|
||||||
public ResponseEntity<?> getOrderForTerminal(
|
public ResponseEntity<?> getCounters(
|
||||||
@PathVariable String orderNumber,
|
@RequestHeader(value = "X-API-KEY", required = false) String apiKeyHeader,
|
||||||
@RequestHeader("X-API-KEY") String apiKey) {
|
@RequestHeader(value = "Authorization", required = false) String authHeader) {
|
||||||
|
|
||||||
|
String apiKey = extractApiKey(apiKeyHeader, authHeader);
|
||||||
|
if (apiKey == null) {
|
||||||
|
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of("message", "Invalid or missing token"));
|
||||||
|
}
|
||||||
|
|
||||||
// 1. Verify API Key
|
// 1. Verify API Key
|
||||||
Optional<Terminal> terminal = terminalRepository.findByApiKey(apiKey);
|
Optional<Terminal> terminal = terminalRepository.findByApiKey(apiKey);
|
||||||
@@ -86,8 +98,115 @@ public class TerminalController {
|
|||||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of("message", "Invalid API Key"));
|
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of("message", "Invalid API Key"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Fetch Order
|
List<Map<String, String>> counters = stallRepository.findAll().stream()
|
||||||
Optional<Order> orderOpt = orderRepository.findByOrderNumber(orderNumber);
|
.map(stall -> Map.of(
|
||||||
|
"counterId", String.valueOf(stall.getId()),
|
||||||
|
"name", stall.getName()
|
||||||
|
))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
return ResponseEntity.ok(Map.of("data", counters));
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> parseAndCleanOrderNumbers(String input) {
|
||||||
|
if (input == null || input.isBlank()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
// Normalize common URL encodings or text replacements of carriage return/newline
|
||||||
|
String normalized = input
|
||||||
|
.replace("%0D", "\r")
|
||||||
|
.replace("%0d", "\r")
|
||||||
|
.replace("%0A", "\n")
|
||||||
|
.replace("%0a", "\n")
|
||||||
|
.replace("0x0d", "\r")
|
||||||
|
.replace("0x0D", "\r")
|
||||||
|
.replace("0x0a", "\n")
|
||||||
|
.replace("0x0A", "\n");
|
||||||
|
|
||||||
|
String[] parts = normalized.split("[\\r\\n\\s,;]+");
|
||||||
|
List<String> cleanList = new ArrayList<>();
|
||||||
|
for (String part : parts) {
|
||||||
|
String clean = part.trim();
|
||||||
|
if (!clean.isEmpty()) {
|
||||||
|
cleanList.add(clean);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cleanList;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/orders")
|
||||||
|
public ResponseEntity<?> getOrderForTerminal(
|
||||||
|
@RequestParam("paymentId") String paymentId,
|
||||||
|
@RequestHeader(value = "X-API-KEY", required = false) String apiKeyHeader,
|
||||||
|
@RequestHeader(value = "Authorization", required = false) String authHeader) {
|
||||||
|
|
||||||
|
String apiKey = extractApiKey(apiKeyHeader, authHeader);
|
||||||
|
if (apiKey == null) {
|
||||||
|
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of("message", "Invalid or missing token"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Verify API Key
|
||||||
|
Optional<Terminal> terminal = terminalRepository.findByApiKey(apiKey);
|
||||||
|
if (terminal.isEmpty()) {
|
||||||
|
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of("message", "Invalid API Key"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Parse and Clean payment IDs
|
||||||
|
List<String> orderNumbers = parseAndCleanOrderNumbers(paymentId);
|
||||||
|
if (orderNumbers.isEmpty()) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("message", "paymentId required"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Try to fetch the first order that exists
|
||||||
|
for (String orderNum : orderNumbers) {
|
||||||
|
Optional<Order> orderOpt = orderRepository.findByOrderNumber(orderNum);
|
||||||
|
if (orderOpt.isPresent()) {
|
||||||
|
Order order = orderOpt.get();
|
||||||
|
|
||||||
|
// Check if order is expired/archived
|
||||||
|
if (order.isArchived()) {
|
||||||
|
continue; // Check other IDs if available, or return GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if order is already fulfilled
|
||||||
|
if ("COMPLETED".equalsIgnoreCase(order.getStatus())) {
|
||||||
|
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||||
|
.body(Map.of("message", "This order has already been fulfilled and cannot be printed again."));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseEntity.ok(order);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(Map.of("message", "Order not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/orders/{orderNumber}")
|
||||||
|
public ResponseEntity<?> getOrderForTerminalByPath(
|
||||||
|
@PathVariable("orderNumber") String orderNumber,
|
||||||
|
@RequestHeader(value = "X-API-KEY", required = false) String apiKeyHeader,
|
||||||
|
@RequestHeader(value = "Authorization", required = false) String authHeader) {
|
||||||
|
|
||||||
|
String apiKey = extractApiKey(apiKeyHeader, authHeader);
|
||||||
|
if (apiKey == null) {
|
||||||
|
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of("message", "Invalid or missing token"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Verify API Key
|
||||||
|
Optional<Terminal> terminal = terminalRepository.findByApiKey(apiKey);
|
||||||
|
if (terminal.isEmpty()) {
|
||||||
|
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of("message", "Invalid API Key"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Parse and Clean order numbers
|
||||||
|
List<String> orderNumbers = parseAndCleanOrderNumbers(orderNumber);
|
||||||
|
if (orderNumbers.isEmpty()) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("message", "orderNumber required"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Try to fetch the first order that exists
|
||||||
|
for (String orderNum : orderNumbers) {
|
||||||
|
Optional<Order> orderOpt = orderRepository.findByOrderNumber(orderNum);
|
||||||
if (orderOpt.isPresent()) {
|
if (orderOpt.isPresent()) {
|
||||||
Order order = orderOpt.get();
|
Order order = orderOpt.get();
|
||||||
|
|
||||||
@@ -104,9 +223,153 @@ public class TerminalController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return ResponseEntity.ok(order);
|
return ResponseEntity.ok(order);
|
||||||
} else {
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(Map.of("message", "Order not found"));
|
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(Map.of("message", "Order not found"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PutMapping("/orders/delivered")
|
||||||
|
@org.springframework.transaction.annotation.Transactional
|
||||||
|
public ResponseEntity<?> markOrderAsDelivered(
|
||||||
|
@RequestBody Map<String, String> body,
|
||||||
|
@RequestHeader(value = "X-API-KEY", required = false) String apiKeyHeader,
|
||||||
|
@RequestHeader(value = "Authorization", required = false) String authHeader) {
|
||||||
|
|
||||||
|
String orderNumber = body.get("orderNumber");
|
||||||
|
if (orderNumber == null || orderNumber.isBlank()) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("message", "orderNumber required"));
|
||||||
|
}
|
||||||
|
|
||||||
|
String apiKey = extractApiKey(apiKeyHeader, authHeader);
|
||||||
|
if (apiKey == null) {
|
||||||
|
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of("message", "Invalid or missing token"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Verify API Key
|
||||||
|
Optional<Terminal> terminal = terminalRepository.findByApiKey(apiKey);
|
||||||
|
if (terminal.isEmpty()) {
|
||||||
|
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of("message", "Invalid API Key"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Parse and Clean order numbers
|
||||||
|
List<String> orderNumbers = parseAndCleanOrderNumbers(orderNumber);
|
||||||
|
if (orderNumbers.isEmpty()) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("message", "orderNumber required"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Process all found orders
|
||||||
|
List<String> successfulOrders = new ArrayList<>();
|
||||||
|
boolean alreadyCompleted = false;
|
||||||
|
boolean archivedFound = false;
|
||||||
|
|
||||||
|
for (String orderNum : orderNumbers) {
|
||||||
|
Optional<Order> orderOpt = orderRepository.findByOrderNumber(orderNum);
|
||||||
|
if (orderOpt.isPresent()) {
|
||||||
|
Order order = orderOpt.get();
|
||||||
|
|
||||||
|
if (order.isArchived()) {
|
||||||
|
archivedFound = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("COMPLETED".equalsIgnoreCase(order.getStatus())) {
|
||||||
|
alreadyCompleted = true;
|
||||||
|
successfulOrders.add(orderNum);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update status to COMPLETED
|
||||||
|
order.setStatus("COMPLETED");
|
||||||
|
orderRepository.save(order);
|
||||||
|
successfulOrders.add(orderNum);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!successfulOrders.isEmpty()) {
|
||||||
|
String msg = alreadyCompleted && successfulOrders.size() == 1
|
||||||
|
? "Order was already marked as delivered."
|
||||||
|
: "Order(s) marked as delivered successfully: " + String.join(", ", successfulOrders);
|
||||||
|
return ResponseEntity.ok(Map.of("success", true, "message", msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (archivedFound) {
|
||||||
|
return ResponseEntity.status(HttpStatus.GONE)
|
||||||
|
.body(Map.of("message", "The requested order(s) have expired/archived."));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(Map.of("message", "Order not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/orders/{orderNumber}/delivered")
|
||||||
|
@org.springframework.transaction.annotation.Transactional
|
||||||
|
public ResponseEntity<?> markOrderAsDeliveredPost(
|
||||||
|
@PathVariable("orderNumber") String orderNumber,
|
||||||
|
@RequestHeader(value = "X-API-KEY", required = false) String apiKeyHeader,
|
||||||
|
@RequestHeader(value = "Authorization", required = false) String authHeader) {
|
||||||
|
|
||||||
|
if (orderNumber == null || orderNumber.isBlank()) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("message", "orderNumber required"));
|
||||||
|
}
|
||||||
|
|
||||||
|
String apiKey = extractApiKey(apiKeyHeader, authHeader);
|
||||||
|
if (apiKey == null) {
|
||||||
|
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of("message", "Invalid or missing token"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Verify API Key
|
||||||
|
Optional<Terminal> terminal = terminalRepository.findByApiKey(apiKey);
|
||||||
|
if (terminal.isEmpty()) {
|
||||||
|
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of("message", "Invalid API Key"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Parse and Clean order numbers
|
||||||
|
List<String> orderNumbers = parseAndCleanOrderNumbers(orderNumber);
|
||||||
|
if (orderNumbers.isEmpty()) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("message", "orderNumber required"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Process all found orders
|
||||||
|
List<String> successfulOrders = new ArrayList<>();
|
||||||
|
boolean alreadyCompleted = false;
|
||||||
|
boolean archivedFound = false;
|
||||||
|
|
||||||
|
for (String orderNum : orderNumbers) {
|
||||||
|
Optional<Order> orderOpt = orderRepository.findByOrderNumber(orderNum);
|
||||||
|
if (orderOpt.isPresent()) {
|
||||||
|
Order order = orderOpt.get();
|
||||||
|
|
||||||
|
if (order.isArchived()) {
|
||||||
|
archivedFound = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("COMPLETED".equalsIgnoreCase(order.getStatus())) {
|
||||||
|
alreadyCompleted = true;
|
||||||
|
successfulOrders.add(orderNum);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update status to COMPLETED
|
||||||
|
order.setStatus("COMPLETED");
|
||||||
|
orderRepository.save(order);
|
||||||
|
successfulOrders.add(orderNum);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!successfulOrders.isEmpty()) {
|
||||||
|
String msg = alreadyCompleted && successfulOrders.size() == 1
|
||||||
|
? "Order was already marked as delivered."
|
||||||
|
: "Order(s) marked as delivered successfully: " + String.join(", ", successfulOrders);
|
||||||
|
return ResponseEntity.ok(Map.of("success", true, "message", msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (archivedFound) {
|
||||||
|
return ResponseEntity.status(HttpStatus.GONE)
|
||||||
|
.body(Map.of("message", "The requested order(s) have expired/archived."));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(Map.of("message", "Order not found"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/validate")
|
@GetMapping("/validate")
|
||||||
@@ -137,17 +400,41 @@ public class TerminalController {
|
|||||||
*
|
*
|
||||||
* No auth required — the device has no credentials yet.
|
* No auth required — the device has no credentials yet.
|
||||||
*/
|
*/
|
||||||
|
@GetMapping("/pair")
|
||||||
|
public ResponseEntity<?> registerDeviceOtp(@RequestParam("otp") String otp) {
|
||||||
|
if (otp == null || otp.isBlank()) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("message", "OTP is required"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> result = devicePairingService.registerOtp(otp, "ESP32-Device");
|
||||||
|
if ("PAIRED".equals(result.get("status"))) {
|
||||||
|
return ResponseEntity.ok(Map.of(
|
||||||
|
"status", "PAIRED",
|
||||||
|
"apiKey", result.get("apiKey"),
|
||||||
|
"terminalId", result.get("terminalId")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return ResponseEntity.ok(Map.of("status", "WAITING"));
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/pair")
|
@PostMapping("/pair")
|
||||||
public ResponseEntity<?> registerDeviceOtp(@RequestBody Map<String, String> body) {
|
public ResponseEntity<?> registerDeviceOtpPost(@RequestBody Map<String, String> body) {
|
||||||
String otp = body.get("otp");
|
String otp = body.get("otp");
|
||||||
String deviceId = body.get("deviceId");
|
String deviceId = body.get("deviceId");
|
||||||
|
|
||||||
if (otp == null || otp.isBlank() || deviceId == null || deviceId.isBlank()) {
|
if (otp == null || otp.isBlank() || deviceId == null || deviceId.isBlank()) {
|
||||||
return ResponseEntity.badRequest().body(Map.of("message", "Both 'otp' and 'deviceId' are required"));
|
return ResponseEntity.badRequest().body(Map.of("message", "otp and deviceId are required"));
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, Object> result = devicePairingService.registerOtp(otp, deviceId);
|
Map<String, Object> result = devicePairingService.registerOtp(otp, deviceId);
|
||||||
return ResponseEntity.ok(result);
|
if ("PAIRED".equals(result.get("status"))) {
|
||||||
|
return ResponseEntity.ok(Map.of(
|
||||||
|
"status", "PAIRED",
|
||||||
|
"apiKey", result.get("apiKey"),
|
||||||
|
"terminalId", result.get("terminalId")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return ResponseEntity.ok(Map.of("status", "WAITING"));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -216,4 +503,14 @@ public class TerminalController {
|
|||||||
}
|
}
|
||||||
return deviceId.substring(0, 4) + "••••" + deviceId.substring(deviceId.length() - 4);
|
return deviceId.substring(0, 4) + "••••" + deviceId.substring(deviceId.length() - 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String extractApiKey(String apiKeyHeader, String authHeader) {
|
||||||
|
if (apiKeyHeader != null && !apiKeyHeader.isBlank()) {
|
||||||
|
return apiKeyHeader;
|
||||||
|
}
|
||||||
|
if (authHeader != null && authHeader.startsWith("Bearer ")) {
|
||||||
|
return authHeader.substring(7).trim();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ import java.util.List;
|
|||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "canteen_orders")
|
@Table(name = "canteen_orders", indexes = {
|
||||||
|
@Index(name = "idx_orders_user_id", columnList = "user_id"),
|
||||||
|
@Index(name = "idx_orders_created_at", columnList = "createdAt")
|
||||||
|
})
|
||||||
public class Order {
|
public class Order {
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
|
|||||||
@@ -8,7 +8,11 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "products")
|
@Table(name = "products", indexes = {
|
||||||
|
@Index(name = "idx_products_product_id", columnList = "productId"),
|
||||||
|
@Index(name = "idx_products_name", columnList = "name"),
|
||||||
|
@Index(name = "idx_products_category", columnList = "category")
|
||||||
|
})
|
||||||
public class Product {
|
public class Product {
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ server.port=8080
|
|||||||
# DATABASE — REQUIRED environment variables
|
# DATABASE — REQUIRED environment variables
|
||||||
# Set these in your environment or a .env file
|
# Set these in your environment or a .env file
|
||||||
# ============================================================
|
# ============================================================
|
||||||
spring.datasource.url=${DB_URL:jdbc:postgresql://localhost:5432/positeasy}
|
spring.datasource.url=jdbc:postgresql://localhost:5432/positeasy
|
||||||
spring.datasource.username=postgres
|
spring.datasource.username=postgres
|
||||||
spring.datasource.password=sidharth
|
spring.datasource.password=sidharth
|
||||||
spring.datasource.driver-class-name=org.postgresql.Driver
|
spring.datasource.driver-class-name=org.postgresql.Driver
|
||||||
@@ -37,3 +37,28 @@ spring.servlet.multipart.max-request-size=10MB
|
|||||||
# CORS — comma-separated list of allowed frontend origins
|
# CORS — comma-separated list of allowed frontend origins
|
||||||
# ============================================================
|
# ============================================================
|
||||||
app.cors.allowed-origins=${APP_CORS_ORIGINS:http://localhost:5173,http://localhost:5174,http://localhost:3000}
|
app.cors.allowed-origins=${APP_CORS_ORIGINS:http://localhost:5173,http://localhost:5174,http://localhost:3000}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# PERFORMANCE & CONCURRENCY OPTIMIZATIONS
|
||||||
|
# ============================================================
|
||||||
|
# Tomcat Thread pool configuration for high concurrency
|
||||||
|
server.tomcat.threads.max=200
|
||||||
|
server.tomcat.threads.min-spare=20
|
||||||
|
server.tomcat.max-connections=10000
|
||||||
|
server.tomcat.accept-count=100
|
||||||
|
server.tomcat.connection-timeout=20000
|
||||||
|
|
||||||
|
# Hikari Database Connection Pool settings
|
||||||
|
spring.datasource.hikari.maximum-pool-size=50
|
||||||
|
spring.datasource.hikari.minimum-idle=10
|
||||||
|
spring.datasource.hikari.idle-timeout=300000
|
||||||
|
spring.datasource.hikari.max-lifetime=1200000
|
||||||
|
spring.datasource.hikari.connection-timeout=20000
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# LOGGING TUNING FOR ROBUSTNESS
|
||||||
|
# ============================================================
|
||||||
|
# Reduce log noise from malformed request line / control character warnings
|
||||||
|
logging.level.org.apache.coyote.http11.Http11InputBuffer=ERROR
|
||||||
|
logging.level.org.apache.tomcat.util.http.parser.HttpParser=ERROR
|
||||||
|
|
||||||
|
|||||||
@@ -180,7 +180,34 @@ Used by the ESP32 to fetch the order details after scanning a QR code.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### E. Validate API Key
|
### E. Mark Order as Delivered
|
||||||
|
|
||||||
|
Used by the ESP32 to mark an order as delivered/completed in the database once the bill has successfully printed.
|
||||||
|
|
||||||
|
* **URL**: `/api/terminals/orders/{orderNumber}/delivered`
|
||||||
|
* **Method**: `POST`
|
||||||
|
* **Headers**:
|
||||||
|
* `X-API-KEY`: `<YOUR_TERMINAL_API_KEY>`
|
||||||
|
* **Path Parameters**:
|
||||||
|
* `orderNumber` (String): The order ID (e.g., `ORD-87A3B2D9`).
|
||||||
|
|
||||||
|
#### Response Codes:
|
||||||
|
* `200 OK`: Order marked as delivered successfully, or was already marked as delivered.
|
||||||
|
* `401 UNAUTHORIZED`: Invalid or missing `X-API-KEY`.
|
||||||
|
* `404 NOT_FOUND`: Order number does not exist.
|
||||||
|
* `410 GONE`: Order has expired/archived.
|
||||||
|
|
||||||
|
#### Response Body (`200 OK` JSON Schema):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Order marked as delivered successfully."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### F. Validate API Key
|
||||||
|
|
||||||
Used by the ESP32 to verify its API key is still valid (optional health check).
|
Used by the ESP32 to verify its API key is still valid (optional health check).
|
||||||
|
|
||||||
@@ -201,7 +228,7 @@ Used by the ESP32 to verify its API key is still valid (optional health check).
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### F. Device Logging
|
### G. Device Logging
|
||||||
|
|
||||||
Used by the ESP32 to submit runtime system logs to the backend.
|
Used by the ESP32 to submit runtime system logs to the backend.
|
||||||
|
|
||||||
@@ -222,7 +249,7 @@ Used by the ESP32 to submit runtime system logs to the backend.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### G. Verify Terminal PIN (Admin Tool)
|
### H. Verify Terminal PIN (Admin Tool)
|
||||||
|
|
||||||
Used during admin operations to reveal the terminal's API Key.
|
Used during admin operations to reveal the terminal's API Key.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user