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,24 @@
-- AlterTable: Purchase ํ™˜๋ถˆ์ •์ฑ… ๋™์˜ ์‹œ์  (#533)
ALTER TABLE `Purchase` ADD COLUMN `refund_policy_agreed_at` DATETIME(3) NULL;

-- AlterTable: Refund ์ˆ˜๋™ ํ™˜๋ถˆ ์›Œํฌํ”Œ๋กœ ์ปฌ๋Ÿผ (#533)
-- status ๊ธฐ๋ณธ๊ฐ’์„ COMPLETED๋กœ ๋‘์–ด ๊ธฐ์กด ์ž๋™ ํ™˜๋ถˆ ๋ ˆ์ฝ”๋“œ๊ฐ€ ๊ทธ๋Œ€๋กœ ์™„๋ฃŒ ์ƒํƒœ๋กœ ๋ฐฑํ•„๋œ๋‹ค.
ALTER TABLE `Refund`
ADD COLUMN `status` ENUM('REQUESTED', 'APPROVED', 'REJECTED', 'COMPLETED') NOT NULL DEFAULT 'COMPLETED',
ADD COLUMN `request_reason` VARCHAR(500) NULL,
ADD COLUMN `reject_reason` VARCHAR(500) NULL,
ADD COLUMN `reviewed_by` INTEGER NULL,
ADD COLUMN `reviewed_at` DATETIME(3) NULL,
ADD COLUMN `payple_fail_code` VARCHAR(40) NULL,
ADD COLUMN `requested_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3);

-- ๊ธฐ์กด ๋ ˆ์ฝ”๋“œ์˜ requested_at์€ ์‹ค์ œ ํ™˜๋ถˆ ์‹œ์ ์œผ๋กœ ๋งž์ถ˜๋‹ค (๊ธฐ๋ณธ๊ฐ’ CURRENT_TIMESTAMP ๋Œ€์‹ ).
UPDATE `Refund` SET `requested_at` = `refunded_at`;

-- refunded_at์€ "ํ™˜๋ถˆ ์™„๋ฃŒ ์‹œ๊ฐ"์œผ๋กœ ์˜๋ฏธ๋ฅผ ์ขํžŒ๋‹ค.
-- REQUESTED/REJECTED ๋‹จ๊ณ„์—์„œ ๊ฐ’์ด ์ฑ„์›Œ์ง€๋ฉด ์™„๋ฃŒ๋œ ํ™˜๋ถˆ๋กœ ์˜ค์ธ๋˜๋ฏ€๋กœ nullable + ๊ธฐ๋ณธ๊ฐ’ ์ œ๊ฑฐ.
-- ๊ธฐ์กด ๋ ˆ์ฝ”๋“œ๋Š” ๋ชจ๋‘ COMPLETED์ด๋ฏ€๋กœ ๊ฐ’์ด ๊ทธ๋Œ€๋กœ ์œ ์ง€๋œ๋‹ค.
ALTER TABLE `Refund` MODIFY COLUMN `refunded_at` DATETIME(3) NULL;

-- CreateIndex
CREATE INDEX `Refund_status_idx` ON `Refund`(`status`);
36 changes: 28 additions & 8 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,7 @@ model Purchase {
created_at DateTime @default(now())
updated_at DateTime @updatedAt
downloaded_at DateTime? // ์ฒซ ๋‹ค์šด๋กœ๋“œ ์‹œ์  โ€” ํ™˜๋ถˆ ๊ฐ€๋Šฅ ์กฐ๊ฑด ํŒ๋‹จ์šฉ (#485)
refund_policy_agreed_at DateTime? // ๊ฒฐ์ œ ์‹œ "์—ด๋žŒ ํ›„ ๋‹จ์ˆœ๋ณ€์‹ฌ ํ™˜๋ถˆ ๋ถˆ๊ฐ€" ๋™์˜ ์‹œ์  (#533)
payment Payment?
refund Refund?
prompt Prompt @relation(fields: [prompt_id], references: [prompt_id])
Expand Down Expand Up @@ -444,23 +445,42 @@ model Payment {
cash_receipt_url String?
}

// ํ™˜๋ถˆ ์ฒ˜๋ฆฌ ์ƒํƒœ (#533)
// ์—ด๋žŒ ์ „ 7์ผ ์ด๋‚ด ์ž๋™ ํ™˜๋ถˆ์€ ๊ณง๋ฐ”๋กœ COMPLETED๋กœ ์ƒ์„ฑ๋˜๊ณ ,
// ์—ด๋žŒ ํ›„ ์ˆ˜๋™ ํ™˜๋ถˆ์€ REQUESTED โ†’ APPROVED/REJECTED โ†’ COMPLETED ๋กœ ์ง„ํ–‰๋œ๋‹ค.
enum RefundStatus {
REQUESTED // ์‚ฌ์šฉ์ž ์‹ ์ฒญ, ๋‹ด๋‹น์ž ๊ฒ€ํ†  ๋Œ€๊ธฐ
APPROVED // ์Šน์ธ๋์œผ๋‚˜ Payple ์ทจ์†Œ ๋ฏธ์™„๋ฃŒ (์นด๋“œ์‚ฌ ๊ธฐ๊ฐ„ ์ดˆ๊ณผ ๋“ฑ) โ€” ์ˆ˜๋™ ์†ก๊ธˆ ๋Œ€์ƒ
REJECTED // ๋‹ด๋‹น์ž ๊ฑฐ์ ˆ
COMPLETED // ํ™˜๋ถˆ ์™„๋ฃŒ
}

model Refund {
refund_id Int @id @default(autoincrement())
purchase_id Int @unique
payment_id Int @unique
refund_id Int @id @default(autoincrement())
purchase_id Int @unique
payment_id Int @unique
user_id Int
amount Int
reason String? @db.VarChar(200)
initiator String @db.VarChar(20) // 'USER' | 'ADMIN'
payple_pay_code String? @db.VarChar(20) // PCD_PAY_CODE (e.g. PAYC0000)
payple_card_trade_num String? @db.VarChar(64) // PCD_PAY_CARDTRADENUM ๊ฐ์‚ฌ ์ถ”์ ์šฉ
refunded_at DateTime @default(now())
reason String? @db.VarChar(200)
initiator String @db.VarChar(20) // 'USER' | 'ADMIN'
payple_pay_code String? @db.VarChar(20) // PCD_PAY_CODE (e.g. PAYC0000)
payple_card_trade_num String? @db.VarChar(64) // PCD_PAY_CARDTRADENUM ๊ฐ์‚ฌ ์ถ”์ ์šฉ
refunded_at DateTime? // ์‹ค์ œ ํ™˜๋ถˆ ์™„๋ฃŒ ์‹œ๊ฐ โ€” REQUESTED/REJECTED ๋‹จ๊ณ„์—์„œ๋Š” null (#533)
// ์ˆ˜๋™ ํ™˜๋ถˆ ์›Œํฌํ”Œ๋กœ (#533)
status RefundStatus @default(COMPLETED) // ๊ธฐ์กด ์ž๋™ ํ™˜๋ถˆ ๋ ˆ์ฝ”๋“œ๋Š” COMPLETED
request_reason String? @db.VarChar(500) // ์‚ฌ์šฉ์ž ์‹ ์ฒญ ์‚ฌ์œ 
reject_reason String? @db.VarChar(500) // ๋‹ด๋‹น์ž ๊ฑฐ์ ˆ ์‚ฌ์œ 
reviewed_by Int? // ์ฒ˜๋ฆฌํ•œ ๊ด€๋ฆฌ์ž user_id โ€” ๊ฐ์‚ฌ์šฉ์ด๋ผ FK ์—†์ด ๊ฐ’๋งŒ ๋ณด๊ด€
reviewed_at DateTime?
payple_fail_code String? @db.VarChar(40) // ์Šน์ธ ํ›„ Payple ์ทจ์†Œ ์‹คํŒจ ์‹œ ์‘๋‹ต ์ฝ”๋“œ
requested_at DateTime @default(now())

purchase Purchase @relation(fields: [purchase_id], references: [purchase_id], onDelete: Cascade)
payment Payment @relation(fields: [payment_id], references: [payment_id], onDelete: Cascade)
user User @relation(fields: [user_id], references: [user_id], onDelete: Cascade)

@@index([user_id])
@@index([status])
}

model Settlement {
Expand Down
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import promptRoutes from "./prompts/routes/prompt.route"; // ํ”„๋กฌํ”„ํŠธ ๊ด€๋ จ
import ReviewRouter from "./reviews/routes/review.route";
import purchaseRouter from "./purchases/routes/purchase.route";
import refundRouter from "./refunds/routes/refund.route";
import adminRefundRouter from "./refunds/routes/admin-refund.route";
import payoutWebhookRouter from "./settlements/routes/payout-webhook.route";
import purchaseWebhookRouter from "./purchases/routes/purchase.webhook.route";
import settlementRouter from "./settlements/routes/settlement.route";
Expand Down Expand Up @@ -173,6 +174,8 @@ app.use("/api/prompts", promptLikeRouter);
app.use("/api/admin/prompts", adminPromptRouter);
app.use("/api/admin/sellers", adminSellerRouter);
app.use("/api/admin/stats", adminStatsRouter);
app.use("/api/admin/refunds", adminRefundRouter);
// adminMemberRouter๋Š” /api/admin ์ „์ฒด๋ฅผ ๋ฐ›์œผ๋ฏ€๋กœ ํ•˜์œ„ ๊ฒฝ๋กœ ๋ผ์šฐํ„ฐ๋ณด๋‹ค ๋’ค์— ๋งˆ์šดํŠธ.
app.use("/api/admin", adminMemberRouter);

// ํŒ ๋ผ์šฐํ„ฐ
Expand Down
6 changes: 5 additions & 1 deletion src/prompts/dtos/prompt.download.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ export interface DownloadedPromptResponseDTO {
message: string;
prompt_id: number;
purchase_id: number;
is_refunded: boolean;
is_refunded: boolean; // ํ™˜๋ถˆ ํ™•์ •(APPROVED/COMPLETED) ์—ฌ๋ถ€
refund_status: string | null; // REQUESTED | APPROVED | REJECTED | COMPLETED (#533)
refundable: boolean; // ์—ด๋žŒ ์ „ + 7์ผ ์ด๋‚ด โ†’ ์ฆ‰์‹œ ํ™˜๋ถˆ ๋ฒ„ํŠผ ํ™œ์„ฑํ™” (#533)
refund_deadline: string | null; // ์ž๋™ ํ™˜๋ถˆ ๋งˆ๊ฐ ์‹œ๊ฐ (#533)
manual_refund_available: boolean; // ์—ด๋žŒ ํ›„ + 3๊ฐœ์›” ์ด๋‚ด โ†’ ํ™˜๋ถˆ ์‹ ์ฒญ ๋ฒ„ํŠผ ํ™œ์„ฑํ™” (#533)
title: string;
description: string;
models: string[];
Expand Down
4 changes: 3 additions & 1 deletion src/prompts/repositories/prompt.download.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ export const PromptDownloadRepository = {
return prisma.purchase.findMany({
where: { user_id: userId },
include: {
refund: { select: { refund_id: true } },
// ํ™˜๋ถˆ ๊ฐ€๋Šฅ ์—ฌ๋ถ€ ํŒ์ •์— ํ•„์š”ํ•œ ํ•„๋“œ (#533) โ€” ํŒ์ • ์ž์ฒด๋Š” refund-policy๊ฐ€ ๋‹ด๋‹น
refund: { select: { refund_id: true, status: true } },
payment: { select: { status: true } },
prompt: {
select: {
prompt_id: true,
Expand Down
18 changes: 17 additions & 1 deletion src/prompts/routes/prompt.download.route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,23 @@ router.get('/:promptId/downloads', authenticateJwt, PromptDownloadController.get
* properties:
* prompt_id: { type: integer }
* purchase_id: { type: integer, description: ํ™˜๋ถˆ API ํ˜ธ์ถœ์— ํ•„์š”ํ•œ ๊ตฌ๋งค ID }
* is_refunded: { type: boolean, description: ํ™˜๋ถˆ ์™„๋ฃŒ ์—ฌ๋ถ€ }
* is_refunded: { type: boolean, description: 'ํ™˜๋ถˆ ํ™•์ • ์—ฌ๋ถ€ (APPROVED/COMPLETED)' }
* refund_status:
* type: string
* nullable: true
* enum: [REQUESTED, APPROVED, REJECTED, COMPLETED]
* description: ํ™˜๋ถˆ ์ง„ํ–‰ ์ƒํƒœ (ํ™˜๋ถˆ ์ด๋ ฅ ์—†์œผ๋ฉด null)
* refundable:
* type: boolean
* description: '์ฆ‰์‹œ ํ™˜๋ถˆ ๋ฒ„ํŠผ ํ™œ์„ฑํ™” ์—ฌ๋ถ€ โ€” ๋ฏธ์—ด๋žŒ + ๊ตฌ๋งค ํ›„ 7์ผ ์ด๋‚ด(KST ๋‚ ์งœ ๊ธฐ์ค€)'
* refund_deadline:
* type: string
* format: date-time
* nullable: true
* description: ์ฆ‰์‹œ ํ™˜๋ถˆ ๋งˆ๊ฐ ์‹œ๊ฐ
* manual_refund_available:
* type: boolean
* description: 'ํ™˜๋ถˆ ์‹ ์ฒญ ๋ฒ„ํŠผ ํ™œ์„ฑํ™” ์—ฌ๋ถ€ โ€” ์—ด๋žŒํ•จ + ๊ตฌ๋งค ํ›„ 3๊ฐœ์›” ์ด๋‚ด'
* title: { type: string }
* description: { type: string, nullable: true }
* price: { type: integer }
Expand Down
22 changes: 19 additions & 3 deletions src/prompts/services/prompt.download.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ import { PromptDownloadRepository } from '../repositories/prompt.download.reposi
import { PromptDownloadResponseDTO, DownloadedPromptResponseDTO } from '../dtos/prompt.download.dto';
import { AppError } from '../../errors/AppError';
import prisma from "../../config/prisma";
import {
checkAutoRefund,
checkManualRefund,
isRefundSettled,
toPolicyInput,
} from '../../refunds/utils/refund-policy';

export const PromptDownloadService = {
async getPromptContent(userId: number, promptId: number): Promise<PromptDownloadResponseDTO> {
Expand All @@ -22,11 +28,12 @@ async getPromptContent(userId: number, promptId: number): Promise<PromptDownload
},
include: {
payment: true,
refund: { select: { refund_id: true } },
refund: { select: { refund_id: true, status: true } },
},
});

if (purchase?.refund) {
// ์‹ ์ฒญ(REQUESTED) ๋‹จ๊ณ„๋Š” ์•„์ง ๊ฒ€ํ†  ์ค‘์ด๋ฏ€๋กœ ์—ด๋žŒ์„ ๋ง‰์ง€ ์•Š๋Š”๋‹ค. ํ™•์ •๋œ ๊ฑด๋งŒ ์ฐจ๋‹จ. (#533)
if (isRefundSettled(purchase?.refund?.status)) {
throw new AppError('ํ™˜๋ถˆ๋œ ํ”„๋กฌํ”„ํŠธ๋Š” ๋‹ค์‹œ ๋‹ค์šด๋กœ๋“œํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.', 403, 'Refunded');
}

Expand Down Expand Up @@ -97,6 +104,11 @@ async getDownloadedPrompts(userId: number): Promise<DownloadedPromptResponseDTO[

return downloads.map((purchase) => {
const { prompt } = purchase;
// ํ™˜๋ถˆ ๋ฒ„ํŠผ ํ™œ์„ฑํ™” ํŒ์ • โ€” ๋‹จ๊ฑด ํ™˜๋ถˆ API์™€ ๋™์ผํ•œ ์ •์ฑ… ํ•จ์ˆ˜๋ฅผ ์“ด๋‹ค (#533)
const policyInput = toPolicyInput(purchase);
const autoVerdict = checkAutoRefund(policyInput, userId);
const manualVerdict = checkManualRefund(policyInput, userId);

const userReviewRaw = prompt.reviews[0];
const hasReview = !!userReviewRaw;
const isRecentReview = hasReview && new Date(userReviewRaw.created_at) >= THIRTY_DAYS_AGO;
Expand All @@ -115,7 +127,11 @@ async getDownloadedPrompts(userId: number): Promise<DownloadedPromptResponseDTO[

prompt_id: prompt.prompt_id,
purchase_id: purchase.purchase_id,
is_refunded: !!purchase.refund,
is_refunded: isRefundSettled(purchase.refund?.status),
refund_status: purchase.refund?.status ?? null,
refundable: autoVerdict.eligible,
refund_deadline: autoVerdict.refund_deadline,
manual_refund_available: manualVerdict.eligible,
title: prompt.title,
description: prompt.description,
price: prompt.price,
Expand Down
2 changes: 2 additions & 0 deletions src/purchases/dtos/purchase.request.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ export type PaypleClientPayType = 'card' | 'transfer';
export interface PurchaseRequestDTO {
prompt_id: number;
pay_type?: PaypleClientPayType;
// "๋””์ง€ํ„ธ์ฝ˜ํ…์ธ  ํŠน์„ฑ์ƒ ์—ด๋žŒ(์ œ๊ณต ๊ฐœ์‹œ) ํ›„์—๋Š” ๋‹จ์ˆœ ๋ณ€์‹ฌ ํ™˜๋ถˆ์ด ๋ถˆ๊ฐ€ํ•ฉ๋‹ˆ๋‹ค" ๋™์˜ (#533)
refund_policy_agreed: boolean;
}

export interface PurchaseRequestResponseDTO {
Expand Down
1 change: 1 addition & 0 deletions src/purchases/repositories/purchase.complete.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const PurchaseCompleteRepository = {
prompt_id: number;
amount: number;
is_free: false;
refund_policy_agreed_at?: Date | null;
}) {
return tx.purchase.create({ data });
},
Expand Down
9 changes: 9 additions & 0 deletions src/purchases/routes/purchase.route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const router = Router();
* type: object
* required:
* - prompt_id
* - refund_policy_agreed
* properties:
* prompt_id:
* type: integer
Expand All @@ -39,6 +40,14 @@ const router = Router();
* enum: [card, transfer]
* default: card
* description: ๊ฒฐ์ œ ์ˆ˜๋‹จ (card=์นด๋“œ, transfer=๊ณ„์ขŒ์ด์ฒด)
* refund_policy_agreed:
* type: boolean
* example: true
* description: |
* ํ™˜๋ถˆ์ •์ฑ… ๋™์˜ ์ฒดํฌ๋ฐ•์Šค. ๊ฒฐ์ œ ํ™”๋ฉด์— ์•„๋ž˜ ๋ฌธ๊ตฌ์™€ ํ•จ๊ป˜ ๋…ธ์ถœํ•ด์•ผ ํ•˜๋ฉฐ,
* true๊ฐ€ ์•„๋‹ˆ๋ฉด 400 `RefundPolicyNotAgreed`๋กœ ์ฃผ๋ฌธ์„œ๊ฐ€ ์ƒ์„ฑ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
*
* "๋””์ง€ํ„ธ์ฝ˜ํ…์ธ  ํŠน์„ฑ์ƒ ์—ด๋žŒ(์ œ๊ณต ๊ฐœ์‹œ) ํ›„์—๋Š” ๋‹จ์ˆœ ๋ณ€์‹ฌ ํ™˜๋ถˆ์ด ๋ถˆ๊ฐ€ํ•ฉ๋‹ˆ๋‹ค"
* responses:
* 200:
* description: ์ฃผ๋ฌธ์„œ ์ƒ์„ฑ ์„ฑ๊ณต (ํŽ˜์ดํ”Œ ์ผ๋ฐ˜๊ฒฐ์ œ ์—ฐ๋™ ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜)
Expand Down
4 changes: 3 additions & 1 deletion src/purchases/services/purchase.complete.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { PurchaseRequestRepository } from '../repositories/purchase.request.repo
import { PurchaseCompleteRepository } from '../repositories/purchase.complete.repository';
import { AppError } from '../../errors/AppError';
import prisma from '../../config/prisma';
import { verifyPayplePayment } from '../utils/payple';
import { parseAgreedAt, verifyPayplePayment } from '../utils/payple';
import { calculateSettlementFee } from '../utils/fee';

export const PurchaseCompleteService = {
Expand Down Expand Up @@ -32,6 +32,8 @@ export const PurchaseCompleteService = {
prompt_id: prompt.prompt_id,
amount: serverPrice,
is_free: false,
// ์ฃผ๋ฌธ์„œ ์ƒ์„ฑ ์‹œ ๊ฒ€์ฆ๋œ ํ™˜๋ถˆ์ •์ฑ… ๋™์˜ ์‹œ๊ฐ (#533)
refund_policy_agreed_at: parseAgreedAt(verifiedPayment.customData?.agreed_at),
});

const payment = await PurchaseCompleteRepository.createPaymentTx(tx, {
Expand Down
11 changes: 11 additions & 0 deletions src/purchases/services/purchase.request.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ import { requestPaypleAuth } from '../utils/payple';

export const PurchaseRequestService = {
async createPurchaseRequest(userId: number, dto: PurchaseRequestDTO): Promise<PurchaseRequestResponseDTO> {
// ํ™˜๋ถˆ์ •์ฑ… ๋™์˜ ์—†์ด๋Š” ์ฃผ๋ฌธ์„œ ์ž์ฒด๋ฅผ ๋งŒ๋“ค์ง€ ์•Š๋Š”๋‹ค (#533)
if (dto.refund_policy_agreed !== true) {
throw new AppError(
'๋””์ง€ํ„ธ์ฝ˜ํ…์ธ  ํŠน์„ฑ์ƒ ์—ด๋žŒ(์ œ๊ณต ๊ฐœ์‹œ) ํ›„์—๋Š” ๋‹จ์ˆœ ๋ณ€์‹ฌ ํ™˜๋ถˆ์ด ๋ถˆ๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. ๋™์˜๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.',
400,
'RefundPolicyNotAgreed',
);
}

const prompt = await PurchaseRequestRepository.findPromptWithSeller(dto.prompt_id);
if (!prompt) throw new AppError('ํ”„๋กฌํ”„ํŠธ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.', 404, 'NotFound');

Expand Down Expand Up @@ -35,6 +44,8 @@ export const PurchaseRequestService = {
PCD_USER_DEFINE1: JSON.stringify({
prompt_id: dto.prompt_id,
user_id: userId,
// ๋™์˜ ์‹œ๊ฐ์€ ๊ฒฐ์ œ ์™„๋ฃŒ ์‹œ Purchase์— ๊ธฐ๋ก๋œ๋‹ค (#533)
agreed_at: new Date().toISOString(),
}),
PCD_RST_URL: process.env.PAYPLE_RST_URL || '',
};
Expand Down
5 changes: 4 additions & 1 deletion src/purchases/services/purchase.webhook.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { PurchaseRequestRepository } from '../repositories/purchase.request.repository';
import { PurchaseCompleteRepository } from '../repositories/purchase.complete.repository';
import prisma from '../../config/prisma';
import { PayplePaymentResult, verifyPayplePayment } from '../utils/payple';
import { PayplePaymentResult, parseAgreedAt, verifyPayplePayment } from '../utils/payple';
import { calculateSettlementFee } from '../utils/fee';

export const WebhookService = {
Expand Down Expand Up @@ -44,6 +44,9 @@ export const WebhookService = {
prompt_id: prompt.prompt_id,
amount: serverPrice,
is_free: false,
// ์›นํ›…์ด /complete๋ณด๋‹ค ๋จผ์ € ๋„์ฐฉํ•˜๋ฉด ์—ฌ๊ธฐ์„œ Purchase๊ฐ€ ๋งŒ๋“ค์–ด์ง€๋ฏ€๋กœ
// ๋™์˜ ์‹œ๊ฐ๋„ ๊ฐ™์ด ๊ธฐ๋กํ•ด์•ผ ์œ ์‹ค๋˜์ง€ ์•Š๋Š”๋‹ค (#533)
refund_policy_agreed_at: parseAgreedAt(verified.customData?.agreed_at),
});

const payment = await PurchaseCompleteRepository.createPaymentTx(tx, {
Expand Down
11 changes: 10 additions & 1 deletion src/purchases/utils/payple.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ export type PaypleVerifiedPayment = {
bankName?: string | null;
bankNum?: string | null;
cashReceiptUrl?: string | null;
customData: { prompt_id?: number; user_id?: number };
customData: { prompt_id?: number; user_id?: number; agreed_at?: string };
};

function parseCustomDefine(define?: string): any {
Expand All @@ -126,6 +126,15 @@ function parseCustomDefine(define?: string): any {
}
}

// ์ฃผ๋ฌธ์„œ(PCD_USER_DEFINE1)์— ์‹ค์–ด ๋ณด๋‚ธ ํ™˜๋ถˆ์ •์ฑ… ๋™์˜ ์‹œ๊ฐ.
// ๊ฒฐ์ œ ์™„๋ฃŒ ๊ฒฝ๋กœ๊ฐ€ ํด๋ผ์ด์–ธํŠธ /complete์™€ ์›นํ›… ๋‘ ๊ฐˆ๋ž˜๋ผ ์–‘์ชฝ์ด ๊ฐ™์€ ํŒŒ์„œ๋ฅผ ์“ด๋‹ค. (#533)
// ๊ฐ’์ด ์†์ƒ๋์œผ๋ฉด ๊ธฐ๋ก๋งŒ ์ƒ๋žตํ•˜๊ณ  ๊ฒฐ์ œ ์ž์ฒด๋Š” ์ง„ํ–‰ํ•œ๋‹ค.
export function parseAgreedAt(raw?: string): Date | null {
if (!raw) return null;
const parsed = new Date(raw);
return Number.isNaN(parsed.getTime()) ? null : parsed;
}

function parsePaypleTime(t?: string): Date {
if (!t) return new Date();
const m = t.match(/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/);
Expand Down
Loading
Loading