diff --git a/.DS_Store b/.DS_Store index 66d7014d..9d2444a7 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/Ritz/.DS_Store b/Ritz/.DS_Store new file mode 100644 index 00000000..8484a716 Binary files /dev/null and b/Ritz/.DS_Store differ diff --git a/Ritz/Ritz-Stack.png b/Ritz/Ritz-Stack.png new file mode 100644 index 00000000..ea253a7a Binary files /dev/null and b/Ritz/Ritz-Stack.png differ diff --git a/Ritz/Ritz.psd b/Ritz/Ritz.psd new file mode 100644 index 00000000..55a2b48a Binary files /dev/null and b/Ritz/Ritz.psd differ diff --git a/Ritz/image-removebg-preview (6) 2.png b/Ritz/image-removebg-preview (6) 2.png new file mode 100644 index 00000000..64262338 Binary files /dev/null and b/Ritz/image-removebg-preview (6) 2.png differ diff --git a/Ritz/image-removebg-preview (6).png b/Ritz/image-removebg-preview (6).png new file mode 100644 index 00000000..b790fd75 Binary files /dev/null and b/Ritz/image-removebg-preview (6).png differ 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 ccb7993c..7ff296c5 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 @@ -14,6 +14,9 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.jpa.domain.Specification; +import com.rit.canteen.sales.repository.UserRepository; +import com.rit.canteen.sales.service.OrderArchiverService; +import com.rit.canteen.sales.service.TokenService; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -32,7 +35,13 @@ public class OrderController { private ProductRepository productRepository; @Autowired - private com.rit.canteen.sales.service.OrderArchiverService orderArchiverService; + private UserRepository userRepository; + + @Autowired + private OrderArchiverService orderArchiverService; + + @Autowired + private TokenService tokenService; // Use ThreadLocal to safely store conflicts for the current request context private static final ThreadLocal>> requestConflicts = new ThreadLocal<>(); @@ -163,7 +172,19 @@ public class OrderController { String displayId = String.format("%03d", todaysOrderCount + 1); order.setDisplayOrderId(displayId); - // 4. Final Save + // 4. Token Payment Check + if ("RITZ_TOKEN".equals(order.getPaymentMethod())) { + try { + tokenService.spend(order.getUser().getId(), order.getTotalAmount(), "ORD-" + displayId); + } catch (RuntimeException e) { + if ("INSUFFICIENT_TOKENS".equals(e.getMessage())) { + throw new RuntimeException("INSUFFICIENT_TOKENS"); + } + throw e; + } + } + + // 5. Final Save Order savedOrder = orderRepository.save(order); System.out.println("Placed Order: " + savedOrder.getId() + " -> Display ID: #" + displayId); @@ -188,6 +209,13 @@ public class OrderController { "conflicts", conflicts != null ? conflicts : new ArrayList<>() )); } + if ("INSUFFICIENT_TOKENS".equals(e.getMessage())) { + return ResponseEntity.status(400).body(Map.of( + "success", false, + "errorType", "TOKEN_ERROR", + "message", "Insufficient Ritz Tokens. Please top up your wallet." + )); + } return ResponseEntity.status(500).body(Map.of("success", false, "message", e.getMessage() != null ? e.getMessage() : "Internal Server Error")); } diff --git a/backend/src/main/java/com/rit/canteen/sales/controller/WalletController.java b/backend/src/main/java/com/rit/canteen/sales/controller/WalletController.java new file mode 100644 index 00000000..6828a760 --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/controller/WalletController.java @@ -0,0 +1,59 @@ +package com.rit.canteen.sales.controller; + +import com.rit.canteen.sales.model.TokenTransaction; +import com.rit.canteen.sales.model.User; +import com.rit.canteen.sales.service.TokenService; +import com.rit.canteen.sales.service.UserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/wallet") +public class WalletController { + + @Autowired + private com.rit.canteen.sales.repository.UserRepository userRepository; + + @Autowired + private com.rit.canteen.sales.service.TokenService tokenService; + + @GetMapping("/balance/{userId}") + public ResponseEntity getBalance(@PathVariable Long userId) { + try { + User user = userRepository.findById(userId).orElse(null); + + if (user == null) return ResponseEntity.notFound().build(); + return ResponseEntity.ok(Map.of("balance", user.getRitzTokenBalance())); + } catch (Exception e) { + return ResponseEntity.status(500).body(Map.of("error", e.getMessage())); + } + } + + @GetMapping("/transactions/{userId}") + public ResponseEntity> getTransactions(@PathVariable Long userId) { + return ResponseEntity.ok(tokenService.getTransactions(userId)); + } + + @PostMapping("/topup") + public ResponseEntity topUp(@RequestBody Map request) { + try { + Long userId = Long.valueOf(request.get("userId").toString()); + BigDecimal amount = new BigDecimal(request.get("amount").toString()); + String ref = request.getOrDefault("referenceId", "TOPUP-" + System.currentTimeMillis()).toString(); + + User updatedUser = tokenService.topUp(userId, amount, ref); + return ResponseEntity.ok(Map.of( + "success", true, + "newBalance", updatedUser.getRitzTokenBalance(), + "message", "Successfully added " + amount + " Ritz Tokens" + )); + } 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/LoginResponse.java b/backend/src/main/java/com/rit/canteen/sales/model/LoginResponse.java index 35ed2828..e6d3a5bc 100644 --- a/backend/src/main/java/com/rit/canteen/sales/model/LoginResponse.java +++ b/backend/src/main/java/com/rit/canteen/sales/model/LoginResponse.java @@ -43,14 +43,16 @@ public class LoginResponse { private String mobileNumber; private String name; private boolean isLoggedIn; + private java.math.BigDecimal ritzTokenBalance; public UserDto() {} - public UserDto(Long id, String mobileNumber, String name, boolean isLoggedIn) { + public UserDto(Long id, String mobileNumber, String name, boolean isLoggedIn, java.math.BigDecimal ritzTokenBalance) { this.id = id; this.mobileNumber = mobileNumber; this.name = name; this.isLoggedIn = isLoggedIn; + this.ritzTokenBalance = ritzTokenBalance; } public Long getId() { return id; } @@ -63,6 +65,9 @@ public class LoginResponse { public void setName(String name) { this.name = name; } public boolean isLoggedIn() { return isLoggedIn; } - public void setLoggedIn(boolean loggedIn) { isLoggedIn = loggedIn; } + public void setLoggedIn(boolean loggedIn) { this.isLoggedIn = loggedIn; } + + public java.math.BigDecimal getRitzTokenBalance() { return ritzTokenBalance; } + public void setRitzTokenBalance(java.math.BigDecimal ritzTokenBalance) { this.ritzTokenBalance = ritzTokenBalance; } } } diff --git a/backend/src/main/java/com/rit/canteen/sales/model/TokenTransaction.java b/backend/src/main/java/com/rit/canteen/sales/model/TokenTransaction.java new file mode 100644 index 00000000..4d517719 --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/model/TokenTransaction.java @@ -0,0 +1,72 @@ +package com.rit.canteen.sales.model; + +import jakarta.persistence.*; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Entity +@Table(name = "token_transactions") +public class TokenTransaction { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @Column(nullable = false) + private BigDecimal amount; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private TransactionType type; + + @Column(length = 255) + private String description; + + @Column(nullable = false) + private LocalDateTime timestamp; + + @Column + private String referenceId; // e.g. Order Display ID or Payment ID + + public enum TransactionType { + TOPUP, + SPEND, + REFUND + } + + public TokenTransaction() {} + + public TokenTransaction(User user, BigDecimal amount, TransactionType type, String description, String referenceId) { + this.user = user; + this.amount = amount; + this.type = type; + this.description = description; + this.referenceId = referenceId; + this.timestamp = LocalDateTime.now(); + } + + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + + public User getUser() { return user; } + public void setUser(User user) { this.user = user; } + + public BigDecimal getAmount() { return amount; } + public void setAmount(BigDecimal amount) { this.amount = amount; } + + public TransactionType getType() { return type; } + public void setType(TransactionType type) { this.type = type; } + + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } + + public LocalDateTime getTimestamp() { return timestamp; } + public void setTimestamp(LocalDateTime timestamp) { this.timestamp = timestamp; } + + public String getReferenceId() { return referenceId; } + public void setReferenceId(String referenceId) { this.referenceId = referenceId; } +} diff --git a/backend/src/main/java/com/rit/canteen/sales/model/User.java b/backend/src/main/java/com/rit/canteen/sales/model/User.java index 4b8d5114..86097543 100644 --- a/backend/src/main/java/com/rit/canteen/sales/model/User.java +++ b/backend/src/main/java/com/rit/canteen/sales/model/User.java @@ -34,6 +34,9 @@ public class User { @Column(nullable = false) private LocalDateTime updatedAt; + @Column(nullable = false) + private java.math.BigDecimal ritzTokenBalance = java.math.BigDecimal.ZERO; + @Column(nullable = true) private LocalDateTime lastLoginAt; @@ -48,6 +51,9 @@ public class User { public String getName() { return name; } public void setName(String name) { this.name = name; } + public java.math.BigDecimal getRitzTokenBalance() { return ritzTokenBalance; } + public void setRitzTokenBalance(java.math.BigDecimal ritzTokenBalance) { this.ritzTokenBalance = ritzTokenBalance; } + public String getPinHash() { return pinHash; } public void setPinHash(String pinHash) { this.pinHash = pinHash; } diff --git a/backend/src/main/java/com/rit/canteen/sales/repository/TokenTransactionRepository.java b/backend/src/main/java/com/rit/canteen/sales/repository/TokenTransactionRepository.java new file mode 100644 index 00000000..ac4d91a7 --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/repository/TokenTransactionRepository.java @@ -0,0 +1,11 @@ +package com.rit.canteen.sales.repository; + +import com.rit.canteen.sales.model.TokenTransaction; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import java.util.List; + +@Repository +public interface TokenTransactionRepository extends JpaRepository { + List findByUserIdOrderByTimestampDesc(Long userId); +} diff --git a/backend/src/main/java/com/rit/canteen/sales/service/DatabaseSeeder.java b/backend/src/main/java/com/rit/canteen/sales/service/DatabaseSeeder.java index e6e03677..f39c03e5 100644 --- a/backend/src/main/java/com/rit/canteen/sales/service/DatabaseSeeder.java +++ b/backend/src/main/java/com/rit/canteen/sales/service/DatabaseSeeder.java @@ -28,6 +28,7 @@ public class DatabaseSeeder implements CommandLineRunner { public void run(String... args) throws Exception { repairStallsSchema(); repairOrdersSchema(); + repairUsersSchema(); repairFeedbackSchema(); repairLobColumns(); seedCategories(); @@ -54,6 +55,17 @@ public class DatabaseSeeder implements CommandLineRunner { } } + private void repairUsersSchema() { + System.out.println("Checking schema consistency for 'app_users' table..."); + try { + // Add ritz_token_balance if it doesn't exist + jdbcTemplate.execute("ALTER TABLE app_users ADD COLUMN IF NOT EXISTS ritz_token_balance NUMERIC(19, 2) DEFAULT 0 NOT NULL"); + System.out.println("Users schema consistency confirmed."); + } catch (Exception e) { + System.err.println("Users schema repair notice: " + e.getMessage()); + } + } + private void repairStallsSchema() { System.out.println("Checking schema consistency for 'stalls' table..."); try { diff --git a/backend/src/main/java/com/rit/canteen/sales/service/TokenService.java b/backend/src/main/java/com/rit/canteen/sales/service/TokenService.java new file mode 100644 index 00000000..19cb7c47 --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/service/TokenService.java @@ -0,0 +1,71 @@ +package com.rit.canteen.sales.service; + +import com.rit.canteen.sales.model.TokenTransaction; +import com.rit.canteen.sales.model.User; +import com.rit.canteen.sales.repository.TokenTransactionRepository; +import com.rit.canteen.sales.repository.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.util.List; + +@Service +public class TokenService { + + @Autowired + private UserRepository userRepository; + + @Autowired + private TokenTransactionRepository transactionRepository; + + public List getTransactions(Long userId) { + return transactionRepository.findByUserIdOrderByTimestampDesc(userId); + } + + @Transactional + public User topUp(Long userId, BigDecimal amount, String paymentRef) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new RuntimeException("User not found")); + + BigDecimal currentBalance = user.getRitzTokenBalance() != null ? user.getRitzTokenBalance() : BigDecimal.ZERO; + user.setRitzTokenBalance(currentBalance.add(amount)); + User savedUser = userRepository.save(user); + + TokenTransaction transaction = new TokenTransaction( + user, + amount, + TokenTransaction.TransactionType.TOPUP, + "Wallet Top Up via UPI/Card", + paymentRef + ); + transactionRepository.save(transaction); + + return savedUser; + } + + @Transactional + public void spend(Long userId, BigDecimal amount, String orderRef) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new RuntimeException("User not found")); + + BigDecimal currentBalance = user.getRitzTokenBalance() != null ? user.getRitzTokenBalance() : BigDecimal.ZERO; + + if (currentBalance.compareTo(amount) < 0) { + throw new RuntimeException("INSUFFICIENT_TOKENS"); + } + + user.setRitzTokenBalance(currentBalance.subtract(amount)); + userRepository.save(user); + + TokenTransaction transaction = new TokenTransaction( + user, + amount, + TokenTransaction.TransactionType.SPEND, + "Food Order Payment", + orderRef + ); + transactionRepository.save(transaction); + } +} diff --git a/backend/src/main/java/com/rit/canteen/sales/service/UserService.java b/backend/src/main/java/com/rit/canteen/sales/service/UserService.java index 28f54143..553dc195 100644 --- a/backend/src/main/java/com/rit/canteen/sales/service/UserService.java +++ b/backend/src/main/java/com/rit/canteen/sales/service/UserService.java @@ -61,7 +61,7 @@ public class UserService { userRepository.save(user); LoginResponse.UserDto userDto = new LoginResponse.UserDto( - user.getId(), user.getMobileNumber(), user.getName(), user.isLoggedIn() + user.getId(), user.getMobileNumber(), user.getName(), user.isLoggedIn(), user.getRitzTokenBalance() ); return new LoginResponse(true, "Registration successful. You are now logged in.", userDto); @@ -88,7 +88,7 @@ public class UserService { userRepository.save(user); LoginResponse.UserDto userDto = new LoginResponse.UserDto( - user.getId(), user.getMobileNumber(), user.getName(), user.isLoggedIn() + user.getId(), user.getMobileNumber(), user.getName(), user.isLoggedIn(), user.getRitzTokenBalance() ); return new LoginResponse(true, "Login successful.", userDto); @@ -154,7 +154,7 @@ public class UserService { return null; } User user = userOpt.get(); - return new LoginResponse.UserDto(user.getId(), user.getMobileNumber(), user.getName(), user.isLoggedIn()); + return new LoginResponse.UserDto(user.getId(), user.getMobileNumber(), user.getName(), user.isLoggedIn(), user.getRitzTokenBalance()); } /** @@ -172,7 +172,8 @@ public class UserService { user.getId(), user.getMobileNumber(), user.getName(), - user.isLoggedIn() + user.isLoggedIn(), + user.getRitzTokenBalance() )); } @@ -211,7 +212,7 @@ public class UserService { } userRepository.save(user); - return new LoginResponse.UserDto(user.getId(), user.getMobileNumber(), user.getName(), user.isLoggedIn()); + return new LoginResponse.UserDto(user.getId(), user.getMobileNumber(), user.getName(), user.isLoggedIn(), user.getRitzTokenBalance()); } /** diff --git a/frontend/src/pages/Customers.tsx b/frontend/src/pages/Customers.tsx index 6385c6ec..5e5af612 100644 --- a/frontend/src/pages/Customers.tsx +++ b/frontend/src/pages/Customers.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { Search, User, Phone, Tag, ChevronRight, UserCheck, UserMinus, MoreVertical, LayoutGrid, List, Edit2, Trash2, Shield, X, Eye, EyeOff, Loader2, AlertCircle, CheckCircle } from 'lucide-react'; +import { Search, User, Phone, Tag, ChevronRight, UserCheck, UserMinus, MoreVertical, LayoutGrid, List, Edit2, Trash2, Shield, X, Eye, EyeOff, Loader2, AlertCircle, CheckCircle, CircleDollarSign } from 'lucide-react'; import Pagination from '../components/Pagination'; interface UserDto { @@ -7,6 +7,7 @@ interface UserDto { mobileNumber: string; name: string; loggedIn: boolean; + ritzTokenBalance: number; } const Customers: React.FC = () => { @@ -241,6 +242,7 @@ const Customers: React.FC = () => { Customer Info Contact Info Membership + Wallet Balance Status Actions @@ -271,6 +273,21 @@ const Customers: React.FC = () => { REGULAR CUSTOMER + +
+
+ +
+ + R{user.ritzTokenBalance?.toLocaleString() || '0'} + +
+ + + + R{user.ritzTokenBalance?.toLocaleString() || '0'} + + { )) ) : ( - +

No customers found

@@ -346,6 +363,9 @@ const Customers: React.FC = () => { RIT STUDENT + + R{user.ritzTokenBalance?.toLocaleString() || '0'} TOKENS +
diff --git a/ordering_site/src/pages/ProfileScreen.tsx b/ordering_site/src/pages/ProfileScreen.tsx index 234a75b2..ae04bb35 100644 --- a/ordering_site/src/pages/ProfileScreen.tsx +++ b/ordering_site/src/pages/ProfileScreen.tsx @@ -8,9 +8,9 @@ import { ChevronRight, Phone, ShieldCheck, - User, X, - User as UserIcon + User as UserIcon, + Wallet } from 'lucide-react'; import { useAuth } from '../contexts/AuthContext'; import { motion, AnimatePresence } from 'framer-motion'; @@ -77,6 +77,7 @@ const ProfileScreen: React.FC = () => { const menuItems = [ { icon: ShoppingBag, label: 'My Orders', sub: 'View order history', path: '/orders' }, + { icon: Wallet, label: 'My Wallet', sub: 'Ritz Tokens & History', path: '/wallet' }, { icon: ShieldCheck, label: 'Account Security', sub: 'Change Security PIN', path: '/change-pin' }, { icon: Settings, label: 'Preferences', sub: 'Notifications, Language', path: '#' }, { icon: HelpCircle, label: 'Help & Support', sub: 'FAQs, Contact Us', path: '#' } diff --git a/ordering_site/src/types/index.ts b/ordering_site/src/types/index.ts index 5361fc15..87d5b1d6 100644 --- a/ordering_site/src/types/index.ts +++ b/ordering_site/src/types/index.ts @@ -33,6 +33,7 @@ export interface User { mobileNumber: string; name: string; isLoggedIn: boolean; + ritzTokenBalance: number; } export interface Stall {