@sayren/store-sdk 0.31.0 → 0.33.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 +2961 -829
- package/dist/index.mjs +753 -74
- package/package.json +1 -1
- package/src/client.ts +111 -4
- package/src/index.ts +4 -0
- package/src/schemas/analytics.ts +16 -0
- package/src/schemas/app-manifest.ts +1 -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 +19 -0
- package/src/schemas/inventory.ts +230 -0
- package/src/schemas/market.ts +76 -0
- package/src/schemas/misc.ts +2 -0
- package/src/schemas/order.ts +16 -1
- package/src/schemas/payment.ts +107 -14
- package/src/schemas/product.ts +32 -3
- package/src/schemas/promotion.ts +36 -17
- package/src/schemas/refund-policy.ts +55 -2
- package/src/schemas/scope-descriptions.ts +4 -0
- package/src/schemas/scope.ts +5 -0
- package/src/schemas/shipping-policy.ts +56 -7
- package/src/schemas/store.ts +12 -0
- package/src/schemas/webhook-event.ts +75 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 위치별 재고 (이슈 #132) — 재고 위치(창고·상설 매장·팝업 매장)마다 조합의 가용 재고를 따로 센다. 출고지·반품지·픽업 장소가 위치를
|
|
5
|
+
* 가리키고, 배송 주문은 출고지 위치에서, 방문 수령 주문은 그 매장 위치에서 뺀다. 위치를 하나만 쓰는 상점은 기본 위치 하나다.
|
|
6
|
+
*
|
|
7
|
+
* - 가용 재고(`available`)는 지금 팔 수 있는 수량이다(팔린 미출고분이 이미 빠진 수). 저장하는 값이다.
|
|
8
|
+
* - 출고 대기(`pendingFulfillment`)는 그 위치에 할당됐고 아직 출고 전(주문상품 `PAID`·`CONFIRMED`)인 수량이다.
|
|
9
|
+
* - 실재고(`onHand`)는 가용 재고 + 출고 대기다. 위치에 실제로 있어야 하는 수량이다.
|
|
10
|
+
* - 상품·옵션 API의 `stockQuantity`는 그 상품 기준 위치의 가용 재고다(이름·뜻 유지).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** 재고 조정 사유 — `COUNT`는 실사 줄(`counted`)이 쓴다 */
|
|
14
|
+
export const inventoryAdjustmentReasonSchema = z
|
|
15
|
+
.enum(["RECEIVED", "DAMAGED", "LOST", "FOUND", "RETURN_RESTOCK", "CORRECTION", "COUNT"])
|
|
16
|
+
.describe(
|
|
17
|
+
"조정 사유 — 입고 `RECEIVED`, 파손 `DAMAGED`, 분실 `LOST`, 재발견 `FOUND`, 반품 재입고 `RETURN_RESTOCK`, 정정 `CORRECTION`, 실사 `COUNT`",
|
|
18
|
+
);
|
|
19
|
+
export type InventoryAdjustmentReason = z.infer<typeof inventoryAdjustmentReasonSchema>;
|
|
20
|
+
|
|
21
|
+
export const inventoryLocationSchema = z.object({
|
|
22
|
+
inventoryLocationId: z.string(),
|
|
23
|
+
name: z.string().describe("위치 이름(예: 본사 창고, 성수 팝업)"),
|
|
24
|
+
isDefault: z
|
|
25
|
+
.boolean()
|
|
26
|
+
.describe("기본 위치인가 — 상점마다 하나이고 삭제할 수 없다. 기존 재고가 있는 곳이다"),
|
|
27
|
+
sortOrder: z.number().int(),
|
|
28
|
+
createdAt: z.string(),
|
|
29
|
+
updatedAt: z.string(),
|
|
30
|
+
});
|
|
31
|
+
export type InventoryLocation = z.infer<typeof inventoryLocationSchema>;
|
|
32
|
+
|
|
33
|
+
export const inventoryLocationRequestSchema = z.strictObject({
|
|
34
|
+
name: z.string().trim().min(1).max(50),
|
|
35
|
+
sortOrder: z.number().int().min(0).max(10_000).optional(),
|
|
36
|
+
});
|
|
37
|
+
export type InventoryLocationRequest = z.infer<typeof inventoryLocationRequestSchema>;
|
|
38
|
+
|
|
39
|
+
export const inventoryLevelSchema = z.object({
|
|
40
|
+
productId: z.string(),
|
|
41
|
+
variantId: z
|
|
42
|
+
.string()
|
|
43
|
+
.describe("판매 단위(variant) ID. 주문 상품의 `optionId`와 같은 값 공간이다"),
|
|
44
|
+
inventoryLocationId: z.string(),
|
|
45
|
+
available: z.number().int().describe("가용 재고"),
|
|
46
|
+
pendingFulfillment: z.number().int().describe("출고 대기 — 할당됐고 아직 출고 전인 수량"),
|
|
47
|
+
onHand: z.number().int().describe("실재고 — 가용 재고 + 출고 대기"),
|
|
48
|
+
version: z
|
|
49
|
+
.number()
|
|
50
|
+
.int()
|
|
51
|
+
.describe("쓰기마다 1씩 오르는 버전(`INVENTORY.CHANGED`의 `levelVersion`)"),
|
|
52
|
+
updatedAt: z.string(),
|
|
53
|
+
});
|
|
54
|
+
export type InventoryLevel = z.infer<typeof inventoryLevelSchema>;
|
|
55
|
+
|
|
56
|
+
export const inventoryLevelListQuerySchema = z.object({
|
|
57
|
+
inventoryLocationId: z.string().optional(),
|
|
58
|
+
productId: z.string().optional(),
|
|
59
|
+
variantId: z.string().optional(),
|
|
60
|
+
cursor: z.string().optional().describe("이전 응답의 `nextCursor`. 생략하면 처음부터"),
|
|
61
|
+
limit: z.coerce.number().int().min(1).max(200).optional().describe("한 쪽 크기(기본 100)"),
|
|
62
|
+
});
|
|
63
|
+
export type InventoryLevelListQuery = z.infer<typeof inventoryLevelListQuerySchema>;
|
|
64
|
+
|
|
65
|
+
export const inventoryLevelPageSchema = z.object({
|
|
66
|
+
contents: z.array(inventoryLevelSchema),
|
|
67
|
+
nextCursor: z.string().nullable().describe("다음 페이지 커서. 마지막 페이지면 null"),
|
|
68
|
+
});
|
|
69
|
+
export type InventoryLevelPage = z.infer<typeof inventoryLevelPageSchema>;
|
|
70
|
+
|
|
71
|
+
export const inventoryMovementKindSchema = z
|
|
72
|
+
.enum([
|
|
73
|
+
"OPENING",
|
|
74
|
+
"SALE",
|
|
75
|
+
"RESTORE",
|
|
76
|
+
"REVERT",
|
|
77
|
+
"EXCHANGE_OUT",
|
|
78
|
+
"ADJUST",
|
|
79
|
+
"COUNT",
|
|
80
|
+
"TRANSFER_OUT",
|
|
81
|
+
"TRANSFER_IN",
|
|
82
|
+
"REMOVED",
|
|
83
|
+
])
|
|
84
|
+
.describe(
|
|
85
|
+
"변동 종류 — 시작 `OPENING`, 판매 `SALE`, 복원 `RESTORE`, 복원 되돌리기 `REVERT`, 교환 출고 `EXCHANGE_OUT`, 조정 `ADJUST`, 실사 `COUNT`, 이동 출고 `TRANSFER_OUT`, 이동 입고 `TRANSFER_IN`, 조합 삭제 `REMOVED`",
|
|
86
|
+
);
|
|
87
|
+
export type InventoryMovementKind = z.infer<typeof inventoryMovementKindSchema>;
|
|
88
|
+
|
|
89
|
+
export const inventoryMovementSchema = z.object({
|
|
90
|
+
movementId: z.string(),
|
|
91
|
+
seq: z
|
|
92
|
+
.number()
|
|
93
|
+
.int()
|
|
94
|
+
.describe("상점별 순번 — 커밋 순서와 같고 빈 번호가 없다. `after` 커서에 쓴다"),
|
|
95
|
+
productId: z.string(),
|
|
96
|
+
variantId: z.string(),
|
|
97
|
+
inventoryLocationId: z.string(),
|
|
98
|
+
kind: z.string().describe("변동 종류(`inventoryMovementKindSchema`). 값이 늘 수 있다"),
|
|
99
|
+
delta: z.number().int().describe("가용 재고 변화량"),
|
|
100
|
+
available: z.number().int().describe("변화 뒤 가용 재고"),
|
|
101
|
+
levelVersion: z.number().int(),
|
|
102
|
+
reason: z.string().nullable(),
|
|
103
|
+
note: z.string().nullable(),
|
|
104
|
+
refType: z.string().nullable().describe("출처 유형 — `order_item`·`claim`·`operation`"),
|
|
105
|
+
refId: z.string().nullable(),
|
|
106
|
+
operationId: z.string().nullable().describe("재고 조정·이동 요청 ID"),
|
|
107
|
+
actorType: z.string().describe("행위자 유형 — `seller`·`app`·`agent`·`system`"),
|
|
108
|
+
actorId: z.string().nullable(),
|
|
109
|
+
createdAt: z.string(),
|
|
110
|
+
});
|
|
111
|
+
export type InventoryMovement = z.infer<typeof inventoryMovementSchema>;
|
|
112
|
+
|
|
113
|
+
export const inventoryMovementListQuerySchema = z.object({
|
|
114
|
+
inventoryLocationId: z.string().optional(),
|
|
115
|
+
productId: z.string().optional(),
|
|
116
|
+
variantId: z.string().optional(),
|
|
117
|
+
kind: inventoryMovementKindSchema.optional(),
|
|
118
|
+
after: z.coerce
|
|
119
|
+
.number()
|
|
120
|
+
.int()
|
|
121
|
+
.min(0)
|
|
122
|
+
.optional()
|
|
123
|
+
.describe("이 순번 다음부터(오름차순). 생략하면 처음부터"),
|
|
124
|
+
from: z.string().optional().describe("이 시각 이후(ISO 8601)"),
|
|
125
|
+
to: z.string().optional().describe("이 시각 이전(ISO 8601)"),
|
|
126
|
+
limit: z.coerce.number().int().min(1).max(200).optional().describe("한 쪽 크기(기본 100)"),
|
|
127
|
+
});
|
|
128
|
+
export type InventoryMovementListQuery = z.infer<typeof inventoryMovementListQuerySchema>;
|
|
129
|
+
|
|
130
|
+
export const inventoryMovementPageSchema = z.object({
|
|
131
|
+
contents: z.array(inventoryMovementSchema),
|
|
132
|
+
nextAfter: z
|
|
133
|
+
.number()
|
|
134
|
+
.int()
|
|
135
|
+
.nullable()
|
|
136
|
+
.describe(
|
|
137
|
+
"다음 쪽의 `after` 값. 더 없으면 null(새 변동을 기다릴 때는 마지막 `seq`를 그대로 쓴다)",
|
|
138
|
+
),
|
|
139
|
+
});
|
|
140
|
+
export type InventoryMovementPage = z.infer<typeof inventoryMovementPageSchema>;
|
|
141
|
+
|
|
142
|
+
/** 한 요청의 줄 상한 — 30초 타임아웃·바인드 한도 */
|
|
143
|
+
export const MAX_INVENTORY_OPERATION_LINES = 200;
|
|
144
|
+
|
|
145
|
+
export const inventoryAdjustmentLineSchema = z
|
|
146
|
+
.strictObject({
|
|
147
|
+
variantId: z.string().min(1),
|
|
148
|
+
inventoryLocationId: z.string().min(1).optional().describe("생략하면 기본 위치"),
|
|
149
|
+
delta: z
|
|
150
|
+
.number()
|
|
151
|
+
.int()
|
|
152
|
+
.refine((value) => value !== 0, { message: "delta는 0이 아니어야 합니다" })
|
|
153
|
+
.optional()
|
|
154
|
+
.describe("가용 재고 증감. 감소는 가용 재고를 넘지 않는다"),
|
|
155
|
+
counted: z
|
|
156
|
+
.number()
|
|
157
|
+
.int()
|
|
158
|
+
.min(0)
|
|
159
|
+
.optional()
|
|
160
|
+
.describe(
|
|
161
|
+
"실사 — 센 실물 수량. 실재고가 이 값이 되도록 가용 재고를 맞춘다(센 값 − 출고 대기)",
|
|
162
|
+
),
|
|
163
|
+
})
|
|
164
|
+
.refine((line) => (line.delta === undefined) !== (line.counted === undefined), {
|
|
165
|
+
message: "줄마다 delta와 counted 중 하나만 줍니다",
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
export const inventoryAdjustmentRequestSchema = z.strictObject({
|
|
169
|
+
lines: z.array(inventoryAdjustmentLineSchema).min(1).max(MAX_INVENTORY_OPERATION_LINES),
|
|
170
|
+
reason: inventoryAdjustmentReasonSchema
|
|
171
|
+
.optional()
|
|
172
|
+
.describe("증감 줄(`delta`)의 사유. 실사 줄은 늘 `COUNT`다. 증감 줄이 있으면 필수"),
|
|
173
|
+
note: z.string().trim().max(500).optional(),
|
|
174
|
+
reference: z.string().trim().max(100).optional().describe("외부 참조(입고서 번호 등)"),
|
|
175
|
+
});
|
|
176
|
+
export type InventoryAdjustmentRequest = z.infer<typeof inventoryAdjustmentRequestSchema>;
|
|
177
|
+
|
|
178
|
+
export const inventoryTransferRequestSchema = z.strictObject({
|
|
179
|
+
fromInventoryLocationId: z.string().min(1),
|
|
180
|
+
toInventoryLocationId: z.string().min(1),
|
|
181
|
+
lines: z
|
|
182
|
+
.array(z.strictObject({ variantId: z.string().min(1), quantity: z.number().int().min(1) }))
|
|
183
|
+
.min(1)
|
|
184
|
+
.max(MAX_INVENTORY_OPERATION_LINES),
|
|
185
|
+
note: z.string().trim().max(500).optional(),
|
|
186
|
+
});
|
|
187
|
+
export type InventoryTransferRequest = z.infer<typeof inventoryTransferRequestSchema>;
|
|
188
|
+
|
|
189
|
+
export const inventoryOperationResultSchema = z.object({
|
|
190
|
+
operationId: z.string(),
|
|
191
|
+
kind: z.enum(["ADJUSTMENT", "TRANSFER"]),
|
|
192
|
+
lines: z.array(
|
|
193
|
+
z.object({
|
|
194
|
+
variantId: z.string(),
|
|
195
|
+
inventoryLocationId: z.string(),
|
|
196
|
+
delta: z.number().int().describe("적용한 가용 재고 변화량(실사는 맞춘 차이, 같으면 0)"),
|
|
197
|
+
available: z.number().int().describe("적용 뒤 가용 재고"),
|
|
198
|
+
}),
|
|
199
|
+
),
|
|
200
|
+
createdAt: z.string(),
|
|
201
|
+
});
|
|
202
|
+
export type InventoryOperationResult = z.infer<typeof inventoryOperationResultSchema>;
|
|
203
|
+
|
|
204
|
+
/** 수거 완료의 재입고 지정(Q3) — 생략하면 조합은 할당 순서(교환 → 판매), 위치는 반품지에 연결된 위치 → 할당 위치다 */
|
|
205
|
+
export const claimCollectCompleteRequestSchema = z.object({
|
|
206
|
+
restock: z
|
|
207
|
+
.object({
|
|
208
|
+
optionId: z
|
|
209
|
+
.string()
|
|
210
|
+
.min(1)
|
|
211
|
+
.optional()
|
|
212
|
+
.describe(
|
|
213
|
+
"돌아온 조합 — 그 주문상품의 할당 조합이어야 한다(아니면 400 `INVALID_RESTOCK_OPTION`)",
|
|
214
|
+
),
|
|
215
|
+
inventoryLocationId: z.string().min(1).optional().describe("재입고할 재고 위치"),
|
|
216
|
+
})
|
|
217
|
+
.optional(),
|
|
218
|
+
});
|
|
219
|
+
export type ClaimCollectCompleteRequest = z.infer<typeof claimCollectCompleteRequestSchema>;
|
|
220
|
+
|
|
221
|
+
export const claimRedeliveryRequestSchema = z.object({
|
|
222
|
+
carrierCode: z.string().min(1),
|
|
223
|
+
trackingNumber: z.string().min(1),
|
|
224
|
+
inventoryLocationId: z
|
|
225
|
+
.string()
|
|
226
|
+
.min(1)
|
|
227
|
+
.optional()
|
|
228
|
+
.describe("교환품을 보낼 재고 위치. 생략하면 그 상품의 기준 위치"),
|
|
229
|
+
});
|
|
230
|
+
export type ClaimRedeliveryRequest = z.infer<typeof claimRedeliveryRequestSchema>;
|
|
@@ -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/misc.ts
CHANGED
|
@@ -162,6 +162,8 @@ export const eventTypeSchema = z
|
|
|
162
162
|
"ORDER.FLOW_NOTIFICATION",
|
|
163
163
|
// 커머스 플로우 상태 변경(이슈 #115). 흐름 단계가 바뀔 때마다 간다
|
|
164
164
|
"ORDER.FLOW_STATE_CHANGED",
|
|
165
|
+
// 위치별 재고 변화(이슈 #132). 재고 변동 내역 한 행마다 간다
|
|
166
|
+
"INVENTORY.CHANGED",
|
|
165
167
|
])
|
|
166
168
|
.describe(
|
|
167
169
|
"웹훅 이벤트 타입. `ORDER.ADDRESS_CHANGE_REQUESTED`는 현재 발송하지 않음(호환을 위해 값만 남았고, 구독해도 이벤트가 오지 않는다)",
|
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)
|
|
@@ -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("결제에 사용 중인가"),
|