diff --git a/src/main/java/com/eatsfine/eatsfine/domain/payment/controller/PaymentController.java b/src/main/java/com/eatsfine/eatsfine/domain/payment/controller/PaymentController.java index dc3d4a1..83c10d6 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/payment/controller/PaymentController.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/payment/controller/PaymentController.java @@ -13,6 +13,9 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.GetMapping; @Tag(name = "Payment", description = "결제 관련 API") @RestController @@ -35,4 +38,30 @@ public ApiResponse confirmPayment( @RequestBody @Valid PaymentConfirmDTO dto) { return ApiResponse.onSuccess(paymentService.confirmPayment(dto)); } + + @Operation(summary = "결제 취소", description = "결제 키를 받아 결제를 취소합니다.") + @PostMapping("/{paymentKey}/cancel") + public ApiResponse cancelPayment( + @PathVariable String paymentKey, + @RequestBody @Valid PaymentRequestDTO.CancelPaymentDTO dto) { + return ApiResponse.onSuccess(paymentService.cancelPayment(paymentKey, dto)); + } + + @Operation(summary = "결제 내역 조회", description = "로그인한 사용자의 결제 내역을 조회합니다.") + @GetMapping + public ApiResponse getPaymentList( + @RequestParam(name = "userId", required = false, defaultValue = "1") Long userId, + @RequestParam(name = "page", defaultValue = "1") Integer page, + @RequestParam(name = "limit", defaultValue = "10") Integer limit, + @RequestParam(name = "status", required = false) String status) { + // TODO: userId는 추후 Security Context에서 가져오도록 수정 + return ApiResponse.onSuccess(paymentService.getPaymentList(userId, page, limit, status)); + } + + @Operation(summary = "결제 상세 조회", description = "특정 결제 건의 상세 내역을 조회합니다.") + @GetMapping("/{paymentId}") + public ApiResponse getPaymentDetail( + @PathVariable Long paymentId) { + return ApiResponse.onSuccess(paymentService.getPaymentDetail(paymentId)); + } } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/payment/dto/request/PaymentRequestDTO.java b/src/main/java/com/eatsfine/eatsfine/domain/payment/dto/request/PaymentRequestDTO.java index a7dc788..12929ef 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/payment/dto/request/PaymentRequestDTO.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/payment/dto/request/PaymentRequestDTO.java @@ -4,7 +4,13 @@ public class PaymentRequestDTO { - public record RequestPaymentDTO( - @NotNull Long bookingId) { - } + public record RequestPaymentDTO( + @NotNull Long bookingId) { + } + + public record CancelPaymentDTO( + @NotNull String cancelReason) { + + } + } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/payment/dto/response/PaymentResponseDTO.java b/src/main/java/com/eatsfine/eatsfine/domain/payment/dto/response/PaymentResponseDTO.java index 8f100ed..9a00648 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/payment/dto/response/PaymentResponseDTO.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/payment/dto/response/PaymentResponseDTO.java @@ -4,6 +4,7 @@ import com.eatsfine.eatsfine.domain.payment.enums.PaymentStatus; import java.time.LocalDateTime; +import java.util.List; public class PaymentResponseDTO { @@ -14,4 +15,50 @@ public record PaymentRequestResultDTO( Integer amount, LocalDateTime requestedAt) { } + + public record CancelPaymentResultDTO( + Long paymentId, + String orderId, + String paymentKey, + String status, + LocalDateTime canceledAt) { + } + + public record PaymentHistoryResultDTO( + Long paymentId, + Long bookingId, + String restaurantName, + Integer amount, + String paymentType, + String paymentMethod, + String paymentProvider, + String status, + LocalDateTime approvedAt) { + } + + public record PaymentListResponseDTO( + List payments, + PaginationDTO pagination) { + } + + public record PaginationDTO( + Integer currentPage, + Integer totalPages, + Long totalCount) { + } + + public record PaymentDetailResultDTO( + Long paymentId, + Long bookingId, + String restaurantName, + String paymentMethod, + String paymentProvider, + Integer amount, + String paymentType, + String status, + LocalDateTime requestedAt, + LocalDateTime approvedAt, + String receiptUrl, + String refundInfo) { + } } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/payment/dto/response/TossPaymentResponse.java b/src/main/java/com/eatsfine/eatsfine/domain/payment/dto/response/TossPaymentResponse.java index 5e41d6f..bc6fc4e 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/payment/dto/response/TossPaymentResponse.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/payment/dto/response/TossPaymentResponse.java @@ -19,11 +19,16 @@ public record TossPaymentResponse( String lastTransactionKey, Integer suppliedAmount, Integer vat, - EasyPay easyPay) { + EasyPay easyPay, + Receipt receipt) { public record EasyPay( String provider, Integer amount, Integer discountAmount) { } + + public record Receipt( + String url) { + } } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/payment/entity/Payment.java b/src/main/java/com/eatsfine/eatsfine/domain/payment/entity/Payment.java index ae339a7..ebf4415 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/payment/entity/Payment.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/payment/entity/Payment.java @@ -59,16 +59,28 @@ public class Payment extends BaseEntity { @Column(name = "payment_type", nullable = false) private PaymentType paymentType; + @Column(name = "receipt_url") + private String receiptUrl; + public void setPaymentKey(String paymentKey) { this.paymentKey = paymentKey; } public void completePayment(LocalDateTime approvedAt, PaymentMethod method, String paymentKey, - PaymentProvider provider) { + PaymentProvider provider, String receiptUrl) { this.paymentStatus = PaymentStatus.COMPLETED; this.approvedAt = approvedAt; this.paymentMethod = method; this.paymentKey = paymentKey; this.paymentProvider = provider; + this.receiptUrl = receiptUrl; + } + + public void failPayment() { + this.paymentStatus = PaymentStatus.FAILED; + } + + public void cancelPayment() { + this.paymentStatus = PaymentStatus.REFUNDED; } } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/payment/exception/PaymentException.java b/src/main/java/com/eatsfine/eatsfine/domain/payment/exception/PaymentException.java new file mode 100644 index 0000000..c6d2580 --- /dev/null +++ b/src/main/java/com/eatsfine/eatsfine/domain/payment/exception/PaymentException.java @@ -0,0 +1,11 @@ +package com.eatsfine.eatsfine.domain.payment.exception; + +import com.eatsfine.eatsfine.global.apiPayload.code.BaseErrorCode; +import com.eatsfine.eatsfine.global.apiPayload.exception.GeneralException; + +public class PaymentException extends GeneralException { + + public PaymentException(BaseErrorCode code) { + super(code); + } +} diff --git a/src/main/java/com/eatsfine/eatsfine/domain/payment/repository/PaymentRepository.java b/src/main/java/com/eatsfine/eatsfine/domain/payment/repository/PaymentRepository.java index 39567ac..91cf70b 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/payment/repository/PaymentRepository.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/payment/repository/PaymentRepository.java @@ -1,10 +1,19 @@ package com.eatsfine.eatsfine.domain.payment.repository; import com.eatsfine.eatsfine.domain.payment.entity.Payment; +import com.eatsfine.eatsfine.domain.payment.enums.PaymentStatus; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import java.util.Optional; public interface PaymentRepository extends JpaRepository { Optional findByOrderId(String orderId); + + Optional findByPaymentKey(String paymentKey); + + Page findAllByBooking_User_Id(Long userId, Pageable pageable); + + Page findAllByBooking_User_IdAndPaymentStatus(Long userId, PaymentStatus status, Pageable pageable); } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/payment/service/PaymentService.java b/src/main/java/com/eatsfine/eatsfine/domain/payment/service/PaymentService.java index 4414afe..e8a9cb9 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/payment/service/PaymentService.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/payment/service/PaymentService.java @@ -12,99 +12,223 @@ import com.eatsfine.eatsfine.domain.payment.enums.PaymentStatus; import com.eatsfine.eatsfine.domain.payment.enums.PaymentType; import com.eatsfine.eatsfine.domain.payment.repository.PaymentRepository; +import com.eatsfine.eatsfine.domain.payment.exception.PaymentException; +import com.eatsfine.eatsfine.domain.payment.status.PaymentErrorStatus; import com.eatsfine.eatsfine.global.apiPayload.code.status.ErrorStatus; import com.eatsfine.eatsfine.global.apiPayload.exception.GeneralException; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.client.RestClient; import java.time.LocalDateTime; import java.util.UUID; +import java.util.List; +import java.util.stream.Collectors; +@Slf4j @Service @RequiredArgsConstructor public class PaymentService { - private final PaymentRepository paymentRepository; - private final BookingRepository bookingRepository; - private final RestClient tossPaymentClient; - - @Transactional - public PaymentResponseDTO.PaymentRequestResultDTO requestPayment(PaymentRequestDTO.RequestPaymentDTO dto) { - Booking booking = bookingRepository.findById(dto.bookingId()) - .orElseThrow(() -> new GeneralException(ErrorStatus.BOOKING_NOT_FOUND)); - - // 주문 ID 생성 - String orderId = UUID.randomUUID().toString(); - - // 예약금 검증 - if (booking.getDepositAmount() == null || booking.getDepositAmount() <= 0) { - throw new GeneralException(ErrorStatus.PAYMENT_INVALID_DEPOSIT); + private final PaymentRepository paymentRepository; + private final BookingRepository bookingRepository; + private final RestClient tossPaymentClient; + + @Transactional + public PaymentResponseDTO.PaymentRequestResultDTO requestPayment(PaymentRequestDTO.RequestPaymentDTO dto) { + Booking booking = bookingRepository.findById(dto.bookingId()) + .orElseThrow(() -> new PaymentException(PaymentErrorStatus._BOOKING_NOT_FOUND)); + + // 주문 ID 생성 + String orderId = UUID.randomUUID().toString(); + + // 예약금 검증 + if (booking.getDepositAmount() == null || booking.getDepositAmount() <= 0) { + throw new PaymentException(PaymentErrorStatus._PAYMENT_INVALID_DEPOSIT); + } + + Payment payment = Payment.builder() + .booking(booking) + .orderId(orderId) + .amount(booking.getDepositAmount()) + .paymentStatus(PaymentStatus.PENDING) + .paymentType(PaymentType.DEPOSIT) + .requestedAt(LocalDateTime.now()) + .build(); + + Payment savedPayment = paymentRepository.save(payment); + + return new PaymentResponseDTO.PaymentRequestResultDTO( + savedPayment.getId(), + booking.getId(), + savedPayment.getOrderId(), + savedPayment.getAmount(), + savedPayment.getRequestedAt()); } - Payment payment = Payment.builder() - .booking(booking) - .orderId(orderId) - .amount(booking.getDepositAmount()) - .paymentStatus(PaymentStatus.PENDING) - .paymentType(PaymentType.DEPOSIT) - .requestedAt(LocalDateTime.now()) - .build(); - - Payment savedPayment = paymentRepository.save(payment); - - return new PaymentResponseDTO.PaymentRequestResultDTO( - savedPayment.getId(), - booking.getId(), - savedPayment.getOrderId(), - savedPayment.getAmount(), - savedPayment.getRequestedAt()); - } - - @Transactional - public PaymentResponseDTO.PaymentRequestResultDTO confirmPayment(PaymentConfirmDTO dto) { - Payment payment = paymentRepository.findByOrderId(dto.orderId()) - .orElseThrow(() -> new GeneralException(ErrorStatus.PAYMENT_NOT_FOUND)); - - if (!payment.getAmount().equals(dto.amount())) { - throw new GeneralException(ErrorStatus.PAYMENT_INVALID_AMOUNT); + @Transactional(noRollbackFor = GeneralException.class) + public PaymentResponseDTO.PaymentRequestResultDTO confirmPayment(PaymentConfirmDTO dto) { + Payment payment = paymentRepository.findByOrderId(dto.orderId()) + .orElseThrow(() -> new PaymentException(PaymentErrorStatus._PAYMENT_NOT_FOUND)); + + if (!payment.getAmount().equals(dto.amount())) { + payment.failPayment(); + throw new PaymentException(PaymentErrorStatus._PAYMENT_INVALID_AMOUNT); + } + + // 토스 API 호출 + TossPaymentResponse response; + try { + response = tossPaymentClient.post() + .uri("/v1/payments/confirm") + .body(dto) + .retrieve() + .body(TossPaymentResponse.class); + + if (response == null || !"DONE".equals(response.status())) { + log.error("Toss Payment Confirmation Failed: Status is not DONE"); + payment.failPayment(); + throw new GeneralException(ErrorStatus._INTERNAL_SERVER_ERROR); + } + } catch (Exception e) { + log.error("Toss Payment API Error", e); + payment.failPayment(); + throw new GeneralException(ErrorStatus._INTERNAL_SERVER_ERROR); + } + + // Provider 파싱 + PaymentProvider provider = null; + if (response.easyPay() != null) { + String providerCode = response.easyPay().provider(); + if ("토스페이".equals(providerCode)) { + provider = PaymentProvider.TOSS; + } else if ("카카오페이".equals(providerCode)) { + provider = PaymentProvider.KAKAOPAY; + } + } + + payment.completePayment( + response.approvedAt() != null ? response.approvedAt().toLocalDateTime() + : LocalDateTime.now(), + PaymentMethod.SIMPLE_PAYMENT, + response.paymentKey(), + provider, + response.receipt() != null ? response.receipt().url() : null); + + log.info("Payment confirmed for OrderID: {}", dto.orderId()); + + return new PaymentResponseDTO.PaymentRequestResultDTO( + payment.getId(), + payment.getBooking().getId(), + payment.getOrderId(), + payment.getAmount(), + payment.getRequestedAt()); } - // 토스 API 호출 - TossPaymentResponse response = tossPaymentClient.post() - .uri("/v1/payments/confirm") - .body(dto) - .retrieve() - .body(TossPaymentResponse.class); - - if (response == null || !"DONE".equals(response.status())) { - throw new GeneralException(ErrorStatus._INTERNAL_SERVER_ERROR); + @Transactional(noRollbackFor = GeneralException.class) + public PaymentResponseDTO.CancelPaymentResultDTO cancelPayment(String paymentKey, + PaymentRequestDTO.CancelPaymentDTO dto) { + Payment payment = paymentRepository.findByPaymentKey(paymentKey) + .orElseThrow(() -> new PaymentException(PaymentErrorStatus._PAYMENT_NOT_FOUND)); + + // 토스 결제 취소 API 호출 + TossPaymentResponse response; + try { + response = tossPaymentClient.post() + .uri("/v1/payments/" + paymentKey + "/cancel") + .body(dto) + .retrieve() + .body(TossPaymentResponse.class); + + if (response == null || !"CANCELED".equals(response.status())) { + log.error("Toss Payment Cancel Failed: {}", response); + throw new GeneralException(ErrorStatus._INTERNAL_SERVER_ERROR); + } + } catch (Exception e) { + log.error("Toss Payment Cancel API Error", e); + throw new GeneralException(ErrorStatus._INTERNAL_SERVER_ERROR); + } + + payment.cancelPayment(); + + return new PaymentResponseDTO.CancelPaymentResultDTO( + payment.getId(), + payment.getOrderId(), + payment.getPaymentKey(), + payment.getPaymentStatus().name(), + LocalDateTime.now()); } - // Provider 파싱 - PaymentProvider provider = null; - if (response.easyPay() != null) { - String providerCode = response.easyPay().provider(); - if ("토스페이".equals(providerCode)) { - provider = PaymentProvider.TOSS; - } else if ("카카오페이".equals(providerCode)) { - provider = PaymentProvider.KAKAOPAY; - } + @Transactional(readOnly = true) + public PaymentResponseDTO.PaymentListResponseDTO getPaymentList(Long userId, Integer page, Integer limit, + String status) { + // limit 기본값 처리 (만약 null이면 10) + int size = (limit != null) ? limit : 10; + // page 기본값 처리 (만약 null이면 1, 0보다 작으면 1로 보정). Spring Data는 0-based index이므로 -1 + int pageNumber = (page != null && page > 0) ? page - 1 : 0; + + Pageable pageable = org.springframework.data.domain.PageRequest.of(pageNumber, size); + + Page paymentPage; + if (status != null && !status.isEmpty()) { + PaymentStatus paymentStatus; + try { + paymentStatus = PaymentStatus.valueOf(status.toUpperCase()); + } catch (IllegalArgumentException e) { + // 유효하지 않은 status가 들어오면 BadRequest 예외 발생 + throw new GeneralException(ErrorStatus._BAD_REQUEST); + } + paymentPage = paymentRepository.findAllByBooking_User_IdAndPaymentStatus(userId, paymentStatus, + pageable); + } else { + paymentPage = paymentRepository.findAllByBooking_User_Id(userId, pageable); + } + + List payments = paymentPage.getContent().stream() + .map(payment -> new PaymentResponseDTO.PaymentHistoryResultDTO( + payment.getId(), + payment.getBooking().getId(), + payment.getBooking().getStore().getStoreName(), + payment.getAmount(), + payment.getPaymentType().name(), + payment.getPaymentMethod() != null ? payment.getPaymentMethod().name() + : null, + payment.getPaymentProvider() != null ? payment.getPaymentProvider().name() + : null, + payment.getPaymentStatus().name(), + payment.getApprovedAt())) + .collect(Collectors.toList()); + + PaymentResponseDTO.PaginationDTO pagination = new PaymentResponseDTO.PaginationDTO( + paymentPage.getNumber() + 1, // 0-based -> 1-based + paymentPage.getTotalPages(), + paymentPage.getTotalElements()); + + return new PaymentResponseDTO.PaymentListResponseDTO(payments, pagination); } - payment.completePayment( - response.approvedAt() != null ? response.approvedAt().toLocalDateTime() : LocalDateTime.now(), - PaymentMethod.SIMPLE_PAYMENT, - response.paymentKey(), - provider - ); - - return new PaymentResponseDTO.PaymentRequestResultDTO( - payment.getId(), - payment.getBooking().getId(), - payment.getOrderId(), - payment.getAmount(), - payment.getRequestedAt()); - } + @Transactional(readOnly = true) + public PaymentResponseDTO.PaymentDetailResultDTO getPaymentDetail(Long paymentId) { + Payment payment = paymentRepository.findById(paymentId) + .orElseThrow(() -> new PaymentException(PaymentErrorStatus._PAYMENT_NOT_FOUND)); + + return new PaymentResponseDTO.PaymentDetailResultDTO( + payment.getId(), + payment.getBooking().getId(), + payment.getBooking().getStore().getStoreName(), + payment.getPaymentMethod() != null ? payment.getPaymentMethod().name() : null, + payment.getPaymentProvider() != null ? payment.getPaymentProvider().name() : null, + payment.getAmount(), + payment.getPaymentType().name(), + payment.getPaymentStatus().name(), + payment.getRequestedAt(), + payment.getApprovedAt(), + payment.getReceiptUrl(), + null // 환불 상세 정보는 현재 null 처리 + ); + } } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/payment/status/PaymentErrorStatus.java b/src/main/java/com/eatsfine/eatsfine/domain/payment/status/PaymentErrorStatus.java new file mode 100644 index 0000000..e6dddaa --- /dev/null +++ b/src/main/java/com/eatsfine/eatsfine/domain/payment/status/PaymentErrorStatus.java @@ -0,0 +1,40 @@ +package com.eatsfine.eatsfine.domain.payment.status; + +import com.eatsfine.eatsfine.global.apiPayload.code.BaseErrorCode; +import com.eatsfine.eatsfine.global.apiPayload.code.ErrorReasonDto; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum PaymentErrorStatus implements BaseErrorCode { + + _PAYMENT_INVALID_DEPOSIT(HttpStatus.BAD_REQUEST, "PAYMENT4001", "예약금이 유효하지 않습니다."), + _PAYMENT_INVALID_AMOUNT(HttpStatus.BAD_REQUEST, "PAYMENT4002", "결제 금액이 일치하지 않습니다."), + _PAYMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "PAYMENT4003", "결제 정보를 찾을 수 없습니다."), + _BOOKING_NOT_FOUND(HttpStatus.NOT_FOUND, "BOOKING4001", "예약을 찾을 수 없습니다."); + + private final HttpStatus httpStatus; + private final String code; + private final String message; + + @Override + public ErrorReasonDto getReason() { + return ErrorReasonDto.builder() + .isSuccess(false) + .code(code) + .message(message) + .build(); + } + + @Override + public ErrorReasonDto getReasonHttpStatus() { + return ErrorReasonDto.builder() + .httpStatus(httpStatus) + .isSuccess(false) + .code(code) + .message(message) + .build(); + } +} diff --git a/src/main/java/com/eatsfine/eatsfine/global/apiPayload/code/status/ErrorStatus.java b/src/main/java/com/eatsfine/eatsfine/global/apiPayload/code/status/ErrorStatus.java index f10d8c8..2d63881 100644 --- a/src/main/java/com/eatsfine/eatsfine/global/apiPayload/code/status/ErrorStatus.java +++ b/src/main/java/com/eatsfine/eatsfine/global/apiPayload/code/status/ErrorStatus.java @@ -13,15 +13,7 @@ public enum ErrorStatus implements BaseErrorCode { _BAD_REQUEST(HttpStatus.BAD_REQUEST, "COMMON400", "잘못된 요청입니다."), _UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "COMMON401", "인증이 필요합니다."), _FORBIDDEN(HttpStatus.FORBIDDEN, "COMMON403", "금지된 요청입니다."), - _NOT_FOUND(HttpStatus.NOT_FOUND, "COMMON404", "존재하지 않는 요청입니다."), - - // 예약금 관련 에러 - PAYMENT_INVALID_DEPOSIT(HttpStatus.BAD_REQUEST, "PAYMENT4001", "예약금이 유효하지 않습니다."), - PAYMENT_INVALID_AMOUNT(HttpStatus.BAD_REQUEST, "PAYMENT4002", "결제 금액이 일치하지 않습니다."), - PAYMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "PAYMENT4003", "결제 정보를 찾을 수 없습니다."), - - // 예약 관련 에러 - BOOKING_NOT_FOUND(HttpStatus.NOT_FOUND, "BOOKING4001", "예약을 찾을 수 없습니다."); + _NOT_FOUND(HttpStatus.NOT_FOUND, "COMMON404", "존재하지 않는 요청입니다."); private final HttpStatus httpStatus; private final String code; diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index e65dfae..1623481 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -22,4 +22,4 @@ cloud: region: test-region s3: bucket: test-bucket - base-url: https://test-bucket.s3.test-region.amazonaws.com \ No newline at end of file + base-url: https://test-bucket.s3.test-region.amazonaws.com