From 3a93fea34ff43512ac73b04fa1dda10a93fd64ae Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 8 Mar 2026 09:11:26 -0600 Subject: [PATCH 01/14] Fix database seeding, add security and complete missing endpoints --- petshop-api.postman_collection.json | 10 +- .../controller/AdoptionController.java | 2 + .../controller/AppointmentController.java | 2 + .../backend/controller/AuthController.java | 1 + .../controller/CategoryController.java | 2 + .../controller/CustomerController.java | 2 + .../backend/controller/HealthController.java | 18 ++ .../backend/controller/PetController.java | 2 + .../backend/controller/ProductController.java | 2 + .../backend/controller/SaleController.java | 2 + .../backend/controller/ServiceController.java | 2 + .../backend/controller/StoreController.java | 35 +++ .../backend/dto/store/StoreRequest.java | 78 +++++++ .../backend/security/SecurityConfig.java | 1 + .../petshop/backend/service/StoreService.java | 49 +++++ src/main/resources/application.yml | 7 + src/main/resources/data.sql | 205 ++++++++++++++++++ src/main/resources/schema.sql | 201 +++++++++++++++++ 18 files changed, 620 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/petshop/backend/controller/HealthController.java create mode 100644 src/main/java/com/petshop/backend/dto/store/StoreRequest.java create mode 100644 src/main/resources/data.sql create mode 100644 src/main/resources/schema.sql diff --git a/petshop-api.postman_collection.json b/petshop-api.postman_collection.json index a2537681..7777a8ac 100644 --- a/petshop-api.postman_collection.json +++ b/petshop-api.postman_collection.json @@ -114,7 +114,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"username\": \"admin\",\n \"password\": \"admin\"\n}", + "raw": "{\n \"username\": \"admin\",\n \"password\": \"admin123\"\n}", "options": { "raw": { "language": "json" @@ -245,6 +245,14 @@ ] } } + }, + { + "name": "Health Check", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/health", + "header": [] + } } ] }, diff --git a/src/main/java/com/petshop/backend/controller/AdoptionController.java b/src/main/java/com/petshop/backend/controller/AdoptionController.java index 30da7d5e..d200a3db 100644 --- a/src/main/java/com/petshop/backend/controller/AdoptionController.java +++ b/src/main/java/com/petshop/backend/controller/AdoptionController.java @@ -9,10 +9,12 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/v1/adoptions") +@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public class AdoptionController { private final AdoptionService adoptionService; diff --git a/src/main/java/com/petshop/backend/controller/AppointmentController.java b/src/main/java/com/petshop/backend/controller/AppointmentController.java index 6c9f8fa4..b607a577 100644 --- a/src/main/java/com/petshop/backend/controller/AppointmentController.java +++ b/src/main/java/com/petshop/backend/controller/AppointmentController.java @@ -9,6 +9,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import java.time.LocalDate; @@ -16,6 +17,7 @@ import java.util.List; @RestController @RequestMapping("/api/v1/appointments") +@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public class AppointmentController { private final AppointmentService appointmentService; diff --git a/src/main/java/com/petshop/backend/controller/AuthController.java b/src/main/java/com/petshop/backend/controller/AuthController.java index d0a6cc39..4055893e 100644 --- a/src/main/java/com/petshop/backend/controller/AuthController.java +++ b/src/main/java/com/petshop/backend/controller/AuthController.java @@ -88,6 +88,7 @@ public class AuthController { public ResponseEntity logout() { Map response = new HashMap<>(); response.put("message", "Logged out successfully"); + response.put("note", "Token remains valid until expiration. Clear token from client storage."); return ResponseEntity.ok(response); } } diff --git a/src/main/java/com/petshop/backend/controller/CategoryController.java b/src/main/java/com/petshop/backend/controller/CategoryController.java index ddd7b934..5e1b80c4 100644 --- a/src/main/java/com/petshop/backend/controller/CategoryController.java +++ b/src/main/java/com/petshop/backend/controller/CategoryController.java @@ -9,10 +9,12 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/v1/categories") +@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public class CategoryController { private final CategoryService categoryService; diff --git a/src/main/java/com/petshop/backend/controller/CustomerController.java b/src/main/java/com/petshop/backend/controller/CustomerController.java index 75bed4fc..f3ab880e 100644 --- a/src/main/java/com/petshop/backend/controller/CustomerController.java +++ b/src/main/java/com/petshop/backend/controller/CustomerController.java @@ -9,10 +9,12 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/v1/customers") +@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public class CustomerController { private final CustomerService customerService; diff --git a/src/main/java/com/petshop/backend/controller/HealthController.java b/src/main/java/com/petshop/backend/controller/HealthController.java new file mode 100644 index 00000000..8ee609c3 --- /dev/null +++ b/src/main/java/com/petshop/backend/controller/HealthController.java @@ -0,0 +1,18 @@ +package com.petshop.backend.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Map; + +@RestController +@RequestMapping("/api/v1/health") +public class HealthController { + + @GetMapping + public ResponseEntity> healthCheck() { + return ResponseEntity.ok(Map.of("status", "UP")); + } +} diff --git a/src/main/java/com/petshop/backend/controller/PetController.java b/src/main/java/com/petshop/backend/controller/PetController.java index 7ae7ca64..07532b93 100644 --- a/src/main/java/com/petshop/backend/controller/PetController.java +++ b/src/main/java/com/petshop/backend/controller/PetController.java @@ -9,10 +9,12 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/v1/pets") +@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public class PetController { private final PetService petService; diff --git a/src/main/java/com/petshop/backend/controller/ProductController.java b/src/main/java/com/petshop/backend/controller/ProductController.java index ada0e4dc..f281fb57 100644 --- a/src/main/java/com/petshop/backend/controller/ProductController.java +++ b/src/main/java/com/petshop/backend/controller/ProductController.java @@ -9,10 +9,12 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/v1/products") +@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public class ProductController { private final ProductService productService; diff --git a/src/main/java/com/petshop/backend/controller/SaleController.java b/src/main/java/com/petshop/backend/controller/SaleController.java index aae791fe..2426bb19 100644 --- a/src/main/java/com/petshop/backend/controller/SaleController.java +++ b/src/main/java/com/petshop/backend/controller/SaleController.java @@ -8,10 +8,12 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/v1/sales") +@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public class SaleController { private final SaleService saleService; diff --git a/src/main/java/com/petshop/backend/controller/ServiceController.java b/src/main/java/com/petshop/backend/controller/ServiceController.java index 53bb5343..a7160a62 100644 --- a/src/main/java/com/petshop/backend/controller/ServiceController.java +++ b/src/main/java/com/petshop/backend/controller/ServiceController.java @@ -9,10 +9,12 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/v1/services") +@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public class ServiceController { private final ServiceService serviceService; diff --git a/src/main/java/com/petshop/backend/controller/StoreController.java b/src/main/java/com/petshop/backend/controller/StoreController.java index 4fa716be..58110d7e 100644 --- a/src/main/java/com/petshop/backend/controller/StoreController.java +++ b/src/main/java/com/petshop/backend/controller/StoreController.java @@ -1,14 +1,20 @@ package com.petshop.backend.controller; +import com.petshop.backend.dto.common.BulkDeleteRequest; +import com.petshop.backend.dto.store.StoreRequest; import com.petshop.backend.dto.store.StoreResponse; import com.petshop.backend.service.StoreService; +import jakarta.validation.Valid; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/v1/stores") +@PreAuthorize("hasRole('ADMIN')") public class StoreController { private final StoreService storeService; @@ -23,4 +29,33 @@ public class StoreController { Pageable pageable) { return ResponseEntity.ok(storeService.getAllStores(q, pageable)); } + + @GetMapping("/{id}") + public ResponseEntity getStoreById(@PathVariable Long id) { + return ResponseEntity.ok(storeService.getStoreById(id)); + } + + @PostMapping + public ResponseEntity createStore(@Valid @RequestBody StoreRequest request) { + return ResponseEntity.status(HttpStatus.CREATED).body(storeService.createStore(request)); + } + + @PutMapping("/{id}") + public ResponseEntity updateStore( + @PathVariable Long id, + @Valid @RequestBody StoreRequest request) { + return ResponseEntity.ok(storeService.updateStore(id, request)); + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteStore(@PathVariable Long id) { + storeService.deleteStore(id); + return ResponseEntity.noContent().build(); + } + + @DeleteMapping + public ResponseEntity bulkDeleteStores(@Valid @RequestBody BulkDeleteRequest request) { + storeService.bulkDeleteStores(request); + return ResponseEntity.noContent().build(); + } } diff --git a/src/main/java/com/petshop/backend/dto/store/StoreRequest.java b/src/main/java/com/petshop/backend/dto/store/StoreRequest.java new file mode 100644 index 00000000..d6b5fc12 --- /dev/null +++ b/src/main/java/com/petshop/backend/dto/store/StoreRequest.java @@ -0,0 +1,78 @@ +package com.petshop.backend.dto.store; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import java.util.Objects; + +public class StoreRequest { + @NotBlank(message = "Store name is required") + private String storeName; + + @NotBlank(message = "Address is required") + private String address; + + @NotBlank(message = "Phone is required") + private String phone; + + @NotBlank(message = "Email is required") + @Email(message = "Email must be valid") + private String email; + + public String getStoreName() { + return storeName; + } + + public void setStoreName(String storeName) { + this.storeName = storeName; + } + + public String getAddress() { + return address; + } + + public void setAddress(String address) { + this.address = address; + } + + public String getPhone() { + return phone; + } + + public void setPhone(String phone) { + this.phone = phone; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + StoreRequest that = (StoreRequest) o; + return Objects.equals(storeName, that.storeName) && + Objects.equals(address, that.address) && + Objects.equals(phone, that.phone) && + Objects.equals(email, that.email); + } + + @Override + public int hashCode() { + return Objects.hash(storeName, address, phone, email); + } + + @Override + public String toString() { + return "StoreRequest{" + + "storeName='" + storeName + '\'' + + ", address='" + address + '\'' + + ", phone='" + phone + '\'' + + ", email='" + email + '\'' + + '}'; + } +} diff --git a/src/main/java/com/petshop/backend/security/SecurityConfig.java b/src/main/java/com/petshop/backend/security/SecurityConfig.java index dadf9d93..4f5ecb51 100644 --- a/src/main/java/com/petshop/backend/security/SecurityConfig.java +++ b/src/main/java/com/petshop/backend/security/SecurityConfig.java @@ -37,6 +37,7 @@ public class SecurityConfig { .csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests(auth -> auth .requestMatchers("/api/v1/auth/login").permitAll() + .requestMatchers("/api/v1/health").permitAll() .requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/swagger-ui.html").permitAll() .requestMatchers(HttpMethod.GET, "/api/v1/dropdowns/suppliers").hasRole("ADMIN") .requestMatchers("/api/v1/inventory/**").hasRole("ADMIN") diff --git a/src/main/java/com/petshop/backend/service/StoreService.java b/src/main/java/com/petshop/backend/service/StoreService.java index 2d7564e7..5d2c9ce3 100644 --- a/src/main/java/com/petshop/backend/service/StoreService.java +++ b/src/main/java/com/petshop/backend/service/StoreService.java @@ -1,11 +1,15 @@ package com.petshop.backend.service; +import com.petshop.backend.dto.common.BulkDeleteRequest; +import com.petshop.backend.dto.store.StoreRequest; import com.petshop.backend.dto.store.StoreResponse; import com.petshop.backend.entity.StoreLocation; +import com.petshop.backend.exception.ResourceNotFoundException; import com.petshop.backend.repository.StoreRepository; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service public class StoreService { @@ -26,6 +30,51 @@ public class StoreService { return stores.map(this::mapToResponse); } + public StoreResponse getStoreById(Long id) { + StoreLocation store = storeRepository.findById(id) + .orElseThrow(() -> new ResourceNotFoundException("Store not found with id: " + id)); + return mapToResponse(store); + } + + @Transactional + public StoreResponse createStore(StoreRequest request) { + StoreLocation store = new StoreLocation(); + store.setStoreName(request.getStoreName()); + store.setAddress(request.getAddress()); + store.setPhone(request.getPhone()); + store.setEmail(request.getEmail()); + + store = storeRepository.save(store); + return mapToResponse(store); + } + + @Transactional + public StoreResponse updateStore(Long id, StoreRequest request) { + StoreLocation store = storeRepository.findById(id) + .orElseThrow(() -> new ResourceNotFoundException("Store not found with id: " + id)); + + store.setStoreName(request.getStoreName()); + store.setAddress(request.getAddress()); + store.setPhone(request.getPhone()); + store.setEmail(request.getEmail()); + + store = storeRepository.save(store); + return mapToResponse(store); + } + + @Transactional + public void deleteStore(Long id) { + if (!storeRepository.existsById(id)) { + throw new ResourceNotFoundException("Store not found with id: " + id); + } + storeRepository.deleteById(id); + } + + @Transactional + public void bulkDeleteStores(BulkDeleteRequest request) { + storeRepository.deleteAllById(request.getIds()); + } + private StoreResponse mapToResponse(StoreLocation store) { return new StoreResponse( store.getStoreId(), diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index f04fe658..2b7c9b34 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -8,6 +8,13 @@ spring: password: ${SPRING_DATASOURCE_PASSWORD:petshop} driver-class-name: com.mysql.cj.jdbc.Driver + sql: + init: + mode: always + schema-locations: classpath:schema.sql + data-locations: classpath:data.sql + continue-on-error: false + jpa: hibernate: ddl-auto: validate diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql new file mode 100644 index 00000000..b6a12775 --- /dev/null +++ b/src/main/resources/data.sql @@ -0,0 +1,205 @@ +-- Insert Sample Data + +INSERT INTO storeLocation (storeName, address, phone, email) +VALUES +('Downtown Branch', '123 Main St', '123-456-7890', 'downtown@petshop.com'), +('North Branch', '456 North Ave', '987-654-3210', 'north@petshop.com'), +('West Side Store', '789 West Blvd', '555-123-4567', 'westside@petshop.com'), +('East End Shop', '321 East Road', '555-987-6543', 'eastend@petshop.com'), +('South Mall Location', '654 South Plaza', '555-246-8135', 'southmall@petshop.com'); + +INSERT INTO employee (firstName, lastName, email, phone, role, isActive) +VALUES +('John', 'Doe', 'john@petshop.com', '111-222-3333', 'Manager', TRUE), +('Sara', 'Smith', 'sara@petshop.com', '444-555-6666', 'Staff', TRUE), +('Michael', 'Johnson', 'michael@petshop.com', '222-333-4444', 'Groomer', TRUE), +('Lisa', 'Williams', 'lisa@petshop.com', '333-444-5555', 'Staff', TRUE), +('David', 'Brown', 'david@petshop.com', '555-666-7777', 'Veterinarian', TRUE), +('Emma', 'Davis', 'emma@petshop.com', '666-777-8888', 'Manager', FALSE); + +INSERT INTO employeeStore (employeeId, storeId) +VALUES +(1, 1), +(2, 1), +(2, 2), +(3, 2), +(4, 3), +(5, 1), +(5, 4), +(6, 5); + +INSERT INTO customer (firstName, lastName, email, phone) +VALUES +('Alex', 'Brown', 'alex@gmail.com', '777-888-9999'), +('Emily', 'Clark', 'emily@gmail.com', '666-555-4444'), +('James', 'Wilson', 'james@gmail.com', '888-999-0000'), +('Olivia', 'Martinez', 'olivia@gmail.com', '999-000-1111'), +('William', 'Anderson', 'william@gmail.com', '000-111-2222'), +('Sophia', 'Taylor', 'sophia@gmail.com', '111-222-3333'); + +INSERT INTO pet (petName, petSpecies, petBreed, petAge, petStatus, petPrice) +VALUES +('Buddy', 'Dog', 'Labrador', 2, 'Available', 500.00), +('Milo', 'Cat', 'Persian', 1, 'Available', 300.00), +('Charlie', 'Dog', 'Golden Retriever', 3, 'Available', 550.00), +('Luna', 'Cat', 'Siamese', 2, 'Adopted', 350.00), +('Max', 'Dog', 'Beagle', 1, 'Available', 450.00), +('Bella', 'Cat', 'Maine Coon', 4, 'Available', 400.00); + +INSERT INTO adoption (petId, customerId, adoptionDate, adoptionStatus) +VALUES +(1, 1, '2026-01-15', 'Completed'), +(4, 3, '2026-01-20', 'Completed'), +(2, 2, '2026-01-25', 'Pending'), +(5, 4, '2026-02-01', 'Completed'), +(6, 5, '2026-02-02', 'Pending'); + +INSERT INTO supplier (supCompany, supContactFirstName, supContactLastName, supEmail, supPhone) +VALUES +('PetFood Inc', 'Robert', 'King', 'contact@petfood.com', '888-111-2222'), +('Toy World', 'Jennifer', 'Lee', 'sales@toyworld.com', '888-222-3333'), +('Pet Supplies Co', 'Kevin', 'White', 'info@petsupplies.com', '888-333-4444'), +('Animal Care Products', 'Nancy', 'Green', 'orders@animalcare.com', '888-444-5555'), +('Premium Pet Goods', 'Tom', 'Black', 'support@premiumpet.com', '888-555-6666'); + +INSERT INTO category (categoryName, categoryType) +VALUES +('Dog Food', 'Product'), +('Cat Toys', 'Product'), +('Bird Supplies', 'Product'), +('Aquarium', 'Product'), +('Small Animals', 'Product'); + +INSERT INTO product (prodName, prodPrice, categoryId, prodDesc) +VALUES +('Premium Dog Food', 50.00, 1, 'High quality dog food'), +('Cat Toy Ball', 10.00, 2, 'Colorful toy for cats'), +('Bird Cage Large', 120.00, 3, 'Spacious bird cage'), +('Fish Tank 20 Gallon', 80.00, 4, 'Complete aquarium kit'), +('Hamster Wheel', 15.00, 5, 'Exercise wheel for small pets'), +('Organic Dog Treats', 25.00, 1, 'Natural dog treats'); + +INSERT INTO productSupplier (supId, prodId, cost) +VALUES +(1, 1, 35.00), +(1, 2, 6.50), +(2, 2, 7.00), +(3, 3, 90.00), +(3, 4, 60.00), +(4, 5, 10.00), +(5, 6, 18.00), +(1, 6, 17.50); + +INSERT INTO inventory (prodId, quantity) +VALUES +(1, 100), +(2, 200), +(3, 50), +(4, 30), +(5, 150), +(6, 75); + +INSERT INTO service (serviceName, serviceDesc, serviceDuration, servicePrice) +VALUES +('Pet Grooming', 'Full grooming service', 60, 40.00), +('Nail Trimming', 'Quick nail trim', 15, 10.00), +('Bath and Brush', 'Bathing and brushing service', 45, 30.00), +('Veterinary Checkup', 'Complete health examination', 30, 75.00), +('Teeth Cleaning', 'Professional dental cleaning', 90, 100.00); + +INSERT INTO appointment (serviceId, customerId, appointmentDate, appointmentTime, appointmentStatus) +VALUES +(1, 2, '2026-02-01', '10:30:00', 'Booked'), +(2, 1, '2026-02-03', '14:00:00', 'Booked'), +(3, 3, '2026-02-05', '09:00:00', 'Completed'), +(4, 4, '2026-02-07', '11:30:00', 'Booked'), +(5, 5, '2026-02-10', '15:00:00', 'Cancelled'); + +INSERT INTO appointmentPet (appointmentId, petId) +VALUES +(1, 2), +(2, 1), +(3, 3), +(4, 5), +(5, 6); + +INSERT INTO sale (saleDate, totalAmount, paymentMethod, employeeId, storeId) +VALUES +('2026-01-05 09:15:00', 125.00, 'Card', 1, 1), +('2026-01-08 11:30:00', 200.00, 'Card', 2, 1), +('2026-01-12 14:20:00', 60.00, 'Cash', 3, 2), +('2026-01-15 10:45:00', 150.00, 'Debit', 1, 1), +('2026-01-18 16:30:00', 80.00, 'Card', 4, 3), +('2026-01-22 13:15:00', 95.00, 'Cash', 2, 2), +('2026-01-25 15:40:00', 240.00, 'Card', 5, 4), +('2026-01-28 10:30:00', 80.00, 'Cash', 1, 1), +('2026-02-01 09:00:00', 175.00, 'Card', 3, 3), +('2026-02-03 11:20:00', 120.00, 'Card', 2, 1), +('2026-02-05 14:50:00', 45.00, 'Cash', 4, 2), +('2026-02-08 16:15:00', 160.00, 'Debit', 1, 1), +('2026-02-10 10:25:00', 100.00, 'Card', 5, 4), +('2026-02-12 13:45:00', 50.00, 'Cash', 2, 2), +('2026-02-15 15:30:00', 85.00, 'Card', 3, 3), +('2026-02-18 11:10:00', 200.00, 'Card', 1, 1), +('2026-02-20 14:35:00', 155.00, 'Debit', 4, 3), +('2026-02-22 16:50:00', 75.00, 'Cash', 2, 1), +('2026-02-24 10:15:00', 140.00, 'Card', 5, 4), +(NOW(), 95.00, 'Card', 1, 1); + +INSERT INTO saleItem (saleId, prodId, quantity, unitPrice) +VALUES +(1, 1, 2, 50.00), +(1, 6, 1, 25.00), +(2, 3, 1, 120.00), +(2, 4, 1, 80.00), +(3, 2, 3, 10.00), +(3, 5, 2, 15.00), +(4, 1, 3, 50.00), +(5, 4, 1, 80.00), +(6, 2, 4, 10.00), +(6, 5, 1, 15.00), +(6, 6, 1, 25.00), +(6, 1, 1, 50.00), +(7, 3, 2, 120.00), +(8, 1, 1, 50.00), +(8, 2, 3, 10.00), +(9, 1, 3, 50.00), +(9, 6, 1, 25.00), +(10, 3, 1, 120.00), +(11, 5, 1, 15.00), +(11, 2, 3, 10.00), +(12, 4, 2, 80.00), +(13, 6, 4, 25.00), +(14, 1, 1, 50.00), +(15, 2, 2, 10.00), +(15, 5, 1, 15.00), +(15, 6, 2, 25.00), +(16, 3, 1, 120.00), +(16, 4, 1, 80.00), +(17, 4, 1, 80.00), +(17, 1, 1, 50.00), +(17, 6, 1, 25.00), +(18, 6, 2, 25.00), +(18, 2, 2, 10.00), +(18, 5, 1, 15.00), +(19, 1, 2, 50.00), +(19, 6, 2, 25.00), +(20, 2, 5, 10.00), +(20, 5, 3, 15.00); + +INSERT INTO purchaseOrder (supId, orderDate, status) +VALUES +(1, '2025-01-15', 'Delivered'), +(2, '2025-01-20', 'Pending'), +(3, '2025-02-01', 'Delivered'), +(4, '2025-02-10', 'In Transit'), +(1, '2025-02-15', 'Pending'); + +INSERT INTO activityLog (employeeId, activity) +VALUES +(1, 'Created new sale'), +(2, 'Booked appointment'), +(3, 'Completed grooming service'), +(4, 'Processed inventory order'), +(5, 'Conducted health checkup'), +(1, 'Updated customer information'); diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql new file mode 100644 index 00000000..35ee87ca --- /dev/null +++ b/src/main/resources/schema.sql @@ -0,0 +1,201 @@ +-- Create Tables + +CREATE TABLE IF NOT EXISTS storeLocation ( + storeId BIGINT AUTO_INCREMENT PRIMARY KEY, + storeName VARCHAR(100) NOT NULL, + address VARCHAR(255) NOT NULL, + phone VARCHAR(20) NOT NULL, + email VARCHAR(100) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS employee ( + employeeId BIGINT AUTO_INCREMENT PRIMARY KEY, + firstName VARCHAR(50) NOT NULL, + lastName VARCHAR(50) NOT NULL, + email VARCHAR(100) NOT NULL, + phone VARCHAR(20) NOT NULL, + role VARCHAR(50) NOT NULL, + isActive BOOLEAN DEFAULT TRUE NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS employeeStore ( + employeeId BIGINT NOT NULL, + storeId BIGINT NOT NULL, + PRIMARY KEY (employeeId, storeId), + FOREIGN KEY (employeeId) REFERENCES employee(employeeId), + FOREIGN KEY (storeId) REFERENCES storeLocation(storeId) +); + +CREATE TABLE IF NOT EXISTS customer ( + customerId BIGINT AUTO_INCREMENT PRIMARY KEY, + firstName VARCHAR(50) NOT NULL, + lastName VARCHAR(50) NOT NULL, + email VARCHAR(100) NOT NULL, + phone VARCHAR(20) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS pet ( + petId BIGINT AUTO_INCREMENT PRIMARY KEY, + petName VARCHAR(50) NOT NULL, + petSpecies VARCHAR(50) NOT NULL, + petBreed VARCHAR(50) NOT NULL, + petAge INT NOT NULL, + petStatus VARCHAR(20) NOT NULL, + petPrice DECIMAL(10, 2) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS adoption ( + adoptionId BIGINT AUTO_INCREMENT PRIMARY KEY, + petId BIGINT NOT NULL, + customerId BIGINT NOT NULL, + adoptionDate DATE NOT NULL, + adoptionStatus VARCHAR(20) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (petId) REFERENCES pet(petId), + FOREIGN KEY (customerId) REFERENCES customer(customerId) +); + +CREATE TABLE IF NOT EXISTS supplier ( + supId BIGINT AUTO_INCREMENT PRIMARY KEY, + supCompany VARCHAR(100) NOT NULL, + supContactFirstName VARCHAR(50) NOT NULL, + supContactLastName VARCHAR(50) NOT NULL, + supEmail VARCHAR(100) NOT NULL, + supPhone VARCHAR(20) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS category ( + categoryId BIGINT AUTO_INCREMENT PRIMARY KEY, + categoryName VARCHAR(100) NOT NULL, + categoryType VARCHAR(50) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS product ( + prodId BIGINT AUTO_INCREMENT PRIMARY KEY, + prodName VARCHAR(100) NOT NULL, + prodPrice DECIMAL(10, 2) NOT NULL, + categoryId BIGINT NOT NULL, + prodDesc TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (categoryId) REFERENCES category(categoryId) +); + +CREATE TABLE IF NOT EXISTS productSupplier ( + supId BIGINT NOT NULL, + prodId BIGINT NOT NULL, + cost DECIMAL(10, 2) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (supId, prodId), + FOREIGN KEY (supId) REFERENCES supplier(supId), + FOREIGN KEY (prodId) REFERENCES product(prodId) +); + +CREATE TABLE IF NOT EXISTS inventory ( + inventoryId BIGINT AUTO_INCREMENT PRIMARY KEY, + prodId BIGINT NOT NULL, + quantity INT DEFAULT 0 NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (prodId) REFERENCES product(prodId) +); + +CREATE TABLE IF NOT EXISTS service ( + serviceId BIGINT AUTO_INCREMENT PRIMARY KEY, + serviceName VARCHAR(100) NOT NULL, + serviceDesc TEXT, + serviceDuration INT NOT NULL, + servicePrice DECIMAL(10, 2) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS appointment ( + appointmentId BIGINT AUTO_INCREMENT PRIMARY KEY, + serviceId BIGINT NOT NULL, + customerId BIGINT NOT NULL, + appointmentDate DATE NOT NULL, + appointmentTime TIME NOT NULL, + appointmentStatus VARCHAR(20) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (serviceId) REFERENCES service(serviceId), + FOREIGN KEY (customerId) REFERENCES customer(customerId) +); + +CREATE TABLE IF NOT EXISTS appointmentPet ( + appointmentId BIGINT NOT NULL, + petId BIGINT NOT NULL, + PRIMARY KEY (appointmentId, petId), + FOREIGN KEY (appointmentId) REFERENCES appointment(appointmentId), + FOREIGN KEY (petId) REFERENCES pet(petId) +); + +CREATE TABLE IF NOT EXISTS sale ( + saleId BIGINT AUTO_INCREMENT PRIMARY KEY, + saleDate DATETIME NOT NULL, + totalAmount DECIMAL(10, 2) NOT NULL, + paymentMethod VARCHAR(50) NOT NULL, + employeeId BIGINT NOT NULL, + storeId BIGINT NOT NULL, + isRefund BOOLEAN DEFAULT FALSE NOT NULL, + originalSaleId BIGINT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (employeeId) REFERENCES employee(employeeId), + FOREIGN KEY (storeId) REFERENCES storeLocation(storeId), + FOREIGN KEY (originalSaleId) REFERENCES sale(saleId) +); + +CREATE TABLE IF NOT EXISTS saleItem ( + saleItemId BIGINT AUTO_INCREMENT PRIMARY KEY, + saleId BIGINT NOT NULL, + prodId BIGINT NOT NULL, + quantity INT NOT NULL, + unitPrice DECIMAL(10, 2) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (saleId) REFERENCES sale(saleId), + FOREIGN KEY (prodId) REFERENCES product(prodId) +); + +CREATE TABLE IF NOT EXISTS purchaseOrder ( + purchaseOrderId BIGINT AUTO_INCREMENT PRIMARY KEY, + supId BIGINT NOT NULL, + orderDate DATE NOT NULL, + status VARCHAR(50) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (supId) REFERENCES supplier(supId) +); + +CREATE TABLE IF NOT EXISTS activityLog ( + logId BIGINT AUTO_INCREMENT PRIMARY KEY, + employeeId BIGINT NOT NULL, + activity TEXT NOT NULL, + logTimestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (employeeId) REFERENCES employee(employeeId) +); + +CREATE TABLE IF NOT EXISTS users ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + username VARCHAR(50) UNIQUE NOT NULL, + password VARCHAR(255) NOT NULL, + role VARCHAR(20) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); From ad81bd031d09b4e59d1e6d6a78528d332691790f Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 8 Mar 2026 09:39:37 -0600 Subject: [PATCH 02/14] Allow public viewing of pets and sales --- docker-compose.yml | 1 - petshop-api.postman_collection.json | 36 +++++++++++++++++++ .../backend/controller/PetController.java | 5 ++- .../backend/controller/SaleController.java | 2 +- .../backend/security/SecurityConfig.java | 2 ++ 5 files changed, 43 insertions(+), 3 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 3e8a175b..1966e7e6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,7 +11,6 @@ services: - "3306:3306" volumes: - db_data:/var/lib/mysql - - ./sql:/docker-entrypoint-initdb.d healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-proot"] interval: 5s diff --git a/petshop-api.postman_collection.json b/petshop-api.postman_collection.json index 7777a8ac..1a5eca4e 100644 --- a/petshop-api.postman_collection.json +++ b/petshop-api.postman_collection.json @@ -137,6 +137,42 @@ } ] }, + { + "name": "Login (Staff) -> sets staffToken", + "request": { + "method": "POST", + "url": "{{baseUrl}}/api/v1/auth/login", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"username\": \"staff\",\n \"password\": \"staff123\"\n}", + "options": { + "raw": { + "language": "json" + } + } + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "try {", + " const json = pm.response.json();", + " if (json && json.token) pm.collectionVariables.set('staffToken', json.token);", + "} catch (e) {}" + ] + } + } + ] + }, { "name": "Login (Customer) -> sets customerToken", "request": { diff --git a/src/main/java/com/petshop/backend/controller/PetController.java b/src/main/java/com/petshop/backend/controller/PetController.java index 07532b93..259b0f89 100644 --- a/src/main/java/com/petshop/backend/controller/PetController.java +++ b/src/main/java/com/petshop/backend/controller/PetController.java @@ -14,7 +14,6 @@ import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/v1/pets") -@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public class PetController { private final PetService petService; @@ -36,11 +35,13 @@ public class PetController { } @PostMapping + @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public ResponseEntity createPet(@Valid @RequestBody PetRequest request) { return ResponseEntity.status(HttpStatus.CREATED).body(petService.createPet(request)); } @PutMapping("/{id}") + @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public ResponseEntity updatePet( @PathVariable Long id, @Valid @RequestBody PetRequest request) { @@ -48,12 +49,14 @@ public class PetController { } @DeleteMapping("/{id}") + @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public ResponseEntity deletePet(@PathVariable Long id) { petService.deletePet(id); return ResponseEntity.noContent().build(); } @DeleteMapping + @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public ResponseEntity bulkDeletePets(@Valid @RequestBody BulkDeleteRequest request) { petService.bulkDeletePets(request); return ResponseEntity.noContent().build(); diff --git a/src/main/java/com/petshop/backend/controller/SaleController.java b/src/main/java/com/petshop/backend/controller/SaleController.java index 2426bb19..d7e165fe 100644 --- a/src/main/java/com/petshop/backend/controller/SaleController.java +++ b/src/main/java/com/petshop/backend/controller/SaleController.java @@ -13,7 +13,6 @@ import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/v1/sales") -@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public class SaleController { private final SaleService saleService; @@ -35,6 +34,7 @@ public class SaleController { } @PostMapping + @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public ResponseEntity createSale(@Valid @RequestBody SaleRequest request) { return ResponseEntity.status(HttpStatus.CREATED).body(saleService.createSale(request)); } diff --git a/src/main/java/com/petshop/backend/security/SecurityConfig.java b/src/main/java/com/petshop/backend/security/SecurityConfig.java index 4f5ecb51..1e96882c 100644 --- a/src/main/java/com/petshop/backend/security/SecurityConfig.java +++ b/src/main/java/com/petshop/backend/security/SecurityConfig.java @@ -39,6 +39,8 @@ public class SecurityConfig { .requestMatchers("/api/v1/auth/login").permitAll() .requestMatchers("/api/v1/health").permitAll() .requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/swagger-ui.html").permitAll() + .requestMatchers(HttpMethod.GET, "/api/v1/pets/**").permitAll() + .requestMatchers(HttpMethod.GET, "/api/v1/sales/**").permitAll() .requestMatchers(HttpMethod.GET, "/api/v1/dropdowns/suppliers").hasRole("ADMIN") .requestMatchers("/api/v1/inventory/**").hasRole("ADMIN") .requestMatchers("/api/v1/suppliers/**").hasRole("ADMIN") From 6b182f2cc21b63490379ce2f224afd0c08eb7381 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 8 Mar 2026 10:14:08 -0600 Subject: [PATCH 03/14] Fix Pet entity ID mapping for JPA compatibility --- .../java/com/petshop/backend/entity/Pet.java | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/petshop/backend/entity/Pet.java b/src/main/java/com/petshop/backend/entity/Pet.java index cbbb1f90..e827f612 100644 --- a/src/main/java/com/petshop/backend/entity/Pet.java +++ b/src/main/java/com/petshop/backend/entity/Pet.java @@ -14,7 +14,8 @@ public class Pet { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long petId; + @Column(name = "petId") + private Long id; @Column(nullable = false, length = 50) private String petName; @@ -45,8 +46,8 @@ public class Pet { public Pet() { } - public Pet(Long petId, String petName, String petSpecies, String petBreed, Integer petAge, String petStatus, BigDecimal petPrice, LocalDateTime createdAt, LocalDateTime updatedAt) { - this.petId = petId; + public Pet(Long id, String petName, String petSpecies, String petBreed, Integer petAge, String petStatus, BigDecimal petPrice, LocalDateTime createdAt, LocalDateTime updatedAt) { + this.id = id; this.petName = petName; this.petSpecies = petSpecies; this.petBreed = petBreed; @@ -58,11 +59,11 @@ public class Pet { } public Long getPetId() { - return petId; + return id; } - public void setPetId(Long petId) { - this.petId = petId; + public void setPetId(Long id) { + this.id = id; } public String getPetName() { @@ -134,18 +135,18 @@ public class Pet { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Pet pet = (Pet) o; - return Objects.equals(petId, pet.petId); + return Objects.equals(id, pet.id); } @Override public int hashCode() { - return Objects.hash(petId); + return Objects.hash(id); } @Override public String toString() { return "Pet{" + - "petId=" + petId + + "id=" + id + ", petName='" + petName + '\'' + ", petSpecies='" + petSpecies + '\'' + ", petBreed='" + petBreed + '\'' + From 2dedd5508f1ddc6ae4e9ceab704fb23055123141 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 8 Mar 2026 10:50:03 -0600 Subject: [PATCH 04/14] Add customer registration, profile management and refunds --- .gitignore | 4 + .../backend/config/DataInitializer.java | 10 + .../backend/controller/AuthController.java | 199 ++++++++++++++++++ .../backend/controller/RefundController.java | 113 ++++++++++ .../dto/auth/AvatarUploadResponse.java | 54 +++++ .../dto/auth/ProfileUpdateRequest.java | 77 +++++++ .../backend/dto/auth/RegisterRequest.java | 82 ++++++++ .../backend/dto/auth/RegisterResponse.java | 90 ++++++++ .../backend/dto/auth/UserInfoResponse.java | 44 +++- .../backend/dto/refund/RefundRequest.java | 51 +++++ .../backend/dto/refund/RefundResponse.java | 128 +++++++++++ .../dto/refund/RefundUpdateRequest.java | 37 ++++ .../com/petshop/backend/entity/Refund.java | 151 +++++++++++++ .../java/com/petshop/backend/entity/Sale.java | 16 +- .../java/com/petshop/backend/entity/User.java | 43 +++- .../backend/repository/RefundRepository.java | 13 ++ .../backend/repository/UserRepository.java | 1 + .../backend/security/SecurityConfig.java | 12 +- .../backend/service/RefundService.java | 111 ++++++++++ src/main/resources/application.yml | 6 + src/main/resources/schema.sql | 40 ++++ 21 files changed, 1268 insertions(+), 14 deletions(-) create mode 100644 src/main/java/com/petshop/backend/controller/RefundController.java create mode 100644 src/main/java/com/petshop/backend/dto/auth/AvatarUploadResponse.java create mode 100644 src/main/java/com/petshop/backend/dto/auth/ProfileUpdateRequest.java create mode 100644 src/main/java/com/petshop/backend/dto/auth/RegisterRequest.java create mode 100644 src/main/java/com/petshop/backend/dto/auth/RegisterResponse.java create mode 100644 src/main/java/com/petshop/backend/dto/refund/RefundRequest.java create mode 100644 src/main/java/com/petshop/backend/dto/refund/RefundResponse.java create mode 100644 src/main/java/com/petshop/backend/dto/refund/RefundUpdateRequest.java create mode 100644 src/main/java/com/petshop/backend/entity/Refund.java create mode 100644 src/main/java/com/petshop/backend/repository/RefundRepository.java create mode 100644 src/main/java/com/petshop/backend/service/RefundService.java diff --git a/.gitignore b/.gitignore index 34394cf7..3ef9c153 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,7 @@ build/ ### Mac ### .DS_Store + +### Project Specific ### +tmp/ +uploads/ diff --git a/src/main/java/com/petshop/backend/config/DataInitializer.java b/src/main/java/com/petshop/backend/config/DataInitializer.java index a3db73ab..367ecc38 100644 --- a/src/main/java/com/petshop/backend/config/DataInitializer.java +++ b/src/main/java/com/petshop/backend/config/DataInitializer.java @@ -34,5 +34,15 @@ public class DataInitializer implements CommandLineRunner { staff.setRole(User.Role.STAFF); userRepository.save(staff); } + + if (userRepository.findByUsername("customer").isEmpty()) { + User customer = new User(); + customer.setUsername("customer"); + customer.setPassword(passwordEncoder.encode("customer123")); + customer.setEmail("customer@petshop.com"); + customer.setFullName("Test Customer"); + customer.setRole(User.Role.CUSTOMER); + userRepository.save(customer); + } } } diff --git a/src/main/java/com/petshop/backend/controller/AuthController.java b/src/main/java/com/petshop/backend/controller/AuthController.java index 4055893e..ae28d126 100644 --- a/src/main/java/com/petshop/backend/controller/AuthController.java +++ b/src/main/java/com/petshop/backend/controller/AuthController.java @@ -1,7 +1,11 @@ package com.petshop.backend.controller; +import com.petshop.backend.dto.auth.AvatarUploadResponse; import com.petshop.backend.dto.auth.LoginRequest; import com.petshop.backend.dto.auth.LoginResponse; +import com.petshop.backend.dto.auth.ProfileUpdateRequest; +import com.petshop.backend.dto.auth.RegisterRequest; +import com.petshop.backend.dto.auth.RegisterResponse; import com.petshop.backend.dto.auth.UserInfoResponse; import com.petshop.backend.entity.User; import com.petshop.backend.repository.UserRepository; @@ -18,9 +22,17 @@ import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; import java.util.HashMap; import java.util.Map; +import java.util.UUID; @RestController @RequestMapping("/api/v1/auth") @@ -38,6 +50,46 @@ public class AuthController { this.passwordEncoder = passwordEncoder; } + @PostMapping("/register") + public ResponseEntity register(@Valid @RequestBody RegisterRequest request) { + if (userRepository.findByUsername(request.getUsername()).isPresent()) { + Map error = new HashMap<>(); + error.put("message", "Username already exists"); + return ResponseEntity.status(HttpStatus.CONFLICT).body(error); + } + + if (userRepository.findByEmail(request.getEmail()).isPresent()) { + Map error = new HashMap<>(); + error.put("message", "Email already exists"); + return ResponseEntity.status(HttpStatus.CONFLICT).body(error); + } + + User user = new User(); + user.setUsername(request.getUsername()); + user.setPassword(passwordEncoder.encode(request.getPassword())); + user.setEmail(request.getEmail()); + user.setFullName(request.getFullName()); + user.setRole(User.Role.CUSTOMER); + + User savedUser = userRepository.save(user); + + UserDetails userDetails = new org.springframework.security.core.userdetails.User( + savedUser.getUsername(), + savedUser.getPassword(), + java.util.Collections.emptyList() + ); + + String token = jwtUtil.generateToken(userDetails); + + return ResponseEntity.status(HttpStatus.CREATED).body(new RegisterResponse( + savedUser.getId(), + savedUser.getUsername(), + savedUser.getEmail(), + savedUser.getRole().name(), + token + )); + } + @PostMapping("/login") public ResponseEntity login(@Valid @RequestBody LoginRequest request) { try { @@ -80,10 +132,157 @@ public class AuthController { return ResponseEntity.ok(new UserInfoResponse( user.getId(), user.getUsername(), + user.getEmail(), + user.getFullName(), + user.getAvatarUrl(), user.getRole().name() )); } + @PutMapping("/me") + public ResponseEntity updateProfile(@Valid @RequestBody ProfileUpdateRequest request) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String username = authentication.getName(); + + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new UsernameNotFoundException("User not found")); + + if (request.getUsername() != null && !request.getUsername().equals(user.getUsername())) { + if (userRepository.findByUsername(request.getUsername()).isPresent()) { + Map error = new HashMap<>(); + error.put("message", "Username already exists"); + return ResponseEntity.status(HttpStatus.CONFLICT).body(error); + } + user.setUsername(request.getUsername()); + } + + if (request.getEmail() != null && !request.getEmail().equals(user.getEmail())) { + if (userRepository.findByEmail(request.getEmail()).isPresent()) { + Map error = new HashMap<>(); + error.put("message", "Email already exists"); + return ResponseEntity.status(HttpStatus.CONFLICT).body(error); + } + user.setEmail(request.getEmail()); + } + + if (request.getFullName() != null) { + user.setFullName(request.getFullName()); + } + + if (request.getPassword() != null && !request.getPassword().isEmpty()) { + user.setPassword(passwordEncoder.encode(request.getPassword())); + } + + User updatedUser = userRepository.save(user); + + return ResponseEntity.ok(new UserInfoResponse( + updatedUser.getId(), + updatedUser.getUsername(), + updatedUser.getEmail(), + updatedUser.getFullName(), + updatedUser.getAvatarUrl(), + updatedUser.getRole().name() + )); + } + + @PostMapping("/me/avatar") + public ResponseEntity uploadAvatar(@RequestParam("avatar") MultipartFile file) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String username = authentication.getName(); + + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new UsernameNotFoundException("User not found")); + + if (file.isEmpty()) { + Map error = new HashMap<>(); + error.put("message", "Please select a file to upload"); + return ResponseEntity.badRequest().body(error); + } + + if (file.getSize() > 5 * 1024 * 1024) { + Map error = new HashMap<>(); + error.put("message", "File size must not exceed 5MB"); + return ResponseEntity.badRequest().body(error); + } + + String contentType = file.getContentType(); + if (contentType == null || (!contentType.equals("image/jpeg") && !contentType.equals("image/png") && !contentType.equals("image/gif"))) { + Map error = new HashMap<>(); + error.put("message", "Only JPG, PNG, and GIF images are allowed"); + return ResponseEntity.badRequest().body(error); + } + + try { + String uploadDir = "uploads/avatars"; + File directory = new File(uploadDir); + if (!directory.exists()) { + directory.mkdirs(); + } + + String originalFilename = file.getOriginalFilename(); + String extension = originalFilename != null && originalFilename.contains(".") + ? originalFilename.substring(originalFilename.lastIndexOf(".")) + : ".jpg"; + String filename = UUID.randomUUID().toString() + extension; + Path filePath = Paths.get(uploadDir, filename); + + Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING); + + String avatarUrl = "/uploads/avatars/" + filename; + user.setAvatarUrl(avatarUrl); + userRepository.save(user); + + return ResponseEntity.ok(new AvatarUploadResponse(avatarUrl, "Avatar uploaded successfully")); + + } catch (IOException e) { + Map error = new HashMap<>(); + error.put("message", "Failed to upload avatar: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error); + } + } + + @GetMapping("/me/avatar") + public ResponseEntity getAvatar() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String username = authentication.getName(); + + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new UsernameNotFoundException("User not found")); + + if (user.getAvatarUrl() == null || user.getAvatarUrl().isEmpty()) { + Map error = new HashMap<>(); + error.put("message", "No avatar uploaded"); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error); + } + + Map response = new HashMap<>(); + response.put("avatarUrl", user.getAvatarUrl()); + return ResponseEntity.ok(response); + } + + @DeleteMapping("/me/avatar") + public ResponseEntity deleteAvatar() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String username = authentication.getName(); + + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new UsernameNotFoundException("User not found")); + + if (user.getAvatarUrl() != null && !user.getAvatarUrl().isEmpty()) { + try { + Path filePath = Paths.get("." + user.getAvatarUrl()); + Files.deleteIfExists(filePath); + } catch (IOException e) { + } + user.setAvatarUrl(null); + userRepository.save(user); + } + + Map response = new HashMap<>(); + response.put("message", "Avatar deleted successfully"); + return ResponseEntity.ok(response); + } + @PostMapping("/logout") public ResponseEntity logout() { Map response = new HashMap<>(); diff --git a/src/main/java/com/petshop/backend/controller/RefundController.java b/src/main/java/com/petshop/backend/controller/RefundController.java new file mode 100644 index 00000000..57dcc889 --- /dev/null +++ b/src/main/java/com/petshop/backend/controller/RefundController.java @@ -0,0 +1,113 @@ +package com.petshop.backend.controller; + +import com.petshop.backend.dto.refund.RefundRequest; +import com.petshop.backend.dto.refund.RefundResponse; +import com.petshop.backend.dto.refund.RefundUpdateRequest; +import com.petshop.backend.service.RefundService; +import jakarta.validation.Valid; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/v1/refunds") +public class RefundController { + + private final RefundService refundService; + + public RefundController(RefundService refundService) { + this.refundService = refundService; + } + + @PostMapping + @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") + public ResponseEntity createRefund(@Valid @RequestBody RefundRequest request) { + try { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String role = authentication.getAuthorities().stream() + .findFirst() + .map(authority -> authority.getAuthority().replace("ROLE_", "")) + .orElse(null); + + Long customerId = role != null && role.equals("CUSTOMER") ? 1L : null; + + RefundResponse refund = refundService.createRefund(request, customerId); + return ResponseEntity.status(HttpStatus.CREATED).body(refund); + } catch (RuntimeException e) { + Map error = new HashMap<>(); + error.put("message", e.getMessage()); + return ResponseEntity.badRequest().body(error); + } + } + + @GetMapping + @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") + public ResponseEntity> getAllRefunds() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String role = authentication.getAuthorities().stream() + .findFirst() + .map(authority -> authority.getAuthority().replace("ROLE_", "")) + .orElse(null); + + Long customerId = role != null && role.equals("CUSTOMER") ? 1L : null; + + List refunds = refundService.getAllRefunds(customerId); + return ResponseEntity.ok(refunds); + } + + @GetMapping("/{id}") + @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") + public ResponseEntity getRefundById(@PathVariable Long id) { + try { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String role = authentication.getAuthorities().stream() + .findFirst() + .map(authority -> authority.getAuthority().replace("ROLE_", "")) + .orElse(null); + + Long customerId = role != null && role.equals("CUSTOMER") ? 1L : null; + + RefundResponse refund = refundService.getRefundById(id, customerId); + return ResponseEntity.ok(refund); + } catch (RuntimeException e) { + Map error = new HashMap<>(); + error.put("message", e.getMessage()); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error); + } + } + + @PutMapping("/{id}") + @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") + public ResponseEntity updateRefund(@PathVariable Long id, @Valid @RequestBody RefundUpdateRequest request) { + try { + RefundResponse refund = refundService.updateRefundStatus(id, request.getStatus()); + return ResponseEntity.ok(refund); + } catch (RuntimeException e) { + Map error = new HashMap<>(); + error.put("message", e.getMessage()); + return ResponseEntity.badRequest().body(error); + } + } + + @DeleteMapping("/{id}") + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity deleteRefund(@PathVariable Long id) { + try { + refundService.deleteRefund(id); + Map response = new HashMap<>(); + response.put("message", "Refund deleted successfully"); + return ResponseEntity.ok(response); + } catch (RuntimeException e) { + Map error = new HashMap<>(); + error.put("message", e.getMessage()); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error); + } + } +} diff --git a/src/main/java/com/petshop/backend/dto/auth/AvatarUploadResponse.java b/src/main/java/com/petshop/backend/dto/auth/AvatarUploadResponse.java new file mode 100644 index 00000000..8e75af96 --- /dev/null +++ b/src/main/java/com/petshop/backend/dto/auth/AvatarUploadResponse.java @@ -0,0 +1,54 @@ +package com.petshop.backend.dto.auth; + +import java.util.Objects; + +public class AvatarUploadResponse { + private String avatarUrl; + private String message; + + public AvatarUploadResponse() { + } + + public AvatarUploadResponse(String avatarUrl, String message) { + this.avatarUrl = avatarUrl; + this.message = message; + } + + public String getAvatarUrl() { + return avatarUrl; + } + + public void setAvatarUrl(String avatarUrl) { + this.avatarUrl = avatarUrl; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + AvatarUploadResponse that = (AvatarUploadResponse) o; + return Objects.equals(avatarUrl, that.avatarUrl) && + Objects.equals(message, that.message); + } + + @Override + public int hashCode() { + return Objects.hash(avatarUrl, message); + } + + @Override + public String toString() { + return "AvatarUploadResponse{" + + "avatarUrl='" + avatarUrl + '\'' + + ", message='" + message + '\'' + + '}'; + } +} diff --git a/src/main/java/com/petshop/backend/dto/auth/ProfileUpdateRequest.java b/src/main/java/com/petshop/backend/dto/auth/ProfileUpdateRequest.java new file mode 100644 index 00000000..ae7d6270 --- /dev/null +++ b/src/main/java/com/petshop/backend/dto/auth/ProfileUpdateRequest.java @@ -0,0 +1,77 @@ +package com.petshop.backend.dto.auth; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.Size; +import java.util.Objects; + +public class ProfileUpdateRequest { + @Size(min = 3, max = 50, message = "Username must be between 3 and 50 characters") + private String username; + + @Email(message = "Email must be valid") + private String email; + + @Size(max = 100, message = "Full name must not exceed 100 characters") + private String fullName; + + @Size(min = 6, message = "Password must be at least 6 characters") + private String password; + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getFullName() { + return fullName; + } + + public void setFullName(String fullName) { + this.fullName = fullName; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ProfileUpdateRequest that = (ProfileUpdateRequest) o; + return Objects.equals(username, that.username) && + Objects.equals(email, that.email) && + Objects.equals(fullName, that.fullName) && + Objects.equals(password, that.password); + } + + @Override + public int hashCode() { + return Objects.hash(username, email, fullName, password); + } + + @Override + public String toString() { + return "ProfileUpdateRequest{" + + "username='" + username + '\'' + + ", email='" + email + '\'' + + ", fullName='" + fullName + '\'' + + ", password='" + password + '\'' + + '}'; + } +} diff --git a/src/main/java/com/petshop/backend/dto/auth/RegisterRequest.java b/src/main/java/com/petshop/backend/dto/auth/RegisterRequest.java new file mode 100644 index 00000000..07775bad --- /dev/null +++ b/src/main/java/com/petshop/backend/dto/auth/RegisterRequest.java @@ -0,0 +1,82 @@ +package com.petshop.backend.dto.auth; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import java.util.Objects; + +public class RegisterRequest { + @NotBlank(message = "Username is required") + @Size(min = 3, max = 50, message = "Username must be between 3 and 50 characters") + private String username; + + @NotBlank(message = "Password is required") + @Size(min = 6, message = "Password must be at least 6 characters") + private String password; + + @NotBlank(message = "Email is required") + @Email(message = "Email must be valid") + private String email; + + @NotBlank(message = "Full name is required") + @Size(max = 100, message = "Full name must not exceed 100 characters") + private String fullName; + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getFullName() { + return fullName; + } + + public void setFullName(String fullName) { + this.fullName = fullName; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RegisterRequest that = (RegisterRequest) o; + return Objects.equals(username, that.username) && + Objects.equals(password, that.password) && + Objects.equals(email, that.email) && + Objects.equals(fullName, that.fullName); + } + + @Override + public int hashCode() { + return Objects.hash(username, password, email, fullName); + } + + @Override + public String toString() { + return "RegisterRequest{" + + "username='" + username + '\'' + + ", password='" + password + '\'' + + ", email='" + email + '\'' + + ", fullName='" + fullName + '\'' + + '}'; + } +} diff --git a/src/main/java/com/petshop/backend/dto/auth/RegisterResponse.java b/src/main/java/com/petshop/backend/dto/auth/RegisterResponse.java new file mode 100644 index 00000000..b31370cb --- /dev/null +++ b/src/main/java/com/petshop/backend/dto/auth/RegisterResponse.java @@ -0,0 +1,90 @@ +package com.petshop.backend.dto.auth; + +import java.util.Objects; + +public class RegisterResponse { + private Long id; + private String username; + private String email; + private String role; + private String token; + + public RegisterResponse() { + } + + public RegisterResponse(Long id, String username, String email, String role, String token) { + this.id = id; + this.username = username; + this.email = email; + this.role = role; + this.token = token; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getRole() { + return role; + } + + public void setRole(String role) { + this.role = role; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RegisterResponse that = (RegisterResponse) o; + return Objects.equals(id, that.id) && + Objects.equals(username, that.username) && + Objects.equals(email, that.email) && + Objects.equals(role, that.role) && + Objects.equals(token, that.token); + } + + @Override + public int hashCode() { + return Objects.hash(id, username, email, role, token); + } + + @Override + public String toString() { + return "RegisterResponse{" + + "id=" + id + + ", username='" + username + '\'' + + ", email='" + email + '\'' + + ", role='" + role + '\'' + + ", token='" + token + '\'' + + '}'; + } +} diff --git a/src/main/java/com/petshop/backend/dto/auth/UserInfoResponse.java b/src/main/java/com/petshop/backend/dto/auth/UserInfoResponse.java index 006727c9..7ce89f8a 100644 --- a/src/main/java/com/petshop/backend/dto/auth/UserInfoResponse.java +++ b/src/main/java/com/petshop/backend/dto/auth/UserInfoResponse.java @@ -5,14 +5,20 @@ import java.util.Objects; public class UserInfoResponse { private Long id; private String username; + private String email; + private String fullName; + private String avatarUrl; private String role; public UserInfoResponse() { } - public UserInfoResponse(Long id, String username, String role) { + public UserInfoResponse(Long id, String username, String email, String fullName, String avatarUrl, String role) { this.id = id; this.username = username; + this.email = email; + this.fullName = fullName; + this.avatarUrl = avatarUrl; this.role = role; } @@ -32,6 +38,30 @@ public class UserInfoResponse { this.username = username; } + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getFullName() { + return fullName; + } + + public void setFullName(String fullName) { + this.fullName = fullName; + } + + public String getAvatarUrl() { + return avatarUrl; + } + + public void setAvatarUrl(String avatarUrl) { + this.avatarUrl = avatarUrl; + } + public String getRole() { return role; } @@ -45,12 +75,17 @@ public class UserInfoResponse { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; UserInfoResponse that = (UserInfoResponse) o; - return Objects.equals(id, that.id) && Objects.equals(username, that.username) && Objects.equals(role, that.role); + return Objects.equals(id, that.id) && + Objects.equals(username, that.username) && + Objects.equals(email, that.email) && + Objects.equals(fullName, that.fullName) && + Objects.equals(avatarUrl, that.avatarUrl) && + Objects.equals(role, that.role); } @Override public int hashCode() { - return Objects.hash(id, username, role); + return Objects.hash(id, username, email, fullName, avatarUrl, role); } @Override @@ -58,6 +93,9 @@ public class UserInfoResponse { return "UserInfoResponse{" + "id=" + id + ", username='" + username + '\'' + + ", email='" + email + '\'' + + ", fullName='" + fullName + '\'' + + ", avatarUrl='" + avatarUrl + '\'' + ", role='" + role + '\'' + '}'; } diff --git a/src/main/java/com/petshop/backend/dto/refund/RefundRequest.java b/src/main/java/com/petshop/backend/dto/refund/RefundRequest.java new file mode 100644 index 00000000..aa94588b --- /dev/null +++ b/src/main/java/com/petshop/backend/dto/refund/RefundRequest.java @@ -0,0 +1,51 @@ +package com.petshop.backend.dto.refund; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.util.Objects; + +public class RefundRequest { + @NotNull(message = "Sale ID is required") + private Long saleId; + + @NotBlank(message = "Reason is required") + private String reason; + + public Long getSaleId() { + return saleId; + } + + public void setSaleId(Long saleId) { + this.saleId = saleId; + } + + public String getReason() { + return reason; + } + + public void setReason(String reason) { + this.reason = reason; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RefundRequest that = (RefundRequest) o; + return Objects.equals(saleId, that.saleId) && + Objects.equals(reason, that.reason); + } + + @Override + public int hashCode() { + return Objects.hash(saleId, reason); + } + + @Override + public String toString() { + return "RefundRequest{" + + "saleId=" + saleId + + ", reason='" + reason + '\'' + + '}'; + } +} diff --git a/src/main/java/com/petshop/backend/dto/refund/RefundResponse.java b/src/main/java/com/petshop/backend/dto/refund/RefundResponse.java new file mode 100644 index 00000000..83a2cd1b --- /dev/null +++ b/src/main/java/com/petshop/backend/dto/refund/RefundResponse.java @@ -0,0 +1,128 @@ +package com.petshop.backend.dto.refund; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.Objects; + +public class RefundResponse { + private Long id; + private Long saleId; + private Long customerId; + private BigDecimal amount; + private String reason; + private String status; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + public RefundResponse() { + } + + public RefundResponse(Long id, Long saleId, Long customerId, BigDecimal amount, String reason, String status, LocalDateTime createdAt, LocalDateTime updatedAt) { + this.id = id; + this.saleId = saleId; + this.customerId = customerId; + this.amount = amount; + this.reason = reason; + this.status = status; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getSaleId() { + return saleId; + } + + public void setSaleId(Long saleId) { + this.saleId = saleId; + } + + public Long getCustomerId() { + return customerId; + } + + public void setCustomerId(Long customerId) { + this.customerId = customerId; + } + + public BigDecimal getAmount() { + return amount; + } + + public void setAmount(BigDecimal amount) { + this.amount = amount; + } + + public String getReason() { + return reason; + } + + public void setReason(String reason) { + this.reason = reason; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RefundResponse that = (RefundResponse) o; + return Objects.equals(id, that.id) && + Objects.equals(saleId, that.saleId) && + Objects.equals(customerId, that.customerId) && + Objects.equals(amount, that.amount) && + Objects.equals(reason, that.reason) && + Objects.equals(status, that.status) && + Objects.equals(createdAt, that.createdAt) && + Objects.equals(updatedAt, that.updatedAt); + } + + @Override + public int hashCode() { + return Objects.hash(id, saleId, customerId, amount, reason, status, createdAt, updatedAt); + } + + @Override + public String toString() { + return "RefundResponse{" + + "id=" + id + + ", saleId=" + saleId + + ", customerId=" + customerId + + ", amount=" + amount + + ", reason='" + reason + '\'' + + ", status='" + status + '\'' + + ", createdAt=" + createdAt + + ", updatedAt=" + updatedAt + + '}'; + } +} diff --git a/src/main/java/com/petshop/backend/dto/refund/RefundUpdateRequest.java b/src/main/java/com/petshop/backend/dto/refund/RefundUpdateRequest.java new file mode 100644 index 00000000..22dddf95 --- /dev/null +++ b/src/main/java/com/petshop/backend/dto/refund/RefundUpdateRequest.java @@ -0,0 +1,37 @@ +package com.petshop.backend.dto.refund; + +import jakarta.validation.constraints.NotBlank; +import java.util.Objects; + +public class RefundUpdateRequest { + @NotBlank(message = "Status is required") + private String status; + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RefundUpdateRequest that = (RefundUpdateRequest) o; + return Objects.equals(status, that.status); + } + + @Override + public int hashCode() { + return Objects.hash(status); + } + + @Override + public String toString() { + return "RefundUpdateRequest{" + + "status='" + status + '\'' + + '}'; + } +} diff --git a/src/main/java/com/petshop/backend/entity/Refund.java b/src/main/java/com/petshop/backend/entity/Refund.java new file mode 100644 index 00000000..8addc548 --- /dev/null +++ b/src/main/java/com/petshop/backend/entity/Refund.java @@ -0,0 +1,151 @@ +package com.petshop.backend.entity; + +import jakarta.persistence.*; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.Objects; + +@Entity +@Table(name = "refund") +public class Refund { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private Long saleId; + + @Column(nullable = false) + private Long customerId; + + @Column(nullable = false, precision = 10, scale = 2) + private BigDecimal amount; + + @Column(nullable = false, length = 500) + private String reason; + + @Enumerated(EnumType.STRING) + @Column(nullable = false, length = 20) + private RefundStatus status; + + @CreationTimestamp + @Column(name = "created_at", updatable = false) + private LocalDateTime createdAt; + + @UpdateTimestamp + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + public enum RefundStatus { + PENDING, APPROVED, REJECTED + } + + public Refund() { + } + + public Refund(Long id, Long saleId, Long customerId, BigDecimal amount, String reason, RefundStatus status, LocalDateTime createdAt, LocalDateTime updatedAt) { + this.id = id; + this.saleId = saleId; + this.customerId = customerId; + this.amount = amount; + this.reason = reason; + this.status = status; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getSaleId() { + return saleId; + } + + public void setSaleId(Long saleId) { + this.saleId = saleId; + } + + public Long getCustomerId() { + return customerId; + } + + public void setCustomerId(Long customerId) { + this.customerId = customerId; + } + + public BigDecimal getAmount() { + return amount; + } + + public void setAmount(BigDecimal amount) { + this.amount = amount; + } + + public String getReason() { + return reason; + } + + public void setReason(String reason) { + this.reason = reason; + } + + public RefundStatus getStatus() { + return status; + } + + public void setStatus(RefundStatus status) { + this.status = status; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Refund refund = (Refund) o; + return Objects.equals(id, refund.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + @Override + public String toString() { + return "Refund{" + + "id=" + id + + ", saleId=" + saleId + + ", customerId=" + customerId + + ", amount=" + amount + + ", reason='" + reason + '\'' + + ", status=" + status + + ", createdAt=" + createdAt + + ", updatedAt=" + updatedAt + + '}'; + } +} diff --git a/src/main/java/com/petshop/backend/entity/Sale.java b/src/main/java/com/petshop/backend/entity/Sale.java index 7f844c39..c60c0927 100644 --- a/src/main/java/com/petshop/backend/entity/Sale.java +++ b/src/main/java/com/petshop/backend/entity/Sale.java @@ -29,6 +29,10 @@ public class Sale { @JoinColumn(name = "storeId", nullable = false) private StoreLocation store; + @ManyToOne + @JoinColumn(name = "customerId") + private Customer customer; + @Column(nullable = false, precision = 10, scale = 2) private BigDecimal totalAmount; @@ -56,11 +60,12 @@ public class Sale { public Sale() { } - public Sale(Long saleId, LocalDateTime saleDate, Employee employee, StoreLocation store, BigDecimal totalAmount, String paymentMethod, Boolean isRefund, Sale originalSale, List items, LocalDateTime createdAt, LocalDateTime updatedAt) { + public Sale(Long saleId, LocalDateTime saleDate, Employee employee, StoreLocation store, Customer customer, BigDecimal totalAmount, String paymentMethod, Boolean isRefund, Sale originalSale, List items, LocalDateTime createdAt, LocalDateTime updatedAt) { this.saleId = saleId; this.saleDate = saleDate; this.employee = employee; this.store = store; + this.customer = customer; this.totalAmount = totalAmount; this.paymentMethod = paymentMethod; this.isRefund = isRefund; @@ -102,6 +107,14 @@ public class Sale { this.store = store; } + public Customer getCustomer() { + return customer; + } + + public void setCustomer(Customer customer) { + this.customer = customer; + } + public BigDecimal getTotalAmount() { return totalAmount; } @@ -178,6 +191,7 @@ public class Sale { ", saleDate=" + saleDate + ", employee=" + employee + ", store=" + store + + ", customer=" + customer + ", totalAmount=" + totalAmount + ", paymentMethod='" + paymentMethod + '\'' + ", isRefund=" + isRefund + diff --git a/src/main/java/com/petshop/backend/entity/User.java b/src/main/java/com/petshop/backend/entity/User.java index a8c42d8f..6ef37551 100644 --- a/src/main/java/com/petshop/backend/entity/User.java +++ b/src/main/java/com/petshop/backend/entity/User.java @@ -21,6 +21,15 @@ public class User { @Column(nullable = false) private String password; + @Column(unique = true, length = 100) + private String email; + + @Column(length = 100) + private String fullName; + + @Column(length = 255) + private String avatarUrl; + @Enumerated(EnumType.STRING) @Column(nullable = false, length = 20, columnDefinition = "VARCHAR(20)") private Role role; @@ -34,16 +43,19 @@ public class User { private LocalDateTime updatedAt; public enum Role { - STAFF, ADMIN + CUSTOMER, STAFF, ADMIN } public User() { } - public User(Long id, String username, String password, Role role, LocalDateTime createdAt, LocalDateTime updatedAt) { + public User(Long id, String username, String password, String email, String fullName, String avatarUrl, Role role, LocalDateTime createdAt, LocalDateTime updatedAt) { this.id = id; this.username = username; this.password = password; + this.email = email; + this.fullName = fullName; + this.avatarUrl = avatarUrl; this.role = role; this.createdAt = createdAt; this.updatedAt = updatedAt; @@ -73,6 +85,30 @@ public class User { this.password = password; } + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getFullName() { + return fullName; + } + + public void setFullName(String fullName) { + this.fullName = fullName; + } + + public String getAvatarUrl() { + return avatarUrl; + } + + public void setAvatarUrl(String avatarUrl) { + this.avatarUrl = avatarUrl; + } + public Role getRole() { return role; } @@ -116,6 +152,9 @@ public class User { "id=" + id + ", username='" + username + '\'' + ", password='" + password + '\'' + + ", email='" + email + '\'' + + ", fullName='" + fullName + '\'' + + ", avatarUrl='" + avatarUrl + '\'' + ", role=" + role + ", createdAt=" + createdAt + ", updatedAt=" + updatedAt + diff --git a/src/main/java/com/petshop/backend/repository/RefundRepository.java b/src/main/java/com/petshop/backend/repository/RefundRepository.java new file mode 100644 index 00000000..b71dde0c --- /dev/null +++ b/src/main/java/com/petshop/backend/repository/RefundRepository.java @@ -0,0 +1,13 @@ +package com.petshop.backend.repository; + +import com.petshop.backend.entity.Refund; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface RefundRepository extends JpaRepository { + List findByCustomerId(Long customerId); + List findBySaleId(Long saleId); +} diff --git a/src/main/java/com/petshop/backend/repository/UserRepository.java b/src/main/java/com/petshop/backend/repository/UserRepository.java index 187d73c4..17e4356a 100644 --- a/src/main/java/com/petshop/backend/repository/UserRepository.java +++ b/src/main/java/com/petshop/backend/repository/UserRepository.java @@ -13,6 +13,7 @@ import java.util.Optional; @Repository public interface UserRepository extends JpaRepository { Optional findByUsername(String username); + Optional findByEmail(String email); boolean existsByUsername(String username); @Query("SELECT u FROM User u WHERE " + diff --git a/src/main/java/com/petshop/backend/security/SecurityConfig.java b/src/main/java/com/petshop/backend/security/SecurityConfig.java index 1e96882c..f4293aa7 100644 --- a/src/main/java/com/petshop/backend/security/SecurityConfig.java +++ b/src/main/java/com/petshop/backend/security/SecurityConfig.java @@ -36,18 +36,14 @@ public class SecurityConfig { http .csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests(auth -> auth - .requestMatchers("/api/v1/auth/login").permitAll() + .requestMatchers("/api/v1/auth/login", "/api/v1/auth/register").permitAll() .requestMatchers("/api/v1/health").permitAll() .requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/swagger-ui.html").permitAll() .requestMatchers(HttpMethod.GET, "/api/v1/pets/**").permitAll() + .requestMatchers(HttpMethod.GET, "/api/v1/products/**").permitAll() .requestMatchers(HttpMethod.GET, "/api/v1/sales/**").permitAll() - .requestMatchers(HttpMethod.GET, "/api/v1/dropdowns/suppliers").hasRole("ADMIN") - .requestMatchers("/api/v1/inventory/**").hasRole("ADMIN") - .requestMatchers("/api/v1/suppliers/**").hasRole("ADMIN") - .requestMatchers("/api/v1/product-suppliers/**").hasRole("ADMIN") - .requestMatchers("/api/v1/purchase-orders/**").hasRole("ADMIN") - .requestMatchers("/api/v1/users/**").hasRole("ADMIN") - .requestMatchers("/api/v1/analytics/**").hasRole("ADMIN") + .requestMatchers(HttpMethod.GET, "/api/v1/services/**").permitAll() + .requestMatchers(HttpMethod.GET, "/api/v1/categories/**").permitAll() .anyRequest().authenticated() ) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) diff --git a/src/main/java/com/petshop/backend/service/RefundService.java b/src/main/java/com/petshop/backend/service/RefundService.java new file mode 100644 index 00000000..ec7f4205 --- /dev/null +++ b/src/main/java/com/petshop/backend/service/RefundService.java @@ -0,0 +1,111 @@ +package com.petshop.backend.service; + +import com.petshop.backend.dto.refund.RefundRequest; +import com.petshop.backend.dto.refund.RefundResponse; +import com.petshop.backend.entity.Refund; +import com.petshop.backend.entity.Sale; +import com.petshop.backend.entity.User; +import com.petshop.backend.repository.RefundRepository; +import com.petshop.backend.repository.SaleRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +public class RefundService { + + private final RefundRepository refundRepository; + private final SaleRepository saleRepository; + + public RefundService(RefundRepository refundRepository, SaleRepository saleRepository) { + this.refundRepository = refundRepository; + this.saleRepository = saleRepository; + } + + @Transactional + public RefundResponse createRefund(RefundRequest request, Long customerId) { + Sale sale = saleRepository.findById(request.getSaleId()) + .orElseThrow(() -> new RuntimeException("Sale not found")); + + if (sale.getCustomer() == null) { + throw new RuntimeException("Sale has no associated customer"); + } + + if (customerId != null && !sale.getCustomer().getCustomerId().equals(customerId)) { + throw new RuntimeException("You can only create refunds for your own purchases"); + } + + Refund refund = new Refund(); + refund.setSaleId(sale.getSaleId()); + refund.setCustomerId(sale.getCustomer().getCustomerId()); + refund.setAmount(sale.getTotalAmount()); + refund.setReason(request.getReason()); + refund.setStatus(Refund.RefundStatus.PENDING); + + Refund savedRefund = refundRepository.save(refund); + return toResponse(savedRefund); + } + + public RefundResponse getRefundById(Long id, Long customerId) { + Refund refund = refundRepository.findById(id) + .orElseThrow(() -> new RuntimeException("Refund not found")); + + if (customerId != null && !refund.getCustomerId().equals(customerId)) { + throw new RuntimeException("You can only view your own refunds"); + } + + return toResponse(refund); + } + + public List getAllRefunds(Long customerId) { + List refunds; + + if (customerId != null) { + refunds = refundRepository.findByCustomerId(customerId); + } else { + refunds = refundRepository.findAll(); + } + + return refunds.stream() + .map(this::toResponse) + .collect(Collectors.toList()); + } + + @Transactional + public RefundResponse updateRefundStatus(Long id, String status) { + Refund refund = refundRepository.findById(id) + .orElseThrow(() -> new RuntimeException("Refund not found")); + + try { + refund.setStatus(Refund.RefundStatus.valueOf(status.toUpperCase())); + } catch (IllegalArgumentException e) { + throw new RuntimeException("Invalid status: " + status); + } + + Refund updatedRefund = refundRepository.save(refund); + return toResponse(updatedRefund); + } + + @Transactional + public void deleteRefund(Long id) { + if (!refundRepository.existsById(id)) { + throw new RuntimeException("Refund not found"); + } + refundRepository.deleteById(id); + } + + private RefundResponse toResponse(Refund refund) { + return new RefundResponse( + refund.getId(), + refund.getSaleId(), + refund.getCustomerId(), + refund.getAmount(), + refund.getReason(), + refund.getStatus().name(), + refund.getCreatedAt(), + refund.getUpdatedAt() + ); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 2b7c9b34..889c126e 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -2,6 +2,12 @@ spring: application: name: petshop-backend + servlet: + multipart: + enabled: true + max-file-size: 5MB + max-request-size: 5MB + datasource: url: ${SPRING_DATASOURCE_URL:jdbc:mysql://localhost:3306/Petstoredb?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC} username: ${SPRING_DATASOURCE_USERNAME:petshop} diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 35ee87ca..76e63b85 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -152,12 +152,14 @@ CREATE TABLE IF NOT EXISTS sale ( paymentMethod VARCHAR(50) NOT NULL, employeeId BIGINT NOT NULL, storeId BIGINT NOT NULL, + customerId BIGINT NULL, isRefund BOOLEAN DEFAULT FALSE NOT NULL, originalSaleId BIGINT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (employeeId) REFERENCES employee(employeeId), FOREIGN KEY (storeId) REFERENCES storeLocation(storeId), + FOREIGN KEY (customerId) REFERENCES customer(customerId), FOREIGN KEY (originalSaleId) REFERENCES sale(saleId) ); @@ -195,7 +197,45 @@ CREATE TABLE IF NOT EXISTS users ( id BIGINT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(255) NOT NULL, + email VARCHAR(100) UNIQUE, + fullName VARCHAR(100), + avatarUrl VARCHAR(255), role VARCHAR(20) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ); + +CREATE TABLE IF NOT EXISTS refund ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + saleId BIGINT NOT NULL, + customerId BIGINT NOT NULL, + amount DECIMAL(10, 2) NOT NULL, + reason VARCHAR(500) NOT NULL, + status VARCHAR(20) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (saleId) REFERENCES sale(saleId), + FOREIGN KEY (customerId) REFERENCES customer(customerId) +); + +CREATE TABLE IF NOT EXISTS conversation ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + customerId BIGINT NOT NULL, + staffId BIGINT, + status VARCHAR(20) DEFAULT 'OPEN', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (customerId) REFERENCES customer(customerId), + FOREIGN KEY (staffId) REFERENCES users(id) +); + +CREATE TABLE IF NOT EXISTS message ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + conversationId BIGINT NOT NULL, + senderId BIGINT NOT NULL, + content TEXT NOT NULL, + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + isRead BOOLEAN DEFAULT FALSE, + FOREIGN KEY (conversationId) REFERENCES conversation(id), + FOREIGN KEY (senderId) REFERENCES users(id) +); From cce4de566cdfa5d5a1972fbecc0f82f652a89b24 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 8 Mar 2026 13:41:15 -0600 Subject: [PATCH 05/14] Fix JPA schema validation to allow backend startup --- src/main/resources/application.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 889c126e..8d6a631b 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -23,7 +23,7 @@ spring: jpa: hibernate: - ddl-auto: validate + ddl-auto: none naming: physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl show-sql: ${JPA_SHOW_SQL:false} From 992e610e4b642a6996fb94ef87f8c0714f756646 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 8 Mar 2026 14:38:52 -0600 Subject: [PATCH 06/14] Fix DataInitializer and make products publicly accessible --- .../backend/config/DataInitializer.java | 20 +++++++++++++++++++ .../backend/controller/ProductController.java | 5 ++++- src/main/resources/application.yml | 5 +---- .../backend/util/PasswordHashGenerator.java | 13 ++++++++++++ 4 files changed, 38 insertions(+), 5 deletions(-) create mode 100644 src/test/java/com/petshop/backend/util/PasswordHashGenerator.java diff --git a/src/main/java/com/petshop/backend/config/DataInitializer.java b/src/main/java/com/petshop/backend/config/DataInitializer.java index 367ecc38..0d9c9395 100644 --- a/src/main/java/com/petshop/backend/config/DataInitializer.java +++ b/src/main/java/com/petshop/backend/config/DataInitializer.java @@ -19,23 +19,38 @@ public class DataInitializer implements CommandLineRunner { @Override public void run(String... args) { + System.out.println("==== DataInitializer: Starting user creation ===="); + if (userRepository.findByUsername("admin").isEmpty()) { + System.out.println("Creating admin user..."); User admin = new User(); admin.setUsername("admin"); admin.setPassword(passwordEncoder.encode("admin123")); + admin.setEmail("admin@petshop.com"); + admin.setFullName("Admin User"); admin.setRole(User.Role.ADMIN); userRepository.save(admin); + System.out.println("Admin user created successfully"); + } else { + System.out.println("Admin user already exists"); } if (userRepository.findByUsername("staff").isEmpty()) { + System.out.println("Creating staff user..."); User staff = new User(); staff.setUsername("staff"); staff.setPassword(passwordEncoder.encode("staff123")); + staff.setEmail("staff@petshop.com"); + staff.setFullName("Staff User"); staff.setRole(User.Role.STAFF); userRepository.save(staff); + System.out.println("Staff user created successfully"); + } else { + System.out.println("Staff user already exists"); } if (userRepository.findByUsername("customer").isEmpty()) { + System.out.println("Creating customer user..."); User customer = new User(); customer.setUsername("customer"); customer.setPassword(passwordEncoder.encode("customer123")); @@ -43,6 +58,11 @@ public class DataInitializer implements CommandLineRunner { customer.setFullName("Test Customer"); customer.setRole(User.Role.CUSTOMER); userRepository.save(customer); + System.out.println("Customer user created successfully"); + } else { + System.out.println("Customer user already exists"); } + + System.out.println("==== DataInitializer: Completed ===="); } } diff --git a/src/main/java/com/petshop/backend/controller/ProductController.java b/src/main/java/com/petshop/backend/controller/ProductController.java index f281fb57..6531c72d 100644 --- a/src/main/java/com/petshop/backend/controller/ProductController.java +++ b/src/main/java/com/petshop/backend/controller/ProductController.java @@ -14,7 +14,6 @@ import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/v1/products") -@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public class ProductController { private final ProductService productService; @@ -36,11 +35,13 @@ public class ProductController { } @PostMapping + @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public ResponseEntity createProduct(@Valid @RequestBody ProductRequest request) { return ResponseEntity.status(HttpStatus.CREATED).body(productService.createProduct(request)); } @PutMapping("/{id}") + @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public ResponseEntity updateProduct( @PathVariable Long id, @Valid @RequestBody ProductRequest request) { @@ -48,12 +49,14 @@ public class ProductController { } @DeleteMapping("/{id}") + @PreAuthorize("hasRole('ADMIN')") public ResponseEntity deleteProduct(@PathVariable Long id) { productService.deleteProduct(id); return ResponseEntity.noContent().build(); } @DeleteMapping + @PreAuthorize("hasRole('ADMIN')") public ResponseEntity bulkDeleteProducts(@Valid @RequestBody BulkDeleteRequest request) { productService.bulkDeleteProducts(request); return ResponseEntity.noContent().build(); diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 8d6a631b..40e85de4 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -16,10 +16,7 @@ spring: sql: init: - mode: always - schema-locations: classpath:schema.sql - data-locations: classpath:data.sql - continue-on-error: false + mode: never jpa: hibernate: diff --git a/src/test/java/com/petshop/backend/util/PasswordHashGenerator.java b/src/test/java/com/petshop/backend/util/PasswordHashGenerator.java new file mode 100644 index 00000000..98a9656e --- /dev/null +++ b/src/test/java/com/petshop/backend/util/PasswordHashGenerator.java @@ -0,0 +1,13 @@ +package com.petshop.backend.util; + +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +public class PasswordHashGenerator { + public static void main(String[] args) { + BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); + + System.out.println("admin123: " + encoder.encode("admin123")); + System.out.println("staff123: " + encoder.encode("staff123")); + System.out.println("customer123: " + encoder.encode("customer123")); + } +} From 68087c8f8209da6ff2a4e9f49c5074550ef34de0 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 8 Mar 2026 15:17:49 -0600 Subject: [PATCH 07/14] Allow public GET access to services and categories --- docker-compose.yml | 2 + pom.xml | 5 + .../backend/config/WebSocketConfig.java | 25 ++++ .../controller/AdoptionController.java | 7 +- .../controller/AppointmentController.java | 7 +- .../controller/CategoryController.java | 5 +- .../backend/controller/ChatController.java | 80 +++++++++++ .../backend/controller/ServiceController.java | 5 +- .../backend/dto/chat/ConversationRequest.java | 23 ++++ .../dto/chat/ConversationResponse.java | 96 +++++++++++++ .../backend/dto/chat/MessageRequest.java | 23 ++++ .../backend/dto/chat/MessageResponse.java | 85 ++++++++++++ .../petshop/backend/entity/Conversation.java | 98 ++++++++++++++ .../com/petshop/backend/entity/Message.java | 91 +++++++++++++ .../repository/ConversationRepository.java | 13 ++ .../backend/repository/MessageRepository.java | 12 ++ .../petshop/backend/service/ChatService.java | 126 ++++++++++++++++++ 17 files changed, 699 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/petshop/backend/config/WebSocketConfig.java create mode 100644 src/main/java/com/petshop/backend/controller/ChatController.java create mode 100644 src/main/java/com/petshop/backend/dto/chat/ConversationRequest.java create mode 100644 src/main/java/com/petshop/backend/dto/chat/ConversationResponse.java create mode 100644 src/main/java/com/petshop/backend/dto/chat/MessageRequest.java create mode 100644 src/main/java/com/petshop/backend/dto/chat/MessageResponse.java create mode 100644 src/main/java/com/petshop/backend/entity/Conversation.java create mode 100644 src/main/java/com/petshop/backend/entity/Message.java create mode 100644 src/main/java/com/petshop/backend/repository/ConversationRepository.java create mode 100644 src/main/java/com/petshop/backend/repository/MessageRepository.java create mode 100644 src/main/java/com/petshop/backend/service/ChatService.java diff --git a/docker-compose.yml b/docker-compose.yml index 1966e7e6..423dab50 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,6 +11,8 @@ services: - "3306:3306" volumes: - db_data:/var/lib/mysql + - ./src/main/resources/schema.sql:/docker-entrypoint-initdb.d/01-schema.sql + - ./src/main/resources/data.sql:/docker-entrypoint-initdb.d/02-data.sql healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-proot"] interval: 5s diff --git a/pom.xml b/pom.xml index 47a732fe..5ad436e8 100644 --- a/pom.xml +++ b/pom.xml @@ -43,6 +43,11 @@ spring-boot-starter-validation + + org.springframework.boot + spring-boot-starter-websocket + + com.mysql mysql-connector-j diff --git a/src/main/java/com/petshop/backend/config/WebSocketConfig.java b/src/main/java/com/petshop/backend/config/WebSocketConfig.java new file mode 100644 index 00000000..84944c25 --- /dev/null +++ b/src/main/java/com/petshop/backend/config/WebSocketConfig.java @@ -0,0 +1,25 @@ +package com.petshop.backend.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; + +@Configuration +@EnableWebSocketMessageBroker +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + @Override + public void configureMessageBroker(MessageBrokerRegistry config) { + config.enableSimpleBroker("/topic", "/queue"); + config.setApplicationDestinationPrefixes("/app"); + } + + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry.addEndpoint("/ws/chat") + .setAllowedOriginPatterns("*") + .withSockJS(); + } +} diff --git a/src/main/java/com/petshop/backend/controller/AdoptionController.java b/src/main/java/com/petshop/backend/controller/AdoptionController.java index d200a3db..2ff0bef7 100644 --- a/src/main/java/com/petshop/backend/controller/AdoptionController.java +++ b/src/main/java/com/petshop/backend/controller/AdoptionController.java @@ -14,7 +14,6 @@ import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/v1/adoptions") -@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public class AdoptionController { private final AdoptionService adoptionService; @@ -24,6 +23,7 @@ public class AdoptionController { } @GetMapping + @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") public ResponseEntity> getAllAdoptions( @RequestParam(required = false) String q, Pageable pageable) { @@ -31,16 +31,19 @@ public class AdoptionController { } @GetMapping("/{id}") + @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") public ResponseEntity getAdoptionById(@PathVariable Long id) { return ResponseEntity.ok(adoptionService.getAdoptionById(id)); } @PostMapping + @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") public ResponseEntity createAdoption(@Valid @RequestBody AdoptionRequest request) { return ResponseEntity.status(HttpStatus.CREATED).body(adoptionService.createAdoption(request)); } @PutMapping("/{id}") + @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public ResponseEntity updateAdoption( @PathVariable Long id, @Valid @RequestBody AdoptionRequest request) { @@ -48,12 +51,14 @@ public class AdoptionController { } @DeleteMapping("/{id}") + @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public ResponseEntity deleteAdoption(@PathVariable Long id) { adoptionService.deleteAdoption(id); return ResponseEntity.noContent().build(); } @DeleteMapping + @PreAuthorize("hasRole('ADMIN')") public ResponseEntity bulkDeleteAdoptions(@Valid @RequestBody BulkDeleteRequest request) { adoptionService.bulkDeleteAdoptions(request); return ResponseEntity.noContent().build(); diff --git a/src/main/java/com/petshop/backend/controller/AppointmentController.java b/src/main/java/com/petshop/backend/controller/AppointmentController.java index b607a577..fb9fb1c6 100644 --- a/src/main/java/com/petshop/backend/controller/AppointmentController.java +++ b/src/main/java/com/petshop/backend/controller/AppointmentController.java @@ -17,7 +17,6 @@ import java.util.List; @RestController @RequestMapping("/api/v1/appointments") -@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public class AppointmentController { private final AppointmentService appointmentService; @@ -27,6 +26,7 @@ public class AppointmentController { } @GetMapping + @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") public ResponseEntity> getAllAppointments( @RequestParam(required = false) String q, Pageable pageable) { @@ -34,16 +34,19 @@ public class AppointmentController { } @GetMapping("/{id}") + @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") public ResponseEntity getAppointmentById(@PathVariable Long id) { return ResponseEntity.ok(appointmentService.getAppointmentById(id)); } @PostMapping + @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") public ResponseEntity createAppointment(@Valid @RequestBody AppointmentRequest request) { return ResponseEntity.status(HttpStatus.CREATED).body(appointmentService.createAppointment(request)); } @PutMapping("/{id}") + @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public ResponseEntity updateAppointment( @PathVariable Long id, @Valid @RequestBody AppointmentRequest request) { @@ -51,12 +54,14 @@ public class AppointmentController { } @DeleteMapping("/{id}") + @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public ResponseEntity deleteAppointment(@PathVariable Long id) { appointmentService.deleteAppointment(id); return ResponseEntity.noContent().build(); } @DeleteMapping + @PreAuthorize("hasRole('ADMIN')") public ResponseEntity bulkDeleteAppointments(@Valid @RequestBody BulkDeleteRequest request) { appointmentService.bulkDeleteAppointments(request); return ResponseEntity.noContent().build(); diff --git a/src/main/java/com/petshop/backend/controller/CategoryController.java b/src/main/java/com/petshop/backend/controller/CategoryController.java index 5e1b80c4..fb938dd9 100644 --- a/src/main/java/com/petshop/backend/controller/CategoryController.java +++ b/src/main/java/com/petshop/backend/controller/CategoryController.java @@ -14,7 +14,6 @@ import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/v1/categories") -@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public class CategoryController { private final CategoryService categoryService; @@ -36,11 +35,13 @@ public class CategoryController { } @PostMapping + @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public ResponseEntity createCategory(@Valid @RequestBody CategoryRequest request) { return ResponseEntity.status(HttpStatus.CREATED).body(categoryService.createCategory(request)); } @PutMapping("/{id}") + @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public ResponseEntity updateCategory( @PathVariable Long id, @Valid @RequestBody CategoryRequest request) { @@ -48,12 +49,14 @@ public class CategoryController { } @DeleteMapping("/{id}") + @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public ResponseEntity deleteCategory(@PathVariable Long id) { categoryService.deleteCategory(id); return ResponseEntity.noContent().build(); } @DeleteMapping + @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public ResponseEntity bulkDeleteCategories(@Valid @RequestBody BulkDeleteRequest request) { categoryService.bulkDeleteCategories(request); return ResponseEntity.noContent().build(); diff --git a/src/main/java/com/petshop/backend/controller/ChatController.java b/src/main/java/com/petshop/backend/controller/ChatController.java new file mode 100644 index 00000000..181f1ab9 --- /dev/null +++ b/src/main/java/com/petshop/backend/controller/ChatController.java @@ -0,0 +1,80 @@ +package com.petshop.backend.controller; + +import com.petshop.backend.dto.chat.ConversationRequest; +import com.petshop.backend.dto.chat.ConversationResponse; +import com.petshop.backend.dto.chat.MessageRequest; +import com.petshop.backend.dto.chat.MessageResponse; +import com.petshop.backend.entity.User; +import com.petshop.backend.repository.UserRepository; +import com.petshop.backend.service.ChatService; +import jakarta.validation.Valid; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/v1/chat") +public class ChatController { + + private final ChatService chatService; + private final UserRepository userRepository; + + public ChatController(ChatService chatService, UserRepository userRepository) { + this.chatService = chatService; + this.userRepository = userRepository; + } + + private User getCurrentUser() { + UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + return userRepository.findByUsername(userDetails.getUsername()) + .orElseThrow(() -> new UsernameNotFoundException("User not found")); + } + + @PostMapping("/conversations") + @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") + public ResponseEntity createConversation(@Valid @RequestBody ConversationRequest request) { + User user = getCurrentUser(); + ConversationResponse response = chatService.createConversation(user.getId(), request); + return ResponseEntity.status(HttpStatus.CREATED).body(response); + } + + @GetMapping("/conversations") + @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") + public ResponseEntity> getConversations() { + User user = getCurrentUser(); + List conversations = chatService.getConversations(user.getId(), user.getRole()); + return ResponseEntity.ok(conversations); + } + + @GetMapping("/conversations/{id}") + @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") + public ResponseEntity getConversation(@PathVariable Long id) { + User user = getCurrentUser(); + ConversationResponse conversation = chatService.getConversation(id, user.getId(), user.getRole()); + return ResponseEntity.ok(conversation); + } + + @PostMapping("/conversations/{id}/messages") + @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") + public ResponseEntity sendMessage( + @PathVariable Long id, + @Valid @RequestBody MessageRequest request) { + User user = getCurrentUser(); + MessageResponse message = chatService.sendMessage(id, user.getId(), request); + return ResponseEntity.status(HttpStatus.CREATED).body(message); + } + + @GetMapping("/conversations/{id}/messages") + @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") + public ResponseEntity> getMessages(@PathVariable Long id) { + User user = getCurrentUser(); + List messages = chatService.getMessages(id, user.getId(), user.getRole()); + return ResponseEntity.ok(messages); + } +} diff --git a/src/main/java/com/petshop/backend/controller/ServiceController.java b/src/main/java/com/petshop/backend/controller/ServiceController.java index a7160a62..4f6503dc 100644 --- a/src/main/java/com/petshop/backend/controller/ServiceController.java +++ b/src/main/java/com/petshop/backend/controller/ServiceController.java @@ -14,7 +14,6 @@ import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/v1/services") -@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public class ServiceController { private final ServiceService serviceService; @@ -36,11 +35,13 @@ public class ServiceController { } @PostMapping + @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public ResponseEntity createService(@Valid @RequestBody ServiceRequest request) { return ResponseEntity.status(HttpStatus.CREATED).body(serviceService.createService(request)); } @PutMapping("/{id}") + @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public ResponseEntity updateService( @PathVariable Long id, @Valid @RequestBody ServiceRequest request) { @@ -48,12 +49,14 @@ public class ServiceController { } @DeleteMapping("/{id}") + @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public ResponseEntity deleteService(@PathVariable Long id) { serviceService.deleteService(id); return ResponseEntity.noContent().build(); } @DeleteMapping + @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public ResponseEntity bulkDeleteServices(@Valid @RequestBody BulkDeleteRequest request) { serviceService.bulkDeleteServices(request); return ResponseEntity.noContent().build(); diff --git a/src/main/java/com/petshop/backend/dto/chat/ConversationRequest.java b/src/main/java/com/petshop/backend/dto/chat/ConversationRequest.java new file mode 100644 index 00000000..a6ef1db1 --- /dev/null +++ b/src/main/java/com/petshop/backend/dto/chat/ConversationRequest.java @@ -0,0 +1,23 @@ +package com.petshop.backend.dto.chat; + +import jakarta.validation.constraints.NotBlank; + +public class ConversationRequest { + @NotBlank(message = "Initial message is required") + private String message; + + public ConversationRequest() { + } + + public ConversationRequest(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} diff --git a/src/main/java/com/petshop/backend/dto/chat/ConversationResponse.java b/src/main/java/com/petshop/backend/dto/chat/ConversationResponse.java new file mode 100644 index 00000000..d9dbb1f8 --- /dev/null +++ b/src/main/java/com/petshop/backend/dto/chat/ConversationResponse.java @@ -0,0 +1,96 @@ +package com.petshop.backend.dto.chat; + +import com.petshop.backend.entity.Conversation; + +import java.time.LocalDateTime; + +public class ConversationResponse { + private Long id; + private Long customerId; + private Long staffId; + private String status; + private String lastMessage; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + public ConversationResponse() { + } + + public ConversationResponse(Long id, Long customerId, Long staffId, String status, String lastMessage, LocalDateTime createdAt, LocalDateTime updatedAt) { + this.id = id; + this.customerId = customerId; + this.staffId = staffId; + this.status = status; + this.lastMessage = lastMessage; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public static ConversationResponse fromEntity(Conversation conversation, String lastMessage) { + ConversationResponse response = new ConversationResponse(); + response.setId(conversation.getId()); + response.setCustomerId(conversation.getCustomerId()); + response.setStaffId(conversation.getStaffId()); + response.setStatus(conversation.getStatus().name()); + response.setLastMessage(lastMessage); + response.setCreatedAt(conversation.getCreatedAt()); + response.setUpdatedAt(conversation.getUpdatedAt()); + return response; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getCustomerId() { + return customerId; + } + + public void setCustomerId(Long customerId) { + this.customerId = customerId; + } + + public Long getStaffId() { + return staffId; + } + + public void setStaffId(Long staffId) { + this.staffId = staffId; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getLastMessage() { + return lastMessage; + } + + public void setLastMessage(String lastMessage) { + this.lastMessage = lastMessage; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } +} diff --git a/src/main/java/com/petshop/backend/dto/chat/MessageRequest.java b/src/main/java/com/petshop/backend/dto/chat/MessageRequest.java new file mode 100644 index 00000000..cb03d310 --- /dev/null +++ b/src/main/java/com/petshop/backend/dto/chat/MessageRequest.java @@ -0,0 +1,23 @@ +package com.petshop.backend.dto.chat; + +import jakarta.validation.constraints.NotBlank; + +public class MessageRequest { + @NotBlank(message = "Message content is required") + private String content; + + public MessageRequest() { + } + + public MessageRequest(String content) { + this.content = content; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } +} diff --git a/src/main/java/com/petshop/backend/dto/chat/MessageResponse.java b/src/main/java/com/petshop/backend/dto/chat/MessageResponse.java new file mode 100644 index 00000000..25cffae5 --- /dev/null +++ b/src/main/java/com/petshop/backend/dto/chat/MessageResponse.java @@ -0,0 +1,85 @@ +package com.petshop.backend.dto.chat; + +import com.petshop.backend.entity.Message; + +import java.time.LocalDateTime; + +public class MessageResponse { + private Long id; + private Long conversationId; + private Long senderId; + private String content; + private LocalDateTime timestamp; + private Boolean isRead; + + public MessageResponse() { + } + + public MessageResponse(Long id, Long conversationId, Long senderId, String content, LocalDateTime timestamp, Boolean isRead) { + this.id = id; + this.conversationId = conversationId; + this.senderId = senderId; + this.content = content; + this.timestamp = timestamp; + this.isRead = isRead; + } + + public static MessageResponse fromEntity(Message message) { + MessageResponse response = new MessageResponse(); + response.setId(message.getId()); + response.setConversationId(message.getConversationId()); + response.setSenderId(message.getSenderId()); + response.setContent(message.getContent()); + response.setTimestamp(message.getTimestamp()); + response.setIsRead(message.getIsRead()); + return response; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getConversationId() { + return conversationId; + } + + public void setConversationId(Long conversationId) { + this.conversationId = conversationId; + } + + public Long getSenderId() { + return senderId; + } + + public void setSenderId(Long senderId) { + this.senderId = senderId; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public LocalDateTime getTimestamp() { + return timestamp; + } + + public void setTimestamp(LocalDateTime timestamp) { + this.timestamp = timestamp; + } + + public Boolean getIsRead() { + return isRead; + } + + public void setIsRead(Boolean isRead) { + this.isRead = isRead; + } +} diff --git a/src/main/java/com/petshop/backend/entity/Conversation.java b/src/main/java/com/petshop/backend/entity/Conversation.java new file mode 100644 index 00000000..1d8008e1 --- /dev/null +++ b/src/main/java/com/petshop/backend/entity/Conversation.java @@ -0,0 +1,98 @@ +package com.petshop.backend.entity; + +import jakarta.persistence.*; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "conversation") +public class Conversation { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private Long customerId; + + @Column + private Long staffId; + + @Enumerated(EnumType.STRING) + @Column(length = 20, nullable = false) + private ConversationStatus status = ConversationStatus.OPEN; + + @CreationTimestamp + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; + + @UpdateTimestamp + @Column(name = "updated_at", nullable = false) + private LocalDateTime updatedAt; + + public enum ConversationStatus { + OPEN, CLOSED + } + + public Conversation() { + } + + public Conversation(Long id, Long customerId, Long staffId, ConversationStatus status, LocalDateTime createdAt, LocalDateTime updatedAt) { + this.id = id; + this.customerId = customerId; + this.staffId = staffId; + this.status = status; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getCustomerId() { + return customerId; + } + + public void setCustomerId(Long customerId) { + this.customerId = customerId; + } + + public Long getStaffId() { + return staffId; + } + + public void setStaffId(Long staffId) { + this.staffId = staffId; + } + + public ConversationStatus getStatus() { + return status; + } + + public void setStatus(ConversationStatus status) { + this.status = status; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } +} diff --git a/src/main/java/com/petshop/backend/entity/Message.java b/src/main/java/com/petshop/backend/entity/Message.java new file mode 100644 index 00000000..33777bf5 --- /dev/null +++ b/src/main/java/com/petshop/backend/entity/Message.java @@ -0,0 +1,91 @@ +package com.petshop.backend.entity; + +import jakarta.persistence.*; +import org.hibernate.annotations.CreationTimestamp; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "message") +public class Message { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private Long conversationId; + + @Column(nullable = false) + private Long senderId; + + @Column(nullable = false, columnDefinition = "TEXT") + private String content; + + @CreationTimestamp + @Column(nullable = false, updatable = false) + private LocalDateTime timestamp; + + @Column(nullable = false) + private Boolean isRead = false; + + public Message() { + } + + public Message(Long id, Long conversationId, Long senderId, String content, LocalDateTime timestamp, Boolean isRead) { + this.id = id; + this.conversationId = conversationId; + this.senderId = senderId; + this.content = content; + this.timestamp = timestamp; + this.isRead = isRead; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getConversationId() { + return conversationId; + } + + public void setConversationId(Long conversationId) { + this.conversationId = conversationId; + } + + public Long getSenderId() { + return senderId; + } + + public void setSenderId(Long senderId) { + this.senderId = senderId; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public LocalDateTime getTimestamp() { + return timestamp; + } + + public void setTimestamp(LocalDateTime timestamp) { + this.timestamp = timestamp; + } + + public Boolean getIsRead() { + return isRead; + } + + public void setIsRead(Boolean isRead) { + this.isRead = isRead; + } +} diff --git a/src/main/java/com/petshop/backend/repository/ConversationRepository.java b/src/main/java/com/petshop/backend/repository/ConversationRepository.java new file mode 100644 index 00000000..142853d6 --- /dev/null +++ b/src/main/java/com/petshop/backend/repository/ConversationRepository.java @@ -0,0 +1,13 @@ +package com.petshop.backend.repository; + +import com.petshop.backend.entity.Conversation; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface ConversationRepository extends JpaRepository { + List findByCustomerId(Long customerId); + List findByStaffId(Long staffId); +} diff --git a/src/main/java/com/petshop/backend/repository/MessageRepository.java b/src/main/java/com/petshop/backend/repository/MessageRepository.java new file mode 100644 index 00000000..6c87be6e --- /dev/null +++ b/src/main/java/com/petshop/backend/repository/MessageRepository.java @@ -0,0 +1,12 @@ +package com.petshop.backend.repository; + +import com.petshop.backend.entity.Message; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface MessageRepository extends JpaRepository { + List findByConversationIdOrderByTimestampAsc(Long conversationId); +} diff --git a/src/main/java/com/petshop/backend/service/ChatService.java b/src/main/java/com/petshop/backend/service/ChatService.java new file mode 100644 index 00000000..e7d7ec00 --- /dev/null +++ b/src/main/java/com/petshop/backend/service/ChatService.java @@ -0,0 +1,126 @@ +package com.petshop.backend.service; + +import com.petshop.backend.dto.chat.ConversationRequest; +import com.petshop.backend.dto.chat.ConversationResponse; +import com.petshop.backend.dto.chat.MessageRequest; +import com.petshop.backend.dto.chat.MessageResponse; +import com.petshop.backend.entity.Conversation; +import com.petshop.backend.entity.Message; +import com.petshop.backend.entity.User; +import com.petshop.backend.exception.ResourceNotFoundException; +import com.petshop.backend.repository.ConversationRepository; +import com.petshop.backend.repository.MessageRepository; +import com.petshop.backend.repository.UserRepository; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +public class ChatService { + + private final ConversationRepository conversationRepository; + private final MessageRepository messageRepository; + private final UserRepository userRepository; + + public ChatService(ConversationRepository conversationRepository, + MessageRepository messageRepository, + UserRepository userRepository) { + this.conversationRepository = conversationRepository; + this.messageRepository = messageRepository; + this.userRepository = userRepository; + } + + @Transactional + public ConversationResponse createConversation(Long userId, ConversationRequest request) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new ResourceNotFoundException("User not found")); + + Conversation conversation = new Conversation(); + conversation.setCustomerId(userId); + conversation.setStatus(Conversation.ConversationStatus.OPEN); + conversation = conversationRepository.save(conversation); + + Message message = new Message(); + message.setConversationId(conversation.getId()); + message.setSenderId(userId); + message.setContent(request.getMessage()); + message.setIsRead(false); + messageRepository.save(message); + + return ConversationResponse.fromEntity(conversation, request.getMessage()); + } + + public List getConversations(Long userId, User.Role role) { + List conversations; + + if (role == User.Role.CUSTOMER) { + conversations = conversationRepository.findByCustomerId(userId); + } else if (role == User.Role.STAFF) { + conversations = conversationRepository.findByStaffId(userId); + if (conversations.isEmpty()) { + conversations = conversationRepository.findAll(); + } + } else { + conversations = conversationRepository.findAll(); + } + + return conversations.stream() + .map(conv -> { + List messages = messageRepository.findByConversationIdOrderByTimestampAsc(conv.getId()); + String lastMessage = messages.isEmpty() ? "" : messages.get(messages.size() - 1).getContent(); + return ConversationResponse.fromEntity(conv, lastMessage); + }) + .collect(Collectors.toList()); + } + + public ConversationResponse getConversation(Long conversationId, Long userId, User.Role role) { + Conversation conversation = conversationRepository.findById(conversationId) + .orElseThrow(() -> new ResourceNotFoundException("Conversation not found")); + + if (role == User.Role.CUSTOMER && !conversation.getCustomerId().equals(userId)) { + throw new AccessDeniedException("You can only view your own conversations"); + } + + List messages = messageRepository.findByConversationIdOrderByTimestampAsc(conversationId); + String lastMessage = messages.isEmpty() ? "" : messages.get(messages.size() - 1).getContent(); + + return ConversationResponse.fromEntity(conversation, lastMessage); + } + + @Transactional + public MessageResponse sendMessage(Long conversationId, Long userId, MessageRequest request) { + Conversation conversation = conversationRepository.findById(conversationId) + .orElseThrow(() -> new ResourceNotFoundException("Conversation not found")); + + Message message = new Message(); + message.setConversationId(conversationId); + message.setSenderId(userId); + message.setContent(request.getContent()); + message.setIsRead(false); + message = messageRepository.save(message); + + if (conversation.getStaffId() == null && !userId.equals(conversation.getCustomerId())) { + conversation.setStaffId(userId); + conversationRepository.save(conversation); + } + + return MessageResponse.fromEntity(message); + } + + public List getMessages(Long conversationId, Long userId, User.Role role) { + Conversation conversation = conversationRepository.findById(conversationId) + .orElseThrow(() -> new ResourceNotFoundException("Conversation not found")); + + if (role == User.Role.CUSTOMER && !conversation.getCustomerId().equals(userId)) { + throw new AccessDeniedException("You can only view messages from your own conversations"); + } + + List messages = messageRepository.findByConversationIdOrderByTimestampAsc(conversationId); + return messages.stream() + .map(MessageResponse::fromEntity) + .collect(Collectors.toList()); + } +} From d7d294130f42d13ea6696de99710b4407d63f510 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 8 Mar 2026 15:23:16 -0600 Subject: [PATCH 08/14] Complete Postman collection with all endpoints --- petshop-api.postman_collection.json | 3536 ++++++++++++--------------- 1 file changed, 1550 insertions(+), 1986 deletions(-) diff --git a/petshop-api.postman_collection.json b/petshop-api.postman_collection.json index 1a5eca4e..50d4fa3a 100644 --- a/petshop-api.postman_collection.json +++ b/petshop-api.postman_collection.json @@ -1,8 +1,8 @@ { "info": { - "name": "PetShop Unified API v1 (RBAC + Dropdowns + Chat)", - "_postman_id": "4d2d4e10-4338-4b85-a8ef-aa9db2ed75be", - "description": "Unified /api/v1 endpoints for Desktop, Android, and Website.\n\nVariables:\n- baseUrl (example http://localhost:8080)\n- staffToken (set by staff/admin login)\n- customerToken (set by customer login)\n\nNotes:\n- Login test scripts assume response contains {\"token\":\"...\"}.\n- Bulk delete uses DELETE with JSON body {ids:[...]}.", + "name": "PetShop API Complete Collection", + "_postman_id": "petshop-api-complete-v1", + "description": "Complete API collection with all 95+ verified endpoints", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, "variable": [ @@ -10,74 +10,266 @@ "key": "baseUrl", "value": "http://localhost:8080" }, - { - "key": "staffToken", - "value": "" - }, { "key": "customerToken", "value": "" }, { - "key": "petId", - "value": "1" + "key": "staffToken", + "value": "" }, { - "key": "adoptionId", - "value": "1" - }, - { - "key": "appointmentId", - "value": "1" - }, - { - "key": "serviceId", - "value": "1" - }, - { - "key": "prodId", - "value": "1" - }, - { - "key": "categoryId", - "value": "1" - }, - { - "key": "supId", - "value": "1" - }, - { - "key": "inventoryId", - "value": "1" - }, - { - "key": "saleId", - "value": "1" - }, - { - "key": "purchaseOrderId", - "value": "1" - }, - { - "key": "userId", - "value": "1" - }, - { - "key": "employeeId", - "value": "1" - }, - { - "key": "roomId", - "value": "1" - }, - { - "key": "storeId", - "value": "1" + "key": "adminToken", + "value": "" } ], "item": [ { - "name": "Auth", + "name": "1 Public Endpoints", + "item": [ + { + "name": "Health Check", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/health", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ] + } + }, + { + "name": "Get All Pets", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/pets", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ] + } + }, + { + "name": "Get Pet by ID", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/pets/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ] + } + }, + { + "name": "Get All Products", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/products", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ] + } + }, + { + "name": "Get Product by ID", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/products/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ] + } + }, + { + "name": "Get All Sales", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/sales", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ] + } + }, + { + "name": "Get Sale by ID", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/sales/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ] + } + }, + { + "name": "Get All Services", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/services", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ] + } + }, + { + "name": "Get Service by ID", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/services/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ] + } + }, + { + "name": "Get All Categories", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/categories", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ] + } + }, + { + "name": "Get Category by ID", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/categories/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ] + } + }, + { + "name": "Check Appointment Availability", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/appointments/availability?storeId=1&serviceId=1&date=2026-03-15", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ] + } + }, + { + "name": "Dropdowns", + "item": [ + { + "name": "Get Pets Dropdown", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/dropdowns/pets", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ] + } + }, + { + "name": "Get Customers Dropdown", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/dropdowns/customers", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ] + } + }, + { + "name": "Get Services Dropdown", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/dropdowns/services", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ] + } + }, + { + "name": "Get Products Dropdown", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/dropdowns/products", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ] + } + }, + { + "name": "Get Categories Dropdown", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/dropdowns/categories", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ] + } + }, + { + "name": "Get Stores Dropdown", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/dropdowns/stores", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ] + } + } + ] + } + ] + }, + { + "name": "2 Authentication", "item": [ { "name": "Register Customer", @@ -92,17 +284,12 @@ ], "body": { "mode": "raw", - "raw": "{\n \"firstName\": \"Chris\",\n \"lastName\": \"Ng\",\n \"email\": \"chris.ng@example.com\",\n \"phone\": \"555-0120\",\n \"password\": \"ChangeMe123\"\n}", - "options": { - "raw": { - "language": "json" - } - } + "raw": "{\n \"username\": \"newcustomer\",\n \"password\": \"password123\",\n \"email\": \"new@example.com\",\n \"fullName\": \"New Customer\"\n}" } } }, { - "name": "Login (Staff/Admin) -> sets staffToken", + "name": "Login as Customer", "request": { "method": "POST", "url": "{{baseUrl}}/api/v1/auth/login", @@ -114,31 +301,29 @@ ], "body": { "mode": "raw", - "raw": "{\n \"username\": \"admin\",\n \"password\": \"admin123\"\n}", - "options": { - "raw": { - "language": "json" - } - } + "raw": "{\n \"username\": \"customer\",\n \"password\": \"customer123\"\n}" } }, "event": [ { "listen": "test", "script": { - "type": "text/javascript", "exec": [ - "try {", - " const json = pm.response.json();", - " if (json && json.token) pm.collectionVariables.set('staffToken', json.token);", - "} catch (e) {}" + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});", + "", + "var jsonData = pm.response.json();", + "if (jsonData.token) {", + " pm.collectionVariables.set('customerToken', jsonData.token);", + "}" ] } } ] }, { - "name": "Login (Staff) -> sets staffToken", + "name": "Login as Staff", "request": { "method": "POST", "url": "{{baseUrl}}/api/v1/auth/login", @@ -150,31 +335,29 @@ ], "body": { "mode": "raw", - "raw": "{\n \"username\": \"staff\",\n \"password\": \"staff123\"\n}", - "options": { - "raw": { - "language": "json" - } - } + "raw": "{\n \"username\": \"staff\",\n \"password\": \"staff123\"\n}" } }, "event": [ { "listen": "test", "script": { - "type": "text/javascript", "exec": [ - "try {", - " const json = pm.response.json();", - " if (json && json.token) pm.collectionVariables.set('staffToken', json.token);", - "} catch (e) {}" + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});", + "", + "var jsonData = pm.response.json();", + "if (jsonData.token) {", + " pm.collectionVariables.set('staffToken', jsonData.token);", + "}" ] } } ] }, { - "name": "Login (Customer) -> sets customerToken", + "name": "Login as Admin", "request": { "method": "POST", "url": "{{baseUrl}}/api/v1/auth/login", @@ -184,599 +367,235 @@ "value": "application/json" } ], - "description": "If login uses username instead of email, adjust request body.", "body": { "mode": "raw", - "raw": "{\n \"email\": \"chris.ng@example.com\",\n \"password\": \"ChangeMe123\"\n}", - "options": { - "raw": { - "language": "json" - } - } + "raw": "{\n \"username\": \"admin\",\n \"password\": \"admin123\"\n}" } }, "event": [ { "listen": "test", "script": { - "type": "text/javascript", "exec": [ - "try {", - " const json = pm.response.json();", - " if (json && json.token) pm.collectionVariables.set('customerToken', json.token);", - "} catch (e) {}" + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});", + "", + "var jsonData = pm.response.json();", + "if (jsonData.token) {", + " pm.collectionVariables.set('adminToken', jsonData.token);", + "}" ] } } ] }, { - "name": "Logout (Staff/Admin)", - "request": { - "method": "POST", - "url": "{{baseUrl}}/api/v1/auth/logout", - "header": [], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - } - } - }, - { - "name": "Logout (Customer)", - "request": { - "method": "POST", - "url": "{{baseUrl}}/api/v1/auth/logout", - "header": [], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{customerToken}}", - "type": "string" - } - ] - } - } - }, - { - "name": "Auth Me (Staff/Admin)", + "name": "Get My Profile", "request": { "method": "GET", "url": "{{baseUrl}}/api/v1/auth/me", - "header": [], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - } + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{customerToken}}", + "type": "text" + } + ] } }, { - "name": "Auth Me (Customer)", + "name": "Update My Profile", "request": { - "method": "GET", + "method": "PUT", "url": "{{baseUrl}}/api/v1/auth/me", - "header": [], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{customerToken}}", - "type": "string" - } - ] - } - } - }, - { - "name": "Health Check", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/health", - "header": [] - } - } - ] - }, - { - "name": "My Account (/me)", - "item": [ - { - "name": "Get My Profile (Staff/Admin)", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/me", - "header": [], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - } - } - }, - { - "name": "Update My Profile (Staff/Admin)", - "request": { - "method": "PUT", - "url": "{{baseUrl}}/api/v1/me", "header": [ { "key": "Content-Type", "value": "application/json" - } - ], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - }, - "body": { - "mode": "raw", - "raw": "{\n \"phone\": \"555-0999\"\n}", - "options": { - "raw": { - "language": "json" - } - } - } - } - }, - { - "name": "Get My Profile (Customer)", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/me", - "header": [], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{customerToken}}", - "type": "string" - } - ] - } - } - }, - { - "name": "Update My Profile (Customer)", - "request": { - "method": "PUT", - "url": "{{baseUrl}}/api/v1/me", - "header": [ + }, { - "key": "Content-Type", - "value": "application/json" + "key": "Authorization", + "value": "Bearer {{customerToken}}", + "type": "text" } ], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{customerToken}}", - "type": "string" - } - ] - }, "body": { "mode": "raw", - "raw": "{\n \"phone\": \"555-0999\"\n}", - "options": { - "raw": { - "language": "json" - } - } - } - } - }, - { - "name": "Upload My Avatar (Staff/Admin) [multipart]", - "request": { - "method": "PUT", - "url": "{{baseUrl}}/api/v1/me/avatar", - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - }, - "header": [], - "body": { - "mode": "formdata", - "formdata": [ - { - "key": "file", - "type": "file", - "src": "" - } - ] - }, - "description": "Uploads avatar image for the logged-in staff/admin. Public access via GET /api/v1/staff/{employeeId}/avatar." - } - }, - { - "name": "Upload My Avatar (Customer) [multipart]", - "request": { - "method": "PUT", - "url": "{{baseUrl}}/api/v1/me/avatar", - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{customerToken}}", - "type": "string" - } - ] - }, - "header": [], - "body": { - "mode": "formdata", - "formdata": [ - { - "key": "file", - "type": "file", - "src": "" - } - ] - }, - "description": "Optional if customer avatars are supported." - } - } - ] - }, - { - "name": "Dropdowns (lightweight id+name)", - "item": [ - { - "name": "Dropdown - Pets (staff)", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/dropdowns/pets", - "header": [], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - } - } - }, - { - "name": "Dropdown - Customers (staff)", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/dropdowns/customers", - "header": [], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - } - } - }, - { - "name": "Dropdown - Services (staff)", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/dropdowns/services", - "header": [], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - } - } - }, - { - "name": "Dropdown - Products (staff)", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/dropdowns/products", - "header": [], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - } - } - }, - { - "name": "Dropdown - Suppliers (admin)", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/dropdowns/suppliers", - "header": [], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - } - } - }, - { - "name": "Dropdown - Categories (staff)", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/dropdowns/categories", - "header": [], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - } - } - }, - { - "name": "Dropdown - Stores (staff)", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/dropdowns/stores", - "header": [], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - } - } - }, - { - "name": "Dropdown - Services (customer)", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/dropdowns/services", - "header": [], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{customerToken}}", - "type": "string" - } - ] - } - } - }, - { - "name": "Dropdown - Stores (customer)", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/dropdowns/stores", - "header": [], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{customerToken}}", - "type": "string" - } - ] + "raw": "{\n \"fullName\": \"Updated Name\",\n \"email\": \"updated@example.com\"\n}" } } } ] }, { - "name": "Resources (Staff)", + "name": "3 Customer Features", "item": [ { - "name": "Pets", + "name": "Chat", "item": [ { - "name": "List/Search", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/pets?q=&page=0&size=50&sort=id,desc", - "header": [], - "description": "Returns UI-ready rows (joined fields included).", - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - } - } - }, - { - "name": "Create", + "name": "Create Conversation", "request": { "method": "POST", - "url": "{{baseUrl}}/api/v1/pets", + "url": "{{baseUrl}}/api/v1/chat/conversations", "header": [ { "key": "Content-Type", "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{customerToken}}", + "type": "text" } ], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - }, "body": { "mode": "raw", - "raw": "{\n \"petName\": \"Buddy\",\n \"petSpecies\": \"Dog\",\n \"petBreed\": \"Labrador\",\n \"petAge\": 3,\n \"petStatus\": \"Available\",\n \"petPrice\": 499.99\n}", - "options": { - "raw": { - "language": "json" - } - } + "raw": "{\n \"message\": \"I need help\"\n}" } } }, { - "name": "Get One", + "name": "List Conversations", "request": { "method": "GET", - "url": "{{baseUrl}}/api/v1/pets/{{petId}}", - "header": [], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - } - } - }, - { - "name": "Update (PUT)", - "request": { - "method": "PUT", - "url": "{{baseUrl}}/api/v1/pets/{{petId}}", + "url": "{{baseUrl}}/api/v1/chat/conversations", "header": [ { "key": "Content-Type", "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{customerToken}}", + "type": "text" } - ], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - }, - "body": { - "mode": "raw", - "raw": "{\n \"petName\": \"Buddy\",\n \"petSpecies\": \"Dog\",\n \"petBreed\": \"Labrador\",\n \"petAge\": 4,\n \"petStatus\": \"Available\",\n \"petPrice\": 450.0\n}", - "options": { - "raw": { - "language": "json" - } - } - } + ] } }, { - "name": "Delete One", + "name": "Get Conversation", "request": { - "method": "DELETE", - "url": "{{baseUrl}}/api/v1/pets/{{petId}}", - "header": [], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - } - } - }, - { - "name": "Bulk Delete (DELETE body)", - "request": { - "method": "DELETE", - "url": "{{baseUrl}}/api/v1/pets", + "method": "GET", + "url": "{{baseUrl}}/api/v1/chat/conversations/1", "header": [ { "key": "Content-Type", "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{customerToken}}", + "type": "text" + } + ] + } + }, + { + "name": "Send Message", + "request": { + "method": "POST", + "url": "{{baseUrl}}/api/v1/chat/conversations/1/messages", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{customerToken}}", + "type": "text" } ], - "description": "Bulk delete with JSON body.", - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - }, "body": { "mode": "raw", - "raw": "{\n \"ids\": [\n 1,\n 2,\n 3\n ]\n}", - "options": { - "raw": { - "language": "json" - } + "raw": "{\n \"content\": \"Hello\"\n}" + } + } + }, + { + "name": "Get Messages", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/chat/conversations/1/messages", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{customerToken}}", + "type": "text" } + ] + } + } + ] + }, + { + "name": "Appointments", + "item": [ + { + "name": "List Appointments", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/appointments", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{customerToken}}", + "type": "text" + } + ] + } + }, + { + "name": "Get Appointment", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/appointments/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{customerToken}}", + "type": "text" + } + ] + } + }, + { + "name": "Create Appointment", + "request": { + "method": "POST", + "url": "{{baseUrl}}/api/v1/appointments", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{customerToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"customerId\": 1,\n \"storeId\": 1,\n \"serviceId\": 1,\n \"appointmentDate\": \"2026-03-15\",\n \"appointmentTime\": \"10:00:00\",\n \"notes\": \"Test\"\n}" } } } @@ -786,26 +605,43 @@ "name": "Adoptions", "item": [ { - "name": "List/Search", + "name": "List Adoptions", "request": { "method": "GET", - "url": "{{baseUrl}}/api/v1/adoptions?q=&page=0&size=50&sort=id,desc", - "header": [], - "description": "Returns UI-ready rows (joined fields included).", - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - } + "url": "{{baseUrl}}/api/v1/adoptions", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{customerToken}}", + "type": "text" + } + ] } }, { - "name": "Create", + "name": "Get Adoption", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/adoptions/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{customerToken}}", + "type": "text" + } + ] + } + }, + { + "name": "Create Adoption", "request": { "method": "POST", "url": "{{baseUrl}}/api/v1/adoptions", @@ -813,459 +649,173 @@ { "key": "Content-Type", "value": "application/json" - } - ], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - }, - "body": { - "mode": "raw", - "raw": "{\n \"petId\": \"{{petId}}\",\n \"customerId\": 1,\n \"adoptionDate\": \"2026-03-04\",\n \"adoptionStatus\": \"Pending\"\n}", - "options": { - "raw": { - "language": "json" - } - } - } - } - }, - { - "name": "Get One", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/adoptions/{{adoptionId}}", - "header": [], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - } - } - }, - { - "name": "Update (PUT)", - "request": { - "method": "PUT", - "url": "{{baseUrl}}/api/v1/adoptions/{{adoptionId}}", - "header": [ + }, { - "key": "Content-Type", - "value": "application/json" + "key": "Authorization", + "value": "Bearer {{customerToken}}", + "type": "text" } ], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - }, "body": { "mode": "raw", - "raw": "{\n \"petId\": \"{{petId}}\",\n \"customerId\": 1,\n \"adoptionDate\": \"2026-03-04\",\n \"adoptionStatus\": \"Approved\"\n}", - "options": { - "raw": { - "language": "json" - } - } - } - } - }, - { - "name": "Delete One", - "request": { - "method": "DELETE", - "url": "{{baseUrl}}/api/v1/adoptions/{{adoptionId}}", - "header": [], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - } - } - }, - { - "name": "Bulk Delete (DELETE body)", - "request": { - "method": "DELETE", - "url": "{{baseUrl}}/api/v1/adoptions", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "description": "Bulk delete with JSON body.", - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - }, - "body": { - "mode": "raw", - "raw": "{\n \"ids\": [\n 1,\n 2,\n 3\n ]\n}", - "options": { - "raw": { - "language": "json" - } - } + "raw": "{\n \"petId\": 1,\n \"customerId\": 1,\n \"adoptionDate\": \"2026-03-10\"\n}" } } } ] }, { - "name": "Appointments", + "name": "Refunds", "item": [ { - "name": "List/Search", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/appointments?q=&page=0&size=50&sort=id,desc", - "header": [], - "description": "Returns UI-ready rows (joined fields included).", - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - } - } - }, - { - "name": "Create", + "name": "Create Refund", "request": { "method": "POST", - "url": "{{baseUrl}}/api/v1/appointments", + "url": "{{baseUrl}}/api/v1/refunds", "header": [ { "key": "Content-Type", "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{customerToken}}", + "type": "text" } ], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - }, "body": { "mode": "raw", - "raw": "{\n \"serviceId\": \"{{serviceId}}\",\n \"customerId\": 1,\n \"appointmentDate\": \"2026-03-04\",\n \"appointmentTime\": \"14:30:00\",\n \"appointmentStatus\": \"Booked\",\n \"petIds\": [\n \"{{petId}}\"\n ]\n}", - "options": { - "raw": { - "language": "json" - } - } + "raw": "{\n \"saleId\": 1,\n \"reason\": \"Defective\",\n \"amount\": 50.0\n}" } } }, { - "name": "Get One", + "name": "List Refunds", "request": { "method": "GET", - "url": "{{baseUrl}}/api/v1/appointments/{{appointmentId}}", - "header": [], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - } - } - }, - { - "name": "Update (PUT)", - "request": { - "method": "PUT", - "url": "{{baseUrl}}/api/v1/appointments/{{appointmentId}}", + "url": "{{baseUrl}}/api/v1/refunds", "header": [ { "key": "Content-Type", "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{customerToken}}", + "type": "text" } - ], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - }, - "body": { - "mode": "raw", - "raw": "{\n \"serviceId\": \"{{serviceId}}\",\n \"customerId\": 1,\n \"appointmentDate\": \"2026-03-05\",\n \"appointmentTime\": \"15:00:00\",\n \"appointmentStatus\": \"Booked\",\n \"petIds\": [\n \"{{petId}}\"\n ]\n}", - "options": { - "raw": { - "language": "json" - } - } - } + ] } }, { - "name": "Delete One", + "name": "Get Refund", "request": { - "method": "DELETE", - "url": "{{baseUrl}}/api/v1/appointments/{{appointmentId}}", - "header": [], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - } - } - }, - { - "name": "Bulk Delete (DELETE body)", - "request": { - "method": "DELETE", - "url": "{{baseUrl}}/api/v1/appointments", + "method": "GET", + "url": "{{baseUrl}}/api/v1/refunds/1", "header": [ { "key": "Content-Type", "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{customerToken}}", + "type": "text" } - ], - "description": "Bulk delete with JSON body.", - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - }, - "body": { - "mode": "raw", - "raw": "{\n \"ids\": [\n 1,\n 2,\n 3\n ]\n}", - "options": { - "raw": { - "language": "json" - } - } - } + ] } } ] - }, + } + ] + }, + { + "name": "4 Staff Operations", + "item": [ { - "name": "Appointments - Availability", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/appointments/availability?storeId={{storeId}}&serviceId={{serviceId}}&date=2026-03-04", - "header": [], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - } - } - }, - { - "name": "Services", + "name": "Pets", "item": [ { - "name": "List/Search", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/services?q=&page=0&size=50&sort=id,desc", - "header": [], - "description": "Returns UI-ready rows (joined fields included).", - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - } - } - }, - { - "name": "Create", + "name": "Create Pet", "request": { "method": "POST", - "url": "{{baseUrl}}/api/v1/services", + "url": "{{baseUrl}}/api/v1/pets", "header": [ { "key": "Content-Type", "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" } ], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - }, "body": { "mode": "raw", - "raw": "{\n \"serviceName\": \"Grooming\",\n \"serviceDesc\": \"Full grooming package\",\n \"serviceDuration\": 60,\n \"servicePrice\": 49.99\n}", - "options": { - "raw": { - "language": "json" - } - } + "raw": "{\n \"petName\": \"Buddy\",\n \"petSpecies\": \"Dog\",\n \"petBreed\": \"Labrador\",\n \"petAge\": 2,\n \"petStatus\": \"Available\",\n \"petPrice\": 500.0\n}" } } }, { - "name": "Get One", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/services/{{serviceId}}", - "header": [], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - } - } - }, - { - "name": "Update (PUT)", + "name": "Update Pet", "request": { "method": "PUT", - "url": "{{baseUrl}}/api/v1/services/{{serviceId}}", + "url": "{{baseUrl}}/api/v1/pets/1", "header": [ { "key": "Content-Type", "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" } ], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - }, "body": { "mode": "raw", - "raw": "{\n \"serviceName\": \"Grooming\",\n \"serviceDesc\": \"Updated description\",\n \"serviceDuration\": 60,\n \"servicePrice\": 54.99\n}", - "options": { - "raw": { - "language": "json" - } - } + "raw": "{\n \"petName\": \"Buddy\",\n \"petSpecies\": \"Dog\",\n \"petBreed\": \"Labrador\",\n \"petAge\": 3,\n \"petStatus\": \"Available\",\n \"petPrice\": 500.0\n}" } } }, { - "name": "Delete One", + "name": "Delete Pet", "request": { "method": "DELETE", - "url": "{{baseUrl}}/api/v1/services/{{serviceId}}", - "header": [], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - } - } - }, - { - "name": "Bulk Delete (DELETE body)", - "request": { - "method": "DELETE", - "url": "{{baseUrl}}/api/v1/services", + "url": "{{baseUrl}}/api/v1/pets/1", "header": [ { "key": "Content-Type", "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ] + } + }, + { + "name": "Bulk Delete Pets", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/v1/pets", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" } ], - "description": "Bulk delete with JSON body.", - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - }, "body": { "mode": "raw", - "raw": "{\n \"ids\": [\n 1,\n 2,\n 3\n ]\n}", - "options": { - "raw": { - "language": "json" - } - } + "raw": "{\n \"ids\": [\n 1,\n 2\n ]\n}" } } } @@ -1275,26 +825,7 @@ "name": "Products", "item": [ { - "name": "List/Search", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/products?q=&page=0&size=50&sort=id,desc", - "header": [], - "description": "Returns UI-ready rows (joined fields included).", - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - } - } - }, - { - "name": "Create", + "name": "Create Product", "request": { "method": "POST", "url": "{{baseUrl}}/api/v1/products", @@ -1302,127 +833,154 @@ { "key": "Content-Type", "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" } ], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - }, "body": { "mode": "raw", - "raw": "{\n \"prodName\": \"Dog Food\",\n \"prodPrice\": 19.99,\n \"prodDesc\": \"Large bag\",\n \"categoryId\": \"{{categoryId}}\"\n}", - "options": { - "raw": { - "language": "json" - } - } + "raw": "{\n \"prodName\": \"Dog Food\",\n \"categoryId\": 1,\n \"prodDesc\": \"Premium\",\n \"prodPrice\": 50.0\n}" } } }, { - "name": "Get One", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/products/{{prodId}}", - "header": [], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - } - } - }, - { - "name": "Update (PUT)", + "name": "Update Product", "request": { "method": "PUT", - "url": "{{baseUrl}}/api/v1/products/{{prodId}}", + "url": "{{baseUrl}}/api/v1/products/1", "header": [ { "key": "Content-Type", "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" } ], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - }, "body": { "mode": "raw", - "raw": "{\n \"prodName\": \"Dog Food\",\n \"prodPrice\": 21.99,\n \"prodDesc\": \"Large bag\",\n \"categoryId\": \"{{categoryId}}\"\n}", - "options": { - "raw": { - "language": "json" - } - } + "raw": "{\n \"prodName\": \"Dog Food\",\n \"categoryId\": 1,\n \"prodDesc\": \"Premium\",\n \"prodPrice\": 55.0\n}" } } - }, + } + ] + }, + { + "name": "Sales", + "item": [ { - "name": "Delete One", + "name": "Create Sale", "request": { - "method": "DELETE", - "url": "{{baseUrl}}/api/v1/products/{{prodId}}", - "header": [], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - } - } - }, - { - "name": "Bulk Delete (DELETE body)", - "request": { - "method": "DELETE", - "url": "{{baseUrl}}/api/v1/products", + "method": "POST", + "url": "{{baseUrl}}/api/v1/sales", "header": [ { "key": "Content-Type", "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" } ], - "description": "Bulk delete with JSON body.", - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - }, "body": { "mode": "raw", - "raw": "{\n \"ids\": [\n 1,\n 2,\n 3\n ]\n}", - "options": { - "raw": { - "language": "json" - } + "raw": "{\n \"employeeId\": 1,\n \"storeId\": 1,\n \"totalAmount\": 100.0,\n \"paymentMethod\": \"Card\",\n \"isRefund\": false\n}" + } + } + } + ] + }, + { + "name": "Services", + "item": [ + { + "name": "Create Service", + "request": { + "method": "POST", + "url": "{{baseUrl}}/api/v1/services", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" } + ], + "body": { + "mode": "raw", + "raw": "{\n \"serviceName\": \"Grooming\",\n \"serviceDesc\": \"Full grooming\",\n \"servicePrice\": 75.0\n}" + } + } + }, + { + "name": "Update Service", + "request": { + "method": "PUT", + "url": "{{baseUrl}}/api/v1/services/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"serviceName\": \"Grooming\",\n \"serviceDesc\": \"Full grooming\",\n \"servicePrice\": 80.0\n}" + } + } + }, + { + "name": "Delete Service", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/v1/services/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ] + } + }, + { + "name": "Bulk Delete Services", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/v1/services", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"ids\": [\n 1\n ]\n}" } } } @@ -1432,26 +990,7 @@ "name": "Categories", "item": [ { - "name": "List/Search", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/categories?q=&page=0&size=50&sort=id,desc", - "header": [], - "description": "Returns UI-ready rows (joined fields included).", - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - } - } - }, - { - "name": "Create", + "name": "Create Category", "request": { "method": "POST", "url": "{{baseUrl}}/api/v1/categories", @@ -1459,99 +998,61 @@ { "key": "Content-Type", "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" } ], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - }, "body": { "mode": "raw", - "raw": "{\n \"categoryName\": \"Food\",\n \"categoryType\": \"PRODUCT\"\n}", - "options": { - "raw": { - "language": "json" - } - } + "raw": "{\n \"categoryName\": \"Dog Supplies\"\n}" } } }, { - "name": "Get One", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/categories/{{categoryId}}", - "header": [], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - } - } - }, - { - "name": "Update (PUT)", + "name": "Update Category", "request": { "method": "PUT", - "url": "{{baseUrl}}/api/v1/categories/{{categoryId}}", + "url": "{{baseUrl}}/api/v1/categories/1", "header": [ { "key": "Content-Type", "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" } ], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - }, "body": { "mode": "raw", - "raw": "{\n \"categoryName\": \"Food & Treats\",\n \"categoryType\": \"PRODUCT\"\n}", - "options": { - "raw": { - "language": "json" - } - } + "raw": "{\n \"categoryName\": \"Dog Supplies\"\n}" } } }, { - "name": "Delete One", + "name": "Delete Category", "request": { "method": "DELETE", - "url": "{{baseUrl}}/api/v1/categories/{{categoryId}}", - "header": [], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - } + "url": "{{baseUrl}}/api/v1/categories/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ] } }, { - "name": "Bulk Delete (DELETE body)", + "name": "Bulk Delete Categories", "request": { "method": "DELETE", "url": "{{baseUrl}}/api/v1/categories", @@ -1559,27 +1060,258 @@ { "key": "Content-Type", "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" } ], - "description": "Bulk delete with JSON body.", - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - }, "body": { "mode": "raw", - "raw": "{\n \"ids\": [\n 1,\n 2,\n 3\n ]\n}", - "options": { - "raw": { - "language": "json" - } + "raw": "{\n \"ids\": [\n 1\n ]\n}" + } + } + } + ] + }, + { + "name": "Customers", + "item": [ + { + "name": "List Customers", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/customers", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" } + ] + } + }, + { + "name": "Get Customer", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/customers/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ] + } + }, + { + "name": "Create Customer", + "request": { + "method": "POST", + "url": "{{baseUrl}}/api/v1/customers", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"firstName\": \"John\",\n \"lastName\": \"Doe\",\n \"email\": \"john@example.com\",\n \"phone\": \"555-0100\",\n \"address\": \"123 Main St\"\n}" + } + } + }, + { + "name": "Update Customer", + "request": { + "method": "PUT", + "url": "{{baseUrl}}/api/v1/customers/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"firstName\": \"John\",\n \"lastName\": \"Doe\",\n \"email\": \"john@example.com\",\n \"phone\": \"555-0100\",\n \"address\": \"123 Main St\"\n}" + } + } + }, + { + "name": "Delete Customer", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/v1/customers/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ] + } + }, + { + "name": "Bulk Delete Customers", + "request": { + "method": "POST", + "url": "{{baseUrl}}/api/v1/customers/bulk-delete", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"ids\": [\n 1\n ]\n}" + } + } + } + ] + }, + { + "name": "Appointments Management", + "item": [ + { + "name": "Update Appointment", + "request": { + "method": "PUT", + "url": "{{baseUrl}}/api/v1/appointments/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"customerId\": 1,\n \"storeId\": 1,\n \"serviceId\": 1,\n \"appointmentDate\": \"2026-03-15\",\n \"appointmentTime\": \"14:00:00\",\n \"notes\": \"Updated\"\n}" + } + } + }, + { + "name": "Delete Appointment", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/v1/appointments/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ] + } + } + ] + }, + { + "name": "Adoptions Management", + "item": [ + { + "name": "Update Adoption", + "request": { + "method": "PUT", + "url": "{{baseUrl}}/api/v1/adoptions/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"petId\": 1,\n \"customerId\": 1,\n \"adoptionDate\": \"2026-03-11\"\n}" + } + } + }, + { + "name": "Delete Adoption", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/v1/adoptions/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ] + } + } + ] + }, + { + "name": "Refunds Management", + "item": [ + { + "name": "Update Refund", + "request": { + "method": "PUT", + "url": "{{baseUrl}}/api/v1/refunds/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"status\": \"APPROVED\"\n}" } } } @@ -1588,32 +1320,317 @@ ] }, { - "name": "Admin-only Resources", + "name": "5 Admin Operations", "item": [ { - "name": "Inventory", + "name": "Analytics Dashboard", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/analytics/dashboard", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ] + } + }, + { + "name": "Users", "item": [ { - "name": "List/Search", + "name": "List Users", "request": { "method": "GET", - "url": "{{baseUrl}}/api/v1/inventory?q=&page=0&size=50&sort=id,desc", - "header": [], - "description": "Returns UI-ready rows (joined fields included).", - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] + "url": "{{baseUrl}}/api/v1/users", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ] + } + }, + { + "name": "Get User", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/users/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ] + } + }, + { + "name": "Create User", + "request": { + "method": "POST", + "url": "{{baseUrl}}/api/v1/users", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"username\": \"newuser\",\n \"password\": \"password123\",\n \"role\": \"STAFF\"\n}" } } }, { - "name": "Create", + "name": "Update User", + "request": { + "method": "PUT", + "url": "{{baseUrl}}/api/v1/users/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"username\": \"user1\",\n \"role\": \"STAFF\"\n}" + } + } + }, + { + "name": "Delete User", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/v1/users/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ] + } + }, + { + "name": "Bulk Delete Users", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/v1/users", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"ids\": [\n 1\n ]\n}" + } + } + } + ] + }, + { + "name": "Stores", + "item": [ + { + "name": "List Stores", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/stores", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ] + } + }, + { + "name": "Get Store", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/stores/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ] + } + }, + { + "name": "Create Store", + "request": { + "method": "POST", + "url": "{{baseUrl}}/api/v1/stores", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"storeName\": \"New Branch\",\n \"storeAddress\": \"456 Oak St\",\n \"storePhone\": \"555-0200\"\n}" + } + } + }, + { + "name": "Update Store", + "request": { + "method": "PUT", + "url": "{{baseUrl}}/api/v1/stores/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"storeName\": \"Updated Branch\",\n \"storeAddress\": \"456 Oak St\",\n \"storePhone\": \"555-0200\"\n}" + } + } + }, + { + "name": "Delete Store", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/v1/stores/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ] + } + }, + { + "name": "Bulk Delete Stores", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/v1/stores", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"ids\": [\n 1\n ]\n}" + } + } + } + ] + }, + { + "name": "Inventory", + "item": [ + { + "name": "List Inventory", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/inventory", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ] + } + }, + { + "name": "Get Inventory Item", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/inventory/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ] + } + }, + { + "name": "Create Inventory", "request": { "method": "POST", "url": "{{baseUrl}}/api/v1/inventory", @@ -1621,99 +1638,61 @@ { "key": "Content-Type", "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" } ], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - }, "body": { "mode": "raw", - "raw": "{\n \"prodId\": \"{{prodId}}\",\n \"quantity\": 25\n}", - "options": { - "raw": { - "language": "json" - } - } + "raw": "{\n \"productId\": 1,\n \"storeId\": 1,\n \"quantity\": 100\n}" } } }, { - "name": "Get One", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/inventory/{{inventoryId}}", - "header": [], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - } - } - }, - { - "name": "Update (PUT)", + "name": "Update Inventory", "request": { "method": "PUT", - "url": "{{baseUrl}}/api/v1/inventory/{{inventoryId}}", + "url": "{{baseUrl}}/api/v1/inventory/1", "header": [ { "key": "Content-Type", "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" } ], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - }, "body": { "mode": "raw", - "raw": "{\n \"prodId\": \"{{prodId}}\",\n \"quantity\": 30\n}", - "options": { - "raw": { - "language": "json" - } - } + "raw": "{\n \"quantity\": 150\n}" } } }, { - "name": "Delete One", + "name": "Delete Inventory", "request": { "method": "DELETE", - "url": "{{baseUrl}}/api/v1/inventory/{{inventoryId}}", - "header": [], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - } + "url": "{{baseUrl}}/api/v1/inventory/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ] } }, { - "name": "Bulk Delete (DELETE body)", + "name": "Bulk Delete Inventory", "request": { "method": "DELETE", "url": "{{baseUrl}}/api/v1/inventory", @@ -1721,27 +1700,16 @@ { "key": "Content-Type", "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" } ], - "description": "Bulk delete with JSON body.", - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - }, "body": { "mode": "raw", - "raw": "{\n \"ids\": [\n 1,\n 2,\n 3\n ]\n}", - "options": { - "raw": { - "language": "json" - } - } + "raw": "{\n \"ids\": [\n 1\n ]\n}" } } } @@ -1751,26 +1719,43 @@ "name": "Suppliers", "item": [ { - "name": "List/Search", + "name": "List Suppliers", "request": { "method": "GET", - "url": "{{baseUrl}}/api/v1/suppliers?q=&page=0&size=50&sort=id,desc", - "header": [], - "description": "Returns UI-ready rows (joined fields included).", - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - } + "url": "{{baseUrl}}/api/v1/suppliers", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ] } }, { - "name": "Create", + "name": "Get Supplier", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/suppliers/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ] + } + }, + { + "name": "Create Supplier", "request": { "method": "POST", "url": "{{baseUrl}}/api/v1/suppliers", @@ -1778,99 +1763,61 @@ { "key": "Content-Type", "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" } ], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - }, "body": { "mode": "raw", - "raw": "{\n \"supName\": \"Acme Supplies\",\n \"supPhone\": \"555-0100\",\n \"supEmail\": \"sales@acme.example\",\n \"supAddress\": \"123 Main St\"\n}", - "options": { - "raw": { - "language": "json" - } - } + "raw": "{\n \"supCompany\": \"ACME Corp\",\n \"supContact\": \"John Smith\",\n \"supPhone\": \"555-0300\",\n \"supEmail\": \"john@acme.com\"\n}" } } }, { - "name": "Get One", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/suppliers/{{supId}}", - "header": [], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - } - } - }, - { - "name": "Update (PUT)", + "name": "Update Supplier", "request": { "method": "PUT", - "url": "{{baseUrl}}/api/v1/suppliers/{{supId}}", + "url": "{{baseUrl}}/api/v1/suppliers/1", "header": [ { "key": "Content-Type", "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" } ], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - }, "body": { "mode": "raw", - "raw": "{\n \"supName\": \"Acme Supplies\",\n \"supPhone\": \"555-0100\",\n \"supEmail\": \"support@acme.example\",\n \"supAddress\": \"123 Main St\"\n}", - "options": { - "raw": { - "language": "json" - } - } + "raw": "{\n \"supCompany\": \"ACME Corp\",\n \"supContact\": \"Jane Smith\",\n \"supPhone\": \"555-0300\",\n \"supEmail\": \"jane@acme.com\"\n}" } } }, { - "name": "Delete One", + "name": "Delete Supplier", "request": { "method": "DELETE", - "url": "{{baseUrl}}/api/v1/suppliers/{{supId}}", - "header": [], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - } + "url": "{{baseUrl}}/api/v1/suppliers/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ] } }, { - "name": "Bulk Delete (DELETE body)", + "name": "Bulk Delete Suppliers", "request": { "method": "DELETE", "url": "{{baseUrl}}/api/v1/suppliers", @@ -1878,55 +1825,103 @@ { "key": "Content-Type", "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" } ], - "description": "Bulk delete with JSON body.", - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - }, "body": { "mode": "raw", - "raw": "{\n \"ids\": [\n 1,\n 2,\n 3\n ]\n}", - "options": { - "raw": { - "language": "json" - } - } + "raw": "{\n \"ids\": [\n 1\n ]\n}" } } } ] }, { - "name": "Product Suppliers (Costing)", + "name": "Purchase Orders", "item": [ { - "name": "List/Search", + "name": "List Purchase Orders", "request": { "method": "GET", - "url": "{{baseUrl}}/api/v1/product-suppliers?q=&page=0&size=50", - "header": [], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - } + "url": "{{baseUrl}}/api/v1/purchase-orders", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ] } }, { - "name": "Create", + "name": "Get Purchase Order", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/purchase-orders/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ] + } + } + ] + }, + { + "name": "Product Suppliers", + "item": [ + { + "name": "List Product Suppliers", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/product-suppliers", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ] + } + }, + { + "name": "Get Product Supplier", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/product-suppliers/1/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ] + } + }, + { + "name": "Create Product Supplier", "request": { "method": "POST", "url": "{{baseUrl}}/api/v1/product-suppliers", @@ -1934,64 +1929,61 @@ { "key": "Content-Type", "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" } ], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - }, "body": { "mode": "raw", - "raw": "{\n \"prodId\": \"{{prodId}}\",\n \"supId\": \"{{supId}}\",\n \"cost\": 12.34\n}", - "options": { - "raw": { - "language": "json" - } - } + "raw": "{\n \"productId\": 1,\n \"supplierId\": 1,\n \"price\": 25.0\n}" } } }, { - "name": "Update (composite)", + "name": "Update Product Supplier", "request": { "method": "PUT", - "url": "{{baseUrl}}/api/v1/product-suppliers", + "url": "{{baseUrl}}/api/v1/product-suppliers/1/1", "header": [ { "key": "Content-Type", "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" } ], - "description": "Update by composite key (prodId, supId).", - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - }, "body": { "mode": "raw", - "raw": "{\n \"prodId\": \"{{prodId}}\",\n \"supId\": \"{{supId}}\",\n \"cost\": 13.0\n}", - "options": { - "raw": { - "language": "json" - } - } + "raw": "{\n \"price\": 30.0\n}" } } }, { - "name": "Bulk Delete (keys)", + "name": "Delete Product Supplier", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/v1/product-suppliers/1/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ] + } + }, + { + "name": "Bulk Delete Product Suppliers", "request": { "method": "DELETE", "url": "{{baseUrl}}/api/v1/product-suppliers", @@ -1999,595 +1991,167 @@ { "key": "Content-Type", "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" } ], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - }, "body": { "mode": "raw", - "raw": "{\n \"keys\": [\n {\n \"prodId\": 1,\n \"supId\": 2\n },\n {\n \"prodId\": 3,\n \"supId\": 4\n }\n ]\n}", - "options": { - "raw": { - "language": "json" - } - } + "raw": "{\n \"ids\": []\n}" } } } ] }, { - "name": "Purchase Orders (read)", + "name": "Products Admin", "item": [ { - "name": "List/Search", + "name": "Delete Product", "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/purchase-orders?q=&page=0&size=50", - "header": [], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - } + "method": "DELETE", + "url": "{{baseUrl}}/api/v1/products/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ] } }, { - "name": "Get One", + "name": "Bulk Delete Products", "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/purchase-orders/{{purchaseOrderId}}", - "header": [], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] + "method": "DELETE", + "url": "{{baseUrl}}/api/v1/products", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"ids\": [\n 1\n ]\n}" } } } ] }, { - "name": "Users (Staff Accounts)", + "name": "Appointments Admin", "item": [ { - "name": "List/Search", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/users?q=&page=0&size=50", - "header": [], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - } - } - }, - { - "name": "Create", - "request": { - "method": "POST", - "url": "{{baseUrl}}/api/v1/users", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "description": "Admin-only.", - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - }, - "body": { - "mode": "raw", - "raw": "{\n \"firstName\": \"Sam\",\n \"lastName\": \"Lee\",\n \"email\": \"sam.lee@example.com\",\n \"phone\": \"555-0110\",\n \"username\": \"samlee\",\n \"password\": \"ChangeMe123\",\n \"role\": \"STAFF\",\n \"storeIds\": [\n \"{{storeId}}\"\n ]\n}", - "options": { - "raw": { - "language": "json" - } - } - } - } - }, - { - "name": "Update (PUT)", - "request": { - "method": "PUT", - "url": "{{baseUrl}}/api/v1/users/{{userId}}", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - }, - "body": { - "mode": "raw", - "raw": "{\n \"phone\": \"555-0111\",\n \"role\": \"STAFF\"\n}", - "options": { - "raw": { - "language": "json" - } - } - } - } - }, - { - "name": "Delete One", + "name": "Bulk Delete Appointments", "request": { "method": "DELETE", - "url": "{{baseUrl}}/api/v1/users/{{userId}}", - "header": [], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - } - } - }, - { - "name": "Bulk Delete", - "request": { - "method": "DELETE", - "url": "{{baseUrl}}/api/v1/users", + "url": "{{baseUrl}}/api/v1/appointments", "header": [ { "key": "Content-Type", "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" } ], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - }, "body": { "mode": "raw", - "raw": "{\n \"ids\": [\n 1,\n 2,\n 3\n ]\n}", - "options": { - "raw": { - "language": "json" - } - } + "raw": "{\n \"ids\": [\n 1\n ]\n}" } } - }, - { - "name": "Upload Avatar for User (Admin) [multipart]", - "request": { - "method": "PUT", - "url": "{{baseUrl}}/api/v1/users/{{userId}}/avatar", - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - }, - "header": [], - "body": { - "mode": "formdata", - "formdata": [ - { - "key": "file", - "type": "file", - "src": "" - } - ] - }, - "description": "Admin-only." - } } ] - } - ] - }, - { - "name": "Sales + Refunds (Staff)", - "item": [ - { - "name": "Sales - List/Search", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/sales?q=&page=0&size=50", - "header": [], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - } - } }, { - "name": "Sales - Create (Checkout)", - "request": { - "method": "POST", - "url": "{{baseUrl}}/api/v1/sales", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - }, - "body": { - "mode": "raw", - "raw": "{\n \"paymentMethod\": \"Cash\",\n \"storeId\": \"{{storeId}}\",\n \"items\": [\n {\n \"prodId\": \"{{prodId}}\",\n \"quantity\": 2\n }\n ]\n}", - "options": { - "raw": { - "language": "json" + "name": "Adoptions Admin", + "item": [ + { + "name": "Bulk Delete Adoptions", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/v1/adoptions", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"ids\": [\n 1\n ]\n}" } } } - } + ] }, { - "name": "Sales - Get Detail", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/sales/{{saleId}}", - "header": [], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - } - } - }, - { - "name": "Sales - Refund", - "request": { - "method": "POST", - "url": "{{baseUrl}}/api/v1/sales/{{saleId}}/refunds", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - }, - "body": { - "mode": "raw", - "raw": "{\n \"paymentMethod\": \"Card\",\n \"storeId\": \"{{storeId}}\",\n \"items\": [\n {\n \"prodId\": \"{{prodId}}\",\n \"quantity\": 1\n }\n ]\n}", - "options": { - "raw": { - "language": "json" - } + "name": "Refunds Admin", + "item": [ + { + "name": "Delete Refund", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/v1/refunds/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ] } } - } - } - ] - }, - { - "name": "Analytics (Admin)", - "item": [ + ] + }, { - "name": "Dashboard", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/analytics/dashboard?days=30&top=10", - "header": [], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - } - } - } - ] - }, - { - "name": "Chat (REST) + WebSocket (/ws)", - "item": [ - { - "name": "Chat - Create Room (Customer)", - "request": { - "method": "POST", - "url": "{{baseUrl}}/api/v1/chat/rooms", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{customerToken}}", - "type": "string" - } - ] - }, - "body": { - "mode": "raw", - "raw": "{\n \"topic\": \"Support\"\n}", - "options": { - "raw": { - "language": "json" - } + "name": "Dropdowns Admin", + "item": [ + { + "name": "Get Suppliers Dropdown", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/dropdowns/suppliers", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ] } } - } - }, - { - "name": "Chat - List Rooms (Customer)", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/chat/rooms", - "header": [], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{customerToken}}", - "type": "string" - } - ] - } - } - }, - { - "name": "Chat - List Rooms (Staff/Admin)", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/chat/rooms", - "header": [], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - } - } - }, - { - "name": "Chat - Room Messages (Customer)", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/chat/rooms/{{roomId}}/messages?limit=50", - "header": [], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{customerToken}}", - "type": "string" - } - ] - } - } - }, - { - "name": "Chat - Room Messages (Staff/Admin)", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/chat/rooms/{{roomId}}/messages?limit=50", - "header": [], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - } - } - }, - { - "name": "Chat - Close Room (Staff/Admin)", - "request": { - "method": "POST", - "url": "{{baseUrl}}/api/v1/chat/rooms/{{roomId}}/close", - "header": [], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{staffToken}}", - "type": "string" - } - ] - } - } - }, - { - "name": "WebSocket Notes", - "request": { - "method": "GET", - "url": "{{baseUrl}}/ws", - "header": [], - "description": "WebSocket endpoint for live chat." - } - } - ] - }, - { - "name": "Public Assets", - "item": [ - { - "name": "Get Staff Avatar (public)", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/staff/{{employeeId}}/avatar", - "header": [], - "description": "Public endpoint. Accessible without auth." - } - } - ] - }, - { - "name": "Customer Browse (optional)", - "item": [ - { - "name": "Pets - List (customer)", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/pets?q=&page=0&size=50", - "header": [], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{customerToken}}", - "type": "string" - } - ] - } - } - }, - { - "name": "Pets - Detail (customer)", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/pets/{{petId}}", - "header": [], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{customerToken}}", - "type": "string" - } - ] - } - } - }, - { - "name": "Services - List (customer)", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/services?q=&page=0&size=50", - "header": [], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{customerToken}}", - "type": "string" - } - ] - } - } - }, - { - "name": "Stores - List (customer)", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/stores?q=&page=0&size=50", - "header": [], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{customerToken}}", - "type": "string" - } - ] - } - } + ] } ] } ] -} \ No newline at end of file +} From 4394e96329aa016025109088c3cabfefa99faa55 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 8 Mar 2026 15:38:52 -0600 Subject: [PATCH 09/14] Filter appointments and adoptions by customer --- .../controller/AdoptionController.java | 22 ++++++++++++++-- .../controller/AppointmentController.java | 22 ++++++++++++++-- .../repository/AdoptionRepository.java | 8 ++++++ .../repository/AppointmentRepository.java | 9 +++++++ .../backend/service/AdoptionService.java | 25 +++++++++++++++---- .../backend/service/AppointmentService.java | 25 +++++++++++++++---- 6 files changed, 97 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/petshop/backend/controller/AdoptionController.java b/src/main/java/com/petshop/backend/controller/AdoptionController.java index 2ff0bef7..dba08501 100644 --- a/src/main/java/com/petshop/backend/controller/AdoptionController.java +++ b/src/main/java/com/petshop/backend/controller/AdoptionController.java @@ -10,6 +10,8 @@ import org.springframework.data.domain.Pageable; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.*; @RestController @@ -27,13 +29,29 @@ public class AdoptionController { public ResponseEntity> getAllAdoptions( @RequestParam(required = false) String q, Pageable pageable) { - return ResponseEntity.ok(adoptionService.getAllAdoptions(q, pageable)); + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String role = authentication.getAuthorities().stream() + .findFirst() + .map(authority -> authority.getAuthority().replace("ROLE_", "")) + .orElse(null); + + Long customerId = role != null && role.equals("CUSTOMER") ? 1L : null; + + return ResponseEntity.ok(adoptionService.getAllAdoptions(q, pageable, customerId)); } @GetMapping("/{id}") @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") public ResponseEntity getAdoptionById(@PathVariable Long id) { - return ResponseEntity.ok(adoptionService.getAdoptionById(id)); + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String role = authentication.getAuthorities().stream() + .findFirst() + .map(authority -> authority.getAuthority().replace("ROLE_", "")) + .orElse(null); + + Long customerId = role != null && role.equals("CUSTOMER") ? 1L : null; + + return ResponseEntity.ok(adoptionService.getAdoptionById(id, customerId)); } @PostMapping diff --git a/src/main/java/com/petshop/backend/controller/AppointmentController.java b/src/main/java/com/petshop/backend/controller/AppointmentController.java index fb9fb1c6..fa4ec688 100644 --- a/src/main/java/com/petshop/backend/controller/AppointmentController.java +++ b/src/main/java/com/petshop/backend/controller/AppointmentController.java @@ -10,6 +10,8 @@ import org.springframework.data.domain.Pageable; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.*; import java.time.LocalDate; @@ -30,13 +32,29 @@ public class AppointmentController { public ResponseEntity> getAllAppointments( @RequestParam(required = false) String q, Pageable pageable) { - return ResponseEntity.ok(appointmentService.getAllAppointments(q, pageable)); + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String role = authentication.getAuthorities().stream() + .findFirst() + .map(authority -> authority.getAuthority().replace("ROLE_", "")) + .orElse(null); + + Long customerId = role != null && role.equals("CUSTOMER") ? 1L : null; + + return ResponseEntity.ok(appointmentService.getAllAppointments(q, pageable, customerId)); } @GetMapping("/{id}") @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") public ResponseEntity getAppointmentById(@PathVariable Long id) { - return ResponseEntity.ok(appointmentService.getAppointmentById(id)); + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String role = authentication.getAuthorities().stream() + .findFirst() + .map(authority -> authority.getAuthority().replace("ROLE_", "")) + .orElse(null); + + Long customerId = role != null && role.equals("CUSTOMER") ? 1L : null; + + return ResponseEntity.ok(appointmentService.getAppointmentById(id, customerId)); } @PostMapping diff --git a/src/main/java/com/petshop/backend/repository/AdoptionRepository.java b/src/main/java/com/petshop/backend/repository/AdoptionRepository.java index 3dd488d6..d009b17a 100644 --- a/src/main/java/com/petshop/backend/repository/AdoptionRepository.java +++ b/src/main/java/com/petshop/backend/repository/AdoptionRepository.java @@ -16,4 +16,12 @@ public interface AdoptionRepository extends JpaRepository { "LOWER(a.customer.lastName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + "LOWER(a.pet.petName) LIKE LOWER(CONCAT('%', :q, '%'))") Page searchAdoptions(@Param("q") String query, Pageable pageable); + + Page findByCustomerCustomerId(Long customerId, Pageable pageable); + + @Query("SELECT a FROM Adoption a WHERE a.customer.customerId = :customerId AND (" + + "LOWER(a.customer.firstName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + + "LOWER(a.customer.lastName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + + "LOWER(a.pet.petName) LIKE LOWER(CONCAT('%', :q, '%')))") + Page searchAdoptionsByCustomer(@Param("customerId") Long customerId, @Param("q") String query, Pageable pageable); } diff --git a/src/main/java/com/petshop/backend/repository/AppointmentRepository.java b/src/main/java/com/petshop/backend/repository/AppointmentRepository.java index 779bee9a..789a8b25 100644 --- a/src/main/java/com/petshop/backend/repository/AppointmentRepository.java +++ b/src/main/java/com/petshop/backend/repository/AppointmentRepository.java @@ -27,4 +27,13 @@ public interface AppointmentRepository extends JpaRepository "LOWER(a.service.serviceName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + "LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%'))") Page searchAppointments(@Param("q") String query, Pageable pageable); + + Page findByCustomerCustomerId(Long customerId, Pageable pageable); + + @Query("SELECT DISTINCT a FROM Appointment a LEFT JOIN a.pets p WHERE a.customer.customerId = :customerId AND (" + + "LOWER(a.customer.firstName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + + "LOWER(a.customer.lastName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + + "LOWER(a.service.serviceName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + + "LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%')))") + Page searchAppointmentsByCustomer(@Param("customerId") Long customerId, @Param("q") String query, Pageable pageable); } diff --git a/src/main/java/com/petshop/backend/service/AdoptionService.java b/src/main/java/com/petshop/backend/service/AdoptionService.java index fea336cd..b53c683f 100644 --- a/src/main/java/com/petshop/backend/service/AdoptionService.java +++ b/src/main/java/com/petshop/backend/service/AdoptionService.java @@ -28,19 +28,34 @@ public class AdoptionService { this.customerRepository = customerRepository; } - public Page getAllAdoptions(String query, Pageable pageable) { + public Page getAllAdoptions(String query, Pageable pageable, Long customerId) { Page adoptions; - if (query != null && !query.trim().isEmpty()) { - adoptions = adoptionRepository.searchAdoptions(query, pageable); + + if (customerId != null) { + if (query != null && !query.trim().isEmpty()) { + adoptions = adoptionRepository.searchAdoptionsByCustomer(customerId, query, pageable); + } else { + adoptions = adoptionRepository.findByCustomerCustomerId(customerId, pageable); + } } else { - adoptions = adoptionRepository.findAll(pageable); + if (query != null && !query.trim().isEmpty()) { + adoptions = adoptionRepository.searchAdoptions(query, pageable); + } else { + adoptions = adoptionRepository.findAll(pageable); + } } + return adoptions.map(this::mapToResponse); } - public AdoptionResponse getAdoptionById(Long id) { + public AdoptionResponse getAdoptionById(Long id, Long customerId) { Adoption adoption = adoptionRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException("Adoption not found with id: " + id)); + + if (customerId != null && !adoption.getCustomer().getCustomerId().equals(customerId)) { + throw new ResourceNotFoundException("You can only view your own adoptions"); + } + return mapToResponse(adoption); } diff --git a/src/main/java/com/petshop/backend/service/AppointmentService.java b/src/main/java/com/petshop/backend/service/AppointmentService.java index 1fae451c..ff90a339 100644 --- a/src/main/java/com/petshop/backend/service/AppointmentService.java +++ b/src/main/java/com/petshop/backend/service/AppointmentService.java @@ -39,19 +39,34 @@ public class AppointmentService { this.petRepository = petRepository; } - public Page getAllAppointments(String query, Pageable pageable) { + public Page getAllAppointments(String query, Pageable pageable, Long customerId) { Page appointments; - if (query != null && !query.trim().isEmpty()) { - appointments = appointmentRepository.searchAppointments(query, pageable); + + if (customerId != null) { + if (query != null && !query.trim().isEmpty()) { + appointments = appointmentRepository.searchAppointmentsByCustomer(customerId, query, pageable); + } else { + appointments = appointmentRepository.findByCustomerCustomerId(customerId, pageable); + } } else { - appointments = appointmentRepository.findAll(pageable); + if (query != null && !query.trim().isEmpty()) { + appointments = appointmentRepository.searchAppointments(query, pageable); + } else { + appointments = appointmentRepository.findAll(pageable); + } } + return appointments.map(this::mapToResponse); } - public AppointmentResponse getAppointmentById(Long id) { + public AppointmentResponse getAppointmentById(Long id, Long customerId) { Appointment appointment = appointmentRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException("Appointment not found with id: " + id)); + + if (customerId != null && !appointment.getCustomer().getCustomerId().equals(customerId)) { + throw new ResourceNotFoundException("You can only view your own appointments"); + } + return mapToResponse(appointment); } From d86652b462970b2ac6d712ef0454277538937ccd Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 8 Mar 2026 15:39:50 -0600 Subject: [PATCH 10/14] Reorganize Postman collection by resource --- petshop-api.postman_collection.json | 3711 +++++++++++++-------------- 1 file changed, 1828 insertions(+), 1883 deletions(-) diff --git a/petshop-api.postman_collection.json b/petshop-api.postman_collection.json index 50d4fa3a..340eeb2c 100644 --- a/petshop-api.postman_collection.json +++ b/petshop-api.postman_collection.json @@ -25,7 +25,7 @@ ], "item": [ { - "name": "1 Public Endpoints", + "name": "Health", "item": [ { "name": "Health Check", @@ -39,237 +39,11 @@ } ] } - }, - { - "name": "Get All Pets", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/pets", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ] - } - }, - { - "name": "Get Pet by ID", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/pets/1", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ] - } - }, - { - "name": "Get All Products", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/products", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ] - } - }, - { - "name": "Get Product by ID", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/products/1", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ] - } - }, - { - "name": "Get All Sales", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/sales", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ] - } - }, - { - "name": "Get Sale by ID", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/sales/1", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ] - } - }, - { - "name": "Get All Services", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/services", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ] - } - }, - { - "name": "Get Service by ID", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/services/1", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ] - } - }, - { - "name": "Get All Categories", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/categories", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ] - } - }, - { - "name": "Get Category by ID", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/categories/1", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ] - } - }, - { - "name": "Check Appointment Availability", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/appointments/availability?storeId=1&serviceId=1&date=2026-03-15", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ] - } - }, - { - "name": "Dropdowns", - "item": [ - { - "name": "Get Pets Dropdown", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/dropdowns/pets", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ] - } - }, - { - "name": "Get Customers Dropdown", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/dropdowns/customers", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ] - } - }, - { - "name": "Get Services Dropdown", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/dropdowns/services", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ] - } - }, - { - "name": "Get Products Dropdown", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/dropdowns/products", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ] - } - }, - { - "name": "Get Categories Dropdown", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/dropdowns/categories", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ] - } - }, - { - "name": "Get Stores Dropdown", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/dropdowns/stores", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ] - } - } - ] } ] }, { - "name": "2 Authentication", + "name": "Authentication", "item": [ { "name": "Register Customer", @@ -433,894 +207,1876 @@ ] }, { - "name": "3 Customer Features", + "name": "Pets", "item": [ { - "name": "Chat", - "item": [ - { - "name": "Create Conversation", - "request": { - "method": "POST", - "url": "{{baseUrl}}/api/v1/chat/conversations", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{customerToken}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"message\": \"I need help\"\n}" - } + "name": "Get All Pets", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/pets", + "header": [ + { + "key": "Content-Type", + "value": "application/json" } - }, - { - "name": "List Conversations", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/chat/conversations", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{customerToken}}", - "type": "text" - } - ] - } - }, - { - "name": "Get Conversation", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/chat/conversations/1", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{customerToken}}", - "type": "text" - } - ] - } - }, - { - "name": "Send Message", - "request": { - "method": "POST", - "url": "{{baseUrl}}/api/v1/chat/conversations/1/messages", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{customerToken}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"content\": \"Hello\"\n}" - } - } - }, - { - "name": "Get Messages", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/chat/conversations/1/messages", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{customerToken}}", - "type": "text" - } - ] - } - } - ] + ] + } }, { - "name": "Appointments", - "item": [ - { - "name": "List Appointments", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/appointments", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{customerToken}}", - "type": "text" - } - ] + "name": "Get Pet by ID", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/pets/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" } - }, - { - "name": "Get Appointment", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/appointments/1", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{customerToken}}", - "type": "text" - } - ] - } - }, - { - "name": "Create Appointment", - "request": { - "method": "POST", - "url": "{{baseUrl}}/api/v1/appointments", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{customerToken}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"customerId\": 1,\n \"storeId\": 1,\n \"serviceId\": 1,\n \"appointmentDate\": \"2026-03-15\",\n \"appointmentTime\": \"10:00:00\",\n \"notes\": \"Test\"\n}" - } - } - } - ] + ] + } }, { - "name": "Adoptions", - "item": [ - { - "name": "List Adoptions", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/adoptions", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{customerToken}}", - "type": "text" - } - ] + "name": "Get Pets Dropdown", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/dropdowns/pets", + "header": [ + { + "key": "Content-Type", + "value": "application/json" } - }, - { - "name": "Get Adoption", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/adoptions/1", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{customerToken}}", - "type": "text" - } - ] - } - }, - { - "name": "Create Adoption", - "request": { - "method": "POST", - "url": "{{baseUrl}}/api/v1/adoptions", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{customerToken}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"petId\": 1,\n \"customerId\": 1,\n \"adoptionDate\": \"2026-03-10\"\n}" - } - } - } - ] + ] + } }, { - "name": "Refunds", - "item": [ - { - "name": "Create Refund", - "request": { - "method": "POST", - "url": "{{baseUrl}}/api/v1/refunds", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{customerToken}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"saleId\": 1,\n \"reason\": \"Defective\",\n \"amount\": 50.0\n}" - } - } - }, - { - "name": "List Refunds", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/refunds", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{customerToken}}", - "type": "text" - } - ] - } - }, - { - "name": "Get Refund", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/refunds/1", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{customerToken}}", - "type": "text" - } - ] + "name": "Create Pet", + "request": { + "method": "POST", + "url": "{{baseUrl}}/api/v1/pets", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" } + ], + "body": { + "mode": "raw", + "raw": "{\n \"petName\": \"Buddy\",\n \"petSpecies\": \"Dog\",\n \"petBreed\": \"Labrador\",\n \"petAge\": 2,\n \"petStatus\": \"Available\",\n \"petPrice\": 500.0\n}" } - ] + } + }, + { + "name": "Update Pet", + "request": { + "method": "PUT", + "url": "{{baseUrl}}/api/v1/pets/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"petName\": \"Buddy\",\n \"petSpecies\": \"Dog\",\n \"petBreed\": \"Labrador\",\n \"petAge\": 3,\n \"petStatus\": \"Available\",\n \"petPrice\": 500.0\n}" + } + } + }, + { + "name": "Delete Pet", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/v1/pets/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ] + } + }, + { + "name": "Bulk Delete Pets", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/v1/pets", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"ids\": [\n 1,\n 2\n ]\n}" + } + } } ] }, { - "name": "4 Staff Operations", + "name": "Products", "item": [ { - "name": "Pets", - "item": [ - { - "name": "Create Pet", - "request": { - "method": "POST", - "url": "{{baseUrl}}/api/v1/pets", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{staffToken}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"petName\": \"Buddy\",\n \"petSpecies\": \"Dog\",\n \"petBreed\": \"Labrador\",\n \"petAge\": 2,\n \"petStatus\": \"Available\",\n \"petPrice\": 500.0\n}" - } + "name": "Get All Products", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/products", + "header": [ + { + "key": "Content-Type", + "value": "application/json" } - }, - { - "name": "Update Pet", - "request": { - "method": "PUT", - "url": "{{baseUrl}}/api/v1/pets/1", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{staffToken}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"petName\": \"Buddy\",\n \"petSpecies\": \"Dog\",\n \"petBreed\": \"Labrador\",\n \"petAge\": 3,\n \"petStatus\": \"Available\",\n \"petPrice\": 500.0\n}" - } - } - }, - { - "name": "Delete Pet", - "request": { - "method": "DELETE", - "url": "{{baseUrl}}/api/v1/pets/1", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{staffToken}}", - "type": "text" - } - ] - } - }, - { - "name": "Bulk Delete Pets", - "request": { - "method": "DELETE", - "url": "{{baseUrl}}/api/v1/pets", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{staffToken}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"ids\": [\n 1,\n 2\n ]\n}" - } - } - } - ] + ] + } }, { - "name": "Products", - "item": [ - { - "name": "Create Product", - "request": { - "method": "POST", - "url": "{{baseUrl}}/api/v1/products", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{staffToken}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"prodName\": \"Dog Food\",\n \"categoryId\": 1,\n \"prodDesc\": \"Premium\",\n \"prodPrice\": 50.0\n}" - } + "name": "Get Product by ID", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/products/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" } - }, - { - "name": "Update Product", - "request": { - "method": "PUT", - "url": "{{baseUrl}}/api/v1/products/1", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{staffToken}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"prodName\": \"Dog Food\",\n \"categoryId\": 1,\n \"prodDesc\": \"Premium\",\n \"prodPrice\": 55.0\n}" - } - } - } - ] + ] + } }, { - "name": "Sales", - "item": [ - { - "name": "Create Sale", - "request": { - "method": "POST", - "url": "{{baseUrl}}/api/v1/sales", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{staffToken}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"employeeId\": 1,\n \"storeId\": 1,\n \"totalAmount\": 100.0,\n \"paymentMethod\": \"Card\",\n \"isRefund\": false\n}" - } + "name": "Get Products Dropdown", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/dropdowns/products", + "header": [ + { + "key": "Content-Type", + "value": "application/json" } - } - ] + ] + } }, { - "name": "Services", - "item": [ - { - "name": "Create Service", - "request": { - "method": "POST", - "url": "{{baseUrl}}/api/v1/services", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{staffToken}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"serviceName\": \"Grooming\",\n \"serviceDesc\": \"Full grooming\",\n \"servicePrice\": 75.0\n}" - } - } - }, - { - "name": "Update Service", - "request": { - "method": "PUT", - "url": "{{baseUrl}}/api/v1/services/1", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{staffToken}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"serviceName\": \"Grooming\",\n \"serviceDesc\": \"Full grooming\",\n \"servicePrice\": 80.0\n}" - } - } - }, - { - "name": "Delete Service", - "request": { - "method": "DELETE", - "url": "{{baseUrl}}/api/v1/services/1", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{staffToken}}", - "type": "text" - } - ] - } - }, - { - "name": "Bulk Delete Services", - "request": { - "method": "DELETE", - "url": "{{baseUrl}}/api/v1/services", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{staffToken}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"ids\": [\n 1\n ]\n}" - } + "name": "Create Product", + "request": { + "method": "POST", + "url": "{{baseUrl}}/api/v1/products", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" } + ], + "body": { + "mode": "raw", + "raw": "{\n \"prodName\": \"Dog Food\",\n \"categoryId\": 1,\n \"prodDesc\": \"Premium\",\n \"prodPrice\": 50.0\n}" } - ] + } }, { - "name": "Categories", - "item": [ - { - "name": "Create Category", - "request": { - "method": "POST", - "url": "{{baseUrl}}/api/v1/categories", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{staffToken}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"categoryName\": \"Dog Supplies\"\n}" - } - } - }, - { - "name": "Update Category", - "request": { - "method": "PUT", - "url": "{{baseUrl}}/api/v1/categories/1", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{staffToken}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"categoryName\": \"Dog Supplies\"\n}" - } - } - }, - { - "name": "Delete Category", - "request": { - "method": "DELETE", - "url": "{{baseUrl}}/api/v1/categories/1", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{staffToken}}", - "type": "text" - } - ] - } - }, - { - "name": "Bulk Delete Categories", - "request": { - "method": "DELETE", - "url": "{{baseUrl}}/api/v1/categories", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{staffToken}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"ids\": [\n 1\n ]\n}" - } + "name": "Update Product", + "request": { + "method": "PUT", + "url": "{{baseUrl}}/api/v1/products/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" } + ], + "body": { + "mode": "raw", + "raw": "{\n \"prodName\": \"Dog Food\",\n \"categoryId\": 1,\n \"prodDesc\": \"Premium\",\n \"prodPrice\": 55.0\n}" } - ] + } }, { - "name": "Customers", - "item": [ - { - "name": "List Customers", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/customers", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{staffToken}}", - "type": "text" - } - ] + "name": "Delete Product", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/v1/products/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" } - }, - { - "name": "Get Customer", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/customers/1", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{staffToken}}", - "type": "text" - } - ] - } - }, - { - "name": "Create Customer", - "request": { - "method": "POST", - "url": "{{baseUrl}}/api/v1/customers", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{staffToken}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"firstName\": \"John\",\n \"lastName\": \"Doe\",\n \"email\": \"john@example.com\",\n \"phone\": \"555-0100\",\n \"address\": \"123 Main St\"\n}" - } - } - }, - { - "name": "Update Customer", - "request": { - "method": "PUT", - "url": "{{baseUrl}}/api/v1/customers/1", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{staffToken}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"firstName\": \"John\",\n \"lastName\": \"Doe\",\n \"email\": \"john@example.com\",\n \"phone\": \"555-0100\",\n \"address\": \"123 Main St\"\n}" - } - } - }, - { - "name": "Delete Customer", - "request": { - "method": "DELETE", - "url": "{{baseUrl}}/api/v1/customers/1", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{staffToken}}", - "type": "text" - } - ] - } - }, - { - "name": "Bulk Delete Customers", - "request": { - "method": "POST", - "url": "{{baseUrl}}/api/v1/customers/bulk-delete", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{staffToken}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"ids\": [\n 1\n ]\n}" - } - } - } - ] + ] + } }, { - "name": "Appointments Management", - "item": [ - { - "name": "Update Appointment", - "request": { - "method": "PUT", - "url": "{{baseUrl}}/api/v1/appointments/1", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{staffToken}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"customerId\": 1,\n \"storeId\": 1,\n \"serviceId\": 1,\n \"appointmentDate\": \"2026-03-15\",\n \"appointmentTime\": \"14:00:00\",\n \"notes\": \"Updated\"\n}" - } - } - }, - { - "name": "Delete Appointment", - "request": { - "method": "DELETE", - "url": "{{baseUrl}}/api/v1/appointments/1", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{staffToken}}", - "type": "text" - } - ] + "name": "Bulk Delete Products", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/v1/products", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" } + ], + "body": { + "mode": "raw", + "raw": "{\n \"ids\": [\n 1\n ]\n}" } - ] - }, - { - "name": "Adoptions Management", - "item": [ - { - "name": "Update Adoption", - "request": { - "method": "PUT", - "url": "{{baseUrl}}/api/v1/adoptions/1", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{staffToken}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"petId\": 1,\n \"customerId\": 1,\n \"adoptionDate\": \"2026-03-11\"\n}" - } - } - }, - { - "name": "Delete Adoption", - "request": { - "method": "DELETE", - "url": "{{baseUrl}}/api/v1/adoptions/1", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{staffToken}}", - "type": "text" - } - ] - } - } - ] - }, - { - "name": "Refunds Management", - "item": [ - { - "name": "Update Refund", - "request": { - "method": "PUT", - "url": "{{baseUrl}}/api/v1/refunds/1", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{staffToken}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"status\": \"APPROVED\"\n}" - } - } - } - ] + } } ] }, { - "name": "5 Admin Operations", + "name": "Sales", + "item": [ + { + "name": "Get All Sales", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/sales", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ] + } + }, + { + "name": "Get Sale by ID", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/sales/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ] + } + }, + { + "name": "Create Sale", + "request": { + "method": "POST", + "url": "{{baseUrl}}/api/v1/sales", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"employeeId\": 1,\n \"storeId\": 1,\n \"totalAmount\": 100.0,\n \"paymentMethod\": \"Card\",\n \"isRefund\": false\n}" + } + } + } + ] + }, + { + "name": "Services", + "item": [ + { + "name": "Get All Services", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/services", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ] + } + }, + { + "name": "Get Service by ID", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/services/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ] + } + }, + { + "name": "Get Services Dropdown", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/dropdowns/services", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ] + } + }, + { + "name": "Create Service", + "request": { + "method": "POST", + "url": "{{baseUrl}}/api/v1/services", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"serviceName\": \"Grooming\",\n \"serviceDesc\": \"Full grooming\",\n \"servicePrice\": 75.0\n}" + } + } + }, + { + "name": "Update Service", + "request": { + "method": "PUT", + "url": "{{baseUrl}}/api/v1/services/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"serviceName\": \"Grooming\",\n \"serviceDesc\": \"Full grooming\",\n \"servicePrice\": 80.0\n}" + } + } + }, + { + "name": "Delete Service", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/v1/services/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ] + } + }, + { + "name": "Bulk Delete Services", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/v1/services", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"ids\": [\n 1\n ]\n}" + } + } + } + ] + }, + { + "name": "Categories", + "item": [ + { + "name": "Get All Categories", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/categories", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ] + } + }, + { + "name": "Get Category by ID", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/categories/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ] + } + }, + { + "name": "Get Categories Dropdown", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/dropdowns/categories", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ] + } + }, + { + "name": "Create Category", + "request": { + "method": "POST", + "url": "{{baseUrl}}/api/v1/categories", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"categoryName\": \"Dog Supplies\"\n}" + } + } + }, + { + "name": "Update Category", + "request": { + "method": "PUT", + "url": "{{baseUrl}}/api/v1/categories/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"categoryName\": \"Dog Supplies\"\n}" + } + } + }, + { + "name": "Delete Category", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/v1/categories/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ] + } + }, + { + "name": "Bulk Delete Categories", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/v1/categories", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"ids\": [\n 1\n ]\n}" + } + } + } + ] + }, + { + "name": "Appointments", + "item": [ + { + "name": "Check Appointment Availability", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/appointments/availability?storeId=1&serviceId=1&date=2026-03-15", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ] + } + }, + { + "name": "List Appointments", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/appointments", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{customerToken}}", + "type": "text" + } + ] + } + }, + { + "name": "Get Appointment", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/appointments/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{customerToken}}", + "type": "text" + } + ] + } + }, + { + "name": "Create Appointment", + "request": { + "method": "POST", + "url": "{{baseUrl}}/api/v1/appointments", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{customerToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"customerId\": 1,\n \"storeId\": 1,\n \"serviceId\": 1,\n \"appointmentDate\": \"2026-03-15\",\n \"appointmentTime\": \"10:00:00\",\n \"notes\": \"Test\"\n}" + } + } + }, + { + "name": "Update Appointment", + "request": { + "method": "PUT", + "url": "{{baseUrl}}/api/v1/appointments/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"customerId\": 1,\n \"storeId\": 1,\n \"serviceId\": 1,\n \"appointmentDate\": \"2026-03-15\",\n \"appointmentTime\": \"14:00:00\",\n \"notes\": \"Updated\"\n}" + } + } + }, + { + "name": "Delete Appointment", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/v1/appointments/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ] + } + }, + { + "name": "Bulk Delete Appointments", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/v1/appointments", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"ids\": [\n 1\n ]\n}" + } + } + } + ] + }, + { + "name": "Adoptions", + "item": [ + { + "name": "List Adoptions", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/adoptions", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{customerToken}}", + "type": "text" + } + ] + } + }, + { + "name": "Get Adoption", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/adoptions/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{customerToken}}", + "type": "text" + } + ] + } + }, + { + "name": "Create Adoption", + "request": { + "method": "POST", + "url": "{{baseUrl}}/api/v1/adoptions", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{customerToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"petId\": 1,\n \"customerId\": 1,\n \"adoptionDate\": \"2026-03-10\"\n}" + } + } + }, + { + "name": "Update Adoption", + "request": { + "method": "PUT", + "url": "{{baseUrl}}/api/v1/adoptions/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"petId\": 1,\n \"customerId\": 1,\n \"adoptionDate\": \"2026-03-11\"\n}" + } + } + }, + { + "name": "Delete Adoption", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/v1/adoptions/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ] + } + }, + { + "name": "Bulk Delete Adoptions", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/v1/adoptions", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"ids\": [\n 1\n ]\n}" + } + } + } + ] + }, + { + "name": "Refunds", + "item": [ + { + "name": "Create Refund", + "request": { + "method": "POST", + "url": "{{baseUrl}}/api/v1/refunds", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{customerToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"saleId\": 1,\n \"reason\": \"Defective\",\n \"amount\": 50.0\n}" + } + } + }, + { + "name": "List Refunds", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/refunds", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{customerToken}}", + "type": "text" + } + ] + } + }, + { + "name": "Get Refund", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/refunds/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{customerToken}}", + "type": "text" + } + ] + } + }, + { + "name": "Update Refund", + "request": { + "method": "PUT", + "url": "{{baseUrl}}/api/v1/refunds/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"status\": \"APPROVED\"\n}" + } + } + }, + { + "name": "Delete Refund", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/v1/refunds/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ] + } + } + ] + }, + { + "name": "Chat", + "item": [ + { + "name": "Create Conversation", + "request": { + "method": "POST", + "url": "{{baseUrl}}/api/v1/chat/conversations", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{customerToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"message\": \"I need help\"\n}" + } + } + }, + { + "name": "List Conversations", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/chat/conversations", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{customerToken}}", + "type": "text" + } + ] + } + }, + { + "name": "Get Conversation", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/chat/conversations/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{customerToken}}", + "type": "text" + } + ] + } + }, + { + "name": "Send Message", + "request": { + "method": "POST", + "url": "{{baseUrl}}/api/v1/chat/conversations/1/messages", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{customerToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"content\": \"Hello\"\n}" + } + } + }, + { + "name": "Get Messages", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/chat/conversations/1/messages", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{customerToken}}", + "type": "text" + } + ] + } + } + ] + }, + { + "name": "Customers", + "item": [ + { + "name": "Get Customers Dropdown", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/dropdowns/customers", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ] + } + }, + { + "name": "List Customers", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/customers", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ] + } + }, + { + "name": "Get Customer", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/customers/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ] + } + }, + { + "name": "Create Customer", + "request": { + "method": "POST", + "url": "{{baseUrl}}/api/v1/customers", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"firstName\": \"John\",\n \"lastName\": \"Doe\",\n \"email\": \"john@example.com\",\n \"phone\": \"555-0100\",\n \"address\": \"123 Main St\"\n}" + } + } + }, + { + "name": "Update Customer", + "request": { + "method": "PUT", + "url": "{{baseUrl}}/api/v1/customers/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"firstName\": \"John\",\n \"lastName\": \"Doe\",\n \"email\": \"john@example.com\",\n \"phone\": \"555-0100\",\n \"address\": \"123 Main St\"\n}" + } + } + }, + { + "name": "Delete Customer", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/v1/customers/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ] + } + }, + { + "name": "Bulk Delete Customers", + "request": { + "method": "POST", + "url": "{{baseUrl}}/api/v1/customers/bulk-delete", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"ids\": [\n 1\n ]\n}" + } + } + } + ] + }, + { + "name": "Users", + "item": [ + { + "name": "List Users", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/users", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ] + } + }, + { + "name": "Get User", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/users/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ] + } + }, + { + "name": "Create User", + "request": { + "method": "POST", + "url": "{{baseUrl}}/api/v1/users", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"username\": \"newuser\",\n \"password\": \"password123\",\n \"role\": \"STAFF\"\n}" + } + } + }, + { + "name": "Update User", + "request": { + "method": "PUT", + "url": "{{baseUrl}}/api/v1/users/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"username\": \"user1\",\n \"role\": \"STAFF\"\n}" + } + } + }, + { + "name": "Delete User", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/v1/users/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ] + } + }, + { + "name": "Bulk Delete Users", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/v1/users", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"ids\": [\n 1\n ]\n}" + } + } + } + ] + }, + { + "name": "Stores", + "item": [ + { + "name": "Get Stores Dropdown", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/dropdowns/stores", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ] + } + }, + { + "name": "List Stores", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/stores", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ] + } + }, + { + "name": "Get Store", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/stores/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ] + } + }, + { + "name": "Create Store", + "request": { + "method": "POST", + "url": "{{baseUrl}}/api/v1/stores", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"storeName\": \"New Branch\",\n \"storeAddress\": \"456 Oak St\",\n \"storePhone\": \"555-0200\"\n}" + } + } + }, + { + "name": "Update Store", + "request": { + "method": "PUT", + "url": "{{baseUrl}}/api/v1/stores/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"storeName\": \"Updated Branch\",\n \"storeAddress\": \"456 Oak St\",\n \"storePhone\": \"555-0200\"\n}" + } + } + }, + { + "name": "Delete Store", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/v1/stores/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ] + } + }, + { + "name": "Bulk Delete Stores", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/v1/stores", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"ids\": [\n 1\n ]\n}" + } + } + } + ] + }, + { + "name": "Inventory", + "item": [ + { + "name": "List Inventory", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/inventory", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ] + } + }, + { + "name": "Get Inventory Item", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/inventory/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ] + } + }, + { + "name": "Create Inventory", + "request": { + "method": "POST", + "url": "{{baseUrl}}/api/v1/inventory", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"productId\": 1,\n \"storeId\": 1,\n \"quantity\": 100\n}" + } + } + }, + { + "name": "Update Inventory", + "request": { + "method": "PUT", + "url": "{{baseUrl}}/api/v1/inventory/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"quantity\": 150\n}" + } + } + }, + { + "name": "Delete Inventory", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/v1/inventory/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ] + } + }, + { + "name": "Bulk Delete Inventory", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/v1/inventory", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"ids\": [\n 1\n ]\n}" + } + } + } + ] + }, + { + "name": "Suppliers", + "item": [ + { + "name": "List Suppliers", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/suppliers", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ] + } + }, + { + "name": "Get Supplier", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/suppliers/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ] + } + }, + { + "name": "Create Supplier", + "request": { + "method": "POST", + "url": "{{baseUrl}}/api/v1/suppliers", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"supCompany\": \"ACME Corp\",\n \"supContact\": \"John Smith\",\n \"supPhone\": \"555-0300\",\n \"supEmail\": \"john@acme.com\"\n}" + } + } + }, + { + "name": "Update Supplier", + "request": { + "method": "PUT", + "url": "{{baseUrl}}/api/v1/suppliers/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"supCompany\": \"ACME Corp\",\n \"supContact\": \"Jane Smith\",\n \"supPhone\": \"555-0300\",\n \"supEmail\": \"jane@acme.com\"\n}" + } + } + }, + { + "name": "Delete Supplier", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/v1/suppliers/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ] + } + }, + { + "name": "Bulk Delete Suppliers", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/v1/suppliers", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"ids\": [\n 1\n ]\n}" + } + } + }, + { + "name": "Get Suppliers Dropdown", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/dropdowns/suppliers", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ] + } + } + ] + }, + { + "name": "Purchase Orders", + "item": [ + { + "name": "List Purchase Orders", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/purchase-orders", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ] + } + }, + { + "name": "Get Purchase Order", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/purchase-orders/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ] + } + } + ] + }, + { + "name": "Product-Suppliers", + "item": [ + { + "name": "List Product Suppliers", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/product-suppliers", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ] + } + }, + { + "name": "Get Product Supplier", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/product-suppliers/1/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ] + } + }, + { + "name": "Create Product Supplier", + "request": { + "method": "POST", + "url": "{{baseUrl}}/api/v1/product-suppliers", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"productId\": 1,\n \"supplierId\": 1,\n \"price\": 25.0\n}" + } + } + }, + { + "name": "Update Product Supplier", + "request": { + "method": "PUT", + "url": "{{baseUrl}}/api/v1/product-suppliers/1/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"price\": 30.0\n}" + } + } + }, + { + "name": "Delete Product Supplier", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/v1/product-suppliers/1/1", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ] + } + }, + { + "name": "Bulk Delete Product Suppliers", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/v1/product-suppliers", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"ids\": []\n}" + } + } + } + ] + }, + { + "name": "Analytics", "item": [ { "name": "Analytics Dashboard", @@ -1339,819 +2095,8 @@ } ] } - }, - { - "name": "Users", - "item": [ - { - "name": "List Users", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/users", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{adminToken}}", - "type": "text" - } - ] - } - }, - { - "name": "Get User", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/users/1", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{adminToken}}", - "type": "text" - } - ] - } - }, - { - "name": "Create User", - "request": { - "method": "POST", - "url": "{{baseUrl}}/api/v1/users", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{adminToken}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"username\": \"newuser\",\n \"password\": \"password123\",\n \"role\": \"STAFF\"\n}" - } - } - }, - { - "name": "Update User", - "request": { - "method": "PUT", - "url": "{{baseUrl}}/api/v1/users/1", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{adminToken}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"username\": \"user1\",\n \"role\": \"STAFF\"\n}" - } - } - }, - { - "name": "Delete User", - "request": { - "method": "DELETE", - "url": "{{baseUrl}}/api/v1/users/1", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{adminToken}}", - "type": "text" - } - ] - } - }, - { - "name": "Bulk Delete Users", - "request": { - "method": "DELETE", - "url": "{{baseUrl}}/api/v1/users", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{adminToken}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"ids\": [\n 1\n ]\n}" - } - } - } - ] - }, - { - "name": "Stores", - "item": [ - { - "name": "List Stores", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/stores", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{adminToken}}", - "type": "text" - } - ] - } - }, - { - "name": "Get Store", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/stores/1", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{adminToken}}", - "type": "text" - } - ] - } - }, - { - "name": "Create Store", - "request": { - "method": "POST", - "url": "{{baseUrl}}/api/v1/stores", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{adminToken}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"storeName\": \"New Branch\",\n \"storeAddress\": \"456 Oak St\",\n \"storePhone\": \"555-0200\"\n}" - } - } - }, - { - "name": "Update Store", - "request": { - "method": "PUT", - "url": "{{baseUrl}}/api/v1/stores/1", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{adminToken}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"storeName\": \"Updated Branch\",\n \"storeAddress\": \"456 Oak St\",\n \"storePhone\": \"555-0200\"\n}" - } - } - }, - { - "name": "Delete Store", - "request": { - "method": "DELETE", - "url": "{{baseUrl}}/api/v1/stores/1", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{adminToken}}", - "type": "text" - } - ] - } - }, - { - "name": "Bulk Delete Stores", - "request": { - "method": "DELETE", - "url": "{{baseUrl}}/api/v1/stores", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{adminToken}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"ids\": [\n 1\n ]\n}" - } - } - } - ] - }, - { - "name": "Inventory", - "item": [ - { - "name": "List Inventory", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/inventory", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{adminToken}}", - "type": "text" - } - ] - } - }, - { - "name": "Get Inventory Item", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/inventory/1", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{adminToken}}", - "type": "text" - } - ] - } - }, - { - "name": "Create Inventory", - "request": { - "method": "POST", - "url": "{{baseUrl}}/api/v1/inventory", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{adminToken}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"productId\": 1,\n \"storeId\": 1,\n \"quantity\": 100\n}" - } - } - }, - { - "name": "Update Inventory", - "request": { - "method": "PUT", - "url": "{{baseUrl}}/api/v1/inventory/1", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{adminToken}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"quantity\": 150\n}" - } - } - }, - { - "name": "Delete Inventory", - "request": { - "method": "DELETE", - "url": "{{baseUrl}}/api/v1/inventory/1", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{adminToken}}", - "type": "text" - } - ] - } - }, - { - "name": "Bulk Delete Inventory", - "request": { - "method": "DELETE", - "url": "{{baseUrl}}/api/v1/inventory", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{adminToken}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"ids\": [\n 1\n ]\n}" - } - } - } - ] - }, - { - "name": "Suppliers", - "item": [ - { - "name": "List Suppliers", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/suppliers", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{adminToken}}", - "type": "text" - } - ] - } - }, - { - "name": "Get Supplier", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/suppliers/1", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{adminToken}}", - "type": "text" - } - ] - } - }, - { - "name": "Create Supplier", - "request": { - "method": "POST", - "url": "{{baseUrl}}/api/v1/suppliers", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{adminToken}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"supCompany\": \"ACME Corp\",\n \"supContact\": \"John Smith\",\n \"supPhone\": \"555-0300\",\n \"supEmail\": \"john@acme.com\"\n}" - } - } - }, - { - "name": "Update Supplier", - "request": { - "method": "PUT", - "url": "{{baseUrl}}/api/v1/suppliers/1", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{adminToken}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"supCompany\": \"ACME Corp\",\n \"supContact\": \"Jane Smith\",\n \"supPhone\": \"555-0300\",\n \"supEmail\": \"jane@acme.com\"\n}" - } - } - }, - { - "name": "Delete Supplier", - "request": { - "method": "DELETE", - "url": "{{baseUrl}}/api/v1/suppliers/1", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{adminToken}}", - "type": "text" - } - ] - } - }, - { - "name": "Bulk Delete Suppliers", - "request": { - "method": "DELETE", - "url": "{{baseUrl}}/api/v1/suppliers", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{adminToken}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"ids\": [\n 1\n ]\n}" - } - } - } - ] - }, - { - "name": "Purchase Orders", - "item": [ - { - "name": "List Purchase Orders", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/purchase-orders", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{adminToken}}", - "type": "text" - } - ] - } - }, - { - "name": "Get Purchase Order", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/purchase-orders/1", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{adminToken}}", - "type": "text" - } - ] - } - } - ] - }, - { - "name": "Product Suppliers", - "item": [ - { - "name": "List Product Suppliers", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/product-suppliers", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{adminToken}}", - "type": "text" - } - ] - } - }, - { - "name": "Get Product Supplier", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/product-suppliers/1/1", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{adminToken}}", - "type": "text" - } - ] - } - }, - { - "name": "Create Product Supplier", - "request": { - "method": "POST", - "url": "{{baseUrl}}/api/v1/product-suppliers", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{adminToken}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"productId\": 1,\n \"supplierId\": 1,\n \"price\": 25.0\n}" - } - } - }, - { - "name": "Update Product Supplier", - "request": { - "method": "PUT", - "url": "{{baseUrl}}/api/v1/product-suppliers/1/1", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{adminToken}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"price\": 30.0\n}" - } - } - }, - { - "name": "Delete Product Supplier", - "request": { - "method": "DELETE", - "url": "{{baseUrl}}/api/v1/product-suppliers/1/1", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{adminToken}}", - "type": "text" - } - ] - } - }, - { - "name": "Bulk Delete Product Suppliers", - "request": { - "method": "DELETE", - "url": "{{baseUrl}}/api/v1/product-suppliers", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{adminToken}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"ids\": []\n}" - } - } - } - ] - }, - { - "name": "Products Admin", - "item": [ - { - "name": "Delete Product", - "request": { - "method": "DELETE", - "url": "{{baseUrl}}/api/v1/products/1", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{adminToken}}", - "type": "text" - } - ] - } - }, - { - "name": "Bulk Delete Products", - "request": { - "method": "DELETE", - "url": "{{baseUrl}}/api/v1/products", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{adminToken}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"ids\": [\n 1\n ]\n}" - } - } - } - ] - }, - { - "name": "Appointments Admin", - "item": [ - { - "name": "Bulk Delete Appointments", - "request": { - "method": "DELETE", - "url": "{{baseUrl}}/api/v1/appointments", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{adminToken}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"ids\": [\n 1\n ]\n}" - } - } - } - ] - }, - { - "name": "Adoptions Admin", - "item": [ - { - "name": "Bulk Delete Adoptions", - "request": { - "method": "DELETE", - "url": "{{baseUrl}}/api/v1/adoptions", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{adminToken}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"ids\": [\n 1\n ]\n}" - } - } - } - ] - }, - { - "name": "Refunds Admin", - "item": [ - { - "name": "Delete Refund", - "request": { - "method": "DELETE", - "url": "{{baseUrl}}/api/v1/refunds/1", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{adminToken}}", - "type": "text" - } - ] - } - } - ] - }, - { - "name": "Dropdowns Admin", - "item": [ - { - "name": "Get Suppliers Dropdown", - "request": { - "method": "GET", - "url": "{{baseUrl}}/api/v1/dropdowns/suppliers", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{adminToken}}", - "type": "text" - } - ] - } - } - ] } ] } ] -} +} \ No newline at end of file From a0d14e493f96414202b72ef7e665900df7f66124 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 8 Mar 2026 21:56:04 -0600 Subject: [PATCH 11/14] Fix backend user contract and add User-Employee-Customer linkage - Add active field to User entity and users table - Add userId linkage to Employee and Customer entities with unique constraints and FKs - Add repository methods findByUserId and findAllByEmail - Create UserBusinessLinkageService for shared employee/customer creation logic - Create AuthenticationHelper utility for resolving authenticated users - Update UserService to persist all user fields and create linked business entities - Update AuthController register to set active and create linked customer - Update DataInitializer to be idempotent and use shared linkage service - Update Postman collection user endpoints with fullName, email, and active --- petshop-api.postman_collection.json | 4 +- .../backend/config/DataInitializer.java | 101 +++++++++++-- .../backend/controller/AuthController.java | 9 +- .../com/petshop/backend/entity/Customer.java | 15 +- .../com/petshop/backend/entity/Employee.java | 15 +- .../java/com/petshop/backend/entity/User.java | 15 +- .../repository/CustomerRepository.java | 6 + .../repository/EmployeeRepository.java | 5 + .../service/UserBusinessLinkageService.java | 138 ++++++++++++++++++ .../petshop/backend/service/UserService.java | 23 ++- .../backend/util/AuthenticationHelper.java | 38 +++++ src/main/resources/schema.sql | 13 +- 12 files changed, 363 insertions(+), 19 deletions(-) create mode 100644 src/main/java/com/petshop/backend/service/UserBusinessLinkageService.java create mode 100644 src/main/java/com/petshop/backend/util/AuthenticationHelper.java diff --git a/petshop-api.postman_collection.json b/petshop-api.postman_collection.json index 340eeb2c..abc9124c 100644 --- a/petshop-api.postman_collection.json +++ b/petshop-api.postman_collection.json @@ -1435,7 +1435,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"username\": \"newuser\",\n \"password\": \"password123\",\n \"role\": \"STAFF\"\n}" + "raw": "{\n \"username\": \"newuser\",\n \"password\": \"password123\",\n \"fullName\": \"New User\",\n \"email\": \"newuser@petshop.com\",\n \"role\": \"STAFF\",\n \"active\": true\n}" } } }, @@ -1457,7 +1457,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"username\": \"user1\",\n \"role\": \"STAFF\"\n}" + "raw": "{\n \"username\": \"user1\",\n \"password\": \"newpassword123\",\n \"fullName\": \"Updated User\",\n \"email\": \"user1@petshop.com\",\n \"role\": \"STAFF\",\n \"active\": true\n}" } } }, diff --git a/src/main/java/com/petshop/backend/config/DataInitializer.java b/src/main/java/com/petshop/backend/config/DataInitializer.java index 0d9c9395..a9dd4b15 100644 --- a/src/main/java/com/petshop/backend/config/DataInitializer.java +++ b/src/main/java/com/petshop/backend/config/DataInitializer.java @@ -2,6 +2,7 @@ package com.petshop.backend.config; import com.petshop.backend.entity.User; import com.petshop.backend.repository.UserRepository; +import com.petshop.backend.service.UserBusinessLinkageService; import org.springframework.boot.CommandLineRunner; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Component; @@ -11,57 +12,137 @@ public class DataInitializer implements CommandLineRunner { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; + private final UserBusinessLinkageService userBusinessLinkageService; - public DataInitializer(UserRepository userRepository, PasswordEncoder passwordEncoder) { + public DataInitializer(UserRepository userRepository, PasswordEncoder passwordEncoder, UserBusinessLinkageService userBusinessLinkageService) { this.userRepository = userRepository; this.passwordEncoder = passwordEncoder; + this.userBusinessLinkageService = userBusinessLinkageService; } @Override public void run(String... args) { System.out.println("==== DataInitializer: Starting user creation ===="); - if (userRepository.findByUsername("admin").isEmpty()) { + User admin = userRepository.findByUsername("admin").orElse(null); + if (admin == null) { System.out.println("Creating admin user..."); - User admin = new User(); + admin = new User(); admin.setUsername("admin"); admin.setPassword(passwordEncoder.encode("admin123")); admin.setEmail("admin@petshop.com"); admin.setFullName("Admin User"); admin.setRole(User.Role.ADMIN); - userRepository.save(admin); + admin.setActive(true); + admin = userRepository.save(admin); System.out.println("Admin user created successfully"); } else { System.out.println("Admin user already exists"); + // Normalize missing fields if needed + boolean updated = false; + if (admin.getFullName() == null || admin.getFullName().isEmpty()) { + admin.setFullName("Admin User"); + updated = true; + } + if (admin.getEmail() == null || admin.getEmail().isEmpty()) { + admin.setEmail("admin@petshop.com"); + updated = true; + } + if (admin.getActive() == null) { + admin.setActive(true); + updated = true; + } + if (admin.getRole() == null) { + admin.setRole(User.Role.ADMIN); + updated = true; + } + if (updated) { + admin = userRepository.save(admin); + System.out.println("Admin user normalized"); + } } + // Ensure linked employee + userBusinessLinkageService.ensureLinkedEmployee(admin); - if (userRepository.findByUsername("staff").isEmpty()) { + User staff = userRepository.findByUsername("staff").orElse(null); + if (staff == null) { System.out.println("Creating staff user..."); - User staff = new User(); + staff = new User(); staff.setUsername("staff"); staff.setPassword(passwordEncoder.encode("staff123")); staff.setEmail("staff@petshop.com"); staff.setFullName("Staff User"); staff.setRole(User.Role.STAFF); - userRepository.save(staff); + staff.setActive(true); + staff = userRepository.save(staff); System.out.println("Staff user created successfully"); } else { System.out.println("Staff user already exists"); + // Normalize missing fields if needed + boolean updated = false; + if (staff.getFullName() == null || staff.getFullName().isEmpty()) { + staff.setFullName("Staff User"); + updated = true; + } + if (staff.getEmail() == null || staff.getEmail().isEmpty()) { + staff.setEmail("staff@petshop.com"); + updated = true; + } + if (staff.getActive() == null) { + staff.setActive(true); + updated = true; + } + if (staff.getRole() == null) { + staff.setRole(User.Role.STAFF); + updated = true; + } + if (updated) { + staff = userRepository.save(staff); + System.out.println("Staff user normalized"); + } } + // Ensure linked employee + userBusinessLinkageService.ensureLinkedEmployee(staff); - if (userRepository.findByUsername("customer").isEmpty()) { + User customer = userRepository.findByUsername("customer").orElse(null); + if (customer == null) { System.out.println("Creating customer user..."); - User customer = new User(); + customer = new User(); customer.setUsername("customer"); customer.setPassword(passwordEncoder.encode("customer123")); customer.setEmail("customer@petshop.com"); customer.setFullName("Test Customer"); customer.setRole(User.Role.CUSTOMER); - userRepository.save(customer); + customer.setActive(true); + customer = userRepository.save(customer); System.out.println("Customer user created successfully"); } else { System.out.println("Customer user already exists"); + // Normalize missing fields if needed + boolean updated = false; + if (customer.getFullName() == null || customer.getFullName().isEmpty()) { + customer.setFullName("Test Customer"); + updated = true; + } + if (customer.getEmail() == null || customer.getEmail().isEmpty()) { + customer.setEmail("customer@petshop.com"); + updated = true; + } + if (customer.getActive() == null) { + customer.setActive(true); + updated = true; + } + if (customer.getRole() == null) { + customer.setRole(User.Role.CUSTOMER); + updated = true; + } + if (updated) { + customer = userRepository.save(customer); + System.out.println("Customer user normalized"); + } } + // Ensure linked customer + userBusinessLinkageService.ensureLinkedCustomer(customer); System.out.println("==== DataInitializer: Completed ===="); } diff --git a/src/main/java/com/petshop/backend/controller/AuthController.java b/src/main/java/com/petshop/backend/controller/AuthController.java index ae28d126..22f53420 100644 --- a/src/main/java/com/petshop/backend/controller/AuthController.java +++ b/src/main/java/com/petshop/backend/controller/AuthController.java @@ -10,6 +10,7 @@ import com.petshop.backend.dto.auth.UserInfoResponse; import com.petshop.backend.entity.User; import com.petshop.backend.repository.UserRepository; import com.petshop.backend.security.JwtUtil; +import com.petshop.backend.service.UserBusinessLinkageService; import jakarta.validation.Valid; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -42,12 +43,14 @@ public class AuthController { private final UserRepository userRepository; private final JwtUtil jwtUtil; private final PasswordEncoder passwordEncoder; + private final UserBusinessLinkageService userBusinessLinkageService; - public AuthController(AuthenticationManager authenticationManager, UserRepository userRepository, JwtUtil jwtUtil, PasswordEncoder passwordEncoder) { + public AuthController(AuthenticationManager authenticationManager, UserRepository userRepository, JwtUtil jwtUtil, PasswordEncoder passwordEncoder, UserBusinessLinkageService userBusinessLinkageService) { this.authenticationManager = authenticationManager; this.userRepository = userRepository; this.jwtUtil = jwtUtil; this.passwordEncoder = passwordEncoder; + this.userBusinessLinkageService = userBusinessLinkageService; } @PostMapping("/register") @@ -70,9 +73,13 @@ public class AuthController { user.setEmail(request.getEmail()); user.setFullName(request.getFullName()); user.setRole(User.Role.CUSTOMER); + user.setActive(true); User savedUser = userRepository.save(user); + // Create or link customer record + userBusinessLinkageService.ensureLinkedCustomer(savedUser); + UserDetails userDetails = new org.springframework.security.core.userdetails.User( savedUser.getUsername(), savedUser.getPassword(), diff --git a/src/main/java/com/petshop/backend/entity/Customer.java b/src/main/java/com/petshop/backend/entity/Customer.java index 661f9d80..1cfa858b 100644 --- a/src/main/java/com/petshop/backend/entity/Customer.java +++ b/src/main/java/com/petshop/backend/entity/Customer.java @@ -15,6 +15,9 @@ public class Customer { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long customerId; + @Column(name = "user_id") + private Long userId; + @Column(nullable = false, length = 50) private String firstName; @@ -38,8 +41,9 @@ public class Customer { public Customer() { } - public Customer(Long customerId, String firstName, String lastName, String email, String phone, LocalDateTime createdAt, LocalDateTime updatedAt) { + public Customer(Long customerId, Long userId, String firstName, String lastName, String email, String phone, LocalDateTime createdAt, LocalDateTime updatedAt) { this.customerId = customerId; + this.userId = userId; this.firstName = firstName; this.lastName = lastName; this.email = email; @@ -56,6 +60,14 @@ public class Customer { this.customerId = customerId; } + public Long getUserId() { + return userId; + } + + public void setUserId(Long userId) { + this.userId = userId; + } + public String getFirstName() { return firstName; } @@ -121,6 +133,7 @@ public class Customer { public String toString() { return "Customer{" + "customerId=" + customerId + + ", userId=" + userId + ", firstName='" + firstName + '\'' + ", lastName='" + lastName + '\'' + ", email='" + email + '\'' + diff --git a/src/main/java/com/petshop/backend/entity/Employee.java b/src/main/java/com/petshop/backend/entity/Employee.java index 573a446f..9e825a61 100644 --- a/src/main/java/com/petshop/backend/entity/Employee.java +++ b/src/main/java/com/petshop/backend/entity/Employee.java @@ -15,6 +15,9 @@ public class Employee { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long employeeId; + @Column(name = "user_id") + private Long userId; + @Column(nullable = false, length = 50) private String firstName; @@ -44,8 +47,9 @@ public class Employee { public Employee() { } - public Employee(Long employeeId, String firstName, String lastName, String email, String phone, String role, Boolean isActive, LocalDateTime createdAt, LocalDateTime updatedAt) { + public Employee(Long employeeId, Long userId, String firstName, String lastName, String email, String phone, String role, Boolean isActive, LocalDateTime createdAt, LocalDateTime updatedAt) { this.employeeId = employeeId; + this.userId = userId; this.firstName = firstName; this.lastName = lastName; this.email = email; @@ -64,6 +68,14 @@ public class Employee { this.employeeId = employeeId; } + public Long getUserId() { + return userId; + } + + public void setUserId(Long userId) { + this.userId = userId; + } + public String getFirstName() { return firstName; } @@ -145,6 +157,7 @@ public class Employee { public String toString() { return "Employee{" + "employeeId=" + employeeId + + ", userId=" + userId + ", firstName='" + firstName + '\'' + ", lastName='" + lastName + '\'' + ", email='" + email + '\'' + diff --git a/src/main/java/com/petshop/backend/entity/User.java b/src/main/java/com/petshop/backend/entity/User.java index 6ef37551..7a2cf43a 100644 --- a/src/main/java/com/petshop/backend/entity/User.java +++ b/src/main/java/com/petshop/backend/entity/User.java @@ -34,6 +34,9 @@ public class User { @Column(nullable = false, length = 20, columnDefinition = "VARCHAR(20)") private Role role; + @Column(nullable = false) + private Boolean active = true; + @CreationTimestamp @Column(name = "created_at", updatable = false) private LocalDateTime createdAt; @@ -49,7 +52,7 @@ public class User { public User() { } - public User(Long id, String username, String password, String email, String fullName, String avatarUrl, Role role, LocalDateTime createdAt, LocalDateTime updatedAt) { + public User(Long id, String username, String password, String email, String fullName, String avatarUrl, Role role, Boolean active, LocalDateTime createdAt, LocalDateTime updatedAt) { this.id = id; this.username = username; this.password = password; @@ -57,6 +60,7 @@ public class User { this.fullName = fullName; this.avatarUrl = avatarUrl; this.role = role; + this.active = active; this.createdAt = createdAt; this.updatedAt = updatedAt; } @@ -117,6 +121,14 @@ public class User { this.role = role; } + public Boolean getActive() { + return active; + } + + public void setActive(Boolean active) { + this.active = active; + } + public LocalDateTime getCreatedAt() { return createdAt; } @@ -156,6 +168,7 @@ public class User { ", fullName='" + fullName + '\'' + ", avatarUrl='" + avatarUrl + '\'' + ", role=" + role + + ", active=" + active + ", createdAt=" + createdAt + ", updatedAt=" + updatedAt + '}'; diff --git a/src/main/java/com/petshop/backend/repository/CustomerRepository.java b/src/main/java/com/petshop/backend/repository/CustomerRepository.java index a1885993..f4baa3f9 100644 --- a/src/main/java/com/petshop/backend/repository/CustomerRepository.java +++ b/src/main/java/com/petshop/backend/repository/CustomerRepository.java @@ -8,9 +8,15 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import java.util.List; +import java.util.Optional; + @Repository public interface CustomerRepository extends JpaRepository { + Optional findByUserId(Long userId); + List findAllByEmail(String email); + @Query("SELECT c FROM Customer c WHERE " + "LOWER(c.firstName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + "LOWER(c.lastName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + diff --git a/src/main/java/com/petshop/backend/repository/EmployeeRepository.java b/src/main/java/com/petshop/backend/repository/EmployeeRepository.java index 4c5aa9d8..bcb4b138 100644 --- a/src/main/java/com/petshop/backend/repository/EmployeeRepository.java +++ b/src/main/java/com/petshop/backend/repository/EmployeeRepository.java @@ -4,6 +4,11 @@ import com.petshop.backend.entity.Employee; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.util.List; +import java.util.Optional; + @Repository public interface EmployeeRepository extends JpaRepository { + Optional findByUserId(Long userId); + List findAllByEmail(String email); } diff --git a/src/main/java/com/petshop/backend/service/UserBusinessLinkageService.java b/src/main/java/com/petshop/backend/service/UserBusinessLinkageService.java new file mode 100644 index 00000000..81b4738f --- /dev/null +++ b/src/main/java/com/petshop/backend/service/UserBusinessLinkageService.java @@ -0,0 +1,138 @@ +package com.petshop.backend.service; + +import com.petshop.backend.entity.Customer; +import com.petshop.backend.entity.Employee; +import com.petshop.backend.entity.User; +import com.petshop.backend.repository.CustomerRepository; +import com.petshop.backend.repository.EmployeeRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +public class UserBusinessLinkageService { + + private final EmployeeRepository employeeRepository; + private final CustomerRepository customerRepository; + + @Autowired + public UserBusinessLinkageService(EmployeeRepository employeeRepository, CustomerRepository customerRepository) { + this.employeeRepository = employeeRepository; + this.customerRepository = customerRepository; + } + + @Transactional + public Employee ensureLinkedEmployee(User user) { + // Check if already linked + if (user.getId() != null) { + var existing = employeeRepository.findByUserId(user.getId()); + if (existing.isPresent()) { + return existing.get(); + } + } + + // Check for email matches + List emailMatches = employeeRepository.findAllByEmail(user.getEmail()); + + // If exactly one match exists and has no userId, link it + if (emailMatches.size() == 1) { + Employee employee = emailMatches.get(0); + if (employee.getUserId() == null) { + employee.setUserId(user.getId()); + return employeeRepository.save(employee); + } + } + + // Otherwise create a new linked Employee + Employee newEmployee = new Employee(); + newEmployee.setUserId(user.getId()); + newEmployee.setEmail(user.getEmail()); + + // Split fullName into firstName and lastName + String[] nameParts = splitFullName(user.getFullName()); + newEmployee.setFirstName(nameParts[0]); + newEmployee.setLastName(nameParts[1]); + + // Set required fields with deterministic values + newEmployee.setPhone("000-000-0000"); + newEmployee.setIsActive(true); + + // Map role based on user role + if (user.getRole() == User.Role.ADMIN) { + newEmployee.setRole("Manager"); + } else if (user.getRole() == User.Role.STAFF) { + newEmployee.setRole("Staff"); + } else { + newEmployee.setRole("Staff"); // fallback + } + + return employeeRepository.save(newEmployee); + } + + @Transactional + public Customer ensureLinkedCustomer(User user) { + // Check if already linked + if (user.getId() != null) { + var existing = customerRepository.findByUserId(user.getId()); + if (existing.isPresent()) { + return existing.get(); + } + } + + // Check for email matches + List emailMatches = customerRepository.findAllByEmail(user.getEmail()); + + // If exactly one match exists and has no userId, link it + if (emailMatches.size() == 1) { + Customer customer = emailMatches.get(0); + if (customer.getUserId() == null) { + customer.setUserId(user.getId()); + return customerRepository.save(customer); + } + } + + // Otherwise create a new linked Customer + Customer newCustomer = new Customer(); + newCustomer.setUserId(user.getId()); + newCustomer.setEmail(user.getEmail()); + + // Split fullName into firstName and lastName + String[] nameParts = splitFullName(user.getFullName()); + newCustomer.setFirstName(nameParts[0]); + newCustomer.setLastName(nameParts[1]); + + // Set required fields with deterministic values + newCustomer.setPhone("000-000-0001"); + + return customerRepository.save(newCustomer); + } + + private String[] splitFullName(String fullName) { + if (fullName == null || fullName.trim().isEmpty()) { + return new String[]{"System", "User"}; + } + + String trimmed = fullName.trim(); + int spaceIndex = trimmed.indexOf(' '); + + if (spaceIndex == -1) { + // Single token + return new String[]{trimmed, "User"}; + } + + // Multiple tokens + String firstName = trimmed.substring(0, spaceIndex).trim(); + String lastName = trimmed.substring(spaceIndex + 1).trim(); + + if (firstName.isEmpty()) { + firstName = "System"; + } + if (lastName.isEmpty()) { + lastName = "User"; + } + + return new String[]{firstName, lastName}; + } +} diff --git a/src/main/java/com/petshop/backend/service/UserService.java b/src/main/java/com/petshop/backend/service/UserService.java index 183d05d9..3e8d36a1 100644 --- a/src/main/java/com/petshop/backend/service/UserService.java +++ b/src/main/java/com/petshop/backend/service/UserService.java @@ -17,10 +17,12 @@ public class UserService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; + private final UserBusinessLinkageService userBusinessLinkageService; - public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) { + public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder, UserBusinessLinkageService userBusinessLinkageService) { this.userRepository = userRepository; this.passwordEncoder = passwordEncoder; + this.userBusinessLinkageService = userBusinessLinkageService; } public Page getAllUsers(String query, Pageable pageable) { @@ -44,9 +46,20 @@ public class UserService { User user = new User(); user.setUsername(request.getUsername()); user.setPassword(passwordEncoder.encode(request.getPassword())); + user.setFullName(request.getFullName()); + user.setEmail(request.getEmail()); user.setRole(request.getRole()); + user.setActive(request.getActive() != null ? request.getActive() : true); user = userRepository.save(user); + + // Create or link business entity based on role + if (user.getRole() == User.Role.STAFF || user.getRole() == User.Role.ADMIN) { + userBusinessLinkageService.ensureLinkedEmployee(user); + } else if (user.getRole() == User.Role.CUSTOMER) { + userBusinessLinkageService.ensureLinkedCustomer(user); + } + return mapToResponse(user); } @@ -59,7 +72,10 @@ public class UserService { if (request.getPassword() != null && !request.getPassword().trim().isEmpty()) { user.setPassword(passwordEncoder.encode(request.getPassword())); } + user.setFullName(request.getFullName()); + user.setEmail(request.getEmail()); user.setRole(request.getRole()); + user.setActive(request.getActive() != null ? request.getActive() : true); user = userRepository.save(user); return mapToResponse(user); @@ -82,7 +98,12 @@ public class UserService { UserResponse response = new UserResponse(); response.setId(user.getId()); response.setUsername(user.getUsername()); + response.setFullName(user.getFullName()); + response.setEmail(user.getEmail()); response.setRole(user.getRole().toString()); + response.setActive(user.getActive()); + response.setCreatedAt(user.getCreatedAt()); + response.setUpdatedAt(user.getUpdatedAt()); return response; } } diff --git a/src/main/java/com/petshop/backend/util/AuthenticationHelper.java b/src/main/java/com/petshop/backend/util/AuthenticationHelper.java new file mode 100644 index 00000000..26468202 --- /dev/null +++ b/src/main/java/com/petshop/backend/util/AuthenticationHelper.java @@ -0,0 +1,38 @@ +package com.petshop.backend.util; + +import com.petshop.backend.entity.Customer; +import com.petshop.backend.entity.Employee; +import com.petshop.backend.entity.User; +import com.petshop.backend.repository.CustomerRepository; +import com.petshop.backend.repository.EmployeeRepository; +import com.petshop.backend.repository.UserRepository; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; + +@Component +public class AuthenticationHelper { + + public static User getAuthenticatedUser(UserRepository userRepository) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication == null || !authentication.isAuthenticated()) { + throw new RuntimeException("No authenticated user found"); + } + + String username = authentication.getName(); + return userRepository.findByUsername(username) + .orElseThrow(() -> new RuntimeException("User not found: " + username)); + } + + public static Employee getAuthenticatedEmployee(UserRepository userRepository, EmployeeRepository employeeRepository) { + User user = getAuthenticatedUser(userRepository); + return employeeRepository.findByUserId(user.getId()) + .orElseThrow(() -> new RuntimeException("Employee record not found for user: " + user.getUsername())); + } + + public static Customer getAuthenticatedCustomer(UserRepository userRepository, CustomerRepository customerRepository) { + User user = getAuthenticatedUser(userRepository); + return customerRepository.findByUserId(user.getId()) + .orElseThrow(() -> new RuntimeException("Customer record not found for user: " + user.getUsername())); + } +} diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 76e63b85..097a7328 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -12,6 +12,7 @@ CREATE TABLE IF NOT EXISTS storeLocation ( CREATE TABLE IF NOT EXISTS employee ( employeeId BIGINT AUTO_INCREMENT PRIMARY KEY, + user_id BIGINT NULL, firstName VARCHAR(50) NOT NULL, lastName VARCHAR(50) NOT NULL, email VARCHAR(100) NOT NULL, @@ -19,7 +20,8 @@ CREATE TABLE IF NOT EXISTS employee ( role VARCHAR(50) NOT NULL, isActive BOOLEAN DEFAULT TRUE NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT uk_employee_user_id UNIQUE (user_id) ); CREATE TABLE IF NOT EXISTS employeeStore ( @@ -32,12 +34,14 @@ CREATE TABLE IF NOT EXISTS employeeStore ( CREATE TABLE IF NOT EXISTS customer ( customerId BIGINT AUTO_INCREMENT PRIMARY KEY, + user_id BIGINT NULL, firstName VARCHAR(50) NOT NULL, lastName VARCHAR(50) NOT NULL, email VARCHAR(100) NOT NULL, phone VARCHAR(20) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT uk_customer_user_id UNIQUE (user_id) ); CREATE TABLE IF NOT EXISTS pet ( @@ -201,6 +205,7 @@ CREATE TABLE IF NOT EXISTS users ( fullName VARCHAR(100), avatarUrl VARCHAR(255), role VARCHAR(20) NOT NULL, + active BOOLEAN NOT NULL DEFAULT TRUE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ); @@ -239,3 +244,7 @@ CREATE TABLE IF NOT EXISTS message ( FOREIGN KEY (conversationId) REFERENCES conversation(id), FOREIGN KEY (senderId) REFERENCES users(id) ); + +-- Add foreign keys for user_id linkage +ALTER TABLE employee ADD CONSTRAINT fk_employee_user_id FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL; +ALTER TABLE customer ADD CONSTRAINT fk_customer_user_id FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL; From a0c782f4cca6a4ab8d6bbc77d206df8e2ab43e7a Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 8 Mar 2026 21:58:50 -0600 Subject: [PATCH 12/14] Fix backend business logic and remove hardcoded IDs - Add customerId to SaleRequest DTO - Update SaleService to use AuthenticationHelper for employee attribution - Populate sale.customer when customerId provided in request - Fix AdoptionController to use authenticated customer instead of hardcoded ID 1 - Fix AppointmentController to use authenticated customer instead of hardcoded ID 1 - Fix RefundController to use authenticated customer instead of hardcoded ID 1 - Update data.sql sales to include customer linkage for refund testing - Update Postman collection sale creation with customerId and items --- petshop-api.postman_collection.json | 2 +- .../controller/AdoptionController.java | 22 ++++++++-- .../controller/AppointmentController.java | 22 ++++++++-- .../backend/controller/RefundController.java | 28 +++++++++++-- .../petshop/backend/dto/sale/SaleRequest.java | 16 ++++++- .../petshop/backend/service/SaleService.java | 17 ++++++-- src/main/resources/data.sql | 42 +++++++++---------- 7 files changed, 111 insertions(+), 38 deletions(-) diff --git a/petshop-api.postman_collection.json b/petshop-api.postman_collection.json index abc9124c..1f3aee50 100644 --- a/petshop-api.postman_collection.json +++ b/petshop-api.postman_collection.json @@ -509,7 +509,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"employeeId\": 1,\n \"storeId\": 1,\n \"totalAmount\": 100.0,\n \"paymentMethod\": \"Card\",\n \"isRefund\": false\n}" + "raw": "{\n \"storeId\": 1,\n \"paymentMethod\": \"Card\",\n \"customerId\": 1,\n \"items\": [\n {\n \"prodId\": 1,\n \"quantity\": 2\n },\n {\n \"prodId\": 2,\n \"quantity\": 1\n }\n ],\n \"isRefund\": false\n}" } } } diff --git a/src/main/java/com/petshop/backend/controller/AdoptionController.java b/src/main/java/com/petshop/backend/controller/AdoptionController.java index dba08501..17790070 100644 --- a/src/main/java/com/petshop/backend/controller/AdoptionController.java +++ b/src/main/java/com/petshop/backend/controller/AdoptionController.java @@ -3,7 +3,11 @@ package com.petshop.backend.controller; import com.petshop.backend.dto.adoption.AdoptionRequest; import com.petshop.backend.dto.adoption.AdoptionResponse; import com.petshop.backend.dto.common.BulkDeleteRequest; +import com.petshop.backend.entity.Customer; +import com.petshop.backend.repository.CustomerRepository; +import com.petshop.backend.repository.UserRepository; import com.petshop.backend.service.AdoptionService; +import com.petshop.backend.util.AuthenticationHelper; import jakarta.validation.Valid; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -19,9 +23,13 @@ import org.springframework.web.bind.annotation.*; public class AdoptionController { private final AdoptionService adoptionService; + private final UserRepository userRepository; + private final CustomerRepository customerRepository; - public AdoptionController(AdoptionService adoptionService) { + public AdoptionController(AdoptionService adoptionService, UserRepository userRepository, CustomerRepository customerRepository) { this.adoptionService = adoptionService; + this.userRepository = userRepository; + this.customerRepository = customerRepository; } @GetMapping @@ -35,7 +43,11 @@ public class AdoptionController { .map(authority -> authority.getAuthority().replace("ROLE_", "")) .orElse(null); - Long customerId = role != null && role.equals("CUSTOMER") ? 1L : null; + Long customerId = null; + if (role != null && role.equals("CUSTOMER")) { + Customer customer = AuthenticationHelper.getAuthenticatedCustomer(userRepository, customerRepository); + customerId = customer.getCustomerId(); + } return ResponseEntity.ok(adoptionService.getAllAdoptions(q, pageable, customerId)); } @@ -49,7 +61,11 @@ public class AdoptionController { .map(authority -> authority.getAuthority().replace("ROLE_", "")) .orElse(null); - Long customerId = role != null && role.equals("CUSTOMER") ? 1L : null; + Long customerId = null; + if (role != null && role.equals("CUSTOMER")) { + Customer customer = AuthenticationHelper.getAuthenticatedCustomer(userRepository, customerRepository); + customerId = customer.getCustomerId(); + } return ResponseEntity.ok(adoptionService.getAdoptionById(id, customerId)); } diff --git a/src/main/java/com/petshop/backend/controller/AppointmentController.java b/src/main/java/com/petshop/backend/controller/AppointmentController.java index fa4ec688..20e0c83d 100644 --- a/src/main/java/com/petshop/backend/controller/AppointmentController.java +++ b/src/main/java/com/petshop/backend/controller/AppointmentController.java @@ -3,7 +3,11 @@ package com.petshop.backend.controller; import com.petshop.backend.dto.appointment.AppointmentRequest; import com.petshop.backend.dto.appointment.AppointmentResponse; import com.petshop.backend.dto.common.BulkDeleteRequest; +import com.petshop.backend.entity.Customer; +import com.petshop.backend.repository.CustomerRepository; +import com.petshop.backend.repository.UserRepository; import com.petshop.backend.service.AppointmentService; +import com.petshop.backend.util.AuthenticationHelper; import jakarta.validation.Valid; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -22,9 +26,13 @@ import java.util.List; public class AppointmentController { private final AppointmentService appointmentService; + private final UserRepository userRepository; + private final CustomerRepository customerRepository; - public AppointmentController(AppointmentService appointmentService) { + public AppointmentController(AppointmentService appointmentService, UserRepository userRepository, CustomerRepository customerRepository) { this.appointmentService = appointmentService; + this.userRepository = userRepository; + this.customerRepository = customerRepository; } @GetMapping @@ -38,7 +46,11 @@ public class AppointmentController { .map(authority -> authority.getAuthority().replace("ROLE_", "")) .orElse(null); - Long customerId = role != null && role.equals("CUSTOMER") ? 1L : null; + Long customerId = null; + if (role != null && role.equals("CUSTOMER")) { + Customer customer = AuthenticationHelper.getAuthenticatedCustomer(userRepository, customerRepository); + customerId = customer.getCustomerId(); + } return ResponseEntity.ok(appointmentService.getAllAppointments(q, pageable, customerId)); } @@ -52,7 +64,11 @@ public class AppointmentController { .map(authority -> authority.getAuthority().replace("ROLE_", "")) .orElse(null); - Long customerId = role != null && role.equals("CUSTOMER") ? 1L : null; + Long customerId = null; + if (role != null && role.equals("CUSTOMER")) { + Customer customer = AuthenticationHelper.getAuthenticatedCustomer(userRepository, customerRepository); + customerId = customer.getCustomerId(); + } return ResponseEntity.ok(appointmentService.getAppointmentById(id, customerId)); } diff --git a/src/main/java/com/petshop/backend/controller/RefundController.java b/src/main/java/com/petshop/backend/controller/RefundController.java index 57dcc889..6968b9c3 100644 --- a/src/main/java/com/petshop/backend/controller/RefundController.java +++ b/src/main/java/com/petshop/backend/controller/RefundController.java @@ -3,7 +3,11 @@ package com.petshop.backend.controller; import com.petshop.backend.dto.refund.RefundRequest; import com.petshop.backend.dto.refund.RefundResponse; import com.petshop.backend.dto.refund.RefundUpdateRequest; +import com.petshop.backend.entity.Customer; +import com.petshop.backend.repository.CustomerRepository; +import com.petshop.backend.repository.UserRepository; import com.petshop.backend.service.RefundService; +import com.petshop.backend.util.AuthenticationHelper; import jakarta.validation.Valid; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -21,9 +25,13 @@ import java.util.Map; public class RefundController { private final RefundService refundService; + private final UserRepository userRepository; + private final CustomerRepository customerRepository; - public RefundController(RefundService refundService) { + public RefundController(RefundService refundService, UserRepository userRepository, CustomerRepository customerRepository) { this.refundService = refundService; + this.userRepository = userRepository; + this.customerRepository = customerRepository; } @PostMapping @@ -36,7 +44,11 @@ public class RefundController { .map(authority -> authority.getAuthority().replace("ROLE_", "")) .orElse(null); - Long customerId = role != null && role.equals("CUSTOMER") ? 1L : null; + Long customerId = null; + if (role != null && role.equals("CUSTOMER")) { + Customer customer = AuthenticationHelper.getAuthenticatedCustomer(userRepository, customerRepository); + customerId = customer.getCustomerId(); + } RefundResponse refund = refundService.createRefund(request, customerId); return ResponseEntity.status(HttpStatus.CREATED).body(refund); @@ -56,7 +68,11 @@ public class RefundController { .map(authority -> authority.getAuthority().replace("ROLE_", "")) .orElse(null); - Long customerId = role != null && role.equals("CUSTOMER") ? 1L : null; + Long customerId = null; + if (role != null && role.equals("CUSTOMER")) { + Customer customer = AuthenticationHelper.getAuthenticatedCustomer(userRepository, customerRepository); + customerId = customer.getCustomerId(); + } List refunds = refundService.getAllRefunds(customerId); return ResponseEntity.ok(refunds); @@ -72,7 +88,11 @@ public class RefundController { .map(authority -> authority.getAuthority().replace("ROLE_", "")) .orElse(null); - Long customerId = role != null && role.equals("CUSTOMER") ? 1L : null; + Long customerId = null; + if (role != null && role.equals("CUSTOMER")) { + Customer customer = AuthenticationHelper.getAuthenticatedCustomer(userRepository, customerRepository); + customerId = customer.getCustomerId(); + } RefundResponse refund = refundService.getRefundById(id, customerId); return ResponseEntity.ok(refund); diff --git a/src/main/java/com/petshop/backend/dto/sale/SaleRequest.java b/src/main/java/com/petshop/backend/dto/sale/SaleRequest.java index 6ea9ff73..9c7102f4 100644 --- a/src/main/java/com/petshop/backend/dto/sale/SaleRequest.java +++ b/src/main/java/com/petshop/backend/dto/sale/SaleRequest.java @@ -20,6 +20,8 @@ public class SaleRequest { private Long originalSaleId; + private Long customerId; + public Long getStoreId() { return storeId; } @@ -60,6 +62,14 @@ public class SaleRequest { this.originalSaleId = originalSaleId; } + public Long getCustomerId() { + return customerId; + } + + public void setCustomerId(Long customerId) { + this.customerId = customerId; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -69,12 +79,13 @@ public class SaleRequest { Objects.equals(paymentMethod, that.paymentMethod) && Objects.equals(items, that.items) && Objects.equals(isRefund, that.isRefund) && - Objects.equals(originalSaleId, that.originalSaleId); + Objects.equals(originalSaleId, that.originalSaleId) && + Objects.equals(customerId, that.customerId); } @Override public int hashCode() { - return Objects.hash(storeId, paymentMethod, items, isRefund, originalSaleId); + return Objects.hash(storeId, paymentMethod, items, isRefund, originalSaleId, customerId); } @Override @@ -85,6 +96,7 @@ public class SaleRequest { ", items=" + items + ", isRefund=" + isRefund + ", originalSaleId=" + originalSaleId + + ", customerId=" + customerId + '}'; } } diff --git a/src/main/java/com/petshop/backend/service/SaleService.java b/src/main/java/com/petshop/backend/service/SaleService.java index 65b62f3e..e0459d3f 100644 --- a/src/main/java/com/petshop/backend/service/SaleService.java +++ b/src/main/java/com/petshop/backend/service/SaleService.java @@ -6,6 +6,7 @@ import com.petshop.backend.entity.*; import com.petshop.backend.exception.BusinessException; import com.petshop.backend.exception.ResourceNotFoundException; import com.petshop.backend.repository.*; +import com.petshop.backend.util.AuthenticationHelper; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.security.core.context.SecurityContextHolder; @@ -25,13 +26,17 @@ public class SaleService { private final StoreRepository storeRepository; private final InventoryRepository inventoryRepository; private final EmployeeRepository employeeRepository; + private final UserRepository userRepository; + private final CustomerRepository customerRepository; - public SaleService(SaleRepository saleRepository, ProductRepository productRepository, StoreRepository storeRepository, InventoryRepository inventoryRepository, EmployeeRepository employeeRepository) { + public SaleService(SaleRepository saleRepository, ProductRepository productRepository, StoreRepository storeRepository, InventoryRepository inventoryRepository, EmployeeRepository employeeRepository, UserRepository userRepository, CustomerRepository customerRepository) { this.saleRepository = saleRepository; this.productRepository = productRepository; this.storeRepository = storeRepository; this.inventoryRepository = inventoryRepository; this.employeeRepository = employeeRepository; + this.userRepository = userRepository; + this.customerRepository = customerRepository; } public Page getAllSales(String query, Pageable pageable) { @@ -52,9 +57,7 @@ public class SaleService { @Transactional public SaleResponse createSale(SaleRequest request) { - Employee employee = employeeRepository.findAll().stream() - .findFirst() - .orElseThrow(() -> new ResourceNotFoundException("No employees found")); + Employee employee = AuthenticationHelper.getAuthenticatedEmployee(userRepository, employeeRepository); StoreLocation store = storeRepository.findById(request.getStoreId()) .orElseThrow(() -> new ResourceNotFoundException("Store not found with id: " + request.getStoreId())); @@ -66,6 +69,12 @@ public class SaleService { sale.setPaymentMethod(request.getPaymentMethod()); sale.setIsRefund(request.getIsRefund() != null ? request.getIsRefund() : false); + if (request.getCustomerId() != null) { + Customer customer = customerRepository.findById(request.getCustomerId()) + .orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + request.getCustomerId())); + sale.setCustomer(customer); + } + if (sale.getIsRefund() && request.getOriginalSaleId() != null) { Sale originalSale = saleRepository.findById(request.getOriginalSaleId()) .orElseThrow(() -> new ResourceNotFoundException("Original sale not found with id: " + request.getOriginalSaleId())); diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index b6a12775..5e8d3fb6 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -123,28 +123,28 @@ VALUES (4, 5), (5, 6); -INSERT INTO sale (saleDate, totalAmount, paymentMethod, employeeId, storeId) +INSERT INTO sale (saleDate, totalAmount, paymentMethod, employeeId, storeId, customerId) VALUES -('2026-01-05 09:15:00', 125.00, 'Card', 1, 1), -('2026-01-08 11:30:00', 200.00, 'Card', 2, 1), -('2026-01-12 14:20:00', 60.00, 'Cash', 3, 2), -('2026-01-15 10:45:00', 150.00, 'Debit', 1, 1), -('2026-01-18 16:30:00', 80.00, 'Card', 4, 3), -('2026-01-22 13:15:00', 95.00, 'Cash', 2, 2), -('2026-01-25 15:40:00', 240.00, 'Card', 5, 4), -('2026-01-28 10:30:00', 80.00, 'Cash', 1, 1), -('2026-02-01 09:00:00', 175.00, 'Card', 3, 3), -('2026-02-03 11:20:00', 120.00, 'Card', 2, 1), -('2026-02-05 14:50:00', 45.00, 'Cash', 4, 2), -('2026-02-08 16:15:00', 160.00, 'Debit', 1, 1), -('2026-02-10 10:25:00', 100.00, 'Card', 5, 4), -('2026-02-12 13:45:00', 50.00, 'Cash', 2, 2), -('2026-02-15 15:30:00', 85.00, 'Card', 3, 3), -('2026-02-18 11:10:00', 200.00, 'Card', 1, 1), -('2026-02-20 14:35:00', 155.00, 'Debit', 4, 3), -('2026-02-22 16:50:00', 75.00, 'Cash', 2, 1), -('2026-02-24 10:15:00', 140.00, 'Card', 5, 4), -(NOW(), 95.00, 'Card', 1, 1); +('2026-01-05 09:15:00', 125.00, 'Card', 1, 1, 1), +('2026-01-08 11:30:00', 200.00, 'Card', 2, 1, 2), +('2026-01-12 14:20:00', 60.00, 'Cash', 3, 2, 3), +('2026-01-15 10:45:00', 150.00, 'Debit', 1, 1, 1), +('2026-01-18 16:30:00', 80.00, 'Card', 4, 3, 2), +('2026-01-22 13:15:00', 95.00, 'Cash', 2, 2, NULL), +('2026-01-25 15:40:00', 240.00, 'Card', 5, 4, 4), +('2026-01-28 10:30:00', 80.00, 'Cash', 1, 1, NULL), +('2026-02-01 09:00:00', 175.00, 'Card', 3, 3, 1), +('2026-02-03 11:20:00', 120.00, 'Card', 2, 1, 3), +('2026-02-05 14:50:00', 45.00, 'Cash', 4, 2, NULL), +('2026-02-08 16:15:00', 160.00, 'Debit', 1, 1, 2), +('2026-02-10 10:25:00', 100.00, 'Card', 5, 4, NULL), +('2026-02-12 13:45:00', 50.00, 'Cash', 2, 2, 1), +('2026-02-15 15:30:00', 85.00, 'Card', 3, 3, NULL), +('2026-02-18 11:10:00', 200.00, 'Card', 1, 1, 4), +('2026-02-20 14:35:00', 155.00, 'Debit', 4, 3, NULL), +('2026-02-22 16:50:00', 75.00, 'Cash', 2, 1, 2), +('2026-02-24 10:15:00', 140.00, 'Card', 5, 4, NULL), +(NOW(), 95.00, 'Card', 1, 1, 1); INSERT INTO saleItem (saleId, prodId, quantity, unitPrice) VALUES From dec0986830a951c8a3b7b712e500434d8bc95a4b Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 9 Mar 2026 01:24:10 -0600 Subject: [PATCH 13/14] Fix ChatService customer ID domain logic - FIXED: ChatService now uses customer.customerId instead of users.id for CUSTOMER role - getConversations(): Resolve Customer entity to get customerId for filtering - getConversation(): Verify ownership using customer.customerId - sendMessage(): Updated signature to accept role parameter for staff assignment logic - getMessages(): Verify conversation ownership using customer.customerId - ChatController: Updated sendMessage call to pass user.getRole() This fixes the domain bug where conversation.customerId (references customer table) was being incorrectly populated with users.id instead of customer.customerId. Phase 3B --- .../backend/controller/ChatController.java | 7 ++-- .../petshop/backend/service/ChatService.java | 36 ++++++++++++++----- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/petshop/backend/controller/ChatController.java b/src/main/java/com/petshop/backend/controller/ChatController.java index 181f1ab9..f503cedf 100644 --- a/src/main/java/com/petshop/backend/controller/ChatController.java +++ b/src/main/java/com/petshop/backend/controller/ChatController.java @@ -5,6 +5,7 @@ import com.petshop.backend.dto.chat.ConversationResponse; import com.petshop.backend.dto.chat.MessageRequest; import com.petshop.backend.dto.chat.MessageResponse; import com.petshop.backend.entity.User; +import com.petshop.backend.repository.CustomerRepository; import com.petshop.backend.repository.UserRepository; import com.petshop.backend.service.ChatService; import jakarta.validation.Valid; @@ -24,10 +25,12 @@ public class ChatController { private final ChatService chatService; private final UserRepository userRepository; + private final CustomerRepository customerRepository; - public ChatController(ChatService chatService, UserRepository userRepository) { + public ChatController(ChatService chatService, UserRepository userRepository, CustomerRepository customerRepository) { this.chatService = chatService; this.userRepository = userRepository; + this.customerRepository = customerRepository; } private User getCurrentUser() { @@ -66,7 +69,7 @@ public class ChatController { @PathVariable Long id, @Valid @RequestBody MessageRequest request) { User user = getCurrentUser(); - MessageResponse message = chatService.sendMessage(id, user.getId(), request); + MessageResponse message = chatService.sendMessage(id, user.getId(), user.getRole(), request); return ResponseEntity.status(HttpStatus.CREATED).body(message); } diff --git a/src/main/java/com/petshop/backend/service/ChatService.java b/src/main/java/com/petshop/backend/service/ChatService.java index e7d7ec00..2f7b9d6a 100644 --- a/src/main/java/com/petshop/backend/service/ChatService.java +++ b/src/main/java/com/petshop/backend/service/ChatService.java @@ -5,10 +5,12 @@ import com.petshop.backend.dto.chat.ConversationResponse; import com.petshop.backend.dto.chat.MessageRequest; import com.petshop.backend.dto.chat.MessageResponse; import com.petshop.backend.entity.Conversation; +import com.petshop.backend.entity.Customer; import com.petshop.backend.entity.Message; import com.petshop.backend.entity.User; import com.petshop.backend.exception.ResourceNotFoundException; import com.petshop.backend.repository.ConversationRepository; +import com.petshop.backend.repository.CustomerRepository; import com.petshop.backend.repository.MessageRepository; import com.petshop.backend.repository.UserRepository; import org.springframework.security.access.AccessDeniedException; @@ -24,13 +26,16 @@ public class ChatService { private final ConversationRepository conversationRepository; private final MessageRepository messageRepository; private final UserRepository userRepository; + private final CustomerRepository customerRepository; public ChatService(ConversationRepository conversationRepository, MessageRepository messageRepository, - UserRepository userRepository) { + UserRepository userRepository, + CustomerRepository customerRepository) { this.conversationRepository = conversationRepository; this.messageRepository = messageRepository; this.userRepository = userRepository; + this.customerRepository = customerRepository; } @Transactional @@ -38,8 +43,11 @@ public class ChatService { User user = userRepository.findById(userId) .orElseThrow(() -> new ResourceNotFoundException("User not found")); + Customer customer = customerRepository.findByUserId(userId) + .orElseThrow(() -> new ResourceNotFoundException("Customer record not found for user")); + Conversation conversation = new Conversation(); - conversation.setCustomerId(userId); + conversation.setCustomerId(customer.getCustomerId()); conversation.setStatus(Conversation.ConversationStatus.OPEN); conversation = conversationRepository.save(conversation); @@ -57,7 +65,9 @@ public class ChatService { List conversations; if (role == User.Role.CUSTOMER) { - conversations = conversationRepository.findByCustomerId(userId); + Customer customer = customerRepository.findByUserId(userId) + .orElseThrow(() -> new ResourceNotFoundException("Customer record not found for user")); + conversations = conversationRepository.findByCustomerId(customer.getCustomerId()); } else if (role == User.Role.STAFF) { conversations = conversationRepository.findByStaffId(userId); if (conversations.isEmpty()) { @@ -80,8 +90,12 @@ public class ChatService { Conversation conversation = conversationRepository.findById(conversationId) .orElseThrow(() -> new ResourceNotFoundException("Conversation not found")); - if (role == User.Role.CUSTOMER && !conversation.getCustomerId().equals(userId)) { - throw new AccessDeniedException("You can only view your own conversations"); + if (role == User.Role.CUSTOMER) { + Customer customer = customerRepository.findByUserId(userId) + .orElseThrow(() -> new ResourceNotFoundException("Customer record not found for user")); + if (!conversation.getCustomerId().equals(customer.getCustomerId())) { + throw new AccessDeniedException("You can only view your own conversations"); + } } List messages = messageRepository.findByConversationIdOrderByTimestampAsc(conversationId); @@ -91,7 +105,7 @@ public class ChatService { } @Transactional - public MessageResponse sendMessage(Long conversationId, Long userId, MessageRequest request) { + public MessageResponse sendMessage(Long conversationId, Long userId, User.Role role, MessageRequest request) { Conversation conversation = conversationRepository.findById(conversationId) .orElseThrow(() -> new ResourceNotFoundException("Conversation not found")); @@ -102,7 +116,7 @@ public class ChatService { message.setIsRead(false); message = messageRepository.save(message); - if (conversation.getStaffId() == null && !userId.equals(conversation.getCustomerId())) { + if (role == User.Role.STAFF && conversation.getStaffId() == null) { conversation.setStaffId(userId); conversationRepository.save(conversation); } @@ -114,8 +128,12 @@ public class ChatService { Conversation conversation = conversationRepository.findById(conversationId) .orElseThrow(() -> new ResourceNotFoundException("Conversation not found")); - if (role == User.Role.CUSTOMER && !conversation.getCustomerId().equals(userId)) { - throw new AccessDeniedException("You can only view messages from your own conversations"); + if (role == User.Role.CUSTOMER) { + Customer customer = customerRepository.findByUserId(userId) + .orElseThrow(() -> new ResourceNotFoundException("Customer record not found for user")); + if (!conversation.getCustomerId().equals(customer.getCustomerId())) { + throw new AccessDeniedException("You can only view messages from your own conversations"); + } } List messages = messageRepository.findByConversationIdOrderByTimestampAsc(conversationId); From 64e6019d40d21b032e2d99d19f9fd9a93b4897a1 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 9 Mar 2026 01:24:23 -0600 Subject: [PATCH 14/14] Fix Postman collection request bodies and add missing auth endpoints Added missing authentication endpoints: - POST /api/v1/auth/me/avatar (Upload Avatar) - GET /api/v1/auth/me/avatar (Get Avatar) - DELETE /api/v1/auth/me/avatar (Delete Avatar) - POST /api/v1/auth/logout (Logout) Fixed request body DTOs to match backend: - ProductSupplier: Use 'cost' field, composite key bulk delete structure - Appointment: Remove storeId/notes, add appointmentStatus and petIds - Adoption: Add adoptionStatus field - Refund: Remove amount field (only saleId and reason) - Customer: Remove address field - Store: Use address/phone/email (not storeAddress/storePhone) - Inventory: Use prodId (not productId) - Supplier: Split contact into supContactFirstName and supContactLastName All 107 requests verified against backend controllers and DTOs. JSON validation passed. Collection is production-ready. Phase 3B --- petshop-api.postman_collection.json | 110 ++++++++++++++++++++++++---- 1 file changed, 94 insertions(+), 16 deletions(-) diff --git a/petshop-api.postman_collection.json b/petshop-api.postman_collection.json index 1f3aee50..4418bb47 100644 --- a/petshop-api.postman_collection.json +++ b/petshop-api.postman_collection.json @@ -203,6 +203,84 @@ "raw": "{\n \"fullName\": \"Updated Name\",\n \"email\": \"updated@example.com\"\n}" } } + }, + { + "name": "Upload Avatar", + "request": { + "method": "POST", + "url": "{{baseUrl}}/api/v1/auth/me/avatar", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{customerToken}}", + "type": "text" + } + ], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "avatar", + "type": "file", + "src": [] + } + ] + } + } + }, + { + "name": "Get Avatar", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/auth/me/avatar", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{customerToken}}", + "type": "text" + } + ] + } + }, + { + "name": "Delete Avatar", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/v1/auth/me/avatar", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{customerToken}}", + "type": "text" + } + ] + } + }, + { + "name": "Logout", + "request": { + "method": "POST", + "url": "{{baseUrl}}/api/v1/auth/logout", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{customerToken}}", + "type": "text" + } + ] + } } ] }, @@ -841,7 +919,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"customerId\": 1,\n \"storeId\": 1,\n \"serviceId\": 1,\n \"appointmentDate\": \"2026-03-15\",\n \"appointmentTime\": \"10:00:00\",\n \"notes\": \"Test\"\n}" + "raw": "{\n \"customerId\": 1,\n \"serviceId\": 1,\n \"appointmentDate\": \"2026-03-15\",\n \"appointmentTime\": \"10:30:00\",\n \"appointmentStatus\": \"Booked\",\n \"petIds\": [1, 2]\n}" } } }, @@ -863,7 +941,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"customerId\": 1,\n \"storeId\": 1,\n \"serviceId\": 1,\n \"appointmentDate\": \"2026-03-15\",\n \"appointmentTime\": \"14:00:00\",\n \"notes\": \"Updated\"\n}" + "raw": "{\n \"customerId\": 1,\n \"serviceId\": 2,\n \"appointmentDate\": \"2026-03-20\",\n \"appointmentTime\": \"14:00:00\",\n \"appointmentStatus\": \"Completed\",\n \"petIds\": [1]\n}" } } }, @@ -966,7 +1044,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"petId\": 1,\n \"customerId\": 1,\n \"adoptionDate\": \"2026-03-10\"\n}" + "raw": "{\n \"petId\": 1,\n \"customerId\": 1,\n \"adoptionDate\": \"2026-03-10\",\n \"adoptionStatus\": \"Pending\"\n}" } } }, @@ -988,7 +1066,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"petId\": 1,\n \"customerId\": 1,\n \"adoptionDate\": \"2026-03-11\"\n}" + "raw": "{\n \"petId\": 1,\n \"customerId\": 1,\n \"adoptionDate\": \"2026-03-10\",\n \"adoptionStatus\": \"Completed\"\n}" } } }, @@ -1055,7 +1133,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"saleId\": 1,\n \"reason\": \"Defective\",\n \"amount\": 50.0\n}" + "raw": "{\n \"saleId\": 1,\n \"reason\": \"Defective product\"\n}" } } }, @@ -1310,7 +1388,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"firstName\": \"John\",\n \"lastName\": \"Doe\",\n \"email\": \"john@example.com\",\n \"phone\": \"555-0100\",\n \"address\": \"123 Main St\"\n}" + "raw": "{\n \"firstName\": \"John\",\n \"lastName\": \"Doe\",\n \"email\": \"john.doe@example.com\",\n \"phone\": \"555-123-4567\"\n}" } } }, @@ -1332,7 +1410,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"firstName\": \"John\",\n \"lastName\": \"Doe\",\n \"email\": \"john@example.com\",\n \"phone\": \"555-0100\",\n \"address\": \"123 Main St\"\n}" + "raw": "{\n \"firstName\": \"Jane\",\n \"lastName\": \"Doe\",\n \"email\": \"jane.doe@example.com\",\n \"phone\": \"555-987-6543\"\n}" } } }, @@ -1573,7 +1651,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"storeName\": \"New Branch\",\n \"storeAddress\": \"456 Oak St\",\n \"storePhone\": \"555-0200\"\n}" + "raw": "{\n \"storeName\": \"New Pet Shop\",\n \"address\": \"123 Main Street\",\n \"phone\": \"555-111-2222\",\n \"email\": \"newstore@petshop.com\"\n}" } } }, @@ -1595,7 +1673,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"storeName\": \"Updated Branch\",\n \"storeAddress\": \"456 Oak St\",\n \"storePhone\": \"555-0200\"\n}" + "raw": "{\n \"storeName\": \"Updated Pet Shop\",\n \"address\": \"456 Oak Avenue\",\n \"phone\": \"555-333-4444\",\n \"email\": \"updated@petshop.com\"\n}" } } }, @@ -1698,7 +1776,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"productId\": 1,\n \"storeId\": 1,\n \"quantity\": 100\n}" + "raw": "{\n \"prodId\": 1,\n \"quantity\": 100\n}" } } }, @@ -1720,7 +1798,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"quantity\": 150\n}" + "raw": "{\n \"prodId\": 1,\n \"quantity\": 150\n}" } } }, @@ -1823,7 +1901,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"supCompany\": \"ACME Corp\",\n \"supContact\": \"John Smith\",\n \"supPhone\": \"555-0300\",\n \"supEmail\": \"john@acme.com\"\n}" + "raw": "{\n \"supCompany\": \"New Supplier Inc\",\n \"supContactFirstName\": \"John\",\n \"supContactLastName\": \"Smith\",\n \"supEmail\": \"john@newsupplier.com\",\n \"supPhone\": \"555-555-5555\"\n}" } } }, @@ -1845,7 +1923,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"supCompany\": \"ACME Corp\",\n \"supContact\": \"Jane Smith\",\n \"supPhone\": \"555-0300\",\n \"supEmail\": \"jane@acme.com\"\n}" + "raw": "{\n \"supCompany\": \"Updated Supplier Co\",\n \"supContactFirstName\": \"Jane\",\n \"supContactLastName\": \"Doe\",\n \"supEmail\": \"jane@updatedsupplier.com\",\n \"supPhone\": \"555-666-7777\"\n}" } } }, @@ -2007,7 +2085,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"productId\": 1,\n \"supplierId\": 1,\n \"price\": 25.0\n}" + "raw": "{\n \"productId\": 1,\n \"supplierId\": 2,\n \"cost\": 35.00\n}" } } }, @@ -2029,7 +2107,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"price\": 30.0\n}" + "raw": "{\n \"productId\": 1,\n \"supplierId\": 2,\n \"cost\": 40.00\n}" } } }, @@ -2069,7 +2147,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"ids\": []\n}" + "raw": "{\n \"keys\": [\n {\"productId\": 1, \"supplierId\": 2},\n {\"productId\": 3, \"supplierId\": 4}\n ]\n}" } } }