diff --git a/applications/credit-application/src/main/java/com/pda/creditapplication/controller/CreditController.java b/applications/credit-application/src/main/java/com/pda/creditapplication/controller/CreditController.java index be49f8f..2ceb949 100644 --- a/applications/credit-application/src/main/java/com/pda/creditapplication/controller/CreditController.java +++ b/applications/credit-application/src/main/java/com/pda/creditapplication/controller/CreditController.java @@ -2,9 +2,11 @@ import com.pda.apiutils.ApiUtils; import com.pda.apiutils.GlobalResponse; +import com.pda.creditapplication.controller.dto.req.TransferRequest; import com.pda.creditapplication.controller.dto.req.WithdrawRequest; import com.pda.creditapplication.service.CreditService; import com.pda.creditapplication.service.dto.req.SetAmountServiceRequest; +import com.pda.creditapplication.service.dto.req.TransferServiceRequest; import com.pda.tofinsecurity.user.AuthUser; import com.pda.tofinsecurity.user.AuthUserInfo; import io.swagger.v3.oas.annotations.Operation; @@ -14,7 +16,6 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -33,8 +34,7 @@ public class CreditController { @PostMapping("/withdraw") @Operation(summary = "크레딧 인출", description = "크레딧 인출 API", security = @SecurityRequirement(name = "bearerAuth")) - @ApiResponse(responseCode = "201", description = "성공") - @ResponseStatus(HttpStatus.CREATED) + @ApiResponse(responseCode = "200", description = "성공") public GlobalResponse withdraw(@AuthUser AuthUserInfo userInfo, @Valid @RequestBody WithdrawRequest request) { creditService.withdraw(SetAmountServiceRequest.builder() .userId(userInfo.getId()) @@ -51,4 +51,18 @@ public GlobalResponse withdraw(@AuthUser AuthUserInfo userInfo, @Valid @Re public GlobalResponse getCredit(@AuthUser AuthUserInfo userInfo) { return ApiUtils.success("크레딧 보유 현황 조회", creditService.getAmount(userInfo.getId())); } + + @PostMapping("/transfer") + @Operation(summary = "크레딧 이체", description = "크레딧 이체 API", + security = @SecurityRequirement(name = "bearerAuth")) + @ApiResponse(responseCode = "200", description = "성공") + public GlobalResponse transfer(@AuthUser AuthUserInfo userInfo, @Valid @RequestBody TransferRequest request) { + creditService.transfer(TransferServiceRequest.builder() + .fromUserId(userInfo.getId()) + .toUserId(request.getToUserId()) + .amount(request.getAmount()) + .transactionDateTime(request.getTransactionDateTime()) + .build()); + return ApiUtils.success("인출 성공"); + } } diff --git a/applications/credit-application/src/main/java/com/pda/creditapplication/controller/dto/req/TransferRequest.java b/applications/credit-application/src/main/java/com/pda/creditapplication/controller/dto/req/TransferRequest.java new file mode 100644 index 0000000..9155653 --- /dev/null +++ b/applications/credit-application/src/main/java/com/pda/creditapplication/controller/dto/req/TransferRequest.java @@ -0,0 +1,28 @@ +package com.pda.creditapplication.controller.dto.req; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.PastOrPresent; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Getter +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Builder +@Schema(description = "크레딧 전송 Request") +public class TransferRequest { + @Schema(description = "전송 목적지 유저", example = "1") + private Long toUserId; + @Min(1) + @Schema(description = "차감액(양수만 가능)", example = "100", requiredMode = Schema.RequiredMode.REQUIRED) + private Long amount; + @PastOrPresent + @Schema(description = "차감 발생 날짜/시간", example = "2024-06-19T10:41:04.172+09:00", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime transactionDateTime; +} diff --git a/applications/credit-application/src/main/java/com/pda/creditapplication/service/CreditService.java b/applications/credit-application/src/main/java/com/pda/creditapplication/service/CreditService.java index 04627a3..3833b86 100644 --- a/applications/credit-application/src/main/java/com/pda/creditapplication/service/CreditService.java +++ b/applications/credit-application/src/main/java/com/pda/creditapplication/service/CreditService.java @@ -5,6 +5,7 @@ import com.pda.creditapplication.repository.CreditStore; import com.pda.creditapplication.repository.CreditStoreRepository; import com.pda.creditapplication.service.dto.req.SetAmountServiceRequest; +import com.pda.creditapplication.service.dto.req.TransferServiceRequest; import com.pda.exceptionhandler.exceptions.BadRequestException; import lombok.RequiredArgsConstructor; import org.springframework.scheduling.annotation.Async; @@ -56,6 +57,20 @@ public void withdraw(final SetAmountServiceRequest request) { } @Transactional + public void transfer(final TransferServiceRequest request) { + withdraw(SetAmountServiceRequest.builder() + .amount(request.getAmount()) + .transactionDateTime(LocalDateTime.now()) + .userId(request.getFromUserId()) + .build()); + + deposit(SetAmountServiceRequest.builder() + .amount(request.getAmount()) + .transactionDateTime(LocalDateTime.now()) + .userId(request.getToUserId()) + .build()); + } + public Long getAmount(final Long userId) { return storeRepository.findByUserId(userId) .orElseThrow(() -> new BadRequestException("유저가 존재하지 않습니다.")).getAmount(); diff --git a/applications/credit-application/src/main/java/com/pda/creditapplication/service/dto/req/TransferServiceRequest.java b/applications/credit-application/src/main/java/com/pda/creditapplication/service/dto/req/TransferServiceRequest.java new file mode 100644 index 0000000..d0cb1af --- /dev/null +++ b/applications/credit-application/src/main/java/com/pda/creditapplication/service/dto/req/TransferServiceRequest.java @@ -0,0 +1,18 @@ +package com.pda.creditapplication.service.dto.req; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Builder +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Getter +public class TransferServiceRequest { + private Long fromUserId; + private Long toUserId; + private Long amount; + private LocalDateTime transactionDateTime; +} diff --git a/applications/user-application/src/main/java/com/pda/userapplication/configs/security/CustomSecurityConfig.java b/applications/user-application/src/main/java/com/pda/userapplication/configs/security/CustomSecurityConfig.java index 5fc7cc2..eb4e6b7 100644 --- a/applications/user-application/src/main/java/com/pda/userapplication/configs/security/CustomSecurityConfig.java +++ b/applications/user-application/src/main/java/com/pda/userapplication/configs/security/CustomSecurityConfig.java @@ -19,9 +19,15 @@ public SecurityRequestMatcherChain securityRequestMatcherChain() { .add(SecurityRequestMatcher.hasRoleOf(UserRole.ADMIN, "/test")); securityRequestMatcherChain .add(SecurityRequestMatcher.hasAnyRolesOf(List.of(UserRole.NORMAL, UserRole.FINFLUENCER), - "/users/assets", "/users/public-options", "/users/detail-info", "/users/tendency", "/users/profile")); + "/users/assets", "/users/public-options", "/users/detail-info", + "/users/tendency", "/users/profile")); + securityRequestMatcherChain + .add(SecurityRequestMatcher.hasAnyRolesOf(List.of(UserRole.NORMAL, UserRole.FINFLUENCER), + HttpMethod.POST, "/users/{id:[0-9]+}/portfolios")); securityRequestMatcherChain .add(SecurityRequestMatcher.authenticatedOf(HttpMethod.POST, "/users/{id:[0-9]+}/follow")); + securityRequestMatcherChain + .add(SecurityRequestMatcher.authenticatedOf("/users/{id:[0-9]+}/portfolios")); securityRequestMatcherChain .add(SecurityRequestMatcher.hasRoleOf(UserRole.NORMAL, HttpMethod.PUT, "/finfluencer")); diff --git a/applications/user-application/src/main/java/com/pda/userapplication/domains/PortfolioSubscribeLog.java b/applications/user-application/src/main/java/com/pda/userapplication/domains/PortfolioSubscribeLog.java new file mode 100644 index 0000000..d6c3c01 --- /dev/null +++ b/applications/user-application/src/main/java/com/pda/userapplication/domains/PortfolioSubscribeLog.java @@ -0,0 +1,26 @@ +package com.pda.userapplication.domains; + +import com.pda.userapplication.domains.vo.UserId; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Builder +public class PortfolioSubscribeLog { + private Long id; + private Integer period; + private UserId fromUserId; + private UserId toUserId; + private LocalDateTime startAt; + + public boolean isSubscribed() { + LocalDateTime now = LocalDateTime.now(); + + return now.isBefore(startAt.plusDays(period*7)); + } +} diff --git a/applications/user-application/src/main/java/com/pda/userapplication/inadapters/controllers/PortfolioController.java b/applications/user-application/src/main/java/com/pda/userapplication/inadapters/controllers/PortfolioController.java new file mode 100644 index 0000000..bf69345 --- /dev/null +++ b/applications/user-application/src/main/java/com/pda/userapplication/inadapters/controllers/PortfolioController.java @@ -0,0 +1,51 @@ +package com.pda.userapplication.inadapters.controllers; + +import com.pda.apiutils.ApiUtils; +import com.pda.apiutils.GlobalResponse; +import com.pda.tofinsecurity.user.AuthUser; +import com.pda.tofinsecurity.user.AuthUserInfo; +import com.pda.userapplication.services.in.PortfolioUseCase; +import com.pda.userapplication.services.in.dto.req.PortfolioSubscribeServiceRequest; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "Portfolio", description = "포트폴리오 API") +@RestController +@RequestMapping("/users") +@RequiredArgsConstructor +public class PortfolioController { + private final PortfolioUseCase portfolioUseCase; + + @PostMapping("/{id}/portfolios") + @Operation(summary = "포트폴리오 구독", description = "자기자신 or 돈없음 or 이미 구독 중 => X ", + security = @SecurityRequirement(name = "bearerAuth")) + @ApiResponse(responseCode = "201", description = "성공") + @ResponseStatus(HttpStatus.CREATED) + public GlobalResponse subscribe(@AuthUser AuthUserInfo authUser, @PathVariable Long id) { + portfolioUseCase.subscribe(PortfolioSubscribeServiceRequest.builder() + .myId(authUser.getId()) + .toId(id) + .token(authUser.getToken()) + .build()); + return ApiUtils.success("구독 완료"); + } + + @GetMapping("/{id}/portfolios") + @Operation(summary = "포트폴리오 조회", description = "포트폴리오 조회 API", + security = @SecurityRequirement(name = "bearerAuth")) + @ApiResponse(responseCode = "201", description = "성공") + public GlobalResponse getPortfolios(@AuthUser AuthUserInfo authUser, @PathVariable Long id) { + portfolioUseCase.getPortfolios(authUser.getId(), id); + return ApiUtils.success("포트폴리오 조회"); + } +} diff --git a/applications/user-application/src/main/java/com/pda/userapplication/inadapters/controllers/UserController.java b/applications/user-application/src/main/java/com/pda/userapplication/inadapters/controllers/UserController.java index e77f161..7e61674 100644 --- a/applications/user-application/src/main/java/com/pda/userapplication/inadapters/controllers/UserController.java +++ b/applications/user-application/src/main/java/com/pda/userapplication/inadapters/controllers/UserController.java @@ -189,14 +189,6 @@ public GlobalResponse searchByNickname( .build())); } - @GetMapping("/{id}/portfolios") - @Operation(summary = "유저 포트폴리오 조회", description = "유저 포트폴리오 조회 -> 인증 필수 아님", - security = @SecurityRequirement(name = "bearerAuth")) - @ApiResponse(responseCode = "200", description = "성공") - public GlobalResponse getPortfolios(@AuthUser AuthUserInfo authUser, @PathVariable("id") Long id) { - return ApiUtils.success("유저 포트폴리오 조회 완료"); - } - @GetMapping("/products") @Operation(summary = "유저 보유 상품 조회", description = "유저 본인의 보유 상품 조회", security = @SecurityRequirement(name = "bearerAuth")) diff --git a/applications/user-application/src/main/java/com/pda/userapplication/outadapters/api/ApiAdapter.java b/applications/user-application/src/main/java/com/pda/userapplication/outadapters/api/ApiAdapter.java index 362c225..97fd269 100644 --- a/applications/user-application/src/main/java/com/pda/userapplication/outadapters/api/ApiAdapter.java +++ b/applications/user-application/src/main/java/com/pda/userapplication/outadapters/api/ApiAdapter.java @@ -6,6 +6,7 @@ import com.pda.userapplication.domains.NormalUser; import com.pda.userapplication.services.out.CreditOutputPort; import com.pda.userapplication.services.out.GetAssetsOutputPort; +import com.pda.userapplication.services.out.dto.req.TransferCreditRequest; import com.pda.userapplication.services.out.dto.res.AssetInfoResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -50,6 +51,7 @@ public AssetInfoResponse getAssets(NormalUser normalUser) { return mono.block().getData(); } + // TODO: 이거 아직 안씀 @Override public AssetInfoResponse getAssetsExcludePortfolio(NormalUser normalUser) { AssetInfoResponse result = webClient.get().uri(uriBuilder -> uriBuilder @@ -73,7 +75,7 @@ public AssetInfoResponse getAssetsExcludePortfolio(NormalUser normalUser) { } @Override - public boolean consumeCredit(Long amount, String token) { + public void consumeCredit(Long amount, String token) { Map body = new HashMap<>(); body.put("amount", amount); body.put("transactionDateTime", LocalDateTime.now()); @@ -89,7 +91,25 @@ public boolean consumeCredit(Long amount, String token) { return response.bodyToMono(new ParameterizedTypeReference>() {}); }).block(); + } - return true; + @Override + public void transferCredit(TransferCreditRequest request) { + Map body = new HashMap<>(); + body.put("amount", request.getAmount()); + body.put("transactionDateTime", LocalDateTime.now()); + body.put("toUserId", request.getToUserId().toLong()); + + webClient.post().uri(creditUrl+"/credit/transfer") + .header("Authorization", String.format("Bearer %s", request.getToken())) + .body(BodyInserters.fromValue(body)) + .exchangeToMono(response -> { + if (!response.statusCode().is2xxSuccessful()) { + log.error("Credit Server Exception: " + response.statusCode()); + throw new BadRequestException("크레딧 이체 실패"); + } + + return response.bodyToMono(new ParameterizedTypeReference>() {}); + }).block(); } } diff --git a/applications/user-application/src/main/java/com/pda/userapplication/outadapters/jpa/entities/portfolio/SubscribeLogEntity.java b/applications/user-application/src/main/java/com/pda/userapplication/outadapters/jpa/entities/portfolio/SubscribeLogEntity.java new file mode 100644 index 0000000..8b53f5d --- /dev/null +++ b/applications/user-application/src/main/java/com/pda/userapplication/outadapters/jpa/entities/portfolio/SubscribeLogEntity.java @@ -0,0 +1,53 @@ +package com.pda.userapplication.outadapters.jpa.entities.portfolio; + +import com.pda.userapplication.outadapters.jpa.entities.BaseTimeEntity; +import com.pda.userapplication.outadapters.jpa.entities.user.UserEntity; +import jakarta.persistence.Column; +import jakarta.persistence.ConstraintMode; +import jakarta.persistence.Entity; +import jakarta.persistence.ForeignKey; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.ColumnDefault; +import org.hibernate.annotations.Comment; +import org.hibernate.annotations.DynamicInsert; +import org.hibernate.annotations.DynamicUpdate; + +@Entity +@Table(name = "SubscribeLog") +@Getter +@NoArgsConstructor +@Builder +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@DynamicInsert +@DynamicUpdate +public class SubscribeLogEntity extends BaseTimeEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + + @ColumnDefault("5") + @Comment("구독 기간(주단위 5주 통일)") + @Column(name = "period", nullable = false, updatable = false) + private Integer period; + + @ManyToOne + @JoinColumn(name = "from_user_id", nullable = false, + foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)) + private UserEntity fromUser; + + @ManyToOne + @JoinColumn(name = "to_user_id", nullable = false, + foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)) + private UserEntity toUser; +} diff --git a/applications/user-application/src/main/java/com/pda/userapplication/outadapters/jpa/entities/portfolio/SubscribeLogJpaAdapter.java b/applications/user-application/src/main/java/com/pda/userapplication/outadapters/jpa/entities/portfolio/SubscribeLogJpaAdapter.java new file mode 100644 index 0000000..60cf4fb --- /dev/null +++ b/applications/user-application/src/main/java/com/pda/userapplication/outadapters/jpa/entities/portfolio/SubscribeLogJpaAdapter.java @@ -0,0 +1,43 @@ +package com.pda.userapplication.outadapters.jpa.entities.portfolio; + +import com.pda.userapplication.domains.PortfolioSubscribeLog; +import com.pda.userapplication.domains.vo.UserId; +import com.pda.userapplication.outadapters.jpa.entities.user.UserEntity; +import com.pda.userapplication.services.out.PortfolioSubscribeOutputPort; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class SubscribeLogJpaAdapter implements PortfolioSubscribeOutputPort { + private final SubscribeLogRepository subscribeLogRepository; + + @Override + public PortfolioSubscribeLog subscribe(UserId fromUserId, UserId toUserId) { + return toDomain(subscribeLogRepository.save(SubscribeLogEntity.builder() + .fromUser(UserEntity.builder().id(fromUserId.toLong()).build()) + .toUser(UserEntity.builder().id(toUserId.toLong()).build()) + .period(5) // 5주로 통일이다. + .build()), fromUserId, toUserId); + } + + @Override + public List findSubscribeLogsBy(UserId fromUserId, UserId toUserId) { + return subscribeLogRepository.findByFromUserAndToUserOrderByIdDesc(UserEntity.builder().id(fromUserId.toLong()).build(), + UserEntity.builder().id(toUserId.toLong()).build()) + .stream().map(sub -> toDomain(sub, fromUserId, toUserId)) + .toList(); + } + + private PortfolioSubscribeLog toDomain(final SubscribeLogEntity subscribeLogEntity, UserId fromUserId, UserId toUserId) { + return PortfolioSubscribeLog.builder() + .id(subscribeLogEntity.getId()) + .period(subscribeLogEntity.getPeriod()) + .startAt(subscribeLogEntity.getCreatedAt()) + .fromUserId(fromUserId) + .toUserId(toUserId) + .build(); + } +} diff --git a/applications/user-application/src/main/java/com/pda/userapplication/outadapters/jpa/entities/portfolio/SubscribeLogRepository.java b/applications/user-application/src/main/java/com/pda/userapplication/outadapters/jpa/entities/portfolio/SubscribeLogRepository.java new file mode 100644 index 0000000..72d544f --- /dev/null +++ b/applications/user-application/src/main/java/com/pda/userapplication/outadapters/jpa/entities/portfolio/SubscribeLogRepository.java @@ -0,0 +1,12 @@ +package com.pda.userapplication.outadapters.jpa.entities.portfolio; + +import com.pda.userapplication.outadapters.jpa.entities.user.UserEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface SubscribeLogRepository extends JpaRepository { + List findByFromUserAndToUserOrderByIdDesc(UserEntity fromUser, UserEntity toUser); +} diff --git a/applications/user-application/src/main/java/com/pda/userapplication/services/FinfluencerService.java b/applications/user-application/src/main/java/com/pda/userapplication/services/FinfluencerService.java index cd37f34..1972951 100644 --- a/applications/user-application/src/main/java/com/pda/userapplication/services/FinfluencerService.java +++ b/applications/user-application/src/main/java/com/pda/userapplication/services/FinfluencerService.java @@ -47,16 +47,18 @@ public TokenInfoServiceResponse becomeFinfluencer(Long userId, String token) { log.info(String.valueOf(followInfo.getNumOfFollowers()), "fuck followers"); if (followInfo.getNumOfFollowers() < 300) throw new BadRequestException("핀플루언서로 등업하기 위해서 300명 이상의 팔로워들이 필요합니다."); - if (!creditOutputPort.consumeCredit(500L, token)) - throw new BadRequestException("크레딧 차감 실패"); + + user.setRole(UserRole.FINFLUENCER); - sendUpdateUserOutputPort.sendUserOutput(UserUpdateOutputRequest.builder() - .userId(userId) - .role(UserRole.FINFLUENCER) - .build()); User saveUser = saveUserOutputPort.save(user); + // 크레딧 차감 + creditOutputPort.consumeCredit(500L, token); + sendUpdateUserOutputPort.sendUserOutput(UserUpdateOutputRequest.builder() + .userId(userId) + .role(UserRole.FINFLUENCER) + .build()); return toTokenInfoServiceResponse( generateTokenAndSaveRefresh(saveUser), saveUser); } diff --git a/applications/user-application/src/main/java/com/pda/userapplication/services/PortfolioService.java b/applications/user-application/src/main/java/com/pda/userapplication/services/PortfolioService.java new file mode 100644 index 0000000..c51fc0b --- /dev/null +++ b/applications/user-application/src/main/java/com/pda/userapplication/services/PortfolioService.java @@ -0,0 +1,72 @@ +package com.pda.userapplication.services; + +import com.pda.exceptionhandler.exceptions.BadRequestException; +import com.pda.tofinenums.user.UserRole; +import com.pda.userapplication.domains.PortfolioSubscribeLog; +import com.pda.userapplication.domains.User; +import com.pda.userapplication.domains.vo.UserId; +import com.pda.userapplication.services.in.PortfolioUseCase; +import com.pda.userapplication.services.in.dto.req.PortfolioSubscribeServiceRequest; +import com.pda.userapplication.services.out.CreditOutputPort; +import com.pda.userapplication.services.out.PortfolioSubscribeOutputPort; +import com.pda.userapplication.services.out.ReadUserOutputPort; +import com.pda.userapplication.services.out.dto.req.TransferCreditRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +@Slf4j +@Transactional(readOnly = true) +public class PortfolioService implements PortfolioUseCase { + private final ReadUserOutputPort readUserOutputPort; + private final PortfolioSubscribeOutputPort portfolioSubscribeOutputPort; + private final CreditOutputPort creditOutputPort; + + @Override + public void getPortfolios(Long myId, Long toUserId) { + User myUser = readUserOutputPort.getUserByUserId(UserId.of(myId)); + User targetUser = readUserOutputPort.getUserByUserId(UserId.of(toUserId)); + + // TODO: 포트폴리오 처리하기 + // 포트폴리오 + // 자산연결 안되어 있으면...? + // 만약 자기 자신 거 조회면 디테일하게 + // 만약 자기 자신 것이 아니면 + // - 대상이 핀플루언서면 + // - 구독 중이면 -> 대충 보여주기 + // - 구독 중이 아니면 -> 에러 + // - 대상이 일반 유저면 -> 대충 보여주기 + } + + @Transactional + @Override + public void subscribe(final PortfolioSubscribeServiceRequest request) { + if (request.getMyId().equals(request.getToId())) throw new BadRequestException("본인 포트폴리오를 구독할 필요가 없습니다"); + + User myUser = readUserOutputPort.getUserByUserId(UserId.of(request.getMyId())); + User targetUser = readUserOutputPort.getUserByUserId(UserId.of(request.getToId())); + + if (!targetUser.getRole().equals(UserRole.FINFLUENCER)) + throw new BadRequestException("핀플루언서만 포트폴리오가 잠겨있습니다."); + + List logs = portfolioSubscribeOutputPort + .findSubscribeLogsBy(myUser.getId(), targetUser.getId()); + + logs.forEach(log -> { + if (log.isSubscribed()) throw new BadRequestException("남아있는 구독이 있습니다."); + }); + + portfolioSubscribeOutputPort.subscribe(myUser.getId(), targetUser.getId()); + creditOutputPort.transferCredit(TransferCreditRequest.builder() + .token(request.getToken()) + .amount(50L) + .toUserId(targetUser.getId()) + .token(request.getToken()) + .build()); + } +} diff --git a/applications/user-application/src/main/java/com/pda/userapplication/services/in/PortfolioUseCase.java b/applications/user-application/src/main/java/com/pda/userapplication/services/in/PortfolioUseCase.java new file mode 100644 index 0000000..8f55109 --- /dev/null +++ b/applications/user-application/src/main/java/com/pda/userapplication/services/in/PortfolioUseCase.java @@ -0,0 +1,8 @@ +package com.pda.userapplication.services.in; + +import com.pda.userapplication.services.in.dto.req.PortfolioSubscribeServiceRequest; + +public interface PortfolioUseCase { + void getPortfolios(Long myId, Long toUserId); + void subscribe(PortfolioSubscribeServiceRequest request); +} diff --git a/applications/user-application/src/main/java/com/pda/userapplication/services/in/dto/req/PortfolioSubscribeServiceRequest.java b/applications/user-application/src/main/java/com/pda/userapplication/services/in/dto/req/PortfolioSubscribeServiceRequest.java new file mode 100644 index 0000000..d6411f3 --- /dev/null +++ b/applications/user-application/src/main/java/com/pda/userapplication/services/in/dto/req/PortfolioSubscribeServiceRequest.java @@ -0,0 +1,15 @@ +package com.pda.userapplication.services.in.dto.req; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class PortfolioSubscribeServiceRequest { + private String token; + private Long myId; + private Long toId; +} diff --git a/applications/user-application/src/main/java/com/pda/userapplication/services/in/dto/res/UserServiceResponse.java b/applications/user-application/src/main/java/com/pda/userapplication/services/in/dto/res/UserServiceResponse.java index e13b1a8..d8406f3 100644 --- a/applications/user-application/src/main/java/com/pda/userapplication/services/in/dto/res/UserServiceResponse.java +++ b/applications/user-application/src/main/java/com/pda/userapplication/services/in/dto/res/UserServiceResponse.java @@ -15,25 +15,25 @@ @NoArgsConstructor(access = AccessLevel.PRIVATE) @Schema(description = "유저 Res") public class UserServiceResponse { - @Schema(name = "유저 id(PK)", example = "1") + @Schema(description = "유저 id(PK)", example = "1") private Long id; - @Schema(name = "유저 닉네임", example = "동원참치") + @Schema(description = "유저 닉네임", example = "동원참치") private String nickname; - @Schema(name = "유저 프로필 이미지", example = "유저 프로필 이미지") + @Schema(description = "유저 프로필 이미지", example = "유저 프로필 이미지") private String profileImage; - @Schema(name = "유저 투핀 아이디", example = "dongwon0103") + @Schema(description = "유저 투핀 아이디", example = "dongwon0103") private String tofinId; - @Schema(name = "유저 롤", example = "NORMAL") + @Schema(description = "유저 롤", example = "NORMAL") private UserRole role; - @Schema(name = "유저 직업", example = "대학생") + @Schema(description = "유저 직업", example = "대학생") private String job; - @Schema(name = "유저 연령대", example = "30") + @Schema(description = "유저 연령대", example = "30") private Integer ageRange; - @Schema(name = "팔로우 중 인지", example = "false") + @Schema(description = "팔로우 중 인지", example = "false") @JsonProperty("isFollow") private boolean follow; - @Schema(name = "팔로워 수", example = "4") + @Schema(description = "팔로워 수", example = "4") private Long followers; - @Schema(name = "팔로잉 수", example = "5") + @Schema(description = "팔로잉 수", example = "5") private Long followings; } diff --git a/applications/user-application/src/main/java/com/pda/userapplication/services/out/CreditOutputPort.java b/applications/user-application/src/main/java/com/pda/userapplication/services/out/CreditOutputPort.java index 9d91ae4..a9f0ee1 100644 --- a/applications/user-application/src/main/java/com/pda/userapplication/services/out/CreditOutputPort.java +++ b/applications/user-application/src/main/java/com/pda/userapplication/services/out/CreditOutputPort.java @@ -1,5 +1,8 @@ package com.pda.userapplication.services.out; +import com.pda.userapplication.services.out.dto.req.TransferCreditRequest; + public interface CreditOutputPort { - boolean consumeCredit(Long amount, String token); + void consumeCredit(Long amount, String token); + void transferCredit(TransferCreditRequest request); } diff --git a/applications/user-application/src/main/java/com/pda/userapplication/services/out/PortfolioSubscribeOutputPort.java b/applications/user-application/src/main/java/com/pda/userapplication/services/out/PortfolioSubscribeOutputPort.java new file mode 100644 index 0000000..844a672 --- /dev/null +++ b/applications/user-application/src/main/java/com/pda/userapplication/services/out/PortfolioSubscribeOutputPort.java @@ -0,0 +1,11 @@ +package com.pda.userapplication.services.out; + +import com.pda.userapplication.domains.PortfolioSubscribeLog; +import com.pda.userapplication.domains.vo.UserId; + +import java.util.List; + +public interface PortfolioSubscribeOutputPort { + PortfolioSubscribeLog subscribe(UserId fromUserId, UserId toUserId); + List findSubscribeLogsBy(UserId fromUserId, UserId toUserId); +} diff --git a/applications/user-application/src/main/java/com/pda/userapplication/services/out/dto/req/TransferCreditRequest.java b/applications/user-application/src/main/java/com/pda/userapplication/services/out/dto/req/TransferCreditRequest.java new file mode 100644 index 0000000..048b443 --- /dev/null +++ b/applications/user-application/src/main/java/com/pda/userapplication/services/out/dto/req/TransferCreditRequest.java @@ -0,0 +1,16 @@ +package com.pda.userapplication.services.out.dto.req; + +import com.pda.userapplication.domains.vo.UserId; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class TransferCreditRequest { + private Long amount; + private String token; + private UserId toUserId; +} diff --git a/applications/user-application/src/test/java/com/pda/userapplication/domains/PortfolioSubscribeLogTest.java b/applications/user-application/src/test/java/com/pda/userapplication/domains/PortfolioSubscribeLogTest.java new file mode 100644 index 0000000..7d868a4 --- /dev/null +++ b/applications/user-application/src/test/java/com/pda/userapplication/domains/PortfolioSubscribeLogTest.java @@ -0,0 +1,41 @@ +package com.pda.userapplication.domains; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; + +class PortfolioSubscribeLogTest { + + @Test + void isSubscribed() { + // given + LocalDateTime now = LocalDateTime.now(); + PortfolioSubscribeLog sub = PortfolioSubscribeLog.builder() + .period(1) + .startAt(now.minusDays(6).minusHours(23).minusMinutes(59)) + .build(); + + // when + boolean result = sub.isSubscribed(); + + // then + Assertions.assertThat(result).isTrue(); + } + + @Test + void isNotSubscribed() { + // given + LocalDateTime now = LocalDateTime.now(); + PortfolioSubscribeLog sub = PortfolioSubscribeLog.builder() + .period(1) + .startAt(now.minusDays(8)) + .build(); + + // when + boolean result = sub.isSubscribed(); + + // then + Assertions.assertThat(result).isFalse(); + } +} \ No newline at end of file diff --git a/utils/tofin-security/src/main/java/com/pda/tofinsecurity/hooks/SecurityRequestMatcher.java b/utils/tofin-security/src/main/java/com/pda/tofinsecurity/hooks/SecurityRequestMatcher.java index 2ff8a6d..00f9fc6 100644 --- a/utils/tofin-security/src/main/java/com/pda/tofinsecurity/hooks/SecurityRequestMatcher.java +++ b/utils/tofin-security/src/main/java/com/pda/tofinsecurity/hooks/SecurityRequestMatcher.java @@ -110,7 +110,7 @@ public static SecurityRequestMatcher hasAnyRolesOf(List roles, String .build(); } - public static SecurityRequestMatcher anyHasAnyRoles(List roles, HttpMethod method, String ...urls) { + public static SecurityRequestMatcher hasAnyRolesOf(List roles, HttpMethod method, String ...urls) { return SecurityRequestMatcher.builder() .requestHookType(RequestHookType.HAS_ANY_ROLE) .urls(urls) @@ -119,7 +119,7 @@ public static SecurityRequestMatcher anyHasAnyRoles(List roles, HttpMe .build(); } - public static SecurityRequestMatcher anyHasAnyRoles(List roles) { + public static SecurityRequestMatcher hasAnyRolesOf(List roles) { return SecurityRequestMatcher.builder() .requestHookType(RequestHookType.HAS_ANY_ROLE) .roles(roles)