diff --git a/src/main/java/hanium/modic/backend/domain/ai/aiChat/service/AiImagePermissionService.java b/src/main/java/hanium/modic/backend/domain/ai/aiChat/service/AiImagePermissionService.java index 38945b05..e51ee2af 100644 --- a/src/main/java/hanium/modic/backend/domain/ai/aiChat/service/AiImagePermissionService.java +++ b/src/main/java/hanium/modic/backend/domain/ai/aiChat/service/AiImagePermissionService.java @@ -1,6 +1,7 @@ package hanium.modic.backend.domain.ai.aiChat.service; import static hanium.modic.backend.common.error.ErrorCode.*; +import static hanium.modic.backend.domain.transaction.enums.HistoryType.*; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -16,6 +17,7 @@ import hanium.modic.backend.domain.post.repository.PostEntityRepository; import hanium.modic.backend.domain.ticket.service.TicketService; import hanium.modic.backend.domain.transaction.service.AccountService; +import hanium.modic.backend.domain.transaction.service.HistoryService; import hanium.modic.backend.domain.user.entity.UserEntity; import hanium.modic.backend.domain.user.repository.UserEntityRepository; import hanium.modic.backend.infra.redis.distributedLock.LockManager; @@ -27,6 +29,7 @@ public class AiImagePermissionService { private final AccountService accountService; + private final HistoryService historyService; private final TicketService ticketService; private final PostEntityRepository postRepository; private final AiChatRoomRepository aiChatRoomRepository; @@ -50,7 +53,11 @@ public void buyAiImagePermissionByCoin(Long userId, Long postId) { // 3) 코인을 원작자에게 전송, 코인 거래는 별도의 트랜잭션으로 동작하여 후처리, 예외는 전파 accountService.transferCoin(userId, post.getUserId(), post.getNonCommercialPrice()); - // 4) 알림 + // 4) 히스토리 저장 + historyService.saveTransferHistories(userId, post.getNonCommercialPrice(), POST_PURCHASE, post.getTitle(), + post.getTitle()); + + // 5) 알림 UserEntity user = userEntityRepository.findById(userId) .orElseThrow(() -> new AppException(USER_NOT_FOUND_EXCEPTION)); notificationService.createNotification( diff --git a/src/main/java/hanium/modic/backend/domain/transaction/entity/History.java b/src/main/java/hanium/modic/backend/domain/transaction/entity/History.java new file mode 100644 index 00000000..9b8dd6ec --- /dev/null +++ b/src/main/java/hanium/modic/backend/domain/transaction/entity/History.java @@ -0,0 +1,63 @@ +package hanium.modic.backend.domain.transaction.entity; + +import static jakarta.persistence.EnumType.*; + +import hanium.modic.backend.common.entity.BaseEntity; +import hanium.modic.backend.domain.transaction.enums.HistoryType; +import hanium.modic.backend.domain.transaction.enums.TransactionDirection; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +// 코인 거래에 대한 이력 관리 +@Table(name = "histories") +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class History extends BaseEntity { + + @Id + @Column(name = "id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "user_id", nullable = false) + private Long userId; + + @Column(name = "history_type", nullable = false) + @Enumerated(STRING) + private HistoryType historyType; + + @Column(name = "body", nullable = false, length = 500) + private String body; + + @Column(name = "direction", nullable = false, updatable = false) + @Enumerated(STRING) + private TransactionDirection direction; + + @Column(name = "amount", nullable = false) + private Long amount; + + @Builder + private History( + Long userId, + HistoryType historyType, + String body, + TransactionDirection direction, + Long amount + ) { + this.userId = userId; + this.historyType = historyType; + this.body = body; + this.direction = direction; + this.amount = amount; + } +} diff --git a/src/main/java/hanium/modic/backend/domain/transaction/enums/HistoryType.java b/src/main/java/hanium/modic/backend/domain/transaction/enums/HistoryType.java new file mode 100644 index 00000000..d9920e96 --- /dev/null +++ b/src/main/java/hanium/modic/backend/domain/transaction/enums/HistoryType.java @@ -0,0 +1,6 @@ +package hanium.modic.backend.domain.transaction.enums; + +public enum HistoryType { + POST_PURCHASE, + COIN_TRANSFER, +} diff --git a/src/main/java/hanium/modic/backend/domain/transaction/repository/CoinTransactionEntityRepository.java b/src/main/java/hanium/modic/backend/domain/transaction/repository/CoinTransactionEntityRepository.java index 60a3153f..8e0cdd6c 100644 --- a/src/main/java/hanium/modic/backend/domain/transaction/repository/CoinTransactionEntityRepository.java +++ b/src/main/java/hanium/modic/backend/domain/transaction/repository/CoinTransactionEntityRepository.java @@ -1,8 +1,28 @@ package hanium.modic.backend.domain.transaction.repository; +import java.time.LocalDateTime; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import hanium.modic.backend.domain.transaction.entity.CoinTransactionEntity; public interface CoinTransactionEntityRepository extends JpaRepository { + + @Query(""" + SELECT e FROM CoinTransactionEntity e + WHERE e.accountId = :accountId + AND e.effectiveAt <= :timestampA + AND (e.discardedAt IS NULL OR e.discardedAt >= :timestampA) + AND e.accountVersion <= :version + """) + Page findSnapshot( + @Param("accountId") Long accountId, + @Param("timestampA") LocalDateTime timestampA, + @Param("version") Long version, + Pageable pageable + ); } diff --git a/src/main/java/hanium/modic/backend/domain/transaction/repository/HistoryRepository.java b/src/main/java/hanium/modic/backend/domain/transaction/repository/HistoryRepository.java new file mode 100644 index 00000000..35048ae3 --- /dev/null +++ b/src/main/java/hanium/modic/backend/domain/transaction/repository/HistoryRepository.java @@ -0,0 +1,13 @@ +package hanium.modic.backend.domain.transaction.repository; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +import hanium.modic.backend.domain.transaction.entity.History; + +public interface HistoryRepository extends JpaRepository { + + // 최신 순 페이지 조회 + Page findAllByUserIdOrderByCreateAtDesc(Long userId, Pageable pageable); +} diff --git a/src/main/java/hanium/modic/backend/domain/transaction/service/AccountService.java b/src/main/java/hanium/modic/backend/domain/transaction/service/AccountService.java index f986b920..42032c7a 100644 --- a/src/main/java/hanium/modic/backend/domain/transaction/service/AccountService.java +++ b/src/main/java/hanium/modic/backend/domain/transaction/service/AccountService.java @@ -1,18 +1,29 @@ package hanium.modic.backend.domain.transaction.service; import static hanium.modic.backend.common.error.ErrorCode.*; +import static hanium.modic.backend.domain.transaction.enums.HistoryType.*; +import java.time.LocalDateTime; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import hanium.modic.backend.common.error.exception.AppException; +import hanium.modic.backend.common.response.PageResponse; import hanium.modic.backend.domain.notification.dto.NotificationPayload; import hanium.modic.backend.domain.notification.enums.NotificationType; import hanium.modic.backend.domain.notification.service.NotificationService; import hanium.modic.backend.domain.transaction.entity.Account; +import hanium.modic.backend.domain.transaction.entity.CoinTransactionEntity; import hanium.modic.backend.domain.transaction.repository.AccountRepository; +import hanium.modic.backend.domain.transaction.repository.CoinTransactionEntityRepository; import hanium.modic.backend.domain.user.entity.UserEntity; import hanium.modic.backend.domain.user.repository.UserEntityRepository; +import hanium.modic.backend.web.transaction.dto.response.GetTransactionEntityResponse; +import hanium.modic.backend.web.transaction.dto.response.GetTransactionsResponse; import hanium.modic.backend.web.transaction.dto.response.GetCoinBalanceResponse; import lombok.RequiredArgsConstructor; @@ -23,6 +34,7 @@ public class AccountService { // 계좌, 거래 관련 private final AccountRepository accountRepository; private final LedgerService ledgerService; + private final CoinTransactionEntityRepository coinTransactionEntityRepository; // 알림 관련 private final NotificationService notificationService; @@ -30,6 +42,9 @@ public class AccountService { // 유저 관련 private final UserEntityRepository userEntityRepository; + // 히스토리 관련 + private final HistoryService historyService; + // 코인 잔액 조회 public GetCoinBalanceResponse getCoinBalance(final Long userId) { Account account = accountRepository.findById(userId) @@ -46,13 +61,26 @@ public void transferCoin(final long fromUserId, final long toUserId, long coin) throw new AppException(COIN_TRANSFER_SAME_USER_EXCEPTION); } + UserEntity fromUser = userEntityRepository.findById(fromUserId) + .orElseThrow(() -> new AppException(USER_NOT_FOUND_EXCEPTION)); // 받는 사람 존재 확인 UserEntity toUser = userEntityRepository.findById(toUserId) .orElseThrow(() -> new AppException(USER_NOT_FOUND_EXCEPTION)); + // 양도 처리 ledgerService.transfer(fromUserId, toUserId, coin); + // 히스토리 저장 + historyService.saveTransferHistories( + fromUserId, + coin, + COIN_TRANSFER, + toUser.getName() + "(" + toUser.getEmail() + ")", + fromUser.getName() + "(" + fromUser.getEmail() + ")" + ); + + // 알림 notificationService.createNotification( toUserId, @@ -62,4 +90,35 @@ public void transferCoin(final long fromUserId, final long toUserId, long coin) .build() ); } + + // 코인 거래 내역 조회 + public GetTransactionsResponse getTransactions( + final long userId, + final int page, + final int size + ) { + // 현재 시점 + LocalDateTime now = LocalDateTime.now(); + + // 계좌 조회 + Account account = accountRepository.findByUserId(userId) + .orElseThrow(() -> new AppException(ACCOUNT_NOT_FOUND_EXCEPTION)); + + // 거래 내역 조회 및 응답 생성 + Pageable pageable = PageRequest.of(page, size); + Page coinTransactionEntities = coinTransactionEntityRepository.findSnapshot( + account.getId(), + now, + account.getLedgerVersion(), + pageable + ); + PageResponse pageResponse = PageResponse.of( + coinTransactionEntities.map(GetTransactionEntityResponse::from) + ); + + return new GetTransactionsResponse( + account.getPostedBalance(), + pageResponse + ); + } } diff --git a/src/main/java/hanium/modic/backend/domain/transaction/service/HistoryService.java b/src/main/java/hanium/modic/backend/domain/transaction/service/HistoryService.java new file mode 100644 index 00000000..e1d52e04 --- /dev/null +++ b/src/main/java/hanium/modic/backend/domain/transaction/service/HistoryService.java @@ -0,0 +1,84 @@ +package hanium.modic.backend.domain.transaction.service; + +import static hanium.modic.backend.common.error.ErrorCode.*; + +import java.time.LocalDateTime; +import java.util.List; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import hanium.modic.backend.common.error.exception.AppException; +import hanium.modic.backend.common.response.PageResponse; +import hanium.modic.backend.domain.transaction.entity.Account; +import hanium.modic.backend.domain.transaction.entity.History; +import hanium.modic.backend.domain.transaction.enums.HistoryType; +import hanium.modic.backend.domain.transaction.enums.TransactionDirection; +import hanium.modic.backend.domain.transaction.repository.AccountRepository; +import hanium.modic.backend.domain.transaction.repository.HistoryRepository; +import hanium.modic.backend.web.history.dto.response.GetHistoriesResponse; +import hanium.modic.backend.web.history.dto.response.GetHistoryEntityResponse; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class HistoryService { + + private final HistoryRepository historyRepository; + private final AccountRepository accountRepository; + + @Transactional + public void saveTransferHistories( + long userId, + long amount, + HistoryType historyType, + String fromBody, + String toBody + ) { + History senderHistory = History.builder() + .userId(userId) + .historyType(historyType) + .body(fromBody) + .direction(TransactionDirection.DEBIT) + .amount(amount) + .build(); + History receiverHistory = History.builder() + .userId(userId) + .historyType(historyType) + .body(toBody) + .direction(TransactionDirection.CREDIT) + .amount(amount) + .build(); + + historyRepository.saveAll(List.of(senderHistory, receiverHistory)); + } + + // 코인 거래 내역 조회 + public GetHistoriesResponse getHistories( + final long userId, + final int page, + final int size + ) { + // 계좌 조회 + Account account = accountRepository.findByUserId(userId) + .orElseThrow(() -> new AppException(ACCOUNT_NOT_FOUND_EXCEPTION)); + + // 거래 내역 조회 및 응답 생성 + Pageable pageable = PageRequest.of(page, size); + Page histories = historyRepository.findAllByUserIdOrderByCreateAtDesc( + userId, + pageable + ); + PageResponse pageResponse = PageResponse.of( + histories.map(GetHistoryEntityResponse::from) + ); + + return new GetHistoriesResponse( + account.getPostedBalance(), + pageResponse + ); + } +} diff --git a/src/main/java/hanium/modic/backend/web/history/controller/HistoryController.java b/src/main/java/hanium/modic/backend/web/history/controller/HistoryController.java new file mode 100644 index 00000000..fd492571 --- /dev/null +++ b/src/main/java/hanium/modic/backend/web/history/controller/HistoryController.java @@ -0,0 +1,50 @@ +package hanium.modic.backend.web.history.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import hanium.modic.backend.common.annotation.user.CurrentUser; +import hanium.modic.backend.common.response.AppResponse; +import hanium.modic.backend.domain.transaction.service.HistoryService; +import hanium.modic.backend.domain.user.entity.UserEntity; +import hanium.modic.backend.web.history.dto.response.GetHistoriesResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; + +@Tag(name = "거래 기록", description = "거래기록 관련 API") +@RestController +@RequiredArgsConstructor(access = AccessLevel.PROTECTED) +@RequestMapping("/api/history") +@Validated +public class HistoryController { + + private final HistoryService historyService; + + @GetMapping + @Operation( + summary = "유저 거래 내역 조회 API", + description = """ + 로그인한 유저의 코인 거래 내역 및 잔액을 조회합니다. +
+ 주의 : 유저 코인 조회 API와 순간적으로 조회 결과가 불일치할 수 있습니다. + 해당 API의 거래 내역은 해당 API의 계좌 잔액과 결과가 항상 일치합니다. + 따라서 반드시 해당 API를 통해 잔액과 거래 내역을 함께 조회하시길 권장합니다. + """ + ) + public ResponseEntity> getTransactions( + @CurrentUser UserEntity user, + @RequestParam(required = false, defaultValue = "0") @Min(value = 0, message = "페이지 번호는 0 이상이어야 합니다") Integer page, + @RequestParam(required = false, defaultValue = "10") @Min(value = 10, message = "페이지 크기는 10 이상이어야 합니다.") @Max(value = 20, message = "페이지 크기는 20 이하여야 합니다.") Integer size + ) { + GetHistoriesResponse response = historyService.getHistories(user.getId(), page, size); + return ResponseEntity.ok(AppResponse.ok(response)); + } +} diff --git a/src/main/java/hanium/modic/backend/web/history/dto/response/GetHistoriesResponse.java b/src/main/java/hanium/modic/backend/web/history/dto/response/GetHistoriesResponse.java new file mode 100644 index 00000000..5dbb040b --- /dev/null +++ b/src/main/java/hanium/modic/backend/web/history/dto/response/GetHistoriesResponse.java @@ -0,0 +1,9 @@ +package hanium.modic.backend.web.history.dto.response; + +import hanium.modic.backend.common.response.PageResponse; + +public record GetHistoriesResponse( + long coin, + PageResponse transactions +) { +} diff --git a/src/main/java/hanium/modic/backend/web/history/dto/response/GetHistoryEntityResponse.java b/src/main/java/hanium/modic/backend/web/history/dto/response/GetHistoryEntityResponse.java new file mode 100644 index 00000000..05fa9f52 --- /dev/null +++ b/src/main/java/hanium/modic/backend/web/history/dto/response/GetHistoryEntityResponse.java @@ -0,0 +1,25 @@ +package hanium.modic.backend.web.history.dto.response; + +import java.time.LocalDateTime; + +import hanium.modic.backend.domain.transaction.entity.History; +import hanium.modic.backend.domain.transaction.enums.HistoryType; +import hanium.modic.backend.domain.transaction.enums.TransactionDirection; + +public record GetHistoryEntityResponse( + HistoryType historyType, + String body, + long amount, + TransactionDirection direction, + LocalDateTime createdAt +) { + public static GetHistoryEntityResponse from(History history) { + return new GetHistoryEntityResponse( + history.getHistoryType(), + history.getBody(), + history.getAmount(), + history.getDirection(), + history.getCreateAt() + ); + } +} diff --git a/src/main/java/hanium/modic/backend/web/transaction/controller/AccountController.java b/src/main/java/hanium/modic/backend/web/transaction/controller/AccountController.java index 65abb59d..214062cd 100644 --- a/src/main/java/hanium/modic/backend/web/transaction/controller/AccountController.java +++ b/src/main/java/hanium/modic/backend/web/transaction/controller/AccountController.java @@ -7,6 +7,7 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import hanium.modic.backend.common.annotation.user.CurrentUser; @@ -14,11 +15,14 @@ import hanium.modic.backend.common.swagger.ApiErrorMapping; import hanium.modic.backend.domain.transaction.service.AccountService; import hanium.modic.backend.domain.user.entity.UserEntity; +import hanium.modic.backend.web.transaction.dto.response.GetTransactionsResponse; import hanium.modic.backend.web.transaction.dto.request.TransferCoinsRequest; import hanium.modic.backend.web.transaction.dto.response.GetCoinBalanceResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; import lombok.RequiredArgsConstructor; @Tag(name = "Account", description = "계좌 관련 API") @@ -59,4 +63,27 @@ public ResponseEntity> transferCoins( return ResponseEntity.ok().build(); } + + @GetMapping("/transactions") + @Operation( + summary = "유저 코인 거래 내역 조회 API", + description = """ + 로그인한 유저의 코인 거래 내역 및 잔액을 조회합니다. +
+ 주의 : 유저 코인 조회 API와 순간적으로 조회 결과가 불일치할 수 있습니다. + 해당 API의 거래 내역은 해당 API의 계좌 잔액과 결과가 항상 일치합니다. + 따라서 반드시 해당 API를 통해 잔액과 거래 내역을 함께 조회하시길 권장합니다. + """ + ) + @ApiErrorMapping({ + ACCOUNT_NOT_FOUND_EXCEPTION + }) + public ResponseEntity> getTransactions( + @CurrentUser UserEntity user, + @RequestParam(required = false, defaultValue = "0") @Min(value = 0, message = "페이지 번호는 0 이상이어야 합니다") Integer page, + @RequestParam(required = false, defaultValue = "10") @Min(value = 10, message = "페이지 크기는 10 이상이어야 합니다.") @Max(value = 20, message = "페이지 크기는 20 이하여야 합니다.") Integer size + ) { + GetTransactionsResponse response = accountService.getTransactions(user.getId(), page, size); + return ResponseEntity.ok(AppResponse.ok(response)); + } } diff --git a/src/main/java/hanium/modic/backend/web/transaction/dto/response/GetTransactionEntityResponse.java b/src/main/java/hanium/modic/backend/web/transaction/dto/response/GetTransactionEntityResponse.java new file mode 100644 index 00000000..9f7ca9f5 --- /dev/null +++ b/src/main/java/hanium/modic/backend/web/transaction/dto/response/GetTransactionEntityResponse.java @@ -0,0 +1,23 @@ +package hanium.modic.backend.web.transaction.dto.response; + +import java.time.LocalDateTime; + +import hanium.modic.backend.domain.transaction.entity.CoinTransactionEntity; +import hanium.modic.backend.domain.transaction.enums.TransactionDirection; +import hanium.modic.backend.domain.transaction.enums.TransactionStatus; + +public record GetTransactionEntityResponse( + TransactionStatus status, + TransactionDirection direction, + long amount, + LocalDateTime effectiveAt +) { + public static GetTransactionEntityResponse from(CoinTransactionEntity entity) { + return new GetTransactionEntityResponse( + entity.getStatus(), + entity.getDirection(), + entity.getAmount(), + entity.getEffectiveAt() + ); + } +} diff --git a/src/main/java/hanium/modic/backend/web/transaction/dto/response/GetTransactionsResponse.java b/src/main/java/hanium/modic/backend/web/transaction/dto/response/GetTransactionsResponse.java new file mode 100644 index 00000000..a1270e42 --- /dev/null +++ b/src/main/java/hanium/modic/backend/web/transaction/dto/response/GetTransactionsResponse.java @@ -0,0 +1,9 @@ +package hanium.modic.backend.web.transaction.dto.response; + +import hanium.modic.backend.common.response.PageResponse; + +public record GetTransactionsResponse( + long coin, + PageResponse transactions +) { +} diff --git a/src/test/java/hanium/modic/backend/domain/ai/service/AiImagePermissionServiceTest.java b/src/test/java/hanium/modic/backend/domain/ai/service/AiImagePermissionServiceTest.java index 8226705c..fa696240 100644 --- a/src/test/java/hanium/modic/backend/domain/ai/service/AiImagePermissionServiceTest.java +++ b/src/test/java/hanium/modic/backend/domain/ai/service/AiImagePermissionServiceTest.java @@ -25,6 +25,7 @@ import hanium.modic.backend.domain.post.repository.PostEntityRepository; import hanium.modic.backend.domain.ticket.service.TicketService; import hanium.modic.backend.domain.transaction.service.AccountService; +import hanium.modic.backend.domain.transaction.service.HistoryService; import hanium.modic.backend.domain.user.entity.UserEntity; import hanium.modic.backend.domain.user.factory.UserFactory; import hanium.modic.backend.domain.user.repository.UserEntityRepository; @@ -57,6 +58,9 @@ class AiImagePermissionServiceTest { @Mock private UserEntityRepository userEntityRepository; + @Mock + private HistoryService historyService; + @Test @DisplayName("코인으로 AI 이미지 생성권 구매 - 성공") void buyAiImagePermissionByCoin_Success() { @@ -69,6 +73,7 @@ void buyAiImagePermissionByCoin_Success() { .thenReturn(1); when(userEntityRepository.findById(anyLong())).thenReturn(Optional.of(testUser)); doNothing().when(notificationService).createNotification(anyLong(), any(), any()); + doNothing().when(historyService).saveTransferHistories(anyLong(), anyLong(), any(), any(), any()); // when aiImagePermissionService.buyAiImagePermissionByCoin(testUser.getId(), testPost.getId());