@sayren/store-sdk 0.9.0 → 0.11.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.mjs CHANGED
@@ -722,6 +722,21 @@ const claimReasonSchema = z.enum([
722
722
  "DIFFERENT_FROM_PAGE",
723
723
  "DELAY"
724
724
  ]);
725
+ /**
726
+ * 반품지 (이슈 #30) — 반품·교환 수거가 안내하는 주소다. 스토어 주소록의 기본 반품지이거나,
727
+ * 상품이 반품지를 따로 지정했으면 그 주소다.
728
+ *
729
+ * 취소 클레임은 보낼 물건이 없어 null이고, 반품지를 등록하지 않은 스토어도 null이다.
730
+ * 구매자가 직접 보내는 수거(`BUYER_SEND`)에 이 주소가 필요하다.
731
+ */
732
+ const claimReturnAddressSchema = z.object({
733
+ name: z.string().describe("주소 이름(셀러가 붙인 별칭). 예: 본사 물류센터"),
734
+ contactName: z.string().describe("수취인 이름"),
735
+ phone: z.string().describe("연락처"),
736
+ zipCode: z.string(),
737
+ address1: z.string(),
738
+ address2: z.string().nullable()
739
+ });
725
740
  const claimSchema = z.object({
726
741
  claimId: z.string(),
727
742
  type: claimTypeSchema,
@@ -733,8 +748,9 @@ const claimSchema = z.object({
733
748
  reason: claimReasonSchema,
734
749
  reasonDetail: z.string().nullable(),
735
750
  responsibility: z.enum(["BUYER", "SELLER"]),
736
- claimDeliveryFee: z.number().int(),
737
- expectedRefundAmount: z.number().int().nullable(),
751
+ claimDeliveryFee: z.number().int().describe("구매자가 부담하는 반품·교환 배송비. 원 단위"),
752
+ expectedRefundAmount: z.number().int().nullable().describe("접수 시점에 계산한 예상 환불액 — 수량 비율 상품 금액 − 반품 배송비 − 조건부 무료배송 미달 차감. **배송비 환불은 포함되지 않고 `expectedDeliveryFeeRefundAmount`로 따로 온다**(승인 시점의 배송 묶음 상태로 확정되기 때문이다). 교환은 환불이 없어 null이다"),
753
+ expectedDeliveryFeeRefundAmount: z.number().int().nullable().describe("접수 시점에 계산한 예상 배송비 환불액. 배송 묶음(같은 상품의 주문상품)이 모두 취소되거나 판매자 귀책일 때만 0보다 크다. 이 필드가 생기기 전에 접수된 클레임은 null이다"),
738
754
  exchangeOption: z.object({
739
755
  optionId: z.string(),
740
756
  optionName: z.string()
@@ -749,7 +765,8 @@ const claimSchema = z.object({
749
765
  /** 보류 중이면 자동 해제 시각(ISO 8601), 아니면 null */
750
766
  holdExpiresAt: z.string().nullable(),
751
767
  /** 보류를 이미 썼는지 — 보류는 클레임당 1회라 해제·만료 뒤에도 true */
752
- holdUsed: z.boolean()
768
+ holdUsed: z.boolean(),
769
+ returnAddress: claimReturnAddressSchema.nullable().describe("수거 안내에 쓸 반품지(이슈 #30). 상품이 지정한 반품지, 없으면 스토어 기본 반품지다. 취소 클레임과 반품지를 등록하지 않은 스토어는 null")
753
770
  });
754
771
  /** `GET /v1/claims` 쿼리 — OpenAPI 문서용 형태(값은 쿼리 문자열) */
755
772
  const claimListQuerySchema = z.object({
@@ -788,8 +805,11 @@ const rejectClaimRequestSchema = z.object({
788
805
  });
789
806
  const approveClaimResultSchema = z.object({
790
807
  claimId: z.string(),
791
- refundAmount: z.number().int(),
792
- deductAmount: z.number().int(),
808
+ refundAmount: z.number().int().describe("실제 환불한 금액 — 수량 비율 상품 금액 + 배송비 환불 − 반품 배송비 − 조건부 무료배송 미달 차감 − 추가 차감(`deductAmount`)"),
809
+ deductAmount: z.number().int().describe("승인 때 셀러가 추가로 차감한 금액"),
810
+ quantity: z.number().int().describe("환불한 수량"),
811
+ deliveryFeeRefundAmount: z.number().int().describe("환불에 포함된 배송비. 배송 묶음 전량 취소나 판매자 귀책일 때만 0보다 크다"),
812
+ deliveryFeeChargeAmount: z.number().int().describe("환불액에서 뺀 **배송비** — 반품·교환 배송비 + 조건부 무료배송 미달 차감. 추가 차감(`deductAmount`)은 포함하지 않는다"),
793
813
  refundedAt: z.string()
794
814
  });
795
815
  //#endregion
@@ -838,6 +858,64 @@ const customerAddressSchema = z.object({
838
858
  address1: z.string(),
839
859
  address2: z.string().nullable()
840
860
  });
861
+ /** 동의 종류 — 이용약관·개인정보 수집이용은 가입 필수, 마케팅 수신은 선택 */
862
+ const consentKindSchema = z.enum([
863
+ "TERMS",
864
+ "PRIVACY",
865
+ "MARKETING"
866
+ ]);
867
+ const consentActionSchema = z.enum(["AGREED", "WITHDRAWN"]);
868
+ /**
869
+ * 동의가 일어난 경로. 값이 추가될 수 있어 모르는 값은 그대로 보여 준다.
870
+ * `SIGNUP` 비밀번호 가입 · `SOCIAL_SIGNUP` 소셜 가입 · `DELEGATED` 셀러 자체 회원 연동 ·
871
+ * `RECONSENT` 약관 개정 재동의 · `MEMBER_UPDATE` 구매자가 직접 변경 · `SELLER` 셀러가 처리
872
+ */
873
+ const consentSourceSchema = z.enum([
874
+ "SIGNUP",
875
+ "SOCIAL_SIGNUP",
876
+ "DELEGATED",
877
+ "RECONSENT",
878
+ "MEMBER_UPDATE",
879
+ "SELLER"
880
+ ]);
881
+ /** 동의 이력 한 줄 — 추가만 되고 지워지지 않는다(구매자 탈퇴 시에만 함께 지워진다) */
882
+ const customerConsentSchema = z.object({
883
+ consentId: z.string(),
884
+ kind: consentKindSchema,
885
+ /** 동의한 약관 버전. 마케팅은 버전이 없어 null */
886
+ version: z.string().nullable(),
887
+ /** 동의 시점의 약관 문서 주소. 스토어가 주소를 정하지 않았거나 마케팅이면 null */
888
+ documentUrl: z.string().nullable(),
889
+ action: consentActionSchema,
890
+ source: consentSourceSchema,
891
+ occurredAt: z.string()
892
+ });
893
+ /** 종류별 현재 동의 상태 */
894
+ const customerConsentStateSchema = z.object({
895
+ kind: consentKindSchema,
896
+ agreed: z.boolean(),
897
+ /** 동의한 약관 버전. 마케팅이거나 이력이 없으면 null */
898
+ agreedVersion: z.string().nullable(),
899
+ /** 스토어의 현재 약관 버전. 마케팅은 null */
900
+ currentVersion: z.string().nullable(),
901
+ /** 스토어가 정한 현재 약관 문서 주소. 정하지 않았거나 마케팅이면 null */
902
+ currentDocumentUrl: z.string().nullable(),
903
+ updatedAt: z.string().nullable(),
904
+ /** 약관이 개정돼 다시 받아야 하는가. 마케팅은 항상 false */
905
+ reconsentRequired: z.boolean()
906
+ });
907
+ const customerConsentsSchema = z.object({
908
+ terms: customerConsentStateSchema,
909
+ privacy: customerConsentStateSchema,
910
+ marketing: customerConsentStateSchema,
911
+ /** 필수 동의 중 하나라도 현재 버전이 아니면 true */
912
+ reconsentRequired: z.boolean()
913
+ });
914
+ /** `GET /v1/customers/{id}/consents` 쿼리 — 최근 순이다 */
915
+ const customerConsentListQuerySchema = z.object({
916
+ kind: consentKindSchema.optional().describe("동의 종류 필터. 생략하면 전체"),
917
+ ...pageQueryShape()
918
+ });
841
919
  /** 구매자 상세 — 휴대폰 전체를 보여 준다 */
842
920
  const customerDetailSchema = z.object({
843
921
  customerId: z.string(),
@@ -857,6 +935,7 @@ const customerDetailSchema = z.object({
857
935
  /** 소셜 연결 시각. 비밀번호는 가입 시각 */
858
936
  linkedAt: z.string()
859
937
  })).describe("로그인 방법 — 이메일·비밀번호(`password`)와 연결된 소셜 계정"),
938
+ consents: customerConsentsSchema.describe("약관·개인정보·마케팅 동의의 현재 상태. 이력은 `GET /v1/customers/{customerId}/consents`"),
860
939
  addresses: z.array(customerAddressSchema),
861
940
  stats: customerStatsSchema.extend({
862
941
  canceledOrReturnedCount: z.number().int().describe("취소된 주문 상품 + 완료된 반품 수"),
@@ -1425,6 +1504,42 @@ const paymentRecordStatusSchema = z.enum([
1425
1504
  "FAILED"
1426
1505
  ]);
1427
1506
  const paymentAttemptStatusSchema = z.string().describe("시도 상태 — ready·approving·approved·unknown·declined·prepare_failed·canceled·failed·abandoned·voided. 새 값이 추가될 수 있으니 모르는 값은 '진행 중이 아닌 시도'로 다룬다");
1507
+ /**
1508
+ * 현금영수증 상태 — 값이 추가될 수 있어 응답은 문자열로 받는다. 화면 분기는 이 목록으로 하고 모르는 값은 처리 중으로 다룬다.
1509
+ * - `REQUESTED`: 구매자가 신청했고 결제 승인 전이다
1510
+ * - `PENDING`: 발급·취소를 기다린다(승인 직후, 또는 일시 오류로 자동 재시도 대기)
1511
+ * - `ISSUED`: 발급됐다. 부분 환불이 있었으면 그만큼 취소(또는 재발급)된 금액이다
1512
+ * - `CANCELED`: 전액 환불로 취소됐다
1513
+ * - `FAILED`: 발급·취소가 실패했고 자동 재시도가 끝났다. `failCode`·`failMessage`를 보고 재발급한다
1514
+ * - `SKIPPED`: 발급 대상이 아니다(승인된 결제수단이 계좌이체·가상계좌가 아님)
1515
+ */
1516
+ const CASH_RECEIPT_STATUS_VALUES = [
1517
+ "REQUESTED",
1518
+ "PENDING",
1519
+ "ISSUED",
1520
+ "CANCELED",
1521
+ "FAILED",
1522
+ "SKIPPED"
1523
+ ];
1524
+ const cashReceiptSchema = z.object({
1525
+ cashReceiptId: z.string(),
1526
+ status: z.string().describe("상태. `REQUESTED` 결제 전 · `PENDING` 발급 대기 · `ISSUED` 발급 · `CANCELED` 취소 · `FAILED` 실패 · `SKIPPED` 발급 대상 아님. 값이 추가될 수 있다"),
1527
+ type: z.string().describe("용도. `INCOME_DEDUCTION` 소득공제 · `EXPENSE_PROOF` 지출증빙"),
1528
+ identityType: z.string().describe("식별 번호 종류. `PHONE` 휴대폰 · `CARD` 현금영수증카드 · `BUSINESS` 사업자등록번호"),
1529
+ identityNumberMasked: z.string().describe("가린 식별 번호 (예: 010****5678). 원문은 조회할 수 없다"),
1530
+ amount: z.number().int().describe("발급해야 할 금액 = 결제 금액 − 환불 완료 금액. 승인 전에는 결제 금액"),
1531
+ issuedAmount: z.number().int().describe("PG에 현재 발급돼 있는 금액. 취소됐거나 발급 전이면 0"),
1532
+ provider: pgProviderSchema.nullable().describe("발급 PG — 결제를 승인한 PG. 승인 전에는 null"),
1533
+ issueNumber: z.string().nullable().describe("현금영수증 승인 번호"),
1534
+ receiptUrl: z.string().nullable().describe("현금영수증 확인 주소"),
1535
+ issuedAt: z.string().nullable().describe("최근 발급 시각 (ISO 8601)"),
1536
+ canceledAt: z.string().nullable().describe("최근 취소 시각 (ISO 8601)"),
1537
+ failCode: z.string().nullable().describe("마지막 실패 코드. 성공하면 비운다"),
1538
+ failMessage: z.string().nullable().describe("마지막 실패 사유"),
1539
+ reissuable: z.boolean().describe("콘솔 재발급으로 다시 시도할 수 있는가 (`FAILED`·`PENDING`)"),
1540
+ requestedAt: z.string().describe("신청 시각 (ISO 8601)"),
1541
+ updatedAt: z.string().describe("마지막 변경 시각 (ISO 8601)")
1542
+ });
1428
1543
  const paymentRecordSchema = z.object({
1429
1544
  paymentId: z.string().describe("결제 건 id. 상세 조회 키"),
1430
1545
  orderId: z.string().nullable().describe("승인돼 만들어진 주문. 승인 전에 끝난 결제는 null"),
@@ -1444,7 +1559,8 @@ const paymentRecordSchema = z.object({
1444
1559
  failCode: z.string().nullable().describe("실패 코드. 실패로 끝난 결제만"),
1445
1560
  failMessage: z.string().nullable().describe("실패 사유"),
1446
1561
  paidAt: z.string().nullable().describe("승인 시각 (ISO 8601)"),
1447
- createdAt: z.string().describe("결제 시작 시각 (ISO 8601)")
1562
+ createdAt: z.string().describe("결제 시작 시각 (ISO 8601)"),
1563
+ cashReceiptStatus: z.string().nullable().optional().default(null).describe("현금영수증 상태(`cashReceiptSchema.status`와 같은 값). 신청이 없으면 null. 서버는 항상 싣는다")
1448
1564
  });
1449
1565
  const paymentRecordPageSchema = pageSchema(paymentRecordSchema);
1450
1566
  const paymentAttemptRecordSchema = z.object({
@@ -1478,7 +1594,8 @@ const paymentRecordDetailSchema = paymentRecordSchema.extend({
1478
1594
  refundableAmount: z.number().int().describe("환불 가능 잔액 = 승인 금액 - 환불 완료 금액"),
1479
1595
  merchantId: z.string().nullable().describe("가맹점 식별자 지문 (토스 clientKey, 포트원 Store ID). 비밀 값은 없다"),
1480
1596
  attempts: z.array(paymentAttemptRecordSchema).describe("시도 이력. 순번 오름차순"),
1481
- refunds: z.array(paymentRefundRecordSchema).describe("환불 이력. 실패한 요청도 남는다")
1597
+ refunds: z.array(paymentRefundRecordSchema).describe("환불 이력. 실패한 요청도 남는다"),
1598
+ cashReceipt: cashReceiptSchema.nullable().optional().default(null).describe("현금영수증. 구매자가 신청하지 않았으면 null. 서버는 항상 싣는다")
1482
1599
  });
1483
1600
  /** `GET /v1/payments` 쿼리 — OpenAPI 문서용 형태(값은 쿼리 문자열) */
1484
1601
  const paymentListQuerySchema = z.object({
@@ -1526,10 +1643,12 @@ const orderItemSchema = z.object({
1526
1643
  optionId: z.string().nullable(),
1527
1644
  optionName: z.string().nullable(),
1528
1645
  sellerCode: z.string().nullable(),
1529
- quantity: z.number().int(),
1646
+ quantity: z.number().int().describe("주문 수량"),
1647
+ canceledQuantity: z.number().int().describe("취소·반품으로 환불된 누적 수량. 수량 단위 부분 취소가 없었으면 0이다"),
1648
+ activeQuantity: z.number().int().describe("살아 있는 수량 (`quantity` − `canceledQuantity`). 0이면 주문상품이 전량 취소됐다"),
1530
1649
  unitPrice: z.number().int(),
1531
1650
  discountAmount: z.number().int(),
1532
- totalPrice: z.number().int(),
1651
+ totalPrice: z.number().int().describe("주문상품 금액 (할인 반영 단가 × 주문 수량)"),
1533
1652
  hold: z.object({
1534
1653
  reason: z.string(),
1535
1654
  expectedDispatchDate: z.string()
@@ -1565,7 +1684,8 @@ const orderSchema = z.object({
1565
1684
  deliveryFee: z.number().int(),
1566
1685
  discountAmount: z.number().int(),
1567
1686
  totalAmount: z.number().int(),
1568
- paidAt: z.string().nullable()
1687
+ paidAt: z.string().nullable(),
1688
+ cashReceipt: cashReceiptSchema.nullable().optional().default(null).describe("현금영수증. 구매자가 계좌이체·가상계좌 결제에서 신청했을 때만 있고 그 밖에는 null. 서버는 항상 싣는다")
1569
1689
  }),
1570
1690
  testPayment: z.boolean().describe("테스트 결제(샌드박스 결제) 주문인가. true면 실제로 돈이 오가지 않았어요. 상품을 발송하지 않도록 주의하세요"),
1571
1691
  items: z.array(orderItemSchema)
@@ -1612,7 +1732,14 @@ const dispatchRequestSchema = z.object({
1612
1732
  deliveryMethod: deliveryMethodSchema,
1613
1733
  carrierCode: z.string().optional(),
1614
1734
  trackingNumber: z.string().optional(),
1615
- dispatchedAt: z.string().optional()
1735
+ /**
1736
+ * 발송 시각. ISO 8601이고 시간대(`Z` 또는 `+09:00`)를 포함해야 한다. 생략하면 현재 시각이다.
1737
+ *
1738
+ * 전에는 `z.string()`이라 아무 문자열이나 그대로 저장됐다. 이 값은 배송 정보 jsonb에 들어가
1739
+ * 자동 배송완료·추적 동기화 잡의 대상 선별 조건이 된다 — 잘못된 한 행이 스토어 구분 없이 도는
1740
+ * 잡 전체를 멈출 수 있어 입력에서 좁혔다. 검증을 통과한 값도 저장 전에 UTC ISO로 정규화한다.
1741
+ */
1742
+ dispatchedAt: z.iso.datetime({ offset: true }).optional()
1616
1743
  }).refine((value) => value.deliveryMethod === "DIRECT" || value.deliveryMethod === "NOTHING" ? true : !!value.carrierCode && !!value.trackingNumber, { message: "COURIER/QUICK 발송은 carrierCode와 trackingNumber가 필수입니다" });
1617
1744
  const deliveryInfoSchema = z.object({
1618
1745
  orderItemId: z.string(),
@@ -1629,12 +1756,26 @@ const deliveryInfoSchema = z.object({
1629
1756
  dispatchedAt: z.string(),
1630
1757
  deliveredAt: z.string().nullable()
1631
1758
  });
1759
+ /**
1760
+ * 택배 추적 단계. 응답에서는 문자열로 받는다(값이 추가될 수 있다) — 현재 값은 `ACCEPTED`(접수)·`IN_TRANSIT`(이동 중)·
1761
+ * `OUT_FOR_DELIVERY`(배송 출발)·`DELIVERED`(배송완료)다.
1762
+ */
1763
+ const trackingStageResponseSchema = z.string();
1632
1764
  /** `GET /v1/order-items/{orderItemId}/delivery` 응답 — 배송 정보 + 배송 추적 이벤트 */
1633
- const deliveryStatusSchema = deliveryInfoSchema.extend({ trackingEvents: z.array(z.object({
1634
- status: z.string(),
1635
- location: z.string(),
1636
- occurredAt: z.string()
1637
- })).optional() });
1765
+ const deliveryStatusSchema = deliveryInfoSchema.extend({
1766
+ trackingEvents: z.array(z.object({
1767
+ status: z.string().describe("택배사가 알려 준 처리 내용(예: 집화처리, 배송출발)"),
1768
+ location: z.string().describe("처리 장소. 모르면 빈 문자열"),
1769
+ occurredAt: z.string(),
1770
+ stage: trackingStageResponseSchema.nullable().optional().default(null).describe("이 이벤트의 추적 단계. 배달 실패처럼 단계로 나눌 수 없는 이벤트는 null. 서버는 항상 싣는다. 현재 값은 `ACCEPTED`·`IN_TRANSIT`·`OUT_FOR_DELIVERY`·`DELIVERED`이고 값이 추가될 수 있어 문자열로 받는다")
1771
+ })).optional().describe("배송 추적 이력. 시각 오름차순(오래된 것부터)"),
1772
+ tracking: z.object({
1773
+ result: z.string().nullable().describe("마지막 추적 조회 결과. 조회에 성공한 적이 없으면 null. 현재 값은 `FOUND`(조회됨)·`NOT_FOUND`(택배사 전산에 송장 없음)·`UNSUPPORTED_CARRIER`(추적을 지원하지 않는 택배사)이고 값이 추가될 수 있어 문자열로 받는다"),
1774
+ stage: trackingStageResponseSchema.nullable().describe("현재 추적 단계. 조회되지 않았으면 null. 현재 값은 `ACCEPTED`·`IN_TRANSIT`·`OUT_FOR_DELIVERY`·`DELIVERED`이고 값이 추가될 수 있어 문자열로 받는다"),
1775
+ checkedAt: z.string().nullable().describe("택배사에서 추적 정보를 마지막으로 받아 온 시각(ISO 8601)"),
1776
+ carrierDeliveredAt: z.string().nullable().describe("택배사가 알려 준 배송완료 시각(ISO 8601). 추적으로 배송완료가 반영되면 주문상품의 `deliveredAt`은 반영 시각이라 이 값과 다를 수 있다")
1777
+ }).nullable().optional().default(null).describe("택배 추적 요약. 택배 발송이 아니거나 추적을 쓰지 않으면 null. 추적이 배송완료면 주문상품이 자동으로 배송완료된다. 서버는 항상 싣는다")
1778
+ });
1638
1779
  /** `PUT /v1/order-items/{orderItemId}/dispatch/tracking` 응답 — 바뀐 송장 */
1639
1780
  const trackingUpdateResultSchema = z.object({
1640
1781
  orderItemId: z.string(),
@@ -1663,12 +1804,17 @@ const cancelBySellerRequestSchema = z.object({
1663
1804
  "DELIVERY_IMPOSSIBLE",
1664
1805
  "ETC"
1665
1806
  ]),
1666
- detail: z.string().max(500).optional()
1807
+ detail: z.string().max(500).optional(),
1808
+ quantity: z.number().int().min(1).optional().describe("취소할 수량. 생략하면 남은 수량 전체다. 남은 수량보다 크면 400 `INVALID_QUANTITY`. 일부만 취소하면 주문상품은 상태를 유지한 채 `canceledQuantity`가 늘고, 전 수량이 소진되면 `CANCELED`가 된다")
1667
1809
  });
1668
1810
  /** `POST /v1/order-items/{orderItemId}/cancel` 응답 — 셀러 직권취소 환불 결과 */
1669
1811
  const cancelBySellerResultSchema = z.object({
1670
1812
  orderItemId: z.string(),
1671
- refundAmount: z.number().int(),
1813
+ refundAmount: z.number().int().describe("실제 환불한 금액 — 수량 비율 상품 금액 + 배송비 환불 − 조건부 무료배송 미달 차감"),
1814
+ quantity: z.number().int().describe("취소한 수량"),
1815
+ canceledQuantity: z.number().int().describe("취소 후 누적 취소 수량"),
1816
+ deliveryFeeRefundAmount: z.number().int().describe("환불에 포함된 배송비. 배송 묶음(같은 상품)이 모두 취소됐고 환불 정책이 켜져 있을 때만 0보다 크다"),
1817
+ deliveryFeeChargeAmount: z.number().int().describe("조건부 무료배송 기준 미달로 환불액에서 뺀 배송비. 정책이 꺼져 있으면 항상 0이다"),
1672
1818
  refundedAt: z.string()
1673
1819
  });
1674
1820
  /** `PUT /v1/order-items/{orderItemId}/memo` 응답 — 빈 문자열로 저장하면 메모가 지워져 `null`이다 */
@@ -1693,6 +1839,116 @@ const purchaseDecisionExtendResultSchema = z.object({
1693
1839
  */
1694
1840
  const addressChangeRequestListSchema = z.array(z.record(z.string(), z.unknown())).describe("배송지 변경 요청 목록 (현재 항상 빈 배열)");
1695
1841
  //#endregion
1842
+ //#region src/schemas/refund-policy.ts
1843
+ /**
1844
+ * 환불 정책 (이슈 #25) — 취소·반품·교환에서 **배송비를 누가 부담하는지**를 정한다.
1845
+ *
1846
+ * 두 층이다. 스토어 설정(`PATCH /v1/store`의 `refundPolicy`)이 기본값이고, 상품별 정책
1847
+ * (`POST/PUT /v1/products`의 `refundPolicy`)이 그 상품에만 덮어쓴다. 상품이 정하지 않은 항목은
1848
+ * 스토어 기본을 따르고, 스토어도 정하지 않았으면 `REFUND_POLICY_DEFAULTS`다.
1849
+ *
1850
+ * 한 주문에 정책이 섞이면 **상품별로 계산한다**. 배송비 묶음도 상품 단위다(주문서·장바구니의
1851
+ * 배송비 합산과 같은 규칙: 같은 상품의 주문상품이 한 묶음이고 묶음마다 배송비가 붙는다).
1852
+ */
1853
+ const descriptions$1 = {
1854
+ preShipmentDeliveryFeeRefund: "발송 전 취소로 배송 묶음(같은 상품의 주문상품)이 모두 취소되면 그 묶음의 배송비를 환불한다. false면 배송비를 환불하지 않는다. 기본 true",
1855
+ returnDeliveryFee: "단순 변심(구매자 귀책) 반품의 반품 배송비. 원 단위. 환불액에서 뺀다. 기본 0(무료). 상품별 정책에서 이 값을 주면 상품의 `returnFee`도 같은 값으로 맞춘다",
1856
+ exchangeDeliveryFee: "단순 변심(구매자 귀책) 교환의 교환 배송비. 원 단위. 구매자에게 청구할 금액을 안내값으로 싣는다. 기본 0(무료). 상품별 정책에서 이 값을 주면 상품의 `exchangeFee`도 같은 값으로 맞춘다",
1857
+ sellerFaultFreeShipping: "판매자 귀책(셀러 직권취소, 불량·오배송 반품)이면 배송비를 셀러가 부담한다 — 배송비를 환불하고 반품비를 받지 않는다. false면 구매자 귀책과 같은 규칙을 쓴다. 기본 true",
1858
+ conditionalFreeShortfallCharge: "조건부 무료배송(`CONDITIONAL_FREE`) 상품에서 일부 취소로 남은 금액이 무료 기준액에 미달하면 그 배송비를 환불액에서 뺀다. false면 추가 청구 없이 셀러가 부담한다. 기본 false"
1859
+ };
1860
+ /** 유효 환불 정책 — 스토어 기본 + 상품별 덮어쓰기를 합친 결과 */
1861
+ const refundPolicySchema = z.object({
1862
+ preShipmentDeliveryFeeRefund: z.boolean().describe(descriptions$1.preShipmentDeliveryFeeRefund),
1863
+ returnDeliveryFee: z.number().int().min(0).describe(descriptions$1.returnDeliveryFee),
1864
+ exchangeDeliveryFee: z.number().int().min(0).describe(descriptions$1.exchangeDeliveryFee),
1865
+ sellerFaultFreeShipping: z.boolean().describe(descriptions$1.sellerFaultFreeShipping),
1866
+ conditionalFreeShortfallCharge: z.boolean().describe(descriptions$1.conditionalFreeShortfallCharge)
1867
+ });
1868
+ /**
1869
+ * 스토어가 정하지 않았을 때 서버가 쓰는 값.
1870
+ *
1871
+ * `as const`가 아니라 `RefundPolicy`로 타입을 좁힌다 — 리터럴 타입이면 `Partial<typeof …>`로 일부만 바꿀 때
1872
+ * `true` 자리에 `false`를 넣을 수 없다.
1873
+ */
1874
+ const REFUND_POLICY_DEFAULTS = {
1875
+ preShipmentDeliveryFeeRefund: true,
1876
+ returnDeliveryFee: 0,
1877
+ exchangeDeliveryFee: 0,
1878
+ sellerFaultFreeShipping: true,
1879
+ conditionalFreeShortfallCharge: false
1880
+ };
1881
+ /**
1882
+ * 부분 정책 — 스토어 설정 요청과 상품별 덮어쓰기가 쓴다. 준 항목만 바뀐다.
1883
+ *
1884
+ * `strictObject`라 모르는 키는 400이다. 오타(`returnFee` 등)를 조용히 무시하면 셀러는 저장됐다고
1885
+ * 믿는데 정책은 기본값으로 남는다.
1886
+ */
1887
+ const refundPolicyPatchSchema = z.strictObject({
1888
+ preShipmentDeliveryFeeRefund: z.boolean().optional().describe(descriptions$1.preShipmentDeliveryFeeRefund),
1889
+ returnDeliveryFee: z.number().int().min(0).optional().describe(descriptions$1.returnDeliveryFee),
1890
+ exchangeDeliveryFee: z.number().int().min(0).optional().describe(descriptions$1.exchangeDeliveryFee),
1891
+ sellerFaultFreeShipping: z.boolean().optional().describe(descriptions$1.sellerFaultFreeShipping),
1892
+ conditionalFreeShortfallCharge: z.boolean().optional().describe(descriptions$1.conditionalFreeShortfallCharge)
1893
+ });
1894
+ /**
1895
+ * 유효 정책 해석 — 상품별 덮어쓰기 → 스토어 기본 → `REFUND_POLICY_DEFAULTS` 순이다.
1896
+ *
1897
+ * 반품비·교환비는 레거시 필드(`products.returnFee`·`exchangeFee`)와 **연결**한다. 상품이
1898
+ * `refundPolicy.returnDeliveryFee`를 정하면 그 값이 원천이고(서버가 `returnFee`도 같은 값으로 맞춘다),
1899
+ * 정하지 않았으면 레거시 값이 0보다 클 때 그 값을 쓴다. 0이면 스토어 기본을 따른다 —
1900
+ * 정책이 생기기 전에 만든 상품(반품비 0)이 스토어 기본을 물려받게 하는 규칙이다.
1901
+ */
1902
+ function resolveRefundPolicy(storePolicy, product) {
1903
+ return {
1904
+ ...REFUND_POLICY_DEFAULTS,
1905
+ ...dropUndefined(storePolicy),
1906
+ ...productRefundPolicyOverride(product) ?? {}
1907
+ };
1908
+ }
1909
+ /**
1910
+ * 상품의 **덮어쓰기 자체** — 레거시 반품비·교환비를 같은 형태로 승격한 값이다. 덮어쓰기가 없으면 null.
1911
+ *
1912
+ * `resolveRefundPolicy`가 합친 결과로는 "상품이 정한 값"과 "스토어 기본을 물려받은 값"을 가를 수 없다.
1913
+ * 관리 API 응답의 `refundPolicy`와 콘솔 폼의 덮어쓰기 스위치가 이 함수를 원천으로 쓴다 — 가르지 못하면
1914
+ * 스토어 기본을 쓰는 상품을 저장할 때 그 순간의 기본값이 상품에 박히거나, 레거시 값이 조용히 사라진다.
1915
+ */
1916
+ function productRefundPolicyOverride(product) {
1917
+ const override = dropUndefined(product?.refundPolicy);
1918
+ const legacy = {};
1919
+ if (override.returnDeliveryFee === void 0 && (product?.returnFee ?? 0) > 0) legacy.returnDeliveryFee = product?.returnFee ?? 0;
1920
+ if (override.exchangeDeliveryFee === void 0 && (product?.exchangeFee ?? 0) > 0) legacy.exchangeDeliveryFee = product?.exchangeFee ?? 0;
1921
+ const merged = {
1922
+ ...legacy,
1923
+ ...override
1924
+ };
1925
+ if (product?.refundPolicy == null && Object.keys(merged).length === 0) return null;
1926
+ return merged;
1927
+ }
1928
+ /** `undefined` 값을 빼고 얕게 복사한다 — 전개 순서로 덮어쓸 때 `undefined`가 기본값을 지우지 않게 */
1929
+ function dropUndefined(patch) {
1930
+ if (!patch) return {};
1931
+ return Object.fromEntries(Object.entries(patch).filter(([, value]) => value !== void 0));
1932
+ }
1933
+ /**
1934
+ * 수량 단위 부분 취소의 환불 기준액 — **누적 내림의 차분**이다.
1935
+ *
1936
+ * 원 단위 반올림 규칙: `floor(주문상품 금액 × 누적 수량 ÷ 전체 수량)`을 두 번 구해 그 차를 쓴다.
1937
+ * 부분 취소를 몇 번 나눠도 합이 주문상품 금액과 정확히 같아서 1원이 남거나 넘치지 않는다.
1938
+ * 할인은 이미 단가에 들어 있어(주문 단위 쿠폰이 없다) 별도 안분이 없다.
1939
+ *
1940
+ * @param totalPrice 주문상품 금액 (할인 반영 단가 × 전체 수량)
1941
+ * @param quantity 주문상품 전체 수량
1942
+ * @param settledQuantity 이미 취소·반품된 누적 수량
1943
+ * @param cancelQuantity 이번에 취소·반품할 수량
1944
+ */
1945
+ function proratedRefundBase(totalPrice, quantity, settledQuantity, cancelQuantity) {
1946
+ if (quantity <= 0 || cancelQuantity <= 0) return 0;
1947
+ const before = Math.floor(totalPrice * settledQuantity / quantity);
1948
+ const after = Math.floor(totalPrice * (settledQuantity + cancelQuantity) / quantity);
1949
+ return Math.max(0, after - before);
1950
+ }
1951
+ //#endregion
1696
1952
  //#region src/schemas/upload.ts
1697
1953
  /**
1698
1954
  * 업로드 용도. 허용 확장자와 크기 상한이 용도별로 다르다.
@@ -1858,6 +2114,7 @@ const productSchema = productSummarySchema.extend({
1858
2114
  conditionalFreeAmount: z.number().int().nullable().optional(),
1859
2115
  returnFee: z.number().int().optional(),
1860
2116
  exchangeFee: z.number().int().optional(),
2117
+ refundPolicy: refundPolicyPatchSchema.nullable().optional().describe("상품별 환불 정책 덮어쓰기. null이면 스토어 기본 환불 정책을 쓴다"),
1861
2118
  shippingAddressId: z.string().nullable().optional(),
1862
2119
  returnAddressId: z.string().nullable().optional(),
1863
2120
  createdAt: z.string().optional()
@@ -1980,6 +2237,21 @@ const variantStockSchema = z.object({
1980
2237
  variantId: z.string(),
1981
2238
  stockQuantity: z.number().int()
1982
2239
  });
2240
+ /**
2241
+ * 출고지·반품지 지정 문구 — POST·PUT·PATCH가 같은 규칙을 쓰므로 설명도 한 곳이다 (이슈 #30).
2242
+ *
2243
+ * 세 값이 뜻이 다르다. **생략**은 「정하지 않음」이라 서버가 그 시점 스토어 기본 주소를 박아 저장하고,
2244
+ * **null**은 「스토어 기본을 따른다」는 명시적 비우기다. 주소록 항목은 상품이 참조 중이면 지울 수 없어
2245
+ * (`409 ADDRESS_IN_USE`) 참조를 푸는 수단이 필요한데, 생략을 기본값으로 되채우면 기본 주소는 영원히
2246
+ * 풀리지 않는다 — null이 그 경로다. 배송 묶음 판정은 null을 그 시점 스토어 기본 출고지로 정규화하므로
2247
+ * 두 표현이 배송비를 갈라놓지 않는다.
2248
+ */
2249
+ const ADDRESS_DESCRIPTIONS = {
2250
+ shipping: "출고지 주소 id(`GET /v1/seller-addresses?type=SHIPPING`). 생략하면 스토어 기본 출고지를 저장하고, `null`이면 비워 둔다(스토어 기본 출고지를 따른다). 이 스토어의 `SHIPPING` 주소가 아니면 거부한다",
2251
+ return: "반품지 주소 id(`GET /v1/seller-addresses?type=RETURN`). 생략하면 스토어 기본 반품지를 저장하고, `null`이면 비워 둔다(스토어 기본 반품지를 따른다). 이 스토어의 `RETURN` 주소가 아니면 거부한다",
2252
+ patchShipping: "출고지 주소 id. **생략은 바꾸지 않음**이고 `null`은 비우기다(스토어 기본 출고지를 따른다). 이 스토어의 `SHIPPING` 주소가 아니면 거부한다",
2253
+ patchReturn: "반품지 주소 id. **생략은 바꾸지 않음**이고 `null`은 비우기다(스토어 기본 반품지를 따른다). 이 스토어의 `RETURN` 주소가 아니면 거부한다"
2254
+ };
1983
2255
  const productCreateRequestSchema = z.object({
1984
2256
  name: z.string().min(1).max(100),
1985
2257
  categoryId: z.string().min(1, "카테고리를 선택해주세요"),
@@ -1997,18 +2269,21 @@ const productCreateRequestSchema = z.object({
1997
2269
  value: z.string()
1998
2270
  })).optional(),
1999
2271
  deliveryType: deliveryTypeSchema.default("PAID").describe("배송비 유형. FREE 무료, PAID 유료, CONDITIONAL_FREE 조건부 무료"),
2000
- deliveryFee: z.number().int().default(0).describe("배송비. 원 단위"),
2272
+ deliveryFee: z.number().int().min(0).optional().describe("배송비. 원 단위. **생략하면 스토어 기본 배송비**(`GET /v1/store`의 `shippingPolicy.baseDeliveryFee`)를 쓴다. `deliveryType`이 FREE면 0으로 저장한다"),
2001
2273
  conditionalFreeAmount: z.number().int().optional().describe("CONDITIONAL_FREE일 때 무료 배송이 되는 주문 금액. 원 단위"),
2002
- returnFee: z.number().int().default(0).describe("반품 배송비. 원 단위"),
2003
- exchangeFee: z.number().int().default(0).describe("교환 배송비. 원 단위"),
2004
- shippingAddressId: z.string().optional(),
2005
- returnAddressId: z.string().optional()
2274
+ returnFee: z.number().int().default(0).describe("반품 배송비. 원 단위. `refundPolicy.returnDeliveryFee`와 같은 값이다"),
2275
+ exchangeFee: z.number().int().default(0).describe("교환 배송비. 원 단위. `refundPolicy.exchangeDeliveryFee`와 같은 값이다"),
2276
+ refundPolicy: refundPolicyPatchSchema.nullish().describe("상품별 환불 정책 — 준 항목만 스토어 기본을 덮어쓴다. `null`이나 생략은 스토어 기본을 그대로 쓴다는 뜻이다. `returnDeliveryFee`·`exchangeDeliveryFee`를 주면 `returnFee`·`exchangeFee`도 같은 값으로 맞춘다"),
2277
+ shippingAddressId: z.string().nullable().optional().describe(ADDRESS_DESCRIPTIONS.shipping),
2278
+ returnAddressId: z.string().nullable().optional().describe(ADDRESS_DESCRIPTIONS.return)
2006
2279
  }).extend(optionModelRequestSchema.shape);
2007
2280
  const productPatchRequestSchema = z.object({
2008
2281
  name: z.string().max(100).optional(),
2009
2282
  salePrice: z.number().int().multipleOf(10).optional(),
2010
2283
  status: productStatusSchema.optional(),
2011
- deliveryFee: z.number().int().optional()
2284
+ deliveryFee: z.number().int().optional(),
2285
+ shippingAddressId: z.string().nullable().optional().describe(ADDRESS_DESCRIPTIONS.patchShipping),
2286
+ returnAddressId: z.string().nullable().optional().describe(ADDRESS_DESCRIPTIONS.patchReturn)
2012
2287
  }).refine((value) => Object.values(value).some((v) => v !== void 0), { message: "최소 1개 필드가 필요합니다" });
2013
2288
  const productSortSchema = z.enum([
2014
2289
  "createdAt.desc",
@@ -2041,6 +2316,441 @@ const discountRequestSchema = z.object({
2041
2316
  endAt: z.string().optional()
2042
2317
  });
2043
2318
  //#endregion
2319
+ //#region src/schemas/shipping-policy.ts
2320
+ /**
2321
+ * 배송비 규칙 (이슈 #29) — 배송비를 **몇 번, 얼마나** 부과하는지를 정한다.
2322
+ *
2323
+ * 세 축이다.
2324
+ * 1. **배송 묶음**: 같은 출고지 + 같은 배송 정책인 주문상품은 한 묶음이고 배송비가 한 번만 붙는다
2325
+ * (`deliveryBundleKeyOf`). 이슈 #25의 「묶음 = 같은 상품」 규칙을 일반화한 것이다 — 키를 상품 id에서
2326
+ * 정책 지문으로 바꿨고, 주문상품이 주문 시점 키를 스냅샷해(`order_items.delivery_bundle_key`)
2327
+ * 환불 정산(`refund-settlement.ts`)과 주문 스냅샷(`orders.delivery_fee_settlements`)이 같은 키를 쓴다.
2328
+ * 2. **무료 기준**: 상품별 조건부 무료(`CONDITIONAL_FREE`, 묶음 소계 기준)와 스토어 전체 무료 기준
2329
+ * (`freeThreshold`, 주문 상품 금액 합 기준)이 있다. 둘 중 하나라도 충족하면 그 배송비는 0이다.
2330
+ * 3. **지역 추가 배송비**: 우편번호가 제주·도서산간이면 **묶음마다** 추가된다. 묶음이 곧 한 번의 발송이라
2331
+ * 두 출고지에서 각각 보내면 추가 운임도 두 번 든다.
2332
+ *
2333
+ * 지역 추가 배송비는 **무료배송과 무관하게 부과한다.** 무료 기준은 셀러가 기본 운임을 부담하겠다는
2334
+ * 뜻이지, 도서 지역 추가 운임까지 부담하겠다는 뜻은 아니다(국내 택배사 공통 관행).
2335
+ *
2336
+ * 계산은 `quoteDelivery` 하나다. 장바구니·주문서·결제 시작·환불 정산이 모두 이 함수를 쓴다 —
2337
+ * 네 곳이 각자 계산하면 화면 금액과 결제 금액이 갈라진다.
2338
+ */
2339
+ const descriptions = {
2340
+ baseDeliveryFee: "스토어 기본 배송비. 원 단위. 상품 등록·수정에서 `deliveryFee`를 생략하면 이 값을 쓴다. 기본 3000",
2341
+ freeThreshold: "스토어 전체 무료배송 기준액. 주문 상품 금액 합이 이 값 이상이면 모든 묶음의 배송비가 0이다. null이면 전체 무료배송을 쓰지 않는다(상품별 조건부 무료는 그대로 동작한다). 기본 null",
2342
+ jejuSurcharge: "제주 지역 추가 배송비. 원 단위. **배송 묶음마다** 더한다. 기본 0",
2343
+ isolatedSurcharge: "도서산간 지역 추가 배송비. 원 단위. **배송 묶음마다** 더한다. 기본 0"
2344
+ };
2345
+ /** 유효 배송비 정책 — 스토어가 정하지 않은 항목은 `SHIPPING_POLICY_DEFAULTS`로 채운 값이다 */
2346
+ const shippingPolicySchema = z.object({
2347
+ baseDeliveryFee: z.number().int().min(0).describe(descriptions.baseDeliveryFee),
2348
+ freeThreshold: z.number().int().min(1).nullable().describe(descriptions.freeThreshold),
2349
+ jejuSurcharge: z.number().int().min(0).describe(descriptions.jejuSurcharge),
2350
+ isolatedSurcharge: z.number().int().min(0).describe(descriptions.isolatedSurcharge)
2351
+ });
2352
+ /**
2353
+ * 스토어가 정하지 않았을 때 서버가 쓰는 값.
2354
+ *
2355
+ * `as const`가 아니라 `ShippingPolicy`로 좁힌다 — 리터럴 타입이면 `Partial<typeof …>`로 일부만 바꿀 때
2356
+ * `null` 자리에 숫자를 넣을 수 없다(`REFUND_POLICY_DEFAULTS`와 같은 이유).
2357
+ */
2358
+ const SHIPPING_POLICY_DEFAULTS = {
2359
+ baseDeliveryFee: 3e3,
2360
+ freeThreshold: null,
2361
+ jejuSurcharge: 0,
2362
+ isolatedSurcharge: 0
2363
+ };
2364
+ /**
2365
+ * 부분 정책 — `PATCH /v1/store`의 `shippingPolicy`가 쓴다. 준 항목만 바뀐다.
2366
+ *
2367
+ * `strictObject`라 모르는 키는 400이다. 오타를 조용히 무시하면 셀러는 저장됐다고 믿는데
2368
+ * 정책은 기본값으로 남는다(`refundPolicyPatchSchema`와 같은 이유).
2369
+ */
2370
+ const shippingPolicyPatchSchema = z.strictObject({
2371
+ baseDeliveryFee: z.number().int().min(0).optional().describe(descriptions.baseDeliveryFee),
2372
+ freeThreshold: z.number().int().min(1).nullable().optional().describe(descriptions.freeThreshold),
2373
+ jejuSurcharge: z.number().int().min(0).optional().describe(descriptions.jejuSurcharge),
2374
+ isolatedSurcharge: z.number().int().min(0).optional().describe(descriptions.isolatedSurcharge)
2375
+ });
2376
+ /** 유효 정책 해석 — 스토어가 정한 항목 + `SHIPPING_POLICY_DEFAULTS` */
2377
+ function resolveShippingPolicy(patch) {
2378
+ if (!patch) return { ...SHIPPING_POLICY_DEFAULTS };
2379
+ const given = Object.fromEntries(Object.entries(patch).filter(([, value]) => value !== void 0));
2380
+ return {
2381
+ ...SHIPPING_POLICY_DEFAULTS,
2382
+ ...given
2383
+ };
2384
+ }
2385
+ /** 지역 구분 — 우편번호로 판정한다 */
2386
+ const remoteAreaSchema = z.enum([
2387
+ "NONE",
2388
+ "JEJU",
2389
+ "ISOLATED"
2390
+ ]);
2391
+ /**
2392
+ * 제주·도서산간 우편번호 범위 — **코드 상수다.** 금액은 스토어가 정하고 범위는 플랫폼이 고정한다.
2393
+ *
2394
+ * 국내 택배사가 공표하는 제주·도서산간 할증 구간을 5자리 우편번호 범위로 옮긴 표다. 셀러마다
2395
+ * 범위가 다르면 같은 주소에 다른 금액이 나가고 CS가 셀러 수만큼 갈라진다. 스토어별 범위 편집은
2396
+ * 후속 과제이고, 지금은 이 표가 유일한 원천이다.
2397
+ *
2398
+ * ## 근거
2399
+ *
2400
+ * 원천은 **CJ대한통운이 공표한 「제주 및 도서산간 지역 추가 운송비」 표**(구간 시작·끝 우편번호와
2401
+ * 지역명이 붙은 그 표)다. 같은 표가 한진택배 공지와 쇼핑몰 솔루션 안내에도 같은 값으로 실린다.
2402
+ * 서로 다른 게시자의 전재본 네 곳이 인천·전남·경남 구간까지 글자 그대로 일치하는 것을 확인했다.
2403
+ *
2404
+ * - CJ대한통운 표 전재 (구간별 금액 포함): https://ichibanhouse.com/article/공지뉴스/1/16/
2405
+ * - 같은 표의 다른 전재본: https://peppytail.com/article/공지사항/1/4046/
2406
+ * - 시행 시점(2020-02-01)과 구간별 행정구역명을 함께 적은 전재본:
2407
+ * https://tosstoss.co.kr/bbs/content.php?co_id=pilotage
2408
+ * - 솔루션 안내 「제주도 및 도서산간 지역 기준표」:
2409
+ * https://help.campaignus.me/ko/articles/제주도-및-도서산간-지역-기준표-342d2e5e
2410
+ *
2411
+ * 라벨의 행정구역명은 우편번호 배정을 따로 확인해 고쳤다. 택배사 표의 지역명은 축약·오기가 있다.
2412
+ * 확인한 것만 남기고, 확인하지 못한 구간은 이 표에서 뺀다 — 판정하지 못하면 받지 않는 쪽이
2413
+ * `remoteAreaOf`의 규칙과 같다.
2414
+ *
2415
+ * ## 표에서 뺀 구간
2416
+ *
2417
+ * - **54000**: 택배사 표는 「경남 통영 섬지역3」으로 적었지만 이 번호는 **전북 군산시 옥도면**이다.
2418
+ * 게다가 공정거래위원회가 2025-10-07 발표에서 연륙교로 이어진 신시도·야미도·선유도와 비연륙
2419
+ * 도서(방축도·관리도)가 같은 우편번호를 쓰는 대표 사례로 이 지역을 들었다. 라벨도 부과 근거도
2420
+ * 확정하지 못해 뺀다.
2421
+ * - **강화군 본섬(23000~23003, 23011~23099)**: 택배사 표의 강화 할증 구간은 23004~23010
2422
+ * (교동면·삼산면·서도면)뿐이다. 강화대교·초지대교로 이어진 본섬은 도선료가 없어 어느 택배사도
2423
+ * 할증하지 않는다. 「누락」이 아니라 대상이 아니다.
2424
+ *
2425
+ * ## 한계
2426
+ *
2427
+ * 우편번호 단위 판정은 한 번호 안에 연륙도서와 비연륙도서가 섞인 구간에서 과대 부과가 된다.
2428
+ * 공정위가 2025-10-07에 온라인몰 13곳의 이 방식을 시정하게 한 지적이 그것이다
2429
+ * (https://eiec.kdi.re.kr/policy/materialView.do?num=272070). 아래 구간 중 33411(보령 오천면,
2430
+ * 원산도가 해저터널로 연결), 46768~46771(강서구 가덕도, 거가대교로 연결)이 그런 혼재 구간이다.
2431
+ * 도로명주소·건물관리번호 단위 판정은 후속 과제다.
2432
+ *
2433
+ * 범위는 **닫힌 구간**(`from` ≤ zip ≤ `to`)이고 겹치지 않는다. 앞에서부터 처음 맞는 항목을 쓴다.
2434
+ * 순서는 콘솔 「추가 배송비 지역」 표의 행 순서이기도 하다(`remoteAreaGroups`). 제주를 먼저 두고
2435
+ * 도서산간은 우편번호 오름차순으로 둔다.
2436
+ */
2437
+ const REMOTE_AREA_ZIP_RANGES = [
2438
+ {
2439
+ area: "JEJU",
2440
+ from: 63e3,
2441
+ to: 63644,
2442
+ label: "제주특별자치도"
2443
+ },
2444
+ {
2445
+ area: "ISOLATED",
2446
+ from: 22386,
2447
+ to: 22388,
2448
+ label: "인천 영종구 무의동"
2449
+ },
2450
+ {
2451
+ area: "ISOLATED",
2452
+ from: 23004,
2453
+ to: 23010,
2454
+ label: "인천 강화군 도서"
2455
+ },
2456
+ {
2457
+ area: "ISOLATED",
2458
+ from: 23100,
2459
+ to: 23116,
2460
+ label: "인천 옹진군 도서"
2461
+ },
2462
+ {
2463
+ area: "ISOLATED",
2464
+ from: 23124,
2465
+ to: 23136,
2466
+ label: "인천 옹진군 도서"
2467
+ },
2468
+ {
2469
+ area: "ISOLATED",
2470
+ from: 31708,
2471
+ to: 31708,
2472
+ label: "충남 당진시 도서"
2473
+ },
2474
+ {
2475
+ area: "ISOLATED",
2476
+ from: 32133,
2477
+ to: 32133,
2478
+ label: "충남 태안군 도서"
2479
+ },
2480
+ {
2481
+ area: "ISOLATED",
2482
+ from: 33411,
2483
+ to: 33411,
2484
+ label: "충남 보령시 도서"
2485
+ },
2486
+ {
2487
+ area: "ISOLATED",
2488
+ from: 40200,
2489
+ to: 40240,
2490
+ label: "경북 울릉군"
2491
+ },
2492
+ {
2493
+ area: "ISOLATED",
2494
+ from: 46768,
2495
+ to: 46771,
2496
+ label: "부산 강서구 도서"
2497
+ },
2498
+ {
2499
+ area: "ISOLATED",
2500
+ from: 52570,
2501
+ to: 52571,
2502
+ label: "경남 사천시 도서"
2503
+ },
2504
+ {
2505
+ area: "ISOLATED",
2506
+ from: 53031,
2507
+ to: 53033,
2508
+ label: "경남 통영시 도서"
2509
+ },
2510
+ {
2511
+ area: "ISOLATED",
2512
+ from: 53089,
2513
+ to: 53104,
2514
+ label: "경남 통영시 도서"
2515
+ },
2516
+ {
2517
+ area: "ISOLATED",
2518
+ from: 56347,
2519
+ to: 56349,
2520
+ label: "전북 부안군 위도면"
2521
+ },
2522
+ {
2523
+ area: "ISOLATED",
2524
+ from: 57068,
2525
+ to: 57069,
2526
+ label: "전남 영광군 낙월면"
2527
+ },
2528
+ {
2529
+ area: "ISOLATED",
2530
+ from: 58760,
2531
+ to: 58762,
2532
+ label: "전남 목포시 도서"
2533
+ },
2534
+ {
2535
+ area: "ISOLATED",
2536
+ from: 58800,
2537
+ to: 58810,
2538
+ label: "전남 신안군"
2539
+ },
2540
+ {
2541
+ area: "ISOLATED",
2542
+ from: 58816,
2543
+ to: 58818,
2544
+ label: "전남 신안군"
2545
+ },
2546
+ {
2547
+ area: "ISOLATED",
2548
+ from: 58826,
2549
+ to: 58826,
2550
+ label: "전남 신안군"
2551
+ },
2552
+ {
2553
+ area: "ISOLATED",
2554
+ from: 58828,
2555
+ to: 58866,
2556
+ label: "전남 신안군"
2557
+ },
2558
+ {
2559
+ area: "ISOLATED",
2560
+ from: 58953,
2561
+ to: 58958,
2562
+ label: "전남 진도군 조도면"
2563
+ },
2564
+ {
2565
+ area: "ISOLATED",
2566
+ from: 59102,
2567
+ to: 59103,
2568
+ label: "전남 완도군 도서"
2569
+ },
2570
+ {
2571
+ area: "ISOLATED",
2572
+ from: 59106,
2573
+ to: 59106,
2574
+ label: "전남 완도군 도서"
2575
+ },
2576
+ {
2577
+ area: "ISOLATED",
2578
+ from: 59127,
2579
+ to: 59127,
2580
+ label: "전남 완도군 도서"
2581
+ },
2582
+ {
2583
+ area: "ISOLATED",
2584
+ from: 59129,
2585
+ to: 59129,
2586
+ label: "전남 완도군 도서"
2587
+ },
2588
+ {
2589
+ area: "ISOLATED",
2590
+ from: 59137,
2591
+ to: 59166,
2592
+ label: "전남 완도군 도서"
2593
+ },
2594
+ {
2595
+ area: "ISOLATED",
2596
+ from: 59421,
2597
+ to: 59421,
2598
+ label: "전남 보성군 장도"
2599
+ },
2600
+ {
2601
+ area: "ISOLATED",
2602
+ from: 59531,
2603
+ to: 59531,
2604
+ label: "전남 고흥군 도서"
2605
+ },
2606
+ {
2607
+ area: "ISOLATED",
2608
+ from: 59551,
2609
+ to: 59551,
2610
+ label: "전남 고흥군 도서"
2611
+ },
2612
+ {
2613
+ area: "ISOLATED",
2614
+ from: 59563,
2615
+ to: 59563,
2616
+ label: "전남 고흥군 도서"
2617
+ },
2618
+ {
2619
+ area: "ISOLATED",
2620
+ from: 59568,
2621
+ to: 59568,
2622
+ label: "전남 고흥군 도서"
2623
+ },
2624
+ {
2625
+ area: "ISOLATED",
2626
+ from: 59650,
2627
+ to: 59650,
2628
+ label: "전남 여수시 도서"
2629
+ },
2630
+ {
2631
+ area: "ISOLATED",
2632
+ from: 59766,
2633
+ to: 59766,
2634
+ label: "전남 여수시 도서"
2635
+ },
2636
+ {
2637
+ area: "ISOLATED",
2638
+ from: 59781,
2639
+ to: 59790,
2640
+ label: "전남 여수시 도서"
2641
+ }
2642
+ ];
2643
+ /**
2644
+ * 우편번호 → 지역 구분. 5자리 숫자가 아니면 `NONE`이다.
2645
+ *
2646
+ * 형식 검증은 배송지 스키마(`/^[0-9]{5}$/`)가 하고, 여기서는 **판정하지 못하면 추가 배송비를 받지 않는다**.
2647
+ * 반대로 두면 우편번호를 잘못 넣은 주문에 근거 없는 금액이 붙는다.
2648
+ */
2649
+ function remoteAreaOf(zipCode) {
2650
+ if (!zipCode || !/^[0-9]{5}$/.test(zipCode)) return "NONE";
2651
+ const zip = Number.parseInt(zipCode, 10);
2652
+ for (const range of REMOTE_AREA_ZIP_RANGES) if (zip >= range.from && zip <= range.to) return range.area;
2653
+ return "NONE";
2654
+ }
2655
+ /** 지역 구분의 안내 이름 — 판정된 범위의 `label`이다. 판정되지 않으면 null */
2656
+ function remoteAreaLabelOf(zipCode) {
2657
+ if (!zipCode || !/^[0-9]{5}$/.test(zipCode)) return null;
2658
+ const zip = Number.parseInt(zipCode, 10);
2659
+ return REMOTE_AREA_ZIP_RANGES.find((r) => zip >= r.from && zip <= r.to)?.label ?? null;
2660
+ }
2661
+ /** 묶음 하나에 붙는 지역 추가 배송비 */
2662
+ function remoteSurchargeOf(policy, area) {
2663
+ if (area === "JEJU") return policy.jejuSurcharge;
2664
+ if (area === "ISOLATED") return policy.isolatedSurcharge;
2665
+ return 0;
2666
+ }
2667
+ /**
2668
+ * 배송 묶음 키 — **같은 출고지 + 같은 배송 정책이면 같은 키다.**
2669
+ *
2670
+ * 키는 주문 시점에 주문상품 행에 스냅샷된다(`order_items.delivery_bundle_key`). 셀러가 주문 뒤
2671
+ * 출고지나 배송비를 바꿔도 그 주문의 묶음은 움직이지 않는다 — 움직이면 이미 환불한 묶음의 키가
2672
+ * 달라져 `orders.delivery_fee_settlements`의 이중 환불 방지 표시가 무력해진다.
2673
+ *
2674
+ * `-`는 「값 없음」이다. 사람이 읽을 수 있게 남기고, 값 자체에 `:`가 들어갈 수 없는 형식만 쓴다
2675
+ * (주소 id는 `adr_…`, 나머지는 열거형·정수).
2676
+ */
2677
+ function deliveryBundleKeyOf(product) {
2678
+ const origin = product.shippingAddressId ?? "-";
2679
+ const threshold = product.deliveryType === "CONDITIONAL_FREE" ? product.conditionalFreeAmount ?? 0 : "-";
2680
+ const fee = product.deliveryType === "FREE" ? 0 : product.deliveryFee;
2681
+ return `b:${origin}:${product.deliveryType}:${fee}:${threshold}`;
2682
+ }
2683
+ /**
2684
+ * 기본 배송비를 면제한 근거 — 면제하지 않았으면 null이다.
2685
+ *
2686
+ * 환불 정산이 근거를 구분해야 한다. 조건부 무료 기준 미달 차감은 **상품별 조건부 무료(`CONDITIONAL`)로
2687
+ * 면제된 묶음만** 대상이다. 스토어 전체 무료 기준(`STORE_THRESHOLD`)이나 무료배송 상품(`FREE`)은 셀러가
2688
+ * 배송비를 받지 않기로 한 것이므로, 일부 취소로 기준이 깨져도 구매자에게 배송비를 물리지 않는다.
2689
+ *
2690
+ * 세 값이 겹치면 `FREE` → `STORE_THRESHOLD` → `CONDITIONAL` 순으로 센 근거를 남긴다.
2691
+ */
2692
+ const deliveryFeeWaiverSchema = z.enum([
2693
+ "FREE",
2694
+ "STORE_THRESHOLD",
2695
+ "CONDITIONAL"
2696
+ ]);
2697
+ /**
2698
+ * 배송비 계산 — **순수 함수다.** 장바구니 요약, 주문서 금액, 결제 시작의 최종 확정,
2699
+ * 주문 배송비 스냅샷이 모두 이 한 곳을 쓴다.
2700
+ *
2701
+ * 순서가 규칙이다. 묶음별 조건부 무료를 먼저 보고, 그다음 스토어 전체 무료 기준으로 기본 배송비를
2702
+ * 한꺼번에 0으로 만들고, 마지막에 지역 추가 배송비를 묶음마다 더한다. 추가 배송비는 무료 기준에
2703
+ * 걸리지 않는다.
2704
+ */
2705
+ function quoteDelivery(input) {
2706
+ const { policy } = input;
2707
+ const area = remoteAreaOf(input.zipCode);
2708
+ const surcharge = remoteSurchargeOf(policy, area);
2709
+ const groups = /* @__PURE__ */ new Map();
2710
+ let productAmount = 0;
2711
+ for (const line of input.lines) {
2712
+ const product = input.productOf(line.productId);
2713
+ if (!product) continue;
2714
+ productAmount += line.amount;
2715
+ const key = deliveryBundleKeyOf(product);
2716
+ const group = groups.get(key) ?? {
2717
+ product,
2718
+ ids: /* @__PURE__ */ new Set(),
2719
+ sum: 0
2720
+ };
2721
+ group.ids.add(line.productId);
2722
+ group.sum += line.amount;
2723
+ groups.set(key, group);
2724
+ }
2725
+ const freeByThreshold = policy.freeThreshold !== null && productAmount >= policy.freeThreshold && groups.size > 0;
2726
+ const bundles = [];
2727
+ for (const [bundleKey, group] of groups) {
2728
+ const { product } = group;
2729
+ const freeThreshold = product.deliveryType === "CONDITIONAL_FREE" && product.conditionalFreeAmount !== null ? product.conditionalFreeAmount ?? 0 : null;
2730
+ const listFee = product.deliveryType === "FREE" ? 0 : product.deliveryFee;
2731
+ const waivedBy = product.deliveryType === "FREE" ? "FREE" : freeByThreshold ? "STORE_THRESHOLD" : freeThreshold !== null && group.sum >= freeThreshold ? "CONDITIONAL" : null;
2732
+ bundles.push({
2733
+ bundleKey,
2734
+ productIds: [...group.ids],
2735
+ fee: waivedBy === null ? listFee : 0,
2736
+ waivedBy,
2737
+ listFee,
2738
+ freeThreshold,
2739
+ remoteSurcharge: surcharge
2740
+ });
2741
+ }
2742
+ const baseFee = bundles.reduce((sum, b) => sum + b.fee, 0);
2743
+ const remoteSurcharge = bundles.reduce((sum, b) => sum + b.remoteSurcharge, 0);
2744
+ return {
2745
+ bundles,
2746
+ baseFee,
2747
+ remoteSurcharge,
2748
+ deliveryFee: baseFee + remoteSurcharge,
2749
+ remoteArea: area,
2750
+ freeByThreshold
2751
+ };
2752
+ }
2753
+ //#endregion
2044
2754
  //#region src/schemas/store.ts
2045
2755
  /**
2046
2756
  * 스토어 코드 규칙 — 스토어프론트 API가 테넌트를 가르는 값이다(`X-Store-Code` 헤더).
@@ -2116,6 +2826,16 @@ const analyticsOriginSchema = z.string().trim().max(255).refine(isWebOrigin, "ht
2116
2826
  const MAX_ANALYTICS_ALLOWED_ORIGINS = 20;
2117
2827
  /** 자동 배송완료·자동 구매확정 대기일 상한 */
2118
2828
  const MAX_AUTO_TRANSITION_DAYS = 30;
2829
+ /**
2830
+ * 약관 버전 — 스토어가 정하는 문자열이다(`1`·`2`·`2026-09-26`·`v1.2` 등). 형식은 강제하지 않고 값이 다르면 다른 버전이다.
2831
+ * 올리면 그 약관에 동의한 구매자의 `reconsentRequired`가 켜지고, 재동의가 이력에 남는다. 되돌리면 재동의 요구도 풀린다.
2832
+ */
2833
+ const policyVersionSchema = z.string().trim().min(1).max(20).describe("약관 버전 문자열(1~20자). 예: `1`, `2026-09-26`");
2834
+ /**
2835
+ * 약관 문서 주소 — 구매자에게 보여 주는 약관 본문의 주소다. 동의 시점 값이 이력 행에 복사되므로
2836
+ * 나중에 주소를 바꿔도 "무엇에 동의했는지"가 남는다. `null`로 지울 수 있다.
2837
+ */
2838
+ const policyDocumentUrlSchema = z.url().max(500).refine((value) => value.startsWith("https://"), "https 주소만 쓸 수 있어요").nullable();
2119
2839
  const autoDeliverDaysDescription = "발송 후 자동 배송완료까지의 일수(0~30, 기본 7). 발송(`DISPATCHED`)·배송중(`DELIVERING`) 주문상품을 이 기간이 지나면 배송완료(`DELIVERED`)로 바꾼다. `0`이면 자동 배송완료를 하지 않는다";
2120
2840
  const autoConfirmDaysDescription = "배송완료 후 자동 구매확정까지의 일수(0~30, 기본 7). 배송완료 시점에 구매확정 예정일(`autoDecisionDate`)을 이 값으로 잡고, 그 날짜가 지나면 구매확정(`PURCHASE_DECIDED`)으로 바꾼다. 진행 중인 클레임이 있으면 건너뛴다. `0`이면 자동 구매확정을 하지 않는다";
2121
2841
  /** `PATCH /v1/store` 본문 — 바꿀 필드만 보낸다 */
@@ -2128,7 +2848,13 @@ const updateStoreRequestSchema = z.object({
2128
2848
  storefrontOrigins: z.array(analyticsOriginSchema).max(20).optional().describe("스토어프론트 도메인(최대 20개) — 소셜 로그인 뒤 돌아갈 주소와 방문 분석 수집을 함께 제한한다. `analyticsAllowedOrigins`와 같은 목록이다(둘 다 주면 이 값이 이긴다)"),
2129
2849
  guestCheckoutEnabled: z.boolean().optional().describe("비회원 주문 허용. false(회원만)면 로그인하지 않은 구매자의 주문서 생성·결제 요청이 403 `GUEST_CHECKOUT_DISABLED`다. 장바구니 담기와 이미 받은 비회원 주문의 조회·취소는 막지 않는다"),
2130
2850
  autoDeliverDays: z.number().int().min(0).max(30).optional().describe(autoDeliverDaysDescription),
2131
- autoConfirmDays: z.number().int().min(0).max(30).optional().describe(autoConfirmDaysDescription)
2851
+ autoConfirmDays: z.number().int().min(0).max(30).optional().describe(autoConfirmDaysDescription),
2852
+ refundPolicy: refundPolicyPatchSchema.optional().describe("스토어 기본 환불 정책 — 준 항목만 바꾼다. 상품별 정책(`products.refundPolicy`)이 없는 상품에 적용된다"),
2853
+ shippingPolicy: shippingPolicyPatchSchema.optional().describe("스토어 배송비 규칙 — 준 항목만 바꾼다. 기본 배송비, 전체 무료배송 기준액, 제주·도서산간 추가 배송비다. 바꾼 값은 **새 주문서부터** 적용된다(이미 만든 주문서·주문의 배송비는 스냅샷이 원천이다)"),
2854
+ termsVersion: policyVersionSchema.optional().describe("이용약관 버전. 올리면 이 약관에 동의한 구매자에게 재동의를 요구한다(`GET /storefront/v1/me`의 `reconsentRequired`)"),
2855
+ privacyVersion: policyVersionSchema.optional().describe("개인정보 수집이용 동의 버전. 올리면 구매자에게 재동의를 요구한다"),
2856
+ termsUrl: policyDocumentUrlSchema.optional().describe("이용약관 문서 주소(https). 동의 시점의 값이 동의 이력에 복사된다. `null`을 주면 지운다"),
2857
+ privacyUrl: policyDocumentUrlSchema.optional().describe("개인정보 수집이용 동의 문서 주소(https). `null`을 주면 지운다")
2132
2858
  }).refine((value) => Object.values(value).some((v) => v !== void 0), { message: "최소 1개 필드가 필요합니다" });
2133
2859
  const storeSchema = z.object({
2134
2860
  storeId: z.string(),
@@ -2148,6 +2874,14 @@ const storeSchema = z.object({
2148
2874
  guestCheckoutEnabled: z.boolean().describe("비회원 주문 허용. false면 회원만 주문할 수 있다"),
2149
2875
  autoDeliverDays: z.number().int().describe(autoDeliverDaysDescription),
2150
2876
  autoConfirmDays: z.number().int().describe(autoConfirmDaysDescription),
2877
+ refundPolicy: refundPolicySchema.describe("스토어 기본 환불 정책. 스토어가 정하지 않은 항목은 플랫폼 기본값으로 채워 항상 전부 싣는다"),
2878
+ shippingPolicy: shippingPolicySchema.describe("스토어 배송비 규칙. 스토어가 정하지 않은 항목은 플랫폼 기본값으로 채워 항상 전부 싣는다"),
2879
+ termsVersion: z.string().describe("현재 이용약관 버전. 가입·재동의가 이 값으로 이력에 남는다"),
2880
+ privacyVersion: z.string().describe("현재 개인정보 수집이용 동의 버전"),
2881
+ termsVersionUpdatedAt: z.string().nullable().describe("이용약관 버전을 마지막으로 바꾼 시각. 바꾼 적이 없으면 null"),
2882
+ privacyVersionUpdatedAt: z.string().nullable(),
2883
+ termsUrl: z.string().nullable().describe("이용약관 문서 주소. 정하지 않았으면 null이고 동의 이력에도 주소가 남지 않는다"),
2884
+ privacyUrl: z.string().nullable().describe("개인정보 수집이용 동의 문서 주소"),
2151
2885
  memberCount: z.number().int(),
2152
2886
  createdAt: z.string()
2153
2887
  });
@@ -2410,7 +3144,12 @@ function createStoreClient(options) {
2410
3144
  * 그 밖의 값은 400 `INVALID_PARAMETER`. 행마다 `testPayment`가 붙는다.
2411
3145
  */
2412
3146
  list: (params) => storeHttp.request("GET", "/orders", pageSchema(orderSummarySchema), { query: params }),
2413
- get: (orderId) => storeHttp.request("GET", path`/orders/${orderId}`, orderSchema)
3147
+ get: (orderId) => storeHttp.request("GET", path`/orders/${orderId}`, orderSchema),
3148
+ /**
3149
+ * `POST /v1/orders/{orderId}/cash-receipt/reissue` — 실패했거나 재시도 대기 중인 현금영수증을 지금 다시 발급·취소한다.
3150
+ * 콘솔 OWNER·ADMIN 전용(PAT·M2M 403 `INSUFFICIENT_ROLE`). 404 `CASH_RECEIPT_NOT_FOUND`, 409 `CASH_RECEIPT_NOT_REISSUABLE`
3151
+ */
3152
+ reissueCashReceipt: (orderId) => storeHttp.request("POST", path`/orders/${orderId}/cash-receipt/reissue`, cashReceiptSchema)
2414
3153
  },
2415
3154
  orderItems: {
2416
3155
  /** `GET /v1/order-items`. `testPayment` 필터는 `orders.list`와 같다 */
@@ -2500,6 +3239,8 @@ function createStoreClient(options) {
2500
3239
  list: (params) => storeHttp.request("GET", "/customers", pageSchema(customerSummarySchema), { query: params }),
2501
3240
  get: (customerId) => storeHttp.request("GET", path`/customers/${customerId}`, customerDetailSchema),
2502
3241
  orders: (customerId, params) => storeHttp.request("GET", path`/customers/${customerId}/orders`, pageSchema(customerOrderSchema), { query: params }),
3242
+ /** 동의 이력 — 최근 순. 가입·재동의·마케팅 변경이 한 줄씩 남는다 */
3243
+ consents: (customerId, params) => storeHttp.request("GET", path`/customers/${customerId}/consents`, pageSchema(customerConsentSchema), { query: params }),
2503
3244
  withdraw: (customerId, body) => storeHttp.requestVoid("POST", path`/customers/${customerId}/withdraw`, { body: withdrawCustomerRequestSchema.parse(body) })
2504
3245
  },
2505
3246
  customerInquiries: {
@@ -2875,6 +3616,11 @@ const claimWebhookEventDataSchema = z.object({
2875
3616
  orderItemId: z.string().describe("클레임 대상 주문 상품 ID"),
2876
3617
  claimType: claimTypeSchema.describe("클레임 유형"),
2877
3618
  status: claimStatusSchema.describe("이벤트 발생 시점의 클레임 상태"),
3619
+ quantity: z.number().int().optional().default(0).describe("클레임 대상 수량. 이 필드가 없는 이전 이벤트는 0으로 읽는다"),
3620
+ refundAmount: z.number().int().nullable().optional().default(null).describe("환불액 (원). 배송비 환불을 포함하고 배송비 차감·추가 차감을 뺀 실제 금액이다. 환불이 확정되지 않은 접수 이벤트는 예상 환불액이고, 교환처럼 환불이 없으면 null이다"),
3621
+ deliveryFeeRefundAmount: z.number().int().optional().default(0).describe("환불액에 포함된 배송비 (원). 이 필드가 없는 이전 이벤트는 0으로 읽는다"),
3622
+ deliveryFeeChargeAmount: z.number().int().optional().default(0).describe("환불액에서 뺀 배송비 (원) — 반품·교환 배송비와 조건부 무료배송 미달 차감의 합. 셀러가 승인에 넣은 추가 차감은 `deductAmount`로 따로 온다. 이 필드가 없는 이전 이벤트는 0으로 읽는다"),
3623
+ deductAmount: z.number().int().optional().default(0).describe("환불액에서 뺀 추가 차감 (원) — 셀러가 클레임 승인에 넣은 금액이다(상품 훼손 등). 배송비 차감과 별개이므로 `deliveryFeeChargeAmount`와 더해서 뺀다. 접수 이벤트와 이 필드가 없는 이전 이벤트는 0이다"),
2878
3624
  testPayment: z.boolean().optional().default(false).describe("테스트 결제(샌드박스 결제) 주문의 클레임인가. 이 필드가 없는 이전 이벤트는 false로 읽는다")
2879
3625
  });
2880
3626
  const inquiryWebhookEventDataSchema = z.object({
@@ -3085,4 +3831,4 @@ function createStoreSession(options) {
3085
3831
  };
3086
3832
  }
3087
3833
  //#endregion
3088
- export { ApiError, MAX_ANALYTICS_ALLOWED_ORIGINS, MAX_AUTO_TRANSITION_DAYS, MAX_OPTION_GROUPS, MAX_PRODUCT_IMAGES, MAX_VALUES_PER_GROUP, MAX_VARIANTS, PAYMENT_ROUTING_EXCLUSION_CODES, PORTONE_CHANNEL_NAME_MAX_LENGTH, PORTONE_MAX_CHANNELS, PORTONE_MIN_CHANNELS, RESERVED_STORE_CODES, STORE_CODE_PATTERN, STORE_ROLE_SCOPES, STORE_SCOPE_RESOURCES, STORE_SESSION_KEYS, TOSS_WIDGET_CLIENT_KEY_PATTERN, TOSS_WIDGET_KEY_MESSAGE, TOSS_WIDGET_SECRET_KEY_PATTERN, UPLOAD_PURPOSE_LIMITS, VARIANT_STOCK_EXCLUSIVE_MESSAGE, WEBHOOK_DEFAULT_TOLERANCE_SEC, WEBHOOK_DELIVERY_ID_HEADER, WEBHOOK_EVENT_ID_HEADER, WEBHOOK_EVENT_TYPE_HEADER, WEBHOOK_HEADERS, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_USER_AGENT, addressChangeRequestListSchema, addressTypeSchema, analyticsChannelSchema, analyticsCheckoutOutcomeSchema, analyticsCheckoutSchema, analyticsDeviceRowSchema, analyticsDevicesSchema, analyticsFunnelStepSchema, analyticsOriginSchema, analyticsOverviewSchema, analyticsPagesQuerySchema, analyticsPagesSchema, analyticsProductRowSchema, analyticsProductSortSchema, analyticsProductsQuerySchema, analyticsProductsSchema, analyticsRangeQuerySchema, analyticsRealtimeSchema, analyticsSearchQuerySchema, analyticsSearchSchema, analyticsSourceGroupBySchema, analyticsSourceRowSchema, analyticsSourcesQuerySchema, analyticsSourcesSchema, analyticsTotalsSchema, apiClientListQuerySchema, apiClientSchema, apiClientSecretRotatedSchema, apiClientStatusSchema, apiClientTypeSchema, apiLogErrorSchema, apiLogListQuerySchema, apiLogPageSchema, apiLogPrincipalSchema, apiLogPrincipalTypeSchema, apiLogSchema, apiLogStatusFilterSchema, apiLogSummarySchema, apiTokenCreateRequestSchema, apiTokenCreatedSchema, apiTokenSchema, approveClaimResultSchema, buildAuthorizeUrl, buildQuery, bulkResultSchema, cancelBySellerRequestSchema, cancelBySellerResultSchema, carrierSchema, categoryAttributeSchema, categoryCreateRequestSchema, categoryDeleteQuerySchema, categoryDeleteStrategySchema, categoryListQuerySchema, categoryPatchRequestSchema, categoryProductSortSchema, categorySchema, categorySearchResultSchema, categoryTreeNodeSchema, categoryWithPathSchema, changeTypeSchema, claimDetailSchema, claimHoldResultSchema, claimListQuerySchema, claimReasonSchema, claimSchema, claimStatusSchema, claimTypeSchema, claimWebhookEventDataSchema, claimWebhookEventSchema, clientCredentialsTokenResponseSchema, createPkcePair, createStoreClient, createStoreSession, customerAddressSchema, customerDetailSchema, customerInquiryCategorySchema, customerInquiryListQuerySchema, customerListQuerySchema, customerOrderSchema, customerSortSchema, customerStatsSchema, customerSummarySchema, customerWithdrawalBlockedDetailsSchema, deliveryInfoSchema, deliveryMethodSchema, deliveryStatusSchema, deliveryTypeSchema, discountRequestSchema, discountSchema, dispatchRequestSchema, easyPayProviderResponseSchema, easyPayProviderSchema, eventTypeSchema, exchangeAuthorizationCode, exchangeStoreTokenViaOAuth, holdOrderItemRequestSchema, inquiryListQuerySchema, inquirySchema, inquiryWebhookEventDataSchema, inquiryWebhookEventSchema, invitationListQuerySchema, invitationSchema, isReservedStoreCode, isStoreScope, isWebOrigin, liveGateConditionCodeSchema, liveGateConditionSchema, liveGateErrorDetailsSchema, liveGateSchema, loginProviderSchema, loginProviderStatusSchema, loginProviderTestSchema, loginSettingsSchema, maskedSecretSchema, myAccountSchema, myStoreSchema, oauthClientRegistrationRequestSchema, oauthClientRegistrationResponseSchema, oauthTokenResponseSchema, optionGroupRequestSchema, optionGroupSchema, optionModelRequestSchema, optionModelSchema, optionValueRequestSchema, optionValueSchema, orderItemListQuerySchema, orderItemMemoResultSchema, orderItemPageSchema, orderItemSchema, orderItemStatusSchema, orderListQuerySchema, orderSchema, orderSummaryPageSchema, orderSummarySchema, orderWebhookEventDataSchema, orderWebhookEventSchema, pageInfoSchema, pageQueryShape, pageSchema, paymentAttemptRecordSchema, paymentAttemptStatusSchema, paymentCredentialSetSchema, paymentListQuerySchema, paymentProviderSchema, paymentProviderVerificationSchema, paymentRecordDetailSchema, paymentRecordPageSchema, paymentRecordSchema, paymentRecordStatusSchema, paymentRefundRecordSchema, paymentSettingsSchema, pgEnvironmentSchema, pgPaymentMethodSchema, pgProviderSchema, pgVerificationResultSchema, portOneChannelSchema, portOneCredentialsInputSchema, productCreateRequestSchema, productImageRejection, productImageUrlSchema, productListQuerySchema, productOutOfStockWebhookEventDataSchema, productOutOfStockWebhookEventSchema, productPatchRequestSchema, productSchema, productSortSchema, productStatusSchema, productSummarySchema, productVariantRequestSchema, productVariantSchema, purchaseDecisionExtendResultSchema, purchaseDecisionRequestResultSchema, refreshUserToken, rejectClaimRequestSchema, reviewListQuerySchema, reviewReportResultSchema, reviewSchema, reviewSortSchema, reviewWebhookEventDataSchema, reviewWebhookEventSchema, salesReportGroupBySchema, salesReportQuerySchema, salesReportRowSchema, salesReportSchema, salesTotalsSchema, saveLoginProviderRequestSchema, scopeAccessSchema, sellerAddressListQuerySchema, sellerAddressRequestSchema, sellerAddressSchema, sellerCustomerInquirySchema, settlementDetailQuerySchema, settlementDetailSchema, settlementListQuerySchema, settlementStatusSchema, settlementSummarySchema, settlementWithDetailsSchema, shippingAddressSchema, storeCodeSchema, storeMemberRoleUpdatedSchema, storeMemberSchema, storeRoleSchema, storeSchema, storeScopeResourceSchema, storeTokenResponseSchema, tossPaymentsCredentialsInputSchema, trackingUpdateResultSchema, updateLoginProviderRequestSchema, updateLoginSettingsRequestSchema, updatePaymentProviderRequestSchema, updatePaymentSettingsRequestSchema, updateStoreRequestSchema, uploadExtensionOf, uploadPurposeSchema, uploadRejection, uploadRequestSchema, uploadTicketSchema, upsertPortOneProviderRequestSchema, upsertTossPaymentsProviderRequestSchema, userTokenResponseSchema, variantStockRequestSchema, variantStockSchema, verifyPaymentProviderRequestSchema, webhookCreatedSchema, webhookDeliveryErrorCodeSchema, webhookDeliveryListQuerySchema, webhookDeliverySchema, webhookDeliveryStatusSchema, webhookEventSchema, webhookRedeliverFailedResultSchema, webhookRequestSchema, webhookSchema, webhookSecretRotatedSchema, webhookStatusSchema, webhookTestRequestSchema, webhookTestResultSchema, withdrawCustomerRequestSchema };
3834
+ export { ApiError, CASH_RECEIPT_STATUS_VALUES, MAX_ANALYTICS_ALLOWED_ORIGINS, MAX_AUTO_TRANSITION_DAYS, MAX_OPTION_GROUPS, MAX_PRODUCT_IMAGES, MAX_VALUES_PER_GROUP, MAX_VARIANTS, PAYMENT_ROUTING_EXCLUSION_CODES, PORTONE_CHANNEL_NAME_MAX_LENGTH, PORTONE_MAX_CHANNELS, PORTONE_MIN_CHANNELS, REFUND_POLICY_DEFAULTS, REMOTE_AREA_ZIP_RANGES, RESERVED_STORE_CODES, SHIPPING_POLICY_DEFAULTS, STORE_CODE_PATTERN, STORE_ROLE_SCOPES, STORE_SCOPE_RESOURCES, STORE_SESSION_KEYS, TOSS_WIDGET_CLIENT_KEY_PATTERN, TOSS_WIDGET_KEY_MESSAGE, TOSS_WIDGET_SECRET_KEY_PATTERN, UPLOAD_PURPOSE_LIMITS, VARIANT_STOCK_EXCLUSIVE_MESSAGE, WEBHOOK_DEFAULT_TOLERANCE_SEC, WEBHOOK_DELIVERY_ID_HEADER, WEBHOOK_EVENT_ID_HEADER, WEBHOOK_EVENT_TYPE_HEADER, WEBHOOK_HEADERS, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_USER_AGENT, addressChangeRequestListSchema, addressTypeSchema, analyticsChannelSchema, analyticsCheckoutOutcomeSchema, analyticsCheckoutSchema, analyticsDeviceRowSchema, analyticsDevicesSchema, analyticsFunnelStepSchema, analyticsOriginSchema, analyticsOverviewSchema, analyticsPagesQuerySchema, analyticsPagesSchema, analyticsProductRowSchema, analyticsProductSortSchema, analyticsProductsQuerySchema, analyticsProductsSchema, analyticsRangeQuerySchema, analyticsRealtimeSchema, analyticsSearchQuerySchema, analyticsSearchSchema, analyticsSourceGroupBySchema, analyticsSourceRowSchema, analyticsSourcesQuerySchema, analyticsSourcesSchema, analyticsTotalsSchema, apiClientListQuerySchema, apiClientSchema, apiClientSecretRotatedSchema, apiClientStatusSchema, apiClientTypeSchema, apiLogErrorSchema, apiLogListQuerySchema, apiLogPageSchema, apiLogPrincipalSchema, apiLogPrincipalTypeSchema, apiLogSchema, apiLogStatusFilterSchema, apiLogSummarySchema, apiTokenCreateRequestSchema, apiTokenCreatedSchema, apiTokenSchema, approveClaimResultSchema, buildAuthorizeUrl, buildQuery, bulkResultSchema, cancelBySellerRequestSchema, cancelBySellerResultSchema, carrierSchema, cashReceiptSchema, categoryAttributeSchema, categoryCreateRequestSchema, categoryDeleteQuerySchema, categoryDeleteStrategySchema, categoryListQuerySchema, categoryPatchRequestSchema, categoryProductSortSchema, categorySchema, categorySearchResultSchema, categoryTreeNodeSchema, categoryWithPathSchema, changeTypeSchema, claimDetailSchema, claimHoldResultSchema, claimListQuerySchema, claimReasonSchema, claimReturnAddressSchema, claimSchema, claimStatusSchema, claimTypeSchema, claimWebhookEventDataSchema, claimWebhookEventSchema, clientCredentialsTokenResponseSchema, consentActionSchema, consentKindSchema, consentSourceSchema, createPkcePair, createStoreClient, createStoreSession, customerAddressSchema, customerConsentListQuerySchema, customerConsentSchema, customerConsentStateSchema, customerConsentsSchema, customerDetailSchema, customerInquiryCategorySchema, customerInquiryListQuerySchema, customerListQuerySchema, customerOrderSchema, customerSortSchema, customerStatsSchema, customerSummarySchema, customerWithdrawalBlockedDetailsSchema, deliveryBundleKeyOf, deliveryFeeWaiverSchema, deliveryInfoSchema, deliveryMethodSchema, deliveryStatusSchema, deliveryTypeSchema, discountRequestSchema, discountSchema, dispatchRequestSchema, easyPayProviderResponseSchema, easyPayProviderSchema, eventTypeSchema, exchangeAuthorizationCode, exchangeStoreTokenViaOAuth, holdOrderItemRequestSchema, inquiryListQuerySchema, inquirySchema, inquiryWebhookEventDataSchema, inquiryWebhookEventSchema, invitationListQuerySchema, invitationSchema, isReservedStoreCode, isStoreScope, isWebOrigin, liveGateConditionCodeSchema, liveGateConditionSchema, liveGateErrorDetailsSchema, liveGateSchema, loginProviderSchema, loginProviderStatusSchema, loginProviderTestSchema, loginSettingsSchema, maskedSecretSchema, myAccountSchema, myStoreSchema, oauthClientRegistrationRequestSchema, oauthClientRegistrationResponseSchema, oauthTokenResponseSchema, optionGroupRequestSchema, optionGroupSchema, optionModelRequestSchema, optionModelSchema, optionValueRequestSchema, optionValueSchema, orderItemListQuerySchema, orderItemMemoResultSchema, orderItemPageSchema, orderItemSchema, orderItemStatusSchema, orderListQuerySchema, orderSchema, orderSummaryPageSchema, orderSummarySchema, orderWebhookEventDataSchema, orderWebhookEventSchema, pageInfoSchema, pageQueryShape, pageSchema, paymentAttemptRecordSchema, paymentAttemptStatusSchema, paymentCredentialSetSchema, paymentListQuerySchema, paymentProviderSchema, paymentProviderVerificationSchema, paymentRecordDetailSchema, paymentRecordPageSchema, paymentRecordSchema, paymentRecordStatusSchema, paymentRefundRecordSchema, paymentSettingsSchema, pgEnvironmentSchema, pgPaymentMethodSchema, pgProviderSchema, pgVerificationResultSchema, policyDocumentUrlSchema, policyVersionSchema, portOneChannelSchema, portOneCredentialsInputSchema, productCreateRequestSchema, productImageRejection, productImageUrlSchema, productListQuerySchema, productOutOfStockWebhookEventDataSchema, productOutOfStockWebhookEventSchema, productPatchRequestSchema, productRefundPolicyOverride, productSchema, productSortSchema, productStatusSchema, productSummarySchema, productVariantRequestSchema, productVariantSchema, proratedRefundBase, purchaseDecisionExtendResultSchema, purchaseDecisionRequestResultSchema, quoteDelivery, refreshUserToken, refundPolicyPatchSchema, refundPolicySchema, rejectClaimRequestSchema, remoteAreaLabelOf, remoteAreaOf, remoteAreaSchema, remoteSurchargeOf, resolveRefundPolicy, resolveShippingPolicy, reviewListQuerySchema, reviewReportResultSchema, reviewSchema, reviewSortSchema, reviewWebhookEventDataSchema, reviewWebhookEventSchema, salesReportGroupBySchema, salesReportQuerySchema, salesReportRowSchema, salesReportSchema, salesTotalsSchema, saveLoginProviderRequestSchema, scopeAccessSchema, sellerAddressListQuerySchema, sellerAddressRequestSchema, sellerAddressSchema, sellerCustomerInquirySchema, settlementDetailQuerySchema, settlementDetailSchema, settlementListQuerySchema, settlementStatusSchema, settlementSummarySchema, settlementWithDetailsSchema, shippingAddressSchema, shippingPolicyPatchSchema, shippingPolicySchema, storeCodeSchema, storeMemberRoleUpdatedSchema, storeMemberSchema, storeRoleSchema, storeSchema, storeScopeResourceSchema, storeTokenResponseSchema, tossPaymentsCredentialsInputSchema, trackingStageResponseSchema, trackingUpdateResultSchema, updateLoginProviderRequestSchema, updateLoginSettingsRequestSchema, updatePaymentProviderRequestSchema, updatePaymentSettingsRequestSchema, updateStoreRequestSchema, uploadExtensionOf, uploadPurposeSchema, uploadRejection, uploadRequestSchema, uploadTicketSchema, upsertPortOneProviderRequestSchema, upsertTossPaymentsProviderRequestSchema, userTokenResponseSchema, variantStockRequestSchema, variantStockSchema, verifyPaymentProviderRequestSchema, webhookCreatedSchema, webhookDeliveryErrorCodeSchema, webhookDeliveryListQuerySchema, webhookDeliverySchema, webhookDeliveryStatusSchema, webhookEventSchema, webhookRedeliverFailedResultSchema, webhookRequestSchema, webhookSchema, webhookSecretRotatedSchema, webhookStatusSchema, webhookTestRequestSchema, webhookTestResultSchema, withdrawCustomerRequestSchema };