diff --git a/.env.example b/.env.example index 25d09da..fe38945 100644 --- a/.env.example +++ b/.env.example @@ -43,7 +43,7 @@ TOSS_PAY_API_KEY=change-me TOSS_PAY_RETURN_URL=http://localhost:3000/credit/payment-result TOSS_PAY_CANCEL_URL=http://localhost:3000/credit/payment-cancel TOSS_PAY_RESULT_CALLBACK_URL=http://localhost:8080/api/payments/toss/callback -PAYMENT_COUPON_CODE=ABCD-EFGH-IJKL +PAYMENT_COUPON_CODE=ABCDEFGHIJKL JOB_POSTING_CLASSIFICATION_CONFIDENCE_THRESHOLD=0.65 APP_WORKER_EXCHANGE=jobdri.worker.exchange diff --git a/.env.production.example b/.env.production.example index 634f4e4..b250acc 100644 --- a/.env.production.example +++ b/.env.production.example @@ -53,7 +53,7 @@ TOSS_PAY_API_KEY=change-me TOSS_PAY_RETURN_URL=https://jobdri.site/credit/payment-result TOSS_PAY_CANCEL_URL=https://jobdri.site/credit/payment-cancel TOSS_PAY_RESULT_CALLBACK_URL=https://api.jobdri.site/api/payments/toss/callback -PAYMENT_COUPON_CODE=ABCD-EFGH-IJKL +PAYMENT_COUPON_CODE=ABCDEFGHIJKL AWS_REGION=ap-northeast-2 S3_BUCKET=your-s3-bucket diff --git a/src/main/java/com/jobdri/jobdri_api/domain/payment/dto/request/CouponRedeemRequest.java b/src/main/java/com/jobdri/jobdri_api/domain/payment/dto/request/CouponRedeemRequest.java index 2213dbe..4f85e89 100644 --- a/src/main/java/com/jobdri/jobdri_api/domain/payment/dto/request/CouponRedeemRequest.java +++ b/src/main/java/com/jobdri/jobdri_api/domain/payment/dto/request/CouponRedeemRequest.java @@ -6,8 +6,8 @@ public record CouponRedeemRequest( @NotBlank(message = "couponCode는 필수입니다.") @Pattern( - regexp = "^[A-Za-z0-9]{4}-[A-Za-z0-9]{4}-[A-Za-z0-9]{4}$", - message = "couponCode는 XXXX-XXXX-XXXX 형식이어야 합니다." + regexp = "^[A-Za-z0-9]{12}$", + message = "couponCode는 영문 또는 숫자 12자리여야 합니다." ) String couponCode ) { diff --git a/src/test/java/com/jobdri/jobdri_api/domain/payment/service/CouponServiceTest.java b/src/test/java/com/jobdri/jobdri_api/domain/payment/service/CouponServiceTest.java index f07e4ef..d5cb25a 100644 --- a/src/test/java/com/jobdri/jobdri_api/domain/payment/service/CouponServiceTest.java +++ b/src/test/java/com/jobdri/jobdri_api/domain/payment/service/CouponServiceTest.java @@ -9,6 +9,7 @@ import com.jobdri.jobdri_api.domain.user.repository.UserRepository; import com.jobdri.jobdri_api.global.apiPayload.code.GeneralErrorCode; import com.jobdri.jobdri_api.global.apiPayload.exception.GeneralException; +import jakarta.validation.Validator; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -44,19 +45,22 @@ class CouponServiceTest { @Autowired private CouponRedemptionRepository couponRedemptionRepository; + @Autowired + private Validator validator; + @Test @DisplayName("유효한 쿠폰 번호를 등록하면 크레딧 1회가 충전되고 사용 이력이 저장된다") void redeem() { User user = saveUser("coupon-redeem@example.com"); - CouponRedeemResponse response = couponService.redeem(user, new CouponRedeemRequest(" test-coup-2026 ")); + CouponRedeemResponse response = couponService.redeem(user, new CouponRedeemRequest(" testcoup2026 ")); - assertThat(response.couponCode()).isEqualTo("TEST-COUP-2026"); + assertThat(response.couponCode()).isEqualTo("TESTCOUP2026"); assertThat(response.creditAmount()).isEqualTo(1); assertThat(response.creditBalance()).isEqualTo(2); assertThat(response.redeemedAt()).isNotNull(); assertThat(userRepository.findById(user.getId()).orElseThrow().getCredit()).isEqualTo(2); - assertThat(couponRedemptionRepository.findByUserIdAndCouponCode(user.getId(), "TEST-COUP-2026")).isPresent(); + assertThat(couponRedemptionRepository.findByUserIdAndCouponCode(user.getId(), "TESTCOUP2026")).isPresent(); assertThat(creditTransactionRepository.findAllByUserIdAndTypeOrderByCreatedAtDescIdDesc( user.getId(), CreditTransactionType.COUPON @@ -68,7 +72,7 @@ void redeem() { void redeemThrowsWhenCouponCodeIsInvalid() { User user = saveUser("coupon-invalid@example.com"); - assertThatThrownBy(() -> couponService.redeem(user, new CouponRedeemRequest("ABCD-EFGH-IJKL"))) + assertThatThrownBy(() -> couponService.redeem(user, new CouponRedeemRequest("ABCDEFGHIJKM"))) .isInstanceOf(GeneralException.class) .extracting("code") .isEqualTo(GeneralErrorCode.COUPON_INVALID); @@ -78,9 +82,9 @@ void redeemThrowsWhenCouponCodeIsInvalid() { @DisplayName("같은 사용자가 동일 쿠폰을 다시 등록하면 중복 사용을 막는다") void redeemThrowsWhenCouponAlreadyRedeemed() { User user = saveUser("coupon-duplicate@example.com"); - couponService.redeem(user, new CouponRedeemRequest("TEST-COUP-2026")); + couponService.redeem(user, new CouponRedeemRequest("TESTCOUP2026")); - assertThatThrownBy(() -> couponService.redeem(user, new CouponRedeemRequest("TEST-COUP-2026"))) + assertThatThrownBy(() -> couponService.redeem(user, new CouponRedeemRequest("TESTCOUP2026"))) .isInstanceOf(GeneralException.class) .extracting("code") .isEqualTo(GeneralErrorCode.COUPON_ALREADY_REDEEMED); @@ -96,7 +100,7 @@ void redeemThrowsWhenCouponAlreadyRedeemed() { @DisplayName("동일 쿠폰 등록 요청이 동시에 들어와도 한 번만 충전한다") void redeemConcurrentlyChargesOnlyOnce() throws Exception { User user = saveUser("coupon-concurrent@example.com"); - CouponRedeemRequest request = new CouponRedeemRequest("TEST-COUP-2026"); + CouponRedeemRequest request = new CouponRedeemRequest("TESTCOUP2026"); List results = runConcurrently(2, () -> { try { @@ -115,13 +119,21 @@ void redeemConcurrentlyChargesOnlyOnce() throws Exception { .extracting("code") .isEqualTo(GeneralErrorCode.COUPON_ALREADY_REDEEMED)); assertThat(userRepository.findById(user.getId()).orElseThrow().getCredit()).isEqualTo(2); - assertThat(couponRedemptionRepository.findByUserIdAndCouponCode(user.getId(), "TEST-COUP-2026")).isPresent(); + assertThat(couponRedemptionRepository.findByUserIdAndCouponCode(user.getId(), "TESTCOUP2026")).isPresent(); assertThat(creditTransactionRepository.findAllByUserIdAndTypeOrderByCreatedAtDescIdDesc( user.getId(), CreditTransactionType.COUPON )).hasSize(1); } + @Test + @DisplayName("쿠폰 번호는 대시 없는 영문 또는 숫자 12자리 형식만 허용한다") + void validateCouponCodeFormat() { + assertThat(validator.validate(new CouponRedeemRequest("ABCDEFGHIJKL"))).isEmpty(); + assertThat(validator.validate(new CouponRedeemRequest("ABCD-EFGH-IJKL"))).isNotEmpty(); + assertThat(validator.validate(new CouponRedeemRequest("ABCDEFGHIJK"))).isNotEmpty(); + } + private User saveUser(String email) { return userRepository.save(User.signup("테스트 사용자", email, "encoded-password")); } diff --git a/src/test/resources/application-test.yaml b/src/test/resources/application-test.yaml index c52ca3f..cb1fb3e 100644 --- a/src/test/resources/application-test.yaml +++ b/src/test/resources/application-test.yaml @@ -81,7 +81,7 @@ job-posting: payment: coupon: - code: TEST-COUP-2026 + code: TESTCOUP2026 toss: client-key: test-toss-client-key secret-key: test-toss-secret-key