diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 00000000..1a527ca6 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,55 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea/* +!.idea/runConfigurations/ +!.idea/.gitignore +!.idea/compiler.xml +!.idea/encodings.xml +!.idea/jarRepositories.xml +!.idea/misc.xml +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac ### +.DS_Store + +### Project Specific ### +tmp/ +uploads/ + +### Temp and backup files ### +*.backup +*.backup* +*.bak +*.tmp +*.py +temp_*.json +last_part.json +fix_*.py diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 00000000..95c76bc2 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,13 @@ +# Build +FROM maven:3.9-eclipse-temurin-17 AS build +WORKDIR /app +COPY pom.xml . +COPY src ./src +RUN mvn -q -DskipTests package + +# Run +FROM eclipse-temurin:17-jre +WORKDIR /app +COPY --from=build /app/target/*.jar app.jar +EXPOSE 8080 +ENTRYPOINT ["java","-jar","app.jar"] diff --git a/backend/docker-compose.db.yml b/backend/docker-compose.db.yml new file mode 100644 index 00000000..7d8408d0 --- /dev/null +++ b/backend/docker-compose.db.yml @@ -0,0 +1,21 @@ +services: + db: + image: mysql:8.0 + container_name: petshop-db + environment: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: Petstoredb + MYSQL_USER: petshop + MYSQL_PASSWORD: petshop + ports: + - "3306:3306" + volumes: + - db_data:/var/lib/mysql + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-proot"] + interval: 5s + timeout: 5s + retries: 30 + +volumes: + db_data: diff --git a/backend/docker-compose.dev.yml b/backend/docker-compose.dev.yml new file mode 100644 index 00000000..774e5393 --- /dev/null +++ b/backend/docker-compose.dev.yml @@ -0,0 +1,23 @@ +services: + db: + image: mysql:8.0 + container_name: petshop-db + restart: always + environment: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: Petstoredb + MYSQL_USER: petshop + MYSQL_PASSWORD: petshop + ports: + - "3306:3306" + volumes: + - db_data:/var/lib/mysql + healthcheck: + test: ["CMD", "mysql", "-uroot", "-proot", "-e", "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='Petstoredb' AND table_name='users';"] + interval: 10s + timeout: 5s + retries: 30 + start_period: 40s + +volumes: + db_data: diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml new file mode 100644 index 00000000..1966e7e6 --- /dev/null +++ b/backend/docker-compose.yml @@ -0,0 +1,36 @@ +services: + db: + image: mysql:8.0 + container_name: petshop-db + environment: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: Petstoredb + MYSQL_USER: petshop + MYSQL_PASSWORD: petshop + ports: + - "3306:3306" + volumes: + - db_data:/var/lib/mysql + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-proot"] + interval: 5s + timeout: 5s + retries: 30 + + api: + build: . + container_name: petshop-api + environment: + SPRING_DATASOURCE_URL: jdbc:mysql://db:3306/Petstoredb?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC + SPRING_DATASOURCE_USERNAME: petshop + SPRING_DATASOURCE_PASSWORD: petshop + # Change this in real use (must be at least 32 characters) + JWT_SECRET: change_me_please_this_secret_key_is_long_enough_for_jwt_hmac_sha256 + ports: + - "8080:8080" + depends_on: + db: + condition: service_healthy + +volumes: + db_data: diff --git a/backend/petshop-api.postman_collection.json b/backend/petshop-api.postman_collection.json new file mode 100644 index 00000000..d95a5b86 --- /dev/null +++ b/backend/petshop-api.postman_collection.json @@ -0,0 +1,4471 @@ +{ + "info": { + "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": [ + { + "key": "baseUrl", + "value": "http://localhost:8080" + }, + { + "key": "customerToken", + "value": "" + }, + { + "key": "staffToken", + "value": "" + }, + { + "key": "adminToken", + "value": "" + }, + { + "key": "petId", + "value": "" + }, + { + "key": "productId", + "value": "" + }, + { + "key": "saleId", + "value": "" + }, + { + "key": "customerSaleId", + "value": "" + }, + { + "key": "serviceId", + "value": "1" + }, + { + "key": "categoryId", + "value": "1" + }, + { + "key": "appointmentId", + "value": "" + }, + { + "key": "adoptionId", + "value": "" + }, + { + "key": "refundId", + "value": "" + }, + { + "key": "conversationId", + "value": "" + }, + { + "key": "customerId", + "value": "1" + }, + { + "key": "disposableCustomerId", + "value": "" + }, + { + "key": "userId", + "value": "" + }, + { + "key": "storeId", + "value": "1" + }, + { + "key": "inventoryId", + "value": "1" + }, + { + "key": "supplierId", + "value": "1" + }, + { + "key": "avatarFile", + "value": "postman/avatar.png" + }, + { + "key": "bulkPetId", + "value": "" + }, + { + "key": "bulkServiceId", + "value": "" + }, + { + "key": "bulkCategoryId", + "value": "" + }, + { + "key": "bulkCustomerId", + "value": "" + }, + { + "key": "bulkUserId", + "value": "" + }, + { + "key": "bulkStoreId", + "value": "" + }, + { + "key": "bulkInventoryId", + "value": "" + } + ], + "item": [ + { + "name": "Health", + "item": [ + { + "name": "Health Check", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/health", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ] + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});" + ] + } + } + ] + } + ] + }, + { + "name": "Authentication", + "item": [ + { + "name": "Register Customer", + "request": { + "method": "POST", + "url": "{{baseUrl}}/api/v1/auth/register", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"username\": \"newcustomer{{$timestamp}}\",\n \"password\": \"password123\",\n \"email\": \"new{{$timestamp}}@example.com\",\n \"phone\": \"+1-555-01{{$randomInt}}\",\n \"fullName\": \"New Customer\"\n}" + } + }, + "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.token) pm.collectionVariables.set('customerToken', jsonData.token);" + ] + } + } + ] + }, + { + "name": "Login as Customer", + "request": { + "method": "POST", + "url": "{{baseUrl}}/api/v1/auth/login", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"username\": \"customer\",\n \"password\": \"customer123\"\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "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 as Staff", + "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}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "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 as Admin", + "request": { + "method": "POST", + "url": "{{baseUrl}}/api/v1/auth/login", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"username\": \"admin\",\n \"password\": \"admin123\"\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "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": "Get My Profile", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/auth/me", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{customerToken}}", + "type": "text" + } + ] + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});" + ] + } + } + ] + }, + { + "name": "Update My Profile", + "request": { + "method": "PUT", + "url": "{{baseUrl}}/api/v1/auth/me", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{customerToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"fullName\": \"Updated Name\",\n \"email\": \"updated@example.com\"\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});" + ] + } + } + ] + }, + { + "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": "{{avatarFile}}" + } + ] + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});", + "var jsonData = pm.response.json();", + "pm.expect(jsonData.avatarUrl).to.be.a('string');" + ] + } + } + ] + }, + { + "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" + } + ] + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});", + "var jsonData = pm.response.json();", + "pm.expect(jsonData.avatarUrl).to.be.a('string');" + ] + } + } + ] + }, + { + "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" + } + ] + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});" + ] + } + } + ] + }, + { + "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" + } + ] + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});" + ] + } + } + ] + } + ] + }, + { + "name": "Pets", + "item": [ + { + "name": "Get All Pets", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/pets", + "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": "Get Pet by ID", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/pets/1", + "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": "Get Pets Dropdown", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/dropdowns/pets", + "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": { + "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\": \"Postman Pet\",\n \"petSpecies\": \"Dog\",\n \"petBreed\": \"Mixed\",\n \"petAge\": 2,\n \"petStatus\": \"Available\",\n \"petPrice\": 350.00\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.petId !== undefined) pm.collectionVariables.set('petId', jsonData.petId);" + ] + } + } + ] + }, + { + "name": "Update Pet", + "request": { + "method": "PUT", + "url": "{{baseUrl}}/api/v1/pets/{{petId}}", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"petName\": \"Postman Pet Updated\",\n \"petSpecies\": \"Dog\",\n \"petBreed\": \"Mixed\",\n \"petAge\": 3,\n \"petStatus\": \"Available\",\n \"petPrice\": 375.00\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 Pet", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/v1/pets/{{petId}}", + "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 204', function () {", + " pm.response.to.have.status(204);", + "});" + ] + } + } + ] + }, + { + "name": "Create Pet For Bulk Delete", + "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\": \"Postman Pet\",\n \"petSpecies\": \"Dog\",\n \"petBreed\": \"Mixed\",\n \"petAge\": 2,\n \"petStatus\": \"Available\",\n \"petPrice\": 350.00\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.petId !== undefined) pm.collectionVariables.set('bulkPetId', jsonData.petId);" + ] + } + } + ] + }, + { + "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 {{bulkPetId}}\n ]\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 204', function () {", + " pm.response.to.have.status(204);", + "});" + ] + } + } + ] + } + ] + }, + { + "name": "Products", + "item": [ + { + "name": "Get All Products", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/products", + "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": "Get Product by ID", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/products/1", + "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": "Get Products Dropdown", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/dropdowns/products", + "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 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\": \"Postman Product\",\n \"categoryId\": {{categoryId}},\n \"prodDesc\": \"Created from Postman\",\n \"prodPrice\": 19.99\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.prodId !== undefined) pm.collectionVariables.set('productId', jsonData.prodId);" + ] + } + } + ] + }, + { + "name": "Update Product", + "request": { + "method": "PUT", + "url": "{{baseUrl}}/api/v1/products/{{productId}}", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"prodName\": \"Postman Product Updated\",\n \"categoryId\": {{categoryId}},\n \"prodDesc\": \"Updated from Postman\",\n \"prodPrice\": 24.99\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 Product", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/v1/products/{{productId}}", + "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 403', function () {", + " pm.response.to.have.status(403);", + "});" + ] + } + } + ] + }, + { + "name": "Bulk Delete Products", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/v1/products", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"ids\": [\n 1\n ]\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 403', function () {", + " pm.response.to.have.status(403);", + "});" + ] + } + } + ] + } + ] + }, + { + "name": "Sales", + "item": [ + { + "name": "Get All Sales", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/sales", + "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": "Get Sale by ID", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/sales/1", + "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 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 \"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}" + } + }, + "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.saleId !== undefined) {", + " pm.collectionVariables.set('saleId', jsonData.saleId);", + " pm.collectionVariables.set('customerSaleId', jsonData.saleId);", + "}" + ] + } + } + ] + } + ] + }, + { + "name": "Services", + "item": [ + { + "name": "Get All Services", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/services", + "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": "Get Service by ID", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/services/1", + "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": "Get Services Dropdown", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/dropdowns/services", + "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 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\": \"Postman Service\",\n \"serviceDesc\": \"Created from Postman\",\n \"serviceDuration\": 30,\n \"servicePrice\": 25.00\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.serviceId !== undefined) pm.collectionVariables.set('serviceId', jsonData.serviceId);" + ] + } + } + ] + }, + { + "name": "Update Service", + "request": { + "method": "PUT", + "url": "{{baseUrl}}/api/v1/services/{{serviceId}}", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"serviceName\": \"Postman Service Updated\",\n \"serviceDesc\": \"Updated from Postman\",\n \"serviceDuration\": 30,\n \"servicePrice\": 30.00\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 Service", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/v1/services/{{serviceId}}", + "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 204', function () {", + " pm.response.to.have.status(204);", + "});" + ] + } + } + ] + }, + { + "name": "Create Service For Bulk Delete", + "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\": \"Postman Service\",\n \"serviceDesc\": \"Created from Postman\",\n \"serviceDuration\": 30,\n \"servicePrice\": 25.00\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.serviceId !== undefined) pm.collectionVariables.set('bulkServiceId', jsonData.serviceId);" + ] + } + } + ] + }, + { + "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 {{bulkServiceId}}\n ]\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 204', function () {", + " pm.response.to.have.status(204);", + "});" + ] + } + } + ] + } + ] + }, + { + "name": "Categories", + "item": [ + { + "name": "Get All Categories", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/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": "Get Category by ID", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/categories/1", + "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": "Get Categories Dropdown", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/dropdowns/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": { + "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\": \"Postman Category\",\n \"categoryType\": \"Product\"\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.categoryId !== undefined) pm.collectionVariables.set('categoryId', jsonData.categoryId);" + ] + } + } + ] + }, + { + "name": "Update Category", + "request": { + "method": "PUT", + "url": "{{baseUrl}}/api/v1/categories/{{categoryId}}", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"categoryName\": \"Postman Category Updated\",\n \"categoryType\": \"Product\"\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 Category", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/v1/categories/{{categoryId}}", + "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 204', function () {", + " pm.response.to.have.status(204);", + "});" + ] + } + } + ] + }, + { + "name": "Create Category For Bulk Delete", + "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\": \"Postman Category\",\n \"categoryType\": \"Product\"\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.categoryId !== undefined) pm.collectionVariables.set('bulkCategoryId', jsonData.categoryId);" + ] + } + } + ] + }, + { + "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 {{bulkCategoryId}}\n ]\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 204', function () {", + " pm.response.to.have.status(204);", + "});" + ] + } + } + ] + } + ] + }, + { + "name": "Appointments", + "item": [ + { + "name": "Check Appointment Availability", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/appointments/availability?storeId=1&serviceId=1&date=2026-12-20", + "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": "List Appointments", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/appointments", + "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": "Get Appointment", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/appointments/1", + "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 Appointment", + "request": { + "method": "POST", + "url": "{{baseUrl}}/api/v1/appointments", + "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-12-20\",\n \"appointmentTime\": \"10:00:00\",\n \"appointmentStatus\": \"Booked\",\n \"petIds\": [1]\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.appointmentId !== undefined) pm.collectionVariables.set('appointmentId', jsonData.appointmentId);" + ] + } + } + ] + }, + { + "name": "Update Appointment", + "request": { + "method": "PUT", + "url": "{{baseUrl}}/api/v1/appointments/{{appointmentId}}", + "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-12-20\",\n \"appointmentTime\": \"11:00:00\",\n \"appointmentStatus\": \"Booked\",\n \"petIds\": [1]\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 Appointment", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/v1/appointments/{{appointmentId}}", + "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 204', function () {", + " pm.response.to.have.status(204);", + "});" + ] + } + } + ] + }, + { + "name": "Bulk Delete Appointments", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/v1/appointments", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"ids\": [\n 1\n ]\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 403', function () {", + " pm.response.to.have.status(403);", + "});" + ] + } + } + ] + } + ] + }, + { + "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 {{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": "Get Adoption", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/adoptions/1", + "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 Adoption", + "request": { + "method": "POST", + "url": "{{baseUrl}}/api/v1/adoptions", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"petId\": 3,\n \"customerId\": 1,\n \"adoptionDate\": \"2026-12-21\",\n \"adoptionStatus\": \"Pending\"\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.adoptionId !== undefined) pm.collectionVariables.set('adoptionId', jsonData.adoptionId);" + ] + } + } + ] + }, + { + "name": "Update Adoption", + "request": { + "method": "PUT", + "url": "{{baseUrl}}/api/v1/adoptions/{{adoptionId}}", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"petId\": 3,\n \"customerId\": 1,\n \"adoptionDate\": \"2026-12-22\",\n \"adoptionStatus\": \"Completed\"\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 Adoption", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/v1/adoptions/{{adoptionId}}", + "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 204', function () {", + " pm.response.to.have.status(204);", + "});" + ] + } + } + ] + }, + { + "name": "Bulk Delete Adoptions", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/v1/adoptions", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"ids\": [\n 1\n ]\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 403', function () {", + " pm.response.to.have.status(403);", + "});" + ] + } + } + ] + } + ] + }, + { + "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 {{staffToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"saleId\": 1,\n \"reason\": \"Defective product\"\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 !== undefined) pm.collectionVariables.set('refundId', jsonData.id);" + ] + } + } + ] + }, + { + "name": "List Refunds", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/refunds", + "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": "Get Refund", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/refunds/{{refundId}}", + "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": "Update Refund", + "request": { + "method": "PUT", + "url": "{{baseUrl}}/api/v1/refunds/{{refundId}}", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"status\": \"APPROVED\"\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 Refund", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/v1/refunds/{{refundId}}", + "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 403', function () {", + " pm.response.to.have.status(403);", + "});" + ] + } + } + ] + } + ] + }, + { + "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}", + "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();", + "pm.expect(jsonData.id).to.exist;", + "pm.collectionVariables.set('conversationId', jsonData.id);" + ] + } + } + ] + }, + { + "name": "Customer 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" + } + ] + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});" + ] + } + } + ] + }, + { + "name": "Staff List Conversations", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/chat/conversations", + "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": "Get Conversation", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/chat/conversations/{{conversationId}}", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{customerToken}}", + "type": "text" + } + ] + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});" + ] + } + } + ] + }, + { + "name": "Customer Request Human", + "request": { + "method": "POST", + "url": "{{baseUrl}}/api/v1/chat/conversations/{{conversationId}}/request-human", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{customerToken}}", + "type": "text" + } + ] + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});", + "var jsonData = pm.response.json();", + "pm.expect(jsonData.mode).to.eql('AUTOMATED');" + ] + } + } + ] + }, + { + "name": "Customer Send Message", + "request": { + "method": "POST", + "url": "{{baseUrl}}/api/v1/chat/conversations/{{conversationId}}/messages", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{customerToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"content\": \"Customer follow-up from Postman\"\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);", + "});" + ] + } + } + ] + }, + { + "name": "Staff Send Message", + "request": { + "method": "POST", + "url": "{{baseUrl}}/api/v1/chat/conversations/{{conversationId}}/messages", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"content\": \"Reply from Postman\"\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();", + "pm.expect(jsonData.senderId).to.exist;", + "pm.expect(jsonData.content).to.eql('Reply from Postman');" + ] + } + } + ] + }, + { + "name": "Customer Get Messages", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/chat/conversations/{{conversationId}}/messages", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{customerToken}}", + "type": "text" + } + ] + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});", + "var jsonData = pm.response.json();", + "pm.expect(jsonData.length).to.be.above(0);" + ] + } + } + ] + }, + { + "name": "Staff Get Messages", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/chat/conversations/{{conversationId}}/messages", + "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);", + "});", + "var jsonData = pm.response.json();", + "pm.expect(jsonData.length).to.be.above(0);" + ] + } + } + ] + } + ] + }, + { + "name": "Customers", + "item": [ + { + "name": "Get Customers Dropdown", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/dropdowns/customers", + "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": "List Customers", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/customers", + "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": "Get Customer", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/customers/{{customerId}}", + "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 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\": \"Postman\",\n \"lastName\": \"Customer\",\n \"email\": \"postman.customer.{{$guid}}@example.com\",\n \"phone\": \"555-100-2000\"\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.customerId !== undefined) pm.collectionVariables.set('customerId', jsonData.customerId);" + ] + } + } + ] + }, + { + "name": "Update Customer", + "request": { + "method": "PUT", + "url": "{{baseUrl}}/api/v1/customers/{{customerId}}", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"firstName\": \"Postman\",\n \"lastName\": \"Customer Updated\",\n \"email\": \"postman.customer.updated.{{$timestamp}}@example.com\",\n \"phone\": \"555-100-2001\"\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 Customer", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/v1/customers/{{customerId}}", + "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 204', function () {", + " pm.response.to.have.status(204);", + "});" + ] + } + } + ] + }, + { + "name": "Create Customer For Bulk Delete", + "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\": \"Postman\",\n \"lastName\": \"Customer\",\n \"email\": \"postman.customer.{{$guid}}@example.com\",\n \"phone\": \"555-100-2000\"\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.customerId !== undefined) pm.collectionVariables.set('bulkCustomerId', jsonData.customerId);" + ] + } + } + ] + }, + { + "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 {{bulkCustomerId}}\n ]\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 204', function () {", + " pm.response.to.have.status(204);", + "});" + ] + } + } + ] + } + ] + }, + { + "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" + } + ] + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});" + ] + } + } + ] + }, + { + "name": "List Staff Users", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/users?role=STAFF", + "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);", + "});", + "var jsonData = pm.response.json();", + "pm.test('All returned users are STAFF', function () {", + " pm.expect(jsonData.content.every(function (user) { return user.role === 'STAFF'; })).to.be.true;", + "});" + ] + } + } + ] + }, + { + "name": "List Admin Users", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/users?role=ADMIN", + "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);", + "});", + "var jsonData = pm.response.json();", + "pm.test('All returned users are ADMIN', function () {", + " pm.expect(jsonData.content.every(function (user) { return user.role === 'ADMIN'; })).to.be.true;", + "});" + ] + } + } + ] + }, + { + "name": "List Customer Users", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/users?role=CUSTOMER", + "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);", + "});", + "var jsonData = pm.response.json();", + "pm.test('All returned users are CUSTOMER', function () {", + " pm.expect(jsonData.content.every(function (user) { return user.role === 'CUSTOMER'; })).to.be.true;", + "});" + ] + } + } + ] + }, + { + "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" + } + ] + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});" + ] + } + } + ] + }, + { + "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\": \"postman_user_{{$guid}}\",\n \"password\": \"secret123\",\n \"fullName\": \"Postman User\",\n \"email\": \"postman.user.{{$guid}}@example.com\",\n \"role\": \"STAFF\",\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 !== undefined) pm.collectionVariables.set('userId', jsonData.id);" + ] + } + } + ] + }, + { + "name": "Update User", + "request": { + "method": "PUT", + "url": "{{baseUrl}}/api/v1/users/{{userId}}", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"username\": \"postman_user_{{$timestamp}}\",\n \"password\": \"secret123\",\n \"fullName\": \"Postman User Updated\",\n \"email\": \"postman.user.updated.{{$timestamp}}@example.com\",\n \"role\": \"STAFF\",\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 User", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/v1/users/{{userId}}", + "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": "Create User For Bulk Delete", + "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\": \"postman_user_{{$guid}}\",\n \"password\": \"secret123\",\n \"fullName\": \"Postman User\",\n \"email\": \"postman.user.{{$guid}}@example.com\",\n \"role\": \"STAFF\",\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 !== undefined) pm.collectionVariables.set('bulkUserId', jsonData.id);" + ] + } + } + ] + }, + { + "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 {{bulkUserId}}\n ]\n}", + "options": { + "raw": { + "language": "json" + } + } + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 204', function () {", + " pm.response.to.have.status(204);", + "});" + ] + } + } + ] + } + ] + }, + { + "name": "Stores", + "item": [ + { + "name": "Get Stores Dropdown", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/dropdowns/stores", + "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": "List Stores", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/stores", + "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 Store", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/stores/{{storeId}}", + "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 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\": \"Postman Store {{$timestamp}}\",\n \"address\": \"100 Postman Ave\",\n \"phone\": \"555-200-3000\",\n \"email\": \"postman.store.{{$guid}}@example.com\"\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.storeId !== undefined) pm.collectionVariables.set('storeId', jsonData.storeId);" + ] + } + } + ] + }, + { + "name": "Update Store", + "request": { + "method": "PUT", + "url": "{{baseUrl}}/api/v1/stores/{{storeId}}", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"storeName\": \"Postman Store Updated\",\n \"address\": \"101 Postman Ave\",\n \"phone\": \"555-200-3001\",\n \"email\": \"postman.store.updated@example.com\"\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 Store", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/v1/stores/{{storeId}}", + "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": "Create Store For Bulk Delete", + "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\": \"Postman Store {{$timestamp}}\",\n \"address\": \"100 Postman Ave\",\n \"phone\": \"555-200-3000\",\n \"email\": \"postman.store.{{$guid}}@example.com\"\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.storeId !== undefined) pm.collectionVariables.set('bulkStoreId', jsonData.storeId);" + ] + } + } + ] + }, + { + "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 {{bulkStoreId}}\n ]\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 204', function () {", + " pm.response.to.have.status(204);", + "});" + ] + } + } + ] + } + ] + }, + { + "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" + } + ] + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});" + ] + } + } + ] + }, + { + "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" + } + ] + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});" + ] + } + } + ] + }, + { + "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 \"prodId\": {{productId}},\n \"quantity\": 10\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.inventoryId !== undefined) pm.collectionVariables.set('inventoryId', jsonData.inventoryId);" + ] + } + } + ] + }, + { + "name": "Update Inventory", + "request": { + "method": "PUT", + "url": "{{baseUrl}}/api/v1/inventory/{{inventoryId}}", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"prodId\": {{productId}},\n \"quantity\": 12\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 Inventory", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/v1/inventory/{{inventoryId}}", + "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": "Create Inventory For Bulk Delete", + "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 \"prodId\": {{productId}},\n \"quantity\": 10\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.inventoryId !== undefined) pm.collectionVariables.set('bulkInventoryId', jsonData.inventoryId);" + ] + } + } + ] + }, + { + "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 {{bulkInventoryId}}\n ]\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 204', function () {", + " pm.response.to.have.status(204);", + "});" + ] + } + } + ] + } + ] + }, + { + "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" + } + ] + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});" + ] + } + } + ] + }, + { + "name": "Get Supplier", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/suppliers/{{supplierId}}", + "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 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\": \"Postman Supplier {{$timestamp}}\",\n \"supContactFirstName\": \"Post\",\n \"supContactLastName\": \"Man\",\n \"supEmail\": \"postman.supplier.{{$guid}}@example.com\",\n \"supPhone\": \"555-300-4000\"\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.supId !== undefined) pm.collectionVariables.set('supplierId', jsonData.supId);" + ] + } + } + ] + }, + { + "name": "Update Supplier", + "request": { + "method": "PUT", + "url": "{{baseUrl}}/api/v1/suppliers/{{supplierId}}", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"supCompany\": \"Postman Supplier Updated {{$timestamp}}\",\n \"supContactFirstName\": \"Post\",\n \"supContactLastName\": \"Man\",\n \"supEmail\": \"postman.supplier.updated.{{$timestamp}}@example.com\",\n \"supPhone\": \"555-300-4001\"\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 Supplier", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/v1/suppliers/{{supplierId}}", + "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 403', function () {", + " pm.response.to.have.status(403);", + "});" + ] + } + } + ] + }, + { + "name": "Bulk Delete Suppliers", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/v1/suppliers", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"ids\": [\n 1\n ]\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 403', function () {", + " pm.response.to.have.status(403);", + "});" + ] + } + } + ] + }, + { + "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" + } + ] + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});" + ] + } + } + ] + } + ] + }, + { + "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" + } + ] + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});" + ] + } + } + ] + }, + { + "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" + } + ] + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});" + ] + } + } + ] + } + ] + }, + { + "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" + } + ] + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});" + ] + } + } + ] + }, + { + "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" + } + ] + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});" + ] + } + } + ] + }, + { + "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\": {{productId}},\n \"supplierId\": {{supplierId}},\n \"cost\": 7.50\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);", + "});" + ] + } + } + ] + }, + { + "name": "Update Product Supplier", + "request": { + "method": "PUT", + "url": "{{baseUrl}}/api/v1/product-suppliers/{{productId}}/{{supplierId}}", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"productId\": {{productId}},\n \"supplierId\": {{supplierId}},\n \"cost\": 7.75\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 Product Supplier", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/v1/product-suppliers/{{productId}}/{{supplierId}}", + "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": "Create Product Supplier For Bulk Delete", + "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\": {{productId}},\n \"supplierId\": {{supplierId}},\n \"cost\": 7.50\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);", + "});" + ] + } + } + ] + }, + { + "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 \"keys\": [\n {\"productId\": {{productId}}, \"supplierId\": {{supplierId}}}\n ]\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 204', function () {", + " pm.response.to.have.status(204);", + "});" + ] + } + } + ] + } + ] + }, + { + "name": "Analytics", + "item": [ + { + "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" + } + ] + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});" + ] + } + } + ] + } + ] + } + ] +} diff --git a/backend/pom.xml b/backend/pom.xml new file mode 100644 index 00000000..de496351 --- /dev/null +++ b/backend/pom.xml @@ -0,0 +1,199 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 4.0.3 + + + + com.petshop + backend + 1.0.0 + PetShop Backend + Spring Boot backend for PetShop desktop application + + + 25 + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-validation + + + + org.springframework.boot + spring-boot-starter-websocket + + + + com.mysql + mysql-connector-j + runtime + + + + org.flywaydb + flyway-core + + + + org.flywaydb + flyway-mysql + + + + io.jsonwebtoken + jjwt-api + 0.12.3 + + + + io.jsonwebtoken + jjwt-impl + 0.12.3 + runtime + + + + io.jsonwebtoken + jjwt-jackson + 0.12.3 + runtime + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 3.0.1 + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 25 + + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.5.0 + + + require-java-25 + + enforce + + + + + [25,) + JDK 25 or newer is required. Configure IntelliJ and Maven to use JDK 25 before running the backend. + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.codehaus.mojo + exec-maven-plugin + 3.1.0 + + + dev-stack + + java + + + com.petshop.backend.DevStackApplication + runtime + + + + reset-db + + java + + + com.petshop.backend.ResetDatabaseApplication + runtime + + + + docker-up + + exec + + + docker + + compose + -f + docker-compose.dev.yml + up + -d + --wait + db + + + + + docker-down + + exec + + + docker + + compose + -f + docker-compose.dev.yml + down + -v + --remove-orphans + + + + + + + + diff --git a/backend/postman/avatar.png b/backend/postman/avatar.png new file mode 100644 index 00000000..05feaba1 Binary files /dev/null and b/backend/postman/avatar.png differ diff --git a/backend/src/main/java/com/petshop/backend/BackendApplication.java b/backend/src/main/java/com/petshop/backend/BackendApplication.java new file mode 100644 index 00000000..87268e9a --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/BackendApplication.java @@ -0,0 +1,17 @@ +package com.petshop.backend; + +import com.petshop.backend.config.FlywayContextInitializer; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.web.config.EnableSpringDataWebSupport; + +@SpringBootApplication +@EnableSpringDataWebSupport(pageSerializationMode = EnableSpringDataWebSupport.PageSerializationMode.VIA_DTO) +public class BackendApplication { + public static void main(String[] args) { + RuntimeClasspathValidator.validate(); + new SpringApplicationBuilder(BackendApplication.class) + .initializers(new FlywayContextInitializer()) + .run(args); + } +} diff --git a/backend/src/main/java/com/petshop/backend/DevStackApplication.java b/backend/src/main/java/com/petshop/backend/DevStackApplication.java new file mode 100644 index 00000000..38846aaa --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/DevStackApplication.java @@ -0,0 +1,177 @@ +package com.petshop.backend; + +import com.petshop.backend.config.FlywayContextInitializer; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.web.server.PortInUseException; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.event.ContextClosedEvent; + +import java.net.BindException; +import java.time.Duration; +import java.util.Locale; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +public class DevStackApplication { + + private static final Duration WATCH_INTERVAL = Duration.ofSeconds(2); + + public static void main(String[] args) { + DockerComposeSupport docker = new DockerComposeSupport(); + ConfigurableApplicationContext context = null; + CountDownLatch shutdownLatch = new CountDownLatch(1); + ScheduledExecutorService scheduler = null; + AtomicBoolean shuttingDown = new AtomicBoolean(false); + RuntimeException startupFailure = null; + + try { + validateJavaVersion(); + RuntimeClasspathValidator.validate(); + docker.ensureDockerAvailable(); + docker.startDatabase(); + context = new SpringApplicationBuilder(BackendApplication.class) + .initializers(new FlywayContextInitializer()) + .run(args); + context.addApplicationListener(event -> { + if (event instanceof ContextClosedEvent) { + shutdownLatch.countDown(); + } + }); + scheduler = startDatabaseWatch(docker, context, shuttingDown); + shutdownLatch.await(); + } catch (RuntimeException ex) { + startupFailure = ex; + throw new IllegalStateException(describeStartupFailure(ex), ex); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + throw new IllegalStateException("Dev stack interrupted", ex); + } finally { + if (scheduler != null) { + scheduler.shutdownNow(); + } + if (context != null && context.isActive()) { + context.close(); + } + if (!shuttingDown.get()) { + try { + docker.stopDatabase(); + } catch (RuntimeException stopFailure) { + if (startupFailure != null) { + System.err.println(stopFailure.getMessage()); + } else { + throw stopFailure; + } + } + } + } + } + + private static void validateJavaVersion() { + int feature = Runtime.version().feature(); + if (feature < 25) { + throw new IllegalStateException("JDK 25 or newer is required. IntelliJ is currently using Java " + Runtime.version() + "."); + } + } + + private static ScheduledExecutorService startDatabaseWatch(DockerComposeSupport docker, ConfigurableApplicationContext context, AtomicBoolean shuttingDown) { + ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + scheduler.scheduleWithFixedDelay(() -> { + if (shuttingDown.get()) { + return; + } + if (!docker.isDatabaseRunning()) { + shuttingDown.set(true); + if (context.isActive()) { + context.close(); + } + } + }, WATCH_INTERVAL.toSeconds(), WATCH_INTERVAL.toSeconds(), TimeUnit.SECONDS); + return scheduler; + } + + private static String describeStartupFailure(RuntimeException ex) { + Throwable cause = deepestCause(ex); + String message = cause.getMessage(); + String lowerMessage = ""; + if (message != null) { + lowerMessage = message.toLowerCase(Locale.ROOT); + } + + switch (classifyStartupFailure(cause, lowerMessage)) { + case SERVER_PORT_IN_USE: + return "Backend startup failed because port 8080 is already in use. Stop the other process or set SERVER_PORT to a different value."; + case DOCKER_NOT_RUNNING: + return "Backend startup failed because Docker is not available. Start Docker Desktop and try again."; + case DOCKER_NOT_INSTALLED: + return "Backend startup failed because Docker is not installed or not on PATH. Install Docker Desktop and reopen IntelliJ."; + case DATABASE_PORT_IN_USE: + return "Backend startup failed because port 3306 is already in use. Stop the conflicting MySQL service or container, then run Reset Database and try again."; + case STALE_SCHEMA: + return "Backend startup failed because the database schema is stale or incomplete. Run Reset Database and then start Pet Shop Application again."; + case FLYWAY_MIGRATION_FAILED: + return "Backend startup failed during Flyway migration. Run Reset Database and check the database logs if the problem persists."; + case DATASOURCE_CONFIG_MISSING: + return "Backend startup failed because datasource configuration was not loaded. Reload the Maven project and rerun Pet Shop Application."; + case DATABASE_UNREACHABLE: + return "Backend startup failed because it could not connect to MySQL. Make sure Docker Desktop is running, the database container is healthy, and port 3306 is reachable."; + case UNKNOWN: + if (message == null || message.isBlank()) { + return "Backend startup failed: " + ex.getClass().getSimpleName(); + } + return "Backend startup failed: " + message; + default: + throw new IllegalStateException("Unhandled startup failure classification"); + } + } + + private static StartupFailure classifyStartupFailure(Throwable cause, String lowerMessage) { + if (cause instanceof PortInUseException || cause instanceof BindException || lowerMessage.contains("port 8080") && lowerMessage.contains("already in use")) { + return StartupFailure.SERVER_PORT_IN_USE; + } + if (lowerMessage.contains("docker desktop") || lowerMessage.contains("docker daemon") || lowerMessage.contains("docker engine") || lowerMessage.contains("cannot connect to the docker daemon")) { + return StartupFailure.DOCKER_NOT_RUNNING; + } + if (lowerMessage.contains("docker executable") || lowerMessage.contains("no such file or directory") && lowerMessage.contains("docker")) { + return StartupFailure.DOCKER_NOT_INSTALLED; + } + if (lowerMessage.contains("port is already allocated") || lowerMessage.contains("address already in use") && lowerMessage.contains("3306")) { + return StartupFailure.DATABASE_PORT_IN_USE; + } + if (lowerMessage.contains("schema validation: missing table")) { + return StartupFailure.STALE_SCHEMA; + } + if (lowerMessage.contains("flyway") || lowerMessage.contains("migration")) { + return StartupFailure.FLYWAY_MIGRATION_FAILED; + } + if (lowerMessage.contains("datasource properties are required")) { + return StartupFailure.DATASOURCE_CONFIG_MISSING; + } + if (lowerMessage.contains("access denied for user") || lowerMessage.contains("communications link failure") || lowerMessage.contains("connection refused") || lowerMessage.contains("unable to determine dialect without jdbc metadata")) { + return StartupFailure.DATABASE_UNREACHABLE; + } + return StartupFailure.UNKNOWN; + } + + private static Throwable deepestCause(Throwable throwable) { + Throwable current = throwable; + while (current.getCause() != null && current.getCause() != current) { + current = current.getCause(); + } + return current; + } + + private enum StartupFailure { + SERVER_PORT_IN_USE, + DOCKER_NOT_RUNNING, + DOCKER_NOT_INSTALLED, + DATABASE_PORT_IN_USE, + STALE_SCHEMA, + FLYWAY_MIGRATION_FAILED, + DATASOURCE_CONFIG_MISSING, + DATABASE_UNREACHABLE, + UNKNOWN + } +} diff --git a/backend/src/main/java/com/petshop/backend/DockerComposeSupport.java b/backend/src/main/java/com/petshop/backend/DockerComposeSupport.java new file mode 100644 index 00000000..1ff43a77 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/DockerComposeSupport.java @@ -0,0 +1,173 @@ +package com.petshop.backend; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +final class DockerComposeSupport { + + private final Path projectDir = Paths.get("").toAbsolutePath(); + + void ensureDockerAvailable() { + CommandResult versionResult = runCommand(List.of(resolveDockerExecutable(), "version"), false); + if (versionResult.exitCode != 0) { + throw new IllegalStateException(describeDockerFailure(versionResult.output)); + } + } + + void startDatabase() { + runCommand(composeCommand("up", "-d", "--wait", "db")); + } + + void stopDatabase() { + runCommand(composeCommand("stop", "db")); + } + + void resetDatabase() { + runCommand(composeCommand("down", "-v", "--remove-orphans")); + String volumeName = getDatabaseVolumeName(); + if (volumeExists(volumeName)) { + runCommand(List.of(resolveDockerExecutable(), "volume", "rm", "-f", volumeName)); + } + } + + boolean isDatabaseRunning() { + CommandResult result = runCommand(composeCommand("ps", "--status", "running", "--services", "db"), false); + if (result.exitCode != 0) { + return false; + } + return result.output.lines() + .map(String::trim) + .anyMatch("db"::equals); + } + + private boolean volumeExists(String volumeName) { + CommandResult result = runCommand(List.of(resolveDockerExecutable(), "volume", "ls", "--format", "{{.Name}}"), false); + if (result.exitCode != 0) { + return false; + } + return result.output.lines() + .map(String::trim) + .anyMatch(volumeName::equals); + } + + private String getDatabaseVolumeName() { + return projectDir.getFileName().toString() + "_db_data"; + } + + private List composeCommand(String... args) { + List command = new ArrayList<>(); + command.add(resolveDockerExecutable()); + command.add("compose"); + command.add("-f"); + command.add("docker-compose.dev.yml"); + for (String arg : args) { + command.add(arg); + } + return command; + } + + private String resolveDockerExecutable() { + String os = System.getProperty("os.name", "").toLowerCase(Locale.ROOT); + if (os.contains("win")) { + Path dockerPath = Paths.get("C:", "Program Files", "Docker", "Docker", "resources", "bin", "docker.exe"); + if (Files.isRegularFile(dockerPath)) { + return dockerPath.toString(); + } + Path dockerPathAlt = Paths.get("C:", "Program Files", "Docker", "Docker", "resources", "docker.exe"); + if (Files.isRegularFile(dockerPathAlt)) { + return dockerPathAlt.toString(); + } + return "docker.exe"; + } + return "docker"; + } + + private void runCommand(List command) { + CommandResult result = runCommand(command, true); + if (result.exitCode != 0) { + throw new IllegalStateException(describeCommandFailure(command, result.output)); + } + } + + private CommandResult runCommand(List command, boolean printOutput) { + ProcessBuilder builder = new ProcessBuilder(command); + builder.directory(projectDir.toFile()); + builder.redirectErrorStream(true); + + try { + Process process = builder.start(); + StringBuilder output = new StringBuilder(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + String line; + while ((line = reader.readLine()) != null) { + output.append(line).append(System.lineSeparator()); + if (printOutput) { + System.out.println(line); + } + } + } + int exitCode = process.waitFor(); + return new CommandResult(exitCode, output.toString().trim()); + } catch (IOException ex) { + String executable = command.isEmpty() ? "docker" : command.getFirst(); + throw new IllegalStateException("Unable to run " + executable + ". Install Docker Desktop and make sure it is available on PATH.", ex); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + throw new IllegalStateException("Docker command interrupted", ex); + } + } + + private String describeDockerFailure(String output) { + String lowerOutput = ""; + if (output != null) { + lowerOutput = output.toLowerCase(Locale.ROOT); + } + if (lowerOutput.contains("docker desktop") || lowerOutput.contains("docker daemon") || lowerOutput.contains("docker engine") || lowerOutput.contains("cannot connect") || lowerOutput.contains("failed to connect") || lowerOutput.contains("docker_api") || lowerOutput.contains("docker api") || lowerOutput.contains("docker_engine") || lowerOutput.contains("pipe/docker_engine") || lowerOutput.contains("npipe")) { + return "Docker Desktop is not running. Start Docker Desktop and rerun the command."; + } + if (output == null || output.isBlank()) { + return "Docker is unavailable. Start Docker Desktop and rerun the command."; + } + return output; + } + + private String describeCommandFailure(List command, String output) { + String renderedCommand = String.join(" ", command); + String lowerOutput = ""; + if (output != null) { + lowerOutput = output.toLowerCase(Locale.ROOT); + } + if (renderedCommand.contains(" up ") || renderedCommand.endsWith(" up -d --wait db")) { + if (lowerOutput.contains("port is already allocated") || lowerOutput.contains("address already in use")) { + return "Database startup failed because port 3306 is already in use. Stop the conflicting MySQL service or container, then run Reset Database."; + } + if (lowerOutput.contains("docker desktop") || lowerOutput.contains("docker daemon") || lowerOutput.contains("docker engine")) { + return "Database startup failed because Docker Desktop is not running."; + } + if (output == null || output.isBlank()) { + return "Database startup failed while bringing up the Docker MySQL container."; + } + return output; + } + if (renderedCommand.contains(" volume rm ")) { + if (output == null || output.isBlank()) { + return "Database reset failed while removing the Docker volume."; + } + return output; + } + if (output == null || output.isBlank()) { + return "Command failed: " + renderedCommand; + } + return output; + } + + private record CommandResult(int exitCode, String output) { + } +} diff --git a/backend/src/main/java/com/petshop/backend/ResetDatabaseApplication.java b/backend/src/main/java/com/petshop/backend/ResetDatabaseApplication.java new file mode 100644 index 00000000..056ec36e --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/ResetDatabaseApplication.java @@ -0,0 +1,10 @@ +package com.petshop.backend; + +public class ResetDatabaseApplication { + + public static void main(String[] args) { + DockerComposeSupport docker = new DockerComposeSupport(); + docker.ensureDockerAvailable(); + docker.resetDatabase(); + } +} diff --git a/backend/src/main/java/com/petshop/backend/RuntimeClasspathValidator.java b/backend/src/main/java/com/petshop/backend/RuntimeClasspathValidator.java new file mode 100644 index 00000000..ad18d198 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/RuntimeClasspathValidator.java @@ -0,0 +1,24 @@ +package com.petshop.backend; + +import java.net.URL; + +final class RuntimeClasspathValidator { + + private RuntimeClasspathValidator() { + } + + static void validate() { + if (!resourceExists("application.yml")) { + throw new IllegalStateException("Backend resources are missing from the runtime classpath. Reimport the Maven project in IntelliJ and run the shared Maven run configuration."); + } + if (!resourceExists("db/migration/V1__baseline_schema.sql")) { + throw new IllegalStateException("Flyway migration files are missing from the runtime classpath. Reimport the Maven project in IntelliJ and run the shared Maven run configuration."); + } + } + + private static boolean resourceExists(String path) { + ClassLoader classLoader = RuntimeClasspathValidator.class.getClassLoader(); + URL resource = classLoader.getResource(path); + return resource != null; + } +} diff --git a/backend/src/main/java/com/petshop/backend/config/DataInitializer.java b/backend/src/main/java/com/petshop/backend/config/DataInitializer.java new file mode 100644 index 00000000..4a8c7470 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/config/DataInitializer.java @@ -0,0 +1,167 @@ +package com.petshop.backend.config; + +import com.petshop.backend.entity.User; +import com.petshop.backend.repository.UserRepository; +import com.petshop.backend.service.StoreAssignmentService; +import com.petshop.backend.service.UserBusinessLinkageService; +import org.springframework.boot.CommandLineRunner; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Component; + +@Component +public class DataInitializer implements CommandLineRunner { + + private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + private final UserBusinessLinkageService userBusinessLinkageService; + private final StoreAssignmentService storeAssignmentService; + + public DataInitializer(UserRepository userRepository, PasswordEncoder passwordEncoder, UserBusinessLinkageService userBusinessLinkageService, StoreAssignmentService storeAssignmentService) { + this.userRepository = userRepository; + this.passwordEncoder = passwordEncoder; + this.userBusinessLinkageService = userBusinessLinkageService; + this.storeAssignmentService = storeAssignmentService; + } + + @Override + public void run(String... args) { + System.out.println("==== DataInitializer: Starting user creation ===="); + + User admin = userRepository.findByUsername("admin").orElse(null); + if (admin == null) { + System.out.println("Creating admin user..."); + admin = new User(); + admin.setUsername("admin"); + admin.setPassword(passwordEncoder.encode("admin123")); + admin.setEmail("admin@petshop.com"); + admin.setFullName("Admin User"); + admin.setPhone("000-000-1000"); + admin.setRole(User.Role.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.getPhone() == null || admin.getPhone().isEmpty()) { + admin.setPhone("000-000-1000"); + 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 + storeAssignmentService.assignStoreIfMissing(userBusinessLinkageService.ensureLinkedEmployee(admin), 1L); + + User staff = userRepository.findByUsername("staff").orElse(null); + if (staff == null) { + System.out.println("Creating staff user..."); + staff = new User(); + staff.setUsername("staff"); + staff.setPassword(passwordEncoder.encode("staff123")); + staff.setEmail("staff@petshop.com"); + staff.setFullName("Staff User"); + staff.setPhone("000-000-1001"); + staff.setRole(User.Role.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.getPhone() == null || staff.getPhone().isEmpty()) { + staff.setPhone("000-000-1001"); + 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 + storeAssignmentService.assignStoreIfMissing(userBusinessLinkageService.ensureLinkedEmployee(staff), 1L); + + User customer = userRepository.findByUsername("customer").orElse(null); + if (customer == null) { + System.out.println("Creating customer user..."); + customer = new User(); + customer.setUsername("customer"); + customer.setPassword(passwordEncoder.encode("customer123")); + customer.setEmail("customer@petshop.com"); + customer.setFullName("Test Customer"); + customer.setPhone("000-000-1002"); + customer.setRole(User.Role.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.getPhone() == null || customer.getPhone().isEmpty()) { + customer.setPhone("000-000-1002"); + 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/backend/src/main/java/com/petshop/backend/config/FlywayContextInitializer.java b/backend/src/main/java/com/petshop/backend/config/FlywayContextInitializer.java new file mode 100644 index 00000000..3fb28d10 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/config/FlywayContextInitializer.java @@ -0,0 +1,40 @@ +package com.petshop.backend.config; + +import org.flywaydb.core.Flyway; +import org.flywaydb.core.api.MigrationVersion; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.env.ConfigurableEnvironment; + +import java.util.Arrays; + +public class FlywayContextInitializer implements ApplicationContextInitializer { + + @Override + public void initialize(ConfigurableApplicationContext applicationContext) { + ConfigurableEnvironment environment = applicationContext.getEnvironment(); + + String url = environment.getProperty("spring.datasource.url"); + String username = environment.getProperty("spring.datasource.username"); + String password = environment.getProperty("spring.datasource.password"); + + if (url == null || username == null || password == null) { + throw new IllegalStateException("Datasource properties are required before startup"); + } + + String[] locations = Arrays.stream(environment + .getProperty("spring.flyway.locations", "classpath:db/migration") + .split(",")) + .map(String::trim) + .filter(location -> !location.isEmpty()) + .toArray(String[]::new); + + Flyway.configure() + .dataSource(url, username, password) + .locations(locations) + .baselineOnMigrate(environment.getProperty("spring.flyway.baseline-on-migrate", Boolean.class, false)) + .baselineVersion(MigrationVersion.fromVersion(environment.getProperty("spring.flyway.baseline-version", "1"))) + .load() + .migrate(); + } +} diff --git a/backend/src/main/java/com/petshop/backend/config/TomcatPathToleranceConfig.java b/backend/src/main/java/com/petshop/backend/config/TomcatPathToleranceConfig.java new file mode 100644 index 00000000..9a89c5ab --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/config/TomcatPathToleranceConfig.java @@ -0,0 +1,19 @@ +package com.petshop.backend.config; + +import org.springframework.boot.tomcat.servlet.TomcatServletWebServerFactory; +import org.springframework.boot.web.server.WebServerFactoryCustomizer; +import org.springframework.stereotype.Component; + +@Component +public class TomcatPathToleranceConfig implements WebServerFactoryCustomizer { + + @Override + public void customize(TomcatServletWebServerFactory factory) { + factory.addConnectorCustomizers(connector -> { + connector.setAllowBackslash(true); + connector.setEncodedReverseSolidusHandling("decode"); + connector.setProperty("relaxedPathChars", "\\"); + connector.setProperty("relaxedQueryChars", "\\"); + }); + } +} diff --git a/backend/src/main/java/com/petshop/backend/config/TrailingSlashNormalizationFilter.java b/backend/src/main/java/com/petshop/backend/config/TrailingSlashNormalizationFilter.java new file mode 100644 index 00000000..38ececb9 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/config/TrailingSlashNormalizationFilter.java @@ -0,0 +1,91 @@ +package com.petshop.backend.config; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Component +@Order(Ordered.HIGHEST_PRECEDENCE) +public class TrailingSlashNormalizationFilter extends OncePerRequestFilter { + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) { + String requestUri = request.getRequestURI(); + if (requestUri == null || requestUri.isBlank()) { + return true; + } + return requestUri.equals(normalizePath(requestUri)); + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + String normalizedUri = normalizePath(request.getRequestURI()); + String normalizedServletPath = normalizePath(request.getServletPath()); + String normalizedPathInfo = normalizePath(request.getPathInfo()); + + HttpServletRequestWrapper wrapper = new HttpServletRequestWrapper(request) { + @Override + public String getRequestURI() { + return normalizedUri; + } + + @Override + public StringBuffer getRequestURL() { + String original = super.getRequestURL().toString(); + int schemeSeparator = original.indexOf("://"); + int pathStart = schemeSeparator >= 0 ? original.indexOf('/', schemeSeparator + 3) : original.indexOf('/'); + if (pathStart < 0) { + return new StringBuffer(original); + } + String prefix = original.substring(0, pathStart); + return new StringBuffer(prefix + normalizedUri); + } + + @Override + public String getServletPath() { + return normalizedServletPath; + } + + @Override + public String getPathInfo() { + return normalizedPathInfo; + } + }; + + filterChain.doFilter(wrapper, response); + } + + private String normalizePath(String value) { + if (value == null) { + return null; + } + String normalized = value.replace('\\', '/'); + while (normalized.contains("//")) { + normalized = normalized.replace("//", "/"); + } + if (shouldLowercase(normalized)) { + normalized = normalized.toLowerCase(java.util.Locale.ROOT); + } + int end = normalized.length(); + while (end > 1 && normalized.charAt(end - 1) == '/') { + end--; + } + return normalized.substring(0, end); + } + + private boolean shouldLowercase(String path) { + String lower = path.toLowerCase(java.util.Locale.ROOT); + return lower.startsWith("/api/") + || lower.equals("/api") + || lower.startsWith("/ws/") + || lower.equals("/ws"); + } +} diff --git a/backend/src/main/java/com/petshop/backend/config/WebSocketAuthChannelInterceptor.java b/backend/src/main/java/com/petshop/backend/config/WebSocketAuthChannelInterceptor.java new file mode 100644 index 00000000..c7f23fc4 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/config/WebSocketAuthChannelInterceptor.java @@ -0,0 +1,237 @@ +package com.petshop.backend.config; + +import com.petshop.backend.entity.User; +import com.petshop.backend.repository.UserRepository; +import com.petshop.backend.security.AppPrincipal; +import com.petshop.backend.security.JwtUtil; +import com.petshop.backend.service.ChatService; +import io.jsonwebtoken.JwtException; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.simp.stomp.StompCommand; +import org.springframework.messaging.simp.stomp.StompHeaderAccessor; +import org.springframework.messaging.support.ChannelInterceptor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.stereotype.Component; + +import java.security.Principal; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +@Component +public class WebSocketAuthChannelInterceptor implements ChannelInterceptor { + + private final JwtUtil jwtUtil; + private final UserRepository userRepository; + private final ChatService chatService; + + public WebSocketAuthChannelInterceptor(JwtUtil jwtUtil, UserRepository userRepository, ChatService chatService) { + this.jwtUtil = jwtUtil; + this.userRepository = userRepository; + this.chatService = chatService; + } + + @Override + public Message preSend(Message message, MessageChannel channel) { + StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message); + StompCommand command = accessor.getCommand(); + + if (command == null) { + return message; + } + + if (StompCommand.CONNECT.equals(command)) { + String tokenHeader = firstHeader(accessor, "Authorization"); + String token = extractToken(tokenHeader != null ? tokenHeader : firstHeader(accessor, "token")); + if (token == null || token.isBlank()) { + throw new IllegalArgumentException("Missing websocket token"); + } + + Long userId = extractUserId(token); + User user = userId == null ? null : userRepository.findById(userId).orElse(null); + if (user == null) { + throw new IllegalArgumentException("User not found"); + } + if (user.getActive() == null || !user.getActive()) { + throw new IllegalArgumentException("User account is inactive"); + } + if (!jwtUtil.validateToken(token, user)) { + throw new IllegalArgumentException("Invalid websocket token"); + } + + AppPrincipal principal = new AppPrincipal( + user.getId(), + user.getUsername(), + user.getRole(), + user.getTokenVersion() + ); + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( + principal, + null, + principal.getAuthorities() + ); + accessor.setUser(authentication); + accessor.getSessionAttributes().put("user", authentication); + return message; + } + + if (StompCommand.DISCONNECT.equals(command) || StompCommand.UNSUBSCRIBE.equals(command)) { + return message; + } + + User user = resolveUser(accessor.getUser(), accessor); + if (user == null) { + throw new IllegalArgumentException("Unauthenticated websocket session"); + } + + if (StompCommand.SUBSCRIBE.equals(command)) { + authorizeSubscription(accessor.getDestination(), user); + } else if (StompCommand.SEND.equals(command)) { + authorizeSend(accessor.getDestination(), user); + } + + return message; + } + + private User resolveUser(Principal principal, StompHeaderAccessor accessor) { + Principal currentPrincipal = principal; + if (currentPrincipal == null && accessor.getSessionAttributes() != null) { + Object sessionUser = accessor.getSessionAttributes().get("user"); + if (sessionUser instanceof Principal storedPrincipal) { + accessor.setUser(storedPrincipal); + currentPrincipal = storedPrincipal; + } + } + + if (currentPrincipal instanceof UsernamePasswordAuthenticationToken authenticationToken + && authenticationToken.getPrincipal() instanceof AppPrincipal appPrincipal) { + return userRepository.findById(appPrincipal.getUserId()) + .orElseThrow(() -> new IllegalArgumentException("User not found")); + } + + if (currentPrincipal instanceof AppPrincipal appPrincipal) { + return userRepository.findById(appPrincipal.getUserId()) + .orElseThrow(() -> new IllegalArgumentException("User not found")); + } + + String tokenHeader = firstHeader(accessor, "Authorization"); + String token = extractToken(tokenHeader != null ? tokenHeader : firstHeader(accessor, "token")); + if (token == null || token.isBlank()) { + return null; + } + + Long userId = extractUserId(token); + User user = userId == null ? null : userRepository.findById(userId).orElse(null); + if (user == null) { + throw new IllegalArgumentException("User not found"); + } + if (user.getActive() == null || !user.getActive()) { + throw new IllegalArgumentException("User account is inactive"); + } + if (!jwtUtil.validateToken(token, user)) { + throw new IllegalArgumentException("Invalid websocket token"); + } + return user; + } + + private void authorizeSubscription(String destination, User user) { + destination = normalizeDestination(destination); + if (destination == null || destination.startsWith("/user/queue/")) { + return; + } + + if ("/topic/chat/conversations".equals(destination)) { + if (user.getRole() == User.Role.CUSTOMER) { + throw new IllegalArgumentException("Customers cannot subscribe to staff conversation feed"); + } + return; + } + + Long conversationId = extractConversationId(destination, "/topic/chat/conversations/"); + if (conversationId != null && chatService.hasConversationAccess(conversationId, user.getId(), user.getRole())) { + return; + } + + throw new IllegalArgumentException("Not authorized to subscribe to destination"); + } + + private void authorizeSend(String destination, User user) { + destination = normalizeDestination(destination); + Long conversationId = extractConversationId(destination, "/app/chat/conversations/"); + if (conversationId != null && destination.endsWith("/messages") && chatService.hasConversationAccess(conversationId, user.getId(), user.getRole())) { + return; + } + + throw new IllegalArgumentException("Not authorized to send to destination"); + } + + private Long extractConversationId(String destination, String prefix) { + if (destination == null || !destination.startsWith(prefix)) { + return null; + } + + String suffix = destination.substring(prefix.length()); + String[] parts = suffix.split("/"); + if (parts.length == 0 || parts[0].isBlank()) { + return null; + } + + try { + return Long.parseLong(parts[0]); + } catch (NumberFormatException ex) { + return null; + } + } + + private String firstHeader(StompHeaderAccessor accessor, String name) { + List values = accessor.getNativeHeader(name); + if (values != null && !values.isEmpty()) { + return values.get(0); + } + for (String headerName : accessor.toNativeHeaderMap().keySet()) { + if (headerName.equalsIgnoreCase(name)) { + List alternateValues = accessor.getNativeHeader(headerName); + return alternateValues == null || alternateValues.isEmpty() ? null : alternateValues.get(0); + } + } + return null; + } + + private String extractToken(String rawValue) { + if (rawValue == null || rawValue.isBlank()) { + return null; + } + String normalized = rawValue.trim(); + return normalized.regionMatches(true, 0, "Bearer ", 0, 7) ? normalized.substring(7) : normalized; + } + + private String normalizeDestination(String destination) { + if (destination == null || destination.isBlank()) { + return destination; + } + String normalized = destination.replace('\\', '/'); + while (normalized.contains("//")) { + normalized = normalized.replace("//", "/"); + } + return normalized.toLowerCase(Locale.ROOT); + } + + private Long extractUserId(String token) { + try { + return jwtUtil.extractUserId(token); + } catch (JwtException | IllegalArgumentException ex) { + throw new IllegalArgumentException("Invalid websocket token: " + ex.getMessage(), ex); + } + } + + public Map buildErrorPayload(Exception ex, String destination, Principal principal) { + Map response = new LinkedHashMap<>(); + response.put("message", ex.getMessage() == null || ex.getMessage().isBlank() ? "WebSocket request failed" : ex.getMessage()); + response.put("details", ex.getClass().getSimpleName()); + response.put("destination", normalizeDestination(destination)); + response.put("authenticated", principal != null); + return response; + } +} diff --git a/backend/src/main/java/com/petshop/backend/config/WebSocketConfig.java b/backend/src/main/java/com/petshop/backend/config/WebSocketConfig.java new file mode 100644 index 00000000..67dc1048 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/config/WebSocketConfig.java @@ -0,0 +1,45 @@ +package com.petshop.backend.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.simp.config.ChannelRegistration; +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 { + + private final WebSocketAuthChannelInterceptor webSocketAuthChannelInterceptor; + + public WebSocketConfig(WebSocketAuthChannelInterceptor webSocketAuthChannelInterceptor) { + this.webSocketAuthChannelInterceptor = webSocketAuthChannelInterceptor; + } + + @Override + public void configureMessageBroker(MessageBrokerRegistry config) { + config.enableSimpleBroker("/topic", "/queue"); + config.setApplicationDestinationPrefixes("/app"); + config.setUserDestinationPrefix("/user"); + } + + @Override + public void configureClientInboundChannel(ChannelRegistration registration) { + registration.interceptors(webSocketAuthChannelInterceptor); + } + + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry.addEndpoint("/ws/chat") + .setAllowedOriginPatterns("*"); + registry.addEndpoint("/ws/chat/") + .setAllowedOriginPatterns("*"); + registry.addEndpoint("/ws/chat-sockjs") + .setAllowedOriginPatterns("*") + .withSockJS(); + registry.addEndpoint("/ws/chat-sockjs/") + .setAllowedOriginPatterns("*") + .withSockJS(); + } +} diff --git a/backend/src/main/java/com/petshop/backend/controller/AdoptionController.java b/backend/src/main/java/com/petshop/backend/controller/AdoptionController.java new file mode 100644 index 00000000..a3f67002 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/controller/AdoptionController.java @@ -0,0 +1,113 @@ +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; +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 +@RequestMapping("/api/v1/adoptions") +public class AdoptionController { + + private final AdoptionService adoptionService; + private final UserRepository userRepository; + private final CustomerRepository customerRepository; + + public AdoptionController(AdoptionService adoptionService, UserRepository userRepository, CustomerRepository customerRepository) { + this.adoptionService = adoptionService; + this.userRepository = userRepository; + this.customerRepository = customerRepository; + } + + @GetMapping + @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") + public ResponseEntity> getAllAdoptions( + @RequestParam(required = false) String q, + Pageable pageable) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String role = authentication.getAuthorities().stream() + .findFirst() + .map(authority -> authority.getAuthority().replace("ROLE_", "")) + .orElse(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)); + } + + @GetMapping("/{id}") + @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") + public ResponseEntity getAdoptionById(@PathVariable Long id) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String role = authentication.getAuthorities().stream() + .findFirst() + .map(authority -> authority.getAuthority().replace("ROLE_", "")) + .orElse(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)); + } + + @PostMapping + @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") + public ResponseEntity createAdoption(@Valid @RequestBody AdoptionRequest request) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String role = authentication.getAuthorities().stream() + .findFirst() + .map(authority -> authority.getAuthority().replace("ROLE_", "")) + .orElse(null); + + if (role != null && role.equals("CUSTOMER")) { + Customer customer = AuthenticationHelper.getAuthenticatedCustomer(userRepository, customerRepository); + if (!request.getCustomerId().equals(customer.getCustomerId())) { + throw new org.springframework.security.access.AccessDeniedException("You can only create adoptions for yourself"); + } + } + + 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) { + return ResponseEntity.ok(adoptionService.updateAdoption(id, request)); + } + + @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/backend/src/main/java/com/petshop/backend/controller/AnalyticsController.java b/backend/src/main/java/com/petshop/backend/controller/AnalyticsController.java new file mode 100644 index 00000000..6c7b9b9e --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/controller/AnalyticsController.java @@ -0,0 +1,26 @@ +package com.petshop.backend.controller; + +import com.petshop.backend.dto.analytics.DashboardResponse; +import com.petshop.backend.service.AnalyticsService; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/v1/analytics") +@PreAuthorize("hasRole('ADMIN')") +public class AnalyticsController { + + private final AnalyticsService analyticsService; + + public AnalyticsController(AnalyticsService analyticsService) { + this.analyticsService = analyticsService; + } + + @GetMapping("/dashboard") + public ResponseEntity getDashboard( + @RequestParam(defaultValue = "30") int days, + @RequestParam(defaultValue = "10") int top) { + return ResponseEntity.ok(analyticsService.getDashboardData(days, top)); + } +} diff --git a/backend/src/main/java/com/petshop/backend/controller/AppointmentController.java b/backend/src/main/java/com/petshop/backend/controller/AppointmentController.java new file mode 100644 index 00000000..35246e05 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/controller/AppointmentController.java @@ -0,0 +1,125 @@ +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; +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; +import java.util.List; + +@RestController +@RequestMapping("/api/v1/appointments") +public class AppointmentController { + + private final AppointmentService appointmentService; + private final UserRepository userRepository; + private final CustomerRepository customerRepository; + + public AppointmentController(AppointmentService appointmentService, UserRepository userRepository, CustomerRepository customerRepository) { + this.appointmentService = appointmentService; + this.userRepository = userRepository; + this.customerRepository = customerRepository; + } + + @GetMapping + @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") + public ResponseEntity> getAllAppointments( + @RequestParam(required = false) String q, + Pageable pageable) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String role = authentication.getAuthorities().stream() + .findFirst() + .map(authority -> authority.getAuthority().replace("ROLE_", "")) + .orElse(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)); + } + + @GetMapping("/{id}") + @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") + public ResponseEntity getAppointmentById(@PathVariable Long id) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String role = authentication.getAuthorities().stream() + .findFirst() + .map(authority -> authority.getAuthority().replace("ROLE_", "")) + .orElse(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)); + } + + @PostMapping + @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") + public ResponseEntity createAppointment(@Valid @RequestBody AppointmentRequest request) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String role = authentication.getAuthorities().stream() + .findFirst() + .map(authority -> authority.getAuthority().replace("ROLE_", "")) + .orElse(null); + + if (role != null && role.equals("CUSTOMER")) { + Customer customer = AuthenticationHelper.getAuthenticatedCustomer(userRepository, customerRepository); + if (!request.getCustomerId().equals(customer.getCustomerId())) { + throw new org.springframework.security.access.AccessDeniedException("You can only create appointments for yourself"); + } + } + + 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) { + return ResponseEntity.ok(appointmentService.updateAppointment(id, request)); + } + + @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(); + } + + @GetMapping("/availability") + public ResponseEntity> checkAvailability( + @RequestParam Long storeId, + @RequestParam Long serviceId, + @RequestParam String date) { + LocalDate appointmentDate = LocalDate.parse(date); + return ResponseEntity.ok(appointmentService.checkAvailability(storeId, serviceId, appointmentDate)); + } +} diff --git a/backend/src/main/java/com/petshop/backend/controller/AuthController.java b/backend/src/main/java/com/petshop/backend/controller/AuthController.java new file mode 100644 index 00000000..2bd2b47d --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/controller/AuthController.java @@ -0,0 +1,353 @@ +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.EmployeeStore; +import com.petshop.backend.entity.User; +import com.petshop.backend.repository.EmployeeRepository; +import com.petshop.backend.repository.EmployeeStoreRepository; +import com.petshop.backend.repository.UserRepository; +import com.petshop.backend.security.JwtUtil; +import com.petshop.backend.service.UserBusinessLinkageService; +import com.petshop.backend.util.AuthenticationHelper; +import jakarta.validation.Valid; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.DisabledException; +import org.springframework.security.authentication.InternalAuthenticationServiceException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +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") +public class AuthController { + + private final AuthenticationManager authenticationManager; + private final UserRepository userRepository; + private final JwtUtil jwtUtil; + private final PasswordEncoder passwordEncoder; + private final UserBusinessLinkageService userBusinessLinkageService; + private final EmployeeRepository employeeRepository; + private final EmployeeStoreRepository employeeStoreRepository; + + public AuthController(AuthenticationManager authenticationManager, UserRepository userRepository, JwtUtil jwtUtil, PasswordEncoder passwordEncoder, UserBusinessLinkageService userBusinessLinkageService, EmployeeRepository employeeRepository, EmployeeStoreRepository employeeStoreRepository) { + this.authenticationManager = authenticationManager; + this.userRepository = userRepository; + this.jwtUtil = jwtUtil; + this.passwordEncoder = passwordEncoder; + this.userBusinessLinkageService = userBusinessLinkageService; + this.employeeRepository = employeeRepository; + this.employeeStoreRepository = employeeStoreRepository; + } + + @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); + } + + String phone = trimToNull(request.getPhone()); + if (phone != null && userRepository.findByPhone(phone).isPresent()) { + Map error = new HashMap<>(); + error.put("message", "Phone 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.setPhone(phone); + user.setRole(User.Role.CUSTOMER); + user.setActive(true); + + User savedUser = userRepository.save(user); + + // Create or link customer record + userBusinessLinkageService.ensureLinkedCustomer(savedUser); + + String token = jwtUtil.generateToken(savedUser); + + return ResponseEntity.status(HttpStatus.CREATED).body(new RegisterResponse( + savedUser.getId(), + savedUser.getUsername(), + savedUser.getEmail(), + savedUser.getPhone(), + savedUser.getRole().name(), + token + )); + } + + @PostMapping("/login") + public ResponseEntity login(@Valid @RequestBody LoginRequest request) { + try { + authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword()) + ); + + User user = userRepository.findByUsername(request.getUsername()) + .orElseThrow(() -> new UsernameNotFoundException("User not found")); + + String token = jwtUtil.generateToken(user); + + return ResponseEntity.ok(new LoginResponse( + token, + user.getUsername(), + user.getRole().name() + )); + + } catch (BadCredentialsException e) { + Map error = new HashMap<>(); + error.put("message", "Invalid username or password"); + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(error); + } catch (InternalAuthenticationServiceException e) { + if (e.getCause() instanceof DisabledException disabledException) { + Map error = new HashMap<>(); + error.put("message", disabledException.getMessage()); + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(error); + } + throw e; + } catch (DisabledException e) { + Map error = new HashMap<>(); + error.put("message", e.getMessage()); + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(error); + } + } + + @GetMapping("/me") + public ResponseEntity getCurrentUser() { + User user = getAuthenticatedUser(); + + EmployeeStore employeeStore = resolveEmployeeStore(user); + + return ResponseEntity.ok(new UserInfoResponse( + user.getId(), + user.getUsername(), + user.getEmail(), + user.getFullName(), + user.getPhone(), + user.getAvatarUrl(), + user.getRole().name(), + employeeStore != null ? employeeStore.getStore().getStoreId() : null, + employeeStore != null ? employeeStore.getStore().getStoreName() : null + )); + } + + @PutMapping("/me") + public ResponseEntity updateProfile(@Valid @RequestBody ProfileUpdateRequest request) { + User user = getAuthenticatedUser(); + boolean invalidateToken = false; + + 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()); + invalidateToken = true; + } + + 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.getPhone() != null) { + String phone = trimToNull(request.getPhone()); + if (!java.util.Objects.equals(phone, user.getPhone())) { + if (phone != null && userRepository.findByPhone(phone) + .filter(existing -> !existing.getId().equals(user.getId())) + .isPresent()) { + Map error = new HashMap<>(); + error.put("message", "Phone already exists"); + return ResponseEntity.status(HttpStatus.CONFLICT).body(error); + } + user.setPhone(phone); + } + } + + if (request.getPassword() != null && !request.getPassword().isEmpty()) { + user.setPassword(passwordEncoder.encode(request.getPassword())); + invalidateToken = true; + } + + if (invalidateToken) { + user.setTokenVersion(user.getTokenVersion() + 1); + } + + User updatedUser = userRepository.save(user); + userBusinessLinkageService.syncLinkedRecords(updatedUser); + + EmployeeStore employeeStore = resolveEmployeeStore(updatedUser); + + return ResponseEntity.ok(new UserInfoResponse( + updatedUser.getId(), + updatedUser.getUsername(), + updatedUser.getEmail(), + updatedUser.getFullName(), + updatedUser.getPhone(), + updatedUser.getAvatarUrl(), + updatedUser.getRole().name(), + employeeStore != null ? employeeStore.getStore().getStoreId() : null, + employeeStore != null ? employeeStore.getStore().getStoreName() : null + )); + } + + private EmployeeStore resolveEmployeeStore(User user) { + if (user.getRole() == User.Role.CUSTOMER) { + return null; + } + + return employeeRepository.findByUserId(user.getId()) + .flatMap(employee -> employeeStoreRepository.findByEmployeeEmployeeId(employee.getEmployeeId())) + .orElse(null); + } + + private String trimToNull(String value) { + if (value == null) { + return null; + } + String trimmed = value.trim(); + return trimmed.isEmpty() ? null : trimmed; + } + + @PostMapping("/me/avatar") + public ResponseEntity uploadAvatar(@RequestParam("avatar") MultipartFile file) { + User user = getAuthenticatedUser(); + + 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() { + User user = getAuthenticatedUser(); + + 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() { + User user = getAuthenticatedUser(); + + 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<>(); + response.put("message", "Logged out successfully"); + response.put("note", "Token remains valid until expiration. Clear token from client storage."); + return ResponseEntity.ok(response); + } + + private User getAuthenticatedUser() { + try { + return AuthenticationHelper.getAuthenticatedUser(userRepository); + } catch (RuntimeException ex) { + throw new UsernameNotFoundException(ex.getMessage(), ex); + } + } +} diff --git a/backend/src/main/java/com/petshop/backend/controller/CategoryController.java b/backend/src/main/java/com/petshop/backend/controller/CategoryController.java new file mode 100644 index 00000000..fb938dd9 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/controller/CategoryController.java @@ -0,0 +1,64 @@ +package com.petshop.backend.controller; + +import com.petshop.backend.dto.category.CategoryRequest; +import com.petshop.backend.dto.category.CategoryResponse; +import com.petshop.backend.dto.common.BulkDeleteRequest; +import com.petshop.backend.service.CategoryService; +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/categories") +public class CategoryController { + + private final CategoryService categoryService; + + public CategoryController(CategoryService categoryService) { + this.categoryService = categoryService; + } + + @GetMapping + public ResponseEntity> getAllCategories( + @RequestParam(required = false) String q, + Pageable pageable) { + return ResponseEntity.ok(categoryService.getAllCategories(q, pageable)); + } + + @GetMapping("/{id}") + public ResponseEntity getCategoryById(@PathVariable Long id) { + return ResponseEntity.ok(categoryService.getCategoryById(id)); + } + + @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) { + return ResponseEntity.ok(categoryService.updateCategory(id, request)); + } + + @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/backend/src/main/java/com/petshop/backend/controller/ChatController.java b/backend/src/main/java/com/petshop/backend/controller/ChatController.java new file mode 100644 index 00000000..8cfb5df3 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/controller/ChatController.java @@ -0,0 +1,99 @@ +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.CustomerRepository; +import com.petshop.backend.repository.UserRepository; +import com.petshop.backend.service.ChatRealtimeService; +import com.petshop.backend.service.ChatService; +import com.petshop.backend.util.AuthenticationHelper; +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.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 ChatRealtimeService chatRealtimeService; + private final UserRepository userRepository; + private final CustomerRepository customerRepository; + + public ChatController(ChatService chatService, ChatRealtimeService chatRealtimeService, UserRepository userRepository, CustomerRepository customerRepository) { + this.chatService = chatService; + this.chatRealtimeService = chatRealtimeService; + this.userRepository = userRepository; + this.customerRepository = customerRepository; + } + + private User getCurrentUser() { + try { + return AuthenticationHelper.getAuthenticatedUser(userRepository); + } catch (RuntimeException ex) { + throw new UsernameNotFoundException(ex.getMessage(), ex); + } + } + + @PostMapping("/conversations") + @PreAuthorize("hasRole('CUSTOMER')") + public ResponseEntity createConversation(@Valid @RequestBody ConversationRequest request) { + User user = getCurrentUser(); + ConversationResponse response = chatService.createConversation(user.getId(), request); + chatRealtimeService.publishNewConversation(response); + 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(), user.getRole(), request); + chatRealtimeService.publishMessage(id, message); + chatRealtimeService.publishConversationUpdate(id); + 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); + } + + @PostMapping("/conversations/{id}/request-human") + @PreAuthorize("hasRole('CUSTOMER')") + public ResponseEntity requestHumanTakeover(@PathVariable Long id) { + User user = getCurrentUser(); + ConversationResponse conversation = chatService.requestHumanTakeover(id, user.getId(), user.getRole()); + chatRealtimeService.publishConversationUpdate(id); + return ResponseEntity.ok(conversation); + } +} diff --git a/backend/src/main/java/com/petshop/backend/controller/ChatWebSocketController.java b/backend/src/main/java/com/petshop/backend/controller/ChatWebSocketController.java new file mode 100644 index 00000000..ed0a3718 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/controller/ChatWebSocketController.java @@ -0,0 +1,123 @@ +package com.petshop.backend.controller; + +import com.petshop.backend.config.WebSocketAuthChannelInterceptor; +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.security.AppPrincipal; +import com.petshop.backend.security.JwtUtil; +import com.petshop.backend.service.ChatRealtimeService; +import com.petshop.backend.service.ChatService; +import jakarta.validation.Valid; +import org.springframework.messaging.handler.annotation.DestinationVariable; +import org.springframework.messaging.handler.annotation.MessageExceptionHandler; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.Payload; +import org.springframework.messaging.simp.SimpMessageHeaderAccessor; +import org.springframework.messaging.simp.annotation.SendToUser; +import org.springframework.stereotype.Controller; + +import java.security.Principal; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +@Controller +public class ChatWebSocketController { + + private final ChatService chatService; + private final ChatRealtimeService chatRealtimeService; + private final UserRepository userRepository; + private final JwtUtil jwtUtil; + private final WebSocketAuthChannelInterceptor webSocketAuthChannelInterceptor; + + public ChatWebSocketController( + ChatService chatService, + ChatRealtimeService chatRealtimeService, + UserRepository userRepository, + JwtUtil jwtUtil, + WebSocketAuthChannelInterceptor webSocketAuthChannelInterceptor + ) { + this.chatService = chatService; + this.chatRealtimeService = chatRealtimeService; + this.userRepository = userRepository; + this.jwtUtil = jwtUtil; + this.webSocketAuthChannelInterceptor = webSocketAuthChannelInterceptor; + } + + @MessageMapping("/chat/conversations/{id}/messages") + @SendToUser("/queue/chat/errors") + public void sendMessage(@DestinationVariable Long id, @Valid @Payload MessageRequest request, SimpMessageHeaderAccessor headerAccessor) { + User user = resolveUser(headerAccessor); + MessageResponse message = chatService.sendMessage(id, user.getId(), user.getRole(), request); + chatRealtimeService.publishMessage(id, message); + chatRealtimeService.publishConversationUpdate(id); + } + + @MessageExceptionHandler({IllegalArgumentException.class, RuntimeException.class}) + @SendToUser("/queue/chat/errors") + public Map handleMessageException(Exception ex, SimpMessageHeaderAccessor headerAccessor) { + return webSocketAuthChannelInterceptor.buildErrorPayload(ex, headerAccessor.getDestination(), headerAccessor.getUser()); + } + + private User resolveUser(SimpMessageHeaderAccessor headerAccessor) { + Principal principal = headerAccessor.getUser(); + if (principal instanceof org.springframework.security.authentication.UsernamePasswordAuthenticationToken authenticationToken + && authenticationToken.getPrincipal() instanceof AppPrincipal appPrincipal) { + return userRepository.findById(appPrincipal.getUserId()) + .orElseThrow(() -> new IllegalArgumentException("User not found")); + } + + if (principal instanceof AppPrincipal appPrincipal) { + return userRepository.findById(appPrincipal.getUserId()) + .orElseThrow(() -> new IllegalArgumentException("User not found")); + } + + String tokenHeader = firstHeader(headerAccessor, "Authorization"); + if (tokenHeader == null || tokenHeader.isBlank()) { + tokenHeader = firstHeader(headerAccessor, "token"); + } + if (tokenHeader == null || tokenHeader.isBlank()) { + throw new IllegalArgumentException("User not authenticated"); + } + + String token = extractToken(tokenHeader); + Long userId; + try { + userId = jwtUtil.extractUserId(token); + } catch (RuntimeException ex) { + throw new IllegalArgumentException("Invalid websocket token", ex); + } + User user = userId == null ? null : userRepository.findById(userId).orElse(null); + if (user == null) { + throw new IllegalArgumentException("User not found"); + } + if (user.getActive() == null || !user.getActive()) { + throw new IllegalArgumentException("User account is inactive"); + } + if (!jwtUtil.validateToken(token, user)) { + throw new IllegalArgumentException("Invalid websocket token"); + } + return user; + } + + private String firstHeader(SimpMessageHeaderAccessor headerAccessor, String name) { + List values = headerAccessor.getNativeHeader(name); + if (values != null && !values.isEmpty()) { + return values.get(0); + } + Map> headers = headerAccessor.toNativeHeaderMap(); + for (Map.Entry> entry : headers.entrySet()) { + if (entry.getKey().equalsIgnoreCase(name)) { + return entry.getValue() == null || entry.getValue().isEmpty() ? null : entry.getValue().get(0); + } + } + return null; + } + + private String extractToken(String rawValue) { + String normalized = rawValue.trim(); + return normalized.regionMatches(true, 0, "Bearer ", 0, 7) ? normalized.substring(7) : normalized; + } +} diff --git a/backend/src/main/java/com/petshop/backend/controller/CustomerController.java b/backend/src/main/java/com/petshop/backend/controller/CustomerController.java new file mode 100644 index 00000000..f3ab880e --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/controller/CustomerController.java @@ -0,0 +1,61 @@ +package com.petshop.backend.controller; + +import com.petshop.backend.dto.common.BulkDeleteRequest; +import com.petshop.backend.dto.customer.CustomerRequest; +import com.petshop.backend.dto.customer.CustomerResponse; +import com.petshop.backend.service.CustomerService; +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/customers") +@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") +public class CustomerController { + + private final CustomerService customerService; + + public CustomerController(CustomerService customerService) { + this.customerService = customerService; + } + + @GetMapping + public ResponseEntity> getAllCustomers( + @RequestParam(required = false) String q, + Pageable pageable) { + return ResponseEntity.ok(customerService.getAllCustomers(q, pageable)); + } + + @GetMapping("/{id}") + public ResponseEntity getCustomerById(@PathVariable Long id) { + return ResponseEntity.ok(customerService.getCustomerById(id)); + } + + @PostMapping + public ResponseEntity createCustomer(@Valid @RequestBody CustomerRequest request) { + return ResponseEntity.status(HttpStatus.CREATED).body(customerService.createCustomer(request)); + } + + @PutMapping("/{id}") + public ResponseEntity updateCustomer( + @PathVariable Long id, + @Valid @RequestBody CustomerRequest request) { + return ResponseEntity.ok(customerService.updateCustomer(id, request)); + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteCustomer(@PathVariable Long id) { + customerService.deleteCustomer(id); + return ResponseEntity.noContent().build(); + } + + @PostMapping("/bulk-delete") + public ResponseEntity bulkDeleteCustomers(@Valid @RequestBody BulkDeleteRequest request) { + customerService.bulkDeleteCustomers(request); + return ResponseEntity.noContent().build(); + } +} diff --git a/backend/src/main/java/com/petshop/backend/controller/DropdownController.java b/backend/src/main/java/com/petshop/backend/controller/DropdownController.java new file mode 100644 index 00000000..16763b8a --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/controller/DropdownController.java @@ -0,0 +1,103 @@ +package com.petshop.backend.controller; + +import com.petshop.backend.dto.common.DropdownOption; +import com.petshop.backend.repository.*; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.stream.Collectors; + +@RestController +@RequestMapping("/api/v1/dropdowns") +public class DropdownController { + + private final PetRepository petRepository; + private final CustomerRepository customerRepository; + private final ServiceRepository serviceRepository; + private final ProductRepository productRepository; + private final CategoryRepository categoryRepository; + private final StoreRepository storeRepository; + private final SupplierRepository supplierRepository; + + public DropdownController(PetRepository petRepository, CustomerRepository customerRepository, + ServiceRepository serviceRepository, ProductRepository productRepository, + CategoryRepository categoryRepository, StoreRepository storeRepository, + SupplierRepository supplierRepository) { + this.petRepository = petRepository; + this.customerRepository = customerRepository; + this.serviceRepository = serviceRepository; + this.productRepository = productRepository; + this.categoryRepository = categoryRepository; + this.storeRepository = storeRepository; + this.supplierRepository = supplierRepository; + } + + @GetMapping("/pets") + public ResponseEntity> getPets() { + return ResponseEntity.ok( + petRepository.findAll().stream() + .map(p -> new DropdownOption(p.getPetId(), p.getPetName())) + .collect(Collectors.toList()) + ); + } + + @GetMapping("/customers") + @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") + public ResponseEntity> getCustomers() { + return ResponseEntity.ok( + customerRepository.findAll().stream() + .map(c -> new DropdownOption(c.getCustomerId(), c.getFirstName() + " " + c.getLastName())) + .collect(Collectors.toList()) + ); + } + + @GetMapping("/services") + public ResponseEntity> getServices() { + return ResponseEntity.ok( + serviceRepository.findAll().stream() + .map(s -> new DropdownOption(s.getServiceId(), s.getServiceName())) + .collect(Collectors.toList()) + ); + } + + @GetMapping("/products") + public ResponseEntity> getProducts() { + return ResponseEntity.ok( + productRepository.findAll().stream() + .map(p -> new DropdownOption(p.getProdId(), p.getProdName())) + .collect(Collectors.toList()) + ); + } + + @GetMapping("/categories") + public ResponseEntity> getCategories() { + return ResponseEntity.ok( + categoryRepository.findAll().stream() + .map(c -> new DropdownOption(c.getCategoryId(), c.getCategoryName())) + .collect(Collectors.toList()) + ); + } + + @GetMapping("/stores") + public ResponseEntity> getStores() { + return ResponseEntity.ok( + storeRepository.findAll().stream() + .map(s -> new DropdownOption(s.getStoreId(), s.getStoreName())) + .collect(Collectors.toList()) + ); + } + + @GetMapping("/suppliers") + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity> getSuppliers() { + return ResponseEntity.ok( + supplierRepository.findAll().stream() + .map(s -> new DropdownOption(s.getSupId(), s.getSupCompany())) + .collect(Collectors.toList()) + ); + } +} diff --git a/backend/src/main/java/com/petshop/backend/controller/EmployeeController.java b/backend/src/main/java/com/petshop/backend/controller/EmployeeController.java new file mode 100644 index 00000000..1c567623 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/controller/EmployeeController.java @@ -0,0 +1,49 @@ +package com.petshop.backend.controller; + +import com.petshop.backend.dto.employee.EmployeeRequest; +import com.petshop.backend.dto.employee.EmployeeResponse; +import com.petshop.backend.service.EmployeeService; +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/employees") +@PreAuthorize("hasRole('ADMIN')") +public class EmployeeController { + private final EmployeeService employeeService; + + public EmployeeController(EmployeeService employeeService) { + this.employeeService = employeeService; + } + + @GetMapping + public ResponseEntity> getAllEmployees(@RequestParam(required = false) String q, Pageable pageable) { + return ResponseEntity.ok(employeeService.getAllEmployees(q, pageable)); + } + + @GetMapping("/{id}") + public ResponseEntity getEmployeeById(@PathVariable Long id) { + return ResponseEntity.ok(employeeService.getEmployeeById(id)); + } + + @PostMapping + public ResponseEntity createEmployee(@Valid @RequestBody EmployeeRequest request) { + return ResponseEntity.status(HttpStatus.CREATED).body(employeeService.createEmployee(request)); + } + + @PutMapping("/{id}") + public ResponseEntity updateEmployee(@PathVariable Long id, @Valid @RequestBody EmployeeRequest request) { + return ResponseEntity.ok(employeeService.updateEmployee(id, request)); + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteEmployee(@PathVariable Long id) { + employeeService.deleteEmployee(id); + return ResponseEntity.noContent().build(); + } +} diff --git a/backend/src/main/java/com/petshop/backend/controller/HealthController.java b/backend/src/main/java/com/petshop/backend/controller/HealthController.java new file mode 100644 index 00000000..8ee609c3 --- /dev/null +++ b/backend/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/backend/src/main/java/com/petshop/backend/controller/InventoryController.java b/backend/src/main/java/com/petshop/backend/controller/InventoryController.java new file mode 100644 index 00000000..35ccbff9 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/controller/InventoryController.java @@ -0,0 +1,61 @@ +package com.petshop.backend.controller; + +import com.petshop.backend.dto.common.BulkDeleteRequest; +import com.petshop.backend.dto.inventory.InventoryRequest; +import com.petshop.backend.dto.inventory.InventoryResponse; +import com.petshop.backend.service.InventoryService; +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/inventory") +@PreAuthorize("hasRole('ADMIN')") +public class InventoryController { + + private final InventoryService inventoryService; + + public InventoryController(InventoryService inventoryService) { + this.inventoryService = inventoryService; + } + + @GetMapping + public ResponseEntity> getAllInventory( + @RequestParam(required = false) String q, + Pageable pageable) { + return ResponseEntity.ok(inventoryService.getAllInventory(q, pageable)); + } + + @GetMapping("/{id}") + public ResponseEntity getInventoryById(@PathVariable Long id) { + return ResponseEntity.ok(inventoryService.getInventoryById(id)); + } + + @PostMapping + public ResponseEntity createInventory(@Valid @RequestBody InventoryRequest request) { + return ResponseEntity.status(HttpStatus.CREATED).body(inventoryService.createInventory(request)); + } + + @PutMapping("/{id}") + public ResponseEntity updateInventory( + @PathVariable Long id, + @Valid @RequestBody InventoryRequest request) { + return ResponseEntity.ok(inventoryService.updateInventory(id, request)); + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteInventory(@PathVariable Long id) { + inventoryService.deleteInventory(id); + return ResponseEntity.noContent().build(); + } + + @DeleteMapping + public ResponseEntity bulkDeleteInventory(@Valid @RequestBody BulkDeleteRequest request) { + inventoryService.bulkDeleteInventory(request); + return ResponseEntity.noContent().build(); + } +} diff --git a/backend/src/main/java/com/petshop/backend/controller/PetController.java b/backend/src/main/java/com/petshop/backend/controller/PetController.java new file mode 100644 index 00000000..259b0f89 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/controller/PetController.java @@ -0,0 +1,64 @@ +package com.petshop.backend.controller; + +import com.petshop.backend.dto.common.BulkDeleteRequest; +import com.petshop.backend.dto.pet.PetRequest; +import com.petshop.backend.dto.pet.PetResponse; +import com.petshop.backend.service.PetService; +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/pets") +public class PetController { + + private final PetService petService; + + public PetController(PetService petService) { + this.petService = petService; + } + + @GetMapping + public ResponseEntity> getAllPets( + @RequestParam(required = false) String q, + Pageable pageable) { + return ResponseEntity.ok(petService.getAllPets(q, pageable)); + } + + @GetMapping("/{id}") + public ResponseEntity getPetById(@PathVariable Long id) { + return ResponseEntity.ok(petService.getPetById(id)); + } + + @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) { + return ResponseEntity.ok(petService.updatePet(id, request)); + } + + @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/backend/src/main/java/com/petshop/backend/controller/ProductController.java b/backend/src/main/java/com/petshop/backend/controller/ProductController.java new file mode 100644 index 00000000..6531c72d --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/controller/ProductController.java @@ -0,0 +1,64 @@ +package com.petshop.backend.controller; + +import com.petshop.backend.dto.common.BulkDeleteRequest; +import com.petshop.backend.dto.product.ProductRequest; +import com.petshop.backend.dto.product.ProductResponse; +import com.petshop.backend.service.ProductService; +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/products") +public class ProductController { + + private final ProductService productService; + + public ProductController(ProductService productService) { + this.productService = productService; + } + + @GetMapping + public ResponseEntity> getAllProducts( + @RequestParam(required = false) String q, + Pageable pageable) { + return ResponseEntity.ok(productService.getAllProducts(q, pageable)); + } + + @GetMapping("/{id}") + public ResponseEntity getProductById(@PathVariable Long id) { + return ResponseEntity.ok(productService.getProductById(id)); + } + + @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) { + return ResponseEntity.ok(productService.updateProduct(id, request)); + } + + @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/backend/src/main/java/com/petshop/backend/controller/ProductSupplierController.java b/backend/src/main/java/com/petshop/backend/controller/ProductSupplierController.java new file mode 100644 index 00000000..e6d78e28 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/controller/ProductSupplierController.java @@ -0,0 +1,66 @@ +package com.petshop.backend.controller; + +import com.petshop.backend.dto.productsupplier.BulkDeleteProductSupplierRequest; +import com.petshop.backend.dto.productsupplier.ProductSupplierRequest; +import com.petshop.backend.dto.productsupplier.ProductSupplierResponse; +import com.petshop.backend.service.ProductSupplierService; +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/product-suppliers") +@PreAuthorize("hasRole('ADMIN')") +public class ProductSupplierController { + + private final ProductSupplierService productSupplierService; + + public ProductSupplierController(ProductSupplierService productSupplierService) { + this.productSupplierService = productSupplierService; + } + + @GetMapping + public ResponseEntity> getAllProductSuppliers( + @RequestParam(required = false) String q, + Pageable pageable) { + return ResponseEntity.ok(productSupplierService.getAllProductSuppliers(q, pageable)); + } + + @GetMapping("/{productId}/{supplierId}") + public ResponseEntity getProductSupplierById( + @PathVariable Long productId, + @PathVariable Long supplierId) { + return ResponseEntity.ok(productSupplierService.getProductSupplierById(productId, supplierId)); + } + + @PostMapping + public ResponseEntity createProductSupplier(@Valid @RequestBody ProductSupplierRequest request) { + return ResponseEntity.status(HttpStatus.CREATED).body(productSupplierService.createProductSupplier(request)); + } + + @PutMapping("/{productId}/{supplierId}") + public ResponseEntity updateProductSupplier( + @PathVariable Long productId, + @PathVariable Long supplierId, + @Valid @RequestBody ProductSupplierRequest request) { + return ResponseEntity.ok(productSupplierService.updateProductSupplier(productId, supplierId, request)); + } + + @DeleteMapping("/{productId}/{supplierId}") + public ResponseEntity deleteProductSupplier( + @PathVariable Long productId, + @PathVariable Long supplierId) { + productSupplierService.deleteProductSupplier(productId, supplierId); + return ResponseEntity.noContent().build(); + } + + @DeleteMapping + public ResponseEntity bulkDeleteProductSuppliers(@Valid @RequestBody BulkDeleteProductSupplierRequest request) { + productSupplierService.bulkDeleteProductSuppliers(request); + return ResponseEntity.noContent().build(); + } +} diff --git a/backend/src/main/java/com/petshop/backend/controller/PurchaseOrderController.java b/backend/src/main/java/com/petshop/backend/controller/PurchaseOrderController.java new file mode 100644 index 00000000..369f6995 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/controller/PurchaseOrderController.java @@ -0,0 +1,33 @@ +package com.petshop.backend.controller; + +import com.petshop.backend.dto.purchaseorder.PurchaseOrderResponse; +import com.petshop.backend.service.PurchaseOrderService; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/v1/purchase-orders") +@PreAuthorize("hasRole('ADMIN')") +public class PurchaseOrderController { + + private final PurchaseOrderService purchaseOrderService; + + public PurchaseOrderController(PurchaseOrderService purchaseOrderService) { + this.purchaseOrderService = purchaseOrderService; + } + + @GetMapping + public ResponseEntity> getAllPurchaseOrders( + @RequestParam(required = false) String q, + Pageable pageable) { + return ResponseEntity.ok(purchaseOrderService.getAllPurchaseOrders(q, pageable)); + } + + @GetMapping("/{id}") + public ResponseEntity getPurchaseOrderById(@PathVariable Long id) { + return ResponseEntity.ok(purchaseOrderService.getPurchaseOrderById(id)); + } +} diff --git a/backend/src/main/java/com/petshop/backend/controller/RefundController.java b/backend/src/main/java/com/petshop/backend/controller/RefundController.java new file mode 100644 index 00000000..6968b9c3 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/controller/RefundController.java @@ -0,0 +1,133 @@ +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; +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; + private final UserRepository userRepository; + private final CustomerRepository customerRepository; + + public RefundController(RefundService refundService, UserRepository userRepository, CustomerRepository customerRepository) { + this.refundService = refundService; + this.userRepository = userRepository; + this.customerRepository = customerRepository; + } + + @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 = 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); + } 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 = 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); + } + + @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 = 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); + } 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/backend/src/main/java/com/petshop/backend/controller/SaleController.java b/backend/src/main/java/com/petshop/backend/controller/SaleController.java new file mode 100644 index 00000000..5d29f80d --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/controller/SaleController.java @@ -0,0 +1,43 @@ +package com.petshop.backend.controller; + +import com.petshop.backend.dto.sale.SaleRequest; +import com.petshop.backend.dto.sale.SaleResponse; +import com.petshop.backend.service.SaleService; +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/sales") +public class SaleController { + + private final SaleService saleService; + + public SaleController(SaleService saleService) { + this.saleService = saleService; + } + + @GetMapping + @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") + public ResponseEntity> getAllSales( + @RequestParam(required = false) String q, + Pageable pageable) { + return ResponseEntity.ok(saleService.getAllSales(q, pageable)); + } + + @GetMapping("/{id}") + @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") + public ResponseEntity getSaleById(@PathVariable Long id) { + return ResponseEntity.ok(saleService.getSaleById(id)); + } + + @PostMapping + @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") + public ResponseEntity createSale(@Valid @RequestBody SaleRequest request) { + return ResponseEntity.status(HttpStatus.CREATED).body(saleService.createSale(request)); + } +} diff --git a/backend/src/main/java/com/petshop/backend/controller/ServiceController.java b/backend/src/main/java/com/petshop/backend/controller/ServiceController.java new file mode 100644 index 00000000..4f6503dc --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/controller/ServiceController.java @@ -0,0 +1,64 @@ +package com.petshop.backend.controller; + +import com.petshop.backend.dto.common.BulkDeleteRequest; +import com.petshop.backend.dto.service.ServiceRequest; +import com.petshop.backend.dto.service.ServiceResponse; +import com.petshop.backend.service.ServiceService; +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/services") +public class ServiceController { + + private final ServiceService serviceService; + + public ServiceController(ServiceService serviceService) { + this.serviceService = serviceService; + } + + @GetMapping + public ResponseEntity> getAllServices( + @RequestParam(required = false) String q, + Pageable pageable) { + return ResponseEntity.ok(serviceService.getAllServices(q, pageable)); + } + + @GetMapping("/{id}") + public ResponseEntity getServiceById(@PathVariable Long id) { + return ResponseEntity.ok(serviceService.getServiceById(id)); + } + + @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) { + return ResponseEntity.ok(serviceService.updateService(id, request)); + } + + @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/backend/src/main/java/com/petshop/backend/controller/StoreController.java b/backend/src/main/java/com/petshop/backend/controller/StoreController.java new file mode 100644 index 00000000..58110d7e --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/controller/StoreController.java @@ -0,0 +1,61 @@ +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; + + public StoreController(StoreService storeService) { + this.storeService = storeService; + } + + @GetMapping + public ResponseEntity> getAllStores( + @RequestParam(required = false) String q, + 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/backend/src/main/java/com/petshop/backend/controller/SupplierController.java b/backend/src/main/java/com/petshop/backend/controller/SupplierController.java new file mode 100644 index 00000000..eb5c065d --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/controller/SupplierController.java @@ -0,0 +1,61 @@ +package com.petshop.backend.controller; + +import com.petshop.backend.dto.common.BulkDeleteRequest; +import com.petshop.backend.dto.supplier.SupplierRequest; +import com.petshop.backend.dto.supplier.SupplierResponse; +import com.petshop.backend.service.SupplierService; +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/suppliers") +@PreAuthorize("hasRole('ADMIN')") +public class SupplierController { + + private final SupplierService supplierService; + + public SupplierController(SupplierService supplierService) { + this.supplierService = supplierService; + } + + @GetMapping + public ResponseEntity> getAllSuppliers( + @RequestParam(required = false) String q, + Pageable pageable) { + return ResponseEntity.ok(supplierService.getAllSuppliers(q, pageable)); + } + + @GetMapping("/{id}") + public ResponseEntity getSupplierById(@PathVariable Long id) { + return ResponseEntity.ok(supplierService.getSupplierById(id)); + } + + @PostMapping + public ResponseEntity createSupplier(@Valid @RequestBody SupplierRequest request) { + return ResponseEntity.status(HttpStatus.CREATED).body(supplierService.createSupplier(request)); + } + + @PutMapping("/{id}") + public ResponseEntity updateSupplier( + @PathVariable Long id, + @Valid @RequestBody SupplierRequest request) { + return ResponseEntity.ok(supplierService.updateSupplier(id, request)); + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteSupplier(@PathVariable Long id) { + supplierService.deleteSupplier(id); + return ResponseEntity.noContent().build(); + } + + @DeleteMapping + public ResponseEntity bulkDeleteSuppliers(@Valid @RequestBody BulkDeleteRequest request) { + supplierService.bulkDeleteSuppliers(request); + return ResponseEntity.noContent().build(); + } +} diff --git a/backend/src/main/java/com/petshop/backend/controller/UserController.java b/backend/src/main/java/com/petshop/backend/controller/UserController.java new file mode 100644 index 00000000..8f7e07c3 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/controller/UserController.java @@ -0,0 +1,63 @@ +package com.petshop.backend.controller; + +import com.petshop.backend.dto.common.BulkDeleteRequest; +import com.petshop.backend.dto.user.UserRequest; +import com.petshop.backend.dto.user.UserResponse; +import com.petshop.backend.entity.User; +import com.petshop.backend.service.UserService; +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/users") +@PreAuthorize("hasRole('ADMIN')") +public class UserController { + + private final UserService userService; + + public UserController(UserService userService) { + this.userService = userService; + } + + @GetMapping + public ResponseEntity> getAllUsers( + @RequestParam(required = false) String q, + @RequestParam(required = false) String role, + Pageable pageable) { + return ResponseEntity.ok(userService.getAllUsers(q, role, pageable)); + } + + @GetMapping("/{id}") + public ResponseEntity getUserById(@PathVariable Long id) { + return ResponseEntity.ok(userService.getUserById(id)); + } + + @PostMapping + public ResponseEntity createUser(@Valid @RequestBody UserRequest request) { + return ResponseEntity.status(HttpStatus.CREATED).body(userService.createUser(request)); + } + + @PutMapping("/{id}") + public ResponseEntity updateUser( + @PathVariable Long id, + @Valid @RequestBody UserRequest request) { + return ResponseEntity.ok(userService.updateUser(id, request)); + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteUser(@PathVariable Long id) { + userService.deleteUser(id); + return ResponseEntity.noContent().build(); + } + + @DeleteMapping + public ResponseEntity bulkDeleteUsers(@Valid @RequestBody BulkDeleteRequest request) { + userService.bulkDeleteUsers(request); + return ResponseEntity.noContent().build(); + } +} diff --git a/backend/src/main/java/com/petshop/backend/dto/adoption/AdoptionRequest.java b/backend/src/main/java/com/petshop/backend/dto/adoption/AdoptionRequest.java new file mode 100644 index 00000000..1807f5b4 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/dto/adoption/AdoptionRequest.java @@ -0,0 +1,78 @@ +package com.petshop.backend.dto.adoption; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.time.LocalDate; +import java.util.Objects; + +public class AdoptionRequest { + @NotNull(message = "Pet ID is required") + private Long petId; + + @NotNull(message = "Customer ID is required") + private Long customerId; + + @NotNull(message = "Adoption date is required") + private LocalDate adoptionDate; + + @NotBlank(message = "Adoption status is required") + private String adoptionStatus; + + public Long getPetId() { + return petId; + } + + public void setPetId(Long petId) { + this.petId = petId; + } + + public Long getCustomerId() { + return customerId; + } + + public void setCustomerId(Long customerId) { + this.customerId = customerId; + } + + public LocalDate getAdoptionDate() { + return adoptionDate; + } + + public void setAdoptionDate(LocalDate adoptionDate) { + this.adoptionDate = adoptionDate; + } + + public String getAdoptionStatus() { + return adoptionStatus; + } + + public void setAdoptionStatus(String adoptionStatus) { + this.adoptionStatus = adoptionStatus; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + AdoptionRequest that = (AdoptionRequest) o; + return Objects.equals(petId, that.petId) && + Objects.equals(customerId, that.customerId) && + Objects.equals(adoptionDate, that.adoptionDate) && + Objects.equals(adoptionStatus, that.adoptionStatus); + } + + @Override + public int hashCode() { + return Objects.hash(petId, customerId, adoptionDate, adoptionStatus); + } + + @Override + public String toString() { + return "AdoptionRequest{" + + "petId=" + petId + + ", customerId=" + customerId + + ", adoptionDate=" + adoptionDate + + ", adoptionStatus='" + adoptionStatus + '\'' + + '}'; + } +} diff --git a/backend/src/main/java/com/petshop/backend/dto/adoption/AdoptionResponse.java b/backend/src/main/java/com/petshop/backend/dto/adoption/AdoptionResponse.java new file mode 100644 index 00000000..6f2d0556 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/dto/adoption/AdoptionResponse.java @@ -0,0 +1,144 @@ +package com.petshop.backend.dto.adoption; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.Objects; + +public class AdoptionResponse { + private Long adoptionId; + private Long petId; + private String petName; + private Long customerId; + private String customerName; + private LocalDate adoptionDate; + private String adoptionStatus; + private BigDecimal adoptionFee; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + public AdoptionResponse() { + } + + public AdoptionResponse(Long adoptionId, Long petId, String petName, Long customerId, String customerName, LocalDate adoptionDate, String adoptionStatus, BigDecimal adoptionFee, LocalDateTime createdAt, LocalDateTime updatedAt) { + this.adoptionId = adoptionId; + this.petId = petId; + this.petName = petName; + this.customerId = customerId; + this.customerName = customerName; + this.adoptionDate = adoptionDate; + this.adoptionStatus = adoptionStatus; + this.adoptionFee = adoptionFee; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public Long getAdoptionId() { + return adoptionId; + } + + public void setAdoptionId(Long adoptionId) { + this.adoptionId = adoptionId; + } + + public Long getPetId() { + return petId; + } + + public void setPetId(Long petId) { + this.petId = petId; + } + + public String getPetName() { + return petName; + } + + public void setPetName(String petName) { + this.petName = petName; + } + + public Long getCustomerId() { + return customerId; + } + + public void setCustomerId(Long customerId) { + this.customerId = customerId; + } + + public String getCustomerName() { + return customerName; + } + + public void setCustomerName(String customerName) { + this.customerName = customerName; + } + + public LocalDate getAdoptionDate() { + return adoptionDate; + } + + public void setAdoptionDate(LocalDate adoptionDate) { + this.adoptionDate = adoptionDate; + } + + public String getAdoptionStatus() { + return adoptionStatus; + } + + public void setAdoptionStatus(String adoptionStatus) { + this.adoptionStatus = adoptionStatus; + } + + public BigDecimal getAdoptionFee() { + return adoptionFee; + } + + public void setAdoptionFee(BigDecimal adoptionFee) { + this.adoptionFee = adoptionFee; + } + + 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; + AdoptionResponse that = (AdoptionResponse) o; + return Objects.equals(adoptionId, that.adoptionId) && Objects.equals(petId, that.petId) && Objects.equals(petName, that.petName) && Objects.equals(customerId, that.customerId) && Objects.equals(customerName, that.customerName) && Objects.equals(adoptionDate, that.adoptionDate) && Objects.equals(adoptionStatus, that.adoptionStatus) && Objects.equals(adoptionFee, that.adoptionFee) && Objects.equals(createdAt, that.createdAt) && Objects.equals(updatedAt, that.updatedAt); + } + + @Override + public int hashCode() { + return Objects.hash(adoptionId, petId, petName, customerId, customerName, adoptionDate, adoptionStatus, adoptionFee, createdAt, updatedAt); + } + + @Override + public String toString() { + return "AdoptionResponse{" + + "adoptionId=" + adoptionId + + ", petId=" + petId + + ", petName='" + petName + '\'' + + ", customerId=" + customerId + + ", customerName='" + customerName + '\'' + + ", adoptionDate=" + adoptionDate + + ", adoptionStatus='" + adoptionStatus + '\'' + + ", adoptionFee=" + adoptionFee + + ", createdAt=" + createdAt + + ", updatedAt=" + updatedAt + + '}'; + } +} diff --git a/backend/src/main/java/com/petshop/backend/dto/analytics/DashboardResponse.java b/backend/src/main/java/com/petshop/backend/dto/analytics/DashboardResponse.java new file mode 100644 index 00000000..c884d24c --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/dto/analytics/DashboardResponse.java @@ -0,0 +1,343 @@ +package com.petshop.backend.dto.analytics; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Objects; + +public class DashboardResponse { + private SalesSummary salesSummary; + private InventorySummary inventorySummary; + private List topProducts; + private List dailySales; + + public DashboardResponse() { + } + + public DashboardResponse(SalesSummary salesSummary, InventorySummary inventorySummary, List topProducts, List dailySales) { + this.salesSummary = salesSummary; + this.inventorySummary = inventorySummary; + this.topProducts = topProducts; + this.dailySales = dailySales; + } + + public SalesSummary getSalesSummary() { + return salesSummary; + } + + public void setSalesSummary(SalesSummary salesSummary) { + this.salesSummary = salesSummary; + } + + public InventorySummary getInventorySummary() { + return inventorySummary; + } + + public void setInventorySummary(InventorySummary inventorySummary) { + this.inventorySummary = inventorySummary; + } + + public List getTopProducts() { + return topProducts; + } + + public void setTopProducts(List topProducts) { + this.topProducts = topProducts; + } + + public List getDailySales() { + return dailySales; + } + + public void setDailySales(List dailySales) { + this.dailySales = dailySales; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + DashboardResponse that = (DashboardResponse) o; + return Objects.equals(salesSummary, that.salesSummary) && Objects.equals(inventorySummary, that.inventorySummary) && Objects.equals(topProducts, that.topProducts) && Objects.equals(dailySales, that.dailySales); + } + + @Override + public int hashCode() { + return Objects.hash(salesSummary, inventorySummary, topProducts, dailySales); + } + + @Override + public String toString() { + return "DashboardResponse{" + + "salesSummary=" + salesSummary + + ", inventorySummary=" + inventorySummary + + ", topProducts=" + topProducts + + ", dailySales=" + dailySales + + '}'; + } + + public static class SalesSummary { + private BigDecimal totalRevenue; + private Long totalSales; + private BigDecimal totalRefunds; + private Long totalRefundCount; + + public SalesSummary() { + } + + public SalesSummary(BigDecimal totalRevenue, Long totalSales, BigDecimal totalRefunds, Long totalRefundCount) { + this.totalRevenue = totalRevenue; + this.totalSales = totalSales; + this.totalRefunds = totalRefunds; + this.totalRefundCount = totalRefundCount; + } + + public BigDecimal getTotalRevenue() { + return totalRevenue; + } + + public void setTotalRevenue(BigDecimal totalRevenue) { + this.totalRevenue = totalRevenue; + } + + public Long getTotalSales() { + return totalSales; + } + + public void setTotalSales(Long totalSales) { + this.totalSales = totalSales; + } + + public BigDecimal getTotalRefunds() { + return totalRefunds; + } + + public void setTotalRefunds(BigDecimal totalRefunds) { + this.totalRefunds = totalRefunds; + } + + public Long getTotalRefundCount() { + return totalRefundCount; + } + + public void setTotalRefundCount(Long totalRefundCount) { + this.totalRefundCount = totalRefundCount; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SalesSummary that = (SalesSummary) o; + return Objects.equals(totalRevenue, that.totalRevenue) && Objects.equals(totalSales, that.totalSales) && Objects.equals(totalRefunds, that.totalRefunds) && Objects.equals(totalRefundCount, that.totalRefundCount); + } + + @Override + public int hashCode() { + return Objects.hash(totalRevenue, totalSales, totalRefunds, totalRefundCount); + } + + @Override + public String toString() { + return "SalesSummary{" + + "totalRevenue=" + totalRevenue + + ", totalSales=" + totalSales + + ", totalRefunds=" + totalRefunds + + ", totalRefundCount=" + totalRefundCount + + '}'; + } +} + + public static class InventorySummary { + private Long totalProducts; + private Long lowStockProducts; + private Long outOfStockProducts; + + public InventorySummary() { + } + + public InventorySummary(Long totalProducts, Long lowStockProducts, Long outOfStockProducts) { + this.totalProducts = totalProducts; + this.lowStockProducts = lowStockProducts; + this.outOfStockProducts = outOfStockProducts; + } + + public Long getTotalProducts() { + return totalProducts; + } + + public void setTotalProducts(Long totalProducts) { + this.totalProducts = totalProducts; + } + + public Long getLowStockProducts() { + return lowStockProducts; + } + + public void setLowStockProducts(Long lowStockProducts) { + this.lowStockProducts = lowStockProducts; + } + + public Long getOutOfStockProducts() { + return outOfStockProducts; + } + + public void setOutOfStockProducts(Long outOfStockProducts) { + this.outOfStockProducts = outOfStockProducts; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + InventorySummary that = (InventorySummary) o; + return Objects.equals(totalProducts, that.totalProducts) && Objects.equals(lowStockProducts, that.lowStockProducts) && Objects.equals(outOfStockProducts, that.outOfStockProducts); + } + + @Override + public int hashCode() { + return Objects.hash(totalProducts, lowStockProducts, outOfStockProducts); + } + + @Override + public String toString() { + return "InventorySummary{" + + "totalProducts=" + totalProducts + + ", lowStockProducts=" + lowStockProducts + + ", outOfStockProducts=" + outOfStockProducts + + '}'; + } +} + + public static class TopProduct { + private Long productId; + private String productName; + private Long quantitySold; + private BigDecimal revenue; + + public TopProduct() { + } + + public TopProduct(Long productId, String productName, Long quantitySold, BigDecimal revenue) { + this.productId = productId; + this.productName = productName; + this.quantitySold = quantitySold; + this.revenue = revenue; + } + + public Long getProductId() { + return productId; + } + + public void setProductId(Long productId) { + this.productId = productId; + } + + public String getProductName() { + return productName; + } + + public void setProductName(String productName) { + this.productName = productName; + } + + public Long getQuantitySold() { + return quantitySold; + } + + public void setQuantitySold(Long quantitySold) { + this.quantitySold = quantitySold; + } + + public BigDecimal getRevenue() { + return revenue; + } + + public void setRevenue(BigDecimal revenue) { + this.revenue = revenue; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TopProduct that = (TopProduct) o; + return Objects.equals(productId, that.productId) && Objects.equals(productName, that.productName) && Objects.equals(quantitySold, that.quantitySold) && Objects.equals(revenue, that.revenue); + } + + @Override + public int hashCode() { + return Objects.hash(productId, productName, quantitySold, revenue); + } + + @Override + public String toString() { + return "TopProduct{" + + "productId=" + productId + + ", productName='" + productName + '\'' + + ", quantitySold=" + quantitySold + + ", revenue=" + revenue + + '}'; + } +} + + public static class DailySales { + private String date; + private BigDecimal revenue; + private Long salesCount; + + public DailySales() { + } + + public DailySales(String date, BigDecimal revenue, Long salesCount) { + this.date = date; + this.revenue = revenue; + this.salesCount = salesCount; + } + + public String getDate() { + return date; + } + + public void setDate(String date) { + this.date = date; + } + + public BigDecimal getRevenue() { + return revenue; + } + + public void setRevenue(BigDecimal revenue) { + this.revenue = revenue; + } + + public Long getSalesCount() { + return salesCount; + } + + public void setSalesCount(Long salesCount) { + this.salesCount = salesCount; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + DailySales that = (DailySales) o; + return Objects.equals(date, that.date) && Objects.equals(revenue, that.revenue) && Objects.equals(salesCount, that.salesCount); + } + + @Override + public int hashCode() { + return Objects.hash(date, revenue, salesCount); + } + + @Override + public String toString() { + return "DailySales{" + + "date='" + date + '\'' + + ", revenue=" + revenue + + ", salesCount=" + salesCount + + '}'; + } +} +} diff --git a/backend/src/main/java/com/petshop/backend/dto/appointment/AppointmentRequest.java b/backend/src/main/java/com/petshop/backend/dto/appointment/AppointmentRequest.java new file mode 100644 index 00000000..247e5ae4 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/dto/appointment/AppointmentRequest.java @@ -0,0 +1,119 @@ +package com.petshop.backend.dto.appointment; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; +import java.util.Objects; + +public class AppointmentRequest { + @NotNull(message = "Customer ID is required") + private Long customerId; + + @NotNull(message = "Store ID is required") + private Long storeId; + + @NotNull(message = "Service ID is required") + private Long serviceId; + + @NotNull(message = "Appointment date is required") + private LocalDate appointmentDate; + + @NotNull(message = "Appointment time is required") + private LocalTime appointmentTime; + + @NotNull(message = "Appointment status is required") + private String appointmentStatus; + + @NotEmpty(message = "At least one pet must be specified") + private List petIds; + + public Long getCustomerId() { + return customerId; + } + + public void setCustomerId(Long customerId) { + this.customerId = customerId; + } + + public Long getStoreId() { + return storeId; + } + + public void setStoreId(Long storeId) { + this.storeId = storeId; + } + + public Long getServiceId() { + return serviceId; + } + + public void setServiceId(Long serviceId) { + this.serviceId = serviceId; + } + + public LocalDate getAppointmentDate() { + return appointmentDate; + } + + public void setAppointmentDate(LocalDate appointmentDate) { + this.appointmentDate = appointmentDate; + } + + public LocalTime getAppointmentTime() { + return appointmentTime; + } + + public void setAppointmentTime(LocalTime appointmentTime) { + this.appointmentTime = appointmentTime; + } + + public String getAppointmentStatus() { + return appointmentStatus; + } + + public void setAppointmentStatus(String appointmentStatus) { + this.appointmentStatus = appointmentStatus; + } + + public List getPetIds() { + return petIds; + } + + public void setPetIds(List petIds) { + this.petIds = petIds; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + AppointmentRequest that = (AppointmentRequest) o; + return Objects.equals(customerId, that.customerId) && + Objects.equals(storeId, that.storeId) && + Objects.equals(serviceId, that.serviceId) && + Objects.equals(appointmentDate, that.appointmentDate) && + Objects.equals(appointmentTime, that.appointmentTime) && + Objects.equals(appointmentStatus, that.appointmentStatus) && + Objects.equals(petIds, that.petIds); + } + + @Override + public int hashCode() { + return Objects.hash(customerId, storeId, serviceId, appointmentDate, appointmentTime, appointmentStatus, petIds); + } + + @Override + public String toString() { + return "AppointmentRequest{" + + "customerId=" + customerId + + ", storeId=" + storeId + + ", serviceId=" + serviceId + + ", appointmentDate=" + appointmentDate + + ", appointmentTime=" + appointmentTime + + ", appointmentStatus='" + appointmentStatus + '\'' + + ", petIds=" + petIds + + '}'; + } +} diff --git a/backend/src/main/java/com/petshop/backend/dto/appointment/AppointmentResponse.java b/backend/src/main/java/com/petshop/backend/dto/appointment/AppointmentResponse.java new file mode 100644 index 00000000..c7d2e8d7 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/dto/appointment/AppointmentResponse.java @@ -0,0 +1,189 @@ +package com.petshop.backend.dto.appointment; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.List; +import java.util.Objects; + +public class AppointmentResponse { + private Long appointmentId; + private Long customerId; + private String customerName; + private Long storeId; + private String storeName; + private Long serviceId; + private String serviceName; + private LocalDate appointmentDate; + private LocalTime appointmentTime; + private String appointmentStatus; + private List petNames; + private List petIds; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + public AppointmentResponse() { + } + + public AppointmentResponse(Long appointmentId, Long customerId, String customerName, Long storeId, String storeName, Long serviceId, String serviceName, LocalDate appointmentDate, LocalTime appointmentTime, String appointmentStatus, List petNames, List petIds, LocalDateTime createdAt, LocalDateTime updatedAt) { + this.appointmentId = appointmentId; + this.customerId = customerId; + this.customerName = customerName; + this.storeId = storeId; + this.storeName = storeName; + this.serviceId = serviceId; + this.serviceName = serviceName; + this.appointmentDate = appointmentDate; + this.appointmentTime = appointmentTime; + this.appointmentStatus = appointmentStatus; + this.petNames = petNames; + this.petIds = petIds; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public Long getAppointmentId() { + return appointmentId; + } + + public void setAppointmentId(Long appointmentId) { + this.appointmentId = appointmentId; + } + + public Long getCustomerId() { + return customerId; + } + + public void setCustomerId(Long customerId) { + this.customerId = customerId; + } + + public String getCustomerName() { + return customerName; + } + + public void setCustomerName(String customerName) { + this.customerName = customerName; + } + + public Long getStoreId() { + return storeId; + } + + public void setStoreId(Long storeId) { + this.storeId = storeId; + } + + public String getStoreName() { + return storeName; + } + + public void setStoreName(String storeName) { + this.storeName = storeName; + } + + public Long getServiceId() { + return serviceId; + } + + public void setServiceId(Long serviceId) { + this.serviceId = serviceId; + } + + public String getServiceName() { + return serviceName; + } + + public void setServiceName(String serviceName) { + this.serviceName = serviceName; + } + + public LocalDate getAppointmentDate() { + return appointmentDate; + } + + public void setAppointmentDate(LocalDate appointmentDate) { + this.appointmentDate = appointmentDate; + } + + public LocalTime getAppointmentTime() { + return appointmentTime; + } + + public void setAppointmentTime(LocalTime appointmentTime) { + this.appointmentTime = appointmentTime; + } + + public String getAppointmentStatus() { + return appointmentStatus; + } + + public void setAppointmentStatus(String appointmentStatus) { + this.appointmentStatus = appointmentStatus; + } + + public List getPetNames() { + return petNames; + } + + public void setPetNames(List petNames) { + this.petNames = petNames; + } + + public List getPetIds() { + return petIds; + } + + public void setPetIds(List petIds) { + this.petIds = petIds; + } + + 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; + AppointmentResponse that = (AppointmentResponse) o; + return Objects.equals(appointmentId, that.appointmentId) && Objects.equals(customerId, that.customerId) && Objects.equals(customerName, that.customerName) && Objects.equals(storeId, that.storeId) && Objects.equals(storeName, that.storeName) && Objects.equals(serviceId, that.serviceId) && Objects.equals(serviceName, that.serviceName) && Objects.equals(appointmentDate, that.appointmentDate) && Objects.equals(appointmentTime, that.appointmentTime) && Objects.equals(appointmentStatus, that.appointmentStatus) && Objects.equals(petNames, that.petNames) && Objects.equals(petIds, that.petIds) && Objects.equals(createdAt, that.createdAt) && Objects.equals(updatedAt, that.updatedAt); + } + + @Override + public int hashCode() { + return Objects.hash(appointmentId, customerId, customerName, storeId, storeName, serviceId, serviceName, appointmentDate, appointmentTime, appointmentStatus, petNames, petIds, createdAt, updatedAt); + } + + @Override + public String toString() { + return "AppointmentResponse{" + + "appointmentId=" + appointmentId + + ", customerId=" + customerId + + ", customerName='" + customerName + '\'' + + ", storeId=" + storeId + + ", storeName='" + storeName + '\'' + + ", serviceId=" + serviceId + + ", serviceName='" + serviceName + '\'' + + ", appointmentDate=" + appointmentDate + + ", appointmentTime=" + appointmentTime + + ", appointmentStatus='" + appointmentStatus + '\'' + + ", petNames=" + petNames + + ", petIds=" + petIds + + ", createdAt=" + createdAt + + ", updatedAt=" + updatedAt + + '}'; + } +} diff --git a/backend/src/main/java/com/petshop/backend/dto/auth/AvatarUploadResponse.java b/backend/src/main/java/com/petshop/backend/dto/auth/AvatarUploadResponse.java new file mode 100644 index 00000000..8e75af96 --- /dev/null +++ b/backend/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/backend/src/main/java/com/petshop/backend/dto/auth/LoginRequest.java b/backend/src/main/java/com/petshop/backend/dto/auth/LoginRequest.java new file mode 100644 index 00000000..970c4719 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/dto/auth/LoginRequest.java @@ -0,0 +1,50 @@ +package com.petshop.backend.dto.auth; + +import jakarta.validation.constraints.NotBlank; +import java.util.Objects; + +public class LoginRequest { + @NotBlank(message = "Username is required") + private String username; + + @NotBlank(message = "Password is required") + private String password; + + 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; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + LoginRequest that = (LoginRequest) o; + return Objects.equals(username, that.username) && + Objects.equals(password, that.password); + } + + @Override + public int hashCode() { + return Objects.hash(username, password); + } + + @Override + public String toString() { + return "LoginRequest{" + + "username='" + username + '\'' + + ", password='" + password + '\'' + + '}'; + } +} diff --git a/backend/src/main/java/com/petshop/backend/dto/auth/LoginResponse.java b/backend/src/main/java/com/petshop/backend/dto/auth/LoginResponse.java new file mode 100644 index 00000000..e3d01c11 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/dto/auth/LoginResponse.java @@ -0,0 +1,64 @@ +package com.petshop.backend.dto.auth; + +import java.util.Objects; + +public class LoginResponse { + private String token; + private String username; + private String role; + + public LoginResponse() { + } + + public LoginResponse(String token, String username, String role) { + this.token = token; + this.username = username; + this.role = role; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getRole() { + return role; + } + + public void setRole(String role) { + this.role = role; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + LoginResponse that = (LoginResponse) o; + return Objects.equals(token, that.token) && Objects.equals(username, that.username) && Objects.equals(role, that.role); + } + + @Override + public int hashCode() { + return Objects.hash(token, username, role); + } + + @Override + public String toString() { + return "LoginResponse{" + + "token='" + token + '\'' + + ", username='" + username + '\'' + + ", role='" + role + '\'' + + '}'; + } +} diff --git a/backend/src/main/java/com/petshop/backend/dto/auth/ProfileUpdateRequest.java b/backend/src/main/java/com/petshop/backend/dto/auth/ProfileUpdateRequest.java new file mode 100644 index 00000000..58959678 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/dto/auth/ProfileUpdateRequest.java @@ -0,0 +1,90 @@ +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(max = 20, message = "Phone must not exceed 20 characters") + private String phone; + + @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 getPhone() { + return phone; + } + + public void setPhone(String phone) { + this.phone = phone; + } + + 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(phone, that.phone) && + Objects.equals(password, that.password); + } + + @Override + public int hashCode() { + return Objects.hash(username, email, fullName, phone, password); + } + + @Override + public String toString() { + return "ProfileUpdateRequest{" + + "username='" + username + '\'' + + ", email='" + email + '\'' + + ", fullName='" + fullName + '\'' + + ", phone='" + phone + '\'' + + ", password='" + password + '\'' + + '}'; + } +} diff --git a/backend/src/main/java/com/petshop/backend/dto/auth/RegisterRequest.java b/backend/src/main/java/com/petshop/backend/dto/auth/RegisterRequest.java new file mode 100644 index 00000000..2791746c --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/dto/auth/RegisterRequest.java @@ -0,0 +1,96 @@ +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; + + @NotBlank(message = "Phone is required") + @Size(max = 20, message = "Phone must not exceed 20 characters") + private String phone; + + 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; + } + + public String getPhone() { + return phone; + } + + public void setPhone(String phone) { + this.phone = phone; + } + + @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) && + Objects.equals(phone, that.phone); + } + + @Override + public int hashCode() { + return Objects.hash(username, password, email, fullName, phone); + } + + @Override + public String toString() { + return "RegisterRequest{" + + "username='" + username + '\'' + + ", password='" + password + '\'' + + ", email='" + email + '\'' + + ", fullName='" + fullName + '\'' + + ", phone='" + phone + '\'' + + '}'; + } +} diff --git a/backend/src/main/java/com/petshop/backend/dto/auth/RegisterResponse.java b/backend/src/main/java/com/petshop/backend/dto/auth/RegisterResponse.java new file mode 100644 index 00000000..7e016985 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/dto/auth/RegisterResponse.java @@ -0,0 +1,102 @@ +package com.petshop.backend.dto.auth; + +import java.util.Objects; + +public class RegisterResponse { + private Long id; + private String username; + private String email; + private String phone; + private String role; + private String token; + + public RegisterResponse() { + } + + public RegisterResponse(Long id, String username, String email, String phone, String role, String token) { + this.id = id; + this.username = username; + this.email = email; + this.phone = phone; + 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 getPhone() { + return phone; + } + + public void setPhone(String phone) { + this.phone = phone; + } + + 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(phone, that.phone) && + Objects.equals(role, that.role) && + Objects.equals(token, that.token); + } + + @Override + public int hashCode() { + return Objects.hash(id, username, email, phone, role, token); + } + + @Override + public String toString() { + return "RegisterResponse{" + + "id=" + id + + ", username='" + username + '\'' + + ", email='" + email + '\'' + + ", phone='" + phone + '\'' + + ", role='" + role + '\'' + + ", token='" + token + '\'' + + '}'; + } +} diff --git a/backend/src/main/java/com/petshop/backend/dto/auth/UserInfoResponse.java b/backend/src/main/java/com/petshop/backend/dto/auth/UserInfoResponse.java new file mode 100644 index 00000000..ba714a49 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/dto/auth/UserInfoResponse.java @@ -0,0 +1,138 @@ +package com.petshop.backend.dto.auth; + +import java.util.Objects; + +public class UserInfoResponse { + private Long id; + private String username; + private String email; + private String fullName; + private String phone; + private String avatarUrl; + private String role; + private Long storeId; + private String storeName; + + public UserInfoResponse() { + } + + public UserInfoResponse(Long id, String username, String email, String fullName, String phone, String avatarUrl, String role, Long storeId, String storeName) { + this.id = id; + this.username = username; + this.email = email; + this.fullName = fullName; + this.phone = phone; + this.avatarUrl = avatarUrl; + this.role = role; + this.storeId = storeId; + this.storeName = storeName; + } + + 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 getFullName() { + return fullName; + } + + public void setFullName(String fullName) { + this.fullName = fullName; + } + + public String getPhone() { + return phone; + } + + public void setPhone(String phone) { + this.phone = phone; + } + + public String getAvatarUrl() { + return avatarUrl; + } + + public void setAvatarUrl(String avatarUrl) { + this.avatarUrl = avatarUrl; + } + + public String getRole() { + return role; + } + + public void setRole(String role) { + this.role = role; + } + + public Long getStoreId() { + return storeId; + } + + public void setStoreId(Long storeId) { + this.storeId = storeId; + } + + public String getStoreName() { + return storeName; + } + + public void setStoreName(String storeName) { + this.storeName = storeName; + } + + @Override + public boolean equals(Object o) { + 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(email, that.email) && + Objects.equals(fullName, that.fullName) && + Objects.equals(phone, that.phone) && + Objects.equals(avatarUrl, that.avatarUrl) && + Objects.equals(role, that.role) && + Objects.equals(storeId, that.storeId) && + Objects.equals(storeName, that.storeName); + } + + @Override + public int hashCode() { + return Objects.hash(id, username, email, fullName, phone, avatarUrl, role, storeId, storeName); + } + + @Override + public String toString() { + return "UserInfoResponse{" + + "id=" + id + + ", username='" + username + '\'' + + ", email='" + email + '\'' + + ", fullName='" + fullName + '\'' + + ", phone='" + phone + '\'' + + ", avatarUrl='" + avatarUrl + '\'' + + ", role='" + role + '\'' + + ", storeId=" + storeId + + ", storeName='" + storeName + '\'' + + '}'; + } +} diff --git a/backend/src/main/java/com/petshop/backend/dto/category/CategoryRequest.java b/backend/src/main/java/com/petshop/backend/dto/category/CategoryRequest.java new file mode 100644 index 00000000..c012ae21 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/dto/category/CategoryRequest.java @@ -0,0 +1,49 @@ +package com.petshop.backend.dto.category; + +import jakarta.validation.constraints.NotBlank; +import java.util.Objects; + +public class CategoryRequest { + @NotBlank(message = "Category name is required") + private String categoryName; + + private String categoryType; + + public String getCategoryName() { + return categoryName; + } + + public void setCategoryName(String categoryName) { + this.categoryName = categoryName; + } + + public String getCategoryType() { + return categoryType; + } + + public void setCategoryType(String categoryType) { + this.categoryType = categoryType; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CategoryRequest that = (CategoryRequest) o; + return Objects.equals(categoryName, that.categoryName) && + Objects.equals(categoryType, that.categoryType); + } + + @Override + public int hashCode() { + return Objects.hash(categoryName, categoryType); + } + + @Override + public String toString() { + return "CategoryRequest{" + + "categoryName='" + categoryName + '\'' + + ", categoryType='" + categoryType + '\'' + + '}'; + } +} diff --git a/backend/src/main/java/com/petshop/backend/dto/category/CategoryResponse.java b/backend/src/main/java/com/petshop/backend/dto/category/CategoryResponse.java new file mode 100644 index 00000000..6d9e1569 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/dto/category/CategoryResponse.java @@ -0,0 +1,87 @@ +package com.petshop.backend.dto.category; + +import java.time.LocalDateTime; +import java.util.Objects; + +public class CategoryResponse { + private Long categoryId; + private String categoryName; + private String categoryType; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + public CategoryResponse() { + } + + public CategoryResponse(Long categoryId, String categoryName, String categoryType, LocalDateTime createdAt, LocalDateTime updatedAt) { + this.categoryId = categoryId; + this.categoryName = categoryName; + this.categoryType = categoryType; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public Long getCategoryId() { + return categoryId; + } + + public void setCategoryId(Long categoryId) { + this.categoryId = categoryId; + } + + public String getCategoryName() { + return categoryName; + } + + public void setCategoryName(String categoryName) { + this.categoryName = categoryName; + } + + public String getCategoryType() { + return categoryType; + } + + public void setCategoryType(String categoryType) { + this.categoryType = categoryType; + } + + 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; + CategoryResponse that = (CategoryResponse) o; + return Objects.equals(categoryId, that.categoryId) && Objects.equals(categoryName, that.categoryName) && Objects.equals(categoryType, that.categoryType) && Objects.equals(createdAt, that.createdAt) && Objects.equals(updatedAt, that.updatedAt); + } + + @Override + public int hashCode() { + return Objects.hash(categoryId, categoryName, categoryType, createdAt, updatedAt); + } + + @Override + public String toString() { + return "CategoryResponse{" + + "categoryId=" + categoryId + + ", categoryName='" + categoryName + '\'' + + ", categoryType='" + categoryType + '\'' + + ", createdAt=" + createdAt + + ", updatedAt=" + updatedAt + + '}'; + } +} diff --git a/backend/src/main/java/com/petshop/backend/dto/chat/ConversationRequest.java b/backend/src/main/java/com/petshop/backend/dto/chat/ConversationRequest.java new file mode 100644 index 00000000..a6ef1db1 --- /dev/null +++ b/backend/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/backend/src/main/java/com/petshop/backend/dto/chat/ConversationResponse.java b/backend/src/main/java/com/petshop/backend/dto/chat/ConversationResponse.java new file mode 100644 index 00000000..86078ab2 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/dto/chat/ConversationResponse.java @@ -0,0 +1,118 @@ +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 mode; + private String lastMessage; + private LocalDateTime humanRequestedAt; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + public ConversationResponse() { + } + + public ConversationResponse(Long id, Long customerId, Long staffId, String status, String mode, String lastMessage, LocalDateTime humanRequestedAt, LocalDateTime createdAt, LocalDateTime updatedAt) { + this.id = id; + this.customerId = customerId; + this.staffId = staffId; + this.status = status; + this.mode = mode; + this.lastMessage = lastMessage; + this.humanRequestedAt = humanRequestedAt; + 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.setMode(conversation.getMode().name()); + response.setLastMessage(lastMessage); + response.setHumanRequestedAt(conversation.getHumanRequestedAt()); + 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 getMode() { + return mode; + } + + public void setMode(String mode) { + this.mode = mode; + } + + public String getLastMessage() { + return lastMessage; + } + + public void setLastMessage(String lastMessage) { + this.lastMessage = lastMessage; + } + + public LocalDateTime getHumanRequestedAt() { + return humanRequestedAt; + } + + public void setHumanRequestedAt(LocalDateTime humanRequestedAt) { + this.humanRequestedAt = humanRequestedAt; + } + + 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/backend/src/main/java/com/petshop/backend/dto/chat/MessageRequest.java b/backend/src/main/java/com/petshop/backend/dto/chat/MessageRequest.java new file mode 100644 index 00000000..cb03d310 --- /dev/null +++ b/backend/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/backend/src/main/java/com/petshop/backend/dto/chat/MessageResponse.java b/backend/src/main/java/com/petshop/backend/dto/chat/MessageResponse.java new file mode 100644 index 00000000..25cffae5 --- /dev/null +++ b/backend/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/backend/src/main/java/com/petshop/backend/dto/common/BulkDeleteRequest.java b/backend/src/main/java/com/petshop/backend/dto/common/BulkDeleteRequest.java new file mode 100644 index 00000000..bec920f1 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/dto/common/BulkDeleteRequest.java @@ -0,0 +1,38 @@ +package com.petshop.backend.dto.common; + +import jakarta.validation.constraints.NotEmpty; +import java.util.List; +import java.util.Objects; + +public class BulkDeleteRequest { + @NotEmpty(message = "IDs list cannot be empty") + private List ids; + + public List getIds() { + return ids; + } + + public void setIds(List ids) { + this.ids = ids; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + BulkDeleteRequest that = (BulkDeleteRequest) o; + return Objects.equals(ids, that.ids); + } + + @Override + public int hashCode() { + return Objects.hash(ids); + } + + @Override + public String toString() { + return "BulkDeleteRequest{" + + "ids=" + ids + + '}'; + } +} diff --git a/backend/src/main/java/com/petshop/backend/dto/common/DropdownOption.java b/backend/src/main/java/com/petshop/backend/dto/common/DropdownOption.java new file mode 100644 index 00000000..6ea0d058 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/dto/common/DropdownOption.java @@ -0,0 +1,53 @@ +package com.petshop.backend.dto.common; + +import java.util.Objects; + +public class DropdownOption { + private Long id; + private String label; + + public DropdownOption() { + } + + public DropdownOption(Long id, String label) { + this.id = id; + this.label = label; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + DropdownOption that = (DropdownOption) o; + return Objects.equals(id, that.id) && Objects.equals(label, that.label); + } + + @Override + public int hashCode() { + return Objects.hash(id, label); + } + + @Override + public String toString() { + return "DropdownOption{" + + "id=" + id + + ", label='" + label + '\'' + + '}'; + } +} diff --git a/backend/src/main/java/com/petshop/backend/dto/customer/CustomerRequest.java b/backend/src/main/java/com/petshop/backend/dto/customer/CustomerRequest.java new file mode 100644 index 00000000..ded898e3 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/dto/customer/CustomerRequest.java @@ -0,0 +1,64 @@ +package com.petshop.backend.dto.customer; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import java.util.Objects; + +public class CustomerRequest { + @NotBlank(message = "First name is required") + private String firstName; + + @NotBlank(message = "Last name is required") + private String lastName; + + @Email(message = "Invalid email format") + private String email; + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + 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; + CustomerRequest that = (CustomerRequest) o; + return Objects.equals(firstName, that.firstName) && + Objects.equals(lastName, that.lastName) && + Objects.equals(email, that.email); + } + + @Override + public int hashCode() { + return Objects.hash(firstName, lastName, email); + } + + @Override + public String toString() { + return "CustomerRequest{" + + "firstName='" + firstName + '\'' + + ", lastName='" + lastName + '\'' + + ", email='" + email + '\'' + + '}'; + } +} diff --git a/backend/src/main/java/com/petshop/backend/dto/customer/CustomerResponse.java b/backend/src/main/java/com/petshop/backend/dto/customer/CustomerResponse.java new file mode 100644 index 00000000..bd05bf76 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/dto/customer/CustomerResponse.java @@ -0,0 +1,98 @@ +package com.petshop.backend.dto.customer; + +import java.time.LocalDateTime; +import java.util.Objects; + +public class CustomerResponse { + private Long customerId; + private String firstName; + private String lastName; + private String email; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + public CustomerResponse() { + } + + public CustomerResponse(Long customerId, String firstName, String lastName, String email, LocalDateTime createdAt, LocalDateTime updatedAt) { + this.customerId = customerId; + this.firstName = firstName; + this.lastName = lastName; + this.email = email; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public Long getCustomerId() { + return customerId; + } + + public void setCustomerId(Long customerId) { + this.customerId = customerId; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + 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; + CustomerResponse that = (CustomerResponse) o; + return Objects.equals(customerId, that.customerId) && Objects.equals(firstName, that.firstName) && Objects.equals(lastName, that.lastName) && Objects.equals(email, that.email) && Objects.equals(createdAt, that.createdAt) && Objects.equals(updatedAt, that.updatedAt); + } + + @Override + public int hashCode() { + return Objects.hash(customerId, firstName, lastName, email, createdAt, updatedAt); + } + + @Override + public String toString() { + return "CustomerResponse{" + + "customerId=" + customerId + + ", firstName='" + firstName + '\'' + + ", lastName='" + lastName + '\'' + + ", email='" + email + '\'' + + ", createdAt=" + createdAt + + ", updatedAt=" + updatedAt + + '}'; + } +} diff --git a/backend/src/main/java/com/petshop/backend/dto/employee/EmployeeRequest.java b/backend/src/main/java/com/petshop/backend/dto/employee/EmployeeRequest.java new file mode 100644 index 00000000..f5fb9020 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/dto/employee/EmployeeRequest.java @@ -0,0 +1,51 @@ +package com.petshop.backend.dto.employee; + +import com.petshop.backend.entity.User; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +public class EmployeeRequest { + @NotBlank(message = "Username is required") + @Size(min = 3, max = 50, message = "Username must be between 3 and 50 characters") + private String username; + + @Size(min = 6, message = "Password must be at least 6 characters") + private String password; + + @NotBlank(message = "First name is required") + private String firstName; + + @NotBlank(message = "Last name is required") + private String lastName; + + @Email(message = "Invalid email format") + private String email; + + @NotBlank(message = "Phone is required") + @Size(max = 20, message = "Phone must not exceed 20 characters") + private String phone; + + @NotNull(message = "Role is required") + private User.Role role; + + private Boolean active = true; + + 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 getFirstName() { return firstName; } + public void setFirstName(String firstName) { this.firstName = firstName; } + public String getLastName() { return lastName; } + public void setLastName(String lastName) { this.lastName = lastName; } + public String getEmail() { return email; } + public void setEmail(String email) { this.email = email; } + public String getPhone() { return phone; } + public void setPhone(String phone) { this.phone = phone; } + public User.Role getRole() { return role; } + public void setRole(User.Role role) { this.role = role; } + public Boolean getActive() { return active; } + public void setActive(Boolean active) { this.active = active; } +} diff --git a/backend/src/main/java/com/petshop/backend/dto/employee/EmployeeResponse.java b/backend/src/main/java/com/petshop/backend/dto/employee/EmployeeResponse.java new file mode 100644 index 00000000..a159fc35 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/dto/employee/EmployeeResponse.java @@ -0,0 +1,43 @@ +package com.petshop.backend.dto.employee; + +import java.time.LocalDateTime; + +public class EmployeeResponse { + private Long employeeId; + private Long userId; + private String username; + private String firstName; + private String lastName; + private String fullName; + private String email; + private String phone; + private String role; + private Boolean active; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + public Long getEmployeeId() { return employeeId; } + public void setEmployeeId(Long employeeId) { this.employeeId = employeeId; } + public Long getUserId() { return userId; } + public void setUserId(Long userId) { this.userId = userId; } + public String getUsername() { return username; } + public void setUsername(String username) { this.username = username; } + public String getFirstName() { return firstName; } + public void setFirstName(String firstName) { this.firstName = firstName; } + public String getLastName() { return lastName; } + public void setLastName(String lastName) { this.lastName = lastName; } + public String getFullName() { return fullName; } + public void setFullName(String fullName) { this.fullName = fullName; } + public String getEmail() { return email; } + public void setEmail(String email) { this.email = email; } + public String getPhone() { return phone; } + public void setPhone(String phone) { this.phone = phone; } + public String getRole() { return role; } + public void setRole(String role) { this.role = role; } + public Boolean getActive() { return active; } + public void setActive(Boolean active) { this.active = active; } + 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/backend/src/main/java/com/petshop/backend/dto/inventory/InventoryRequest.java b/backend/src/main/java/com/petshop/backend/dto/inventory/InventoryRequest.java new file mode 100644 index 00000000..2dd953c6 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/dto/inventory/InventoryRequest.java @@ -0,0 +1,52 @@ +package com.petshop.backend.dto.inventory; + +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.PositiveOrZero; +import java.util.Objects; + +public class InventoryRequest { + @NotNull(message = "Product ID is required") + private Long prodId; + + @NotNull(message = "Quantity is required") + @PositiveOrZero(message = "Quantity must be zero or positive") + private Integer quantity; + + public Long getProdId() { + return prodId; + } + + public void setProdId(Long prodId) { + this.prodId = prodId; + } + + public Integer getQuantity() { + return quantity; + } + + public void setQuantity(Integer quantity) { + this.quantity = quantity; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + InventoryRequest that = (InventoryRequest) o; + return Objects.equals(prodId, that.prodId) && + Objects.equals(quantity, that.quantity); + } + + @Override + public int hashCode() { + return Objects.hash(prodId, quantity); + } + + @Override + public String toString() { + return "InventoryRequest{" + + "prodId=" + prodId + + ", quantity=" + quantity + + '}'; + } +} diff --git a/backend/src/main/java/com/petshop/backend/dto/inventory/InventoryResponse.java b/backend/src/main/java/com/petshop/backend/dto/inventory/InventoryResponse.java new file mode 100644 index 00000000..710dcbf4 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/dto/inventory/InventoryResponse.java @@ -0,0 +1,109 @@ +package com.petshop.backend.dto.inventory; + +import java.time.LocalDateTime; +import java.util.Objects; + +public class InventoryResponse { + private Long inventoryId; + private Long prodId; + private String productName; + private String categoryName; + private Integer quantity; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + public InventoryResponse() { + } + + public InventoryResponse(Long inventoryId, Long prodId, String productName, String categoryName, Integer quantity, LocalDateTime createdAt, LocalDateTime updatedAt) { + this.inventoryId = inventoryId; + this.prodId = prodId; + this.productName = productName; + this.categoryName = categoryName; + this.quantity = quantity; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public Long getInventoryId() { + return inventoryId; + } + + public void setInventoryId(Long inventoryId) { + this.inventoryId = inventoryId; + } + + public Long getProdId() { + return prodId; + } + + public void setProdId(Long prodId) { + this.prodId = prodId; + } + + public String getProductName() { + return productName; + } + + public void setProductName(String productName) { + this.productName = productName; + } + + public String getCategoryName() { + return categoryName; + } + + public void setCategoryName(String categoryName) { + this.categoryName = categoryName; + } + + public Integer getQuantity() { + return quantity; + } + + public void setQuantity(Integer quantity) { + this.quantity = quantity; + } + + 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; + InventoryResponse that = (InventoryResponse) o; + return Objects.equals(inventoryId, that.inventoryId) && Objects.equals(prodId, that.prodId) && Objects.equals(productName, that.productName) && Objects.equals(categoryName, that.categoryName) && Objects.equals(quantity, that.quantity) && Objects.equals(createdAt, that.createdAt) && Objects.equals(updatedAt, that.updatedAt); + } + + @Override + public int hashCode() { + return Objects.hash(inventoryId, prodId, productName, categoryName, quantity, createdAt, updatedAt); + } + + @Override + public String toString() { + return "InventoryResponse{" + + "inventoryId=" + inventoryId + + ", prodId=" + prodId + + ", productName='" + productName + '\'' + + ", categoryName='" + categoryName + '\'' + + ", quantity=" + quantity + + ", createdAt=" + createdAt + + ", updatedAt=" + updatedAt + + '}'; + } +} diff --git a/backend/src/main/java/com/petshop/backend/dto/pet/PetRequest.java b/backend/src/main/java/com/petshop/backend/dto/pet/PetRequest.java new file mode 100644 index 00000000..db3f71c9 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/dto/pet/PetRequest.java @@ -0,0 +1,103 @@ +package com.petshop.backend.dto.pet; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import java.math.BigDecimal; +import java.util.Objects; + +public class PetRequest { + @NotBlank(message = "Pet name is required") + private String petName; + + @NotBlank(message = "Species is required") + private String petSpecies; + + private String petBreed; + + @Positive(message = "Age must be positive") + private Integer petAge; + + @NotNull(message = "Status is required") + private String petStatus; + + private BigDecimal petPrice; + + public String getPetName() { + return petName; + } + + public void setPetName(String petName) { + this.petName = petName; + } + + public String getPetSpecies() { + return petSpecies; + } + + public void setPetSpecies(String petSpecies) { + this.petSpecies = petSpecies; + } + + public String getPetBreed() { + return petBreed; + } + + public void setPetBreed(String petBreed) { + this.petBreed = petBreed; + } + + public Integer getPetAge() { + return petAge; + } + + public void setPetAge(Integer petAge) { + this.petAge = petAge; + } + + public String getPetStatus() { + return petStatus; + } + + public void setPetStatus(String petStatus) { + this.petStatus = petStatus; + } + + public BigDecimal getPetPrice() { + return petPrice; + } + + public void setPetPrice(BigDecimal petPrice) { + this.petPrice = petPrice; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PetRequest that = (PetRequest) o; + return Objects.equals(petName, that.petName) && + Objects.equals(petSpecies, that.petSpecies) && + Objects.equals(petBreed, that.petBreed) && + Objects.equals(petAge, that.petAge) && + petStatus == that.petStatus && + Objects.equals(petPrice, that.petPrice); + } + + @Override + public int hashCode() { + return Objects.hash(petName, petSpecies, petBreed, petAge, petStatus, petPrice); + } + + @Override + public String toString() { + return "PetRequest{" + + "petName='" + petName + '\'' + + ", petSpecies='" + petSpecies + '\'' + + ", petBreed='" + petBreed + '\'' + + ", petAge=" + petAge + + ", petStatus=" + petStatus + + ", petPrice=" + petPrice + + '}'; + } +} diff --git a/backend/src/main/java/com/petshop/backend/dto/pet/PetResponse.java b/backend/src/main/java/com/petshop/backend/dto/pet/PetResponse.java new file mode 100644 index 00000000..f691361a --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/dto/pet/PetResponse.java @@ -0,0 +1,132 @@ +package com.petshop.backend.dto.pet; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.Objects; + +public class PetResponse { + private Long petId; + private String petName; + private String petSpecies; + private String petBreed; + private Integer petAge; + private String petStatus; + private BigDecimal petPrice; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + public PetResponse() { + } + + public PetResponse(Long petId, String petName, String petSpecies, String petBreed, Integer petAge, String petStatus, BigDecimal petPrice, LocalDateTime createdAt, LocalDateTime updatedAt) { + this.petId = petId; + this.petName = petName; + this.petSpecies = petSpecies; + this.petBreed = petBreed; + this.petAge = petAge; + this.petStatus = petStatus; + this.petPrice = petPrice; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public Long getPetId() { + return petId; + } + + public void setPetId(Long petId) { + this.petId = petId; + } + + public String getPetName() { + return petName; + } + + public void setPetName(String petName) { + this.petName = petName; + } + + public String getPetSpecies() { + return petSpecies; + } + + public void setPetSpecies(String petSpecies) { + this.petSpecies = petSpecies; + } + + public String getPetBreed() { + return petBreed; + } + + public void setPetBreed(String petBreed) { + this.petBreed = petBreed; + } + + public Integer getPetAge() { + return petAge; + } + + public void setPetAge(Integer petAge) { + this.petAge = petAge; + } + + public String getPetStatus() { + return petStatus; + } + + public void setPetStatus(String petStatus) { + this.petStatus = petStatus; + } + + public BigDecimal getPetPrice() { + return petPrice; + } + + public void setPetPrice(BigDecimal petPrice) { + this.petPrice = petPrice; + } + + 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; + PetResponse that = (PetResponse) o; + return Objects.equals(petId, that.petId) && Objects.equals(petName, that.petName) && Objects.equals(petSpecies, that.petSpecies) && Objects.equals(petBreed, that.petBreed) && Objects.equals(petAge, that.petAge) && Objects.equals(petStatus, that.petStatus) && Objects.equals(petPrice, that.petPrice) && Objects.equals(createdAt, that.createdAt) && Objects.equals(updatedAt, that.updatedAt); + } + + @Override + public int hashCode() { + return Objects.hash(petId, petName, petSpecies, petBreed, petAge, petStatus, petPrice, createdAt, updatedAt); + } + + @Override + public String toString() { + return "PetResponse{" + + "petId=" + petId + + ", petName='" + petName + '\'' + + ", petSpecies='" + petSpecies + '\'' + + ", petBreed='" + petBreed + '\'' + + ", petAge=" + petAge + + ", petStatus='" + petStatus + '\'' + + ", petPrice=" + petPrice + + ", createdAt=" + createdAt + + ", updatedAt=" + updatedAt + + '}'; + } +} diff --git a/backend/src/main/java/com/petshop/backend/dto/product/ProductRequest.java b/backend/src/main/java/com/petshop/backend/dto/product/ProductRequest.java new file mode 100644 index 00000000..71dd600f --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/dto/product/ProductRequest.java @@ -0,0 +1,79 @@ +package com.petshop.backend.dto.product; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import java.math.BigDecimal; +import java.util.Objects; + +public class ProductRequest { + @NotBlank(message = "Product name is required") + private String prodName; + + @NotNull(message = "Category ID is required") + private Long categoryId; + + private String prodDesc; + + @NotNull(message = "Product price is required") + @Positive(message = "Price must be positive") + private BigDecimal prodPrice; + + public String getProdName() { + return prodName; + } + + public void setProdName(String prodName) { + this.prodName = prodName; + } + + public Long getCategoryId() { + return categoryId; + } + + public void setCategoryId(Long categoryId) { + this.categoryId = categoryId; + } + + public String getProdDesc() { + return prodDesc; + } + + public void setProdDesc(String prodDesc) { + this.prodDesc = prodDesc; + } + + public BigDecimal getProdPrice() { + return prodPrice; + } + + public void setProdPrice(BigDecimal prodPrice) { + this.prodPrice = prodPrice; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ProductRequest that = (ProductRequest) o; + return Objects.equals(prodName, that.prodName) && + Objects.equals(categoryId, that.categoryId) && + Objects.equals(prodDesc, that.prodDesc) && + Objects.equals(prodPrice, that.prodPrice); + } + + @Override + public int hashCode() { + return Objects.hash(prodName, categoryId, prodDesc, prodPrice); + } + + @Override + public String toString() { + return "ProductRequest{" + + "prodName='" + prodName + '\'' + + ", categoryId=" + categoryId + + ", prodDesc='" + prodDesc + '\'' + + ", prodPrice=" + prodPrice + + '}'; + } +} diff --git a/backend/src/main/java/com/petshop/backend/dto/product/ProductResponse.java b/backend/src/main/java/com/petshop/backend/dto/product/ProductResponse.java new file mode 100644 index 00000000..96baa5ce --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/dto/product/ProductResponse.java @@ -0,0 +1,121 @@ +package com.petshop.backend.dto.product; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.Objects; + +public class ProductResponse { + private Long prodId; + private String prodName; + private Long categoryId; + private String categoryName; + private String prodDesc; + private BigDecimal prodPrice; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + public ProductResponse() { + } + + public ProductResponse(Long prodId, String prodName, Long categoryId, String categoryName, String prodDesc, BigDecimal prodPrice, LocalDateTime createdAt, LocalDateTime updatedAt) { + this.prodId = prodId; + this.prodName = prodName; + this.categoryId = categoryId; + this.categoryName = categoryName; + this.prodDesc = prodDesc; + this.prodPrice = prodPrice; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public Long getProdId() { + return prodId; + } + + public void setProdId(Long prodId) { + this.prodId = prodId; + } + + public String getProdName() { + return prodName; + } + + public void setProdName(String prodName) { + this.prodName = prodName; + } + + public Long getCategoryId() { + return categoryId; + } + + public void setCategoryId(Long categoryId) { + this.categoryId = categoryId; + } + + public String getCategoryName() { + return categoryName; + } + + public void setCategoryName(String categoryName) { + this.categoryName = categoryName; + } + + public String getProdDesc() { + return prodDesc; + } + + public void setProdDesc(String prodDesc) { + this.prodDesc = prodDesc; + } + + public BigDecimal getProdPrice() { + return prodPrice; + } + + public void setProdPrice(BigDecimal prodPrice) { + this.prodPrice = prodPrice; + } + + 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; + ProductResponse that = (ProductResponse) o; + return Objects.equals(prodId, that.prodId) && Objects.equals(prodName, that.prodName) && Objects.equals(categoryId, that.categoryId) && Objects.equals(categoryName, that.categoryName) && Objects.equals(prodDesc, that.prodDesc) && Objects.equals(prodPrice, that.prodPrice) && Objects.equals(createdAt, that.createdAt) && Objects.equals(updatedAt, that.updatedAt); + } + + @Override + public int hashCode() { + return Objects.hash(prodId, prodName, categoryId, categoryName, prodDesc, prodPrice, createdAt, updatedAt); + } + + @Override + public String toString() { + return "ProductResponse{" + + "prodId=" + prodId + + ", prodName='" + prodName + '\'' + + ", categoryId=" + categoryId + + ", categoryName='" + categoryName + '\'' + + ", prodDesc='" + prodDesc + '\'' + + ", prodPrice=" + prodPrice + + ", createdAt=" + createdAt + + ", updatedAt=" + updatedAt + + '}'; + } +} diff --git a/backend/src/main/java/com/petshop/backend/dto/productsupplier/BulkDeleteProductSupplierRequest.java b/backend/src/main/java/com/petshop/backend/dto/productsupplier/BulkDeleteProductSupplierRequest.java new file mode 100644 index 00000000..7eb8a3cc --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/dto/productsupplier/BulkDeleteProductSupplierRequest.java @@ -0,0 +1,38 @@ +package com.petshop.backend.dto.productsupplier; + +import jakarta.validation.constraints.NotEmpty; +import java.util.List; +import java.util.Objects; + +public class BulkDeleteProductSupplierRequest { + @NotEmpty(message = "Keys list cannot be empty") + private List keys; + + public List getKeys() { + return keys; + } + + public void setKeys(List keys) { + this.keys = keys; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + BulkDeleteProductSupplierRequest that = (BulkDeleteProductSupplierRequest) o; + return Objects.equals(keys, that.keys); + } + + @Override + public int hashCode() { + return Objects.hash(keys); + } + + @Override + public String toString() { + return "BulkDeleteProductSupplierRequest{" + + "keys=" + keys + + '}'; + } +} diff --git a/backend/src/main/java/com/petshop/backend/dto/productsupplier/ProductSupplierKey.java b/backend/src/main/java/com/petshop/backend/dto/productsupplier/ProductSupplierKey.java new file mode 100644 index 00000000..557dbeda --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/dto/productsupplier/ProductSupplierKey.java @@ -0,0 +1,54 @@ +package com.petshop.backend.dto.productsupplier; + +import java.util.Objects; + +public class ProductSupplierKey { + private Long productId; + private Long supplierId; + + public ProductSupplierKey() { + } + + public ProductSupplierKey(Long productId, Long supplierId) { + this.productId = productId; + this.supplierId = supplierId; + } + + public Long getProductId() { + return productId; + } + + public void setProductId(Long productId) { + this.productId = productId; + } + + public Long getSupplierId() { + return supplierId; + } + + public void setSupplierId(Long supplierId) { + this.supplierId = supplierId; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ProductSupplierKey that = (ProductSupplierKey) o; + return Objects.equals(productId, that.productId) && Objects.equals(supplierId, that.supplierId); + } + + @Override + public int hashCode() { + return Objects.hash(productId, supplierId); + } + + @Override + public String toString() { + return "ProductSupplierKey{" + + "productId=" + productId + + ", supplierId=" + supplierId + + '}'; + } +} + diff --git a/backend/src/main/java/com/petshop/backend/dto/productsupplier/ProductSupplierRequest.java b/backend/src/main/java/com/petshop/backend/dto/productsupplier/ProductSupplierRequest.java new file mode 100644 index 00000000..1bc05210 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/dto/productsupplier/ProductSupplierRequest.java @@ -0,0 +1,66 @@ +package com.petshop.backend.dto.productsupplier; + +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import java.math.BigDecimal; +import java.util.Objects; + +public class ProductSupplierRequest { + @NotNull(message = "Product ID is required") + private Long productId; + + @NotNull(message = "Supplier ID is required") + private Long supplierId; + + @NotNull(message = "Cost is required") + @Positive(message = "Cost must be positive") + private BigDecimal cost; + + public Long getProductId() { + return productId; + } + + public void setProductId(Long productId) { + this.productId = productId; + } + + public Long getSupplierId() { + return supplierId; + } + + public void setSupplierId(Long supplierId) { + this.supplierId = supplierId; + } + + public BigDecimal getCost() { + return cost; + } + + public void setCost(BigDecimal cost) { + this.cost = cost; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ProductSupplierRequest that = (ProductSupplierRequest) o; + return Objects.equals(productId, that.productId) && + Objects.equals(supplierId, that.supplierId) && + Objects.equals(cost, that.cost); + } + + @Override + public int hashCode() { + return Objects.hash(productId, supplierId, cost); + } + + @Override + public String toString() { + return "ProductSupplierRequest{" + + "productId=" + productId + + ", supplierId=" + supplierId + + ", cost=" + cost + + '}'; + } +} diff --git a/backend/src/main/java/com/petshop/backend/dto/productsupplier/ProductSupplierResponse.java b/backend/src/main/java/com/petshop/backend/dto/productsupplier/ProductSupplierResponse.java new file mode 100644 index 00000000..4190dabb --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/dto/productsupplier/ProductSupplierResponse.java @@ -0,0 +1,110 @@ +package com.petshop.backend.dto.productsupplier; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.Objects; + +public class ProductSupplierResponse { + private Long productId; + private String productName; + private Long supplierId; + private String supplierName; + private BigDecimal cost; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + public ProductSupplierResponse() { + } + + public ProductSupplierResponse(Long productId, String productName, Long supplierId, String supplierName, BigDecimal cost, LocalDateTime createdAt, LocalDateTime updatedAt) { + this.productId = productId; + this.productName = productName; + this.supplierId = supplierId; + this.supplierName = supplierName; + this.cost = cost; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public Long getProductId() { + return productId; + } + + public void setProductId(Long productId) { + this.productId = productId; + } + + public String getProductName() { + return productName; + } + + public void setProductName(String productName) { + this.productName = productName; + } + + public Long getSupplierId() { + return supplierId; + } + + public void setSupplierId(Long supplierId) { + this.supplierId = supplierId; + } + + public String getSupplierName() { + return supplierName; + } + + public void setSupplierName(String supplierName) { + this.supplierName = supplierName; + } + + public BigDecimal getCost() { + return cost; + } + + public void setCost(BigDecimal cost) { + this.cost = cost; + } + + 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; + ProductSupplierResponse that = (ProductSupplierResponse) o; + return Objects.equals(productId, that.productId) && Objects.equals(productName, that.productName) && Objects.equals(supplierId, that.supplierId) && Objects.equals(supplierName, that.supplierName) && Objects.equals(cost, that.cost) && Objects.equals(createdAt, that.createdAt) && Objects.equals(updatedAt, that.updatedAt); + } + + @Override + public int hashCode() { + return Objects.hash(productId, productName, supplierId, supplierName, cost, createdAt, updatedAt); + } + + @Override + public String toString() { + return "ProductSupplierResponse{" + + "productId=" + productId + + ", productName='" + productName + '\'' + + ", supplierId=" + supplierId + + ", supplierName='" + supplierName + '\'' + + ", cost=" + cost + + ", createdAt=" + createdAt + + ", updatedAt=" + updatedAt + + '}'; + } +} diff --git a/backend/src/main/java/com/petshop/backend/dto/purchaseorder/PurchaseOrderResponse.java b/backend/src/main/java/com/petshop/backend/dto/purchaseorder/PurchaseOrderResponse.java new file mode 100644 index 00000000..bf575d9d --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/dto/purchaseorder/PurchaseOrderResponse.java @@ -0,0 +1,110 @@ +package com.petshop.backend.dto.purchaseorder; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.Objects; + +public class PurchaseOrderResponse { + private Long purchaseOrderId; + private Long supId; + private String supplierName; + private LocalDate orderDate; + private String status; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + public PurchaseOrderResponse() { + } + + public PurchaseOrderResponse(Long purchaseOrderId, Long supId, String supplierName, LocalDate orderDate, String status, LocalDateTime createdAt, LocalDateTime updatedAt) { + this.purchaseOrderId = purchaseOrderId; + this.supId = supId; + this.supplierName = supplierName; + this.orderDate = orderDate; + this.status = status; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public Long getPurchaseOrderId() { + return purchaseOrderId; + } + + public void setPurchaseOrderId(Long purchaseOrderId) { + this.purchaseOrderId = purchaseOrderId; + } + + public Long getSupId() { + return supId; + } + + public void setSupId(Long supId) { + this.supId = supId; + } + + public String getSupplierName() { + return supplierName; + } + + public void setSupplierName(String supplierName) { + this.supplierName = supplierName; + } + + public LocalDate getOrderDate() { + return orderDate; + } + + public void setOrderDate(LocalDate orderDate) { + this.orderDate = orderDate; + } + + 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; + PurchaseOrderResponse that = (PurchaseOrderResponse) o; + return Objects.equals(purchaseOrderId, that.purchaseOrderId) && Objects.equals(supId, that.supId) && Objects.equals(supplierName, that.supplierName) && Objects.equals(orderDate, that.orderDate) && Objects.equals(status, that.status) && Objects.equals(createdAt, that.createdAt) && Objects.equals(updatedAt, that.updatedAt); + } + + @Override + public int hashCode() { + return Objects.hash(purchaseOrderId, supId, supplierName, orderDate, status, createdAt, updatedAt); + } + + @Override + public String toString() { + return "PurchaseOrderResponse{" + + "purchaseOrderId=" + purchaseOrderId + + ", supId=" + supId + + ", supplierName='" + supplierName + '\'' + + ", orderDate=" + orderDate + + ", status='" + status + '\'' + + ", createdAt=" + createdAt + + ", updatedAt=" + updatedAt + + '}'; + } +} diff --git a/backend/src/main/java/com/petshop/backend/dto/refund/RefundRequest.java b/backend/src/main/java/com/petshop/backend/dto/refund/RefundRequest.java new file mode 100644 index 00000000..aa94588b --- /dev/null +++ b/backend/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/backend/src/main/java/com/petshop/backend/dto/refund/RefundResponse.java b/backend/src/main/java/com/petshop/backend/dto/refund/RefundResponse.java new file mode 100644 index 00000000..83a2cd1b --- /dev/null +++ b/backend/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/backend/src/main/java/com/petshop/backend/dto/refund/RefundUpdateRequest.java b/backend/src/main/java/com/petshop/backend/dto/refund/RefundUpdateRequest.java new file mode 100644 index 00000000..22dddf95 --- /dev/null +++ b/backend/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/backend/src/main/java/com/petshop/backend/dto/sale/SaleItemRequest.java b/backend/src/main/java/com/petshop/backend/dto/sale/SaleItemRequest.java new file mode 100644 index 00000000..94e094c3 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/dto/sale/SaleItemRequest.java @@ -0,0 +1,51 @@ +package com.petshop.backend.dto.sale; + +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import java.util.Objects; + +public class SaleItemRequest { + @NotNull(message = "Product ID is required") + private Long prodId; + + @NotNull(message = "Quantity is required") + private Integer quantity; + + public Long getProdId() { + return prodId; + } + + public void setProdId(Long prodId) { + this.prodId = prodId; + } + + public Integer getQuantity() { + return quantity; + } + + public void setQuantity(Integer quantity) { + this.quantity = quantity; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SaleItemRequest that = (SaleItemRequest) o; + return Objects.equals(prodId, that.prodId) && + Objects.equals(quantity, that.quantity); + } + + @Override + public int hashCode() { + return Objects.hash(prodId, quantity); + } + + @Override + public String toString() { + return "SaleItemRequest{" + + "prodId=" + prodId + + ", quantity=" + quantity + + '}'; + } +} diff --git a/backend/src/main/java/com/petshop/backend/dto/sale/SaleRequest.java b/backend/src/main/java/com/petshop/backend/dto/sale/SaleRequest.java new file mode 100644 index 00000000..9c7102f4 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/dto/sale/SaleRequest.java @@ -0,0 +1,102 @@ +package com.petshop.backend.dto.sale; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import java.util.List; +import java.util.Objects; + +public class SaleRequest { + @NotNull(message = "Store ID is required") + private Long storeId; + + private String paymentMethod; + + @NotEmpty(message = "At least one item is required") + @Valid + private List items; + + private Boolean isRefund = false; + + private Long originalSaleId; + + private Long customerId; + + public Long getStoreId() { + return storeId; + } + + public void setStoreId(Long storeId) { + this.storeId = storeId; + } + + public String getPaymentMethod() { + return paymentMethod; + } + + public void setPaymentMethod(String paymentMethod) { + this.paymentMethod = paymentMethod; + } + + public List getItems() { + return items; + } + + public void setItems(List items) { + this.items = items; + } + + public Boolean getIsRefund() { + return isRefund; + } + + public void setIsRefund(Boolean isRefund) { + this.isRefund = isRefund; + } + + public Long getOriginalSaleId() { + return originalSaleId; + } + + public void setOriginalSaleId(Long originalSaleId) { + 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; + if (o == null || getClass() != o.getClass()) return false; + SaleRequest that = (SaleRequest) o; + return Objects.equals(storeId, that.storeId) && + Objects.equals(paymentMethod, that.paymentMethod) && + Objects.equals(items, that.items) && + Objects.equals(isRefund, that.isRefund) && + Objects.equals(originalSaleId, that.originalSaleId) && + Objects.equals(customerId, that.customerId); + } + + @Override + public int hashCode() { + return Objects.hash(storeId, paymentMethod, items, isRefund, originalSaleId, customerId); + } + + @Override + public String toString() { + return "SaleRequest{" + + "storeId=" + storeId + + ", paymentMethod='" + paymentMethod + '\'' + + ", items=" + items + + ", isRefund=" + isRefund + + ", originalSaleId=" + originalSaleId + + ", customerId=" + customerId + + '}'; + } +} diff --git a/backend/src/main/java/com/petshop/backend/dto/sale/SaleResponse.java b/backend/src/main/java/com/petshop/backend/dto/sale/SaleResponse.java new file mode 100644 index 00000000..969b28d3 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/dto/sale/SaleResponse.java @@ -0,0 +1,249 @@ +package com.petshop.backend.dto.sale; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Objects; + +public class SaleResponse { + private Long saleId; + private LocalDateTime saleDate; + private Long employeeId; + private String employeeName; + private Long storeId; + private String storeName; + private BigDecimal totalAmount; + private String paymentMethod; + private Boolean isRefund; + private Long originalSaleId; + private List items; + private LocalDateTime createdAt; + + public SaleResponse() { + } + + public SaleResponse(Long saleId, LocalDateTime saleDate, Long employeeId, String employeeName, Long storeId, String storeName, BigDecimal totalAmount, String paymentMethod, Boolean isRefund, Long originalSaleId, List items, LocalDateTime createdAt) { + this.saleId = saleId; + this.saleDate = saleDate; + this.employeeId = employeeId; + this.employeeName = employeeName; + this.storeId = storeId; + this.storeName = storeName; + this.totalAmount = totalAmount; + this.paymentMethod = paymentMethod; + this.isRefund = isRefund; + this.originalSaleId = originalSaleId; + this.items = items; + this.createdAt = createdAt; + } + + public Long getSaleId() { + return saleId; + } + + public void setSaleId(Long saleId) { + this.saleId = saleId; + } + + public LocalDateTime getSaleDate() { + return saleDate; + } + + public void setSaleDate(LocalDateTime saleDate) { + this.saleDate = saleDate; + } + + public Long getEmployeeId() { + return employeeId; + } + + public void setEmployeeId(Long employeeId) { + this.employeeId = employeeId; + } + + public String getEmployeeName() { + return employeeName; + } + + public void setEmployeeName(String employeeName) { + this.employeeName = employeeName; + } + + public Long getStoreId() { + return storeId; + } + + public void setStoreId(Long storeId) { + this.storeId = storeId; + } + + public String getStoreName() { + return storeName; + } + + public void setStoreName(String storeName) { + this.storeName = storeName; + } + + public BigDecimal getTotalAmount() { + return totalAmount; + } + + public void setTotalAmount(BigDecimal totalAmount) { + this.totalAmount = totalAmount; + } + + public String getPaymentMethod() { + return paymentMethod; + } + + public void setPaymentMethod(String paymentMethod) { + this.paymentMethod = paymentMethod; + } + + public Boolean getIsRefund() { + return isRefund; + } + + public void setIsRefund(Boolean isRefund) { + this.isRefund = isRefund; + } + + public Long getOriginalSaleId() { + return originalSaleId; + } + + public void setOriginalSaleId(Long originalSaleId) { + this.originalSaleId = originalSaleId; + } + + public List getItems() { + return items; + } + + public void setItems(List items) { + this.items = items; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SaleResponse that = (SaleResponse) o; + return Objects.equals(saleId, that.saleId) && Objects.equals(saleDate, that.saleDate) && Objects.equals(employeeId, that.employeeId) && Objects.equals(employeeName, that.employeeName) && Objects.equals(storeId, that.storeId) && Objects.equals(storeName, that.storeName) && Objects.equals(totalAmount, that.totalAmount) && Objects.equals(paymentMethod, that.paymentMethod) && Objects.equals(isRefund, that.isRefund) && Objects.equals(originalSaleId, that.originalSaleId) && Objects.equals(items, that.items) && Objects.equals(createdAt, that.createdAt); + } + + @Override + public int hashCode() { + return Objects.hash(saleId, saleDate, employeeId, employeeName, storeId, storeName, totalAmount, paymentMethod, isRefund, originalSaleId, items, createdAt); + } + + @Override + public String toString() { + return "SaleResponse{" + + "saleId=" + saleId + + ", saleDate=" + saleDate + + ", employeeId=" + employeeId + + ", employeeName='" + employeeName + '\'' + + ", storeId=" + storeId + + ", storeName='" + storeName + '\'' + + ", totalAmount=" + totalAmount + + ", paymentMethod='" + paymentMethod + '\'' + + ", isRefund=" + isRefund + + ", originalSaleId=" + originalSaleId + + ", items=" + items + + ", createdAt=" + createdAt + + '}'; + } + + public static class SaleItemResponse { + private Long saleItemId; + private Long prodId; + private String productName; + private Integer quantity; + private BigDecimal unitPrice; + + public SaleItemResponse() { + } + + public SaleItemResponse(Long saleItemId, Long prodId, String productName, Integer quantity, BigDecimal unitPrice) { + this.saleItemId = saleItemId; + this.prodId = prodId; + this.productName = productName; + this.quantity = quantity; + this.unitPrice = unitPrice; + } + + public Long getSaleItemId() { + return saleItemId; + } + + public void setSaleItemId(Long saleItemId) { + this.saleItemId = saleItemId; + } + + public Long getProdId() { + return prodId; + } + + public void setProdId(Long prodId) { + this.prodId = prodId; + } + + public String getProductName() { + return productName; + } + + public void setProductName(String productName) { + this.productName = productName; + } + + public Integer getQuantity() { + return quantity; + } + + public void setQuantity(Integer quantity) { + this.quantity = quantity; + } + + public BigDecimal getUnitPrice() { + return unitPrice; + } + + public void setUnitPrice(BigDecimal unitPrice) { + this.unitPrice = unitPrice; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SaleItemResponse that = (SaleItemResponse) o; + return Objects.equals(saleItemId, that.saleItemId) && Objects.equals(prodId, that.prodId) && Objects.equals(productName, that.productName) && Objects.equals(quantity, that.quantity) && Objects.equals(unitPrice, that.unitPrice); + } + + @Override + public int hashCode() { + return Objects.hash(saleItemId, prodId, productName, quantity, unitPrice); + } + + @Override + public String toString() { + return "SaleItemResponse{" + + "saleItemId=" + saleItemId + + ", prodId=" + prodId + + ", productName='" + productName + '\'' + + ", quantity=" + quantity + + ", unitPrice=" + unitPrice + + '}'; + } + } +} diff --git a/backend/src/main/java/com/petshop/backend/dto/service/ServiceRequest.java b/backend/src/main/java/com/petshop/backend/dto/service/ServiceRequest.java new file mode 100644 index 00000000..6b4550ec --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/dto/service/ServiceRequest.java @@ -0,0 +1,79 @@ +package com.petshop.backend.dto.service; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import java.math.BigDecimal; +import java.util.Objects; + +public class ServiceRequest { + @NotBlank(message = "Service name is required") + private String serviceName; + + private String serviceDesc; + + @NotNull(message = "Service price is required") + @Positive(message = "Price must be positive") + private BigDecimal servicePrice; + + @Positive(message = "Duration must be positive") + private Integer serviceDuration; + + public String getServiceName() { + return serviceName; + } + + public void setServiceName(String serviceName) { + this.serviceName = serviceName; + } + + public String getServiceDesc() { + return serviceDesc; + } + + public void setServiceDesc(String serviceDesc) { + this.serviceDesc = serviceDesc; + } + + public BigDecimal getServicePrice() { + return servicePrice; + } + + public void setServicePrice(BigDecimal servicePrice) { + this.servicePrice = servicePrice; + } + + public Integer getServiceDuration() { + return serviceDuration; + } + + public void setServiceDuration(Integer serviceDuration) { + this.serviceDuration = serviceDuration; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ServiceRequest that = (ServiceRequest) o; + return Objects.equals(serviceName, that.serviceName) && + Objects.equals(serviceDesc, that.serviceDesc) && + Objects.equals(servicePrice, that.servicePrice) && + Objects.equals(serviceDuration, that.serviceDuration); + } + + @Override + public int hashCode() { + return Objects.hash(serviceName, serviceDesc, servicePrice, serviceDuration); + } + + @Override + public String toString() { + return "ServiceRequest{" + + "serviceName='" + serviceName + '\'' + + ", serviceDesc='" + serviceDesc + '\'' + + ", servicePrice=" + servicePrice + + ", serviceDuration=" + serviceDuration + + '}'; + } +} diff --git a/backend/src/main/java/com/petshop/backend/dto/service/ServiceResponse.java b/backend/src/main/java/com/petshop/backend/dto/service/ServiceResponse.java new file mode 100644 index 00000000..53a2be5b --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/dto/service/ServiceResponse.java @@ -0,0 +1,110 @@ +package com.petshop.backend.dto.service; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.Objects; + +public class ServiceResponse { + private Long serviceId; + private String serviceName; + private String serviceDesc; + private BigDecimal servicePrice; + private Integer serviceDuration; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + public ServiceResponse() { + } + + public ServiceResponse(Long serviceId, String serviceName, String serviceDesc, BigDecimal servicePrice, Integer serviceDuration, LocalDateTime createdAt, LocalDateTime updatedAt) { + this.serviceId = serviceId; + this.serviceName = serviceName; + this.serviceDesc = serviceDesc; + this.servicePrice = servicePrice; + this.serviceDuration = serviceDuration; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public Long getServiceId() { + return serviceId; + } + + public void setServiceId(Long serviceId) { + this.serviceId = serviceId; + } + + public String getServiceName() { + return serviceName; + } + + public void setServiceName(String serviceName) { + this.serviceName = serviceName; + } + + public String getServiceDesc() { + return serviceDesc; + } + + public void setServiceDesc(String serviceDesc) { + this.serviceDesc = serviceDesc; + } + + public BigDecimal getServicePrice() { + return servicePrice; + } + + public void setServicePrice(BigDecimal servicePrice) { + this.servicePrice = servicePrice; + } + + public Integer getServiceDuration() { + return serviceDuration; + } + + public void setServiceDuration(Integer serviceDuration) { + this.serviceDuration = serviceDuration; + } + + 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; + ServiceResponse that = (ServiceResponse) o; + return Objects.equals(serviceId, that.serviceId) && Objects.equals(serviceName, that.serviceName) && Objects.equals(serviceDesc, that.serviceDesc) && Objects.equals(servicePrice, that.servicePrice) && Objects.equals(serviceDuration, that.serviceDuration) && Objects.equals(createdAt, that.createdAt) && Objects.equals(updatedAt, that.updatedAt); + } + + @Override + public int hashCode() { + return Objects.hash(serviceId, serviceName, serviceDesc, servicePrice, serviceDuration, createdAt, updatedAt); + } + + @Override + public String toString() { + return "ServiceResponse{" + + "serviceId=" + serviceId + + ", serviceName='" + serviceName + '\'' + + ", serviceDesc='" + serviceDesc + '\'' + + ", servicePrice=" + servicePrice + + ", serviceDuration=" + serviceDuration + + ", createdAt=" + createdAt + + ", updatedAt=" + updatedAt + + '}'; + } +} diff --git a/backend/src/main/java/com/petshop/backend/dto/store/StoreRequest.java b/backend/src/main/java/com/petshop/backend/dto/store/StoreRequest.java new file mode 100644 index 00000000..d6b5fc12 --- /dev/null +++ b/backend/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/backend/src/main/java/com/petshop/backend/dto/store/StoreResponse.java b/backend/src/main/java/com/petshop/backend/dto/store/StoreResponse.java new file mode 100644 index 00000000..e6fd563e --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/dto/store/StoreResponse.java @@ -0,0 +1,98 @@ +package com.petshop.backend.dto.store; + +import java.time.LocalDateTime; +import java.util.Objects; + +public class StoreResponse { + private Long storeId; + private String storeName; + private String address; + private String phone; + private String email; + private LocalDateTime createdAt; + + public StoreResponse() { + } + + public StoreResponse(Long storeId, String storeName, String address, String phone, String email, LocalDateTime createdAt) { + this.storeId = storeId; + this.storeName = storeName; + this.address = address; + this.phone = phone; + this.email = email; + this.createdAt = createdAt; + } + + public Long getStoreId() { + return storeId; + } + + public void setStoreId(Long storeId) { + this.storeId = storeId; + } + + 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; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + StoreResponse that = (StoreResponse) o; + return Objects.equals(storeId, that.storeId); + } + + @Override + public int hashCode() { + return Objects.hash(storeId); + } + + @Override + public String toString() { + return "StoreResponse{" + + "storeId=" + storeId + + ", storeName='" + storeName + '\'' + + ", address='" + address + '\'' + + ", phone='" + phone + '\'' + + ", email='" + email + '\'' + + ", createdAt=" + createdAt + + '}'; + } +} diff --git a/backend/src/main/java/com/petshop/backend/dto/supplier/SupplierRequest.java b/backend/src/main/java/com/petshop/backend/dto/supplier/SupplierRequest.java new file mode 100644 index 00000000..b7ae7efb --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/dto/supplier/SupplierRequest.java @@ -0,0 +1,87 @@ +package com.petshop.backend.dto.supplier; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import java.util.Objects; + +public class SupplierRequest { + @NotBlank(message = "Supplier company is required") + private String supCompany; + + private String supContactFirstName; + + private String supContactLastName; + + @Email(message = "Invalid email format") + private String supEmail; + + private String supPhone; + + public String getSupCompany() { + return supCompany; + } + + public void setSupCompany(String supCompany) { + this.supCompany = supCompany; + } + + public String getSupContactFirstName() { + return supContactFirstName; + } + + public void setSupContactFirstName(String supContactFirstName) { + this.supContactFirstName = supContactFirstName; + } + + public String getSupContactLastName() { + return supContactLastName; + } + + public void setSupContactLastName(String supContactLastName) { + this.supContactLastName = supContactLastName; + } + + public String getSupEmail() { + return supEmail; + } + + public void setSupEmail(String supEmail) { + this.supEmail = supEmail; + } + + public String getSupPhone() { + return supPhone; + } + + public void setSupPhone(String supPhone) { + this.supPhone = supPhone; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SupplierRequest that = (SupplierRequest) o; + return Objects.equals(supCompany, that.supCompany) && + Objects.equals(supContactFirstName, that.supContactFirstName) && + Objects.equals(supContactLastName, that.supContactLastName) && + Objects.equals(supEmail, that.supEmail) && + Objects.equals(supPhone, that.supPhone); + } + + @Override + public int hashCode() { + return Objects.hash(supCompany, supContactFirstName, supContactLastName, supEmail, supPhone); + } + + @Override + public String toString() { + return "SupplierRequest{" + + "supCompany='" + supCompany + '\'' + + ", supContactFirstName='" + supContactFirstName + '\'' + + ", supContactLastName='" + supContactLastName + '\'' + + ", supEmail='" + supEmail + '\'' + + ", supPhone='" + supPhone + '\'' + + '}'; + } +} diff --git a/backend/src/main/java/com/petshop/backend/dto/supplier/SupplierResponse.java b/backend/src/main/java/com/petshop/backend/dto/supplier/SupplierResponse.java new file mode 100644 index 00000000..d23b4a2b --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/dto/supplier/SupplierResponse.java @@ -0,0 +1,120 @@ +package com.petshop.backend.dto.supplier; + +import java.time.LocalDateTime; +import java.util.Objects; + +public class SupplierResponse { + private Long supId; + private String supCompany; + private String supContactFirstName; + private String supContactLastName; + private String supEmail; + private String supPhone; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + public SupplierResponse() { + } + + public SupplierResponse(Long supId, String supCompany, String supContactFirstName, String supContactLastName, String supEmail, String supPhone, LocalDateTime createdAt, LocalDateTime updatedAt) { + this.supId = supId; + this.supCompany = supCompany; + this.supContactFirstName = supContactFirstName; + this.supContactLastName = supContactLastName; + this.supEmail = supEmail; + this.supPhone = supPhone; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public Long getSupId() { + return supId; + } + + public void setSupId(Long supId) { + this.supId = supId; + } + + public String getSupCompany() { + return supCompany; + } + + public void setSupCompany(String supCompany) { + this.supCompany = supCompany; + } + + public String getSupContactFirstName() { + return supContactFirstName; + } + + public void setSupContactFirstName(String supContactFirstName) { + this.supContactFirstName = supContactFirstName; + } + + public String getSupContactLastName() { + return supContactLastName; + } + + public void setSupContactLastName(String supContactLastName) { + this.supContactLastName = supContactLastName; + } + + public String getSupEmail() { + return supEmail; + } + + public void setSupEmail(String supEmail) { + this.supEmail = supEmail; + } + + public String getSupPhone() { + return supPhone; + } + + public void setSupPhone(String supPhone) { + this.supPhone = supPhone; + } + + 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; + SupplierResponse that = (SupplierResponse) o; + return Objects.equals(supId, that.supId) && Objects.equals(supCompany, that.supCompany) && Objects.equals(supContactFirstName, that.supContactFirstName) && Objects.equals(supContactLastName, that.supContactLastName) && Objects.equals(supEmail, that.supEmail) && Objects.equals(supPhone, that.supPhone) && Objects.equals(createdAt, that.createdAt) && Objects.equals(updatedAt, that.updatedAt); + } + + @Override + public int hashCode() { + return Objects.hash(supId, supCompany, supContactFirstName, supContactLastName, supEmail, supPhone, createdAt, updatedAt); + } + + @Override + public String toString() { + return "SupplierResponse{" + + "supId=" + supId + + ", supCompany='" + supCompany + '\'' + + ", supContactFirstName='" + supContactFirstName + '\'' + + ", supContactLastName='" + supContactLastName + '\'' + + ", supEmail='" + supEmail + '\'' + + ", supPhone='" + supPhone + '\'' + + ", createdAt=" + createdAt + + ", updatedAt=" + updatedAt + + '}'; + } +} diff --git a/backend/src/main/java/com/petshop/backend/dto/user/UserRequest.java b/backend/src/main/java/com/petshop/backend/dto/user/UserRequest.java new file mode 100644 index 00000000..09a9036d --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/dto/user/UserRequest.java @@ -0,0 +1,119 @@ +package com.petshop.backend.dto.user; + +import com.petshop.backend.entity.User; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import java.util.Objects; + +public class UserRequest { + @NotBlank(message = "Username is required") + @Size(min = 3, max = 50, message = "Username must be between 3 and 50 characters") + private String username; + + @Size(min = 6, message = "Password must be at least 6 characters") + private String password; + + @NotBlank(message = "Full name is required") + private String fullName; + + @Email(message = "Invalid email format") + private String email; + + @Size(max = 20, message = "Phone must not exceed 20 characters") + private String phone; + + @NotNull(message = "Role is required") + private User.Role role; + + private Boolean active = true; + + 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 getFullName() { + return fullName; + } + + public void setFullName(String fullName) { + this.fullName = fullName; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPhone() { + return phone; + } + + public void setPhone(String phone) { + this.phone = phone; + } + + public User.Role getRole() { + return role; + } + + public void setRole(User.Role role) { + this.role = role; + } + + public Boolean getActive() { + return active; + } + + public void setActive(Boolean active) { + this.active = active; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + UserRequest that = (UserRequest) o; + return Objects.equals(username, that.username) && + Objects.equals(password, that.password) && + Objects.equals(fullName, that.fullName) && + Objects.equals(email, that.email) && + Objects.equals(phone, that.phone) && + role == that.role && + Objects.equals(active, that.active); + } + + @Override + public int hashCode() { + return Objects.hash(username, password, fullName, email, phone, role, active); + } + + @Override + public String toString() { + return "UserRequest{" + + "username='" + username + '\'' + + ", password='" + password + '\'' + + ", fullName='" + fullName + '\'' + + ", email='" + email + '\'' + + ", phone='" + phone + '\'' + + ", role=" + role + + ", active=" + active + + '}'; + } +} diff --git a/backend/src/main/java/com/petshop/backend/dto/user/UserResponse.java b/backend/src/main/java/com/petshop/backend/dto/user/UserResponse.java new file mode 100644 index 00000000..9d7167c2 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/dto/user/UserResponse.java @@ -0,0 +1,131 @@ +package com.petshop.backend.dto.user; + +import java.time.LocalDateTime; +import java.util.Objects; + +public class UserResponse { + private Long id; + private String username; + private String fullName; + private String email; + private String phone; + private String role; + private Boolean active; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + public UserResponse() { + } + + public UserResponse(Long id, String username, String fullName, String email, String phone, String role, Boolean active, LocalDateTime createdAt, LocalDateTime updatedAt) { + this.id = id; + this.username = username; + this.fullName = fullName; + this.email = email; + this.phone = phone; + this.role = role; + this.active = active; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + 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 getFullName() { + return fullName; + } + + public void setFullName(String fullName) { + this.fullName = fullName; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPhone() { + return phone; + } + + public void setPhone(String phone) { + this.phone = phone; + } + + public String getRole() { + return role; + } + + public void setRole(String role) { + this.role = role; + } + + public Boolean getActive() { + return active; + } + + public void setActive(Boolean active) { + this.active = active; + } + + 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; + UserResponse that = (UserResponse) o; + return Objects.equals(id, that.id) && Objects.equals(username, that.username) && Objects.equals(fullName, that.fullName) && Objects.equals(email, that.email) && Objects.equals(phone, that.phone) && Objects.equals(role, that.role) && Objects.equals(active, that.active) && Objects.equals(createdAt, that.createdAt) && Objects.equals(updatedAt, that.updatedAt); + } + + @Override + public int hashCode() { + return Objects.hash(id, username, fullName, email, phone, role, active, createdAt, updatedAt); + } + + @Override + public String toString() { + return "UserResponse{" + + "id=" + id + + ", username='" + username + '\'' + + ", fullName='" + fullName + '\'' + + ", email='" + email + '\'' + + ", phone='" + phone + '\'' + + ", role='" + role + '\'' + + ", active=" + active + + ", createdAt=" + createdAt + + ", updatedAt=" + updatedAt + + '}'; + } +} diff --git a/backend/src/main/java/com/petshop/backend/entity/ActivityLog.java b/backend/src/main/java/com/petshop/backend/entity/ActivityLog.java new file mode 100644 index 00000000..211f75de --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/entity/ActivityLog.java @@ -0,0 +1,90 @@ +package com.petshop.backend.entity; + +import jakarta.persistence.*; + +import java.time.LocalDateTime; +import java.util.Objects; + +@Entity +@Table(name = "activityLog") +public class ActivityLog { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long logId; + + @ManyToOne + @JoinColumn(name = "employeeId", nullable = false) + private Employee employee; + + @Column(nullable = false, columnDefinition = "TEXT") + private String activity; + + @Column(nullable = false) + private LocalDateTime logTimestamp = LocalDateTime.now(); + + public ActivityLog() { + } + + public ActivityLog(Long logId, Employee employee, String activity, LocalDateTime logTimestamp) { + this.logId = logId; + this.employee = employee; + this.activity = activity; + this.logTimestamp = logTimestamp; + } + + public Long getLogId() { + return logId; + } + + public void setLogId(Long logId) { + this.logId = logId; + } + + public Employee getEmployee() { + return employee; + } + + public void setEmployee(Employee employee) { + this.employee = employee; + } + + public String getActivity() { + return activity; + } + + public void setActivity(String activity) { + this.activity = activity; + } + + public LocalDateTime getLogTimestamp() { + return logTimestamp; + } + + public void setLogTimestamp(LocalDateTime logTimestamp) { + this.logTimestamp = logTimestamp; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ActivityLog that = (ActivityLog) o; + return Objects.equals(logId, that.logId); + } + + @Override + public int hashCode() { + return Objects.hash(logId); + } + + @Override + public String toString() { + return "ActivityLog{" + + "logId=" + logId + + ", employee=" + employee + + ", activity='" + activity + '\'' + + ", logTimestamp=" + logTimestamp + + '}'; + } +} diff --git a/backend/src/main/java/com/petshop/backend/entity/Adoption.java b/backend/src/main/java/com/petshop/backend/entity/Adoption.java new file mode 100644 index 00000000..84912ba9 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/entity/Adoption.java @@ -0,0 +1,136 @@ +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.LocalDate; +import java.time.LocalDateTime; +import java.util.Objects; + +@Entity +@Table(name = "adoption") +public class Adoption { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long adoptionId; + + @ManyToOne + @JoinColumn(name = "petId", nullable = false) + private Pet pet; + + @ManyToOne + @JoinColumn(name = "customerId", nullable = false) + private Customer customer; + + @Column(nullable = false) + private LocalDate adoptionDate; + + @Column(nullable = false, length = 20) + private String adoptionStatus; + + @CreationTimestamp + @Column(name = "created_at", updatable = false) + private LocalDateTime createdAt; + + @UpdateTimestamp + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + public Adoption() { + } + + public Adoption(Long adoptionId, Pet pet, Customer customer, LocalDate adoptionDate, String adoptionStatus, LocalDateTime createdAt, LocalDateTime updatedAt) { + this.adoptionId = adoptionId; + this.pet = pet; + this.customer = customer; + this.adoptionDate = adoptionDate; + this.adoptionStatus = adoptionStatus; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public Long getAdoptionId() { + return adoptionId; + } + + public void setAdoptionId(Long adoptionId) { + this.adoptionId = adoptionId; + } + + public Pet getPet() { + return pet; + } + + public void setPet(Pet pet) { + this.pet = pet; + } + + public Customer getCustomer() { + return customer; + } + + public void setCustomer(Customer customer) { + this.customer = customer; + } + + public LocalDate getAdoptionDate() { + return adoptionDate; + } + + public void setAdoptionDate(LocalDate adoptionDate) { + this.adoptionDate = adoptionDate; + } + + public String getAdoptionStatus() { + return adoptionStatus; + } + + public void setAdoptionStatus(String adoptionStatus) { + this.adoptionStatus = adoptionStatus; + } + + 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; + Adoption adoption = (Adoption) o; + return Objects.equals(adoptionId, adoption.adoptionId); + } + + @Override + public int hashCode() { + return Objects.hash(adoptionId); + } + + @Override + public String toString() { + return "Adoption{" + + "adoptionId=" + adoptionId + + ", pet=" + pet + + ", customer=" + customer + + ", adoptionDate=" + adoptionDate + + ", adoptionStatus='" + adoptionStatus + '\'' + + ", createdAt=" + createdAt + + ", updatedAt=" + updatedAt + + '}'; + } +} diff --git a/backend/src/main/java/com/petshop/backend/entity/Appointment.java b/backend/src/main/java/com/petshop/backend/entity/Appointment.java new file mode 100644 index 00000000..101ff885 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/entity/Appointment.java @@ -0,0 +1,183 @@ +package com.petshop.backend.entity; + +import jakarta.persistence.*; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +@Entity +@Table(name = "appointment") +public class Appointment { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long appointmentId; + + @ManyToOne + @JoinColumn(name = "customerId", nullable = false) + private Customer customer; + + @ManyToOne + @JoinColumn(name = "storeId", nullable = false) + private StoreLocation store; + + @ManyToOne + @JoinColumn(name = "serviceId", nullable = false) + private Service service; + + @Column(nullable = false) + private LocalDate appointmentDate; + + @Column(nullable = false) + private LocalTime appointmentTime; + + @Column(nullable = false, length = 20) + private String appointmentStatus; + + @ManyToMany + @JoinTable( + name = "appointmentPet", + joinColumns = @JoinColumn(name = "appointmentId"), + inverseJoinColumns = @JoinColumn(name = "petId") + ) + private Set pets = new HashSet<>(); + + @CreationTimestamp + @Column(name = "created_at", updatable = false) + private LocalDateTime createdAt; + + @UpdateTimestamp + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + public Appointment() { + } + + public Appointment(Long appointmentId, Customer customer, StoreLocation store, Service service, LocalDate appointmentDate, LocalTime appointmentTime, String appointmentStatus, Set pets, LocalDateTime createdAt, LocalDateTime updatedAt) { + this.appointmentId = appointmentId; + this.customer = customer; + this.store = store; + this.service = service; + this.appointmentDate = appointmentDate; + this.appointmentTime = appointmentTime; + this.appointmentStatus = appointmentStatus; + this.pets = pets; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public Long getAppointmentId() { + return appointmentId; + } + + public void setAppointmentId(Long appointmentId) { + this.appointmentId = appointmentId; + } + + public Customer getCustomer() { + return customer; + } + + public void setCustomer(Customer customer) { + this.customer = customer; + } + + public StoreLocation getStore() { + return store; + } + + public void setStore(StoreLocation store) { + this.store = store; + } + + public Service getService() { + return service; + } + + public void setService(Service service) { + this.service = service; + } + + public LocalDate getAppointmentDate() { + return appointmentDate; + } + + public void setAppointmentDate(LocalDate appointmentDate) { + this.appointmentDate = appointmentDate; + } + + public LocalTime getAppointmentTime() { + return appointmentTime; + } + + public void setAppointmentTime(LocalTime appointmentTime) { + this.appointmentTime = appointmentTime; + } + + public String getAppointmentStatus() { + return appointmentStatus; + } + + public void setAppointmentStatus(String appointmentStatus) { + this.appointmentStatus = appointmentStatus; + } + + public Set getPets() { + return pets; + } + + public void setPets(Set pets) { + this.pets = pets; + } + + 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; + Appointment that = (Appointment) o; + return Objects.equals(appointmentId, that.appointmentId); + } + + @Override + public int hashCode() { + return Objects.hash(appointmentId); + } + + @Override + public String toString() { + return "Appointment{" + + "appointmentId=" + appointmentId + + ", customer=" + customer + + ", store=" + store + + ", service=" + service + + ", appointmentDate=" + appointmentDate + + ", appointmentTime=" + appointmentTime + + ", appointmentStatus='" + appointmentStatus + '\'' + + ", pets=" + pets + + ", createdAt=" + createdAt + + ", updatedAt=" + updatedAt + + '}'; + } +} diff --git a/backend/src/main/java/com/petshop/backend/entity/Category.java b/backend/src/main/java/com/petshop/backend/entity/Category.java new file mode 100644 index 00000000..ee79207e --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/entity/Category.java @@ -0,0 +1,106 @@ +package com.petshop.backend.entity; + +import jakarta.persistence.*; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.time.LocalDateTime; +import java.util.Objects; + +@Entity +@Table(name = "category") +public class Category { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long categoryId; + + @Column(nullable = false, length = 100) + private String categoryName; + + @Column(nullable = false, length = 50) + private String categoryType; + + @CreationTimestamp + @Column(name = "created_at", updatable = false) + private LocalDateTime createdAt; + + @UpdateTimestamp + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + public Category() { + } + + public Category(Long categoryId, String categoryName, String categoryType, LocalDateTime createdAt, LocalDateTime updatedAt) { + this.categoryId = categoryId; + this.categoryName = categoryName; + this.categoryType = categoryType; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public Long getCategoryId() { + return categoryId; + } + + public void setCategoryId(Long categoryId) { + this.categoryId = categoryId; + } + + public String getCategoryName() { + return categoryName; + } + + public void setCategoryName(String categoryName) { + this.categoryName = categoryName; + } + + public String getCategoryType() { + return categoryType; + } + + public void setCategoryType(String categoryType) { + this.categoryType = categoryType; + } + + 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; + Category category = (Category) o; + return Objects.equals(categoryId, category.categoryId); + } + + @Override + public int hashCode() { + return Objects.hash(categoryId); + } + + @Override + public String toString() { + return "Category{" + + "categoryId=" + categoryId + + ", categoryName='" + categoryName + '\'' + + ", categoryType='" + categoryType + '\'' + + ", createdAt=" + createdAt + + ", updatedAt=" + updatedAt + + '}'; + } +} diff --git a/backend/src/main/java/com/petshop/backend/entity/Conversation.java b/backend/src/main/java/com/petshop/backend/entity/Conversation.java new file mode 100644 index 00000000..080228eb --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/entity/Conversation.java @@ -0,0 +1,127 @@ +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, columnDefinition = "VARCHAR(20)") + private ConversationStatus status = ConversationStatus.OPEN; + + @Enumerated(EnumType.STRING) + @Column(length = 20, nullable = false, columnDefinition = "VARCHAR(20)") + private ConversationMode mode = ConversationMode.AUTOMATED; + + @Column + private LocalDateTime humanRequestedAt; + + @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 enum ConversationMode { + AUTOMATED, HUMAN + } + + public Conversation() { + } + + public Conversation(Long id, Long customerId, Long staffId, ConversationStatus status, ConversationMode mode, LocalDateTime humanRequestedAt, LocalDateTime createdAt, LocalDateTime updatedAt) { + this.id = id; + this.customerId = customerId; + this.staffId = staffId; + this.status = status; + this.mode = mode; + this.humanRequestedAt = humanRequestedAt; + 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 ConversationMode getMode() { + return mode; + } + + public void setMode(ConversationMode mode) { + this.mode = mode; + } + + public LocalDateTime getHumanRequestedAt() { + return humanRequestedAt; + } + + public void setHumanRequestedAt(LocalDateTime humanRequestedAt) { + this.humanRequestedAt = humanRequestedAt; + } + + 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/backend/src/main/java/com/petshop/backend/entity/Customer.java b/backend/src/main/java/com/petshop/backend/entity/Customer.java new file mode 100644 index 00000000..09035619 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/entity/Customer.java @@ -0,0 +1,132 @@ +package com.petshop.backend.entity; + +import jakarta.persistence.*; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.time.LocalDateTime; +import java.util.Objects; + +@Entity +@Table(name = "customer") +public class Customer { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long customerId; + + @Column(name = "user_id") + private Long userId; + + @Column(nullable = false, length = 50) + private String firstName; + + @Column(nullable = false, length = 50) + private String lastName; + + @Column(nullable = false, length = 100) + private String email; + + @CreationTimestamp + @Column(name = "created_at", updatable = false) + private LocalDateTime createdAt; + + @UpdateTimestamp + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + public Customer() { + } + + public Customer(Long customerId, Long userId, String firstName, String lastName, String email, LocalDateTime createdAt, LocalDateTime updatedAt) { + this.customerId = customerId; + this.userId = userId; + this.firstName = firstName; + this.lastName = lastName; + this.email = email; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public Long getCustomerId() { + return customerId; + } + + public void setCustomerId(Long customerId) { + this.customerId = customerId; + } + + public Long getUserId() { + return userId; + } + + public void setUserId(Long userId) { + this.userId = userId; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + 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; + Customer customer = (Customer) o; + return Objects.equals(customerId, customer.customerId); + } + + @Override + public int hashCode() { + return Objects.hash(customerId); + } + + @Override + public String toString() { + return "Customer{" + + "customerId=" + customerId + + ", userId=" + userId + + ", firstName='" + firstName + '\'' + + ", lastName='" + lastName + '\'' + + ", email='" + email + '\'' + + ", createdAt=" + createdAt + + ", updatedAt=" + updatedAt + + '}'; + } +} diff --git a/backend/src/main/java/com/petshop/backend/entity/Employee.java b/backend/src/main/java/com/petshop/backend/entity/Employee.java new file mode 100644 index 00000000..c88216f6 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/entity/Employee.java @@ -0,0 +1,158 @@ +package com.petshop.backend.entity; + +import jakarta.persistence.*; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.time.LocalDateTime; +import java.util.Objects; + +@Entity +@Table(name = "employee") +public class Employee { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long employeeId; + + @Column(name = "user_id") + private Long userId; + + @Column(nullable = false, length = 50) + private String firstName; + + @Column(nullable = false, length = 50) + private String lastName; + + @Column(nullable = false, length = 100) + private String email; + + @Column(nullable = false, length = 50) + private String role; + + @Column(nullable = false) + private Boolean isActive = true; + + @CreationTimestamp + @Column(name = "created_at", updatable = false) + private LocalDateTime createdAt; + + @UpdateTimestamp + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + public Employee() { + } + + public Employee(Long employeeId, Long userId, String firstName, String lastName, String email, String role, Boolean isActive, LocalDateTime createdAt, LocalDateTime updatedAt) { + this.employeeId = employeeId; + this.userId = userId; + this.firstName = firstName; + this.lastName = lastName; + this.email = email; + this.role = role; + this.isActive = isActive; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public Long getEmployeeId() { + return employeeId; + } + + public void setEmployeeId(Long employeeId) { + this.employeeId = employeeId; + } + + public Long getUserId() { + return userId; + } + + public void setUserId(Long userId) { + this.userId = userId; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + 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 Boolean getIsActive() { + return isActive; + } + + public void setIsActive(Boolean isActive) { + this.isActive = isActive; + } + + 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; + Employee employee = (Employee) o; + return Objects.equals(employeeId, employee.employeeId); + } + + @Override + public int hashCode() { + return Objects.hash(employeeId); + } + + @Override + public String toString() { + return "Employee{" + + "employeeId=" + employeeId + + ", userId=" + userId + + ", firstName='" + firstName + '\'' + + ", lastName='" + lastName + '\'' + + ", email='" + email + '\'' + + ", role='" + role + '\'' + + ", isActive=" + isActive + + ", createdAt=" + createdAt + + ", updatedAt=" + updatedAt + + '}'; + } +} diff --git a/backend/src/main/java/com/petshop/backend/entity/EmployeeStore.java b/backend/src/main/java/com/petshop/backend/entity/EmployeeStore.java new file mode 100644 index 00000000..daa2a2e2 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/entity/EmployeeStore.java @@ -0,0 +1,117 @@ +package com.petshop.backend.entity; + +import jakarta.persistence.*; + +import java.io.Serializable; +import java.util.Objects; + +@Entity +@Table(name = "employeeStore") +@IdClass(EmployeeStore.EmployeeStoreId.class) +public class EmployeeStore { + + @Id + @ManyToOne + @JoinColumn(name = "employeeId", nullable = false) + private Employee employee; + + @Id + @ManyToOne + @JoinColumn(name = "storeId", nullable = false) + private StoreLocation store; + + public EmployeeStore() { + } + + public EmployeeStore(Employee employee, StoreLocation store) { + this.employee = employee; + this.store = store; + } + + public Employee getEmployee() { + return employee; + } + + public void setEmployee(Employee employee) { + this.employee = employee; + } + + public StoreLocation getStore() { + return store; + } + + public void setStore(StoreLocation store) { + this.store = store; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + EmployeeStore that = (EmployeeStore) o; + return Objects.equals(employee, that.employee) && Objects.equals(store, that.store); + } + + @Override + public int hashCode() { + return Objects.hash(employee, store); + } + + @Override + public String toString() { + return "EmployeeStore{" + + "employee=" + employee + + ", store=" + store + + '}'; + } + + public static class EmployeeStoreId implements Serializable { + private Long employee; + private Long store; + + public EmployeeStoreId() { + } + + public EmployeeStoreId(Long employee, Long store) { + this.employee = employee; + this.store = store; + } + + public Long getEmployee() { + return employee; + } + + public void setEmployee(Long employee) { + this.employee = employee; + } + + public Long getStore() { + return store; + } + + public void setStore(Long store) { + this.store = store; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + EmployeeStoreId that = (EmployeeStoreId) o; + return Objects.equals(employee, that.employee) && Objects.equals(store, that.store); + } + + @Override + public int hashCode() { + return Objects.hash(employee, store); + } + + @Override + public String toString() { + return "EmployeeStoreId{" + + "employee=" + employee + + ", store=" + store + + '}'; + } + } +} diff --git a/backend/src/main/java/com/petshop/backend/entity/Inventory.java b/backend/src/main/java/com/petshop/backend/entity/Inventory.java new file mode 100644 index 00000000..07b93501 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/entity/Inventory.java @@ -0,0 +1,107 @@ +package com.petshop.backend.entity; + +import jakarta.persistence.*; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.time.LocalDateTime; +import java.util.Objects; + +@Entity +@Table(name = "inventory") +public class Inventory { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long inventoryId; + + @ManyToOne + @JoinColumn(name = "prodId", nullable = false) + private Product product; + + @Column(nullable = false) + private Integer quantity = 0; + + @CreationTimestamp + @Column(name = "created_at", updatable = false) + private LocalDateTime createdAt; + + @UpdateTimestamp + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + public Inventory() { + } + + public Inventory(Long inventoryId, Product product, Integer quantity, LocalDateTime createdAt, LocalDateTime updatedAt) { + this.inventoryId = inventoryId; + this.product = product; + this.quantity = quantity; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public Long getInventoryId() { + return inventoryId; + } + + public void setInventoryId(Long inventoryId) { + this.inventoryId = inventoryId; + } + + public Product getProduct() { + return product; + } + + public void setProduct(Product product) { + this.product = product; + } + + public Integer getQuantity() { + return quantity; + } + + public void setQuantity(Integer quantity) { + this.quantity = quantity; + } + + 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; + Inventory inventory = (Inventory) o; + return Objects.equals(inventoryId, inventory.inventoryId); + } + + @Override + public int hashCode() { + return Objects.hash(inventoryId); + } + + @Override + public String toString() { + return "Inventory{" + + "inventoryId=" + inventoryId + + ", product=" + product + + ", quantity=" + quantity + + ", createdAt=" + createdAt + + ", updatedAt=" + updatedAt + + '}'; + } +} diff --git a/backend/src/main/java/com/petshop/backend/entity/Message.java b/backend/src/main/java/com/petshop/backend/entity/Message.java new file mode 100644 index 00000000..33777bf5 --- /dev/null +++ b/backend/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/backend/src/main/java/com/petshop/backend/entity/Pet.java b/backend/src/main/java/com/petshop/backend/entity/Pet.java new file mode 100644 index 00000000..e827f612 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/entity/Pet.java @@ -0,0 +1,160 @@ +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 = "pet") +public class Pet { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "petId") + private Long id; + + @Column(nullable = false, length = 50) + private String petName; + + @Column(nullable = false, length = 50) + private String petSpecies; + + @Column(nullable = false, length = 50) + private String petBreed; + + @Column(nullable = false) + private Integer petAge; + + @Column(nullable = false, length = 20) + private String petStatus; + + @Column(nullable = false, precision = 10, scale = 2) + private BigDecimal petPrice; + + @CreationTimestamp + @Column(name = "created_at", updatable = false) + private LocalDateTime createdAt; + + @UpdateTimestamp + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + public Pet() { + } + + 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; + this.petAge = petAge; + this.petStatus = petStatus; + this.petPrice = petPrice; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public Long getPetId() { + return id; + } + + public void setPetId(Long id) { + this.id = id; + } + + public String getPetName() { + return petName; + } + + public void setPetName(String petName) { + this.petName = petName; + } + + public String getPetSpecies() { + return petSpecies; + } + + public void setPetSpecies(String petSpecies) { + this.petSpecies = petSpecies; + } + + public String getPetBreed() { + return petBreed; + } + + public void setPetBreed(String petBreed) { + this.petBreed = petBreed; + } + + public Integer getPetAge() { + return petAge; + } + + public void setPetAge(Integer petAge) { + this.petAge = petAge; + } + + public String getPetStatus() { + return petStatus; + } + + public void setPetStatus(String petStatus) { + this.petStatus = petStatus; + } + + public BigDecimal getPetPrice() { + return petPrice; + } + + public void setPetPrice(BigDecimal petPrice) { + this.petPrice = petPrice; + } + + 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; + Pet pet = (Pet) o; + return Objects.equals(id, pet.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + @Override + public String toString() { + return "Pet{" + + "id=" + id + + ", petName='" + petName + '\'' + + ", petSpecies='" + petSpecies + '\'' + + ", petBreed='" + petBreed + '\'' + + ", petAge=" + petAge + + ", petStatus='" + petStatus + '\'' + + ", petPrice=" + petPrice + + ", createdAt=" + createdAt + + ", updatedAt=" + updatedAt + + '}'; + } +} diff --git a/backend/src/main/java/com/petshop/backend/entity/Product.java b/backend/src/main/java/com/petshop/backend/entity/Product.java new file mode 100644 index 00000000..9eb9c2d6 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/entity/Product.java @@ -0,0 +1,134 @@ +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 = "product") +public class Product { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long prodId; + + @Column(nullable = false, length = 100) + private String prodName; + + @ManyToOne + @JoinColumn(name = "categoryId", nullable = false) + private Category category; + + @Column(columnDefinition = "TEXT") + private String prodDesc; + + @Column(nullable = false, precision = 10, scale = 2) + private BigDecimal prodPrice; + + @CreationTimestamp + @Column(name = "created_at", updatable = false) + private LocalDateTime createdAt; + + @UpdateTimestamp + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + public Product() { + } + + public Product(Long prodId, String prodName, Category category, String prodDesc, BigDecimal prodPrice, LocalDateTime createdAt, LocalDateTime updatedAt) { + this.prodId = prodId; + this.prodName = prodName; + this.category = category; + this.prodDesc = prodDesc; + this.prodPrice = prodPrice; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public Long getProdId() { + return prodId; + } + + public void setProdId(Long prodId) { + this.prodId = prodId; + } + + public String getProdName() { + return prodName; + } + + public void setProdName(String prodName) { + this.prodName = prodName; + } + + public Category getCategory() { + return category; + } + + public void setCategory(Category category) { + this.category = category; + } + + public String getProdDesc() { + return prodDesc; + } + + public void setProdDesc(String prodDesc) { + this.prodDesc = prodDesc; + } + + public BigDecimal getProdPrice() { + return prodPrice; + } + + public void setProdPrice(BigDecimal prodPrice) { + this.prodPrice = prodPrice; + } + + 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; + Product product = (Product) o; + return Objects.equals(prodId, product.prodId); + } + + @Override + public int hashCode() { + return Objects.hash(prodId); + } + + @Override + public String toString() { + return "Product{" + + "prodId=" + prodId + + ", prodName='" + prodName + '\'' + + ", category=" + category + + ", prodDesc='" + prodDesc + '\'' + + ", prodPrice=" + prodPrice + + ", createdAt=" + createdAt + + ", updatedAt=" + updatedAt + + '}'; + } +} diff --git a/backend/src/main/java/com/petshop/backend/entity/ProductSupplier.java b/backend/src/main/java/com/petshop/backend/entity/ProductSupplier.java new file mode 100644 index 00000000..95aa6017 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/entity/ProductSupplier.java @@ -0,0 +1,162 @@ +package com.petshop.backend.entity; + +import jakarta.persistence.*; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.Objects; + +@Entity +@Table(name = "productSupplier") +@IdClass(ProductSupplier.ProductSupplierId.class) +public class ProductSupplier { + + @Id + @ManyToOne + @JoinColumn(name = "prodId", nullable = false) + private Product product; + + @Id + @ManyToOne + @JoinColumn(name = "supId", nullable = false) + private Supplier supplier; + + @Column(nullable = false, precision = 10, scale = 2) + private BigDecimal cost; + + @CreationTimestamp + @Column(name = "created_at", updatable = false) + private LocalDateTime createdAt; + + @UpdateTimestamp + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + public ProductSupplier() { + } + + public ProductSupplier(Product product, Supplier supplier, BigDecimal cost, LocalDateTime createdAt, LocalDateTime updatedAt) { + this.product = product; + this.supplier = supplier; + this.cost = cost; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public Product getProduct() { + return product; + } + + public void setProduct(Product product) { + this.product = product; + } + + public Supplier getSupplier() { + return supplier; + } + + public void setSupplier(Supplier supplier) { + this.supplier = supplier; + } + + public BigDecimal getCost() { + return cost; + } + + public void setCost(BigDecimal cost) { + this.cost = cost; + } + + 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; + ProductSupplier that = (ProductSupplier) o; + return Objects.equals(product, that.product) && Objects.equals(supplier, that.supplier); + } + + @Override + public int hashCode() { + return Objects.hash(product, supplier); + } + + @Override + public String toString() { + return "ProductSupplier{" + + "product=" + product + + ", supplier=" + supplier + + ", cost=" + cost + + ", createdAt=" + createdAt + + ", updatedAt=" + updatedAt + + '}'; + } + + public static class ProductSupplierId implements Serializable { + private Long product; + private Long supplier; + + public ProductSupplierId() { + } + + public ProductSupplierId(Long product, Long supplier) { + this.product = product; + this.supplier = supplier; + } + + public Long getProduct() { + return product; + } + + public void setProduct(Long product) { + this.product = product; + } + + public Long getSupplier() { + return supplier; + } + + public void setSupplier(Long supplier) { + this.supplier = supplier; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ProductSupplierId that = (ProductSupplierId) o; + return Objects.equals(product, that.product) && Objects.equals(supplier, that.supplier); + } + + @Override + public int hashCode() { + return Objects.hash(product, supplier); + } + + @Override + public String toString() { + return "ProductSupplierId{" + + "product=" + product + + ", supplier=" + supplier + + '}'; + } + } +} diff --git a/backend/src/main/java/com/petshop/backend/entity/PurchaseOrder.java b/backend/src/main/java/com/petshop/backend/entity/PurchaseOrder.java new file mode 100644 index 00000000..76fc9a9d --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/entity/PurchaseOrder.java @@ -0,0 +1,124 @@ +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.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +@Entity +@Table(name = "purchaseOrder") +public class PurchaseOrder { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long purchaseOrderId; + + @ManyToOne + @JoinColumn(name = "supId", nullable = false) + private Supplier supplier; + + @Column(nullable = false) + private LocalDate orderDate; + + @Column(nullable = false, length = 50) + private String status; + + @CreationTimestamp + @Column(name = "created_at", updatable = false) + private LocalDateTime createdAt; + + @UpdateTimestamp + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + public PurchaseOrder() { + } + + public PurchaseOrder(Long purchaseOrderId, Supplier supplier, LocalDate orderDate, String status, LocalDateTime createdAt, LocalDateTime updatedAt) { + this.purchaseOrderId = purchaseOrderId; + this.supplier = supplier; + this.orderDate = orderDate; + this.status = status; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public Long getPurchaseOrderId() { + return purchaseOrderId; + } + + public void setPurchaseOrderId(Long purchaseOrderId) { + this.purchaseOrderId = purchaseOrderId; + } + + public Supplier getSupplier() { + return supplier; + } + + public void setSupplier(Supplier supplier) { + this.supplier = supplier; + } + + public LocalDate getOrderDate() { + return orderDate; + } + + public void setOrderDate(LocalDate orderDate) { + this.orderDate = orderDate; + } + + 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; + PurchaseOrder that = (PurchaseOrder) o; + return Objects.equals(purchaseOrderId, that.purchaseOrderId); + } + + @Override + public int hashCode() { + return Objects.hash(purchaseOrderId); + } + + @Override + public String toString() { + return "PurchaseOrder{" + + "purchaseOrderId=" + purchaseOrderId + + ", supplier=" + supplier + + ", orderDate=" + orderDate + + ", status='" + status + '\'' + + ", createdAt=" + createdAt + + ", updatedAt=" + updatedAt + + '}'; + } +} diff --git a/backend/src/main/java/com/petshop/backend/entity/Refund.java b/backend/src/main/java/com/petshop/backend/entity/Refund.java new file mode 100644 index 00000000..4973427e --- /dev/null +++ b/backend/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, columnDefinition = "VARCHAR(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/backend/src/main/java/com/petshop/backend/entity/Sale.java b/backend/src/main/java/com/petshop/backend/entity/Sale.java new file mode 100644 index 00000000..c60c0927 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/entity/Sale.java @@ -0,0 +1,204 @@ +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.ArrayList; +import java.util.List; +import java.util.Objects; + +@Entity +@Table(name = "sale") +public class Sale { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long saleId; + + @Column(nullable = false) + private LocalDateTime saleDate = LocalDateTime.now(); + + @ManyToOne + @JoinColumn(name = "employeeId", nullable = false) + private Employee employee; + + @ManyToOne + @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; + + @Column(nullable = false, length = 50) + private String paymentMethod; + + @Column(nullable = false) + private Boolean isRefund = false; + + @ManyToOne + @JoinColumn(name = "originalSaleId") + private Sale originalSale; + + @OneToMany(mappedBy = "sale", cascade = CascadeType.ALL) + private List items = new ArrayList<>(); + + @CreationTimestamp + @Column(name = "created_at", updatable = false) + private LocalDateTime createdAt; + + @UpdateTimestamp + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + public Sale() { + } + + 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; + this.originalSale = originalSale; + this.items = items; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public Long getSaleId() { + return saleId; + } + + public void setSaleId(Long saleId) { + this.saleId = saleId; + } + + public LocalDateTime getSaleDate() { + return saleDate; + } + + public void setSaleDate(LocalDateTime saleDate) { + this.saleDate = saleDate; + } + + public Employee getEmployee() { + return employee; + } + + public void setEmployee(Employee employee) { + this.employee = employee; + } + + public StoreLocation getStore() { + return store; + } + + public void setStore(StoreLocation store) { + this.store = store; + } + + public Customer getCustomer() { + return customer; + } + + public void setCustomer(Customer customer) { + this.customer = customer; + } + + public BigDecimal getTotalAmount() { + return totalAmount; + } + + public void setTotalAmount(BigDecimal totalAmount) { + this.totalAmount = totalAmount; + } + + public String getPaymentMethod() { + return paymentMethod; + } + + public void setPaymentMethod(String paymentMethod) { + this.paymentMethod = paymentMethod; + } + + public Boolean getIsRefund() { + return isRefund; + } + + public void setIsRefund(Boolean isRefund) { + this.isRefund = isRefund; + } + + public Sale getOriginalSale() { + return originalSale; + } + + public void setOriginalSale(Sale originalSale) { + this.originalSale = originalSale; + } + + public List getItems() { + return items; + } + + public void setItems(List items) { + this.items = items; + } + + 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; + Sale sale = (Sale) o; + return Objects.equals(saleId, sale.saleId); + } + + @Override + public int hashCode() { + return Objects.hash(saleId); + } + + @Override + public String toString() { + return "Sale{" + + "saleId=" + saleId + + ", saleDate=" + saleDate + + ", employee=" + employee + + ", store=" + store + + ", customer=" + customer + + ", totalAmount=" + totalAmount + + ", paymentMethod='" + paymentMethod + '\'' + + ", isRefund=" + isRefund + + ", originalSale=" + originalSale + + ", items=" + items + + ", createdAt=" + createdAt + + ", updatedAt=" + updatedAt + + '}'; + } +} diff --git a/backend/src/main/java/com/petshop/backend/entity/SaleItem.java b/backend/src/main/java/com/petshop/backend/entity/SaleItem.java new file mode 100644 index 00000000..b80ab370 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/entity/SaleItem.java @@ -0,0 +1,135 @@ +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 = "saleItem") +public class SaleItem { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long saleItemId; + + @ManyToOne + @JoinColumn(name = "saleId", nullable = false) + private Sale sale; + + @ManyToOne + @JoinColumn(name = "prodId", nullable = false) + private Product product; + + @Column(nullable = false) + private Integer quantity; + + @Column(nullable = false, precision = 10, scale = 2) + private BigDecimal unitPrice; + + @CreationTimestamp + @Column(name = "created_at", updatable = false) + private LocalDateTime createdAt; + + @UpdateTimestamp + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + public SaleItem() { + } + + public SaleItem(Long saleItemId, Sale sale, Product product, Integer quantity, BigDecimal unitPrice, LocalDateTime createdAt, LocalDateTime updatedAt) { + this.saleItemId = saleItemId; + this.sale = sale; + this.product = product; + this.quantity = quantity; + this.unitPrice = unitPrice; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public Long getSaleItemId() { + return saleItemId; + } + + public void setSaleItemId(Long saleItemId) { + this.saleItemId = saleItemId; + } + + public Sale getSale() { + return sale; + } + + public void setSale(Sale sale) { + this.sale = sale; + } + + public Product getProduct() { + return product; + } + + public void setProduct(Product product) { + this.product = product; + } + + public Integer getQuantity() { + return quantity; + } + + public void setQuantity(Integer quantity) { + this.quantity = quantity; + } + + public BigDecimal getUnitPrice() { + return unitPrice; + } + + public void setUnitPrice(BigDecimal unitPrice) { + this.unitPrice = unitPrice; + } + + 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; + SaleItem saleItem = (SaleItem) o; + return Objects.equals(saleItemId, saleItem.saleItemId); + } + + @Override + public int hashCode() { + return Objects.hash(saleItemId); + } + + @Override + public String toString() { + return "SaleItem{" + + "saleItemId=" + saleItemId + + ", sale=" + sale + + ", product=" + product + + ", quantity=" + quantity + + ", unitPrice=" + unitPrice + + ", createdAt=" + createdAt + + ", updatedAt=" + updatedAt + + '}'; + } +} diff --git a/backend/src/main/java/com/petshop/backend/entity/Service.java b/backend/src/main/java/com/petshop/backend/entity/Service.java new file mode 100644 index 00000000..a73387c8 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/entity/Service.java @@ -0,0 +1,133 @@ +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 = "service") +public class Service { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long serviceId; + + @Column(nullable = false, length = 100) + private String serviceName; + + @Column(columnDefinition = "TEXT") + private String serviceDesc; + + @Column(nullable = false, precision = 10, scale = 2) + private BigDecimal servicePrice; + + @Column(nullable = false) + private Integer serviceDuration; + + @CreationTimestamp + @Column(name = "created_at", updatable = false) + private LocalDateTime createdAt; + + @UpdateTimestamp + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + public Service() { + } + + public Service(Long serviceId, String serviceName, String serviceDesc, BigDecimal servicePrice, Integer serviceDuration, LocalDateTime createdAt, LocalDateTime updatedAt) { + this.serviceId = serviceId; + this.serviceName = serviceName; + this.serviceDesc = serviceDesc; + this.servicePrice = servicePrice; + this.serviceDuration = serviceDuration; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public Long getServiceId() { + return serviceId; + } + + public void setServiceId(Long serviceId) { + this.serviceId = serviceId; + } + + public String getServiceName() { + return serviceName; + } + + public void setServiceName(String serviceName) { + this.serviceName = serviceName; + } + + public String getServiceDesc() { + return serviceDesc; + } + + public void setServiceDesc(String serviceDesc) { + this.serviceDesc = serviceDesc; + } + + public BigDecimal getServicePrice() { + return servicePrice; + } + + public void setServicePrice(BigDecimal servicePrice) { + this.servicePrice = servicePrice; + } + + public Integer getServiceDuration() { + return serviceDuration; + } + + public void setServiceDuration(Integer serviceDuration) { + this.serviceDuration = serviceDuration; + } + + 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; + Service service = (Service) o; + return Objects.equals(serviceId, service.serviceId); + } + + @Override + public int hashCode() { + return Objects.hash(serviceId); + } + + @Override + public String toString() { + return "Service{" + + "serviceId=" + serviceId + + ", serviceName='" + serviceName + '\'' + + ", serviceDesc='" + serviceDesc + '\'' + + ", servicePrice=" + servicePrice + + ", serviceDuration=" + serviceDuration + + ", createdAt=" + createdAt + + ", updatedAt=" + updatedAt + + '}'; + } +} diff --git a/backend/src/main/java/com/petshop/backend/entity/StoreLocation.java b/backend/src/main/java/com/petshop/backend/entity/StoreLocation.java new file mode 100644 index 00000000..6b1a2ced --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/entity/StoreLocation.java @@ -0,0 +1,132 @@ +package com.petshop.backend.entity; + +import jakarta.persistence.*; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.time.LocalDateTime; +import java.util.Objects; + +@Entity +@Table(name = "storeLocation") +public class StoreLocation { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long storeId; + + @Column(nullable = false, length = 100) + private String storeName; + + @Column(nullable = false, length = 255) + private String address; + + @Column(nullable = false, length = 20) + private String phone; + + @Column(nullable = false, length = 100) + private String email; + + @CreationTimestamp + @Column(name = "created_at", updatable = false) + private LocalDateTime createdAt; + + @UpdateTimestamp + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + public StoreLocation() { + } + + public StoreLocation(Long storeId, String storeName, String address, String phone, String email, LocalDateTime createdAt, LocalDateTime updatedAt) { + this.storeId = storeId; + this.storeName = storeName; + this.address = address; + this.phone = phone; + this.email = email; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public Long getStoreId() { + return storeId; + } + + public void setStoreId(Long storeId) { + this.storeId = storeId; + } + + 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; + } + + 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; + StoreLocation that = (StoreLocation) o; + return Objects.equals(storeId, that.storeId); + } + + @Override + public int hashCode() { + return Objects.hash(storeId); + } + + @Override + public String toString() { + return "StoreLocation{" + + "storeId=" + storeId + + ", storeName='" + storeName + '\'' + + ", address='" + address + '\'' + + ", phone='" + phone + '\'' + + ", email='" + email + '\'' + + ", createdAt=" + createdAt + + ", updatedAt=" + updatedAt + + '}'; + } +} diff --git a/backend/src/main/java/com/petshop/backend/entity/Supplier.java b/backend/src/main/java/com/petshop/backend/entity/Supplier.java new file mode 100644 index 00000000..5dc35f17 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/entity/Supplier.java @@ -0,0 +1,145 @@ +package com.petshop.backend.entity; + +import jakarta.persistence.*; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.time.LocalDateTime; +import java.util.Objects; + +@Entity +@Table(name = "supplier") +public class Supplier { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long supId; + + @Column(nullable = false, length = 100) + private String supCompany; + + @Column(nullable = false, length = 50) + private String supContactFirstName; + + @Column(nullable = false, length = 50) + private String supContactLastName; + + @Column(nullable = false, length = 100) + private String supEmail; + + @Column(nullable = false, length = 20) + private String supPhone; + + @CreationTimestamp + @Column(name = "created_at", updatable = false) + private LocalDateTime createdAt; + + @UpdateTimestamp + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + public Supplier() { + } + + public Supplier(Long supId, String supCompany, String supContactFirstName, String supContactLastName, String supEmail, String supPhone, LocalDateTime createdAt, LocalDateTime updatedAt) { + this.supId = supId; + this.supCompany = supCompany; + this.supContactFirstName = supContactFirstName; + this.supContactLastName = supContactLastName; + this.supEmail = supEmail; + this.supPhone = supPhone; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public Long getSupId() { + return supId; + } + + public void setSupId(Long supId) { + this.supId = supId; + } + + public String getSupCompany() { + return supCompany; + } + + public void setSupCompany(String supCompany) { + this.supCompany = supCompany; + } + + public String getSupContactFirstName() { + return supContactFirstName; + } + + public void setSupContactFirstName(String supContactFirstName) { + this.supContactFirstName = supContactFirstName; + } + + public String getSupContactLastName() { + return supContactLastName; + } + + public void setSupContactLastName(String supContactLastName) { + this.supContactLastName = supContactLastName; + } + + public String getSupEmail() { + return supEmail; + } + + public void setSupEmail(String supEmail) { + this.supEmail = supEmail; + } + + public String getSupPhone() { + return supPhone; + } + + public void setSupPhone(String supPhone) { + this.supPhone = supPhone; + } + + 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; + Supplier supplier = (Supplier) o; + return Objects.equals(supId, supplier.supId); + } + + @Override + public int hashCode() { + return Objects.hash(supId); + } + + @Override + public String toString() { + return "Supplier{" + + "supId=" + supId + + ", supCompany='" + supCompany + '\'' + + ", supContactFirstName='" + supContactFirstName + '\'' + + ", supContactLastName='" + supContactLastName + '\'' + + ", supEmail='" + supEmail + '\'' + + ", supPhone='" + supPhone + '\'' + + ", createdAt=" + createdAt + + ", updatedAt=" + updatedAt + + '}'; + } +} diff --git a/backend/src/main/java/com/petshop/backend/entity/User.java b/backend/src/main/java/com/petshop/backend/entity/User.java new file mode 100644 index 00000000..cdec2754 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/entity/User.java @@ -0,0 +1,202 @@ +package com.petshop.backend.entity; + +import jakarta.persistence.*; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.time.LocalDateTime; +import java.util.Objects; + +@Entity +@Table(name = "users") +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, unique = true, length = 50) + private String username; + + @Column(nullable = false) + private String password; + + @Column(unique = true, length = 100) + private String email; + + @Column(length = 100) + private String fullName; + + @Column(length = 20) + private String phone; + + @Column(length = 255) + private String avatarUrl; + + @Enumerated(EnumType.STRING) + @Column(nullable = false, length = 20, columnDefinition = "VARCHAR(20)") + private Role role; + + @Column(nullable = false) + private Boolean active = true; + + @Column(nullable = false) + private Integer tokenVersion = 0; + + @CreationTimestamp + @Column(name = "created_at", updatable = false) + private LocalDateTime createdAt; + + @UpdateTimestamp + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + public enum Role { + CUSTOMER, STAFF, ADMIN + } + + public User() { + } + + public User(Long id, String username, String password, String email, String fullName, String phone, String avatarUrl, Role role, Boolean active, Integer tokenVersion, LocalDateTime createdAt, LocalDateTime updatedAt) { + this.id = id; + this.username = username; + this.password = password; + this.email = email; + this.fullName = fullName; + this.phone = phone; + this.avatarUrl = avatarUrl; + this.role = role; + this.active = active; + this.tokenVersion = tokenVersion; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + 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 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; + } + + public String getPhone() { + return phone; + } + + public void setPhone(String phone) { + this.phone = phone; + } + + public String getAvatarUrl() { + return avatarUrl; + } + + public void setAvatarUrl(String avatarUrl) { + this.avatarUrl = avatarUrl; + } + + public Role getRole() { + return role; + } + + public void setRole(Role role) { + this.role = role; + } + + public Boolean getActive() { + return active; + } + + public void setActive(Boolean active) { + this.active = active; + } + + public Integer getTokenVersion() { + return tokenVersion; + } + + public void setTokenVersion(Integer tokenVersion) { + this.tokenVersion = tokenVersion; + } + + 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; + User user = (User) o; + return Objects.equals(id, user.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + @Override + public String toString() { + return "User{" + + "id=" + id + + ", username='" + username + '\'' + + ", password='" + password + '\'' + + ", email='" + email + '\'' + + ", fullName='" + fullName + '\'' + + ", phone='" + phone + '\'' + + ", avatarUrl='" + avatarUrl + '\'' + + ", role=" + role + + ", active=" + active + + ", tokenVersion=" + tokenVersion + + ", createdAt=" + createdAt + + ", updatedAt=" + updatedAt + + '}'; + } +} diff --git a/backend/src/main/java/com/petshop/backend/exception/ApiErrorResponder.java b/backend/src/main/java/com/petshop/backend/exception/ApiErrorResponder.java new file mode 100644 index 00000000..39f4d66c --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/exception/ApiErrorResponder.java @@ -0,0 +1,32 @@ +package com.petshop.backend.exception; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.time.LocalDateTime; + +@Component +public class ApiErrorResponder { + + private final ObjectMapper objectMapper = JsonMapper.builder().findAndAddModules().build(); + + public void write(HttpServletResponse response, HttpStatus status, String message, String details, String path) throws IOException { + response.setStatus(status.value()); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + objectMapper.writeValue( + response.getWriter(), + new ApiErrorResponse( + status.value(), + message, + details, + path, + LocalDateTime.now() + ) + ); + } +} diff --git a/backend/src/main/java/com/petshop/backend/exception/ApiErrorResponse.java b/backend/src/main/java/com/petshop/backend/exception/ApiErrorResponse.java new file mode 100644 index 00000000..b3aea542 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/exception/ApiErrorResponse.java @@ -0,0 +1,12 @@ +package com.petshop.backend.exception; + +import java.time.LocalDateTime; + +public record ApiErrorResponse( + int status, + String message, + String details, + String path, + LocalDateTime timestamp +) { +} diff --git a/backend/src/main/java/com/petshop/backend/exception/BusinessException.java b/backend/src/main/java/com/petshop/backend/exception/BusinessException.java new file mode 100644 index 00000000..005ee62b --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/exception/BusinessException.java @@ -0,0 +1,7 @@ +package com.petshop.backend.exception; + +public class BusinessException extends RuntimeException { + public BusinessException(String message) { + super(message); + } +} diff --git a/backend/src/main/java/com/petshop/backend/exception/GlobalExceptionHandler.java b/backend/src/main/java/com/petshop/backend/exception/GlobalExceptionHandler.java new file mode 100644 index 00000000..b41f8789 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/exception/GlobalExceptionHandler.java @@ -0,0 +1,112 @@ +package com.petshop.backend.exception; + +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import org.springframework.web.server.ResponseStatusException; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(ResourceNotFoundException.class) + public ResponseEntity handleResourceNotFound(ResourceNotFoundException ex, HttpServletRequest request) { + return buildErrorResponse(HttpStatus.NOT_FOUND, ex.getMessage(), ex, request); + } + + @ExceptionHandler(BusinessException.class) + public ResponseEntity handleBusinessException(BusinessException ex, HttpServletRequest request) { + return buildErrorResponse(HttpStatus.BAD_REQUEST, ex.getMessage(), ex, request); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity> handleValidationExceptions(MethodArgumentNotValidException ex, HttpServletRequest request) { + Map errors = new HashMap<>(); + ex.getBindingResult().getAllErrors().forEach((error) -> { + String fieldName = ((FieldError) error).getField(); + String errorMessage = error.getDefaultMessage(); + errors.put(fieldName, errorMessage); + }); + + Map response = new HashMap<>(); + response.put("status", HttpStatus.BAD_REQUEST.value()); + response.put("message", "Validation failed"); + response.put("errors", errors); + response.put("details", buildDetails(ex)); + response.put("path", request.getRequestURI()); + response.put("timestamp", LocalDateTime.now()); + + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response); + } + + @ExceptionHandler(org.springframework.security.access.AccessDeniedException.class) + public ResponseEntity handleAccessDeniedException(org.springframework.security.access.AccessDeniedException ex, HttpServletRequest request) { + return buildErrorResponse(HttpStatus.FORBIDDEN, ex.getMessage(), ex, request); + } + + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity handleIllegalArgumentException(IllegalArgumentException ex, HttpServletRequest request) { + return buildErrorResponse(HttpStatus.BAD_REQUEST, ex.getMessage(), ex, request); + } + + @ExceptionHandler(DataIntegrityViolationException.class) + public ResponseEntity handleDataIntegrityViolationException(DataIntegrityViolationException ex, HttpServletRequest request) { + return buildErrorResponse(HttpStatus.BAD_REQUEST, "Operation violates existing data relationships", ex, request); + } + + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + public ResponseEntity handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException ex, HttpServletRequest request) { + String message = "Invalid value for parameter: " + ex.getName(); + if (ex.getValue() != null) { + message += " (" + ex.getValue() + ")"; + } + return buildErrorResponse(HttpStatus.BAD_REQUEST, message, ex, request); + } + + @ExceptionHandler(ResponseStatusException.class) + public ResponseEntity handleResponseStatusException(ResponseStatusException ex, HttpServletRequest request) { + String message = ex.getReason() != null ? ex.getReason() : ex.getMessage(); + return buildErrorResponse(HttpStatus.valueOf(ex.getStatusCode().value()), message, ex, request); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleGenericException(Exception ex, HttpServletRequest request) { + String message = ex.getMessage() == null || ex.getMessage().isBlank() + ? "Unexpected server error" + : ex.getMessage(); + return buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, message, ex, request); + } + + private ResponseEntity buildErrorResponse(HttpStatus status, String message, Exception ex, HttpServletRequest request) { + ApiErrorResponse error = new ApiErrorResponse( + status.value(), + message, + buildDetails(ex), + request.getRequestURI(), + LocalDateTime.now() + ); + return ResponseEntity.status(status).body(error); + } + + private String buildDetails(Exception ex) { + Throwable rootCause = ex; + while (rootCause.getCause() != null && rootCause.getCause() != rootCause) { + rootCause = rootCause.getCause(); + } + + String rootMessage = rootCause.getMessage(); + if (rootMessage == null || rootMessage.isBlank()) { + return rootCause.getClass().getSimpleName(); + } + return rootCause.getClass().getSimpleName() + ": " + rootMessage; + } +} diff --git a/backend/src/main/java/com/petshop/backend/exception/ResourceNotFoundException.java b/backend/src/main/java/com/petshop/backend/exception/ResourceNotFoundException.java new file mode 100644 index 00000000..08b426b7 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/exception/ResourceNotFoundException.java @@ -0,0 +1,7 @@ +package com.petshop.backend.exception; + +public class ResourceNotFoundException extends RuntimeException { + public ResourceNotFoundException(String message) { + super(message); + } +} diff --git a/backend/src/main/java/com/petshop/backend/repository/ActivityLogRepository.java b/backend/src/main/java/com/petshop/backend/repository/ActivityLogRepository.java new file mode 100644 index 00000000..5c5db4c5 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/repository/ActivityLogRepository.java @@ -0,0 +1,9 @@ +package com.petshop.backend.repository; + +import com.petshop.backend.entity.ActivityLog; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface ActivityLogRepository extends JpaRepository { +} diff --git a/backend/src/main/java/com/petshop/backend/repository/AdoptionRepository.java b/backend/src/main/java/com/petshop/backend/repository/AdoptionRepository.java new file mode 100644 index 00000000..d009b17a --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/repository/AdoptionRepository.java @@ -0,0 +1,27 @@ +package com.petshop.backend.repository; + +import com.petshop.backend.entity.Adoption; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +@Repository +public interface AdoptionRepository extends JpaRepository { + + @Query("SELECT a FROM Adoption a WHERE " + + "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 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/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java b/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java new file mode 100644 index 00000000..5c7b6ec0 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java @@ -0,0 +1,39 @@ +package com.petshop.backend.repository; + +import com.petshop.backend.entity.Appointment; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; + +@Repository +public interface AppointmentRepository extends JpaRepository { + + @Query("SELECT a FROM Appointment a WHERE a.appointmentDate = :date AND a.appointmentTime = :time") + List findByDateAndTime(@Param("date") LocalDate date, @Param("time") LocalTime time); + + @Query("SELECT a FROM Appointment a JOIN FETCH a.service WHERE a.store.storeId = :storeId AND a.appointmentDate = :date AND LOWER(a.appointmentStatus) <> 'cancelled'") + List findByStoreAndDate(@Param("storeId") Long storeId, @Param("date") LocalDate date); + + @Query("SELECT DISTINCT a FROM Appointment a LEFT JOIN a.pets p WHERE " + + "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 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/backend/src/main/java/com/petshop/backend/repository/CategoryRepository.java b/backend/src/main/java/com/petshop/backend/repository/CategoryRepository.java new file mode 100644 index 00000000..ceb30e53 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/repository/CategoryRepository.java @@ -0,0 +1,22 @@ +package com.petshop.backend.repository; + +import com.petshop.backend.entity.Category; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface CategoryRepository extends JpaRepository { + + Optional findByCategoryName(String categoryName); + + @Query("SELECT c FROM Category c WHERE " + + "LOWER(c.categoryName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + + "LOWER(c.categoryType) LIKE LOWER(CONCAT('%', :q, '%'))") + Page searchCategories(@Param("q") String query, Pageable pageable); +} diff --git a/backend/src/main/java/com/petshop/backend/repository/ConversationRepository.java b/backend/src/main/java/com/petshop/backend/repository/ConversationRepository.java new file mode 100644 index 00000000..98d457b8 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/repository/ConversationRepository.java @@ -0,0 +1,14 @@ +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); + List findByStaffIdIsNull(); +} diff --git a/backend/src/main/java/com/petshop/backend/repository/CustomerRepository.java b/backend/src/main/java/com/petshop/backend/repository/CustomerRepository.java new file mode 100644 index 00000000..56e03dbc --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/repository/CustomerRepository.java @@ -0,0 +1,26 @@ +package com.petshop.backend.repository; + +import com.petshop.backend.entity.Customer; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +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 " + + "LOWER(c.email) LIKE LOWER(CONCAT('%', :q, '%')) OR " + + "EXISTS (SELECT u FROM User u WHERE u.id = c.userId AND LOWER(COALESCE(u.phone, '')) LIKE LOWER(CONCAT('%', :q, '%')))") + Page searchCustomers(@Param("q") String query, Pageable pageable); +} diff --git a/backend/src/main/java/com/petshop/backend/repository/EmployeeRepository.java b/backend/src/main/java/com/petshop/backend/repository/EmployeeRepository.java new file mode 100644 index 00000000..cfbf715f --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/repository/EmployeeRepository.java @@ -0,0 +1,28 @@ +package com.petshop.backend.repository; + +import com.petshop.backend.entity.Employee; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +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 EmployeeRepository extends JpaRepository { + Optional findByUserId(Long userId); + List findAllByEmail(String email); + + @Query("SELECT e FROM Employee e WHERE " + + "LOWER(e.firstName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + + "LOWER(e.lastName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + + "LOWER(e.email) LIKE LOWER(CONCAT('%', :q, '%')) OR " + + "LOWER(e.role) LIKE LOWER(CONCAT('%', :q, '%')) OR " + + "EXISTS (SELECT u FROM User u WHERE u.id = e.userId AND (" + + "LOWER(u.username) LIKE LOWER(CONCAT('%', :q, '%')) OR " + + "LOWER(COALESCE(u.phone, '')) LIKE LOWER(CONCAT('%', :q, '%'))))") + Page searchEmployees(@Param("q") String query, Pageable pageable); +} diff --git a/backend/src/main/java/com/petshop/backend/repository/EmployeeStoreRepository.java b/backend/src/main/java/com/petshop/backend/repository/EmployeeStoreRepository.java new file mode 100644 index 00000000..1c847817 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/repository/EmployeeStoreRepository.java @@ -0,0 +1,12 @@ +package com.petshop.backend.repository; + +import com.petshop.backend.entity.EmployeeStore; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface EmployeeStoreRepository extends JpaRepository { + Optional findByEmployeeEmployeeId(Long employeeId); +} diff --git a/backend/src/main/java/com/petshop/backend/repository/InventoryRepository.java b/backend/src/main/java/com/petshop/backend/repository/InventoryRepository.java new file mode 100644 index 00000000..0e9d358c --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/repository/InventoryRepository.java @@ -0,0 +1,23 @@ +package com.petshop.backend.repository; + +import com.petshop.backend.entity.Inventory; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface InventoryRepository extends JpaRepository { + + @Query("SELECT i FROM Inventory i WHERE i.product.prodId = :productId") + Optional findByProductId(@Param("productId") Long productId); + + @Query("SELECT i FROM Inventory i WHERE " + + "LOWER(i.product.prodName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + + "LOWER(i.product.category.categoryName) LIKE LOWER(CONCAT('%', :q, '%'))") + Page searchInventory(@Param("q") String query, Pageable pageable); +} diff --git a/backend/src/main/java/com/petshop/backend/repository/MessageRepository.java b/backend/src/main/java/com/petshop/backend/repository/MessageRepository.java new file mode 100644 index 00000000..6c87be6e --- /dev/null +++ b/backend/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/backend/src/main/java/com/petshop/backend/repository/PetRepository.java b/backend/src/main/java/com/petshop/backend/repository/PetRepository.java new file mode 100644 index 00000000..fa9aa5e7 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/repository/PetRepository.java @@ -0,0 +1,19 @@ +package com.petshop.backend.repository; + +import com.petshop.backend.entity.Pet; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +@Repository +public interface PetRepository extends JpaRepository { + + @Query("SELECT p FROM Pet p WHERE " + + "LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + + "LOWER(p.petSpecies) LIKE LOWER(CONCAT('%', :q, '%')) OR " + + "LOWER(p.petBreed) LIKE LOWER(CONCAT('%', :q, '%'))") + Page searchPets(@Param("q") String query, Pageable pageable); +} diff --git a/backend/src/main/java/com/petshop/backend/repository/ProductRepository.java b/backend/src/main/java/com/petshop/backend/repository/ProductRepository.java new file mode 100644 index 00000000..94f7fb81 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/repository/ProductRepository.java @@ -0,0 +1,18 @@ +package com.petshop.backend.repository; + +import com.petshop.backend.entity.Product; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +@Repository +public interface ProductRepository extends JpaRepository { + + @Query("SELECT p FROM Product p WHERE " + + "LOWER(p.prodName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + + "LOWER(p.prodDesc) LIKE LOWER(CONCAT('%', :q, '%'))") + Page searchProducts(@Param("q") String query, Pageable pageable); +} diff --git a/backend/src/main/java/com/petshop/backend/repository/ProductSupplierRepository.java b/backend/src/main/java/com/petshop/backend/repository/ProductSupplierRepository.java new file mode 100644 index 00000000..46e87945 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/repository/ProductSupplierRepository.java @@ -0,0 +1,18 @@ +package com.petshop.backend.repository; + +import com.petshop.backend.entity.ProductSupplier; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +@Repository +public interface ProductSupplierRepository extends JpaRepository { + + @Query("SELECT ps FROM ProductSupplier ps WHERE " + + "LOWER(ps.product.prodName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + + "LOWER(ps.supplier.supCompany) LIKE LOWER(CONCAT('%', :q, '%'))") + Page searchProductSuppliers(@Param("q") String query, Pageable pageable); +} diff --git a/backend/src/main/java/com/petshop/backend/repository/PurchaseOrderRepository.java b/backend/src/main/java/com/petshop/backend/repository/PurchaseOrderRepository.java new file mode 100644 index 00000000..d3b445c4 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/repository/PurchaseOrderRepository.java @@ -0,0 +1,17 @@ +package com.petshop.backend.repository; + +import com.petshop.backend.entity.PurchaseOrder; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +@Repository +public interface PurchaseOrderRepository extends JpaRepository { + + @Query("SELECT po FROM PurchaseOrder po WHERE " + + "LOWER(po.supplier.supCompany) LIKE LOWER(CONCAT('%', :q, '%'))") + Page searchPurchaseOrders(@Param("q") String query, Pageable pageable); +} diff --git a/backend/src/main/java/com/petshop/backend/repository/RefundRepository.java b/backend/src/main/java/com/petshop/backend/repository/RefundRepository.java new file mode 100644 index 00000000..b71dde0c --- /dev/null +++ b/backend/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/backend/src/main/java/com/petshop/backend/repository/SaleItemRepository.java b/backend/src/main/java/com/petshop/backend/repository/SaleItemRepository.java new file mode 100644 index 00000000..0b67f95e --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/repository/SaleItemRepository.java @@ -0,0 +1,9 @@ +package com.petshop.backend.repository; + +import com.petshop.backend.entity.SaleItem; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface SaleItemRepository extends JpaRepository { +} diff --git a/backend/src/main/java/com/petshop/backend/repository/SaleRepository.java b/backend/src/main/java/com/petshop/backend/repository/SaleRepository.java new file mode 100644 index 00000000..f70b3dec --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/repository/SaleRepository.java @@ -0,0 +1,23 @@ +package com.petshop.backend.repository; + +import com.petshop.backend.entity.Sale; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface SaleRepository extends JpaRepository { + + @Query("SELECT s FROM Sale s WHERE " + + "LOWER(s.employee.firstName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + + "LOWER(s.employee.lastName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + + "LOWER(s.store.storeName) LIKE LOWER(CONCAT('%', :q, '%'))") + Page searchSales(@Param("q") String query, Pageable pageable); + + List findByOriginalSaleSaleId(Long originalSaleId); +} diff --git a/backend/src/main/java/com/petshop/backend/repository/ServiceRepository.java b/backend/src/main/java/com/petshop/backend/repository/ServiceRepository.java new file mode 100644 index 00000000..7b057856 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/repository/ServiceRepository.java @@ -0,0 +1,18 @@ +package com.petshop.backend.repository; + +import com.petshop.backend.entity.Service; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +@Repository +public interface ServiceRepository extends JpaRepository { + + @Query("SELECT s FROM Service s WHERE " + + "LOWER(s.serviceName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + + "LOWER(s.serviceDesc) LIKE LOWER(CONCAT('%', :q, '%'))") + Page searchServices(@Param("q") String query, Pageable pageable); +} diff --git a/backend/src/main/java/com/petshop/backend/repository/StoreRepository.java b/backend/src/main/java/com/petshop/backend/repository/StoreRepository.java new file mode 100644 index 00000000..5ee3758b --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/repository/StoreRepository.java @@ -0,0 +1,18 @@ +package com.petshop.backend.repository; + +import com.petshop.backend.entity.StoreLocation; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +@Repository +public interface StoreRepository extends JpaRepository { + + @Query("SELECT s FROM StoreLocation s WHERE " + + "LOWER(s.storeName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + + "LOWER(s.address) LIKE LOWER(CONCAT('%', :q, '%'))") + Page searchStores(@Param("q") String query, Pageable pageable); +} diff --git a/backend/src/main/java/com/petshop/backend/repository/SupplierRepository.java b/backend/src/main/java/com/petshop/backend/repository/SupplierRepository.java new file mode 100644 index 00000000..c7dd2307 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/repository/SupplierRepository.java @@ -0,0 +1,19 @@ +package com.petshop.backend.repository; + +import com.petshop.backend.entity.Supplier; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +@Repository +public interface SupplierRepository extends JpaRepository { + + @Query("SELECT s FROM Supplier s WHERE " + + "LOWER(s.supCompany) LIKE LOWER(CONCAT('%', :q, '%')) OR " + + "LOWER(s.supContactFirstName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + + "LOWER(s.supContactLastName) LIKE LOWER(CONCAT('%', :q, '%'))") + Page searchSuppliers(@Param("q") String query, Pageable pageable); +} diff --git a/backend/src/main/java/com/petshop/backend/repository/UserRepository.java b/backend/src/main/java/com/petshop/backend/repository/UserRepository.java new file mode 100644 index 00000000..6bec352f --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/repository/UserRepository.java @@ -0,0 +1,34 @@ +package com.petshop.backend.repository; + +import com.petshop.backend.entity.User; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface UserRepository extends JpaRepository { + Optional findByUsername(String username); + Optional findByEmail(String email); + Optional findByPhone(String phone); + boolean existsByUsername(String username); + Page findByRole(User.Role role, Pageable pageable); + + @Query("SELECT u FROM User u WHERE " + + "LOWER(u.username) LIKE LOWER(CONCAT('%', :q, '%')) OR " + + "LOWER(COALESCE(u.fullName, '')) LIKE LOWER(CONCAT('%', :q, '%')) OR " + + "LOWER(COALESCE(u.email, '')) LIKE LOWER(CONCAT('%', :q, '%')) OR " + + "LOWER(COALESCE(u.phone, '')) LIKE LOWER(CONCAT('%', :q, '%'))") + Page searchUsers(@Param("q") String query, Pageable pageable); + + @Query("SELECT u FROM User u WHERE u.role = :role AND (" + + "LOWER(u.username) LIKE LOWER(CONCAT('%', :q, '%')) OR " + + "LOWER(COALESCE(u.fullName, '')) LIKE LOWER(CONCAT('%', :q, '%')) OR " + + "LOWER(COALESCE(u.email, '')) LIKE LOWER(CONCAT('%', :q, '%')) OR " + + "LOWER(COALESCE(u.phone, '')) LIKE LOWER(CONCAT('%', :q, '%')))") + Page searchUsersByRole(@Param("q") String query, @Param("role") User.Role role, Pageable pageable); +} diff --git a/backend/src/main/java/com/petshop/backend/security/AppPrincipal.java b/backend/src/main/java/com/petshop/backend/security/AppPrincipal.java new file mode 100644 index 00000000..30ceca66 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/security/AppPrincipal.java @@ -0,0 +1,51 @@ +package com.petshop.backend.security; + +import com.petshop.backend.entity.User; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; + +import java.security.Principal; +import java.util.Collection; +import java.util.List; + +public class AppPrincipal implements Principal { + + private final Long userId; + private final String username; + private final User.Role role; + private final Integer tokenVersion; + private final List authorities; + + public AppPrincipal(Long userId, String username, User.Role role, Integer tokenVersion) { + this.userId = userId; + this.username = username; + this.role = role; + this.tokenVersion = tokenVersion; + this.authorities = List.of(new SimpleGrantedAuthority("ROLE_" + role.name())); + } + + public Long getUserId() { + return userId; + } + + @Override + public String getName() { + return username; + } + + public String getUsername() { + return username; + } + + public User.Role getRole() { + return role; + } + + public Integer getTokenVersion() { + return tokenVersion; + } + + public Collection getAuthorities() { + return authorities; + } +} diff --git a/backend/src/main/java/com/petshop/backend/security/JwtAuthenticationFilter.java b/backend/src/main/java/com/petshop/backend/security/JwtAuthenticationFilter.java new file mode 100644 index 00000000..a4caaaef --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/security/JwtAuthenticationFilter.java @@ -0,0 +1,86 @@ +package com.petshop.backend.security; + +import com.petshop.backend.entity.User; +import com.petshop.backend.exception.ApiErrorResponder; +import com.petshop.backend.repository.UserRepository; +import io.jsonwebtoken.JwtException; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.lang.NonNull; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +@Component +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtUtil jwtUtil; + private final UserRepository userRepository; + private final ApiErrorResponder apiErrorResponder; + + public JwtAuthenticationFilter(JwtUtil jwtUtil, UserRepository userRepository, ApiErrorResponder apiErrorResponder) { + this.jwtUtil = jwtUtil; + this.userRepository = userRepository; + this.apiErrorResponder = apiErrorResponder; + } + + @Override + protected void doFilterInternal( + @NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain + ) throws ServletException, IOException { + final String authHeader = request.getHeader("Authorization"); + final String jwt; + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + filterChain.doFilter(request, response); + return; + } + + jwt = authHeader.substring(7); + Long userId; + try { + userId = jwtUtil.extractUserId(jwt); + } catch (JwtException | IllegalArgumentException ex) { + writeUnauthorized(request, response, "Invalid or expired token", ex); + return; + } + + if (userId != null && SecurityContextHolder.getContext().getAuthentication() == null) { + User user = userRepository.findById(userId).orElse(null); + if (user == null || user.getActive() == null || !user.getActive()) { + writeUnauthorized(request, response, "User account is inactive", null); + return; + } + if (!jwtUtil.validateToken(jwt, user)) { + writeUnauthorized(request, response, "Invalid or expired token", null); + return; + } + + AppPrincipal principal = new AppPrincipal( + user.getId(), + user.getUsername(), + user.getRole(), + user.getTokenVersion() + ); + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( + principal, + null, + principal.getAuthorities() + ); + authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authToken); + } + filterChain.doFilter(request, response); + } + + private void writeUnauthorized(HttpServletRequest request, HttpServletResponse response, String message, Exception ex) throws IOException { + String details = ex == null ? message : ex.getClass().getSimpleName() + ": " + ex.getMessage(); + apiErrorResponder.write(response, org.springframework.http.HttpStatus.UNAUTHORIZED, message, details, request.getRequestURI()); + } +} diff --git a/backend/src/main/java/com/petshop/backend/security/JwtUtil.java b/backend/src/main/java/com/petshop/backend/security/JwtUtil.java new file mode 100644 index 00000000..3d4541bc --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/security/JwtUtil.java @@ -0,0 +1,95 @@ +package com.petshop.backend.security; + +import com.petshop.backend.entity.User; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +@Component +public class JwtUtil { + + @Value("${jwt.secret}") + private String secret; + + @Value("${jwt.expiration}") + private Long expiration; + + private SecretKey getSigningKey() { + return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); + } + + public String extractUsername(String token) { + return extractAllClaims(token).get("username", String.class); + } + + public Long extractUserId(String token) { + return Long.parseLong(extractClaim(token, Claims::getSubject)); + } + + public String extractRole(String token) { + return extractAllClaims(token).get("role", String.class); + } + + public Integer extractTokenVersion(String token) { + Number tokenVersion = extractAllClaims(token).get("tokenVersion", Number.class); + return tokenVersion == null ? null : tokenVersion.intValue(); + } + + public Date extractExpiration(String token) { + return extractClaim(token, Claims::getExpiration); + } + + public T extractClaim(String token, Function claimsResolver) { + final Claims claims = extractAllClaims(token); + return claimsResolver.apply(claims); + } + + private Claims extractAllClaims(String token) { + return Jwts.parser() + .verifyWith(getSigningKey()) + .build() + .parseSignedClaims(token) + .getPayload(); + } + + private Boolean isTokenExpired(String token) { + return extractExpiration(token).before(new Date()); + } + + public String generateToken(User user) { + Map claims = new HashMap<>(); + claims.put("username", user.getUsername()); + claims.put("role", user.getRole().name()); + claims.put("tokenVersion", user.getTokenVersion()); + return createToken(claims, user.getId().toString()); + } + + private String createToken(Map claims, String subject) { + return Jwts.builder() + .claims(claims) + .subject(subject) + .issuedAt(new Date(System.currentTimeMillis())) + .expiration(new Date(System.currentTimeMillis() + expiration)) + .signWith(getSigningKey()) + .compact(); + } + + public Boolean validateToken(String token, User user) { + Long userId = extractUserId(token); + String role = extractRole(token); + Integer tokenVersion = extractTokenVersion(token); + return user.getId().equals(userId) + && user.getRole().name().equals(role) + && user.getTokenVersion().equals(tokenVersion) + && !isTokenExpired(token); + } +} diff --git a/backend/src/main/java/com/petshop/backend/security/RestAccessDeniedHandler.java b/backend/src/main/java/com/petshop/backend/security/RestAccessDeniedHandler.java new file mode 100644 index 00000000..2ef240e9 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/security/RestAccessDeniedHandler.java @@ -0,0 +1,33 @@ +package com.petshop.backend.security; + +import com.petshop.backend.exception.ApiErrorResponder; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +public class RestAccessDeniedHandler implements AccessDeniedHandler { + + private final ApiErrorResponder apiErrorResponder; + + public RestAccessDeniedHandler(ApiErrorResponder apiErrorResponder) { + this.apiErrorResponder = apiErrorResponder; + } + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { + apiErrorResponder.write( + response, + HttpStatus.FORBIDDEN, + "Access Denied", + accessDeniedException.getClass().getSimpleName() + ": " + accessDeniedException.getMessage(), + request.getRequestURI() + ); + } +} diff --git a/backend/src/main/java/com/petshop/backend/security/RestAuthenticationEntryPoint.java b/backend/src/main/java/com/petshop/backend/security/RestAuthenticationEntryPoint.java new file mode 100644 index 00000000..2ae541b4 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/security/RestAuthenticationEntryPoint.java @@ -0,0 +1,33 @@ +package com.petshop.backend.security; + +import com.petshop.backend.exception.ApiErrorResponder; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint { + + private final ApiErrorResponder apiErrorResponder; + + public RestAuthenticationEntryPoint(ApiErrorResponder apiErrorResponder) { + this.apiErrorResponder = apiErrorResponder; + } + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { + apiErrorResponder.write( + response, + HttpStatus.UNAUTHORIZED, + "Authentication required", + authException.getClass().getSimpleName() + ": " + authException.getMessage(), + request.getRequestURI() + ); + } +} diff --git a/backend/src/main/java/com/petshop/backend/security/SecurityConfig.java b/backend/src/main/java/com/petshop/backend/security/SecurityConfig.java new file mode 100644 index 00000000..00ce63f8 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/security/SecurityConfig.java @@ -0,0 +1,85 @@ +package com.petshop.backend.security; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.ProviderManager; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +@EnableWebSecurity +@EnableMethodSecurity +public class SecurityConfig { + + private final JwtAuthenticationFilter jwtAuthFilter; + private final UserDetailsService userDetailsService; + private final RestAuthenticationEntryPoint restAuthenticationEntryPoint; + private final RestAccessDeniedHandler restAccessDeniedHandler; + + public SecurityConfig( + JwtAuthenticationFilter jwtAuthFilter, + UserDetailsService userDetailsService, + RestAuthenticationEntryPoint restAuthenticationEntryPoint, + RestAccessDeniedHandler restAccessDeniedHandler + ) { + this.jwtAuthFilter = jwtAuthFilter; + this.userDetailsService = userDetailsService; + this.restAuthenticationEntryPoint = restAuthenticationEntryPoint; + this.restAccessDeniedHandler = restAccessDeniedHandler; + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/api/v1/auth/login", "/api/v1/auth/register").permitAll() + .requestMatchers("/api/v1/health").permitAll() + .requestMatchers("/ws/chat/**", "/ws/chat-sockjs/**").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/services/**").permitAll() + .requestMatchers(HttpMethod.GET, "/api/v1/categories/**").permitAll() + .requestMatchers(HttpMethod.GET, "/api/v1/appointments/availability").permitAll() + .anyRequest().authenticated() + ) + .exceptionHandling(ex -> ex + .authenticationEntryPoint(restAuthenticationEntryPoint) + .accessDeniedHandler(restAccessDeniedHandler) + ) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authenticationProvider(daoAuthenticationProvider()) + .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } + + private DaoAuthenticationProvider daoAuthenticationProvider() { + DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(userDetailsService); + authProvider.setPasswordEncoder(passwordEncoder()); + return authProvider; + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { + return new ProviderManager(daoAuthenticationProvider()); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/backend/src/main/java/com/petshop/backend/security/UserDetailsServiceImpl.java b/backend/src/main/java/com/petshop/backend/security/UserDetailsServiceImpl.java new file mode 100644 index 00000000..f6956615 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/security/UserDetailsServiceImpl.java @@ -0,0 +1,38 @@ +package com.petshop.backend.security; + +import com.petshop.backend.entity.User; +import com.petshop.backend.repository.UserRepository; +import org.springframework.security.authentication.DisabledException; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +import java.util.Collections; + +@Service +public class UserDetailsServiceImpl implements UserDetailsService { + + private final UserRepository userRepository; + + public UserDetailsServiceImpl(UserRepository userRepository) { + this.userRepository = userRepository; + } + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new UsernameNotFoundException("User not found: " + username)); + + if (user.getActive() == null || !user.getActive()) { + throw new DisabledException("User account is inactive"); + } + + return new org.springframework.security.core.userdetails.User( + user.getUsername(), + user.getPassword(), + Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + user.getRole().name())) + ); + } +} diff --git a/backend/src/main/java/com/petshop/backend/service/AdoptionService.java b/backend/src/main/java/com/petshop/backend/service/AdoptionService.java new file mode 100644 index 00000000..a8f7f476 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/service/AdoptionService.java @@ -0,0 +1,127 @@ +package com.petshop.backend.service; + +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.Adoption; +import com.petshop.backend.entity.Customer; +import com.petshop.backend.entity.Pet; +import com.petshop.backend.exception.ResourceNotFoundException; +import com.petshop.backend.repository.AdoptionRepository; +import com.petshop.backend.repository.CustomerRepository; +import com.petshop.backend.repository.PetRepository; +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 AdoptionService { + + private final AdoptionRepository adoptionRepository; + private final PetRepository petRepository; + private final CustomerRepository customerRepository; + + public AdoptionService(AdoptionRepository adoptionRepository, PetRepository petRepository, CustomerRepository customerRepository) { + this.adoptionRepository = adoptionRepository; + this.petRepository = petRepository; + this.customerRepository = customerRepository; + } + + public Page getAllAdoptions(String query, Pageable pageable, Long customerId) { + Page adoptions; + + if (customerId != null) { + if (query != null && !query.trim().isEmpty()) { + adoptions = adoptionRepository.searchAdoptionsByCustomer(customerId, query, pageable); + } else { + adoptions = adoptionRepository.findByCustomerCustomerId(customerId, pageable); + } + } else { + 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, 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); + } + + @Transactional + public AdoptionResponse createAdoption(AdoptionRequest request) { + Pet pet = petRepository.findById(request.getPetId()) + .orElseThrow(() -> new ResourceNotFoundException("Pet not found with id: " + request.getPetId())); + + Customer customer = customerRepository.findById(request.getCustomerId()) + .orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + request.getCustomerId())); + + Adoption adoption = new Adoption(); + adoption.setPet(pet); + adoption.setCustomer(customer); + adoption.setAdoptionDate(request.getAdoptionDate()); + adoption.setAdoptionStatus(request.getAdoptionStatus()); + + adoption = adoptionRepository.save(adoption); + return mapToResponse(adoption); + } + + @Transactional + public AdoptionResponse updateAdoption(Long id, AdoptionRequest request) { + Adoption adoption = adoptionRepository.findById(id) + .orElseThrow(() -> new ResourceNotFoundException("Adoption not found with id: " + id)); + + Pet pet = petRepository.findById(request.getPetId()) + .orElseThrow(() -> new ResourceNotFoundException("Pet not found with id: " + request.getPetId())); + + Customer customer = customerRepository.findById(request.getCustomerId()) + .orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + request.getCustomerId())); + + adoption.setPet(pet); + adoption.setCustomer(customer); + adoption.setAdoptionDate(request.getAdoptionDate()); + adoption.setAdoptionStatus(request.getAdoptionStatus()); + + adoption = adoptionRepository.save(adoption); + return mapToResponse(adoption); + } + + @Transactional + public void deleteAdoption(Long id) { + if (!adoptionRepository.existsById(id)) { + throw new ResourceNotFoundException("Adoption not found with id: " + id); + } + adoptionRepository.deleteById(id); + } + + @Transactional + public void bulkDeleteAdoptions(BulkDeleteRequest request) { + adoptionRepository.deleteAllById(request.getIds()); + } + + private AdoptionResponse mapToResponse(Adoption adoption) { + return new AdoptionResponse( + adoption.getAdoptionId(), + adoption.getPet().getPetId(), + adoption.getPet().getPetName(), + adoption.getCustomer().getCustomerId(), + adoption.getCustomer().getFirstName() + " " + adoption.getCustomer().getLastName(), + adoption.getAdoptionDate(), + adoption.getAdoptionStatus(), + adoption.getPet().getPetPrice(), + adoption.getCreatedAt(), + adoption.getUpdatedAt() + ); + } +} diff --git a/backend/src/main/java/com/petshop/backend/service/AnalyticsService.java b/backend/src/main/java/com/petshop/backend/service/AnalyticsService.java new file mode 100644 index 00000000..32e6a5a7 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/service/AnalyticsService.java @@ -0,0 +1,141 @@ +package com.petshop.backend.service; + +import com.petshop.backend.dto.analytics.DashboardResponse; +import com.petshop.backend.entity.Inventory; +import com.petshop.backend.entity.Product; +import com.petshop.backend.entity.Sale; +import com.petshop.backend.repository.InventoryRepository; +import com.petshop.backend.repository.ProductRepository; +import com.petshop.backend.repository.SaleRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.stream.Collectors; + +@Service +public class AnalyticsService { + + private final SaleRepository saleRepository; + private final InventoryRepository inventoryRepository; + private final ProductRepository productRepository; + + public AnalyticsService(SaleRepository saleRepository, + InventoryRepository inventoryRepository, ProductRepository productRepository) { + this.saleRepository = saleRepository; + this.inventoryRepository = inventoryRepository; + this.productRepository = productRepository; + } + + @Transactional(readOnly = true) + public DashboardResponse getDashboardData(int days, int top) { + LocalDateTime startDate = LocalDateTime.now().minusDays(days); + + List sales = saleRepository.findAll().stream() + .filter(sale -> sale.getSaleDate().isAfter(startDate)) + .collect(Collectors.toList()); + + DashboardResponse.SalesSummary salesSummary = calculateSalesSummary(sales); + DashboardResponse.InventorySummary inventorySummary = calculateInventorySummary(); + List topProducts = calculateTopProducts(sales, top); + List dailySales = calculateDailySales(sales, days); + + return new DashboardResponse(salesSummary, inventorySummary, topProducts, dailySales); + } + + private DashboardResponse.SalesSummary calculateSalesSummary(List sales) { + BigDecimal totalRevenue = sales.stream() + .filter(sale -> !sale.getIsRefund()) + .map(Sale::getTotalAmount) + .reduce(BigDecimal.ZERO, BigDecimal::add); + + Long totalSales = sales.stream() + .filter(sale -> !sale.getIsRefund()) + .count(); + + BigDecimal totalRefunds = sales.stream() + .filter(Sale::getIsRefund) + .map(Sale::getTotalAmount) + .reduce(BigDecimal.ZERO, BigDecimal::add); + + Long totalRefundCount = sales.stream() + .filter(Sale::getIsRefund) + .count(); + + return new DashboardResponse.SalesSummary(totalRevenue, totalSales, totalRefunds, totalRefundCount); + } + + private DashboardResponse.InventorySummary calculateInventorySummary() { + List allInventory = inventoryRepository.findAll(); + + Long totalProducts = productRepository.count(); + + Long lowStockProducts = allInventory.stream() + .filter(inv -> inv.getQuantity() > 0 && inv.getQuantity() <= 10) + .map(inv -> inv.getProduct().getProdId()) + .distinct() + .count(); + + Long outOfStockProducts = allInventory.stream() + .filter(inv -> inv.getQuantity() == 0) + .map(inv -> inv.getProduct().getProdId()) + .distinct() + .count(); + + return new DashboardResponse.InventorySummary(totalProducts, lowStockProducts, outOfStockProducts); + } + + private List calculateTopProducts(List sales, int top) { + Map productSalesMap = new HashMap<>(); + + for (Sale sale : sales) { + for (var item : sale.getItems()) { + Long productId = item.getProduct().getProdId(); + String productName = item.getProduct().getProdName(); + Long quantitySold = Long.valueOf(item.getQuantity()); + BigDecimal revenue = item.getUnitPrice().multiply(BigDecimal.valueOf(item.getQuantity())); + + productSalesMap.compute(productId, (key, existing) -> { + if (existing == null) { + return new DashboardResponse.TopProduct(productId, productName, quantitySold, revenue); + } else { + existing.setQuantitySold(existing.getQuantitySold() + quantitySold); + existing.setRevenue(existing.getRevenue().add(revenue)); + return existing; + } + }); + } + } + + return productSalesMap.values().stream() + .sorted((p1, p2) -> p2.getRevenue().compareTo(p1.getRevenue())) + .limit(top) + .collect(Collectors.toList()); + } + + private List calculateDailySales(List sales, int days) { + Map dailySalesMap = new LinkedHashMap<>(); + + LocalDate startDate = LocalDate.now().minusDays(days - 1); + for (int i = 0; i < days; i++) { + LocalDate date = startDate.plusDays(i); + String dateStr = date.format(DateTimeFormatter.ISO_LOCAL_DATE); + dailySalesMap.put(date, new DashboardResponse.DailySales(dateStr, BigDecimal.ZERO, 0L)); + } + + for (Sale sale : sales) { + LocalDate saleDate = sale.getSaleDate().toLocalDate(); + if (dailySalesMap.containsKey(saleDate)) { + DashboardResponse.DailySales dailySale = dailySalesMap.get(saleDate); + dailySale.setRevenue(dailySale.getRevenue().add(sale.getTotalAmount())); + dailySale.setSalesCount(dailySale.getSalesCount() + 1); + } + } + + return new ArrayList<>(dailySalesMap.values()); + } +} diff --git a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java new file mode 100644 index 00000000..9abcf831 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java @@ -0,0 +1,279 @@ +package com.petshop.backend.service; + +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.Appointment; +import com.petshop.backend.entity.Customer; +import com.petshop.backend.entity.Employee; +import com.petshop.backend.entity.EmployeeStore; +import com.petshop.backend.entity.Pet; +import com.petshop.backend.entity.StoreLocation; +import com.petshop.backend.entity.User; +import com.petshop.backend.exception.ResourceNotFoundException; +import com.petshop.backend.repository.AppointmentRepository; +import com.petshop.backend.repository.CustomerRepository; +import com.petshop.backend.repository.EmployeeRepository; +import com.petshop.backend.repository.EmployeeStoreRepository; +import com.petshop.backend.repository.PetRepository; +import com.petshop.backend.repository.ServiceRepository; +import com.petshop.backend.repository.StoreRepository; +import com.petshop.backend.repository.UserRepository; +import com.petshop.backend.util.AuthenticationHelper; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +@Service +public class AppointmentService { + + private final AppointmentRepository appointmentRepository; + private final CustomerRepository customerRepository; + private final ServiceRepository serviceRepository; + private final PetRepository petRepository; + private final StoreRepository storeRepository; + private final UserRepository userRepository; + private final EmployeeRepository employeeRepository; + private final EmployeeStoreRepository employeeStoreRepository; + + public AppointmentService(AppointmentRepository appointmentRepository, CustomerRepository customerRepository, ServiceRepository serviceRepository, PetRepository petRepository, StoreRepository storeRepository, UserRepository userRepository, EmployeeRepository employeeRepository, EmployeeStoreRepository employeeStoreRepository) { + this.appointmentRepository = appointmentRepository; + this.customerRepository = customerRepository; + this.serviceRepository = serviceRepository; + this.petRepository = petRepository; + this.storeRepository = storeRepository; + this.userRepository = userRepository; + this.employeeRepository = employeeRepository; + this.employeeStoreRepository = employeeStoreRepository; + } + + @Transactional(readOnly = true) + public Page getAllAppointments(String query, Pageable pageable, Long customerId) { + Page appointments; + + if (customerId != null) { + if (query != null && !query.trim().isEmpty()) { + appointments = appointmentRepository.searchAppointmentsByCustomer(customerId, query, pageable); + } else { + appointments = appointmentRepository.findByCustomerCustomerId(customerId, pageable); + } + } else { + if (query != null && !query.trim().isEmpty()) { + appointments = appointmentRepository.searchAppointments(query, pageable); + } else { + appointments = appointmentRepository.findAll(pageable); + } + } + + return appointments.map(this::mapToResponse); + } + + @Transactional(readOnly = true) + 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); + } + + @Transactional + public AppointmentResponse createAppointment(AppointmentRequest request) { + validateAppointmentRequest(request); + + Customer customer = customerRepository.findById(request.getCustomerId()) + .orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + request.getCustomerId())); + + StoreLocation store = storeRepository.findById(request.getStoreId()) + .orElseThrow(() -> new ResourceNotFoundException("Store not found with id: " + request.getStoreId())); + + com.petshop.backend.entity.Service service = serviceRepository.findById(request.getServiceId()) + .orElseThrow(() -> new ResourceNotFoundException("Service not found with id: " + request.getServiceId())); + + validateStoreAccess(store.getStoreId()); + validateAvailability(store, service, request.getAppointmentDate(), request.getAppointmentTime(), null); + + Set pets = fetchPets(request.getPetIds()); + + Appointment appointment = new Appointment(); + appointment.setCustomer(customer); + appointment.setStore(store); + appointment.setService(service); + appointment.setAppointmentDate(request.getAppointmentDate()); + appointment.setAppointmentTime(request.getAppointmentTime()); + appointment.setAppointmentStatus(request.getAppointmentStatus()); + appointment.setPets(pets); + + appointment = appointmentRepository.save(appointment); + return mapToResponse(appointment); + } + + @Transactional + public AppointmentResponse updateAppointment(Long id, AppointmentRequest request) { + validateAppointmentRequest(request); + + Appointment appointment = appointmentRepository.findById(id) + .orElseThrow(() -> new ResourceNotFoundException("Appointment not found with id: " + id)); + + Customer customer = customerRepository.findById(request.getCustomerId()) + .orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + request.getCustomerId())); + + StoreLocation store = storeRepository.findById(request.getStoreId()) + .orElseThrow(() -> new ResourceNotFoundException("Store not found with id: " + request.getStoreId())); + + com.petshop.backend.entity.Service service = serviceRepository.findById(request.getServiceId()) + .orElseThrow(() -> new ResourceNotFoundException("Service not found with id: " + request.getServiceId())); + + validateStoreAccess(store.getStoreId()); + validateAvailability(store, service, request.getAppointmentDate(), request.getAppointmentTime(), id); + + Set pets = fetchPets(request.getPetIds()); + + appointment.setCustomer(customer); + appointment.setStore(store); + appointment.setService(service); + appointment.setAppointmentDate(request.getAppointmentDate()); + appointment.setAppointmentTime(request.getAppointmentTime()); + appointment.setAppointmentStatus(request.getAppointmentStatus()); + appointment.setPets(pets); + + appointment = appointmentRepository.save(appointment); + return mapToResponse(appointment); + } + + @Transactional + public void deleteAppointment(Long id) { + if (!appointmentRepository.existsById(id)) { + throw new ResourceNotFoundException("Appointment not found with id: " + id); + } + appointmentRepository.deleteById(id); + } + + @Transactional + public void bulkDeleteAppointments(BulkDeleteRequest request) { + appointmentRepository.deleteAllById(request.getIds()); + } + + @Transactional(readOnly = true) + public List checkAvailability(Long storeId, Long serviceId, LocalDate date) { + storeRepository.findById(storeId) + .orElseThrow(() -> new ResourceNotFoundException("Store not found with id: " + storeId)); + + com.petshop.backend.entity.Service service = serviceRepository.findById(serviceId) + .orElseThrow(() -> new ResourceNotFoundException("Service not found with id: " + serviceId)); + + List existingAppointments = appointmentRepository.findByStoreAndDate(storeId, date); + + List availableSlots = new ArrayList<>(); + LocalTime startTime = LocalTime.of(9, 0); + LocalTime endTime = LocalTime.of(17, 0); + LocalTime latestStart = endTime.minusMinutes(service.getServiceDuration()); + + LocalTime currentTime = startTime; + while (!currentTime.isAfter(latestStart)) { + if (isSlotAvailable(existingAppointments, service, currentTime, null)) { + availableSlots.add(currentTime.toString()); + } + currentTime = currentTime.plusMinutes(30); + } + + return availableSlots; + } + + private void validateAppointmentRequest(AppointmentRequest request) { + if ("Booked".equalsIgnoreCase(request.getAppointmentStatus())) { + LocalDateTime appointmentDateTime = LocalDateTime.of(request.getAppointmentDate(), request.getAppointmentTime()); + if (appointmentDateTime.isBefore(LocalDateTime.now())) { + throw new IllegalArgumentException("Booked appointments must be scheduled in the future"); + } + } + } + + private Set fetchPets(List petIds) { + Set pets = new HashSet<>(); + for (Long petId : petIds) { + Pet pet = petRepository.findById(petId) + .orElseThrow(() -> new ResourceNotFoundException("Pet not found with id: " + petId)); + pets.add(pet); + } + return pets; + } + + private AppointmentResponse mapToResponse(Appointment appointment) { + List petNames = appointment.getPets().stream() + .map(Pet::getPetName) + .collect(Collectors.toList()); + + List petIds = appointment.getPets().stream() + .map(Pet::getPetId) + .collect(Collectors.toList()); + + return new AppointmentResponse( + appointment.getAppointmentId(), + appointment.getCustomer().getCustomerId(), + appointment.getCustomer().getFirstName() + " " + appointment.getCustomer().getLastName(), + appointment.getStore().getStoreId(), + appointment.getStore().getStoreName(), + appointment.getService().getServiceId(), + appointment.getService().getServiceName(), + appointment.getAppointmentDate(), + appointment.getAppointmentTime(), + appointment.getAppointmentStatus(), + petNames, + petIds, + appointment.getCreatedAt(), + appointment.getUpdatedAt() + ); + } + + private void validateAvailability(StoreLocation store, com.petshop.backend.entity.Service service, LocalDate date, LocalTime time, Long appointmentIdToIgnore) { + List existingAppointments = appointmentRepository.findByStoreAndDate(store.getStoreId(), date); + if (!isSlotAvailable(existingAppointments, service, time, appointmentIdToIgnore)) { + throw new IllegalArgumentException("Appointment time is not available for the selected store and service"); + } + } + + private boolean isSlotAvailable(List existingAppointments, com.petshop.backend.entity.Service requestedService, LocalTime requestedStart, Long appointmentIdToIgnore) { + LocalTime requestedEnd = requestedStart.plusMinutes(requestedService.getServiceDuration()); + for (Appointment existingAppointment : existingAppointments) { + if (appointmentIdToIgnore != null && appointmentIdToIgnore.equals(existingAppointment.getAppointmentId())) { + continue; + } + LocalTime existingStart = existingAppointment.getAppointmentTime(); + LocalTime existingEnd = existingStart.plusMinutes(existingAppointment.getService().getServiceDuration()); + if (requestedStart.isBefore(existingEnd) && existingStart.isBefore(requestedEnd)) { + return false; + } + } + return true; + } + + private void validateStoreAccess(Long requestedStoreId) { + User user = AuthenticationHelper.getAuthenticatedUser(userRepository); + if (user.getRole() != User.Role.STAFF) { + return; + } + + Employee employee = AuthenticationHelper.getAuthenticatedEmployee(userRepository, employeeRepository); + EmployeeStore employeeStore = employeeStoreRepository.findByEmployeeEmployeeId(employee.getEmployeeId()) + .orElseThrow(() -> new AccessDeniedException("Authenticated staff member is not assigned to a store")); + + if (!employeeStore.getStore().getStoreId().equals(requestedStoreId)) { + throw new AccessDeniedException("Staff can only manage appointments for their assigned store"); + } + } +} diff --git a/backend/src/main/java/com/petshop/backend/service/CategoryService.java b/backend/src/main/java/com/petshop/backend/service/CategoryService.java new file mode 100644 index 00000000..3da87dd0 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/service/CategoryService.java @@ -0,0 +1,83 @@ +package com.petshop.backend.service; + +import com.petshop.backend.dto.category.CategoryRequest; +import com.petshop.backend.dto.category.CategoryResponse; +import com.petshop.backend.dto.common.BulkDeleteRequest; +import com.petshop.backend.entity.Category; +import com.petshop.backend.exception.ResourceNotFoundException; +import com.petshop.backend.repository.CategoryRepository; +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 CategoryService { + + private final CategoryRepository categoryRepository; + + public CategoryService(CategoryRepository categoryRepository) { + this.categoryRepository = categoryRepository; + } + + public Page getAllCategories(String query, Pageable pageable) { + Page categories; + if (query != null && !query.trim().isEmpty()) { + categories = categoryRepository.searchCategories(query, pageable); + } else { + categories = categoryRepository.findAll(pageable); + } + return categories.map(this::mapToResponse); + } + + public CategoryResponse getCategoryById(Long id) { + Category category = categoryRepository.findById(id) + .orElseThrow(() -> new ResourceNotFoundException("Category not found with id: " + id)); + return mapToResponse(category); + } + + @Transactional + public CategoryResponse createCategory(CategoryRequest request) { + Category category = new Category(); + category.setCategoryName(request.getCategoryName()); + category.setCategoryType(request.getCategoryType()); + + category = categoryRepository.save(category); + return mapToResponse(category); + } + + @Transactional + public CategoryResponse updateCategory(Long id, CategoryRequest request) { + Category category = categoryRepository.findById(id) + .orElseThrow(() -> new ResourceNotFoundException("Category not found with id: " + id)); + + category.setCategoryName(request.getCategoryName()); + category.setCategoryType(request.getCategoryType()); + + category = categoryRepository.save(category); + return mapToResponse(category); + } + + @Transactional + public void deleteCategory(Long id) { + if (!categoryRepository.existsById(id)) { + throw new ResourceNotFoundException("Category not found with id: " + id); + } + categoryRepository.deleteById(id); + } + + @Transactional + public void bulkDeleteCategories(BulkDeleteRequest request) { + categoryRepository.deleteAllById(request.getIds()); + } + + private CategoryResponse mapToResponse(Category category) { + return new CategoryResponse( + category.getCategoryId(), + category.getCategoryName(), + category.getCategoryType(), + category.getCreatedAt(), + category.getUpdatedAt() + ); + } +} diff --git a/backend/src/main/java/com/petshop/backend/service/ChatRealtimeService.java b/backend/src/main/java/com/petshop/backend/service/ChatRealtimeService.java new file mode 100644 index 00000000..9b835823 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/service/ChatRealtimeService.java @@ -0,0 +1,75 @@ +package com.petshop.backend.service; + +import com.petshop.backend.dto.chat.ConversationResponse; +import com.petshop.backend.dto.chat.MessageResponse; +import com.petshop.backend.entity.Conversation; +import com.petshop.backend.entity.Customer; +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.messaging.simp.SimpMessagingTemplate; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class ChatRealtimeService { + + private final SimpMessagingTemplate messagingTemplate; + private final ConversationRepository conversationRepository; + private final MessageRepository messageRepository; + private final CustomerRepository customerRepository; + private final UserRepository userRepository; + + public ChatRealtimeService(SimpMessagingTemplate messagingTemplate, ConversationRepository conversationRepository, MessageRepository messageRepository, CustomerRepository customerRepository, UserRepository userRepository) { + this.messagingTemplate = messagingTemplate; + this.conversationRepository = conversationRepository; + this.messageRepository = messageRepository; + this.customerRepository = customerRepository; + this.userRepository = userRepository; + } + + public void publishNewConversation(ConversationResponse conversation) { + messagingTemplate.convertAndSend("/topic/chat/conversations", conversation); + sendConversationToCustomerQueue(conversation); + } + + public void publishMessage(Long conversationId, MessageResponse message) { + messagingTemplate.convertAndSend("/topic/chat/conversations/" + conversationId, message); + } + + public void publishConversationUpdate(Long conversationId) { + Conversation conversation = conversationRepository.findById(conversationId) + .orElseThrow(() -> new ResourceNotFoundException("Conversation not found")); + List messages = messageRepository.findByConversationIdOrderByTimestampAsc(conversationId); + String lastMessage = messages.isEmpty() ? "" : messages.get(messages.size() - 1).getContent(); + ConversationResponse response = ConversationResponse.fromEntity(conversation, lastMessage); + + messagingTemplate.convertAndSend("/topic/chat/conversations", response); + sendConversationToCustomerQueue(response); + sendConversationToStaffQueue(response); + } + + private void sendConversationToCustomerQueue(ConversationResponse conversation) { + Customer customer = customerRepository.findById(conversation.getCustomerId()) + .orElseThrow(() -> new ResourceNotFoundException("Customer not found")); + if (customer.getUserId() == null) { + return; + } + User customerUser = userRepository.findById(customer.getUserId()) + .orElseThrow(() -> new ResourceNotFoundException("User not found")); + messagingTemplate.convertAndSendToUser(customerUser.getUsername(), "/queue/chat/conversations", conversation); + } + + private void sendConversationToStaffQueue(ConversationResponse conversation) { + if (conversation.getStaffId() == null) { + return; + } + User staffUser = userRepository.findById(conversation.getStaffId()) + .orElseThrow(() -> new ResourceNotFoundException("User not found")); + messagingTemplate.convertAndSendToUser(staffUser.getUsername(), "/queue/chat/conversations", conversation); + } +} diff --git a/backend/src/main/java/com/petshop/backend/service/ChatService.java b/backend/src/main/java/com/petshop/backend/service/ChatService.java new file mode 100644 index 00000000..f66cbdb5 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/service/ChatService.java @@ -0,0 +1,208 @@ +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.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; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +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; + private final CustomerRepository customerRepository; + + public ChatService(ConversationRepository conversationRepository, + MessageRepository messageRepository, + UserRepository userRepository, + CustomerRepository customerRepository) { + this.conversationRepository = conversationRepository; + this.messageRepository = messageRepository; + this.userRepository = userRepository; + this.customerRepository = customerRepository; + } + + @Transactional + public ConversationResponse createConversation(Long userId, ConversationRequest request) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new ResourceNotFoundException("User not found")); + + if (user.getRole() != User.Role.CUSTOMER) { + throw new AccessDeniedException("Only customers can start new conversations"); + } + + Customer customer = customerRepository.findByUserId(userId) + .orElseThrow(() -> new ResourceNotFoundException("Customer record not found for user")); + + Conversation conversation = new Conversation(); + conversation.setCustomerId(customer.getCustomerId()); + conversation.setStatus(Conversation.ConversationStatus.OPEN); + conversation.setMode(Conversation.ConversationMode.AUTOMATED); + 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) { + 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) { + List assignedToMe = conversationRepository.findByStaffId(userId); + List unassigned = conversationRepository.findByStaffIdIsNull(); + conversations = new java.util.ArrayList<>(assignedToMe); + conversations.addAll(unassigned); + } 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 (!hasConversationAccess(conversation, userId, role)) { + if (role == User.Role.CUSTOMER) { + throw new AccessDeniedException("You can only view your own conversations"); + } + if (role == User.Role.STAFF) { + throw new AccessDeniedException("You can only view conversations assigned to you or unassigned 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, User.Role role, MessageRequest request) { + Conversation conversation = conversationRepository.findById(conversationId) + .orElseThrow(() -> new ResourceNotFoundException("Conversation not found")); + + if (!hasConversationAccess(conversation, userId, role)) { + if (role == User.Role.CUSTOMER) { + throw new AccessDeniedException("You can only send messages to your own conversations"); + } + if (role == User.Role.STAFF) { + throw new AccessDeniedException("You can only reply to conversations assigned to you or unassigned conversations"); + } + } + + Message message = new Message(); + message.setConversationId(conversationId); + message.setSenderId(userId); + message.setContent(request.getContent()); + message.setIsRead(false); + message = messageRepository.save(message); + + if (role == User.Role.STAFF && conversation.getStaffId() == null) { + conversation.setStaffId(userId); + } + + if (role == User.Role.STAFF) { + conversation.setMode(Conversation.ConversationMode.HUMAN); + conversationRepository.save(conversation); + } + + return MessageResponse.fromEntity(message); + } + + @Transactional + public ConversationResponse requestHumanTakeover(Long conversationId, Long userId, User.Role role) { + Conversation conversation = conversationRepository.findById(conversationId) + .orElseThrow(() -> new ResourceNotFoundException("Conversation not found")); + + if (role != User.Role.CUSTOMER || !hasConversationAccess(conversation, userId, role)) { + throw new AccessDeniedException("You can only request human takeover for your own conversations"); + } + + if (conversation.getHumanRequestedAt() == null) { + conversation.setHumanRequestedAt(LocalDateTime.now()); + } + conversationRepository.save(conversation); + + List messages = messageRepository.findByConversationIdOrderByTimestampAsc(conversationId); + String lastMessage = messages.isEmpty() ? "" : messages.get(messages.size() - 1).getContent(); + return ConversationResponse.fromEntity(conversation, lastMessage); + } + + public List getMessages(Long conversationId, Long userId, User.Role role) { + Conversation conversation = conversationRepository.findById(conversationId) + .orElseThrow(() -> new ResourceNotFoundException("Conversation not found")); + + if (!hasConversationAccess(conversation, userId, role)) { + if (role == User.Role.CUSTOMER) { + throw new AccessDeniedException("You can only view messages from your own conversations"); + } + if (role == User.Role.STAFF) { + throw new AccessDeniedException("You can only view messages from conversations assigned to you or unassigned conversations"); + } + } + + List messages = messageRepository.findByConversationIdOrderByTimestampAsc(conversationId); + return messages.stream() + .map(MessageResponse::fromEntity) + .collect(Collectors.toList()); + } + + public boolean hasConversationAccess(Long conversationId, Long userId, User.Role role) { + Conversation conversation = conversationRepository.findById(conversationId) + .orElseThrow(() -> new ResourceNotFoundException("Conversation not found")); + return hasConversationAccess(conversation, userId, role); + } + + private boolean hasConversationAccess(Conversation conversation, Long userId, User.Role role) { + if (role == User.Role.ADMIN) { + return true; + } + + if (role == User.Role.CUSTOMER) { + Customer customer = customerRepository.findByUserId(userId) + .orElseThrow(() -> new ResourceNotFoundException("Customer record not found for user")); + return conversation.getCustomerId().equals(customer.getCustomerId()); + } + + if (role == User.Role.STAFF) { + return conversation.getStaffId() == null || conversation.getStaffId().equals(userId); + } + + return false; + } +} diff --git a/backend/src/main/java/com/petshop/backend/service/CustomerService.java b/backend/src/main/java/com/petshop/backend/service/CustomerService.java new file mode 100644 index 00000000..040be22a --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/service/CustomerService.java @@ -0,0 +1,102 @@ +package com.petshop.backend.service; + +import com.petshop.backend.dto.common.BulkDeleteRequest; +import com.petshop.backend.dto.customer.CustomerRequest; +import com.petshop.backend.dto.customer.CustomerResponse; +import com.petshop.backend.entity.Customer; +import com.petshop.backend.exception.ResourceNotFoundException; +import com.petshop.backend.repository.CustomerRepository; +import com.petshop.backend.repository.UserRepository; +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 CustomerService { + + private final CustomerRepository customerRepository; + private final UserRepository userRepository; + + public CustomerService(CustomerRepository customerRepository, UserRepository userRepository) { + this.customerRepository = customerRepository; + this.userRepository = userRepository; + } + + public Page getAllCustomers(String query, Pageable pageable) { + Page customers; + if (query != null && !query.trim().isEmpty()) { + customers = customerRepository.searchCustomers(query, pageable); + } else { + customers = customerRepository.findAll(pageable); + } + return customers.map(this::mapToResponse); + } + + public CustomerResponse getCustomerById(Long id) { + Customer customer = customerRepository.findById(id) + .orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + id)); + return mapToResponse(customer); + } + + @Transactional + public CustomerResponse createCustomer(CustomerRequest request) { + Customer customer = new Customer(); + customer.setFirstName(request.getFirstName()); + customer.setLastName(request.getLastName()); + customer.setEmail(request.getEmail()); + + customer = customerRepository.save(customer); + syncLinkedUser(customer); + return mapToResponse(customer); + } + + @Transactional + public CustomerResponse updateCustomer(Long id, CustomerRequest request) { + Customer customer = customerRepository.findById(id) + .orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + id)); + + customer.setFirstName(request.getFirstName()); + customer.setLastName(request.getLastName()); + customer.setEmail(request.getEmail()); + + customer = customerRepository.save(customer); + syncLinkedUser(customer); + return mapToResponse(customer); + } + + @Transactional + public void deleteCustomer(Long id) { + if (!customerRepository.existsById(id)) { + throw new ResourceNotFoundException("Customer not found with id: " + id); + } + customerRepository.deleteById(id); + } + + @Transactional + public void bulkDeleteCustomers(BulkDeleteRequest request) { + customerRepository.deleteAllById(request.getIds()); + } + + private CustomerResponse mapToResponse(Customer customer) { + return new CustomerResponse( + customer.getCustomerId(), + customer.getFirstName(), + customer.getLastName(), + customer.getEmail(), + customer.getCreatedAt(), + customer.getUpdatedAt() + ); + } + + private void syncLinkedUser(Customer customer) { + if (customer.getUserId() == null) { + return; + } + userRepository.findById(customer.getUserId()).ifPresent(user -> { + user.setEmail(customer.getEmail()); + user.setFullName((customer.getFirstName() + " " + customer.getLastName()).trim()); + userRepository.save(user); + }); + } +} diff --git a/backend/src/main/java/com/petshop/backend/service/EmployeeService.java b/backend/src/main/java/com/petshop/backend/service/EmployeeService.java new file mode 100644 index 00000000..baf83bb8 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/service/EmployeeService.java @@ -0,0 +1,183 @@ +package com.petshop.backend.service; + +import com.petshop.backend.dto.employee.EmployeeRequest; +import com.petshop.backend.dto.employee.EmployeeResponse; +import com.petshop.backend.entity.Employee; +import com.petshop.backend.entity.User; +import com.petshop.backend.exception.ResourceNotFoundException; +import com.petshop.backend.repository.EmployeeRepository; +import com.petshop.backend.repository.UserRepository; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.server.ResponseStatusException; + +import static org.springframework.http.HttpStatus.CONFLICT; + +@Service +public class EmployeeService { + private final EmployeeRepository employeeRepository; + private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + private final UserBusinessLinkageService userBusinessLinkageService; + + public EmployeeService(EmployeeRepository employeeRepository, UserRepository userRepository, PasswordEncoder passwordEncoder, UserBusinessLinkageService userBusinessLinkageService) { + this.employeeRepository = employeeRepository; + this.userRepository = userRepository; + this.passwordEncoder = passwordEncoder; + this.userBusinessLinkageService = userBusinessLinkageService; + } + + public Page getAllEmployees(String query, Pageable pageable) { + Page employees; + if (query != null && !query.trim().isEmpty()) { + employees = employeeRepository.searchEmployees(query, pageable); + } else { + employees = employeeRepository.findAll(pageable); + } + return employees.map(this::mapToResponse); + } + + public EmployeeResponse getEmployeeById(Long id) { + Employee employee = employeeRepository.findById(id) + .orElseThrow(() -> new ResourceNotFoundException("Employee not found with id: " + id)); + return mapToResponse(employee); + } + + @Transactional + public EmployeeResponse createEmployee(EmployeeRequest request) { + validateRole(request.getRole()); + if (request.getPassword() == null || request.getPassword().trim().length() < 6) { + throw new IllegalArgumentException("Password must be at least 6 characters"); + } + if (userRepository.findByUsername(request.getUsername()).isPresent()) { + throw new ResponseStatusException(CONFLICT, "Username already exists"); + } + if (request.getEmail() != null && userRepository.findByEmail(request.getEmail()).isPresent()) { + throw new ResponseStatusException(CONFLICT, "Email already exists"); + } + String phone = trimToNull(request.getPhone()); + if (phone != null && userRepository.findByPhone(phone).isPresent()) { + throw new ResponseStatusException(CONFLICT, "Phone already exists"); + } + + User user = new User(); + user.setUsername(request.getUsername()); + user.setPassword(passwordEncoder.encode(request.getPassword())); + user.setFullName(fullName(request)); + user.setEmail(request.getEmail()); + user.setPhone(phone); + user.setRole(request.getRole()); + user.setActive(request.getActive() != null ? request.getActive() : true); + user = userRepository.save(user); + + Employee employee = userBusinessLinkageService.ensureLinkedEmployee(user); + return mapToResponse(employee, user); + } + + @Transactional + public EmployeeResponse updateEmployee(Long id, EmployeeRequest request) { + Employee employee = employeeRepository.findById(id) + .orElseThrow(() -> new ResourceNotFoundException("Employee not found with id: " + id)); + User user = requireLinkedUser(employee); + + validateRole(request.getRole()); + if (!user.getUsername().equals(request.getUsername()) && userRepository.findByUsername(request.getUsername()).isPresent()) { + throw new ResponseStatusException(CONFLICT, "Username already exists"); + } + if (!java.util.Objects.equals(user.getEmail(), request.getEmail()) && request.getEmail() != null && userRepository.findByEmail(request.getEmail()).isPresent()) { + throw new ResponseStatusException(CONFLICT, "Email already exists"); + } + String phone = trimToNull(request.getPhone()); + Long currentUserId = user.getId(); + if (!java.util.Objects.equals(user.getPhone(), phone)) { + userRepository.findByPhone(phone) + .filter(existing -> !existing.getId().equals(currentUserId)) + .ifPresent(existing -> { throw new ResponseStatusException(CONFLICT, "Phone already exists"); }); + } + + user.setUsername(request.getUsername()); + if (request.getPassword() != null && !request.getPassword().trim().isEmpty()) { + user.setPassword(passwordEncoder.encode(request.getPassword())); + user.setTokenVersion(user.getTokenVersion() + 1); + } + user.setEmail(request.getEmail()); + user.setPhone(phone); + user.setFullName(fullName(request)); + user.setRole(request.getRole()); + user.setActive(request.getActive() != null ? request.getActive() : true); + user = userRepository.save(user); + + employee = userBusinessLinkageService.ensureLinkedEmployee(user); + return mapToResponse(employee, user); + } + + @Transactional + public void deleteEmployee(Long id) { + Employee employee = employeeRepository.findById(id) + .orElseThrow(() -> new ResourceNotFoundException("Employee not found with id: " + id)); + if (employee.getUserId() != null && userRepository.existsById(employee.getUserId())) { + userRepository.deleteById(employee.getUserId()); + return; + } + employeeRepository.deleteById(id); + } + + private EmployeeResponse mapToResponse(Employee employee) { + User user = employee.getUserId() == null ? null : userRepository.findById(employee.getUserId()).orElse(null); + return mapToResponse(employee, user); + } + + private EmployeeResponse mapToResponse(Employee employee, User user) { + EmployeeResponse response = new EmployeeResponse(); + response.setEmployeeId(employee.getEmployeeId()); + response.setUserId(user != null ? user.getId() : employee.getUserId()); + response.setUsername(user != null ? user.getUsername() : null); + response.setFirstName(employee.getFirstName()); + response.setLastName(employee.getLastName()); + response.setFullName(user != null ? user.getFullName() : fullName(employee)); + response.setEmail(user != null ? user.getEmail() : employee.getEmail()); + response.setPhone(user != null ? user.getPhone() : null); + response.setRole(user != null ? user.getRole().name() : normalizeRole(employee.getRole())); + response.setActive(user != null ? user.getActive() : employee.getIsActive()); + response.setCreatedAt(employee.getCreatedAt()); + response.setUpdatedAt(employee.getUpdatedAt()); + return response; + } + + private User requireLinkedUser(Employee employee) { + if (employee.getUserId() == null) { + throw new ResourceNotFoundException("Employee user account not found"); + } + return userRepository.findById(employee.getUserId()) + .orElseThrow(() -> new ResourceNotFoundException("Employee user account not found")); + } + + private void validateRole(User.Role role) { + if (role != User.Role.STAFF && role != User.Role.ADMIN) { + throw new IllegalArgumentException("Employee role must be STAFF or ADMIN"); + } + } + + private String fullName(EmployeeRequest request) { + return (request.getFirstName().trim() + " " + request.getLastName().trim()).trim(); + } + + private String fullName(Employee employee) { + return (employee.getFirstName().trim() + " " + employee.getLastName().trim()).trim(); + } + + private String normalizeRole(String role) { + return role == null ? null : role.trim().toUpperCase(java.util.Locale.ROOT); + } + + private String trimToNull(String value) { + if (value == null) { + return null; + } + String trimmed = value.trim(); + return trimmed.isEmpty() ? null : trimmed; + } +} diff --git a/backend/src/main/java/com/petshop/backend/service/InventoryService.java b/backend/src/main/java/com/petshop/backend/service/InventoryService.java new file mode 100644 index 00000000..ee63aea7 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/service/InventoryService.java @@ -0,0 +1,95 @@ +package com.petshop.backend.service; + +import com.petshop.backend.dto.common.BulkDeleteRequest; +import com.petshop.backend.dto.inventory.InventoryRequest; +import com.petshop.backend.dto.inventory.InventoryResponse; +import com.petshop.backend.entity.Inventory; +import com.petshop.backend.entity.Product; +import com.petshop.backend.exception.ResourceNotFoundException; +import com.petshop.backend.repository.InventoryRepository; +import com.petshop.backend.repository.ProductRepository; +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 InventoryService { + + private final InventoryRepository inventoryRepository; + private final ProductRepository productRepository; + + public InventoryService(InventoryRepository inventoryRepository, ProductRepository productRepository) { + this.inventoryRepository = inventoryRepository; + this.productRepository = productRepository; + } + + public Page getAllInventory(String query, Pageable pageable) { + Page inventory; + if (query != null && !query.trim().isEmpty()) { + inventory = inventoryRepository.searchInventory(query, pageable); + } else { + inventory = inventoryRepository.findAll(pageable); + } + return inventory.map(this::mapToResponse); + } + + public InventoryResponse getInventoryById(Long id) { + Inventory inventory = inventoryRepository.findById(id) + .orElseThrow(() -> new ResourceNotFoundException("Inventory not found with id: " + id)); + return mapToResponse(inventory); + } + + @Transactional + public InventoryResponse createInventory(InventoryRequest request) { + Product product = productRepository.findById(request.getProdId()) + .orElseThrow(() -> new ResourceNotFoundException("Product not found with id: " + request.getProdId())); + + Inventory inventory = new Inventory(); + inventory.setProduct(product); + inventory.setQuantity(request.getQuantity()); + + inventory = inventoryRepository.save(inventory); + return mapToResponse(inventory); + } + + @Transactional + public InventoryResponse updateInventory(Long id, InventoryRequest request) { + Inventory inventory = inventoryRepository.findById(id) + .orElseThrow(() -> new ResourceNotFoundException("Inventory not found with id: " + id)); + + Product product = productRepository.findById(request.getProdId()) + .orElseThrow(() -> new ResourceNotFoundException("Product not found with id: " + request.getProdId())); + + inventory.setProduct(product); + inventory.setQuantity(request.getQuantity()); + + inventory = inventoryRepository.save(inventory); + return mapToResponse(inventory); + } + + @Transactional + public void deleteInventory(Long id) { + if (!inventoryRepository.existsById(id)) { + throw new ResourceNotFoundException("Inventory not found with id: " + id); + } + inventoryRepository.deleteById(id); + } + + @Transactional + public void bulkDeleteInventory(BulkDeleteRequest request) { + inventoryRepository.deleteAllById(request.getIds()); + } + + private InventoryResponse mapToResponse(Inventory inventory) { + return new InventoryResponse( + inventory.getInventoryId(), + inventory.getProduct().getProdId(), + inventory.getProduct().getProdName(), + inventory.getProduct().getCategory().getCategoryName(), + inventory.getQuantity(), + inventory.getCreatedAt(), + inventory.getUpdatedAt() + ); + } +} diff --git a/backend/src/main/java/com/petshop/backend/service/PetService.java b/backend/src/main/java/com/petshop/backend/service/PetService.java new file mode 100644 index 00000000..b59d589b --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/service/PetService.java @@ -0,0 +1,95 @@ +package com.petshop.backend.service; + +import com.petshop.backend.dto.common.BulkDeleteRequest; +import com.petshop.backend.dto.pet.PetRequest; +import com.petshop.backend.dto.pet.PetResponse; +import com.petshop.backend.entity.Pet; +import com.petshop.backend.exception.ResourceNotFoundException; +import com.petshop.backend.repository.PetRepository; +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 PetService { + + private final PetRepository petRepository; + + public PetService(PetRepository petRepository) { + this.petRepository = petRepository; + } + + public Page getAllPets(String query, Pageable pageable) { + Page pets; + if (query != null && !query.trim().isEmpty()) { + pets = petRepository.searchPets(query, pageable); + } else { + pets = petRepository.findAll(pageable); + } + return pets.map(this::mapToResponse); + } + + public PetResponse getPetById(Long id) { + Pet pet = petRepository.findById(id) + .orElseThrow(() -> new ResourceNotFoundException("Pet not found with id: " + id)); + return mapToResponse(pet); + } + + @Transactional + public PetResponse createPet(PetRequest request) { + Pet pet = new Pet(); + pet.setPetName(request.getPetName()); + pet.setPetSpecies(request.getPetSpecies()); + pet.setPetBreed(request.getPetBreed()); + pet.setPetAge(request.getPetAge()); + pet.setPetStatus(request.getPetStatus()); + pet.setPetPrice(request.getPetPrice()); + + pet = petRepository.save(pet); + return mapToResponse(pet); + } + + @Transactional + public PetResponse updatePet(Long id, PetRequest request) { + Pet pet = petRepository.findById(id) + .orElseThrow(() -> new ResourceNotFoundException("Pet not found with id: " + id)); + + pet.setPetName(request.getPetName()); + pet.setPetSpecies(request.getPetSpecies()); + pet.setPetBreed(request.getPetBreed()); + pet.setPetAge(request.getPetAge()); + pet.setPetStatus(request.getPetStatus()); + pet.setPetPrice(request.getPetPrice()); + + pet = petRepository.save(pet); + return mapToResponse(pet); + } + + @Transactional + public void deletePet(Long id) { + if (!petRepository.existsById(id)) { + throw new ResourceNotFoundException("Pet not found with id: " + id); + } + petRepository.deleteById(id); + } + + @Transactional + public void bulkDeletePets(BulkDeleteRequest request) { + petRepository.deleteAllById(request.getIds()); + } + + private PetResponse mapToResponse(Pet pet) { + return new PetResponse( + pet.getPetId(), + pet.getPetName(), + pet.getPetSpecies(), + pet.getPetBreed(), + pet.getPetAge(), + pet.getPetStatus(), + pet.getPetPrice(), + pet.getCreatedAt(), + pet.getUpdatedAt() + ); + } +} diff --git a/backend/src/main/java/com/petshop/backend/service/ProductService.java b/backend/src/main/java/com/petshop/backend/service/ProductService.java new file mode 100644 index 00000000..b907e38f --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/service/ProductService.java @@ -0,0 +1,100 @@ +package com.petshop.backend.service; + +import com.petshop.backend.dto.common.BulkDeleteRequest; +import com.petshop.backend.dto.product.ProductRequest; +import com.petshop.backend.dto.product.ProductResponse; +import com.petshop.backend.entity.Category; +import com.petshop.backend.entity.Product; +import com.petshop.backend.exception.ResourceNotFoundException; +import com.petshop.backend.repository.CategoryRepository; +import com.petshop.backend.repository.ProductRepository; +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 ProductService { + + private final ProductRepository productRepository; + private final CategoryRepository categoryRepository; + + public ProductService(ProductRepository productRepository, CategoryRepository categoryRepository) { + this.productRepository = productRepository; + this.categoryRepository = categoryRepository; + } + + public Page getAllProducts(String query, Pageable pageable) { + Page products; + if (query != null && !query.trim().isEmpty()) { + products = productRepository.searchProducts(query, pageable); + } else { + products = productRepository.findAll(pageable); + } + return products.map(this::mapToResponse); + } + + public ProductResponse getProductById(Long id) { + Product product = productRepository.findById(id) + .orElseThrow(() -> new ResourceNotFoundException("Product not found with id: " + id)); + return mapToResponse(product); + } + + @Transactional + public ProductResponse createProduct(ProductRequest request) { + Category category = categoryRepository.findById(request.getCategoryId()) + .orElseThrow(() -> new ResourceNotFoundException("Category not found with id: " + request.getCategoryId())); + + Product product = new Product(); + product.setProdName(request.getProdName()); + product.setCategory(category); + product.setProdDesc(request.getProdDesc()); + product.setProdPrice(request.getProdPrice()); + + product = productRepository.save(product); + return mapToResponse(product); + } + + @Transactional + public ProductResponse updateProduct(Long id, ProductRequest request) { + Product product = productRepository.findById(id) + .orElseThrow(() -> new ResourceNotFoundException("Product not found with id: " + id)); + + Category category = categoryRepository.findById(request.getCategoryId()) + .orElseThrow(() -> new ResourceNotFoundException("Category not found with id: " + request.getCategoryId())); + + product.setProdName(request.getProdName()); + product.setCategory(category); + product.setProdDesc(request.getProdDesc()); + product.setProdPrice(request.getProdPrice()); + + product = productRepository.save(product); + return mapToResponse(product); + } + + @Transactional + public void deleteProduct(Long id) { + if (!productRepository.existsById(id)) { + throw new ResourceNotFoundException("Product not found with id: " + id); + } + productRepository.deleteById(id); + } + + @Transactional + public void bulkDeleteProducts(BulkDeleteRequest request) { + productRepository.deleteAllById(request.getIds()); + } + + private ProductResponse mapToResponse(Product product) { + return new ProductResponse( + product.getProdId(), + product.getProdName(), + product.getCategory().getCategoryId(), + product.getCategory().getCategoryName(), + product.getProdDesc(), + product.getProdPrice(), + product.getCreatedAt(), + product.getUpdatedAt() + ); + } +} diff --git a/backend/src/main/java/com/petshop/backend/service/ProductSupplierService.java b/backend/src/main/java/com/petshop/backend/service/ProductSupplierService.java new file mode 100644 index 00000000..7e3677a9 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/service/ProductSupplierService.java @@ -0,0 +1,109 @@ +package com.petshop.backend.service; + +import com.petshop.backend.dto.productsupplier.BulkDeleteProductSupplierRequest; +import com.petshop.backend.dto.productsupplier.ProductSupplierRequest; +import com.petshop.backend.dto.productsupplier.ProductSupplierResponse; +import com.petshop.backend.entity.Product; +import com.petshop.backend.entity.ProductSupplier; +import com.petshop.backend.entity.Supplier; +import com.petshop.backend.exception.ResourceNotFoundException; +import com.petshop.backend.repository.ProductRepository; +import com.petshop.backend.repository.ProductSupplierRepository; +import com.petshop.backend.repository.SupplierRepository; +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 ProductSupplierService { + + private final ProductSupplierRepository productSupplierRepository; + private final ProductRepository productRepository; + private final SupplierRepository supplierRepository; + + public ProductSupplierService(ProductSupplierRepository productSupplierRepository, ProductRepository productRepository, SupplierRepository supplierRepository) { + this.productSupplierRepository = productSupplierRepository; + this.productRepository = productRepository; + this.supplierRepository = supplierRepository; + } + + public Page getAllProductSuppliers(String query, Pageable pageable) { + Page productSuppliers; + if (query != null && !query.trim().isEmpty()) { + productSuppliers = productSupplierRepository.searchProductSuppliers(query, pageable); + } else { + productSuppliers = productSupplierRepository.findAll(pageable); + } + return productSuppliers.map(this::mapToResponse); + } + + public ProductSupplierResponse getProductSupplierById(Long productId, Long supplierId) { + ProductSupplier.ProductSupplierId id = new ProductSupplier.ProductSupplierId(productId, supplierId); + ProductSupplier productSupplier = productSupplierRepository.findById(id) + .orElseThrow(() -> new ResourceNotFoundException( + "ProductSupplier not found with productId: " + productId + " and supplierId: " + supplierId)); + return mapToResponse(productSupplier); + } + + @Transactional + public ProductSupplierResponse createProductSupplier(ProductSupplierRequest request) { + Product product = productRepository.findById(request.getProductId()) + .orElseThrow(() -> new ResourceNotFoundException("Product not found with id: " + request.getProductId())); + + Supplier supplier = supplierRepository.findById(request.getSupplierId()) + .orElseThrow(() -> new ResourceNotFoundException("Supplier not found with id: " + request.getSupplierId())); + + ProductSupplier productSupplier = new ProductSupplier(); + productSupplier.setProduct(product); + productSupplier.setSupplier(supplier); + productSupplier.setCost(request.getCost()); + + productSupplier = productSupplierRepository.save(productSupplier); + return mapToResponse(productSupplier); + } + + @Transactional + public ProductSupplierResponse updateProductSupplier(Long productId, Long supplierId, ProductSupplierRequest request) { + ProductSupplier.ProductSupplierId id = new ProductSupplier.ProductSupplierId(productId, supplierId); + ProductSupplier productSupplier = productSupplierRepository.findById(id) + .orElseThrow(() -> new ResourceNotFoundException( + "ProductSupplier not found with productId: " + productId + " and supplierId: " + supplierId)); + + productSupplier.setCost(request.getCost()); + + productSupplier = productSupplierRepository.save(productSupplier); + return mapToResponse(productSupplier); + } + + @Transactional + public void deleteProductSupplier(Long productId, Long supplierId) { + ProductSupplier.ProductSupplierId id = new ProductSupplier.ProductSupplierId(productId, supplierId); + if (!productSupplierRepository.existsById(id)) { + throw new ResourceNotFoundException( + "ProductSupplier not found with productId: " + productId + " and supplierId: " + supplierId); + } + productSupplierRepository.deleteById(id); + } + + @Transactional + public void bulkDeleteProductSuppliers(BulkDeleteProductSupplierRequest request) { + request.getKeys().forEach(key -> { + ProductSupplier.ProductSupplierId id = new ProductSupplier.ProductSupplierId( + key.getProductId(), key.getSupplierId()); + productSupplierRepository.deleteById(id); + }); + } + + private ProductSupplierResponse mapToResponse(ProductSupplier productSupplier) { + return new ProductSupplierResponse( + productSupplier.getProduct().getProdId(), + productSupplier.getProduct().getProdName(), + productSupplier.getSupplier().getSupId(), + productSupplier.getSupplier().getSupCompany(), + productSupplier.getCost(), + productSupplier.getCreatedAt(), + productSupplier.getUpdatedAt() + ); + } +} diff --git a/backend/src/main/java/com/petshop/backend/service/PurchaseOrderService.java b/backend/src/main/java/com/petshop/backend/service/PurchaseOrderService.java new file mode 100644 index 00000000..97286a9c --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/service/PurchaseOrderService.java @@ -0,0 +1,47 @@ +package com.petshop.backend.service; + +import com.petshop.backend.dto.purchaseorder.PurchaseOrderResponse; +import com.petshop.backend.entity.PurchaseOrder; +import com.petshop.backend.exception.ResourceNotFoundException; +import com.petshop.backend.repository.PurchaseOrderRepository; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +@Service +public class PurchaseOrderService { + + private final PurchaseOrderRepository purchaseOrderRepository; + + public PurchaseOrderService(PurchaseOrderRepository purchaseOrderRepository) { + this.purchaseOrderRepository = purchaseOrderRepository; + } + + public Page getAllPurchaseOrders(String query, Pageable pageable) { + Page purchaseOrders; + if (query != null && !query.trim().isEmpty()) { + purchaseOrders = purchaseOrderRepository.searchPurchaseOrders(query, pageable); + } else { + purchaseOrders = purchaseOrderRepository.findAll(pageable); + } + return purchaseOrders.map(this::mapToResponse); + } + + public PurchaseOrderResponse getPurchaseOrderById(Long id) { + PurchaseOrder purchaseOrder = purchaseOrderRepository.findById(id) + .orElseThrow(() -> new ResourceNotFoundException("PurchaseOrder not found with id: " + id)); + return mapToResponse(purchaseOrder); + } + + private PurchaseOrderResponse mapToResponse(PurchaseOrder purchaseOrder) { + return new PurchaseOrderResponse( + purchaseOrder.getPurchaseOrderId(), + purchaseOrder.getSupplier().getSupId(), + purchaseOrder.getSupplier().getSupCompany(), + purchaseOrder.getOrderDate(), + purchaseOrder.getStatus(), + purchaseOrder.getCreatedAt(), + purchaseOrder.getUpdatedAt() + ); + } +} diff --git a/backend/src/main/java/com/petshop/backend/service/RefundService.java b/backend/src/main/java/com/petshop/backend/service/RefundService.java new file mode 100644 index 00000000..ec7f4205 --- /dev/null +++ b/backend/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/backend/src/main/java/com/petshop/backend/service/SaleService.java b/backend/src/main/java/com/petshop/backend/service/SaleService.java new file mode 100644 index 00000000..b426dc38 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/service/SaleService.java @@ -0,0 +1,218 @@ +package com.petshop.backend.service; + +import com.petshop.backend.dto.sale.SaleRequest; +import com.petshop.backend.dto.sale.SaleResponse; +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.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Service +public class SaleService { + + private final SaleRepository saleRepository; + private final ProductRepository productRepository; + private final StoreRepository storeRepository; + private final InventoryRepository inventoryRepository; + private final EmployeeRepository employeeRepository; + private final EmployeeStoreRepository employeeStoreRepository; + private final UserRepository userRepository; + private final CustomerRepository customerRepository; + + public SaleService(SaleRepository saleRepository, ProductRepository productRepository, StoreRepository storeRepository, InventoryRepository inventoryRepository, EmployeeRepository employeeRepository, EmployeeStoreRepository employeeStoreRepository, UserRepository userRepository, CustomerRepository customerRepository) { + this.saleRepository = saleRepository; + this.productRepository = productRepository; + this.storeRepository = storeRepository; + this.inventoryRepository = inventoryRepository; + this.employeeRepository = employeeRepository; + this.employeeStoreRepository = employeeStoreRepository; + this.userRepository = userRepository; + this.customerRepository = customerRepository; + } + + @Transactional(readOnly = true) + public Page getAllSales(String query, Pageable pageable) { + Page sales; + if (query != null && !query.trim().isEmpty()) { + sales = saleRepository.searchSales(query, pageable); + } else { + sales = saleRepository.findAll(pageable); + } + return sales.map(this::mapToResponse); + } + + @Transactional(readOnly = true) + public SaleResponse getSaleById(Long id) { + Sale sale = saleRepository.findById(id) + .orElseThrow(() -> new ResourceNotFoundException("Sale not found with id: " + id)); + return mapToResponse(sale); + } + + @Transactional + public SaleResponse createSale(SaleRequest request) { + User user = AuthenticationHelper.getAuthenticatedUser(userRepository); + Employee employee = AuthenticationHelper.getAuthenticatedEmployee(userRepository, employeeRepository); + Long employeeStoreId = employeeStoreRepository.findByEmployeeEmployeeId(employee.getEmployeeId()) + .orElseThrow(() -> new BusinessException("Authenticated staff member is not assigned to a store")) + .getStore() + .getStoreId(); + + StoreLocation store = storeRepository.findById(request.getStoreId()) + .orElseThrow(() -> new ResourceNotFoundException("Store not found with id: " + request.getStoreId())); + + if (user.getRole() == User.Role.STAFF && !employeeStoreId.equals(store.getStoreId())) { + throw new BusinessException("Staff can only create sales for their assigned store"); + } + + Sale sale = new Sale(); + sale.setSaleDate(LocalDateTime.now()); + sale.setEmployee(employee); + sale.setStore(store); + 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())); + sale.setOriginalSale(originalSale); + } + + BigDecimal totalAmount = BigDecimal.ZERO; + List saleItems = new ArrayList<>(); + + if (sale.getIsRefund() && sale.getOriginalSale() != null) { + for (var itemRequest : request.getItems()) { + Product product = productRepository.findById(itemRequest.getProdId()) + .orElseThrow(() -> new ResourceNotFoundException("Product not found with id: " + itemRequest.getProdId())); + + SaleItem originalItem = sale.getOriginalSale().getItems().stream() + .filter(item -> item.getProduct().getProdId().equals(itemRequest.getProdId())) + .findFirst() + .orElseThrow(() -> new BusinessException("Product " + itemRequest.getProdId() + " was not in the original sale")); + + if (itemRequest.getQuantity() > originalItem.getQuantity()) { + throw new BusinessException("Refund quantity " + itemRequest.getQuantity() + + " exceeds original quantity " + originalItem.getQuantity() + + " for product: " + product.getProdName()); + } + + int alreadyRefundedQuantity = saleRepository.findByOriginalSaleSaleId(sale.getOriginalSale().getSaleId()).stream() + .flatMap(existingRefund -> existingRefund.getItems().stream()) + .filter(existingRefundItem -> existingRefundItem.getProduct().getProdId().equals(itemRequest.getProdId())) + .mapToInt(existingRefundItem -> Math.abs(existingRefundItem.getQuantity())) + .sum(); + + int refundableQuantity = originalItem.getQuantity() - alreadyRefundedQuantity; + if (itemRequest.getQuantity() > refundableQuantity) { + throw new BusinessException("Refund quantity " + itemRequest.getQuantity() + + " exceeds remaining refundable quantity " + refundableQuantity + + " for product: " + product.getProdName()); + } + + Inventory inventory = inventoryRepository.findByProductId(itemRequest.getProdId()) + .orElseThrow(() -> new ResourceNotFoundException("Inventory not found for product " + itemRequest.getProdId())); + + inventory.setQuantity(inventory.getQuantity() + itemRequest.getQuantity()); + inventoryRepository.save(inventory); + + BigDecimal unitPrice = originalItem.getUnitPrice(); + BigDecimal itemTotal = unitPrice.multiply(BigDecimal.valueOf(itemRequest.getQuantity())); + + SaleItem saleItem = new SaleItem(); + saleItem.setSale(sale); + saleItem.setProduct(product); + saleItem.setQuantity(-itemRequest.getQuantity()); + saleItem.setUnitPrice(unitPrice); + + saleItems.add(saleItem); + totalAmount = totalAmount.add(itemTotal); + } + totalAmount = totalAmount.negate(); + } else { + for (var itemRequest : request.getItems()) { + Product product = productRepository.findById(itemRequest.getProdId()) + .orElseThrow(() -> new ResourceNotFoundException("Product not found with id: " + itemRequest.getProdId())); + + Inventory inventory = inventoryRepository.findByProductId(itemRequest.getProdId()) + .orElseThrow(() -> new ResourceNotFoundException("Inventory not found for product " + itemRequest.getProdId())); + + if (inventory.getQuantity() < itemRequest.getQuantity()) { + throw new BusinessException("Insufficient stock for product: " + product.getProdName() + + ". Available: " + inventory.getQuantity() + ", requested: " + itemRequest.getQuantity()); + } + + inventory.setQuantity(inventory.getQuantity() - itemRequest.getQuantity()); + inventoryRepository.save(inventory); + + BigDecimal unitPrice = product.getProdPrice(); + BigDecimal itemTotal = unitPrice.multiply(BigDecimal.valueOf(itemRequest.getQuantity())); + + SaleItem saleItem = new SaleItem(); + saleItem.setSale(sale); + saleItem.setProduct(product); + saleItem.setQuantity(itemRequest.getQuantity()); + saleItem.setUnitPrice(unitPrice); + + saleItems.add(saleItem); + totalAmount = totalAmount.add(itemTotal); + } + } + + sale.setTotalAmount(totalAmount); + sale.setItems(saleItems); + + Sale savedSale = saleRepository.save(sale); + return mapToResponse(savedSale); + } + + private SaleResponse mapToResponse(Sale sale) { + SaleResponse response = new SaleResponse(); + response.setSaleId(sale.getSaleId()); + response.setSaleDate(sale.getSaleDate()); + response.setEmployeeId(sale.getEmployee().getEmployeeId()); + response.setEmployeeName(sale.getEmployee().getFirstName() + " " + sale.getEmployee().getLastName()); + + if (sale.getStore() != null) { + response.setStoreId(sale.getStore().getStoreId()); + response.setStoreName(sale.getStore().getStoreName()); + } + + response.setTotalAmount(sale.getTotalAmount()); + response.setPaymentMethod(sale.getPaymentMethod()); + response.setIsRefund(sale.getIsRefund()); + if (sale.getOriginalSale() != null) { + response.setOriginalSaleId(sale.getOriginalSale().getSaleId()); + } + response.setCreatedAt(sale.getCreatedAt()); + + List itemResponses = new ArrayList<>(); + for (SaleItem item : sale.getItems()) { + SaleResponse.SaleItemResponse itemResponse = new SaleResponse.SaleItemResponse(); + itemResponse.setSaleItemId(item.getSaleItemId()); + itemResponse.setProdId(item.getProduct().getProdId()); + itemResponse.setProductName(item.getProduct().getProdName()); + itemResponse.setQuantity(item.getQuantity()); + itemResponse.setUnitPrice(item.getUnitPrice()); + itemResponses.add(itemResponse); + } + response.setItems(itemResponses); + + return response; + } +} diff --git a/backend/src/main/java/com/petshop/backend/service/ServiceService.java b/backend/src/main/java/com/petshop/backend/service/ServiceService.java new file mode 100644 index 00000000..5243f101 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/service/ServiceService.java @@ -0,0 +1,88 @@ +package com.petshop.backend.service; + +import com.petshop.backend.dto.common.BulkDeleteRequest; +import com.petshop.backend.dto.service.ServiceRequest; +import com.petshop.backend.dto.service.ServiceResponse; +import com.petshop.backend.exception.ResourceNotFoundException; +import com.petshop.backend.repository.ServiceRepository; +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 ServiceService { + + private final ServiceRepository serviceRepository; + + public ServiceService(ServiceRepository serviceRepository) { + this.serviceRepository = serviceRepository; + } + + public Page getAllServices(String query, Pageable pageable) { + Page services; + if (query != null && !query.trim().isEmpty()) { + services = serviceRepository.searchServices(query, pageable); + } else { + services = serviceRepository.findAll(pageable); + } + return services.map(this::mapToResponse); + } + + public ServiceResponse getServiceById(Long id) { + com.petshop.backend.entity.Service service = serviceRepository.findById(id) + .orElseThrow(() -> new ResourceNotFoundException("Service not found with id: " + id)); + return mapToResponse(service); + } + + @Transactional + public ServiceResponse createService(ServiceRequest request) { + com.petshop.backend.entity.Service service = new com.petshop.backend.entity.Service(); + service.setServiceName(request.getServiceName()); + service.setServiceDesc(request.getServiceDesc()); + service.setServicePrice(request.getServicePrice()); + service.setServiceDuration(request.getServiceDuration()); + + service = serviceRepository.save(service); + return mapToResponse(service); + } + + @Transactional + public ServiceResponse updateService(Long id, ServiceRequest request) { + com.petshop.backend.entity.Service service = serviceRepository.findById(id) + .orElseThrow(() -> new ResourceNotFoundException("Service not found with id: " + id)); + + service.setServiceName(request.getServiceName()); + service.setServiceDesc(request.getServiceDesc()); + service.setServicePrice(request.getServicePrice()); + service.setServiceDuration(request.getServiceDuration()); + + service = serviceRepository.save(service); + return mapToResponse(service); + } + + @Transactional + public void deleteService(Long id) { + if (!serviceRepository.existsById(id)) { + throw new ResourceNotFoundException("Service not found with id: " + id); + } + serviceRepository.deleteById(id); + } + + @Transactional + public void bulkDeleteServices(BulkDeleteRequest request) { + serviceRepository.deleteAllById(request.getIds()); + } + + private ServiceResponse mapToResponse(com.petshop.backend.entity.Service service) { + return new ServiceResponse( + service.getServiceId(), + service.getServiceName(), + service.getServiceDesc(), + service.getServicePrice(), + service.getServiceDuration(), + service.getCreatedAt(), + service.getUpdatedAt() + ); + } +} diff --git a/backend/src/main/java/com/petshop/backend/service/StoreAssignmentService.java b/backend/src/main/java/com/petshop/backend/service/StoreAssignmentService.java new file mode 100644 index 00000000..31cc18d5 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/service/StoreAssignmentService.java @@ -0,0 +1,34 @@ +package com.petshop.backend.service; + +import com.petshop.backend.entity.Employee; +import com.petshop.backend.entity.EmployeeStore; +import com.petshop.backend.entity.StoreLocation; +import com.petshop.backend.exception.ResourceNotFoundException; +import com.petshop.backend.repository.EmployeeStoreRepository; +import com.petshop.backend.repository.StoreRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class StoreAssignmentService { + + private final EmployeeStoreRepository employeeStoreRepository; + private final StoreRepository storeRepository; + + public StoreAssignmentService(EmployeeStoreRepository employeeStoreRepository, StoreRepository storeRepository) { + this.employeeStoreRepository = employeeStoreRepository; + this.storeRepository = storeRepository; + } + + @Transactional + public void assignStoreIfMissing(Employee employee, Long storeId) { + if (employeeStoreRepository.findByEmployeeEmployeeId(employee.getEmployeeId()).isPresent()) { + return; + } + + StoreLocation store = storeRepository.findById(storeId) + .orElseThrow(() -> new ResourceNotFoundException("Store not found with id: " + storeId)); + + employeeStoreRepository.save(new EmployeeStore(employee, store)); + } +} diff --git a/backend/src/main/java/com/petshop/backend/service/StoreService.java b/backend/src/main/java/com/petshop/backend/service/StoreService.java new file mode 100644 index 00000000..5d2c9ce3 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/service/StoreService.java @@ -0,0 +1,88 @@ +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 { + + private final StoreRepository storeRepository; + + public StoreService(StoreRepository storeRepository) { + this.storeRepository = storeRepository; + } + + public Page getAllStores(String query, Pageable pageable) { + Page stores; + if (query != null && !query.trim().isEmpty()) { + stores = storeRepository.searchStores(query, pageable); + } else { + stores = storeRepository.findAll(pageable); + } + 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(), + store.getStoreName(), + store.getAddress(), + store.getPhone(), + store.getEmail(), + store.getCreatedAt() + ); + } +} diff --git a/backend/src/main/java/com/petshop/backend/service/SupplierService.java b/backend/src/main/java/com/petshop/backend/service/SupplierService.java new file mode 100644 index 00000000..2e80eeaa --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/service/SupplierService.java @@ -0,0 +1,92 @@ +package com.petshop.backend.service; + +import com.petshop.backend.dto.common.BulkDeleteRequest; +import com.petshop.backend.dto.supplier.SupplierRequest; +import com.petshop.backend.dto.supplier.SupplierResponse; +import com.petshop.backend.entity.Supplier; +import com.petshop.backend.exception.ResourceNotFoundException; +import com.petshop.backend.repository.SupplierRepository; +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 SupplierService { + + private final SupplierRepository supplierRepository; + + public SupplierService(SupplierRepository supplierRepository) { + this.supplierRepository = supplierRepository; + } + + public Page getAllSuppliers(String query, Pageable pageable) { + Page suppliers; + if (query != null && !query.trim().isEmpty()) { + suppliers = supplierRepository.searchSuppliers(query, pageable); + } else { + suppliers = supplierRepository.findAll(pageable); + } + return suppliers.map(this::mapToResponse); + } + + public SupplierResponse getSupplierById(Long id) { + Supplier supplier = supplierRepository.findById(id) + .orElseThrow(() -> new ResourceNotFoundException("Supplier not found with id: " + id)); + return mapToResponse(supplier); + } + + @Transactional + public SupplierResponse createSupplier(SupplierRequest request) { + Supplier supplier = new Supplier(); + supplier.setSupCompany(request.getSupCompany()); + supplier.setSupContactFirstName(request.getSupContactFirstName()); + supplier.setSupContactLastName(request.getSupContactLastName()); + supplier.setSupEmail(request.getSupEmail()); + supplier.setSupPhone(request.getSupPhone()); + + supplier = supplierRepository.save(supplier); + return mapToResponse(supplier); + } + + @Transactional + public SupplierResponse updateSupplier(Long id, SupplierRequest request) { + Supplier supplier = supplierRepository.findById(id) + .orElseThrow(() -> new ResourceNotFoundException("Supplier not found with id: " + id)); + + supplier.setSupCompany(request.getSupCompany()); + supplier.setSupContactFirstName(request.getSupContactFirstName()); + supplier.setSupContactLastName(request.getSupContactLastName()); + supplier.setSupEmail(request.getSupEmail()); + supplier.setSupPhone(request.getSupPhone()); + + supplier = supplierRepository.save(supplier); + return mapToResponse(supplier); + } + + @Transactional + public void deleteSupplier(Long id) { + if (!supplierRepository.existsById(id)) { + throw new ResourceNotFoundException("Supplier not found with id: " + id); + } + supplierRepository.deleteById(id); + } + + @Transactional + public void bulkDeleteSuppliers(BulkDeleteRequest request) { + supplierRepository.deleteAllById(request.getIds()); + } + + private SupplierResponse mapToResponse(Supplier supplier) { + return new SupplierResponse( + supplier.getSupId(), + supplier.getSupCompany(), + supplier.getSupContactFirstName(), + supplier.getSupContactLastName(), + supplier.getSupEmail(), + supplier.getSupPhone(), + supplier.getCreatedAt(), + supplier.getUpdatedAt() + ); + } +} diff --git a/backend/src/main/java/com/petshop/backend/service/UserBusinessLinkageService.java b/backend/src/main/java/com/petshop/backend/service/UserBusinessLinkageService.java new file mode 100644 index 00000000..05751688 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/service/UserBusinessLinkageService.java @@ -0,0 +1,152 @@ +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) { + if (user.getId() != null) { + var existing = employeeRepository.findByUserId(user.getId()); + if (existing.isPresent()) { + return syncEmployee(existing.get(), user); + } + } + + List emailMatches = employeeRepository.findAllByEmail(user.getEmail()); + + if (emailMatches.size() == 1) { + Employee employee = emailMatches.get(0); + if (employee.getUserId() == null) { + employee.setUserId(user.getId()); + return syncEmployee(employee, user); + } + } + + Employee newEmployee = new Employee(); + newEmployee.setUserId(user.getId()); + newEmployee.setEmail(user.getEmail()); + + String[] nameParts = splitFullName(user.getFullName()); + newEmployee.setFirstName(nameParts[0]); + newEmployee.setLastName(nameParts[1]); + + newEmployee.setIsActive(true); + + if (user.getRole() == User.Role.ADMIN) { + newEmployee.setRole("Manager"); + } else if (user.getRole() == User.Role.STAFF) { + newEmployee.setRole("Staff"); + } else { + newEmployee.setRole("Staff"); + } + + return syncEmployee(newEmployee, user); + } + + @Transactional + public Customer ensureLinkedCustomer(User user) { + if (user.getId() != null) { + var existing = customerRepository.findByUserId(user.getId()); + if (existing.isPresent()) { + return syncCustomer(existing.get(), user); + } + } + + List emailMatches = customerRepository.findAllByEmail(user.getEmail()); + + if (emailMatches.size() == 1) { + Customer customer = emailMatches.get(0); + if (customer.getUserId() == null) { + customer.setUserId(user.getId()); + return syncCustomer(customer, user); + } + } + + Customer newCustomer = new Customer(); + newCustomer.setUserId(user.getId()); + newCustomer.setEmail(user.getEmail()); + + String[] nameParts = splitFullName(user.getFullName()); + newCustomer.setFirstName(nameParts[0]); + newCustomer.setLastName(nameParts[1]); + + return syncCustomer(newCustomer, user); + } + + @Transactional + public void syncLinkedRecords(User user) { + if (user.getRole() == User.Role.CUSTOMER) { + ensureLinkedCustomer(user); + return; + } + ensureLinkedEmployee(user); + } + + private Employee syncEmployee(Employee employee, User user) { + employee.setUserId(user.getId()); + employee.setEmail(user.getEmail()); + String[] nameParts = splitFullName(user.getFullName()); + employee.setFirstName(nameParts[0]); + employee.setLastName(nameParts[1]); + if (user.getRole() == User.Role.ADMIN) { + employee.setRole("Manager"); + } else { + employee.setRole("Staff"); + } + return employeeRepository.save(employee); + } + + private Customer syncCustomer(Customer customer, User user) { + customer.setUserId(user.getId()); + customer.setEmail(user.getEmail()); + String[] nameParts = splitFullName(user.getFullName()); + customer.setFirstName(nameParts[0]); + customer.setLastName(nameParts[1]); + return customerRepository.save(customer); + } + + 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) { + return new String[]{trimmed, "User"}; + } + + 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/backend/src/main/java/com/petshop/backend/service/UserService.java b/backend/src/main/java/com/petshop/backend/service/UserService.java new file mode 100644 index 00000000..3c219172 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/service/UserService.java @@ -0,0 +1,166 @@ +package com.petshop.backend.service; + +import com.petshop.backend.dto.common.BulkDeleteRequest; +import com.petshop.backend.dto.user.UserRequest; +import com.petshop.backend.dto.user.UserResponse; +import com.petshop.backend.entity.User; +import com.petshop.backend.exception.ResourceNotFoundException; +import com.petshop.backend.repository.UserRepository; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.server.ResponseStatusException; + +import java.util.Locale; + +import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.CONFLICT; + +@Service +public class UserService { + + private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + private final UserBusinessLinkageService userBusinessLinkageService; + + public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder, UserBusinessLinkageService userBusinessLinkageService) { + this.userRepository = userRepository; + this.passwordEncoder = passwordEncoder; + this.userBusinessLinkageService = userBusinessLinkageService; + } + + public Page getAllUsers(String query, String role, Pageable pageable) { + User.Role parsedRole = parseRole(role); + Page users; + boolean hasQuery = query != null && !query.trim().isEmpty(); + if (hasQuery && parsedRole != null) { + users = userRepository.searchUsersByRole(query, parsedRole, pageable); + } else if (hasQuery) { + users = userRepository.searchUsers(query, pageable); + } else if (parsedRole != null) { + users = userRepository.findByRole(parsedRole, pageable); + } else { + users = userRepository.findAll(pageable); + } + return users.map(this::mapToResponse); + } + + public UserResponse getUserById(Long id) { + User user = userRepository.findById(id) + .orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + id)); + return mapToResponse(user); + } + + @Transactional + public UserResponse createUser(UserRequest request) { + User user = new User(); + user.setUsername(request.getUsername()); + user.setPassword(passwordEncoder.encode(request.getPassword())); + user.setFullName(request.getFullName()); + user.setEmail(request.getEmail()); + user.setPhone(trimToNull(request.getPhone())); + user.setRole(request.getRole()); + user.setActive(request.getActive() != null ? request.getActive() : true); + + validateUniquePhone(user.getPhone(), null); + + user = userRepository.save(user); + + userBusinessLinkageService.syncLinkedRecords(user); + + return mapToResponse(user); + } + + @Transactional + public UserResponse updateUser(Long id, UserRequest request) { + User user = userRepository.findById(id) + .orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + id)); + + boolean invalidateToken = + !user.getUsername().equals(request.getUsername()) + || user.getRole() != request.getRole() + || !user.getActive().equals(request.getActive() != null ? request.getActive() : true); + + user.setUsername(request.getUsername()); + if (request.getPassword() != null && !request.getPassword().trim().isEmpty()) { + user.setPassword(passwordEncoder.encode(request.getPassword())); + invalidateToken = true; + } + user.setFullName(request.getFullName()); + user.setEmail(request.getEmail()); + String phone = trimToNull(request.getPhone()); + if (!java.util.Objects.equals(user.getPhone(), phone)) { + validateUniquePhone(phone, user.getId()); + } + user.setPhone(phone); + user.setRole(request.getRole()); + user.setActive(request.getActive() != null ? request.getActive() : true); + if (invalidateToken) { + user.setTokenVersion(user.getTokenVersion() + 1); + } + + user = userRepository.save(user); + userBusinessLinkageService.syncLinkedRecords(user); + return mapToResponse(user); + } + + @Transactional + public void deleteUser(Long id) { + if (!userRepository.existsById(id)) { + throw new ResourceNotFoundException("User not found with id: " + id); + } + userRepository.deleteById(id); + } + + @Transactional + public void bulkDeleteUsers(BulkDeleteRequest request) { + userRepository.deleteAllById(request.getIds()); + } + + private UserResponse mapToResponse(User user) { + UserResponse response = new UserResponse(); + response.setId(user.getId()); + response.setUsername(user.getUsername()); + response.setFullName(user.getFullName()); + response.setEmail(user.getEmail()); + response.setPhone(user.getPhone()); + response.setRole(user.getRole().toString()); + response.setActive(user.getActive()); + response.setCreatedAt(user.getCreatedAt()); + response.setUpdatedAt(user.getUpdatedAt()); + return response; + } + + private void validateUniquePhone(String phone, Long currentUserId) { + if (phone == null || phone.isBlank()) { + return; + } + userRepository.findByPhone(phone) + .filter(existing -> !existing.getId().equals(currentUserId)) + .ifPresent(existing -> { + throw new ResponseStatusException(CONFLICT, "Phone already exists"); + }); + } + + private String trimToNull(String value) { + if (value == null) { + return null; + } + String trimmed = value.trim(); + return trimmed.isEmpty() ? null : trimmed; + } + + private User.Role parseRole(String role) { + String normalizedRole = trimToNull(role); + if (normalizedRole == null) { + return null; + } + try { + return User.Role.valueOf(normalizedRole.toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException ex) { + throw new ResponseStatusException(BAD_REQUEST, "Invalid value for parameter: role"); + } + } +} diff --git a/backend/src/main/java/com/petshop/backend/util/AuthenticationHelper.java b/backend/src/main/java/com/petshop/backend/util/AuthenticationHelper.java new file mode 100644 index 00000000..b1ab33a1 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/util/AuthenticationHelper.java @@ -0,0 +1,62 @@ +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 com.petshop.backend.security.AppPrincipal; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; + +@Component +public class AuthenticationHelper { + + public static Authentication getAuthentication() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication == null || !authentication.isAuthenticated()) { + throw new RuntimeException("No authenticated user found"); + } + return authentication; + } + + public static AppPrincipal getAuthenticatedPrincipal() { + Object principal = getAuthentication().getPrincipal(); + if (principal instanceof AppPrincipal appPrincipal) { + return appPrincipal; + } + throw new RuntimeException("Authenticated principal is not supported"); + } + + public static Long getAuthenticatedUserId() { + return getAuthenticatedPrincipal().getUserId(); + } + + public static User getAuthenticatedUser(UserRepository userRepository) { + Authentication authentication = getAuthentication(); + Object principal = authentication.getPrincipal(); + + if (principal instanceof AppPrincipal appPrincipal) { + return userRepository.findById(appPrincipal.getUserId()) + .orElseThrow(() -> new RuntimeException("User not found: " + appPrincipal.getUserId())); + } + + 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/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml new file mode 100644 index 00000000..81f7ed51 --- /dev/null +++ b/backend/src/main/resources/application.yml @@ -0,0 +1,56 @@ +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} + password: ${SPRING_DATASOURCE_PASSWORD:petshop} + driver-class-name: com.mysql.cj.jdbc.Driver + + sql: + init: + mode: never + + jpa: + hibernate: + ddl-auto: validate + naming: + physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl + show-sql: ${JPA_SHOW_SQL:false} + properties: + hibernate: + format_sql: true + open-in-view: false + + flyway: + enabled: true + baseline-on-migrate: true + baseline-version: 0 + +server: + port: ${SERVER_PORT:8080} + servlet: + context-path: / + +springdoc: + api-docs: + path: /v3/api-docs + swagger-ui: + path: /swagger-ui + +jwt: + secret: ${JWT_SECRET:change_me_please_make_this_at_least_32_characters_long_for_security} + expiration: ${JWT_EXPIRATION:86400000} + +logging: + level: + com.petshop: ${LOG_LEVEL:INFO} + org.springframework.security: ${LOG_LEVEL_SECURITY:WARN} + org.springdoc.core.events.SpringDocAppInitializer: ERROR diff --git a/backend/src/main/resources/db/migration/V1__baseline_schema.sql b/backend/src/main/resources/db/migration/V1__baseline_schema.sql new file mode 100644 index 00000000..ae1ca009 --- /dev/null +++ b/backend/src/main/resources/db/migration/V1__baseline_schema.sql @@ -0,0 +1,250 @@ +-- 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, + user_id BIGINT NULL, + 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, + CONSTRAINT uk_employee_user_id UNIQUE (user_id) +); + +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, + 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, + CONSTRAINT uk_customer_user_id UNIQUE (user_id) +); + +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, + 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) +); + +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, + email VARCHAR(100) UNIQUE, + 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 +); + +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) NOT NULL DEFAULT 'OPEN', + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL 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 NOT NULL DEFAULT CURRENT_TIMESTAMP, + isRead BOOLEAN NOT NULL DEFAULT FALSE, + 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; diff --git a/backend/src/main/resources/db/migration/V2__seed_data.sql b/backend/src/main/resources/db/migration/V2__seed_data.sql new file mode 100644 index 00000000..5e8d3fb6 --- /dev/null +++ b/backend/src/main/resources/db/migration/V2__seed_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, customerId) +VALUES +('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 +(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/backend/src/main/resources/db/migration/V3__appointment_store_and_employee_store_constraints.sql b/backend/src/main/resources/db/migration/V3__appointment_store_and_employee_store_constraints.sql new file mode 100644 index 00000000..2e65bc98 --- /dev/null +++ b/backend/src/main/resources/db/migration/V3__appointment_store_and_employee_store_constraints.sql @@ -0,0 +1,19 @@ +ALTER TABLE appointment + ADD COLUMN storeId BIGINT NULL AFTER customerId; + +UPDATE appointment +SET storeId = 1 +WHERE storeId IS NULL; + +ALTER TABLE appointment + MODIFY COLUMN storeId BIGINT NOT NULL, + ADD CONSTRAINT fk_appointment_store FOREIGN KEY (storeId) REFERENCES storeLocation(storeId); + +DELETE es1 +FROM employeeStore es1 +JOIN employeeStore es2 + ON es1.employeeId = es2.employeeId + AND es1.storeId > es2.storeId; + +ALTER TABLE employeeStore + ADD CONSTRAINT uk_employeeStore_employee UNIQUE (employeeId); diff --git a/backend/src/main/resources/db/migration/V4__conversation_mode_and_takeover.sql b/backend/src/main/resources/db/migration/V4__conversation_mode_and_takeover.sql new file mode 100644 index 00000000..271c0e72 --- /dev/null +++ b/backend/src/main/resources/db/migration/V4__conversation_mode_and_takeover.sql @@ -0,0 +1,9 @@ +ALTER TABLE conversation + ADD COLUMN mode VARCHAR(20) NOT NULL DEFAULT 'AUTOMATED' AFTER status, + ADD COLUMN humanRequestedAt TIMESTAMP NULL AFTER mode; + +UPDATE conversation +SET mode = CASE + WHEN staffId IS NULL THEN 'AUTOMATED' + ELSE 'HUMAN' +END; diff --git a/backend/src/main/resources/db/migration/V5__user_token_version.sql b/backend/src/main/resources/db/migration/V5__user_token_version.sql new file mode 100644 index 00000000..455c4bc8 --- /dev/null +++ b/backend/src/main/resources/db/migration/V5__user_token_version.sql @@ -0,0 +1,2 @@ +ALTER TABLE users + ADD COLUMN tokenVersion INT NOT NULL DEFAULT 0 AFTER active; diff --git a/backend/src/main/resources/db/migration/V6__user_phone.sql b/backend/src/main/resources/db/migration/V6__user_phone.sql new file mode 100644 index 00000000..95478fdc --- /dev/null +++ b/backend/src/main/resources/db/migration/V6__user_phone.sql @@ -0,0 +1,8 @@ +ALTER TABLE users + ADD COLUMN phone VARCHAR(20) NULL AFTER fullName; + +UPDATE users u +LEFT JOIN customer c ON c.user_id = u.id +LEFT JOIN employee e ON e.user_id = u.id +SET u.phone = COALESCE(NULLIF(c.phone, ''), NULLIF(e.phone, ''), u.phone) +WHERE u.phone IS NULL OR u.phone = ''; diff --git a/backend/src/main/resources/db/migration/V7__employee_customer_phone_cutover.sql b/backend/src/main/resources/db/migration/V7__employee_customer_phone_cutover.sql new file mode 100644 index 00000000..fa922a82 --- /dev/null +++ b/backend/src/main/resources/db/migration/V7__employee_customer_phone_cutover.sql @@ -0,0 +1,11 @@ +UPDATE users u +LEFT JOIN customer c ON c.user_id = u.id +LEFT JOIN employee e ON e.user_id = u.id +SET u.phone = COALESCE(NULLIF(u.phone, ''), NULLIF(c.phone, ''), NULLIF(e.phone, '')) +WHERE u.phone IS NULL OR u.phone = ''; + +ALTER TABLE customer + DROP COLUMN phone; + +ALTER TABLE employee + DROP COLUMN phone;