ユニバーサル コマース プロトコル(UCP)の対応
- Dominant language
- PHP
- Stars
- 788
- Forks
- 719
- Avg merge
- 4d 4h
- Merged PRs (30d)
- 39
Description
### 概要 (Overview)
**Universal Commerce Protocol (UCP)** を使用した購入完了機能の実装。最新仕様 **`v2026-04-08`** に準拠する。
UCP は ucp.dev で公開されるオープン仕様で、Google Merchant 他が採用。AI エージェントから加盟店の購入フローを呼び出すための標準プロトコル。
関連 issue: 共通基盤 #6777、ACP 対応 #6776、**Product Feed / Catalog 配信基盤 #6794** (Catalog capability の独立 issue)。
### 期待する内容 (Expect)
- UCP の Checkout capability を使ってエージェント経由で購入完了できる
- 将来的に Cart / Catalog / Identity Linking の各 capability に拡張可能な設計
### 参照仕様
- **UCP リポジトリ: https://github.com/Universal-Commerce-Protocol/ucp** (`v2026-04-08`, Apache 2.0)
- 公式サイト: https://ucp.dev/ / 仕様: https://ucp.dev/specification/overview
- 公式ツール (Apache 2.0):
- [`Universal-Commerce-Protocol/conformance`](https://github.com/Universal-Commerce-Protocol/conformance) — HTTP 経由の公式 conformance test (Python / uv)
- [`Universal-Commerce-Protocol/ucp-schema`](https://github.com/Universal-Commerce-Protocol/ucp-schema) — schema validator CLI (Rust / cargo)
- [`Universal-Commerce-Protocol/samples`](https://github.com/Universal-Commerce-Protocol/samples) — flower_shop fixture / REST server 参考実装
- Google Merchant UCP: https://developers.google.com/merchant/ucp (日本語: https://developers.google.com/merchant/ucp/guides/checkout/native?hl=ja )
- OAuth 2.0 認可基盤: [eccube-api4](https://github.com/EC-CUBE/eccube-api4) (`ec-cube/api42`)
### 対象ブランチ
**`4.4` (Symfony 7 対応版 / 現行メイン開発ブランチ)**
---
## UCP の構成 (v2026-04-08)
### capability 一覧
| capability | 用途 | 本 issue の扱い |
|---|---|---|
| **Checkout** | エージェントから購入フローを実行 | **本 issue (Phase 1〜5)** |
| **Order** | 注文取得・状態管理 (Get Order、webhook) | Phase 2 に含める |
| **Cart** | エージェント側でカートを構築 | 別 issue 化候補 |
| **Catalog** | エージェントが実時間で在庫・価格・variants を取得 | **#6794** で独立 issue 化 |
| **Identity Linking** | OAuth 2.0 で会員特典を引き継ぎ | 別 issue 化候補 |
| **Embedded Protocol** | Cart transport binding、reauthentication | 別 issue 化候補 |
| **Eligibility & Verification** | Claims verification、disclosure | 別 issue 化候補 |
### 対応トランスポート
UCP は仕様レベルで **REST / MCP / A2A / embedded** をサポート。本 issue ではまず **REST** を実装し、MCP transport は別タスクの EC-CUBE MCP Server と統合検討。
### SLO (Service Level Objectives)
全体目安: 可用性 **≥ 95%**、レイテンシ **p50 ≤ 1 秒 / p95 ≤ 10 秒**。Google Native Checkout ドキュメントではエンドポイント別に細分化されている (Phase 5 / テスト計画 Layer 5 参照)。
---
## 実装必須エンドポイント (Checkout capability)
正準パスは UCP discovery profile の `services[].endpoint` から解決する (パスはハードコードせず `RequestContext` から生成):
| メソッド | エンドポイント | 役割 |
|---|---|---|
| `POST` | `/checkout-sessions` | セッション作成 (商品・数量・通貨を受け取り、税・送料・決済ハンドラ情報を返す) |
| `GET` | `/checkout-sessions/{id}` | セッション取得 (v2026-04-08 で正式化) |
| `PUT` | `/checkout-sessions/{id}` | セッション更新 (配送先・決済変更に伴う税・送料・配送オプション再計算) |
| `POST` | `/checkout-sessions/{id}/complete` | セッション完了 (注文確定、注文 ID と permalink を返す)。**単発の同期確定でなく「中断→再開」する状態機械**。買い手の追加対応が必要な場合は HTTP 200 + `status: requires_escalation` + `continue_url` (MUST・絶対 HTTPS) を返し (エラーではない)、買い手が business UI で完了後に再開 complete or order webhook で `completed`。状態遷移・在庫引当の保持/回収は共通基盤 #6777 `AgentCheckoutCompletionService` へ委譲 |
### Order Management 関連
- `Get Order` — `currency` 必須、platform 認証必須
- 注文ステータス更新の通知 (Webhook)
---
## 認証
UCP v2026-04-08 で正式化された認証要件:
- **OAuth 2.0 foundation** + **capability-driven scopes**
- **Request/Response Signing** (RFC 9421 HTTP Message Signatures)
- **Abuse 防止シグナル** の伝達
OAuth 2.0 認可サーバーは **eccube-api4 (`ec-cube/api42`)** の `league/oauth2-server-bundle` を再利用する (前提依存)。capability-driven scope (`ucp:checkout`, `ucp:cart`, `ucp:catalog`, `ucp:identity`) は eccube-api4 に追加登録する。共通基盤 #6777 の `AgentCommerceOAuth2Authenticator` と `UcpMessageSigner` を介して各エンドポイントで認証・署名検証を行う。
### Request/Response Signing の鍵方式 (EC JWK・一次仕様で確認済)
UCP の署名は **RFC 9421 HTTP Message Signatures** を用い、鍵は **EC 公開鍵 JWK** が profile schema で確定している:
- `Signature-Input` / `Signature: sig1=::` ヘッダで署名。`UCP-Agent` ヘッダが signer の profile URI (`/.well-known/ucp` を指す) を運ぶ
- `signing_keys[]` の各要素は **`kty:"EC"`** / **`crv:"P-256"` または `"P-384"`** / `x` / `y` / `kid` (必須)、`use:"sig"` / `alg` (任意)。秘密鍵パラメータ (`d` 等) は profile に含めてはならない
- **`ed25519` (OKP) は UCP profile schema 非適合**。`UcpMessageSigner` のテスト鍵・本番鍵は **EC P-256** を用いる
- ACP は署名鍵方式が未固定・JWK 広告 deferred のため非対称 (#6776 参照)
## Discovery (`/.well-known/ucp`)
UCP は capability discovery を **標準 well-known URI `/.well-known/ucp`** で公開する (ucp.dev 仕様で標準化)。agent は `https://{origin}/.well-known/ucp` を取得して対応 capability・version・transport・エンドポイント・公開鍵を認証前に判定する。
### profile (Business Discovery Profile) の構造
- ラッパー = `ucp` オブジェクト + optional `signing_keys` 配列
- `ucp.version` (`YYYY-MM-DD`, 必須) / `services` / `capabilities` / `payment_handlers` は **逆ドメイン名 (reverse-domain) でキー付けされたレジストリ** (値はオブジェクト配列) / `supported_versions` (optional, `version → profile URI` マップ)
- `services[].transport` は必須 enum `rest` / `mcp` / `a2a` / `embedded`。business では `endpoint` (URI) を指定 (embedded は `config`)
- `signing_keys[]` = 上記 EC 公開鍵 JWK
- 配信要件: **HTTPS 必須・3xx リダイレクト禁止・`Cache-Control: public, max-age ≥ 60s`**
### 設置形態と配信制約 (RFC 8615)
well-known は **オリジン (scheme+host+port) のルートに固定・1 オリジン 1 枚**。設置形態別の可否・配信方針 (ルートドメイン / サブディレクトリ / 1 オリジン複数ショップ、サブドメイン推奨) は **共通基盤 #6777 「10. 設置形態と discovery 配信」** に準拠。profile 内の `services[].endpoint` / `capabilities[].endpoint` は `RequestContext` / 設定サイト URL から動的生成し、設置パスをハードコードしない。
プライバシーポリシー / 利用規約の URL は **EC-CUBE 標準ページから自動設定**する。`BaseInfo` に URL カラムは追加せず、既存ルート `help_privacy` (`/help/privacy`、`dtb_page`「プライバシーポリシー」) / `help_agreement` (`/help/agreement`、「ご利用規約」) から `UrlGeneratorInterface::ABSOLUTE_URL` で絶対 HTTPS URL を生成し、profile の `links[]` 等へ反映する (URL は `RequestContext` 由来でハードコードしない)。独自ページを使う店舗向けに、解決はリゾルバ Service 経由とし `app/Customize` で差し替え可能にする。
---
## データモデル
### `CheckoutSession` エンティティ
**共通基盤 #6777 の汎用 `CheckoutSession` を使用**。UCP 固有の状態は `metadata` カラムで保持。`protocol` は `ucp` 固定、`currency` は v2026-04-08 で必須。状態は #6777 の正規化ステータス (`incomplete`/`ready`/`requires_action`/`in_progress`/`completed`/`canceled`/`expired`) を使用。
#### UCP status ↔ 正規化ステータスのマッピング (`UcpCheckoutSessionMapper`)
| UCP status (v2026-04-08) | 共通基盤 正規化 (#6777) | 備考 |
|---|---|---|
| `incomplete` | `incomplete` | |
| `ready_for_complete` | `ready` | 名称差をマッパーで変換 |
| `requires_escalation` | **`requires_action`** | continue_url を返す中間状態。在庫は引当のまま保持・再開を待つ |
| `complete_in_progress` | **`in_progress`** | 非同期確定待ち (IPN/Webhook で `completed` へ) |
| `completed` | `completed` | |
| `canceled` | `canceled` | |
`requires_escalation` (UCP・buyer ハンドオフ) と ACP `authentication_required` は、共通層では「在庫を保持して再開を待つ」点で同一のため単一 `requires_action` に正規化される。UCP 固有の status 原文は `metadata` に保持する。
### `BaseInfo` への追加フィールド
**本 issue で `BaseInfo` に追加するカラムは無い。**
- `ucp_checkout_enabled` (boolean) は共通基盤 #6777 で定義済 (デフォルト `false`、UCP checkout の有効化を制御。discovery / catalog は常時公開でフラグ無し)。
- **Google Pay 等の決済ハンドラ固有設定 (`merchant_id` 等) は `BaseInfo` にカラム化しない** — 決済はプラグイン化方針 (Stripe 同様) のため、決済ハンドラプラグインが UCP discovery profile の `payment_handlers` に寄与する。
- 国コード変換 (ISO numeric→alpha-2) は新規マスタ `mtb_country_iso_code` (#6777 / PR #6802) を `CountryIsoCodeRepository` 経由で解決する (`BaseInfo` には持たない)。
> 注: OAuth 2.0 クライアント ID / シークレットは eccube-api4 の OAuth2 クライアントエンティティで管理されるため、`BaseInfo` には追加しない。
> 注: Request/Response signing 用の **EC 秘密鍵 (P-256)** は `BaseInfo` カラムに格納しない (DB ダンプ・バックアップ・レプリカへの秘密伝播を避ける)。共通の鍵保管ディレクトリ `app/keystore/agent-commerce/ucp_signing.key` に保管し、環境変数 `ECCUBE_AGENT_COMMERCE_UCP_SIGNING_KEY` でパス上書き可 (AWS Secrets Manager / Azure Key Vault 連携。eccube-api4 の OAuth2 鍵と同方式)。既定ファイルは初回利用時に自動生成 (perm 600)。対応する **EC 公開鍵 JWK** は秘密鍵から実行時に導出して `/.well-known/ucp` profile の `signing_keys[]` に公開する (秘密鍵パラメータ `d` 等は混入させない)。ed25519 は profile schema 非適合。鍵保管ディレクトリの新設・保護 (`.htaccess`/nginx 多重防御)・`KeyProvider` 抽象は **#6797** で共通化する。
> 注: プライバシーポリシー / 利用規約の URL は `BaseInfo` カラムにしない。既存の標準ページ (ルート `help_privacy` / `help_agreement`、`dtb_page`「プライバシーポリシー」「ご利用規約」) から絶対 URL を実行時生成して profile `links[]` に反映する (自動設定)。独自ページ利用時はリゾルバ Service を `app/Customize` で差し替える。
---
## データ形式のマッピング
### 住所フォーマット変換 (EC-CUBE → UCP)
| EC-CUBE | UCP | 備考 |
|---|---|---|
| `name01` + `name02` | `recipient_name` | 姓名結合 |
| `postal_code` | `postal_code` | そのまま |
| `Pref->getName()` | `region` | 都道府県名 |
| `addr01` | `locality` | 市区町村 |
| `addr02` | `address1`, `address2` | 番地・建物名 |
| `Order.Country` (`mtb_country.id` = ISO 3166-1 **numeric**) | `country` (ISO 3166-1 **alpha-2**) | **`Master\Country` に ISO 取得メソッドは無い**。共通基盤 `AddressMappingService` が**新規マスタ `mtb_country_iso_code`** (id=ISO numeric/name=alpha-2) を `CountryIsoCodeRepository` 経由で解決して生成 (PR #6802) |
→ 共通基盤 #6777 の `AddressMappingService` で実装。
### 購入者情報マッピング
| EC-CUBE | UCP | 備考 |
|---|---|---|
| `name01` | `last_name` | 姓 |
| `name02` | `first_name` | 名 |
| `email` | `email` | メールアドレス |
| `phone_number` | `phone` | 電話番号 |
### 金額単位変換 (Minor Unit)
UCP は金額をマイナー単位 (整数) で表現。JPY/KRW/TWD はゼロデシマル (変換不要)。→ 共通基盤 #6777 の `MinorUnitConverter` (bcmath、負数対応)。
### `totals` 配列の構造 (v2026-04-08)
- `type` フィールドは **開放文字列** (subtotal / tax / shipping / discount / fee / 任意追加)
- `amount` フィールドは **負数対応** (割引を負数で表現)
- `type: "fee"` には **`lines` 配列** を追加可能 (手数料の詳細内訳)
- 複数行アイテムを 1 セッション内で処理するマルチアイテムフロー対応
---
## 実装タスク
### Phase 1: 基盤構築
- [ ] 共通基盤 #6777 を import (CheckoutSession、AddressMappingService、MinorUnitConverter、AgentCheckoutPurchaseFlowAdapter)
- [ ] UCP 固有マッパー (`UcpCheckoutSessionMapper`) 設計
- [ ] **`BaseInfo` への UCP 専用カラム追加は無し** (`ucp_checkout_enabled` は #6777 / Google Pay の merchant_id は決済ハンドラプラグインが discovery に寄与 / プライバシー・利用規約 URL は標準ページ自動生成 / 署名用 EC 秘密鍵は #6797 の `app/keystore/`)
- [ ] 法務ページ URL リゾルバ (`help_privacy` / `help_agreement` から `ABSOLUTE_URL` 生成、`app/Customize` 差し替え可) → profile `links[]`
### Phase 2: API 実装 (Checkout capability)
- [ ] `POST /checkout-sessions` (作成)
- [ ] `GET /checkout-sessions/{id}` (取得 — v2026-04-08 で追加)
- [ ] `PUT /checkout-sessions/{id}` (更新)
- [ ] `POST /checkout-sessions/{id}/complete` (確定)
- [ ] Get Order エンドポイント (currency 必須、platform 認証)
- [ ] 既存 `Order` システムとの連携
### Phase 3: 変換・サービス層
- [ ] 住所・金額マッピング (共通基盤利用)
- [ ] `totals` 仕様準拠 (負数、`lines` 内訳、開放型 type)
- [ ] 複数行アイテム対応
- [ ] 税・送料再計算 (`PurchaseFlow` 連携)
### Phase 4: 認証・セキュリティ
- [ ] eccube-api4 に UCP 用 scope (`ucp:checkout` 等) を登録 (eccube-api4 リポジトリ側で別 issue)
- [ ] 共通基盤 #6777 の OAuth2 認証ミドルウェアを UCP エンドポイントに適用
- [ ] `UcpMessageSigner` (RFC 9421 / EC P-256 JWK) 実装
- [ ] リクエストバリデーション / エラーハンドリング (2 系統、後述)
- [ ] レート制限 / Abuse 防止シグナルの伝達
### Phase 5: テスト・運用
- [ ] Layer 1〜5 (スキーマ契約、エンドポイント単体、マッピング、認証・署名、SLO) 実装
- [ ] Layer 6a (Google Merchant Center Diagnostics) 結合 ※日本でも利用可能
- [ ] Layer 7 (公式 conformance test) を CI に組み込み
- [ ] API 仕様書 / Google Merchant Center オンボーディング手順ドキュメント
- [ ] Layer 6b (UCP Sandbox) は GA 後の別タスク
### Phase 6 以降 (別 issue 化候補)
- **UCP Cart capability** / **Catalog capability (→ #6794)** / **Identity Linking** / **Embedded Protocol** / **Eligibility & Verification** / **JSON-RPC・AP2・A2A transport**
---
## 配送モデルのマッピング仕様 (UCP)
差異は **共通基盤 #6777 に準拠**。本 issue では UCP 固有のフィールド対応のみ:
| UCP フィールド | EC-CUBE 側マッピング |
|---|---|
| `fulfillment_address` | 共通基盤 `AddressMappingService` 経由で `Shipping` 1 件作成 |
| `fulfillment_options[]` | 共通基盤 `FulfillmentOptionMapperInterface` で `(Delivery × DeliveryFee)` を展開 |
| `fulfillment_options[].carrier_code` | `Delivery.service_name` または独自コード (`BaseInfo` 拡張で対応可) |
| `fulfillment_options[].cost` | `DeliveryFee.fee` を共通基盤 `MinorUnitConverter` で minor unit 変換 |
| `fulfillment_options[].estimated_delivery_days_min / max` | 注文明細の `ProductClass` が参照する **`DeliveryDuration.getDuration()` の最大値**から算出 |
| `selected_fulfillment_option` | 内部 `Delivery` に解決し `Shipping.Delivery` に設定 |
| `totals[]` の代引手数料 | UCP v2026-04-08 の `type: "fee"` で分離。`lines[]` 内訳に **`PaymentOption` (`delivery_id`×`payment_id`) 経由で解決した `Payment.charge`** を表示 (`Delivery` に手数料カラムは無い) |
| Abuse 防止シグナル | `fulfillment_options` 計算結果と最終 `totals` の差分検証 |
### Lodging / Food 拡張
UCP は Lodging / Food 業界へ拡張中だが、EC-CUBE は EC コアに集中。将来必要になれば `metadata` 拡張ポイントで対応 (本 issue スコープ外)。
---
## エラーモデル (2 系統)
| 種類 | HTTP ステータス | レスポンス形式 | 検証内容 |
|---|---|---|---|
| **プロトコルエラー** | 4xx / 5xx | 標準 HTTP エラー | 不正リクエスト、認証失敗、サーバーエラー |
| **ビジネスロジックエラー** | **200** | `messages[]` 配列 (`type: error\|warning` / `code` / `severity`) | 在庫切れ、配送不可、決済拒否、価格不一致 |
→ 「在庫切れで HTTP 400」ではなく「**HTTP 200 + `messages[].type: error`**」が正しい仕様。
#### 追加対応 (escalation) はエラーではなく中間状態 (2026-06-17 追記)
**`requires_escalation` (continue_url を伴う buyer ハンドオフ) は上記いずれの「エラー」にも該当しない。** complete が返す**正常な中間状態**であり、`status: requires_escalation` (HTTP 200) + `continue_url` (MUST・絶対 HTTPS) で表現する。複数回呼ばれる状態機械である点が要 (詳細は #6777「7. PaymentHandler 抽象 + complete 状態機械」)。
- **`severity` の正準語彙** (一次仕様 `checkout.md:122-145` で確認済): `recoverable` / `requires_buyer_input` / `requires_buyer_review` / `unrecoverable` の **4 値**。`requires_*` 系が `status: requires_escalation` に寄与する。※下記「追記 (2026-06-11)」に記した SwagUcp 由来の `recoverable|fatal` は **誤り** (SwagUcp の独自表現)。実装は一次仕様の 4 値に従う。
- **決済 FAILED 後の遷移**: 再開 complete で決済が失敗した場合は常に在庫を rollback (解放) し、severity が `recoverable` / `requires_buyer_*` なら `ready` 据置 (再 complete 可)、`unrecoverable` なら `canceled`。
- **continue_url の寿命**は `expires_at` に紐づく (`checkout.md:483`)。escalation/`requires_action` 中は在庫を引き当てたまま保持し、その確保期限は `eccube.yaml` の `eccube_agent_checkout_escalation_expire` (既定 15 分) で管理する (#6777)。超過で `findExpired` → rollback → `expired`。
---
## テスト計画
### Layer 0: 仕様適合性 (Spec Conformance / 要件トレーサビリティ)
`specifications/ucp/` 配下の一次仕様 (UCP `v2026-04-08`) の各**規範要件 (MUST / MUST NOT / SHALL / REQUIRED)** を検証テストケースに紐付け、**「どの仕様要件を満たし、実装済みか」を追跡可能**にするためのレイヤー。Layer 1〜7 の機能テストとは独立した観点で、横断的に要件の充足状況をスナップショットする。公式 `conformance` (Layer 7) は HTTP 経由のブラックボックス検証だが、本レイヤーは**個々の MUST 文を 1:1 でコードに紐付ける**点で補完的。
**方針:**
- **1 テストケース = 1 規範要件**。テストメソッドの docblock に**仕様書の GitHub URL** (該当行アンカー付き) を記載する。
- **assertion の第3引数 (失敗メッセージ) に仕様要件文をそのまま記載**する (例: `'MUST be an absolute HTTPS URL'`)。テスト失敗時に**どの MUST 違反か**がメッセージから直接読め、レビュー時も「コード ↔ 仕様」を 1:1 で確認できる。
- **未実装の要件は `$this->markTestIncomplete()` で `incomplete` とする**。CI 上で「未充足要件」として可視化し、実装完了時に incomplete を外す運用とする。
- **要件カタログ (全 MUST 一覧) の網羅的な管理は別 issue** とし、本 issue にはテスト計画と**サンプル数本のみ**を示す。本テスト群はそのカタログに対する**実装状況の生きたスナップショット**として機能する。
- URL は版固定の参照とする (UCP docs は `v2026-04-08` 時点)。行番号は仕様更新で前後しうるため、参照は**要件文の文面**を正とする。
> **対象スコープ**: 本 issue は UCP Checkout / Order capability が対象。Catalog capability の規範要件は #6794、discovery profile (`/.well-known/ucp`) と署名鍵 JWK の要件は #6777 / #6794 のトレーサビリティに委ねる (本レイヤーでは checkout エンドポイントが参照する範囲に限定)。
#### サンプルテスト
> 以下はパターンを示す**サンプル**。実装済みの要件はそのまま assert し、未実装は `markTestIncomplete()` とする。
**UCP Checkout REST 適合性 (ヘッダ・冪等性・2 系統エラー):**
```php
// tests/Eccube/Tests/Service/AgentCommerce/Conformance/UcpCheckoutConformanceTest.php
/**
* UCP Checkout REST binding conformance.
*
* @see https://github.com/Universal-Commerce-Protocol/ucp/blob/main/docs/specification/checkout-rest.md
* (v2026-04-08)
*/
class UcpCheckoutConformanceTest extends AbstractWebTestCase
{
public function testEndpointsServedOverHttps(): void
{
// "All REST endpoints MUST be served over HTTPS with minimum TLS version 1.3."
// @see https://github.com/Universal-Commerce-Protocol/ucp/blob/main/docs/specification/checkout-rest.md#L41
$this->markTestIncomplete('未実装: 全エンドポイントの HTTPS/TLS1.3 強制 (配信は #6777 設置形態に依存)');
}
public function testIdempotencyKeyReuseWithDifferentParamsReturns409(): void
{
// "When provided, the server MUST: ... Return 409 Conflict if the key is
// reused with different parameters."
// @see https://github.com/Universal-Commerce-Protocol/ucp/blob/main/docs/specification/checkout-rest.md#L1257
$this->markTestIncomplete('未実装: Idempotency-Key 再利用 (異パラメータ) で 409 Conflict');
}
public function testUnavailableMerchandiseReturnsHttp200WithMessages(): void
{
// 2 系統エラー: "Business outcomes ... are returned with HTTP 200 and the
// UCP envelope containing messages". 在庫切れは 4xx ではない。
// @see https://github.com/Universal-Commerce-Protocol/ucp/blob/main/docs/specification/checkout-rest.md#L1294
$client = $this->createClientWithScope('ucp:checkout');
// 在庫切れ商品で CheckoutSession を作成 → 確定
$response = $this->completeWithOutOfStockItem($client);
self::assertSame(200, $response->getStatusCode(),
'MUST: Business outcomes are returned with HTTP 200 and the UCP envelope containing messages.');
$body = json_decode($response->getContent(), true);
self::assertArrayHasKey('messages', $body,
'MUST: Business outcomes are returned with HTTP 200 and the UCP envelope containing messages.');
}
public function testConfirmationEmailSentAfterCompletion(): void
{
// "Businesses ... MUST send a confirmation email after the checkout has been completed."
// @see https://github.com/Universal-Commerce-Protocol/ucp/blob/main/docs/specification/checkout.md#L540
$this->markTestIncomplete('未実装: complete 後の確認メール送信');
}
}
```
**UCP escalation (`continue_url`) 適合性:**
```php
// tests/Eccube/Tests/Service/AgentCommerce/Conformance/UcpEscalationConformanceTest.php
public function testContinueUrlIsAbsoluteHttpsWhenEscalating(): void
{
// "Businesses MUST provide continue_url when returning status = requires_escalation."
// "The continue_url MUST be an absolute HTTPS URL".
// @see https://github.com/Universal-Commerce-Protocol/ucp/blob/main/docs/specification/checkout.md#L470
// @see https://github.com/Universal-Commerce-Protocol/ucp/blob/main/docs/specification/checkout.md#L478
$session = $this->mapper->toUcpSession($this->createEscalatingOrder());
self::assertArrayHasKey('continue_url', $session,
'MUST: Businesses MUST provide continue_url when returning status = requires_escalation.');
self::assertStringStartsWith('https://', $session['continue_url'],
'MUST: The continue_url MUST be an absolute HTTPS URL.');
}
```
### Layer 1: UCP スキーマ契約テスト (外部接続なし)
- UCP 公式リポジトリの OpenAPI / JSON Schema を `tests/fixtures/ucp/` にベンダリング、**`v2026-04-08` pin**
- `justinrainbow/json-schema` で request/response を validate
- 公式 **`ucp-schema`** (Rust) を CI に導入し、`allOf` 合成・`ucp_request`/`ucp_response` annotations を解決:
```bash
cargo install ucp-schema
ucp-schema validate payload.json --op create --schema-local-base ./schemas
ucp-schema lint schemas/
```
- v2026-04-08 追加分を厳密テスト: `totals` 開放型 type / 負数 / `type:"fee"` の `lines`、複数行 LineItem、`GET /checkout-sessions/{id}`
### Layer 2: エンドポイント単体 (PHPUnit、認証/決済モック)
| エンドポイント | 検証ケース |
|---|---|
| `POST /checkout-sessions` | 通貨必須、複数行 LineItem、不正商品 |
| `GET /checkout-sessions/{id}` | 存在確認、未存在で 404、他マーチャントのセッション遮断 |
| `PUT /checkout-sessions/{id}` | 配送先変更による再計算、状態遷移バリデーション |
| `POST /.../{id}/complete` | 注文確定、`Order.agent_protocol=ucp` 記録、permalink 返却、**`requires_escalation` (HTTP 200) → `continue_url` (絶対 HTTPS) → 再開 complete → completed**、**非同期 `complete_in_progress` → IPN/Webhook で確定**、決済 FAILED 後の severity 別遷移 (`ready` / `canceled`) |
| `Get Order` | currency 必須、platform 認証 |
→ **エラー 2 系統**(プロトコル 4xx/5xx vs ビジネス 200+`messages[]`)を両方網羅。
### Layer 3: マッピング層 (PHPUnit)
- 住所・購入者・金額の変換 (共通基盤利用)、numeric→alpha-2、代引 `PaymentOption`→`Payment.charge`、配送日数 `DeliveryDuration.duration` max
- `totals` の負数・`lines` 内訳・開放型 type
- 標準ページの絶対 URL (`help_privacy` / `help_agreement` から自動生成) → UCP `links[]`
### Layer 4: 認証・署名層
- `ucp:checkout` scope の認可、capability boundary
- `UcpMessageSigner`: **EC P-256 JWK** での RFC 9421 署名生成 → 検証ラウンドトリップ、鍵ローテーション、不正署名 reject (テスト鍵 `tests/fixtures/keys/` は EC P-256)
- `/.well-known/ucp` profile の妥当性 (`signing_keys[]` が EC 公開鍵 JWK・秘密鍵非混入、reverse-domain キー、`Cache-Control`)
- Abuse 防止シグナルの伝達検証
### Layer 5: SLO 計測 (PHPUnit + Symfony Stopwatch、nightly)
Google Native Checkout ドキュメントのエンドポイント別 SLO に基づき計測 (CI ノイズが大きく参考値扱い):
| エンドポイント | 可用性 | p50 | p95 |
|---|---|---|---|
| `POST /checkout-sessions` | ≥ 95% | ≤ 1 s | ≤ 4 s |
| `PUT /checkout-sessions/{id}` | ≥ 95% | ≤ 1 s | ≤ 5 s |
| `POST /checkout-sessions/{id}/complete` | ≥ 95% | ≤ 6 s | ≤ 10 s |
| `GET /checkout-sessions/{id}` | ≥ 95% | ≤ 1 s | ≤ 4 s (要仕様確認) |
### Layer 6a: Google Merchant Center Diagnostics (日本でも利用可能)
- Merchant Center テストアカウント (#6794 と共用) に Shopping Feed を登録し、Diagnostics → IDP で errors / warnings / notifications を分類確認
- → UCP 専用ではなく Shopping Feed の汎用検証だが、UCP eligible 商品の判定に影響
### Layer 6b: UCP Sandbox (GA 後の別タスク)
- 米国・カナダ・豪のみの限定パイロット。**日本では現時点で利用不可**のためスコープ外
### Layer 7: 公式 conformance test + モックエージェント E2E
- **`Universal-Commerce-Protocol/conformance`** (Python/uv、Google 公式推奨) を CI から実行。13 のテストファイル (checkout_lifecycle / idempotency / order / protocol / ap2 / binding / business_logic / card_credential / fulfillment / invalid_input / simulation_url_security / validation / webhook) を EC-CUBE ローカルサーバーに向けて実行
- `Universal-Commerce-Protocol/samples` の flower_shop fixture を活用
- Codeception ベースの独自 E2E (`tests/E2E/Ucp/`) で EC-CUBE 固有挙動 (Customize 拡張、PurchaseFlow、Stripe 連携) を補完
### CI 統合
- Layer 0 (仕様適合性): PR ごと。`incomplete` 件数を集計し「未充足要件」として可視化 (件数の増加は要 review)
- Layer 1〜4: PR ごと (Layer 1 の `ucp-schema` は cargo install をキャッシュ)
- Layer 5: nightly (レイテンシ分布を artifact 化)
- Layer 6a: 手動 trigger (`workflow_dispatch`)
- Layer 6b: GA 後
- Layer 7: PR ごと smoke (主要 3 ファイル) + nightly 13 ファイル全件
---
## 技術的考慮事項
1. **共通基盤 #6777 への依存**: CheckoutSession、住所/金額マッピング、PurchaseFlow 連携、OAuth 2.0 認証基盤は共通基盤側で整備
2. **既存購入フローとの共存**: 既存 `ShoppingController` と UCP 経由フローを並列稼働
3. **`PurchaseFlow` の再利用**: 既存プロセッサで税・送料・在庫を再計算
4. **セッション管理**: 有効期限管理とクリーンアップコマンド
5. **決済処理**: UCP PaymentHandler (`exchangePaymentToken()`) と Stripe UCP 決済ハンドラの連携。`exchangePaymentToken()` の戻り値は `authorize()` に渡る中立 payment データで、**`authorize()` / `capture()` は `void` でなく `PaymentOutcome` (COMPLETED / REQUIRES_ACTION / PENDING / FAILED) を返す** (#6777 改訂)。`authorize()` は状態機械の入口で、初回と再開 (escalation 完了後) の双方から呼ばれ、REQUIRES_ACTION 時の `actionData` に `continue_url` 相当を載せる
6. **SLO 準拠**: Phase 5 で継続監視 (Prometheus exporter は本 issue 範囲外)
7. **MCP transport**: 別タスクの EC-CUBE MCP Server と統合点を設計
8. **app/Customize 拡張**: マッピングは Service 経由で差し替え可能に
9. **eccube-api4 前提依存**: OAuth 2.0 認可サーバーを再利用。未インストール時は 503 / 機能フラグ。詳細は #6777 参照
---
## 既存実装の参考箇所
- `src/Eccube/Controller/ShoppingController.php`
- `src/Eccube/Entity/Order.php`
- `src/Eccube/Service/PurchaseFlow/PurchaseFlow.php`
---
## 追記 (2026-06-11): 認証モデルの確定(UCP 一次仕様 + 参照実装 SwagUcp で裏取り)
Shopware の UCP 参照実装 **SwagUcp**(`agentic-commerce-lab/SwagUcp`・MIT/Apache-2.0)を精読し、UCP 一次仕様と突合した結果、当初想定を訂正する。
### インバウンド認証は RFC 9421 署名が標準・**OAuth2/api4 非依存**
- `UCP-Agent` ヘッダ(profile URL)が全リクエストで必須、認証は **RFC 9421 HTTP Message Signatures**(`Signature-Input`/`Signature`)。専用仕様 `signatures.md`。(一次仕様 `checkout-rest.md:1252,1359-1367` / `order-rest.md:247`)
- → 主ゲートは **`UcpRequestSignatureVerifier`**: エージェントの `/.well-known/ucp` から公開鍵(EC P-256)を取得し署名検証 + `UCP-Agent` profile 解析 + ドメイン許可リスト。**eccube-api4 に依存しない**。
- eccube-api4 [#188](https://github.com/EC-CUBE/eccube-api4/issues/188)(client_credentials)は **ACP 寄りの前提**で、UCP checkout のクリティカルパスから外す。会員 ID 連携(`ucp:identity`)のみ eccube-api4 [#189](https://github.com/EC-CUBE/eccube-api4/issues/189) 待ち。
- 既存の `AgentCommerceOAuth2Authenticator` は ACP / `ucp:identity` 用に残す。
- 注: SwagUcp は detached JWS だが**仕様は RFC 9421**。spec 準拠で実装する。
### status 正準語彙
- UCP の正準ステータスは `incomplete` / **`ready_for_complete`** / `completed` / `canceled`(`checkout.md:105`)。共通基盤マスタ `mtb_checkout_session_status` は `ready` を使用 → UcpCheckoutSessionMapper で `ready`↔`ready_for_complete` を変換(または name を合わせる)。
### SwagUcp から取り入れるパターン
- **capability negotiation**: 交集合 + `extends` の推移的 pruning + date バージョン互換(`platform<=business`)。
- discovery のバージョン別プロファイル形状、エラー `messages[]` の `severity: recoverable|fatal`。
- ただし SwagUcp が spec 逸脱する点(署名=detached JWS / エラー=HTTP 4xx 寄り・カート隔離やローテーション未実装)は**採らず一次仕様に従う**。カート隔離(`agent_owned`)・status マスタ化・鍵ローテーション・schema 検証・Idempotency は EC-CUBE が先行。
### 共通基盤の前提
- CheckoutSession 中核・shopping flow 再利用・ゲスト基準線・`Cart.agent_owned` 隔離・マスタ化は #6777(PR #6825)で landing 済み。本トラックは checkout エンドポイント + 署名検証に集中。
---
## 追記 (2026-06-17): complete を「中断→再開」状態機械として再設計(#6777 追随)
共通基盤 #6777 で、追加対応 (escalation) はエラーでなく complete が返す正常な中間状態であり、complete は状態機械であることを確定した(#6777「7. PaymentHandler 抽象 + complete 状態機械」)。本 issue (UCP) は以下を前提に実装する:
- **complete は状態機械**。買い手の追加対応が必要な場合 `status: requires_escalation` (HTTP 200) + `continue_url` (MUST・絶対 HTTPS) を返し、買い手が business UI で完了後に再開 complete or order webhook で `completed`。状態遷移・在庫引当の保持/回収・トランザクション境界は #6777 `AgentCheckoutCompletionService` へ委譲する。
- **正規化ステータス対応**: UCP `requires_escalation` → `requires_action`、`complete_in_progress` → `in_progress`、`ready_for_complete` → `ready`(#6777 で `mtb_checkout_session_status` に `requires_action`/`in_progress` を追加)。`UcpCheckoutSessionMapper` で双方向変換。UCP 固有 status は `metadata` 保持。
- **決済ハンドラの戻り値は `PaymentOutcome`** (COMPLETED / REQUIRES_ACTION / PENDING / FAILED)。`exchangePaymentToken()` の戻り値が `authorize()` に渡る。REQUIRES_ACTION 時の `actionData` に `continue_url` 相当を載せる。
- **決済 FAILED 後の遷移**: 常に rollback で在庫解放し、severity (`recoverable`/`requires_buyer_*` → `ready` / `unrecoverable` → `canceled`)。**severity 正準は 4 値**(`recoverable`/`requires_buyer_input`/`requires_buyer_review`/`unrecoverable`)。「追記 (2026-06-11)」の `recoverable|fatal` は SwagUcp 由来の誤りで、一次仕様の 4 値に従う。
- **非同期確定**: `complete_in_progress` は IPN/Webhook で確定通知を受け `complete()` 再開経路で commit(セッションレス IPN・`OrderStateMachine` ガード・`sample-payment-plugin::receiveComplete` と同型)。GET ポーリングは不採用。
- **在庫確保期限**: escalation/`requires_action` 中は在庫を引き当てたまま保持し、`continue_url` 寿命が紐づく `expires_at` を `eccube.yaml` の `eccube_agent_checkout_escalation_expire`(既定 15 分)で管理。超過で `findExpired` → rollback → `expired`。
- **Idempotency は共通基盤 #6777 の DB 一意制約ベース store を consume**(`dtb_agent_checkout_idempotency` の `unique(idempotency_key, subject)`)。当初の Symfony Lock + cache 方式(ノードローカル)から作り替え、単一の共有 DB だけでマルチインスタンス (AWS 等) の越境・並行二重実行を防ぐ。`subject` に認証済みエージェント (UCP-Agent profile) を保持し越境リプレイを防止。本 PR は consume のみ(Idempotency-Key 必須/409 は controller 層)。
Contributor guide
Assessment
This issue has not been assessed yet.