diff --git a/payment-service/build.gradle b/payment-service/build.gradle index a1e163a..b3442e8 100644 --- a/payment-service/build.gradle +++ b/payment-service/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'org.springframework.boot' version '3.5.0' + id 'org.springframework.boot' version '3.5.4' id 'io.spring.dependency-management' version '1.1.7' } @@ -8,9 +8,13 @@ group = 'com.synapse' version = '0.0.1-SNAPSHOT' java { - toolchain { - languageVersion = JavaLanguageVersion.of(21) - } + sourceCompatibility = JavaVersion.VERSION_21 +} + +configurations { + compileOnly { + extendsFrom annotationProcessor + } } repositories { @@ -18,36 +22,30 @@ repositories { maven { url 'https://jitpack.io' } } -dependencies { - // PortOne - implementation 'io.portone:server-sdk:0.19.0' - // Security - implementation 'org.springframework.boot:spring-boot-starter-security' - // Web - implementation 'org.springframework.boot:spring-boot-starter-web' - // JPA - implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - // PostgreSQL - runtimeOnly 'org.postgresql:postgresql' - // OAuth2 Resource Server - implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server' - // WebFlux - implementation 'org.springframework.boot:spring-boot-starter-webflux' - // Lombok - compileOnly 'org.projectlombok:lombok' - annotationProcessor 'org.projectlombok:lombok' - //H2 Database - implementation 'com.h2database:h2' - // Validation - implementation 'org.springframework.boot:spring-boot-starter-validation' - // Test - testImplementation 'org.springframework.boot:spring-boot-starter-test' - testCompileOnly 'org.projectlombok:lombok' - testAnnotationProcessor 'org.projectlombok:lombok' - testImplementation 'org.springframework.security:spring-security-test' - testRuntimeOnly 'org.junit.platform:junit-platform-launcher' +subprojects { + apply plugin: 'java' + apply plugin: 'io.spring.dependency-management' + apply plugin: 'org.springframework.boot' + + repositories { + mavenCentral() + } + + dependencies { + // Web + implementation 'org.springframework.boot:spring-boot-starter-web' + // Lombok + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + // Validation + implementation 'org.springframework.boot:spring-boot-starter-validation' + } } tasks.named('test') { useJUnitPlatform() } + +bootJar { + enabled = false +} diff --git a/payment-service/payment-service-api/.gitignore b/payment-service/payment-service-api/.gitignore new file mode 100644 index 0000000..c2065bc --- /dev/null +++ b/payment-service/payment-service-api/.gitignore @@ -0,0 +1,37 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/payment-service/payment-service-api/build.gradle b/payment-service/payment-service-api/build.gradle new file mode 100644 index 0000000..69a4897 --- /dev/null +++ b/payment-service/payment-service-api/build.gradle @@ -0,0 +1,15 @@ +repositories { + mavenCentral() +} + +dependencies { + +} + +tasks.named('test') { + useJUnitPlatform() +} + +bootJar { + enabled = false +} diff --git a/payment-service/payment-service-api/src/main/java/com/synapse/payment_service_api/dto/request/CancelSubscriptionRequest.java b/payment-service/payment-service-api/src/main/java/com/synapse/payment_service_api/dto/request/CancelSubscriptionRequest.java new file mode 100644 index 0000000..99f2e74 --- /dev/null +++ b/payment-service/payment-service-api/src/main/java/com/synapse/payment_service_api/dto/request/CancelSubscriptionRequest.java @@ -0,0 +1,10 @@ +package com.synapse.payment_service_api.dto.request; + +import jakarta.validation.constraints.NotBlank; + +public record CancelSubscriptionRequest( + @NotBlank(message = "취소 사유는 필수입니다.") + String reason +) { + +} diff --git a/payment-service/payment-service-api/src/main/java/com/synapse/payment_service_api/dto/request/PaymentRequestDto.java b/payment-service/payment-service-api/src/main/java/com/synapse/payment_service_api/dto/request/PaymentRequestDto.java new file mode 100644 index 0000000..f1da9c9 --- /dev/null +++ b/payment-service/payment-service-api/src/main/java/com/synapse/payment_service_api/dto/request/PaymentRequestDto.java @@ -0,0 +1,10 @@ +package com.synapse.payment_service_api.dto.request; + +import jakarta.validation.constraints.NotBlank; + +public record PaymentRequestDto( + @NotBlank(message = "구독 티어는 필수입니다") + String tier +) { + +} diff --git a/payment-service/src/main/java/com/synapse/payment_service/dto/request/PaymentVerificationRequest.java b/payment-service/payment-service-api/src/main/java/com/synapse/payment_service_api/dto/request/PaymentVerificationRequest.java similarity index 57% rename from payment-service/src/main/java/com/synapse/payment_service/dto/request/PaymentVerificationRequest.java rename to payment-service/payment-service-api/src/main/java/com/synapse/payment_service_api/dto/request/PaymentVerificationRequest.java index 8d148b4..9523dbd 100644 --- a/payment-service/src/main/java/com/synapse/payment_service/dto/request/PaymentVerificationRequest.java +++ b/payment-service/payment-service-api/src/main/java/com/synapse/payment_service_api/dto/request/PaymentVerificationRequest.java @@ -1,13 +1,13 @@ -package com.synapse.payment_service.dto.request; +package com.synapse.payment_service_api.dto.request; import jakarta.validation.constraints.NotBlank; public record PaymentVerificationRequest( - @NotBlank(message = "paymentId는 필수입니다") - String paymentId, - - @NotBlank(message = "iamPortTransactionId는 필수입니다") + @NotBlank(message = "paymentId는 필수입니다") + String paymentId, + + @NotBlank(message = "iamPortTransactionId는 필수입니다") String iamPortTransactionId ) { - + } diff --git a/payment-service/src/main/java/com/synapse/payment_service/dto/request/PaymentWebhookRequest.java b/payment-service/payment-service-api/src/main/java/com/synapse/payment_service_api/dto/request/PaymentWebhookRequest.java similarity index 77% rename from payment-service/src/main/java/com/synapse/payment_service/dto/request/PaymentWebhookRequest.java rename to payment-service/payment-service-api/src/main/java/com/synapse/payment_service_api/dto/request/PaymentWebhookRequest.java index 366092c..85e643c 100644 --- a/payment-service/src/main/java/com/synapse/payment_service/dto/request/PaymentWebhookRequest.java +++ b/payment-service/payment-service-api/src/main/java/com/synapse/payment_service_api/dto/request/PaymentWebhookRequest.java @@ -1,51 +1,50 @@ -package com.synapse.payment_service.dto.request; +package com.synapse.payment_service_api.dto.request; + +import java.io.IOException; +import java.util.Optional; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import java.io.IOException; -import java.util.Optional; - public record PaymentWebhookRequest( - @JsonProperty("type") - String type, - @JsonProperty("timestamp") - String timestamp, - @JsonProperty("data") - JsonNode data + @JsonProperty("type") String type, + @JsonProperty("timestamp") String timestamp, + @JsonProperty("data") JsonNode data ) { - public static PaymentWebhookRequest from(String requestBody, ObjectMapper objectMapper) throws IOException { return objectMapper.readValue(requestBody, PaymentWebhookRequest.class); } public String getPaymentId() { - if (data == null) return null; + if (data == null) + return null; return extractText(data, "paymentId"); } - + public String getTransactionId() { - if (data == null) return null; + if (data == null) + return null; return extractText(data, "transactionId"); } - + public String getBillingKey() { - if (data == null) return null; + if (data == null) + return null; return extractText(data, "billingKey"); } - + private String extractText(JsonNode node, String fieldName) { return Optional.ofNullable(node.get(fieldName)) .map(JsonNode::asText) .orElse(null); } - + public boolean isTransactionWebhook() { return type != null && type.startsWith("Transaction."); } - + public boolean isBillingKeyWebhook() { return type != null && type.startsWith("BillingKey."); } -} \ No newline at end of file +} diff --git a/payment-service/src/main/java/com/synapse/payment_service/dto/response/PaymentPreparationResponse.java b/payment-service/payment-service-api/src/main/java/com/synapse/payment_service_api/dto/response/PaymentPreparationResponse.java similarity index 72% rename from payment-service/src/main/java/com/synapse/payment_service/dto/response/PaymentPreparationResponse.java rename to payment-service/payment-service-api/src/main/java/com/synapse/payment_service_api/dto/response/PaymentPreparationResponse.java index 5f5b4ce..b040fcd 100644 --- a/payment-service/src/main/java/com/synapse/payment_service/dto/response/PaymentPreparationResponse.java +++ b/payment-service/payment-service-api/src/main/java/com/synapse/payment_service_api/dto/response/PaymentPreparationResponse.java @@ -1,4 +1,4 @@ -package com.synapse.payment_service.dto.response; +package com.synapse.payment_service_api.dto.response; import java.math.BigDecimal; @@ -7,5 +7,5 @@ public record PaymentPreparationResponse( String orderName, BigDecimal amount ) { - + } diff --git a/payment-service/payment-service/.gitignore b/payment-service/payment-service/.gitignore new file mode 100644 index 0000000..14af79c --- /dev/null +++ b/payment-service/payment-service/.gitignore @@ -0,0 +1,39 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +**/security/* diff --git a/payment-service/payment-service/build.gradle b/payment-service/payment-service/build.gradle new file mode 100644 index 0000000..ee9000f --- /dev/null +++ b/payment-service/payment-service/build.gradle @@ -0,0 +1,31 @@ +repositories { + mavenCentral() +} + +dependencies { + implementation project(":payment-service-api") + // PortOne + implementation 'io.portone:server-sdk:0.19.0' + // Security + implementation 'org.springframework.boot:spring-boot-starter-security' + // JPA + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + // PostgreSQL + runtimeOnly 'org.postgresql:postgresql' + // OAuth2 Resource Server + implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server' + // WebFlux + implementation 'org.springframework.boot:spring-boot-starter-webflux' + //H2 Database + implementation 'com.h2database:h2' + // Test + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testCompileOnly 'org.projectlombok:lombok' + testAnnotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.security:spring-security-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/payment-service/src/main/java/com/synapse/payment_service/PaymentServiceApplication.java b/payment-service/payment-service/src/main/java/com/synapse/payment_service/PaymentServiceApplication.java similarity index 79% rename from payment-service/src/main/java/com/synapse/payment_service/PaymentServiceApplication.java rename to payment-service/payment-service/src/main/java/com/synapse/payment_service/PaymentServiceApplication.java index 7f26790..d553e39 100644 --- a/payment-service/src/main/java/com/synapse/payment_service/PaymentServiceApplication.java +++ b/payment-service/payment-service/src/main/java/com/synapse/payment_service/PaymentServiceApplication.java @@ -2,9 +2,7 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.scheduling.annotation.EnableScheduling; -@EnableScheduling @SpringBootApplication public class PaymentServiceApplication { diff --git a/payment-service/payment-service/src/main/java/com/synapse/payment_service/PaymentServiceConfiguration.java b/payment-service/payment-service/src/main/java/com/synapse/payment_service/PaymentServiceConfiguration.java new file mode 100644 index 0000000..94b2faf --- /dev/null +++ b/payment-service/payment-service/src/main/java/com/synapse/payment_service/PaymentServiceConfiguration.java @@ -0,0 +1,14 @@ +package com.synapse.payment_service; + +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.scheduling.annotation.EnableScheduling; + +@EnableScheduling +@ComponentScan +@EnableAutoConfiguration +@EnableJpaAuditing +public class PaymentServiceConfiguration { + +} diff --git a/payment-service/src/main/java/com/synapse/payment_service/config/ObjectMapperConfig.java b/payment-service/payment-service/src/main/java/com/synapse/payment_service/configuration/ObjectMapperConfig.java similarity index 90% rename from payment-service/src/main/java/com/synapse/payment_service/config/ObjectMapperConfig.java rename to payment-service/payment-service/src/main/java/com/synapse/payment_service/configuration/ObjectMapperConfig.java index 41f0f24..074f663 100644 --- a/payment-service/src/main/java/com/synapse/payment_service/config/ObjectMapperConfig.java +++ b/payment-service/payment-service/src/main/java/com/synapse/payment_service/configuration/ObjectMapperConfig.java @@ -1,4 +1,4 @@ -package com.synapse.payment_service.config; +package com.synapse.payment_service.configuration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -8,7 +8,7 @@ @Configuration public class ObjectMapperConfig { - + @Bean public ObjectMapper objectMapper() { ObjectMapper objectMapper = new ObjectMapper(); diff --git a/payment-service/src/main/java/com/synapse/payment_service/config/PortOneClientConfig.java b/payment-service/payment-service/src/main/java/com/synapse/payment_service/configuration/PortOneClientConfig.java similarity index 94% rename from payment-service/src/main/java/com/synapse/payment_service/config/PortOneClientConfig.java rename to payment-service/payment-service/src/main/java/com/synapse/payment_service/configuration/PortOneClientConfig.java index 04c8295..8c1816b 100644 --- a/payment-service/src/main/java/com/synapse/payment_service/config/PortOneClientConfig.java +++ b/payment-service/payment-service/src/main/java/com/synapse/payment_service/configuration/PortOneClientConfig.java @@ -1,4 +1,4 @@ -package com.synapse.payment_service.config; +package com.synapse.payment_service.configuration; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; diff --git a/payment-service/src/main/java/com/synapse/payment_service/config/PortOneClientProperties.java b/payment-service/payment-service/src/main/java/com/synapse/payment_service/configuration/PortOneClientProperties.java similarity index 84% rename from payment-service/src/main/java/com/synapse/payment_service/config/PortOneClientProperties.java rename to payment-service/payment-service/src/main/java/com/synapse/payment_service/configuration/PortOneClientProperties.java index 896f58b..92252ec 100644 --- a/payment-service/src/main/java/com/synapse/payment_service/config/PortOneClientProperties.java +++ b/payment-service/payment-service/src/main/java/com/synapse/payment_service/configuration/PortOneClientProperties.java @@ -1,4 +1,4 @@ -package com.synapse.payment_service.config; +package com.synapse.payment_service.configuration; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -10,5 +10,5 @@ public record PortOneClientProperties( String webhookSecret, String channelKey ) { - + } diff --git a/payment-service/src/main/java/com/synapse/payment_service/config/ResourceServerConfig.java b/payment-service/payment-service/src/main/java/com/synapse/payment_service/configuration/ResourceServerConfig.java similarity index 72% rename from payment-service/src/main/java/com/synapse/payment_service/config/ResourceServerConfig.java rename to payment-service/payment-service/src/main/java/com/synapse/payment_service/configuration/ResourceServerConfig.java index 807fcd0..b121003 100644 --- a/payment-service/src/main/java/com/synapse/payment_service/config/ResourceServerConfig.java +++ b/payment-service/payment-service/src/main/java/com/synapse/payment_service/configuration/ResourceServerConfig.java @@ -1,9 +1,9 @@ -package com.synapse.payment_service.config; - -import static org.springframework.security.config.Customizer.withDefaults; +package com.synapse.payment_service.configuration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.authorization.AuthorityAuthorizationManager; +import org.springframework.security.authorization.AuthorizationManagers; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; @@ -15,17 +15,15 @@ import com.synapse.payment_service.filter.MemberAuthenticationFilter; -import lombok.RequiredArgsConstructor; +import static org.springframework.security.config.Customizer.withDefaults; -import org.springframework.security.authorization.AuthorizationManagers; -import org.springframework.security.authorization.AuthorityAuthorizationManager; +import lombok.RequiredArgsConstructor; @Configuration(proxyBeanMethods = false) @EnableMethodSecurity @EnableWebSecurity @RequiredArgsConstructor public class ResourceServerConfig { - private final MemberAuthenticationFilter memberAuthenticationFilter; @Bean @@ -36,20 +34,17 @@ public SecurityFilterChain securityResourceServerFilterChain(HttpSecurity http) .httpBasic(basic -> basic.disable()) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(authorize -> authorize - .requestMatchers("/api/webhooks/**").permitAll() // 웹훅은 인증 없이 허용 - .requestMatchers("/api/payments/**").authenticated() // 결제 API는 인증 필요 + .requestMatchers("/api/webhooks/**").permitAll() // 웹훅은 인증 없이 허용 + .requestMatchers("/api/payments/**").authenticated() // 결제 API는 인증 필요 .requestMatchers("/api/internal/**").access(AuthorizationManagers.allOf( - AuthorityAuthorizationManager.hasAuthority("SCOPE_api.internal"), - AuthorityAuthorizationManager.hasAuthority("SCOPE_account:read") - )) - .anyRequest().authenticated() - ) - .oauth2ResourceServer(oauth2 -> oauth2.jwt(withDefaults())) - .addFilterBefore(memberAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) - .exceptionHandling(exceptions -> exceptions - .authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint()) - .accessDeniedHandler(new BearerTokenAccessDeniedHandler()) - ); + AuthorityAuthorizationManager.hasAuthority("SCOPE_api.internal"), + AuthorityAuthorizationManager.hasAuthority("SCOPE_account:read"))) + .anyRequest().authenticated()) + .oauth2ResourceServer(oauth2 -> oauth2.jwt(withDefaults())) + .addFilterBefore(memberAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) + .exceptionHandling(exceptions -> exceptions + .authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint()) + .accessDeniedHandler(new BearerTokenAccessDeniedHandler())); return http.build(); } } diff --git a/payment-service/src/main/java/com/synapse/payment_service/controller/PaymentController.java b/payment-service/payment-service/src/main/java/com/synapse/payment_service/controller/PaymentController.java similarity index 86% rename from payment-service/src/main/java/com/synapse/payment_service/controller/PaymentController.java rename to payment-service/payment-service/src/main/java/com/synapse/payment_service/controller/PaymentController.java index e738159..9f5a0e8 100644 --- a/payment-service/src/main/java/com/synapse/payment_service/controller/PaymentController.java +++ b/payment-service/payment-service/src/main/java/com/synapse/payment_service/controller/PaymentController.java @@ -9,11 +9,11 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import com.synapse.payment_service.dto.request.CancelSubscriptionRequest; -import com.synapse.payment_service.dto.request.PaymentRequestDto; -import com.synapse.payment_service.dto.request.PaymentVerificationRequest; -import com.synapse.payment_service.dto.response.PaymentPreparationResponse; import com.synapse.payment_service.service.PaymentService; +import com.synapse.payment_service_api.dto.request.CancelSubscriptionRequest; +import com.synapse.payment_service_api.dto.request.PaymentRequestDto; +import com.synapse.payment_service_api.dto.request.PaymentVerificationRequest; +import com.synapse.payment_service_api.dto.response.PaymentPreparationResponse; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -39,6 +39,7 @@ public ResponseEntity requestPayment( /** * 아임포트에서 결제 완료 후 호출되는 메서드로 실제로 결제가 되었는지 확인하는 API 입니다. + * * @param request * @return */ diff --git a/payment-service/src/main/java/com/synapse/payment_service/controller/WebHookController.java b/payment-service/payment-service/src/main/java/com/synapse/payment_service/controller/WebHookController.java similarity index 100% rename from payment-service/src/main/java/com/synapse/payment_service/controller/WebHookController.java rename to payment-service/payment-service/src/main/java/com/synapse/payment_service/controller/WebHookController.java diff --git a/payment-service/src/main/java/com/synapse/payment_service/common/BaseEntity.java b/payment-service/payment-service/src/main/java/com/synapse/payment_service/domain/common/BaseEntity.java similarity index 58% rename from payment-service/src/main/java/com/synapse/payment_service/common/BaseEntity.java rename to payment-service/payment-service/src/main/java/com/synapse/payment_service/domain/common/BaseEntity.java index 0f8c2ca..a3721a6 100644 --- a/payment-service/src/main/java/com/synapse/payment_service/common/BaseEntity.java +++ b/payment-service/payment-service/src/main/java/com/synapse/payment_service/domain/common/BaseEntity.java @@ -1,20 +1,26 @@ -package com.synapse.payment_service.common; +package com.synapse.payment_service.domain.common; import org.springframework.data.annotation.CreatedBy; import org.springframework.data.annotation.LastModifiedBy; import org.springframework.data.jpa.domain.support.AuditingEntityListener; -import jakarta.persistence.*; +import jakarta.persistence.Column; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import lombok.AccessLevel; import lombok.Getter; +import lombok.NoArgsConstructor; @Getter @MappedSuperclass @EntityListeners(AuditingEntityListener.class) +@NoArgsConstructor(access = AccessLevel.PROTECTED) public abstract class BaseEntity extends BaseTimeEntity { @CreatedBy @Column(updatable = false) private String createdBy; @LastModifiedBy - private String lastModifiedBy; + private String updatedBy; + } diff --git a/payment-service/src/main/java/com/synapse/payment_service/common/BaseTimeEntity.java b/payment-service/payment-service/src/main/java/com/synapse/payment_service/domain/common/BaseTimeEntity.java similarity index 55% rename from payment-service/src/main/java/com/synapse/payment_service/common/BaseTimeEntity.java rename to payment-service/payment-service/src/main/java/com/synapse/payment_service/domain/common/BaseTimeEntity.java index f5b246b..6d908e8 100644 --- a/payment-service/src/main/java/com/synapse/payment_service/common/BaseTimeEntity.java +++ b/payment-service/payment-service/src/main/java/com/synapse/payment_service/domain/common/BaseTimeEntity.java @@ -1,4 +1,4 @@ -package com.synapse.payment_service.common; +package com.synapse.payment_service.domain.common; import java.time.LocalDateTime; @@ -6,17 +6,20 @@ import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; -import jakarta.persistence.*; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import lombok.AccessLevel; import lombok.Getter; +import lombok.NoArgsConstructor; @Getter @MappedSuperclass @EntityListeners(AuditingEntityListener.class) +@NoArgsConstructor(access = AccessLevel.PROTECTED) public abstract class BaseTimeEntity { @CreatedDate - @Column(updatable = false) - private LocalDateTime createdDate; + private LocalDateTime createdAt; @LastModifiedDate - private LocalDateTime updatedDate; + private LocalDateTime updatedAt; } diff --git a/payment-service/src/main/java/com/synapse/payment_service/domain/Order.java b/payment-service/payment-service/src/main/java/com/synapse/payment_service/domain/entity/Order.java similarity index 87% rename from payment-service/src/main/java/com/synapse/payment_service/domain/Order.java rename to payment-service/payment-service/src/main/java/com/synapse/payment_service/domain/entity/Order.java index 5b8907f..a3f35c9 100644 --- a/payment-service/src/main/java/com/synapse/payment_service/domain/Order.java +++ b/payment-service/payment-service/src/main/java/com/synapse/payment_service/domain/entity/Order.java @@ -1,16 +1,16 @@ -package com.synapse.payment_service.domain; +package com.synapse.payment_service.domain.entity; import java.math.BigDecimal; import java.time.ZonedDateTime; import java.util.UUID; -import com.synapse.payment_service.common.BaseEntity; +import com.synapse.payment_service.domain.common.BaseEntity; import com.synapse.payment_service.domain.enums.PaymentStatus; import com.synapse.payment_service.domain.enums.SubscriptionTier; import com.synapse.payment_service.exception.ExceptionCode; import com.synapse.payment_service.exception.PaymentVerificationException; -import io.portone.sdk.server.payment.Payment; +import io.portone.sdk.server.payment.Payment; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Builder; @@ -46,7 +46,10 @@ public class Order extends BaseEntity { private ZonedDateTime paidAt; @Builder - public Order(Subscription subscription, String iamPortTransactionId, String paymentId, BigDecimal amount, PaymentStatus status, ZonedDateTime paidAt) { + public Order( + Subscription subscription, String iamPortTransactionId, String paymentId, + BigDecimal amount, PaymentStatus status, ZonedDateTime paidAt + ) { this.subscription = subscription; this.iamPortTransactionId = iamPortTransactionId; this.paymentId = paymentId; @@ -92,13 +95,13 @@ public static Order createForSubscription(Subscription subscription, Subscriptio BigDecimal amount = tier.getMonthlyPrice(); String orderName = tier.getTierName() + "_subscription"; String paymentId = orderName + "_" + UUID.randomUUID(); - + return Order.builder() - .subscription(subscription) - .paymentId(paymentId) - .amount(amount) - .status(PaymentStatus.PENDING) - .build(); + .subscription(subscription) + .paymentId(paymentId) + .amount(amount) + .status(PaymentStatus.PENDING) + .build(); } public String getOrderName() { diff --git a/payment-service/src/main/java/com/synapse/payment_service/domain/Subscription.java b/payment-service/payment-service/src/main/java/com/synapse/payment_service/domain/entity/Subscription.java similarity index 75% rename from payment-service/src/main/java/com/synapse/payment_service/domain/Subscription.java rename to payment-service/payment-service/src/main/java/com/synapse/payment_service/domain/entity/Subscription.java index b18e558..071d19b 100644 --- a/payment-service/src/main/java/com/synapse/payment_service/domain/Subscription.java +++ b/payment-service/payment-service/src/main/java/com/synapse/payment_service/domain/entity/Subscription.java @@ -1,10 +1,10 @@ -package com.synapse.payment_service.domain; +package com.synapse.payment_service.domain.entity; import java.time.ZonedDateTime; import java.time.temporal.TemporalAdjusters; import java.util.UUID; -import com.synapse.payment_service.common.BaseEntity; +import com.synapse.payment_service.domain.common.BaseTimeEntity; import com.synapse.payment_service.domain.enums.SubscriptionStatus; import com.synapse.payment_service.domain.enums.SubscriptionTier; @@ -18,39 +18,42 @@ @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @Table(name = "subscriptions") -public class Subscription extends BaseEntity { +public class Subscription extends BaseTimeEntity { @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; + @Column(name = "subscription_id", nullable = false, unique = true, columnDefinition = "uuid") + private UUID id; - // account-service의 Member와 1:1로 매핑되는 고유 식별자 - @Column(nullable = false, unique = true, columnDefinition = "uuid") + @Column(name = "member_id", nullable = false, unique = true, columnDefinition = "uuid") private UUID memberId; @Enumerated(EnumType.STRING) - @Column(nullable = false) + @Column(name = "tier", nullable = false) private SubscriptionTier tier; - @Column(nullable = false) + @Column(name = "remaining_chat_credits", nullable = false) private int remainingChatCredits; + @Column(name = "expires_at") private ZonedDateTime expiresAt; + @Column(name = "billing_key") private String billingKey; @Enumerated(EnumType.STRING) - @Column(nullable = false) + @Column(name = "status", nullable = false) private SubscriptionStatus status; - @Column(nullable = false) + @Column(name = "auto_renew", nullable = false) private boolean autoRenew = true; - @Column(nullable = false) + @Column(name = "retry_count", nullable = false) private Integer retryCount = 0; @Builder - public Subscription(UUID memberId, SubscriptionTier tier, int remainingChatCredits, ZonedDateTime expiresAt, SubscriptionStatus status) { + public Subscription(UUID id, UUID memberId, SubscriptionTier tier, int remainingChatCredits, ZonedDateTime expiresAt, + SubscriptionStatus status) { + this.id = id; this.memberId = memberId; this.tier = tier; this.remainingChatCredits = remainingChatCredits; @@ -70,11 +73,11 @@ public void updateBillingKey(String billingKey) { this.billingKey = billingKey; this.remainingChatCredits = this.tier.getMaxRequestCount(); } - + public void renewSubscription(SubscriptionTier newTier) { // 기존 구독 갱신 - 기존 만료일에서 1달 연장 ZonedDateTime currentExpiresAt = this.expiresAt != null ? this.expiresAt : ZonedDateTime.now(); - + // 현재 만료일이 해당 월의 마지막 날인지 확인 boolean isLastDayOfMonth = currentExpiresAt.getDayOfMonth() == currentExpiresAt.toLocalDate().lengthOfMonth(); @@ -85,7 +88,7 @@ public void renewSubscription(SubscriptionTier newTier) { // 그렇지 않다면, 단순히 한 달을 더 한다 (예: 15일 -> 다음 달 15일) this.expiresAt = currentExpiresAt.plusMonths(1); } - + // 갱신 시 크레딧도 해당 티어의 기본 크레딧으로 초기화 this.remainingChatCredits = this.tier.getMaxRequestCount(); this.status = SubscriptionStatus.ACTIVE; diff --git a/payment-service/src/main/java/com/synapse/payment_service/domain/enums/PaymentStatus.java b/payment-service/payment-service/src/main/java/com/synapse/payment_service/domain/enums/PaymentStatus.java similarity index 88% rename from payment-service/src/main/java/com/synapse/payment_service/domain/enums/PaymentStatus.java rename to payment-service/payment-service/src/main/java/com/synapse/payment_service/domain/enums/PaymentStatus.java index d6afbe1..f7eba2b 100644 --- a/payment-service/src/main/java/com/synapse/payment_service/domain/enums/PaymentStatus.java +++ b/payment-service/payment-service/src/main/java/com/synapse/payment_service/domain/enums/PaymentStatus.java @@ -6,7 +6,7 @@ public enum PaymentStatus { CANCELED, // 결제 취소 (환불) PENDING, // 결제 대기 PARTIAL_CANCELLED, // 부분 취소 - PAY_PENDING, // 결제 완료 대기 + PAY_PENDING, // 결제 완료 대기 READY, // 준비 상태 VIRTUAL_ACCOUNT_ISSUED // 가상계좌 발급 완료 } diff --git a/payment-service/src/main/java/com/synapse/payment_service/domain/enums/SubscriptionStatus.java b/payment-service/payment-service/src/main/java/com/synapse/payment_service/domain/enums/SubscriptionStatus.java similarity index 100% rename from payment-service/src/main/java/com/synapse/payment_service/domain/enums/SubscriptionStatus.java rename to payment-service/payment-service/src/main/java/com/synapse/payment_service/domain/enums/SubscriptionStatus.java diff --git a/payment-service/src/main/java/com/synapse/payment_service/domain/enums/SubscriptionTier.java b/payment-service/payment-service/src/main/java/com/synapse/payment_service/domain/enums/SubscriptionTier.java similarity index 98% rename from payment-service/src/main/java/com/synapse/payment_service/domain/enums/SubscriptionTier.java rename to payment-service/payment-service/src/main/java/com/synapse/payment_service/domain/enums/SubscriptionTier.java index 6520fab..5eaa4d0 100644 --- a/payment-service/src/main/java/com/synapse/payment_service/domain/enums/SubscriptionTier.java +++ b/payment-service/payment-service/src/main/java/com/synapse/payment_service/domain/enums/SubscriptionTier.java @@ -10,8 +10,7 @@ public enum SubscriptionTier { PRO("pro", "subscription-pro", 100, new BigDecimal("100000")), FREE("free", "subscription-free", 10, BigDecimal.ZERO), - UNKNOWN("unknown", "subscription-free", 10, BigDecimal.ZERO) - ; + UNKNOWN("unknown", "subscription-free", 10, BigDecimal.ZERO); private final String tierName; private final String policyName; diff --git a/payment-service/src/main/java/com/synapse/payment_service/repository/OrderRepository.java b/payment-service/payment-service/src/main/java/com/synapse/payment_service/domain/repository/OrderRepository.java similarity index 62% rename from payment-service/src/main/java/com/synapse/payment_service/repository/OrderRepository.java rename to payment-service/payment-service/src/main/java/com/synapse/payment_service/domain/repository/OrderRepository.java index c472b5f..38e60c3 100644 --- a/payment-service/src/main/java/com/synapse/payment_service/repository/OrderRepository.java +++ b/payment-service/payment-service/src/main/java/com/synapse/payment_service/domain/repository/OrderRepository.java @@ -1,13 +1,14 @@ -package com.synapse.payment_service.repository; +package com.synapse.payment_service.domain.repository; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; -import com.synapse.payment_service.domain.Order; -import com.synapse.payment_service.domain.Subscription; +import com.synapse.payment_service.domain.entity.Order; +import com.synapse.payment_service.domain.entity.Subscription; public interface OrderRepository extends JpaRepository { Optional findByPaymentId(String paymentId); + Optional findBySubscription(Subscription subscription); } diff --git a/payment-service/src/main/java/com/synapse/payment_service/repository/SubscriptionRepository.java b/payment-service/payment-service/src/main/java/com/synapse/payment_service/domain/repository/SubscriptionRepository.java similarity index 84% rename from payment-service/src/main/java/com/synapse/payment_service/repository/SubscriptionRepository.java rename to payment-service/payment-service/src/main/java/com/synapse/payment_service/domain/repository/SubscriptionRepository.java index a0196ee..48e47e0 100644 --- a/payment-service/src/main/java/com/synapse/payment_service/repository/SubscriptionRepository.java +++ b/payment-service/payment-service/src/main/java/com/synapse/payment_service/domain/repository/SubscriptionRepository.java @@ -1,4 +1,4 @@ -package com.synapse.payment_service.repository; +package com.synapse.payment_service.domain.repository; import java.time.ZonedDateTime; import java.util.List; @@ -9,21 +9,22 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; -import com.synapse.payment_service.domain.Subscription; +import com.synapse.payment_service.domain.entity.Subscription; import com.synapse.payment_service.domain.enums.SubscriptionStatus; -public interface SubscriptionRepository extends JpaRepository { +public interface SubscriptionRepository extends JpaRepository { Optional findByMemberId(UUID memberId); + Optional findByBillingKey(String billingKey); - + @Query("SELECT s FROM Subscription s WHERE s.status = 'ACTIVE' AND s.autoRenew = true AND s.billingKey IS NOT NULL AND s.expiresAt >= :startOfDay AND s.expiresAt < :endOfDay") List findActiveSubscriptionsDueForRenewal(@Param("startOfDay") ZonedDateTime startOfDay, @Param("endOfDay") ZonedDateTime endOfDay); - + /** * 만료 처리 대상 구독을 조회합니다. * CANCELED 또는 PAYMENT_FAILED 상태이고, 만료일이 지난 구독을 반환합니다. * - * @param statuses 조회할 구독 상태 목록 (CANCELED, PAYMENT_FAILED) + * @param statuses 조회할 구독 상태 목록 (CANCELED, PAYMENT_FAILED) * @param currentTime 현재 시간 * @return 만료 처리 대상 구독 목록 */ diff --git a/payment-service/src/main/java/com/synapse/payment_service/exception/ExceptionCode.java b/payment-service/payment-service/src/main/java/com/synapse/payment_service/exception/ExceptionCode.java similarity index 98% rename from payment-service/src/main/java/com/synapse/payment_service/exception/ExceptionCode.java rename to payment-service/payment-service/src/main/java/com/synapse/payment_service/exception/ExceptionCode.java index f20a004..8c057de 100644 --- a/payment-service/src/main/java/com/synapse/payment_service/exception/ExceptionCode.java +++ b/payment-service/payment-service/src/main/java/com/synapse/payment_service/exception/ExceptionCode.java @@ -1,30 +1,30 @@ package com.synapse.payment_service.exception; +import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.CONFLICT; +import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; +import static org.springframework.http.HttpStatus.NOT_FOUND; +import static org.springframework.http.HttpStatus.UNAUTHORIZED; + import org.springframework.http.HttpStatus; import lombok.Getter; import lombok.RequiredArgsConstructor; -import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; -import static org.springframework.http.HttpStatus.NOT_FOUND; -import static org.springframework.http.HttpStatus.UNAUTHORIZED; -import static org.springframework.http.HttpStatus.CONFLICT; -import static org.springframework.http.HttpStatus.BAD_REQUEST; - @Getter @RequiredArgsConstructor public enum ExceptionCode { PORT_ONE_CLIENT_ERROR(INTERNAL_SERVER_ERROR, "P001", "포트원 클라이언트 오류"), - + SUBSCRIPTION_NOT_FOUND(NOT_FOUND, "P002", "구독 정보를 찾을 수 없습니다"), ORDER_NOT_FOUND(NOT_FOUND, "P003", "주문 정보를 찾을 수 없습니다"), BILLING_KEY_NOT_FOUND(NOT_FOUND, "P008", "빌링키 정보를 찾을 수 없습니다"), - + PAYMENT_VERIFICATION_FAILED(BAD_REQUEST, "P004", "존재하지 않는 거래입니다"), PAYMENT_NOT_RECOGNIZED(INTERNAL_SERVER_ERROR, "P005", "결제 정보를 인식할 수 없습니다"), PAYMENT_AMOUNT_MISMATCH(CONFLICT, "P006", "결제 금액이 불일치합니다"), UNSUPPORTED_PAYMENT_STATUS(INTERNAL_SERVER_ERROR, "P007", "지원하지 않는 결제 상태입니다"), - + UNAUTHORIZED_USER(UNAUTHORIZED, "P009", "권한이 없습니다"), ; diff --git a/payment-service/src/main/java/com/synapse/payment_service/exception/NotFoundException.java b/payment-service/payment-service/src/main/java/com/synapse/payment_service/exception/NotFoundException.java similarity index 100% rename from payment-service/src/main/java/com/synapse/payment_service/exception/NotFoundException.java rename to payment-service/payment-service/src/main/java/com/synapse/payment_service/exception/NotFoundException.java diff --git a/payment-service/src/main/java/com/synapse/payment_service/exception/PaymentException.java b/payment-service/payment-service/src/main/java/com/synapse/payment_service/exception/PaymentException.java similarity index 100% rename from payment-service/src/main/java/com/synapse/payment_service/exception/PaymentException.java rename to payment-service/payment-service/src/main/java/com/synapse/payment_service/exception/PaymentException.java diff --git a/payment-service/src/main/java/com/synapse/payment_service/exception/PaymentVerificationException.java b/payment-service/payment-service/src/main/java/com/synapse/payment_service/exception/PaymentVerificationException.java similarity index 98% rename from payment-service/src/main/java/com/synapse/payment_service/exception/PaymentVerificationException.java rename to payment-service/payment-service/src/main/java/com/synapse/payment_service/exception/PaymentVerificationException.java index 02db0e3..de5b17d 100644 --- a/payment-service/src/main/java/com/synapse/payment_service/exception/PaymentVerificationException.java +++ b/payment-service/payment-service/src/main/java/com/synapse/payment_service/exception/PaymentVerificationException.java @@ -1,7 +1,7 @@ package com.synapse.payment_service.exception; - + public class PaymentVerificationException extends PaymentException { public PaymentVerificationException(ExceptionCode exceptionCode) { super(exceptionCode); } -} \ No newline at end of file +} diff --git a/payment-service/src/main/java/com/synapse/payment_service/exception/UnauthorizedException.java b/payment-service/payment-service/src/main/java/com/synapse/payment_service/exception/UnauthorizedException.java similarity index 100% rename from payment-service/src/main/java/com/synapse/payment_service/exception/UnauthorizedException.java rename to payment-service/payment-service/src/main/java/com/synapse/payment_service/exception/UnauthorizedException.java diff --git a/payment-service/src/main/java/com/synapse/payment_service/filter/MemberAuthenticationFilter.java b/payment-service/payment-service/src/main/java/com/synapse/payment_service/filter/MemberAuthenticationFilter.java similarity index 97% rename from payment-service/src/main/java/com/synapse/payment_service/filter/MemberAuthenticationFilter.java rename to payment-service/payment-service/src/main/java/com/synapse/payment_service/filter/MemberAuthenticationFilter.java index 6d75d5c..34f5489 100644 --- a/payment-service/src/main/java/com/synapse/payment_service/filter/MemberAuthenticationFilter.java +++ b/payment-service/payment-service/src/main/java/com/synapse/payment_service/filter/MemberAuthenticationFilter.java @@ -23,11 +23,11 @@ @Component @RequiredArgsConstructor public class MemberAuthenticationFilter extends OncePerRequestFilter { - + @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) - throws ServletException, IOException { - + throws ServletException, IOException { + String memberId = request.getHeader("X-Authenticated-Member-Id"); String memberRole = request.getHeader("X-Authenticated-Member-Role"); diff --git a/payment-service/src/main/java/com/synapse/payment_service/service/PaymentService.java b/payment-service/payment-service/src/main/java/com/synapse/payment_service/service/PaymentService.java similarity index 76% rename from payment-service/src/main/java/com/synapse/payment_service/service/PaymentService.java rename to payment-service/payment-service/src/main/java/com/synapse/payment_service/service/PaymentService.java index bf8eafd..837bb1e 100644 --- a/payment-service/src/main/java/com/synapse/payment_service/service/PaymentService.java +++ b/payment-service/payment-service/src/main/java/com/synapse/payment_service/service/PaymentService.java @@ -6,27 +6,27 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import com.synapse.payment_service.domain.Order; -import com.synapse.payment_service.domain.Subscription; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.synapse.payment_service.domain.entity.Order; +import com.synapse.payment_service.domain.entity.Subscription; import com.synapse.payment_service.domain.enums.SubscriptionTier; -import com.synapse.payment_service.dto.request.CancelSubscriptionRequest; -import com.synapse.payment_service.dto.request.PaymentRequestDto; -import com.synapse.payment_service.dto.request.PaymentVerificationRequest; -import com.synapse.payment_service.dto.request.PaymentWebhookRequest; -import com.synapse.payment_service.dto.response.PaymentPreparationResponse; +import com.synapse.payment_service.domain.repository.OrderRepository; +import com.synapse.payment_service.domain.repository.SubscriptionRepository; import com.synapse.payment_service.exception.ExceptionCode; import com.synapse.payment_service.exception.NotFoundException; import com.synapse.payment_service.exception.PaymentVerificationException; import com.synapse.payment_service.exception.UnauthorizedException; -import com.synapse.payment_service.repository.OrderRepository; -import com.synapse.payment_service.repository.SubscriptionRepository; -import com.synapse.payment_service.service.converter.PaymentStatusConverter; +import com.synapse.payment_service.service.convert.PaymentStatusConverter; +import com.synapse.payment_service_api.dto.request.CancelSubscriptionRequest; +import com.synapse.payment_service_api.dto.request.PaymentRequestDto; +import com.synapse.payment_service_api.dto.request.PaymentVerificationRequest; +import com.synapse.payment_service_api.dto.request.PaymentWebhookRequest; +import com.synapse.payment_service_api.dto.response.PaymentPreparationResponse; import io.portone.sdk.server.PortOneClient; import io.portone.sdk.server.payment.Payment; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import com.fasterxml.jackson.databind.ObjectMapper; @Slf4j @Transactional(readOnly = true) @@ -43,9 +43,10 @@ public class PaymentService { @Transactional public PaymentPreparationResponse preparePayment(UUID memberId, PaymentRequestDto request) { SubscriptionTier tier = SubscriptionTier.valueOf(request.tier().toUpperCase()); - + Subscription subscription = subscriptionRepository.findByMemberId(memberId) - .orElseThrow(() -> new NotFoundException(ExceptionCode.SUBSCRIPTION_NOT_FOUND)); // 현재 인증 서버와 연동이 안되어있기 때문에 테스트로 검증 + .orElseThrow(() -> new NotFoundException(ExceptionCode.SUBSCRIPTION_NOT_FOUND)); // 현재 인증 서버와 연동이 안되어있기 + // 때문에 테스트로 검증 // 도메인 객체의 팩토리 메서드 사용 Order order = Order.createForSubscription(subscription, tier); @@ -62,12 +63,11 @@ public void verifyAndProcess(PaymentVerificationRequest request, UUID memberId) processPaymentVerification(request.paymentId(), request.iamPortTransactionId(), memberId); } - // 웹 훅 용입니다. @Transactional public void verifyAndProcessWebhook(String requestBody) throws IOException { PaymentWebhookRequest webhookRequest = PaymentWebhookRequest.from(requestBody, objectMapper); - if(webhookRequest.isTransactionWebhook()) { + if (webhookRequest.isTransactionWebhook()) { String paymentId = webhookRequest.getPaymentId(); String transactionId = webhookRequest.getTransactionId(); processPaymentVerification(paymentId, transactionId, null); // 웹훅은 memberId null로 전달 @@ -77,8 +77,8 @@ public void verifyAndProcessWebhook(String requestBody) throws IOException { // 결제 검증 (memberId가 null이면 웹훅용, 아니면 클라이언트용) private void processPaymentVerification(String paymentId, String iamPortTransactionId, UUID memberId) { Order order = orderRepository.findByPaymentId(paymentId) - .orElseThrow(() -> new NotFoundException(ExceptionCode.ORDER_NOT_FOUND)); - + .orElseThrow(() -> new NotFoundException(ExceptionCode.ORDER_NOT_FOUND)); + // 클라이언트 요청인 경우에만 권한 검증 (웹훅은 memberId가 null) if (memberId != null && !order.getSubscription().getMemberId().equals(memberId)) { throw new UnauthorizedException(ExceptionCode.UNAUTHORIZED_USER); @@ -107,13 +107,13 @@ private void processPaymentVerification(String paymentId, String iamPortTransact try { order.validatePaymentAmount(recognizedPayment); } catch (PaymentVerificationException e) { - log.error("결제 금액 불일치. 주문금액={}, 실제결제금액={}, paymentId={}", - order.getAmount(), recognizedPayment.getAmount().getTotal(), order.getPaymentId()); + log.error("결제 금액 불일치. 주문금액={}, 실제결제금액={}, paymentId={}", + order.getAmount(), recognizedPayment.getAmount().getTotal(), order.getPaymentId()); throw e; } paymentStatusConverter.processPayment(order, payment); - + // 도메인 객체를 통한 빌링키 처리 if (order.hasBillingKey(recognizedPayment)) { Subscription subscription = order.getSubscription(); @@ -126,19 +126,20 @@ private void processPaymentVerification(String paymentId, String iamPortTransact @Transactional public void cancelSubscription(UUID memberId, CancelSubscriptionRequest request) { Subscription subscription = subscriptionRepository.findByMemberId(memberId) - .orElseThrow(() -> new NotFoundException(ExceptionCode.SUBSCRIPTION_NOT_FOUND)); + .orElseThrow(() -> new NotFoundException(ExceptionCode.SUBSCRIPTION_NOT_FOUND)); Order order = orderRepository.findBySubscription(subscription) - .orElseThrow(() -> new NotFoundException(ExceptionCode.ORDER_NOT_FOUND)); - + .orElseThrow(() -> new NotFoundException(ExceptionCode.ORDER_NOT_FOUND)); + // String paymentId = order.getPaymentId(); - // portOneClient.getPayment().cancelPayment(paymentId, null, null, null, request.reason(), null, null, null, null).join(); + // portOneClient.getPayment().cancelPayment(paymentId, null, null, null, + // request.reason(), null, null, null, null).join(); // 도메인 객체의 비즈니스 메서드 사용 order.cancel(); subscription.deactivate(); - + orderRepository.save(order); subscriptionRepository.save(subscription); } diff --git a/payment-service/src/main/java/com/synapse/payment_service/service/SubscriptionBillingService.java b/payment-service/payment-service/src/main/java/com/synapse/payment_service/service/SubscriptionBillingService.java similarity index 83% rename from payment-service/src/main/java/com/synapse/payment_service/service/SubscriptionBillingService.java rename to payment-service/payment-service/src/main/java/com/synapse/payment_service/service/SubscriptionBillingService.java index 787adba..4bde3ce 100644 --- a/payment-service/src/main/java/com/synapse/payment_service/service/SubscriptionBillingService.java +++ b/payment-service/payment-service/src/main/java/com/synapse/payment_service/service/SubscriptionBillingService.java @@ -8,12 +8,12 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import com.synapse.payment_service.config.PortOneClientProperties; -import com.synapse.payment_service.domain.Order; -import com.synapse.payment_service.domain.Subscription; +import com.synapse.payment_service.configuration.PortOneClientProperties; +import com.synapse.payment_service.domain.entity.Order; +import com.synapse.payment_service.domain.entity.Subscription; import com.synapse.payment_service.domain.enums.PaymentStatus; -import com.synapse.payment_service.repository.OrderRepository; -import com.synapse.payment_service.repository.SubscriptionRepository; +import com.synapse.payment_service.domain.repository.OrderRepository; +import com.synapse.payment_service.domain.repository.SubscriptionRepository; import io.portone.sdk.server.PortOneClient; import io.portone.sdk.server.common.PaymentAmountInput; @@ -21,10 +21,10 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -@Transactional(readOnly = true) +@Slf4j @Service +@Transactional(readOnly = true) @RequiredArgsConstructor -@Slf4j public class SubscriptionBillingService { private final SubscriptionRepository subscriptionRepository; private final OrderRepository orderRepository; @@ -37,7 +37,7 @@ public void processDailySubscriptions() { LocalDate today = LocalDate.now(); ZonedDateTime startOfDay = today.atStartOfDay(ZoneId.systemDefault()); ZonedDateTime endOfDay = today.plusDays(1).atStartOfDay(ZoneId.systemDefault()); - + List targets = subscriptionRepository.findActiveSubscriptionsDueForRenewal(startOfDay, endOfDay); for (Subscription subscription : targets) { chargeWithBillingKey(subscription); @@ -48,19 +48,19 @@ private void chargeWithBillingKey(Subscription subscription) { // Order 객체를 먼저 생성하여 일관된 로직 사용 Order order = Order.createForSubscription(subscription, subscription.getTier()); orderRepository.save(order); // PENDING 상태로 저장 - + String billingKey = subscription.getBillingKey(); PaymentAmountInput amount = new PaymentAmountInput(subscription.getTier().getMonthlyPrice().longValue(), 0L, 0L); // 빌링키 결제 요청 try { PayWithBillingKeyResponse response = portOneClient.getPayment().payWithBillingKey( - order.getPaymentId(), - billingKey, - portOneClientProperties.channelKey(), - order.getOrderName(), - null, null, amount, null, null, null, null, null, null, null, null, null, null, null, null, null, null - ).join(); + order.getPaymentId(), + billingKey, + portOneClientProperties.channelKey(), + order.getOrderName(), + null, null, amount, null, null, null, null, null, null, null, null, null, null, null, null, null, + null).join(); successHandler(response, order, subscription); log.info("구독 결제 성공. paymentId={}, orderName={}, subscriptionId={}", order.getPaymentId(), order.getOrderName(), subscription.getId()); } catch (Exception e) { @@ -73,7 +73,7 @@ private void successHandler(PayWithBillingKeyResponse response, Order order, Sub // 기존 Order 객체 업데이트 order.updateIamPortTransactionId(response.getPayment().getPgTxId()); order.markAsPaid(); - + subscription.renewSubscription(subscription.getTier()); orderRepository.save(order); @@ -83,7 +83,7 @@ private void failureHandler(Order order, Subscription subscription) { // 기존 Order 객체 상태를 FAILED로 업데이트 order.updateStatus(PaymentStatus.FAILED); orderRepository.save(order); - + // 구독 상태를 PAYMENT_FAILED로 변경하고 retryCount 증가 subscription.handlePaymentFailure(); subscriptionRepository.save(subscription); diff --git a/payment-service/src/main/java/com/synapse/payment_service/service/converter/DelegatingPaymentStatusConverter.java b/payment-service/payment-service/src/main/java/com/synapse/payment_service/service/convert/DelegatingPaymentStatusConverter.java similarity index 63% rename from payment-service/src/main/java/com/synapse/payment_service/service/converter/DelegatingPaymentStatusConverter.java rename to payment-service/payment-service/src/main/java/com/synapse/payment_service/service/convert/DelegatingPaymentStatusConverter.java index b495b7e..8d7b347 100644 --- a/payment-service/src/main/java/com/synapse/payment_service/service/converter/DelegatingPaymentStatusConverter.java +++ b/payment-service/payment-service/src/main/java/com/synapse/payment_service/service/convert/DelegatingPaymentStatusConverter.java @@ -1,4 +1,4 @@ -package com.synapse.payment_service.service.converter; +package com.synapse.payment_service.service.convert; import java.util.Arrays; import java.util.Collections; @@ -8,16 +8,16 @@ import org.springframework.stereotype.Component; import org.springframework.util.Assert; -import com.synapse.payment_service.domain.Order; +import com.synapse.payment_service.domain.entity.Order; import com.synapse.payment_service.exception.ExceptionCode; import com.synapse.payment_service.exception.PaymentVerificationException; -import com.synapse.payment_service.service.converter.impl.CancelledPaymentConverter; -import com.synapse.payment_service.service.converter.impl.FailedPaymentConverter; -import com.synapse.payment_service.service.converter.impl.PaidPaymentConverter; -import com.synapse.payment_service.service.converter.impl.PartialCancelledPaymentConverter; -import com.synapse.payment_service.service.converter.impl.PayPendingPaymentConverter; -import com.synapse.payment_service.service.converter.impl.ReadyPaymentConverter; -import com.synapse.payment_service.service.converter.impl.VirtualAccountIssuedPaymentConverter; +import com.synapse.payment_service.service.convert.impl.CancelledPaymentConverter; +import com.synapse.payment_service.service.convert.impl.FailedPaymentConverter; +import com.synapse.payment_service.service.convert.impl.PaidPaymentConverter; +import com.synapse.payment_service.service.convert.impl.PartialCancelledPaymentConverter; +import com.synapse.payment_service.service.convert.impl.PayPendingPaymentConverter; +import com.synapse.payment_service.service.convert.impl.ReadyPaymentConverter; +import com.synapse.payment_service.service.convert.impl.VirtualAccountIssuedPaymentConverter; import io.portone.sdk.server.payment.Payment; import lombok.extern.slf4j.Slf4j; @@ -30,14 +30,13 @@ public class DelegatingPaymentStatusConverter implements PaymentStatusConverter public DelegatingPaymentStatusConverter() { List paymentConverters = Arrays.asList( - new PaidPaymentConverter(), - new CancelledPaymentConverter(), - new PartialCancelledPaymentConverter(), - new PayPendingPaymentConverter(), - new ReadyPaymentConverter(), - new VirtualAccountIssuedPaymentConverter(), - new FailedPaymentConverter() - ); + new PaidPaymentConverter(), + new CancelledPaymentConverter(), + new PartialCancelledPaymentConverter(), + new PayPendingPaymentConverter(), + new ReadyPaymentConverter(), + new VirtualAccountIssuedPaymentConverter(), + new FailedPaymentConverter()); this.converters = Collections.unmodifiableList(new LinkedList<>(paymentConverters)); } @@ -59,7 +58,7 @@ public void processPayment(Order order, Payment payment) { return; } } - + // 처리할 수 있는 컨버터가 없는 경우 log.error("지원하지 않는 결제 상태: {}", payment.getClass()); throw new PaymentVerificationException(ExceptionCode.UNSUPPORTED_PAYMENT_STATUS); diff --git a/payment-service/src/main/java/com/synapse/payment_service/service/converter/PaymentStatusConverter.java b/payment-service/payment-service/src/main/java/com/synapse/payment_service/service/convert/PaymentStatusConverter.java similarity index 66% rename from payment-service/src/main/java/com/synapse/payment_service/service/converter/PaymentStatusConverter.java rename to payment-service/payment-service/src/main/java/com/synapse/payment_service/service/convert/PaymentStatusConverter.java index f3b8e58..d1b5009 100644 --- a/payment-service/src/main/java/com/synapse/payment_service/service/converter/PaymentStatusConverter.java +++ b/payment-service/payment-service/src/main/java/com/synapse/payment_service/service/convert/PaymentStatusConverter.java @@ -1,21 +1,18 @@ -package com.synapse.payment_service.service.converter; +package com.synapse.payment_service.service.convert; -import com.synapse.payment_service.domain.Order; +import com.synapse.payment_service.domain.entity.Order; import io.portone.sdk.server.payment.Payment; -/** - * 결제 상태별 처리를 담당하는 컨버터 인터페이스 - */ public interface PaymentStatusConverter { - + /** * 해당 컨버터가 주어진 결제 상태를 처리할 수 있는지 확인 */ boolean canHandle(Class paymentStatus); - + /** * 결제 상태에 따른 주문 처리 로직 실행 */ void processPayment(Order order, Payment payment); -} \ No newline at end of file +} diff --git a/payment-service/src/main/java/com/synapse/payment_service/service/converter/impl/CancelledPaymentConverter.java b/payment-service/payment-service/src/main/java/com/synapse/payment_service/service/convert/impl/CancelledPaymentConverter.java similarity index 81% rename from payment-service/src/main/java/com/synapse/payment_service/service/converter/impl/CancelledPaymentConverter.java rename to payment-service/payment-service/src/main/java/com/synapse/payment_service/service/convert/impl/CancelledPaymentConverter.java index 546bf64..838af6d 100644 --- a/payment-service/src/main/java/com/synapse/payment_service/service/converter/impl/CancelledPaymentConverter.java +++ b/payment-service/payment-service/src/main/java/com/synapse/payment_service/service/convert/impl/CancelledPaymentConverter.java @@ -1,10 +1,10 @@ -package com.synapse.payment_service.service.converter.impl; +package com.synapse.payment_service.service.convert.impl; import org.springframework.transaction.annotation.Transactional; -import com.synapse.payment_service.domain.Order; +import com.synapse.payment_service.domain.entity.Order; import com.synapse.payment_service.domain.enums.PaymentStatus; -import com.synapse.payment_service.service.converter.PaymentStatusConverter; +import com.synapse.payment_service.service.convert.PaymentStatusConverter; import io.portone.sdk.server.payment.CancelledPayment; import io.portone.sdk.server.payment.Payment; @@ -22,13 +22,13 @@ public boolean canHandle(Class paymentStatus) { @Transactional public void processPayment(Order order, Payment payment) { log.info("결제 취소 처리 시작. paymentId={}", order.getPaymentId()); - + // 주문 상태를 취소로 업데이트 order.updateStatus(PaymentStatus.CANCELED); - + // 구독 비활성화 order.getSubscription().deactivate(); - + log.info("결제 취소 처리 완료. paymentId={}", order.getPaymentId()); } -} +} diff --git a/payment-service/src/main/java/com/synapse/payment_service/service/converter/impl/FailedPaymentConverter.java b/payment-service/payment-service/src/main/java/com/synapse/payment_service/service/convert/impl/FailedPaymentConverter.java similarity index 78% rename from payment-service/src/main/java/com/synapse/payment_service/service/converter/impl/FailedPaymentConverter.java rename to payment-service/payment-service/src/main/java/com/synapse/payment_service/service/convert/impl/FailedPaymentConverter.java index 92a4bb0..4e546a5 100644 --- a/payment-service/src/main/java/com/synapse/payment_service/service/converter/impl/FailedPaymentConverter.java +++ b/payment-service/payment-service/src/main/java/com/synapse/payment_service/service/convert/impl/FailedPaymentConverter.java @@ -1,10 +1,10 @@ -package com.synapse.payment_service.service.converter.impl; +package com.synapse.payment_service.service.convert.impl; import org.springframework.transaction.annotation.Transactional; -import com.synapse.payment_service.domain.Order; +import com.synapse.payment_service.domain.entity.Order; import com.synapse.payment_service.domain.enums.PaymentStatus; -import com.synapse.payment_service.service.converter.PaymentStatusConverter; +import com.synapse.payment_service.service.convert.PaymentStatusConverter; import io.portone.sdk.server.payment.FailedPayment; import io.portone.sdk.server.payment.Payment; @@ -24,4 +24,4 @@ public void processPayment(Order order, Payment payment) { // 주문 상태를 실패로 업데이트 order.updateStatus(PaymentStatus.FAILED); } -} +} diff --git a/payment-service/src/main/java/com/synapse/payment_service/service/converter/impl/PaidPaymentConverter.java b/payment-service/payment-service/src/main/java/com/synapse/payment_service/service/convert/impl/PaidPaymentConverter.java similarity index 84% rename from payment-service/src/main/java/com/synapse/payment_service/service/converter/impl/PaidPaymentConverter.java rename to payment-service/payment-service/src/main/java/com/synapse/payment_service/service/convert/impl/PaidPaymentConverter.java index 04b1a45..fe04b81 100644 --- a/payment-service/src/main/java/com/synapse/payment_service/service/converter/impl/PaidPaymentConverter.java +++ b/payment-service/payment-service/src/main/java/com/synapse/payment_service/service/convert/impl/PaidPaymentConverter.java @@ -1,14 +1,14 @@ -package com.synapse.payment_service.service.converter.impl; +package com.synapse.payment_service.service.convert.impl; import org.springframework.transaction.annotation.Transactional; -import com.synapse.payment_service.domain.Order; -import com.synapse.payment_service.domain.Subscription; +import com.synapse.payment_service.domain.entity.Order; +import com.synapse.payment_service.domain.entity.Subscription; import com.synapse.payment_service.domain.enums.PaymentStatus; import com.synapse.payment_service.domain.enums.SubscriptionTier; import com.synapse.payment_service.exception.ExceptionCode; import com.synapse.payment_service.exception.PaymentVerificationException; -import com.synapse.payment_service.service.converter.PaymentStatusConverter; +import com.synapse.payment_service.service.convert.PaymentStatusConverter; import io.portone.sdk.server.payment.PaidPayment; import io.portone.sdk.server.payment.Payment; @@ -28,14 +28,14 @@ public void processPayment(Order order, Payment payment) { if (!(payment instanceof Payment.Recognized recognizedPayment)) { throw new PaymentVerificationException(ExceptionCode.PAYMENT_NOT_RECOGNIZED); } - log.info("결제 완료 처리 시작. paymentId={}, amount={}", + log.info("결제 완료 처리 시작. paymentId={}, amount={}", order.getPaymentId(), recognizedPayment.getAmount().getTotal()); - + order.updateStatus(PaymentStatus.PAID); Subscription subscription = order.getSubscription(); subscription.updateBillingKey(recognizedPayment.getBillingKey()); subscription.renewSubscription(SubscriptionTier.PRO); - + log.info("결제 완료 처리 완료. paymentId={}", order.getPaymentId()); } -} +} diff --git a/payment-service/src/main/java/com/synapse/payment_service/service/converter/impl/PartialCancelledPaymentConverter.java b/payment-service/payment-service/src/main/java/com/synapse/payment_service/service/convert/impl/PartialCancelledPaymentConverter.java similarity index 83% rename from payment-service/src/main/java/com/synapse/payment_service/service/converter/impl/PartialCancelledPaymentConverter.java rename to payment-service/payment-service/src/main/java/com/synapse/payment_service/service/convert/impl/PartialCancelledPaymentConverter.java index 468a2ab..a0b1725 100644 --- a/payment-service/src/main/java/com/synapse/payment_service/service/converter/impl/PartialCancelledPaymentConverter.java +++ b/payment-service/payment-service/src/main/java/com/synapse/payment_service/service/convert/impl/PartialCancelledPaymentConverter.java @@ -1,10 +1,10 @@ -package com.synapse.payment_service.service.converter.impl; +package com.synapse.payment_service.service.convert.impl; import org.springframework.transaction.annotation.Transactional; -import com.synapse.payment_service.domain.Order; +import com.synapse.payment_service.domain.entity.Order; import com.synapse.payment_service.domain.enums.PaymentStatus; -import com.synapse.payment_service.service.converter.PaymentStatusConverter; +import com.synapse.payment_service.service.convert.PaymentStatusConverter; import io.portone.sdk.server.payment.PartialCancelledPayment; import io.portone.sdk.server.payment.Payment; @@ -22,11 +22,11 @@ public boolean canHandle(Class paymentStatus) { @Transactional public void processPayment(Order order, Payment payment) { log.info("부분 취소 처리 시작. paymentId={}", order.getPaymentId()); - + // 주문 상태를 부분 취소로 업데이트 order.updateStatus(PaymentStatus.PARTIAL_CANCELLED); - + // 부분 취소 시에는 구독은 유지하되 크레딧 조정 등의 로직이 필요할 수 있음 log.info("부분 취소 처리 완료. paymentId={}", order.getPaymentId()); } -} +} diff --git a/payment-service/src/main/java/com/synapse/payment_service/service/converter/impl/PayPendingPaymentConverter.java b/payment-service/payment-service/src/main/java/com/synapse/payment_service/service/convert/impl/PayPendingPaymentConverter.java similarity index 81% rename from payment-service/src/main/java/com/synapse/payment_service/service/converter/impl/PayPendingPaymentConverter.java rename to payment-service/payment-service/src/main/java/com/synapse/payment_service/service/convert/impl/PayPendingPaymentConverter.java index 4e97c0b..2764eb4 100644 --- a/payment-service/src/main/java/com/synapse/payment_service/service/converter/impl/PayPendingPaymentConverter.java +++ b/payment-service/payment-service/src/main/java/com/synapse/payment_service/service/convert/impl/PayPendingPaymentConverter.java @@ -1,10 +1,10 @@ -package com.synapse.payment_service.service.converter.impl; +package com.synapse.payment_service.service.convert.impl; import org.springframework.transaction.annotation.Transactional; -import com.synapse.payment_service.domain.Order; +import com.synapse.payment_service.domain.entity.Order; import com.synapse.payment_service.domain.enums.PaymentStatus; -import com.synapse.payment_service.service.converter.PaymentStatusConverter; +import com.synapse.payment_service.service.convert.PaymentStatusConverter; import io.portone.sdk.server.payment.PayPendingPayment; import io.portone.sdk.server.payment.Payment; @@ -22,10 +22,10 @@ public boolean canHandle(Class paymentStatus) { @Transactional public void processPayment(Order order, Payment payment) { log.info("결제 완료 대기 처리 시작. paymentId={}", order.getPaymentId()); - + // 주문 상태를 결제 완료 대기로 업데이트 order.updateStatus(PaymentStatus.PAY_PENDING); - + log.info("결제 완료 대기 처리 완료. paymentId={}", order.getPaymentId()); } -} +} diff --git a/payment-service/src/main/java/com/synapse/payment_service/service/converter/impl/ReadyPaymentConverter.java b/payment-service/payment-service/src/main/java/com/synapse/payment_service/service/convert/impl/ReadyPaymentConverter.java similarity index 80% rename from payment-service/src/main/java/com/synapse/payment_service/service/converter/impl/ReadyPaymentConverter.java rename to payment-service/payment-service/src/main/java/com/synapse/payment_service/service/convert/impl/ReadyPaymentConverter.java index 0480742..814fb48 100644 --- a/payment-service/src/main/java/com/synapse/payment_service/service/converter/impl/ReadyPaymentConverter.java +++ b/payment-service/payment-service/src/main/java/com/synapse/payment_service/service/convert/impl/ReadyPaymentConverter.java @@ -1,10 +1,10 @@ -package com.synapse.payment_service.service.converter.impl; +package com.synapse.payment_service.service.convert.impl; import org.springframework.transaction.annotation.Transactional; -import com.synapse.payment_service.domain.Order; +import com.synapse.payment_service.domain.entity.Order; import com.synapse.payment_service.domain.enums.PaymentStatus; -import com.synapse.payment_service.service.converter.PaymentStatusConverter; +import com.synapse.payment_service.service.convert.PaymentStatusConverter; import io.portone.sdk.server.payment.Payment; import io.portone.sdk.server.payment.ReadyPayment; @@ -22,10 +22,10 @@ public boolean canHandle(Class paymentStatus) { @Transactional public void processPayment(Order order, Payment payment) { log.info("결제 준비 상태 처리 시작. paymentId={}", order.getPaymentId()); - + // 주문 상태를 준비로 업데이트 order.updateStatus(PaymentStatus.READY); - + log.info("결제 준비 상태 처리 완료. paymentId={}", order.getPaymentId()); } -} +} diff --git a/payment-service/src/main/java/com/synapse/payment_service/service/converter/impl/VirtualAccountIssuedPaymentConverter.java b/payment-service/payment-service/src/main/java/com/synapse/payment_service/service/convert/impl/VirtualAccountIssuedPaymentConverter.java similarity index 82% rename from payment-service/src/main/java/com/synapse/payment_service/service/converter/impl/VirtualAccountIssuedPaymentConverter.java rename to payment-service/payment-service/src/main/java/com/synapse/payment_service/service/convert/impl/VirtualAccountIssuedPaymentConverter.java index ea30948..1942a0f 100644 --- a/payment-service/src/main/java/com/synapse/payment_service/service/converter/impl/VirtualAccountIssuedPaymentConverter.java +++ b/payment-service/payment-service/src/main/java/com/synapse/payment_service/service/convert/impl/VirtualAccountIssuedPaymentConverter.java @@ -1,10 +1,10 @@ -package com.synapse.payment_service.service.converter.impl; +package com.synapse.payment_service.service.convert.impl; import org.springframework.transaction.annotation.Transactional; -import com.synapse.payment_service.domain.Order; +import com.synapse.payment_service.domain.entity.Order; import com.synapse.payment_service.domain.enums.PaymentStatus; -import com.synapse.payment_service.service.converter.PaymentStatusConverter; +import com.synapse.payment_service.service.convert.PaymentStatusConverter; import io.portone.sdk.server.payment.Payment; import io.portone.sdk.server.payment.VirtualAccountIssuedPayment; @@ -22,10 +22,10 @@ public boolean canHandle(Class paymentStatus) { @Transactional public void processPayment(Order order, Payment payment) { log.info("가상계좌 발급 완료 처리 시작. paymentId={}", order.getPaymentId()); - + // 주문 상태를 가상계좌 발급 완료로 업데이트 order.updateStatus(PaymentStatus.VIRTUAL_ACCOUNT_ISSUED); - + log.info("가상계좌 발급 완료 처리 완료. paymentId={}", order.getPaymentId()); } -} +} diff --git a/payment-service/src/main/java/com/synapse/payment_service/service/scheduler/SubscriptionScheduler.java b/payment-service/payment-service/src/main/java/com/synapse/payment_service/service/scheduler/SubscriptionScheduler.java similarity index 85% rename from payment-service/src/main/java/com/synapse/payment_service/service/scheduler/SubscriptionScheduler.java rename to payment-service/payment-service/src/main/java/com/synapse/payment_service/service/scheduler/SubscriptionScheduler.java index 39105e8..bf5fb36 100644 --- a/payment-service/src/main/java/com/synapse/payment_service/service/scheduler/SubscriptionScheduler.java +++ b/payment-service/payment-service/src/main/java/com/synapse/payment_service/service/scheduler/SubscriptionScheduler.java @@ -6,17 +6,17 @@ import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; -import com.synapse.payment_service.domain.Subscription; +import com.synapse.payment_service.domain.entity.Subscription; import com.synapse.payment_service.domain.enums.SubscriptionStatus; -import com.synapse.payment_service.repository.SubscriptionRepository; +import com.synapse.payment_service.domain.repository.SubscriptionRepository; import com.synapse.payment_service.service.SubscriptionBillingService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +@Slf4j @Component @RequiredArgsConstructor -@Slf4j public class SubscriptionScheduler { private final SubscriptionBillingService subscriptionBillingService; private final SubscriptionRepository subscriptionRepository; @@ -31,32 +31,31 @@ public void runDailyBilling() { @Scheduled(cron = "0 0 0 * * ?") public void expireSubscriptions() { log.info("구독 만료 처리 스케줄러를 시작합니다."); - + ZonedDateTime currentTime = ZonedDateTime.now(); - + // CANCELED 또는 PAYMENT_FAILED 상태이며 만료일이 지난 구독들을 조회 List expiredSubscriptions = subscriptionRepository.findByStatusInAndExpiresAtBefore( - List.of(SubscriptionStatus.CANCELED, SubscriptionStatus.PAYMENT_FAILED), - currentTime - ); - + List.of(SubscriptionStatus.CANCELED, SubscriptionStatus.PAYMENT_FAILED), + currentTime); + if (expiredSubscriptions.isEmpty()) { log.info("만료 처리할 구독이 없습니다."); return; } - + log.info("만료 처리 대상 구독 수: {}", expiredSubscriptions.size()); - + // 각 구독의 상태를 EXPIRED로 변경하고 autoRenew를 false로 설정 for (Subscription subscription : expiredSubscriptions) { subscription.expireSubscription(); - log.debug("구독 ID {} 상태를 EXPIRED로 변경하고 자동 갱신을 비활성화했습니다. (회원 ID: {})", - subscription.getId(), subscription.getMemberId()); + log.debug("구독 ID {} 상태를 EXPIRED로 변경하고 자동 갱신을 비활성화했습니다. (회원 ID: {})", + subscription.getId(), subscription.getMemberId()); } - + // 변경사항을 데이터베이스에 저장 subscriptionRepository.saveAll(expiredSubscriptions); - + log.info("구독 만료 처리 스케줄러를 종료합니다. 처리된 구독 수: {}", expiredSubscriptions.size()); } } diff --git a/payment-service/src/main/resources/application-local.yml b/payment-service/payment-service/src/main/resources/application-local.yml similarity index 100% rename from payment-service/src/main/resources/application-local.yml rename to payment-service/payment-service/src/main/resources/application-local.yml diff --git a/payment-service/src/main/resources/application.yml b/payment-service/payment-service/src/main/resources/application.yml similarity index 100% rename from payment-service/src/main/resources/application.yml rename to payment-service/payment-service/src/main/resources/application.yml diff --git a/payment-service/payment-service/src/test/java/com/synapse/payment_service/TestConfig.java b/payment-service/payment-service/src/test/java/com/synapse/payment_service/TestConfig.java new file mode 100644 index 0000000..e7fd863 --- /dev/null +++ b/payment-service/payment-service/src/test/java/com/synapse/payment_service/TestConfig.java @@ -0,0 +1,14 @@ +package com.synapse.payment_service; + +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +@Transactional +@ActiveProfiles("test") +@SpringBootTest(classes = PaymentServiceConfiguration.class) +@AutoConfigureMockMvc +public class TestConfig { + +} diff --git a/payment-service/src/test/java/com/synapse/payment_service/controller/test/InternalApiController.java b/payment-service/payment-service/src/test/java/com/synapse/payment_service/controller/InternalApiController.java similarity index 93% rename from payment-service/src/test/java/com/synapse/payment_service/controller/test/InternalApiController.java rename to payment-service/payment-service/src/test/java/com/synapse/payment_service/controller/InternalApiController.java index 8d1bbd4..0441328 100644 --- a/payment-service/src/test/java/com/synapse/payment_service/controller/test/InternalApiController.java +++ b/payment-service/payment-service/src/test/java/com/synapse/payment_service/controller/InternalApiController.java @@ -1,4 +1,4 @@ -package com.synapse.payment_service.controller.test; +package com.synapse.payment_service.controller; import org.springframework.http.ResponseEntity; import org.springframework.security.core.Authentication; diff --git a/payment-service/src/test/java/com/synapse/payment_service/domain/SubscriptionTest.java b/payment-service/payment-service/src/test/java/com/synapse/payment_service/domain/SubscriptionTest.java similarity index 98% rename from payment-service/src/test/java/com/synapse/payment_service/domain/SubscriptionTest.java rename to payment-service/payment-service/src/test/java/com/synapse/payment_service/domain/SubscriptionTest.java index ac62dca..2989da9 100644 --- a/payment-service/src/test/java/com/synapse/payment_service/domain/SubscriptionTest.java +++ b/payment-service/payment-service/src/test/java/com/synapse/payment_service/domain/SubscriptionTest.java @@ -9,6 +9,7 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import com.synapse.payment_service.domain.entity.Subscription; import com.synapse.payment_service.domain.enums.SubscriptionStatus; import com.synapse.payment_service.domain.enums.SubscriptionTier; @@ -107,4 +108,4 @@ void handlePaymentFailure_fromCanceledStatus_shouldChangeToPaymentFailed() { assertThat(subscription.getStatus()).isEqualTo(SubscriptionStatus.PAYMENT_FAILED); assertThat(subscription.getRetryCount()).isEqualTo(1); } -} \ No newline at end of file +} diff --git a/payment-service/src/test/java/com/synapse/payment_service/integrationtest/PaymentIntegrationTest.java b/payment-service/payment-service/src/test/java/com/synapse/payment_service/integrationtest/PaymentIntegrationTest.java similarity index 82% rename from payment-service/src/test/java/com/synapse/payment_service/integrationtest/PaymentIntegrationTest.java rename to payment-service/payment-service/src/test/java/com/synapse/payment_service/integrationtest/PaymentIntegrationTest.java index 39aff11..ada6e27 100644 --- a/payment-service/src/test/java/com/synapse/payment_service/integrationtest/PaymentIntegrationTest.java +++ b/payment-service/payment-service/src/test/java/com/synapse/payment_service/integrationtest/PaymentIntegrationTest.java @@ -1,15 +1,16 @@ package com.synapse.payment_service.integrationtest; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import java.math.BigDecimal; import java.time.Instant; +import java.time.ZonedDateTime; import java.util.UUID; import java.util.concurrent.CompletableFuture; @@ -17,38 +18,31 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; -import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; -import org.springframework.transaction.annotation.Transactional; import com.fasterxml.jackson.databind.ObjectMapper; -import com.synapse.payment_service.domain.Order; -import com.synapse.payment_service.domain.Subscription; +import com.synapse.payment_service.TestConfig; +import com.synapse.payment_service.domain.entity.Order; +import com.synapse.payment_service.domain.entity.Subscription; import com.synapse.payment_service.domain.enums.PaymentStatus; import com.synapse.payment_service.domain.enums.SubscriptionStatus; import com.synapse.payment_service.domain.enums.SubscriptionTier; -import com.synapse.payment_service.dto.request.PaymentRequestDto; -import com.synapse.payment_service.dto.response.PaymentPreparationResponse; -import com.synapse.payment_service.repository.OrderRepository; -import com.synapse.payment_service.repository.SubscriptionRepository; +import com.synapse.payment_service.domain.repository.OrderRepository; +import com.synapse.payment_service.domain.repository.SubscriptionRepository; +import com.synapse.payment_service_api.dto.request.PaymentRequestDto; +import com.synapse.payment_service_api.dto.response.PaymentPreparationResponse; import io.portone.sdk.server.PortOneClient; +import io.portone.sdk.server.payment.PaidPayment; import io.portone.sdk.server.payment.Payment; import io.portone.sdk.server.payment.PaymentAmount; import io.portone.sdk.server.payment.PaymentClient; -import io.portone.sdk.server.payment.PaidPayment; import io.portone.sdk.server.webhook.WebhookVerifier; -@SpringBootTest -@AutoConfigureMockMvc -@Transactional -@ActiveProfiles("test") -public class PaymentIntegrationTest { +public class PaymentIntegrationTest extends TestConfig { @Autowired private MockMvc mockMvc; @@ -72,11 +66,13 @@ public class PaymentIntegrationTest { void setUp() { this.memberId = UUID.randomUUID(); Subscription subscription = Subscription.builder() - .memberId(memberId) - .tier(SubscriptionTier.FREE) - .remainingChatCredits(10) - .status(SubscriptionStatus.CANCELED) - .build(); + .id(UUID.randomUUID()) + .memberId(memberId) + .tier(SubscriptionTier.FREE) + .remainingChatCredits(10) + .expiresAt(ZonedDateTime.now().plusDays(30)) + .status(SubscriptionStatus.CANCELED) + .build(); subscriptionRepository.save(subscription); } @@ -109,7 +105,7 @@ void handleWebhook_success() throws Exception { // given // 1. 먼저 /request API를 호출하여 PENDING 상태의 주문을 생성 PaymentRequestDto request = new PaymentRequestDto("PRO"); - + ResultActions prepareResult = mockMvc.perform(post("/api/payments/request") .header("X-Authenticated-Member-Id", memberId.toString()) .contentType(MediaType.APPLICATION_JSON) @@ -121,35 +117,36 @@ void handleWebhook_success() throws Exception { .andExpect(jsonPath("$.paymentId").exists()) .andReturn() .getResponse().getContentAsString(); - - PaymentPreparationResponse prepResponse = objectMapper.readValue(responseJson, PaymentPreparationResponse.class); + + PaymentPreparationResponse prepResponse = objectMapper.readValue(responseJson, + PaymentPreparationResponse.class); String paymentId = prepResponse.paymentId(); BigDecimal amount = prepResponse.amount(); String iamPortTransactionId = "imp_test_12345"; - + // 3. Mock PortOneClient가 위변조 없는 정상 데이터를 반환하도록 설정 PaidPayment mockPaidPayment = mock(PaidPayment.class); PaymentAmount mockAmount = mock(PaymentAmount.class); - + when(mockAmount.getTotal()).thenReturn(amount.longValue()); when(mockPaidPayment.getAmount()).thenReturn(mockAmount); - + CompletableFuture mockFuture = CompletableFuture.completedFuture(mockPaidPayment); given(portOneClient.getPayment()).willReturn(paymentClient); given(paymentClient.getPayment(iamPortTransactionId)).willReturn(mockFuture); - + // 4. WebhookVerifier가 항상 검증에 성공하도록 설정 // 5. 포트원 웹훅 페이로드 생성 (실제 포트원 SDK 사용) String webhookJson = """ - { - "type": "Transaction.Paid", - "data": { - "paymentId": "%s", - "transactionId": "%s" + { + "type": "Transaction.Paid", + "data": { + "paymentId": "%s", + "transactionId": "%s" + } } - } - """.formatted(paymentId, iamPortTransactionId); + """.formatted(paymentId, iamPortTransactionId); // when - 웹훅 API 호출 ResultActions webhookResult = mockMvc.perform(post("/api/webhooks/portone") @@ -158,15 +155,15 @@ void handleWebhook_success() throws Exception { .header(WebhookVerifier.HEADER_TIMESTAMP, String.valueOf(Instant.now().getEpochSecond())) .contentType(MediaType.APPLICATION_JSON) .content(webhookJson)); - + // then webhookResult.andExpect(status().isOk()); - + // DB 검증 - 트랜잭션이 커밋된 후 검증 Order completedOrder = orderRepository.findByPaymentId(paymentId).get(); assertThat(completedOrder.getStatus()).isEqualTo(PaymentStatus.PAID); assertThat(completedOrder.getSubscription().getStatus()).isEqualTo(SubscriptionStatus.ACTIVE); - + Subscription updatedSubscription = subscriptionRepository.findByMemberId(memberId).get(); assertThat(updatedSubscription.getStatus()).isEqualTo(SubscriptionStatus.ACTIVE); assertThat(updatedSubscription.getTier()).isEqualTo(SubscriptionTier.PRO); diff --git a/payment-service/src/test/java/com/synapse/payment_service/integrationtest/ResourceServerIntegrationTest.java b/payment-service/payment-service/src/test/java/com/synapse/payment_service/integrationtest/ResourceServerIntegrationTest.java similarity index 86% rename from payment-service/src/test/java/com/synapse/payment_service/integrationtest/ResourceServerIntegrationTest.java rename to payment-service/payment-service/src/test/java/com/synapse/payment_service/integrationtest/ResourceServerIntegrationTest.java index 347a1d1..135e8c4 100644 --- a/payment-service/src/test/java/com/synapse/payment_service/integrationtest/ResourceServerIntegrationTest.java +++ b/payment-service/payment-service/src/test/java/com/synapse/payment_service/integrationtest/ResourceServerIntegrationTest.java @@ -11,28 +11,21 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Import; -import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; -import org.springframework.transaction.annotation.Transactional; -import com.synapse.payment_service.controller.test.InternalApiController; +import com.synapse.payment_service.TestConfig; +import com.synapse.payment_service.controller.InternalApiController; + import io.portone.sdk.server.PortOneClient; -@SpringBootTest -@AutoConfigureMockMvc -@Transactional -@ActiveProfiles("test") @Import(InternalApiController.class) -public class ResourceServerIntegrationTest { - +public class ResourceServerIntegrationTest extends TestConfig { @Autowired private MockMvc mockMvc; - + @MockitoBean private PortOneClient portOneClient; diff --git a/payment-service/src/test/java/com/synapse/payment_service/repository/SubscriptionRepositoryTest.java b/payment-service/payment-service/src/test/java/com/synapse/payment_service/repository/SubscriptionRepositoryTest.java similarity index 83% rename from payment-service/src/test/java/com/synapse/payment_service/repository/SubscriptionRepositoryTest.java rename to payment-service/payment-service/src/test/java/com/synapse/payment_service/repository/SubscriptionRepositoryTest.java index d589755..37d003c 100644 --- a/payment-service/src/test/java/com/synapse/payment_service/repository/SubscriptionRepositoryTest.java +++ b/payment-service/payment-service/src/test/java/com/synapse/payment_service/repository/SubscriptionRepositoryTest.java @@ -11,17 +11,15 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.test.context.ActiveProfiles; -import com.synapse.payment_service.domain.Subscription; +import com.synapse.payment_service.TestConfig; +import com.synapse.payment_service.domain.entity.Subscription; import com.synapse.payment_service.domain.enums.SubscriptionStatus; import com.synapse.payment_service.domain.enums.SubscriptionTier; +import com.synapse.payment_service.domain.repository.SubscriptionRepository; -@DataJpaTest -@ActiveProfiles("test") @DisplayName("SubscriptionRepository 테스트") -class SubscriptionRepositoryTest { +class SubscriptionRepositoryTest extends TestConfig { @Autowired private SubscriptionRepository subscriptionRepository; @@ -42,13 +40,16 @@ void setUp() { void findActiveSubscriptionsDueForRenewal_shouldReturnOnlyExactDateMatches() { // given // 정확히 targetDate에 만료되는 구독 (조회되어야 함) - Subscription exactDateSubscription = createActiveSubscription(targetDate.atStartOfDay().atZone(ZonedDateTime.now().getZone())); - + Subscription exactDateSubscription = createActiveSubscription( + targetDate.atStartOfDay().atZone(ZonedDateTime.now().getZone())); + // 과거 날짜에 만료된 구독 (조회되지 않아야 함) - Subscription pastDateSubscription = createActiveSubscription(pastDate.atStartOfDay().atZone(ZonedDateTime.now().getZone())); - + Subscription pastDateSubscription = createActiveSubscription( + pastDate.atStartOfDay().atZone(ZonedDateTime.now().getZone())); + // 미래 날짜에 만료되는 구독 (조회되지 않아야 함) - Subscription futureDateSubscription = createActiveSubscription(futureDate.atStartOfDay().atZone(ZonedDateTime.now().getZone())); + Subscription futureDateSubscription = createActiveSubscription( + futureDate.atStartOfDay().atZone(ZonedDateTime.now().getZone())); subscriptionRepository.saveAll(List.of(exactDateSubscription, pastDateSubscription, futureDateSubscription)); @@ -67,9 +68,12 @@ void findActiveSubscriptionsDueForRenewal_shouldReturnOnlyExactDateMatches() { void findActiveSubscriptionsDueForRenewal_shouldNotReturnPastDueSubscriptions() { // given // 과거 여러 날짜에 만료된 구독들 - Subscription pastSubscription1 = createActiveSubscription(pastDate.minusDays(5).atStartOfDay().atZone(ZonedDateTime.now().getZone())); - Subscription pastSubscription2 = createActiveSubscription(pastDate.minusDays(10).atStartOfDay().atZone(ZonedDateTime.now().getZone())); - Subscription pastSubscription3 = createActiveSubscription(pastDate.atStartOfDay().atZone(ZonedDateTime.now().getZone())); + Subscription pastSubscription1 = createActiveSubscription( + pastDate.minusDays(5).atStartOfDay().atZone(ZonedDateTime.now().getZone())); + Subscription pastSubscription2 = createActiveSubscription( + pastDate.minusDays(10).atStartOfDay().atZone(ZonedDateTime.now().getZone())); + Subscription pastSubscription3 = createActiveSubscription( + pastDate.atStartOfDay().atZone(ZonedDateTime.now().getZone())); subscriptionRepository.saveAll(List.of(pastSubscription1, pastSubscription2, pastSubscription3)); @@ -87,9 +91,12 @@ void findActiveSubscriptionsDueForRenewal_shouldNotReturnPastDueSubscriptions() void findActiveSubscriptionsDueForRenewal_shouldNotReturnFutureSubscriptions() { // given // 미래 여러 날짜에 만료되는 구독들 - Subscription futureSubscription1 = createActiveSubscription(futureDate.plusDays(1).atStartOfDay().atZone(ZonedDateTime.now().getZone())); - Subscription futureSubscription2 = createActiveSubscription(futureDate.plusDays(10).atStartOfDay().atZone(ZonedDateTime.now().getZone())); - Subscription futureSubscription3 = createActiveSubscription(futureDate.atStartOfDay().atZone(ZonedDateTime.now().getZone())); + Subscription futureSubscription1 = createActiveSubscription( + futureDate.plusDays(1).atStartOfDay().atZone(ZonedDateTime.now().getZone())); + Subscription futureSubscription2 = createActiveSubscription( + futureDate.plusDays(10).atStartOfDay().atZone(ZonedDateTime.now().getZone())); + Subscription futureSubscription3 = createActiveSubscription( + futureDate.atStartOfDay().atZone(ZonedDateTime.now().getZone())); subscriptionRepository.saveAll(List.of(futureSubscription1, futureSubscription2, futureSubscription3)); @@ -107,6 +114,7 @@ void findActiveSubscriptionsDueForRenewal_shouldNotReturnFutureSubscriptions() { void findActiveSubscriptionsDueForRenewal_shouldNotReturnInactiveSubscriptions() { // given Subscription canceledSubscription = Subscription.builder() + .id(UUID.randomUUID()) .memberId(UUID.randomUUID()) .tier(SubscriptionTier.PRO) .remainingChatCredits(SubscriptionTier.PRO.getMaxRequestCount()) @@ -116,6 +124,7 @@ void findActiveSubscriptionsDueForRenewal_shouldNotReturnInactiveSubscriptions() canceledSubscription.updateBillingKey("test-billing-key"); Subscription paymentFailedSubscription = Subscription.builder() + .id(UUID.randomUUID()) .memberId(UUID.randomUUID()) .tier(SubscriptionTier.PRO) .remainingChatCredits(SubscriptionTier.PRO.getMaxRequestCount()) @@ -140,9 +149,10 @@ void findActiveSubscriptionsDueForRenewal_shouldNotReturnInactiveSubscriptions() void findActiveSubscriptionsDueForRenewal_shouldNotReturnNonAutoRenewSubscriptions() { // given // autoRenew가 false인 구독 생성 (CANCELED 상태) - Subscription subscription = createActiveSubscription(targetDate.atStartOfDay().atZone(ZonedDateTime.now().getZone())); + Subscription subscription = createActiveSubscription( + targetDate.atStartOfDay().atZone(ZonedDateTime.now().getZone())); subscription.deactivate(); // autoRenew를 false로 설정하고 상태를 CANCELED로 변경 - + subscriptionRepository.save(subscription); // when @@ -159,6 +169,7 @@ void findActiveSubscriptionsDueForRenewal_shouldNotReturnNonAutoRenewSubscriptio void findActiveSubscriptionsDueForRenewal_shouldNotReturnSubscriptionsWithoutBillingKey() { // given Subscription subscriptionWithoutBillingKey = Subscription.builder() + .id(UUID.randomUUID()) .memberId(UUID.randomUUID()) .tier(SubscriptionTier.PRO) .remainingChatCredits(SubscriptionTier.PRO.getMaxRequestCount()) @@ -182,9 +193,12 @@ void findActiveSubscriptionsDueForRenewal_shouldNotReturnSubscriptionsWithoutBil @DisplayName("모든 조건을 만족하는 여러 구독이 모두 조회된다") void findActiveSubscriptionsDueForRenewal_shouldReturnAllValidSubscriptions() { // given - Subscription subscription1 = createActiveSubscription(targetDate.atStartOfDay().atZone(ZonedDateTime.now().getZone())); - Subscription subscription2 = createActiveSubscription(targetDate.atTime(12, 30).atZone(ZonedDateTime.now().getZone())); - Subscription subscription3 = createActiveSubscription(targetDate.atTime(23, 59).atZone(ZonedDateTime.now().getZone())); + Subscription subscription1 = createActiveSubscription( + targetDate.atStartOfDay().atZone(ZonedDateTime.now().getZone())); + Subscription subscription2 = createActiveSubscription( + targetDate.atTime(12, 30).atZone(ZonedDateTime.now().getZone())); + Subscription subscription3 = createActiveSubscription( + targetDate.atTime(23, 59).atZone(ZonedDateTime.now().getZone())); subscriptionRepository.saveAll(List.of(subscription1, subscription2, subscription3)); @@ -209,20 +223,21 @@ void findByStatusInAndExpiresAtBefore_shouldReturnCanceledExpiredSubscriptions() // CANCELED 상태이고 만료일이 지난 구독 (조회되어야 함) Subscription canceledExpiredSubscription = createSubscriptionWithStatus(SubscriptionStatus.CANCELED, pastTime); - + // CANCELED 상태이지만 아직 만료되지 않은 구독 (조회되지 않아야 함) - Subscription canceledNotExpiredSubscription = createSubscriptionWithStatus(SubscriptionStatus.CANCELED, futureTime); - + Subscription canceledNotExpiredSubscription = createSubscriptionWithStatus(SubscriptionStatus.CANCELED, + futureTime); + // ACTIVE 상태이고 만료일이 지난 구독 (조회되지 않아야 함) Subscription activeExpiredSubscription = createSubscriptionWithStatus(SubscriptionStatus.ACTIVE, pastTime); - subscriptionRepository.saveAll(List.of(canceledExpiredSubscription, canceledNotExpiredSubscription, activeExpiredSubscription)); + subscriptionRepository.saveAll( + List.of(canceledExpiredSubscription, canceledNotExpiredSubscription, activeExpiredSubscription)); // when List result = subscriptionRepository.findByStatusInAndExpiresAtBefore( - List.of(SubscriptionStatus.CANCELED, SubscriptionStatus.PAYMENT_FAILED), - currentTime - ); + List.of(SubscriptionStatus.CANCELED, SubscriptionStatus.PAYMENT_FAILED), + currentTime); // then assertThat(result).hasSize(1); @@ -239,18 +254,19 @@ void findByStatusInAndExpiresAtBefore_shouldReturnPaymentFailedExpiredSubscripti ZonedDateTime futureTime = currentTime.plusDays(1); // PAYMENT_FAILED 상태이고 만료일이 지난 구독 (조회되어야 함) - Subscription paymentFailedExpiredSubscription = createSubscriptionWithStatus(SubscriptionStatus.PAYMENT_FAILED, pastTime); - + Subscription paymentFailedExpiredSubscription = createSubscriptionWithStatus(SubscriptionStatus.PAYMENT_FAILED, + pastTime); + // PAYMENT_FAILED 상태이지만 아직 만료되지 않은 구독 (조회되지 않아야 함) - Subscription paymentFailedNotExpiredSubscription = createSubscriptionWithStatus(SubscriptionStatus.PAYMENT_FAILED, futureTime); + Subscription paymentFailedNotExpiredSubscription = createSubscriptionWithStatus( + SubscriptionStatus.PAYMENT_FAILED, futureTime); subscriptionRepository.saveAll(List.of(paymentFailedExpiredSubscription, paymentFailedNotExpiredSubscription)); // when List result = subscriptionRepository.findByStatusInAndExpiresAtBefore( - List.of(SubscriptionStatus.CANCELED, SubscriptionStatus.PAYMENT_FAILED), - currentTime - ); + List.of(SubscriptionStatus.CANCELED, SubscriptionStatus.PAYMENT_FAILED), + currentTime); // then assertThat(result).hasSize(1); @@ -273,23 +289,22 @@ void findByStatusInAndExpiresAtBefore_shouldReturnBothCanceledAndPaymentFailedEx Subscription paymentFailedExpired1 = createSubscriptionWithStatus(SubscriptionStatus.PAYMENT_FAILED, pastTime1); Subscription paymentFailedExpired2 = createSubscriptionWithStatus(SubscriptionStatus.PAYMENT_FAILED, pastTime3); - subscriptionRepository.saveAll(List.of(canceledExpired1, canceledExpired2, paymentFailedExpired1, paymentFailedExpired2)); + subscriptionRepository + .saveAll(List.of(canceledExpired1, canceledExpired2, paymentFailedExpired1, paymentFailedExpired2)); // when List result = subscriptionRepository.findByStatusInAndExpiresAtBefore( - List.of(SubscriptionStatus.CANCELED, SubscriptionStatus.PAYMENT_FAILED), - currentTime - ); + List.of(SubscriptionStatus.CANCELED, SubscriptionStatus.PAYMENT_FAILED), + currentTime); // then assertThat(result).hasSize(4); assertThat(result).extracting(Subscription::getId) .containsExactlyInAnyOrder( - canceledExpired1.getId(), - canceledExpired2.getId(), - paymentFailedExpired1.getId(), - paymentFailedExpired2.getId() - ); + canceledExpired1.getId(), + canceledExpired2.getId(), + paymentFailedExpired1.getId(), + paymentFailedExpired2.getId()); } @Test @@ -307,9 +322,8 @@ void findByStatusInAndExpiresAtBefore_shouldNotReturnOtherStatusSubscriptions() // when List result = subscriptionRepository.findByStatusInAndExpiresAtBefore( - List.of(SubscriptionStatus.CANCELED, SubscriptionStatus.PAYMENT_FAILED), - currentTime - ); + List.of(SubscriptionStatus.CANCELED, SubscriptionStatus.PAYMENT_FAILED), + currentTime); // then assertThat(result).isEmpty(); @@ -320,11 +334,13 @@ void findByStatusInAndExpiresAtBefore_shouldNotReturnOtherStatusSubscriptions() void findByStatusInAndExpiresAtBefore_shouldNotReturnCurrentOrFutureSubscriptions() { // given ZonedDateTime currentTime = ZonedDateTime.now(); + ZonedDateTime currentTimeExact = currentTime.plusSeconds(1); // 시간 정밀도 문제 해결을 위해 1초 후 시간 사용 ZonedDateTime futureTime = currentTime.plusDays(1); // 현재 시간과 같은 만료일을 가진 구독 - Subscription canceledCurrentSubscription = createSubscriptionWithStatus(SubscriptionStatus.CANCELED, currentTime); - + Subscription canceledCurrentSubscription = createSubscriptionWithStatus(SubscriptionStatus.CANCELED, + currentTimeExact); + // 미래 만료일을 가진 구독 Subscription canceledFutureSubscription = createSubscriptionWithStatus(SubscriptionStatus.CANCELED, futureTime); @@ -332,9 +348,8 @@ void findByStatusInAndExpiresAtBefore_shouldNotReturnCurrentOrFutureSubscription // when List result = subscriptionRepository.findByStatusInAndExpiresAtBefore( - List.of(SubscriptionStatus.CANCELED, SubscriptionStatus.PAYMENT_FAILED), - currentTime - ); + List.of(SubscriptionStatus.CANCELED, SubscriptionStatus.PAYMENT_FAILED), + currentTime); // then assertThat(result).isEmpty(); @@ -352,9 +367,8 @@ void findByStatusInAndExpiresAtBefore_shouldReturnEmptyForEmptyStatusList() { // when List result = subscriptionRepository.findByStatusInAndExpiresAtBefore( - List.of(), - currentTime - ); + List.of(), + currentTime); // then assertThat(result).isEmpty(); @@ -362,6 +376,7 @@ void findByStatusInAndExpiresAtBefore_shouldReturnEmptyForEmptyStatusList() { private Subscription createActiveSubscription(ZonedDateTime expiresAt) { Subscription subscription = Subscription.builder() + .id(UUID.randomUUID()) .memberId(UUID.randomUUID()) .tier(SubscriptionTier.PRO) .remainingChatCredits(SubscriptionTier.PRO.getMaxRequestCount()) @@ -374,6 +389,7 @@ private Subscription createActiveSubscription(ZonedDateTime expiresAt) { private Subscription createSubscriptionWithStatus(SubscriptionStatus status, ZonedDateTime expiresAt) { Subscription subscription = Subscription.builder() + .id(UUID.randomUUID()) .memberId(UUID.randomUUID()) .tier(SubscriptionTier.PRO) .remainingChatCredits(SubscriptionTier.PRO.getMaxRequestCount()) @@ -383,4 +399,4 @@ private Subscription createSubscriptionWithStatus(SubscriptionStatus status, Zon subscription.updateBillingKey("test-billing-key-" + UUID.randomUUID()); return subscription; } -} \ No newline at end of file +} diff --git a/payment-service/src/test/java/com/synapse/payment_service/service/PaymentServiceTest.java b/payment-service/payment-service/src/test/java/com/synapse/payment_service/service/PaymentServiceTest.java similarity index 81% rename from payment-service/src/test/java/com/synapse/payment_service/service/PaymentServiceTest.java rename to payment-service/payment-service/src/test/java/com/synapse/payment_service/service/PaymentServiceTest.java index 8dabbd1..974cb57 100644 --- a/payment-service/src/test/java/com/synapse/payment_service/service/PaymentServiceTest.java +++ b/payment-service/payment-service/src/test/java/com/synapse/payment_service/service/PaymentServiceTest.java @@ -1,14 +1,18 @@ package com.synapse.payment_service.service; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.when; +import static org.mockito.ArgumentMatchers.argThat; + import java.math.BigDecimal; +import java.time.ZonedDateTime; import java.util.Optional; import java.util.UUID; import java.util.concurrent.CompletableFuture; @@ -16,33 +20,31 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; import com.fasterxml.jackson.databind.ObjectMapper; -import com.synapse.payment_service.domain.Order; -import com.synapse.payment_service.domain.Subscription; +import com.synapse.payment_service.TestConfig; +import com.synapse.payment_service.domain.entity.Order; +import com.synapse.payment_service.domain.entity.Subscription; import com.synapse.payment_service.domain.enums.PaymentStatus; import com.synapse.payment_service.domain.enums.SubscriptionStatus; import com.synapse.payment_service.domain.enums.SubscriptionTier; -import com.synapse.payment_service.dto.request.PaymentRequestDto; -import com.synapse.payment_service.dto.request.PaymentVerificationRequest; -import com.synapse.payment_service.dto.response.PaymentPreparationResponse; -import com.synapse.payment_service.repository.OrderRepository; -import com.synapse.payment_service.repository.SubscriptionRepository; -import com.synapse.payment_service.service.converter.PaymentStatusConverter; -import com.synapse.payment_service.service.converter.DelegatingPaymentStatusConverter; +import com.synapse.payment_service.domain.repository.OrderRepository; +import com.synapse.payment_service.domain.repository.SubscriptionRepository; +import com.synapse.payment_service.service.convert.DelegatingPaymentStatusConverter; +import com.synapse.payment_service.service.convert.PaymentStatusConverter; +import com.synapse.payment_service_api.dto.request.PaymentRequestDto; +import com.synapse.payment_service_api.dto.request.PaymentVerificationRequest; +import com.synapse.payment_service_api.dto.response.PaymentPreparationResponse; import io.portone.sdk.server.PortOneClient; +import io.portone.sdk.server.payment.PaidPayment; import io.portone.sdk.server.payment.Payment; import io.portone.sdk.server.payment.PaymentAmount; import io.portone.sdk.server.payment.PaymentClient; -import io.portone.sdk.server.payment.PaidPayment; -@ExtendWith(MockitoExtension.class) -public class PaymentServiceTest { +public class PaymentServiceTest extends TestConfig { @InjectMocks private PaymentService paymentService; @@ -76,7 +78,7 @@ void preparePayment_success() { // given PaymentRequestDto request = new PaymentRequestDto("PRO"); String orderName = "pro_subscription"; - Subscription mockSubscription = Subscription.builder().memberId(memberId).tier(SubscriptionTier.FREE).build(); + Subscription mockSubscription = Subscription.builder().id(UUID.randomUUID()).memberId(memberId).tier(SubscriptionTier.FREE).remainingChatCredits(10).expiresAt(ZonedDateTime.now().plusDays(30)).status(SubscriptionStatus.ACTIVE).build(); given(subscriptionRepository.findByMemberId(memberId)).willReturn(Optional.of(mockSubscription)); given(orderRepository.save(any(Order.class))).willAnswer(invocation -> invocation.getArgument(0)); @@ -95,8 +97,12 @@ void preparePayment_success() { @DisplayName("결제 검증 성공: 위변조가 없는 결제 건에 대해 구독 상태를 성공적으로 업데이트한다") void verifyAndProcess_success() { Subscription mockSubscription = Subscription.builder() + .id(UUID.randomUUID()) .memberId(memberId) .tier(SubscriptionTier.FREE) + .remainingChatCredits(10) + .expiresAt(ZonedDateTime.now().plusDays(30)) + .status(SubscriptionStatus.ACTIVE) .build(); // given @@ -110,17 +116,17 @@ void verifyAndProcess_success() { // PortOne API의 응답을 모의 처리 Payment.Recognized mockApiResponse = mock(Payment.Recognized.class); PaymentAmount mockAmount = mock(PaymentAmount.class); - + when(mockAmount.getTotal()).thenReturn(100000L); when(mockApiResponse.getAmount()).thenReturn(mockAmount); given(orderRepository.findByPaymentId(paymentId)).willReturn(Optional.of(pendingOrder)); given(portOneClient.getPayment()).willReturn(paymentClient); - + // CompletableFuture Mock 설정 CompletableFuture mockFuture = CompletableFuture.completedFuture(mockApiResponse); given(paymentClient.getPayment(iamPortTransactionId)).willReturn(mockFuture); - + doAnswer(invocation -> { Order order = invocation.getArgument(0); order.updateStatus(PaymentStatus.PAID); @@ -143,10 +149,14 @@ void verifyAndProcess_success() { void verifyAndProcess_withRealDelegatingConverter() { // given Subscription mockSubscription = Subscription.builder() + .id(UUID.randomUUID()) .memberId(memberId) .tier(SubscriptionTier.FREE) + .remainingChatCredits(10) + .expiresAt(ZonedDateTime.now().plusDays(30)) + .status(SubscriptionStatus.ACTIVE) .build(); - + Order pendingOrder = Order.builder() .paymentId(paymentId) .amount(new BigDecimal("100000")) @@ -157,26 +167,27 @@ void verifyAndProcess_withRealDelegatingConverter() { // 실제 DelegatingPaymentStatusConverter 사용 (내부에 PaidPaymentConverter 포함) PaymentStatusConverter realDelegatingConverter = new DelegatingPaymentStatusConverter(); PaymentService paymentServiceWithRealConverter = new PaymentService( - subscriptionRepository, orderRepository, portOneClient, + subscriptionRepository, orderRepository, portOneClient, realDelegatingConverter, objectMapper); // PaidPayment 타입으로 모킹 (실제 결제 완료 상태) PaidPayment mockPaidPayment = mock(PaidPayment.class); PaymentAmount mockAmount = mock(PaymentAmount.class); - + when(mockAmount.getTotal()).thenReturn(100000L); when(mockPaidPayment.getAmount()).thenReturn(mockAmount); given(orderRepository.findByPaymentId(paymentId)).willReturn(Optional.of(pendingOrder)); given(portOneClient.getPayment()).willReturn(paymentClient); - - // CompletableFuture Mock 설정 여기서는 .join()으로 테스트 불가능 -> .join 메서드는 런타임시에 동기화를 진행하는데 테스트중에 null이 들어가버린다. + + // CompletableFuture Mock 설정 여기서는 .join()으로 테스트 불가능 -> .join 메서드는 런타임시에 동기화를 + // 진행하는데 테스트중에 null이 들어가버린다. CompletableFuture mockFuture = CompletableFuture.completedFuture(mockPaidPayment); given(paymentClient.getPayment(iamPortTransactionId)).willReturn(mockFuture); // when paymentServiceWithRealConverter.verifyAndProcess( - new PaymentVerificationRequest(paymentId, iamPortTransactionId), memberId); + new PaymentVerificationRequest(paymentId, iamPortTransactionId), memberId); // then - 실제 PaidPaymentConverter 로직에 의한 상태 변경 검증 assertThat(pendingOrder.getStatus()).isEqualTo(PaymentStatus.PAID); diff --git a/payment-service/payment-service/src/test/java/com/synapse/payment_service/service/SubscriptionBillingServiceTest.java b/payment-service/payment-service/src/test/java/com/synapse/payment_service/service/SubscriptionBillingServiceTest.java new file mode 100644 index 0000000..b81b6dd --- /dev/null +++ b/payment-service/payment-service/src/test/java/com/synapse/payment_service/service/SubscriptionBillingServiceTest.java @@ -0,0 +1,141 @@ +package com.synapse.payment_service.service; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.never; + + +import java.time.ZonedDateTime; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import com.synapse.payment_service.TestConfig; +import com.synapse.payment_service.configuration.PortOneClientProperties; +import com.synapse.payment_service.domain.entity.Order; +import com.synapse.payment_service.domain.entity.Subscription; +import com.synapse.payment_service.domain.enums.SubscriptionTier; +import com.synapse.payment_service.domain.repository.OrderRepository; +import com.synapse.payment_service.domain.repository.SubscriptionRepository; + +import io.portone.sdk.server.PortOneClient; +import io.portone.sdk.server.common.PaymentAmountInput; +import io.portone.sdk.server.payment.BillingKeyPaymentSummary; +import io.portone.sdk.server.payment.PayWithBillingKeyResponse; +import io.portone.sdk.server.payment.PaymentClient; + +public class SubscriptionBillingServiceTest extends TestConfig { + @InjectMocks + private SubscriptionBillingService subscriptionBillingService; + + @Mock + private SubscriptionRepository subscriptionRepository; + @Mock + private OrderRepository orderRepository; + @Mock + private PortOneClient portoneClient; + @Mock + private PaymentClient paymentClient; + @Mock + private PortOneClientProperties portOneClientProperties; + + @Test + @DisplayName("정기 결제 성공: 만료일이 된 구독에 대해 빌링키 결제를 성공하고 상태를 갱신한다") + void processDailySubscriptions_success() { + // given + Subscription mockSubscription = mock(Subscription.class); + given(mockSubscription.getBillingKey()).willReturn("test-billing-key"); + given(mockSubscription.getTier()).willReturn(SubscriptionTier.PRO); + given(mockSubscription.getId()).willReturn(UUID.randomUUID()); + + List targets = List.of(mockSubscription); + given(subscriptionRepository.findActiveSubscriptionsDueForRenewal(any(ZonedDateTime.class), + any(ZonedDateTime.class))).willReturn(targets); + + // PortOne SDK의 응답을 모의 처리 + PayWithBillingKeyResponse mockResponse = mock(PayWithBillingKeyResponse.class); + BillingKeyPaymentSummary mockPaymentResult = mock(BillingKeyPaymentSummary.class); + + given(portoneClient.getPayment()).willReturn(paymentClient); + given(portOneClientProperties.channelKey()).willReturn("test-channel-key"); + + // CompletableFuture가 성공적으로 완료되도록 설정 + CompletableFuture successFuture = CompletableFuture.completedFuture(mockResponse); + + // payWithBillingKey 호출이 성공하는 CompletableFuture를 반환하도록 설정 + given(paymentClient.payWithBillingKey( + anyString(), anyString(), anyString(), anyString(), + any(), any(), any(PaymentAmountInput.class), any(), + any(), any(), any(), any(), + any(), any(), any(), any(), + any(), any(), any(), any(), any())).willReturn(successFuture); + + // mock 응답 설정 - 모든 필요한 값들을 완벽하게 설정 + given(mockResponse.getPayment()).willReturn(mockPaymentResult); + given(mockPaymentResult.getPgTxId()).willReturn("test-pg-tx-id"); + + // when + subscriptionBillingService.processDailySubscriptions(); + + // then + // 성공 핸들러가 호출되어 renewSubscription이 호출되었는지 확인 + verify(mockSubscription, times(1)).renewSubscription(SubscriptionTier.PRO); + + // Order가 2번 저장되었는지 확인 (PENDING -> PAID) + verify(orderRepository, times(2)).save(any(Order.class)); + + // 실패 핸들러는 호출되지 않았음을 확인 + verify(mockSubscription, never()).handlePaymentFailure(); + verify(subscriptionRepository, never()).save(mockSubscription); + } + + @Test + @DisplayName("정기 결제 실패: PortOne API 호출이 실패하면, 실패 주문을 저장하고 구독 상태를 PAYMENT_FAILED로 변경한다") + void processDailySubscriptions_fail() { + // given + Subscription mockSubscription = mock(Subscription.class); + given(mockSubscription.getBillingKey()).willReturn("test-billing-key"); + given(mockSubscription.getTier()).willReturn(SubscriptionTier.PRO); + given(mockSubscription.getId()).willReturn(UUID.randomUUID()); + + List targets = List.of(mockSubscription); + given(subscriptionRepository.findActiveSubscriptionsDueForRenewal(any(ZonedDateTime.class), + any(ZonedDateTime.class))).willReturn(targets); + + // PortOne SDK가 예외를 던지는 상황을 모의 처리 + given(portoneClient.getPayment()).willReturn(paymentClient); + given(portOneClientProperties.channelKey()).willReturn("test-channel-key"); + given(paymentClient.payWithBillingKey( + anyString(), anyString(), anyString(), anyString(), + any(), anyString(), any(PaymentAmountInput.class), any(), + any(), any(), any(), any(), + any(), any(), any(), any(), + any(), any(), anyString(), any(), any())) + .willReturn(CompletableFuture.failedFuture(new RuntimeException("PG사 연동 실패"))); + + // when + subscriptionBillingService.processDailySubscriptions(); + + // then + // Order가 2번 저장되었는지 확인 (PENDING -> FAILED) + verify(orderRepository, times(2)).save(any(Order.class)); + + // handlePaymentFailure() 메서드가 호출되었는지 검증 + verify(mockSubscription, times(1)).handlePaymentFailure(); + + // 변경된 subscription이 저장되었는지 검증 + verify(subscriptionRepository, times(1)).save(mockSubscription); + + // renewSubscription은 호출되지 않았음을 검증 (실패했으므로) + verify(mockSubscription, never()).renewSubscription(any(SubscriptionTier.class)); + } +} diff --git a/payment-service/src/test/java/com/synapse/payment_service/service/scheduler/SubscriptionSchedulerIntegrationTest.java b/payment-service/payment-service/src/test/java/com/synapse/payment_service/service/scheduler/SubscriptionSchedulerIntegrationTest.java similarity index 88% rename from payment-service/src/test/java/com/synapse/payment_service/service/scheduler/SubscriptionSchedulerIntegrationTest.java rename to payment-service/payment-service/src/test/java/com/synapse/payment_service/service/scheduler/SubscriptionSchedulerIntegrationTest.java index 59d9395..ba1fe67 100644 --- a/payment-service/src/test/java/com/synapse/payment_service/service/scheduler/SubscriptionSchedulerIntegrationTest.java +++ b/payment-service/payment-service/src/test/java/com/synapse/payment_service/service/scheduler/SubscriptionSchedulerIntegrationTest.java @@ -10,20 +10,15 @@ 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; -import org.springframework.transaction.annotation.Transactional; -import com.synapse.payment_service.domain.Subscription; +import com.synapse.payment_service.TestConfig; +import com.synapse.payment_service.domain.entity.Subscription; import com.synapse.payment_service.domain.enums.SubscriptionStatus; import com.synapse.payment_service.domain.enums.SubscriptionTier; -import com.synapse.payment_service.repository.SubscriptionRepository; +import com.synapse.payment_service.domain.repository.SubscriptionRepository; -@SpringBootTest -@ActiveProfiles("test") -@Transactional @DisplayName("SubscriptionScheduler 통합 테스트") -class SubscriptionSchedulerIntegrationTest { +class SubscriptionSchedulerIntegrationTest extends TestConfig { @Autowired private SubscriptionScheduler subscriptionScheduler; @@ -45,6 +40,7 @@ void setUp() { // 만료된 CANCELED 구독 생성 canceledSubscription = Subscription.builder() + .id(UUID.randomUUID()) .memberId(UUID.randomUUID()) .tier(SubscriptionTier.PRO) .remainingChatCredits(100) @@ -52,9 +48,10 @@ void setUp() { .status(SubscriptionStatus.CANCELED) .build(); canceledSubscription.deactivate(); // autoRenew를 false로 설정하지만 테스트를 위해 다시 true로 설정 - + // 만료된 PAYMENT_FAILED 구독 생성 paymentFailedSubscription = Subscription.builder() + .id(UUID.randomUUID()) .memberId(UUID.randomUUID()) .tier(SubscriptionTier.PRO) .remainingChatCredits(200) @@ -64,6 +61,7 @@ void setUp() { // 만료되지 않은 ACTIVE 구독 생성 (비교 대상) activeSubscription = Subscription.builder() + .id(UUID.randomUUID()) .memberId(UUID.randomUUID()) .tier(SubscriptionTier.FREE) .remainingChatCredits(100) @@ -79,15 +77,18 @@ void setUp() { void expireSubscriptions_shouldUpdateExpiredSubscriptionsInDatabase() { // given // setUp에서 만료된 구독들이 준비됨 - + // when subscriptionScheduler.expireSubscriptions(); // then // 만료된 구독들이 EXPIRED 상태로 변경되고 autoRenew가 false가 되었는지 확인 - Subscription updatedCanceledSubscription = subscriptionRepository.findById(canceledSubscription.getId()).orElseThrow(); - Subscription updatedPaymentFailedSubscription = subscriptionRepository.findById(paymentFailedSubscription.getId()).orElseThrow(); - Subscription updatedActiveSubscription = subscriptionRepository.findById(activeSubscription.getId()).orElseThrow(); + Subscription updatedCanceledSubscription = subscriptionRepository.findById(canceledSubscription.getId()) + .orElseThrow(); + Subscription updatedPaymentFailedSubscription = subscriptionRepository + .findById(paymentFailedSubscription.getId()).orElseThrow(); + Subscription updatedActiveSubscription = subscriptionRepository.findById(activeSubscription.getId()) + .orElseThrow(); // 만료된 구독들은 EXPIRED 상태가 되고 autoRenew가 false가 되어야 함 assertThat(updatedCanceledSubscription.getStatus()).isEqualTo(SubscriptionStatus.EXPIRED); @@ -106,9 +107,10 @@ void expireSubscriptions_shouldUpdateExpiredSubscriptionsInDatabase() { void expireSubscriptions_shouldNotChangeAnythingWhenNoExpiredSubscriptions() { // given subscriptionRepository.deleteAll(); - + // 만료되지 않은 구독만 생성 Subscription futureSubscription = Subscription.builder() + .id(UUID.randomUUID()) .memberId(UUID.randomUUID()) .tier(SubscriptionTier.FREE) .remainingChatCredits(100) @@ -131,9 +133,10 @@ void expireSubscriptions_shouldNotChangeAnythingWhenNoExpiredSubscriptions() { void expireSubscriptions_shouldNotProcessActiveExpiredSubscriptions() { // given subscriptionRepository.deleteAll(); - + // 만료된 ACTIVE 구독 생성 (이는 처리 대상이 아님) Subscription expiredActiveSubscription = Subscription.builder() + .id(UUID.randomUUID()) .memberId(UUID.randomUUID()) .tier(SubscriptionTier.PRO) .remainingChatCredits(100) @@ -146,8 +149,9 @@ void expireSubscriptions_shouldNotProcessActiveExpiredSubscriptions() { subscriptionScheduler.expireSubscriptions(); // then - Subscription unchangedSubscription = subscriptionRepository.findById(expiredActiveSubscription.getId()).orElseThrow(); + Subscription unchangedSubscription = subscriptionRepository.findById(expiredActiveSubscription.getId()) + .orElseThrow(); assertThat(unchangedSubscription.getStatus()).isEqualTo(SubscriptionStatus.ACTIVE); assertThat(unchangedSubscription.isAutoRenew()).isTrue(); } -} \ No newline at end of file +} diff --git a/payment-service/src/test/java/com/synapse/payment_service/service/scheduler/SubscriptionSchedulerTest.java b/payment-service/payment-service/src/test/java/com/synapse/payment_service/service/scheduler/SubscriptionSchedulerTest.java similarity index 89% rename from payment-service/src/test/java/com/synapse/payment_service/service/scheduler/SubscriptionSchedulerTest.java rename to payment-service/payment-service/src/test/java/com/synapse/payment_service/service/scheduler/SubscriptionSchedulerTest.java index 9fd0eac..b26fa21 100644 --- a/payment-service/src/test/java/com/synapse/payment_service/service/scheduler/SubscriptionSchedulerTest.java +++ b/payment-service/payment-service/src/test/java/com/synapse/payment_service/service/scheduler/SubscriptionSchedulerTest.java @@ -1,11 +1,11 @@ package com.synapse.payment_service.service.scheduler; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyList; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.never; import java.time.ZonedDateTime; import java.util.Arrays; @@ -16,20 +16,18 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import com.synapse.payment_service.domain.Subscription; +import com.synapse.payment_service.TestConfig; +import com.synapse.payment_service.domain.entity.Subscription; import com.synapse.payment_service.domain.enums.SubscriptionStatus; import com.synapse.payment_service.domain.enums.SubscriptionTier; -import com.synapse.payment_service.repository.SubscriptionRepository; +import com.synapse.payment_service.domain.repository.SubscriptionRepository; import com.synapse.payment_service.service.SubscriptionBillingService; -@ExtendWith(MockitoExtension.class) @DisplayName("SubscriptionScheduler 테스트") -class SubscriptionSchedulerTest { +class SubscriptionSchedulerTest extends TestConfig { @Mock private SubscriptionBillingService subscriptionBillingService; @@ -48,6 +46,7 @@ void setUp() { ZonedDateTime pastTime = ZonedDateTime.now().minusDays(1); canceledSubscription = Subscription.builder() + .id(UUID.randomUUID()) .memberId(UUID.randomUUID()) .tier(SubscriptionTier.PRO) .remainingChatCredits(SubscriptionTier.PRO.getMaxRequestCount()) @@ -56,6 +55,7 @@ void setUp() { .build(); paymentFailedSubscription = Subscription.builder() + .id(UUID.randomUUID()) .memberId(UUID.randomUUID()) .tier(SubscriptionTier.PRO) .remainingChatCredits(SubscriptionTier.PRO.getMaxRequestCount()) @@ -78,10 +78,9 @@ void expireSubscriptions_shouldUpdateExpiredSubscriptionsToExpiredStatusAndDisab // then verify(subscriptionRepository, times(1)).findByStatusInAndExpiresAtBefore( anyList(), - any(ZonedDateTime.class) - ); + any(ZonedDateTime.class)); verify(subscriptionRepository, times(1)).saveAll(expiredSubscriptions); - + // 각 구독의 expireSubscription 메서드가 호출되었는지 확인 // (실제로는 mock 객체이므로 상태 변경을 직접 검증할 수는 없지만, 메서드 호출 로직은 검증됨) } @@ -99,8 +98,7 @@ void expireSubscriptions_shouldNotSaveWhenNoExpiredSubscriptions() { // then verify(subscriptionRepository, times(1)).findByStatusInAndExpiresAtBefore( anyList(), - any(ZonedDateTime.class) - ); + any(ZonedDateTime.class)); verify(subscriptionRepository, never()).saveAll(anyList()); } @@ -113,4 +111,4 @@ void runDailyBilling_shouldCallSubscriptionBillingService() { // then verify(subscriptionBillingService, times(1)).processDailySubscriptions(); } -} \ No newline at end of file +} diff --git a/payment-service/src/test/resources/application-test.yml b/payment-service/payment-service/src/test/resources/application-test.yml similarity index 100% rename from payment-service/src/test/resources/application-test.yml rename to payment-service/payment-service/src/test/resources/application-test.yml diff --git a/payment-service/src/test/resources/application.yml b/payment-service/payment-service/src/test/resources/application.yml similarity index 100% rename from payment-service/src/test/resources/application.yml rename to payment-service/payment-service/src/test/resources/application.yml diff --git a/payment-service/settings.gradle b/payment-service/settings.gradle index d90e184..ea79a57 100644 --- a/payment-service/settings.gradle +++ b/payment-service/settings.gradle @@ -1 +1,4 @@ rootProject.name = 'payment-service' + +include ('payment-service') +include ('payment-service-api') diff --git a/payment-service/src/main/java/com/synapse/payment_service/config/JpaAuditingConfig.java b/payment-service/src/main/java/com/synapse/payment_service/config/JpaAuditingConfig.java deleted file mode 100644 index b954afe..0000000 --- a/payment-service/src/main/java/com/synapse/payment_service/config/JpaAuditingConfig.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.synapse.payment_service.config; - -import org.springframework.context.annotation.Configuration; -import org.springframework.data.jpa.repository.config.EnableJpaAuditing; - -@Configuration -@EnableJpaAuditing -public class JpaAuditingConfig { - -} diff --git a/payment-service/src/main/java/com/synapse/payment_service/dto/request/CancelSubscriptionRequest.java b/payment-service/src/main/java/com/synapse/payment_service/dto/request/CancelSubscriptionRequest.java deleted file mode 100644 index aa8a191..0000000 --- a/payment-service/src/main/java/com/synapse/payment_service/dto/request/CancelSubscriptionRequest.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.synapse.payment_service.dto.request; - -import jakarta.validation.constraints.NotBlank; - -public record CancelSubscriptionRequest( - @NotBlank(message = "취소 사유는 필수입니다.") - String reason -) { - -} diff --git a/payment-service/src/main/java/com/synapse/payment_service/dto/request/PaymentRequestDto.java b/payment-service/src/main/java/com/synapse/payment_service/dto/request/PaymentRequestDto.java deleted file mode 100644 index 8bbf75d..0000000 --- a/payment-service/src/main/java/com/synapse/payment_service/dto/request/PaymentRequestDto.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.synapse.payment_service.dto.request; - -import jakarta.validation.constraints.NotBlank; - -public record PaymentRequestDto( - @NotBlank(message = "구독 티어는 필수입니다") - String tier -) { - -} diff --git a/payment-service/src/test/java/com/synapse/payment_service/PaymentServiceApplicationTests.java b/payment-service/src/test/java/com/synapse/payment_service/PaymentServiceApplicationTests.java deleted file mode 100644 index 3afb7b1..0000000 --- a/payment-service/src/test/java/com/synapse/payment_service/PaymentServiceApplicationTests.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.synapse.payment_service; - -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class PaymentServiceApplicationTests { - -} diff --git a/payment-service/src/test/java/com/synapse/payment_service/service/SubscriptionBillingServiceTest.java b/payment-service/src/test/java/com/synapse/payment_service/service/SubscriptionBillingServiceTest.java deleted file mode 100644 index 1773920..0000000 --- a/payment-service/src/test/java/com/synapse/payment_service/service/SubscriptionBillingServiceTest.java +++ /dev/null @@ -1,222 +0,0 @@ -package com.synapse.payment_service.service; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.never; - -import java.time.ZonedDateTime; -import java.util.List; -import java.util.UUID; -import java.util.concurrent.CompletableFuture; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.transaction.annotation.Transactional; - -import com.synapse.payment_service.config.PortOneClientProperties; -import com.synapse.payment_service.domain.Order; -import com.synapse.payment_service.domain.Subscription; -import com.synapse.payment_service.domain.enums.PaymentStatus; -import com.synapse.payment_service.domain.enums.SubscriptionStatus; -import com.synapse.payment_service.domain.enums.SubscriptionTier; -import com.synapse.payment_service.repository.OrderRepository; -import com.synapse.payment_service.repository.SubscriptionRepository; - -import io.portone.sdk.server.PortOneClient; -import io.portone.sdk.server.common.PaymentAmountInput; -import io.portone.sdk.server.payment.PaymentClient; -import io.portone.sdk.server.payment.BillingKeyPaymentSummary; -import io.portone.sdk.server.payment.PayWithBillingKeyResponse; - -@ExtendWith(MockitoExtension.class) -public class SubscriptionBillingServiceTest { - @InjectMocks - private SubscriptionBillingService subscriptionBillingService; - - @Mock - private SubscriptionRepository subscriptionRepository; - @Mock - private OrderRepository orderRepository; - @Mock - private PortOneClient portoneClient; - @Mock - private PaymentClient paymentClient; - @Mock - private PortOneClientProperties portOneClientProperties; - - @Test - @DisplayName("정기 결제 성공: 만료일이 된 구독에 대해 빌링키 결제를 성공하고 상태를 갱신한다") - void processDailySubscriptions_success() { - // given - Subscription mockSubscription = mock(Subscription.class); - given(mockSubscription.getBillingKey()).willReturn("test-billing-key"); - given(mockSubscription.getTier()).willReturn(SubscriptionTier.PRO); - given(mockSubscription.getId()).willReturn(1L); - - List targets = List.of(mockSubscription); - given(subscriptionRepository.findActiveSubscriptionsDueForRenewal(any(ZonedDateTime.class), any(ZonedDateTime.class))).willReturn(targets); - - // PortOne SDK의 응답을 모의 처리 - PayWithBillingKeyResponse mockResponse = mock(PayWithBillingKeyResponse.class); - BillingKeyPaymentSummary mockPaymentResult = mock(BillingKeyPaymentSummary.class); - - given(portoneClient.getPayment()).willReturn(paymentClient); - given(portOneClientProperties.channelKey()).willReturn("test-channel-key"); - - // CompletableFuture가 성공적으로 완료되도록 설정 - CompletableFuture successFuture = CompletableFuture.completedFuture(mockResponse); - - // payWithBillingKey 호출이 성공하는 CompletableFuture를 반환하도록 설정 - given(paymentClient.payWithBillingKey( - anyString(), anyString(), anyString(), anyString(), - any(), any(), any(PaymentAmountInput.class), any(), - any(), any(), any(), any(), - any(), any(), any(), any(), - any(), any(), any(), any(), any() - )).willReturn(successFuture); - - // mock 응답 설정 - 모든 필요한 값들을 완벽하게 설정 - given(mockResponse.getPayment()).willReturn(mockPaymentResult); - given(mockPaymentResult.getPgTxId()).willReturn("test-pg-tx-id"); - - // when - subscriptionBillingService.processDailySubscriptions(); - - // then - // 성공 핸들러가 호출되어 renewSubscription이 호출되었는지 확인 - verify(mockSubscription, times(1)).renewSubscription(SubscriptionTier.PRO); - - // Order가 2번 저장되었는지 확인 (PENDING -> PAID) - verify(orderRepository, times(2)).save(any(Order.class)); - - // 실패 핸들러는 호출되지 않았음을 확인 - verify(mockSubscription, never()).handlePaymentFailure(); - verify(subscriptionRepository, never()).save(mockSubscription); - } - - @Test - @DisplayName("정기 결제 실패: PortOne API 호출이 실패하면, 실패 주문을 저장하고 구독 상태를 PAYMENT_FAILED로 변경한다") - void processDailySubscriptions_fail() { - // given - Subscription mockSubscription = mock(Subscription.class); - given(mockSubscription.getBillingKey()).willReturn("test-billing-key"); - given(mockSubscription.getTier()).willReturn(SubscriptionTier.PRO); - given(mockSubscription.getId()).willReturn(1L); - - List targets = List.of(mockSubscription); - given(subscriptionRepository.findActiveSubscriptionsDueForRenewal(any(ZonedDateTime.class), any(ZonedDateTime.class))).willReturn(targets); - - // PortOne SDK가 예외를 던지는 상황을 모의 처리 - given(portoneClient.getPayment()).willReturn(paymentClient); - given(portOneClientProperties.channelKey()).willReturn("test-channel-key"); - given(paymentClient.payWithBillingKey( - anyString(), anyString(), anyString(), anyString(), - any(), anyString(), any(PaymentAmountInput.class), any(), - any(), any(), any(), any(), - any(), any(), any(), any(), - any(), any(), anyString(), any(), any() - )).willReturn(CompletableFuture.failedFuture(new RuntimeException("PG사 연동 실패"))); - - // when - subscriptionBillingService.processDailySubscriptions(); - - // then - // Order가 2번 저장되었는지 확인 (PENDING -> FAILED) - verify(orderRepository, times(2)).save(any(Order.class)); - - // handlePaymentFailure() 메서드가 호출되었는지 검증 - verify(mockSubscription, times(1)).handlePaymentFailure(); - - // 변경된 subscription이 저장되었는지 검증 - verify(subscriptionRepository, times(1)).save(mockSubscription); - - // renewSubscription은 호출되지 않았음을 검증 (실패했으므로) - verify(mockSubscription, never()).renewSubscription(any(SubscriptionTier.class)); - } -} - -/** - * 실제 데이터베이스를 사용하는 통합 테스트 - */ -@SpringBootTest -@ActiveProfiles("test") -@Transactional -class SubscriptionBillingServiceIntegrationTest { - - @Autowired - private SubscriptionRepository subscriptionRepository; - - @Autowired - private OrderRepository orderRepository; - - @Test - @DisplayName("통합 테스트: failureHandler 호출 시 구독 상태가 PAYMENT_FAILED로 변경되고 retryCount가 1 증가한다") - void failureHandler_integrationTest_shouldUpdateSubscriptionStatusAndRetryCount() { - // given - UUID memberId = UUID.randomUUID(); - Subscription subscription = Subscription.builder() - .memberId(memberId) - .tier(SubscriptionTier.PRO) - .remainingChatCredits(SubscriptionTier.PRO.getMaxRequestCount()) - .status(SubscriptionStatus.ACTIVE) - .expiresAt(ZonedDateTime.now().plusDays(30)) - .build(); - - // billingKey 설정 - subscription.updateBillingKey("test-billing-key"); - - Subscription savedSubscription = subscriptionRepository.save(subscription); - - // 초기 상태 확인 - assertThat(savedSubscription.getStatus()).isEqualTo(SubscriptionStatus.ACTIVE); - assertThat(savedSubscription.getRetryCount()).isEqualTo(0); - - // when - failureHandler 로직을 직접 실행 - String paymentId = "test-payment-id"; - - // 결제 실패 정보 저장 (failureHandler 로직과 동일) - Order order = Order.builder() - .subscription(savedSubscription) - .paymentId(paymentId) - .amount(savedSubscription.getTier().getMonthlyPrice()) - .status(PaymentStatus.FAILED) - .build(); - orderRepository.save(order); - - // 구독 상태를 PAYMENT_FAILED로 변경하고 retryCount 증가 (failureHandler 로직과 동일) - savedSubscription.handlePaymentFailure(); - subscriptionRepository.save(savedSubscription); - - // then - // 데이터베이스에서 다시 조회하여 상태 확인 - Subscription updatedSubscription = subscriptionRepository.findById(savedSubscription.getId()).orElseThrow(); - - // 구독 상태가 PAYMENT_FAILED로 변경되었는지 확인 - assertThat(updatedSubscription.getStatus()).isEqualTo(SubscriptionStatus.PAYMENT_FAILED); - - // retryCount가 1 증가했는지 확인 - assertThat(updatedSubscription.getRetryCount()).isEqualTo(1); - - // 실패한 Order가 저장되었는지 확인 - List orders = orderRepository.findAll(); - assertThat(orders).hasSize(1); - - Order failedOrder = orders.get(0); - assertThat(failedOrder.getStatus()).isEqualTo(PaymentStatus.FAILED); - assertThat(failedOrder.getSubscription().getId()).isEqualTo(savedSubscription.getId()); - assertThat(failedOrder.getAmount()).isEqualTo(SubscriptionTier.PRO.getMonthlyPrice()); - assertThat(failedOrder.getPaymentId()).isEqualTo(paymentId); - } -}