fix audit report mismatches across backend and android
This commit is contained in:
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user