diff --git a/src/main/java/com/weeth/domain/account/application/dto/AccountDTO.java b/src/main/java/com/weeth/domain/account/application/dto/AccountDTO.java deleted file mode 100644 index a9794584..00000000 --- a/src/main/java/com/weeth/domain/account/application/dto/AccountDTO.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.weeth.domain.account.application.dto; - -import jakarta.validation.constraints.NotNull; - -import java.time.LocalDateTime; -import java.util.List; - -public class AccountDTO { - - public record Response( - Long accountId, - String description, - Integer totalAmount, - Integer currentAmount, - LocalDateTime time, - Integer cardinal, - List receipts - ) {} - - public record Save( - String description, - @NotNull Integer totalAmount, - @NotNull Integer cardinal - ) {} -} diff --git a/src/main/java/com/weeth/domain/account/application/dto/ReceiptDTO.java b/src/main/java/com/weeth/domain/account/application/dto/ReceiptDTO.java deleted file mode 100644 index 71ff3974..00000000 --- a/src/main/java/com/weeth/domain/account/application/dto/ReceiptDTO.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.weeth.domain.account.application.dto; - -import jakarta.validation.Valid; -import jakarta.validation.constraints.NotNull; -import com.weeth.domain.file.application.dto.request.FileSaveRequest; -import com.weeth.domain.file.application.dto.response.FileResponse; - -import java.time.LocalDate; -import java.util.List; - -public class ReceiptDTO { - - public record Response( - Long id, - String description, - String source, - Integer amount, - LocalDate date, - List fileUrls - ) { - } - - public record Save( - String description, - String source, - @NotNull Integer amount, - @NotNull LocalDate date, - @NotNull Integer cardinal, - @Valid List<@NotNull FileSaveRequest> files - ) { - } - - public record Update( - String description, - String source, - @NotNull Integer amount, - @NotNull LocalDate date, - @NotNull Integer cardinal, - @Valid List<@NotNull FileSaveRequest> files - ) { - } -} diff --git a/src/main/java/com/weeth/domain/account/application/exception/AccountErrorCode.java b/src/main/java/com/weeth/domain/account/application/exception/AccountErrorCode.java deleted file mode 100644 index d694c551..00000000 --- a/src/main/java/com/weeth/domain/account/application/exception/AccountErrorCode.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.weeth.domain.account.application.exception; - -import com.weeth.global.common.exception.ErrorCodeInterface; -import com.weeth.global.common.exception.ExplainError; -import lombok.AllArgsConstructor; -import lombok.Getter; -import org.springframework.http.HttpStatus; - -@Getter -@AllArgsConstructor -public enum AccountErrorCode implements ErrorCodeInterface { - - @ExplainError("요청한 회비 장부 ID가 존재하지 않을 때 발생합니다.") - ACCOUNT_NOT_FOUND(2100, HttpStatus.NOT_FOUND, "존재하지 않는 장부입니다."), - - @ExplainError("이미 존재하는 장부를 중복 생성하려고 할 때 발생합니다.") - ACCOUNT_EXISTS(2101, HttpStatus.BAD_REQUEST, "이미 생성된 장부입니다."), - - @ExplainError("요청한 영수증 내역이 존재하지 않을 때 발생합니다.") - RECEIPT_NOT_FOUND(2102, HttpStatus.NOT_FOUND, "존재하지 않는 내역입니다."); - - private final int code; - private final HttpStatus status; - private final String message; -} diff --git a/src/main/java/com/weeth/domain/account/application/exception/AccountExistsException.java b/src/main/java/com/weeth/domain/account/application/exception/AccountExistsException.java deleted file mode 100644 index 9e6ed8b5..00000000 --- a/src/main/java/com/weeth/domain/account/application/exception/AccountExistsException.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.weeth.domain.account.application.exception; - -import com.weeth.global.common.exception.BaseException; - -public class AccountExistsException extends BaseException { - public AccountExistsException() { - super(AccountErrorCode.ACCOUNT_EXISTS); - } -} - diff --git a/src/main/java/com/weeth/domain/account/application/exception/AccountNotFoundException.java b/src/main/java/com/weeth/domain/account/application/exception/AccountNotFoundException.java deleted file mode 100644 index 2e480f40..00000000 --- a/src/main/java/com/weeth/domain/account/application/exception/AccountNotFoundException.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.weeth.domain.account.application.exception; - -import com.weeth.global.common.exception.BaseException; - -public class AccountNotFoundException extends BaseException { - public AccountNotFoundException() { - super(AccountErrorCode.ACCOUNT_NOT_FOUND); - } -} diff --git a/src/main/java/com/weeth/domain/account/application/exception/ReceiptNotFoundException.java b/src/main/java/com/weeth/domain/account/application/exception/ReceiptNotFoundException.java deleted file mode 100644 index ac11d282..00000000 --- a/src/main/java/com/weeth/domain/account/application/exception/ReceiptNotFoundException.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.weeth.domain.account.application.exception; - -import com.weeth.global.common.exception.BaseException; - -public class ReceiptNotFoundException extends BaseException { - public ReceiptNotFoundException() { - super(AccountErrorCode.RECEIPT_NOT_FOUND); - } -} diff --git a/src/main/java/com/weeth/domain/account/application/mapper/AccountMapper.java b/src/main/java/com/weeth/domain/account/application/mapper/AccountMapper.java deleted file mode 100644 index f428cc88..00000000 --- a/src/main/java/com/weeth/domain/account/application/mapper/AccountMapper.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.weeth.domain.account.application.mapper; - -import com.weeth.domain.account.application.dto.AccountDTO; -import com.weeth.domain.account.application.dto.ReceiptDTO; -import com.weeth.domain.account.domain.entity.Account; -import org.mapstruct.*; - -import java.util.List; - -@Mapper(componentModel = MappingConstants.ComponentModel.SPRING, unmappedTargetPolicy = ReportingPolicy.IGNORE) -public interface AccountMapper { - - @Mapping(target = "accountId", source = "account.id") - @Mapping(target = "receipts", source = "receipts") - @Mapping(target = "time", source = "account.modifiedAt") - AccountDTO.Response to(Account account, List receipts); - - @Mapping(target = "currentAmount", source = "totalAmount") - Account from(AccountDTO.Save dto); -} diff --git a/src/main/java/com/weeth/domain/account/application/mapper/ReceiptMapper.java b/src/main/java/com/weeth/domain/account/application/mapper/ReceiptMapper.java deleted file mode 100644 index c2a8f8d7..00000000 --- a/src/main/java/com/weeth/domain/account/application/mapper/ReceiptMapper.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.weeth.domain.account.application.mapper; - -import com.weeth.domain.account.application.dto.ReceiptDTO; -import com.weeth.domain.account.domain.entity.Account; -import com.weeth.domain.account.domain.entity.Receipt; -import com.weeth.domain.file.application.dto.response.FileResponse; -import org.mapstruct.Mapper; -import org.mapstruct.Mapping; -import org.mapstruct.MappingConstants; -import org.mapstruct.ReportingPolicy; - -import java.util.List; - -@Mapper(componentModel = MappingConstants.ComponentModel.SPRING, unmappedTargetPolicy = ReportingPolicy.IGNORE) -public interface ReceiptMapper { - - List to(List account); - - ReceiptDTO.Response to(Receipt receipt, List fileUrls); - - @Mapping(target = "id", ignore = true) - @Mapping(target = "description", source = "dto.description") - @Mapping(target = "account", source = "account") - Receipt from(ReceiptDTO.Save dto, Account account); -} diff --git a/src/main/java/com/weeth/domain/account/application/usecase/AccountUseCase.java b/src/main/java/com/weeth/domain/account/application/usecase/AccountUseCase.java deleted file mode 100644 index a9eb972b..00000000 --- a/src/main/java/com/weeth/domain/account/application/usecase/AccountUseCase.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.weeth.domain.account.application.usecase; - -import com.weeth.domain.account.application.dto.AccountDTO; - -public interface AccountUseCase { - AccountDTO.Response find(Integer cardinal); - - void save(AccountDTO.Save dto); -} diff --git a/src/main/java/com/weeth/domain/account/application/usecase/AccountUseCaseImpl.java b/src/main/java/com/weeth/domain/account/application/usecase/AccountUseCaseImpl.java deleted file mode 100644 index d2d9ca00..00000000 --- a/src/main/java/com/weeth/domain/account/application/usecase/AccountUseCaseImpl.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.weeth.domain.account.application.usecase; - -import com.weeth.domain.account.application.dto.AccountDTO; -import com.weeth.domain.account.application.dto.ReceiptDTO; -import com.weeth.domain.account.application.exception.AccountExistsException; -import com.weeth.domain.account.application.mapper.AccountMapper; -import com.weeth.domain.account.application.mapper.ReceiptMapper; -import com.weeth.domain.account.domain.entity.Account; -import com.weeth.domain.account.domain.entity.Receipt; -import com.weeth.domain.account.domain.service.AccountGetService; -import com.weeth.domain.account.domain.service.AccountSaveService; -import com.weeth.domain.account.domain.service.ReceiptGetService; -import com.weeth.domain.file.application.dto.response.FileResponse; -import com.weeth.domain.file.application.mapper.FileMapper; -import com.weeth.domain.file.domain.entity.FileOwnerType; -import com.weeth.domain.file.domain.repository.FileReader; -import com.weeth.domain.user.domain.service.CardinalGetService; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; - -@Service -@RequiredArgsConstructor -public class AccountUseCaseImpl implements AccountUseCase { - - private final AccountGetService accountGetService; - private final AccountSaveService accountSaveService; - private final ReceiptGetService receiptGetService; - private final FileReader fileReader; - private final CardinalGetService cardinalGetService; - - private final AccountMapper accountMapper; - private final ReceiptMapper receiptMapper; - private final FileMapper fileMapper; - - @Override - public AccountDTO.Response find(Integer cardinal) { - Account account = accountGetService.find(cardinal); - List receipts = receiptGetService.findAllByAccountId(account.getId()); - List response = receipts.stream() - .map(receipt -> receiptMapper.to(receipt, getFiles(receipt.getId()))) - .toList(); - - return accountMapper.to(account, response); - } - - @Override - @Transactional - public void save(AccountDTO.Save dto) { - validate(dto); - cardinalGetService.findByAdminSide(dto.cardinal()); - - accountSaveService.save(accountMapper.from(dto)); - } - - private void validate(AccountDTO.Save dto) { - if (accountGetService.validate(dto.cardinal())) - throw new AccountExistsException(); - } - - private List getFiles(Long receiptId) { - return fileReader.findAll(FileOwnerType.RECEIPT, receiptId, null).stream() - .map(fileMapper::toFileResponse) - .toList(); - } -} diff --git a/src/main/java/com/weeth/domain/account/application/usecase/ReceiptUseCase.java b/src/main/java/com/weeth/domain/account/application/usecase/ReceiptUseCase.java deleted file mode 100644 index 855a24a2..00000000 --- a/src/main/java/com/weeth/domain/account/application/usecase/ReceiptUseCase.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.weeth.domain.account.application.usecase; - -import com.weeth.domain.account.application.dto.ReceiptDTO; - -public interface ReceiptUseCase { - void save(ReceiptDTO.Save dto); - - void update(Long receiptId, ReceiptDTO.Update dto); - - void delete(Long id); -} diff --git a/src/main/java/com/weeth/domain/account/application/usecase/ReceiptUseCaseImpl.java b/src/main/java/com/weeth/domain/account/application/usecase/ReceiptUseCaseImpl.java deleted file mode 100644 index 0a646340..00000000 --- a/src/main/java/com/weeth/domain/account/application/usecase/ReceiptUseCaseImpl.java +++ /dev/null @@ -1,85 +0,0 @@ -package com.weeth.domain.account.application.usecase; - -import jakarta.transaction.Transactional; -import com.weeth.domain.account.application.dto.ReceiptDTO; -import com.weeth.domain.account.application.mapper.ReceiptMapper; -import com.weeth.domain.account.domain.entity.Account; -import com.weeth.domain.account.domain.entity.Receipt; -import com.weeth.domain.account.domain.service.*; -import com.weeth.domain.file.application.mapper.FileMapper; -import com.weeth.domain.file.domain.entity.File; -import com.weeth.domain.file.domain.entity.FileOwnerType; -import com.weeth.domain.file.domain.repository.FileReader; -import com.weeth.domain.file.domain.repository.FileRepository; -import com.weeth.domain.user.domain.service.CardinalGetService; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -import java.util.List; - -@Service -@RequiredArgsConstructor -public class ReceiptUseCaseImpl implements ReceiptUseCase { - - private final ReceiptGetService receiptGetService; - private final ReceiptDeleteService receiptDeleteService; - private final ReceiptSaveService receiptSaveService; - private final ReceiptUpdateService receiptUpdateService; - private final AccountGetService accountGetService; - - private final FileReader fileReader; - private final FileRepository fileRepository; - - private final CardinalGetService cardinalGetService; - - private final ReceiptMapper mapper; - private final FileMapper fileMapper; - - - @Override - @Transactional - public void save(ReceiptDTO.Save dto) { - cardinalGetService.findByAdminSide(dto.cardinal()); - - Account account = accountGetService.find(dto.cardinal()); - Receipt receipt = receiptSaveService.save(mapper.from(dto, account)); - account.spend(receipt); - - List files = fileMapper.toFileList(dto.files(), FileOwnerType.RECEIPT, receipt.getId()); - fileRepository.saveAll(files); - } - - @Override - @Transactional - public void update(Long receiptId, ReceiptDTO.Update dto){ - Account account = accountGetService.find(dto.cardinal()); - Receipt receipt = receiptGetService.find(receiptId); - account.cancel(receipt); - - if(!dto.files().isEmpty()){ // 업데이트하려는 파일이 있다면 파일을 전체 삭제한 뒤 저장 - List fileList = getFiles(receiptId); - fileRepository.deleteAll(fileList); - - List files = fileMapper.toFileList(dto.files(), FileOwnerType.RECEIPT, receipt.getId()); - fileRepository.saveAll(files); - } - receiptUpdateService.update(receipt, dto); - account.spend(receipt); - } - - private List getFiles(Long receiptId) { - return fileReader.findAll(FileOwnerType.RECEIPT, receiptId, null); - } - - @Override - @Transactional - public void delete(Long id) { - Receipt receipt = receiptGetService.find(id); - List fileList = fileReader.findAll(FileOwnerType.RECEIPT, id, null); - - receipt.getAccount().cancel(receipt); - - fileRepository.deleteAll(fileList); - receiptDeleteService.delete(receipt); - } -} diff --git a/src/main/java/com/weeth/domain/account/domain/entity/Account.java b/src/main/java/com/weeth/domain/account/domain/entity/Account.java deleted file mode 100644 index 032749bb..00000000 --- a/src/main/java/com/weeth/domain/account/domain/entity/Account.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.weeth.domain.account.domain.entity; - -import jakarta.persistence.*; -import com.weeth.global.common.entity.BaseEntity; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.experimental.SuperBuilder; - -import java.util.ArrayList; -import java.util.List; - -@Entity -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor -@SuperBuilder -public class Account extends BaseEntity { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "account_id") - private Long id; - - private String description; - - private Integer totalAmount; - - private Integer currentAmount; - - private Integer cardinal; - - @OneToMany(mappedBy = "account", cascade = CascadeType.REMOVE, orphanRemoval = true) - private List receipts = new ArrayList<>(); - - public void spend(Receipt receipt) { - this.receipts.add(receipt); - this.currentAmount -= receipt.getAmount(); - } - - public void cancel(Receipt receipt) { - this.receipts.remove(receipt); - this.currentAmount += receipt.getAmount(); - } -} diff --git a/src/main/java/com/weeth/domain/account/domain/entity/Receipt.java b/src/main/java/com/weeth/domain/account/domain/entity/Receipt.java deleted file mode 100644 index 83ea940e..00000000 --- a/src/main/java/com/weeth/domain/account/domain/entity/Receipt.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.weeth.domain.account.domain.entity; - -import jakarta.persistence.*; -import com.weeth.domain.account.application.dto.ReceiptDTO; -import com.weeth.global.common.entity.BaseEntity; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.experimental.SuperBuilder; - -import java.time.LocalDate; - -@Entity -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor -@SuperBuilder -public class Receipt extends BaseEntity { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "receipt_id") - private Long id; - - private String description; - - private String source; - - private Integer amount; - - private LocalDate date; - - @ManyToOne - @JoinColumn(name = "account_id") - private Account account; - - public void update(ReceiptDTO.Update dto){ - this.description = dto.description(); - this.source = dto.source(); - this.amount = dto.amount(); - this.date = dto.date(); - } - -} diff --git a/src/main/java/com/weeth/domain/account/domain/repository/AccountRepository.java b/src/main/java/com/weeth/domain/account/domain/repository/AccountRepository.java deleted file mode 100644 index 0599083f..00000000 --- a/src/main/java/com/weeth/domain/account/domain/repository/AccountRepository.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.weeth.domain.account.domain.repository; - -import com.weeth.domain.account.domain.entity.Account; -import org.springframework.data.jpa.repository.JpaRepository; - -import java.util.Optional; - -public interface AccountRepository extends JpaRepository { - - Optional findByCardinal(Integer cardinal); - - boolean existsByCardinal(Integer cardinal); -} diff --git a/src/main/java/com/weeth/domain/account/domain/repository/ReceiptRepository.java b/src/main/java/com/weeth/domain/account/domain/repository/ReceiptRepository.java deleted file mode 100644 index 588a79ff..00000000 --- a/src/main/java/com/weeth/domain/account/domain/repository/ReceiptRepository.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.weeth.domain.account.domain.repository; - -import com.weeth.domain.account.domain.entity.Receipt; -import org.springframework.data.jpa.repository.JpaRepository; - -import java.util.List; - -public interface ReceiptRepository extends JpaRepository { - List findAllByAccountIdOrderByCreatedAtDesc(Long accountId); -} diff --git a/src/main/java/com/weeth/domain/account/domain/service/AccountGetService.java b/src/main/java/com/weeth/domain/account/domain/service/AccountGetService.java deleted file mode 100644 index bfc948f8..00000000 --- a/src/main/java/com/weeth/domain/account/domain/service/AccountGetService.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.weeth.domain.account.domain.service; - -import com.weeth.domain.account.domain.entity.Account; -import com.weeth.domain.account.domain.repository.AccountRepository; -import com.weeth.domain.account.application.exception.AccountNotFoundException; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class AccountGetService { - - private final AccountRepository accountRepository; - - public Account find(Integer cardinal) { - return accountRepository.findByCardinal(cardinal) - .orElseThrow(AccountNotFoundException::new); - } - - public boolean validate(Integer cardinal) { - return accountRepository.existsByCardinal(cardinal); - } -} diff --git a/src/main/java/com/weeth/domain/account/domain/service/AccountSaveService.java b/src/main/java/com/weeth/domain/account/domain/service/AccountSaveService.java deleted file mode 100644 index d0bf2ccb..00000000 --- a/src/main/java/com/weeth/domain/account/domain/service/AccountSaveService.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.weeth.domain.account.domain.service; - -import com.weeth.domain.account.domain.entity.Account; -import com.weeth.domain.account.domain.repository.AccountRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class AccountSaveService { - - private final AccountRepository accountRepository; - - public void save(Account account) { - accountRepository.save(account); - } -} diff --git a/src/main/java/com/weeth/domain/account/domain/service/ReceiptDeleteService.java b/src/main/java/com/weeth/domain/account/domain/service/ReceiptDeleteService.java deleted file mode 100644 index 7caca70e..00000000 --- a/src/main/java/com/weeth/domain/account/domain/service/ReceiptDeleteService.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.weeth.domain.account.domain.service; - -import com.weeth.domain.account.domain.entity.Receipt; -import com.weeth.domain.account.domain.repository.ReceiptRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class ReceiptDeleteService { - - private final ReceiptRepository receiptRepository; - - public void delete(Receipt receipt) { - receiptRepository.delete(receipt); - } -} diff --git a/src/main/java/com/weeth/domain/account/domain/service/ReceiptGetService.java b/src/main/java/com/weeth/domain/account/domain/service/ReceiptGetService.java deleted file mode 100644 index 61312284..00000000 --- a/src/main/java/com/weeth/domain/account/domain/service/ReceiptGetService.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.weeth.domain.account.domain.service; - -import com.weeth.domain.account.domain.entity.Receipt; -import com.weeth.domain.account.domain.repository.ReceiptRepository; -import com.weeth.domain.account.application.exception.ReceiptNotFoundException; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -import java.util.List; - -@Service -@RequiredArgsConstructor -public class ReceiptGetService { - - private final ReceiptRepository receiptRepository; - - public Receipt find(Long id) { - return receiptRepository.findById(id) - .orElseThrow(ReceiptNotFoundException::new); - } - - public List findAllByAccountId(Long accountId) { - return receiptRepository.findAllByAccountIdOrderByCreatedAtDesc(accountId); - } -} diff --git a/src/main/java/com/weeth/domain/account/domain/service/ReceiptSaveService.java b/src/main/java/com/weeth/domain/account/domain/service/ReceiptSaveService.java deleted file mode 100644 index 22a3933a..00000000 --- a/src/main/java/com/weeth/domain/account/domain/service/ReceiptSaveService.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.weeth.domain.account.domain.service; - -import com.weeth.domain.account.domain.entity.Receipt; -import com.weeth.domain.account.domain.repository.ReceiptRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class ReceiptSaveService { - - private final ReceiptRepository receiptRepository; - - public Receipt save(Receipt receipt) { - return receiptRepository.save(receipt); - } -} diff --git a/src/main/java/com/weeth/domain/account/domain/service/ReceiptUpdateService.java b/src/main/java/com/weeth/domain/account/domain/service/ReceiptUpdateService.java deleted file mode 100644 index 95462683..00000000 --- a/src/main/java/com/weeth/domain/account/domain/service/ReceiptUpdateService.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.weeth.domain.account.domain.service; - -import com.weeth.domain.account.application.dto.ReceiptDTO; -import com.weeth.domain.account.domain.entity.Receipt; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class ReceiptUpdateService { - public void update(Receipt receipt, ReceiptDTO.Update dto) { - receipt.update(dto); - } -} \ No newline at end of file diff --git a/src/main/java/com/weeth/domain/account/presentation/AccountAdminController.java b/src/main/java/com/weeth/domain/account/presentation/AccountAdminController.java deleted file mode 100644 index bf1c565f..00000000 --- a/src/main/java/com/weeth/domain/account/presentation/AccountAdminController.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.weeth.domain.account.presentation; - -import com.weeth.domain.account.application.dto.AccountDTO; -import com.weeth.domain.account.application.exception.AccountErrorCode; -import com.weeth.domain.account.application.usecase.AccountUseCase; -import com.weeth.global.common.exception.ApiErrorCodeExample; -import com.weeth.global.common.response.CommonResponse; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; -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.RestController; - -import static com.weeth.domain.account.presentation.AccountResponseCode.ACCOUNT_SAVE_SUCCESS; - -@Tag(name = "ACCOUNT ADMIN", description = "[ADMIN] 회비 어드민 API") -@RestController -@RequiredArgsConstructor -@RequestMapping("/api/v1/admin/account") -@ApiErrorCodeExample(AccountErrorCode.class) -public class AccountAdminController { - - private final AccountUseCase accountUseCase; - - @PostMapping - @Operation(summary="회비 총 금액 기입") - public CommonResponse save(@RequestBody @Valid AccountDTO.Save dto) { - accountUseCase.save(dto); - return CommonResponse.success(ACCOUNT_SAVE_SUCCESS); - } -} diff --git a/src/main/java/com/weeth/domain/account/presentation/AccountController.java b/src/main/java/com/weeth/domain/account/presentation/AccountController.java deleted file mode 100644 index 1cb72b9d..00000000 --- a/src/main/java/com/weeth/domain/account/presentation/AccountController.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.weeth.domain.account.presentation; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import com.weeth.domain.account.application.dto.AccountDTO; -import com.weeth.domain.account.application.exception.AccountErrorCode; -import com.weeth.domain.account.application.usecase.AccountUseCase; -import com.weeth.global.common.exception.ApiErrorCodeExample; -import com.weeth.global.common.response.CommonResponse; -import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import static com.weeth.domain.account.presentation.AccountResponseCode.ACCOUNT_FIND_SUCCESS; -@Tag(name = "ACCOUNT", description = "회비 API") -@RestController -@RequiredArgsConstructor -@RequestMapping("/api/v1/account") -@ApiErrorCodeExample(AccountErrorCode.class) -public class AccountController { - - private final AccountUseCase accountUseCase; - - @GetMapping("/{cardinal}") - @Operation(summary="회비 내역 조회") - public CommonResponse find(@PathVariable Integer cardinal) { - return CommonResponse.success(ACCOUNT_FIND_SUCCESS,accountUseCase.find(cardinal)); - } -} diff --git a/src/main/java/com/weeth/domain/account/presentation/AccountResponseCode.java b/src/main/java/com/weeth/domain/account/presentation/AccountResponseCode.java deleted file mode 100644 index 4d1bf484..00000000 --- a/src/main/java/com/weeth/domain/account/presentation/AccountResponseCode.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.weeth.domain.account.presentation; - -import com.weeth.global.common.response.ResponseCodeInterface; -import lombok.Getter; -import org.springframework.http.HttpStatus; - -@Getter -public enum AccountResponseCode implements ResponseCodeInterface { - // AccountAdminController 관련 - ACCOUNT_SAVE_SUCCESS(1100, HttpStatus.OK, "회비가 성공적으로 저장되었습니다."), - - // AccountController 관련 - ACCOUNT_FIND_SUCCESS(1101, HttpStatus.OK, "회비가 성공적으로 조회되었습니다."), - - // ReceiptAdminController 관련 - RECEIPT_SAVE_SUCCESS(1102, HttpStatus.OK, "영수증이 성공적으로 저장되었습니다."), - RECEIPT_DELETE_SUCCESS(1103, HttpStatus.OK, "영수증이 성공적으로 삭제되었습니다."), - RECEIPT_UPDATE_SUCCESS(1104, HttpStatus.OK, "영수증이 성공적으로 업데이트 되었습니다."); - - private final int code; - private final HttpStatus status; - private final String message; - - AccountResponseCode(int code, HttpStatus status, String message) { - this.code = code; - this.status = status; - this.message = message; - } -} diff --git a/src/main/java/com/weeth/domain/account/presentation/ReceiptAdminController.java b/src/main/java/com/weeth/domain/account/presentation/ReceiptAdminController.java deleted file mode 100644 index c9dfb434..00000000 --- a/src/main/java/com/weeth/domain/account/presentation/ReceiptAdminController.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.weeth.domain.account.presentation; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import com.weeth.domain.account.application.dto.ReceiptDTO; -import com.weeth.domain.account.application.exception.AccountErrorCode; -import com.weeth.domain.account.application.usecase.ReceiptUseCase; -import com.weeth.global.common.exception.ApiErrorCodeExample; -import com.weeth.global.common.response.CommonResponse; -import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.*; -import static com.weeth.domain.account.presentation.AccountResponseCode.*; - -@Tag(name = "RECEIPT ADMIN", description = "[ADMIN] 회비 어드민 API") -@RestController -@RequiredArgsConstructor -@RequestMapping("/api/v1/admin/receipts") -@ApiErrorCodeExample(AccountErrorCode.class) -public class ReceiptAdminController { - - private final ReceiptUseCase receiptUseCase; - - @PostMapping - @Operation(summary="회비 사용 내역 기입") - public CommonResponse save(@RequestBody @Valid ReceiptDTO.Save dto) { - receiptUseCase.save(dto); - return CommonResponse.success(RECEIPT_SAVE_SUCCESS); - } - - @DeleteMapping("/{receiptId}") - @Operation(summary="회비 사용 내역 취소") - public CommonResponse delete(@PathVariable Long receiptId) { - receiptUseCase.delete(receiptId); - return CommonResponse.success(RECEIPT_DELETE_SUCCESS); - } - - @PatchMapping("/{receiptId}") - @Operation(summary="회비 사용 내역 수정") - public CommonResponse update(@PathVariable Long receiptId, @RequestBody @Valid ReceiptDTO.Update dto) { - receiptUseCase.update(receiptId, dto); - return CommonResponse.success(RECEIPT_UPDATE_SUCCESS); - } -} diff --git a/src/main/kotlin/com/weeth/domain/account/application/dto/request/AccountSaveRequest.kt b/src/main/kotlin/com/weeth/domain/account/application/dto/request/AccountSaveRequest.kt new file mode 100644 index 00000000..cc9be88c --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/account/application/dto/request/AccountSaveRequest.kt @@ -0,0 +1,19 @@ +package com.weeth.domain.account.application.dto.request + +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.NotBlank +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Positive + +data class AccountSaveRequest( + @field:Schema(description = "회비 설명", example = "2024년 2학기 회비") + @field:NotBlank + val description: String, + @field:Schema(description = "총 금액", example = "100000") + @field:NotNull + @field:Positive + val totalAmount: Int, + @field:Schema(description = "기수", example = "4") + @field:NotNull + val cardinal: Int, +) diff --git a/src/main/kotlin/com/weeth/domain/account/application/dto/request/ReceiptSaveRequest.kt b/src/main/kotlin/com/weeth/domain/account/application/dto/request/ReceiptSaveRequest.kt new file mode 100644 index 00000000..c177a3fa --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/account/application/dto/request/ReceiptSaveRequest.kt @@ -0,0 +1,27 @@ +package com.weeth.domain.account.application.dto.request + +import com.weeth.domain.file.application.dto.request.FileSaveRequest +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.Valid +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Positive +import java.time.LocalDate + +data class ReceiptSaveRequest( + @field:Schema(description = "영수증 설명", example = "간식비") + val description: String?, + @field:Schema(description = "출처", example = "편의점") + val source: String?, + @field:Schema(description = "사용 금액", example = "10000") + @field:NotNull + @field:Positive + val amount: Int, + @field:Schema(description = "사용 날짜", example = "2024-09-01") + @field:NotNull + val date: LocalDate, + @field:Schema(description = "기수", example = "4") + @field:NotNull + val cardinal: Int, + @field:Valid + val files: List<@NotNull FileSaveRequest>?, +) diff --git a/src/main/kotlin/com/weeth/domain/account/application/dto/request/ReceiptUpdateRequest.kt b/src/main/kotlin/com/weeth/domain/account/application/dto/request/ReceiptUpdateRequest.kt new file mode 100644 index 00000000..cb146aff --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/account/application/dto/request/ReceiptUpdateRequest.kt @@ -0,0 +1,31 @@ +package com.weeth.domain.account.application.dto.request + +import com.weeth.domain.file.application.dto.request.FileSaveRequest +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.Valid +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Positive +import java.time.LocalDate + +data class ReceiptUpdateRequest( + @field:Schema(description = "영수증 설명", example = "간식비") + val description: String?, + @field:Schema(description = "출처", example = "편의점") + val source: String?, + @field:Schema(description = "사용 금액", example = "10000") + @field:NotNull + @field:Positive + val amount: Int, + @field:Schema(description = "사용 날짜", example = "2024-09-01") + @field:NotNull + val date: LocalDate, + @field:Schema(description = "기수", example = "4") + @field:NotNull + val cardinal: Int, + @field:Schema( + description = "첨부 파일 변경 규약: null=변경 안 함, []=전체 삭제, 배열 전달=해당 목록으로 교체", + nullable = true, + ) + @field:Valid + val files: List<@NotNull FileSaveRequest>?, +) diff --git a/src/main/kotlin/com/weeth/domain/account/application/dto/response/AccountResponse.kt b/src/main/kotlin/com/weeth/domain/account/application/dto/response/AccountResponse.kt new file mode 100644 index 00000000..3ac8b44d --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/account/application/dto/response/AccountResponse.kt @@ -0,0 +1,21 @@ +package com.weeth.domain.account.application.dto.response + +import io.swagger.v3.oas.annotations.media.Schema +import java.time.LocalDateTime + +data class AccountResponse( + @field:Schema(description = "회비 ID", example = "1") + val accountId: Long, + @field:Schema(description = "회비 설명", example = "2024년 2학기 회비") + val description: String, + @field:Schema(description = "총 금액", example = "100000") + val totalAmount: Int, + @field:Schema(description = "현재 금액", example = "90000") + val currentAmount: Int, + @field:Schema(description = "최종 수정 시각") + val time: LocalDateTime?, + @field:Schema(description = "기수", example = "40") + val cardinal: Int, + @field:Schema(description = "영수증 목록") + val receipts: List, +) diff --git a/src/main/kotlin/com/weeth/domain/account/application/dto/response/ReceiptResponse.kt b/src/main/kotlin/com/weeth/domain/account/application/dto/response/ReceiptResponse.kt new file mode 100644 index 00000000..df8d1d7b --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/account/application/dto/response/ReceiptResponse.kt @@ -0,0 +1,20 @@ +package com.weeth.domain.account.application.dto.response + +import com.weeth.domain.file.application.dto.response.FileResponse +import io.swagger.v3.oas.annotations.media.Schema +import java.time.LocalDate + +data class ReceiptResponse( + @field:Schema(description = "영수증 ID", example = "1") + val id: Long, + @field:Schema(description = "영수증 설명", example = "간식비") + val description: String?, + @field:Schema(description = "출처", example = "편의점") + val source: String?, + @field:Schema(description = "사용 금액", example = "10000") + val amount: Int, + @field:Schema(description = "사용 날짜", example = "2024-09-01") + val date: LocalDate, + @field:Schema(description = "첨부 파일 목록") + val fileUrls: List, +) diff --git a/src/main/kotlin/com/weeth/domain/account/application/exception/AccountErrorCode.kt b/src/main/kotlin/com/weeth/domain/account/application/exception/AccountErrorCode.kt new file mode 100644 index 00000000..06ecb74d --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/account/application/exception/AccountErrorCode.kt @@ -0,0 +1,30 @@ +package com.weeth.domain.account.application.exception + +import com.weeth.global.common.exception.ErrorCodeInterface +import com.weeth.global.common.exception.ExplainError +import org.springframework.http.HttpStatus + +enum class AccountErrorCode( + private val code: Int, + private val status: HttpStatus, + private val message: String, +) : ErrorCodeInterface { + @ExplainError("요청한 회비 장부 ID가 존재하지 않을 때 발생합니다.") + ACCOUNT_NOT_FOUND(2100, HttpStatus.NOT_FOUND, "존재하지 않는 장부입니다."), + + @ExplainError("이미 존재하는 장부를 중복 생성하려고 할 때 발생합니다.") + ACCOUNT_EXISTS(2101, HttpStatus.BAD_REQUEST, "이미 생성된 장부입니다."), + + @ExplainError("요청한 영수증 내역이 존재하지 않을 때 발생합니다.") + RECEIPT_NOT_FOUND(2102, HttpStatus.NOT_FOUND, "존재하지 않는 내역입니다."), + + @ExplainError("영수증이 요청한 기수의 장부에 속하지 않을 때 발생합니다.") + RECEIPT_ACCOUNT_MISMATCH(2103, HttpStatus.BAD_REQUEST, "영수증이 해당 기수의 장부에 속하지 않습니다."), + ; + + override fun getCode(): Int = code + + override fun getStatus(): HttpStatus = status + + override fun getMessage(): String = message +} diff --git a/src/main/kotlin/com/weeth/domain/account/application/exception/AccountExistsException.kt b/src/main/kotlin/com/weeth/domain/account/application/exception/AccountExistsException.kt new file mode 100644 index 00000000..5886dead --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/account/application/exception/AccountExistsException.kt @@ -0,0 +1,5 @@ +package com.weeth.domain.account.application.exception + +import com.weeth.global.common.exception.BaseException + +class AccountExistsException : BaseException(AccountErrorCode.ACCOUNT_EXISTS) diff --git a/src/main/kotlin/com/weeth/domain/account/application/exception/AccountNotFoundException.kt b/src/main/kotlin/com/weeth/domain/account/application/exception/AccountNotFoundException.kt new file mode 100644 index 00000000..c7dc5a24 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/account/application/exception/AccountNotFoundException.kt @@ -0,0 +1,5 @@ +package com.weeth.domain.account.application.exception + +import com.weeth.global.common.exception.BaseException + +class AccountNotFoundException : BaseException(AccountErrorCode.ACCOUNT_NOT_FOUND) diff --git a/src/main/kotlin/com/weeth/domain/account/application/exception/ReceiptAccountMismatchException.kt b/src/main/kotlin/com/weeth/domain/account/application/exception/ReceiptAccountMismatchException.kt new file mode 100644 index 00000000..04a34880 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/account/application/exception/ReceiptAccountMismatchException.kt @@ -0,0 +1,5 @@ +package com.weeth.domain.account.application.exception + +import com.weeth.global.common.exception.BaseException + +class ReceiptAccountMismatchException : BaseException(AccountErrorCode.RECEIPT_ACCOUNT_MISMATCH) diff --git a/src/main/kotlin/com/weeth/domain/account/application/exception/ReceiptNotFoundException.kt b/src/main/kotlin/com/weeth/domain/account/application/exception/ReceiptNotFoundException.kt new file mode 100644 index 00000000..db6f1b51 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/account/application/exception/ReceiptNotFoundException.kt @@ -0,0 +1,5 @@ +package com.weeth.domain.account.application.exception + +import com.weeth.global.common.exception.BaseException + +class ReceiptNotFoundException : BaseException(AccountErrorCode.RECEIPT_NOT_FOUND) diff --git a/src/main/kotlin/com/weeth/domain/account/application/mapper/AccountMapper.kt b/src/main/kotlin/com/weeth/domain/account/application/mapper/AccountMapper.kt new file mode 100644 index 00000000..67fac93b --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/account/application/mapper/AccountMapper.kt @@ -0,0 +1,23 @@ +package com.weeth.domain.account.application.mapper + +import com.weeth.domain.account.application.dto.response.AccountResponse +import com.weeth.domain.account.application.dto.response.ReceiptResponse +import com.weeth.domain.account.domain.entity.Account +import org.springframework.stereotype.Component + +@Component +class AccountMapper { + fun toResponse( + account: Account, + receipts: List, + ): AccountResponse = + AccountResponse( + accountId = account.id, + description = account.description, + totalAmount = account.totalAmount, + currentAmount = account.currentAmount, + time = account.modifiedAt, + cardinal = account.cardinal, + receipts = receipts, + ) +} diff --git a/src/main/kotlin/com/weeth/domain/account/application/mapper/ReceiptMapper.kt b/src/main/kotlin/com/weeth/domain/account/application/mapper/ReceiptMapper.kt new file mode 100644 index 00000000..9999da3a --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/account/application/mapper/ReceiptMapper.kt @@ -0,0 +1,30 @@ +package com.weeth.domain.account.application.mapper + +import com.weeth.domain.account.application.dto.response.ReceiptResponse +import com.weeth.domain.account.domain.entity.Receipt +import com.weeth.domain.file.application.dto.response.FileResponse +import org.springframework.stereotype.Component + +@Component +class ReceiptMapper { + fun toResponse( + receipt: Receipt, + fileUrls: List, + ): ReceiptResponse = + ReceiptResponse( + id = receipt.id, + description = receipt.description, + source = receipt.source, + amount = receipt.amount, + date = receipt.date, + fileUrls = fileUrls, + ) + + fun toResponses( + receipts: List, + filesByReceiptId: Map>, + ): List = + receipts.map { receipt -> + toResponse(receipt, filesByReceiptId[receipt.id] ?: emptyList()) + } +} diff --git a/src/main/kotlin/com/weeth/domain/account/application/usecase/command/ManageAccountUseCase.kt b/src/main/kotlin/com/weeth/domain/account/application/usecase/command/ManageAccountUseCase.kt new file mode 100644 index 00000000..70a54fad --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/account/application/usecase/command/ManageAccountUseCase.kt @@ -0,0 +1,22 @@ +package com.weeth.domain.account.application.usecase.command + +import com.weeth.domain.account.application.dto.request.AccountSaveRequest +import com.weeth.domain.account.application.exception.AccountExistsException +import com.weeth.domain.account.domain.entity.Account +import com.weeth.domain.account.domain.repository.AccountRepository +import com.weeth.domain.user.domain.service.CardinalGetService +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class ManageAccountUseCase( + private val accountRepository: AccountRepository, + private val cardinalGetService: CardinalGetService, +) { + @Transactional + fun save(request: AccountSaveRequest) { + if (accountRepository.existsByCardinal(request.cardinal)) throw AccountExistsException() + cardinalGetService.findByAdminSide(request.cardinal) + accountRepository.save(Account.create(request.description, request.totalAmount, request.cardinal)) + } +} diff --git a/src/main/kotlin/com/weeth/domain/account/application/usecase/command/ManageReceiptUseCase.kt b/src/main/kotlin/com/weeth/domain/account/application/usecase/command/ManageReceiptUseCase.kt new file mode 100644 index 00000000..ef0c939f --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/account/application/usecase/command/ManageReceiptUseCase.kt @@ -0,0 +1,66 @@ +package com.weeth.domain.account.application.usecase.command + +import com.weeth.domain.account.application.dto.request.ReceiptSaveRequest +import com.weeth.domain.account.application.dto.request.ReceiptUpdateRequest +import com.weeth.domain.account.application.exception.AccountNotFoundException +import com.weeth.domain.account.application.exception.ReceiptAccountMismatchException +import com.weeth.domain.account.application.exception.ReceiptNotFoundException +import com.weeth.domain.account.domain.entity.Receipt +import com.weeth.domain.account.domain.repository.AccountRepository +import com.weeth.domain.account.domain.repository.ReceiptRepository +import com.weeth.domain.account.domain.vo.Money +import com.weeth.domain.file.application.mapper.FileMapper +import com.weeth.domain.file.domain.entity.FileOwnerType +import com.weeth.domain.file.domain.repository.FileReader +import com.weeth.domain.file.domain.repository.FileRepository +import com.weeth.domain.user.domain.service.CardinalGetService +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class ManageReceiptUseCase( + private val receiptRepository: ReceiptRepository, + private val accountRepository: AccountRepository, + private val fileReader: FileReader, + private val fileRepository: FileRepository, + private val cardinalGetService: CardinalGetService, + private val fileMapper: FileMapper, +) { + @Transactional + fun save(request: ReceiptSaveRequest) { + cardinalGetService.findByAdminSide(request.cardinal) + val account = accountRepository.findByCardinal(request.cardinal) ?: throw AccountNotFoundException() + val receipt = + receiptRepository.save( + Receipt.create(request.description, request.source, request.amount, request.date, account), + ) + account.spend(Money.of(request.amount)) + fileRepository.saveAll(fileMapper.toFileList(request.files, FileOwnerType.RECEIPT, receipt.id)) + } + + @Transactional + fun update( + receiptId: Long, + request: ReceiptUpdateRequest, + ) { + cardinalGetService.findByAdminSide(request.cardinal) + val account = accountRepository.findByCardinal(request.cardinal) ?: throw AccountNotFoundException() + val receipt = receiptRepository.findByIdOrNull(receiptId) ?: throw ReceiptNotFoundException() + if (receipt.account.id != account.id) throw ReceiptAccountMismatchException() + account.adjustSpend(Money.of(receipt.amount), Money.of(request.amount)) + if (request.files != null) { + fileRepository.deleteAll(fileReader.findAll(FileOwnerType.RECEIPT, receiptId, null)) + fileRepository.saveAll(fileMapper.toFileList(request.files, FileOwnerType.RECEIPT, receiptId)) + } + receipt.update(request.description, request.source, request.amount, request.date) + } + + @Transactional + fun delete(receiptId: Long) { + val receipt = receiptRepository.findByIdOrNull(receiptId) ?: throw ReceiptNotFoundException() + receipt.account.cancelSpend(Money.of(receipt.amount)) + fileRepository.deleteAll(fileReader.findAll(FileOwnerType.RECEIPT, receiptId, null)) + receiptRepository.delete(receipt) + } +} diff --git a/src/main/kotlin/com/weeth/domain/account/application/usecase/query/GetAccountQueryService.kt b/src/main/kotlin/com/weeth/domain/account/application/usecase/query/GetAccountQueryService.kt new file mode 100644 index 00000000..8d10b081 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/account/application/usecase/query/GetAccountQueryService.kt @@ -0,0 +1,35 @@ +package com.weeth.domain.account.application.usecase.query + +import com.weeth.domain.account.application.dto.response.AccountResponse +import com.weeth.domain.account.application.exception.AccountNotFoundException +import com.weeth.domain.account.application.mapper.AccountMapper +import com.weeth.domain.account.application.mapper.ReceiptMapper +import com.weeth.domain.account.domain.repository.AccountRepository +import com.weeth.domain.account.domain.repository.ReceiptRepository +import com.weeth.domain.file.application.mapper.FileMapper +import com.weeth.domain.file.domain.entity.FileOwnerType +import com.weeth.domain.file.domain.repository.FileReader +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +@Transactional(readOnly = true) +class GetAccountQueryService( + private val accountRepository: AccountRepository, + private val receiptRepository: ReceiptRepository, + private val fileReader: FileReader, + private val accountMapper: AccountMapper, + private val receiptMapper: ReceiptMapper, + private val fileMapper: FileMapper, +) { + fun findByCardinal(cardinal: Int): AccountResponse { + val account = accountRepository.findByCardinal(cardinal) ?: throw AccountNotFoundException() + val receipts = receiptRepository.findAllByAccountIdOrderByCreatedAtDesc(account.id) + val receiptIds = receipts.map { it.id } + val filesByReceiptId = + fileReader + .findAll(FileOwnerType.RECEIPT, receiptIds, null) + .groupBy({ it.ownerId }, { fileMapper.toFileResponse(it) }) + return accountMapper.toResponse(account, receiptMapper.toResponses(receipts, filesByReceiptId)) + } +} diff --git a/src/main/kotlin/com/weeth/domain/account/domain/entity/Account.kt b/src/main/kotlin/com/weeth/domain/account/domain/entity/Account.kt new file mode 100644 index 00000000..98f1b8e8 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/account/domain/entity/Account.kt @@ -0,0 +1,61 @@ +package com.weeth.domain.account.domain.entity + +import com.weeth.domain.account.domain.vo.Money +import com.weeth.global.common.entity.BaseEntity +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id + +@Entity +class Account( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "account_id") + val id: Long = 0, + @Column(nullable = false) + val description: String, + @Column(nullable = false) + val totalAmount: Int, + @Column(nullable = false) + var currentAmount: Int, + @Column(nullable = false) + val cardinal: Int, +) : BaseEntity() { + fun spend(amount: Money) { + require(amount.value > 0) { "사용 금액은 0보다 커야 합니다: ${amount.value}" } + check(currentAmount >= amount.value) { "잔액이 부족합니다. 현재: $currentAmount, 요청: ${amount.value}" } + currentAmount -= amount.value + } + + fun cancelSpend(amount: Money) { + require(amount.value > 0) { "취소 금액은 0보다 커야 합니다: ${amount.value}" } + check(currentAmount + amount.value <= totalAmount) { "총액을 초과할 수 없습니다. 총액: $totalAmount" } + currentAmount += amount.value + } + + fun adjustSpend( + oldAmount: Money, + newAmount: Money, + ) { + cancelSpend(oldAmount) + spend(newAmount) + } + + companion object { + fun create( + description: String, + totalAmount: Int, + cardinal: Int, + ): Account { + require(totalAmount > 0) { "총액은 0보다 커야 합니다: $totalAmount" } + return Account( + description = description, + totalAmount = totalAmount, + currentAmount = totalAmount, + cardinal = cardinal, + ) + } + } +} diff --git a/src/main/kotlin/com/weeth/domain/account/domain/entity/Receipt.kt b/src/main/kotlin/com/weeth/domain/account/domain/entity/Receipt.kt new file mode 100644 index 00000000..b63786e1 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/account/domain/entity/Receipt.kt @@ -0,0 +1,63 @@ +package com.weeth.domain.account.domain.entity + +import com.weeth.global.common.entity.BaseEntity +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.FetchType +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import java.time.LocalDate + +@Entity +class Receipt( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "receipt_id") + val id: Long = 0, + @Column + var description: String?, + @Column + var source: String?, + @Column(nullable = false) + var amount: Int, + @Column(nullable = false) + var date: LocalDate, + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "account_id") + val account: Account, +) : BaseEntity() { + fun update( + description: String?, + source: String?, + amount: Int, + date: LocalDate, + ) { + require(amount > 0) { "금액은 0보다 커야 합니다: $amount" } + this.description = description + this.source = source + this.amount = amount + this.date = date + } + + companion object { + fun create( + description: String?, + source: String?, + amount: Int, + date: LocalDate, + account: Account, + ): Receipt { + require(amount > 0) { "금액은 0보다 커야 합니다: $amount" } + return Receipt( + description = description, + source = source, + amount = amount, + date = date, + account = account, + ) + } + } +} diff --git a/src/main/kotlin/com/weeth/domain/account/domain/repository/AccountRepository.kt b/src/main/kotlin/com/weeth/domain/account/domain/repository/AccountRepository.kt new file mode 100644 index 00000000..c7a9daeb --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/account/domain/repository/AccountRepository.kt @@ -0,0 +1,10 @@ +package com.weeth.domain.account.domain.repository + +import com.weeth.domain.account.domain.entity.Account +import org.springframework.data.jpa.repository.JpaRepository + +interface AccountRepository : JpaRepository { + fun findByCardinal(cardinal: Int): Account? + + fun existsByCardinal(cardinal: Int): Boolean +} diff --git a/src/main/kotlin/com/weeth/domain/account/domain/repository/ReceiptRepository.kt b/src/main/kotlin/com/weeth/domain/account/domain/repository/ReceiptRepository.kt new file mode 100644 index 00000000..4872fa45 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/account/domain/repository/ReceiptRepository.kt @@ -0,0 +1,8 @@ +package com.weeth.domain.account.domain.repository + +import com.weeth.domain.account.domain.entity.Receipt +import org.springframework.data.jpa.repository.JpaRepository + +interface ReceiptRepository : JpaRepository { + fun findAllByAccountIdOrderByCreatedAtDesc(accountId: Long): List +} diff --git a/src/main/kotlin/com/weeth/domain/account/domain/vo/Money.kt b/src/main/kotlin/com/weeth/domain/account/domain/vo/Money.kt new file mode 100644 index 00000000..2e856076 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/account/domain/vo/Money.kt @@ -0,0 +1,20 @@ +package com.weeth.domain.account.domain.vo + +@JvmInline +value class Money( + val value: Int, +) { + init { + require(value >= 0) { "금액은 0 이상이어야 합니다: $value" } + } + + operator fun plus(other: Money) = Money(value + other.value) + + operator fun minus(other: Money) = Money(value - other.value) + + companion object { + val ZERO = Money(0) + + fun of(value: Int) = Money(value) + } +} diff --git a/src/main/kotlin/com/weeth/domain/account/presentation/AccountAdminController.kt b/src/main/kotlin/com/weeth/domain/account/presentation/AccountAdminController.kt new file mode 100644 index 00000000..92cc5a46 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/account/presentation/AccountAdminController.kt @@ -0,0 +1,32 @@ +package com.weeth.domain.account.presentation + +import com.weeth.domain.account.application.dto.request.AccountSaveRequest +import com.weeth.domain.account.application.exception.AccountErrorCode +import com.weeth.domain.account.application.usecase.command.ManageAccountUseCase +import com.weeth.domain.account.presentation.AccountResponseCode.ACCOUNT_SAVE_SUCCESS +import com.weeth.global.common.exception.ApiErrorCodeExample +import com.weeth.global.common.response.CommonResponse +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.tags.Tag +import jakarta.validation.Valid +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.RestController + +@Tag(name = "ACCOUNT ADMIN", description = "[ADMIN] 회비 어드민 API") +@RestController +@RequestMapping("/api/v1/admin/account") +@ApiErrorCodeExample(AccountErrorCode::class) +class AccountAdminController( + private val manageAccountUseCase: ManageAccountUseCase, +) { + @PostMapping + @Operation(summary = "회비 총 금액 기입") + fun save( + @RequestBody @Valid dto: AccountSaveRequest, + ): CommonResponse { + manageAccountUseCase.save(dto) + return CommonResponse.success(ACCOUNT_SAVE_SUCCESS) + } +} diff --git a/src/main/kotlin/com/weeth/domain/account/presentation/AccountController.kt b/src/main/kotlin/com/weeth/domain/account/presentation/AccountController.kt new file mode 100644 index 00000000..96d9e4c6 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/account/presentation/AccountController.kt @@ -0,0 +1,28 @@ +package com.weeth.domain.account.presentation + +import com.weeth.domain.account.application.dto.response.AccountResponse +import com.weeth.domain.account.application.exception.AccountErrorCode +import com.weeth.domain.account.application.usecase.query.GetAccountQueryService +import com.weeth.domain.account.presentation.AccountResponseCode.ACCOUNT_FIND_SUCCESS +import com.weeth.global.common.exception.ApiErrorCodeExample +import com.weeth.global.common.response.CommonResponse +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@Tag(name = "ACCOUNT", description = "회비 API") +@RestController +@RequestMapping("/api/v1/account") +@ApiErrorCodeExample(AccountErrorCode::class) +class AccountController( + private val getAccountQueryService: GetAccountQueryService, +) { + @GetMapping("/{cardinal}") + @Operation(summary = "회비 내역 조회") + fun find( + @PathVariable cardinal: Int, + ): CommonResponse = CommonResponse.success(ACCOUNT_FIND_SUCCESS, getAccountQueryService.findByCardinal(cardinal)) +} diff --git a/src/main/kotlin/com/weeth/domain/account/presentation/AccountResponseCode.kt b/src/main/kotlin/com/weeth/domain/account/presentation/AccountResponseCode.kt new file mode 100644 index 00000000..647dfc5e --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/account/presentation/AccountResponseCode.kt @@ -0,0 +1,16 @@ +package com.weeth.domain.account.presentation + +import com.weeth.global.common.response.ResponseCodeInterface +import org.springframework.http.HttpStatus + +enum class AccountResponseCode( + override val code: Int, + override val status: HttpStatus, + override val message: String, +) : ResponseCodeInterface { + ACCOUNT_SAVE_SUCCESS(1100, HttpStatus.OK, "회비가 성공적으로 저장되었습니다."), + ACCOUNT_FIND_SUCCESS(1101, HttpStatus.OK, "회비가 성공적으로 조회되었습니다."), + RECEIPT_SAVE_SUCCESS(1102, HttpStatus.OK, "영수증이 성공적으로 저장되었습니다."), + RECEIPT_DELETE_SUCCESS(1103, HttpStatus.OK, "영수증이 성공적으로 삭제되었습니다."), + RECEIPT_UPDATE_SUCCESS(1104, HttpStatus.OK, "영수증이 성공적으로 업데이트 되었습니다."), +} diff --git a/src/main/kotlin/com/weeth/domain/account/presentation/ReceiptAdminController.kt b/src/main/kotlin/com/weeth/domain/account/presentation/ReceiptAdminController.kt new file mode 100644 index 00000000..3df7b96f --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/account/presentation/ReceiptAdminController.kt @@ -0,0 +1,57 @@ +package com.weeth.domain.account.presentation + +import com.weeth.domain.account.application.dto.request.ReceiptSaveRequest +import com.weeth.domain.account.application.dto.request.ReceiptUpdateRequest +import com.weeth.domain.account.application.exception.AccountErrorCode +import com.weeth.domain.account.application.usecase.command.ManageReceiptUseCase +import com.weeth.domain.account.presentation.AccountResponseCode.RECEIPT_DELETE_SUCCESS +import com.weeth.domain.account.presentation.AccountResponseCode.RECEIPT_SAVE_SUCCESS +import com.weeth.domain.account.presentation.AccountResponseCode.RECEIPT_UPDATE_SUCCESS +import com.weeth.global.common.exception.ApiErrorCodeExample +import com.weeth.global.common.response.CommonResponse +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.tags.Tag +import jakarta.validation.Valid +import org.springframework.web.bind.annotation.DeleteMapping +import org.springframework.web.bind.annotation.PatchMapping +import org.springframework.web.bind.annotation.PathVariable +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.RestController + +@Tag(name = "RECEIPT ADMIN", description = "[ADMIN] 회비 어드민 API") +@RestController +@RequestMapping("/api/v1/admin/receipts") +@ApiErrorCodeExample(AccountErrorCode::class) +class ReceiptAdminController( + private val manageReceiptUseCase: ManageReceiptUseCase, +) { + @PostMapping + @Operation(summary = "회비 사용 내역 기입") + fun save( + @RequestBody @Valid dto: ReceiptSaveRequest, + ): CommonResponse { + manageReceiptUseCase.save(dto) + return CommonResponse.success(RECEIPT_SAVE_SUCCESS) + } + + @DeleteMapping("/{receiptId}") + @Operation(summary = "회비 사용 내역 취소") + fun delete( + @PathVariable receiptId: Long, + ): CommonResponse { + manageReceiptUseCase.delete(receiptId) + return CommonResponse.success(RECEIPT_DELETE_SUCCESS) + } + + @PatchMapping("/{receiptId}") + @Operation(summary = "회비 사용 내역 수정") + fun update( + @PathVariable receiptId: Long, + @RequestBody @Valid dto: ReceiptUpdateRequest, + ): CommonResponse { + manageReceiptUseCase.update(receiptId, dto) + return CommonResponse.success(RECEIPT_UPDATE_SUCCESS) + } +} diff --git a/src/test/kotlin/com/weeth/domain/account/application/usecase/ReceiptUseCaseImplTest.kt b/src/test/kotlin/com/weeth/domain/account/application/usecase/ReceiptUseCaseImplTest.kt deleted file mode 100644 index b3d0853f..00000000 --- a/src/test/kotlin/com/weeth/domain/account/application/usecase/ReceiptUseCaseImplTest.kt +++ /dev/null @@ -1,97 +0,0 @@ -package com.weeth.domain.account.application.usecase - -import com.weeth.domain.account.application.dto.ReceiptDTO -import com.weeth.domain.account.application.mapper.ReceiptMapper -import com.weeth.domain.account.domain.entity.Account -import com.weeth.domain.account.domain.entity.Receipt -import com.weeth.domain.account.domain.service.AccountGetService -import com.weeth.domain.account.domain.service.ReceiptDeleteService -import com.weeth.domain.account.domain.service.ReceiptGetService -import com.weeth.domain.account.domain.service.ReceiptSaveService -import com.weeth.domain.account.domain.service.ReceiptUpdateService -import com.weeth.domain.file.application.dto.request.FileSaveRequest -import com.weeth.domain.file.application.mapper.FileMapper -import com.weeth.domain.file.domain.entity.File -import com.weeth.domain.file.domain.entity.FileOwnerType -import com.weeth.domain.file.domain.repository.FileReader -import com.weeth.domain.file.domain.repository.FileRepository -import com.weeth.domain.user.domain.service.CardinalGetService -import io.kotest.core.spec.style.DescribeSpec -import io.mockk.every -import io.mockk.mockk -import io.mockk.verify -import java.time.LocalDate - -class ReceiptUseCaseImplTest : - DescribeSpec({ - val receiptGetService = mockk() - val receiptDeleteService = mockk() - val receiptSaveService = mockk() - val receiptUpdateService = mockk(relaxUnitFun = true) - val accountGetService = mockk() - val fileReader = mockk() - val fileRepository = mockk(relaxed = true) - val cardinalGetService = mockk() - val receiptMapper = mockk() - val fileMapper = mockk() - - val useCase = - ReceiptUseCaseImpl( - receiptGetService, - receiptDeleteService, - receiptSaveService, - receiptUpdateService, - accountGetService, - fileReader, - fileRepository, - cardinalGetService, - receiptMapper, - fileMapper, - ) - - describe("update") { - it("업데이트 파일이 있으면 기존 파일을 삭제 후 새 파일을 저장한다") { - val receiptId = 10L - val account = - Account - .builder() - .id(1L) - .totalAmount(10000) - .currentAmount(10000) - .cardinal(40) - .receipts(mutableListOf()) - .build() - val receipt = - Receipt - .builder() - .id(receiptId) - .amount(1000) - .account(account) - .build() - - val dto = - ReceiptDTO.Update( - "desc", - "source", - 2000, - LocalDate.of(2026, 1, 1), - 40, - listOf(FileSaveRequest("new.png", "TEMP/2026-02/new.png", 100L, "image/png")), - ) - - val oldFiles = listOf(mockk()) - val newFiles = listOf(mockk()) - - every { accountGetService.find(dto.cardinal()) } returns account - every { receiptGetService.find(receiptId) } returns receipt - every { fileReader.findAll(FileOwnerType.RECEIPT, receiptId, null) } returns oldFiles - every { fileMapper.toFileList(dto.files(), FileOwnerType.RECEIPT, receiptId) } returns newFiles - - useCase.update(receiptId, dto) - - verify(exactly = 1) { fileRepository.deleteAll(oldFiles) } - verify(exactly = 1) { fileRepository.saveAll(newFiles) } - verify(exactly = 1) { receiptUpdateService.update(receipt, dto) } - } - } - }) diff --git a/src/test/kotlin/com/weeth/domain/account/application/usecase/command/ManageAccountUseCaseTest.kt b/src/test/kotlin/com/weeth/domain/account/application/usecase/command/ManageAccountUseCaseTest.kt new file mode 100644 index 00000000..ea07505f --- /dev/null +++ b/src/test/kotlin/com/weeth/domain/account/application/usecase/command/ManageAccountUseCaseTest.kt @@ -0,0 +1,47 @@ +package com.weeth.domain.account.application.usecase.command + +import com.weeth.domain.account.application.dto.request.AccountSaveRequest +import com.weeth.domain.account.application.exception.AccountExistsException +import com.weeth.domain.account.domain.repository.AccountRepository +import com.weeth.domain.user.domain.service.CardinalGetService +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.DescribeSpec +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify + +class ManageAccountUseCaseTest : + DescribeSpec({ + val accountRepository = mockk(relaxed = true) + val cardinalGetService = mockk(relaxUnitFun = true) + val useCase = ManageAccountUseCase(accountRepository, cardinalGetService) + + beforeTest { + clearMocks(accountRepository, cardinalGetService) + } + + describe("save") { + context("이미 존재하는 기수로 저장 시") { + it("AccountExistsException을 던진다") { + val dto = AccountSaveRequest("설명", 100_000, 40) + every { accountRepository.existsByCardinal(40) } returns true + + shouldThrow { useCase.save(dto) } + } + } + + context("정상 저장 시") { + it("account가 저장된다") { + val dto = AccountSaveRequest("설명", 100_000, 40) + every { accountRepository.existsByCardinal(40) } returns false + every { cardinalGetService.findByAdminSide(40) } returns mockk() + every { accountRepository.save(any()) } answers { firstArg() } + + useCase.save(dto) + + verify(exactly = 1) { accountRepository.save(any()) } + } + } + } + }) diff --git a/src/test/kotlin/com/weeth/domain/account/application/usecase/command/ManageReceiptUseCaseTest.kt b/src/test/kotlin/com/weeth/domain/account/application/usecase/command/ManageReceiptUseCaseTest.kt new file mode 100644 index 00000000..2da320fb --- /dev/null +++ b/src/test/kotlin/com/weeth/domain/account/application/usecase/command/ManageReceiptUseCaseTest.kt @@ -0,0 +1,199 @@ +package com.weeth.domain.account.application.usecase.command + +import com.weeth.domain.account.application.dto.request.ReceiptSaveRequest +import com.weeth.domain.account.application.dto.request.ReceiptUpdateRequest +import com.weeth.domain.account.application.exception.AccountNotFoundException +import com.weeth.domain.account.application.exception.ReceiptAccountMismatchException +import com.weeth.domain.account.domain.repository.AccountRepository +import com.weeth.domain.account.domain.repository.ReceiptRepository +import com.weeth.domain.account.domain.vo.Money +import com.weeth.domain.account.fixture.AccountTestFixture +import com.weeth.domain.account.fixture.ReceiptTestFixture +import com.weeth.domain.file.application.dto.request.FileSaveRequest +import com.weeth.domain.file.application.mapper.FileMapper +import com.weeth.domain.file.domain.entity.File +import com.weeth.domain.file.domain.entity.FileOwnerType +import com.weeth.domain.file.domain.repository.FileReader +import com.weeth.domain.file.domain.repository.FileRepository +import com.weeth.domain.user.domain.service.CardinalGetService +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.DescribeSpec +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import java.time.LocalDate +import java.util.Optional + +class ManageReceiptUseCaseTest : + DescribeSpec({ + val receiptRepository = mockk(relaxUnitFun = true) + val accountRepository = mockk() + val fileReader = mockk() + val fileRepository = mockk(relaxed = true) + val cardinalGetService = mockk(relaxUnitFun = true) + val fileMapper = mockk() + val useCase = + ManageReceiptUseCase( + receiptRepository, + accountRepository, + fileReader, + fileRepository, + cardinalGetService, + fileMapper, + ) + + beforeTest { + clearMocks(receiptRepository, accountRepository, fileReader, fileRepository, cardinalGetService, fileMapper) + } + + describe("save") { + context("파일이 있는 경우") { + it("영수증 저장 후 fileRepository.saveAll이 호출된다") { + val account = AccountTestFixture.createAccount(cardinal = 40) + val savedReceipt = ReceiptTestFixture.createReceipt(id = 10L, amount = 5_000, account = account) + val files = listOf(mockk()) + val dto = + ReceiptSaveRequest( + "간식비", + "편의점", + 5_000, + LocalDate.of(2024, 9, 1), + 40, + listOf(FileSaveRequest("receipt.png", "TEMP/2024-09/receipt.png", 200L, "image/png")), + ) + + every { cardinalGetService.findByAdminSide(40) } returns mockk() + every { accountRepository.findByCardinal(40) } returns account + every { receiptRepository.save(any()) } returns savedReceipt + every { fileMapper.toFileList(dto.files, FileOwnerType.RECEIPT, savedReceipt.id) } returns files + + useCase.save(dto) + + verify(exactly = 1) { receiptRepository.save(any()) } + verify(exactly = 1) { fileRepository.saveAll(files) } + } + } + + context("파일이 없는 경우") { + it("fileRepository.saveAll은 빈 리스트로 호출된다") { + val account = AccountTestFixture.createAccount(cardinal = 40) + val savedReceipt = ReceiptTestFixture.createReceipt(id = 11L, amount = 3_000, account = account) + val dto = ReceiptSaveRequest("교통비", "지하철", 3_000, LocalDate.of(2024, 9, 2), 40, emptyList()) + + every { cardinalGetService.findByAdminSide(40) } returns mockk() + every { accountRepository.findByCardinal(40) } returns account + every { receiptRepository.save(any()) } returns savedReceipt + every { fileMapper.toFileList(emptyList(), FileOwnerType.RECEIPT, savedReceipt.id) } returns emptyList() + + useCase.save(dto) + + verify(exactly = 1) { receiptRepository.save(any()) } + verify(exactly = 1) { fileRepository.saveAll(emptyList()) } + } + } + + context("존재하지 않는 기수로 저장 시") { + it("AccountNotFoundException을 던진다") { + val dto = ReceiptSaveRequest("간식비", "편의점", 5_000, LocalDate.of(2024, 9, 1), 99, null) + + every { cardinalGetService.findByAdminSide(99) } returns mockk() + every { accountRepository.findByCardinal(99) } returns null + + shouldThrow { useCase.save(dto) } + } + } + } + + describe("update") { + it("업데이트 파일이 있으면 기존 파일을 삭제 후 새 파일을 저장한다") { + val receiptId = 10L + val account = AccountTestFixture.createAccount(cardinal = 40) + val receipt = ReceiptTestFixture.createReceipt(id = receiptId, amount = 1_000, account = account) + account.spend(Money.of(receipt.amount)) + val dto = + ReceiptUpdateRequest( + "desc", + "source", + 2_000, + LocalDate.of(2026, 1, 1), + 40, + listOf(FileSaveRequest("new.png", "TEMP/2026-02/new.png", 100L, "image/png")), + ) + val oldFiles = listOf(mockk()) + val newFiles = listOf(mockk()) + + every { cardinalGetService.findByAdminSide(dto.cardinal) } returns mockk() + every { accountRepository.findByCardinal(dto.cardinal) } returns account + + every { receiptRepository.findById(receiptId) } returns Optional.of(receipt) + every { fileReader.findAll(FileOwnerType.RECEIPT, receiptId, null) } returns oldFiles + every { fileMapper.toFileList(dto.files, FileOwnerType.RECEIPT, receiptId) } returns newFiles + + useCase.update(receiptId, dto) + + verify(exactly = 1) { fileRepository.deleteAll(oldFiles) } + verify(exactly = 1) { fileRepository.saveAll(newFiles) } + } + + it("다른 기수의 장부에 속한 영수증을 수정하면 ReceiptAccountMismatchException을 던진다") { + val receiptId = 20L + val accountA = AccountTestFixture.createAccount(id = 1L, cardinal = 40) + val accountB = AccountTestFixture.createAccount(id = 2L, cardinal = 41) + val receipt = ReceiptTestFixture.createReceipt(id = receiptId, amount = 1_000, account = accountB) + val dto = ReceiptUpdateRequest("desc", "source", 2_000, LocalDate.of(2026, 1, 1), 40, null) + + every { cardinalGetService.findByAdminSide(dto.cardinal) } returns mockk() + every { accountRepository.findByCardinal(dto.cardinal) } returns accountA + every { receiptRepository.findById(receiptId) } returns Optional.of(receipt) + + shouldThrow { useCase.update(receiptId, dto) } + } + + it("빈 리스트로 업데이트 시 기존 파일을 모두 삭제한다") { + val receiptId = 11L + val account = AccountTestFixture.createAccount(cardinal = 40) + val receipt = ReceiptTestFixture.createReceipt(id = receiptId, amount = 1_000, account = account) + account.spend(Money.of(receipt.amount)) + val dto = + ReceiptUpdateRequest( + "desc", + "source", + 2_000, + LocalDate.of(2026, 1, 1), + 40, + emptyList(), + ) + val oldFiles = listOf(mockk()) + + every { cardinalGetService.findByAdminSide(dto.cardinal) } returns mockk() + every { accountRepository.findByCardinal(dto.cardinal) } returns account + every { receiptRepository.findById(receiptId) } returns Optional.of(receipt) + every { fileReader.findAll(FileOwnerType.RECEIPT, receiptId, null) } returns oldFiles + every { fileMapper.toFileList(emptyList(), FileOwnerType.RECEIPT, receiptId) } returns emptyList() + + useCase.update(receiptId, dto) + + verify(exactly = 1) { fileRepository.deleteAll(oldFiles) } + verify(exactly = 1) { fileRepository.saveAll(emptyList()) } + } + } + + describe("delete") { + it("관련 파일 삭제 후 cancelSpend가 호출되고 영수증이 삭제된다") { + val receiptId = 5L + val account = AccountTestFixture.createAccount(currentAmount = 100_000) + val receipt = ReceiptTestFixture.createReceipt(id = receiptId, amount = 10_000, account = account) + account.spend(Money.of(receipt.amount)) + val files = listOf(mockk()) + + every { receiptRepository.findById(receiptId) } returns Optional.of(receipt) + every { fileReader.findAll(FileOwnerType.RECEIPT, receiptId, null) } returns files + + useCase.delete(receiptId) + + verify(exactly = 1) { fileRepository.deleteAll(files) } + verify(exactly = 1) { receiptRepository.delete(receipt) } + } + } + }) diff --git a/src/test/kotlin/com/weeth/domain/account/application/usecase/query/GetAccountQueryServiceTest.kt b/src/test/kotlin/com/weeth/domain/account/application/usecase/query/GetAccountQueryServiceTest.kt new file mode 100644 index 00000000..d4a28466 --- /dev/null +++ b/src/test/kotlin/com/weeth/domain/account/application/usecase/query/GetAccountQueryServiceTest.kt @@ -0,0 +1,89 @@ +package com.weeth.domain.account.application.usecase.query + +import com.weeth.domain.account.application.exception.AccountNotFoundException +import com.weeth.domain.account.application.mapper.AccountMapper +import com.weeth.domain.account.application.mapper.ReceiptMapper +import com.weeth.domain.account.domain.repository.AccountRepository +import com.weeth.domain.account.domain.repository.ReceiptRepository +import com.weeth.domain.account.fixture.AccountTestFixture +import com.weeth.domain.account.fixture.ReceiptTestFixture +import com.weeth.domain.file.application.mapper.FileMapper +import com.weeth.domain.file.domain.entity.FileOwnerType +import com.weeth.domain.file.domain.repository.FileReader +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.shouldBe +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify + +class GetAccountQueryServiceTest : + DescribeSpec({ + val accountRepository = mockk() + val receiptRepository = mockk() + val fileReader = mockk() + val accountMapper = mockk() + val receiptMapper = mockk() + val fileMapper = mockk() + val queryService = + GetAccountQueryService( + accountRepository, + receiptRepository, + fileReader, + accountMapper, + receiptMapper, + fileMapper, + ) + + beforeTest { + clearMocks(accountRepository, receiptRepository, fileReader, accountMapper, receiptMapper, fileMapper) + } + + describe("findByCardinal") { + context("존재하는 기수 조회 시") { + it("영수증이 있으면 fileReader.findAll을 receiptIds 배치로 1회 호출한다") { + val account = AccountTestFixture.createAccount(cardinal = 40) + val receipt1 = ReceiptTestFixture.createReceipt(id = 1L, account = account) + val receipt2 = ReceiptTestFixture.createReceipt(id = 2L, account = account) + val accountResponse = mockk() + + every { accountRepository.findByCardinal(40) } returns account + every { receiptRepository.findAllByAccountIdOrderByCreatedAtDesc(account.id) } returns + listOf(receipt1, receipt2) + every { fileReader.findAll(FileOwnerType.RECEIPT, listOf(1L, 2L), null) } returns emptyList() + every { fileMapper.toFileResponse(any()) } returns mockk() + every { receiptMapper.toResponses(any(), any()) } returns emptyList() + every { accountMapper.toResponse(account, emptyList()) } returns accountResponse + + val result = queryService.findByCardinal(40) + + result shouldBe accountResponse + verify(exactly = 1) { fileReader.findAll(FileOwnerType.RECEIPT, listOf(1L, 2L), null) } + } + + it("영수증이 없으면 fileReader.findAll을 빈 리스트로 호출한다") { + val account = AccountTestFixture.createAccount(cardinal = 40) + val accountResponse = mockk() + + every { accountRepository.findByCardinal(40) } returns account + every { receiptRepository.findAllByAccountIdOrderByCreatedAtDesc(account.id) } returns emptyList() + every { fileReader.findAll(FileOwnerType.RECEIPT, emptyList(), null) } returns emptyList() + every { receiptMapper.toResponses(emptyList(), emptyMap()) } returns emptyList() + every { accountMapper.toResponse(account, emptyList()) } returns accountResponse + + queryService.findByCardinal(40) + + verify(exactly = 1) { fileReader.findAll(FileOwnerType.RECEIPT, emptyList(), null) } + } + } + + context("존재하지 않는 기수 조회 시") { + it("AccountNotFoundException을 던진다") { + every { accountRepository.findByCardinal(99) } returns null + + shouldThrow { queryService.findByCardinal(99) } + } + } + } + }) diff --git a/src/test/kotlin/com/weeth/domain/account/domain/entity/AccountTest.kt b/src/test/kotlin/com/weeth/domain/account/domain/entity/AccountTest.kt new file mode 100644 index 00000000..0dc3cdd3 --- /dev/null +++ b/src/test/kotlin/com/weeth/domain/account/domain/entity/AccountTest.kt @@ -0,0 +1,54 @@ +package com.weeth.domain.account.domain.entity + +import com.weeth.domain.account.domain.vo.Money +import com.weeth.domain.account.fixture.AccountTestFixture +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe + +class AccountTest : + StringSpec({ + "spend은 currentAmount를 Money 금액만큼 감소시킨다" { + val account = AccountTestFixture.createAccount(currentAmount = 100_000) + + account.spend(Money.of(10_000)) + + account.currentAmount shouldBe 90_000 + } + + "cancelSpend은 currentAmount를 Money 금액만큼 복원한다" { + val account = AccountTestFixture.createAccount(currentAmount = 90_000) + + account.cancelSpend(Money.of(10_000)) + + account.currentAmount shouldBe 100_000 + } + + "adjustSpend는 기존 금액을 취소하고 새 금액을 차감한다" { + val account = AccountTestFixture.createAccount(totalAmount = 100_000, currentAmount = 90_000) + + account.adjustSpend(Money.of(10_000), Money.of(20_000)) + + account.currentAmount shouldBe 80_000 + } + + "spend 시 잔액이 부족하면 IllegalStateException을 던진다" { + val account = AccountTestFixture.createAccount(currentAmount = 5_000) + + shouldThrow { account.spend(Money.of(10_000)) } + } + + "cancelSpend 시 총액을 초과하면 IllegalStateException을 던진다" { + val account = AccountTestFixture.createAccount(totalAmount = 100_000, currentAmount = 100_000) + + shouldThrow { account.cancelSpend(Money.of(1)) } + } + + "create는 currentAmount를 totalAmount와 동일하게 초기화한다" { + val account = Account.create("2학기 회비", 200_000, 41) + + account.currentAmount shouldBe 200_000 + account.totalAmount shouldBe 200_000 + account.cardinal shouldBe 41 + } + }) diff --git a/src/test/kotlin/com/weeth/domain/account/domain/entity/ReceiptTest.kt b/src/test/kotlin/com/weeth/domain/account/domain/entity/ReceiptTest.kt new file mode 100644 index 00000000..ac753511 --- /dev/null +++ b/src/test/kotlin/com/weeth/domain/account/domain/entity/ReceiptTest.kt @@ -0,0 +1,35 @@ +package com.weeth.domain.account.domain.entity + +import com.weeth.domain.account.fixture.ReceiptTestFixture +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe +import java.time.LocalDate + +class ReceiptTest : + StringSpec({ + "update는 영수증 필드를 갱신한다" { + val receipt = + ReceiptTestFixture.createReceipt( + description = "기존 설명", + source = "기존 출처", + amount = 5_000, + date = LocalDate.of(2024, 1, 1), + ) + + receipt.update("새로운 설명", "새 출처", 20_000, LocalDate.of(2025, 6, 1)) + + receipt.description shouldBe "새로운 설명" + receipt.source shouldBe "새 출처" + receipt.amount shouldBe 20_000 + receipt.date shouldBe LocalDate.of(2025, 6, 1) + } + + "update 시 amount가 0 이하면 IllegalArgumentException을 던진다" { + val receipt = ReceiptTestFixture.createReceipt(amount = 5_000) + + shouldThrow { + receipt.update("설명", "출처", 0, LocalDate.of(2025, 6, 1)) + } + } + }) diff --git a/src/test/kotlin/com/weeth/domain/account/fixture/AccountTestFixture.kt b/src/test/kotlin/com/weeth/domain/account/fixture/AccountTestFixture.kt new file mode 100644 index 00000000..1b515748 --- /dev/null +++ b/src/test/kotlin/com/weeth/domain/account/fixture/AccountTestFixture.kt @@ -0,0 +1,20 @@ +package com.weeth.domain.account.fixture + +import com.weeth.domain.account.domain.entity.Account + +object AccountTestFixture { + fun createAccount( + id: Long = 1L, + description: String = "2024년 2학기 회비", + totalAmount: Int = 100_000, + currentAmount: Int = 100_000, + cardinal: Int = 40, + ): Account = + Account( + id = id, + description = description, + totalAmount = totalAmount, + currentAmount = currentAmount, + cardinal = cardinal, + ) +} diff --git a/src/test/kotlin/com/weeth/domain/account/fixture/ReceiptTestFixture.kt b/src/test/kotlin/com/weeth/domain/account/fixture/ReceiptTestFixture.kt new file mode 100644 index 00000000..b02c7535 --- /dev/null +++ b/src/test/kotlin/com/weeth/domain/account/fixture/ReceiptTestFixture.kt @@ -0,0 +1,24 @@ +package com.weeth.domain.account.fixture + +import com.weeth.domain.account.domain.entity.Account +import com.weeth.domain.account.domain.entity.Receipt +import java.time.LocalDate + +object ReceiptTestFixture { + fun createReceipt( + id: Long = 1L, + description: String = "간식비", + source: String = "편의점", + amount: Int = 10_000, + date: LocalDate = LocalDate.of(2024, 9, 1), + account: Account = AccountTestFixture.createAccount(), + ): Receipt = + Receipt( + id = id, + description = description, + source = source, + amount = amount, + date = date, + account = account, + ) +}