@sayren/store-sdk 0.29.7 → 0.29.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sayren/store-sdk",
3
- "version": "0.29.7",
3
+ "version": "0.29.8",
4
4
  "main": "./dist/index.mjs",
5
5
  "types": "./dist/index.d.mts",
6
6
  "exports": {
package/src/client.ts CHANGED
@@ -69,6 +69,28 @@ import {
69
69
  collectionProductsReplaceRequestSchema,
70
70
  collectionSchema,
71
71
  } from "./schemas/collection";
72
+ import {
73
+ type CreateCommerceFlowRequest,
74
+ commerceFlowCheckSchema,
75
+ commerceFlowSchema,
76
+ commerceFlowSummarySchema,
77
+ createCommerceFlowRequestSchema,
78
+ flowActionRequestSchema,
79
+ flowActionResultSchema,
80
+ flowAssignmentRuleSchema,
81
+ flowPresetSummarySchema,
82
+ flowSecretSchema,
83
+ orderItemFlowSchema,
84
+ resolvedFlowSchema,
85
+ type SaveFlowAssignmentRulesRequest,
86
+ type SaveFlowDraftRequest,
87
+ type SaveFlowSecretRequest,
88
+ saveFlowAssignmentRulesRequestSchema,
89
+ saveFlowDraftRequestSchema,
90
+ saveFlowSecretRequestSchema,
91
+ type TestFlowAssignmentRequest,
92
+ testFlowAssignmentRequestSchema,
93
+ } from "./schemas/commerce-flow";
72
94
  import { bulkResultSchema, type PageParams, pageSchema } from "./schemas/common";
73
95
  import {
74
96
  type CreateCouponRequest,
@@ -95,6 +117,11 @@ import {
95
117
  type WithdrawCustomerRequest,
96
118
  withdrawCustomerRequestSchema,
97
119
  } from "./schemas/customer";
120
+ import {
121
+ type PickupLocationRequestInput,
122
+ pickupLocationRequestSchema,
123
+ pickupLocationSchema,
124
+ } from "./schemas/fulfillment";
98
125
  import {
99
126
  type LoginProvider,
100
127
  loginProviderTestSchema,
@@ -215,6 +242,12 @@ import {
215
242
  savedOptionSetRequestSchema,
216
243
  savedOptionSetSchema,
217
244
  } from "./schemas/saved-option-set";
245
+ import {
246
+ calendarDaySchema,
247
+ calendarQuerySchema,
248
+ type SetCalendarRequest,
249
+ setCalendarRequestSchema,
250
+ } from "./schemas/stay";
218
251
  import {
219
252
  type AddressType,
220
253
  type Invitation,
@@ -506,6 +539,24 @@ export function createStoreClient(options: StoreClientOptions) {
506
539
  }),
507
540
  removeDiscount: (productId: string) =>
508
541
  storeHttp.requestVoid("DELETE", path`/products/${productId}/discount`),
542
+ /** 날짜별 재고 상품(`inventoryMode: "DATED"`, 이슈 #118)의 기간(포함) 달력. 없는 날은 싣지 않는다 */
543
+ getCalendar: (productId: string, query: z.input<typeof calendarQuerySchema>) =>
544
+ storeHttp.request(
545
+ "GET",
546
+ path`/products/${productId}/calendar`,
547
+ z.array(calendarDaySchema),
548
+ {
549
+ query: calendarQuerySchema.parse(query) as Query,
550
+ },
551
+ ),
552
+ /** 기간의 날짜마다 조합별 재고·가격을 덮어쓴다. `price: null`은 조합 가격으로 되돌린다 */
553
+ setCalendar: (productId: string, body: SetCalendarRequest) =>
554
+ storeHttp.request(
555
+ "PUT",
556
+ path`/products/${productId}/calendar`,
557
+ z.object({ updated: z.number().int() }),
558
+ { body: setCalendarRequestSchema.parse(body) },
559
+ ),
509
560
  },
510
561
 
511
562
  /**
@@ -730,6 +781,106 @@ export function createStoreClient(options: StoreClientOptions) {
730
781
  }),
731
782
  getDelivery: (orderItemId: string) =>
732
783
  storeHttp.request("GET", path`/order-items/${orderItemId}/delivery`, deliveryStatusSchema),
784
+ /** `GET /order-items/{id}/flow` — 커머스 플로우의 지금 상태·할 수 있는 행동·타이머(#115). 스코프 `order:r` */
785
+ getFlow: (orderItemId: string) =>
786
+ storeHttp.request("GET", path`/order-items/${orderItemId}/flow`, orderItemFlowSchema),
787
+ /**
788
+ * `POST /order-items/{id}/actions/{key}` — `getFlow().actions`에 켜진 행동을 실행한다(#115). 묶음 진행이면
789
+ * `affectedOrderItemIds`에 함께 움직인 주문상품이 온다. 에러: 409 `ACTION_NOT_AVAILABLE`
790
+ */
791
+ act: (
792
+ orderItemId: string,
793
+ key: string,
794
+ input: Record<string, unknown> = {},
795
+ idempotencyKey?: string,
796
+ ) =>
797
+ storeHttp.request(
798
+ "POST",
799
+ path`/order-items/${orderItemId}/actions/${key}`,
800
+ flowActionResultSchema,
801
+ { body: flowActionRequestSchema.parse({ input }), headers: idempotency(idempotencyKey) },
802
+ ),
803
+ /**
804
+ * `POST /order-items/{id}/events/{key}` — 현장 단말·연동의 상점 사건(`custom.<이름>`)을 플로우에 넣는다(#115).
805
+ * 에러: 400 `FLOW_EVENT_KEY_INVALID`, 409 `FLOW_EVENT_NOT_ACCEPTED`
806
+ */
807
+ emitEvent: (
808
+ orderItemId: string,
809
+ key: `custom.${string}`,
810
+ input: Record<string, unknown> = {},
811
+ idempotencyKey?: string,
812
+ ) =>
813
+ storeHttp.request(
814
+ "POST",
815
+ path`/order-items/${orderItemId}/events/${key}`,
816
+ flowActionResultSchema,
817
+ { body: flowActionRequestSchema.parse({ input }), headers: idempotency(idempotencyKey) },
818
+ ),
819
+ },
820
+
821
+ /**
822
+ * 커머스 플로우 (이슈 #115) — 프리셋 설치·초안 저장·검사·게시와 상품 할당 규칙. 조회 `flow:r`, 쓰기 `flow:rw`.
823
+ * 게시한 버전은 불변이고, 진행 중 주문은 결제 때 붙은 버전으로 끝까지 간다.
824
+ */
825
+ commerceFlows: {
826
+ presets: () => storeHttp.request("GET", "/flow-presets", z.array(flowPresetSummarySchema)),
827
+ list: () => storeHttp.request("GET", "/commerce-flows", z.array(commerceFlowSummarySchema)),
828
+ get: (flowId: string) =>
829
+ storeHttp.request("GET", path`/commerce-flows/${flowId}`, commerceFlowSchema),
830
+ /** 프리셋 설치 또는 빈 플로우. 에러: 409 `FLOW_KEY_TAKEN`·`FLOW_PRESET_UNAVAILABLE` */
831
+ create: (body: CreateCommerceFlowRequest) =>
832
+ storeHttp.request("POST", "/commerce-flows", commerceFlowSchema, {
833
+ body: createCommerceFlowRequestSchema.parse(body),
834
+ }),
835
+ /** 초안 저장. `revision`을 주면 그 초안일 때만 저장한다(409 `FLOW_REVISION_CONFLICT`) */
836
+ saveDraft: (flowId: string, body: SaveFlowDraftRequest, revision?: number) =>
837
+ storeHttp.request("PUT", path`/commerce-flows/${flowId}/draft`, commerceFlowSchema, {
838
+ body: saveFlowDraftRequestSchema.parse(body),
839
+ headers: revision === undefined ? undefined : { "if-match": String(revision) },
840
+ }),
841
+ check: (flowId: string) =>
842
+ storeHttp.request(
843
+ "POST",
844
+ path`/commerce-flows/${flowId}/draft/check`,
845
+ commerceFlowCheckSchema,
846
+ ),
847
+ /** 게시. 차단 오류가 있으면 409 `PUBLISH_BLOCKED`(`details.blocking`) */
848
+ publish: (flowId: string) =>
849
+ storeHttp.request(
850
+ "POST",
851
+ path`/commerce-flows/${flowId}/draft/publish`,
852
+ commerceFlowSchema,
853
+ ),
854
+ rules: () =>
855
+ storeHttp.request(
856
+ "GET",
857
+ "/commerce-flows/assignment-rules",
858
+ z.array(flowAssignmentRuleSchema),
859
+ ),
860
+ /** 할당 규칙 전체 교체(우선순위는 배열 순서). 에러: 400 `FLOW_ASSIGNMENT_CONDITION_INVALID`, 409 `FLOW_NOT_PUBLISHED` */
861
+ saveRules: (body: SaveFlowAssignmentRulesRequest) =>
862
+ storeHttp.request(
863
+ "PUT",
864
+ "/commerce-flows/assignment-rules",
865
+ z.array(flowAssignmentRuleSchema),
866
+ { body: saveFlowAssignmentRulesRequestSchema.parse(body) },
867
+ ),
868
+ testRules: (body: TestFlowAssignmentRequest) =>
869
+ storeHttp.request("POST", "/commerce-flows/assignment-rules/test", resolvedFlowSchema, {
870
+ body: testFlowAssignmentRequestSchema.parse(body),
871
+ }),
872
+ /** 이 상품에 지금 붙을 플로우 */
873
+ forProduct: (productId: string) =>
874
+ storeHttp.request("GET", path`/products/${productId}/flow`, resolvedFlowSchema),
875
+ /** HTTP 노드 시크릿 목록(헤더 값 제외) */
876
+ secrets: () => storeHttp.request("GET", "/commerce-flows/secrets", z.array(flowSecretSchema)),
877
+ /** 시크릿 저장(같은 이름이면 덮어쓴다). 상점 플랫폼 OWNER·ADMIN만. 에러: 400 `FLOW_SECRET_URL_INVALID` */
878
+ saveSecret: (name: string, body: SaveFlowSecretRequest) =>
879
+ storeHttp.request("PUT", path`/commerce-flows/secrets/${name}`, flowSecretSchema, {
880
+ body: saveFlowSecretRequestSchema.parse(body),
881
+ }),
882
+ removeSecret: (name: string) =>
883
+ storeHttp.requestVoid("DELETE", path`/commerce-flows/secrets/${name}`),
733
884
  },
734
885
 
735
886
  carriers: {
@@ -1335,6 +1486,21 @@ export function createStoreClient(options: StoreClientOptions) {
1335
1486
  storeHttp.requestVoid("POST", path`/seller-addresses/${addressId}/default`),
1336
1487
  },
1337
1488
 
1489
+ /** 픽업 장소 (이슈 #115) — 방문 수령 주문의 수령 장소. 조회 `store:r`, 쓰기 `store:rw`(상점 플랫폼 OWNER·ADMIN) */
1490
+ pickupLocations: {
1491
+ list: () => storeHttp.request("GET", "/pickup-locations", z.array(pickupLocationSchema)),
1492
+ create: (body: PickupLocationRequestInput) =>
1493
+ storeHttp.request("POST", "/pickup-locations", pickupLocationSchema, {
1494
+ body: pickupLocationRequestSchema.parse(body),
1495
+ }),
1496
+ update: (locationId: string, body: PickupLocationRequestInput) =>
1497
+ storeHttp.request("PUT", path`/pickup-locations/${locationId}`, pickupLocationSchema, {
1498
+ body: pickupLocationRequestSchema.parse(body),
1499
+ }),
1500
+ remove: (locationId: string) =>
1501
+ storeHttp.requestVoid("DELETE", path`/pickup-locations/${locationId}`),
1502
+ },
1503
+
1338
1504
  /**
1339
1505
  * 스토어프론트 호스팅 (이슈 #53) — 조회 `storefront:r`, 쓰기 `storefront:rw`(OWNER·ADMIN).
1340
1506
  * 쓰기 응답은 바뀐 사이트다. Cloudflare 반영은 커밋 뒤라 `provisioning.status`가 잠시 `PENDING`일 수 있다.
package/src/index.ts CHANGED
@@ -23,6 +23,7 @@ export * from "./schemas/auth";
23
23
  export * from "./schemas/category";
24
24
  export * from "./schemas/claim";
25
25
  export * from "./schemas/collection";
26
+ export * from "./schemas/commerce-flow";
26
27
  export * from "./schemas/commerce-number";
27
28
  export * from "./schemas/common";
28
29
  export * from "./schemas/coupon";
@@ -40,6 +41,7 @@ export * from "./schemas/refund-policy";
40
41
  export * from "./schemas/saved-option-set";
41
42
  export * from "./schemas/scope";
42
43
  export * from "./schemas/shipping-policy";
44
+ export * from "./schemas/stay";
43
45
  export * from "./schemas/store";
44
46
  export * from "./schemas/storefront-hosting";
45
47
  export * from "./schemas/subscription";
@@ -0,0 +1,259 @@
1
+ import { z } from "zod";
2
+ import { claimStatusSchema } from "./claim";
3
+ import { orderItemStatusSchema } from "./common";
4
+
5
+ /**
6
+ * 커머스 플로우(이슈 #115) — 주문상품이 따르는 플로우의 지금 상태와 할 수 있는 행동. 공개 계약은 필드 추가로만 자란다.
7
+ * 행동 키·필드 키·꺼진 이유는 상점·앱이 늘리는 **열린 문자열**이다(모르는 값도 파싱한다). 공개 상태(`coreStatus`)만 닫힌 8개 값이다.
8
+ * 화면은 이 값만 그리고 상태 이름으로 분기하지 않는다 — 판정은 서버가 한다.
9
+ */
10
+
11
+ export const flowInputFieldSchema = z.object({
12
+ key: z.string(),
13
+ type: z.string().describe("text · number · select · date · datetime · image (늘어날 수 있다)"),
14
+ required: z.boolean(),
15
+ label: z.string(),
16
+ maxLength: z.number().int().optional(),
17
+ maxFiles: z.number().int().optional(),
18
+ options: z.array(z.union([z.string(), z.number()])).optional(),
19
+ });
20
+ export type FlowInputField = z.infer<typeof flowInputFieldSchema>;
21
+
22
+ export const flowActionSchema = z.object({
23
+ key: z.string().describe("행동 키. 플랫폼 키(예: CONFIRM_ORDER)와 상점·앱이 정한 키가 섞인다"),
24
+ label: z.string().describe("보여 줄 이름"),
25
+ mode: z
26
+ .enum(["INSTANT", "REQUEST", "VIEW"])
27
+ .describe("INSTANT 바로 실행 · REQUEST 승인 요청(클레임) · VIEW 보기"),
28
+ enabled: z.boolean(),
29
+ disabled: z
30
+ .object({
31
+ reason: z.string().describe("EXPIRED · CONDITION (늘어날 수 있다)"),
32
+ message: z.string().optional(),
33
+ })
34
+ .optional()
35
+ .describe("꺼진 이유. 켜져 있으면 없다"),
36
+ availableUntil: z.string().optional().describe("이 시각까지만 할 수 있다(ISO 8601)"),
37
+ subflow: z.string().optional().describe("이 행동이 시작하는 하위 플로우(클레임)"),
38
+ input: z.array(flowInputFieldSchema).optional().describe("받는 입력 폼"),
39
+ partialQuantity: z.union([z.boolean(), z.literal("UNIT")]).optional(),
40
+ });
41
+ export type FlowAction = z.infer<typeof flowActionSchema>;
42
+
43
+ /** 커스텀 필드 값 — 문자열(날짜·일시는 ISO 8601)·숫자·참거짓·null, 수량 단위 필드는 문자열 배열 */
44
+ export const flowFieldValueSchema = z.union([
45
+ z.string(),
46
+ z.number(),
47
+ z.boolean(),
48
+ z.null(),
49
+ z.array(z.string()),
50
+ ]);
51
+ export type FlowFieldValue = z.infer<typeof flowFieldValueSchema>;
52
+
53
+ export const flowStateSchema = z.object({
54
+ key: z.string().describe("플로우 안 상태 키(상점이 정한다). 화면 분기에 쓰지 않는다"),
55
+ label: z.string().describe("보는 사람(셀러·구매자)의 상태 이름"),
56
+ coreStatus: orderItemStatusSchema.optional().describe("공개 상태(8개)"),
57
+ claimStatus: claimStatusSchema.optional(),
58
+ message: z.string().optional().describe("보는 사람에게 보여 줄 안내문"),
59
+ enteredAt: z.string().describe("이 상태에 들어온 시각(ISO 8601)"),
60
+ deadlineAt: z.string().optional().describe("가장 가까운 기한(ISO 8601)"),
61
+ attachments: z
62
+ .array(z.string())
63
+ .optional()
64
+ .describe("가장 최근 행동이 붙인 첨부 주소(시안 등, #115). 없으면 생략"),
65
+ progress: z.boolean().optional().describe("진행 단계 표시에 넣는 상태인가"),
66
+ fields: z.record(z.string(), flowFieldValueSchema).describe("보는 사람 공개 범위의 커스텀 필드"),
67
+ });
68
+ export type FlowState = z.infer<typeof flowStateSchema>;
69
+
70
+ export const orderItemFlowSchema = z.object({
71
+ orderItemId: z.string(),
72
+ flow: z.object({ flowId: z.string(), version: z.number().int() }),
73
+ flowState: flowStateSchema,
74
+ actions: z.array(flowActionSchema),
75
+ timers: z
76
+ .array(z.object({ key: z.string(), dueAt: z.string(), paused: z.boolean() }))
77
+ .describe("걸린 타이머(셀러 보기)"),
78
+ ended: z.boolean(),
79
+ });
80
+ export type OrderItemFlow = z.infer<typeof orderItemFlowSchema>;
81
+
82
+ export const flowActionRequestSchema = z.object({
83
+ input: z.record(z.string(), z.unknown()).optional().describe("행동 입력 — 행동의 `input` 폼 키"),
84
+ });
85
+ export type FlowActionRequest = z.infer<typeof flowActionRequestSchema>;
86
+
87
+ /**
88
+ * 행동 실행 결과. 지금은 동기 완료(COMPLETED)뿐이다. 외부 호출이 든 전이는 202와 함께 ACCEPTED(runId)가 온다(HTTP·앱 노드 단계에서 더한다).
89
+ * 동기 거절은 409(ACTION_NOT_AVAILABLE)다.
90
+ */
91
+ export const flowActionResultSchema = z.object({
92
+ status: z.literal("COMPLETED"),
93
+ flowState: flowStateSchema,
94
+ actions: z.array(flowActionSchema),
95
+ /** 이 행동으로 바뀐 주문상품(주문 묶음 진행이면 여럿) */
96
+ affectedOrderItemIds: z.array(z.string()),
97
+ });
98
+ export type FlowActionResult = z.infer<typeof flowActionResultSchema>;
99
+
100
+ // ── 플로우 정의 관리(이슈 #115 D3a) — `/v1/commerce-flows/**`, 스코프 `flow:r|rw` ─────────────────────
101
+
102
+ /** 플로우 작성 그래프와 정의는 형태가 크고 노드 종류가 늘어나므로 여기서는 열린 객체로 싣는다(검증은 서버 `…/draft/check`) */
103
+ const flowDocumentSchema = z.record(z.string(), z.unknown());
104
+
105
+ export const flowPresetSummarySchema = z.object({
106
+ key: z.string(),
107
+ presetVersion: z.number().int(),
108
+ name: z.string(),
109
+ description: z.string(),
110
+ availability: z
111
+ .enum(["AVAILABLE", "BLOCKED"])
112
+ .describe("BLOCKED면 의존 이슈(dependsOn)가 끝나야 설치할 수 있다"),
113
+ dependsOn: z.array(z.string()),
114
+ requires: flowDocumentSchema.describe(
115
+ "프리셋이 쓰는 Core 기능(진행 단위·이행 방식·주문 입력 등)",
116
+ ),
117
+ variables: z.array(flowDocumentSchema).describe("셀러가 값만 입력하는 변수 정의"),
118
+ });
119
+ export type FlowPresetSummary = z.infer<typeof flowPresetSummarySchema>;
120
+
121
+ export const commerceFlowSummarySchema = z.object({
122
+ flowId: z.string(),
123
+ key: z.string().describe("상점 안 유일한 키"),
124
+ name: z.string(),
125
+ progression: z.enum(["ITEM", "ORDER_GROUP"]),
126
+ fulfillmentMethods: z.array(z.string()),
127
+ presetKey: z.string().nullable().describe("설치한 프리셋(설치 뒤 연결되지 않는다)"),
128
+ presetVersion: z.number().int().nullable(),
129
+ activeVersion: z.number().int().nullable().describe("게시된 버전. 새 주문이 따른다"),
130
+ archivedAt: z.string().nullable(),
131
+ createdAt: z.string(),
132
+ updatedAt: z.string(),
133
+ });
134
+ export type CommerceFlowSummary = z.infer<typeof commerceFlowSummarySchema>;
135
+
136
+ export const commerceFlowVersionViewSchema = z.object({
137
+ version: z.number().int(),
138
+ status: z.enum(["DRAFT", "ACTIVE", "LEGACY"]),
139
+ revision: z.number().int().describe("초안 저장의 If-Match 값"),
140
+ graph: flowDocumentSchema,
141
+ definition: flowDocumentSchema,
142
+ variables: z.record(z.string(), z.unknown()),
143
+ publishedAt: z.string().nullable(),
144
+ });
145
+
146
+ export const commerceFlowSchema = commerceFlowSummarySchema.extend({
147
+ draft: commerceFlowVersionViewSchema.nullable(),
148
+ active: commerceFlowVersionViewSchema.nullable(),
149
+ versions: z.array(
150
+ z.object({
151
+ version: z.number().int(),
152
+ status: z.enum(["DRAFT", "ACTIVE", "LEGACY"]),
153
+ publishedAt: z.string().nullable(),
154
+ }),
155
+ ),
156
+ });
157
+ export type CommerceFlow = z.infer<typeof commerceFlowSchema>;
158
+
159
+ export const createCommerceFlowRequestSchema = z.object({
160
+ key: z
161
+ .string()
162
+ .regex(/^[a-z][a-z0-9-]{1,48}$/)
163
+ .describe("상점 안 유일한 키(소문자·숫자·하이픈)"),
164
+ name: z.string().min(1).max(60).optional(),
165
+ presetKey: z.string().optional().describe("설치할 프리셋. 없으면 빈 플로우"),
166
+ variables: z.record(z.string(), z.unknown()).optional().describe("프리셋 변수 값"),
167
+ });
168
+ export type CreateCommerceFlowRequest = z.infer<typeof createCommerceFlowRequestSchema>;
169
+
170
+ export const saveFlowDraftRequestSchema = z.object({
171
+ graph: flowDocumentSchema,
172
+ definition: flowDocumentSchema.optional(),
173
+ variables: z.record(z.string(), z.unknown()).optional(),
174
+ });
175
+ export type SaveFlowDraftRequest = z.infer<typeof saveFlowDraftRequestSchema>;
176
+
177
+ const flowIssueSchema = z.object({
178
+ code: z.string(),
179
+ message: z.string(),
180
+ nodeId: z.string().optional(),
181
+ edgeId: z.string().optional(),
182
+ });
183
+
184
+ export const commerceFlowCheckSchema = z.object({
185
+ publishable: z.boolean().describe("게시를 막는 오류(실행 불가·공개 계약·한도)가 없다"),
186
+ blocking: z.array(flowIssueSchema),
187
+ warnings: z.array(flowIssueSchema).describe("업무 논리 경고 — 게시를 막지 않는다"),
188
+ });
189
+ export type CommerceFlowCheck = z.infer<typeof commerceFlowCheckSchema>;
190
+
191
+ // ── 할당 규칙(이슈 #115 D3b) — `/v1/commerce-flows/assignment-rules`, 스코프 `flow:r|rw` ─────────────────
192
+
193
+ export const flowAssignmentRuleSchema = z.object({
194
+ ruleId: z.string(),
195
+ priority: z.number().int().describe("작을수록 먼저 본다"),
196
+ condition: z
197
+ .string()
198
+ .describe(
199
+ 'CEL 조건식. 읽는 값: product(id·category·categoryPath·tags), order(amount·paymentMethod), buyer(member). 예: `"cat_1" in product.categoryPath`',
200
+ ),
201
+ flowId: z.string(),
202
+ });
203
+ export type FlowAssignmentRule = z.infer<typeof flowAssignmentRuleSchema>;
204
+
205
+ export const saveFlowAssignmentRulesRequestSchema = z.object({
206
+ rules: z
207
+ .array(z.object({ condition: z.string().min(1).max(500), flowId: z.string() }))
208
+ .max(50)
209
+ .describe("위에서부터 처음 맞는 규칙의 게시된 플로우를 쓴다. 목록 전체를 바꾼다"),
210
+ });
211
+ export type SaveFlowAssignmentRulesRequest = z.infer<typeof saveFlowAssignmentRulesRequestSchema>;
212
+
213
+ export const resolvedFlowSchema = z.object({
214
+ flowId: z.string(),
215
+ version: z.number().int(),
216
+ source: z
217
+ .enum(["RULE", "PLATFORM"])
218
+ .describe("RULE 할당 규칙 · PLATFORM 맞는 규칙이 없어 플랫폼 기본 플로우"),
219
+ ruleId: z.string().optional(),
220
+ });
221
+ export type ResolvedFlow = z.infer<typeof resolvedFlowSchema>;
222
+
223
+ export const testFlowAssignmentRequestSchema = z.object({
224
+ productId: z.string(),
225
+ orderAmount: z.number().int().nonnegative().optional(),
226
+ paymentMethod: z.string().optional(),
227
+ member: z.boolean().optional(),
228
+ });
229
+ export type TestFlowAssignmentRequest = z.infer<typeof testFlowAssignmentRequestSchema>;
230
+
231
+ // ─── 플로우 시크릿(이슈 #115 E5b) ─────────────────────────────
232
+
233
+ /** 시크릿 이름 — 플로우 변수 `secretRef`의 값이다 */
234
+ export const flowSecretNameSchema = z
235
+ .string()
236
+ .regex(/^[a-z][a-z0-9-]{1,39}$/)
237
+ .describe("시크릿 이름(소문자·숫자·하이픈 2~40자). 플로우 변수(시크릿)의 값으로 쓴다");
238
+
239
+ /** HTTP 노드가 부를 셀러 API — 헤더 값은 응답에 싣지 않는다 */
240
+ export const flowSecretSchema = z.object({
241
+ name: z.string(),
242
+ url: z.string().describe("요청 주소(https)"),
243
+ headerNames: z.array(z.string()).describe("저장한 헤더 이름(값은 보이지 않는다)"),
244
+ updatedAt: z.string(),
245
+ });
246
+ export type FlowSecret = z.infer<typeof flowSecretSchema>;
247
+
248
+ export const saveFlowSecretRequestSchema = z.object({
249
+ url: z
250
+ .url()
251
+ .refine((value) => value.startsWith("https://"), { message: "https 주소만 받습니다" })
252
+ .describe("요청 주소. https만, 사설·루프백 주소는 400 `FLOW_SECRET_URL_INVALID`"),
253
+ headers: z
254
+ .record(z.string().regex(/^[A-Za-z0-9-]{1,64}$/), z.string().max(2000))
255
+ .refine((value) => Object.keys(value).length <= 10, { message: "헤더는 10개까지입니다" })
256
+ .default({})
257
+ .describe("함께 보낼 헤더(인증 등). 값은 암호화해 두고 다시 보여 주지 않는다"),
258
+ });
259
+ export type SaveFlowSecretRequest = z.input<typeof saveFlowSecretRequestSchema>;