@sayren/store-sdk 0.41.0 → 0.43.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.
Files changed (53) hide show
  1. package/dist/index.d.mts +72 -72
  2. package/dist/index.mjs +160 -160
  3. package/dist/server.mjs +1 -1
  4. package/dist/{webhook-headers-D974dE4T.mjs → webhook-headers-CTez3xDY.mjs} +81 -3
  5. package/package.json +1 -1
  6. package/src/client.ts +1 -1
  7. package/src/http.ts +1 -1
  8. package/src/oauth.ts +1 -1
  9. package/src/schemas/ai.ts +40 -33
  10. package/src/schemas/analytics.ts +1 -1
  11. package/src/schemas/api-log.ts +1 -1
  12. package/src/schemas/app-manifest.ts +1 -1
  13. package/src/schemas/apps.ts +19 -16
  14. package/src/schemas/audit.ts +7 -4
  15. package/src/schemas/auth.ts +1 -1
  16. package/src/schemas/category.ts +37 -32
  17. package/src/schemas/claim.ts +15 -12
  18. package/src/schemas/collection.ts +76 -65
  19. package/src/schemas/commerce-flow.ts +107 -86
  20. package/src/schemas/commerce-number.ts +1 -1
  21. package/src/schemas/common.ts +1 -1
  22. package/src/schemas/coupon.ts +88 -80
  23. package/src/schemas/currency-prices.ts +1 -1
  24. package/src/schemas/currency.ts +1 -1
  25. package/src/schemas/customer-delegation.ts +31 -28
  26. package/src/schemas/customer.ts +10 -7
  27. package/src/schemas/fulfillment.ts +1 -1
  28. package/src/schemas/inventory.ts +29 -24
  29. package/src/schemas/locale.ts +1 -1
  30. package/src/schemas/login.ts +18 -11
  31. package/src/schemas/market.ts +1 -1
  32. package/src/schemas/misc.ts +7 -4
  33. package/src/schemas/order.ts +70 -59
  34. package/src/schemas/payment.ts +108 -89
  35. package/src/schemas/plans.ts +1 -1
  36. package/src/schemas/points.ts +33 -28
  37. package/src/schemas/product-info.ts +13 -10
  38. package/src/schemas/product-localization.ts +1 -1
  39. package/src/schemas/product.ts +18 -15
  40. package/src/schemas/promotion.ts +1 -1
  41. package/src/schemas/refund-policy.ts +1 -1
  42. package/src/schemas/saved-option-set.ts +6 -6
  43. package/src/schemas/scope.ts +1 -1
  44. package/src/schemas/shipping-policy.ts +1 -1
  45. package/src/schemas/starter.ts +16 -13
  46. package/src/schemas/stay.ts +16 -13
  47. package/src/schemas/store.ts +130 -114
  48. package/src/schemas/storefront-hosting.ts +272 -220
  49. package/src/schemas/strict.ts +84 -0
  50. package/src/schemas/subscription.ts +1 -1
  51. package/src/schemas/telemetry.ts +1 -1
  52. package/src/schemas/upload.ts +16 -13
  53. package/src/schemas/webhook-event.ts +1 -1
@@ -1,6 +1,7 @@
1
- import { z } from "zod";
1
+ import * as z from "zod";
2
2
  import { claimStatusSchema } from "./claim";
3
3
  import { orderItemStatusSchema } from "./common";
4
+ import { strictRequest } from "./strict";
4
5
 
5
6
  /**
6
7
  * 커머스 플로우(이슈 #115) — 주문상품이 따르는 플로우의 지금 상태와 할 수 있는 행동. 공개 계약은 필드 추가로만 자란다.
@@ -146,10 +147,12 @@ export const orderItemFlowSchema = z.object({
146
147
  export type OrderItemFlow = z.infer<typeof orderItemFlowSchema>;
147
148
 
148
149
  /** 상태 직접 이동(#115) — 연결선을 지나지 않고 다른 상태로 옮긴다. 블록(재고·환불·송장)은 실행하지 않는다 */
149
- export const moveFlowStateRequestSchema = z.object({
150
- to: z.string().min(1).describe("옮길 상태 키(`moveTargets[].key`)"),
151
- reason: z.string().trim().max(200).optional().describe("옮긴 사유(실행 기록에 남는다)"),
152
- });
150
+ export const moveFlowStateRequestSchema = strictRequest(
151
+ z.object({
152
+ to: z.string().min(1).describe("옮길 상태 키(`moveTargets[].key`)"),
153
+ reason: z.string().trim().max(200).optional().describe("옮긴 사유(실행 기록에 남는다)"),
154
+ }),
155
+ );
153
156
  export type MoveFlowStateRequest = z.infer<typeof moveFlowStateRequestSchema>;
154
157
 
155
158
  export const flowRunStepSchema = z.object({
@@ -295,9 +298,11 @@ export type CommerceFlow = z.infer<typeof commerceFlowSchema>;
295
298
  export type CommerceFlowVersionView = z.infer<typeof commerceFlowVersionViewSchema>;
296
299
 
297
300
  /** 진행 중 주문을 활성 버전으로 옮기기(#115) — 한 번에 `limit`개까지. 남으면 다시 부른다 */
298
- export const migrateFlowVersionRequestSchema = z.object({
299
- limit: z.number().int().min(1).max(500).optional().describe("한 번에 옮길 흐름 수(기본 100)"),
300
- });
301
+ export const migrateFlowVersionRequestSchema = strictRequest(
302
+ z.object({
303
+ limit: z.number().int().min(1).max(500).optional().describe("한 번에 옮길 흐름 수(기본 100)"),
304
+ }),
305
+ );
301
306
  export type MigrateFlowVersionRequest = z.infer<typeof migrateFlowVersionRequestSchema>;
302
307
 
303
308
  export const migrateFlowVersionResultSchema = z.object({
@@ -321,40 +326,44 @@ export const migrateFlowVersionResultSchema = z.object({
321
326
  });
322
327
  export type MigrateFlowVersionResult = z.infer<typeof migrateFlowVersionResultSchema>;
323
328
 
324
- export const createCommerceFlowRequestSchema = z.object({
325
- key: z
326
- .string()
327
- .regex(/^[a-z][a-z0-9-]{1,48}$/)
328
- .describe("상점 안 유일한 키(소문자·숫자·하이픈)"),
329
- name: z.string().min(1).max(60).optional(),
330
- presetKey: z.string().optional().describe("출발한 프리셋(출처로 남는다). 없으면 빈 플로우"),
331
- graph: flowDocumentSchema
332
- .optional()
333
- .describe(
334
- "편집기에서 고친 그래프(#115). 없으면 프리셋 그래프 그대로, 프리셋도 없으면 빈 흐름(결제 완료 노드 하나)이다",
335
- ),
336
- definition: flowDocumentSchema
337
- .optional()
338
- .describe(
339
- "흐름 설정 `requires`(progression·groupClaim·fulfillmentMethods). 없으면 프리셋의 설정",
340
- ),
341
- variables: z
342
- .record(z.string(), z.unknown())
343
- .optional()
344
- .describe("쓰지 않는다(무시). 값은 그래프의 노드 설정 안에 있다(#115)"),
345
- });
329
+ export const createCommerceFlowRequestSchema = strictRequest(
330
+ z.object({
331
+ key: z
332
+ .string()
333
+ .regex(/^[a-z][a-z0-9-]{1,48}$/)
334
+ .describe("상점 안 유일한 키(소문자·숫자·하이픈)"),
335
+ name: z.string().min(1).max(60).optional(),
336
+ presetKey: z.string().optional().describe("출발한 프리셋(출처로 남는다). 없으면 빈 플로우"),
337
+ graph: flowDocumentSchema
338
+ .optional()
339
+ .describe(
340
+ "편집기에서 고친 그래프(#115). 없으면 프리셋 그래프 그대로, 프리셋도 없으면 빈 흐름(결제 완료 노드 하나)이다",
341
+ ),
342
+ definition: flowDocumentSchema
343
+ .optional()
344
+ .describe(
345
+ "흐름 설정 `requires`(progression·groupClaim·fulfillmentMethods). 없으면 프리셋의 설정",
346
+ ),
347
+ variables: z
348
+ .record(z.string(), z.unknown())
349
+ .optional()
350
+ .describe("쓰지 않는다(무시). 값은 그래프의 노드 설정 안에 있다(#115)"),
351
+ }),
352
+ );
346
353
  export type CreateCommerceFlowRequest = z.infer<typeof createCommerceFlowRequestSchema>;
347
354
 
348
- export const saveFlowDraftRequestSchema = z.object({
349
- graph: flowDocumentSchema,
350
- definition: flowDocumentSchema
351
- .optional()
352
- .describe("흐름 설정 `requires`(progression·groupClaim·fulfillmentMethods)"),
353
- variables: z
354
- .record(z.string(), z.unknown())
355
- .optional()
356
- .describe("쓰지 않는다(무시). 값은 그래프의 노드 설정 안에 있다(#115)"),
357
- });
355
+ export const saveFlowDraftRequestSchema = strictRequest(
356
+ z.object({
357
+ graph: flowDocumentSchema,
358
+ definition: flowDocumentSchema
359
+ .optional()
360
+ .describe("흐름 설정 `requires`(progression·groupClaim·fulfillmentMethods)"),
361
+ variables: z
362
+ .record(z.string(), z.unknown())
363
+ .optional()
364
+ .describe("쓰지 않는다(무시). 값은 그래프의 노드 설정 안에 있다(#115)"),
365
+ }),
366
+ );
358
367
  export type SaveFlowDraftRequest = z.infer<typeof saveFlowDraftRequestSchema>;
359
368
 
360
369
  export const flowIssueSchema = z.object({
@@ -475,14 +484,16 @@ export const flowDraftOperationSchema = z.discriminatedUnion("op", [
475
484
  ]);
476
485
  export type FlowDraftOperation = z.infer<typeof flowDraftOperationSchema>;
477
486
 
478
- export const applyFlowOperationsRequestSchema = z.object({
479
- operations: z
480
- .array(flowDraftOperationSchema)
481
- .min(1)
482
- .describe(
483
- "차례대로 적용한다. 묶음은 원자적이다 — 하나라도 형식이 틀리면 아무것도 바꾸지 않는다(400)",
484
- ),
485
- });
487
+ export const applyFlowOperationsRequestSchema = strictRequest(
488
+ z.object({
489
+ operations: z
490
+ .array(flowDraftOperationSchema)
491
+ .min(1)
492
+ .describe(
493
+ "차례대로 적용한다. 묶음은 원자적이다 — 하나라도 형식이 틀리면 아무것도 바꾸지 않는다(400)",
494
+ ),
495
+ }),
496
+ );
486
497
  export type ApplyFlowOperationsRequest = z.infer<typeof applyFlowOperationsRequestSchema>;
487
498
 
488
499
  export const flowOperationsResultSchema = z.object({
@@ -533,17 +544,19 @@ export const flowSecretSchema = z.object({
533
544
  });
534
545
  export type FlowSecret = z.infer<typeof flowSecretSchema>;
535
546
 
536
- export const saveFlowSecretRequestSchema = z.object({
537
- url: z
538
- .url()
539
- .refine((value) => value.startsWith("https://"), { message: "https 주소만 받습니다" })
540
- .describe("요청 주소. https만, 사설·루프백 주소는 400 `FLOW_SECRET_URL_INVALID`"),
541
- headers: z
542
- .record(z.string().regex(/^[A-Za-z0-9-]{1,64}$/), z.string().max(2000))
543
- .refine((value) => Object.keys(value).length <= 10, { message: "헤더는 10개까지입니다" })
544
- .default({})
545
- .describe("함께 보낼 헤더(인증 등). 값은 암호화해 두고 다시 보여 주지 않는다"),
546
- });
547
+ export const saveFlowSecretRequestSchema = strictRequest(
548
+ z.object({
549
+ url: z
550
+ .url()
551
+ .refine((value) => value.startsWith("https://"), { message: "https 주소만 받습니다" })
552
+ .describe("요청 주소. https만, 사설·루프백 주소는 400 `FLOW_SECRET_URL_INVALID`"),
553
+ headers: z
554
+ .record(z.string().regex(/^[A-Za-z0-9-]{1,64}$/), z.string().max(2000))
555
+ .refine((value) => Object.keys(value).length <= 10, { message: "헤더는 10개까지입니다" })
556
+ .default({})
557
+ .describe("함께 보낼 헤더(인증 등). 값은 암호화해 두고 다시 보여 주지 않는다"),
558
+ }),
559
+ );
547
560
  export type SaveFlowSecretRequest = z.input<typeof saveFlowSecretRequestSchema>;
548
561
 
549
562
  /**
@@ -584,22 +597,24 @@ const simulationEventSchema = z.object({
584
597
  key: z.string().optional().describe("행동·사건·타이머 키(start는 없음)"),
585
598
  });
586
599
 
587
- export const simulateFlowRequestSchema = z.object({
588
- startAt: z.iso.datetime({ offset: true }).optional().describe("가상 시계 시작(기본: 지금)"),
589
- context: z
590
- .record(z.string(), z.unknown())
591
- .optional()
592
- .describe("식이 읽을 값(item·order·buyer·product). 비우면 견본 주문 값"),
593
- steps: z
594
- .array(
595
- z.union([
596
- z.object({ event: simulationEventSchema }),
597
- z.object({ advance: z.string().regex(/^P/).describe("시간 경과(ISO 8601 기간)") }),
598
- ]),
599
- )
600
- .min(1)
601
- .max(100),
602
- });
600
+ export const simulateFlowRequestSchema = strictRequest(
601
+ z.object({
602
+ startAt: z.iso.datetime({ offset: true }).optional().describe("가상 시계 시작(기본: 지금)"),
603
+ context: z
604
+ .record(z.string(), z.unknown())
605
+ .optional()
606
+ .describe("식이 읽을 값(item·order·buyer·product). 비우면 견본 주문 값"),
607
+ steps: z
608
+ .array(
609
+ z.union([
610
+ z.object({ event: simulationEventSchema }),
611
+ z.object({ advance: z.string().regex(/^P/).describe("시간 경과(ISO 8601 기간)") }),
612
+ ]),
613
+ )
614
+ .min(1)
615
+ .max(100),
616
+ }),
617
+ );
603
618
  export type SimulateFlowRequest = z.infer<typeof simulateFlowRequestSchema>;
604
619
 
605
620
  export const simulationStepSchema = z.object({
@@ -628,9 +643,13 @@ export const simulateFlowResultSchema = z.object({
628
643
  });
629
644
  export type SimulateFlowResult = z.infer<typeof simulateFlowResultSchema>;
630
645
 
631
- export const replayFlowRequestSchema = z.object({
632
- orderItemId: z.string().describe("재생할 주문상품 — 그 흐름의 실행 기록을 초안으로 다시 돌린다"),
633
- });
646
+ export const replayFlowRequestSchema = strictRequest(
647
+ z.object({
648
+ orderItemId: z
649
+ .string()
650
+ .describe("재생할 주문상품 — 그 흐름의 실행 기록을 초안으로 다시 돌린다"),
651
+ }),
652
+ );
634
653
  export type ReplayFlowRequest = z.infer<typeof replayFlowRequestSchema>;
635
654
 
636
655
  export const replayFlowResultSchema = z.object({
@@ -861,12 +880,14 @@ export const flowEditSessionViewSchema = z.object({
861
880
  });
862
881
  export type FlowEditSessionView = z.infer<typeof flowEditSessionViewSchema>;
863
882
 
864
- export const claimFlowEditSessionRequestSchema = z.object({
865
- takeover: z
866
- .boolean()
867
- .optional()
868
- .describe(
869
- "다른 주체가 잡고 있어도 넘겨받는다. 앞 주체의 저장하지 않은 수정은 사라지고, AI 도우미면 그 실행이 멈춘다",
870
- ),
871
- });
883
+ export const claimFlowEditSessionRequestSchema = strictRequest(
884
+ z.object({
885
+ takeover: z
886
+ .boolean()
887
+ .optional()
888
+ .describe(
889
+ "다른 주체가 잡고 있어도 넘겨받는다. 앞 주체의 저장하지 않은 수정은 사라지고, AI 도우미면 그 실행이 멈춘다",
890
+ ),
891
+ }),
892
+ );
872
893
  export type ClaimFlowEditSessionRequest = z.infer<typeof claimFlowEditSessionRequestSchema>;
@@ -1,4 +1,4 @@
1
- import { z } from "zod";
1
+ import * as z from "zod";
2
2
 
3
3
  /**
4
4
  * 국내 쇼핑몰 번호 체계(이슈 #107) — 공개 ID(`ord_…` 등)와 따로 싣는 숫자 번호의 표기·필드.
@@ -1,4 +1,4 @@
1
- import { z } from "zod";
1
+ import * as z from "zod";
2
2
 
3
3
  export const pageInfoSchema = z.object({
4
4
  page: z.number().int(),
@@ -1,4 +1,4 @@
1
- import { z } from "zod";
1
+ import * as z from "zod";
2
2
  import { pageQueryShape } from "./common";
3
3
  import { currencyCodeSchema } from "./currency";
4
4
  import {
@@ -9,6 +9,7 @@ import {
9
9
  couponKindSchema,
10
10
  couponTargetSchema,
11
11
  } from "./promotion";
12
+ import { strictRequest } from "./strict";
12
13
 
13
14
  /**
14
15
  * 쿠폰 관리 API 계약 (이슈 #47) — `/v1/coupons/**`, 스코프 `promotion:r|rw`.
@@ -86,93 +87,100 @@ const couponTimeField = (description: string) =>
86
87
  z.iso.datetime({ offset: true }).nullable().optional().describe(description);
87
88
 
88
89
  /** `POST /v1/coupons` */
89
- export const createCouponRequestSchema = z
90
- .strictObject({
91
- name: z.string().trim().min(1).max(60).describe(descriptions.name),
90
+ export const createCouponRequestSchema = strictRequest(
91
+ z
92
+ .strictObject({
93
+ name: z.string().trim().min(1).max(60).describe(descriptions.name),
94
+ description: z.string().max(300).nullable().optional().describe(descriptions.description),
95
+ kind: couponKindSchema,
96
+ benefit: couponBenefitSchema,
97
+ conditions: couponConditionsInputSchema
98
+ .optional()
99
+ .describe("사용 조건. 생략한 항목은 기본값(최소 금액 0, 회원 전용, 즉시할인과 중복 가능)"),
100
+ target: couponTargetInputSchema.optional().describe("대상 상품·카테고리. 생략하면 전 상품"),
101
+ issueMethod: couponIssueMethodSchema,
102
+ code: z.string().regex(COUPON_CODE_PATTERN).optional().describe(descriptions.code),
103
+ startsAt: couponTimeField(descriptions.startsAt),
104
+ endsAt: couponTimeField(descriptions.endsAt),
105
+ validDaysAfterIssue: z
106
+ .number()
107
+ .int()
108
+ .min(1)
109
+ .max(3650)
110
+ .nullable()
111
+ .optional()
112
+ .describe(descriptions.validDaysAfterIssue),
113
+ totalIssueLimit: limitField(descriptions.totalIssueLimit),
114
+ totalUseLimit: limitField(descriptions.totalUseLimit),
115
+ perMemberIssueLimit: limitField(descriptions.perMemberIssueLimit),
116
+ perMemberUseLimit: limitField(descriptions.perMemberUseLimit),
117
+ currency: currencyCodeSchema
118
+ .optional()
119
+ .describe(
120
+ "쿠폰 통화(이슈 #130). 정액·최대 할인액·최소 주문 금액이 이 통화 값이고 같은 통화 주문서에만 적용된다. 생략하면 기본 통화(KRW). 판매 통화여야 하고 만든 뒤에는 바꿀 수 없다",
121
+ ),
122
+ })
123
+ .superRefine((body, ctx) => {
124
+ const issue = (path: string, message: string) =>
125
+ ctx.addIssue({ code: "custom", path: [path], message });
126
+ if (body.issueMethod === "CODE" && !body.code)
127
+ issue("code", "코드 입력형 쿠폰은 코드가 필요합니다");
128
+ if (body.issueMethod !== "CODE" && body.code)
129
+ issue("code", "코드는 코드 입력형 쿠폰에만 둡니다");
130
+ if (body.issueMethod === "CODE" && body.validDaysAfterIssue != null) {
131
+ issue("validDaysAfterIssue", "발급 후 유효 일수는 셀러 지급 쿠폰에만 둡니다");
132
+ }
133
+ if (body.issueMethod === "CODE" && body.perMemberIssueLimit != null) {
134
+ issue("perMemberIssueLimit", "1인 발급 수 한도는 발급형 쿠폰에만 둡니다");
135
+ }
136
+ if (
137
+ body.issueMethod === "SIGNUP" &&
138
+ body.perMemberIssueLimit != null &&
139
+ body.perMemberIssueLimit !== 1
140
+ ) {
141
+ issue("perMemberIssueLimit", "회원가입 쿠폰은 1인 1장입니다");
142
+ }
143
+ if (body.issueMethod !== "CODE" && body.perMemberUseLimit != null) {
144
+ issue(
145
+ "perMemberUseLimit",
146
+ "발급형 쿠폰은 발급 한 장이 한 번 쓰입니다. 1인 발급 수 한도를 사용하십시오",
147
+ );
148
+ }
149
+ const memberOnly = body.conditions?.memberOnly ?? COUPON_CONDITIONS_DEFAULTS.memberOnly;
150
+ if (!memberOnly && (body.issueMethod !== "CODE" || body.perMemberUseLimit != null)) {
151
+ issue(
152
+ "conditions",
153
+ "비회원에게 여는 쿠폰은 코드 입력형이면서 1인 사용 한도가 없어야 합니다",
154
+ );
155
+ }
156
+ if (body.startsAt && body.endsAt && Date.parse(body.endsAt) <= Date.parse(body.startsAt)) {
157
+ issue("endsAt", "종료 시각은 시작 시각보다 뒤여야 합니다");
158
+ }
159
+ }),
160
+ );
161
+ export type CreateCouponRequest = z.infer<typeof createCouponRequestSchema>;
162
+
163
+ /**
164
+ * `PATCH /v1/coupons/{couponId}` — 준 항목만 바뀐다. 발급·사용이 시작된 쿠폰(`locked`)은 이름·설명·종료일 연장·
165
+ * 한도 상향만 받고, 그 밖의 항목이 현재 값과 다르면 409 `COUPON_LOCKED`다. 종류·발급 방식은 바꿀 수 없다.
166
+ */
167
+ export const updateCouponRequestSchema = strictRequest(
168
+ z.strictObject({
169
+ name: z.string().trim().min(1).max(60).optional().describe(descriptions.name),
92
170
  description: z.string().max(300).nullable().optional().describe(descriptions.description),
93
- kind: couponKindSchema,
94
- benefit: couponBenefitSchema,
95
- conditions: couponConditionsInputSchema
96
- .optional()
97
- .describe("사용 조건. 생략한 항목은 기본값(최소 금액 0, 회원 전용, 즉시할인과 중복 가능)"),
98
- target: couponTargetInputSchema.optional().describe("대상 상품·카테고리. 생략하면 전 상품"),
99
- issueMethod: couponIssueMethodSchema,
171
+ benefit: couponBenefitSchema.optional(),
172
+ conditions: couponConditionsInputSchema.optional(),
173
+ target: couponTargetInputSchema.optional(),
100
174
  code: z.string().regex(COUPON_CODE_PATTERN).optional().describe(descriptions.code),
101
175
  startsAt: couponTimeField(descriptions.startsAt),
102
176
  endsAt: couponTimeField(descriptions.endsAt),
103
- validDaysAfterIssue: z
104
- .number()
105
- .int()
106
- .min(1)
107
- .max(3650)
108
- .nullable()
109
- .optional()
110
- .describe(descriptions.validDaysAfterIssue),
177
+ validDaysAfterIssue: z.number().int().min(1).max(3650).nullable().optional(),
111
178
  totalIssueLimit: limitField(descriptions.totalIssueLimit),
112
179
  totalUseLimit: limitField(descriptions.totalUseLimit),
113
180
  perMemberIssueLimit: limitField(descriptions.perMemberIssueLimit),
114
181
  perMemberUseLimit: limitField(descriptions.perMemberUseLimit),
115
- currency: currencyCodeSchema
116
- .optional()
117
- .describe(
118
- "쿠폰 통화(이슈 #130). 정액·최대 할인액·최소 주문 금액이 이 통화 값이고 같은 통화 주문서에만 적용된다. 생략하면 기본 통화(KRW). 판매 통화여야 하고 만든 뒤에는 바꿀 수 없다",
119
- ),
120
- })
121
- .superRefine((body, ctx) => {
122
- const issue = (path: string, message: string) =>
123
- ctx.addIssue({ code: "custom", path: [path], message });
124
- if (body.issueMethod === "CODE" && !body.code)
125
- issue("code", "코드 입력형 쿠폰은 코드가 필요합니다");
126
- if (body.issueMethod !== "CODE" && body.code)
127
- issue("code", "코드는 코드 입력형 쿠폰에만 둡니다");
128
- if (body.issueMethod === "CODE" && body.validDaysAfterIssue != null) {
129
- issue("validDaysAfterIssue", "발급 후 유효 일수는 셀러 지급 쿠폰에만 둡니다");
130
- }
131
- if (body.issueMethod === "CODE" && body.perMemberIssueLimit != null) {
132
- issue("perMemberIssueLimit", "1인 발급 수 한도는 발급형 쿠폰에만 둡니다");
133
- }
134
- if (
135
- body.issueMethod === "SIGNUP" &&
136
- body.perMemberIssueLimit != null &&
137
- body.perMemberIssueLimit !== 1
138
- ) {
139
- issue("perMemberIssueLimit", "회원가입 쿠폰은 1인 1장입니다");
140
- }
141
- if (body.issueMethod !== "CODE" && body.perMemberUseLimit != null) {
142
- issue(
143
- "perMemberUseLimit",
144
- "발급형 쿠폰은 발급 한 장이 한 번 쓰입니다. 1인 발급 수 한도를 사용하십시오",
145
- );
146
- }
147
- const memberOnly = body.conditions?.memberOnly ?? COUPON_CONDITIONS_DEFAULTS.memberOnly;
148
- if (!memberOnly && (body.issueMethod !== "CODE" || body.perMemberUseLimit != null)) {
149
- issue("conditions", "비회원에게 여는 쿠폰은 코드 입력형이면서 1인 사용 한도가 없어야 합니다");
150
- }
151
- if (body.startsAt && body.endsAt && Date.parse(body.endsAt) <= Date.parse(body.startsAt)) {
152
- issue("endsAt", "종료 시각은 시작 시각보다 뒤여야 합니다");
153
- }
154
- });
155
- export type CreateCouponRequest = z.infer<typeof createCouponRequestSchema>;
156
-
157
- /**
158
- * `PATCH /v1/coupons/{couponId}` — 준 항목만 바뀐다. 발급·사용이 시작된 쿠폰(`locked`)은 이름·설명·종료일 연장·
159
- * 한도 상향만 받고, 그 밖의 항목이 현재 값과 다르면 409 `COUPON_LOCKED`다. 종류·발급 방식은 바꿀 수 없다.
160
- */
161
- export const updateCouponRequestSchema = z.strictObject({
162
- name: z.string().trim().min(1).max(60).optional().describe(descriptions.name),
163
- description: z.string().max(300).nullable().optional().describe(descriptions.description),
164
- benefit: couponBenefitSchema.optional(),
165
- conditions: couponConditionsInputSchema.optional(),
166
- target: couponTargetInputSchema.optional(),
167
- code: z.string().regex(COUPON_CODE_PATTERN).optional().describe(descriptions.code),
168
- startsAt: couponTimeField(descriptions.startsAt),
169
- endsAt: couponTimeField(descriptions.endsAt),
170
- validDaysAfterIssue: z.number().int().min(1).max(3650).nullable().optional(),
171
- totalIssueLimit: limitField(descriptions.totalIssueLimit),
172
- totalUseLimit: limitField(descriptions.totalUseLimit),
173
- perMemberIssueLimit: limitField(descriptions.perMemberIssueLimit),
174
- perMemberUseLimit: limitField(descriptions.perMemberUseLimit),
175
- });
182
+ }),
183
+ );
176
184
  export type UpdateCouponRequest = z.infer<typeof updateCouponRequestSchema>;
177
185
 
178
186
  /** 쿠폰 응답 */
@@ -1,4 +1,4 @@
1
- import { z } from "zod";
1
+ import * as z from "zod";
2
2
  import { BASE_CURRENCY, currencyCodeSchema, isSaleUnitPrice } from "./currency";
3
3
 
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { z } from "zod";
1
+ import * as z from "zod";
2
2
 
3
3
  /**
4
4
  * 통화표와 금액 변환(이슈 #130). 숫자만 다룬다 — 사람이 읽는 문자열(「1,000원」·「$10.99」)은 각 앱의 `formatMoney`가 만든다.
@@ -1,4 +1,5 @@
1
- import { z } from "zod";
1
+ import * as z from "zod";
2
+ import { strictRequest } from "./strict";
2
3
 
3
4
  /**
4
5
  * 구매자 위임 REST(`POST /v1/customers/delegations`, #71) — 계정 토큰(또는 관리 API 토큰) + `X-Store-Id`로 부른다(`customer:rw`).
@@ -8,33 +9,35 @@ import { z } from "zod";
8
9
 
9
10
  const uidPattern = /^[A-Za-z0-9._:@|-]{1,128}$/;
10
11
 
11
- export const customerDelegationRequestSchema = z
12
- .object({
13
- uid: z
14
- .string()
15
- .regex(uidPattern)
16
- .optional()
17
- .describe(
18
- "셀러 자체 인증의 사용자 id(1~128자, 영문·숫자·`._:@|-`). 처음 보는 id면 구매자를 만든다",
19
- ),
20
- email: z
21
- .email()
22
- .max(254)
23
- .optional()
24
- .describe("uid가 없으면 이메일로 지목한다. uid와 함께 주면 처음 만들 때의 이메일이다"),
25
- name: z.string().max(50).optional().describe("처음 만들 때의 이름"),
26
- authenticatedAt: z.iso
27
- .datetime({ offset: true })
28
- .optional()
29
- .describe(
30
- "사용자가 셀러 서비스에서 실제로 로그인한 시각(uid 위임만). 주면 구매자 토큰의 인증 시각이 된다",
31
- ),
32
- })
33
- .refine((value) => Boolean(value.uid || value.email), {
34
- message: "uid 또는 email이 필요합니다",
35
- path: ["uid"],
36
- })
37
- .describe("구매자 위임 요청");
12
+ export const customerDelegationRequestSchema = strictRequest(
13
+ z
14
+ .object({
15
+ uid: z
16
+ .string()
17
+ .regex(uidPattern)
18
+ .optional()
19
+ .describe(
20
+ "셀러 자체 인증의 사용자 id(1~128자, 영문·숫자·`._:@|-`). 처음 보는 id면 구매자를 만든다",
21
+ ),
22
+ email: z
23
+ .email()
24
+ .max(254)
25
+ .optional()
26
+ .describe("uid가 없으면 이메일로 지목한다. uid와 함께 주면 처음 만들 때의 이메일이다"),
27
+ name: z.string().max(50).optional().describe("처음 만들 때의 이름"),
28
+ authenticatedAt: z.iso
29
+ .datetime({ offset: true })
30
+ .optional()
31
+ .describe(
32
+ "사용자가 셀러 서비스에서 실제로 로그인한 시각(uid 위임만). 주면 구매자 토큰의 인증 시각이 된다",
33
+ ),
34
+ })
35
+ .refine((value) => Boolean(value.uid || value.email), {
36
+ message: "uid 또는 email이 필요합니다",
37
+ path: ["uid"],
38
+ })
39
+ .describe("구매자 위임 요청"),
40
+ );
38
41
  export type CustomerDelegationRequest = z.infer<typeof customerDelegationRequestSchema>;
39
42
 
40
43
  export const customerDelegationSchema = z
@@ -1,5 +1,6 @@
1
- import { z } from "zod";
1
+ import * as z from "zod";
2
2
  import { orderItemStatusSchema, pageQueryShape } from "./common";
3
+ import { strictRequest } from "./strict";
3
4
 
4
5
  /** 구매자 목록 정렬 */
5
6
  export const customerSortSchema = z
@@ -198,12 +199,14 @@ export const customerOrderSchema = z.object({
198
199
  export type CustomerOrder = z.infer<typeof customerOrderSchema>;
199
200
 
200
201
  /** `POST /v1/customers/{id}/withdraw` 본문 */
201
- export const withdrawCustomerRequestSchema = z.object({
202
- reason: z
203
- .enum(["BUYER_REQUEST", "DORMANT", "ABUSE", "OTHER"])
204
- .describe("처리 사유 — 구매자 요청·장기 미이용·부정 이용·기타. 감사 로그에 남는다"),
205
- memo: z.string().max(200).optional().describe("사유 보충(선택)"),
206
- });
202
+ export const withdrawCustomerRequestSchema = strictRequest(
203
+ z.object({
204
+ reason: z
205
+ .enum(["BUYER_REQUEST", "DORMANT", "ABUSE", "OTHER"])
206
+ .describe("처리 사유 — 구매자 요청·장기 미이용·부정 이용·기타. 감사 로그에 남는다"),
207
+ memo: z.string().max(200).optional().describe("사유 보충(선택)"),
208
+ }),
209
+ );
207
210
  export type WithdrawCustomerRequest = z.infer<typeof withdrawCustomerRequestSchema>;
208
211
 
209
212
  /** `WITHDRAWAL_BLOCKED`의 `error.details` — 탈퇴를 막는 항목 수(스토어프론트 탈퇴와 같다) */
@@ -1,4 +1,4 @@
1
- import { z } from "zod";
1
+ import * as z from "zod";
2
2
  import { shippingCurrencyPricesSchema, storedShippingCurrencyPriceSchema } from "./currency-prices";
3
3
 
4
4
  /**
@@ -1,4 +1,5 @@
1
- import { z } from "zod";
1
+ import * as z from "zod";
2
+ import { strictRequest } from "./strict";
2
3
 
3
4
  /**
4
5
  * 위치별 재고 (이슈 #132) — 재고 위치(창고·상설 매장·팝업 매장)마다 조합의 가용 재고를 따로 센다. 출고지·반품지·픽업 장소가 위치를
@@ -202,29 +203,33 @@ export const inventoryOperationResultSchema = z.object({
202
203
  export type InventoryOperationResult = z.infer<typeof inventoryOperationResultSchema>;
203
204
 
204
205
  /** 수거 완료의 재입고 지정(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
- });
206
+ export const claimCollectCompleteRequestSchema = strictRequest(
207
+ z.object({
208
+ restock: z
209
+ .object({
210
+ optionId: z
211
+ .string()
212
+ .min(1)
213
+ .optional()
214
+ .describe(
215
+ "돌아온 조합 — 그 주문상품의 할당 조합이어야 한다(아니면 400 `INVALID_RESTOCK_OPTION`)",
216
+ ),
217
+ inventoryLocationId: z.string().min(1).optional().describe("재입고할 재고 위치"),
218
+ })
219
+ .optional(),
220
+ }),
221
+ );
219
222
  export type ClaimCollectCompleteRequest = z.infer<typeof claimCollectCompleteRequestSchema>;
220
223
 
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
- });
224
+ export const claimRedeliveryRequestSchema = strictRequest(
225
+ z.object({
226
+ carrierCode: z.string().min(1),
227
+ trackingNumber: z.string().min(1),
228
+ inventoryLocationId: z
229
+ .string()
230
+ .min(1)
231
+ .optional()
232
+ .describe("교환품을 보낼 재고 위치. 생략하면 그 상품의 기준 위치"),
233
+ }),
234
+ );
230
235
  export type ClaimRedeliveryRequest = z.infer<typeof claimRedeliveryRequestSchema>;