@sayren/store-sdk 0.32.0 → 0.34.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 +2226 -167
- package/dist/index.mjs +680 -81
- package/package.json +1 -1
- package/src/client.ts +79 -5
- package/src/http.ts +1 -1
- package/src/index.ts +4 -0
- package/src/schemas/analytics.ts +16 -0
- package/src/schemas/coupon.ts +13 -0
- package/src/schemas/currency-prices.ts +138 -0
- package/src/schemas/currency.ts +184 -0
- package/src/schemas/customer.ts +20 -1
- package/src/schemas/fulfillment.ts +7 -0
- package/src/schemas/market.ts +76 -0
- package/src/schemas/order.ts +16 -1
- package/src/schemas/payment.ts +109 -16
- package/src/schemas/points.ts +2 -5
- package/src/schemas/product-info.ts +190 -0
- package/src/schemas/product.ts +73 -5
- package/src/schemas/promotion.ts +36 -17
- package/src/schemas/refund-policy.ts +55 -2
- package/src/schemas/shipping-policy.ts +56 -7
- package/src/schemas/storefront-hosting.ts +6 -10
- package/src/schemas/webhook-event.ts +24 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { currencyCodeSchema } from "./currency";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 판매 권역 (이슈 #130) — 상점이 어느 국가에 어느 통화로 파는지를 정한다.
|
|
6
|
+
*
|
|
7
|
+
* 권역 = 이름 + 통화 하나 + 국가 목록 + 연결 도메인 + 쿠폰·적립금 사용. 국가는 상점 안에서 한 권역에만 속하고, 판매 국가는 보관하지 않은 모든 권역 국가의
|
|
8
|
+
* 합집합이다. 기본 권역은 상점마다 하나이고 그 통화가 기본 통화(KRW)다. 기본 권역은 보관할 수 없고 국가 목록을 비울 수 없다. 권역의 통화는 만들 때만 정한다.
|
|
9
|
+
*
|
|
10
|
+
* 스토어프론트는 요청마다 도메인(`X-Storefront-Host`) → 구매자 국가(`X-Buyer-Country`) → 기본 권역 순으로 권역을 고르고, 주문서가 그 권역·통화를 굳힌다.
|
|
11
|
+
* 외화 권역은 적립금을 켤 수 없다(적립금은 기본 통화 단위다).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const countryCodeSchema = z
|
|
15
|
+
.string()
|
|
16
|
+
.regex(/^[A-Za-z]{2}$/, "국가는 ISO 3166-1 alpha-2 두 글자입니다")
|
|
17
|
+
.transform((value) => value.toUpperCase())
|
|
18
|
+
.describe("국가(ISO 3166-1 alpha-2, 예: KR·US·JP)");
|
|
19
|
+
|
|
20
|
+
const hostnameSchema = z
|
|
21
|
+
.string()
|
|
22
|
+
.min(1)
|
|
23
|
+
.max(253)
|
|
24
|
+
.regex(/^[A-Za-z0-9.-]+$/, "도메인은 호스트 이름만 입력합니다(예: shop.example.com)")
|
|
25
|
+
.transform((value) => value.toLowerCase().replace(/\.$/, ""))
|
|
26
|
+
.describe(
|
|
27
|
+
"연결 도메인(호스트 이름). 셀러가 직접 호스팅하는 스토어프론트가 `X-Storefront-Host`로 보낸다",
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
export const marketSchema = z.object({
|
|
31
|
+
marketId: z.string(),
|
|
32
|
+
name: z.string(),
|
|
33
|
+
currency: currencyCodeSchema,
|
|
34
|
+
isDefault: z
|
|
35
|
+
.boolean()
|
|
36
|
+
.describe("기본 권역. 상점마다 하나이고 보관할 수 없다. 이 통화가 기본 통화다"),
|
|
37
|
+
countries: z.array(z.string()).describe("이 권역 국가. 국가는 상점 안에서 한 권역에만 속한다"),
|
|
38
|
+
domains: z.array(z.string()).describe("이 권역에 연결한 도메인"),
|
|
39
|
+
couponsEnabled: z.boolean().describe("이 권역 주문서에서 쿠폰을 쓸 수 있는가"),
|
|
40
|
+
pointsEnabled: z
|
|
41
|
+
.boolean()
|
|
42
|
+
.describe("이 권역 주문서에서 적립금을 쓸 수 있는가. 외화 권역은 늘 false다"),
|
|
43
|
+
archivedAt: z
|
|
44
|
+
.string()
|
|
45
|
+
.nullable()
|
|
46
|
+
.describe("보관 시각. 보관한 권역은 판정에서 빠지고 그 국가·도메인은 풀린다"),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
export type Market = z.infer<typeof marketSchema>;
|
|
50
|
+
|
|
51
|
+
export const createMarketRequestSchema = z.strictObject({
|
|
52
|
+
name: z.string().trim().min(1).max(50),
|
|
53
|
+
currency: currencyCodeSchema.describe("이 권역의 통화. 만든 뒤에는 바꿀 수 없다"),
|
|
54
|
+
countries: z.array(countryCodeSchema).min(1).max(250),
|
|
55
|
+
domains: z.array(hostnameSchema).max(20).optional(),
|
|
56
|
+
couponsEnabled: z.boolean().optional().describe("기본 false"),
|
|
57
|
+
pointsEnabled: z.boolean().optional().describe("기본 false. 외화 권역은 true로 둘 수 없다"),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
export type CreateMarketRequest = z.infer<typeof createMarketRequestSchema>;
|
|
61
|
+
export type CreateMarketRequestInput = z.input<typeof createMarketRequestSchema>;
|
|
62
|
+
|
|
63
|
+
export const updateMarketRequestSchema = z.strictObject({
|
|
64
|
+
name: z.string().trim().min(1).max(50).optional(),
|
|
65
|
+
countries: z
|
|
66
|
+
.array(countryCodeSchema)
|
|
67
|
+
.max(250)
|
|
68
|
+
.optional()
|
|
69
|
+
.describe("주면 국가 목록 전체를 바꾼다. 다른 권역의 국가는 그 권역에서 먼저 빼야 한다"),
|
|
70
|
+
domains: z.array(hostnameSchema).max(20).optional().describe("주면 도메인 목록 전체를 바꾼다"),
|
|
71
|
+
couponsEnabled: z.boolean().optional(),
|
|
72
|
+
pointsEnabled: z.boolean().optional().describe("외화 권역은 true로 둘 수 없다"),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
export type UpdateMarketRequest = z.infer<typeof updateMarketRequestSchema>;
|
|
76
|
+
export type UpdateMarketRequestInput = z.input<typeof updateMarketRequestSchema>;
|
package/src/schemas/order.ts
CHANGED
|
@@ -21,6 +21,12 @@ export const orderSummarySchema = z.object({
|
|
|
21
21
|
"진행 중 주문상품 가운데 가장 앞 단계(취소 제외, 모두 취소면 CANCELED, #115). 섞인 주문에서 아직 기다리는 단계다. 서버는 항상 싣는다",
|
|
22
22
|
),
|
|
23
23
|
orderedAt: z.string(),
|
|
24
|
+
currency: z
|
|
25
|
+
.string()
|
|
26
|
+
.optional()
|
|
27
|
+
.describe(
|
|
28
|
+
"주문 통화(ISO 4217, 이슈 #130). 금액 필드는 모두 이 통화의 최소 단위다. 외화 주문이 생기면 이 값을 확인한다. 없으면 KRW다. 서버는 항상 싣는다",
|
|
29
|
+
),
|
|
24
30
|
testPayment: z
|
|
25
31
|
.boolean()
|
|
26
32
|
.describe(
|
|
@@ -33,7 +39,10 @@ export type OrderSummary = z.infer<typeof orderSummarySchema>;
|
|
|
33
39
|
export const shippingAddressSchema = z.object({
|
|
34
40
|
receiverName: z.string(),
|
|
35
41
|
phone: z.string(),
|
|
36
|
-
zipCode: z.string(),
|
|
42
|
+
zipCode: z.string().describe("우편번호. 해외 배송지는 그 나라 우편번호이고 없으면 빈 문자열이다"),
|
|
43
|
+
countryCode: z.string().optional().describe("배송 국가(이슈 #130). 없으면 국내(KR)다"),
|
|
44
|
+
region: z.string().nullish().describe("해외 배송지의 1차 행정 구역"),
|
|
45
|
+
city: z.string().nullish().describe("해외 배송지의 도시. 국내 배송지는 없다"),
|
|
37
46
|
address1: z.string(),
|
|
38
47
|
address2: z.string().nullish(),
|
|
39
48
|
deliveryMemo: z.string().nullish(),
|
|
@@ -264,6 +273,12 @@ export const orderSchema = z.object({
|
|
|
264
273
|
"현금영수증. 구매자가 계좌이체·가상계좌 결제에서 신청했을 때만 있고 그 밖에는 null. 서버는 항상 싣는다",
|
|
265
274
|
),
|
|
266
275
|
}),
|
|
276
|
+
currency: z
|
|
277
|
+
.string()
|
|
278
|
+
.optional()
|
|
279
|
+
.describe(
|
|
280
|
+
"주문 통화(ISO 4217, 이슈 #130). 금액 필드는 모두 이 통화의 최소 단위다. 외화 주문이 생기면 이 값을 확인한다. 없으면 KRW다. 서버는 항상 싣는다",
|
|
281
|
+
),
|
|
267
282
|
testPayment: z
|
|
268
283
|
.boolean()
|
|
269
284
|
.describe(
|
package/src/schemas/payment.ts
CHANGED
|
@@ -10,12 +10,21 @@ import { type PageParams, pageSchema } from "./common";
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
export const pgProviderSchema = z
|
|
13
|
-
.enum(["tosspayments", "portone", "kakaopay", "naverpay"])
|
|
13
|
+
.enum(["tosspayments", "portone", "kakaopay", "naverpay", "paypal"])
|
|
14
14
|
.describe(
|
|
15
|
-
"PG 식별자. `tosspayments`=토스페이먼츠, `portone`=포트원(V2), `kakaopay`=카카오페이(직접 연동), `naverpay`=네이버페이 결제형(직접 연동)",
|
|
15
|
+
"PG 식별자. `tosspayments`=토스페이먼츠, `portone`=포트원(V2), `kakaopay`=카카오페이(직접 연동), `naverpay`=네이버페이 결제형(직접 연동), `paypal`=PayPal(직접 연동, 외화)",
|
|
16
16
|
);
|
|
17
17
|
export type PgProvider = z.infer<typeof pgProviderSchema>;
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* 응답에 실리는 PG — 서버가 PG를 추가해도 응답 파싱이 깨지지 않게 문자열로 받는다(`easyPayProviderResponseSchema`와 같은 방식, 이슈 #131 CD2).
|
|
21
|
+
* 요청(우선순위·리포트 필터·경로)은 값을 골라 보내는 자리라 `pgProviderSchema` enum 그대로다. 현재 값은 `pgProviderSchema`의 값이고
|
|
22
|
+
* 화면은 모르는 값을 원문 그대로 보여 준다
|
|
23
|
+
*/
|
|
24
|
+
export const pgProviderResponseSchema = z
|
|
25
|
+
.string()
|
|
26
|
+
.describe("PG 식별자(`pgProviderSchema`의 값). 모르는 값이 와도 응답 파싱이 깨지지 않는다");
|
|
27
|
+
|
|
19
28
|
/**
|
|
20
29
|
* 간편결제 전용 PG — 결제수단이 간편결제 하나이고 간편결제사가 PG 자신이다(`kakaopay` → `KAKAOPAY`).
|
|
21
30
|
* 이 PG는 `easyPayProviders` 설정 없이 결제 옵션이 하나(`EASY_PAY` + 그 간편결제사) 나온다
|
|
@@ -23,6 +32,7 @@ export type PgProvider = z.infer<typeof pgProviderSchema>;
|
|
|
23
32
|
export const DEDICATED_EASY_PAY_PROVIDER = {
|
|
24
33
|
kakaopay: "KAKAOPAY",
|
|
25
34
|
naverpay: "NAVERPAY",
|
|
35
|
+
paypal: "PAYPAL",
|
|
26
36
|
} as const satisfies Partial<Record<PgProvider, string>>;
|
|
27
37
|
|
|
28
38
|
export const pgPaymentMethodSchema = z
|
|
@@ -34,7 +44,7 @@ export type PgPaymentMethod = z.infer<typeof pgPaymentMethodSchema>;
|
|
|
34
44
|
* 간편결제사 — 결제 옵션(스토어프론트 `paymentOptions`)과 PG 설정의 `easyPayProviders`에 쓴다.
|
|
35
45
|
* `@sayren/storefront-sdk`의 `easyPayProviderSchema`와 같은 값이다(api 계약 테스트가 고정한다)
|
|
36
46
|
*/
|
|
37
|
-
export const easyPayProviderSchema = z.enum(["NAVERPAY", "KAKAOPAY", "TOSSPAY", "PAYCO"]);
|
|
47
|
+
export const easyPayProviderSchema = z.enum(["NAVERPAY", "KAKAOPAY", "TOSSPAY", "PAYCO", "PAYPAL"]);
|
|
38
48
|
export type EasyPayProvider = z.infer<typeof easyPayProviderSchema>;
|
|
39
49
|
|
|
40
50
|
/**
|
|
@@ -188,6 +198,55 @@ export const naverPayCredentialsInputSchema = z.object({
|
|
|
188
198
|
});
|
|
189
199
|
export type NaverPayCredentialsInput = z.infer<typeof naverPayCredentialsInputSchema>;
|
|
190
200
|
|
|
201
|
+
/**
|
|
202
|
+
* PayPal이 받는 통화 중 sayren이 여는 것(이슈 #131 설계 §3b) — PayPal 통화 코드(https://developer.paypal.com/api/rest/reference/currency-codes/) ∩ 통화표
|
|
203
|
+
* − 원화(PayPal이 받지 않는다) − CNY·MYR(현지 계정 전용) − TWD(부분 환불 소수 문제, 설계 Q4)
|
|
204
|
+
*/
|
|
205
|
+
export const PAYPAL_CURRENCIES = [
|
|
206
|
+
"USD",
|
|
207
|
+
"EUR",
|
|
208
|
+
"JPY",
|
|
209
|
+
"GBP",
|
|
210
|
+
"HKD",
|
|
211
|
+
"SGD",
|
|
212
|
+
"AUD",
|
|
213
|
+
"CAD",
|
|
214
|
+
"THB",
|
|
215
|
+
"PHP",
|
|
216
|
+
"NZD",
|
|
217
|
+
"CHF",
|
|
218
|
+
] as const;
|
|
219
|
+
|
|
220
|
+
export const payPalCredentialsInputSchema = z.object({
|
|
221
|
+
clientId: z
|
|
222
|
+
.string()
|
|
223
|
+
.trim()
|
|
224
|
+
.min(10)
|
|
225
|
+
.max(200)
|
|
226
|
+
.describe("PayPal REST 앱의 Client ID(테스트 세트는 샌드박스 앱). 공개 값"),
|
|
227
|
+
currencies: z
|
|
228
|
+
.array(z.enum(PAYPAL_CURRENCIES))
|
|
229
|
+
.min(1)
|
|
230
|
+
.max(PAYPAL_CURRENCIES.length)
|
|
231
|
+
.describe(
|
|
232
|
+
"받는 통화 — PayPal 계정에 열어 둔 통화를 고른다. 결제 세션 통화가 여기 있는 주문서에만 PayPal 옵션이 나온다(원화는 받지 않는다)",
|
|
233
|
+
),
|
|
234
|
+
webhookId: z
|
|
235
|
+
.string()
|
|
236
|
+
.trim()
|
|
237
|
+
.min(1)
|
|
238
|
+
.max(100)
|
|
239
|
+
.nullable()
|
|
240
|
+
.optional()
|
|
241
|
+
.describe(
|
|
242
|
+
"PayPal 앱에 등록한 웹훅의 ID(선택). 주면 서버 통지의 서명을 검증해 결제 확인을 앞당긴다. 생략·null이면 통지를 받지 않고 주기 확인만 한다",
|
|
243
|
+
),
|
|
244
|
+
clientSecret: secretInput()
|
|
245
|
+
.optional()
|
|
246
|
+
.describe("Secret. 비밀 값. 최초 연결 시 필수, 수정 시 생략하면 기존 값을 유지한다"),
|
|
247
|
+
});
|
|
248
|
+
export type PayPalCredentialsInput = z.infer<typeof payPalCredentialsInputSchema>;
|
|
249
|
+
|
|
191
250
|
const upsertEnvironment = () =>
|
|
192
251
|
pgEnvironmentSchema.describe(
|
|
193
252
|
"저장할 자격 증명 세트. PG마다 `TEST`(샌드박스)·`LIVE`(실결제) 세트를 따로 둔다. " +
|
|
@@ -220,6 +279,12 @@ export const upsertNaverPayProviderRequestSchema = z.object({
|
|
|
220
279
|
});
|
|
221
280
|
export type UpsertNaverPayProviderRequest = z.infer<typeof upsertNaverPayProviderRequestSchema>;
|
|
222
281
|
|
|
282
|
+
export const upsertPayPalProviderRequestSchema = z.object({
|
|
283
|
+
environment: upsertEnvironment(),
|
|
284
|
+
credentials: payPalCredentialsInputSchema,
|
|
285
|
+
});
|
|
286
|
+
export type UpsertPayPalProviderRequest = z.infer<typeof upsertPayPalProviderRequestSchema>;
|
|
287
|
+
|
|
223
288
|
export const updatePaymentProviderRequestSchema = z
|
|
224
289
|
.object({
|
|
225
290
|
enabled: z
|
|
@@ -304,6 +369,11 @@ const publicConfigSchema = z.union([
|
|
|
304
369
|
z.object({ storeId: z.string(), channels: z.array(portOneChannelSchema) }),
|
|
305
370
|
z.object({ cid: z.string() }),
|
|
306
371
|
z.object({ clientId: z.string(), chainId: z.string() }),
|
|
372
|
+
z.object({
|
|
373
|
+
clientId: z.string(),
|
|
374
|
+
currencies: z.array(z.string()),
|
|
375
|
+
webhookId: z.string().nullable(),
|
|
376
|
+
}),
|
|
307
377
|
]);
|
|
308
378
|
|
|
309
379
|
const verificationSchema = z.object({
|
|
@@ -315,7 +385,7 @@ const verificationSchema = z.object({
|
|
|
315
385
|
export const paymentCredentialSetSchema = z.object({
|
|
316
386
|
environment: pgEnvironmentSchema,
|
|
317
387
|
publicConfig: publicConfigSchema.describe(
|
|
318
|
-
"공개 값. 토스페이먼츠는 `{clientKey}`, 포트원은 `{storeId, channels}`, 카카오페이는 `{cid}`, 네이버페이는 `{clientId, chainId}`",
|
|
388
|
+
"공개 값. 토스페이먼츠는 `{clientKey}`, 포트원은 `{storeId, channels}`, 카카오페이는 `{cid}`, 네이버페이는 `{clientId, chainId}`, PayPal은 `{clientId, currencies, webhookId}`",
|
|
319
389
|
),
|
|
320
390
|
secrets: z
|
|
321
391
|
.array(maskedSecretSchema)
|
|
@@ -340,7 +410,7 @@ export const liveGateConditionCodeSchema = z
|
|
|
340
410
|
.describe(
|
|
341
411
|
"라이브 개방 조건. `TEST_CONNECTION`=TEST 세트 연결 테스트 성공, `SANDBOX_PAYMENT`=현재 TEST 세트로 샌드박스 결제 1건 승인, " +
|
|
342
412
|
"`SANDBOX_REFUND`=그 샌드박스 결제의 환불 1건 성공, `LIVE_CONNECTION`=LIVE 세트 연결 테스트 성공, " +
|
|
343
|
-
"`PLATFORM_LIVE_ENABLED`=플랫폼이 실결제를 열어
|
|
413
|
+
"`PLATFORM_LIVE_ENABLED`=플랫폼이 실결제를 열어 둠(늘 충족), `NOT_TEMPLATE_STORE`=템플릿 상점이 아님(템플릿 상점에만 싣고 늘 미충족), " +
|
|
344
414
|
"`NOT_DEVELOPMENT_STORE`=앱 개발 상점이 아님(앱 개발 상점에만 싣고 늘 미충족)",
|
|
345
415
|
);
|
|
346
416
|
export type LiveGateConditionCode = z.infer<typeof liveGateConditionCodeSchema>;
|
|
@@ -362,7 +432,7 @@ export const liveGateSchema = z.object({
|
|
|
362
432
|
export type LiveGate = z.infer<typeof liveGateSchema>;
|
|
363
433
|
|
|
364
434
|
/**
|
|
365
|
-
* 409 `LIVE_GATE_NOT_MET
|
|
435
|
+
* 409 `LIVE_GATE_NOT_MET`의 `ApiError.details` 형태.
|
|
366
436
|
* 응답 파싱용이라 관대하다 — 알 수 없는 키는 버리고, 형태가 다르면 safeParse 실패로 처리한다.
|
|
367
437
|
*/
|
|
368
438
|
export const liveGateErrorDetailsSchema = z.object({
|
|
@@ -371,7 +441,7 @@ export const liveGateErrorDetailsSchema = z.object({
|
|
|
371
441
|
export type LiveGateErrorDetails = z.infer<typeof liveGateErrorDetailsSchema>;
|
|
372
442
|
|
|
373
443
|
export const paymentProviderSchema = z.object({
|
|
374
|
-
provider:
|
|
444
|
+
provider: pgProviderResponseSchema,
|
|
375
445
|
displayName: z.string().describe("표시 이름 (토스페이먼츠·포트원)"),
|
|
376
446
|
connected: z.boolean().describe("자격 증명이 저장돼 있는가"),
|
|
377
447
|
enabled: z.boolean().describe("결제에 사용 중인가"),
|
|
@@ -499,7 +569,7 @@ export const paymentSettingsSchema = z.object({
|
|
|
499
569
|
providers: z
|
|
500
570
|
.array(paymentProviderSchema)
|
|
501
571
|
.describe(
|
|
502
|
-
"연결된 PG(우선순위순)와 연결되지 않은 토스페이먼츠·포트원.
|
|
572
|
+
"연결된 PG(우선순위순)와 연결되지 않은 토스페이먼츠·포트원. 그 밖의 PG(카카오페이·네이버페이 등)는 연결했을 때만 여기에 온다" +
|
|
503
573
|
"(이 PG를 모르는 이전 SDK가 응답을 읽을 수 있게). 연결할 수 있는 PG 전체는 `connectableProviders`다",
|
|
504
574
|
),
|
|
505
575
|
connectableProviders: z
|
|
@@ -539,7 +609,7 @@ export const paymentSettingsSchema = z.object({
|
|
|
539
609
|
export type PaymentSettings = z.infer<typeof paymentSettingsSchema>;
|
|
540
610
|
|
|
541
611
|
export const paymentProviderVerificationSchema = z.object({
|
|
542
|
-
provider:
|
|
612
|
+
provider: pgProviderResponseSchema,
|
|
543
613
|
environment: pgEnvironmentSchema.describe("연결 테스트한 자격 증명 세트"),
|
|
544
614
|
result: pgVerificationResultSchema,
|
|
545
615
|
message: z.string().nullable().describe("실패 사유 또는 참고 메시지"),
|
|
@@ -592,7 +662,7 @@ export type SalesTotals = z.infer<typeof salesTotalsSchema>;
|
|
|
592
662
|
|
|
593
663
|
export const salesReportRowSchema = salesTotalsSchema.extend({
|
|
594
664
|
date: z.string().nullable().describe("일자 (YYYY-MM-DD, Asia/Seoul). groupBy=DAY일 때만 값"),
|
|
595
|
-
provider:
|
|
665
|
+
provider: pgProviderResponseSchema.nullable().describe("PG. groupBy에 PROVIDER가 포함될 때만 값"),
|
|
596
666
|
method: pgPaymentMethodSchema
|
|
597
667
|
.nullable()
|
|
598
668
|
.describe(
|
|
@@ -602,17 +672,36 @@ export const salesReportRowSchema = salesTotalsSchema.extend({
|
|
|
602
672
|
});
|
|
603
673
|
export type SalesReportRow = z.infer<typeof salesReportRowSchema>;
|
|
604
674
|
|
|
675
|
+
/** 통화별 매출(이슈 #130) — 환산하지 않는다. 금액은 그 통화의 최소 단위다 */
|
|
676
|
+
export const salesReportCurrencySchema = z.object({
|
|
677
|
+
currency: z.string().describe("결제 통화(ISO 4217)"),
|
|
678
|
+
summary: salesTotalsSchema.describe("이 통화의 기간 전체 합계"),
|
|
679
|
+
rows: z.array(salesReportRowSchema).describe("이 통화의 집계 행(`groupBy` 기준)"),
|
|
680
|
+
});
|
|
681
|
+
export type SalesReportCurrency = z.infer<typeof salesReportCurrencySchema>;
|
|
682
|
+
|
|
605
683
|
export const salesReportSchema = z.object({
|
|
606
684
|
from: z.string(),
|
|
607
685
|
to: z.string(),
|
|
608
686
|
timezone: z.literal("Asia/Seoul"),
|
|
609
|
-
currency: z
|
|
687
|
+
currency: z
|
|
688
|
+
.literal("KRW")
|
|
689
|
+
.describe(
|
|
690
|
+
"`summary`·`rows`의 통화. 기본 통화(원화) 결제만 집계한다. 외화 결제는 `byCurrency`에 있다",
|
|
691
|
+
),
|
|
610
692
|
groupBy: salesReportGroupBySchema,
|
|
611
|
-
provider:
|
|
693
|
+
provider: pgProviderResponseSchema.nullable().describe("적용된 PG 필터"),
|
|
612
694
|
method: pgPaymentMethodSchema.nullable().describe("적용된 결제수단 필터"),
|
|
613
695
|
includeTest: z.boolean().describe("테스트 결제(샌드박스 결제)를 집계에 포함했는가"),
|
|
614
696
|
summary: salesTotalsSchema.describe("기간 전체 합계"),
|
|
615
697
|
rows: z.array(salesReportRowSchema).describe("집계 행. 활동이 없는 날짜·PG는 생략된다"),
|
|
698
|
+
byCurrency: z
|
|
699
|
+
.array(salesReportCurrencySchema)
|
|
700
|
+
.optional()
|
|
701
|
+
.default([])
|
|
702
|
+
.describe(
|
|
703
|
+
"통화별 매출(이슈 #130) — 활동이 있는 통화마다 한 항목이고 기본 통화(KRW)도 포함한다. 통화 사이는 환산하지 않는다. 옛 서버 응답은 빈 목록",
|
|
704
|
+
),
|
|
616
705
|
});
|
|
617
706
|
export type SalesReport = z.infer<typeof salesReportSchema>;
|
|
618
707
|
|
|
@@ -677,7 +766,9 @@ export const cashReceiptSchema = z.object({
|
|
|
677
766
|
.int()
|
|
678
767
|
.describe("발급해야 할 금액 = 결제 금액 − 환불 완료 금액. 승인 전에는 결제 금액"),
|
|
679
768
|
issuedAmount: z.number().int().describe("PG에 현재 발급돼 있는 금액. 취소됐거나 발급 전이면 0"),
|
|
680
|
-
provider:
|
|
769
|
+
provider: pgProviderResponseSchema
|
|
770
|
+
.nullable()
|
|
771
|
+
.describe("발급 PG — 결제를 승인한 PG. 승인 전에는 null"),
|
|
681
772
|
issueNumber: z.string().nullable().describe("현금영수증 승인 번호"),
|
|
682
773
|
receiptUrl: z.string().nullable().describe("현금영수증 확인 주소"),
|
|
683
774
|
issuedAt: z.string().nullable().describe("최근 발급 시각 (ISO 8601)"),
|
|
@@ -708,7 +799,9 @@ export const paymentRecordSchema = z.object({
|
|
|
708
799
|
.describe(
|
|
709
800
|
"환불 대기 금액 합계(#115) — 주문만 취소하고 아직 환불하지 않은 몫. 결제 상세에서 환불 처리한다",
|
|
710
801
|
),
|
|
711
|
-
provider:
|
|
802
|
+
provider: pgProviderResponseSchema
|
|
803
|
+
.nullable()
|
|
804
|
+
.describe("승인한 PG. 승인 전에 끝났으면 마지막 시도의 PG"),
|
|
712
805
|
method: pgPaymentMethodSchema.nullable().describe("실제 결제수단. 모르면 요청 수단"),
|
|
713
806
|
easyPayProvider: easyPayProviderResponseSchema
|
|
714
807
|
.nullable()
|
|
@@ -764,7 +857,7 @@ export const paymentAttemptRecordSchema = z.object({
|
|
|
764
857
|
"PG에 보낸 주문번호(이슈 #107) — 토스페이먼츠 orderId·포트원 paymentId. 새 시도는 `{주문번호}-{시도 순번 2자리}`이고, 번호 체계 이전 시도는 시도 id(`attemptId`)와 같습니다. PG 관리자 화면에서 결제를 찾을 때 씁니다",
|
|
765
858
|
),
|
|
766
859
|
sequence: z.number().int().describe("결제 건 안에서의 순번 (1부터)"),
|
|
767
|
-
provider:
|
|
860
|
+
provider: pgProviderResponseSchema,
|
|
768
861
|
status: paymentAttemptStatusSchema,
|
|
769
862
|
method: pgPaymentMethodSchema.describe("주문서에서 고른 결제수단"),
|
|
770
863
|
easyPayProvider: easyPayProviderResponseSchema
|
|
@@ -876,7 +969,7 @@ export type SetPlatformTestKeysRequest = z.infer<typeof setPlatformTestKeysReque
|
|
|
876
969
|
|
|
877
970
|
/** 결제 상태 PG 한 줄 — 비밀 값·공개 키 없이 연결·사용·환경·테스트모드 키만 */
|
|
878
971
|
export const paymentStatusProviderSchema = z.object({
|
|
879
|
-
provider:
|
|
972
|
+
provider: pgProviderResponseSchema,
|
|
880
973
|
displayName: z.string(),
|
|
881
974
|
connected: z.boolean().describe("자격 증명이 저장돼 있는가"),
|
|
882
975
|
enabled: z.boolean().describe("결제에 사용 중인가"),
|
package/src/schemas/points.ts
CHANGED
|
@@ -210,9 +210,7 @@ export type PointPolicyNotice = z.infer<typeof pointPolicyNoticeSchema>;
|
|
|
210
210
|
export const storePointPolicySchema = z.object({
|
|
211
211
|
available: z
|
|
212
212
|
.boolean()
|
|
213
|
-
.describe(
|
|
214
|
-
"적립금 설정을 바꿀 수 있는가. false면 준비 중이라 정책 저장이 409 `POINTS_UNAVAILABLE`이고 모든 적립이 멈춰 있다",
|
|
215
|
-
),
|
|
213
|
+
.describe("적립금 설정을 바꿀 수 있는가. 지금은 늘 true다(이전 응답 호환 필드)"),
|
|
216
214
|
configured: z.boolean().describe("셀러가 정책을 저장한 적이 있는가. false면 기본(모두 끔)이다"),
|
|
217
215
|
reviewReward: reviewRewardPolicySchema.describe("리뷰 적립. 정하지 않았으면 끔·0원"),
|
|
218
216
|
purchaseEarn: purchaseEarnPolicySchema.describe(
|
|
@@ -223,8 +221,7 @@ export const storePointPolicySchema = z.object({
|
|
|
223
221
|
.optional()
|
|
224
222
|
.default(false)
|
|
225
223
|
.describe(
|
|
226
|
-
"적립금 사용(결제에 쓰기)을 켤 수 있는가 (#93).
|
|
227
|
-
"필드가 없는 이전 응답은 false로 읽는다",
|
|
224
|
+
"적립금 사용(결제에 쓰기)을 켤 수 있는가 (#93). 지금은 늘 true다. 필드가 없는 이전 응답은 false로 읽는다",
|
|
228
225
|
),
|
|
229
226
|
use: pointUsePolicySchema
|
|
230
227
|
.optional()
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 상품정보제공 고시·인증 정보·안내 섹션 (이슈 #135).
|
|
5
|
+
*
|
|
6
|
+
* **코드 값은 문자열이다.** 품목군(`schemaId`)·인증 종류·대상·면제 사유는 플랫폼 코드 표(서버 DB)가 원천이고, 코드가 늘어도 이 SDK의
|
|
7
|
+
* 계약이 깨지지 않게 zod enum으로 막지 않는다. 서버가 코드 표에 있는지 검사한다(없으면 400 `INVALID_INFO_CODE`).
|
|
8
|
+
* enum으로 두는 값은 서버가 그 값으로 분기하는 인증 상태(`CERTIFICATION_STATUSES`)뿐이다.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// ── 플랫폼 코드 표 ────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 칸 값의 출처 — 셀러가 비운 칸을 서버가 채운다. 서버가 분기하는 값이라 형태가 정해져 있지만, 새 출처가 늘 수 있어 응답은 열린 객체다.
|
|
15
|
+
* - `legal`: 법정 문구(`text`). 셀러가 적지 않는다
|
|
16
|
+
* - `option`: 이름이 `names` 중 하나인 조합 옵션명의 값 목록
|
|
17
|
+
* - `store`: 상점 필드(`field`, 예: `customerCenterPhone`)
|
|
18
|
+
*/
|
|
19
|
+
export const infoFieldSourceSchema = z.looseObject({
|
|
20
|
+
type: z.string(),
|
|
21
|
+
text: z.string().optional(),
|
|
22
|
+
names: z.array(z.string()).optional(),
|
|
23
|
+
field: z.string().optional(),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export type InfoFieldSource = z.infer<typeof infoFieldSourceSchema>;
|
|
27
|
+
|
|
28
|
+
export const infoSchemaFieldSchema = z.object({
|
|
29
|
+
key: z.string(),
|
|
30
|
+
label: z.string(),
|
|
31
|
+
required: z
|
|
32
|
+
.boolean()
|
|
33
|
+
.describe("법정 필수 칸인가. 서버는 비어 있어도 막지 않는다(표시는 「상품상세 참조」)"),
|
|
34
|
+
maxLength: z.number().int(),
|
|
35
|
+
source: infoFieldSourceSchema.nullable().optional().describe("값 출처. 없으면 셀러가 적는 칸"),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export type InfoSchemaField = z.infer<typeof infoSchemaFieldSchema>;
|
|
39
|
+
|
|
40
|
+
/** 정형 정보 스키마 — 고시 품목군 하나(`kind: "notice"`). 칸 구성은 버전마다 고정이고 상품은 저장한 버전으로 표시된다 */
|
|
41
|
+
export const infoSchemaSchema = z.object({
|
|
42
|
+
schemaId: z.string().describe("품목군 코드(예: `WEAR`)"),
|
|
43
|
+
version: z.number().int(),
|
|
44
|
+
kind: z.string().describe("스키마 종류. 지금은 `notice`(상품정보제공 고시)"),
|
|
45
|
+
name: z.string(),
|
|
46
|
+
fallback: z.string().describe("비운 칸의 표시 문구(예: 「상품상세 참조」)"),
|
|
47
|
+
fields: z.array(infoSchemaFieldSchema).describe("칸. 배열 순서가 표시 순서다"),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
export type InfoSchema = z.infer<typeof infoSchemaSchema>;
|
|
51
|
+
|
|
52
|
+
/** 플랫폼 코드 하나 — 코드 묶음(`setId`) 안에서 유일하다. `attrs`는 묶음마다 다른 부가 정보다 */
|
|
53
|
+
export const platformCodeSchema = z.object({
|
|
54
|
+
code: z.string(),
|
|
55
|
+
label: z.string(),
|
|
56
|
+
attrs: z
|
|
57
|
+
.record(z.string(), z.unknown())
|
|
58
|
+
.describe(
|
|
59
|
+
"부가 정보. `certification.subject`는 `kinds`(이 대상을 충족하는 인증 종류)·`exemptions`(허용 면제 사유)를 싣는다",
|
|
60
|
+
),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
export type PlatformCode = z.infer<typeof platformCodeSchema>;
|
|
64
|
+
|
|
65
|
+
/** 코드 묶음 — `GET /v1/codes/{setId}` */
|
|
66
|
+
export const PLATFORM_CODE_SETS = {
|
|
67
|
+
certificationKind: "certification.kind",
|
|
68
|
+
certificationSubject: "certification.subject",
|
|
69
|
+
certificationExemption: "certification.exemption",
|
|
70
|
+
} as const;
|
|
71
|
+
|
|
72
|
+
// ── 상품 고시 ────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
export const productNoticeSchema = z.object({
|
|
75
|
+
schemaId: z.string(),
|
|
76
|
+
version: z.number().int(),
|
|
77
|
+
values: z.record(z.string(), z.string()).describe("칸 키 → 셀러가 적은 값. 비운 칸은 없다"),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
export type ProductNotice = z.infer<typeof productNoticeSchema>;
|
|
81
|
+
|
|
82
|
+
export const MAX_NOTICE_VALUE_LENGTH = 1500;
|
|
83
|
+
|
|
84
|
+
export const productNoticeRequestSchema = z.object({
|
|
85
|
+
schemaId: z.string().trim().min(1).max(50).describe("품목군 코드(`GET /v1/info-schemas`)"),
|
|
86
|
+
version: z.number().int().positive().optional().describe("품목군 버전. 생략하면 현재 버전"),
|
|
87
|
+
values: z
|
|
88
|
+
.record(z.string().max(50), z.string().max(MAX_NOTICE_VALUE_LENGTH))
|
|
89
|
+
.default({})
|
|
90
|
+
.describe(
|
|
91
|
+
"칸 키 → 값. 출처가 `legal`인 칸은 받지 않는다(400). 빈 문자열은 비운 칸으로 저장하지 않는다. 칸 길이는 스키마 `maxLength`",
|
|
92
|
+
),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
export type ProductNoticeRequest = z.infer<typeof productNoticeRequestSchema>;
|
|
96
|
+
|
|
97
|
+
// ── 상품 인증 ────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
/** 인증 대상 상태 — 서버가 분기한다(대상이면 인증서 필수, 면제면 사유 필수) */
|
|
100
|
+
export const CERTIFICATION_STATUSES = ["REQUIRED", "NOT_REQUIRED", "EXEMPT"] as const;
|
|
101
|
+
|
|
102
|
+
export type CertificationStatus = (typeof CERTIFICATION_STATUSES)[number];
|
|
103
|
+
|
|
104
|
+
export const MAX_CERTIFICATION_ITEMS = 20;
|
|
105
|
+
|
|
106
|
+
export const productCertificationsSchema = z.object({
|
|
107
|
+
subjects: z
|
|
108
|
+
.array(
|
|
109
|
+
z.object({
|
|
110
|
+
subject: z.string().describe("인증 대상 코드(`certification.subject`)"),
|
|
111
|
+
status: z
|
|
112
|
+
.string()
|
|
113
|
+
.describe("`REQUIRED`·`NOT_REQUIRED`·`EXEMPT`. 값이 늘 수 있어 문자열이다"),
|
|
114
|
+
exemption: z.string().nullable().describe("면제 사유 코드. `EXEMPT`일 때만 있다"),
|
|
115
|
+
}),
|
|
116
|
+
)
|
|
117
|
+
.describe("인증 대상 여부. 없는 대상은 `NOT_REQUIRED`다"),
|
|
118
|
+
items: z
|
|
119
|
+
.array(
|
|
120
|
+
z.object({
|
|
121
|
+
kind: z.string().describe("인증 종류 코드(`certification.kind`)"),
|
|
122
|
+
agency: z.string().nullable().describe("인증 기관"),
|
|
123
|
+
number: z.string().describe("인증 번호"),
|
|
124
|
+
}),
|
|
125
|
+
)
|
|
126
|
+
.describe("인증서. 배열 순서가 표시 순서다"),
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
export type ProductCertifications = z.infer<typeof productCertificationsSchema>;
|
|
130
|
+
|
|
131
|
+
export const productCertificationsRequestSchema = z.object({
|
|
132
|
+
subjects: z
|
|
133
|
+
.array(
|
|
134
|
+
z.object({
|
|
135
|
+
subject: z.string().trim().min(1).max(50),
|
|
136
|
+
status: z.enum(CERTIFICATION_STATUSES),
|
|
137
|
+
exemption: z.string().trim().min(1).max(50).nullish(),
|
|
138
|
+
}),
|
|
139
|
+
)
|
|
140
|
+
.max(20)
|
|
141
|
+
.default([])
|
|
142
|
+
.describe(
|
|
143
|
+
"인증 대상 여부. `REQUIRED`인데 그 대상의 인증 종류(`attrs.kinds`) 인증서가 `items`에 없으면 400 `CERTIFICATION_REQUIRED`, " +
|
|
144
|
+
"`EXEMPT`인데 사유가 없거나 대상이 허용하지 않는 사유면 400 `CERTIFICATION_EXEMPTION_INVALID`",
|
|
145
|
+
),
|
|
146
|
+
items: z
|
|
147
|
+
.array(
|
|
148
|
+
z.object({
|
|
149
|
+
kind: z.string().trim().min(1).max(50),
|
|
150
|
+
agency: z.string().trim().max(100).nullish(),
|
|
151
|
+
number: z.string().trim().min(1).max(100),
|
|
152
|
+
}),
|
|
153
|
+
)
|
|
154
|
+
.max(MAX_CERTIFICATION_ITEMS)
|
|
155
|
+
.default([]),
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
export type ProductCertificationsRequest = z.infer<typeof productCertificationsRequestSchema>;
|
|
159
|
+
export type ProductCertificationsRequestInput = z.input<typeof productCertificationsRequestSchema>;
|
|
160
|
+
|
|
161
|
+
// ── 안내 섹션 ────────────────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
/** 상품 하나에 연결할 수 있는 안내 섹션 수 */
|
|
164
|
+
export const MAX_PRODUCT_INFO_SECTIONS = 10;
|
|
165
|
+
export const MAX_INFO_SECTION_TITLE = 100;
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* 안내 섹션 — 사이즈 가이드·세탁 안내처럼 여러 상품이 같이 쓰는 글이다. **상품은 연결만 한다**(복사가 아니다).
|
|
169
|
+
* 섹션을 고치면 연결된 모든 상품의 표시가 바뀐다.
|
|
170
|
+
*/
|
|
171
|
+
export const infoSectionSchema = z.object({
|
|
172
|
+
infoSectionId: z.string(),
|
|
173
|
+
title: z.string(),
|
|
174
|
+
body: z.string().describe("본문 HTML(상세설명과 같은 정화 규칙)"),
|
|
175
|
+
bodyJson: z.string().nullable().describe("에디터 복원용 직렬화 상태"),
|
|
176
|
+
assignedProductCount: z.number().int().describe("이 섹션을 연결한 상품 수"),
|
|
177
|
+
createdAt: z.string(),
|
|
178
|
+
updatedAt: z.string(),
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
export type InfoSection = z.infer<typeof infoSectionSchema>;
|
|
182
|
+
|
|
183
|
+
export const infoSectionRequestSchema = z.object({
|
|
184
|
+
title: z.string().trim().min(1).max(MAX_INFO_SECTION_TITLE),
|
|
185
|
+
body: z.string().default(""),
|
|
186
|
+
bodyJson: z.string().nullable().optional(),
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
export type InfoSectionRequest = z.infer<typeof infoSectionRequestSchema>;
|
|
190
|
+
export type InfoSectionRequestInput = z.input<typeof infoSectionRequestSchema>;
|