diff --git a/backend/petshop-api.postman_collection.json b/backend/petshop-api.postman_collection.json index 1068f4e0..3b36221b 100644 --- a/backend/petshop-api.postman_collection.json +++ b/backend/petshop-api.postman_collection.json @@ -2,7 +2,7 @@ "info": { "name": "PetShop Complete Collection", "_postman_id": "petshop-api-complete-v1", - "description": "Complete API collection with all 95+ verified endpoints", + "description": "Complete API collection for all backend endpoints", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, "variable": [ @@ -124,7 +124,11 @@ }, { "key": "adoptedPetId", - "value": "4" + "value": "15" + }, + { + "key": "employeeId", + "value": "" } ], "item": [ @@ -221,6 +225,7 @@ "});", "var jsonData = pm.response.json();", "if (jsonData.id !== undefined) pm.collectionVariables.set('userId', jsonData.id);", + "if (jsonData.id !== undefined) pm.collectionVariables.set('customerId', jsonData.id);", "if (jsonData.token) pm.collectionVariables.set('customerToken', jsonData.token);" ] } @@ -659,6 +664,37 @@ } ] }, + { + "name": "Dropdown: Pet Species", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/dropdowns/pet-species", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ] + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});" + ] + } + } + ] + }, { "name": "Create Pet", "request": { @@ -723,7 +759,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"petName\": \"Postman Pet Updated\",\n \"petSpecies\": \"Dog\",\n \"petBreed\": \"Mixed\",\n \"petAge\": 3,\n \"petStatus\": \"Owned\",\n \"petPrice\": 375.00,\n \"customerId\": 1\n}", + "raw": "{\n \"petName\": \"Postman Pet Updated\",\n \"petSpecies\": \"Dog\",\n \"petBreed\": \"Mixed\",\n \"petAge\": 3,\n \"petStatus\": \"Owned\",\n \"petPrice\": 375.00,\n \"customerId\": {{customerId}}\n}", "options": { "raw": { "language": "json" @@ -1475,13 +1511,13 @@ }, { "key": "Authorization", - "value": "Bearer {{staffToken}}", + "value": "Bearer {{adminToken}}", "type": "text" } ], "body": { "mode": "raw", - "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}" + "raw": "{\n \"storeId\": {{storeId}},\n \"paymentMethod\": \"Card\",\n \"customerId\": {{customerId}},\n \"items\": [\n {\n \"prodId\": 7,\n \"quantity\": 2\n },\n {\n \"prodId\": 8,\n \"quantity\": 1\n }\n ],\n \"isRefund\": false\n}" } }, "event": [ @@ -1889,6 +1925,37 @@ } ] }, + { + "name": "Dropdown: Product Categories", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/dropdowns/product-categories", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ] + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});" + ] + } + } + ] + }, { "name": "Create Category", "request": { @@ -2119,7 +2186,7 @@ "name": "Check Appointment Availability", "request": { "method": "GET", - "url": "{{baseUrl}}/api/v1/appointments/availability?storeId=1&serviceId=1&date=2026-12-20", + "url": "{{baseUrl}}/api/v1/appointments/availability?storeId={{storeId}}&serviceId={{serviceId}}&date=2026-12-20", "header": [ { "key": "Content-Type", @@ -2150,7 +2217,55 @@ "name": "List Appointments", "request": { "method": "GET", - "url": "{{baseUrl}}/api/v1/appointments", + "url": { + "raw": "{{baseUrl}}/api/v1/appointments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "v1", + "appointments" + ], + "query": [ + { + "key": "q", + "value": "", + "disabled": true, + "description": "optional" + }, + { + "key": "storeId", + "value": "1", + "disabled": true, + "description": "optional" + }, + { + "key": "status", + "value": "Booked", + "disabled": true, + "description": "optional" + }, + { + "key": "date", + "value": "2026-12-20", + "disabled": true, + "description": "optional" + }, + { + "key": "customerId", + "value": "1", + "disabled": true, + "description": "optional" + }, + { + "key": "employeeId", + "value": "1", + "disabled": true, + "description": "optional" + } + ] + }, "header": [ { "key": "Content-Type", @@ -2226,7 +2341,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"customerId\": 1,\n \"storeId\": 1,\n \"serviceId\": 1,\n \"appointmentDate\": \"2026-12-20\",\n \"appointmentTime\": \"10:00:00\",\n \"appointmentStatus\": \"Booked\",\n \"petIds\": [\n 1\n ],\n \"employeeId\": 1\n}", + "raw": "{\n \"customerId\": {{customerId}},\n \"storeId\": {{storeId}},\n \"serviceId\": {{serviceId}},\n \"appointmentDate\": \"2026-12-20\",\n \"appointmentTime\": \"10:00:00\",\n \"appointmentStatus\": \"Booked\",\n \"petId\": {{petId}}\n}", "options": { "raw": { "language": "json" @@ -2268,7 +2383,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"customerId\": 1,\n \"storeId\": 1,\n \"serviceId\": 1,\n \"appointmentDate\": \"2026-12-20\",\n \"appointmentTime\": \"11:00:00\",\n \"appointmentStatus\": \"Booked\",\n \"petIds\": [\n 1\n ],\n \"employeeId\": 1\n}", + "raw": "{\n \"customerId\": {{customerId}},\n \"storeId\": {{storeId}},\n \"serviceId\": {{serviceId}},\n \"appointmentDate\": \"2026-12-20\",\n \"appointmentTime\": \"11:00:00\",\n \"appointmentStatus\": \"Booked\",\n \"petId\": {{petId}}\n}", "options": { "raw": { "language": "json" @@ -2472,7 +2587,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"petId\": 3,\n \"customerId\": 1,\n \"adoptionDate\": \"2026-12-21\",\n \"adoptionStatus\": \"Pending\",\n \"employeeId\": 1\n}", + "raw": "{\n \"petId\": {{adoptedPetId}},\n \"customerId\": {{customerId}},\n \"adoptionDate\": \"2026-12-21\",\n \"adoptionStatus\": \"Pending\",\n \"sourceStoreId\": {{storeId}}\n}", "options": { "raw": { "language": "json" @@ -2514,7 +2629,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"petId\": 3,\n \"customerId\": 1,\n \"adoptionDate\": \"2026-12-22\",\n \"adoptionStatus\": \"Completed\",\n \"employeeId\": 1\n}", + "raw": "{\n \"petId\": {{adoptedPetId}},\n \"customerId\": {{customerId}},\n \"adoptionDate\": \"2026-12-22\",\n \"adoptionStatus\": \"Completed\",\n \"sourceStoreId\": {{storeId}}\n}", "options": { "raw": { "language": "json" @@ -2625,7 +2740,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"saleId\": 1,\n \"reason\": \"Defective product\"\n}", + "raw": "{\n \"saleId\": {{saleId}},\n \"reason\": \"Defective product\",\n \"items\": [\n {\n \"prodId\": 7,\n \"quantity\": 1\n }\n ]\n}", "options": { "raw": { "language": "json" @@ -3104,6 +3219,46 @@ } } ] + }, + { + "name": "Update Conversation", + "request": { + "method": "PUT", + "url": "{{baseUrl}}/api/v1/chat/conversations/{{conversationId}}", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"status\": \"CLOSED\"\n}", + "options": { + "raw": { + "language": "json" + } + } + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});" + ] + } + } + ] } ] }, @@ -3762,6 +3917,186 @@ } ] }, + { + "name": "Employees", + "item": [ + { + "name": "List Employees", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/employees", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ] + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});" + ] + } + } + ] + }, + { + "name": "Get Employee by ID", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/employees/{{employeeId}}", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ] + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});" + ] + } + } + ] + }, + { + "name": "Create Employee", + "request": { + "method": "POST", + "url": "{{baseUrl}}/api/v1/employees", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"username\": \"employee{{$timestamp}}\",\n \"password\": \"employee123\",\n \"firstName\": \"Jane\",\n \"lastName\": \"Smith\",\n \"email\": \"employee{{$timestamp}}@petshop.com\",\n \"phone\": \"403-555-0200\",\n \"role\": \"STAFF\",\n \"staffRole\": \"GROOMER\",\n \"primaryStoreId\": 1,\n \"active\": true\n}", + "options": { + "raw": { + "language": "json" + } + } + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 201', function () {", + " pm.response.to.have.status(201);", + "});", + "var jsonData = pm.response.json();", + "if (jsonData.id) pm.collectionVariables.set('employeeId', jsonData.id);" + ] + } + } + ] + }, + { + "name": "Update Employee", + "request": { + "method": "PUT", + "url": "{{baseUrl}}/api/v1/employees/{{employeeId}}", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"firstName\": \"Jane\",\n \"lastName\": \"Smith\",\n \"email\": \"employee.updated@petshop.com\",\n \"phone\": \"403-555-0201\",\n \"role\": \"STAFF\",\n \"staffRole\": \"VET\",\n \"primaryStoreId\": 1,\n \"active\": true\n}", + "options": { + "raw": { + "language": "json" + } + } + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});" + ] + } + } + ] + }, + { + "name": "Delete Employee", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/v1/employees/{{employeeId}}", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ] + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 204', function () {", + " pm.response.to.have.status(204);", + "});" + ] + } + } + ] + } + ] + }, { "name": "Stores", "item": [ @@ -4119,7 +4454,31 @@ "name": "List Inventory", "request": { "method": "GET", - "url": "{{baseUrl}}/api/v1/inventory", + "url": { + "raw": "{{baseUrl}}/api/v1/inventory", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "v1", + "inventory" + ], + "query": [ + { + "key": "q", + "value": "", + "disabled": true, + "description": "optional" + }, + { + "key": "storeId", + "value": "1", + "disabled": true, + "description": "optional" + } + ] + }, "header": [ { "key": "Content-Type", @@ -4195,7 +4554,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"prodId\": {{productId}},\n \"quantity\": 10\n}", + "raw": "{\n \"prodId\": {{productId}},\n \"quantity\": 10,\n \"storeId\": {{storeId}}\n}", "options": { "raw": { "language": "json" @@ -4237,7 +4596,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"prodId\": {{productId}},\n \"quantity\": 12\n}", + "raw": "{\n \"prodId\": {{productId}},\n \"quantity\": 12,\n \"storeId\": {{storeId}}\n}", "options": { "raw": { "language": "json" @@ -4308,7 +4667,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"prodId\": {{productId}},\n \"quantity\": 10\n}", + "raw": "{\n \"prodId\": {{productId}},\n \"quantity\": 10,\n \"storeId\": {{storeId}}\n}", "options": { "raw": { "language": "json" @@ -4528,7 +4887,7 @@ }, { "key": "Authorization", - "value": "Bearer {{staffToken}}", + "value": "Bearer {{adminToken}}", "type": "text" } ] @@ -4559,7 +4918,7 @@ }, { "key": "Authorization", - "value": "Bearer {{staffToken}}", + "value": "Bearer {{adminToken}}", "type": "text" } ], @@ -4622,7 +4981,31 @@ "name": "List Purchase Orders", "request": { "method": "GET", - "url": "{{baseUrl}}/api/v1/purchase-orders", + "url": { + "raw": "{{baseUrl}}/api/v1/purchase-orders", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "v1", + "purchase-orders" + ], + "query": [ + { + "key": "q", + "value": "", + "disabled": true, + "description": "optional" + }, + { + "key": "storeId", + "value": "1", + "disabled": true, + "description": "optional" + } + ] + }, "header": [ { "key": "Content-Type", @@ -4689,7 +5072,37 @@ "name": "List Product Suppliers", "request": { "method": "GET", - "url": "{{baseUrl}}/api/v1/product-suppliers", + "url": { + "raw": "{{baseUrl}}/api/v1/product-suppliers", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "v1", + "product-suppliers" + ], + "query": [ + { + "key": "q", + "value": "", + "disabled": true, + "description": "optional" + }, + { + "key": "productId", + "value": "{{productId}}", + "disabled": true, + "description": "optional" + }, + { + "key": "supplierId", + "value": "{{supplierId}}", + "disabled": true, + "description": "optional" + } + ] + }, "header": [ { "key": "Content-Type", diff --git a/backend/src/main/java/com/petshop/backend/repository/AdoptionRepository.java b/backend/src/main/java/com/petshop/backend/repository/AdoptionRepository.java index e3c21776..42f1d578 100644 --- a/backend/src/main/java/com/petshop/backend/repository/AdoptionRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/AdoptionRepository.java @@ -29,9 +29,7 @@ public interface AdoptionRepository extends JpaRepository { Optional findFirstByPet_IdAndAdoptionStatusOrderByAdoptionDateDesc(Long petId, String adoptionStatus); - @Query("SELECT CASE WHEN COUNT(a) > 0 THEN true ELSE false END FROM Adoption a WHERE a.pet.id = :petId AND LOWER(a.adoptionStatus) = LOWER(:adoptionStatus) AND a.adoptionId <> :adoptionId") - boolean existsByPetPetIdAndAdoptionStatusIgnoreCaseAndAdoptionIdNot(@Param("petId") Long petId, @Param("adoptionStatus") String adoptionStatus, @Param("adoptionId") Long adoptionId); + boolean existsByPet_IdAndAdoptionStatusIgnoreCaseAndAdoptionIdNot(Long petId, String adoptionStatus, Long adoptionId); - @Query("SELECT CASE WHEN COUNT(a) > 0 THEN true ELSE false END FROM Adoption a WHERE a.pet.id = :petId AND LOWER(a.adoptionStatus) = LOWER(:adoptionStatus)") - boolean existsByPetPetIdAndAdoptionStatusIgnoreCase(@Param("petId") Long petId, @Param("adoptionStatus") String adoptionStatus); + boolean existsByPet_IdAndAdoptionStatusIgnoreCase(Long petId, String adoptionStatus); } diff --git a/backend/src/main/java/com/petshop/backend/service/AdoptionService.java b/backend/src/main/java/com/petshop/backend/service/AdoptionService.java index ec9ef393..5f76947d 100644 --- a/backend/src/main/java/com/petshop/backend/service/AdoptionService.java +++ b/backend/src/main/java/com/petshop/backend/service/AdoptionService.java @@ -199,8 +199,8 @@ public class AdoptionService { private void validatePetAvailability(Pet pet, Long adoptionId) { boolean adoptedElsewhere = adoptionId == null - ? adoptionRepository.existsByPetPetIdAndAdoptionStatusIgnoreCase(pet.getPetId(), ADOPTION_STATUS_COMPLETED) - : adoptionRepository.existsByPetPetIdAndAdoptionStatusIgnoreCaseAndAdoptionIdNot(pet.getPetId(), ADOPTION_STATUS_COMPLETED, adoptionId); + ? adoptionRepository.existsByPet_IdAndAdoptionStatusIgnoreCase(pet.getPetId(), ADOPTION_STATUS_COMPLETED) + : adoptionRepository.existsByPet_IdAndAdoptionStatusIgnoreCaseAndAdoptionIdNot(pet.getPetId(), ADOPTION_STATUS_COMPLETED, adoptionId); if (adoptedElsewhere) { throw new IllegalArgumentException("Selected pet has already been adopted"); } @@ -212,7 +212,7 @@ public class AdoptionService { private void syncPetStatus(Pet pet, String adoptionStatus, Long adoptionId) { boolean completedElsewhere = adoptionId != null - && adoptionRepository.existsByPetPetIdAndAdoptionStatusIgnoreCaseAndAdoptionIdNot(pet.getPetId(), ADOPTION_STATUS_COMPLETED, adoptionId); + && adoptionRepository.existsByPet_IdAndAdoptionStatusIgnoreCaseAndAdoptionIdNot(pet.getPetId(), ADOPTION_STATUS_COMPLETED, adoptionId); if (ADOPTION_STATUS_COMPLETED.equalsIgnoreCase(adoptionStatus) || completedElsewhere) { pet.setPetStatus(PET_STATUS_ADOPTED); } else {