From 2f369c0b175c40aeb29d570588ba0622664c0378 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Tue, 7 Apr 2026 16:06:44 -0600 Subject: [PATCH] fix audit report mismatches across backend and android --- .../petstoremobile/api/InventoryApi.java | 2 - .../petstoremobile/api/MessageApi.java | 12 - .../petstoremobile/api/PurchaseOrderApi.java | 2 +- .../example/petstoremobile/api/SaleApi.java | 8 - .../petstoremobile/api/auth/AuthApi.java | 3 +- .../dtos/AvatarUploadResponse.java | 25 + .../fragments/ChatFragment.java | 28 +- .../fragments/ProfileFragment.java | 2 - .../listfragments/InventoryFragment.java | 2 +- .../repositories/AuthRepository.java | 3 +- .../repositories/ChatRepository.java | 10 - .../repositories/InventoryRepository.java | 4 +- .../viewmodels/AuthViewModel.java | 3 +- .../viewmodels/ChatViewModel.java | 10 - .../viewmodels/InventoryViewModel.java | 4 +- backend/petshop-api.postman_collection.json | 462 +++++++++++++++++- .../controller/DropdownController.java | 10 + .../controller/PetImageController.java | 8 +- .../backend/controller/RefundController.java | 89 ++-- .../backend/exception/ApiErrorResponder.java | 6 +- .../exception/GlobalExceptionHandler.java | 24 + .../backend/service/RefundService.java | 26 +- 22 files changed, 586 insertions(+), 157 deletions(-) create mode 100644 android/app/src/main/java/com/example/petstoremobile/dtos/AvatarUploadResponse.java diff --git a/android/app/src/main/java/com/example/petstoremobile/api/InventoryApi.java b/android/app/src/main/java/com/example/petstoremobile/api/InventoryApi.java index 6c747e6e..99618112 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/InventoryApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/InventoryApi.java @@ -16,13 +16,11 @@ import retrofit2.http.Query; public interface InventoryApi { - // GET /api/v1/inventory?q=...&page=...&size=...&category=...&storeId=...&sort=... @GET("api/v1/inventory") Call> getAllInventory( @Query("page") int page, @Query("size") int size, @Query("q") String query, - @Query("category") String category, @Query("storeId") Long storeId, @Query("sort") String sort); diff --git a/android/app/src/main/java/com/example/petstoremobile/api/MessageApi.java b/android/app/src/main/java/com/example/petstoremobile/api/MessageApi.java index 29ff4ae0..13df781f 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/MessageApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/MessageApi.java @@ -3,14 +3,10 @@ package com.example.petstoremobile.api; import com.example.petstoremobile.dtos.MessageDTO; import com.example.petstoremobile.dtos.SendMessageRequest; import java.util.List; -import okhttp3.MultipartBody; -import okhttp3.RequestBody; import retrofit2.Call; import retrofit2.http.Body; import retrofit2.http.GET; -import retrofit2.http.Multipart; import retrofit2.http.POST; -import retrofit2.http.Part; import retrofit2.http.Path; //api calls to get and send messages @@ -21,12 +17,4 @@ public interface MessageApi { @POST("api/v1/chat/conversations/{id}/messages") Call sendMessage(@Path("id") Long conversationId, @Body SendMessageRequest request); - - @Multipart - @POST("api/v1/chat/conversations/{id}/messages/attachment") - Call sendMessageWithAttachment( - @Path("id") Long conversationId, - @Part("content") RequestBody content, - @Part MultipartBody.Part file - ); } \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/api/PurchaseOrderApi.java b/android/app/src/main/java/com/example/petstoremobile/api/PurchaseOrderApi.java index e5a5a06d..ebb99139 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/PurchaseOrderApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/PurchaseOrderApi.java @@ -13,7 +13,7 @@ public interface PurchaseOrderApi { Call> getAllPurchaseOrders( @Query("page") int page, @Query("size") int size, - @Query("query") String query, + @Query("q") String query, @Query("storeId") Long storeId, @Query("sort") String sort); diff --git a/android/app/src/main/java/com/example/petstoremobile/api/SaleApi.java b/android/app/src/main/java/com/example/petstoremobile/api/SaleApi.java index 55d1eef8..72bfd8f4 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/SaleApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/SaleApi.java @@ -5,10 +5,8 @@ import com.example.petstoremobile.dtos.SaleDTO; import retrofit2.Call; import retrofit2.http.Body; -import retrofit2.http.DELETE; import retrofit2.http.GET; import retrofit2.http.POST; -import retrofit2.http.PUT; import retrofit2.http.Path; import retrofit2.http.Query; @@ -24,10 +22,4 @@ public interface SaleApi { @POST("api/v1/sales") Call createSale(@Body SaleDTO sale); - - @PUT("api/v1/sales/{id}") - Call updateSale(@Path("id") Long id, @Body SaleDTO sale); - - @DELETE("api/v1/sales/{id}") - Call deleteSale(@Path("id") Long id); } \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/api/auth/AuthApi.java b/android/app/src/main/java/com/example/petstoremobile/api/auth/AuthApi.java index 88c5312c..c0d5c1fe 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/auth/AuthApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/auth/AuthApi.java @@ -1,6 +1,7 @@ package com.example.petstoremobile.api.auth; import com.example.petstoremobile.dtos.AuthDTO; +import com.example.petstoremobile.dtos.AvatarUploadResponse; import com.example.petstoremobile.dtos.UserDTO; import java.util.Map; @@ -36,7 +37,7 @@ public interface AuthApi { //upload avatar endpoint @Multipart @POST("api/v1/auth/me/avatar") - Call uploadAvatar(@Part MultipartBody.Part avatar); + Call uploadAvatar(@Part MultipartBody.Part avatar); //delete avatar endpoint @DELETE("api/v1/auth/me/avatar") diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/AvatarUploadResponse.java b/android/app/src/main/java/com/example/petstoremobile/dtos/AvatarUploadResponse.java new file mode 100644 index 00000000..194be1f4 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/AvatarUploadResponse.java @@ -0,0 +1,25 @@ +package com.example.petstoremobile.dtos; + +public class AvatarUploadResponse { + private String avatarUrl; + private String message; + + public AvatarUploadResponse() { + } + + 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; + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java index 5ed6ac6e..b13edd67 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java @@ -9,6 +9,7 @@ import android.provider.OpenableColumns; import android.util.Log; import android.view.*; import android.view.inputmethod.EditorInfo; +import android.widget.Toast; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; @@ -28,21 +29,16 @@ import com.example.petstoremobile.dtos.SendMessageRequest; import com.example.petstoremobile.models.Chat; import com.example.petstoremobile.models.Message; import com.example.petstoremobile.services.ChatNotificationService; -import com.example.petstoremobile.utils.FileUtils; import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.viewmodels.ChatViewModel; import com.example.petstoremobile.websocket.StompChatManager; -import java.io.File; import java.util.*; import javax.inject.Inject; import javax.inject.Named; import dagger.hilt.android.AndroidEntryPoint; -import okhttp3.MediaType; -import okhttp3.MultipartBody; -import okhttp3.RequestBody; @AndroidEntryPoint public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickListener, StompChatManager.MessageListener, @@ -361,26 +357,10 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis binding.etMessage.setText(""); removeAttachment(); - try { - File file = FileUtils.getFileFromUri(requireContext(), uri); - if (file == null) return; - - String mimeType = requireContext().getContentResolver().getType(uri); - RequestBody requestFile = RequestBody.create(file, MediaType.parse(mimeType != null ? mimeType : "application/octet-stream")); - MultipartBody.Part filePart = MultipartBody.Part.createFormData("file", file.getName(), requestFile); - RequestBody contentPart = RequestBody.create(text, MediaType.parse("text/plain")); - - viewModel.sendMessageWithAttachment(activeConversationId, contentPart, filePart).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { - messageList.add(dtoToModel(resource.data)); - messageAdapter.notifyItemInserted(messageList.size() - 1); - scrollToBottom(); - loadConversations(); - } - }); - } catch (Exception e) { - Log.e(TAG, "Error sending message with attachment", e); + if (!text.isEmpty()) { + binding.etMessage.setText(text); } + Toast.makeText(requireContext(), "File attachments are not supported", Toast.LENGTH_SHORT).show(); } /** diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java index c80a82ae..af469295 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java @@ -230,9 +230,7 @@ public class ProfileFragment extends Fragment { viewModel.uploadAvatar(body).observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; if (resource.status == Resource.Status.SUCCESS) { - currentUser = resource.data; Toast.makeText(getContext(), "Avatar updated successfully", Toast.LENGTH_SHORT).show(); - // Reload image after successful upload loadProfileData(); } else if (resource.status == Resource.Status.ERROR) { Toast.makeText(getContext(), "Upload failed: " + resource.message, Toast.LENGTH_SHORT).show(); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java index 1fe9d188..fe68a629 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java @@ -210,7 +210,7 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn } //Load all inventory items from the backend using viewModel - viewModel.getAllInventory(query, null, storeId, currentPage, PAGE_SIZE, "product.prodName").observe(getViewLifecycleOwner(), resource -> { + viewModel.getAllInventory(query, storeId, currentPage, PAGE_SIZE, "product.prodName").observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; // Check the status to see if the resource is loaded and display the data diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/AuthRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/AuthRepository.java index 2ec410f9..6011bac8 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/AuthRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/AuthRepository.java @@ -7,6 +7,7 @@ import androidx.lifecycle.MutableLiveData; import com.example.petstoremobile.api.auth.AuthApi; import com.example.petstoremobile.api.auth.TokenManager; import com.example.petstoremobile.dtos.AuthDTO; +import com.example.petstoremobile.dtos.AvatarUploadResponse; import com.example.petstoremobile.dtos.UserDTO; import com.example.petstoremobile.utils.ErrorUtils; import com.example.petstoremobile.utils.Resource; @@ -83,7 +84,7 @@ public class AuthRepository extends BaseRepository { /** * Uploads a multipart image to be used as the current user's avatar. */ - public LiveData> uploadAvatar(MultipartBody.Part avatar) { + public LiveData> uploadAvatar(MultipartBody.Part avatar) { return executeCall(authApi.uploadAvatar(avatar)); } diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/ChatRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/ChatRepository.java index 8301797d..c3a31ad4 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/ChatRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/ChatRepository.java @@ -17,9 +17,6 @@ import java.util.List; import javax.inject.Inject; import javax.inject.Singleton; -import okhttp3.MultipartBody; -import okhttp3.RequestBody; - /** * Repository for handling chat-related data operations. */ @@ -58,13 +55,6 @@ public class ChatRepository extends BaseRepository { return executeCall(messageApi.sendMessage(conversationId, request)); } - /** - * Sends a message with a file attachment to a conversation. - */ - public LiveData> sendMessageWithAttachment(Long conversationId, RequestBody content, MultipartBody.Part file) { - return executeCall(messageApi.sendMessageWithAttachment(conversationId, content, file)); - } - /** * Fetches a paginated list of customers. */ diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/InventoryRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/InventoryRepository.java index 05719c38..364dc20d 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/InventoryRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/InventoryRepository.java @@ -25,8 +25,8 @@ public class InventoryRepository extends BaseRepository { /** * Retrieves a paginated list of inventory items from the API with optional search, category, storeId and sort. */ - public LiveData>> getAllInventory(String query, String category, Long storeId, int page, int size, String sort) { - return executeCall(inventoryApi.getAllInventory(page, size, query, category, storeId, sort)); + public LiveData>> getAllInventory(String query, Long storeId, int page, int size, String sort) { + return executeCall(inventoryApi.getAllInventory(page, size, query, storeId, sort)); } /** diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AuthViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AuthViewModel.java index 061ee687..36e437bb 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AuthViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AuthViewModel.java @@ -4,6 +4,7 @@ import androidx.lifecycle.LiveData; import androidx.lifecycle.ViewModel; import com.example.petstoremobile.dtos.AuthDTO; +import com.example.petstoremobile.dtos.AvatarUploadResponse; import com.example.petstoremobile.dtos.UserDTO; import com.example.petstoremobile.repositories.AuthRepository; import com.example.petstoremobile.utils.Resource; @@ -48,7 +49,7 @@ public class AuthViewModel extends ViewModel { /** * Uploads a new avatar image for the current user. */ - public LiveData> uploadAvatar(MultipartBody.Part avatar) { + public LiveData> uploadAvatar(MultipartBody.Part avatar) { return repository.uploadAvatar(avatar); } diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ChatViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ChatViewModel.java index 51435f82..2b516490 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ChatViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ChatViewModel.java @@ -16,9 +16,6 @@ import java.util.List; import javax.inject.Inject; import dagger.hilt.android.lifecycle.HiltViewModel; -import okhttp3.MultipartBody; -import okhttp3.RequestBody; - /** * ViewModel for managing chat-related UI state and data operations. */ @@ -52,13 +49,6 @@ public class ChatViewModel extends ViewModel { return repository.sendMessage(conversationId, request); } - /** - * Sends a message with a file attachment to a conversation. - */ - public LiveData> sendMessageWithAttachment(Long conversationId, RequestBody content, MultipartBody.Part file) { - return repository.sendMessageWithAttachment(conversationId, content, file); - } - /** * Fetches a paginated list of customers. */ diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryViewModel.java index 02a5f1bb..6facbb2f 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryViewModel.java @@ -36,8 +36,8 @@ public class InventoryViewModel extends ViewModel { /** * Retrieves a paginated list of inventory items, with optional filtering and sorting. */ - public LiveData>> getAllInventory(String query, String category, Long storeId, int page, int size, String sort) { - return inventoryRepository.getAllInventory(query, category, storeId, page, size, sort); + public LiveData>> getAllInventory(String query, Long storeId, int page, int size, String sort) { + return inventoryRepository.getAllInventory(query, storeId, page, size, sort); } /** diff --git a/backend/petshop-api.postman_collection.json b/backend/petshop-api.postman_collection.json index 5ab9bd98..330585a6 100644 --- a/backend/petshop-api.postman_collection.json +++ b/backend/petshop-api.postman_collection.json @@ -3748,6 +3748,267 @@ } ] }, + { + "name": "Customers Alias (/api/v1/customers)", + "item": [ + { + "name": "List Customers - staff 200", + "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": "List Customers - admin 200", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/customers", + "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 Customers - customer 403", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/customers", + "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 403', function () {", + " pm.response.to.have.status(403);", + "});" + ] + } + } + ] + }, + { + "name": "Get Customer - staff 200", + "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": "Get Customer - admin 200", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/customers/{{customerId}}", + "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 Customer - admin 201", + "request": { + "method": "POST", + "url": "{{baseUrl}}/api/v1/customers", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"firstName\": \"Alias\",\n \"lastName\": \"Customer\",\n \"email\": \"alias.cust@example.com\",\n \"password\": \"Test1234!\",\n \"role\": \"CUSTOMER\"\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 201', function () {", + " pm.response.to.have.status(201);", + "});" + ] + } + } + ] + }, + { + "name": "Update Customer - admin 200", + "request": { + "method": "PUT", + "url": "{{baseUrl}}/api/v1/customers/{{customerId}}", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"firstName\": \"AliasUpdated\"\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});" + ] + } + } + ] + }, + { + "name": "Delete Customer - admin 204", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/v1/customers/{{customerId}}", + "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": "Users", "item": [ @@ -4295,6 +4556,205 @@ } ] }, + { + "name": "Employees Alias (/api/v1/employees)", + "item": [ + { + "name": "List Employees - admin 200", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/employees", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ] + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});" + ] + } + } + ] + }, + { + "name": "List Employees - staff 403", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/employees", + "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": "Get Employee - admin 200", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/employees/{{employeeId}}", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ] + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});" + ] + } + } + ] + }, + { + "name": "Create Employee - admin 201", + "request": { + "method": "POST", + "url": "{{baseUrl}}/api/v1/employees", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"firstName\": \"Alias\",\n \"lastName\": \"Employee\",\n \"email\": \"alias.staff@example.com\",\n \"password\": \"Test1234!\",\n \"role\": \"STAFF\"\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 201', function () {", + " pm.response.to.have.status(201);", + "});" + ] + } + } + ] + }, + { + "name": "Update Employee - admin 200", + "request": { + "method": "PUT", + "url": "{{baseUrl}}/api/v1/employees/{{employeeId}}", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"firstName\": \"AliasUpdated\"\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});" + ] + } + } + ] + }, + { + "name": "Delete Employee - admin 204", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/v1/employees/{{employeeId}}", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ] + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 204', function () {", + " pm.response.to.have.status(204);", + "});" + ] + } + } + ] + } + ] + }, { "name": "Stores", "item": [ @@ -5583,4 +6043,4 @@ ] } ] -} +} \ No newline at end of file diff --git a/backend/src/main/java/com/petshop/backend/controller/DropdownController.java b/backend/src/main/java/com/petshop/backend/controller/DropdownController.java index 4d995cd4..3b0a2009 100644 --- a/backend/src/main/java/com/petshop/backend/controller/DropdownController.java +++ b/backend/src/main/java/com/petshop/backend/controller/DropdownController.java @@ -153,6 +153,16 @@ public class DropdownController { ); } + @GetMapping("/customers/{customerId}/pets") + @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") + public ResponseEntity> getCustomerPets(@PathVariable Long customerId) { + return ResponseEntity.ok( + petRepository.findAllByOwner_IdOrderByPetNameAsc(customerId).stream() + .map(p -> new DropdownOption(p.getPetId(), p.getPetName())) + .collect(Collectors.toList()) + ); + } + @GetMapping("/suppliers") @PreAuthorize("hasRole('ADMIN')") public ResponseEntity> getSuppliers() { diff --git a/backend/src/main/java/com/petshop/backend/controller/PetImageController.java b/backend/src/main/java/com/petshop/backend/controller/PetImageController.java index bd9717ca..93477cc7 100644 --- a/backend/src/main/java/com/petshop/backend/controller/PetImageController.java +++ b/backend/src/main/java/com/petshop/backend/controller/PetImageController.java @@ -48,12 +48,8 @@ public class PetImageController { @GetMapping("/{id}/image") public ResponseEntity getPetImage(@PathVariable Long id) { - try { - PetService.ImagePayload payload = petService.loadPetImage(id, currentUserId(), currentUserRole()); - return ResponseEntity.ok().contentType(payload.mediaType()).body(payload.resource()); - } catch (PetService.ForbiddenImageAccessException ex) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); - } + PetService.ImagePayload payload = petService.loadPetImage(id, currentUserId(), currentUserRole()); + return ResponseEntity.ok().contentType(payload.mediaType()).body(payload.resource()); } @DeleteMapping("/{id}/image") diff --git a/backend/src/main/java/com/petshop/backend/controller/RefundController.java b/backend/src/main/java/com/petshop/backend/controller/RefundController.java index bd6f158b..0001df1e 100644 --- a/backend/src/main/java/com/petshop/backend/controller/RefundController.java +++ b/backend/src/main/java/com/petshop/backend/controller/RefundController.java @@ -15,9 +15,7 @@ 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") @@ -33,27 +31,20 @@ public class RefundController { @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); + public ResponseEntity createRefund(@Valid @RequestBody RefundRequest request) { + 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")) { - User user = AuthenticationHelper.getAuthenticatedUser(userRepository); - customerId = user.getId(); - } - - 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); + Long customerId = null; + if (role != null && role.equals("CUSTOMER")) { + User user = AuthenticationHelper.getAuthenticatedUser(userRepository); + customerId = user.getId(); } + + return ResponseEntity.status(HttpStatus.CREATED).body(refundService.createRefund(request, customerId)); } @GetMapping @@ -77,54 +68,32 @@ public class RefundController { @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); + public ResponseEntity getRefundById(@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")) { - User user = AuthenticationHelper.getAuthenticatedUser(userRepository); - customerId = user.getId(); - } - - 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); + Long customerId = null; + if (role != null && role.equals("CUSTOMER")) { + User user = AuthenticationHelper.getAuthenticatedUser(userRepository); + customerId = user.getId(); } + + return ResponseEntity.ok(refundService.getRefundById(id, customerId)); } @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); - } + public ResponseEntity updateRefund(@PathVariable Long id, @Valid @RequestBody RefundUpdateRequest request) { + return ResponseEntity.ok(refundService.updateRefundStatus(id, request.getStatus())); } @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); - } + public ResponseEntity deleteRefund(@PathVariable Long id) { + refundService.deleteRefund(id); + return ResponseEntity.noContent().build(); } } diff --git a/backend/src/main/java/com/petshop/backend/exception/ApiErrorResponder.java b/backend/src/main/java/com/petshop/backend/exception/ApiErrorResponder.java index 39f4d66c..0fcb82ec 100644 --- a/backend/src/main/java/com/petshop/backend/exception/ApiErrorResponder.java +++ b/backend/src/main/java/com/petshop/backend/exception/ApiErrorResponder.java @@ -1,6 +1,7 @@ package com.petshop.backend.exception; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.json.JsonMapper; import jakarta.servlet.http.HttpServletResponse; import org.springframework.http.HttpStatus; @@ -13,7 +14,10 @@ import java.time.LocalDateTime; @Component public class ApiErrorResponder { - private final ObjectMapper objectMapper = JsonMapper.builder().findAndAddModules().build(); + private final ObjectMapper objectMapper = JsonMapper.builder() + .findAndAddModules() + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .build(); public void write(HttpServletResponse response, HttpStatus status, String message, String details, String path) throws IOException { response.setStatus(status.value()); diff --git a/backend/src/main/java/com/petshop/backend/exception/GlobalExceptionHandler.java b/backend/src/main/java/com/petshop/backend/exception/GlobalExceptionHandler.java index b41f8789..81163287 100644 --- a/backend/src/main/java/com/petshop/backend/exception/GlobalExceptionHandler.java +++ b/backend/src/main/java/com/petshop/backend/exception/GlobalExceptionHandler.java @@ -1,15 +1,19 @@ package com.petshop.backend.exception; +import com.petshop.backend.service.PetService; import jakarta.servlet.http.HttpServletRequest; import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.data.core.PropertyReferenceException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.validation.FieldError; +import org.springframework.web.HttpRequestMethodNotSupportedException; 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 org.springframework.web.servlet.resource.NoResourceFoundException; import java.time.LocalDateTime; import java.util.HashMap; @@ -78,6 +82,26 @@ public class GlobalExceptionHandler { return buildErrorResponse(HttpStatus.valueOf(ex.getStatusCode().value()), message, ex, request); } + @ExceptionHandler(NoResourceFoundException.class) + public ResponseEntity handleNoResourceFound(NoResourceFoundException ex, HttpServletRequest request) { + return buildErrorResponse(HttpStatus.NOT_FOUND, "Route not found", ex, request); + } + + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + public ResponseEntity handleMethodNotSupported(HttpRequestMethodNotSupportedException ex, HttpServletRequest request) { + return buildErrorResponse(HttpStatus.METHOD_NOT_ALLOWED, ex.getMessage(), ex, request); + } + + @ExceptionHandler(PropertyReferenceException.class) + public ResponseEntity handleBadSortProperty(PropertyReferenceException ex, HttpServletRequest request) { + return buildErrorResponse(HttpStatus.BAD_REQUEST, "Invalid sort field: " + ex.getPropertyName(), ex, request); + } + + @ExceptionHandler(PetService.ForbiddenImageAccessException.class) + public ResponseEntity handleForbiddenImageAccess(PetService.ForbiddenImageAccessException ex, HttpServletRequest request) { + return buildErrorResponse(HttpStatus.FORBIDDEN, "Access to this pet image is not allowed", ex, request); + } + @ExceptionHandler(Exception.class) public ResponseEntity handleGenericException(Exception ex, HttpServletRequest request) { String message = ex.getMessage() == null || ex.getMessage().isBlank() diff --git a/backend/src/main/java/com/petshop/backend/service/RefundService.java b/backend/src/main/java/com/petshop/backend/service/RefundService.java index 49cc2f69..90bf397b 100644 --- a/backend/src/main/java/com/petshop/backend/service/RefundService.java +++ b/backend/src/main/java/com/petshop/backend/service/RefundService.java @@ -8,6 +8,8 @@ import com.petshop.backend.entity.Product; import com.petshop.backend.entity.Refund; import com.petshop.backend.entity.RefundItem; import com.petshop.backend.entity.Sale; +import com.petshop.backend.exception.BusinessException; +import com.petshop.backend.exception.ResourceNotFoundException; import com.petshop.backend.repository.ProductRepository; import com.petshop.backend.repository.RefundRepository; import com.petshop.backend.repository.SaleRepository; @@ -40,14 +42,14 @@ public class RefundService { @Transactional public RefundResponse createRefund(RefundRequest request, Long customerId) { Sale sale = saleRepository.findById(request.getSaleId()) - .orElseThrow(() -> new RuntimeException("Sale not found")); + .orElseThrow(() -> new BusinessException("Sale not found")); if (sale.getCustomer() == null) { - throw new RuntimeException("Sale has no associated customer"); + throw new BusinessException("Sale has no associated customer"); } if (customerId != null && !sale.getCustomer().getId().equals(customerId)) { - throw new RuntimeException("You can only create refunds for your own purchases"); + throw new BusinessException("You can only create refunds for your own purchases"); } Refund refund = new Refund(); @@ -59,13 +61,13 @@ public class RefundService { BigDecimal totalAmount = BigDecimal.ZERO; for (var itemRequest : request.getItems()) { Product product = productRepository.findById(itemRequest.getProdId()) - .orElseThrow(() -> new RuntimeException("Product not found: " + itemRequest.getProdId())); - + .orElseThrow(() -> new BusinessException("Product not found: " + itemRequest.getProdId())); + BigDecimal unitPrice = sale.getItems().stream() .filter(item -> item.getProduct().getProdId().equals(itemRequest.getProdId())) .findFirst() .map(item -> item.getUnitPrice()) - .orElseThrow(() -> new RuntimeException("Product " + itemRequest.getProdId() + " was not in original sale")); + .orElseThrow(() -> new BusinessException("Product " + itemRequest.getProdId() + " was not in original sale")); RefundItem refundItem = new RefundItem(); refundItem.setProduct(product); @@ -84,10 +86,10 @@ public class RefundService { @Transactional(readOnly = true) public RefundResponse getRefundById(Long id, Long customerId) { Refund refund = refundRepository.findById(id) - .orElseThrow(() -> new RuntimeException("Refund not found")); + .orElseThrow(() -> new ResourceNotFoundException("Refund not found")); if (customerId != null && !refund.getCustomerId().equals(customerId)) { - throw new RuntimeException("You can only view your own refunds"); + throw new ResourceNotFoundException("You can only view your own refunds"); } return toResponse(refund); @@ -111,18 +113,18 @@ public class RefundService { @Transactional public RefundResponse updateRefundStatus(Long id, String status) { Refund refund = refundRepository.findById(id) - .orElseThrow(() -> new RuntimeException("Refund not found")); + .orElseThrow(() -> new ResourceNotFoundException("Refund not found")); Refund.RefundStatus newStatus; try { newStatus = Refund.RefundStatus.valueOf(status.toUpperCase()); } catch (IllegalArgumentException e) { - throw new RuntimeException("Invalid status: " + status); + throw new BusinessException("Invalid status: " + status); } if (refund.getStatus() == Refund.RefundStatus.PENDING && newStatus == Refund.RefundStatus.APPROVED) { Sale originalSale = saleRepository.findById(refund.getSaleId()) - .orElseThrow(() -> new RuntimeException("Original sale not found")); + .orElseThrow(() -> new ResourceNotFoundException("Original sale not found")); SaleRequest saleRequest = new SaleRequest(); saleRequest.setStoreId(originalSale.getStore().getStoreId()); @@ -150,7 +152,7 @@ public class RefundService { @Transactional public void deleteRefund(Long id) { if (!refundRepository.existsById(id)) { - throw new RuntimeException("Refund not found"); + throw new ResourceNotFoundException("Refund not found"); } refundRepository.deleteById(id); }