@sayren/store-sdk 0.1.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 +5338 -0
- package/dist/index.mjs +2300 -0
- package/dist/server.d.mts +59 -0
- package/dist/server.mjs +125 -0
- package/dist/webhook-headers-D6QmPdEJ.d.mts +22 -0
- package/dist/webhook-headers-ap_exsb9.mjs +22 -0
- package/package.json +48 -0
- package/src/base64.ts +28 -0
- package/src/client.ts +877 -0
- package/src/http.ts +194 -0
- package/src/index.ts +35 -0
- package/src/oauth.ts +132 -0
- package/src/schemas/api-client.ts +121 -0
- package/src/schemas/api-log.ts +104 -0
- package/src/schemas/auth.ts +81 -0
- package/src/schemas/category.ts +98 -0
- package/src/schemas/claim.ts +89 -0
- package/src/schemas/common.ts +61 -0
- package/src/schemas/misc.ts +298 -0
- package/src/schemas/order.ts +233 -0
- package/src/schemas/payment.ts +582 -0
- package/src/schemas/product.ts +272 -0
- package/src/schemas/scope.ts +75 -0
- package/src/schemas/store.ts +143 -0
- package/src/schemas/upload.ts +23 -0
- package/src/schemas/webhook-event.ts +145 -0
- package/src/server.ts +24 -0
- package/src/session.ts +249 -0
- package/src/webhook-headers.ts +21 -0
- package/src/webhook-signature.ts +177 -0
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type PageParams, pageSchema } from "./common";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 스토어 결제(PG) 설정과 매출 리포트 계약.
|
|
6
|
+
*
|
|
7
|
+
* PG는 셀러가 직접 계약한다. 자격 증명은 스토어별로 저장되고, 비밀 값(시크릿 키·API Secret·
|
|
8
|
+
* 웹훅 시크릿)은 저장 후 다시 조회할 수 없다. 응답에는 앞 4자리 마스킹만 나온다. 공개 값
|
|
9
|
+
* (클라이언트 키·Store ID·채널 키)은 원래 브라우저 결제창에 노출되는 값이라 그대로 돌려준다.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export const pgProviderSchema = z
|
|
13
|
+
.enum(["tosspayments", "portone"])
|
|
14
|
+
.describe("PG 식별자. `tosspayments`=토스페이먼츠, `portone`=포트원(V2)");
|
|
15
|
+
export type PgProvider = z.infer<typeof pgProviderSchema>;
|
|
16
|
+
|
|
17
|
+
export const pgPaymentMethodSchema = z
|
|
18
|
+
.enum(["CARD", "BANK_TRANSFER", "VIRTUAL_ACCOUNT", "MOBILE", "EASY_PAY"])
|
|
19
|
+
.describe("결제수단 (스토어프론트 결제 요청의 `paymentMethod`와 같은 값)");
|
|
20
|
+
export type PgPaymentMethod = z.infer<typeof pgPaymentMethodSchema>;
|
|
21
|
+
|
|
22
|
+
export const paymentRoutingModeSchema = z
|
|
23
|
+
.enum(["PRIORITY_DISPLAY", "AUTO_FAILOVER"])
|
|
24
|
+
.describe(
|
|
25
|
+
"PG가 둘 이상 사용 중일 때의 라우팅 방식. `PRIORITY_DISPLAY`=결제창에 사용 중인 PG를 모두 보여 주고 1순위를 기본 선택, " +
|
|
26
|
+
"`AUTO_FAILOVER`=1순위로 결제창을 열고, 결제창을 띄우기 전 장애(PG 준비 실패)일 때만 다음 순위로 자동 전환",
|
|
27
|
+
);
|
|
28
|
+
export type PaymentRoutingMode = z.infer<typeof paymentRoutingModeSchema>;
|
|
29
|
+
|
|
30
|
+
export const effectivePaymentRoutingModeSchema = z
|
|
31
|
+
.enum(["NONE", "SINGLE", "PRIORITY_DISPLAY", "AUTO_FAILOVER"])
|
|
32
|
+
.describe(
|
|
33
|
+
"실제로 적용되는 라우팅. 사용 중인 PG가 0개면 `NONE`(결제 불가), 1개면 `SINGLE`, 2개 이상이면 `routingMode` 값",
|
|
34
|
+
);
|
|
35
|
+
export type EffectivePaymentRoutingMode = z.infer<typeof effectivePaymentRoutingModeSchema>;
|
|
36
|
+
|
|
37
|
+
export const pgEnvironmentSchema = z
|
|
38
|
+
.enum(["TEST", "LIVE"])
|
|
39
|
+
.describe(
|
|
40
|
+
"PG 결제 환경. `TEST`=PG 샌드박스(테스트 키, 실제로 돈이 오가지 않음), `LIVE`=실결제(운영 키)",
|
|
41
|
+
);
|
|
42
|
+
export type PgEnvironment = z.infer<typeof pgEnvironmentSchema>;
|
|
43
|
+
|
|
44
|
+
export const pgVerificationResultSchema = z
|
|
45
|
+
.enum(["SUCCESS", "FAILED"])
|
|
46
|
+
.describe("연결 테스트 결과");
|
|
47
|
+
export type PgVerificationResult = z.infer<typeof pgVerificationResultSchema>;
|
|
48
|
+
|
|
49
|
+
// ─── 자격 증명 입력 ─────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
/** 비밀 값 입력 — 저장 후 다시 조회할 수 없다 */
|
|
52
|
+
const secretInput = () => z.string().trim().min(8).max(512);
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 토스페이먼츠 결제위젯 키 접두사. 결제창은 API 개별 연동(`*_ck_`·`*_sk_`)만 쓴다. 위젯 키는 결제창 SDK가
|
|
56
|
+
* `NOT_SUPPORTED_WIDGET_KEY`로, 서버 API가 404 `NOT_FOUND_MERCHANT`로 거부하므로 저장 전에 막는다.
|
|
57
|
+
* 요청 스키마·api 어댑터·콘솔 폼이 같은 상수를 쓴다(원천). `g` 플래그가 없어 `.test()`가 상태를 갖지 않는다.
|
|
58
|
+
*/
|
|
59
|
+
export const TOSS_WIDGET_CLIENT_KEY_PATTERN = /^(test|live)_gck_/;
|
|
60
|
+
/** 토스페이먼츠 결제위젯 시크릿 키 접두사. {@link TOSS_WIDGET_CLIENT_KEY_PATTERN} 참고 */
|
|
61
|
+
export const TOSS_WIDGET_SECRET_KEY_PATTERN = /^(test|live)_gsk_/;
|
|
62
|
+
/** 결제위젯 키를 거부할 때의 필드 메시지 */
|
|
63
|
+
export const TOSS_WIDGET_KEY_MESSAGE = "결제위젯 키가 아니라 API 개별 연동 키를 입력해주세요";
|
|
64
|
+
|
|
65
|
+
export const tossPaymentsCredentialsInputSchema = z
|
|
66
|
+
.object({
|
|
67
|
+
clientKey: z
|
|
68
|
+
.string()
|
|
69
|
+
.trim()
|
|
70
|
+
.min(8)
|
|
71
|
+
.max(200)
|
|
72
|
+
.describe("클라이언트 키 (`test_ck_…`/`live_ck_…`). 공개 값 — 결제창에서 쓰인다"),
|
|
73
|
+
secretKey: secretInput()
|
|
74
|
+
.optional()
|
|
75
|
+
.describe(
|
|
76
|
+
"시크릿 키 (`test_sk_…`/`live_sk_…`). 비밀 값. 최초 연결 시 필수, 수정 시 생략하면 기존 값을 유지한다",
|
|
77
|
+
),
|
|
78
|
+
webhookSecret: secretInput()
|
|
79
|
+
.nullable()
|
|
80
|
+
.optional()
|
|
81
|
+
.describe("웹훅 서명 검증용 보안 키. 비밀 값. 생략하면 기존 값 유지, null이면 삭제"),
|
|
82
|
+
})
|
|
83
|
+
.superRefine((value, ctx) => {
|
|
84
|
+
// 칸마다 따로 짚는다 — 둘 다 위젯 키면 이슈 2개
|
|
85
|
+
if (TOSS_WIDGET_CLIENT_KEY_PATTERN.test(value.clientKey)) {
|
|
86
|
+
ctx.addIssue({ code: "custom", path: ["clientKey"], message: TOSS_WIDGET_KEY_MESSAGE });
|
|
87
|
+
}
|
|
88
|
+
if (value.secretKey !== undefined && TOSS_WIDGET_SECRET_KEY_PATTERN.test(value.secretKey)) {
|
|
89
|
+
ctx.addIssue({ code: "custom", path: ["secretKey"], message: TOSS_WIDGET_KEY_MESSAGE });
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
export type TossPaymentsCredentialsInput = z.infer<typeof tossPaymentsCredentialsInputSchema>;
|
|
93
|
+
|
|
94
|
+
/** 포트원 채널 목록 최소 개수. 결제수단을 받으려면 채널이 하나는 있어야 한다 */
|
|
95
|
+
export const PORTONE_MIN_CHANNELS = 1;
|
|
96
|
+
/** 포트원 채널 목록 최대 개수 */
|
|
97
|
+
export const PORTONE_MAX_CHANNELS = 20;
|
|
98
|
+
/** 포트원 채널 표시 이름 최대 길이 */
|
|
99
|
+
export const PORTONE_CHANNEL_NAME_MAX_LENGTH = 50;
|
|
100
|
+
|
|
101
|
+
export const portOneChannelSchema = z.object({
|
|
102
|
+
channelKey: z
|
|
103
|
+
.string()
|
|
104
|
+
.trim()
|
|
105
|
+
.min(8)
|
|
106
|
+
.max(200)
|
|
107
|
+
.describe("포트원 콘솔 [연동 정보] > [채널 관리]의 채널 키. 공개 값"),
|
|
108
|
+
name: z
|
|
109
|
+
.string()
|
|
110
|
+
.trim()
|
|
111
|
+
.max(PORTONE_CHANNEL_NAME_MAX_LENGTH)
|
|
112
|
+
.nullable()
|
|
113
|
+
.optional()
|
|
114
|
+
.describe("채널 표시 이름 (예: 카드 전용)"),
|
|
115
|
+
payMethods: z
|
|
116
|
+
.array(pgPaymentMethodSchema)
|
|
117
|
+
.min(1)
|
|
118
|
+
.max(pgPaymentMethodSchema.options.length)
|
|
119
|
+
.describe("이 채널로 받을 결제수단. 결제수단마다 먼저 나열된 채널이 쓰인다"),
|
|
120
|
+
});
|
|
121
|
+
export type PortOneChannel = z.infer<typeof portOneChannelSchema>;
|
|
122
|
+
|
|
123
|
+
export const portOneCredentialsInputSchema = z.object({
|
|
124
|
+
storeId: z.string().trim().min(8).max(200).describe("포트원 Store ID (`store-…`). 공개 값"),
|
|
125
|
+
channels: z
|
|
126
|
+
.array(portOneChannelSchema)
|
|
127
|
+
.min(PORTONE_MIN_CHANNELS)
|
|
128
|
+
.max(PORTONE_MAX_CHANNELS)
|
|
129
|
+
.describe(
|
|
130
|
+
"채널 목록. 포트원 V2는 PG·결제수단별로 채널을 따로 추가하므로 여러 개를 등록할 수 있다",
|
|
131
|
+
),
|
|
132
|
+
apiSecret: secretInput()
|
|
133
|
+
.optional()
|
|
134
|
+
.describe("V2 API Secret. 비밀 값. 최초 연결 시 필수, 수정 시 생략하면 기존 값을 유지한다"),
|
|
135
|
+
webhookSecret: secretInput()
|
|
136
|
+
.nullable()
|
|
137
|
+
.optional()
|
|
138
|
+
.describe("웹훅 서명 검증용 시크릿. 비밀 값. 생략하면 기존 값 유지, null이면 삭제"),
|
|
139
|
+
});
|
|
140
|
+
export type PortOneCredentialsInput = z.infer<typeof portOneCredentialsInputSchema>;
|
|
141
|
+
|
|
142
|
+
const upsertEnvironment = () =>
|
|
143
|
+
pgEnvironmentSchema.describe(
|
|
144
|
+
"저장할 자격 증명 세트. PG마다 `TEST`(샌드박스)·`LIVE`(실결제) 세트를 따로 둔다. " +
|
|
145
|
+
"어느 세트로 결제할지는 이 값이 아니라 샌드박스 스위치(`sandboxMode`)가 정한다",
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
export const upsertTossPaymentsProviderRequestSchema = z.object({
|
|
149
|
+
environment: upsertEnvironment(),
|
|
150
|
+
credentials: tossPaymentsCredentialsInputSchema,
|
|
151
|
+
});
|
|
152
|
+
export type UpsertTossPaymentsProviderRequest = z.infer<
|
|
153
|
+
typeof upsertTossPaymentsProviderRequestSchema
|
|
154
|
+
>;
|
|
155
|
+
|
|
156
|
+
export const upsertPortOneProviderRequestSchema = z.object({
|
|
157
|
+
environment: upsertEnvironment(),
|
|
158
|
+
credentials: portOneCredentialsInputSchema,
|
|
159
|
+
});
|
|
160
|
+
export type UpsertPortOneProviderRequest = z.infer<typeof upsertPortOneProviderRequestSchema>;
|
|
161
|
+
|
|
162
|
+
export const updatePaymentProviderRequestSchema = z
|
|
163
|
+
.object({
|
|
164
|
+
enabled: z
|
|
165
|
+
.boolean()
|
|
166
|
+
.optional()
|
|
167
|
+
.describe(
|
|
168
|
+
"사용 여부. 켜려면 활성 환경(샌드박스면 TEST, 아니면 LIVE) 자격 증명이 모두 저장돼 있어야 한다(`PAYMENT_PROVIDER_INCOMPLETE`)",
|
|
169
|
+
),
|
|
170
|
+
sandboxMode: z
|
|
171
|
+
.boolean()
|
|
172
|
+
.optional()
|
|
173
|
+
.describe(
|
|
174
|
+
"샌드박스 스위치. `true`면 새 결제에 TEST 세트를 쓴다(항상 허용). `false`(라이브 전환)는 라이브 개방 조건을 " +
|
|
175
|
+
"모두 충족해야 한다(`LIVE_GATE_NOT_MET`)",
|
|
176
|
+
),
|
|
177
|
+
})
|
|
178
|
+
.refine((body) => body.enabled !== undefined || body.sandboxMode !== undefined, {
|
|
179
|
+
message: "enabled 또는 sandboxMode 중 하나는 보내야 해요",
|
|
180
|
+
});
|
|
181
|
+
export type UpdatePaymentProviderRequest = z.infer<typeof updatePaymentProviderRequestSchema>;
|
|
182
|
+
|
|
183
|
+
export const verifyPaymentProviderRequestSchema = z.object({
|
|
184
|
+
environment: pgEnvironmentSchema
|
|
185
|
+
.optional()
|
|
186
|
+
.describe("연결 테스트할 자격 증명 세트. 생략하면 활성 환경(샌드박스면 TEST, 아니면 LIVE)"),
|
|
187
|
+
});
|
|
188
|
+
export type VerifyPaymentProviderRequest = z.infer<typeof verifyPaymentProviderRequestSchema>;
|
|
189
|
+
|
|
190
|
+
export const updatePaymentSettingsRequestSchema = z.object({
|
|
191
|
+
routingMode: paymentRoutingModeSchema,
|
|
192
|
+
priority: z
|
|
193
|
+
.array(pgProviderSchema)
|
|
194
|
+
.min(1)
|
|
195
|
+
// PG 개수와 무관한 안전장치 — 거대한 배열이 파싱을 거치지 않게만 막는다. 실제 개수는 서버가 본다
|
|
196
|
+
.max(50)
|
|
197
|
+
.describe(
|
|
198
|
+
"PG 우선순위(앞이 1순위). 연결된 PG 수만큼 **모두 한 번씩** 나열한다(길이 상한 50은 안전장치다). " +
|
|
199
|
+
"빠지거나 중복되거나 연결되지 않은 PG가 있으면 `INVALID_PRIORITY`",
|
|
200
|
+
),
|
|
201
|
+
});
|
|
202
|
+
export type UpdatePaymentSettingsRequest = z.infer<typeof updatePaymentSettingsRequestSchema>;
|
|
203
|
+
|
|
204
|
+
// ─── 조회 응답 ─────────────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
export const maskedSecretSchema = z.object({
|
|
207
|
+
field: z.string().describe("비밀 필드 이름 (`secretKey`·`apiSecret`·`webhookSecret`)"),
|
|
208
|
+
configured: z.boolean().describe("값이 저장돼 있는가"),
|
|
209
|
+
masked: z
|
|
210
|
+
.string()
|
|
211
|
+
.nullable()
|
|
212
|
+
.describe("앞 4자리만 남긴 마스킹 값 (예: `test****`). 저장되지 않았으면 null"),
|
|
213
|
+
});
|
|
214
|
+
export type MaskedSecret = z.infer<typeof maskedSecretSchema>;
|
|
215
|
+
|
|
216
|
+
const publicConfigSchema = z.union([
|
|
217
|
+
z.object({ clientKey: z.string() }),
|
|
218
|
+
z.object({ storeId: z.string(), channels: z.array(portOneChannelSchema) }),
|
|
219
|
+
]);
|
|
220
|
+
|
|
221
|
+
const verificationSchema = z.object({
|
|
222
|
+
result: pgVerificationResultSchema,
|
|
223
|
+
message: z.string().nullable(),
|
|
224
|
+
checkedAt: z.string(),
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
export const paymentCredentialSetSchema = z.object({
|
|
228
|
+
environment: pgEnvironmentSchema,
|
|
229
|
+
publicConfig: publicConfigSchema.describe(
|
|
230
|
+
"공개 값. 토스페이먼츠는 `{clientKey}`, 포트원은 `{storeId, channels}`",
|
|
231
|
+
),
|
|
232
|
+
secrets: z
|
|
233
|
+
.array(maskedSecretSchema)
|
|
234
|
+
.describe("비밀 값의 저장 여부와 마스킹. 평문은 어떤 응답에도 나오지 않는다"),
|
|
235
|
+
lastVerification: verificationSchema
|
|
236
|
+
.nullable()
|
|
237
|
+
.describe("이 세트의 마지막 연결 테스트 결과. 세트를 바꾸면 초기화된다"),
|
|
238
|
+
updatedAt: z.string(),
|
|
239
|
+
});
|
|
240
|
+
export type PaymentCredentialSet = z.infer<typeof paymentCredentialSetSchema>;
|
|
241
|
+
|
|
242
|
+
export const liveGateConditionCodeSchema = z
|
|
243
|
+
.enum([
|
|
244
|
+
"TEST_CONNECTION",
|
|
245
|
+
"SANDBOX_PAYMENT",
|
|
246
|
+
"SANDBOX_REFUND",
|
|
247
|
+
"LIVE_CONNECTION",
|
|
248
|
+
"PLATFORM_LIVE_ENABLED",
|
|
249
|
+
])
|
|
250
|
+
.describe(
|
|
251
|
+
"라이브 개방 조건. `TEST_CONNECTION`=TEST 세트 연결 테스트 성공, `SANDBOX_PAYMENT`=현재 TEST 세트로 샌드박스 결제 1건 승인, " +
|
|
252
|
+
"`SANDBOX_REFUND`=그 샌드박스 결제의 환불 1건 성공, `LIVE_CONNECTION`=LIVE 세트 연결 테스트 성공, " +
|
|
253
|
+
"`PLATFORM_LIVE_ENABLED`=플랫폼이 실결제를 열어 둠",
|
|
254
|
+
);
|
|
255
|
+
export type LiveGateConditionCode = z.infer<typeof liveGateConditionCodeSchema>;
|
|
256
|
+
|
|
257
|
+
export const liveGateConditionSchema = z.object({
|
|
258
|
+
code: liveGateConditionCodeSchema,
|
|
259
|
+
met: z.boolean().describe("충족 여부"),
|
|
260
|
+
message: z.string().describe("조건 설명 또는 미충족 사유"),
|
|
261
|
+
});
|
|
262
|
+
export type LiveGateCondition = z.infer<typeof liveGateConditionSchema>;
|
|
263
|
+
|
|
264
|
+
export const liveGateSchema = z.object({
|
|
265
|
+
status: z
|
|
266
|
+
.enum(["LOCKED", "OPEN"])
|
|
267
|
+
.describe("`OPEN`=조건을 모두 충족해 샌드박스를 끌 수 있다, `LOCKED`=아직 끌 수 없다"),
|
|
268
|
+
conditions: z.array(liveGateConditionSchema),
|
|
269
|
+
openedAt: z.string().nullable().describe("마지막으로 라이브로 전환한 시각. 없으면 null"),
|
|
270
|
+
});
|
|
271
|
+
export type LiveGate = z.infer<typeof liveGateSchema>;
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* 409 `LIVE_GATE_NOT_MET`·`LIVE_PAYMENTS_DISABLED`의 `ApiError.details` 형태.
|
|
275
|
+
* 응답 파싱용이라 관대하다 — 알 수 없는 키는 버리고, 형태가 다르면 safeParse 실패로 처리한다.
|
|
276
|
+
*/
|
|
277
|
+
export const liveGateErrorDetailsSchema = z.object({
|
|
278
|
+
conditions: z.array(liveGateConditionSchema).describe("라이브 개방 조건별 충족 여부 (순서 고정)"),
|
|
279
|
+
});
|
|
280
|
+
export type LiveGateErrorDetails = z.infer<typeof liveGateErrorDetailsSchema>;
|
|
281
|
+
|
|
282
|
+
export const paymentProviderSchema = z.object({
|
|
283
|
+
provider: pgProviderSchema,
|
|
284
|
+
displayName: z.string().describe("표시 이름 (토스페이먼츠·포트원)"),
|
|
285
|
+
connected: z.boolean().describe("자격 증명이 저장돼 있는가"),
|
|
286
|
+
enabled: z.boolean().describe("결제에 사용 중인가"),
|
|
287
|
+
priority: z
|
|
288
|
+
.number()
|
|
289
|
+
.int()
|
|
290
|
+
.nullable()
|
|
291
|
+
.describe("우선순위 (1이 가장 높다). 연결되지 않았으면 null"),
|
|
292
|
+
environment: pgEnvironmentSchema
|
|
293
|
+
.nullable()
|
|
294
|
+
.describe(
|
|
295
|
+
"새 결제에 쓰는 환경(활성 환경). 샌드박스 스위치가 켜져 있으면 `TEST`, 꺼져 있으면 `LIVE`. 연결되지 않았으면 null",
|
|
296
|
+
),
|
|
297
|
+
publicConfig: publicConfigSchema
|
|
298
|
+
.nullable()
|
|
299
|
+
.describe(
|
|
300
|
+
"활성 환경 세트의 공개 값. 토스페이먼츠는 `{clientKey}`, 포트원은 `{storeId, channels}`. 활성 세트가 없으면 null",
|
|
301
|
+
),
|
|
302
|
+
secrets: z
|
|
303
|
+
.array(maskedSecretSchema)
|
|
304
|
+
.describe("활성 환경 세트의 비밀 값 저장 여부와 마스킹. 평문은 어떤 응답에도 나오지 않는다"),
|
|
305
|
+
lastVerification: verificationSchema
|
|
306
|
+
.nullable()
|
|
307
|
+
.describe("활성 환경 세트의 마지막 연결 테스트 결과. 세트를 바꾸면 초기화된다"),
|
|
308
|
+
updatedAt: z.string().nullable(),
|
|
309
|
+
sandboxMode: z
|
|
310
|
+
.boolean()
|
|
311
|
+
.describe(
|
|
312
|
+
"샌드박스 스위치. `true`면 새 결제에 TEST 세트를 쓴다(실제로 돈이 오가지 않는다). 연결되지 않았으면 true",
|
|
313
|
+
),
|
|
314
|
+
credentialSets: z
|
|
315
|
+
.object({
|
|
316
|
+
TEST: paymentCredentialSetSchema.nullable(),
|
|
317
|
+
LIVE: paymentCredentialSetSchema.nullable(),
|
|
318
|
+
})
|
|
319
|
+
.describe("환경별 자격 증명 세트. 등록되지 않은 세트는 null"),
|
|
320
|
+
liveGate: liveGateSchema.describe("라이브 개방 조건. 조건을 모두 충족해야 샌드박스를 끌 수 있다"),
|
|
321
|
+
retiredCredentialCount: z
|
|
322
|
+
.number()
|
|
323
|
+
.int()
|
|
324
|
+
.describe(
|
|
325
|
+
"보관된(가맹점 교체·연결 해제로 새 결제에 더 쓰이지 않는) 자격 증명 수. 그 가맹점으로 받은 결제의 환불·대사에 계속 쓰인다",
|
|
326
|
+
),
|
|
327
|
+
routingExclusion: z
|
|
328
|
+
.string()
|
|
329
|
+
.nullable()
|
|
330
|
+
.optional()
|
|
331
|
+
.default(null)
|
|
332
|
+
.describe(
|
|
333
|
+
"지금 새 결제의 후보가 아닌 사유. 후보면 null. 판정 원천은 서버 라우팅이다(콘솔이 다시 계산하지 않는다). " +
|
|
334
|
+
"후보가 아닌 PG에는 항상 사유 코드를 붙인다(연결 안 된 PG 포함) — 모든 PG는 `candidates`에 있거나 이 값이 null이 아니다. 현재 값: " +
|
|
335
|
+
"`NOT_CONNECTED`(연결 안 됨) · `DISABLED`(사용 중지) · `CREDENTIAL_MISSING`(활성 환경 자격 증명 없음) · " +
|
|
336
|
+
"`SANDBOX_IN_LIVE_STORE`(다른 PG가 라이브라 이 샌드박스 PG는 실구매자 결제에 쓰지 않음) · " +
|
|
337
|
+
"`PLATFORM_BLOCKED`(플랫폼이 실결제를 막음) · `MODE_MISMATCH`(라이브를 연 서버 모드와 현재 모드가 다름). " +
|
|
338
|
+
"값이 추가될 수 있어 문자열로 받는다 — 모르는 값은 후보 아님으로 다룬다." +
|
|
339
|
+
" 서버는 항상 싣는다. 이 필드가 없는 이전 서버 응답은 기본값(null)으로 읽는다",
|
|
340
|
+
),
|
|
341
|
+
});
|
|
342
|
+
export type PaymentProvider = z.infer<typeof paymentProviderSchema>;
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* `PaymentProvider.routingExclusion`의 현재 값 — 응답 스키마는 서버가 사유를 추가해도 파싱이 깨지지 않게 문자열로 받는다.
|
|
346
|
+
* 화면 문구 분기는 이 목록으로 하고, 모르는 값은 "후보 아님"으로 다룬다.
|
|
347
|
+
*/
|
|
348
|
+
export const PAYMENT_ROUTING_EXCLUSION_CODES = [
|
|
349
|
+
"NOT_CONNECTED",
|
|
350
|
+
"DISABLED",
|
|
351
|
+
"CREDENTIAL_MISSING",
|
|
352
|
+
"SANDBOX_IN_LIVE_STORE",
|
|
353
|
+
"PLATFORM_BLOCKED",
|
|
354
|
+
"MODE_MISMATCH",
|
|
355
|
+
] as const;
|
|
356
|
+
export type KnownPaymentRoutingExclusion = (typeof PAYMENT_ROUTING_EXCLUSION_CODES)[number];
|
|
357
|
+
|
|
358
|
+
export const paymentSettingsSchema = z.object({
|
|
359
|
+
routingMode: paymentRoutingModeSchema.describe(
|
|
360
|
+
"저장된 라우팅 방식 (기본 `AUTO_FAILOVER`). PG가 하나뿐이면 무시된다",
|
|
361
|
+
),
|
|
362
|
+
effectiveRoutingMode: effectivePaymentRoutingModeSchema,
|
|
363
|
+
providers: z
|
|
364
|
+
.array(paymentProviderSchema)
|
|
365
|
+
.describe("지원하는 모든 PG. 연결된 것은 우선순위순, 연결되지 않은 것은 뒤에 온다"),
|
|
366
|
+
environment: pgEnvironmentSchema
|
|
367
|
+
.nullable()
|
|
368
|
+
.optional()
|
|
369
|
+
.default(null)
|
|
370
|
+
.describe(
|
|
371
|
+
"지금 결제를 시작하면 쓰는 결제 환경. 사용 가능한 PG 중 하나라도 라이브면 `LIVE`(샌드박스 PG는 제외), 아니면 `TEST`. " +
|
|
372
|
+
"사용 가능한 PG가 없으면 null." +
|
|
373
|
+
" 서버는 항상 싣는다. 이 필드가 없는 이전 서버 응답은 기본값(null)으로 읽는다",
|
|
374
|
+
),
|
|
375
|
+
candidates: z
|
|
376
|
+
.array(z.string())
|
|
377
|
+
.optional()
|
|
378
|
+
.default(() => [])
|
|
379
|
+
.describe(
|
|
380
|
+
"지금 결제를 시작하면 쓰는 PG, 우선순위순(`tosspayments`·`portone`). 비어 있으면 구매자가 결제할 수 없다. " +
|
|
381
|
+
"빠진 PG의 사유는 `providers[].routingExclusion` — 후보가 아닌 PG에는 항상 사유 코드를 붙인다." +
|
|
382
|
+
" 서버는 항상 싣는다. 이 필드가 없는 이전 서버 응답은 기본값(빈 배열)으로 읽는다",
|
|
383
|
+
),
|
|
384
|
+
});
|
|
385
|
+
export type PaymentSettings = z.infer<typeof paymentSettingsSchema>;
|
|
386
|
+
|
|
387
|
+
export const paymentProviderVerificationSchema = z.object({
|
|
388
|
+
provider: pgProviderSchema,
|
|
389
|
+
environment: pgEnvironmentSchema.describe("연결 테스트한 자격 증명 세트"),
|
|
390
|
+
result: pgVerificationResultSchema,
|
|
391
|
+
message: z.string().nullable().describe("실패 사유 또는 참고 메시지"),
|
|
392
|
+
checkedAt: z.string(),
|
|
393
|
+
});
|
|
394
|
+
export type PaymentProviderVerification = z.infer<typeof paymentProviderVerificationSchema>;
|
|
395
|
+
|
|
396
|
+
// ─── 매출 리포트 ───────────────────────────────────────────
|
|
397
|
+
|
|
398
|
+
export const salesReportGroupBySchema = z
|
|
399
|
+
.enum(["DAY", "PROVIDER", "METHOD", "PROVIDER_METHOD"])
|
|
400
|
+
.describe("행 집계 기준. 기본 `DAY`");
|
|
401
|
+
export type SalesReportGroupBy = z.infer<typeof salesReportGroupBySchema>;
|
|
402
|
+
|
|
403
|
+
const reportDate = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "날짜 형식은 YYYY-MM-DD 입니다");
|
|
404
|
+
|
|
405
|
+
export const salesReportQuerySchema = z.object({
|
|
406
|
+
from: reportDate.describe("시작일 (YYYY-MM-DD, Asia/Seoul 기준, 포함)"),
|
|
407
|
+
to: reportDate.describe("종료일 (YYYY-MM-DD, 포함). from부터 최대 92일"),
|
|
408
|
+
provider: pgProviderSchema.optional().describe("PG 필터"),
|
|
409
|
+
method: pgPaymentMethodSchema
|
|
410
|
+
.optional()
|
|
411
|
+
.describe(
|
|
412
|
+
"결제수단 필터 — **실제 결제수단**(PG가 보고한 수단, 모르면 요청 수단) 기준. 결제창에서 결제수단이 바뀔 수 있다(카드 결제창의 간편결제 탭 등). " +
|
|
413
|
+
"관리 주문 API의 method는 호환용이며 간편결제를 CARD로 표시한다. 실제 수단은 approvedMethod를 본다",
|
|
414
|
+
),
|
|
415
|
+
groupBy: salesReportGroupBySchema.optional(),
|
|
416
|
+
includeTest: z
|
|
417
|
+
.enum(["true", "false"])
|
|
418
|
+
.optional()
|
|
419
|
+
.describe(
|
|
420
|
+
"테스트 결제(샌드박스 결제) 포함 여부. 기본 `false` — 테스트 결제의 승인·환불은 집계에서 빠진다",
|
|
421
|
+
),
|
|
422
|
+
});
|
|
423
|
+
export type SalesReportParams = z.infer<typeof salesReportQuerySchema>;
|
|
424
|
+
|
|
425
|
+
export const salesTotalsSchema = z.object({
|
|
426
|
+
paymentCount: z.number().int().describe("승인 건수"),
|
|
427
|
+
paymentAmount: z.number().int().describe("승인 금액 합계"),
|
|
428
|
+
refundCount: z.number().int().describe("환불(취소) 건수. 부분 취소도 1건"),
|
|
429
|
+
refundAmount: z.number().int().describe("환불 금액 합계"),
|
|
430
|
+
netAmount: z.number().int().describe("순매출 = 승인 금액 - 환불 금액"),
|
|
431
|
+
pgFeeAmount: z
|
|
432
|
+
.number()
|
|
433
|
+
.int()
|
|
434
|
+
.nullable()
|
|
435
|
+
.describe("PG 수수료 (참고용). PG에서 확정 값을 받지 못하면 null"),
|
|
436
|
+
});
|
|
437
|
+
export type SalesTotals = z.infer<typeof salesTotalsSchema>;
|
|
438
|
+
|
|
439
|
+
export const salesReportRowSchema = salesTotalsSchema.extend({
|
|
440
|
+
date: z.string().nullable().describe("일자 (YYYY-MM-DD, Asia/Seoul). groupBy=DAY일 때만 값"),
|
|
441
|
+
provider: pgProviderSchema.nullable().describe("PG. groupBy에 PROVIDER가 포함될 때만 값"),
|
|
442
|
+
method: pgPaymentMethodSchema
|
|
443
|
+
.nullable()
|
|
444
|
+
.describe(
|
|
445
|
+
"실제 결제수단(PG가 보고한 수단, 모르면 요청 수단). groupBy에 METHOD가 포함될 때만 값. " +
|
|
446
|
+
"관리 주문 API의 method는 호환용이며 간편결제를 CARD로 표시한다. 실제 수단은 approvedMethod를 본다",
|
|
447
|
+
),
|
|
448
|
+
});
|
|
449
|
+
export type SalesReportRow = z.infer<typeof salesReportRowSchema>;
|
|
450
|
+
|
|
451
|
+
export const salesReportSchema = z.object({
|
|
452
|
+
from: z.string(),
|
|
453
|
+
to: z.string(),
|
|
454
|
+
timezone: z.literal("Asia/Seoul"),
|
|
455
|
+
currency: z.literal("KRW"),
|
|
456
|
+
groupBy: salesReportGroupBySchema,
|
|
457
|
+
provider: pgProviderSchema.nullable().describe("적용된 PG 필터"),
|
|
458
|
+
method: pgPaymentMethodSchema.nullable().describe("적용된 결제수단 필터"),
|
|
459
|
+
includeTest: z.boolean().describe("테스트 결제(샌드박스 결제)를 집계에 포함했는가"),
|
|
460
|
+
summary: salesTotalsSchema.describe("기간 전체 합계"),
|
|
461
|
+
rows: z.array(salesReportRowSchema).describe("집계 행. 활동이 없는 날짜·PG는 생략된다"),
|
|
462
|
+
});
|
|
463
|
+
export type SalesReport = z.infer<typeof salesReportSchema>;
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* 결제 내역 — 결제 건(세션) 단위 공개 상태. 주문 단위 상태와 다르다.
|
|
467
|
+
* - `APPROVED`: 승인됐고 환불이 없다
|
|
468
|
+
* - `PARTIALLY_REFUNDED`·`REFUNDED`: 승인 뒤 일부·전액을 환불했다
|
|
469
|
+
* - `PENDING`: 승인 결과를 아직 확정하지 못했다(대사 잡이 확정한다)
|
|
470
|
+
* - `FAILED`: 승인되지 않고 끝났다(거절·준비 장애·구매자 취소·만료)
|
|
471
|
+
*/
|
|
472
|
+
export const paymentRecordStatusSchema = z.enum([
|
|
473
|
+
"APPROVED",
|
|
474
|
+
"PARTIALLY_REFUNDED",
|
|
475
|
+
"REFUNDED",
|
|
476
|
+
"PENDING",
|
|
477
|
+
"FAILED",
|
|
478
|
+
]);
|
|
479
|
+
export type PaymentRecordStatus = z.infer<typeof paymentRecordStatusSchema>;
|
|
480
|
+
|
|
481
|
+
export const paymentAttemptStatusSchema = z
|
|
482
|
+
.string()
|
|
483
|
+
.describe(
|
|
484
|
+
"시도 상태 — ready·approving·approved·unknown·declined·prepare_failed·canceled·failed·abandoned·voided. " +
|
|
485
|
+
"새 값이 추가될 수 있으니 모르는 값은 '진행 중이 아닌 시도'로 다룬다",
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
export const paymentRecordSchema = z.object({
|
|
489
|
+
paymentId: z.string().describe("결제 건 id. 상세 조회 키"),
|
|
490
|
+
orderId: z.string().nullable().describe("승인돼 만들어진 주문. 승인 전에 끝난 결제는 null"),
|
|
491
|
+
orderName: z.string().describe("주문명 (예: 무선 이어폰 프로 외 1건)"),
|
|
492
|
+
customerName: z.string().describe("구매자명"),
|
|
493
|
+
status: paymentRecordStatusSchema,
|
|
494
|
+
amount: z.number().int().describe("결제 금액"),
|
|
495
|
+
refundedAmount: z.number().int().describe("환불 완료 금액 합계. 실패한 환불은 빼고 센다"),
|
|
496
|
+
provider: pgProviderSchema.nullable().describe("승인한 PG. 승인 전에 끝났으면 마지막 시도의 PG"),
|
|
497
|
+
method: pgPaymentMethodSchema.nullable().describe("실제 결제수단. 모르면 요청 수단"),
|
|
498
|
+
environment: pgEnvironmentSchema.nullable().describe("결제 환경"),
|
|
499
|
+
testPayment: z.boolean().describe("샌드박스 결제인가"),
|
|
500
|
+
pgTransactionId: z.string().nullable().describe("PG 거래 id (토스 paymentKey, 포트원 txId)"),
|
|
501
|
+
attemptId: z.string().nullable().describe("PG에 보낸 주문번호 = 시도 id"),
|
|
502
|
+
attemptCount: z.number().int().describe("시도 횟수"),
|
|
503
|
+
failCode: z.string().nullable().describe("실패 코드. 실패로 끝난 결제만"),
|
|
504
|
+
failMessage: z.string().nullable().describe("실패 사유"),
|
|
505
|
+
paidAt: z.string().nullable().describe("승인 시각 (ISO 8601)"),
|
|
506
|
+
createdAt: z.string().describe("결제 시작 시각 (ISO 8601)"),
|
|
507
|
+
});
|
|
508
|
+
export type PaymentRecord = z.infer<typeof paymentRecordSchema>;
|
|
509
|
+
|
|
510
|
+
export const paymentRecordPageSchema = pageSchema(paymentRecordSchema);
|
|
511
|
+
export type PaymentRecordPage = z.infer<typeof paymentRecordPageSchema>;
|
|
512
|
+
|
|
513
|
+
export const paymentAttemptRecordSchema = z.object({
|
|
514
|
+
attemptId: z.string(),
|
|
515
|
+
sequence: z.number().int().describe("결제 건 안에서의 순번 (1부터)"),
|
|
516
|
+
provider: pgProviderSchema,
|
|
517
|
+
status: paymentAttemptStatusSchema,
|
|
518
|
+
method: pgPaymentMethodSchema.describe("주문서에서 고른 결제수단"),
|
|
519
|
+
approvedMethod: pgPaymentMethodSchema.nullable().describe("PG가 보고한 실제 결제수단"),
|
|
520
|
+
amount: z.number().int(),
|
|
521
|
+
environment: pgEnvironmentSchema,
|
|
522
|
+
pgTransactionId: z.string().nullable(),
|
|
523
|
+
failCode: z.string().nullable(),
|
|
524
|
+
failMessage: z.string().nullable(),
|
|
525
|
+
createdAt: z.string(),
|
|
526
|
+
updatedAt: z.string(),
|
|
527
|
+
});
|
|
528
|
+
export type PaymentAttemptRecord = z.infer<typeof paymentAttemptRecordSchema>;
|
|
529
|
+
|
|
530
|
+
export const paymentRefundRecordSchema = z.object({
|
|
531
|
+
refundId: z.string(),
|
|
532
|
+
amount: z.number().int(),
|
|
533
|
+
reason: z.string().describe("환불 사유 코드"),
|
|
534
|
+
kind: z
|
|
535
|
+
.string()
|
|
536
|
+
.describe("환불 원인 — SELLER_CANCEL·CLAIM·COMPENSATION·DUPLICATE. 새 값이 추가될 수 있다"),
|
|
537
|
+
status: z.string().describe("SUCCEEDED·FAILED"),
|
|
538
|
+
failMessage: z.string().nullable(),
|
|
539
|
+
requestedAt: z.string(),
|
|
540
|
+
completedAt: z.string().nullable(),
|
|
541
|
+
});
|
|
542
|
+
export type PaymentRefundRecord = z.infer<typeof paymentRefundRecordSchema>;
|
|
543
|
+
|
|
544
|
+
export const paymentRecordDetailSchema = paymentRecordSchema.extend({
|
|
545
|
+
refundableAmount: z.number().int().describe("환불 가능 잔액 = 승인 금액 - 환불 완료 금액"),
|
|
546
|
+
merchantId: z
|
|
547
|
+
.string()
|
|
548
|
+
.nullable()
|
|
549
|
+
.describe("가맹점 식별자 지문 (토스 clientKey, 포트원 Store ID). 비밀 값은 없다"),
|
|
550
|
+
attempts: z.array(paymentAttemptRecordSchema).describe("시도 이력. 순번 오름차순"),
|
|
551
|
+
refunds: z.array(paymentRefundRecordSchema).describe("환불 이력. 실패한 요청도 남는다"),
|
|
552
|
+
});
|
|
553
|
+
export type PaymentRecordDetail = z.infer<typeof paymentRecordDetailSchema>;
|
|
554
|
+
|
|
555
|
+
/** `GET /v1/payments` 쿼리 — OpenAPI 문서용 형태(값은 쿼리 문자열) */
|
|
556
|
+
export const paymentListQuerySchema = z.object({
|
|
557
|
+
from: z.string().optional().describe("조회 시작 시각 (ISO 8601, 포함). 생략하면 최근 30일"),
|
|
558
|
+
to: z.string().optional().describe("조회 끝 시각 (ISO 8601, 포함). 기간은 최대 92일"),
|
|
559
|
+
status: z.string().optional().describe("결제 상태 필터 (PaymentRecordStatus)"),
|
|
560
|
+
provider: z.string().optional().describe("PG 필터"),
|
|
561
|
+
method: z.string().optional().describe("실제 결제수단 필터"),
|
|
562
|
+
keyword: z
|
|
563
|
+
.string()
|
|
564
|
+
.optional()
|
|
565
|
+
.describe("결제 id·주문번호·PG 거래 id·시도 id(일치), 구매자명(포함)"),
|
|
566
|
+
includeTest: z
|
|
567
|
+
.string()
|
|
568
|
+
.optional()
|
|
569
|
+
.describe("테스트 결제 포함 여부 (`true`·`false`. 기본 false — 실결제만)"),
|
|
570
|
+
page: z.string().optional().describe("페이지 번호 (1부터)"),
|
|
571
|
+
size: z.string().optional().describe("페이지 크기 (기본 20, 최대 100)"),
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
export interface PaymentListParams extends PageParams {
|
|
575
|
+
from?: string;
|
|
576
|
+
to?: string;
|
|
577
|
+
status?: PaymentRecordStatus;
|
|
578
|
+
provider?: PgProvider;
|
|
579
|
+
method?: PgPaymentMethod;
|
|
580
|
+
keyword?: string;
|
|
581
|
+
includeTest?: boolean;
|
|
582
|
+
}
|