@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
package/src/schemas/order.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { changeTypeSchema, orderItemStatusSchema, type PageParams, pageSchema } from "./common";
|
|
3
|
+
import { orderItemDownloadSchema } from "./digital-asset";
|
|
4
|
+
import { orderItemFulfillmentSnapshotResponseSchema } from "./fulfillment";
|
|
5
|
+
import { MAX_CODE_LENGTH, orderItemCodeSchema } from "./fulfillment-code";
|
|
3
6
|
import { cashReceiptSchema, easyPayProviderResponseSchema } from "./payment";
|
|
4
7
|
|
|
5
8
|
export const orderSummarySchema = z.object({
|
|
@@ -30,6 +33,57 @@ export const shippingAddressSchema = z.object({
|
|
|
30
33
|
|
|
31
34
|
export type ShippingAddress = z.infer<typeof shippingAddressSchema>;
|
|
32
35
|
|
|
36
|
+
/** 비실물 주문상품의 제공 정보 (이슈 #48) */
|
|
37
|
+
export const orderItemFulfillmentSchema = z.object({
|
|
38
|
+
status: z
|
|
39
|
+
.string()
|
|
40
|
+
.describe(
|
|
41
|
+
"제공 상태. `FULFILLED`(제공함)·`REVOKED`(환불 등으로 회수함). 값이 추가될 수 있어 문자열로 받는다",
|
|
42
|
+
),
|
|
43
|
+
mode: z.string().nullable().describe("제공 방식. `AUTO`·`MANUAL`"),
|
|
44
|
+
fulfilledAt: z.string().describe("제공 시각 (ISO 8601)"),
|
|
45
|
+
note: z.string().nullable().describe("셀러가 남긴 이용 안내. 없으면 null"),
|
|
46
|
+
codes: z
|
|
47
|
+
.array(orderItemCodeSchema)
|
|
48
|
+
.optional()
|
|
49
|
+
.default([])
|
|
50
|
+
.describe(
|
|
51
|
+
"코드 발급 상품에 배정한 코드의 가린 값. 평문은 싣지 않는다. 코드 상품이 아니면 빈 배열. 서버는 항상 싣는다",
|
|
52
|
+
),
|
|
53
|
+
download: orderItemDownloadSchema
|
|
54
|
+
.nullable()
|
|
55
|
+
.optional()
|
|
56
|
+
.default(null)
|
|
57
|
+
.describe(
|
|
58
|
+
"파일 다운로드 상품의 횟수·기한·파일 목록. 파일 주소는 싣지 않는다. 다른 유형은 null. 서버는 항상 싣는다",
|
|
59
|
+
),
|
|
60
|
+
mail: z
|
|
61
|
+
.object({
|
|
62
|
+
status: z
|
|
63
|
+
.string()
|
|
64
|
+
.describe(
|
|
65
|
+
"제공 안내 메일 상태. 현재 값은 `PENDING`(보낼 예정)·`SENDING`·`SENT`·`FAILED`·`SKIPPED`(보내지 않음: 스토어 설정 꺼짐·받을 주소 없음)이고 값이 추가될 수 있다",
|
|
66
|
+
),
|
|
67
|
+
sentAt: z.string().nullable(),
|
|
68
|
+
errorCode: z.string().nullable().describe("실패·건너뜀 사유 코드"),
|
|
69
|
+
})
|
|
70
|
+
.nullable()
|
|
71
|
+
.optional()
|
|
72
|
+
.default(null)
|
|
73
|
+
.describe("구매자에게 보낸 제공 안내 메일(이슈 #48). 기록이 없으면 null. 서버는 항상 싣는다"),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
/** 제공 안내 메일 재발송 결과 */
|
|
77
|
+
export const fulfillmentMailResendSchema = z.object({
|
|
78
|
+
mailStatus: z
|
|
79
|
+
.string()
|
|
80
|
+
.describe("재발송 뒤 메일 상태 — `SENT`·`PENDING`(곧 다시 보낸다)·`FAILED`·`SKIPPED`"),
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
export type FulfillmentMailResend = z.infer<typeof fulfillmentMailResendSchema>;
|
|
84
|
+
|
|
85
|
+
export type OrderItemFulfillment = z.infer<typeof orderItemFulfillmentSchema>;
|
|
86
|
+
|
|
33
87
|
export const orderItemSchema = z.object({
|
|
34
88
|
orderItemId: z.string(),
|
|
35
89
|
orderId: z.string(),
|
|
@@ -40,6 +94,10 @@ export const orderItemSchema = z.object({
|
|
|
40
94
|
optionId: z.string().nullable(),
|
|
41
95
|
optionName: z.string().nullable(),
|
|
42
96
|
sellerCode: z.string().nullable(),
|
|
97
|
+
fulfillmentSnapshot: orderItemFulfillmentSnapshotResponseSchema.describe(
|
|
98
|
+
"주문 시점 이행 스냅샷(이슈 #57). `type`이 주문 시점 상품 유형이다. 배송 상품이 아니면 발송·배송완료 대신 제공 처리를 하고, " +
|
|
99
|
+
"`DELIVERED`는 제공 완료(방문 수령은 수령 완료)다. 상품을 나중에 고쳐도 바뀌지 않는다. 실제 제공 기록은 `fulfillment`에 있다",
|
|
100
|
+
),
|
|
43
101
|
quantity: z.number().int().describe("주문 수량"),
|
|
44
102
|
canceledQuantity: z
|
|
45
103
|
.number()
|
|
@@ -51,8 +109,27 @@ export const orderItemSchema = z.object({
|
|
|
51
109
|
.describe("살아 있는 수량 (`quantity` − `canceledQuantity`). 0이면 주문상품이 전량 취소됐다"),
|
|
52
110
|
unitPrice: z.number().int(),
|
|
53
111
|
discountAmount: z.number().int(),
|
|
54
|
-
totalPrice: z
|
|
112
|
+
totalPrice: z
|
|
113
|
+
.number()
|
|
114
|
+
.int()
|
|
115
|
+
.describe("주문상품 금액 (즉시할인 반영 단가 × 주문 수량). 쿠폰 할인 전 금액이다"),
|
|
116
|
+
couponDiscountAmount: z
|
|
117
|
+
.number()
|
|
118
|
+
.int()
|
|
119
|
+
.optional()
|
|
120
|
+
.default(0)
|
|
121
|
+
.describe(
|
|
122
|
+
"이 주문상품에 배분된 쿠폰 할인 — 상품 쿠폰 + 주문 쿠폰 배분. 실결제 금액은 `totalPrice − couponDiscountAmount`이고 " +
|
|
123
|
+
"취소·반품 환불은 실결제 금액을 수량 비율로 나눈다. 쿠폰이 없으면 0. 서버는 항상 싣는다",
|
|
124
|
+
),
|
|
55
125
|
hold: z.object({ reason: z.string(), expectedDispatchDate: z.string() }).nullable(),
|
|
126
|
+
fulfillment: orderItemFulfillmentSchema
|
|
127
|
+
.nullable()
|
|
128
|
+
.optional()
|
|
129
|
+
.default(null)
|
|
130
|
+
.describe(
|
|
131
|
+
"비실물 상품의 제공 정보(이슈 #48). 제공 전이거나 배송 상품이면 null이다. 코드 평문·파일 주소는 싣지 않는다. 서버는 항상 싣는다",
|
|
132
|
+
),
|
|
56
133
|
autoDecisionDate: z.string().nullable(),
|
|
57
134
|
sellerMemo: z.string().nullable(),
|
|
58
135
|
orderedAt: z.string(),
|
|
@@ -67,6 +144,21 @@ export const orderItemSchema = z.object({
|
|
|
67
144
|
|
|
68
145
|
export type OrderItem = z.infer<typeof orderItemSchema>;
|
|
69
146
|
|
|
147
|
+
/** 주문이 받은 쿠폰 한 장 (이슈 #47) */
|
|
148
|
+
export const orderDiscountSchema = z.object({
|
|
149
|
+
couponId: z.string().describe("쿠폰 id"),
|
|
150
|
+
name: z.string().describe("주문 시점 쿠폰 이름"),
|
|
151
|
+
kind: z
|
|
152
|
+
.string()
|
|
153
|
+
.describe(
|
|
154
|
+
"쿠폰 종류. 현재 값은 `ORDER`(주문 쿠폰)·`PRODUCT`(상품 쿠폰)·`DELIVERY`(배송비 쿠폰)이고 값이 추가될 수 있어 문자열로 받는다",
|
|
155
|
+
),
|
|
156
|
+
discountAmount: z.number().int().describe("이 쿠폰의 할인액 (원)"),
|
|
157
|
+
issueId: z.string().nullable().describe("발급 쿠폰 id. 코드 입력형은 null"),
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
export type OrderDiscount = z.infer<typeof orderDiscountSchema>;
|
|
161
|
+
|
|
70
162
|
export const orderSchema = z.object({
|
|
71
163
|
orderId: z.string(),
|
|
72
164
|
orderedAt: z.string(),
|
|
@@ -75,6 +167,13 @@ export const orderSchema = z.object({
|
|
|
75
167
|
phone: z.string(),
|
|
76
168
|
email: z.email().optional(),
|
|
77
169
|
}),
|
|
170
|
+
requiresShipping: z
|
|
171
|
+
.boolean()
|
|
172
|
+
.optional()
|
|
173
|
+
.default(true)
|
|
174
|
+
.describe(
|
|
175
|
+
"택배 배송이 필요한 주문상품이 있는가. false면 `shippingAddress`는 주문자 이름·연락처만 있고 주소는 빈 문자열이다. 서버는 항상 싣는다",
|
|
176
|
+
),
|
|
78
177
|
shippingAddress: shippingAddressSchema,
|
|
79
178
|
payment: z.object({
|
|
80
179
|
method: z
|
|
@@ -102,10 +201,38 @@ export const orderSchema = z.object({
|
|
|
102
201
|
"그 밖의 결제수단과 간편결제 이전 주문은 null. 서버는 항상 싣는다. " +
|
|
103
202
|
"현재 값은 `NAVERPAY`·`KAKAOPAY`·`TOSSPAY`·`PAYCO`이고 값이 추가될 수 있어 문자열로 받는다",
|
|
104
203
|
),
|
|
105
|
-
productAmount: z.number().int(),
|
|
106
|
-
deliveryFee: z.number().int(),
|
|
107
|
-
discountAmount: z
|
|
108
|
-
|
|
204
|
+
productAmount: z.number().int().describe("상품 금액 합 (즉시할인 반영, 쿠폰 전)"),
|
|
205
|
+
deliveryFee: z.number().int().describe("배송비 (쿠폰 전)"),
|
|
206
|
+
discountAmount: z
|
|
207
|
+
.number()
|
|
208
|
+
.int()
|
|
209
|
+
.describe(
|
|
210
|
+
"즉시할인 합 (정가 − 할인 단가) × 수량. 쿠폰 할인은 `couponDiscountAmount`로 따로 온다",
|
|
211
|
+
),
|
|
212
|
+
couponDiscountAmount: z
|
|
213
|
+
.number()
|
|
214
|
+
.int()
|
|
215
|
+
.optional()
|
|
216
|
+
.default(0)
|
|
217
|
+
.describe("상품·주문 쿠폰 할인 합계. 쿠폰이 없으면 0. 서버는 항상 싣는다"),
|
|
218
|
+
deliveryDiscountAmount: z
|
|
219
|
+
.number()
|
|
220
|
+
.int()
|
|
221
|
+
.optional()
|
|
222
|
+
.default(0)
|
|
223
|
+
.describe("배송비 쿠폰 할인. 쿠폰이 없으면 0. 서버는 항상 싣는다"),
|
|
224
|
+
discounts: z
|
|
225
|
+
.array(orderDiscountSchema)
|
|
226
|
+
.optional()
|
|
227
|
+
.default([])
|
|
228
|
+
.describe("주문이 받은 쿠폰. 쿠폰이 없으면 빈 배열. 서버는 항상 싣는다"),
|
|
229
|
+
totalAmount: z
|
|
230
|
+
.number()
|
|
231
|
+
.int()
|
|
232
|
+
.describe(
|
|
233
|
+
"결제 금액 = productAmount + deliveryFee − couponDiscountAmount − deliveryDiscountAmount. " +
|
|
234
|
+
"쿠폰이 없는 주문은 productAmount + deliveryFee다",
|
|
235
|
+
),
|
|
109
236
|
paidAt: z.string().nullable(),
|
|
110
237
|
cashReceipt: cashReceiptSchema
|
|
111
238
|
.nullable()
|
|
@@ -115,6 +242,14 @@ export const orderSchema = z.object({
|
|
|
115
242
|
"현금영수증. 구매자가 계좌이체·가상계좌 결제에서 신청했을 때만 있고 그 밖에는 null. 서버는 항상 싣는다",
|
|
116
243
|
),
|
|
117
244
|
}),
|
|
245
|
+
withdrawalAgreedAt: z
|
|
246
|
+
.string()
|
|
247
|
+
.nullable()
|
|
248
|
+
.optional()
|
|
249
|
+
.default(null)
|
|
250
|
+
.describe(
|
|
251
|
+
"청약철회 제한 안내에 동의한 시각(이슈 #48). 제한 상품이 없던 주문은 null. 서버는 항상 싣는다",
|
|
252
|
+
),
|
|
118
253
|
testPayment: z
|
|
119
254
|
.boolean()
|
|
120
255
|
.describe(
|
|
@@ -159,6 +294,12 @@ export const orderListQuerySchema = z.object({
|
|
|
159
294
|
.optional()
|
|
160
295
|
.describe("주문상품 상태 필터 — 이 상태의 주문상품이 하나라도 있는 주문"),
|
|
161
296
|
keyword: z.string().optional().describe("주문번호(일치)·주문자명·수령인명(포함) 검색"),
|
|
297
|
+
fulfillmentType: z
|
|
298
|
+
.string()
|
|
299
|
+
.optional()
|
|
300
|
+
.describe(
|
|
301
|
+
"상품 유형 필터(이슈 #48) — 이 유형(`SHIPPING`·`DOWNLOAD`·`CODE`·`SERVICE`·`PICKUP`)의 주문상품이 하나라도 있는 주문",
|
|
302
|
+
),
|
|
162
303
|
testPayment: testPaymentQuery,
|
|
163
304
|
...pageQuery,
|
|
164
305
|
size: z.string().optional().describe("페이지 크기 (기본 20, 최대 100)"),
|
|
@@ -187,6 +328,8 @@ export interface OrderListParams extends PageParams {
|
|
|
187
328
|
orderedTo?: string;
|
|
188
329
|
status?: z.infer<typeof orderItemStatusSchema>;
|
|
189
330
|
keyword?: string;
|
|
331
|
+
/** 상품 유형 필터 — 이 유형의 주문상품이 하나라도 있는 주문(이슈 #48) */
|
|
332
|
+
fulfillmentType?: string;
|
|
190
333
|
/** 테스트 결제 필터 — `"true"` 테스트 결제만, `"false"` 실결제만, 생략하면 전부 */
|
|
191
334
|
testPayment?: "true" | "false";
|
|
192
335
|
}
|
|
@@ -345,13 +488,48 @@ export const cancelBySellerRequestSchema = z.object({
|
|
|
345
488
|
|
|
346
489
|
export type CancelBySellerRequest = z.infer<typeof cancelBySellerRequestSchema>;
|
|
347
490
|
|
|
491
|
+
/**
|
|
492
|
+
* 제공 처리 요청 (이슈 #48) — `POST /v1/order-items/{orderItemId}/fulfill`. 수동 제공 비실물 주문상품(이용권·방문 수령 등)을
|
|
493
|
+
* 제공 완료(`DELIVERED`)로 바꾼다.
|
|
494
|
+
*/
|
|
495
|
+
export const fulfillRequestSchema = z.object({
|
|
496
|
+
note: z
|
|
497
|
+
.string()
|
|
498
|
+
.trim()
|
|
499
|
+
.max(1000)
|
|
500
|
+
.optional()
|
|
501
|
+
.describe("구매자에게 보일 이용 안내(예: 예약 방법, 수령 장소). 주문 상세에 실린다"),
|
|
502
|
+
codes: z
|
|
503
|
+
.array(z.string().trim().min(1).max(MAX_CODE_LENGTH))
|
|
504
|
+
.min(1)
|
|
505
|
+
.max(100)
|
|
506
|
+
.optional()
|
|
507
|
+
.describe(
|
|
508
|
+
"수동 제공 코드 발급 상품의 코드 — 셀러 시스템이 발급한 코드를 넘긴다. 남은 수량만큼 보낸다(`CODE_COUNT_MISMATCH`). " +
|
|
509
|
+
"암호화해 저장하고 구매자가 열람할 때만 평문으로 나간다. 다른 유형은 보내지 않는다",
|
|
510
|
+
),
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
export type FulfillRequest = z.infer<typeof fulfillRequestSchema>;
|
|
514
|
+
|
|
515
|
+
/** 제공 처리 일괄 요청 — `POST /v1/order-items/fulfill` */
|
|
516
|
+
export const fulfillBulkRequestSchema = z.object({
|
|
517
|
+
orderItemIds: z.array(z.string()).min(1).max(100),
|
|
518
|
+
note: fulfillRequestSchema.shape.note,
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
export type FulfillBulkRequest = z.infer<typeof fulfillBulkRequestSchema>;
|
|
522
|
+
|
|
348
523
|
/** `POST /v1/order-items/{orderItemId}/cancel` 응답 — 셀러 직권취소 환불 결과 */
|
|
349
524
|
export const cancelBySellerResultSchema = z.object({
|
|
350
525
|
orderItemId: z.string(),
|
|
351
526
|
refundAmount: z
|
|
352
527
|
.number()
|
|
353
528
|
.int()
|
|
354
|
-
.describe(
|
|
529
|
+
.describe(
|
|
530
|
+
"실제 환불한 금액 — 수량 비율 상품 실결제 금액(쿠폰 배분을 뺀 금액) + 배송비 환불 − 조건부 무료배송 미달 차감 " +
|
|
531
|
+
"+ 되돌린 쿠폰 차감(`couponChargeRefundAmount`)",
|
|
532
|
+
),
|
|
355
533
|
quantity: z.number().int().describe("취소한 수량"),
|
|
356
534
|
canceledQuantity: z.number().int().describe("취소 후 누적 취소 수량"),
|
|
357
535
|
deliveryFeeRefundAmount: z
|
|
@@ -364,6 +542,18 @@ export const cancelBySellerResultSchema = z.object({
|
|
|
364
542
|
.number()
|
|
365
543
|
.int()
|
|
366
544
|
.describe("조건부 무료배송 기준 미달로 환불액에서 뺀 배송비. 정책이 꺼져 있으면 항상 0이다"),
|
|
545
|
+
couponChargeAmount: z
|
|
546
|
+
.number()
|
|
547
|
+
.int()
|
|
548
|
+
.optional()
|
|
549
|
+
.default(0)
|
|
550
|
+
.describe("쿠폰 조건 미달 차감. 판매자 취소는 차감하지 않으므로 늘 0이다. 서버는 항상 싣는다"),
|
|
551
|
+
couponChargeRefundAmount: z
|
|
552
|
+
.number()
|
|
553
|
+
.int()
|
|
554
|
+
.optional()
|
|
555
|
+
.default(0)
|
|
556
|
+
.describe("환불액에 더한, 앞선 환불에서 뺀 쿠폰 차감을 되돌린 몫. 서버는 항상 싣는다"),
|
|
367
557
|
refundedAt: z.string(),
|
|
368
558
|
});
|
|
369
559
|
|
package/src/schemas/product.ts
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { type PageParams, pageQueryShape } from "./common";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
fulfillmentTypeResponseSchema,
|
|
5
|
+
productFulfillmentCreateSchema,
|
|
6
|
+
productFulfillmentResponseSchema,
|
|
7
|
+
productFulfillmentUpdateSchema,
|
|
8
|
+
} from "./fulfillment";
|
|
9
|
+
import { productRefundPolicyPatchSchema } from "./refund-policy";
|
|
4
10
|
import { UPLOAD_PURPOSE_LIMITS, uploadExtensionOf } from "./upload";
|
|
5
11
|
|
|
6
12
|
export const productStatusSchema = z.enum(["SALE", "SUSPENDED", "OUT_OF_STOCK", "CLOSED"]);
|
|
7
13
|
|
|
8
14
|
export type ProductStatus = z.infer<typeof productStatusSchema>;
|
|
9
15
|
|
|
10
|
-
export const deliveryTypeSchema = z.enum(["FREE", "PAID", "CONDITIONAL_FREE"]);
|
|
11
|
-
|
|
12
|
-
export type DeliveryType = z.infer<typeof deliveryTypeSchema>;
|
|
13
|
-
|
|
14
16
|
export const productSummarySchema = z.object({
|
|
15
17
|
productId: z.string(),
|
|
16
18
|
name: z.string(),
|
|
@@ -27,6 +29,11 @@ export const productSummarySchema = z.object({
|
|
|
27
29
|
thumbnailUrl: z.url().optional(),
|
|
28
30
|
/** optionGroups.length > 0 */
|
|
29
31
|
hasOptions: z.boolean(),
|
|
32
|
+
fulfillment: z
|
|
33
|
+
.object({ type: fulfillmentTypeResponseSchema })
|
|
34
|
+
.describe(
|
|
35
|
+
"상품 이행의 유형. 유형별 속성은 단건 조회(`GET /v1/products/{productId}`)의 `fulfillment`에 있다",
|
|
36
|
+
),
|
|
30
37
|
updatedAt: z.string(),
|
|
31
38
|
});
|
|
32
39
|
|
|
@@ -73,6 +80,17 @@ export const productVariantSchema = z.object({
|
|
|
73
80
|
|
|
74
81
|
export type ProductVariant = z.infer<typeof productVariantSchema>;
|
|
75
82
|
|
|
83
|
+
export const discountSchema = z.object({
|
|
84
|
+
productId: z.string(),
|
|
85
|
+
discountType: z.enum(["RATE", "AMOUNT"]),
|
|
86
|
+
discountValue: z.number().int(),
|
|
87
|
+
discountedPrice: z.number().int(),
|
|
88
|
+
startAt: z.string().nullable(),
|
|
89
|
+
endAt: z.string().nullable(),
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
export type Discount = z.infer<typeof discountSchema>;
|
|
93
|
+
|
|
76
94
|
export const productSchema = productSummarySchema.extend({
|
|
77
95
|
description: z.string().optional(),
|
|
78
96
|
/**
|
|
@@ -86,17 +104,28 @@ export const productSchema = productSummarySchema.extend({
|
|
|
86
104
|
optionGroups: z.array(optionGroupSchema),
|
|
87
105
|
/** 모든 상품이 최소 1개 variant를 갖는다 (옵션 없으면 default variant 1개) */
|
|
88
106
|
variants: z.array(productVariantSchema),
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
107
|
+
fulfillment: productFulfillmentResponseSchema,
|
|
108
|
+
fulfillmentTypeLocked: z
|
|
109
|
+
.boolean()
|
|
110
|
+
.describe(
|
|
111
|
+
"판매 이력(주문상품)이 있어 상품 유형(`fulfillment.type`)을 바꿀 수 없는가. true면 다른 유형으로 수정하는 요청이 409 `FULFILLMENT_TYPE_LOCKED`다. 같은 유형 안의 속성은 바꿀 수 있다",
|
|
112
|
+
),
|
|
113
|
+
refundPolicy: productRefundPolicyPatchSchema
|
|
114
|
+
.nullable()
|
|
115
|
+
.describe(
|
|
116
|
+
"상품별 환불 정책 덮어쓰기(상품이 정한 항목만). null이면 스토어 기본 환불 정책을 쓴다. " +
|
|
117
|
+
"반품·교환 배송비(`returnDeliveryFee`·`exchangeDeliveryFee`)는 배송 상품에만 있다",
|
|
118
|
+
),
|
|
119
|
+
/**
|
|
120
|
+
* 설정된 즉시할인. `GET /products/{productId}/discount`의 응답과 같은 형태다(이슈 #51).
|
|
121
|
+
* 할인이 없으면 null이다. 이 필드를 싣기 전 서버의 응답에는 없으므로 optional이다.
|
|
122
|
+
*/
|
|
123
|
+
discount: discountSchema
|
|
95
124
|
.nullable()
|
|
96
125
|
.optional()
|
|
97
|
-
.describe(
|
|
98
|
-
|
|
99
|
-
|
|
126
|
+
.describe(
|
|
127
|
+
"설정된 즉시할인. GET /v1/products/{productId}/discount 응답과 같은 형태다. 할인이 없으면 null",
|
|
128
|
+
),
|
|
100
129
|
createdAt: z.string().optional(),
|
|
101
130
|
});
|
|
102
131
|
|
|
@@ -258,106 +287,81 @@ export const variantStockSchema = z.object({
|
|
|
258
287
|
export type VariantStock = z.infer<typeof variantStockSchema>;
|
|
259
288
|
|
|
260
289
|
/**
|
|
261
|
-
*
|
|
290
|
+
* 상품 쓰기 요청의 공통 필드 — 등록(POST)과 전체 수정(PUT)이 같다. 이행(`fulfillment`)만 두 요청의 스키마가 다르다.
|
|
262
291
|
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
* (`409 ADDRESS_IN_USE`) 참조를 푸는 수단이 필요한데, 생략을 기본값으로 되채우면 기본 주소는 영원히
|
|
266
|
-
* 풀리지 않는다 — null이 그 경로다. 배송 묶음 판정은 null을 그 시점 스토어 기본 출고지로 정규화하므로
|
|
267
|
-
* 두 표현이 배송비를 갈라놓지 않는다.
|
|
292
|
+
* 최상위가 `strictObject`다. 모르는 필드는 400이다 — 이슈 #57 이전의 평면 필드(`deliveryFee`·`fulfillmentType`·`returnFee` 등)를
|
|
293
|
+
* 보낸 요청이 조용히 무시되지 않게 한다. 옛 필드는 `fulfillment.{유형}.…`과 `refundPolicy`로 옮겼다.
|
|
268
294
|
*/
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
"
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
.describe("배송비 유형. FREE 무료, PAID 유료, CONDITIONAL_FREE 조건부 무료"),
|
|
311
|
-
deliveryFee: z
|
|
312
|
-
.number()
|
|
313
|
-
.int()
|
|
314
|
-
.min(0)
|
|
315
|
-
.optional()
|
|
316
|
-
.describe(
|
|
317
|
-
"배송비. 원 단위. **생략하면 스토어 기본 배송비**(`GET /v1/store`의 `shippingPolicy.baseDeliveryFee`)를 쓴다. " +
|
|
318
|
-
"`deliveryType`이 FREE면 0으로 저장한다",
|
|
319
|
-
),
|
|
320
|
-
conditionalFreeAmount: z
|
|
321
|
-
.number()
|
|
322
|
-
.int()
|
|
323
|
-
.optional()
|
|
324
|
-
.describe("CONDITIONAL_FREE일 때 무료 배송이 되는 주문 금액. 원 단위"),
|
|
325
|
-
returnFee: z
|
|
326
|
-
.number()
|
|
327
|
-
.int()
|
|
328
|
-
.default(0)
|
|
329
|
-
.describe("반품 배송비. 원 단위. `refundPolicy.returnDeliveryFee`와 같은 값이다"),
|
|
330
|
-
exchangeFee: z
|
|
331
|
-
.number()
|
|
332
|
-
.int()
|
|
333
|
-
.default(0)
|
|
334
|
-
.describe("교환 배송비. 원 단위. `refundPolicy.exchangeDeliveryFee`와 같은 값이다"),
|
|
335
|
-
refundPolicy: refundPolicyPatchSchema
|
|
336
|
-
.nullish()
|
|
337
|
-
.describe(
|
|
338
|
-
"상품별 환불 정책 — 준 항목만 스토어 기본을 덮어쓴다. `null`이나 생략은 스토어 기본을 그대로 쓴다는 뜻이다. " +
|
|
339
|
-
"`returnDeliveryFee`·`exchangeDeliveryFee`를 주면 `returnFee`·`exchangeFee`도 같은 값으로 맞춘다",
|
|
340
|
-
),
|
|
341
|
-
shippingAddressId: z.string().nullable().optional().describe(ADDRESS_DESCRIPTIONS.shipping),
|
|
342
|
-
returnAddressId: z.string().nullable().optional().describe(ADDRESS_DESCRIPTIONS.return),
|
|
343
|
-
})
|
|
344
|
-
.extend(optionModelRequestSchema.shape);
|
|
295
|
+
const productWriteShape = {
|
|
296
|
+
name: z.string().min(1).max(100),
|
|
297
|
+
categoryId: z.string().min(1, "카테고리를 선택해주십시오"),
|
|
298
|
+
salePrice: z
|
|
299
|
+
.number()
|
|
300
|
+
.int()
|
|
301
|
+
.min(10)
|
|
302
|
+
.multipleOf(10, "판매가는 10원 단위여야 합니다 (INVALID_PRICE_UNIT)")
|
|
303
|
+
.describe("판매가. 원(KRW) 단위 정수, 최소 10원, 10원 단위"),
|
|
304
|
+
originalPrice: z.number().int().optional().describe("정가(할인 전 표시 가격). 원 단위"),
|
|
305
|
+
description: z.string().optional(),
|
|
306
|
+
/**
|
|
307
|
+
* 상세설명 에디터(@avarlabs/editor)의 직렬화 상태. 재편집 때 이 값으로 문서를 복원한다.
|
|
308
|
+
* 표시는 `description`(HTML)이 맡는다 — 소비자는 이 필드를 몰라도 된다.
|
|
309
|
+
*/
|
|
310
|
+
descriptionJson: z.string().nullable().optional(),
|
|
311
|
+
images: z
|
|
312
|
+
.array(productImageUrlSchema)
|
|
313
|
+
.min(1)
|
|
314
|
+
.max(MAX_PRODUCT_IMAGES)
|
|
315
|
+
.optional()
|
|
316
|
+
.describe("상품 이미지 주소 목록. 첫 장이 대표 이미지(썸네일)다"),
|
|
317
|
+
attributes: z.array(z.object({ attributeId: z.string(), value: z.string() })).optional(),
|
|
318
|
+
refundPolicy: productRefundPolicyPatchSchema
|
|
319
|
+
.nullish()
|
|
320
|
+
.describe(
|
|
321
|
+
"상품별 환불 정책 — 준 항목만 스토어 기본을 덮어쓴다. `null`이나 생략은 스토어 기본을 그대로 쓴다는 뜻이다. " +
|
|
322
|
+
"반품·교환 배송비(`returnDeliveryFee`·`exchangeDeliveryFee`)는 배송 상품에만 줄 수 있다(다른 유형은 400)",
|
|
323
|
+
),
|
|
324
|
+
...optionModelRequestSchema.shape,
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
export const productCreateRequestSchema = z.strictObject({
|
|
328
|
+
...productWriteShape,
|
|
329
|
+
fulfillment: productFulfillmentCreateSchema
|
|
330
|
+
.optional()
|
|
331
|
+
.describe(
|
|
332
|
+
"상품 이행. `type`이 유형이고 같은 이름의 키가 유형별 속성이다. 생략하면 배송 상품(기본값)이다. " +
|
|
333
|
+
"배송 상품이 아닌 유형은 판매가 열린 유형만 등록할 수 있다(409 `FULFILLMENT_TYPE_NOT_AVAILABLE`)",
|
|
334
|
+
),
|
|
335
|
+
});
|
|
345
336
|
|
|
346
337
|
export type ProductCreateRequest = z.infer<typeof productCreateRequestSchema>;
|
|
338
|
+
export type ProductCreateRequestInput = z.input<typeof productCreateRequestSchema>;
|
|
339
|
+
|
|
340
|
+
const fulfillmentUpdateDescription =
|
|
341
|
+
"상품 이행. 생략하면 바꾸지 않는다. 주면 유형 안 필드를 모두 담아 통째로 바꾼다(현재 값과 병합하지 않는다). " +
|
|
342
|
+
"판매 이력이 있으면 유형을 바꿀 수 없다(409 `FULFILLMENT_TYPE_LOCKED`)";
|
|
343
|
+
|
|
344
|
+
/** 전체 수정(PUT) — 공통 필드는 등록과 같고, 이행은 생략하면 현재 값 유지, 주면 필드가 모두 필수다 */
|
|
345
|
+
export const productReplaceRequestSchema = z.strictObject({
|
|
346
|
+
...productWriteShape,
|
|
347
|
+
fulfillment: productFulfillmentUpdateSchema.optional().describe(fulfillmentUpdateDescription),
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
export type ProductReplaceRequest = z.infer<typeof productReplaceRequestSchema>;
|
|
351
|
+
export type ProductReplaceRequestInput = z.input<typeof productReplaceRequestSchema>;
|
|
347
352
|
|
|
348
353
|
export const productPatchRequestSchema = z
|
|
349
|
-
.
|
|
354
|
+
.strictObject({
|
|
350
355
|
name: z.string().max(100).optional(),
|
|
351
356
|
salePrice: z.number().int().multipleOf(10).optional(),
|
|
352
357
|
status: productStatusSchema.optional(),
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
shippingAddressId: z
|
|
356
|
-
.string()
|
|
358
|
+
fulfillment: productFulfillmentUpdateSchema.optional().describe(fulfillmentUpdateDescription),
|
|
359
|
+
refundPolicy: productRefundPolicyPatchSchema
|
|
357
360
|
.nullable()
|
|
358
361
|
.optional()
|
|
359
|
-
.describe(
|
|
360
|
-
|
|
362
|
+
.describe(
|
|
363
|
+
"상품별 환불 정책. 주면 통째로 바꾼다(준 항목만 스토어 기본을 덮어쓴다). `null`은 덮어쓰기를 지워 스토어 기본을 따른다. 생략하면 바꾸지 않는다",
|
|
364
|
+
),
|
|
361
365
|
})
|
|
362
366
|
.refine((value) => Object.values(value).some((v) => v !== undefined), {
|
|
363
367
|
message: "최소 1개 필드가 필요합니다",
|
|
@@ -396,17 +400,6 @@ export const productListQuerySchema = z.object({
|
|
|
396
400
|
...pageQueryShape(),
|
|
397
401
|
});
|
|
398
402
|
|
|
399
|
-
export const discountSchema = z.object({
|
|
400
|
-
productId: z.string(),
|
|
401
|
-
discountType: z.enum(["RATE", "AMOUNT"]),
|
|
402
|
-
discountValue: z.number().int(),
|
|
403
|
-
discountedPrice: z.number().int(),
|
|
404
|
-
startAt: z.string().nullable(),
|
|
405
|
-
endAt: z.string().nullable(),
|
|
406
|
-
});
|
|
407
|
-
|
|
408
|
-
export type Discount = z.infer<typeof discountSchema>;
|
|
409
|
-
|
|
410
403
|
export const discountRequestSchema = z.object({
|
|
411
404
|
discountType: z.enum(["RATE", "AMOUNT"]),
|
|
412
405
|
discountValue: z.number().int().min(1),
|