diff --git a/build.gradle b/build.gradle index 8a1403ac..68d9a7dc 100644 --- a/build.gradle +++ b/build.gradle @@ -35,17 +35,14 @@ apply { dependencies { /** spring boot starter */ implementation 'org.springframework.boot:spring-boot-starter-data-jpa' -// implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-web' + + /** Json Web Token */ implementation "io.jsonwebtoken:jjwt-api:${jjwt_version}" implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:${swagger_version}" testImplementation 'org.springframework.boot:spring-boot-starter-test' -// testImplementation 'org.springframework.security:spring-security-test' - - /** thymeleaf and spring security integration */ -// implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6' /** lombok */ compileOnly 'org.projectlombok:lombok' @@ -57,8 +54,6 @@ dependencies { runtimeOnly 'com.h2database:h2' runtimeOnly 'com.mysql:mysql-connector-j' - runtimeOnly "io.jsonwebtoken:jjwt-impl:${jjwt_version}" - runtimeOnly "io.jsonwebtoken:jjwt-jackson:${jjwt_version}" testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } diff --git a/src/main/java/com/ordertogether/team14_be/auth/application/service/AuthService.java b/src/main/java/com/ordertogether/team14_be/auth/application/service/AuthService.java index b94808cd..7ec5e4d1 100644 --- a/src/main/java/com/ordertogether/team14_be/auth/application/service/AuthService.java +++ b/src/main/java/com/ordertogether/team14_be/auth/application/service/AuthService.java @@ -3,7 +3,7 @@ import com.ordertogether.team14_be.auth.JwtUtil; import com.ordertogether.team14_be.auth.application.dto.KakaoUserInfo; import com.ordertogether.team14_be.auth.presentation.KakaoClient; -import com.ordertogether.team14_be.memebr.application.service.MemberService; +import com.ordertogether.team14_be.member.application.service.MemberService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; diff --git a/src/main/java/com/ordertogether/team14_be/common/util/IdempotentKeyGenerator.java b/src/main/java/com/ordertogether/team14_be/common/util/IdempotentKeyGenerator.java new file mode 100644 index 00000000..bfe518a5 --- /dev/null +++ b/src/main/java/com/ordertogether/team14_be/common/util/IdempotentKeyGenerator.java @@ -0,0 +1,12 @@ +package com.ordertogether.team14_be.common.util; + +import java.util.UUID; +import lombok.experimental.UtilityClass; + +@UtilityClass +public final class IdempotentKeyGenerator { + + public static String generate(String seed) { + return UUID.nameUUIDFromBytes(seed.getBytes()).toString(); + } +} diff --git a/src/main/java/com/ordertogether/team14_be/common/web/response/ApiResponse.java b/src/main/java/com/ordertogether/team14_be/common/web/response/ApiResponse.java new file mode 100644 index 00000000..23ccb295 --- /dev/null +++ b/src/main/java/com/ordertogether/team14_be/common/web/response/ApiResponse.java @@ -0,0 +1,20 @@ +package com.ordertogether.team14_be.common.web.response; + +import jakarta.annotation.Nullable; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class ApiResponse { + + private Integer status; + private String message; + private T data; + + public static ApiResponse with(HttpStatus httpStatus, String message, @Nullable T data) { + return new ApiResponse<>(httpStatus.value(), message, data); + } +} diff --git a/src/main/java/com/ordertogether/team14_be/config/PersistenceConfig.java b/src/main/java/com/ordertogether/team14_be/config/PersistenceConfig.java index d9de0f5e..8d24b129 100644 --- a/src/main/java/com/ordertogether/team14_be/config/PersistenceConfig.java +++ b/src/main/java/com/ordertogether/team14_be/config/PersistenceConfig.java @@ -1,6 +1,15 @@ package com.ordertogether.team14_be.config; import com.ordertogether.team14_be.common.persistence.auditing.AuditorProvider; +import com.ordertogether.team14_be.payment.persistence.jpa.repository.JpaPaymentEventRepository; +import com.ordertogether.team14_be.payment.persistence.jpa.repository.JpaPaymentOrderRepository; +import com.ordertogether.team14_be.payment.persistence.jpa.repository.JpaProductRepository; +import com.ordertogether.team14_be.payment.persistence.jpa.repository.SimpleJpaPaymentEventRepository; +import com.ordertogether.team14_be.payment.persistence.jpa.repository.SimpleJpaPaymentOrderRepository; +import com.ordertogether.team14_be.payment.persistence.jpa.repository.SimpleJpaProductRepository; +import com.ordertogether.team14_be.payment.persistence.repository.PaymentEventRepository; +import com.ordertogether.team14_be.payment.persistence.repository.PaymentOrderRepository; +import com.ordertogether.team14_be.payment.persistence.repository.ProductRepository; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.domain.AuditorAware; @@ -15,4 +24,24 @@ public class PersistenceConfig { public AuditorAware auditorProvider() { return new AuditorProvider(); } + + @Bean + public PaymentEventRepository paymentEventRepository( + SimpleJpaPaymentEventRepository simpleJpaPaymentEventRepository) { + return new JpaPaymentEventRepository(simpleJpaPaymentEventRepository); + } + + @Bean + public PaymentOrderRepository paymentOrderRepository( + SimpleJpaPaymentOrderRepository simpleJpaPaymentOrderRepository, + SimpleJpaProductRepository simpleJpaProductRepository) { + return new JpaPaymentOrderRepository( + simpleJpaPaymentOrderRepository, simpleJpaProductRepository); + } + + @Bean + public ProductRepository productRepository( + SimpleJpaProductRepository simpleJpaProductRepository) { + return new JpaProductRepository(simpleJpaProductRepository); + } } diff --git a/src/main/java/com/ordertogether/team14_be/memebr/application/service/MemberService.java b/src/main/java/com/ordertogether/team14_be/member/application/service/MemberService.java similarity index 71% rename from src/main/java/com/ordertogether/team14_be/memebr/application/service/MemberService.java rename to src/main/java/com/ordertogether/team14_be/member/application/service/MemberService.java index ee6346b8..6d9c51d3 100644 --- a/src/main/java/com/ordertogether/team14_be/memebr/application/service/MemberService.java +++ b/src/main/java/com/ordertogether/team14_be/member/application/service/MemberService.java @@ -1,7 +1,7 @@ -package com.ordertogether.team14_be.memebr.application.service; +package com.ordertogether.team14_be.member.application.service; -import com.ordertogether.team14_be.memebr.persistence.MemberRepository; -import com.ordertogether.team14_be.memebr.persistence.entity.Member; +import com.ordertogether.team14_be.member.persistence.MemberRepository; +import com.ordertogether.team14_be.member.persistence.entity.Member; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; diff --git a/src/main/java/com/ordertogether/team14_be/memebr/persistence/MemberRepository.java b/src/main/java/com/ordertogether/team14_be/member/persistence/MemberRepository.java similarity index 69% rename from src/main/java/com/ordertogether/team14_be/memebr/persistence/MemberRepository.java rename to src/main/java/com/ordertogether/team14_be/member/persistence/MemberRepository.java index cea83bb6..8cc1815a 100644 --- a/src/main/java/com/ordertogether/team14_be/memebr/persistence/MemberRepository.java +++ b/src/main/java/com/ordertogether/team14_be/member/persistence/MemberRepository.java @@ -1,6 +1,6 @@ -package com.ordertogether.team14_be.memebr.persistence; +package com.ordertogether.team14_be.member.persistence; -import com.ordertogether.team14_be.memebr.persistence.entity.Member; +import com.ordertogether.team14_be.member.persistence.entity.Member; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; diff --git a/src/main/java/com/ordertogether/team14_be/memebr/persistence/entity/Member.java b/src/main/java/com/ordertogether/team14_be/member/persistence/entity/Member.java similarity index 95% rename from src/main/java/com/ordertogether/team14_be/memebr/persistence/entity/Member.java rename to src/main/java/com/ordertogether/team14_be/member/persistence/entity/Member.java index d5338f95..cb06b470 100644 --- a/src/main/java/com/ordertogether/team14_be/memebr/persistence/entity/Member.java +++ b/src/main/java/com/ordertogether/team14_be/member/persistence/entity/Member.java @@ -1,4 +1,4 @@ -package com.ordertogether.team14_be.memebr.persistence.entity; +package com.ordertogether.team14_be.member.persistence.entity; import jakarta.persistence.Column; import jakarta.persistence.Entity; diff --git a/src/main/java/com/ordertogether/team14_be/payment/domain/PaymentEvent.java b/src/main/java/com/ordertogether/team14_be/payment/domain/PaymentEvent.java index 69dc6892..06333daa 100644 --- a/src/main/java/com/ordertogether/team14_be/payment/domain/PaymentEvent.java +++ b/src/main/java/com/ordertogether/team14_be/payment/domain/PaymentEvent.java @@ -1,41 +1,34 @@ package com.ordertogether.team14_be.payment.domain; -import com.ordertogether.team14_be.common.persistence.entity.BaseTimeEntity; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; +import java.math.BigDecimal; +import java.util.List; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.ToString; -import lombok.experimental.SuperBuilder; -@Entity +@Builder @Getter -@SuperBuilder @ToString -@AllArgsConstructor(access = AccessLevel.PRIVATE) -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class PaymentEvent extends BaseTimeEntity { +public class PaymentEvent { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(nullable = false) - private Long buyerId; // 구매자 식별자 + private Long buyerId; + + private List paymentOrders; - @Column(nullable = false) private String orderId; private String orderName; - @Column(nullable = false) - private String paymentKey; // PSP 결제 식별자 + private String paymentKey; + + @Builder.Default private PaymentStatus paymentStatus = PaymentStatus.READY; - @Builder.Default private Boolean isPaymentDone = false; + public Long totalAmount() { + return paymentOrders.stream() + .map(PaymentOrder::getAmount) + .reduce(BigDecimal.ZERO, BigDecimal::add) + .longValue(); + } } diff --git a/src/main/java/com/ordertogether/team14_be/payment/domain/PaymentOrder.java b/src/main/java/com/ordertogether/team14_be/payment/domain/PaymentOrder.java index eaef2452..599d7061 100644 --- a/src/main/java/com/ordertogether/team14_be/payment/domain/PaymentOrder.java +++ b/src/main/java/com/ordertogether/team14_be/payment/domain/PaymentOrder.java @@ -1,53 +1,42 @@ package com.ordertogether.team14_be.payment.domain; -import com.ordertogether.team14_be.common.persistence.entity.BaseTimeEntity; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.ManyToOne; import java.math.BigDecimal; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; +import java.util.Objects; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.ToString; -import lombok.experimental.SuperBuilder; -@Entity +@Builder @Getter -@SuperBuilder @ToString -@AllArgsConstructor(access = AccessLevel.PRIVATE) -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class PaymentOrder extends BaseTimeEntity { +public class PaymentOrder { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(nullable = false) - private Long sellerId; // 판매자 식별자 + private Long productId; - @ManyToOne(fetch = FetchType.LAZY) - private Product productId; - - @Column(nullable = false) private String orderId; - @Column(precision = 10, scale = 2) - private BigDecimal amount; // 결제 금액 + private String orderName; + + private BigDecimal amount; + + public PaymentOrder updateProductInfo(Product product) { + if (isProductMismatch(product)) { + throw new IllegalArgumentException("상품 정보가 일치하지 않습니다."); + } + + this.orderName = product.getName(); + this.amount = product.getPrice(); - @Enumerated(EnumType.STRING) - @Builder.Default - private PaymentOrderStatus paymentOrderStatus = PaymentOrderStatus.READY; + return this; + } - @Builder.Default private Byte retryCount = 0; // 재시도 횟수 + public boolean isMissingProductInfo() { + return Objects.isNull(orderName) && Objects.isNull(amount); + } - @Builder.Default private Byte retryThreshold = 5; // 재시도 허용 임계값 + private boolean isProductMismatch(Product product) { + return !Objects.equals(productId, product.getId()); + } } diff --git a/src/main/java/com/ordertogether/team14_be/payment/domain/PaymentOrderStatus.java b/src/main/java/com/ordertogether/team14_be/payment/domain/PaymentOrderStatus.java deleted file mode 100644 index f72b0506..00000000 --- a/src/main/java/com/ordertogether/team14_be/payment/domain/PaymentOrderStatus.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.ordertogether.team14_be.payment.domain; - -/** 결제 상태 */ -public enum PaymentOrderStatus { - READY, - EXECUTING, - SUCCESS, - FAIL; -} diff --git a/src/main/java/com/ordertogether/team14_be/payment/domain/PaymentStatus.java b/src/main/java/com/ordertogether/team14_be/payment/domain/PaymentStatus.java new file mode 100644 index 00000000..001acfaa --- /dev/null +++ b/src/main/java/com/ordertogether/team14_be/payment/domain/PaymentStatus.java @@ -0,0 +1,16 @@ +package com.ordertogether.team14_be.payment.domain; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** 결제 상태 */ +@Getter +@RequiredArgsConstructor +public enum PaymentStatus { + READY("결제 준비"), + EXECUTING("결제 진행 중"), + SUCCESS("결제 성공"), + FAIL("결제 실패"); + + private final String description; +} diff --git a/src/main/java/com/ordertogether/team14_be/payment/domain/Product.java b/src/main/java/com/ordertogether/team14_be/payment/domain/Product.java index 641fe49e..3cd03891 100644 --- a/src/main/java/com/ordertogether/team14_be/payment/domain/Product.java +++ b/src/main/java/com/ordertogether/team14_be/payment/domain/Product.java @@ -1,34 +1,18 @@ package com.ordertogether.team14_be.payment.domain; -import com.ordertogether.team14_be.common.persistence.entity.BaseTimeEntity; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; import java.math.BigDecimal; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.ToString; -import lombok.experimental.SuperBuilder; -@Entity +@Builder @Getter -@SuperBuilder @ToString -@AllArgsConstructor(access = AccessLevel.PRIVATE) -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class Product extends BaseTimeEntity { +public class Product { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(nullable = false) private String name; - @Column(nullable = false) private BigDecimal price; } diff --git a/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/entity/PaymentEventEntity.java b/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/entity/PaymentEventEntity.java new file mode 100644 index 00000000..6a64f56d --- /dev/null +++ b/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/entity/PaymentEventEntity.java @@ -0,0 +1,49 @@ +package com.ordertogether.team14_be.payment.persistence.jpa.entity; + +import com.ordertogether.team14_be.common.persistence.entity.BaseTimeEntity; +import com.ordertogether.team14_be.payment.domain.PaymentStatus; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import lombok.experimental.SuperBuilder; + +@Entity +@Getter +@SuperBuilder +@ToString +@Table(name = "payment_event") +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class PaymentEventEntity extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private Long buyerId; // 구매자 식별자 + + @Column(nullable = false) + private String orderId; + + @Column(nullable = false) + private String orderName; + + @Column(nullable = false) + private String paymentKey; // PSP 결제 식별자 + + @Builder.Default + @Enumerated(EnumType.STRING) + private PaymentStatus paymentStatus = PaymentStatus.READY; +} diff --git a/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/entity/PaymentOrderEntity.java b/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/entity/PaymentOrderEntity.java new file mode 100644 index 00000000..d86a1bf8 --- /dev/null +++ b/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/entity/PaymentOrderEntity.java @@ -0,0 +1,42 @@ +package com.ordertogether.team14_be.payment.persistence.jpa.entity; + +import com.ordertogether.team14_be.common.persistence.entity.BaseTimeEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.math.BigDecimal; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import lombok.experimental.SuperBuilder; + +@Entity +@Getter +@SuperBuilder +@ToString +@Table(name = "payment_order") +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class PaymentOrderEntity extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private Long productId; + + @Column(nullable = false) + private String orderId; + + @Column(nullable = false) + private String orderName; + + @Column(nullable = false, precision = 10, scale = 2) + private BigDecimal amount; // 결제 금액 +} diff --git a/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/entity/ProductEntity.java b/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/entity/ProductEntity.java new file mode 100644 index 00000000..f149e8bf --- /dev/null +++ b/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/entity/ProductEntity.java @@ -0,0 +1,36 @@ +package com.ordertogether.team14_be.payment.persistence.jpa.entity; + +import com.ordertogether.team14_be.common.persistence.entity.BaseEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.math.BigDecimal; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import lombok.experimental.SuperBuilder; + +@Entity +@Getter +@SuperBuilder +@ToString +@Table(name = "product") +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class ProductEntity extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String name; + + @Column(nullable = false) + private BigDecimal price; +} diff --git a/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/mapper/PaymentEventMapper.java b/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/mapper/PaymentEventMapper.java new file mode 100644 index 00000000..db590005 --- /dev/null +++ b/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/mapper/PaymentEventMapper.java @@ -0,0 +1,31 @@ +package com.ordertogether.team14_be.payment.persistence.jpa.mapper; + +import com.ordertogether.team14_be.payment.domain.PaymentEvent; +import com.ordertogether.team14_be.payment.persistence.jpa.entity.PaymentEventEntity; +import lombok.experimental.UtilityClass; + +@UtilityClass +public final class PaymentEventMapper { + + public static PaymentEventEntity mapToEntity(PaymentEvent domain) { + return PaymentEventEntity.builder() + .id(domain.getId()) + .buyerId(domain.getBuyerId()) + .orderId(domain.getOrderId()) + .orderName(domain.getOrderName()) + .paymentKey(domain.getPaymentKey()) + .paymentStatus(domain.getPaymentStatus()) + .build(); + } + + public static PaymentEvent mapToDomain(PaymentEventEntity entity) { + return PaymentEvent.builder() + .id(entity.getId()) + .buyerId(entity.getBuyerId()) + .orderId(entity.getOrderId()) + .orderName(entity.getOrderName()) + .paymentKey(entity.getPaymentKey()) + .paymentStatus(entity.getPaymentStatus()) + .build(); + } +} diff --git a/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/mapper/PaymentOrderMapper.java b/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/mapper/PaymentOrderMapper.java new file mode 100644 index 00000000..9331c173 --- /dev/null +++ b/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/mapper/PaymentOrderMapper.java @@ -0,0 +1,34 @@ +package com.ordertogether.team14_be.payment.persistence.jpa.mapper; + +import com.ordertogether.team14_be.payment.domain.PaymentOrder; +import com.ordertogether.team14_be.payment.persistence.jpa.entity.PaymentOrderEntity; +import java.util.List; +import lombok.experimental.UtilityClass; + +@UtilityClass +public final class PaymentOrderMapper { + + public static PaymentOrderEntity mapToEntity(PaymentOrder domain) { + return PaymentOrderEntity.builder() + .id(domain.getId()) + .productId(domain.getProductId()) + .orderId(domain.getOrderId()) + .orderName(domain.getOrderName()) + .amount(domain.getAmount()) + .build(); + } + + public static PaymentOrder mapToDomain(PaymentOrderEntity entity) { + return PaymentOrder.builder() + .id(entity.getId()) + .productId(entity.getProductId()) + .orderId(entity.getOrderId()) + .orderName(entity.getOrderName()) + .amount(entity.getAmount()) + .build(); + } + + public static List mapToDomain(List entities) { + return entities.stream().map(PaymentOrderMapper::mapToDomain).toList(); + } +} diff --git a/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/mapper/ProductMapper.java b/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/mapper/ProductMapper.java new file mode 100644 index 00000000..c7313d83 --- /dev/null +++ b/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/mapper/ProductMapper.java @@ -0,0 +1,25 @@ +package com.ordertogether.team14_be.payment.persistence.jpa.mapper; + +import com.ordertogether.team14_be.payment.domain.Product; +import com.ordertogether.team14_be.payment.persistence.jpa.entity.ProductEntity; +import lombok.experimental.UtilityClass; + +@UtilityClass +public final class ProductMapper { + + public static ProductEntity mapToEntity(Product domain) { + return ProductEntity.builder() + .id(domain.getId()) + .name(domain.getName()) + .price(domain.getPrice()) + .build(); + } + + public static Product mapToDomain(ProductEntity entity) { + return Product.builder() + .id(entity.getId()) + .name(entity.getName()) + .price(entity.getPrice()) + .build(); + } +} diff --git a/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/repository/JpaPaymentEventRepository.java b/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/repository/JpaPaymentEventRepository.java new file mode 100644 index 00000000..7c286b90 --- /dev/null +++ b/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/repository/JpaPaymentEventRepository.java @@ -0,0 +1,33 @@ +package com.ordertogether.team14_be.payment.persistence.jpa.repository; + +import com.ordertogether.team14_be.payment.domain.PaymentEvent; +import com.ordertogether.team14_be.payment.persistence.jpa.entity.PaymentEventEntity; +import com.ordertogether.team14_be.payment.persistence.jpa.mapper.PaymentEventMapper; +import com.ordertogether.team14_be.payment.persistence.repository.PaymentEventRepository; +import java.util.Optional; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class JpaPaymentEventRepository implements PaymentEventRepository { + + private final SimpleJpaPaymentEventRepository simpleJpaPaymentEventRepository; + + @Override + public PaymentEvent save(PaymentEvent paymentEvent) { + PaymentEventEntity savedEntity = + simpleJpaPaymentEventRepository.save(PaymentEventMapper.mapToEntity(paymentEvent)); + return PaymentEventMapper.mapToDomain(savedEntity); + } + + @Override + public Optional findById(Long id) { + return simpleJpaPaymentEventRepository.findById(id).map(PaymentEventMapper::mapToDomain); + } + + @Override + public Optional findByOrderId(String orderId) { + return simpleJpaPaymentEventRepository + .findByOrderId(orderId) + .map(PaymentEventMapper::mapToDomain); + } +} diff --git a/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/repository/JpaPaymentOrderRepository.java b/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/repository/JpaPaymentOrderRepository.java new file mode 100644 index 00000000..1eade54e --- /dev/null +++ b/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/repository/JpaPaymentOrderRepository.java @@ -0,0 +1,64 @@ +package com.ordertogether.team14_be.payment.persistence.jpa.repository; + +import com.ordertogether.team14_be.payment.domain.PaymentOrder; +import com.ordertogether.team14_be.payment.persistence.jpa.entity.PaymentOrderEntity; +import com.ordertogether.team14_be.payment.persistence.jpa.entity.ProductEntity; +import com.ordertogether.team14_be.payment.persistence.jpa.mapper.PaymentOrderMapper; +import com.ordertogether.team14_be.payment.persistence.jpa.mapper.ProductMapper; +import com.ordertogether.team14_be.payment.persistence.repository.PaymentOrderRepository; +import java.util.List; +import java.util.Optional; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class JpaPaymentOrderRepository implements PaymentOrderRepository { + + private final SimpleJpaPaymentOrderRepository simpleJpaPaymentOrderRepository; + private final SimpleJpaProductRepository simpleJpaProductRepository; + + /** + * 결제 주문 정보를 저장한다.
+ * - 결제 주문 정보에 상품 정보가 없는 경우 상품 정보를 조회하여 결제 주문 정보에 추가한다. + * + * @param paymentOrder 결제 주문 정보 + * @return 저장된 결제 주문 정보 + */ + @Override + public PaymentOrder save(PaymentOrder paymentOrder) { + addMissingProductInfo(paymentOrder); + PaymentOrderEntity savedEntity = + simpleJpaPaymentOrderRepository.save(PaymentOrderMapper.mapToEntity(paymentOrder)); + + return PaymentOrderMapper.mapToDomain(savedEntity); + } + + private void addMissingProductInfo(PaymentOrder paymentOrder) { + if (paymentOrder.isMissingProductInfo()) { + ProductEntity productEntity = getProductEntity(paymentOrder); + paymentOrder.updateProductInfo(ProductMapper.mapToDomain(productEntity)); + } + } + + @Override + public List saveAll(List paymentOrders) { + List savedEntities = + simpleJpaPaymentOrderRepository.saveAll( + paymentOrders.stream().map(PaymentOrderMapper::mapToEntity).toList()); + + return PaymentOrderMapper.mapToDomain(savedEntities); + } + + @Override + public Optional findById(Long id) { + return simpleJpaPaymentOrderRepository.findById(id).map(PaymentOrderMapper::mapToDomain); + } + + private ProductEntity getProductEntity(PaymentOrder paymentOrder) { + return simpleJpaProductRepository + .findById(paymentOrder.getProductId()) + .orElseThrow( + () -> + new IllegalArgumentException( + String.format("상품 아이디 %s에 해당하는 상품이 없습니다.", paymentOrder.getProductId()))); + } +} diff --git a/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/repository/JpaProductRepository.java b/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/repository/JpaProductRepository.java new file mode 100644 index 00000000..feb9dd1d --- /dev/null +++ b/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/repository/JpaProductRepository.java @@ -0,0 +1,43 @@ +package com.ordertogether.team14_be.payment.persistence.jpa.repository; + +import com.ordertogether.team14_be.payment.domain.Product; +import com.ordertogether.team14_be.payment.persistence.jpa.entity.ProductEntity; +import com.ordertogether.team14_be.payment.persistence.jpa.mapper.ProductMapper; +import com.ordertogether.team14_be.payment.persistence.repository.ProductRepository; +import java.util.List; +import java.util.Optional; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class JpaProductRepository implements ProductRepository { + + private final SimpleJpaProductRepository simpleJpaProductRepository; + + @Override + public Product save(Product product) { + ProductEntity savedProduct = + simpleJpaProductRepository.save(ProductMapper.mapToEntity(product)); + return ProductMapper.mapToDomain(savedProduct); + } + + @Override + public List saveAll(List products) { + return simpleJpaProductRepository + .saveAll(products.stream().map(ProductMapper::mapToEntity).toList()) + .stream() + .map(ProductMapper::mapToDomain) + .toList(); + } + + @Override + public Optional findById(Long id) { + return simpleJpaProductRepository.findById(id).map(ProductMapper::mapToDomain); + } + + @Override + public List findByIdIn(List ids) { + return simpleJpaProductRepository.findByIdIn(ids).stream() + .map(ProductMapper::mapToDomain) + .toList(); + } +} diff --git a/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/repository/SimpleJpaPaymentEventRepository.java b/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/repository/SimpleJpaPaymentEventRepository.java new file mode 100644 index 00000000..dce6ca28 --- /dev/null +++ b/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/repository/SimpleJpaPaymentEventRepository.java @@ -0,0 +1,12 @@ +package com.ordertogether.team14_be.payment.persistence.jpa.repository; + +import com.ordertogether.team14_be.payment.persistence.jpa.entity.PaymentEventEntity; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface SimpleJpaPaymentEventRepository extends JpaRepository { + + Optional findByOrderId(String orderId); +} diff --git a/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/repository/SimpleJpaPaymentOrderRepository.java b/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/repository/SimpleJpaPaymentOrderRepository.java new file mode 100644 index 00000000..f5c5d38e --- /dev/null +++ b/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/repository/SimpleJpaPaymentOrderRepository.java @@ -0,0 +1,8 @@ +package com.ordertogether.team14_be.payment.persistence.jpa.repository; + +import com.ordertogether.team14_be.payment.persistence.jpa.entity.PaymentOrderEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface SimpleJpaPaymentOrderRepository extends JpaRepository {} diff --git a/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/repository/SimpleJpaProductRepository.java b/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/repository/SimpleJpaProductRepository.java new file mode 100644 index 00000000..bf0a46b8 --- /dev/null +++ b/src/main/java/com/ordertogether/team14_be/payment/persistence/jpa/repository/SimpleJpaProductRepository.java @@ -0,0 +1,12 @@ +package com.ordertogether.team14_be.payment.persistence.jpa.repository; + +import com.ordertogether.team14_be.payment.persistence.jpa.entity.ProductEntity; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface SimpleJpaProductRepository extends JpaRepository { + + List findByIdIn(List ids); +} diff --git a/src/main/java/com/ordertogether/team14_be/payment/persistence/repository/PaymentEventRepository.java b/src/main/java/com/ordertogether/team14_be/payment/persistence/repository/PaymentEventRepository.java new file mode 100644 index 00000000..7084d746 --- /dev/null +++ b/src/main/java/com/ordertogether/team14_be/payment/persistence/repository/PaymentEventRepository.java @@ -0,0 +1,13 @@ +package com.ordertogether.team14_be.payment.persistence.repository; + +import com.ordertogether.team14_be.payment.domain.PaymentEvent; +import java.util.Optional; + +public interface PaymentEventRepository { + + PaymentEvent save(PaymentEvent paymentEvent); + + Optional findById(Long id); + + Optional findByOrderId(String orderId); +} diff --git a/src/main/java/com/ordertogether/team14_be/payment/persistence/repository/PaymentOrderRepository.java b/src/main/java/com/ordertogether/team14_be/payment/persistence/repository/PaymentOrderRepository.java new file mode 100644 index 00000000..7fa05b8b --- /dev/null +++ b/src/main/java/com/ordertogether/team14_be/payment/persistence/repository/PaymentOrderRepository.java @@ -0,0 +1,14 @@ +package com.ordertogether.team14_be.payment.persistence.repository; + +import com.ordertogether.team14_be.payment.domain.PaymentOrder; +import java.util.List; +import java.util.Optional; + +public interface PaymentOrderRepository { + + PaymentOrder save(PaymentOrder paymentOrder); + + List saveAll(List paymentOrders); + + Optional findById(Long id); +} diff --git a/src/main/java/com/ordertogether/team14_be/payment/persistence/repository/ProductRepository.java b/src/main/java/com/ordertogether/team14_be/payment/persistence/repository/ProductRepository.java new file mode 100644 index 00000000..e3364f7c --- /dev/null +++ b/src/main/java/com/ordertogether/team14_be/payment/persistence/repository/ProductRepository.java @@ -0,0 +1,16 @@ +package com.ordertogether.team14_be.payment.persistence.repository; + +import com.ordertogether.team14_be.payment.domain.Product; +import java.util.List; +import java.util.Optional; + +public interface ProductRepository { + + Product save(Product product); + + List saveAll(List products); + + Optional findById(Long id); + + List findByIdIn(List ids); +} diff --git a/src/main/java/com/ordertogether/team14_be/payment/service/PaymentPreparationService.java b/src/main/java/com/ordertogether/team14_be/payment/service/PaymentPreparationService.java new file mode 100644 index 00000000..1b2b33cb --- /dev/null +++ b/src/main/java/com/ordertogether/team14_be/payment/service/PaymentPreparationService.java @@ -0,0 +1,94 @@ +package com.ordertogether.team14_be.payment.service; + +import com.ordertogether.team14_be.common.util.IdempotentKeyGenerator; +import com.ordertogether.team14_be.payment.domain.PaymentEvent; +import com.ordertogether.team14_be.payment.domain.PaymentOrder; +import com.ordertogether.team14_be.payment.domain.Product; +import com.ordertogether.team14_be.payment.persistence.repository.PaymentEventRepository; +import com.ordertogether.team14_be.payment.persistence.repository.PaymentOrderRepository; +import com.ordertogether.team14_be.payment.persistence.repository.ProductRepository; +import com.ordertogether.team14_be.payment.web.request.PaymentPrepareRequest; +import com.ordertogether.team14_be.payment.web.response.PaymentPrepareResponse; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +/** 결제 시작 전, 결제 정보를 저장하는 서비스 */ +public class PaymentPreparationService { + + private final PaymentEventRepository paymentEventRepository; + private final PaymentOrderRepository paymentOrderRepository; + private final ProductRepository productRepository; + + /** + * 결제 정보를 저장한다.
+ * {@link PaymentEvent} 와 {@link PaymentOrder} 를 저장한다. + * + * @param request 결제 정보 + * @return 저장된 결제 정보 + */ + @Transactional + public PaymentPrepareResponse prepare(PaymentPrepareRequest request) { + validateDuplicatePayment(request); + + List products = productRepository.findByIdIn(request.getProductIds()); + List paymentOrders = + paymentOrderRepository.saveAll(createPaymentOrders(products, request.getIdempotencySeed())); + PaymentEvent paymentEvent = paymentEventRepository.save(createPaymentEvent(request, products)); + + return PaymentPrepareResponse.of(paymentEvent, paymentOrders); + } + + /** + * 중복 결제 요청인지 확인한다. + * + * @param request 결제 정보 + * @throws IllegalArgumentException 중복 결제 요청일 경우 + */ + private void validateDuplicatePayment(PaymentPrepareRequest request) { + String idempotentKey = IdempotentKeyGenerator.generate(request.getIdempotencySeed()); + paymentEventRepository + .findByOrderId(idempotentKey) + .ifPresent( + paymentEvent -> { + throw new IllegalArgumentException( + "Seed: %s 를 통해 생성된 결제는 이미 %s 상태인 주문입니다." + .formatted( + request.getIdempotencySeed(), + paymentEvent.getPaymentStatus().getDescription())); + }); + } + + private List createPaymentOrders(List products, String idempotencySeed) { + return products.stream().map(product -> createPaymentOrder(product, idempotencySeed)).toList(); + } + + private PaymentOrder createPaymentOrder(Product product, String idempotencySeed) { + return PaymentOrder.builder() + .productId(product.getId()) + .orderId(IdempotentKeyGenerator.generate(idempotencySeed)) + .orderName(product.getName()) + .amount(product.getPrice()) + .build(); + } + + private PaymentEvent createPaymentEvent(PaymentPrepareRequest request, List products) { + String idempotencySeed = request.getIdempotencySeed(); + + return PaymentEvent.builder() + .buyerId(request.getBuyerId()) + .paymentOrders( + products.stream().map(product -> createPaymentOrder(product, idempotencySeed)).toList()) + .orderId(IdempotentKeyGenerator.generate(idempotencySeed)) + .orderName(createOrderName(products)) + .paymentKey(IdempotentKeyGenerator.generate(idempotencySeed)) + .build(); + } + + private String createOrderName(List products) { + return String.join(",", products.stream().map(Product::getName).toList()); + } +} diff --git a/src/main/java/com/ordertogether/team14_be/payment/web/controller/PaymentController.java b/src/main/java/com/ordertogether/team14_be/payment/web/controller/PaymentController.java new file mode 100644 index 00000000..5b17246e --- /dev/null +++ b/src/main/java/com/ordertogether/team14_be/payment/web/controller/PaymentController.java @@ -0,0 +1,31 @@ +package com.ordertogether.team14_be.payment.web.controller; + +import com.ordertogether.team14_be.common.web.response.ApiResponse; +import com.ordertogether.team14_be.payment.service.PaymentPreparationService; +import com.ordertogether.team14_be.payment.web.request.PaymentPrepareRequest; +import com.ordertogether.team14_be.payment.web.response.PaymentPrepareResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +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; + +@RestController +@RequestMapping("/api/v1/payments") +@RequiredArgsConstructor +public class PaymentController { + + private final PaymentPreparationService paymentPreparationService; + + @PostMapping + public ResponseEntity> preparePayment( + @RequestBody PaymentPrepareRequest request) { + // todo: 1L -> UserDetail.getUserId() + request.addBuyerId(1L); + PaymentPrepareResponse data = paymentPreparationService.prepare(request); + + return ResponseEntity.ok(ApiResponse.with(HttpStatus.OK, "결제 정보를 저장하였습니다.", data)); + } +} diff --git a/src/main/java/com/ordertogether/team14_be/payment/web/request/PaymentPrepareRequest.java b/src/main/java/com/ordertogether/team14_be/payment/web/request/PaymentPrepareRequest.java new file mode 100644 index 00000000..aa79b894 --- /dev/null +++ b/src/main/java/com/ordertogether/team14_be/payment/web/request/PaymentPrepareRequest.java @@ -0,0 +1,23 @@ +package com.ordertogether.team14_be.payment.web.request; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import java.util.List; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class PaymentPrepareRequest { + + @JsonIgnore(false) + private Long buyerId; + + private final String idempotencySeed; + + private final List productIds; + + public PaymentPrepareRequest addBuyerId(Long buyerId) { + this.buyerId = buyerId; + return this; + } +} diff --git a/src/main/java/com/ordertogether/team14_be/payment/web/response/PaymentOrderResponse.java b/src/main/java/com/ordertogether/team14_be/payment/web/response/PaymentOrderResponse.java new file mode 100644 index 00000000..2e1faf5a --- /dev/null +++ b/src/main/java/com/ordertogether/team14_be/payment/web/response/PaymentOrderResponse.java @@ -0,0 +1,15 @@ +package com.ordertogether.team14_be.payment.web.response; + +import com.ordertogether.team14_be.payment.domain.PaymentOrder; + +public record PaymentOrderResponse( + Long paymentOrderId, Long productId, String orderId, String orderName, Long amount) { + public static PaymentOrderResponse from(PaymentOrder paymentOrder) { + return new PaymentOrderResponse( + paymentOrder.getId(), + paymentOrder.getProductId(), + paymentOrder.getOrderId(), + paymentOrder.getOrderName(), + paymentOrder.getAmount().longValue()); + } +} diff --git a/src/main/java/com/ordertogether/team14_be/payment/web/response/PaymentPrepareResponse.java b/src/main/java/com/ordertogether/team14_be/payment/web/response/PaymentPrepareResponse.java new file mode 100644 index 00000000..6f96b2be --- /dev/null +++ b/src/main/java/com/ordertogether/team14_be/payment/web/response/PaymentPrepareResponse.java @@ -0,0 +1,24 @@ +package com.ordertogether.team14_be.payment.web.response; + +import com.ordertogether.team14_be.payment.domain.PaymentEvent; +import com.ordertogether.team14_be.payment.domain.PaymentOrder; +import java.util.List; + +public record PaymentPrepareResponse( + Long paymentEventId, + Long buyerId, + List paymentOrders, + String orderId, + String orderName, + String paymentKey) { + public static PaymentPrepareResponse of( + PaymentEvent paymentEvent, List paymentOrders) { + return new PaymentPrepareResponse( + paymentEvent.getId(), + paymentEvent.getBuyerId(), + paymentOrders.stream().map(PaymentOrderResponse::from).toList(), + paymentEvent.getOrderId(), + paymentEvent.getOrderName(), + paymentEvent.getPaymentKey()); + } +} diff --git a/src/main/java/com/ordertogether/team14_be/spot/converter/AbstractCodedEnumConverter.java b/src/main/java/com/ordertogether/team14_be/spot/converter/AbstractCodedEnumConverter.java new file mode 100644 index 00000000..6bad1307 --- /dev/null +++ b/src/main/java/com/ordertogether/team14_be/spot/converter/AbstractCodedEnumConverter.java @@ -0,0 +1,37 @@ +package com.ordertogether.team14_be.spot.converter; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; +import java.util.Arrays; +import java.util.Objects; + +@Converter +public class AbstractCodedEnumConverter & CodedEnum, E> + implements AttributeConverter { + + private final Class clazz; + + public AbstractCodedEnumConverter(Class clazz) { + this.clazz = clazz; + } + + @Override + public E convertToDatabaseColumn( + T attribute) { // Converts the value stored in the entity attribute into the data + // representation to be stored in the database. + return attribute.getCode(); + } + + @Override + public T convertToEntityAttribute( + E dbData) { // Converts the data stored in the database column into the value to be stored + // in the entity attribute. + if (Objects.isNull(dbData)) { + return null; + } + return Arrays.stream(clazz.getEnumConstants()) + .filter(e -> e.getCode().equals(dbData)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Unknown code: " + dbData)); + } +} diff --git a/src/main/java/com/ordertogether/team14_be/spot/converter/CodedEnum.java b/src/main/java/com/ordertogether/team14_be/spot/converter/CodedEnum.java new file mode 100644 index 00000000..57f96996 --- /dev/null +++ b/src/main/java/com/ordertogether/team14_be/spot/converter/CodedEnum.java @@ -0,0 +1,5 @@ +package com.ordertogether.team14_be.spot.converter; + +public interface CodedEnum { + T getCode(); +} diff --git a/src/main/java/com/ordertogether/team14_be/spot/dto/SpotDto.java b/src/main/java/com/ordertogether/team14_be/spot/dto/SpotDto.java index e4deb6bb..4a988d84 100644 --- a/src/main/java/com/ordertogether/team14_be/spot/dto/SpotDto.java +++ b/src/main/java/com/ordertogether/team14_be/spot/dto/SpotDto.java @@ -1,16 +1,15 @@ package com.ordertogether.team14_be.spot.dto; import com.ordertogether.team14_be.spot.entity.Spot; +import com.ordertogether.team14_be.spot.enums.Category; import jakarta.persistence.Column; import java.math.BigDecimal; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; +import java.time.LocalDateTime; +import lombok.*; @Builder -@NoArgsConstructor -@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@AllArgsConstructor(access = AccessLevel.PROTECTED) @Getter public class SpotDto { private Long id; @@ -21,12 +20,17 @@ public class SpotDto { @Column(precision = 11, scale = 8) private BigDecimal lng; - private String category; - private String store_name; - private int minimum_order_amount; - private String together_order_link; - private String pick_up_location; - private String delivery_status; + private Category category; + private String storeName; + private int minimumOrderAmount; + private String togetherOrderLink; + private String pickUpLocation; + private String deliveryStatus; + private boolean isDeleted; + private LocalDateTime createdAt; + private LocalDateTime modifiedAt; + private Long createdBy; + private Long modifiedBy; public Spot toEntity() { return Spot.builder() @@ -34,11 +38,16 @@ public Spot toEntity() { .lat(lat) .lng(lng) .category(category) - .store_name(store_name) - .minimum_order_amount(minimum_order_amount) - .together_order_link(together_order_link) - .pick_up_location(pick_up_location) - .delivery_status(delivery_status) + .storeName(storeName) + .minimumOrderAmount(minimumOrderAmount) + .togetherOrderLink(togetherOrderLink) + .pickUpLocation(pickUpLocation) + .deliveryStatus(deliveryStatus) + .isDeleted(isDeleted) + .createdAt(createdAt) + .modifiedAt(modifiedAt) + .createdBy(createdBy) + .modifiedBy(modifiedBy) .build(); } } diff --git a/src/main/java/com/ordertogether/team14_be/spot/entity/Spot.java b/src/main/java/com/ordertogether/team14_be/spot/entity/Spot.java index b5045259..a658376c 100644 --- a/src/main/java/com/ordertogether/team14_be/spot/entity/Spot.java +++ b/src/main/java/com/ordertogether/team14_be/spot/entity/Spot.java @@ -1,18 +1,22 @@ package com.ordertogether.team14_be.spot.entity; +import com.ordertogether.team14_be.common.persistence.entity.BaseEntity; +import com.ordertogether.team14_be.spot.enums.Category; import jakarta.persistence.*; import java.math.BigDecimal; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.*; +import lombok.experimental.SuperBuilder; +import org.hibernate.annotations.DynamicUpdate; @Entity -@Builder -@AllArgsConstructor +@SuperBuilder // 상속받은 필드도 빌더에서 사용 @NoArgsConstructor +@AllArgsConstructor(access = AccessLevel.PROTECTED) @Getter -public class Spot { +@Table(indexes = {@Index(name = "idx_lat_lng", columnList = "lat, lng")}) +@DynamicUpdate // 변경한 필드만 대응 +public class Spot extends BaseEntity { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -23,14 +27,23 @@ public class Spot { @Column(precision = 11, scale = 8) private BigDecimal lng; - private String category; - private String store_name; - private Integer minimum_order_amount; + private Category category; + private String storeName; + private Integer minimumOrderAmount; @Lob @Column(columnDefinition = "MEDIUMTEXT") - private String together_order_link; + private String togetherOrderLink; + + private String pickUpLocation; + private String deliveryStatus; + @Builder.Default private Boolean isDeleted = false; + + public void delete() { + this.isDeleted = true; + } - private String pick_up_location; - private String delivery_status; + public void restore() { + this.isDeleted = false; + } } diff --git a/src/main/java/com/ordertogether/team14_be/spot/enums/Category.java b/src/main/java/com/ordertogether/team14_be/spot/enums/Category.java new file mode 100644 index 00000000..5c26d0bc --- /dev/null +++ b/src/main/java/com/ordertogether/team14_be/spot/enums/Category.java @@ -0,0 +1,36 @@ +package com.ordertogether.team14_be.spot.enums; + +import com.ordertogether.team14_be.spot.converter.AbstractCodedEnumConverter; +import com.ordertogether.team14_be.spot.converter.CodedEnum; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public enum Category implements CodedEnum { + JOKBAL_BOSSAM("족발, 보쌈"), + JAPANESE_FOOD("돈까스, 회, 일식"), + MEAT("고기"), + KOREAN_STEW("찜, 탕, 찌개"), + WESTERN_STYLE("양식"), + CHINESE_FOOD("중식"), + ASIAN("아시안"), + CHICKEN("치킨"), + CARBOHYDRATE("백반, 죽, 국수"), + BURGER("버거"), + K_SNACK_FOOD("분식"), + CAFE("카페, 디저트"); + + private final String category; + + public String getCode() { + return category; + } + + @jakarta.persistence.Converter(autoApply = true) + static class Converter extends AbstractCodedEnumConverter { + public Converter() { + super(Category.class); + } + } +} diff --git a/src/main/java/com/ordertogether/team14_be/spot/repository/SpotRepository.java b/src/main/java/com/ordertogether/team14_be/spot/repository/SpotRepository.java index aa11f75c..706897b5 100644 --- a/src/main/java/com/ordertogether/team14_be/spot/repository/SpotRepository.java +++ b/src/main/java/com/ordertogether/team14_be/spot/repository/SpotRepository.java @@ -3,10 +3,13 @@ import com.ordertogether.team14_be.spot.entity.Spot; import java.math.BigDecimal; import java.util.List; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository public interface SpotRepository extends JpaRepository { - List findByLatAndLng(BigDecimal lat, BigDecimal lng); + List findByLatAndLngAndIsDeletedFalse(BigDecimal lat, BigDecimal lng); + + Optional findByIdAndIsDeletedFalse(Long id); } diff --git a/src/main/java/com/ordertogether/team14_be/spot/service/SpotService.java b/src/main/java/com/ordertogether/team14_be/spot/service/SpotService.java index 10adef37..a16a6dc4 100644 --- a/src/main/java/com/ordertogether/team14_be/spot/service/SpotService.java +++ b/src/main/java/com/ordertogether/team14_be/spot/service/SpotService.java @@ -4,8 +4,10 @@ import com.ordertogether.team14_be.spot.entity.Spot; import com.ordertogether.team14_be.spot.repository.SpotRepository; import jakarta.persistence.EntityNotFoundException; +import jakarta.transaction.Transactional; import java.math.BigDecimal; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -22,11 +24,12 @@ public SpotService(SpotRepository spotRepository) { // Spot 전체 조회하기 public List getSpot(BigDecimal lat, BigDecimal lng) { - return spotRepository.findByLatAndLng(lat, lng).stream() + return spotRepository.findByLatAndLngAndIsDeletedFalse(lat, lng).stream() .map(this::toDto) .collect(Collectors.toList()); } + @Transactional public SpotDto createSpot(SpotDto spotDto) { Spot spot = spotDto.toEntity(); return toDto(spotRepository.save(spot)); @@ -37,17 +40,20 @@ public SpotDto getSpot(Long id) { Spot spot = spotRepository .findById(id) - .orElseThrow(() -> new EntityNotFoundException("Spot not found")); + .orElseThrow(() -> new EntityNotFoundException("Spot을 찾을 수 없습니다.")); return toDto(spot); } + @Transactional public SpotDto updateSpot(SpotDto spotDto) { Spot spot = spotRepository.save(spotDto.toEntity()); return toDto(spot); } + @Transactional public void deleteSpot(Long id) { - spotRepository.deleteById(id); + Optional spotToDelete = spotRepository.findByIdAndIsDeletedFalse(id); + spotToDelete.ifPresent(Spot::delete); } // Service Layer에서 toDto만들어서 매핑시키기 @@ -55,18 +61,19 @@ public SpotDto toDto(Spot spotInStream) { Spot spot = spotRepository .findById(spotInStream.getId()) - .orElseThrow(() -> new EntityNotFoundException("Spot not found")); + .orElseThrow(() -> new EntityNotFoundException("Spot을 찾을 수 없습니다.")); return SpotDto.builder() .id(spot.getId()) .lat(spot.getLat()) .lng(spot.getLng()) .category(spot.getCategory()) - .store_name(spot.getStore_name()) - .minimum_order_amount(spot.getMinimum_order_amount()) - .together_order_link(spot.getTogether_order_link()) - .pick_up_location(spot.getPick_up_location()) - .delivery_status(spot.getDelivery_status()) + .storeName(spot.getStoreName()) + .minimumOrderAmount(spot.getMinimumOrderAmount()) + .togetherOrderLink(spot.getTogetherOrderLink()) + .pickUpLocation(spot.getPickUpLocation()) + .deliveryStatus(spot.getDeliveryStatus()) + .isDeleted(spot.getIsDeleted()) .build(); } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index a2d13976..6b783ffd 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -3,7 +3,7 @@ spring: name: Team14_BE datasource: - driver-class-name: ${DRIVER_CALSS_NAME} + driver-class-name: ${DRIVER_CLASS_NAME} username: ${USERNAME} password: url: ${URL} @@ -14,6 +14,11 @@ spring: properties: hibernate: format_sql: true + defer-datasource-initialization: true + + sql: + init: + mode: always logging: level: @@ -38,4 +43,4 @@ kakao: key: jwt: - secreat-key: ${JWT_SCREAT_KEY} + secret-key: ${JWT_SECRET_KEY} diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql new file mode 100644 index 00000000..59b6346f --- /dev/null +++ b/src/main/resources/data.sql @@ -0,0 +1,3 @@ +INSERT INTO product (id, name, price, created_at, modified_at, created_by, modified_by) VALUES (1, 'Product 1', 10000, now(), now(), 1, 1); +INSERT INTO product (id, name, price, created_at, modified_at, created_by, modified_by) VALUES (2, 'Product 2', 20000, now(), now(), 1, 1); +INSERT INTO product (id, name, price, created_at, modified_at, created_by, modified_by) VALUES (3, 'Product 3', 30000, now(), now(), 1, 1); diff --git a/src/test/java/com/ordertogether/team14_be/Team14BeApplicationTests.java b/src/test/java/com/ordertogether/team14_be/Team14BeApplicationTests.java index ff77c428..fd45e8ba 100644 --- a/src/test/java/com/ordertogether/team14_be/Team14BeApplicationTests.java +++ b/src/test/java/com/ordertogether/team14_be/Team14BeApplicationTests.java @@ -2,8 +2,10 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; @SpringBootTest +@ActiveProfiles("test") class Team14BeApplicationTests { @Test diff --git a/src/test/java/com/ordertogether/team14_be/helper/PaymentDatabaseHelper.java b/src/test/java/com/ordertogether/team14_be/helper/PaymentDatabaseHelper.java new file mode 100644 index 00000000..73e969bd --- /dev/null +++ b/src/test/java/com/ordertogether/team14_be/helper/PaymentDatabaseHelper.java @@ -0,0 +1,6 @@ +package com.ordertogether.team14_be.helper; + +public interface PaymentDatabaseHelper { + + void clean(); +} diff --git a/src/test/java/com/ordertogether/team14_be/helper/jpa/JpaDatabaseCleanup.java b/src/test/java/com/ordertogether/team14_be/helper/jpa/JpaDatabaseCleanup.java new file mode 100644 index 00000000..68e609ad --- /dev/null +++ b/src/test/java/com/ordertogether/team14_be/helper/jpa/JpaDatabaseCleanup.java @@ -0,0 +1,51 @@ +package com.ordertogether.team14_be.helper.jpa; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import java.util.List; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Component +@Profile("test") +public class JpaDatabaseCleanup implements InitializingBean { + + @PersistenceContext private EntityManager entityManager; + + private List tableNames; + + @Override + public void afterPropertiesSet() { + tableNames = + entityManager.getMetamodel().getEntities().stream() + .filter(e -> e.getJavaType().getAnnotation(Entity.class) != null) + .map(e -> removePostfix(e.getName(), "Entity")) + .map(SnakeCaseStrategy.INSTANCE::translate) + .toList(); + } + + private String removePostfix(String entityName, String postfix) { + if (entityName.endsWith(postfix)) { + return entityName.substring(0, entityName.length() - postfix.length()); + } + return entityName; + } + + @Transactional + public void execute() { + entityManager.flush(); + entityManager.createNativeQuery("SET FOREIGN_KEY_CHECKS = 0").executeUpdate(); + + for (String tableName : tableNames) { + entityManager.createNativeQuery("TRUNCATE TABLE " + tableName).executeUpdate(); + entityManager + .createNativeQuery("ALTER TABLE " + tableName + " AUTO_INCREMENT = 1") + .executeUpdate(); + } + entityManager.createNativeQuery("SET FOREIGN_KEY_CHECKS = 1").executeUpdate(); + } +} diff --git a/src/test/java/com/ordertogether/team14_be/helper/jpa/JpaPaymentDatabaseHelper.java b/src/test/java/com/ordertogether/team14_be/helper/jpa/JpaPaymentDatabaseHelper.java new file mode 100644 index 00000000..7f292b01 --- /dev/null +++ b/src/test/java/com/ordertogether/team14_be/helper/jpa/JpaPaymentDatabaseHelper.java @@ -0,0 +1,17 @@ +package com.ordertogether.team14_be.helper.jpa; + +import com.ordertogether.team14_be.helper.PaymentDatabaseHelper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class JpaPaymentDatabaseHelper implements PaymentDatabaseHelper { + + private final JpaDatabaseCleanup jpaDatabaseCleanup; + + @Override + public void clean() { + jpaDatabaseCleanup.execute(); + } +} diff --git a/src/test/java/com/ordertogether/team14_be/payment/service/PaymentPreparationServiceTest.java b/src/test/java/com/ordertogether/team14_be/payment/service/PaymentPreparationServiceTest.java new file mode 100644 index 00000000..30ac7813 --- /dev/null +++ b/src/test/java/com/ordertogether/team14_be/payment/service/PaymentPreparationServiceTest.java @@ -0,0 +1,81 @@ +package com.ordertogether.team14_be.payment.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.ordertogether.team14_be.helper.PaymentDatabaseHelper; +import com.ordertogether.team14_be.payment.domain.Product; +import com.ordertogether.team14_be.payment.persistence.repository.ProductRepository; +import com.ordertogether.team14_be.payment.web.request.PaymentPrepareRequest; +import com.ordertogether.team14_be.payment.web.response.PaymentPrepareResponse; +import java.math.BigDecimal; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +@ActiveProfiles("test") +@SpringBootTest +class PaymentPreparationServiceTest { + + @Autowired private PaymentPreparationService paymentPreparationService; + @Autowired private ProductRepository productRepository; + + @Autowired private PaymentDatabaseHelper paymentDatabaseHelper; + + @BeforeEach + void setup() { + paymentDatabaseHelper.clean(); + + productRepository.saveAll( + List.of( + Product.builder().id(1L).name("Product 1").price(BigDecimal.valueOf(10000)).build(), + Product.builder().id(2L).name("Product 2").price(BigDecimal.valueOf(20000)).build(), + Product.builder().id(3L).name("Product 3").price(BigDecimal.valueOf(30000)).build())); + } + + @Test + @DisplayName("결제 정보를 성공적으로 저장할 수 있다.") + void shouldSuccessWhenNormalRequest() { + // given + PaymentPrepareRequest request = + new PaymentPrepareRequest("idempotency-seed", List.of(1L, 2L, 3L)).addBuyerId(1L); + + // then + PaymentPrepareResponse response = paymentPreparationService.prepare(request); + + // when + assertThat(response.paymentEventId()).isNotNull(); + assertThat(response.buyerId()).isEqualTo(1L); + assertThat(response.paymentOrders()).hasSize(3); + assertThat(response.orderId()).isNotNull(); + assertThat(response.orderName()).isEqualTo("Product 1,Product 2,Product 3"); + assertThat(response.paymentKey()).isNotNull(); + response.paymentOrders().stream() + .forEach( + paymentOrder -> { + assertAll( + () -> assertThat(paymentOrder.paymentOrderId()).isNotNull(), + () -> assertThat(paymentOrder.productId()).isIn(1L, 2L, 3L), + () -> assertThat(paymentOrder.orderId()).isEqualTo(response.orderId())); + }); + } + + @Test + @DisplayName("이미 저장된 결제 정보는 저장 요청 시, 예외가 발생한다.") + void shouldThrowExceptionWhenAlreadyCompleteRequest() { + // given + PaymentPrepareRequest request = + new PaymentPrepareRequest("idempotency-seed", List.of(1L, 2L, 3L)).addBuyerId(1L); + paymentPreparationService.prepare(request); + + // then + // when + assertThatThrownBy(() -> paymentPreparationService.prepare(request)) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml new file mode 100644 index 00000000..6b783ffd --- /dev/null +++ b/src/test/resources/application-test.yml @@ -0,0 +1,46 @@ +spring: + application: + name: Team14_BE + + datasource: + driver-class-name: ${DRIVER_CLASS_NAME} + username: ${USERNAME} + password: + url: ${URL} + + jpa: + hibernate: + ddl-auto: create + properties: + hibernate: + format_sql: true + defer-datasource-initialization: true + + sql: + init: + mode: always + +logging: + level: + org: + hibernate: + SQL: debug + orm: + jdbc: + bind: trace + +kakao: + client-id: ${KAKAO_CLIENT_ID} + redirect-url: ${KAKAO_REDIRECT_URL} + + auth: + token: + url: ${KAKAO_AUTH_TOKEN_URL} + + user: + api: + url: ${KAKAO_USER_API_URL} + +key: + jwt: + secret-key: ${JWT_SECRET_KEY}