Skip to content

Commit

Permalink
Feat: 포인트 기능 (#54)
Browse files Browse the repository at this point in the history
* feat: 새로운 멤버 생성시 멤버에 포인트 연관 시킨다

* fix: 실수로 이상하게 머지된것 정상적으로 수정

* build: 타임리프 의존성 추가

* feat: 포인트 조회 기능

* feat: 포인트 로그 조회 기능

- 페이징으로 조회

* feat: 포인트 충전 기능 구현

- 관리자 페이지 구현
- 테스트 구현

* feat: 콜백, 안부전화 완료로 인한 시니또 포인트 적립 기능

* feat: 서비스 요청시 포인트 차감 관련 기능과 요청 취소시 차감된거 원복 기능 구현

- 포인트 값 변경을 위한 조회시 비관적 락 걸음

* fix: 머지후 수정사항

* feat: 포인트 적립 완료 상태 변화와 동시에 적립이 되도록하는 기능 추가

* refactor: 포인트 충전 관리자 페이지 path 경로 추가

- css html 분리

* feat: 충전 요청 시 이름+번호4자리를 입금 메시지로 적도록 수정, 충전 실패 상태 추가

* feat: 포인트 출금 요청
- 관리자 페이지 추가

* refactor: 안부전화 삭제 서비스로직 논리적 순서로 개선, 안쓰는 클래스 제거, 예외 처리 추가

* feat: 포인트 로그 전체 조회할 때 최신것부터 소팅하고, 상태도 모두 포함하도록 변경

* refactor: 포인트 충전 관련 메서드명 개선

* test: 포인트 로그 엔티티 테스트 추가

* feat: 출금 할때 수수료 적용

* fix: 치명적 PointLogRepository 오타 수정

* feat: Point 엔티티에 deduct() 메서드내부 로직에 보유한 포인트보다 더 많은 포인트 차감 요청에 대한 예외 추가
  • Loading branch information
zzoe2346 authored Oct 12, 2024
1 parent 7708ed4 commit f829b7d
Show file tree
Hide file tree
Showing 32 changed files with 1,285 additions and 46 deletions.
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ dependencies {
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.2'
implementation group: 'com.twilio.sdk', name: 'twilio', version: '10.5.0'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,35 @@
import com.example.sinitto.member.entity.Member;
import com.example.sinitto.member.entity.Senior;
import com.example.sinitto.member.repository.MemberRepository;
import com.example.sinitto.point.entity.Point;
import com.example.sinitto.point.entity.PointLog;
import com.example.sinitto.point.exception.PointNotFoundException;
import com.example.sinitto.point.repository.PointLogRepository;
import com.example.sinitto.point.repository.PointRepository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;

@Service
public class CallbackService {

public static final int CALLBACK_PRICE = 1500;
private static final String SUCCESS_MESSAGE = "감사합니다. 잠시만 기다려주세요.";
private static final String FAIL_MESSAGE = "등록된 사용자가 아닙니다. 서비스 이용이 불가합니다.";
private static final String FAIL_MESSAGE_NOT_ENROLLED = "등록된 사용자가 아닙니다. 서비스 이용이 불가합니다.";
private static final String FAIL_MESSAGE_NOT_ENOUGH_POINT = "포인트가 부족합니다. 서비스 이용이 불가합니다.";
private final CallbackRepository callbackRepository;
private final MemberRepository memberRepository;
private final SeniorRepository seniorRepository;
private final PointRepository pointRepository;
private final PointLogRepository pointLogRepository;

public CallbackService(CallbackRepository callbackRepository, MemberRepository memberRepository, SeniorRepository seniorRepository) {
public CallbackService(CallbackRepository callbackRepository, MemberRepository memberRepository, SeniorRepository seniorRepository, PointRepository pointRepository, PointLogRepository pointLogRepository) {
this.callbackRepository = callbackRepository;
this.memberRepository = memberRepository;
this.seniorRepository = seniorRepository;
this.pointRepository = pointRepository;
this.pointLogRepository = pointLogRepository;
}

@Transactional(readOnly = true)
Expand Down Expand Up @@ -75,6 +84,11 @@ public void complete(Long memberId, Long callbackId) {
throw new GuardMismatchException("이 API를 요청한 보호자는 이 콜백을 요청 한 시니어의 보호자가 아닙니다.");
}

Point sinittoPoint = pointRepository.findByMemberId(callback.getAssignedMemberId())
.orElseThrow(() -> new PointNotFoundException("포인트 적립 받을 시니또와 연관된 포인트가 없습니다"));
sinittoPoint.earn(CALLBACK_PRICE);
pointLogRepository.save(new PointLog(PointLog.Content.COMPLETE_CALLBACK_AND_EARN.getMessage(), sinittoPoint.getMember(), CALLBACK_PRICE, PointLog.Status.EARN));

callback.changeStatusToComplete();
}

Expand All @@ -91,22 +105,45 @@ public void cancel(Long memberId, Long callbackId) {
callback.changeStatusToWaiting();
}

@Transactional
public String add(String fromNumber) {

String phoneNumber = TwilioHelper.trimPhoneNumber(fromNumber);

Optional<Senior> seniorOptional = seniorRepository.findByPhoneNumber(phoneNumber);
Senior senior = findSeniorByPhoneNumber(phoneNumber);
if (senior == null) {
return TwilioHelper.convertMessageToTwiML(FAIL_MESSAGE_NOT_ENROLLED);
}

if (seniorOptional.isEmpty()) {
return TwilioHelper.convertMessageToTwiML(FAIL_MESSAGE);
Point point = findPointWithWriteLock(senior.getMember().getId());
if (point == null || !point.isSufficientForDeduction(CALLBACK_PRICE)) {
return TwilioHelper.convertMessageToTwiML(FAIL_MESSAGE_NOT_ENOUGH_POINT);
}

Senior senior = seniorOptional.get();
point.deduct(CALLBACK_PRICE);

pointLogRepository.save(
new PointLog(
PointLog.Content.SPEND_COMPLETE_CALLBACK.getMessage(),
senior.getMember(),
CALLBACK_PRICE,
PointLog.Status.SPEND_COMPLETE)
);
callbackRepository.save(new Callback(Callback.Status.WAITING, senior));

return TwilioHelper.convertMessageToTwiML(SUCCESS_MESSAGE);
}

private Senior findSeniorByPhoneNumber(String phoneNumber) {
return seniorRepository.findByPhoneNumber(phoneNumber)
.orElse(null);
}

private Point findPointWithWriteLock(Long memberId) {
return pointRepository.findByMemberIdWithWriteLock(memberId)
.orElse(null);
}

public CallbackResponse getAcceptedCallback(Long memberId) {

checkAuthorization(memberId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@
import com.example.sinitto.member.entity.Sinitto;
import com.example.sinitto.member.exception.MemberNotFoundException;
import com.example.sinitto.member.repository.MemberRepository;
import com.example.sinitto.point.entity.Point;
import com.example.sinitto.point.entity.PointLog;
import com.example.sinitto.point.exception.NotEnoughPointException;
import com.example.sinitto.point.exception.PointNotFoundException;
import com.example.sinitto.point.repository.PointLogRepository;
import com.example.sinitto.point.repository.PointRepository;
import com.example.sinitto.sinitto.exception.SinittoNotFoundException;
import com.example.sinitto.sinitto.repository.SinittoRepository;
import org.springframework.data.domain.Page;
Expand All @@ -41,17 +47,21 @@ public class HelloCallService {
private final MemberRepository memberRepository;
private final SinittoRepository sinittoRepository;
private final HelloCallTimeLogRepository helloCallTimeLogRepository;
private final PointRepository pointRepository;
private final PointLogRepository pointLogRepository;


public HelloCallService(HelloCallRepository helloCallRepository, TimeSlotRepository timeSlotRepository,
SeniorRepository seniorRepository, MemberRepository memberRepository, SinittoRepository sinittoRepository,
HelloCallTimeLogRepository helloCallTimeLogRepository) {
HelloCallTimeLogRepository helloCallTimeLogRepository, PointRepository pointRepository, PointLogRepository pointLogRepository) {
this.helloCallRepository = helloCallRepository;
this.timeSlotRepository = timeSlotRepository;
this.seniorRepository = seniorRepository;
this.memberRepository = memberRepository;
this.sinittoRepository = sinittoRepository;
this.helloCallTimeLogRepository = helloCallTimeLogRepository;
this.pointRepository = pointRepository;
this.pointLogRepository = pointLogRepository;
}

@Transactional
Expand All @@ -72,6 +82,23 @@ public void createHelloCallByGuard(Long memberId, HelloCallRequest helloCallRequ
timeSlotRequest.endTime(), savedHelloCall);
timeSlotRepository.save(timeSlot);
}

Point point = pointRepository.findByMemberIdWithWriteLock(memberId)
.orElseThrow(() -> new PointNotFoundException("멤버에 연관된 포인트가 없습니다."));

if (!point.isSufficientForDeduction(helloCall.getPrice())) {
throw new NotEnoughPointException("포인트가 부족합니다.");
}

point.deduct(helloCall.getPrice());

pointLogRepository.save(
new PointLog(
PointLog.Content.SPEND_COMPLETE_HELLO_CALL.getMessage(),
senior.getMember(),
helloCall.getPrice(),
PointLog.Status.SPEND_COMPLETE
));
}

@Transactional
Expand Down Expand Up @@ -165,6 +192,19 @@ public void deleteHellCallByGuard(Long memberId, Long helloCallId) {
throw new UnauthorizedException("안부전화 신청을 취소할 권한이 없습니다.");
}

Point point = pointRepository.findByMemberIdWithWriteLock(memberId)
.orElseThrow(() -> new PointNotFoundException("멤버에 연관된 포인트가 없습니다."));

point.earn(helloCall.getPrice());

pointLogRepository.save(
new PointLog(
PointLog.Content.SPEND_CANCEL_HELLO_CALL.getMessage(),
member,
helloCall.getPrice(),
PointLog.Status.SPEND_CANCEL)
);

helloCall.checkStatusIsWaiting();
helloCallRepository.delete(helloCall);
}
Expand Down Expand Up @@ -224,11 +264,19 @@ public void makeCompleteHelloCallByGuard(Long memberId, Long helloCallId) {

helloCall.changeStatusToComplete();

Sinitto earnedSinitto = helloCall.getSinitto();
Point sinittoPoint = pointRepository.findByMember(helloCall.getSinitto().getMember())
.orElseThrow(() -> new PointNotFoundException("포인트 적립 받을 시니또와 연관된 포인트가 없습니다"));

//earnedSinitto에게 포인트 지급 로직 필요합니다.
}
sinittoPoint.earn(helloCall.getPrice());

pointLogRepository.save(
new PointLog(
PointLog.Content.COMPLETE_HELLO_CALL_AND_EARN.getMessage(),
sinittoPoint.getMember(),
helloCall.getPrice(),
PointLog.Status.EARN)
);
}

@Transactional(readOnly = true)
public List<HelloCallReportResponse> readAllHelloCallReportByAdmin() {
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/com/example/sinitto/member/entity/Member.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ public void updateMember(String name, String email, String phoneNumber) {
this.phoneNumber = phoneNumber;
}

public String getDepositMessage() {

return name.substring(0, 3) + phoneNumber.substring(phoneNumber.length() - 4);
}

public Long getId() {
return id;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
import com.example.sinitto.member.exception.MemberNotFoundException;
import com.example.sinitto.member.exception.NotUniqueException;
import com.example.sinitto.member.repository.MemberRepository;
import com.example.sinitto.point.entity.Point;
import com.example.sinitto.point.repository.PointRepository;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

Expand All @@ -24,15 +26,15 @@ public class MemberService implements MemberIdProvider {
private final TokenService tokenService;
private final KakaoApiService kakaoApiService;
private final KakaoTokenService kakaoTokenService;
private final PointRepository pointRepository;
private final RedisTemplate<String, String> redisTemplate;


public MemberService(MemberRepository memberRepository, TokenService tokenService, KakaoApiService kakaoApiService, KakaoTokenService kakaoTokenService
,RedisTemplate<String, String> redisTemplate) {
public MemberService(MemberRepository memberRepository, TokenService tokenService, KakaoApiService kakaoApiService, KakaoTokenService kakaoTokenService, PointRepository pointRepository, RedisTemplate<String, String> redisTemplate) {
this.memberRepository = memberRepository;
this.tokenService = tokenService;
this.kakaoApiService = kakaoApiService;
this.kakaoTokenService = kakaoTokenService;
this.pointRepository = pointRepository;
this.redisTemplate = redisTemplate;
}

Expand Down Expand Up @@ -75,6 +77,8 @@ public RegisterResponse registerNewMember(String name, String phoneNumber, Strin
Member newMember = new Member(name, phoneNumber, email, isSinitto);
memberRepository.save(newMember);

pointRepository.save(new Point(0, newMember));

String accessToken = tokenService.generateAccessToken(email);
String refreshToken = tokenService.generateRefreshToken(email);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package com.example.sinitto.point.controller;

import com.example.sinitto.member.entity.Member;
import com.example.sinitto.member.entity.Sinitto;
import com.example.sinitto.member.exception.MemberNotFoundException;
import com.example.sinitto.member.repository.MemberRepository;
import com.example.sinitto.point.dto.PointLogWithBankInfo;
import com.example.sinitto.point.dto.PointLogWithDepositMessage;
import com.example.sinitto.point.entity.PointLog;
import com.example.sinitto.point.service.PointAdminService;
import com.example.sinitto.sinitto.exception.SinittoNotFoundException;
import com.example.sinitto.sinitto.repository.SinittoRepository;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;

import java.util.ArrayList;
import java.util.List;

@Controller
public class PointAdminController {

private final PointAdminService pointAdminService;
private final MemberRepository memberRepository;
private final SinittoRepository sinittoRepository;

public PointAdminController(PointAdminService pointAdminService, MemberRepository memberRepository, SinittoRepository sinittoRepository) {
this.pointAdminService = pointAdminService;
this.memberRepository = memberRepository;
this.sinittoRepository = sinittoRepository;
}

@GetMapping("/admin/point/charge")
public String showAllChargeRequest(Model model) {

List<PointLog> pointLogs = pointAdminService.readAllNotCompletedPointChargeRequest();
List<PointLogWithDepositMessage> logWithDepositMessages = new ArrayList<>();

for (PointLog pointLog : pointLogs) {
Member member = memberRepository.findById(pointLog.getMember().getId())
.orElseThrow(() -> new MemberNotFoundException("멤버를 찾을 수 없습니다"));

PointLogWithDepositMessage pointLogWithDepositMessage = new PointLogWithDepositMessage(
pointLog.getId(),
pointLog.getPrice(),
pointLog.getPostTime(),
pointLog.getStatus(),
member.getDepositMessage()
);

logWithDepositMessages.add(pointLogWithDepositMessage);
}
model.addAttribute("logWithDepositMessages", logWithDepositMessages);

return "/point/charge";
}

@PostMapping("/admin/point/charge/waiting/{pointLogId}")
public String changeToWaiting(@PathVariable Long pointLogId) {

pointAdminService.changeChargeLogToWaiting(pointLogId);
return "redirect:/admin/point/charge";
}

@PostMapping("/admin/point/charge/complete/{pointLogId}")
public String changeToCompleteAndEarn(@PathVariable Long pointLogId) {

pointAdminService.earnPointAndChangeToChargeComplete(pointLogId);
return "redirect:/admin/point/charge";
}

@PostMapping("/admin/point/charge/fail/{pointLogId}")
public String changeToFail(@PathVariable Long pointLogId) {

pointAdminService.changeChargeLogToFail(pointLogId);
return "redirect:/admin/point/charge";
}

@GetMapping("/admin/point/withdraw")
public String showAllWithdrawRequest(Model model) {

List<PointLog> pointLogs = pointAdminService.readAllPointWithdrawRequest();
List<PointLogWithBankInfo> logWithBankInfos = new ArrayList<>();

for (PointLog pointLog : pointLogs) {
Sinitto sinitto = sinittoRepository.findByMemberId(pointLog.getMember().getId())
.orElseThrow(() -> new SinittoNotFoundException("시니또를 찾을 수 없습니다"));

PointLogWithBankInfo pointLogWithBankInfo = new PointLogWithBankInfo(
pointLog.getId(),
pointLog.getPrice(),
pointLog.getPostTime(),
pointLog.getStatus(),
sinitto.getBankName(),
sinitto.getAccountNumber()
);

logWithBankInfos.add(pointLogWithBankInfo);
}
model.addAttribute("logWithBankInfos", logWithBankInfos);

return "/point/withdraw";
}

@PostMapping("/admin/point/withdraw/waiting/{pointLogId}")
public String changeWithdrawLogToWaiting(@PathVariable Long pointLogId) {

pointAdminService.changeWithdrawLogToWaiting(pointLogId);
return "redirect:/admin/point/withdraw";
}

@PostMapping("/admin/point/withdraw/complete/{pointLogId}")
public String changeWithdrawLogToCompleteAndEarn(@PathVariable Long pointLogId) {

pointAdminService.changeWithdrawLogToComplete(pointLogId);
return "redirect:/admin/point/withdraw";
}

@PostMapping("/admin/point/withdraw/fail/{pointLogId}")
public String changeWithdrawLogToFail(@PathVariable Long pointLogId) {

pointAdminService.changeWithdrawLogToFail(pointLogId);
return "redirect:/admin/point/withdraw";
}

}
Loading

0 comments on commit f829b7d

Please sign in to comment.