@sayren/store-sdk 0.50.0 → 0.52.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.
@@ -12,10 +12,12 @@ import {
12
12
  import { currencyCodeSchema } from "./currency";
13
13
  import {
14
14
  COUPON_CONDITIONS_DEFAULTS,
15
+ COUPON_REWARD_KINDS,
15
16
  COUPON_TARGET_LIST_MAX,
16
17
  couponBenefitSchema,
17
18
  couponConditionsSchema,
18
19
  couponKindSchema,
20
+ couponRewardSchema,
19
21
  couponTargetSchema,
20
22
  } from "./promotion";
21
23
  import { strictRequest } from "./strict";
@@ -35,17 +37,34 @@ export const couponStatusSchema = z
35
37
  export type CouponStatus = z.infer<typeof couponStatusSchema>;
36
38
 
37
39
  /**
38
- * 발급 방식 — 코드 입력·셀러 지급·다운로드(구매자가 받기)·회원가입 자동 지급(이슈 #110).
40
+ * 발급 방식 — 코드 입력·셀러 지급·다운로드(구매자가 받기)·회원가입 자동 지급(이슈 #110)·흐름 지급(이슈 #264).
39
41
  * 응답(`couponSchema.issueMethod`)은 값이 늘 수 있어 문자열로 받는다. 이 enum은 요청 검증용이다.
40
42
  */
41
43
  export const couponIssueMethodSchema = z
42
- .enum(["CODE", "MANUAL", "DOWNLOAD", "SIGNUP"])
44
+ .enum(["CODE", "MANUAL", "DOWNLOAD", "SIGNUP", "FLOW", "VOUCHER"])
43
45
  .describe(
44
46
  "발급 방식. `CODE`=구매자가 주문서에 코드를 입력한다, `MANUAL`=셀러가 회원을 골라 지급한다, " +
45
- "`DOWNLOAD`=구매자가 스토어프론트에서 받는다, `SIGNUP`=회원가입 때 자동으로 지급한다",
47
+ "`DOWNLOAD`=구매자가 스토어프론트에서 받는다, `SIGNUP`=회원가입 때 자동으로 지급한다, " +
48
+ "`FLOW`=주문 흐름의 쿠폰 지급 단계가 그 주문의 구매자에게 지급한다, " +
49
+ "`VOUCHER`=셀러가 1회용 쿠폰 번호를 만들어 나눠 주고 구매자가 번호를 등록하면 지급한다",
46
50
  );
47
51
  export type CouponIssueMethod = z.infer<typeof couponIssueMethodSchema>;
48
52
 
53
+ /** 쿠폰 번호 접두어 — 영문·숫자 2~10자, 대문자로 저장한다. 상점 안에서 유일하다 */
54
+ export const VOUCHER_PREFIX_PATTERN = /^[A-Za-z0-9]{2,10}$/;
55
+ /** 쿠폰 번호 한 쿠폰의 최대 개수 */
56
+ export const VOUCHER_COUNT_MAX = 100_000;
57
+
58
+ /** 쿠폰 번호 설정(`VOUCHER`, 이슈 #264) — 만든 뒤에는 바꿀 수 없다 */
59
+ export const couponVoucherInputSchema = z
60
+ .strictObject({
61
+ prefix: z.string().regex(VOUCHER_PREFIX_PATTERN).describe("번호 앞부분(영문·숫자 2~10자)"),
62
+ count: z.number().int().min(1).max(VOUCHER_COUNT_MAX).describe("만들 번호 수(1~100,000)"),
63
+ })
64
+ .describe(
65
+ "쿠폰 번호 설정 — 1회용 번호 `count`개를 만든다. 번호는 `GET /v1/coupons/{couponId}/voucher-codes`로 받는다",
66
+ );
67
+
49
68
  /** 코드 형식 — 영문·숫자·`-`·`_` 6~30자. 대소문자를 구분하지 않고 서버가 대문자로 저장한다 */
50
69
  export const COUPON_CODE_PATTERN = /^[A-Za-z0-9_-]{6,30}$/;
51
70
 
@@ -86,6 +105,9 @@ const descriptions = {
86
105
  "발급 대상 트리(이슈 #222) — 셀러 지급·받기·회원가입 지급에서 누가 받을 수 있는가. 구매자 조건만. 코드 입력형에는 두지 않는다",
87
106
  stacking:
88
107
  "중복 사용 정책(이슈 #222). 생략하면 종류별 한 장·서로 결합·적립금과 함께. 결합은 양쪽 쿠폰이 모두 허용해야 성립한다",
108
+ reward:
109
+ "확장 혜택(구간 할인 `TIERED`·N+1 `BUY_X_GET_Y`·적립금 지급 `POINT`). 주면 `benefit`을 생략하고, 응답의 `benefit`은 가장 가까운 값이다. " +
110
+ "N+1은 상품 쿠폰만, 적립금 지급은 회원 전용 주문 쿠폰만이다. 수정에서 null이면 확장 혜택을 지운다(`benefit`이 필요하다)",
89
111
  issueStartsAt: "받기 시작 시각(받기 쿠폰만, 이슈 #222). 생략하면 사용 시작 시각을 따른다",
90
112
  issueEndsAt:
91
113
  "받기 종료 시각(받기 쿠폰만, 이 시각부터 못 받는다). 생략하면 사용 종료 시각을 따른다",
@@ -99,6 +121,7 @@ const ruleFields = {
99
121
  targetRules: couponTargetRulesSchema.optional().describe(descriptions.targetRules),
100
122
  audience: couponConditionTreeSchema.optional().describe(descriptions.audience),
101
123
  stacking: couponStackingSchema.optional().describe(descriptions.stacking),
124
+ reward: couponRewardSchema.nullable().optional().describe(descriptions.reward),
102
125
  };
103
126
 
104
127
  /**
@@ -124,6 +147,8 @@ export function couponRequestRuleIssues(body: {
124
147
  targetRules?: CouponRuleTrees["targetRules"];
125
148
  audience?: CouponRuleTrees["eligibility"];
126
149
  stacking?: { perOrderLimit: number };
150
+ reward?: { type: "TIERED" | "BUY_X_GET_Y" | "POINT" } | null;
151
+ benefit?: unknown;
127
152
  perMemberUseLimit?: number | null;
128
153
  dailyIssueLimit?: number | null;
129
154
  issueStartsAt?: string | null;
@@ -162,6 +187,27 @@ export function couponRequestRuleIssues(body: {
162
187
  ...(body.audience ? { audience: body.audience } : {}),
163
188
  }),
164
189
  );
190
+ if (!body.reward && body.benefit === undefined)
191
+ issues.push({ path: ["benefit"], message: "혜택(benefit 또는 reward)이 필요합니다" });
192
+ if (
193
+ body.reward &&
194
+ body.kind !== undefined &&
195
+ !(COUPON_REWARD_KINDS[body.reward.type] as readonly string[]).includes(body.kind)
196
+ )
197
+ issues.push({
198
+ path: ["reward", "type"],
199
+ message:
200
+ body.reward.type === "POINT"
201
+ ? "적립금 지급은 주문 쿠폰만 쓸 수 있습니다"
202
+ : body.reward.type === "BUY_X_GET_Y"
203
+ ? "N+1은 상품 쿠폰만 쓸 수 있습니다"
204
+ : "구간 할인은 배송비 쿠폰에 쓸 수 없습니다",
205
+ });
206
+ if (body.reward?.type === "POINT" && !conditions.memberOnly)
207
+ issues.push({
208
+ path: ["reward", "type"],
209
+ message: "적립금 지급은 회원 전용 쿠폰만 쓸 수 있습니다",
210
+ });
165
211
  if (body.stacking && body.stacking.perOrderLimit > 1 && body.kind !== "PRODUCT")
166
212
  issues.push({
167
213
  path: ["stacking", "perOrderLimit"],
@@ -233,13 +279,14 @@ export const createCouponRequestSchema = strictRequest(
233
279
  name: z.string().trim().min(1).max(60).describe(descriptions.name),
234
280
  description: z.string().max(300).nullable().optional().describe(descriptions.description),
235
281
  kind: couponKindSchema,
236
- benefit: couponBenefitSchema,
282
+ benefit: couponBenefitSchema.optional().describe("혜택. `reward`를 주면 생략한다"),
237
283
  conditions: couponConditionsInputSchema
238
284
  .optional()
239
285
  .describe("사용 조건. 생략한 항목은 기본값(최소 금액 0, 회원 전용, 즉시할인과 중복 가능)"),
240
286
  target: couponTargetInputSchema.optional().describe("대상 상품·카테고리. 생략하면 전 상품"),
241
287
  issueMethod: couponIssueMethodSchema,
242
288
  code: z.string().regex(COUPON_CODE_PATTERN).optional().describe(descriptions.code),
289
+ voucher: couponVoucherInputSchema.optional(),
243
290
  startsAt: couponTimeField(descriptions.startsAt),
244
291
  endsAt: couponTimeField(descriptions.endsAt),
245
292
  validDaysAfterIssue: z
@@ -270,6 +317,12 @@ export const createCouponRequestSchema = strictRequest(
270
317
  ctx.addIssue({ code: "custom", path: [path], message });
271
318
  if (body.issueMethod === "CODE" && !body.code)
272
319
  issue("code", "코드 입력형 쿠폰은 코드가 필요합니다");
320
+ if (body.reward && body.benefit)
321
+ issue("benefit", "확장 혜택(reward)을 주면 benefit을 생략합니다");
322
+ if (body.issueMethod === "VOUCHER" && !body.voucher)
323
+ issue("voucher", "쿠폰 번호 쿠폰은 번호 설정이 필요합니다");
324
+ if (body.issueMethod !== "VOUCHER" && body.voucher)
325
+ issue("voucher", "번호 설정은 쿠폰 번호 쿠폰에만 둡니다");
273
326
  if (body.issueMethod !== "CODE" && body.code)
274
327
  issue("code", "코드는 코드 입력형 쿠폰에만 둡니다");
275
328
  if (body.issueMethod === "CODE" && body.validDaysAfterIssue != null) {
@@ -348,7 +401,7 @@ export const couponSchema = z.object({
348
401
  issueMethod: z
349
402
  .string()
350
403
  .describe(
351
- "발급 방식. 현재 값은 `CODE`·`MANUAL`·`DOWNLOAD`(구매자가 받기)·`SIGNUP`(회원가입 자동 지급)이고 값이 추가될 수 있어 문자열로 받는다",
404
+ "발급 방식. 현재 값은 `CODE`·`MANUAL`·`DOWNLOAD`(구매자가 받기)·`SIGNUP`(회원가입 자동 지급)·`FLOW`(주문 흐름 지급)·`VOUCHER`(쿠폰 번호)이고 값이 추가될 수 있어 문자열로 받는다",
352
405
  ),
353
406
  code: z.string().nullable().describe("쿠폰 코드(대문자). 코드 입력형이 아니면 null"),
354
407
  startsAt: z.string().nullable(),
@@ -365,6 +418,12 @@ export const couponSchema = z.object({
365
418
  totalUseLimit: z.number().int().nullable(),
366
419
  perMemberIssueLimit: z.number().int().nullable(),
367
420
  perMemberUseLimit: z.number().int().nullable(),
421
+ voucher: z
422
+ .object({ prefix: z.string(), count: z.number().int() })
423
+ .nullable()
424
+ .optional()
425
+ .default(null)
426
+ .describe("쿠폰 번호 설정(`VOUCHER`만, 이슈 #264). 다른 발급 방식은 null. 서버는 항상 싣는다"),
368
427
  issuedCount: z
369
428
  .number()
370
429
  .int()
@@ -395,6 +454,11 @@ export const couponSchema = z.object({
395
454
  .optional()
396
455
  .default(COUPON_STACKING_DEFAULTS)
397
456
  .describe("중복 사용 정책(이슈 #222). 서버는 항상 싣는다"),
457
+ reward: couponRewardSchema
458
+ .nullable()
459
+ .optional()
460
+ .default(null)
461
+ .describe("확장 혜택. 없으면 null이고 `benefit`이 혜택이다. 서버는 항상 싣는다"),
398
462
  rulesOnly: z
399
463
  .boolean()
400
464
  .optional()
@@ -444,7 +508,7 @@ export const couponIssueSchema = z.object({
444
508
  .string()
445
509
  .optional()
446
510
  .describe(
447
- "발급 경로(이슈 #110). 현재 값은 `MANUAL`(셀러 지급)·`DOWNLOAD`(구매자가 받음)·`SIGNUP`(회원가입 자동 지급)이고 값이 추가될 수 있다. 서버는 항상 싣는다",
511
+ "발급 경로(이슈 #110). 현재 값은 `MANUAL`(셀러 지급)·`DOWNLOAD`(구매자가 받음)·`SIGNUP`(회원가입 자동 지급)·`FLOW`(주문 흐름 지급)·`VOUCHER`(쿠폰 번호 등록)이고 값이 추가될 수 있다. 서버는 항상 싣는다",
448
512
  ),
449
513
  status: couponIssueStatusSchema,
450
514
  validFrom: z.string(),
@@ -460,6 +524,48 @@ export const couponIssueListQuerySchema = z.object({
460
524
  ...pageQueryShape(),
461
525
  });
462
526
 
527
+ /** `GET /v1/coupons/{couponId}/voucher-codes` — 쿠폰 번호 전체(순번 순). 상점 플랫폼이 CSV로 내려받는다 */
528
+ export const couponVoucherCodesSchema = z
529
+ .object({
530
+ prefix: z.string(),
531
+ count: z.number().int(),
532
+ codes: z.array(z.string()).describe("번호 — 순번 1부터. 등록에 쓴 번호도 싣는다"),
533
+ })
534
+ .describe("쿠폰 번호 목록");
535
+ export type CouponVoucherCodes = z.infer<typeof couponVoucherCodesSchema>;
536
+
537
+ /**
538
+ * 발급 대상 전원 지급(이슈 #264) — `POST /v1/coupons/{couponId}/distributions`(202)가 만들고 서버가 뒤에서 처리한다. 셀러 지급(`MANUAL`) 쿠폰을
539
+ * 발급 대상 조건에 맞는 회원 모두에게 준다(셀러 지급과 같은 한도 판정). 쿠폰마다 진행 중 작업은 하나다
540
+ */
541
+ export const couponDistributionSchema = z
542
+ .object({
543
+ distributionId: z.string(),
544
+ couponId: z.string(),
545
+ status: z
546
+ .string()
547
+ .describe(
548
+ "작업 상태. 현재 값은 `QUEUED`(대기)·`RUNNING`(지급 중)·`COMPLETED`(끝남)·`FAILED`(실패)이고 값이 추가될 수 있다",
549
+ ),
550
+ processedCount: z.number().int().describe("지금까지 살펴본 회원 수"),
551
+ issuedCount: z.number().int().describe("이 작업이 지급한 장수"),
552
+ skippedCount: z
553
+ .number()
554
+ .int()
555
+ .describe("건너뛴 회원 수 — 발급 대상이 아니거나 1인 발급 한도에 닿은 회원"),
556
+ stopCode: z
557
+ .string()
558
+ .nullable()
559
+ .describe(
560
+ "일찍 끝난 사유. `COUPON_EXHAUSTED`(총 발급 한도)·`COUPON_DAILY_EXHAUSTED`(하루 발급 한도)·`COUPON_NOT_ACTIVE`(중지·종료)·`COUPON_EXPIRED`(사용 기간 끝) 등. 끝까지 돌았으면 null",
561
+ ),
562
+ createdAt: z.string(),
563
+ startedAt: z.string().nullable(),
564
+ finishedAt: z.string().nullable(),
565
+ })
566
+ .describe("발급 대상 전원 지급 작업");
567
+ export type CouponDistribution = z.infer<typeof couponDistributionSchema>;
568
+
463
569
  /** `POST /v1/coupons/{couponId}/issues` — 셀러 지급. 한 번에 1,000명까지 */
464
570
  export const issueCouponRequestSchema = z.strictObject({
465
571
  customerIds: z
@@ -1,5 +1,6 @@
1
1
  import * as z from "zod";
2
2
  import { strictRequest } from "./strict";
3
+ import { customerUserinfoInputSchema } from "./userinfo";
3
4
 
4
5
  /**
5
6
  * 구매자 위임 REST(`POST /v1/customers/delegations`, #71) — 계정 토큰(또는 관리 API 토큰) + `X-Store-Id`로 부른다(`customer:rw`).
@@ -31,6 +32,11 @@ export const customerDelegationRequestSchema = strictRequest(
31
32
  .describe(
32
33
  "사용자가 셀러 서비스에서 실제로 로그인한 시각(uid 위임만). 주면 구매자 토큰의 인증 시각이 된다",
33
34
  ),
35
+ userinfo: customerUserinfoInputSchema
36
+ .optional()
37
+ .describe(
38
+ "OIDC 클레임(이슈 #264) — 위임할 때마다 준 클레임만 바뀌고 null은 지운다. `email_verified`를 생략하면 위임으로 만든 구매자의 이메일은 확인된 것으로 본다",
39
+ ),
34
40
  })
35
41
  .refine((value) => Boolean(value.uid || value.email), {
36
42
  message: "uid 또는 email이 필요합니다",
@@ -1,6 +1,7 @@
1
1
  import * as z from "zod";
2
2
  import { orderItemStatusSchema, pageQueryShape } from "./common";
3
3
  import { strictRequest } from "./strict";
4
+ import { customerUserinfoSchema } from "./userinfo";
4
5
 
5
6
  /** 구매자 목록 정렬 */
6
7
  export const customerSortSchema = z
@@ -169,6 +170,9 @@ export const customerDetailSchema = z.object({
169
170
  "약관·개인정보·마케팅 동의의 현재 상태. 이력은 `GET /v1/customers/{customerId}/consents`",
170
171
  ),
171
172
  addresses: z.array(customerAddressSchema),
173
+ userinfo: customerUserinfoSchema
174
+ .optional()
175
+ .describe("OIDC userinfo 표준 클레임(이슈 #264, 주소는 기본 배송지). 서버는 항상 싣는다"),
172
176
  stats: customerStatsSchema.extend({
173
177
  canceledOrReturnedCount: z.number().int().describe("취소된 주문 상품 + 완료된 반품 수"),
174
178
  reviewCount: z.number().int(),
@@ -180,6 +180,7 @@ export const POINT_LOT_SOURCES = [
180
180
  "ADJUST",
181
181
  "REFUND_RESTORE",
182
182
  "OPENING",
183
+ "COUPON",
183
184
  ] as const;
184
185
  export type PointLotSource = (typeof POINT_LOT_SOURCES)[number];
185
186
 
@@ -194,6 +195,7 @@ export const POINT_ENTRY_TYPES = [
194
195
  "EXPIRE",
195
196
  "ADJUST",
196
197
  "OPENING",
198
+ "EARN_COUPON",
197
199
  ] as const;
198
200
  export type PointEntryType = (typeof POINT_ENTRY_TYPES)[number];
199
201
 
@@ -257,7 +259,7 @@ export const responsePointEntryTypeSchema = z
257
259
  .describe(
258
260
  "유형. `EARN_REVIEW` 리뷰 적립 · `REVOKE_REVIEW` 리뷰 삭제 회수 · `EARN_PURCHASE` 구매 적립 · `EXPIRE` 소멸 · `ADJUST` 셀러 지급·차감 · " +
259
261
  "`OPENING` 기존 잔액 이전 · `USE_RESERVE` 결제 사용(결제 시작 때 빠짐) · `USE_RELEASE` 결제가 실패·만료되어 되돌림 · " +
260
- "`REFUND_RESTORE` 취소·반품 환불로 반환. 값이 추가될 수 있어 문자열로 받는다",
262
+ "`REFUND_RESTORE` 취소·반품 환불로 반환 · `EARN_COUPON` 적립금 지급 쿠폰. 값이 추가될 수 있어 문자열로 받는다",
261
263
  );
262
264
 
263
265
  /** 원장 항목 — 셀러 조회용(사유·행위자 포함) */
@@ -283,7 +285,7 @@ export const pointLotSchema = z.object({
283
285
  lotId: z.string(),
284
286
  source: z
285
287
  .union([z.enum(POINT_LOT_SOURCES), z.string()])
286
- .describe("출처. `REVIEW`·`PURCHASE`·`ADJUST`·`REFUND_RESTORE`·`OPENING` 등"),
288
+ .describe("출처. `REVIEW`·`PURCHASE`·`ADJUST`·`REFUND_RESTORE`·`OPENING`·`COUPON` 등"),
287
289
  amount: z.number().int().describe("적립한 금액"),
288
290
  remaining: z.number().int().describe("남은 금액"),
289
291
  expiresAt: z.string().nullable().describe("소멸 시각. null이면 소멸 없음"),