Skip to content

Commit

Permalink
Merge pull request #92 from Team-Muffin/feature/user
Browse files Browse the repository at this point in the history
[Feat] Follow / Subscribe Portfolio
  • Loading branch information
EastWon0103 authored Jun 23, 2024
2 parents 8a4cd6e + e1107bf commit 9622bc7
Show file tree
Hide file tree
Showing 22 changed files with 479 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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<Void> withdraw(@AuthUser AuthUserInfo userInfo, @Valid @RequestBody WithdrawRequest request) {
creditService.withdraw(SetAmountServiceRequest.builder()
.userId(userInfo.getId())
Expand All @@ -51,4 +51,18 @@ public GlobalResponse<Void> withdraw(@AuthUser AuthUserInfo userInfo, @Valid @Re
public GlobalResponse<Long> 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<Void> 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("인출 성공");
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"));

Expand Down
Original file line number Diff line number Diff line change
@@ -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));
}
}
Original file line number Diff line number Diff line change
@@ -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<Void> 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<Void> getPortfolios(@AuthUser AuthUserInfo authUser, @PathVariable Long id) {
portfolioUseCase.getPortfolios(authUser.getId(), id);
return ApiUtils.success("포트폴리오 조회");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -189,14 +189,6 @@ public GlobalResponse<GetUserPagingResponse> searchByNickname(
.build()));
}

@GetMapping("/{id}/portfolios")
@Operation(summary = "유저 포트폴리오 조회", description = "유저 포트폴리오 조회 -> 인증 필수 아님",
security = @SecurityRequirement(name = "bearerAuth"))
@ApiResponse(responseCode = "200", description = "성공")
public GlobalResponse<Void> getPortfolios(@AuthUser AuthUserInfo authUser, @PathVariable("id") Long id) {
return ApiUtils.success("유저 포트폴리오 조회 완료");
}

@GetMapping("/products")
@Operation(summary = "유저 보유 상품 조회", description = "유저 본인의 보유 상품 조회",
security = @SecurityRequirement(name = "bearerAuth"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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<String, Object> body = new HashMap<>();
body.put("amount", amount);
body.put("transactionDateTime", LocalDateTime.now());
Expand All @@ -89,7 +91,25 @@ public boolean consumeCredit(Long amount, String token) {

return response.bodyToMono(new ParameterizedTypeReference<GlobalResponse<Void>>() {});
}).block();
}

return true;
@Override
public void transferCredit(TransferCreditRequest request) {
Map<String, Object> 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<GlobalResponse<Void>>() {});
}).block();
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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<PortfolioSubscribeLog> 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();
}
}
Original file line number Diff line number Diff line change
@@ -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<SubscribeLogEntity, Long> {
List<SubscribeLogEntity> findByFromUserAndToUserOrderByIdDesc(UserEntity fromUser, UserEntity toUser);
}
Loading

0 comments on commit 9622bc7

Please sign in to comment.