Skip to content

Commit b4d6804

Browse files
committed
FEAT: 사용자 잔고 변경 기능 추가 #34
* 외부 SDK를 사용해보려 했으나, 프론트엔드 에서 제공해줘야 하는 값이 있어서 백엔드로는 불가능
1 parent ec79cf2 commit b4d6804

File tree

9 files changed

+485
-18
lines changed

9 files changed

+485
-18
lines changed

src/main/java/jshop/domain/user/controller/UserController.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,22 @@
22

33
import jakarta.validation.Valid;
44
import jshop.domain.user.dto.UpdateUserRequest;
5+
import jshop.domain.user.dto.UpdateWalletBalanceRequest;
56
import jshop.domain.user.dto.UserInfoResponse;
67
import jshop.domain.user.service.UserService;
78
import jshop.global.annotation.CurrentUserId;
9+
import jshop.global.common.ErrorCode;
810
import jshop.global.dto.Response;
11+
import jshop.global.exception.JshopException;
912
import lombok.RequiredArgsConstructor;
13+
import lombok.extern.slf4j.Slf4j;
1014
import org.springframework.web.bind.annotation.GetMapping;
1115
import org.springframework.web.bind.annotation.PatchMapping;
1216
import org.springframework.web.bind.annotation.RequestBody;
1317
import org.springframework.web.bind.annotation.RequestMapping;
1418
import org.springframework.web.bind.annotation.RestController;
1519

20+
@Slf4j
1621
@RestController
1722
@RequestMapping("/api/users")
1823
@RequiredArgsConstructor
@@ -32,4 +37,10 @@ public Response<UserInfoResponse> getUserInfo(@CurrentUserId Long userId) {
3237
return Response
3338
.<UserInfoResponse>builder().data(userInfoResponse).build();
3439
}
40+
41+
@PatchMapping("/balance")
42+
public void updateWalletBalance(@CurrentUserId Long userId,
43+
@RequestBody @Valid UpdateWalletBalanceRequest updateWalletBalanceRequest) {
44+
userService.updateWalletBalance(userId, updateWalletBalanceRequest);
45+
}
3546
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package jshop.domain.user.dto;
2+
3+
import jakarta.validation.constraints.Min;
4+
import jakarta.validation.constraints.NotNull;
5+
import jakarta.validation.constraints.Pattern;
6+
import jakarta.validation.constraints.Positive;
7+
import jshop.domain.wallet.entity.WalletChangeType;
8+
import lombok.AllArgsConstructor;
9+
import lombok.Builder;
10+
import lombok.EqualsAndHashCode;
11+
import lombok.Getter;
12+
import lombok.NoArgsConstructor;
13+
import lombok.ToString;
14+
15+
@Getter
16+
@Builder
17+
@NoArgsConstructor
18+
@AllArgsConstructor
19+
@EqualsAndHashCode
20+
@ToString
21+
public class UpdateWalletBalanceRequest {
22+
23+
@NotNull(message = "변경 잔고는 공백일 수 없습니다.")
24+
@Positive
25+
private Long amount;
26+
27+
@NotNull(message = "변경 타입은 공백일 수 없습니다.")
28+
private WalletChangeType type;
29+
}

src/main/java/jshop/domain/user/service/UserService.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66
import jshop.domain.address.repository.AddressRepository;
77
import jshop.domain.user.dto.JoinUserRequest;
88
import jshop.domain.user.dto.UpdateUserRequest;
9+
import jshop.domain.user.dto.UpdateWalletBalanceRequest;
910
import jshop.domain.user.dto.UserInfoResponse;
1011
import jshop.domain.user.entity.User;
1112
import jshop.domain.user.repository.UserRepository;
13+
import jshop.domain.wallet.entity.Wallet;
1214
import jshop.global.common.ErrorCode;
1315
import jshop.global.exception.JshopException;
1416
import jshop.global.utils.UserUtils;
@@ -55,6 +57,21 @@ public void updateUser(Long userId, UpdateUserRequest updateUserRequest) {
5557
user.updateUserInfo(updateUserRequest);
5658
}
5759

60+
@Transactional
61+
public void updateWalletBalance(Long userId, UpdateWalletBalanceRequest updateWalletBalanceRequest) {
62+
User user = getUser(userId);
63+
Wallet wallet = user.getWallet();
64+
switch (updateWalletBalanceRequest.getType()) {
65+
case DEPOSIT:
66+
wallet.deposit(updateWalletBalanceRequest.getAmount());
67+
break;
68+
69+
case WITHDRAW:
70+
wallet.withdraw(updateWalletBalanceRequest.getAmount());
71+
break;
72+
}
73+
}
74+
5875
public User getUser(Long userId) {
5976
Optional<User> optionalUser = userRepository.findById(userId);
6077
return UserUtils.getUserOrThrow(optionalUser, userId);

src/main/java/jshop/domain/wallet/entity/Wallet.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,22 +42,29 @@ public class Wallet extends BaseEntity {
4242
private WalletChangeType walletChangeType;
4343

4444
public static Wallet create() {
45+
return create(0L);
46+
}
47+
48+
public static Wallet create(Long balance) {
4549
return Wallet
46-
.builder().balance(0L).walletChangeType(WalletChangeType.CREATE).build();
50+
.builder().balance(balance).walletChangeType(WalletChangeType.CREATE).build();
4751
}
4852

4953

5054
public Long deposit(Long amount) {
55+
checkAmount(amount);
5156
walletChangeType = WalletChangeType.DEPOSIT;
5257
return balance += amount;
5358
}
5459

5560
public Long refund(Long amount) {
61+
checkAmount(amount);
5662
walletChangeType = WalletChangeType.REFUND;
5763
return balance += amount;
5864
}
5965

6066
public Long purchase(Long amount) {
67+
checkAmount(amount);
6168
if (balance - amount < 0) {
6269
log.error(ErrorCode.WALLET_BALANCE_EXCEPTION.getLogMessage(), balance - amount);
6370
throw JshopException.of(ErrorCode.WALLET_BALANCE_EXCEPTION);
@@ -68,6 +75,7 @@ public Long purchase(Long amount) {
6875
}
6976

7077
public Long withdraw(Long amount) {
78+
checkAmount(amount);
7179
if (balance - amount < 0) {
7280
log.error(ErrorCode.WALLET_BALANCE_EXCEPTION.getLogMessage(), balance - amount);
7381
throw JshopException.of(ErrorCode.WALLET_BALANCE_EXCEPTION);
@@ -76,4 +84,12 @@ public Long withdraw(Long amount) {
7684
walletChangeType = WalletChangeType.WITHDRAW;
7785
return balance -= amount;
7886
}
87+
88+
private boolean checkAmount(Long amount) {
89+
if (amount <= 0) {
90+
log.error(ErrorCode.ILLEGAL_BALANCE_REQUEST.getLogMessage(), amount);
91+
throw JshopException.of(ErrorCode.ILLEGAL_BALANCE_REQUEST);
92+
}
93+
return true;
94+
}
7995
}

src/main/java/jshop/global/common/ErrorCode.java

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -46,35 +46,36 @@ public enum ErrorCode {
4646
INVALID_ORDER_ITEM(10060, "주문 정보가 잘못되었습니다", "주문 수량이나 가격이 잘못되었습니다. 수량 : [{}], 가격 : [{}]", HttpStatus.BAD_REQUEST),
4747

4848
// 비즈니스 로직 오류
49-
ILLEGAL_QUANTITY_REQUEST_EXCEPTION(50_001, "재고 변화량이 잘못되었습니다.", "재고 변화량이 잘못되었습니다. [{}]", HttpStatus.BAD_REQUEST),
50-
NEGATIVE_QUANTITY_EXCEPTION(50_002, "재고는 음수일 수 없습니다.", "재고는 음수일 수 없습니다. [{}]", HttpStatus.BAD_REQUEST),
51-
ILLEGAL_PRICE_EXCEPTION(50_101, "가격이 잘못되었습니다.", "가격은 0보다 커야합니다. [{}].", HttpStatus.BAD_REQUEST),
52-
ILLEGAL_PAGE_REQUEST(60_001, "요청할 수 없는 페이지 입니다.", "요청할 수 없는 페이지 입니다. pageNumber : [{}], pageSize : [{}]",
49+
ILLEGAL_QUANTITY_REQUEST_EXCEPTION(50001, "재고 변화량이 잘못되었습니다.", "재고 변화량이 잘못되었습니다. [{}]", HttpStatus.BAD_REQUEST),
50+
NEGATIVE_QUANTITY_EXCEPTION(50002, "재고는 음수일 수 없습니다.", "재고는 음수일 수 없습니다. [{}]", HttpStatus.BAD_REQUEST),
51+
ILLEGAL_PRICE_EXCEPTION(50101, "가격이 잘못되었습니다.", "가격은 0보다 커야합니다. [{}].", HttpStatus.BAD_REQUEST),
52+
ILLEGAL_PAGE_REQUEST(60001, "요청할 수 없는 페이지 입니다.", "요청할 수 없는 페이지 입니다. pageNumber : [{}], pageSize : [{}]",
5353
HttpStatus.BAD_REQUEST),
54-
ILLEGAL_CART_QUANTITY_REQUEST_EXCEPTION(70_001, "장바구니 수량이 잘못되었습니다.", "장바구니 수량은 1 이상이여야 합니다. [{}]",
54+
ILLEGAL_CART_QUANTITY_REQUEST_EXCEPTION(70001, "장바구니 수량이 잘못되었습니다.", "장바구니 수량은 1 이상이여야 합니다. [{}]",
5555
HttpStatus.BAD_REQUEST),
56-
WALLET_BALANCE_EXCEPTION(80_001, "잔고는 음수일 수 없습니다.", "잔고는 음수일 수 없습니다. [{}]", HttpStatus.BAD_REQUEST),
57-
ALREADY_SHIPPING_ORDER(90_001, "이미 배송이 시작된 주문입니다.", "이미 배송이 시작된 주문입니다. [{}]", HttpStatus.BAD_REQUEST),
58-
ALREADY_CANCLED_DELIVERY(90_100, "이미 취소된 배송입니다.", "이미 취소된 배송입니다. DELIVERY_ID : [{}]", HttpStatus.BAD_REQUEST),
59-
ILLEGAL_DELIVERY_STATE(90_200, "배송 상태가 잘못되었습니다.", "배송 상태가 잘못되었습니다. 현재 상태 : [{}], 원하는 상태 : [{}]",
56+
ILLEGAL_BALANCE_REQUEST(70101, "잔고 변화는 0보다 커야합니다.", "잔고 변화는 0보다 커야합니다. 요청 값 : [{}]", HttpStatus.BAD_REQUEST),
57+
WALLET_BALANCE_EXCEPTION(80001, "잔고는 음수일 수 없습니다.", "잔고는 음수일 수 없습니다. [{}]", HttpStatus.BAD_REQUEST),
58+
ALREADY_SHIPPING_ORDER(90001, "이미 배송이 시작된 주문입니다.", "이미 배송이 시작된 주문입니다. [{}]", HttpStatus.BAD_REQUEST),
59+
ALREADY_CANCLED_DELIVERY(90100, "이미 취소된 배송입니다.", "이미 취소된 배송입니다. DELIVERY_ID : [{}]", HttpStatus.BAD_REQUEST),
60+
ILLEGAL_DELIVERY_STATE(90200, "배송 상태가 잘못되었습니다.", "배송 상태가 잘못되었습니다. 현재 상태 : [{}], 원하는 상태 : [{}]",
6061
HttpStatus.BAD_REQUEST),
61-
ORDER_PRICE_MISMATCH(90_500, "주문 가격과 상품 가격이 맞지 않습니다.", "주문 가격과 상품 가격이 맞지 않습니다. 주문가격 : [{}] , 상품 가격 총합 : [{}]",
62+
ORDER_PRICE_MISMATCH(90500, "주문 가격과 상품 가격이 맞지 않습니다.", "주문 가격과 상품 가격이 맞지 않습니다. 주문가격 : [{}] , 상품 가격 총합 : [{}]",
6263
HttpStatus.BAD_REQUEST),
63-
PRODUCT_PRICE_MISMATCH(90_501, "상품 가격이 변경되었습니다.",
64+
PRODUCT_PRICE_MISMATCH(90501, "상품 가격이 변경되었습니다.",
6465
"주문요청 상품 가격과 실제 상품 가격이 맞지 않습니다. 상품 상세 ID : [{}] 주문 상품 가격 : " + "[{}], 실제 상품 가격 : [{}]", HttpStatus.BAD_REQUEST),
65-
ORDER_QUANTITY_MISMATCH(90_550, "주문 수량과 상품 수량의 합이 맞지 않습니다.",
66+
ORDER_QUANTITY_MISMATCH(90550, "주문 수량과 상품 수량의 합이 맞지 않습니다.",
6667
"주문 수량과 상품 수량의 합이 맞지 않습니다. 주문수량 : [{}] , 상품 수량 총합 : " + "[{}]", HttpStatus.BAD_REQUEST),
6768

6869
// 잘못된 상품
69-
INVALID_PRODUCT_ATTRIBUTE(100_001, "상세 상품 속성이 잘못되었습니다.",
70+
INVALID_PRODUCT_ATTRIBUTE(100001, "상세 상품 속성이 잘못되었습니다.",
7071
"상세상품 속성이 상품 속성에 없습니다. [attributes : {}] [attribute : {}]", HttpStatus.BAD_REQUEST),
71-
INVALID_PRODUCTDETAIL_INVENTORY(100_100, "상세 상품이 잘못되었습니다. 관리자에게 문의하세요",
72+
INVALID_PRODUCTDETAIL_INVENTORY(100100, "상세 상품이 잘못되었습니다. 관리자에게 문의하세요",
7273
"상세 상품의 Inventory가 잘못되었습니다. product " + "detail : [{}]", HttpStatus.INTERNAL_SERVER_ERROR),
73-
INVALID_PRODUCTDETAIL_PRODUCT(100_200, "상세 상품이 잘못되었습니다. 관리자에게 문의하세요", "상세 상품의 Product가 잘못되었습니다. [{}]",
74+
INVALID_PRODUCTDETAIL_PRODUCT(100200, "상세 상품이 잘못되었습니다. 관리자에게 문의하세요", "상세 상품의 Product가 잘못되었습니다. [{}]",
7475
HttpStatus.INTERNAL_SERVER_ERROR),
7576

7677
// 서버 문제
77-
USER_WALLET_NOT_FOUND(200_100, "사용자의 지갑을 찾을 수 없습니다. 관리자에게 문의하세요", "사용자의 지갑을 찾을 수 없습니다. user : [{}]",
78+
USER_WALLET_NOT_FOUND(200100, "사용자의 지갑을 찾을 수 없습니다. 관리자에게 문의하세요", "사용자의 지갑을 찾을 수 없습니다. user : [{}]",
7879
HttpStatus.INTERNAL_SERVER_ERROR);
7980

8081
private final int code;

src/test/java/jshop/domain/user/controller/UserControllerTest.java

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,26 @@
44
import static jshop.utils.MockSecurityContextUtil.mockUserSecurityContext;
55
import static org.mockito.Mockito.times;
66
import static org.mockito.Mockito.verify;
7+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
78
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
89

10+
import java.util.stream.Stream;
911
import jshop.domain.user.dto.UpdateUserRequest;
12+
import jshop.domain.user.dto.UpdateWalletBalanceRequest;
1013
import jshop.domain.user.service.UserService;
14+
import jshop.domain.wallet.entity.WalletChangeType;
15+
import jshop.global.common.ErrorCode;
1116
import jshop.global.controller.GlobalExceptionHandler;
1217
import jshop.utils.config.TestSecurityConfig;
1318
import org.json.JSONObject;
1419
import org.junit.jupiter.api.DisplayName;
1520
import org.junit.jupiter.api.Nested;
1621
import org.junit.jupiter.api.Test;
22+
import org.junit.jupiter.params.ParameterizedTest;
23+
import org.junit.jupiter.params.provider.Arguments;
24+
import org.junit.jupiter.params.provider.MethodSource;
25+
import org.mockito.ArgumentCaptor;
26+
import org.mockito.Captor;
1727
import org.springframework.beans.factory.annotation.Autowired;
1828
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
1929
import org.springframework.boot.test.mock.mockito.MockBean;
@@ -23,6 +33,9 @@
2333
import org.springframework.test.web.servlet.ResultActions;
2434
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
2535

36+
import static org.assertj.core.api.Assertions.assertThat;
37+
import static org.junit.jupiter.api.Assertions.assertThrows;
38+
2639
@WebMvcTest(UserController.class)
2740
@Import({TestSecurityConfig.class, GlobalExceptionHandler.class})
2841
@DisplayName("[단위 테스트] UserController")
@@ -107,4 +120,75 @@ public void updateUser_noAuth() throws Exception {
107120
perform.andExpect(status().isUnauthorized());
108121
}
109122
}
123+
124+
@Nested
125+
@DisplayName("현재 유저의 잔고 변경 검증")
126+
class UpdateWallet {
127+
128+
@Captor
129+
ArgumentCaptor<Long> userIdCaptor;
130+
131+
@Captor
132+
ArgumentCaptor<UpdateWalletBalanceRequest> updateWalletBalanceRequestArgumentCaptor;
133+
134+
private static Stream<Arguments> provideValidArgs() {
135+
String depositRequestStr = """
136+
{ "amount" : 100, "type" : "DEPOSIT"}
137+
""";
138+
139+
String withdrawRequestStr = """
140+
{ "amount" : 100, "type" : "WITHDRAW"}
141+
""";
142+
return Stream.of(Arguments.of(depositRequestStr, WalletChangeType.DEPOSIT),
143+
Arguments.of(withdrawRequestStr, WalletChangeType.WITHDRAW));
144+
}
145+
146+
private static Stream<Arguments> provideInValidArgs() {
147+
String depositRequestStr = """
148+
{ "amount" : -100, "type" : "DEPOSIT"}
149+
""";
150+
151+
String withdrawRequestStr = """
152+
{ "amount" : 0, "type" : "WITHDRAW"}
153+
""";
154+
return Stream.of(Arguments.of(depositRequestStr, WalletChangeType.DEPOSIT),
155+
Arguments.of(withdrawRequestStr, WalletChangeType.WITHDRAW));
156+
}
157+
158+
@ParameterizedTest
159+
@DisplayName("잔고 변화량이 0 이상이고, 변경 타입이 DEPOSIT, WITHDRAW라면 잔고를 갱신할 수 있다.")
160+
@MethodSource("provideValidArgs")
161+
public void updateWalletBalance_success(String request, WalletChangeType type) throws Exception {
162+
// when
163+
mockMvc.perform(MockMvcRequestBuilders
164+
.patch("/api/users/balance")
165+
.with(mockUserSecurityContext())
166+
.contentType(MediaType.APPLICATION_JSON)
167+
.content(request));
168+
169+
// then
170+
verify(userService, times(1)).updateWalletBalance(userIdCaptor.capture(),
171+
updateWalletBalanceRequestArgumentCaptor.capture());
172+
173+
assertThat(updateWalletBalanceRequestArgumentCaptor.getValue().getAmount()).isEqualTo(100);
174+
assertThat(updateWalletBalanceRequestArgumentCaptor.getValue().getType()).isEqualTo(type);
175+
}
176+
177+
@ParameterizedTest
178+
@DisplayName("잔고 변화량이 0 이하면 BAD_REQUEST 발생")
179+
@MethodSource("provideInValidArgs")
180+
public void updateWalletBalance_illegal_amount(String request, WalletChangeType type) throws Exception {
181+
// when
182+
ResultActions perform = mockMvc.perform(MockMvcRequestBuilders
183+
.patch("/api/users/balance")
184+
.with(mockUserSecurityContext())
185+
.contentType(MediaType.APPLICATION_JSON)
186+
.content(request));
187+
188+
// then
189+
perform
190+
.andExpect(status().isBadRequest())
191+
.andExpect(jsonPath("$.errorCode").value(ErrorCode.BAD_REQUEST.getCode()));
192+
}
193+
}
110194
}

0 commit comments

Comments
 (0)