@sayren/store-sdk 0.32.0 → 0.34.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/dist/index.d.mts +2226 -167
- package/dist/index.mjs +680 -81
- package/package.json +1 -1
- package/src/client.ts +79 -5
- package/src/http.ts +1 -1
- package/src/index.ts +4 -0
- package/src/schemas/analytics.ts +16 -0
- package/src/schemas/coupon.ts +13 -0
- package/src/schemas/currency-prices.ts +138 -0
- package/src/schemas/currency.ts +184 -0
- package/src/schemas/customer.ts +20 -1
- package/src/schemas/fulfillment.ts +7 -0
- package/src/schemas/market.ts +76 -0
- package/src/schemas/order.ts +16 -1
- package/src/schemas/payment.ts +109 -16
- package/src/schemas/points.ts +2 -5
- package/src/schemas/product-info.ts +190 -0
- package/src/schemas/product.ts +73 -5
- package/src/schemas/promotion.ts +36 -17
- package/src/schemas/refund-policy.ts +55 -2
- package/src/schemas/shipping-policy.ts +56 -7
- package/src/schemas/storefront-hosting.ts +6 -10
- package/src/schemas/webhook-event.ts +24 -0
package/src/schemas/product.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { type PageParams, pageQueryShape } from "./common";
|
|
3
|
+
import { BASE_CURRENCY, CURRENCIES } from "./currency";
|
|
4
|
+
import {
|
|
5
|
+
additionalCurrencyPricesSchema,
|
|
6
|
+
productCurrencyPricesSchema,
|
|
7
|
+
storedAdditionalCurrencyPriceSchema,
|
|
8
|
+
storedProductCurrencyPriceSchema,
|
|
9
|
+
} from "./currency-prices";
|
|
3
10
|
import {
|
|
4
11
|
fulfillmentTypeResponseSchema,
|
|
5
12
|
productFulfillmentCreateSchema,
|
|
@@ -7,6 +14,13 @@ import {
|
|
|
7
14
|
productFulfillmentUpdateSchema,
|
|
8
15
|
} from "./fulfillment";
|
|
9
16
|
import { pointEarnRateBpSchema } from "./points";
|
|
17
|
+
import {
|
|
18
|
+
MAX_PRODUCT_INFO_SECTIONS,
|
|
19
|
+
productCertificationsRequestSchema,
|
|
20
|
+
productCertificationsSchema,
|
|
21
|
+
productNoticeRequestSchema,
|
|
22
|
+
productNoticeSchema,
|
|
23
|
+
} from "./product-info";
|
|
10
24
|
import { productRefundPolicyPatchSchema } from "./refund-policy";
|
|
11
25
|
import { inventoryModeSchema } from "./stay";
|
|
12
26
|
import { UPLOAD_PURPOSE_LIMITS, uploadExtensionOf } from "./upload";
|
|
@@ -95,6 +109,10 @@ export const productVariantSchema = z.object({
|
|
|
95
109
|
/** 값 이름을 " / "로 이은 파생 표시명. 옵션 없으면 "" */
|
|
96
110
|
name: z.string(),
|
|
97
111
|
additionalPrice: z.number().int(),
|
|
112
|
+
prices: z
|
|
113
|
+
.array(storedAdditionalCurrencyPriceSchema)
|
|
114
|
+
.optional()
|
|
115
|
+
.describe("외화 추가금(이슈 #130, 관리 API만). 기본 통화 값은 `additionalPrice`다"),
|
|
98
116
|
stockQuantity: z.number().int(),
|
|
99
117
|
sku: z.string().nullable(),
|
|
100
118
|
barcode: z.string().nullable(),
|
|
@@ -154,6 +172,10 @@ export const addonValueSchema = z.object({
|
|
|
154
172
|
name: z.string(),
|
|
155
173
|
sortOrder: z.number().int(),
|
|
156
174
|
additionalPrice: z.number().int().describe("이 값을 고르면 단가에 더하는 금액(원, 0 이상)"),
|
|
175
|
+
prices: z
|
|
176
|
+
.array(storedAdditionalCurrencyPriceSchema)
|
|
177
|
+
.optional()
|
|
178
|
+
.describe("외화 추가금(이슈 #130, 관리 API만). 기본 통화 값은 `additionalPrice`다"),
|
|
157
179
|
usable: z.boolean().describe("사용 여부. false면 구매자에게 보이지 않고 고를 수 없다"),
|
|
158
180
|
});
|
|
159
181
|
|
|
@@ -272,6 +294,7 @@ export const addonValueRequestSchema = z.object({
|
|
|
272
294
|
valueId: z.string().optional(),
|
|
273
295
|
name: optionNameSchema,
|
|
274
296
|
additionalPrice: z.number().int().min(0).default(0).describe("추가금(원, 0 이상)"),
|
|
297
|
+
prices: additionalCurrencyPricesSchema.optional(),
|
|
275
298
|
usable: z.boolean().default(true),
|
|
276
299
|
});
|
|
277
300
|
|
|
@@ -350,6 +373,10 @@ export const discountSchema = z.object({
|
|
|
350
373
|
export type Discount = z.infer<typeof discountSchema>;
|
|
351
374
|
|
|
352
375
|
export const productSchema = productSummarySchema.extend({
|
|
376
|
+
prices: z
|
|
377
|
+
.array(storedProductCurrencyPriceSchema)
|
|
378
|
+
.optional()
|
|
379
|
+
.describe("외화 판매가(이슈 #130). 기본 통화 값은 `salePrice`·`originalPrice`다"),
|
|
353
380
|
description: z.string().optional(),
|
|
354
381
|
/**
|
|
355
382
|
* 상세설명 에디터(@avarlabs/editor)의 직렬화 상태. 재편집 때 이 값으로 문서를 복원한다.
|
|
@@ -425,6 +452,19 @@ export const productSchema = productSummarySchema.extend({
|
|
|
425
452
|
.describe(
|
|
426
453
|
"설정된 즉시할인. GET /v1/products/{productId}/discount 응답과 같은 형태다. 할인이 없으면 null",
|
|
427
454
|
),
|
|
455
|
+
notice: productNoticeSchema
|
|
456
|
+
.nullable()
|
|
457
|
+
.optional()
|
|
458
|
+
.describe("상품정보제공 고시(이슈 #135). 없으면 null. 서버는 항상 싣는다"),
|
|
459
|
+
certifications: productCertificationsSchema
|
|
460
|
+
.nullable()
|
|
461
|
+
.optional()
|
|
462
|
+
.describe("인증 정보(이슈 #135). 없으면 null. 서버는 항상 싣는다"),
|
|
463
|
+
infoSectionIds: z
|
|
464
|
+
.array(z.string())
|
|
465
|
+
.optional()
|
|
466
|
+
.default([])
|
|
467
|
+
.describe("연결한 안내 섹션 id(이슈 #135). 배열 순서가 표시 순서다. 서버는 항상 싣는다"),
|
|
428
468
|
createdAt: z.string().optional(),
|
|
429
469
|
});
|
|
430
470
|
|
|
@@ -516,6 +556,7 @@ export const productVariantRequestSchema = z.object({
|
|
|
516
556
|
/** optionGroups 순서대로 각 축에서 고른 값 이름. 서버가 이름 → valueId를 해석한다 */
|
|
517
557
|
valueNames: z.array(z.string()).default([]),
|
|
518
558
|
additionalPrice: z.number().int().default(0),
|
|
559
|
+
prices: additionalCurrencyPricesSchema.optional(),
|
|
519
560
|
/**
|
|
520
561
|
* 생략 = 기존 재고 유지 (신규 variant는 0).
|
|
521
562
|
* 구조 교체(PUT)가 폼 로드 시점의 재고를 되쓰지 않게 하는 옵트인 필드다 —
|
|
@@ -548,14 +589,14 @@ export const optionModelRequestSchema = z.object({
|
|
|
548
589
|
.max(MAX_ADDON_GROUPS)
|
|
549
590
|
.optional()
|
|
550
591
|
.describe(
|
|
551
|
-
"추가 선택 옵션명 전체(이슈 #59). 주면 통째로 바꾸고, 생략하면 등록은 없음·수정은 현재 값을 그대로 둔다. 끄려면 빈 배열을
|
|
592
|
+
"추가 선택 옵션명 전체(이슈 #59). 주면 통째로 바꾸고, 생략하면 등록은 없음·수정은 현재 값을 그대로 둔다. 끄려면 빈 배열을 보낸다",
|
|
552
593
|
),
|
|
553
594
|
customInputs: z
|
|
554
595
|
.array(customInputRequestSchema)
|
|
555
596
|
.max(MAX_CUSTOM_INPUTS)
|
|
556
597
|
.optional()
|
|
557
598
|
.describe(
|
|
558
|
-
"직접 입력 항목 전체(이슈 #59). 배열 순서가 표시 순서다. 주면 통째로 바꾸고, 생략하면 등록은 없음·수정은 현재 값을 그대로 둔다. 항목명에 개인정보성 단어(`CUSTOM_INPUT_FORBIDDEN_WORDS`)가 있으면 400 `CUSTOM_INPUT_LABEL_FORBIDDEN
|
|
599
|
+
"직접 입력 항목 전체(이슈 #59). 배열 순서가 표시 순서다. 주면 통째로 바꾸고, 생략하면 등록은 없음·수정은 현재 값을 그대로 둔다. 항목명에 개인정보성 단어(`CUSTOM_INPUT_FORBIDDEN_WORDS`)가 있으면 400 `CUSTOM_INPUT_LABEL_FORBIDDEN`이다",
|
|
559
600
|
),
|
|
560
601
|
optionSort: optionSortSchema
|
|
561
602
|
.optional()
|
|
@@ -627,14 +668,20 @@ const productImagesRequestSchema = z.array(productImageUrlSchema).min(1).max(MAX
|
|
|
627
668
|
* 최상위가 `strictObject`다. 모르는 필드는 400이다 — 이슈 #57 이전의 평면 필드(`deliveryFee`·`fulfillmentType`·`returnFee` 등)를
|
|
628
669
|
* 보낸 요청이 조용히 무시되지 않게 한다. 옛 필드는 `fulfillment.{유형}.…`과 `refundPolicy`로 옮겼다.
|
|
629
670
|
*/
|
|
671
|
+
/**
|
|
672
|
+
* 기본 통화 판매가 단위 — 통화표가 원천이다(이슈 #130). zod `multipleOf`로 두어 OpenAPI에 그대로 드러난다.
|
|
673
|
+
* 외화 판매가(`prices[]`)는 통화마다 `isSaleUnitPrice`로 검사한다
|
|
674
|
+
*/
|
|
675
|
+
const BASE_SALE_UNIT = CURRENCIES[BASE_CURRENCY].saleUnit;
|
|
676
|
+
|
|
630
677
|
const productWriteShape = {
|
|
631
678
|
name: z.string().min(1).max(100),
|
|
632
679
|
categoryId: z.string().min(1, "카테고리를 선택해주십시오"),
|
|
633
680
|
salePrice: z
|
|
634
681
|
.number()
|
|
635
682
|
.int()
|
|
636
|
-
.min(
|
|
637
|
-
.multipleOf(
|
|
683
|
+
.min(BASE_SALE_UNIT)
|
|
684
|
+
.multipleOf(BASE_SALE_UNIT, "판매가는 10원 단위여야 합니다 (INVALID_PRICE_UNIT)")
|
|
638
685
|
.describe("판매가. 원(KRW) 단위 정수, 최소 10원, 10원 단위"),
|
|
639
686
|
originalPrice: z
|
|
640
687
|
.number()
|
|
@@ -650,6 +697,7 @@ const productWriteShape = {
|
|
|
650
697
|
.describe(
|
|
651
698
|
"구매 적립률 덮어쓰기(만분율, 100 = 1%, 0~3000, 이슈 #93). null이면 상점 기본 적립률, 0이면 이 상품은 적립하지 않는다. 결제 확정 시점 값이 주문상품에 남는다. 전체 수정(PUT)에서 생략하면 지금 값을 유지한다",
|
|
652
699
|
),
|
|
700
|
+
prices: productCurrencyPricesSchema.optional(),
|
|
653
701
|
description: z.string().optional(),
|
|
654
702
|
/**
|
|
655
703
|
* 상세설명 에디터(@avarlabs/editor)의 직렬화 상태. 재편집 때 이 값으로 문서를 복원한다.
|
|
@@ -666,6 +714,25 @@ const productWriteShape = {
|
|
|
666
714
|
"상품별 환불 정책 — 준 항목만 상점 기본을 덮어쓴다. `null`이나 생략은 상점 기본을 그대로 쓴다는 뜻이다. " +
|
|
667
715
|
"반품·교환 배송비(`returnDeliveryFee`·`exchangeDeliveryFee`)는 배송 상품에만 줄 수 있다(다른 유형은 400)",
|
|
668
716
|
),
|
|
717
|
+
notice: productNoticeRequestSchema
|
|
718
|
+
.nullable()
|
|
719
|
+
.optional()
|
|
720
|
+
.describe(
|
|
721
|
+
"상품정보제공 고시(이슈 #135). `null`은 지운다. 등록에서 생략하면 없음, 전체 수정(PUT)에서 생략하면 지금 값을 유지한다",
|
|
722
|
+
),
|
|
723
|
+
certifications: productCertificationsRequestSchema
|
|
724
|
+
.nullable()
|
|
725
|
+
.optional()
|
|
726
|
+
.describe(
|
|
727
|
+
"인증 정보(이슈 #135). `null`은 지운다. 등록에서 생략하면 없음, 전체 수정(PUT)에서 생략하면 지금 값을 유지한다",
|
|
728
|
+
),
|
|
729
|
+
infoSectionIds: z
|
|
730
|
+
.array(z.string().min(1))
|
|
731
|
+
.max(MAX_PRODUCT_INFO_SECTIONS)
|
|
732
|
+
.optional()
|
|
733
|
+
.describe(
|
|
734
|
+
"연결할 안내 섹션 id(이슈 #135). 배열 순서가 표시 순서다. 이 상점의 섹션이 아니면 400 `INFO_SECTION_NOT_FOUND`. 전체 수정(PUT)에서 생략하면 지금 연결을 유지한다",
|
|
735
|
+
),
|
|
669
736
|
...optionModelRequestSchema.shape,
|
|
670
737
|
sourceOptionSetIds: z
|
|
671
738
|
.array(z.string())
|
|
@@ -717,7 +784,7 @@ export type ProductReplaceRequestInput = z.input<typeof productReplaceRequestSch
|
|
|
717
784
|
export const productPatchRequestSchema = z
|
|
718
785
|
.strictObject({
|
|
719
786
|
name: z.string().max(100).optional(),
|
|
720
|
-
salePrice: z.number().int().multipleOf(
|
|
787
|
+
salePrice: z.number().int().multipleOf(BASE_SALE_UNIT).optional(),
|
|
721
788
|
status: productStatusSchema.optional(),
|
|
722
789
|
flowId: z
|
|
723
790
|
.string()
|
|
@@ -732,6 +799,7 @@ export const productPatchRequestSchema = z
|
|
|
732
799
|
"상품 이미지 주소 목록. 주면 통째로 바꾼다(현재 목록과 병합하지 않는다). 첫 장이 대표 이미지(썸네일)다. 생략하면 바꾸지 않는다",
|
|
733
800
|
),
|
|
734
801
|
fulfillment: productFulfillmentUpdateSchema.optional().describe(fulfillmentUpdateDescription),
|
|
802
|
+
prices: productCurrencyPricesSchema.optional(),
|
|
735
803
|
refundPolicy: productRefundPolicyPatchSchema
|
|
736
804
|
.nullable()
|
|
737
805
|
.optional()
|
package/src/schemas/promotion.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { CURRENCIES, KRW_MONEY_RULES, type MoneyRules } from "./currency";
|
|
2
3
|
import { proratedRefundBase } from "./refund-policy";
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -172,7 +173,7 @@ export const couponRejectReasonSchema = z
|
|
|
172
173
|
export type CouponRejectReason = z.infer<typeof couponRejectReasonSchema>;
|
|
173
174
|
|
|
174
175
|
/** 쿠폰 적용 뒤 결제 금액의 하한 — 0원 결제 경로가 없어 이보다 작으면 결제를 시작하지 않는다(400 `PAYMENT_AMOUNT_TOO_LOW`) */
|
|
175
|
-
export const MIN_PAYMENT_AMOUNT =
|
|
176
|
+
export const MIN_PAYMENT_AMOUNT = CURRENCIES.KRW.minPayment;
|
|
176
177
|
|
|
177
178
|
// ─── 계산 ─────────────────────────────────────────────
|
|
178
179
|
|
|
@@ -259,6 +260,11 @@ export interface PricingInput {
|
|
|
259
260
|
* 것처럼 보이고, 탈퇴한 회원의 반품이 회원 전용 조건에 떨어진다. 대상과 최소 금액만 본다.
|
|
260
261
|
*/
|
|
261
262
|
ignoreEligibility?: boolean;
|
|
263
|
+
/**
|
|
264
|
+
* 주문 통화의 규칙 값(이슈 #130) — 정률 할인 내림 단위(판매가 단위)와 적립금 사용 뒤 남길 최소 결제액. 생략하면 KRW 규칙이다.
|
|
265
|
+
* 금액 입력은 모두 이 통화의 최소 단위다
|
|
266
|
+
*/
|
|
267
|
+
moneyRules?: MoneyRules;
|
|
262
268
|
}
|
|
263
269
|
|
|
264
270
|
/** 줄 하나에 들어간 쿠폰별 할인 */
|
|
@@ -325,15 +331,22 @@ export interface PricingResult {
|
|
|
325
331
|
}
|
|
326
332
|
|
|
327
333
|
/**
|
|
328
|
-
* 할인액 — 정률은 10원
|
|
334
|
+
* 할인액 — 정률은 판매가 단위(`rules.saleUnit`, KRW 10원) 내림, 최대 할인액과 기준 금액을 넘지 않는다. 기준 금액이 0 이하면 0이다.
|
|
329
335
|
*
|
|
330
|
-
* 곱셈을 먼저 하고 나눈다. `base × 율 ÷
|
|
331
|
-
* 부동소수 오차가 끼지
|
|
336
|
+
* 곱셈을 먼저 하고 나눈다. `base × 율 ÷ (100 × 단위)`를 내려 단위를 곱하면 `floor(base × 율 / 100 / 단위) × 단위`와 같고
|
|
337
|
+
* 부동소수 오차가 끼지 않는다(KRW는 지금의 `÷ 1000 × 10` 그대로).
|
|
332
338
|
*/
|
|
333
|
-
export function couponDiscountOf(
|
|
339
|
+
export function couponDiscountOf(
|
|
340
|
+
benefit: CouponBenefit,
|
|
341
|
+
base: number,
|
|
342
|
+
rules: MoneyRules = KRW_MONEY_RULES,
|
|
343
|
+
): number {
|
|
334
344
|
if (base <= 0) return 0;
|
|
345
|
+
const unit = rules.saleUnit;
|
|
335
346
|
let raw =
|
|
336
|
-
benefit.type === "RATE"
|
|
347
|
+
benefit.type === "RATE"
|
|
348
|
+
? Math.floor((base * benefit.value) / (100 * unit)) * unit
|
|
349
|
+
: benefit.value;
|
|
337
350
|
if (benefit.type === "RATE" && benefit.maxDiscountAmount !== null) {
|
|
338
351
|
raw = Math.min(raw, benefit.maxDiscountAmount);
|
|
339
352
|
}
|
|
@@ -422,6 +435,7 @@ function periodRejectOf(coupon: PricingCoupon, now: Date): CouponRejectReason |
|
|
|
422
435
|
* 사본 중 앞 장이 만료됐으면 뒤 장이 쓰인다. 결과의 `coupons`는 입력 순서 그대로다. 입력이 같으면 결과도 같다.
|
|
423
436
|
*/
|
|
424
437
|
export function quotePricing(input: PricingInput): PricingResult {
|
|
438
|
+
const rules = input.moneyRules ?? KRW_MONEY_RULES;
|
|
425
439
|
const lines = input.lines.map((line) => ({
|
|
426
440
|
line,
|
|
427
441
|
productDiscount: 0,
|
|
@@ -516,14 +530,14 @@ export function quotePricing(input: PricingInput): PricingResult {
|
|
|
516
530
|
continue;
|
|
517
531
|
}
|
|
518
532
|
chosen = coupon.lineId;
|
|
519
|
-
best = couponDiscountOf(coupon.rule.benefit, lineOf(chosen)?.line.amount ?? 0);
|
|
533
|
+
best = couponDiscountOf(coupon.rule.benefit, lineOf(chosen)?.line.amount ?? 0, rules);
|
|
520
534
|
} else {
|
|
521
535
|
if (candidates.length === 0) {
|
|
522
536
|
result.rejectReason = "STACK_LIMIT";
|
|
523
537
|
continue;
|
|
524
538
|
}
|
|
525
539
|
for (const id of candidates) {
|
|
526
|
-
const discount = couponDiscountOf(coupon.rule.benefit, lineOf(id)?.line.amount ?? 0);
|
|
540
|
+
const discount = couponDiscountOf(coupon.rule.benefit, lineOf(id)?.line.amount ?? 0, rules);
|
|
527
541
|
if (chosen === null || discount > best) {
|
|
528
542
|
chosen = id;
|
|
529
543
|
best = discount;
|
|
@@ -557,7 +571,7 @@ export function quotePricing(input: PricingInput): PricingResult {
|
|
|
557
571
|
.filter((entry): entry is (typeof lines)[number] => entry !== undefined);
|
|
558
572
|
const weights = targets.map((entry) => Math.max(0, entry.line.amount - entry.productDiscount));
|
|
559
573
|
const base = weights.reduce((sum, w) => sum + w, 0);
|
|
560
|
-
const discount = couponDiscountOf(coupon.rule.benefit, base);
|
|
574
|
+
const discount = couponDiscountOf(coupon.rule.benefit, base, rules);
|
|
561
575
|
if (discount <= 0) {
|
|
562
576
|
result.rejectReason = "NO_DISCOUNT";
|
|
563
577
|
continue;
|
|
@@ -591,7 +605,7 @@ export function quotePricing(input: PricingInput): PricingResult {
|
|
|
591
605
|
}
|
|
592
606
|
const weights = deliveryBundles.map((bundle) => Math.max(0, bundle.fee));
|
|
593
607
|
const base = weights.reduce((sum, w) => sum + w, 0);
|
|
594
|
-
const discount = couponDiscountOf(coupon.rule.benefit, base);
|
|
608
|
+
const discount = couponDiscountOf(coupon.rule.benefit, base, rules);
|
|
595
609
|
if (discount <= 0) {
|
|
596
610
|
result.rejectReason = "NO_DISCOUNT";
|
|
597
611
|
continue;
|
|
@@ -625,11 +639,15 @@ export function quotePricing(input: PricingInput): PricingResult {
|
|
|
625
639
|
const bundleWeights = deliveryBundles.map((bundle, index) =>
|
|
626
640
|
Math.max(0, bundle.fee + bundle.remoteSurcharge - (bundles[index]?.couponDiscount ?? 0)),
|
|
627
641
|
);
|
|
628
|
-
const points = pointUseOf(
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
642
|
+
const points = pointUseOf(
|
|
643
|
+
input.points ?? null,
|
|
644
|
+
{
|
|
645
|
+
productPaid: productWeights.reduce((sum, w) => sum + w, 0),
|
|
646
|
+
deliveryPaid: bundleWeights.reduce((sum, w) => sum + w, 0),
|
|
647
|
+
total: beforePoints,
|
|
648
|
+
},
|
|
649
|
+
rules,
|
|
650
|
+
);
|
|
633
651
|
if (points.amount > 0) {
|
|
634
652
|
const productPaid = productWeights.reduce((sum, w) => sum + w, 0);
|
|
635
653
|
const toLines = Math.min(points.amount, productPaid);
|
|
@@ -670,6 +688,7 @@ export function quotePricing(input: PricingInput): PricingResult {
|
|
|
670
688
|
export function pointUseOf(
|
|
671
689
|
points: PricingPoints | null,
|
|
672
690
|
amounts: { productPaid: number; deliveryPaid: number; total: number },
|
|
691
|
+
moneyRules: MoneyRules = KRW_MONEY_RULES,
|
|
673
692
|
): {
|
|
674
693
|
amount: number;
|
|
675
694
|
maxUsable: number;
|
|
@@ -690,8 +709,8 @@ export function pointUseOf(
|
|
|
690
709
|
if (total > 0 && cap >= total) return { amount: total, adjusted: false };
|
|
691
710
|
const amount = floorUnit(Math.min(cap, total));
|
|
692
711
|
const remaining = total - amount;
|
|
693
|
-
if (remaining > 0 && remaining <
|
|
694
|
-
return { amount: floorUnit(total -
|
|
712
|
+
if (remaining > 0 && remaining < moneyRules.minPayment) {
|
|
713
|
+
return { amount: floorUnit(total - moneyRules.minPayment), adjusted: true };
|
|
695
714
|
}
|
|
696
715
|
return { amount, adjusted: false };
|
|
697
716
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { BASE_CURRENCY, currencyCodeSchema } from "./currency";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* 환불 정책 (이슈 #25) — 취소·반품·교환에서 **배송비를 누가 부담하는지**를 정한다.
|
|
@@ -28,8 +29,26 @@ const descriptions = {
|
|
|
28
29
|
couponShortfallCharge:
|
|
29
30
|
"쿠폰 조건 미달 차감. 일부 취소·반품으로 남은 주문상품이 쿠폰 조건(최소 주문 금액 등)을 잃으면 남은 주문상품이 받던 쿠폰 할인을 환불액에서 뺀다. " +
|
|
30
31
|
"판매자 귀책(셀러 직권취소, 불량·오배송 반품)은 빼지 않는다. false면 차감 없이 실결제 금액만 나눠 환불한다. 기본 true",
|
|
32
|
+
currencies:
|
|
33
|
+
"외화별 반품 배송비·교환 배송비(이슈 #130). 키는 판매 통화 코드이고 기본 통화(KRW)는 넣지 않는다. 금액은 그 통화의 최소 단위다. " +
|
|
34
|
+
"상품이 정한 통화 값이 상점 값보다 앞선다. 정하지 않은 외화로는 배송 상품을 팔지 않는다. 주면 맵 전체를 바꾼다",
|
|
31
35
|
} as const;
|
|
32
36
|
|
|
37
|
+
/** 외화 한 통화의 반품·교환 배송비 */
|
|
38
|
+
export const currencyRefundFeesSchema = z.strictObject({
|
|
39
|
+
returnDeliveryFee: z.number().int().min(0).describe("이 통화의 반품 배송비. 통화의 최소 단위"),
|
|
40
|
+
exchangeDeliveryFee: z.number().int().min(0).describe("이 통화의 교환 배송비. 통화의 최소 단위"),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
export type CurrencyRefundFees = z.infer<typeof currencyRefundFeesSchema>;
|
|
44
|
+
|
|
45
|
+
const currencyRefundMapSchema = z
|
|
46
|
+
.partialRecord(currencyCodeSchema, currencyRefundFeesSchema)
|
|
47
|
+
.refine((map) => !(BASE_CURRENCY in map), {
|
|
48
|
+
message: "기본 통화는 currencies에 넣지 않습니다",
|
|
49
|
+
})
|
|
50
|
+
.describe(descriptions.currencies);
|
|
51
|
+
|
|
33
52
|
/** 유효 환불 정책 — 상점 기본 + 상품별 덮어쓰기를 합친 결과 */
|
|
34
53
|
export const refundPolicySchema = z.object({
|
|
35
54
|
preShipmentDeliveryFeeRefund: z.boolean().describe(descriptions.preShipmentDeliveryFeeRefund),
|
|
@@ -44,6 +63,7 @@ export const refundPolicySchema = z.object({
|
|
|
44
63
|
.describe(
|
|
45
64
|
`${descriptions.couponShortfallCharge}. 이 필드가 없는 이전 서버 응답은 true로 읽는다`,
|
|
46
65
|
),
|
|
66
|
+
currencies: currencyRefundMapSchema.optional(),
|
|
47
67
|
});
|
|
48
68
|
|
|
49
69
|
export type RefundPolicy = z.infer<typeof refundPolicySchema>;
|
|
@@ -87,6 +107,7 @@ export const refundPolicyPatchSchema = z.strictObject({
|
|
|
87
107
|
.optional()
|
|
88
108
|
.describe(descriptions.conditionalFreeShortfallCharge),
|
|
89
109
|
couponShortfallCharge: z.boolean().optional().describe(descriptions.couponShortfallCharge),
|
|
110
|
+
currencies: currencyRefundMapSchema.optional(),
|
|
90
111
|
});
|
|
91
112
|
|
|
92
113
|
export type RefundPolicyPatch = z.infer<typeof refundPolicyPatchSchema>;
|
|
@@ -114,9 +135,41 @@ export interface RefundPolicyProductInput {
|
|
|
114
135
|
export function resolveRefundPolicy(
|
|
115
136
|
storePolicy?: RefundPolicyPatch | null,
|
|
116
137
|
product?: RefundPolicyProductInput | null,
|
|
117
|
-
): RefundPolicy
|
|
138
|
+
): RefundPolicy;
|
|
139
|
+
/**
|
|
140
|
+
* 통화의 유효 정책(이슈 #130) — 기본 통화는 위와 같다. 외화는 반품·교환 배송비를 상품 `currencies[통화]` → 상점 `currencies[통화]` 순으로 찾고
|
|
141
|
+
* (부담 규칙 boolean은 통화와 무관하다), 둘 다 없으면 null이다 — 그 통화로 배송 상품을 팔지 않는다(`NO_REFUND_FEE`).
|
|
142
|
+
* 결과의 `currencies`는 지우고 금액 필드가 그 통화 값이다. 통화별 반품비 해석은 이 함수 하나다
|
|
143
|
+
*/
|
|
144
|
+
export function resolveRefundPolicy(
|
|
145
|
+
storePolicy: RefundPolicyPatch | null | undefined,
|
|
146
|
+
product: RefundPolicyProductInput | null | undefined,
|
|
147
|
+
currency: string,
|
|
148
|
+
): RefundPolicy | null;
|
|
149
|
+
export function resolveRefundPolicy(
|
|
150
|
+
storePolicy?: RefundPolicyPatch | null,
|
|
151
|
+
product?: RefundPolicyProductInput | null,
|
|
152
|
+
currency: string = BASE_CURRENCY,
|
|
153
|
+
): RefundPolicy | null {
|
|
118
154
|
const store = { ...REFUND_POLICY_DEFAULTS, ...dropUndefined(storePolicy) };
|
|
119
|
-
|
|
155
|
+
const override = productRefundPolicyOverride(product) ?? {};
|
|
156
|
+
const merged: RefundPolicy = { ...store, ...override };
|
|
157
|
+
if (currency === BASE_CURRENCY) {
|
|
158
|
+
// 상품 덮어쓰기의 currencies가 상점 맵을 통째로 덮지 않게 통화마다 합친다
|
|
159
|
+
if (store.currencies || override.currencies) {
|
|
160
|
+
merged.currencies = { ...store.currencies, ...override.currencies };
|
|
161
|
+
}
|
|
162
|
+
return merged;
|
|
163
|
+
}
|
|
164
|
+
const code = currency as keyof NonNullable<RefundPolicy["currencies"]>;
|
|
165
|
+
const fees = override.currencies?.[code] ?? store.currencies?.[code];
|
|
166
|
+
if (!fees) return null;
|
|
167
|
+
const { currencies: _byCurrency, ...rest } = merged;
|
|
168
|
+
return {
|
|
169
|
+
...rest,
|
|
170
|
+
returnDeliveryFee: fees.returnDeliveryFee,
|
|
171
|
+
exchangeDeliveryFee: fees.exchangeDeliveryFee,
|
|
172
|
+
};
|
|
120
173
|
}
|
|
121
174
|
|
|
122
175
|
/**
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { BASE_CURRENCY, currencyCodeSchema } from "./currency";
|
|
2
3
|
import { type DeliveryType, type FulfillmentType, requiresShipping } from "./fulfillment";
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -32,14 +33,38 @@ const descriptions = {
|
|
|
32
33
|
"null이면 전체 무료배송을 쓰지 않는다(상품별 조건부 무료는 그대로 동작한다). 기본 null",
|
|
33
34
|
jejuSurcharge: "제주 지역 추가 배송비. 원 단위. **배송 묶음마다** 더한다. 기본 0",
|
|
34
35
|
isolatedSurcharge: "도서산간 지역 추가 배송비. 원 단위. **배송 묶음마다** 더한다. 기본 0",
|
|
36
|
+
currencies:
|
|
37
|
+
"외화별 기본 배송비·전체 무료배송 기준액(이슈 #130). 키는 판매 통화 코드이고 기본 통화(KRW)는 넣지 않는다. 금액은 그 통화의 최소 단위다. " +
|
|
38
|
+
"정하지 않은 외화로는 배송 상품을 팔지 않는다. 지역 추가 배송비는 국내(기본 통화) 값만 있다. 주면 맵 전체를 바꾼다",
|
|
35
39
|
} as const;
|
|
36
40
|
|
|
41
|
+
/** 외화 한 통화의 상점 배송 값 */
|
|
42
|
+
export const currencyShippingPolicySchema = z.strictObject({
|
|
43
|
+
baseDeliveryFee: z.number().int().min(0).describe("이 통화의 기본 배송비. 통화의 최소 단위"),
|
|
44
|
+
freeThreshold: z
|
|
45
|
+
.number()
|
|
46
|
+
.int()
|
|
47
|
+
.min(1)
|
|
48
|
+
.nullable()
|
|
49
|
+
.describe("이 통화의 전체 무료배송 기준액. null이면 쓰지 않는다"),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
export type CurrencyShippingPolicy = z.infer<typeof currencyShippingPolicySchema>;
|
|
53
|
+
|
|
54
|
+
const currencyShippingMapSchema = z
|
|
55
|
+
.partialRecord(currencyCodeSchema, currencyShippingPolicySchema)
|
|
56
|
+
.refine((map) => !(BASE_CURRENCY in map), {
|
|
57
|
+
message: "기본 통화는 currencies에 넣지 않습니다",
|
|
58
|
+
})
|
|
59
|
+
.describe(descriptions.currencies);
|
|
60
|
+
|
|
37
61
|
/** 유효 배송비 정책 — 상점이 정하지 않은 항목은 `SHIPPING_POLICY_DEFAULTS`로 채운 값이다 */
|
|
38
62
|
export const shippingPolicySchema = z.object({
|
|
39
63
|
baseDeliveryFee: z.number().int().min(0).describe(descriptions.baseDeliveryFee),
|
|
40
64
|
freeThreshold: z.number().int().min(1).nullable().describe(descriptions.freeThreshold),
|
|
41
65
|
jejuSurcharge: z.number().int().min(0).describe(descriptions.jejuSurcharge),
|
|
42
66
|
isolatedSurcharge: z.number().int().min(0).describe(descriptions.isolatedSurcharge),
|
|
67
|
+
currencies: currencyShippingMapSchema.optional(),
|
|
43
68
|
});
|
|
44
69
|
|
|
45
70
|
export type ShippingPolicy = z.infer<typeof shippingPolicySchema>;
|
|
@@ -68,17 +93,41 @@ export const shippingPolicyPatchSchema = z.strictObject({
|
|
|
68
93
|
freeThreshold: z.number().int().min(1).nullable().optional().describe(descriptions.freeThreshold),
|
|
69
94
|
jejuSurcharge: z.number().int().min(0).optional().describe(descriptions.jejuSurcharge),
|
|
70
95
|
isolatedSurcharge: z.number().int().min(0).optional().describe(descriptions.isolatedSurcharge),
|
|
96
|
+
currencies: currencyShippingMapSchema.optional(),
|
|
71
97
|
});
|
|
72
98
|
|
|
73
99
|
export type ShippingPolicyPatch = z.infer<typeof shippingPolicyPatchSchema>;
|
|
74
100
|
|
|
75
|
-
/**
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
101
|
+
/**
|
|
102
|
+
* 유효 정책 해석 — 상점이 정한 항목 + `SHIPPING_POLICY_DEFAULTS`.
|
|
103
|
+
*
|
|
104
|
+
* `currency`(이슈 #130)를 주면 그 통화의 정책이다. 기본 통화는 지금과 같고, 외화는 `currencies[통화]`의 기본 배송비·무료 기준액에
|
|
105
|
+
* 지역 추가 배송비 0이다(할증 구간은 국내 값). 외화 값이 없으면 null — 그 통화로 배송 상품을 팔지 않는다. 통화별 값 해석은 이 함수 하나다.
|
|
106
|
+
*/
|
|
107
|
+
export function resolveShippingPolicy(patch?: ShippingPolicyPatch | null): ShippingPolicy;
|
|
108
|
+
export function resolveShippingPolicy(
|
|
109
|
+
patch: ShippingPolicyPatch | null | undefined,
|
|
110
|
+
currency: string,
|
|
111
|
+
): ShippingPolicy | null;
|
|
112
|
+
export function resolveShippingPolicy(
|
|
113
|
+
patch?: ShippingPolicyPatch | null,
|
|
114
|
+
currency: string = BASE_CURRENCY,
|
|
115
|
+
): ShippingPolicy | null {
|
|
116
|
+
const given = patch
|
|
117
|
+
? (Object.fromEntries(
|
|
118
|
+
Object.entries(patch).filter(([, value]) => value !== undefined),
|
|
119
|
+
) as ShippingPolicyPatch)
|
|
120
|
+
: {};
|
|
121
|
+
const base: ShippingPolicy = { ...SHIPPING_POLICY_DEFAULTS, ...given };
|
|
122
|
+
if (currency === BASE_CURRENCY) return base;
|
|
123
|
+
const foreign = base.currencies?.[currency as keyof typeof base.currencies];
|
|
124
|
+
if (!foreign) return null;
|
|
125
|
+
return {
|
|
126
|
+
baseDeliveryFee: foreign.baseDeliveryFee,
|
|
127
|
+
freeThreshold: foreign.freeThreshold,
|
|
128
|
+
jejuSurcharge: 0,
|
|
129
|
+
isolatedSurcharge: 0,
|
|
130
|
+
};
|
|
82
131
|
}
|
|
83
132
|
|
|
84
133
|
// ─── 지역 추가 배송비 ─────────────────────────────────────
|
|
@@ -13,7 +13,6 @@ import { RESERVED_STORE_CODES } from "./store";
|
|
|
13
13
|
* 공개 전환·슬러그 변경·호스팅 끄기는 승인(`APPROVAL_REQUIRED`)이다 — 구매자가 보는 화면을 바꾸는 쓰기다.
|
|
14
14
|
*
|
|
15
15
|
* 오류 코드:
|
|
16
|
-
* - 403 `STOREFRONT_HOSTING_DISABLED` — 플랫폼 스위치가 꺼져 새 사이트를 만들 수 없다
|
|
17
16
|
* - 404 `STOREFRONT_SITE_NOT_FOUND` · `STOREFRONT_TEMPLATE_NOT_FOUND` · `STOREFRONT_VERSION_NOT_FOUND`
|
|
18
17
|
* - 409 `STOREFRONT_SITE_EXISTS` — 이미 사이트가 있다(상점당 1개)
|
|
19
18
|
* - 409 `STOREFRONT_SLUG_TAKEN` — 다른 사이트가 쓰거나 보류 중인 슬러그다
|
|
@@ -30,7 +29,7 @@ import { RESERVED_STORE_CODES } from "./store";
|
|
|
30
29
|
* - 503 `STOREFRONT_PREVIEW_NOT_CONFIGURED` — 미리보기 서명 키가 설정되지 않았다
|
|
31
30
|
*
|
|
32
31
|
* 편집(M2b, `/v1/storefront/site/editing`·`files/**`·`checks/**`·`verify`·`builds/**`, 버전 만들기):
|
|
33
|
-
* - 403 `STOREFRONT_EDITING_DISABLED` —
|
|
32
|
+
* - 403 `STOREFRONT_EDITING_DISABLED` — 편집을 쓸 수 없어 셀러 템플릿으로 사이트를 만들 수 없다(사이트 만들기)
|
|
34
33
|
* - 503 `STOREFRONT_EDITING_NOT_CONFIGURED` — 편집 Sandbox가 설정되지 않았다
|
|
35
34
|
* - 409 `EDIT_CAPACITY_FULL` — 플랫폼 전체 동시 편집 상한에 걸렸다. `error.details.retryAfterSeconds` 뒤에 다시 한다
|
|
36
35
|
* - 409 `EDIT_SESSION_REQUIRED` — 편집 미리보기 링크: 편집 환경이 없다
|
|
@@ -392,13 +391,11 @@ export const storefrontSiteSchema = z.object({
|
|
|
392
391
|
});
|
|
393
392
|
export type StorefrontSite = z.infer<typeof storefrontSiteSchema>;
|
|
394
393
|
|
|
395
|
-
/** `GET /v1/storefront/site` —
|
|
394
|
+
/** `GET /v1/storefront/site` — 호스팅 설정과 현재 사이트(없으면 null. 끈 사이트는 싣지 않는다) */
|
|
396
395
|
export const storefrontHostingSchema = z.object({
|
|
397
|
-
enabled: z.boolean().describe("새 사이트를 만들 수 있는가 (
|
|
396
|
+
enabled: z.boolean().describe("새 사이트를 만들 수 있는가 (항상 true)"),
|
|
398
397
|
domain: z.string().describe("호스팅 도메인 (예: `sayren.co`). 사이트 주소는 `{slug}.{domain}`"),
|
|
399
|
-
editingEnabled: z
|
|
400
|
-
.boolean()
|
|
401
|
-
.describe("파일 편집·버전 만들기를 쓸 수 있는가 (플랫폼 편집 스위치와 Sandbox 설정)"),
|
|
398
|
+
editingEnabled: z.boolean().describe("파일 편집·버전 만들기를 쓸 수 있는가 (Sandbox 설정)"),
|
|
402
399
|
site: storefrontSiteSchema.nullable(),
|
|
403
400
|
});
|
|
404
401
|
export type StorefrontHosting = z.infer<typeof storefrontHostingSchema>;
|
|
@@ -574,7 +571,6 @@ export type StorefrontPreviewLink = z.infer<typeof storefrontPreviewLinkSchema>;
|
|
|
574
571
|
// - 결제 복귀·방문 분석·소셜 로그인 허용 목록은 연결된(`ACTIVE`) 도메인을 자동으로 더한다.
|
|
575
572
|
//
|
|
576
573
|
// 오류 코드:
|
|
577
|
-
// - 403 `STOREFRONT_CUSTOM_DOMAINS_DISABLED` — 플랫폼 스위치가 꺼져 도메인을 추가할 수 없다
|
|
578
574
|
// - 400 `STOREFRONT_DOMAIN_INVALID` — 형식이 틀렸거나 연결할 수 없는 도메인이다(`error.details.reason`)
|
|
579
575
|
// - 404 `STOREFRONT_DOMAIN_NOT_FOUND`
|
|
580
576
|
// - 409 `STOREFRONT_DOMAIN_EXISTS` — 이 상점에 이미 추가한 도메인이다
|
|
@@ -798,7 +794,7 @@ export type StorefrontSiteDomain = z.infer<typeof storefrontSiteDomainSchema>;
|
|
|
798
794
|
|
|
799
795
|
/** `GET /v1/storefront/site/domains` — 사이트의 주소 목록(기본 주소 + 내 도메인)과 연결 설정 */
|
|
800
796
|
export const storefrontSiteDomainsSchema = z.object({
|
|
801
|
-
enabled: z.boolean().describe("내 도메인을 추가할 수 있는가 (
|
|
797
|
+
enabled: z.boolean().describe("내 도메인을 추가할 수 있는가 (항상 true)"),
|
|
802
798
|
limit: z.number().int().min(0).describe("상점당 내 도메인 수 상한 (해제한 도메인은 세지 않는다)"),
|
|
803
799
|
target: z.string().describe("CNAME 대상 (예: `sites.sayren.co`)"),
|
|
804
800
|
domains: z.array(storefrontSiteDomainSchema).describe("대표 주소 먼저, 그다음 추가한 순서"),
|
|
@@ -971,7 +967,7 @@ export const storefrontDevLogsSchema = z.object({
|
|
|
971
967
|
export type StorefrontDevLogs = z.infer<typeof storefrontDevLogsSchema>;
|
|
972
968
|
|
|
973
969
|
export const storefrontEditingSchema = z.object({
|
|
974
|
-
enabled: z.boolean().describe("편집을 쓸 수 있는가 (
|
|
970
|
+
enabled: z.boolean().describe("편집을 쓸 수 있는가 (Sandbox 설정)"),
|
|
975
971
|
session: storefrontEditSessionSchema
|
|
976
972
|
.nullable()
|
|
977
973
|
.describe("열린 편집 환경. 잠들었거나 닫았으면 null"),
|
|
@@ -50,6 +50,12 @@ export const orderWebhookEventDataSchema = z.object({
|
|
|
50
50
|
"주문번호(이슈 #107) — 숫자 14자리. 이 필드가 없는 이전 이벤트와 번호 체계 이전 주문은 null로 읽는다",
|
|
51
51
|
),
|
|
52
52
|
status: orderItemStatusSchema.describe("이벤트 발생 시점의 주문 대표 상태"),
|
|
53
|
+
currency: z
|
|
54
|
+
.string()
|
|
55
|
+
.optional()
|
|
56
|
+
.describe(
|
|
57
|
+
"주문 통화(ISO 4217, 이슈 #130). 이 이벤트의 금액은 모두 이 통화의 최소 단위다. 외화 주문이 생기면 이 값을 확인한다. 이 필드가 없는 이전 이벤트는 KRW다",
|
|
58
|
+
),
|
|
53
59
|
testPayment: z
|
|
54
60
|
.boolean()
|
|
55
61
|
.optional()
|
|
@@ -129,6 +135,12 @@ export const orderFulfilledWebhookEventDataSchema = z.object({
|
|
|
129
135
|
'**Deprecated** — 항상 `{ mode: "MANUAL" }`이다. 자동 제공을 없앴고(이슈 #48) store-sdk 0.19 이하 호환으로만 싣는다. 게시된 연동이 store-sdk 0.20 이상으로 넘어간 뒤 다음 릴리스에서 뺀다',
|
|
130
136
|
),
|
|
131
137
|
fulfilledAt: z.string().describe("제공 시각 (ISO 8601). 주문상품의 `deliveredAt`과 같다"),
|
|
138
|
+
currency: z
|
|
139
|
+
.string()
|
|
140
|
+
.optional()
|
|
141
|
+
.describe(
|
|
142
|
+
"주문 통화(ISO 4217, 이슈 #130). 이 이벤트의 금액은 모두 이 통화의 최소 단위다. 외화 주문이 생기면 이 값을 확인한다. 이 필드가 없는 이전 이벤트는 KRW다",
|
|
143
|
+
),
|
|
132
144
|
testPayment: z
|
|
133
145
|
.boolean()
|
|
134
146
|
.describe("테스트 결제(샌드박스 결제) 주문인가. true면 실제로 돈이 오가지 않았다"),
|
|
@@ -204,6 +216,12 @@ export const orderCanceledWebhookEventDataSchema = z.object({
|
|
|
204
216
|
"돌려준 적립금 (원, #93) — 결제에 쓴 적립금 중 이번 취소·반품 수량의 몫이다. 현금 환불(`refundAmount`)과 별개다. " +
|
|
205
217
|
"차감은 현금에서 먼저 빼고 모자란 몫만 여기서 뺀다. 적립금을 쓰지 않은 주문과 이 필드가 없는 이전 응답·이벤트는 0으로 읽는다",
|
|
206
218
|
),
|
|
219
|
+
currency: z
|
|
220
|
+
.string()
|
|
221
|
+
.optional()
|
|
222
|
+
.describe(
|
|
223
|
+
"주문 통화(ISO 4217, 이슈 #130). 이 이벤트의 금액은 모두 이 통화의 최소 단위다. 외화 주문이 생기면 이 값을 확인한다. 이 필드가 없는 이전 이벤트는 KRW다",
|
|
224
|
+
),
|
|
207
225
|
testPayment: z
|
|
208
226
|
.boolean()
|
|
209
227
|
.describe("테스트 결제(샌드박스 결제) 주문인가. true면 실제로 돈이 오가지 않았다"),
|
|
@@ -292,6 +310,12 @@ export const claimWebhookEventDataSchema = z.object({
|
|
|
292
310
|
"돌려준 적립금 (원, #93) — 결제에 쓴 적립금 중 이번 취소·반품 수량의 몫이다. 현금 환불(`refundAmount`)과 별개다. " +
|
|
293
311
|
"차감은 현금에서 먼저 빼고 모자란 몫만 여기서 뺀다. 적립금을 쓰지 않은 주문과 이 필드가 없는 이전 응답·이벤트는 0으로 읽는다",
|
|
294
312
|
),
|
|
313
|
+
currency: z
|
|
314
|
+
.string()
|
|
315
|
+
.optional()
|
|
316
|
+
.describe(
|
|
317
|
+
"주문 통화(ISO 4217, 이슈 #130). 이 이벤트의 금액은 모두 이 통화의 최소 단위다. 외화 주문이 생기면 이 값을 확인한다. 이 필드가 없는 이전 이벤트는 KRW다",
|
|
318
|
+
),
|
|
295
319
|
testPayment: z
|
|
296
320
|
.boolean()
|
|
297
321
|
.optional()
|