Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.swyp.picke.domain.admin.dto.funding.request;

import com.swyp.picke.domain.funding.enums.DeliveryStatus;
import com.swyp.picke.domain.funding.enums.TumblbugPlan;
import com.swyp.picke.domain.ticket.enums.PaymentStatus;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.PositiveOrZero;
import jakarta.validation.constraints.Size;
import java.time.LocalDateTime;

@Schema(description = "텀블벅 펀딩 구매자 수동 등록. 코드는 입력하지 않고 결제 완료 시 PICKE가 발급합니다.")
public record ManualTumblbugBuyerRequest(
@NotNull TumblbugPlan plan,
@NotBlank @Size(max = 50) String campaignId,
@NotBlank @Size(max = 120) String campaignName,
@NotBlank @Size(max = 120) String supporterNumber,
@NotBlank @Size(max = 120) String rewardLineId,
@NotBlank @Size(max = 50) String buyerName,
@NotBlank @Size(max = 120) String rewardName,
@Size(max = 300) String giftText,
@NotNull @Min(5) @Max(30) Integer capacity,
@NotNull @Min(1) @Max(7) Integer quantity,
@NotNull LocalDateTime supportedAt,
@NotNull @PositiveOrZero Long rewardAmount,
@NotNull @PositiveOrZero Long additionalSupportAmount,
@NotNull @PositiveOrZero Long totalSupportAmount,
@NotNull PaymentStatus paymentStatus,
@NotNull DeliveryStatus giftDeliveryStatus
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.swyp.picke.domain.admin.dto.funding.response;

import com.swyp.picke.domain.funding.enums.DeliveryStatus;
import com.swyp.picke.domain.funding.enums.TumblbugPlan;
import com.swyp.picke.domain.ticket.enums.PaymentStatus;
import java.time.LocalDateTime;
import java.util.List;

/** 수동 등록 결과. 발급 코드는 관리자 응답에만 포함하고 로그에는 기록하지 않습니다. */
public record ManualTumblbugBuyerResponse(
Long fundingOrderId,
TumblbugPlan plan,
String campaignId,
String supporterNumber,
String buyerName,
String rewardName,
int quantity,
LocalDateTime supportedAt,
long rewardAmount,
long additionalSupportAmount,
long totalSupportAmount,
PaymentStatus paymentStatus,
DeliveryStatus giftDeliveryStatus,
List<IssuedCode> issuedCodes
) {
public record IssuedCode(Long ticketId, String code) {}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.swyp.picke.domain.funding.controller;

import com.swyp.picke.domain.admin.dto.funding.request.ManualTumblbugBuyerRequest;
import com.swyp.picke.domain.admin.dto.funding.response.ManualTumblbugBuyerResponse;
import com.swyp.picke.domain.funding.dto.request.*;
import com.swyp.picke.domain.funding.dto.response.*;
import com.swyp.picke.domain.funding.service.FundingAdminService;
Expand Down Expand Up @@ -77,6 +79,27 @@ public ApiResponse<TumblbugWorkbookInspectResponse> inspectTumblbugWorkbook(
return ApiResponse.onSuccess(fundingAdminService.inspectTumblbugWorkbook(readXlsx(file)));
}

@Operation(summary = "텀블벅 구매자 공통 등록", description = "Start·Run 공통 엑셀 등록입니다. 컬럼 검증·중복 방지 후 결제 완료 건은 수량만큼 코드를 발급하고 결과를 돌려줍니다.")
@PostMapping(value = "/funding-orders/tumblbug/import", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<ApiResponse<TumblbugSupporterImportResponse>> importTumblbugSupporters(
@Valid @RequestPart("options") TumblbugSupporterImportRequest options,
@RequestPart("file") MultipartFile file,
@AuthenticationPrincipal Long adminUserId
) {
return ResponseEntity.ok().cacheControl(CacheControl.noStore())
.body(ApiResponse.onSuccess(fundingAdminService.importTumblbugSupporters(options, readXlsx(file), adminUserId)));
}

@Operation(summary = "텀블벅 구매자 수동 등록", description = "Start·Run 공통 8개 화면 필드를 수동 저장합니다. 결제 예약은 코드 미발급, 결제 완료는 구매 수량만큼 코드 발급입니다.")
@PostMapping("/funding-orders/tumblbug/manual")
public ResponseEntity<ApiResponse<ManualTumblbugBuyerResponse>> createManualTumblbugBuyer(
@Valid @RequestBody ManualTumblbugBuyerRequest request,
@AuthenticationPrincipal Long adminUserId
) {
return ResponseEntity.ok().cacheControl(CacheControl.noStore())
.body(ApiResponse.onSuccess(fundingAdminService.createManualTumblbugBuyer(request, adminUserId)));
}

@Operation(summary = "텀블벅 후원자 저장 및 코드 엑셀 생성", description = "자동 판별한 후원자 표 데이터를 DB에 저장하고 결제 완료 행의 이용권 코드를 텀블벅 업로드용 엑셀로 반환합니다. 원문 코드는 이 파일에만 포함됩니다.")
@io.swagger.v3.oas.annotations.responses.ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "후원번호-이용권 코드 XLSX 다운로드",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.swyp.picke.domain.funding.dto.request;

import com.swyp.picke.domain.funding.enums.TumblbugPlan;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
Expand All @@ -14,6 +16,15 @@ public record TumblbugSupporterImportRequest(
@NotBlank @Size(max = 31) @Schema(description = "처리할 리워드 시트 이름. 열 제목은 자동 판별") String sheetName,
@Size(max = 120) @Schema(description = "픽케 내부 중복 방지용 리워드 키. 생략하면 시트명을 사용", example = "class-ticket-15") String rewardLineId,
@NotNull @Min(5) @Max(30) @Schema(description = "리워드에 대응하는 이용권 정원. 5, 15, 30", allowableValues = {"5", "15", "30"}, example = "15") Integer capacity,
@Size(max = 50) @Schema(description = "원본 파일명 또는 내보내기 버전. 미입력 시 서버가 생성") String sourceVersion
@Size(max = 50) @Schema(description = "원본 파일명 또는 내보내기 버전. 미입력 시 서버가 생성") String sourceVersion,
@Schema(description = "텀블벅 요금제. 생략하면 기존 호환을 위해 RUN", example = "START") TumblbugPlan plan
) {
public TumblbugSupporterImportRequest(String campaignId, String campaignName, String sheetName,
String rewardLineId, Integer capacity, String sourceVersion) {
this(campaignId, campaignName, sheetName, rewardLineId, capacity, sourceVersion, null);
}

public TumblbugPlan effectivePlan() {
return plan == null ? TumblbugPlan.RUN : plan;
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
package com.swyp.picke.domain.funding.dto.response;

import com.swyp.picke.domain.funding.enums.TumblbugPlan;
import java.time.LocalDateTime;

/** XLSX-only fields attached to the existing admin order response. */
public record TumblbugOrderDetailResponse(
String campaignName,
TumblbugPlan plan,
String supporterNumber,
String rewardLineId,
LocalDateTime supportedAt,
String rewardName,
String giftText,
Long rewardAmount,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.swyp.picke.domain.funding.dto.response;

import com.swyp.picke.domain.funding.enums.DeliveryStatus;
import com.swyp.picke.domain.funding.enums.TumblbugPlan;
import com.swyp.picke.domain.ticket.enums.PaymentStatus;
import java.time.LocalDateTime;
import java.util.List;

/** Start·Run 공통 업로드 결과. 전체 코드는 관리자 전용 응답에서만 전달합니다. */
public record TumblbugSupporterImportResponse(
TumblbugPlan plan,
int importedCount,
int issuedCodeCount,
List<Buyer> buyers
) {
public record Buyer(
int rowNumber,
String supporterNumber,
String buyerName,
String rewardName,
int quantity,
LocalDateTime supportedAt,
Long rewardAmount,
Long additionalSupportAmount,
Long totalSupportAmount,
PaymentStatus paymentStatus,
DeliveryStatus giftDeliveryStatus,
List<IssuedCode> issuedCodes
) {}

public record IssuedCode(Long ticketId, String code) {}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.swyp.picke.domain.funding.entity;

import com.swyp.picke.domain.funding.enums.FundingCampaignStatus;
import com.swyp.picke.domain.funding.enums.TumblbugPlan;
import com.swyp.picke.domain.ticket.enums.FundingPlatform;
import com.swyp.picke.global.common.BaseEntity;
import jakarta.persistence.Column;
Expand Down Expand Up @@ -46,9 +47,18 @@ public class FundingCampaign extends BaseEntity {
@Column(name = "register_policy_version", nullable = false, length = 30)
private String registerPolicyVersion = "1";

@Enumerated(EnumType.STRING)
@Column(name = "tumblbug_plan", nullable = false, length = 20)
private TumblbugPlan tumblbugPlan = TumblbugPlan.RUN;

public FundingCampaign(FundingPlatform platform, String externalCampaignId, String name) {
this(platform, externalCampaignId, name, TumblbugPlan.RUN);
}

public FundingCampaign(FundingPlatform platform, String externalCampaignId, String name, TumblbugPlan tumblbugPlan) {
this.platform = platform;
this.externalCampaignId = externalCampaignId;
this.name = name;
this.tumblbugPlan = tumblbugPlan;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.swyp.picke.domain.funding.enums;

/** 텀블벅 요금제별 코드 전달 기능. 구매자 등록과 코드 발급은 공통입니다. */
public enum TumblbugPlan {
START,
RUN
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@
import com.swyp.picke.domain.funding.entity.TumblbugImportRecord;
import java.util.Collection;
import java.util.List;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.EntityGraph;

public interface TumblbugImportRecordRepository extends JpaRepository<TumblbugImportRecord, Long> {
@EntityGraph(attributePaths = {"campaign", "fundingOrder"})
List<TumblbugImportRecord> findByFundingOrderIdIn(Collection<Long> fundingOrderIds);
@Query("select r from TumblbugImportRecord r join fetch r.campaign where r.fundingOrder.id in :fundingOrderIds")
List<TumblbugImportRecord> findByFundingOrderIdIn(@Param("fundingOrderIds") Collection<Long> fundingOrderIds);
boolean existsByCampaignIdAndSupporterNumberAndRewardLineId(Long campaignId, String supporterNumber, String rewardLineId);

List<TumblbugImportRecord> findByCampaignIdAndSupporterNumberInAndRewardLineId(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.swyp.picke.domain.funding.service;

import com.swyp.picke.domain.admin.dto.funding.request.ManualTumblbugBuyerRequest;
import com.swyp.picke.domain.admin.dto.funding.response.ManualTumblbugBuyerResponse;
import com.swyp.picke.domain.funding.dto.request.TumblbugSupporterImportRequest;
import com.swyp.picke.domain.funding.dto.request.TumblbugDigitalGiftDeliveryUpdateRequest;
import com.swyp.picke.domain.funding.dto.response.*;
Expand All @@ -8,6 +10,8 @@
public interface FundingAdminService {
TumblbugImportPreviewResponse previewTumblbugSupporters(TumblbugSupporterImportRequest request, byte[] file);
TumblbugWorkbookInspectResponse inspectTumblbugWorkbook(byte[] file);
TumblbugSupporterImportResponse importTumblbugSupporters(TumblbugSupporterImportRequest request, byte[] file, Long adminUserId);
ManualTumblbugBuyerResponse createManualTumblbugBuyer(ManualTumblbugBuyerRequest request, Long adminUserId);
byte[] issueTumblbugDigitalGiftCodes(TumblbugSupporterImportRequest request, byte[] file, Long adminUserId);
TumblbugDigitalGiftDeliveryUpdateResponse updateTumblbugDigitalGiftDelivery(
TumblbugDigitalGiftDeliveryUpdateRequest request, Long adminUserId);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.swyp.picke.domain.funding.service;

import com.swyp.picke.domain.funding.enums.TumblbugPlan;
import org.springframework.stereotype.Component;

/** Run: Start 공통 기능에 텀블벅 디지털 선물 코드 XLSX 전달을 더합니다. */
@Component
public class RunTumblbugPlanHandler implements TumblbugPlanHandler {
@Override
public TumblbugPlan plan() {
return TumblbugPlan.RUN;
}

@Override
public boolean supportsDigitalGiftWorkbook() {
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.swyp.picke.domain.funding.service;

import com.swyp.picke.domain.funding.enums.TumblbugPlan;
import org.springframework.stereotype.Component;

/** Start: 명단 업로드·수동 등록·코드 발급 후 텀블벅 1:1 메시지로 전달합니다. */
@Component
public class StartTumblbugPlanHandler implements TumblbugPlanHandler {
@Override
public TumblbugPlan plan() {
return TumblbugPlan.START;
}

@Override
public boolean supportsDigitalGiftWorkbook() {
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ public Map<Long, TumblbugOrderDetailResponse> getDetails(List<Long> orderIds) {
try {
JsonNode row = objectMapper.readTree(record.getSourceSnapshot());
result.put(record.getFundingOrder().getId(), new TumblbugOrderDetailResponse(
record.getCampaign().getName(), record.getSupporterNumber(), record.getRewardLineId(),
text(row, "rewardName"), text(row, "giftText"), number(row, "rewardAmount"),
record.getCampaign().getName(), record.getCampaign().getTumblbugPlan(),
record.getSupporterNumber(), record.getRewardLineId(),
date(row, "paidAt"), text(row, "rewardName"), text(row, "giftText"), number(row, "rewardAmount"),
number(row, "additionalSupportAmount"), number(row, "totalSupportAmount"),
row.hasNonNull("notificationRequested") ? row.get("notificationRequested").asBoolean() : null,
text(row, "notificationRequest"), text(row, "rawPaymentStatus"), text(row, "giftDeliveryStatus")));
Expand All @@ -46,4 +47,13 @@ private String text(JsonNode row, String field) {
private Long number(JsonNode row, String field) {
return row.hasNonNull(field) ? row.get(field).asLong() : null;
}

private java.time.LocalDateTime date(JsonNode row, String field) {
if (!row.hasNonNull(field)) return null;
try {
return java.time.LocalDateTime.parse(row.get(field).asText());
} catch (java.time.format.DateTimeParseException ignored) {
return null;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.swyp.picke.domain.funding.service;

import com.swyp.picke.domain.funding.enums.TumblbugPlan;

/** 요금제별 텀블벅 전달 기능을 구분하는 전략 계약. */
public interface TumblbugPlanHandler {
TumblbugPlan plan();

boolean supportsDigitalGiftWorkbook();
}
Loading
Loading