@sayren/store-sdk 0.30.20 → 0.32.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.
@@ -10,10 +10,21 @@ import { type PageParams, pageSchema } from "./common";
10
10
  */
11
11
 
12
12
  export const pgProviderSchema = z
13
- .enum(["tosspayments", "portone"])
14
- .describe("PG 식별자. `tosspayments`=토스페이먼츠, `portone`=포트원(V2)");
13
+ .enum(["tosspayments", "portone", "kakaopay", "naverpay"])
14
+ .describe(
15
+ "PG 식별자. `tosspayments`=토스페이먼츠, `portone`=포트원(V2), `kakaopay`=카카오페이(직접 연동), `naverpay`=네이버페이 결제형(직접 연동)",
16
+ );
15
17
  export type PgProvider = z.infer<typeof pgProviderSchema>;
16
18
 
19
+ /**
20
+ * 간편결제 전용 PG — 결제수단이 간편결제 하나이고 간편결제사가 PG 자신이다(`kakaopay` → `KAKAOPAY`).
21
+ * 이 PG는 `easyPayProviders` 설정 없이 결제 옵션이 하나(`EASY_PAY` + 그 간편결제사) 나온다
22
+ */
23
+ export const DEDICATED_EASY_PAY_PROVIDER = {
24
+ kakaopay: "KAKAOPAY",
25
+ naverpay: "NAVERPAY",
26
+ } as const satisfies Partial<Record<PgProvider, string>>;
27
+
17
28
  export const pgPaymentMethodSchema = z
18
29
  .enum(["CARD", "BANK_TRANSFER", "VIRTUAL_ACCOUNT", "MOBILE", "EASY_PAY"])
19
30
  .describe("결제수단 (스토어프론트 결제 요청의 `paymentMethod`와 같은 값)");
@@ -138,6 +149,45 @@ export const portOneCredentialsInputSchema = z.object({
138
149
  });
139
150
  export type PortOneCredentialsInput = z.infer<typeof portOneCredentialsInputSchema>;
140
151
 
152
+ /** 카카오페이 테스트 가맹점 코드 — 카카오페이 개발자센터의 단건 결제 테스트 CID */
153
+ export const KAKAOPAY_TEST_CID = "TC0ONETIME";
154
+
155
+ export const kakaoPayCredentialsInputSchema = z.object({
156
+ cid: z
157
+ .string()
158
+ .trim()
159
+ .min(4)
160
+ .max(20)
161
+ .describe(
162
+ "가맹점 코드(CID). 카카오페이와 계약하면 받는다. 공개 값. 테스트 세트는 `TC0ONETIME`(단건 결제 테스트 CID)을 쓸 수 있다",
163
+ ),
164
+ secretKey: secretInput()
165
+ .optional()
166
+ .describe(
167
+ "카카오페이 개발자센터 앱의 Secret key(테스트 세트는 dev, 실결제 세트는 운영 키). 비밀 값. 최초 연결 시 필수, 수정 시 생략하면 기존 값을 유지한다",
168
+ ),
169
+ });
170
+ export type KakaoPayCredentialsInput = z.infer<typeof kakaoPayCredentialsInputSchema>;
171
+
172
+ export const naverPayCredentialsInputSchema = z.object({
173
+ clientId: z
174
+ .string()
175
+ .trim()
176
+ .min(4)
177
+ .max(100)
178
+ .describe("네이버페이 결제형 가입 후 받은 Client ID. 공개 값 — 결제창에서 쓰인다"),
179
+ chainId: z
180
+ .string()
181
+ .trim()
182
+ .min(4)
183
+ .max(100)
184
+ .describe("네이버페이 Chain ID. 공개 값 — 결제창과 API 요청에 쓰인다"),
185
+ clientSecret: secretInput()
186
+ .optional()
187
+ .describe("Client Secret. 비밀 값. 최초 연결 시 필수, 수정 시 생략하면 기존 값을 유지한다"),
188
+ });
189
+ export type NaverPayCredentialsInput = z.infer<typeof naverPayCredentialsInputSchema>;
190
+
141
191
  const upsertEnvironment = () =>
142
192
  pgEnvironmentSchema.describe(
143
193
  "저장할 자격 증명 세트. PG마다 `TEST`(샌드박스)·`LIVE`(실결제) 세트를 따로 둔다. " +
@@ -158,6 +208,18 @@ export const upsertPortOneProviderRequestSchema = z.object({
158
208
  });
159
209
  export type UpsertPortOneProviderRequest = z.infer<typeof upsertPortOneProviderRequestSchema>;
160
210
 
211
+ export const upsertKakaoPayProviderRequestSchema = z.object({
212
+ environment: upsertEnvironment(),
213
+ credentials: kakaoPayCredentialsInputSchema,
214
+ });
215
+ export type UpsertKakaoPayProviderRequest = z.infer<typeof upsertKakaoPayProviderRequestSchema>;
216
+
217
+ export const upsertNaverPayProviderRequestSchema = z.object({
218
+ environment: upsertEnvironment(),
219
+ credentials: naverPayCredentialsInputSchema,
220
+ });
221
+ export type UpsertNaverPayProviderRequest = z.infer<typeof upsertNaverPayProviderRequestSchema>;
222
+
161
223
  export const updatePaymentProviderRequestSchema = z
162
224
  .object({
163
225
  enabled: z
@@ -240,6 +302,8 @@ export type MaskedSecret = z.infer<typeof maskedSecretSchema>;
240
302
  const publicConfigSchema = z.union([
241
303
  z.object({ clientKey: z.string() }),
242
304
  z.object({ storeId: z.string(), channels: z.array(portOneChannelSchema) }),
305
+ z.object({ cid: z.string() }),
306
+ z.object({ clientId: z.string(), chainId: z.string() }),
243
307
  ]);
244
308
 
245
309
  const verificationSchema = z.object({
@@ -251,7 +315,7 @@ const verificationSchema = z.object({
251
315
  export const paymentCredentialSetSchema = z.object({
252
316
  environment: pgEnvironmentSchema,
253
317
  publicConfig: publicConfigSchema.describe(
254
- "공개 값. 토스페이먼츠는 `{clientKey}`, 포트원은 `{storeId, channels}`",
318
+ "공개 값. 토스페이먼츠는 `{clientKey}`, 포트원은 `{storeId, channels}`, 카카오페이는 `{cid}`, 네이버페이는 `{clientId, chainId}`",
255
319
  ),
256
320
  secrets: z
257
321
  .array(maskedSecretSchema)
@@ -324,7 +388,8 @@ export const paymentProviderSchema = z.object({
324
388
  publicConfig: publicConfigSchema
325
389
  .nullable()
326
390
  .describe(
327
- "활성 환경 세트의 공개 값. 토스페이먼츠는 `{clientKey}`, 포트원은 `{storeId, channels}`. 활성 세트가 없으면 null",
391
+ "활성 환경 세트의 공개 값. 토스페이먼츠는 `{clientKey}`, 포트원은 `{storeId, channels}`, 카카오페이는 `{cid}`, " +
392
+ "네이버페이는 `{clientId, chainId}`. 활성 세트가 없으면 null",
328
393
  ),
329
394
  secrets: z
330
395
  .array(maskedSecretSchema)
@@ -345,6 +410,18 @@ export const paymentProviderSchema = z.object({
345
410
  })
346
411
  .describe("환경별 자격 증명 세트. 등록되지 않은 세트는 null"),
347
412
  liveGate: liveGateSchema.describe("라이브 개방 조건. 조건을 모두 충족해야 샌드박스를 끌 수 있다"),
413
+ platformTestKeys: z
414
+ .boolean()
415
+ .optional()
416
+ .default(false)
417
+ .describe(
418
+ "테스트모드 키 활성화. `true`면 TEST 세트가 sayren 지정 테스트 키로 고정된다(TEST 세트를 직접 저장·제거하면 409 `PLATFORM_TEST_KEYS_ENABLED`)",
419
+ ),
420
+ platformTestKeysAvailable: z
421
+ .boolean()
422
+ .optional()
423
+ .default(false)
424
+ .describe("이 PG에 sayren 지정 테스트 키가 있어 테스트모드 키를 켤 수 있는가"),
348
425
  easyPayProviders: z
349
426
  .array(easyPayProviderResponseSchema)
350
427
  .describe(
@@ -421,7 +498,17 @@ export const paymentSettingsSchema = z.object({
421
498
  .describe("결제 도메인 — 결제를 마친 구매자가 돌아올 스토어프론트 origin 목록"),
422
499
  providers: z
423
500
  .array(paymentProviderSchema)
424
- .describe("지원하는 모든 PG. 연결된 것은 우선순위순, 연결되지 않은 것은 뒤에 온다"),
501
+ .describe(
502
+ "연결된 PG(우선순위순)와 연결되지 않은 토스페이먼츠·포트원. 카카오페이·네이버페이는 연결했을 때만 여기에 온다" +
503
+ "(이 PG를 모르는 이전 SDK가 응답을 읽을 수 있게). 연결할 수 있는 PG 전체는 `connectableProviders`다",
504
+ ),
505
+ connectableProviders: z
506
+ .array(paymentProviderSchema)
507
+ .optional()
508
+ .default(() => [])
509
+ .describe(
510
+ "연결되지 않은 PG 전체(카카오페이·네이버페이 포함). 서버는 항상 싣는다. 이 필드가 없는 이전 서버 응답은 기본값(빈 배열)으로 읽는다",
511
+ ),
425
512
  environment: pgEnvironmentSchema
426
513
  .nullable()
427
514
  .optional()
@@ -776,3 +863,38 @@ export interface PaymentListParams extends PageParams {
776
863
  /** 환불 대기가 있는 결제만(#115) */
777
864
  refundHeld?: boolean;
778
865
  }
866
+
867
+ /** 테스트모드 키 활성화 요청(2026-10-14) — `PUT /v1/store/payment-providers/{provider}/platform-test-keys` */
868
+ export const setPlatformTestKeysRequestSchema = z.object({
869
+ enabled: z
870
+ .boolean()
871
+ .describe(
872
+ "`true`면 sayren 지정 테스트 키를 TEST 세트로 저장하고 PG를 켠다. `false`면 고정만 푼다(TEST 세트는 그대로)",
873
+ ),
874
+ });
875
+ export type SetPlatformTestKeysRequest = z.infer<typeof setPlatformTestKeysRequestSchema>;
876
+
877
+ /** 결제 상태 PG 한 줄 — 비밀 값·공개 키 없이 연결·사용·환경·테스트모드 키만 */
878
+ export const paymentStatusProviderSchema = z.object({
879
+ provider: pgProviderSchema,
880
+ displayName: z.string(),
881
+ connected: z.boolean().describe("자격 증명이 저장돼 있는가"),
882
+ enabled: z.boolean().describe("결제에 사용 중인가"),
883
+ environment: pgEnvironmentSchema
884
+ .nullable()
885
+ .describe("새 결제에 쓰는 환경. 연결되지 않았으면 null"),
886
+ platformTestKeys: z.boolean().describe("테스트모드 키 활성화 여부"),
887
+ platformTestKeysAvailable: z.boolean().describe("sayren 지정 테스트 키로 바로 켤 수 있는가"),
888
+ });
889
+ export type PaymentStatusProvider = z.infer<typeof paymentStatusProviderSchema>;
890
+
891
+ /** 결제 상태(2026-10-14) — `GET /v1/store/payment-status`. 지원 PG 전체와 상점의 연결 상태 */
892
+ export const paymentStatusSchema = z.object({
893
+ environment: pgEnvironmentSchema
894
+ .nullable()
895
+ .describe("새 결제 세션의 환경(사용 중인 PG 기준). 쓸 수 있는 PG가 없으면 null"),
896
+ providers: z
897
+ .array(paymentStatusProviderSchema)
898
+ .describe("sayren이 지원하는 PG 전체(연결 안 된 PG 포함)"),
899
+ });
900
+ export type PaymentStatus = z.infer<typeof paymentStatusSchema>;
@@ -21,6 +21,10 @@ export const SCOPE_DESCRIPTIONS: Record<StoreScopeResource, { r: string; rw: str
21
21
  promotion: { r: "쿠폰·적립금 설정을 읽습니다", rw: "쿠폰을 발급하고 적립금을 지급합니다" },
22
22
  storefront: { r: "스토어프론트 설정을 읽습니다", rw: "스토어프론트를 편집하고 발행합니다" },
23
23
  flow: { r: "주문 흐름 설정을 읽습니다", rw: "주문 흐름을 만들고 수정합니다" },
24
+ inventory: {
25
+ r: "위치별 재고와 재고 변동 내역을 읽습니다",
26
+ rw: "재고를 조정·실사하고 위치 사이에서 이동합니다",
27
+ },
24
28
  };
25
29
 
26
30
  /** `{resource}:{r|rw}` → 문장. 카탈로그에 없는 스코프는 null */
@@ -33,6 +33,8 @@ export const STORE_SCOPE_RESOURCES = {
33
33
  storefront: ["r", "rw"],
34
34
  // 커머스 플로우 정의·할당(이슈 #115) — 주문 처리 행동은 order 스코프다
35
35
  flow: ["r", "rw"],
36
+ // 위치별 재고·재고 조정·이동·변동 내역(이슈 #132) — 기존 재고 표면(조합 재고 수정, 상품·옵션의 `stockQuantity`)은 product 스코프다
37
+ inventory: ["r", "rw"],
36
38
  } as const satisfies Record<string, readonly ScopeAccess[]>;
37
39
 
38
40
  export type StoreScopeResource = keyof typeof STORE_SCOPE_RESOURCES;
@@ -71,6 +73,7 @@ export const STORE_ROLE_SCOPES = {
71
73
  "promotion:rw",
72
74
  "storefront:rw",
73
75
  "flow:rw",
76
+ "inventory:rw",
74
77
  ],
75
78
  ADMIN: [
76
79
  "product:rw",
@@ -88,6 +91,7 @@ export const STORE_ROLE_SCOPES = {
88
91
  "promotion:rw",
89
92
  "storefront:rw",
90
93
  "flow:rw",
94
+ "inventory:rw",
91
95
  ],
92
96
  STAFF: [
93
97
  "product:rw",
@@ -99,5 +103,6 @@ export const STORE_ROLE_SCOPES = {
99
103
  "customer:r",
100
104
  "promotion:r",
101
105
  "flow:r",
106
+ "inventory:rw",
102
107
  ],
103
108
  } as const satisfies Record<StoreRole, readonly string[]>;
@@ -461,6 +461,13 @@ export const sellerAddressSchema = z.object({
461
461
  zipCode: z.string(),
462
462
  address1: z.string(),
463
463
  address2: z.string().optional(),
464
+ inventoryLocationId: z
465
+ .string()
466
+ .nullable()
467
+ .optional()
468
+ .describe(
469
+ "이 주소의 재고 위치 ID — 출고지면 배송 주문을 이 위치 재고로 팔고, 반품지면 반품을 이 위치로 들인다. 위치를 하나만 쓰는 상점은 기본 위치다",
470
+ ),
464
471
  createdAt: z.string(),
465
472
  });
466
473
 
@@ -474,6 +481,11 @@ export const sellerAddressRequestSchema = z.object({
474
481
  zipCode: z.string().regex(/^[0-9]{5}$/),
475
482
  address1: z.string().min(1),
476
483
  address2: z.string().optional(),
484
+ inventoryLocationId: z
485
+ .string()
486
+ .min(1)
487
+ .optional()
488
+ .describe("재고 위치 ID. 등록에서 생략하면 기본 위치, 수정에서 생략하면 지금 연결을 유지한다"),
477
489
  });
478
490
 
479
491
  export type SellerAddressRequest = z.infer<typeof sellerAddressRequestSchema>;
@@ -952,6 +952,20 @@ export type StorefrontEditSession = z.infer<typeof storefrontEditSessionSchema>;
952
952
  export const storefrontDevLogsSchema = z.object({
953
953
  stdout: z.string(),
954
954
  stderr: z.string(),
955
+ browser: z
956
+ .array(
957
+ z.object({
958
+ seq: z.number().int().describe("세션 안에서 오르는 순번"),
959
+ at: z.string().describe("받은 시각"),
960
+ level: z.enum(["error", "warn"]),
961
+ message: z.string(),
962
+ url: z.string().describe("로그를 남긴 페이지 경로"),
963
+ }),
964
+ )
965
+ .default([])
966
+ .describe(
967
+ "편집 미리보기를 연 브라우저의 오류·경고(window 오류, 처리되지 않은 Promise 거부, console.error·warn) 최근 100건. 미리보기를 열어 둔 동안만 모인다",
968
+ ),
955
969
  at: z.string(),
956
970
  });
957
971
  export type StorefrontDevLogs = z.infer<typeof storefrontDevLogsSchema>;
@@ -320,11 +320,55 @@ export const productOutOfStockWebhookEventDataSchema = z.object({
320
320
  variantId: z
321
321
  .string()
322
322
  .describe("재고가 0이 된 판매 단위(variant) ID. 주문 상품의 `optionId`와 같은 값 공간이다"),
323
+ inventoryLocationId: z
324
+ .string()
325
+ .optional()
326
+ .describe(
327
+ "재고가 0이 된 재고 위치 ID. 품절은 위치별 가용 재고의 0 전이다. 위치를 하나만 쓰는 상점은 기본 위치다(옛 이벤트에는 없다)",
328
+ ),
323
329
  });
324
330
  export type ProductOutOfStockWebhookEventData = z.infer<
325
331
  typeof productOutOfStockWebhookEventDataSchema
326
332
  >;
327
333
 
334
+ /**
335
+ * 위치별 재고 변화(이슈 #132) — 재고 변동 내역 한 행마다 한 건이다. 같은 위치·조합의 순서는 `levelVersion`(빈 번호 없음)으로 맞추고,
336
+ * 상점 전체 순서는 `movementSeq`(재고 변동 내역 조회의 `after` 커서와 같은 값)다. 자기가 한 조정의 반향은 `actorType`·`actorId`·`operationId`로 거른다.
337
+ * 출고 대기·실재고(상태 전이) 변화는 이벤트를 내지 않는다 — 필요하면 위치별 재고 조회를 다시 읽는다
338
+ */
339
+ export const inventoryChangedWebhookEventDataSchema = z.object({
340
+ productId: z.string().describe("상품 ID"),
341
+ variantId: z
342
+ .string()
343
+ .describe("판매 단위(variant) ID. 주문 상품의 `optionId`와 같은 값 공간이다"),
344
+ inventoryLocationId: z.string().describe("재고 위치 ID"),
345
+ available: z.number().int().describe("변화 뒤 가용 재고"),
346
+ delta: z.number().int().describe("가용 재고 변화량(0이 아니다)"),
347
+ kind: z
348
+ .string()
349
+ .describe(
350
+ "변동 종류 — `OPENING`·`SALE`·`RESTORE`·`REVERT`·`EXCHANGE_OUT`·`ADJUST`·`COUNT`·`TRANSFER_OUT`·`TRANSFER_IN`·`REMOVED`. 값이 늘 수 있다",
351
+ ),
352
+ reason: z.string().nullable().describe("조정·실사 사유 코드. 없으면 null"),
353
+ levelVersion: z
354
+ .number()
355
+ .int()
356
+ .describe("위치별 재고 행의 버전 — 같은 위치·조합 안에서 빈 번호 없이 오른다"),
357
+ movementSeq: z.number().int().describe("상점별 재고 변동 내역 순번"),
358
+ actorType: z.string().describe("행위자 유형 — `seller`·`app`·`agent`·`system`"),
359
+ actorId: z.string().nullable().describe("행위자 ID. 시스템이면 null"),
360
+ refType: z
361
+ .string()
362
+ .nullable()
363
+ .describe("출처 유형 — `order_item`·`claim`·`operation`. 없으면 null"),
364
+ refId: z.string().nullable().describe("출처 ID"),
365
+ operationId: z.string().nullable().describe("재고 조정·이동 요청 ID. 조정·이동이 아니면 null"),
366
+ occurredAt: z.string().describe("변화 시각"),
367
+ });
368
+ export type InventoryChangedWebhookEventData = z.infer<
369
+ typeof inventoryChangedWebhookEventDataSchema
370
+ >;
371
+
328
372
  export const orderWebhookEventSchema = z.object({
329
373
  ...envelopeFields,
330
374
  type: z.enum([
@@ -380,6 +424,12 @@ export const productOutOfStockWebhookEventSchema = z.object({
380
424
  data: productOutOfStockWebhookEventDataSchema,
381
425
  });
382
426
 
427
+ export const inventoryChangedWebhookEventSchema = z.object({
428
+ ...envelopeFields,
429
+ type: z.literal("INVENTORY.CHANGED"),
430
+ data: inventoryChangedWebhookEventDataSchema,
431
+ });
432
+
383
433
  /**
384
434
  * 플로우 알림(이슈 #115) — 상점이 플로우에 둔 알림 블록(`core.notify`, 채널 `webhook`)이 보낸다. 구매자 알림(알림톡·메일)은 이 이벤트를 받아
385
435
  * 상점이 보낸다(플랫폼은 구매자에게 직접 보내지 않는다). 개인정보는 싣지 않는다
@@ -436,6 +486,7 @@ export const webhookEventSchema = z
436
486
  productOutOfStockWebhookEventSchema,
437
487
  flowNotificationWebhookEventSchema,
438
488
  flowStateChangedWebhookEventSchema,
489
+ inventoryChangedWebhookEventSchema,
439
490
  ])
440
491
  .describe(
441
492
  "웹훅 요청 본문 (얇은 이벤트). 개인정보를 싣지 않으며 `data`의 id로 관리 API를 조회한다",