@sayren/store-sdk 0.51.0 → 0.53.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sayren/store-sdk",
3
- "version": "0.51.0",
3
+ "version": "0.53.0",
4
4
  "main": "./dist/index.mjs",
5
5
  "types": "./dist/index.d.mts",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  import { KRW_MONEY_RULES } from "./currency";
2
2
  import {
3
- couponDiscountOf,
3
+ couponRuleDiscountOf,
4
4
  couponStackingOf,
5
5
  couponsCombinable,
6
6
  type PricingCoupon,
@@ -139,14 +139,19 @@ export function bestCouponSelection(input: CouponSelectionInput): CouponSelectio
139
139
  const orders = solo.filter((e) => e.candidate.coupon.rule.kind === "ORDER");
140
140
  const deliveries = solo.filter((e) => e.candidate.coupon.rule.kind === "DELIVERY");
141
141
  const lineIds = input.lines.map((line) => line.lineId);
142
- const lineAmount = new Map(input.lines.map((line) => [line.lineId, line.amount]));
142
+ const lineById = new Map(input.lines.map((line) => [line.lineId, line]));
143
143
 
144
144
  /** 상품 쿠폰 한 장의 줄별 할인 — 대상 줄만, 줄 금액 기준 */
145
145
  const weightsOf = (entry: (typeof products)[number]) => {
146
146
  const eligible = new Set(entry.result?.eligibleLineIds ?? []);
147
147
  return lineIds.map((id) =>
148
148
  eligible.has(id)
149
- ? couponDiscountOf(entry.candidate.coupon.rule.benefit, lineAmount.get(id) ?? 0, rules)
149
+ ? couponRuleDiscountOf(
150
+ entry.candidate.coupon.rule,
151
+ lineById.get(id)?.amount ?? 0,
152
+ lineById.get(id) ?? null,
153
+ rules,
154
+ )
150
155
  : 0,
151
156
  );
152
157
  };
@@ -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";
@@ -103,6 +105,9 @@ const descriptions = {
103
105
  "발급 대상 트리(이슈 #222) — 셀러 지급·받기·회원가입 지급에서 누가 받을 수 있는가. 구매자 조건만. 코드 입력형에는 두지 않는다",
104
106
  stacking:
105
107
  "중복 사용 정책(이슈 #222). 생략하면 종류별 한 장·서로 결합·적립금과 함께. 결합은 양쪽 쿠폰이 모두 허용해야 성립한다",
108
+ reward:
109
+ "확장 혜택(구간 할인 `TIERED`·N+1 `BUY_X_GET_Y`·적립금 지급 `POINT`). 주면 `benefit`을 생략하고, 응답의 `benefit`은 가장 가까운 값이다. " +
110
+ "N+1은 상품 쿠폰만, 적립금 지급은 회원 전용 주문 쿠폰만이다. 수정에서 null이면 확장 혜택을 지운다(`benefit`이 필요하다)",
106
111
  issueStartsAt: "받기 시작 시각(받기 쿠폰만, 이슈 #222). 생략하면 사용 시작 시각을 따른다",
107
112
  issueEndsAt:
108
113
  "받기 종료 시각(받기 쿠폰만, 이 시각부터 못 받는다). 생략하면 사용 종료 시각을 따른다",
@@ -116,6 +121,7 @@ const ruleFields = {
116
121
  targetRules: couponTargetRulesSchema.optional().describe(descriptions.targetRules),
117
122
  audience: couponConditionTreeSchema.optional().describe(descriptions.audience),
118
123
  stacking: couponStackingSchema.optional().describe(descriptions.stacking),
124
+ reward: couponRewardSchema.nullable().optional().describe(descriptions.reward),
119
125
  };
120
126
 
121
127
  /**
@@ -141,6 +147,8 @@ export function couponRequestRuleIssues(body: {
141
147
  targetRules?: CouponRuleTrees["targetRules"];
142
148
  audience?: CouponRuleTrees["eligibility"];
143
149
  stacking?: { perOrderLimit: number };
150
+ reward?: { type: "TIERED" | "BUY_X_GET_Y" | "POINT" } | null;
151
+ benefit?: unknown;
144
152
  perMemberUseLimit?: number | null;
145
153
  dailyIssueLimit?: number | null;
146
154
  issueStartsAt?: string | null;
@@ -179,6 +187,27 @@ export function couponRequestRuleIssues(body: {
179
187
  ...(body.audience ? { audience: body.audience } : {}),
180
188
  }),
181
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
+ });
182
211
  if (body.stacking && body.stacking.perOrderLimit > 1 && body.kind !== "PRODUCT")
183
212
  issues.push({
184
213
  path: ["stacking", "perOrderLimit"],
@@ -250,7 +279,7 @@ export const createCouponRequestSchema = strictRequest(
250
279
  name: z.string().trim().min(1).max(60).describe(descriptions.name),
251
280
  description: z.string().max(300).nullable().optional().describe(descriptions.description),
252
281
  kind: couponKindSchema,
253
- benefit: couponBenefitSchema,
282
+ benefit: couponBenefitSchema.optional().describe("혜택. `reward`를 주면 생략한다"),
254
283
  conditions: couponConditionsInputSchema
255
284
  .optional()
256
285
  .describe("사용 조건. 생략한 항목은 기본값(최소 금액 0, 회원 전용, 즉시할인과 중복 가능)"),
@@ -288,6 +317,8 @@ export const createCouponRequestSchema = strictRequest(
288
317
  ctx.addIssue({ code: "custom", path: [path], message });
289
318
  if (body.issueMethod === "CODE" && !body.code)
290
319
  issue("code", "코드 입력형 쿠폰은 코드가 필요합니다");
320
+ if (body.reward && body.benefit)
321
+ issue("benefit", "확장 혜택(reward)을 주면 benefit을 생략합니다");
291
322
  if (body.issueMethod === "VOUCHER" && !body.voucher)
292
323
  issue("voucher", "쿠폰 번호 쿠폰은 번호 설정이 필요합니다");
293
324
  if (body.issueMethod !== "VOUCHER" && body.voucher)
@@ -423,6 +454,11 @@ export const couponSchema = z.object({
423
454
  .optional()
424
455
  .default(COUPON_STACKING_DEFAULTS)
425
456
  .describe("중복 사용 정책(이슈 #222). 서버는 항상 싣는다"),
457
+ reward: couponRewardSchema
458
+ .nullable()
459
+ .optional()
460
+ .default(null)
461
+ .describe("확장 혜택. 없으면 null이고 `benefit`이 혜택이다. 서버는 항상 싣는다"),
426
462
  rulesOnly: z
427
463
  .boolean()
428
464
  .optional()
@@ -10,8 +10,8 @@ const limitOrContract = (description: string) =>
10
10
  z.number().int().nullable().describe(`${description}. null이면 계약(엔터프라이즈)·한도 없음`);
11
11
 
12
12
  /**
13
- * 요금제 기능 키 값 — 요금제 비교표·상점 플랫폼 요금제 화면이 쓴다. 키는 서버 판정의 기능 키와 같다(추가만 한다). 상위 요금제는 하위 요금제
14
- * 기능을 모두 포함한다
13
+ * 요금제 기능 키 값 — 요금제 비교표·상점 플랫폼 요금제 화면이 쓴다. 키는 서버 판정의 기능 키와 같다(추가만 한다 — 예외는 0.53에서 지운
14
+ * `limits.ai_credits_monthly`). 상위 요금제는 하위 요금제 기능을 모두 포함한다
15
15
  */
16
16
  export const planEntitlementsSchema = z
17
17
  .object({
@@ -24,6 +24,10 @@ export const planEntitlementsSchema = z
24
24
  "관리 API(개인 액세스 토큰·외부 앱 토큰 호출). 상점 플랫폼·CLI·MCP는 모든 요금제에서 쓴다",
25
25
  ),
26
26
  "features.analytics": z.boolean().describe("애널리틱스 화면·조회 API"),
27
+ "features.ai_assistant": z
28
+ .boolean()
29
+ .optional()
30
+ .describe("AI 상점 도우미. false면 새 실행을 시작할 수 없다(403 `PLAN_FEATURE_REQUIRED`)"),
27
31
  "limits.ai_reads_monthly": limitOrContract("MCP 읽기 월 한도"),
28
32
  "limits.ai_actions_monthly": limitOrContract(
29
33
  "MCP 쓰기(AI 작업) 월 한도 — 주문 처리는 막지 않는다",
@@ -34,9 +38,6 @@ export const planEntitlementsSchema = z
34
38
  "limits.private_apps": limitOrContract(
35
39
  "비공개 앱(이 상점 전용 앱) 설치 수(0이면 설치할 수 없다)",
36
40
  ),
37
- "limits.ai_credits_monthly": limitOrContract(
38
- "AI 상점 도우미 월 크레딧(1 크레딧 = 모델 원가 $0.01, 이월 없음)",
39
- ),
40
41
  "limits.storefront_preview_hours_monthly": limitOrContract("편집 환경 시간(월, 시간)"),
41
42
  "limits.storefront_build_minutes_monthly": limitOrContract("빌드 시간(월, 분)"),
42
43
  "limits.storefront_requests_monthly": limitOrContract(
@@ -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이면 소멸 없음"),