Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/guides/en/extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -335,8 +335,8 @@ Arrow-compatible results. The SDK includes PostgreSQL and BigQuery-oriented runn

Use the redesigned `commerce` Extension for new commerce apps. It provides the ID-based order model,
buyer information, order lookup, cancel/return/exchange requests, exchangeable items, shipping
address changes, and structured `ActionResult` responses. Validate provider state before mutations
and return explicit unsupported results when a provider lacks an operation.
address changes, product catalog reads, and structured `ActionResult` responses. Validate provider
state before mutations and return explicit unsupported results when a provider lacks an operation.

[Commerce details](extensions/commerce.md)

Expand Down
17 changes: 13 additions & 4 deletions docs/guides/en/extensions/commerce.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Commerce Extension

The Commerce extension registers commerce order lookups and claim actions through a helper. The read model is the `id`-based `CommerceOrder` (with `CommerceOrderItem`), and actions wrap their result in an `ActionResult`.
The Commerce extension registers commerce order lookups, claim actions, and product catalog reads through a helper. The read model is the `id`-based `CommerceOrder` (with `CommerceOrderItem`), and actions wrap their result in an `ActionResult`.

## Go

Expand All @@ -14,7 +14,8 @@ err := app.Use(commerce.Extension().
AcceptReturnOrder(handler.AcceptReturnOrder).
RequestExchangeOrder(handler.RequestExchangeOrder).
GetExchangeableItems(handler.GetExchangeableItems).
ChangeShippingAddress(handler.ChangeShippingAddress),
ChangeShippingAddress(handler.ChangeShippingAddress).
GetProducts(handler.GetProducts),
)
```

Expand All @@ -28,15 +29,23 @@ Supported methods:
- `extension.commerce.order.requestExchangeOrder`
- `extension.commerce.order.getExchangeableItems`
- `extension.commerce.order.changeShippingAddress`
- `extension.commerce.product.getProducts`

Reuse the SDK-exported value types for addresses, payments, fulfillment, and claims.

`getProducts` is a catalog read, not a search. Its `searchFilter` accepts `productId` (one id or
several), `state`, and `createdAt`; advertise the keys you accept as the enum `allowedValues` of
`getProductsOptions.fieldConfigs["searchFilter.key"]`, reject any other key, and do not accept
`name`. `since` carries the previous `next` cursor, and the app applies a default `limit` of 10 and
caps it at 50.

## TypeScript

Use `@Extension({ name: "commerce", systemVersion: "v1" })` and the canonical schemas exported by
`@channel.io/app-sdk-server`: `CommerceGetAppConfigsOutputSchema`,
`CommerceGetOrdersInputSchema`/`CommerceGetOrdersOutputSchema`, the action input schemas, and
`CommerceResultSchema`. Use the exact relative names listed above and add the class to the NestJS
`CommerceGetOrdersInputSchema`/`CommerceGetOrdersOutputSchema`, the action input schemas,
`CommerceResultSchema`, and `CommerceGetProductsInputSchema`/`CommerceGetProductsOutputSchema` for
the product catalog. Use the exact relative names listed above and add the class to the NestJS
providers.

## Authentication, reliability, and testing
Expand Down
4 changes: 2 additions & 2 deletions docs/guides/ja/extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -334,8 +334,8 @@ Arrow-compatible result を stream してください。SDK は PostgreSQL と B

新しい commerce app は redesigned `commerce` Extension を使います。ID-based order model、
buyer、order lookup、cancel/return/exchange request、exchangeable item、shipping address change、
structured `ActionResult` を提供します。Mutation 前に provider state を検証し、provider が
対応しない operation は明確な unsupported result にしてください。
product catalog read、structured `ActionResult` を提供します。Mutation 前に provider state
検証し、provider が対応しない operation は明確な unsupported result にしてください。

[Commerce 詳細](extensions/commerce.md)

Expand Down
14 changes: 11 additions & 3 deletions docs/guides/ja/extensions/commerce.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Commerce 拡張

Commerce 拡張は、コマース注文の取得とクレームアクションを helper で登録します。取得モデルは `id` ベースの `CommerceOrder`(`CommerceOrderItem` を含む)で、アクションは結果を `ActionResult` でラップします。
Commerce 拡張は、コマース注文の取得・クレームアクション・商品カタログの取得を helper で登録します。取得モデルは `id` ベースの `CommerceOrder`(`CommerceOrderItem` を含む)で、アクションは結果を `ActionResult` でラップします。

## Go

Expand All @@ -14,7 +14,8 @@ err := app.Use(commerce.Extension().
AcceptReturnOrder(handler.AcceptReturnOrder).
RequestExchangeOrder(handler.RequestExchangeOrder).
GetExchangeableItems(handler.GetExchangeableItems).
ChangeShippingAddress(handler.ChangeShippingAddress),
ChangeShippingAddress(handler.ChangeShippingAddress).
GetProducts(handler.GetProducts),
)
```

Expand All @@ -28,15 +29,22 @@ err := app.Use(commerce.Extension().
- `extension.commerce.order.requestExchangeOrder`
- `extension.commerce.order.getExchangeableItems`
- `extension.commerce.order.changeShippingAddress`
- `extension.commerce.product.getProducts`

住所・決済・履行・クレームには SDK が export する値型を再利用します。

`getProducts` は検索ではなくカタログ取得です。`searchFilter` は `productId`(単一・複数 id)・
`state`・`createdAt` を受け取ります。受け取るキーは `getProductsOptions.fieldConfigs["searchFilter.key"]`
の enum `allowedValues` で告知し、それ以外のキーは拒否し、`name` は受け取りません。`since` には
前回の `next` を渡し、`limit` は app が既定 10・上限 50 を適用します。

## TypeScript

`@Extension({ name: "commerce", systemVersion: "v1" })` と
`@channel.io/app-sdk-server` が export する canonical schema を使います。
`CommerceGetAppConfigsOutputSchema`、`CommerceGetOrdersInputSchema`/
`CommerceGetOrdersOutputSchema`、action input schema、`CommerceResultSchema` を使い、上の正確な
`CommerceGetOrdersOutputSchema`、action input schema、`CommerceResultSchema`、商品カタログ用の
`CommerceGetProductsInputSchema`/`CommerceGetProductsOutputSchema` を使い、上の正確な
relative name で Function を登録します。

## 認証・信頼性・test
Expand Down
6 changes: 3 additions & 3 deletions docs/guides/ko/extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -324,9 +324,9 @@ stream하세요. SDK는 PostgreSQL과 BigQuery용 runner를 제공합니다.
## Commerce

새 commerce 앱은 재설계된 `commerce` Extension을 사용합니다. ID 기반 order model, buyer,
order 조회, cancel/return/exchange request, 교환 가능 상품, 배송지 변경, 구조화된 `ActionResult`를
제공합니다. Mutation 전에 provider 상태를 검증하고 provider가 지원하지 않는 동작은 명시적인
unsupported 결과로 반환하세요.
order 조회, cancel/return/exchange request, 교환 가능 상품, 배송지 변경, 상품 카탈로그 조회,
구조화된 `ActionResult`를 제공합니다. Mutation 전에 provider 상태를 검증하고 provider가 지원하지
않는 동작은 명시적인 unsupported 결과로 반환하세요.

[Commerce 상세](extensions/commerce.md)

Expand Down
16 changes: 12 additions & 4 deletions docs/guides/ko/extensions/commerce.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Commerce 확장

Commerce 확장은 커머스 주문 조회와 클레임 액션을 helper로 등록합니다. 조회 모델은 `id` 기반 `CommerceOrder`(`CommerceOrderItem` 포함)이며, 액션은 결과를 `ActionResult`로 감쌉니다.
Commerce 확장은 커머스 주문 조회·클레임 액션·상품 카탈로그 조회를 helper로 등록합니다. 조회 모델은 `id` 기반 `CommerceOrder`(`CommerceOrderItem` 포함)이며, 액션은 결과를 `ActionResult`로 감쌉니다.

## Go

Expand All @@ -14,7 +14,8 @@ err := app.Use(commerce.Extension().
AcceptReturnOrder(handler.AcceptReturnOrder).
RequestExchangeOrder(handler.RequestExchangeOrder).
GetExchangeableItems(handler.GetExchangeableItems).
ChangeShippingAddress(handler.ChangeShippingAddress),
ChangeShippingAddress(handler.ChangeShippingAddress).
GetProducts(handler.GetProducts),
)
```

Expand All @@ -28,16 +29,23 @@ err := app.Use(commerce.Extension().
- `extension.commerce.order.requestExchangeOrder`
- `extension.commerce.order.getExchangeableItems`
- `extension.commerce.order.changeShippingAddress`
- `extension.commerce.product.getProducts`

주소·결제·이행·클레임에는 SDK가 export하는 값 타입을 재사용합니다.

`getProducts`는 검색이 아니라 카탈로그 조회입니다. `searchFilter`는 `productId`(단건·복수 id)·
`state`·`createdAt`을 받습니다. 받는 키는 `getProductsOptions.fieldConfigs["searchFilter.key"]`의
enum `allowedValues`로 광고하고, 그 밖의 키는 거부하며 `name`은 받지 않습니다. `since`에는 이전
응답의 `next`를 넣고, `limit`은 앱이 기본 10·상한 50으로 둡니다.

## TypeScript

`@Extension({ name: "commerce", systemVersion: "v1" })`과
`@channel.io/app-sdk-server`가 export하는 canonical schema를 사용합니다.
`CommerceGetAppConfigsOutputSchema`, `CommerceGetOrdersInputSchema`/
`CommerceGetOrdersOutputSchema`, action input schema, `CommerceResultSchema`를 사용하고 위 목록의
정확한 relative name으로 Function을 등록합니다.
`CommerceGetOrdersOutputSchema`, action input schema, `CommerceResultSchema`, 상품 카탈로그용
`CommerceGetProductsInputSchema`/`CommerceGetProductsOutputSchema`를 사용하고 위 목록의 정확한
relative name으로 Function을 등록합니다.

## 인증·신뢰성·테스트

Expand Down
3 changes: 2 additions & 1 deletion docs/reference/go/EXTENSIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,8 @@ app.Use(commerce.Extension().
AcceptReturnOrder(handler.AcceptReturnOrder).
RequestExchangeOrder(handler.RequestExchangeOrder).
GetExchangeableItems(handler.GetExchangeableItems).
ChangeShippingAddress(handler.ChangeShippingAddress))
ChangeShippingAddress(handler.ChangeShippingAddress).
GetProducts(handler.GetProducts))
```

Commerce uses stable ID-based orders and structured action results. Validate current provider state
Expand Down
5 changes: 5 additions & 0 deletions go/extension/commerce/extension.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ func (b *ExtensionBuilder) ChangeShippingAddress(handler appsdk.TypedHandlerFunc
return b
}

func (b *ExtensionBuilder) GetProducts(handler appsdk.TypedHandlerFunc[GetProductsInput, GetProductsOutput]) *ExtensionBuilder {
b.base.Func(FunctionGetProducts, schemaregistry.Append(FunctionGetProducts, appsdk.HandleProto(handler))...)
return b
}

func (b *ExtensionBuilder) Function(name string, opts ...appsdk.FunctionOption) *ExtensionBuilder {
b.base.Func(name, opts...)
return b
Expand Down
73 changes: 70 additions & 3 deletions go/extension/commerce/extension_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ func newExtension() appsdk.Extension {
AcceptReturnOrder(zero[commerce.AcceptReturnOrderInput, commerce.ActionResult]()).
RequestExchangeOrder(zero[commerce.ExchangeOrderInput, commerce.ActionResult]()).
GetExchangeableItems(zero[commerce.GetExchangeableItemsInput, commerce.GetExchangeableItemsOutput]()).
ChangeShippingAddress(zero[commerce.ChangeShippingAddressInput, commerce.ActionResult]())
ChangeShippingAddress(zero[commerce.ChangeShippingAddressInput, commerce.ActionResult]()).
GetProducts(zero[commerce.GetProductsInput, commerce.GetProductsOutput]())
}

func TestExtensionRegistersFunctions(t *testing.T) {
Expand All @@ -35,8 +36,8 @@ func TestExtensionRegistersFunctions(t *testing.T) {
t.Fatal(err)
}

if got := len(app.Methods()); got != 8 {
t.Fatalf("expected 8 methods, got %d", got)
if got := len(app.Methods()); got != 9 {
t.Fatalf("expected 9 methods, got %d", got)
}

targets := app.AutoRegisterTargets()
Expand All @@ -61,6 +62,7 @@ func TestSchemasMatchCanonicalRegistry(t *testing.T) {
commerce.FunctionRequestExchangeOrder,
commerce.FunctionGetExchangeableItems,
commerce.FunctionChangeShippingAddress,
commerce.FunctionGetProducts,
}

schemas := app.Schemas()
Expand Down Expand Up @@ -108,3 +110,68 @@ func TestGetOrdersUsesProtoJSONNames(t *testing.T) {
t.Fatalf("expected protojson camelCase output, got %+v", out)
}
}

func TestGetProductsKeepsZeroValuesAndOmitsUnsetFields(t *testing.T) {
app := appsdk.New(appsdk.Options{AppID: "app"})
price := 0.0
if err := app.Use(commerce.Extension().
GetProducts(func(_ context.Context, _ appsdk.Context, in *commerce.GetProductsInput) (*commerce.GetProductsOutput, error) {
if in.GetLimit() != 20 {
t.Fatalf("expected limit 20, got %d", in.GetLimit())
}
return &commerce.GetProductsOutput{
Products: []*commerce.Product{{
Id: "product-1",
Name: "gift",
Price: &price,
Variants: []*commerce.ProductVariant{
{Id: "variant-1", Price: &price},
{Id: "variant-2", Price: &price, StockQuantity: &price},
},
}},
Next: "cursor-2",
}, nil
}),
); err != nil {
t.Fatal(err)
}

res := app.HandleRequest(context.Background(), appsdk.FunctionRequest{
Method: commerce.FunctionGetProducts,
Params: json.RawMessage(`{"searchFilter":{"state":"active"},"limit":20}`),
})
if res.Error != nil {
t.Fatalf("unexpected error: %+v", res.Error)
}
var out map[string]any
if err := json.Unmarshal(res.Result, &out); err != nil {
t.Fatal(err)
}
if out["next"] != "cursor-2" {
t.Fatalf("expected next cursor, got %+v", out)
}
products, ok := out["products"].([]any)
if !ok || len(products) != 1 {
t.Fatalf("unexpected products: %+v", out)
}
first := products[0].(map[string]any)
if first["id"] != "product-1" || first["price"] != 0.0 {
t.Fatalf("expected protojson camelCase output with zero price kept, got %+v", first)
}
for _, key := range []string{"state", "originalPrice", "images", "categories", "tags"} {
if _, present := first[key]; present {
t.Fatalf("expected unset %s to be omitted, got %+v", key, first)
}
}
variant := first["variants"].([]any)[0].(map[string]any)
if variant["price"] != 0.0 {
t.Fatalf("expected zero variant price kept, got %+v", variant)
}
if _, present := variant["stockQuantity"]; present {
t.Fatalf("expected unset stockQuantity to be omitted, got %+v", variant)
}
soldOut := first["variants"].([]any)[1].(map[string]any)
if soldOut["stockQuantity"] != 0.0 {
t.Fatalf("expected zero stockQuantity kept (sold out), got %+v", soldOut)
}
}
10 changes: 10 additions & 0 deletions go/extension/commerce/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const (
FunctionRequestExchangeOrder = "extension.commerce.order.requestExchangeOrder"
FunctionGetExchangeableItems = "extension.commerce.order.getExchangeableItems"
FunctionChangeShippingAddress = "extension.commerce.order.changeShippingAddress"
FunctionGetProducts = "extension.commerce.product.getProducts"
)

// commerce 전용 타입
Expand All @@ -34,6 +35,8 @@ type ExchangeOrderInput = sdkv1.CommerceExchangeOrderInput
type GetExchangeableItemsInput = sdkv1.CommerceGetExchangeableItemsInput
type GetExchangeableItemsOutput = sdkv1.CommerceGetExchangeableItemsOutput
type ChangeShippingAddressInput = sdkv1.CommerceChangeShippingAddressInput
type GetProductsInput = sdkv1.CommerceGetProductsInput
type GetProductsOutput = sdkv1.CommerceGetProductsOutput

// 변경 없는 값 타입은 Order* / Buyer 재사용
type Buyer = sdkv1.Buyer
Expand Down Expand Up @@ -62,4 +65,11 @@ type Metafield = sdkv1.OrderMetafield
// 교환 후보. getExchangeableItems 응답에서만 채워진다.
type ExchangeableItem = sdkv1.CommerceExchangeableItem
type ExchangeableVariant = sdkv1.CommerceExchangeableVariant

// 상품 카탈로그. getProducts 응답에서만 채워진다. ProductVariant.Price 는 절대가라
// ExchangeableVariant.AdditionalAmount(추가금)와 뜻이 다르다.
type Product = sdkv1.CommerceProduct
type ProductVariant = sdkv1.CommerceProductVariant

// variant 옵션(name/value). 교환 후보와 상품 variant 가 함께 쓴다.
type VariantOption = sdkv1.CommerceVariantOption
50 changes: 29 additions & 21 deletions go/extension/commerce/value_types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,35 +21,43 @@ const sdkProtoPackage = "channel.app.sdk.v1"
// 별칭을 빠뜨리면 스키마에는 필드가 보이는데 앱이 그 값을 만들 방법이 없는 상태가 된다 —
// 컴파일은 SDK 안에서 멀쩡히 통과해서 릴리즈 전까지 드러나지 않는다(#116).
//
// 아래 목록을 손으로 관리하지만, 진실의 원천은 proto 디스크립터다. Order 에서 도달 가능한
// 메시지를 걸어 목록과 대조하므로, 새 메시지를 추가하고 이 목록을 갱신하지 않으면 실패한다.
// 값은 별칭 자체를 참조하므로 별칭이 없으면 컴파일도 되지 않는다.
func TestOrderValueTypesAreAliased(t *testing.T) {
// 아래 목록을 손으로 관리하지만, 진실의 원천은 proto 디스크립터다. Order·Product·ExchangeableItem
// 에서 도달 가능한 메시지를 걸어 목록과 대조하므로, 새 메시지를 추가하고 이 목록을 갱신하지 않으면
// 실패한다. 값은 별칭 자체를 참조하므로 별칭이 없으면 컴파일도 되지 않는다.
func TestCommerceValueTypesAreAliased(t *testing.T) {
aliases := map[string]any{
"CommerceOrder": commerce.Order{},
"CommerceOrderItem": commerce.OrderItem{},
"CommerceOrderBundleItem": commerce.OrderBundleItem{},
"Buyer": commerce.Buyer{},
"OrderAddress": commerce.Address{},
"OrderPayment": commerce.Payment{},
"OrderFulfillment": commerce.Fulfillment{},
"OrderFulfillmentItem": commerce.FulfillmentItem{},
"OrderClaim": commerce.Claim{},
"OrderClaimability": commerce.Claimability{},
"OrderTaxLine": commerce.TaxLine{},
"OrderAttribute": commerce.Attribute{},
"OrderShippingLine": commerce.ShippingLine{},
"OrderTransaction": commerce.Transaction{},
"OrderMetafield": commerce.Metafield{},
"CommerceOrder": commerce.Order{},
"CommerceOrderItem": commerce.OrderItem{},
"CommerceOrderBundleItem": commerce.OrderBundleItem{},
"Buyer": commerce.Buyer{},
"OrderAddress": commerce.Address{},
"OrderPayment": commerce.Payment{},
"OrderFulfillment": commerce.Fulfillment{},
"OrderFulfillmentItem": commerce.FulfillmentItem{},
"OrderClaim": commerce.Claim{},
"OrderClaimability": commerce.Claimability{},
"OrderTaxLine": commerce.TaxLine{},
"OrderAttribute": commerce.Attribute{},
"OrderShippingLine": commerce.ShippingLine{},
"OrderTransaction": commerce.Transaction{},
"OrderMetafield": commerce.Metafield{},
"CommerceProduct": commerce.Product{},
"CommerceProductVariant": commerce.ProductVariant{},
"CommerceVariantOption": commerce.VariantOption{},
"CommerceExchangeableItem": commerce.ExchangeableItem{},
"CommerceExchangeableVariant": commerce.ExchangeableVariant{},
}

reachable := map[string]protoreflect.MessageDescriptor{}
collectMessages((&commerce.Order{}).ProtoReflect().Descriptor(), reachable)
// 상품 카탈로그와 교환 후보는 주문에서 도달하지 않는 별도 루트다.
collectMessages((&commerce.Product{}).ProtoReflect().Descriptor(), reachable)
collectMessages((&commerce.ExchangeableItem{}).ProtoReflect().Descriptor(), reachable)

for name := range reachable {
if _, ok := aliases[name]; !ok {
t.Errorf(
"proto 메시지 %s 가 주문 계약에서 도달 가능한데 commerce 별칭이 없다 — "+
"proto 메시지 %s 가 주문·상품·교환 계약에서 도달 가능한데 commerce 별칭이 없다 — "+
"extension/commerce/types.go 에 별칭을 추가하고 이 목록에도 넣어라",
name,
)
Expand All @@ -61,7 +69,7 @@ func TestOrderValueTypesAreAliased(t *testing.T) {
for name, value := range aliases {
desc, ok := reachable[name]
if !ok {
// 주문 계약에서 도달하지 않는 별칭은 이 테스트의 관심사가 아니다.
// 주문·상품·교환 계약에서 도달하지 않는 별칭은 이 테스트의 관심사가 아니다.
continue
}

Expand Down
Loading
Loading