@sayren/store-sdk 0.16.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +3239 -250
- package/dist/index.mjs +1615 -184
- package/package.json +1 -1
- package/src/client.ts +194 -5
- package/src/index.ts +5 -0
- package/src/schemas/claim.ts +24 -2
- package/src/schemas/coupon.ts +312 -0
- package/src/schemas/digital-asset.ts +65 -0
- package/src/schemas/fulfillment-code.ts +143 -0
- package/src/schemas/fulfillment.ts +576 -0
- package/src/schemas/misc.ts +5 -1
- package/src/schemas/order.ts +196 -6
- package/src/schemas/product.ts +107 -114
- package/src/schemas/promotion.ts +806 -0
- package/src/schemas/refund-policy.ts +39 -27
- package/src/schemas/scope.ts +4 -0
- package/src/schemas/shipping-policy.ts +28 -4
- package/src/schemas/store.ts +32 -1
- package/src/schemas/upload.ts +48 -2
- package/src/schemas/webhook-event.ts +104 -1
|
@@ -16,17 +16,18 @@ const descriptions = {
|
|
|
16
16
|
"발송 전 취소로 배송 묶음(같은 상품의 주문상품)이 모두 취소되면 그 묶음의 배송비를 환불한다. " +
|
|
17
17
|
"false면 배송비를 환불하지 않는다. 기본 true",
|
|
18
18
|
returnDeliveryFee:
|
|
19
|
-
"단순 변심(구매자 귀책) 반품의 반품 배송비. 원 단위. 환불액에서 뺀다. 기본 0(무료). "
|
|
20
|
-
"상품별 정책에서 이 값을 주면 상품의 `returnFee`도 같은 값으로 맞춘다",
|
|
19
|
+
"단순 변심(구매자 귀책) 반품의 반품 배송비. 원 단위. 환불액에서 뺀다. 기본 0(무료). 배송 상품에만 있다",
|
|
21
20
|
exchangeDeliveryFee:
|
|
22
|
-
"단순 변심(구매자 귀책) 교환의 교환 배송비. 원 단위. 구매자에게 청구할 금액을 안내값으로 싣는다. 기본 0(무료). "
|
|
23
|
-
"상품별 정책에서 이 값을 주면 상품의 `exchangeFee`도 같은 값으로 맞춘다",
|
|
21
|
+
"단순 변심(구매자 귀책) 교환의 교환 배송비. 원 단위. 구매자에게 청구할 금액을 안내값으로 싣는다. 기본 0(무료). 배송 상품에만 있다",
|
|
24
22
|
sellerFaultFreeShipping:
|
|
25
23
|
"판매자 귀책(셀러 직권취소, 불량·오배송 반품)이면 배송비를 셀러가 부담한다 — 배송비를 환불하고 반품비를 받지 않는다. " +
|
|
26
24
|
"false면 구매자 귀책과 같은 규칙을 쓴다. 기본 true",
|
|
27
25
|
conditionalFreeShortfallCharge:
|
|
28
26
|
"조건부 무료배송(`CONDITIONAL_FREE`) 상품에서 일부 취소로 남은 금액이 무료 기준액에 미달하면 그 배송비를 환불액에서 뺀다. " +
|
|
29
27
|
"false면 추가 청구 없이 셀러가 부담한다. 기본 false",
|
|
28
|
+
couponShortfallCharge:
|
|
29
|
+
"쿠폰 조건 미달 차감. 일부 취소·반품으로 남은 주문상품이 쿠폰 조건(최소 주문 금액 등)을 잃으면 남은 주문상품이 받던 쿠폰 할인을 환불액에서 뺀다. " +
|
|
30
|
+
"판매자 귀책(셀러 직권취소, 불량·오배송 반품)은 빼지 않는다. false면 차감 없이 실결제 금액만 나눠 환불한다. 기본 true",
|
|
30
31
|
} as const;
|
|
31
32
|
|
|
32
33
|
/** 유효 환불 정책 — 스토어 기본 + 상품별 덮어쓰기를 합친 결과 */
|
|
@@ -36,6 +37,13 @@ export const refundPolicySchema = z.object({
|
|
|
36
37
|
exchangeDeliveryFee: z.number().int().min(0).describe(descriptions.exchangeDeliveryFee),
|
|
37
38
|
sellerFaultFreeShipping: z.boolean().describe(descriptions.sellerFaultFreeShipping),
|
|
38
39
|
conditionalFreeShortfallCharge: z.boolean().describe(descriptions.conditionalFreeShortfallCharge),
|
|
40
|
+
couponShortfallCharge: z
|
|
41
|
+
.boolean()
|
|
42
|
+
.optional()
|
|
43
|
+
.default(true)
|
|
44
|
+
.describe(
|
|
45
|
+
`${descriptions.couponShortfallCharge}. 이 필드가 없는 이전 서버 응답은 true로 읽는다`,
|
|
46
|
+
),
|
|
39
47
|
});
|
|
40
48
|
|
|
41
49
|
export type RefundPolicy = z.infer<typeof refundPolicySchema>;
|
|
@@ -52,6 +60,7 @@ export const REFUND_POLICY_DEFAULTS: RefundPolicy = {
|
|
|
52
60
|
exchangeDeliveryFee: 0,
|
|
53
61
|
sellerFaultFreeShipping: true,
|
|
54
62
|
conditionalFreeShortfallCharge: false,
|
|
63
|
+
couponShortfallCharge: true,
|
|
55
64
|
};
|
|
56
65
|
|
|
57
66
|
/**
|
|
@@ -77,27 +86,30 @@ export const refundPolicyPatchSchema = z.strictObject({
|
|
|
77
86
|
.boolean()
|
|
78
87
|
.optional()
|
|
79
88
|
.describe(descriptions.conditionalFreeShortfallCharge),
|
|
89
|
+
couponShortfallCharge: z.boolean().optional().describe(descriptions.couponShortfallCharge),
|
|
80
90
|
});
|
|
81
91
|
|
|
82
92
|
export type RefundPolicyPatch = z.infer<typeof refundPolicyPatchSchema>;
|
|
83
93
|
|
|
84
|
-
/**
|
|
94
|
+
/**
|
|
95
|
+
* 상품별 덮어쓰기 — 쿠폰 조건 미달 차감(`couponShortfallCharge`)은 주문 단위 규칙이라 상품이 정하지 않는다.
|
|
96
|
+
* 스토어 정책만 읽는다(이슈 #47).
|
|
97
|
+
*/
|
|
98
|
+
export const productRefundPolicyPatchSchema = refundPolicyPatchSchema.omit({
|
|
99
|
+
couponShortfallCharge: true,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
/** 정책 해석에 쓰는 상품 값 — 상품별 덮어쓰기 */
|
|
85
103
|
export interface RefundPolicyProductInput {
|
|
86
104
|
/** 상품별 덮어쓰기. 없으면 스토어 기본을 그대로 쓴다 */
|
|
87
105
|
refundPolicy?: RefundPolicyPatch | null;
|
|
88
|
-
/** @deprecated 레거시 상품 반품비 — `refundPolicy`가 없는 옛 상품의 값이다 */
|
|
89
|
-
returnFee?: number | null;
|
|
90
|
-
/** @deprecated 레거시 상품 교환비 — `refundPolicy`가 없는 옛 상품의 값이다 */
|
|
91
|
-
exchangeFee?: number | null;
|
|
92
106
|
}
|
|
93
107
|
|
|
94
108
|
/**
|
|
95
109
|
* 유효 정책 해석 — 상품별 덮어쓰기 → 스토어 기본 → `REFUND_POLICY_DEFAULTS` 순이다.
|
|
96
110
|
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
* 정하지 않았으면 레거시 값이 0보다 클 때 그 값을 쓴다. 0이면 스토어 기본을 따른다 —
|
|
100
|
-
* 정책이 생기기 전에 만든 상품(반품비 0)이 스토어 기본을 물려받게 하는 규칙이다.
|
|
111
|
+
* 반품·교환 배송비의 원천은 정책 하나다(이슈 #57 결정 3). 상품의 레거시 반품비·교환비 필드와의 연결은 없앴고,
|
|
112
|
+
* 레거시 값은 마이그레이션이 상품별 정책으로 옮겼다.
|
|
101
113
|
*/
|
|
102
114
|
export function resolveRefundPolicy(
|
|
103
115
|
storePolicy?: RefundPolicyPatch | null,
|
|
@@ -108,28 +120,27 @@ export function resolveRefundPolicy(
|
|
|
108
120
|
}
|
|
109
121
|
|
|
110
122
|
/**
|
|
111
|
-
* 상품의 **덮어쓰기 자체** —
|
|
123
|
+
* 상품의 **덮어쓰기 자체** — 덮어쓰기가 없으면 null.
|
|
112
124
|
*
|
|
113
125
|
* `resolveRefundPolicy`가 합친 결과로는 "상품이 정한 값"과 "스토어 기본을 물려받은 값"을 가를 수 없다.
|
|
114
126
|
* 관리 API 응답의 `refundPolicy`와 콘솔 폼의 덮어쓰기 스위치가 이 함수를 원천으로 쓴다 — 가르지 못하면
|
|
115
|
-
* 스토어 기본을 쓰는 상품을 저장할 때 그 순간의 기본값이 상품에
|
|
127
|
+
* 스토어 기본을 쓰는 상품을 저장할 때 그 순간의 기본값이 상품에 박힌다.
|
|
116
128
|
*/
|
|
117
129
|
export function productRefundPolicyOverride(
|
|
118
130
|
product?: RefundPolicyProductInput | null,
|
|
119
131
|
): RefundPolicyPatch | null {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
if (override.exchangeDeliveryFee === undefined && (product?.exchangeFee ?? 0) > 0) {
|
|
126
|
-
legacy.exchangeDeliveryFee = product?.exchangeFee ?? 0;
|
|
127
|
-
}
|
|
128
|
-
const merged = { ...legacy, ...override };
|
|
129
|
-
if (product?.refundPolicy == null && Object.keys(merged).length === 0) return null;
|
|
130
|
-
return merged;
|
|
132
|
+
if (product?.refundPolicy == null) return null;
|
|
133
|
+
// 쿠폰 조건 미달 차감은 주문 단위라 스토어만 정한다 — 상품 행에 키가 있어도 덮어쓰기로 읽지 않는다(이슈 #47)
|
|
134
|
+
const { couponShortfallCharge: _storeOnly, ...override } = dropUndefined(product.refundPolicy);
|
|
135
|
+
return override;
|
|
131
136
|
}
|
|
132
137
|
|
|
138
|
+
/** 반품·교환 배송비 키 — 배송이 없는 상품의 정책에는 두지 않는다 */
|
|
139
|
+
export const REFUND_POLICY_DELIVERY_FEE_KEYS = [
|
|
140
|
+
"returnDeliveryFee",
|
|
141
|
+
"exchangeDeliveryFee",
|
|
142
|
+
] as const;
|
|
143
|
+
|
|
133
144
|
/** `undefined` 값을 빼고 얕게 복사한다 — 전개 순서로 덮어쓸 때 `undefined`가 기본값을 지우지 않게 */
|
|
134
145
|
function dropUndefined(patch?: RefundPolicyPatch | null): RefundPolicyPatch {
|
|
135
146
|
if (!patch) return {};
|
|
@@ -143,7 +154,8 @@ function dropUndefined(patch?: RefundPolicyPatch | null): RefundPolicyPatch {
|
|
|
143
154
|
*
|
|
144
155
|
* 원 단위 반올림 규칙: `floor(주문상품 금액 × 누적 수량 ÷ 전체 수량)`을 두 번 구해 그 차를 쓴다.
|
|
145
156
|
* 부분 취소를 몇 번 나눠도 합이 주문상품 금액과 정확히 같아서 1원이 남거나 넘치지 않는다.
|
|
146
|
-
*
|
|
157
|
+
* 즉시할인은 이미 단가에 들어 있다. 쿠폰 할인은 단가에 들어가지 않으므로 환불은 실결제 금액을 나누는
|
|
158
|
+
* `proratedPaidRefundBase`(promotion.ts)를 쓴다(이슈 #47).
|
|
147
159
|
*
|
|
148
160
|
* @param totalPrice 주문상품 금액 (할인 반영 단가 × 전체 수량)
|
|
149
161
|
* @param quantity 주문상품 전체 수량
|
package/src/schemas/scope.ts
CHANGED
|
@@ -29,6 +29,7 @@ export const STORE_SCOPE_RESOURCES = {
|
|
|
29
29
|
member: ["r", "rw"],
|
|
30
30
|
customer: ["r", "rw"],
|
|
31
31
|
audit: ["r"],
|
|
32
|
+
promotion: ["r", "rw"],
|
|
32
33
|
} as const satisfies Record<string, readonly ScopeAccess[]>;
|
|
33
34
|
|
|
34
35
|
export type StoreScopeResource = keyof typeof STORE_SCOPE_RESOURCES;
|
|
@@ -65,6 +66,7 @@ export const STORE_ROLE_SCOPES = {
|
|
|
65
66
|
"member:rw",
|
|
66
67
|
"customer:rw",
|
|
67
68
|
"audit:r",
|
|
69
|
+
"promotion:rw",
|
|
68
70
|
],
|
|
69
71
|
ADMIN: [
|
|
70
72
|
"product:rw",
|
|
@@ -79,6 +81,7 @@ export const STORE_ROLE_SCOPES = {
|
|
|
79
81
|
"member:rw",
|
|
80
82
|
"customer:rw",
|
|
81
83
|
"audit:r",
|
|
84
|
+
"promotion:rw",
|
|
82
85
|
],
|
|
83
86
|
STAFF: [
|
|
84
87
|
"product:rw",
|
|
@@ -88,5 +91,6 @@ export const STORE_ROLE_SCOPES = {
|
|
|
88
91
|
"inquiry:rw",
|
|
89
92
|
"review:rw",
|
|
90
93
|
"customer:r",
|
|
94
|
+
"promotion:r",
|
|
91
95
|
],
|
|
92
96
|
} as const satisfies Record<StoreRole, readonly string[]>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import type
|
|
2
|
+
import { type DeliveryType, type FulfillmentType, requiresShipping } from "./fulfillment";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* 배송비 규칙 (이슈 #29) — 배송비를 **몇 번, 얼마나** 부과하는지를 정한다.
|
|
@@ -19,6 +19,9 @@ import type { DeliveryType } from "./product";
|
|
|
19
19
|
*
|
|
20
20
|
* 계산은 `quoteDelivery` 하나다. 장바구니·주문서·결제 시작·환불 정산이 모두 이 함수를 쓴다 —
|
|
21
21
|
* 네 곳이 각자 계산하면 화면 금액과 결제 금액이 갈라진다.
|
|
22
|
+
*
|
|
23
|
+
* **비실물 줄은 배송 계산에서 빠진다**(이슈 #48, `requiresShipping`). 묶음을 만들지 않고, 스토어 전체 무료 기준의
|
|
24
|
+
* 판정 금액(`shippableAmount`)에도 넣지 않는다 — 디지털 상품 금액이 실물 배송비를 면제시키지 않게 한다.
|
|
22
25
|
*/
|
|
23
26
|
|
|
24
27
|
const descriptions = {
|
|
@@ -219,6 +222,11 @@ export interface DeliveryBundleProduct {
|
|
|
219
222
|
conditionalFreeAmount: number | null;
|
|
220
223
|
/** 출고지 주소 id(`seller_addresses`). null이면 스토어 기본 출고지를 쓰는 상품끼리 한 묶음이다 */
|
|
221
224
|
shippingAddressId?: string | null;
|
|
225
|
+
/**
|
|
226
|
+
* 상품 유형 (이슈 #48). 생략·null이면 배송 상품이다. 배송이 필요 없는 유형은 묶음을 만들지 않는다.
|
|
227
|
+
* 줄(`DeliveryFeeLine.fulfillmentType`)에 값이 있으면 그 값이 이긴다.
|
|
228
|
+
*/
|
|
229
|
+
fulfillmentType?: FulfillmentType | null;
|
|
222
230
|
}
|
|
223
231
|
|
|
224
232
|
/**
|
|
@@ -246,6 +254,11 @@ export interface DeliveryFeeLine {
|
|
|
246
254
|
productId: string;
|
|
247
255
|
/** 상품 금액 (할인 반영 단가 × 수량) */
|
|
248
256
|
amount: number;
|
|
257
|
+
/**
|
|
258
|
+
* 이 줄의 상품 유형 — 주문서 스냅샷 값이다(이슈 #48). 주면 상품의 현재 유형보다 우선한다. 주문서를 만든 뒤 셀러가
|
|
259
|
+
* 유형을 바꿔도 결제 시작의 견적과 주문상품 스냅샷이 같은 유형을 보게 한다. 생략하면 상품 값을 쓴다.
|
|
260
|
+
*/
|
|
261
|
+
fulfillmentType?: FulfillmentType | null;
|
|
249
262
|
}
|
|
250
263
|
|
|
251
264
|
/**
|
|
@@ -290,6 +303,13 @@ export interface DeliveryQuote {
|
|
|
290
303
|
remoteArea: RemoteArea;
|
|
291
304
|
/** 스토어 전체 무료 기준(`freeThreshold`) 충족으로 기본 배송비를 면제했는가 */
|
|
292
305
|
freeByThreshold: boolean;
|
|
306
|
+
/**
|
|
307
|
+
* 배송이 필요한 줄의 상품 금액 합 — 스토어 전체 무료 기준의 판정 금액이다(이슈 #48). 비실물 줄은 빠진다.
|
|
308
|
+
* 주문 상품 금액 합(`productAmount`)과 다를 수 있다.
|
|
309
|
+
*/
|
|
310
|
+
shippableAmount: number;
|
|
311
|
+
/** 배송이 필요한 줄이 하나라도 있는가 — 배송 묶음이 하나라도 있는가다. false면 배송지를 받지 않는다 */
|
|
312
|
+
requiresShipping: boolean;
|
|
293
313
|
}
|
|
294
314
|
|
|
295
315
|
export interface DeliveryQuoteInput {
|
|
@@ -319,11 +339,13 @@ export function quoteDelivery(input: DeliveryQuoteInput): DeliveryQuote {
|
|
|
319
339
|
string,
|
|
320
340
|
{ product: DeliveryBundleProduct; ids: Set<string>; sum: number }
|
|
321
341
|
>();
|
|
322
|
-
let
|
|
342
|
+
let shippableAmount = 0;
|
|
323
343
|
for (const line of input.lines) {
|
|
324
344
|
const product = input.productOf(line.productId);
|
|
325
345
|
if (!product) continue;
|
|
326
|
-
|
|
346
|
+
// 비실물은 묶음도 무료 기준 판정 금액도 만들지 않는다 — 디지털 금액이 실물 배송비를 면제시키지 않게 한다
|
|
347
|
+
if (!requiresShipping(line.fulfillmentType ?? product.fulfillmentType)) continue;
|
|
348
|
+
shippableAmount += line.amount;
|
|
327
349
|
const key = deliveryBundleKeyOf(product);
|
|
328
350
|
const group = groups.get(key) ?? { product, ids: new Set<string>(), sum: 0 };
|
|
329
351
|
group.ids.add(line.productId);
|
|
@@ -332,7 +354,7 @@ export function quoteDelivery(input: DeliveryQuoteInput): DeliveryQuote {
|
|
|
332
354
|
}
|
|
333
355
|
|
|
334
356
|
const freeByThreshold =
|
|
335
|
-
policy.freeThreshold !== null &&
|
|
357
|
+
policy.freeThreshold !== null && shippableAmount >= policy.freeThreshold && groups.size > 0;
|
|
336
358
|
|
|
337
359
|
const bundles: DeliveryBundleQuote[] = [];
|
|
338
360
|
for (const [bundleKey, group] of groups) {
|
|
@@ -371,5 +393,7 @@ export function quoteDelivery(input: DeliveryQuoteInput): DeliveryQuote {
|
|
|
371
393
|
deliveryFee: baseFee + remoteSurcharge,
|
|
372
394
|
remoteArea: area,
|
|
373
395
|
freeByThreshold,
|
|
396
|
+
shippableAmount,
|
|
397
|
+
requiresShipping: bundles.length > 0,
|
|
374
398
|
};
|
|
375
399
|
}
|
package/src/schemas/store.ts
CHANGED
|
@@ -166,6 +166,22 @@ export const updateStoreRequestSchema = z
|
|
|
166
166
|
"비회원 주문 허용. false(회원만)면 로그인하지 않은 구매자의 주문서 생성·결제 요청이 403 `GUEST_CHECKOUT_DISABLED`다. " +
|
|
167
167
|
"장바구니 담기와 이미 받은 비회원 주문의 조회·취소는 막지 않는다",
|
|
168
168
|
),
|
|
169
|
+
fulfillmentMailEnabled: z
|
|
170
|
+
.boolean()
|
|
171
|
+
.optional()
|
|
172
|
+
.describe(
|
|
173
|
+
"비실물 상품 제공 안내 메일(이슈 #48). 켜면(기본) 제공이 끝났을 때 구매자에게 안내 메일을 보낸다. 코드·파일 주소는 싣지 않는다",
|
|
174
|
+
),
|
|
175
|
+
withdrawalNotice: z
|
|
176
|
+
.string()
|
|
177
|
+
.trim()
|
|
178
|
+
.min(1)
|
|
179
|
+
.max(1000)
|
|
180
|
+
.nullable()
|
|
181
|
+
.optional()
|
|
182
|
+
.describe(
|
|
183
|
+
"청약철회 제한 고지 문구(이슈 #48). 제한 상품이 있는 주문서에 보인다. null이면 플랫폼 기본 문구를 쓴다",
|
|
184
|
+
),
|
|
169
185
|
autoDeliverDays: z
|
|
170
186
|
.number()
|
|
171
187
|
.int()
|
|
@@ -231,6 +247,17 @@ export const storeSchema = z.object({
|
|
|
231
247
|
"스토어프론트 도메인 — 소셜 로그인 리디렉트와 방문 분석 수집이 함께 쓴다(`analyticsAllowedOrigins`와 같은 값)",
|
|
232
248
|
),
|
|
233
249
|
guestCheckoutEnabled: z.boolean().describe("비회원 주문 허용. false면 회원만 주문할 수 있다"),
|
|
250
|
+
fulfillmentMailEnabled: z
|
|
251
|
+
.boolean()
|
|
252
|
+
.optional()
|
|
253
|
+
.default(true)
|
|
254
|
+
.describe("비실물 상품 제공 안내 메일(이슈 #48). 서버는 항상 싣는다"),
|
|
255
|
+
withdrawalNotice: z
|
|
256
|
+
.string()
|
|
257
|
+
.nullable()
|
|
258
|
+
.optional()
|
|
259
|
+
.default(null)
|
|
260
|
+
.describe("청약철회 제한 고지 문구(이슈 #48). null이면 플랫폼 기본 문구다. 서버는 항상 싣는다"),
|
|
234
261
|
autoDeliverDays: z.number().int().describe(autoDeliverDaysDescription),
|
|
235
262
|
autoConfirmDays: z.number().int().describe(autoConfirmDaysDescription),
|
|
236
263
|
refundPolicy: refundPolicySchema.describe(
|
|
@@ -333,7 +360,11 @@ export const storeMemberRoleUpdatedSchema = z.object({
|
|
|
333
360
|
|
|
334
361
|
export type StoreMemberRoleUpdated = z.infer<typeof storeMemberRoleUpdatedSchema>;
|
|
335
362
|
|
|
336
|
-
|
|
363
|
+
/**
|
|
364
|
+
* 주소 유형 — 출고지·반품지·방문 수령지(이슈 #48). 방문 수령지(`PICKUP`)는 목록에서 `type=PICKUP`으로 조회할 때만 나온다
|
|
365
|
+
* (이 값을 모르는 옛 SDK의 목록 파싱이 깨지지 않게).
|
|
366
|
+
*/
|
|
367
|
+
export const addressTypeSchema = z.enum(["SHIPPING", "RETURN", "PICKUP"]);
|
|
337
368
|
|
|
338
369
|
export type AddressType = z.infer<typeof addressTypeSchema>;
|
|
339
370
|
|
package/src/schemas/upload.ts
CHANGED
|
@@ -5,8 +5,10 @@ import { z } from "zod";
|
|
|
5
5
|
*
|
|
6
6
|
* - `product-image`: 상품 목록·상세의 상품 이미지. 목록 썸네일로 쓰이므로 정지 이미지만 받는다.
|
|
7
7
|
* - `description`: 상세설명 본문에 삽입하는 미디어. 동영상과 svg도 받는다.
|
|
8
|
+
* - `digital-asset`: 파일 다운로드 상품의 판매 파일(이슈 #48). **비공개 객체**로 올라가고 공개 주소가 없다 — 구매자는 제공된
|
|
9
|
+
* 주문상품에서 수명 300초의 서명 주소로만 받는다.
|
|
8
10
|
*/
|
|
9
|
-
export const uploadPurposeSchema = z.enum(["product-image", "description"]);
|
|
11
|
+
export const uploadPurposeSchema = z.enum(["product-image", "description", "digital-asset"]);
|
|
10
12
|
|
|
11
13
|
export type UploadPurpose = z.infer<typeof uploadPurposeSchema>;
|
|
12
14
|
|
|
@@ -34,6 +36,37 @@ export const UPLOAD_PURPOSE_LIMITS: Record<UploadPurpose, UploadPurposeLimit> =
|
|
|
34
36
|
extensions: ["jpg", "jpeg", "png", "gif", "webp", "avif", "svg", "mp4", "webm", "mov"],
|
|
35
37
|
maxBytes: 50 * MB,
|
|
36
38
|
},
|
|
39
|
+
// 판매 파일 — 실행 파일(exe·dmg 등)은 받지 않는다. 크기 상한은 CDN 업로드 한도 안으로 잡는다
|
|
40
|
+
"digital-asset": {
|
|
41
|
+
extensions: [
|
|
42
|
+
"pdf",
|
|
43
|
+
"epub",
|
|
44
|
+
"zip",
|
|
45
|
+
"mp3",
|
|
46
|
+
"wav",
|
|
47
|
+
"flac",
|
|
48
|
+
"m4a",
|
|
49
|
+
"mp4",
|
|
50
|
+
"mov",
|
|
51
|
+
"jpg",
|
|
52
|
+
"jpeg",
|
|
53
|
+
"png",
|
|
54
|
+
"webp",
|
|
55
|
+
"psd",
|
|
56
|
+
"ai",
|
|
57
|
+
"txt",
|
|
58
|
+
"csv",
|
|
59
|
+
"xlsx",
|
|
60
|
+
"docx",
|
|
61
|
+
"pptx",
|
|
62
|
+
],
|
|
63
|
+
maxBytes: 200 * MB,
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** 비공개 업로드(판매 파일)의 PUT 필수 헤더 — CDN이 이 값으로 공개 조회를 막는다 */
|
|
68
|
+
export const PRIVATE_UPLOAD_HEADERS: Readonly<Record<string, string>> = {
|
|
69
|
+
"x-amz-meta-visibility": "private",
|
|
37
70
|
};
|
|
38
71
|
|
|
39
72
|
/** 파일명의 소문자 확장자. 확장자가 없으면 `""` */
|
|
@@ -100,10 +133,23 @@ export const uploadRequestSchema = z.object({
|
|
|
100
133
|
export type UploadRequest = z.infer<typeof uploadRequestSchema>;
|
|
101
134
|
|
|
102
135
|
export const uploadTicketSchema = z.object({
|
|
103
|
-
/** 업로드 완료 후 공개될
|
|
136
|
+
/** 업로드 완료 후 공개될 주소. 비공개 업로드(`digital-asset`)는 이 주소로 받을 수 없다 */
|
|
104
137
|
url: z.url(),
|
|
105
138
|
/** 파일 바이트를 PUT할 presigned URL. 1시간 유효 */
|
|
106
139
|
uploadUrl: z.url(),
|
|
140
|
+
key: z
|
|
141
|
+
.string()
|
|
142
|
+
.optional()
|
|
143
|
+
.describe(
|
|
144
|
+
"객체 키. 비공개 업로드(`digital-asset`)는 이 값으로 상품에 파일을 연결한다(`POST /v1/products/{productId}/digital-assets`). 공개 업로드에는 없다",
|
|
145
|
+
),
|
|
146
|
+
headers: z
|
|
147
|
+
.record(z.string(), z.string())
|
|
148
|
+
.optional()
|
|
149
|
+
.default({})
|
|
150
|
+
.describe(
|
|
151
|
+
"PUT에 반드시 실을 헤더. 비공개 업로드는 `x-amz-meta-visibility: private`이고 빠뜨리면 공개 객체가 된다. 공개 업로드는 빈 객체다",
|
|
152
|
+
),
|
|
107
153
|
});
|
|
108
154
|
|
|
109
155
|
export type UploadTicket = z.infer<typeof uploadTicketSchema>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { claimStatusSchema, claimTypeSchema } from "./claim";
|
|
3
3
|
import { orderItemStatusSchema } from "./common";
|
|
4
|
+
import { orderItemFulfillmentSnapshotResponseSchema } from "./fulfillment";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* 웹훅 발송 형식 (공개 계약). 본문은 얇은 이벤트다. 개인정보를 싣지 않으며
|
|
@@ -23,6 +24,14 @@ const envelopeFields = {
|
|
|
23
24
|
id: z.string().describe("이벤트 ID (`evt_…`, 테스트 전송은 `evt_test_…`). 중복 제거 키"),
|
|
24
25
|
createdAt: z.string().describe("이벤트 발생 일시 (ISO 8601)"),
|
|
25
26
|
storeId: z.string().describe("이벤트가 발생한 스토어 ID"),
|
|
27
|
+
sequence: z
|
|
28
|
+
.number()
|
|
29
|
+
.int()
|
|
30
|
+
.positive()
|
|
31
|
+
.optional()
|
|
32
|
+
.describe(
|
|
33
|
+
"리소스별 순번. 같은 주문·클레임·문의·리뷰·상품의 이벤트마다 1씩 커진다. 도착 순서는 보장하지 않으므로 같은 리소스의 이벤트는 이 값으로 정렬한다. 이 필드가 생기기 전의 이벤트와 테스트 전송에는 없다",
|
|
34
|
+
),
|
|
26
35
|
};
|
|
27
36
|
|
|
28
37
|
export const orderWebhookEventDataSchema = z.object({
|
|
@@ -35,9 +44,59 @@ export const orderWebhookEventDataSchema = z.object({
|
|
|
35
44
|
.describe(
|
|
36
45
|
"테스트 결제(샌드박스 결제) 주문인가. true면 실제로 돈이 오가지 않았다. 이 필드가 없는 이전 이벤트는 false로 읽는다",
|
|
37
46
|
),
|
|
47
|
+
couponDiscountAmount: z
|
|
48
|
+
.number()
|
|
49
|
+
.int()
|
|
50
|
+
.optional()
|
|
51
|
+
.default(0)
|
|
52
|
+
.describe(
|
|
53
|
+
"주문이 받은 상품·주문 쿠폰 할인 합계 (원). 즉시할인(`discountAmount`)과 별개다. " +
|
|
54
|
+
"쿠폰이 없거나 이 필드가 없는 이전 이벤트는 0으로 읽는다",
|
|
55
|
+
),
|
|
56
|
+
orderItemId: z
|
|
57
|
+
.string()
|
|
58
|
+
.optional()
|
|
59
|
+
.describe(
|
|
60
|
+
"이벤트를 만든 주문상품 ID. 비실물 상품 제공으로 보내는 `ORDER.DELIVERED`에만 실린다. " +
|
|
61
|
+
"택배 배송완료와 그 밖의 주문 이벤트에는 없다",
|
|
62
|
+
),
|
|
63
|
+
fulfillmentSnapshot: orderItemFulfillmentSnapshotResponseSchema
|
|
64
|
+
.optional()
|
|
65
|
+
.describe(
|
|
66
|
+
"이벤트를 만든 주문상품의 주문 시점 이행 스냅샷(주문상품 응답의 `fulfillmentSnapshot`과 같다, 이슈 #57). " +
|
|
67
|
+
"비실물 상품의 제공 완료는 `ORDER.DELIVERED`로도 오고 `type`이 `SHIPPING`이 아니다. `orderItemId`가 있는 이벤트에만 실린다",
|
|
68
|
+
),
|
|
38
69
|
});
|
|
39
70
|
export type OrderWebhookEventData = z.infer<typeof orderWebhookEventDataSchema>;
|
|
40
71
|
|
|
72
|
+
/**
|
|
73
|
+
* `ORDER.FULFILLED` — 비실물 주문상품(파일·코드·이용권·방문 수령)을 제공했다(이슈 #48). 자동 제공은 결제 확정과 함께,
|
|
74
|
+
* 수동 제공은 셀러의 제공 처리(`POST /v1/order-items/{orderItemId}/fulfill`)로 온다. 기존 구독자를 위해 같은 순간
|
|
75
|
+
* `ORDER.DELIVERED`도 함께 보낸다. **코드·파일 주소는 싣지 않는다** — 관리 API로 조회한다.
|
|
76
|
+
*/
|
|
77
|
+
export const orderFulfilledWebhookEventDataSchema = z.object({
|
|
78
|
+
orderId: z.string().describe("주문 ID"),
|
|
79
|
+
orderItemId: z.string().describe("제공한 주문상품 ID"),
|
|
80
|
+
status: orderItemStatusSchema.describe("이벤트 발생 시점의 주문 대표 상태"),
|
|
81
|
+
fulfillmentSnapshot: orderItemFulfillmentSnapshotResponseSchema.describe(
|
|
82
|
+
"제공한 주문상품의 주문 시점 이행 스냅샷(주문상품 응답의 `fulfillmentSnapshot`과 같다, 이슈 #57)",
|
|
83
|
+
),
|
|
84
|
+
fulfillment: z
|
|
85
|
+
.object({
|
|
86
|
+
mode: z
|
|
87
|
+
.string()
|
|
88
|
+
.describe(
|
|
89
|
+
"실제 제공 방식. `AUTO`(결제 확정과 함께)·`MANUAL`(셀러 제공 처리). 주문상품 제공 기록의 `mode`와 같다",
|
|
90
|
+
),
|
|
91
|
+
})
|
|
92
|
+
.describe("제공 기록"),
|
|
93
|
+
fulfilledAt: z.string().describe("제공 시각 (ISO 8601). 주문상품의 `deliveredAt`과 같다"),
|
|
94
|
+
testPayment: z
|
|
95
|
+
.boolean()
|
|
96
|
+
.describe("테스트 결제(샌드박스 결제) 주문인가. true면 실제로 돈이 오가지 않았다"),
|
|
97
|
+
});
|
|
98
|
+
export type OrderFulfilledWebhookEventData = z.infer<typeof orderFulfilledWebhookEventDataSchema>;
|
|
99
|
+
|
|
41
100
|
/**
|
|
42
101
|
* `ORDER.CANCELED` — 셀러 직권취소(`POST /v1/order-items/{orderItemId}/cancel`)로 주문상품이 취소됐다.
|
|
43
102
|
* 수량 단위 부분 취소도 이 이벤트다(`quantity` < 주문 수량이면 주문상품 상태가 그대로다).
|
|
@@ -64,7 +123,8 @@ export const orderCanceledWebhookEventDataSchema = z.object({
|
|
|
64
123
|
.number()
|
|
65
124
|
.int()
|
|
66
125
|
.describe(
|
|
67
|
-
"환불액 (원). 수량 비율 상품 금액 + 배송비 환불 − 조건부 무료배송 미달
|
|
126
|
+
"환불액 (원). 수량 비율 상품 실결제 금액(쿠폰 배분을 뺀 금액) + 배송비 환불 − 조건부 무료배송 미달 차감 " +
|
|
127
|
+
"+ 되돌린 쿠폰 차감(`couponChargeRefundAmount`)이다. " +
|
|
68
128
|
"결제사 환불 결과는 결제 내역(`GET /v1/payments/{paymentId}`)으로 확인한다",
|
|
69
129
|
),
|
|
70
130
|
deliveryFeeRefundAmount: z.number().int().describe("환불액에 포함된 배송비 (원)"),
|
|
@@ -72,6 +132,23 @@ export const orderCanceledWebhookEventDataSchema = z.object({
|
|
|
72
132
|
.number()
|
|
73
133
|
.int()
|
|
74
134
|
.describe("환불액에서 뺀 배송비 (원) — 조건부 무료배송 기준 미달 차감"),
|
|
135
|
+
couponChargeAmount: z
|
|
136
|
+
.number()
|
|
137
|
+
.int()
|
|
138
|
+
.optional()
|
|
139
|
+
.default(0)
|
|
140
|
+
.describe(
|
|
141
|
+
"환불액에서 뺀 쿠폰 조건 미달 차감 (원). 판매자 취소는 차감하지 않으므로 늘 0이다. 이 필드가 없는 이전 이벤트는 0으로 읽는다",
|
|
142
|
+
),
|
|
143
|
+
couponChargeRefundAmount: z
|
|
144
|
+
.number()
|
|
145
|
+
.int()
|
|
146
|
+
.optional()
|
|
147
|
+
.default(0)
|
|
148
|
+
.describe(
|
|
149
|
+
"환불액에 더한 금액 (원) — 앞선 환불에서 뺀 쿠폰 조건 미달 차감을 되돌린 몫이다. " +
|
|
150
|
+
"전량 취소면 누적 차감이 모두 되돌아간다. 이 필드가 없는 이전 이벤트는 0으로 읽는다",
|
|
151
|
+
),
|
|
75
152
|
testPayment: z
|
|
76
153
|
.boolean()
|
|
77
154
|
.describe("테스트 결제(샌드박스 결제) 주문인가. true면 실제로 돈이 오가지 않았다"),
|
|
@@ -124,6 +201,25 @@ export const claimWebhookEventDataSchema = z.object({
|
|
|
124
201
|
"환불액에서 뺀 추가 차감 (원) — 셀러가 클레임 승인에 넣은 금액이다(상품 훼손 등). " +
|
|
125
202
|
"배송비 차감과 별개이므로 `deliveryFeeChargeAmount`와 더해서 뺀다. 접수 이벤트와 이 필드가 없는 이전 이벤트는 0이다",
|
|
126
203
|
),
|
|
204
|
+
couponChargeAmount: z
|
|
205
|
+
.number()
|
|
206
|
+
.int()
|
|
207
|
+
.optional()
|
|
208
|
+
.default(0)
|
|
209
|
+
.describe(
|
|
210
|
+
"환불액에서 뺀 쿠폰 조건 미달 차감 (원) — 일부 취소·반품으로 남은 주문상품이 쿠폰 조건(최소 주문 금액 등)을 " +
|
|
211
|
+
"잃어 남은 주문상품이 받던 할인을 뺀 금액이다. 배송비 차감·추가 차감과 별개다. 접수 이벤트는 예상값이다. " +
|
|
212
|
+
"이 필드가 없는 이전 이벤트는 0으로 읽는다",
|
|
213
|
+
),
|
|
214
|
+
couponChargeRefundAmount: z
|
|
215
|
+
.number()
|
|
216
|
+
.int()
|
|
217
|
+
.optional()
|
|
218
|
+
.default(0)
|
|
219
|
+
.describe(
|
|
220
|
+
"환불액에 더한 금액 (원) — 앞선 환불에서 뺀 쿠폰 조건 미달 차감을 되돌린 몫이다. 접수 이벤트는 예상값이다. " +
|
|
221
|
+
"이 필드가 없는 이전 이벤트는 0으로 읽는다",
|
|
222
|
+
),
|
|
127
223
|
testPayment: z
|
|
128
224
|
.boolean()
|
|
129
225
|
.optional()
|
|
@@ -171,6 +267,12 @@ export const orderWebhookEventSchema = z.object({
|
|
|
171
267
|
data: orderWebhookEventDataSchema,
|
|
172
268
|
});
|
|
173
269
|
|
|
270
|
+
export const orderFulfilledWebhookEventSchema = z.object({
|
|
271
|
+
...envelopeFields,
|
|
272
|
+
type: z.literal("ORDER.FULFILLED"),
|
|
273
|
+
data: orderFulfilledWebhookEventDataSchema,
|
|
274
|
+
});
|
|
275
|
+
|
|
174
276
|
export const orderCanceledWebhookEventSchema = z.object({
|
|
175
277
|
...envelopeFields,
|
|
176
278
|
type: z.literal("ORDER.CANCELED"),
|
|
@@ -210,6 +312,7 @@ export const productOutOfStockWebhookEventSchema = z.object({
|
|
|
210
312
|
export const webhookEventSchema = z
|
|
211
313
|
.discriminatedUnion("type", [
|
|
212
314
|
orderWebhookEventSchema,
|
|
315
|
+
orderFulfilledWebhookEventSchema,
|
|
213
316
|
orderCanceledWebhookEventSchema,
|
|
214
317
|
claimWebhookEventSchema,
|
|
215
318
|
inquiryWebhookEventSchema,
|