@sayren/store-sdk 0.10.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sayren/store-sdk",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "main": "./dist/index.mjs",
5
5
  "types": "./dist/index.d.mts",
6
6
  "exports": {
package/src/client.ts CHANGED
@@ -128,6 +128,7 @@ import {
128
128
  purchaseDecisionExtendResultSchema,
129
129
  } from "./schemas/order";
130
130
  import {
131
+ cashReceiptSchema,
131
132
  type PaymentListParams,
132
133
  type PgEnvironment,
133
134
  type PgProvider,
@@ -530,6 +531,12 @@ export function createStoreClient(options: StoreClientOptions) {
530
531
  query: params as unknown as Query,
531
532
  }),
532
533
  get: (orderId: string) => storeHttp.request("GET", path`/orders/${orderId}`, orderSchema),
534
+ /**
535
+ * `POST /v1/orders/{orderId}/cash-receipt/reissue` — 실패했거나 재시도 대기 중인 현금영수증을 지금 다시 발급·취소한다.
536
+ * 콘솔 OWNER·ADMIN 전용(PAT·M2M 403 `INSUFFICIENT_ROLE`). 404 `CASH_RECEIPT_NOT_FOUND`, 409 `CASH_RECEIPT_NOT_REISSUABLE`
537
+ */
538
+ reissueCashReceipt: (orderId: string) =>
539
+ storeHttp.request("POST", path`/orders/${orderId}/cash-receipt/reissue`, cashReceiptSchema),
533
540
  },
534
541
 
535
542
  orderItems: {
package/src/index.ts CHANGED
@@ -26,6 +26,7 @@ export * from "./schemas/payment";
26
26
  export * from "./schemas/product";
27
27
  export * from "./schemas/refund-policy";
28
28
  export * from "./schemas/scope";
29
+ export * from "./schemas/shipping-policy";
29
30
  export * from "./schemas/store";
30
31
  export * from "./schemas/upload";
31
32
  export * from "./schemas/webhook-event";
@@ -30,6 +30,24 @@ export const claimReasonSchema = z.enum([
30
30
 
31
31
  export type ClaimReason = z.infer<typeof claimReasonSchema>;
32
32
 
33
+ /**
34
+ * 반품지 (이슈 #30) — 반품·교환 수거가 안내하는 주소다. 스토어 주소록의 기본 반품지이거나,
35
+ * 상품이 반품지를 따로 지정했으면 그 주소다.
36
+ *
37
+ * 취소 클레임은 보낼 물건이 없어 null이고, 반품지를 등록하지 않은 스토어도 null이다.
38
+ * 구매자가 직접 보내는 수거(`BUYER_SEND`)에 이 주소가 필요하다.
39
+ */
40
+ export const claimReturnAddressSchema = z.object({
41
+ name: z.string().describe("주소 이름(셀러가 붙인 별칭). 예: 본사 물류센터"),
42
+ contactName: z.string().describe("수취인 이름"),
43
+ phone: z.string().describe("연락처"),
44
+ zipCode: z.string(),
45
+ address1: z.string(),
46
+ address2: z.string().nullable(),
47
+ });
48
+
49
+ export type ClaimReturnAddress = z.infer<typeof claimReturnAddressSchema>;
50
+
33
51
  export const claimSchema = z.object({
34
52
  claimId: z.string(),
35
53
  type: claimTypeSchema,
@@ -73,6 +91,12 @@ export const claimSchema = z.object({
73
91
  holdExpiresAt: z.string().nullable(),
74
92
  /** 보류를 이미 썼는지 — 보류는 클레임당 1회라 해제·만료 뒤에도 true */
75
93
  holdUsed: z.boolean(),
94
+ returnAddress: claimReturnAddressSchema
95
+ .nullable()
96
+ .describe(
97
+ "수거 안내에 쓸 반품지(이슈 #30). 상품이 지정한 반품지, 없으면 스토어 기본 반품지다. " +
98
+ "취소 클레임과 반품지를 등록하지 않은 스토어는 null",
99
+ ),
76
100
  });
77
101
 
78
102
  export type Claim = z.infer<typeof claimSchema>;
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { changeTypeSchema, orderItemStatusSchema, type PageParams, pageSchema } from "./common";
3
- import { easyPayProviderResponseSchema } from "./payment";
3
+ import { cashReceiptSchema, easyPayProviderResponseSchema } from "./payment";
4
4
 
5
5
  export const orderSummarySchema = z.object({
6
6
  orderId: z.string(),
@@ -107,6 +107,13 @@ export const orderSchema = z.object({
107
107
  discountAmount: z.number().int(),
108
108
  totalAmount: z.number().int(),
109
109
  paidAt: z.string().nullable(),
110
+ cashReceipt: cashReceiptSchema
111
+ .nullable()
112
+ .optional()
113
+ .default(null)
114
+ .describe(
115
+ "현금영수증. 구매자가 계좌이체·가상계좌 결제에서 신청했을 때만 있고 그 밖에는 null. 서버는 항상 싣는다",
116
+ ),
110
117
  }),
111
118
  testPayment: z
112
119
  .boolean()
@@ -201,7 +208,14 @@ export const dispatchRequestSchema = z
201
208
  deliveryMethod: deliveryMethodSchema,
202
209
  carrierCode: z.string().optional(),
203
210
  trackingNumber: z.string().optional(),
204
- dispatchedAt: z.string().optional(),
211
+ /**
212
+ * 발송 시각. ISO 8601이고 시간대(`Z` 또는 `+09:00`)를 포함해야 한다. 생략하면 현재 시각이다.
213
+ *
214
+ * 전에는 `z.string()`이라 아무 문자열이나 그대로 저장됐다. 이 값은 배송 정보 jsonb에 들어가
215
+ * 자동 배송완료·추적 동기화 잡의 대상 선별 조건이 된다 — 잘못된 한 행이 스토어 구분 없이 도는
216
+ * 잡 전체를 멈출 수 있어 입력에서 좁혔다. 검증을 통과한 값도 저장 전에 UTC ISO로 정규화한다.
217
+ */
218
+ dispatchedAt: z.iso.datetime({ offset: true }).optional(),
205
219
  })
206
220
  .refine(
207
221
  (value) =>
@@ -227,11 +241,65 @@ export const deliveryInfoSchema = z.object({
227
241
 
228
242
  export type DeliveryInfo = z.infer<typeof deliveryInfoSchema>;
229
243
 
244
+ /**
245
+ * 택배 추적 단계. 응답에서는 문자열로 받는다(값이 추가될 수 있다) — 현재 값은 `ACCEPTED`(접수)·`IN_TRANSIT`(이동 중)·
246
+ * `OUT_FOR_DELIVERY`(배송 출발)·`DELIVERED`(배송완료)다.
247
+ */
248
+ export const trackingStageResponseSchema = z.string();
249
+
230
250
  /** `GET /v1/order-items/{orderItemId}/delivery` 응답 — 배송 정보 + 배송 추적 이벤트 */
231
251
  export const deliveryStatusSchema = deliveryInfoSchema.extend({
232
252
  trackingEvents: z
233
- .array(z.object({ status: z.string(), location: z.string(), occurredAt: z.string() }))
234
- .optional(),
253
+ .array(
254
+ z.object({
255
+ status: z.string().describe("택배사가 알려 준 처리 내용(예: 집화처리, 배송출발)"),
256
+ location: z.string().describe("처리 장소. 모르면 빈 문자열"),
257
+ occurredAt: z.string(),
258
+ stage: trackingStageResponseSchema
259
+ .nullable()
260
+ .optional()
261
+ .default(null)
262
+ .describe(
263
+ "이 이벤트의 추적 단계. 배달 실패처럼 단계로 나눌 수 없는 이벤트는 null. 서버는 항상 싣는다. " +
264
+ "현재 값은 `ACCEPTED`·`IN_TRANSIT`·`OUT_FOR_DELIVERY`·`DELIVERED`이고 값이 추가될 수 있어 문자열로 받는다",
265
+ ),
266
+ }),
267
+ )
268
+ .optional()
269
+ .describe("배송 추적 이력. 시각 오름차순(오래된 것부터)"),
270
+ tracking: z
271
+ .object({
272
+ result: z
273
+ .string()
274
+ .nullable()
275
+ .describe(
276
+ "마지막 추적 조회 결과. 조회에 성공한 적이 없으면 null. " +
277
+ "현재 값은 `FOUND`(조회됨)·`NOT_FOUND`(택배사 전산에 송장 없음)·`UNSUPPORTED_CARRIER`(추적을 지원하지 않는 택배사)이고 " +
278
+ "값이 추가될 수 있어 문자열로 받는다",
279
+ ),
280
+ stage: trackingStageResponseSchema
281
+ .nullable()
282
+ .describe(
283
+ "현재 추적 단계. 조회되지 않았으면 null. 현재 값은 `ACCEPTED`·`IN_TRANSIT`·`OUT_FOR_DELIVERY`·`DELIVERED`이고 " +
284
+ "값이 추가될 수 있어 문자열로 받는다",
285
+ ),
286
+ checkedAt: z
287
+ .string()
288
+ .nullable()
289
+ .describe("택배사에서 추적 정보를 마지막으로 받아 온 시각(ISO 8601)"),
290
+ carrierDeliveredAt: z
291
+ .string()
292
+ .nullable()
293
+ .describe(
294
+ "택배사가 알려 준 배송완료 시각(ISO 8601). 추적으로 배송완료가 반영되면 주문상품의 `deliveredAt`은 반영 시각이라 이 값과 다를 수 있다",
295
+ ),
296
+ })
297
+ .nullable()
298
+ .optional()
299
+ .default(null)
300
+ .describe(
301
+ "택배 추적 요약. 택배 발송이 아니거나 추적을 쓰지 않으면 null. 추적이 배송완료면 주문상품이 자동으로 배송완료된다. 서버는 항상 싣는다",
302
+ ),
235
303
  });
236
304
 
237
305
  export type DeliveryStatus = z.infer<typeof deliveryStatusSchema>;
@@ -514,6 +514,57 @@ export const paymentAttemptStatusSchema = z
514
514
  "새 값이 추가될 수 있으니 모르는 값은 '진행 중이 아닌 시도'로 다룬다",
515
515
  );
516
516
 
517
+ /**
518
+ * 현금영수증 상태 — 값이 추가될 수 있어 응답은 문자열로 받는다. 화면 분기는 이 목록으로 하고 모르는 값은 처리 중으로 다룬다.
519
+ * - `REQUESTED`: 구매자가 신청했고 결제 승인 전이다
520
+ * - `PENDING`: 발급·취소를 기다린다(승인 직후, 또는 일시 오류로 자동 재시도 대기)
521
+ * - `ISSUED`: 발급됐다. 부분 환불이 있었으면 그만큼 취소(또는 재발급)된 금액이다
522
+ * - `CANCELED`: 전액 환불로 취소됐다
523
+ * - `FAILED`: 발급·취소가 실패했고 자동 재시도가 끝났다. `failCode`·`failMessage`를 보고 재발급한다
524
+ * - `SKIPPED`: 발급 대상이 아니다(승인된 결제수단이 계좌이체·가상계좌가 아님)
525
+ */
526
+ export const CASH_RECEIPT_STATUS_VALUES = [
527
+ "REQUESTED",
528
+ "PENDING",
529
+ "ISSUED",
530
+ "CANCELED",
531
+ "FAILED",
532
+ "SKIPPED",
533
+ ] as const;
534
+ export type KnownCashReceiptStatus = (typeof CASH_RECEIPT_STATUS_VALUES)[number];
535
+
536
+ export const cashReceiptSchema = z.object({
537
+ cashReceiptId: z.string(),
538
+ status: z
539
+ .string()
540
+ .describe(
541
+ "상태. `REQUESTED` 결제 전 · `PENDING` 발급 대기 · `ISSUED` 발급 · `CANCELED` 취소 · `FAILED` 실패 · `SKIPPED` 발급 대상 아님. 값이 추가될 수 있다",
542
+ ),
543
+ type: z.string().describe("용도. `INCOME_DEDUCTION` 소득공제 · `EXPENSE_PROOF` 지출증빙"),
544
+ identityType: z
545
+ .string()
546
+ .describe("식별 번호 종류. `PHONE` 휴대폰 · `CARD` 현금영수증카드 · `BUSINESS` 사업자등록번호"),
547
+ identityNumberMasked: z
548
+ .string()
549
+ .describe("가린 식별 번호 (예: 010****5678). 원문은 조회할 수 없다"),
550
+ amount: z
551
+ .number()
552
+ .int()
553
+ .describe("발급해야 할 금액 = 결제 금액 − 환불 완료 금액. 승인 전에는 결제 금액"),
554
+ issuedAmount: z.number().int().describe("PG에 현재 발급돼 있는 금액. 취소됐거나 발급 전이면 0"),
555
+ provider: pgProviderSchema.nullable().describe("발급 PG — 결제를 승인한 PG. 승인 전에는 null"),
556
+ issueNumber: z.string().nullable().describe("현금영수증 승인 번호"),
557
+ receiptUrl: z.string().nullable().describe("현금영수증 확인 주소"),
558
+ issuedAt: z.string().nullable().describe("최근 발급 시각 (ISO 8601)"),
559
+ canceledAt: z.string().nullable().describe("최근 취소 시각 (ISO 8601)"),
560
+ failCode: z.string().nullable().describe("마지막 실패 코드. 성공하면 비운다"),
561
+ failMessage: z.string().nullable().describe("마지막 실패 사유"),
562
+ reissuable: z.boolean().describe("콘솔 재발급으로 다시 시도할 수 있는가 (`FAILED`·`PENDING`)"),
563
+ requestedAt: z.string().describe("신청 시각 (ISO 8601)"),
564
+ updatedAt: z.string().describe("마지막 변경 시각 (ISO 8601)"),
565
+ });
566
+ export type CashReceipt = z.infer<typeof cashReceiptSchema>;
567
+
517
568
  export const paymentRecordSchema = z.object({
518
569
  paymentId: z.string().describe("결제 건 id. 상세 조회 키"),
519
570
  orderId: z.string().nullable().describe("승인돼 만들어진 주문. 승인 전에 끝난 결제는 null"),
@@ -542,6 +593,14 @@ export const paymentRecordSchema = z.object({
542
593
  failMessage: z.string().nullable().describe("실패 사유"),
543
594
  paidAt: z.string().nullable().describe("승인 시각 (ISO 8601)"),
544
595
  createdAt: z.string().describe("결제 시작 시각 (ISO 8601)"),
596
+ cashReceiptStatus: z
597
+ .string()
598
+ .nullable()
599
+ .optional()
600
+ .default(null)
601
+ .describe(
602
+ "현금영수증 상태(`cashReceiptSchema.status`와 같은 값). 신청이 없으면 null. 서버는 항상 싣는다",
603
+ ),
545
604
  });
546
605
  export type PaymentRecord = z.infer<typeof paymentRecordSchema>;
547
606
 
@@ -601,6 +660,11 @@ export const paymentRecordDetailSchema = paymentRecordSchema.extend({
601
660
  .describe("가맹점 식별자 지문 (토스 clientKey, 포트원 Store ID). 비밀 값은 없다"),
602
661
  attempts: z.array(paymentAttemptRecordSchema).describe("시도 이력. 순번 오름차순"),
603
662
  refunds: z.array(paymentRefundRecordSchema).describe("환불 이력. 실패한 요청도 남는다"),
663
+ cashReceipt: cashReceiptSchema
664
+ .nullable()
665
+ .optional()
666
+ .default(null)
667
+ .describe("현금영수증. 구매자가 신청하지 않았으면 null. 서버는 항상 싣는다"),
604
668
  });
605
669
  export type PaymentRecordDetail = z.infer<typeof paymentRecordDetailSchema>;
606
670
 
@@ -257,6 +257,30 @@ export const variantStockSchema = z.object({
257
257
 
258
258
  export type VariantStock = z.infer<typeof variantStockSchema>;
259
259
 
260
+ /**
261
+ * 출고지·반품지 지정 문구 — POST·PUT·PATCH가 같은 규칙을 쓰므로 설명도 한 곳이다 (이슈 #30).
262
+ *
263
+ * 세 값이 뜻이 다르다. **생략**은 「정하지 않음」이라 서버가 그 시점 스토어 기본 주소를 박아 저장하고,
264
+ * **null**은 「스토어 기본을 따른다」는 명시적 비우기다. 주소록 항목은 상품이 참조 중이면 지울 수 없어
265
+ * (`409 ADDRESS_IN_USE`) 참조를 푸는 수단이 필요한데, 생략을 기본값으로 되채우면 기본 주소는 영원히
266
+ * 풀리지 않는다 — null이 그 경로다. 배송 묶음 판정은 null을 그 시점 스토어 기본 출고지로 정규화하므로
267
+ * 두 표현이 배송비를 갈라놓지 않는다.
268
+ */
269
+ const ADDRESS_DESCRIPTIONS = {
270
+ shipping:
271
+ "출고지 주소 id(`GET /v1/seller-addresses?type=SHIPPING`). 생략하면 스토어 기본 출고지를 저장하고, " +
272
+ "`null`이면 비워 둔다(스토어 기본 출고지를 따른다). 이 스토어의 `SHIPPING` 주소가 아니면 거부한다",
273
+ return:
274
+ "반품지 주소 id(`GET /v1/seller-addresses?type=RETURN`). 생략하면 스토어 기본 반품지를 저장하고, " +
275
+ "`null`이면 비워 둔다(스토어 기본 반품지를 따른다). 이 스토어의 `RETURN` 주소가 아니면 거부한다",
276
+ patchShipping:
277
+ "출고지 주소 id. **생략은 바꾸지 않음**이고 `null`은 비우기다(스토어 기본 출고지를 따른다). " +
278
+ "이 스토어의 `SHIPPING` 주소가 아니면 거부한다",
279
+ patchReturn:
280
+ "반품지 주소 id. **생략은 바꾸지 않음**이고 `null`은 비우기다(스토어 기본 반품지를 따른다). " +
281
+ "이 스토어의 `RETURN` 주소가 아니면 거부한다",
282
+ } as const;
283
+
260
284
  export const productCreateRequestSchema = z
261
285
  .object({
262
286
  name: z.string().min(1).max(100),
@@ -284,7 +308,15 @@ export const productCreateRequestSchema = z
284
308
  deliveryType: deliveryTypeSchema
285
309
  .default("PAID")
286
310
  .describe("배송비 유형. FREE 무료, PAID 유료, CONDITIONAL_FREE 조건부 무료"),
287
- deliveryFee: z.number().int().default(0).describe("배송비. 원 단위"),
311
+ deliveryFee: z
312
+ .number()
313
+ .int()
314
+ .min(0)
315
+ .optional()
316
+ .describe(
317
+ "배송비. 원 단위. **생략하면 스토어 기본 배송비**(`GET /v1/store`의 `shippingPolicy.baseDeliveryFee`)를 쓴다. " +
318
+ "`deliveryType`이 FREE면 0으로 저장한다",
319
+ ),
288
320
  conditionalFreeAmount: z
289
321
  .number()
290
322
  .int()
@@ -306,8 +338,8 @@ export const productCreateRequestSchema = z
306
338
  "상품별 환불 정책 — 준 항목만 스토어 기본을 덮어쓴다. `null`이나 생략은 스토어 기본을 그대로 쓴다는 뜻이다. " +
307
339
  "`returnDeliveryFee`·`exchangeDeliveryFee`를 주면 `returnFee`·`exchangeFee`도 같은 값으로 맞춘다",
308
340
  ),
309
- shippingAddressId: z.string().optional(),
310
- returnAddressId: z.string().optional(),
341
+ shippingAddressId: z.string().nullable().optional().describe(ADDRESS_DESCRIPTIONS.shipping),
342
+ returnAddressId: z.string().nullable().optional().describe(ADDRESS_DESCRIPTIONS.return),
311
343
  })
312
344
  .extend(optionModelRequestSchema.shape);
313
345
 
@@ -319,6 +351,13 @@ export const productPatchRequestSchema = z
319
351
  salePrice: z.number().int().multipleOf(10).optional(),
320
352
  status: productStatusSchema.optional(),
321
353
  deliveryFee: z.number().int().optional(),
354
+ // PATCH의 생략은 「바꾸지 않음」이다 — 기본 주소로 되채우지 않는다. 비우려면 `null`을 준다
355
+ shippingAddressId: z
356
+ .string()
357
+ .nullable()
358
+ .optional()
359
+ .describe(ADDRESS_DESCRIPTIONS.patchShipping),
360
+ returnAddressId: z.string().nullable().optional().describe(ADDRESS_DESCRIPTIONS.patchReturn),
322
361
  })
323
362
  .refine((value) => Object.values(value).some((v) => v !== undefined), {
324
363
  message: "최소 1개 필드가 필요합니다",