diff --git a/platform/src/main/java/com/commerce/platform/core/application/out/CardBinPromotionOutPort.java b/platform/src/main/java/com/commerce/platform/core/application/out/CardBinPromotionOutPort.java new file mode 100644 index 0000000..b799be8 --- /dev/null +++ b/platform/src/main/java/com/commerce/platform/core/application/out/CardBinPromotionOutPort.java @@ -0,0 +1,9 @@ +package com.commerce.platform.core.application.out; + +import com.commerce.platform.core.domain.aggreate.CardBinPromotion; + +import java.util.List; + +public interface CardBinPromotionOutPort { + List findActivePromotions(); +} diff --git a/platform/src/main/java/com/commerce/platform/core/domain/aggreate/CardBinPromotion.java b/platform/src/main/java/com/commerce/platform/core/domain/aggreate/CardBinPromotion.java new file mode 100644 index 0000000..ee8edbe --- /dev/null +++ b/platform/src/main/java/com/commerce/platform/core/domain/aggreate/CardBinPromotion.java @@ -0,0 +1,93 @@ +package com.commerce.platform.core.domain.aggreate; + +import com.commerce.platform.core.domain.enums.PayProvider; +import com.commerce.platform.core.domain.vo.ValidPeriod; +import com.commerce.platform.core.domain.vo.promotion.BasePromotionData; +import com.commerce.platform.infrastructure.persistence.converter.PromotionDataConverter; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; + +import java.time.LocalDateTime; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(name = "card_bin_promotion") +@Entity +public class CardBinPromotion { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "card_bin", nullable = false) + private String cardBin; + + @Column(name = "card_name", nullable = false) + private String cardName; + + @Enumerated(EnumType.STRING) + @Column(name = "pay_provider", nullable = false) + private PayProvider payProvider; + + /** + * JSON 프로모션 데이터 + * - Converter에서 임시 타입으로 변환 + * - 조회 후 PromotionDataPostProcessor에서 PayProvider에 맞게 재변환 + */ + @JdbcTypeCode(SqlTypes.JSON) + @Column(name = "promotion_data", nullable = false, columnDefinition = "json") + @Convert(converter = PromotionDataConverter.class) + private BasePromotionData promotionData; + + @Column(name = "is_active", nullable = false) + private boolean isActive = true; + + @Embedded + private ValidPeriod validPeriod; + + @Column(name = "last_updated_at", nullable = false) + private LocalDateTime lastUpdatedAt; + + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; + + @Builder + public CardBinPromotion( + String cardBin, + String cardName, + PayProvider payProvider, + BasePromotionData promotionData, + ValidPeriod validPeriod + ) { + this.cardBin = cardBin; + this.cardName = cardName; + this.payProvider = payProvider; + this.promotionData = promotionData; + this.validPeriod = validPeriod; + this.isActive = true; + } + + @PrePersist + protected void onCreate() { + this.createdAt = LocalDateTime.now(); + this.lastUpdatedAt = LocalDateTime.now(); + } + + @PreUpdate + protected void onUpdate() { + this.lastUpdatedAt = LocalDateTime.now(); + } + + public void activate() { + this.isActive = true; + } + + public void deactivate() { + this.isActive = false; + } +} diff --git a/platform/src/main/java/com/commerce/platform/core/domain/vo/promotion/BasePromotionData.java b/platform/src/main/java/com/commerce/platform/core/domain/vo/promotion/BasePromotionData.java new file mode 100644 index 0000000..8eb9907 --- /dev/null +++ b/platform/src/main/java/com/commerce/platform/core/domain/vo/promotion/BasePromotionData.java @@ -0,0 +1,8 @@ +package com.commerce.platform.core.domain.vo.promotion; + +/** + * 카드사별 프로모션 데이터 인터페이스 + * PayProvider 값에 따라 구현체로 역직렬화됨 + */ +public interface BasePromotionData { +} diff --git a/platform/src/main/java/com/commerce/platform/core/domain/vo/promotion/BcPromotionData.java b/platform/src/main/java/com/commerce/platform/core/domain/vo/promotion/BcPromotionData.java new file mode 100644 index 0000000..70a44c7 --- /dev/null +++ b/platform/src/main/java/com/commerce/platform/core/domain/vo/promotion/BcPromotionData.java @@ -0,0 +1,13 @@ +package com.commerce.platform.core.domain.vo.promotion; + +import lombok.Builder; + +@Builder +public record BcPromotionData( + String bc_target, + String bc_payType, + String bc_card_name, + String bc_content, + String bc_condition +) implements BasePromotionData { +} diff --git a/platform/src/main/java/com/commerce/platform/core/domain/vo/promotion/HanaPromotionData.java b/platform/src/main/java/com/commerce/platform/core/domain/vo/promotion/HanaPromotionData.java new file mode 100644 index 0000000..00abb9f --- /dev/null +++ b/platform/src/main/java/com/commerce/platform/core/domain/vo/promotion/HanaPromotionData.java @@ -0,0 +1,13 @@ +package com.commerce.platform.core.domain.vo.promotion; + +import lombok.Builder; + +@Builder +public record HanaPromotionData( + String hana_target, + String hana_payType, + String hana_card_name, + String hana_content, + String hana_condition +) implements BasePromotionData { +} diff --git a/platform/src/main/java/com/commerce/platform/core/domain/vo/promotion/HyundaiPromotionData.java b/platform/src/main/java/com/commerce/platform/core/domain/vo/promotion/HyundaiPromotionData.java new file mode 100644 index 0000000..9376abe --- /dev/null +++ b/platform/src/main/java/com/commerce/platform/core/domain/vo/promotion/HyundaiPromotionData.java @@ -0,0 +1,13 @@ +package com.commerce.platform.core.domain.vo.promotion; + +import lombok.Builder; + +@Builder +public record HyundaiPromotionData( + String hyundai_target, + String hyundai_payType, + String hyundai_card_name, + String hyundai_content, + String hyundai_condition +) implements BasePromotionData { +} diff --git a/platform/src/main/java/com/commerce/platform/core/domain/vo/promotion/KbPromotionData.java b/platform/src/main/java/com/commerce/platform/core/domain/vo/promotion/KbPromotionData.java new file mode 100644 index 0000000..a63d3d3 --- /dev/null +++ b/platform/src/main/java/com/commerce/platform/core/domain/vo/promotion/KbPromotionData.java @@ -0,0 +1,13 @@ +package com.commerce.platform.core.domain.vo.promotion; + +import lombok.Builder; + +@Builder +public record KbPromotionData( + String kb_target, + String kb_payType, + String kb_card_name, + String kb_content, + String kb_condition +) implements BasePromotionData { +} diff --git a/platform/src/main/java/com/commerce/platform/core/domain/vo/promotion/LottePromotionData.java b/platform/src/main/java/com/commerce/platform/core/domain/vo/promotion/LottePromotionData.java new file mode 100644 index 0000000..1e8cdac --- /dev/null +++ b/platform/src/main/java/com/commerce/platform/core/domain/vo/promotion/LottePromotionData.java @@ -0,0 +1,13 @@ +package com.commerce.platform.core.domain.vo.promotion; + +import lombok.Builder; + +@Builder +public record LottePromotionData( + String lotte_target, + String lotte_payType, + String lotte_card_name, + String lotte_content, + String lotte_condition +) implements BasePromotionData { +} diff --git a/platform/src/main/java/com/commerce/platform/core/domain/vo/promotion/NhPromotionData.java b/platform/src/main/java/com/commerce/platform/core/domain/vo/promotion/NhPromotionData.java new file mode 100644 index 0000000..7200eba --- /dev/null +++ b/platform/src/main/java/com/commerce/platform/core/domain/vo/promotion/NhPromotionData.java @@ -0,0 +1,13 @@ +package com.commerce.platform.core.domain.vo.promotion; + +import lombok.Builder; + +@Builder +public record NhPromotionData( + String nh_target, + String nh_payType, + String nh_card_name, + String nh_content, + String nh_condition +) implements BasePromotionData { +} diff --git a/platform/src/main/java/com/commerce/platform/core/domain/vo/promotion/SamsungPromotionData.java b/platform/src/main/java/com/commerce/platform/core/domain/vo/promotion/SamsungPromotionData.java new file mode 100644 index 0000000..256b9bc --- /dev/null +++ b/platform/src/main/java/com/commerce/platform/core/domain/vo/promotion/SamsungPromotionData.java @@ -0,0 +1,13 @@ +package com.commerce.platform.core.domain.vo.promotion; + +import lombok.Builder; + +@Builder +public record SamsungPromotionData( + String samsung_target, + String samsung_payType, + String samsung_card_name, + String samsung_content, + String samsung_condition +) implements BasePromotionData { +} diff --git a/platform/src/main/java/com/commerce/platform/core/domain/vo/promotion/ShinhanPromotionData.java b/platform/src/main/java/com/commerce/platform/core/domain/vo/promotion/ShinhanPromotionData.java new file mode 100644 index 0000000..6034ec8 --- /dev/null +++ b/platform/src/main/java/com/commerce/platform/core/domain/vo/promotion/ShinhanPromotionData.java @@ -0,0 +1,13 @@ +package com.commerce.platform.core.domain.vo.promotion; + +import lombok.Builder; + +@Builder +public record ShinhanPromotionData( + String shinhan_target, + String shinhan_payType, + String shinhan_card_name, + String shinhan_content, + String shinhan_condition +) implements BasePromotionData { +} diff --git a/platform/src/main/java/com/commerce/platform/infrastructure/adaptor/CardBinPromotionAdaptor.java b/platform/src/main/java/com/commerce/platform/infrastructure/adaptor/CardBinPromotionAdaptor.java new file mode 100644 index 0000000..9304353 --- /dev/null +++ b/platform/src/main/java/com/commerce/platform/infrastructure/adaptor/CardBinPromotionAdaptor.java @@ -0,0 +1,36 @@ +package com.commerce.platform.infrastructure.adaptor; + +import com.commerce.platform.core.application.out.CardBinPromotionOutPort; +import com.commerce.platform.core.domain.aggreate.CardBinPromotion; +import com.commerce.platform.infrastructure.persistence.CardBinPromotionRepository; +import com.commerce.platform.infrastructure.persistence.processor.PromotionDataPostProcessor; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * promotionData 후처리 + * CardBinPromotion 반환 + */ +@Log4j2 +@RequiredArgsConstructor +@Component +public class CardBinPromotionAdaptor implements CardBinPromotionOutPort { + + private final CardBinPromotionRepository repository; + private final PromotionDataPostProcessor postProcessor; + + public List findActivePromotions() { + List results = repository.findAllByActive(); + results.forEach(postProcessor::process); + return results; + } + + public CardBinPromotion save(CardBinPromotion entity) { + CardBinPromotion saved = repository.save(entity); + postProcessor.process(saved); + return saved; + } +} diff --git a/platform/src/main/java/com/commerce/platform/infrastructure/persistence/CardBinPromotionRepository.java b/platform/src/main/java/com/commerce/platform/infrastructure/persistence/CardBinPromotionRepository.java new file mode 100644 index 0000000..6ae72ae --- /dev/null +++ b/platform/src/main/java/com/commerce/platform/infrastructure/persistence/CardBinPromotionRepository.java @@ -0,0 +1,23 @@ +package com.commerce.platform.infrastructure.persistence; + +import com.commerce.platform.core.domain.aggreate.CardBinPromotion; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface CardBinPromotionRepository extends JpaRepository { + + /** + * 활성화된 카드 BIN 조회 + */ + @Query(""" + SELECT c FROM CardBinPromotion c + WHERE c.isActive = true + AND CURRENT_DATE BETWEEN c.validPeriod.frDt AND c.validPeriod.toDt + """) + List findAllByActive(); + +} diff --git a/platform/src/main/java/com/commerce/platform/infrastructure/persistence/converter/PromotionDataConverter.java b/platform/src/main/java/com/commerce/platform/infrastructure/persistence/converter/PromotionDataConverter.java new file mode 100644 index 0000000..2963542 --- /dev/null +++ b/platform/src/main/java/com/commerce/platform/infrastructure/persistence/converter/PromotionDataConverter.java @@ -0,0 +1,47 @@ +package com.commerce.platform.infrastructure.persistence.converter; + +import com.commerce.platform.core.domain.vo.promotion.BasePromotionData; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; +import lombok.extern.log4j.Log4j2; + +/** + * BasePromotionData <-> JSON String 변환 Converter + */ +@Log4j2 +@Converter +public class PromotionDataConverter implements AttributeConverter { + + private static final ObjectMapper objectMapper = new ObjectMapper() + .registerModule(new JavaTimeModule()) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + + @Override + public String convertToDatabaseColumn(BasePromotionData attribute) { + if (attribute == null) return null; + + try { + return objectMapper.writeValueAsString(attribute); + } catch (JsonProcessingException e) { + throw new IllegalStateException("프로모션 데이터 직렬화 실패", e); + } + } + + @Override + public BasePromotionData convertToEntityAttribute(String dbData) { + if (dbData == null || dbData.isBlank()) return null; + + // 임시 객체 JsonPromotionData 반환 + return new JsonPromotionData(dbData); + } + + /** + * JSON String을 보관하는 임시 래퍼 클래스 + */ + public record JsonPromotionData(String jsonData) + implements BasePromotionData {} +} diff --git a/platform/src/main/java/com/commerce/platform/infrastructure/persistence/processor/PromotionDataPostProcessor.java b/platform/src/main/java/com/commerce/platform/infrastructure/persistence/processor/PromotionDataPostProcessor.java new file mode 100644 index 0000000..d227858 --- /dev/null +++ b/platform/src/main/java/com/commerce/platform/infrastructure/persistence/processor/PromotionDataPostProcessor.java @@ -0,0 +1,78 @@ +package com.commerce.platform.infrastructure.persistence.processor; + +import com.commerce.platform.core.domain.aggreate.CardBinPromotion; +import com.commerce.platform.core.domain.enums.PayProvider; +import com.commerce.platform.core.domain.vo.promotion.*; +import com.commerce.platform.infrastructure.persistence.converter.PromotionDataConverter; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import lombok.extern.log4j.Log4j2; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Field; + +/** + * CardBinPromotion 조회 후 PayProvider에 따른 promotionData 타입 변환 + */ +@Log4j2 +@Component +public class PromotionDataPostProcessor { + + private static final ObjectMapper objectMapper = new ObjectMapper() + .registerModule(new JavaTimeModule()) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + + public void process(CardBinPromotion entity) { + if (entity == null || entity.getPayProvider() == null) return; + + BasePromotionData currentData = entity.getPromotionData(); + if (currentData == null) return; + + // 카드사에 매핑되는 프로모션 dto 변환 + if (currentData instanceof PromotionDataConverter.JsonPromotionData(String jsonData)) { + try { + BasePromotionData convertedData = convertToProperType(jsonData, entity.getPayProvider()); + setPromotionData(entity, convertedData); + + } catch (Exception e) { + log.error("프로모션 데이터 후처리 실패 - PayProvider: {}", entity.getPayProvider(), e); + } + } + } + + public BasePromotionData convertToProperType(String json, PayProvider payProvider) throws JsonProcessingException { + if (json == null || json.isBlank()) return null; + + Class clazz = getPromotionDataClass(payProvider); + return objectMapper.readValue(json, clazz); + } + + private Class getPromotionDataClass(PayProvider payProvider) { + return switch (payProvider) { + case SAMSUNG -> SamsungPromotionData.class; + case SHIN_HAN -> ShinhanPromotionData.class; + case KB -> KbPromotionData.class; + case HYUNDAI -> HyundaiPromotionData.class; + case BC -> BcPromotionData.class; + case HANA -> HanaPromotionData.class; + case LOTTE -> LottePromotionData.class; + case NH -> NhPromotionData.class; + default -> throw new IllegalArgumentException( + "지원하지 않는 카드사입니다: " + payProvider + ); + }; + } + + private void setPromotionData(CardBinPromotion entity, BasePromotionData data) { + try { + Field field = CardBinPromotion.class.getDeclaredField("promotionData"); + field.setAccessible(true); + field.set(entity, data); + } catch (NoSuchFieldException | IllegalAccessException e) { + log.error("promotionData 필드 설정 실패", e); + throw new IllegalStateException("promotionData 필드 설정 실패", e); + } + } +} diff --git a/platform/src/test/java/com/commerce/platform/infrastructure/adaptor/CardBinPromotionAdaptorTest.java b/platform/src/test/java/com/commerce/platform/infrastructure/adaptor/CardBinPromotionAdaptorTest.java new file mode 100644 index 0000000..4375dec --- /dev/null +++ b/platform/src/test/java/com/commerce/platform/infrastructure/adaptor/CardBinPromotionAdaptorTest.java @@ -0,0 +1,37 @@ +package com.commerce.platform.infrastructure.adaptor; + +import com.commerce.platform.core.domain.aggreate.CardBinPromotion; +import com.commerce.platform.core.domain.enums.PayProvider; +import com.commerce.platform.core.domain.vo.promotion.ShinhanPromotionData; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +@SpringBootTest +class CardBinPromotionAdaptorTest { + @Autowired + private CardBinPromotionAdaptor cardBinPromotionAdaptor; + + @Test + void findActiveByCardBins() { + List activePromotions = cardBinPromotionAdaptor.findActivePromotions(); + + assertAll( + () -> { + assertThat(activePromotions.size()).isEqualTo(7); + assertThat(activePromotions.stream() + .filter(p -> p.getPayProvider().equals(PayProvider.SHIN_HAN)) + .map(CardBinPromotion::getPromotionData) + .findFirst() + .get() + ).isExactlyInstanceOf(ShinhanPromotionData.class); + } + ); + } + +} \ No newline at end of file