fix audit report mismatches across backend and android

This commit is contained in:
2026-04-07 16:06:44 -06:00
parent 0173123898
commit 4500b213c6
22 changed files with 586 additions and 157 deletions

View File

@@ -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<PageResponse<InventoryDTO>> 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);

View File

@@ -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<MessageDTO> sendMessage(@Path("id") Long conversationId, @Body SendMessageRequest request);
@Multipart
@POST("api/v1/chat/conversations/{id}/messages/attachment")
Call<MessageDTO> sendMessageWithAttachment(
@Path("id") Long conversationId,
@Part("content") RequestBody content,
@Part MultipartBody.Part file
);
}

View File

@@ -13,7 +13,7 @@ public interface PurchaseOrderApi {
Call<PageResponse<PurchaseOrderDTO>> 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);

View File

@@ -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<SaleDTO> createSale(@Body SaleDTO sale);
@PUT("api/v1/sales/{id}")
Call<SaleDTO> updateSale(@Path("id") Long id, @Body SaleDTO sale);
@DELETE("api/v1/sales/{id}")
Call<Void> deleteSale(@Path("id") Long id);
}

View File

@@ -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<UserDTO> uploadAvatar(@Part MultipartBody.Part avatar);
Call<AvatarUploadResponse> uploadAvatar(@Part MultipartBody.Part avatar);
//delete avatar endpoint
@DELETE("api/v1/auth/me/avatar")

View File

@@ -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;
}
}

View File

@@ -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();
}
/**

View File

@@ -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();

View File

@@ -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

View File

@@ -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<Resource<UserDTO>> uploadAvatar(MultipartBody.Part avatar) {
public LiveData<Resource<AvatarUploadResponse>> uploadAvatar(MultipartBody.Part avatar) {
return executeCall(authApi.uploadAvatar(avatar));
}

View File

@@ -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<Resource<MessageDTO>> sendMessageWithAttachment(Long conversationId, RequestBody content, MultipartBody.Part file) {
return executeCall(messageApi.sendMessageWithAttachment(conversationId, content, file));
}
/**
* Fetches a paginated list of customers.
*/

View File

@@ -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<Resource<PageResponse<InventoryDTO>>> 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<Resource<PageResponse<InventoryDTO>>> getAllInventory(String query, Long storeId, int page, int size, String sort) {
return executeCall(inventoryApi.getAllInventory(page, size, query, storeId, sort));
}
/**

View File

@@ -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<Resource<UserDTO>> uploadAvatar(MultipartBody.Part avatar) {
public LiveData<Resource<AvatarUploadResponse>> uploadAvatar(MultipartBody.Part avatar) {
return repository.uploadAvatar(avatar);
}

View File

@@ -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<Resource<MessageDTO>> sendMessageWithAttachment(Long conversationId, RequestBody content, MultipartBody.Part file) {
return repository.sendMessageWithAttachment(conversationId, content, file);
}
/**
* Fetches a paginated list of customers.
*/

View File

@@ -36,8 +36,8 @@ public class InventoryViewModel extends ViewModel {
/**
* Retrieves a paginated list of inventory items, with optional filtering and sorting.
*/
public LiveData<Resource<PageResponse<InventoryDTO>>> getAllInventory(String query, String category, Long storeId, int page, int size, String sort) {
return inventoryRepository.getAllInventory(query, category, storeId, page, size, sort);
public LiveData<Resource<PageResponse<InventoryDTO>>> getAllInventory(String query, Long storeId, int page, int size, String sort) {
return inventoryRepository.getAllInventory(query, storeId, page, size, sort);
}
/**

View File

@@ -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 @@
]
}
]
}
}

View File

@@ -153,6 +153,16 @@ public class DropdownController {
);
}
@GetMapping("/customers/{customerId}/pets")
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
public ResponseEntity<List<DropdownOption>> 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<List<DropdownOption>> getSuppliers() {

View File

@@ -48,12 +48,8 @@ public class PetImageController {
@GetMapping("/{id}/image")
public ResponseEntity<Resource> 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")

View File

@@ -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<RefundResponse> 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<String, String> 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<RefundResponse> 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<String, String> 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<String, String> error = new HashMap<>();
error.put("message", e.getMessage());
return ResponseEntity.badRequest().body(error);
}
public ResponseEntity<RefundResponse> 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<String, String> response = new HashMap<>();
response.put("message", "Refund deleted successfully");
return ResponseEntity.ok(response);
} catch (RuntimeException e) {
Map<String, String> error = new HashMap<>();
error.put("message", e.getMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
public ResponseEntity<Void> deleteRefund(@PathVariable Long id) {
refundService.deleteRefund(id);
return ResponseEntity.noContent().build();
}
}

View File

@@ -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());

View File

@@ -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<ApiErrorResponse> handleNoResourceFound(NoResourceFoundException ex, HttpServletRequest request) {
return buildErrorResponse(HttpStatus.NOT_FOUND, "Route not found", ex, request);
}
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public ResponseEntity<ApiErrorResponse> handleMethodNotSupported(HttpRequestMethodNotSupportedException ex, HttpServletRequest request) {
return buildErrorResponse(HttpStatus.METHOD_NOT_ALLOWED, ex.getMessage(), ex, request);
}
@ExceptionHandler(PropertyReferenceException.class)
public ResponseEntity<ApiErrorResponse> handleBadSortProperty(PropertyReferenceException ex, HttpServletRequest request) {
return buildErrorResponse(HttpStatus.BAD_REQUEST, "Invalid sort field: " + ex.getPropertyName(), ex, request);
}
@ExceptionHandler(PetService.ForbiddenImageAccessException.class)
public ResponseEntity<ApiErrorResponse> 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<ApiErrorResponse> handleGenericException(Exception ex, HttpServletRequest request) {
String message = ex.getMessage() == null || ex.getMessage().isBlank()

View File

@@ -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);
}