@sayren/store-sdk 0.29.6 → 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/dist/index.d.mts +1786 -227
- package/dist/index.mjs +581 -37
- package/package.json +1 -1
- package/src/client.ts +166 -0
- package/src/index.ts +2 -0
- package/src/schemas/commerce-flow.ts +259 -0
- package/src/schemas/coupon.ts +29 -10
- package/src/schemas/fulfillment.ts +177 -9
- package/src/schemas/misc.ts +2 -0
- package/src/schemas/order.ts +4 -0
- package/src/schemas/product.ts +71 -0
- package/src/schemas/saved-option-set.ts +8 -0
- package/src/schemas/scope.ts +5 -0
- package/src/schemas/shipping-policy.ts +4 -1
- package/src/schemas/stay.ts +90 -0
- package/src/schemas/upload.ts +6 -1
- package/src/schemas/webhook-event.ts +57 -0
package/package.json
CHANGED
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>;
|
package/src/schemas/coupon.ts
CHANGED
|
@@ -23,11 +23,15 @@ export const couponStatusSchema = z
|
|
|
23
23
|
.describe("쿠폰 상태. `ACTIVE`=사용 중, `PAUSED`=중지(재개 가능), `ENDED`=종료(되돌릴 수 없음)");
|
|
24
24
|
export type CouponStatus = z.infer<typeof couponStatusSchema>;
|
|
25
25
|
|
|
26
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* 발급 방식 — 코드 입력·셀러 지급·다운로드(구매자가 받기)·회원가입 자동 지급(이슈 #110).
|
|
28
|
+
* 응답(`couponSchema.issueMethod`)은 값이 늘 수 있어 문자열로 받는다. 이 enum은 요청 검증용이다.
|
|
29
|
+
*/
|
|
27
30
|
export const couponIssueMethodSchema = z
|
|
28
|
-
.enum(["CODE", "MANUAL"])
|
|
31
|
+
.enum(["CODE", "MANUAL", "DOWNLOAD", "SIGNUP"])
|
|
29
32
|
.describe(
|
|
30
|
-
"발급 방식. `CODE`=구매자가 주문서에 코드를 입력한다, `MANUAL`=셀러가 회원을 골라
|
|
33
|
+
"발급 방식. `CODE`=구매자가 주문서에 코드를 입력한다, `MANUAL`=셀러가 회원을 골라 지급한다, " +
|
|
34
|
+
"`DOWNLOAD`=구매자가 스토어프론트에서 받는다, `SIGNUP`=회원가입 때 자동으로 지급한다",
|
|
31
35
|
);
|
|
32
36
|
export type CouponIssueMethod = z.infer<typeof couponIssueMethodSchema>;
|
|
33
37
|
|
|
@@ -51,10 +55,12 @@ const descriptions = {
|
|
|
51
55
|
"사용 종료 시각(ISO 8601, 시간대 포함). 이 시각부터 못 쓴다. 생략하면 제한이 없다. " +
|
|
52
56
|
"이미 지급한 쿠폰의 유효 기간은 지급 때 정해지므로 뒤에 종료일을 늦춰도 늘어나지 않는다",
|
|
53
57
|
validDaysAfterIssue:
|
|
54
|
-
"발급 후 유효 일수 —
|
|
55
|
-
totalIssueLimit:
|
|
58
|
+
"발급 후 유효 일수 — 발급형(`MANUAL`·`DOWNLOAD`·`SIGNUP`) 쿠폰만. 쿠폰 종료일과 둘 중 먼저 끝나는 쪽이 적용된다",
|
|
59
|
+
totalIssueLimit:
|
|
60
|
+
"총 발급 수 한도. 생략하면 무제한. 다운로드·회원가입 쿠폰은 한도에 닿으면 더 발급하지 않는다",
|
|
56
61
|
totalUseLimit: "총 사용 수 한도. 결제 중인 예약도 센다. 생략하면 무제한",
|
|
57
|
-
perMemberIssueLimit:
|
|
62
|
+
perMemberIssueLimit:
|
|
63
|
+
"1인 발급 수 한도(셀러 지급·다운로드). 생략하면 1인 1장. 회원가입 쿠폰은 1장 고정이라 1만 받는다",
|
|
58
64
|
perMemberUseLimit:
|
|
59
65
|
"1인 사용 수 한도(코드 입력형). 생략하면 무제한. 비회원에게 여는 쿠폰은 둘 수 없다",
|
|
60
66
|
} as const;
|
|
@@ -117,12 +123,19 @@ export const createCouponRequestSchema = z
|
|
|
117
123
|
issue("validDaysAfterIssue", "발급 후 유효 일수는 셀러 지급 쿠폰에만 둡니다");
|
|
118
124
|
}
|
|
119
125
|
if (body.issueMethod === "CODE" && body.perMemberIssueLimit != null) {
|
|
120
|
-
issue("perMemberIssueLimit", "1인 발급 수 한도는
|
|
126
|
+
issue("perMemberIssueLimit", "1인 발급 수 한도는 발급형 쿠폰에만 둡니다");
|
|
127
|
+
}
|
|
128
|
+
if (
|
|
129
|
+
body.issueMethod === "SIGNUP" &&
|
|
130
|
+
body.perMemberIssueLimit != null &&
|
|
131
|
+
body.perMemberIssueLimit !== 1
|
|
132
|
+
) {
|
|
133
|
+
issue("perMemberIssueLimit", "회원가입 쿠폰은 1인 1장입니다");
|
|
121
134
|
}
|
|
122
|
-
if (body.issueMethod
|
|
135
|
+
if (body.issueMethod !== "CODE" && body.perMemberUseLimit != null) {
|
|
123
136
|
issue(
|
|
124
137
|
"perMemberUseLimit",
|
|
125
|
-
"
|
|
138
|
+
"발급형 쿠폰은 발급 한 장이 한 번 쓰입니다. 1인 발급 수 한도를 사용하십시오",
|
|
126
139
|
);
|
|
127
140
|
}
|
|
128
141
|
const memberOnly = body.conditions?.memberOnly ?? COUPON_CONDITIONS_DEFAULTS.memberOnly;
|
|
@@ -168,7 +181,7 @@ export const couponSchema = z.object({
|
|
|
168
181
|
issueMethod: z
|
|
169
182
|
.string()
|
|
170
183
|
.describe(
|
|
171
|
-
"발급 방식. 현재 값은 `CODE`·`MANUAL
|
|
184
|
+
"발급 방식. 현재 값은 `CODE`·`MANUAL`·`DOWNLOAD`(구매자가 받기)·`SIGNUP`(회원가입 자동 지급)이고 값이 추가될 수 있어 문자열로 받는다",
|
|
172
185
|
),
|
|
173
186
|
code: z.string().nullable().describe("쿠폰 코드(대문자). 코드 입력형이 아니면 null"),
|
|
174
187
|
startsAt: z.string().nullable(),
|
|
@@ -214,6 +227,12 @@ export const couponIssueSchema = z.object({
|
|
|
214
227
|
couponId: z.string(),
|
|
215
228
|
customerId: z.string(),
|
|
216
229
|
customerName: z.string().nullable().describe("구매자 이름. 탈퇴한 구매자는 null"),
|
|
230
|
+
source: z
|
|
231
|
+
.string()
|
|
232
|
+
.optional()
|
|
233
|
+
.describe(
|
|
234
|
+
"발급 경로(이슈 #110). 현재 값은 `MANUAL`(셀러 지급)·`DOWNLOAD`(구매자가 받음)·`SIGNUP`(회원가입 자동 지급)이고 값이 추가될 수 있다. 서버는 항상 싣는다",
|
|
235
|
+
),
|
|
217
236
|
status: couponIssueStatusSchema,
|
|
218
237
|
validFrom: z.string(),
|
|
219
238
|
validUntil: z.string().nullable(),
|