Skip to content

feat(agent-commerce): Product Feed/Catalog/Discovery を実装 (#6794 トラックA)#6815

Open
nanasess wants to merge 10 commits into
EC-CUBE:4.4from
nanasess:feature/agentic-commerce-feed
Open

feat(agent-commerce): Product Feed/Catalog/Discovery を実装 (#6794 トラックA)#6815
nanasess wants to merge 10 commits into
EC-CUBE:4.4from
nanasess:feature/agentic-commerce-feed

Conversation

@nanasess

@nanasess nanasess commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

概要 (Overview・Refs Issue)

エージェントコマース共通基盤 Phase 1a (#6802) の上に、CheckoutSession に依存しないトラック A (Product Feed / Catalog / Discovery) を 1 PR で実装します。UCP Discovery + UCP Catalog は国内で活用可能なエコシステムの入口として独立した価値があり、checkout (#6776 / #6574) の完成を待たず先行リリースできます。

このPRは #6802 にスタックしています (派生元 feature/agentic-commerce-base)。#68024.4 にマージされるまで、本PRの diff には #6802 の変更も含まれて見えます。#6802 マージ後に 4.4 へ rebase すると本PRは #6794 分のみに整理されます。レビューは #6802 マージ後を推奨します。

方針 (Policy)

  • eccube-api4 非依存: public read の REST・discovery・ACP feed (outbound Bearer) はすべて api4 を必要としません。追加依存は symfony/http-client (ACP push 用・first-party) のみ。
  • UCP Catalog は POST RPC (/catalog/search/catalog/lookup/catalog/product)。一次仕様 v2026-04-08 (source/services/shopping/rest.openapi.json) で確認。capability dev.ucp.shopping.catalog.search / .lookup。read-only のため RFC 9421 署名は OPTIONAL (discovery の公開鍵広告のみ)。
  • ACP Feed は push モデル: products.jsonl + metadata.json を生成し OpenAI がホストする Feed API へ送出。実機送出は国内 GA 前のため interface 分離 + 認証情報 (base URL + API key) の有無でガードし、テストは MockHttpClientpush 前のスキーマ検証 (justinrainbow/json-schema) を client 側で必須化 (サーバ ack は {accepted:bool} と粗いため)。
  • フラグ方針 (2026-06-09 整理): discovery / catalog は公開して害がないため常時公開(フラグでゲートしない)。*_checkout_enabled (ACP/UCP 共通基盤: エージェントコマース向け CheckoutSession・マッピング層の整備 #6777) は checkout の有無のみを制御する (日本未提供のため既定 OFF・本 PR では未配線、ユニバーサル コマース プロトコル(UCP)の対応 #6574/Agentic Commerce Protocol (ACP) の対応 #6776 で使用)。feed push は認証情報でガード(専用フラグなし)。ucp_catalog_requires_auth は catalog の OAuth 必須モード (api4) 用に残置。
  • 管理画面 + CLI の 2 系統を標準提供 (どちらか一方に依存しない運用を保証)。運用操作ページは /admin/content/agent_commerce (コンテンツ管理メニュー)。定期同期 (cron/Scheduler/Messenger) は opt-in。
  • 秘密鍵は app/keystore/ (共通の鍵保管ディレクトリ app/keystore/ の導入 (各種暗号鍵・シークレットの標準置き場) #6797)、公開鍵 JWK は UcpMessageSigner::getPublicJwks() で実行時導出。signing_keys[] に秘密鍵を混入しない。
  • well-known はオリジン単位・パスは RequestContext 動的生成 (ハードコードしない)。画像絶対 URL も RequestContext の scheme/host/port/baseUrl から組み立て (TRUSTED_PROXIES は RequestContext 経由で考慮)。

実装に関する補足 (Appendix)

新規クラス (主要):

クラス 役割
Catalog/AgentCatalogItemDto ほか DTO 群 protocol 非依存の商品 DTO (immutable readonly)
Catalog/ProductReferenceResolver sku/product_class_id/barcode → ProductClass (barcode は Customize seam)
Catalog/CatalogProvider 表示商品の iterable 供給 (toIterable + 定期 clear() でメモリ蓄積回避)
Catalog/CatalogMapper Product/ProductClass → DTO (表示商品のみ・price02→minor unit・availability・画像絶対 URL)
Catalog/Acp/AcpFeedProductSerializer / AcpFeedGenerator / AcpFeedValidator / AcpFeedClient ACP shape 変換 / products.jsonl+metadata.json 生成 / pre-push スキーマ検証 / push クライアント
Catalog/Ucp/UcpCatalogProductSerializer / UcpCatalogResponseBuilder / UcpCatalogCache UCP shape (price_range 算出) / ucp wrapper / Filesystem+Lock+gzip キャッシュ
Controller/AgentCommerce/UcpCatalogController POST /catalog/{search,lookup,product} (常時公開・gzip)
Discovery/UcpProfileBuilder / Controller/AgentCommerce/UcpDiscoveryController /.well-known/ucp profile 組み立て / 配信 (Cache-Control public max-age=300)
Discovery/PaymentHandlerRegistryInterface (+ Empty 実装) payment_handlers のプラグイン寄与 seam (tagged・既定空)
Command/AgentCommerce/* eccube:acp-feed:push --full / eccube:ucp-catalog:cache:warmup / :clear
Controller/Admin/AgentCommerce/AgentCommerceController 管理画面 (/admin/content/agent_commerce・push / キャッシュ操作・CSRF 検証・検証目的限定の注記)
EventListener/AgentCommerce/CatalogCacheInvalidationListener Product/ProductClass 更新で UCP キャッシュ無効化

設定・本体:

  • composer.json/lock: symfony/http-client 追加
  • app/config/eccube/services.yaml: interface alias / env bind (ACP feed base URL・API key) / cache dir bind
  • app/config/eccube/packages/framework.yaml: lock: flock (外部インフラ不要・共有レンサバ適合)
  • app/config/eccube/packages/eccube_nav.yaml: コンテンツ管理メニューに agent_commerce 追加
  • src/Eccube/Resource/locale/messages.{ja,en}.yaml: 翻訳キー追加
  • spec schema はリポジトリに同梱しない (Apache-2.0 ライセンス露出回避):
    • ACP: src/Eccube/Resource/AgentCommerce/Acp/schema.feed.json のみ同梱 (runtime の pre-push 検証に必須・出所/Apache-2.0 を src/Eccube/Resource/AgentCommerce/README.md に明記)。契約テストもこれを再利用
    • UCP: 同梱せず、CI (unit-test.yml/coverage.yml) で公式リポジトリを var/ に clone (commit 固定)。ローカルは ECCUBE_UCP_SCHEMA_DIRspecifications/ucp フォールバック、無ければ該当テスト skip。手順は tests/Eccube/Tests/Service/AgentCommerce/README.md

レビュー時の注意:

  • api4 依存機能は意図的に未実装 (TODO コメント): UCP Catalog の OAuth2 認証必須モード (ucp_catalog_requires_auth、検証は Phase 1b の AgentCommerceOAuth2Authenticator 待ち) と GraphQL 版。
  • ACP capability discovery (/.well-known/acp.json) は Status:Proposal/unreleased のため未実装。
  • UCP lookup の variants[].inputs (input_correlation) は未実装: lookup_response は要求 id との相関を MUST とするが、要求 id→variant 相関の実装は本 PR 範囲外。スキーマ契約テストが markTestIncomplete で要件を追跡 (UcpCatalogController::lookup に TODO)。search / get_product は適合済。

テスト (Test)

tests/Eccube/Tests/Service/AgentCommerce/ (+ Web/AgentCommerce) に Layer 0 (適合性) / 1 (マッピング) / 2 (スキーマ契約・DB 突合) / 3 (Web: catalog/discovery) / 5 (push) を追加。

  • PHPUnit: 179 tests / 944 assertions / 0 failures・0 errors (Incomplete 7 = 意図的 markTestIncomplete。Layer 3'/Layer 9 + UCP lookup inputs へ委譲)
  • PHPStan level 6 (paths: src): No errors
  • php-cs-fixer: 0 修正
  • rector dry-run: clean
  • ローカルは SQLite (4.4 既定 DB)。PostgreSQL/MySQL は CI マトリクス。

curl での動作確認 (誰でも試せる)

discovery / catalog は認証不要・常時公開なのでそのまま叩けます。ベース URL は環境に合わせて変更してください (例は symfony servehttps://127.0.0.1:8000、自己署名証明書のため -k を付与)。

BASE=https://127.0.0.1:8000

# 1) UCP Discovery (GET) — ucp.version / services / capabilities / signing_keys を確認
curl -k "$BASE/.well-known/ucp" | jq .
curl -k -i "$BASE/.well-known/ucp"            # Cache-Control: public, max-age=300 等のヘッダ確認

# 2) UCP Catalog search (POST・空ボディ=先頭ページ。pagination.limit で件数指定)
curl -k -X POST "$BASE/catalog/search" \
  -H 'Content-Type: application/json' \
  -d '{"query":"ジェラート","pagination":{"limit":5}}' | jq .

# 3) UCP Catalog lookup (商品 ID 配列)
curl -k -X POST "$BASE/catalog/lookup" \
  -H 'Content-Type: application/json' \
  -d '{"ids":["1","2"]}' | jq .

# 4) UCP Catalog product (単一商品・必須 id。未存在 id は 404)
curl -k -X POST "$BASE/catalog/product" \
  -H 'Content-Type: application/json' \
  -d '{"id":"1"}' | jq .

# 5) gzip ネゴシエーション (Content-Encoding: gzip / Vary: Accept-Encoding を確認)
curl -k -sD - -o /dev/null -X POST "$BASE/catalog/search" \
  -H 'Content-Type: application/json' -H 'Accept-Encoding: gzip' -d '{}'

確認ポイント:

  • 価格は税込 (price = price02_inc_tax)。list_priceprice01 > price02 (割引時) のみ税込で出力。商品詳細ページと一致します。
  • レスポンス内の URL は RequestContext 由来 (例 https://127.0.0.1:8000/...)。
  • ACP Feed は OpenAI への outbound push のため curl 不可。確認は認証情報をモック feed service に向けて bin/console eccube:acp-feed:push --feed-id <id> --full を実行 (未設定時は AcpFeedException で停止)。
  • コードやデータ変更後に値が古い場合は UCP Catalog のキャッシュが原因 → bin/console eccube:ucp-catalog:cache:clear (または管理画面のキャッシュクリア)。商品更新時は EventListener が自動クリア。

相談 (Discussion)

マイナーバージョン互換性保持のための制限事項チェックリスト

  • 既存機能の仕様変更はありません (追加はすべて新規クラス・新規ルート・default false フラグの参照のみ)
  • フックポイントの呼び出しタイミングの変更はありません
  • フックポイントのパラメータの削除・データ型の変更はありません
  • twigファイルに渡しているパラメータの削除・データ型の変更はありません
  • Serviceクラスの公開関数の、引数の削除・データ型の変更はありません (新規クラスのみ)
  • 入出力ファイル(CSVなど)のフォーマット変更はありません

レビュワー確認項目

  • 動作確認
  • コードレビュー
  • E2E/Unit テスト確認(テストの追加・変更が必要かどうか)
  • 互換性が保持されているか
  • セキュリティ上の問題がないか
    • 権限を超えた操作が可能にならないか (UCP Catalog/discovery は公開商品データのみ常時公開・ACP push は認証情報でガード・管理操作は CSRF 検証)
    • 不要なファイルアップロードがないか
    • 外部へ公開されるファイルや機能の追加ではないか (well-known/catalog は公開しても害のない情報のみを常時公開・checkout のみ店舗設定でフラグ制御)
    • テンプレートでのエスケープ漏れがないか

🤖 Generated with Claude Code

Summary by CodeRabbit

リリースノート

  • 新機能
    • エージェントコマース(Agent Commerce)機能を追加(ACP/UCP 連携、UCP Discovery 公開、カタログ検索/参照エンドポイント提供)。
    • 管理画面に Agent Commerce の操作パネルと、ACP/UCP の checkout 有効化設定を追加。
    • UCP カタログのキャッシュ生成/クリアを追加。
  • 改善
    • 住所連携に必要な ISO 国コード対応を追加。
  • ドキュメント
    • ACP/UCP 関連の管理文言(日本語・英語)を追加。
  • テスト
    • Agent Commerce 関連の包括的なテストスイートを追加。

nanasess and others added 7 commits June 4, 2026 16:07
ACP/UCP 対応の共通基盤のうち CheckoutSession 非依存の先行スライスを実装。
トラック A (Product Feed / Discovery) を解放する最小集合。

- MinorUnitConverter: 通貨 minor unit 変換 (bcmath / ゼロデシマル / 負数)
- AddressMappingService: 住所マッピング・国コード numeric→alpha-2 (全249件網羅)
- AgentCommerceScopeRegistry: <protocol>:<capability> scope 照合
- KeyStoreInterface / FilesystemKeyStore: 鍵保管 (env パス上書き→既定ファイル、EC-CUBE#6797 雛形)
- AgentCommerceMessageSignerInterface / UcpMessageSigner: RFC 9421 EC P-256 / JWK 公開鍵 / 鍵ローテーション grace
- BaseInfo にフラグ5 (acp/ucp 有効化等、default false) + google_pay_merchant_id + migration
- 秘密鍵は dtb_baseinfo でなく app/keystore/ に保管 (EC-CUBE#6797、.htaccess/.gitignore 多重防御)

プライバシー/利用規約 URL はカラム化せず標準ページ (help_privacy/help_agreement) から自動生成する方針。
検証: PHPUnit 53 tests / 611 assertions / 0 失敗、PHPStan level6 No errors、php-cs-fixer 0件。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
minor-unit は通貨の小数桁数だけ桁が増える (一律 ×100 ではない) ことが
一目で分かるよう、docblock の例を JPY (×1) / USD (×100) の 2 桁までに統一。
3 桁通貨 (BHD 等) の 4 桁例は紛らわしいため削除。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ACP/UCP 用の 6 カラムは BaseInfo エンティティに #[ORM\Column] で定義済みのため、
公式アップデート手順 (doctrine:schema:update --force) で自動反映される。
カラム追加に ALTER TABLE マイグレーションを書かないのが EC-CUBE の慣例
(前例 PR EC-CUBE#4912: カラム追加に ALTER マイグレーション無し、INSERT のみ)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CI の rector ジョブ (PR EC-CUBE#6802) で検出された 8 ファイルの指摘を vendor/bin/rector で適用。
- AddressMappingService: 冗長な (int) キャストと三項を null 合体演算子に簡約
- UcpMessageSigner: コンストラクタプロパティ昇格
- テスト各種: self:: → $this->、final class 化、strict_types 宣言等

AgentCommerce テスト 53 件すべて成功を確認。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
決済はプラグイン化方針 (Stripe 同様) のため、Google Pay の merchant_id は
core BaseInfo に持たせない。UCP discovery の payment_handlers は決済ハンドラ
プラグインが寄与する設計とする。あわせて rector 適用後の php-cs-fixer 整形
(ライセンスヘッダ直後の空行) をテストへ反映。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
AddressMappingService の numeric->alpha-2 ハードコード const (249件) を廃止し、
新規マスタ mtb_country_iso_code で管理する。PR EC-CUBE#6802 レビュー指摘 (マスタテーブル化) に対応。

- 新規マスタ CountryIsoCode / CountryIsoCodeRepository を追加
- mtb_* の固定スキーマ (id/name/sort_no/discriminator_type) に準拠し、id=ISO numeric / name=alpha-2 を格納 (discriminator=countryisocode)
- 新規インストールは import_csv (definition.yml 登録)、既存環境は INSERT データ migration で backfill (mtb_country は改変しない)
- AddressMappingService はリポジトリ経由解決へ変更、テストを Layer 2 化

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CI rector ジョブ (ContainerGetNameToTypeInTestsRector / AssertFuncCallToPHPUnitAssertRector)
の指摘に対応。get('doctrine') + ManagerRegistry 手動構築をやめ、services_test.yaml で
AddressMappingService を public 化しコンテナから FQCN 取得する方式へ変更。

- services_test.yaml: AddressMappingService を public 化 (consumer 未実装で private では除去されるため)
- AddressMappingServiceTest: self::getContainer()->get(AddressMappingService::class) に簡素化

検証: PHPUnit 52/608、PHPStan level6 No errors、php-cs-fixer 0、rector dry-run クリーン。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown

Review Change Stack

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b79c1237-83d4-4de5-9df9-0e8d2a8970b9

📥 Commits

Reviewing files that changed from the base of the PR and between 0c0cdc0 and 4f69656.

⛔ Files ignored due to path filters (1)
  • composer.lock is excluded by !**/*.lock
📒 Files selected for processing (3)
  • .github/workflows/coverage.yml
  • .github/workflows/unit-test.yml
  • composer.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • composer.json

📝 Walkthrough

Walkthrough

Agent Commerce 向けに、ACP フィード送受信、UCP カタログ/ディスカバリ、関連 DTO/変換、管理画面操作、設定、テスト、CI 用スキーマ取得を追加した。国コード、金額変換、署名鍵管理、カタログキャッシュ無効化も含まれる。

Changes

Agent Commerce

Layer / File(s) Summary
基盤型と認証
src/Eccube/Service/AgentCommerce/*, src/Eccube/Entity/BaseInfo.php, src/Eccube/Entity/Master/CountryIsoCode.php, src/Eccube/Repository/Master/CountryIsoCodeRepository.php, app/config/eccube/services.yaml, app/DoctrineMigrations/Version20260604120000.php, tests/Eccube/Tests/Service/AgentCommerce/MinorUnitConverterTest.php, tests/Eccube/Tests/Service/AgentCommerce/AddressMappingServiceTest.php, tests/Eccube/Tests/Service/AgentCommerce/BaseInfoAgentCommerceFlagsTest.php, tests/Eccube/Tests/Service/AgentCommerce/Security/*, tests/Eccube/Tests/Service/AgentCommerce/Conformance/AgentCommerceBaseConformanceTest.php
MinorUnitConverter、住所変換、スコープ/キーストア/署名、BaseInfo フラグ、CountryIsoCode 実体が追加され、関連テストで変換・公開鍵・既定値が確認されている。
カタログ DTO とマッピング
src/Eccube/Service/AgentCommerce/Catalog/Agent*.php, src/Eccube/Service/AgentCommerce/Catalog/AvailabilityStatus.php, src/Eccube/Service/AgentCommerce/Catalog/CatalogMapper.php, src/Eccube/Service/AgentCommerce/Catalog/CatalogProvider*.php, src/Eccube/Service/AgentCommerce/Catalog/ProductReference*.php, src/Eccube/EventListener/AgentCommerce/CatalogCacheInvalidationListener.php, tests/Eccube/Tests/Service/AgentCommerce/Catalog/CatalogMapperTest.php
Agent Catalog 用 DTO、在庫状態、商品参照解決、CatalogMapperCatalogProvider が追加され、商品/バリアントのマッピングとシリアライズを検証するテストが追加された。
ACP フィードパイプライン
src/Eccube/Resource/AgentCommerce/Acp/schema.feed.json, src/Eccube/Resource/AgentCommerce/README.md, src/Eccube/Service/AgentCommerce/Catalog/Acp/*, src/Eccube/Command/AgentCommerce/AcpFeedPushCommand.php, tests/Eccube/Tests/Service/AgentCommerce/Catalog/Acp/*, tests/Eccube/Tests/Service/AgentCommerce/Schema/AcpFeedSchemaContractTest.php, tests/Eccube/Tests/Service/AgentCommerce/Conformance/AcpFeedConformanceTest.php
ACP の JSON Schema、検証、生成、HTTP クライアント、push コマンドが追加され、Feed/Variant/Metadata の契約テストとシリアライズ/クライアントの単体テストが追加された。
UCP カタログと API
src/Eccube/Service/AgentCommerce/Catalog/Ucp/*, src/Eccube/Controller/AgentCommerce/UcpCatalogController.php, src/Eccube/Command/AgentCommerce/UcpCatalog*.php, tests/Eccube/Tests/Web/AgentCommerce/UcpCatalogControllerTest.php, tests/Eccube/Tests/Service/AgentCommerce/Schema/UcpCatalogSchemaContractTest.php, tests/Eccube/Tests/Service/AgentCommerce/Conformance/UcpCatalogConformanceTest.php
UCP のキャッシュ、レスポンスビルダ、検索/取得 API、キャッシュ管理 CLI、Web/契約テストが追加された。
UCP ディスカバリと公開
src/Eccube/Service/AgentCommerce/Discovery/*, src/Eccube/Controller/AgentCommerce/UcpDiscoveryController.php, tests/Eccube/Tests/Web/AgentCommerce/UcpDiscoveryControllerTest.php, tests/Eccube/Tests/Service/AgentCommerce/Conformance/UcpDiscoveryConformanceTest.php
UCP discovery profile、payment handler レジストリ、公開エンドポイント、Web/Conformance テストが追加された。
設定と管理画面
.github/workflows/coverage.yml, .github/workflows/unit-test.yml, .gitignore, .htaccess, app/keystore/.htaccess, app/config/eccube/packages/*, app/config/eccube/services*.yaml, src/Eccube/Form/Type/Admin/ShopMasterType.php, src/Eccube/Resource/doctrine/import_csv/*, src/Eccube/Resource/locale/messages.*.yaml, src/Eccube/Resource/template/admin/Content/AgentCommerce/index.twig, src/Eccube/Resource/template/admin/Setting/Shop/shop_master.twig, src/Eccube/Controller/Admin/AgentCommerce/AgentCommerceController.php, composer.json, tests/Eccube/Tests/Service/AgentCommerce/README.md
CI の UCP スキーマ取得、keystore 保護、フレームワーク lock 設定、サービス登録、管理画面テンプレート/翻訳、フォーム定義、CSV 定義、管理コントローラ、依存追加、README が追加され、画面と設定の配線が行われた。
Schema 検証共通基盤
tests/Eccube/Tests/Service/AgentCommerce/Schema/SchemaValidatorTrait.php, tests/Eccube/Tests/Service/AgentCommerce/Schema/AcpFeedSchemaContractTest.php, tests/Eccube/Tests/Service/AgentCommerce/Schema/UcpCatalogSchemaContractTest.php
ACP/UCP 契約テストで共通利用する schema 解決と検証トレイトを追加した。

Sequence Diagram(s)

sequenceDiagram
  participant 管理画面
  participant AgentCommerceController
  participant AcpFeedClient
  participant UcpCatalogCache
  participant UcpCatalogResponseBuilder
  participant UcpProfileBuilder
  管理画面->>AgentCommerceController: ACP push / UCP cache 操作
  AgentCommerceController->>AcpFeedClient: pushFullReplacement / upsertProducts
  AgentCommerceController->>UcpCatalogCache: warmup / clear
  AgentCommerceController->>UcpCatalogResponseBuilder: buildSearchResponse
  AgentCommerceController->>UcpProfileBuilder: build()
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related issues

Poem

🐰 ぴょんと跳ねて、基盤ぴかり
ACP と UCP、ふたつの道がつながり
署名きらり、キャッシュはふわり
うさぎの鼻先で、schema もにっこり
にんじん色の変更、はーい完了!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 70.55% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PRタイトルはAgent Commerce Phase 1aの主要機能(Product Feed/Catalog/Discovery)の実装を正確に要約しており、変更セットの中心的な目的を明確に示しています。
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@nanasess nanasess force-pushed the feature/agentic-commerce-feed branch from d26f1bd to 35879ae Compare June 8, 2026 07:52
Comment thread .github/workflows/unit-test.yml Outdated
Comment thread .github/workflows/coverage.yml Outdated
@nanasess nanasess force-pushed the feature/agentic-commerce-feed branch 2 times, most recently from 9d06d61 to 1b58753 Compare June 9, 2026 06:20
Comment thread src/Eccube/Resource/locale/messages.ja.yaml Outdated
BaseInfo の有効化フラグを「checkout の有無を制御する」意味へ整理する。
discovery / catalog は公開して害がないため常時公開とし (ゲート撤去は EC-CUBE#6794)、
ACP feed push は認証情報の有無で実質ガードされるためフラグ不要とする。

- 改名: acp_enabled → acp_checkout_enabled / ucp_enabled → ucp_checkout_enabled
  (getter は isAcpCheckoutEnabled / isUcpCheckoutEnabled)
- 削除: acp_feed_enabled (push は base URL + API key の有無でガード) /
  ucp_catalog_api_enabled (UCP Catalog は常時公開)
- 維持: ucp_catalog_requires_auth (catalog の OAuth 必須モードを api4 着手時に実装)
- dtb_base_info.csv (ja/en) ヘッダを 3 フラグへ更新
- 店舗設定 (ShopMasterType / @admin/Setting/Shop/shop_master.twig) に
  acp_checkout_enabled / ucp_checkout_enabled のトグルを追加 (checkout は日本未提供の注記つき)
- BaseInfoAgentCommerceFlagsTest を 3 フラグへ更新

PHPStan level 6 No errors / php-cs-fixer 0 / 関連テスト green。
カラム変更は schema:update 方式 (ALTER マイグレーションは書かない)。

Refs EC-CUBE#6777
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@nanasess nanasess force-pushed the feature/agentic-commerce-feed branch 5 times, most recently from 8c8cd82 to c162752 Compare June 9, 2026 08:31
…トラックA)

エージェントコマース共通基盤 Phase 1a (EC-CUBE#6777) の上に、CheckoutSession 非依存の
トラック A (Product Feed / Catalog / Discovery) を 1 PR で実装する。

- Foundation: protocol 非依存 DTO (AgentCatalogItemDto 等) / ProductReferenceResolver
  (sku/product_class_id/barcode→ProductClass、barcode は Customize seam) / CatalogProvider
  (toIterable でメモリ蓄積回避) / CatalogMapper (表示商品のみ・price02→minor unit・availability・
  画像絶対 URL は RequestContext の scheme/host/port/baseUrl から組み立て、generateUrl(ABSOLUTE_URL) と整合)
- ACP Feed: products.jsonl/metadata.json 生成 + pre-push スキーマ検証 (justinrainbow/json-schema)
  + push クライアント (AcpFeedClientInterface、outbound Bearer、Error shape 変換)。
  実機送出は認証情報 (ECCUBE_AGENT_COMMERCE_ACP_FEED_BASE_URL / _API_KEY) の有無でガード、
  Bearer は #[SensitiveParameter] (有効化フラグは持たない)
- UCP Catalog: POST /catalog/{search,lookup,product} (v2026-04-08 の RPC 形式)。
  UcpCatalogResponseBuilder (ucp wrapper) / FilesystemAdapter+Lock キャッシュ (stampede 防止) / gzip。
  公開商品データのため常時公開 (フラグ無効化なし)。認証必須モード・GraphQL は api4 未導入のため TODO
- Discovery: GET /.well-known/ucp。常時公開。UcpProfileBuilder (signing_keys=getPublicJwks 公開鍵のみ・
  catalog capability を常時宣言・endpoint は RequestContext 動的生成)、Cache-Control public max-age=300。
  payment_handlers はプラグイン寄与の tagged seam (既定空)。checkout capability は EC-CUBE#6574 で追加
- 管理画面 (content/agent_commerce) + CLI の 2 系統を標準提供: eccube:acp-feed:push --full /
  eccube:ucp-catalog:cache:warmup / :clear、管理画面ボタン (CSRF 検証・検証目的限定の注記)。
  checkout の有効化トグル (ucp_checkout_enabled/acp_checkout_enabled) は店舗設定 (EC-CUBE#6777) 側
- キャッシュ無効化 EventListener (Product/ProductClass 更新→UCP キャッシュ clear)
- 依存追加: symfony/http-client (ACP push 用、first-party)。framework.lock: flock (外部インフラ不要)
- テスト: Layer 0 適合性 / Layer 1 マッピング / Layer 2 スキーマ契約・DB 突合 / Layer 3 Web
  / Layer 5 push。177 tests / 941 assertions / 0 failures (incomplete 7)。
  spec schema は同梱せず: ACP は src/ の runtime リソース再利用、UCP は CI で公式リポジトリの
  リリースタグ v2026-04-08 を取得 (var/・gitignore) + ローカル specifications/ フォールバック、無ければ skip

PHPStan level 6 No errors / php-cs-fixer 0 / rector clean。本 PR は eccube-api4 非依存。

Refs EC-CUBE#6794
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@nanasess nanasess force-pushed the feature/agentic-commerce-feed branch from c162752 to 0c0cdc0 Compare June 9, 2026 08:36
@nanasess nanasess marked this pull request as ready for review June 9, 2026 08:39

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 18

🧹 Nitpick comments (7)
tests/Eccube/Tests/Service/AgentCommerce/Catalog/Acp/AcpFeedClientTest.php (1)

63-325: ⚡ Quick win

pushFullReplacement の単体テストが欠けています。

公開 API の主要メソッドは一通り検証されていますが、AcpFeedClientInterface::pushFullReplacement() 経路だけこのテストクラスで未カバーです。生成物・戻り値形状・送信前検証の回帰を防ぐため、1 ケースでも追加しておくのが安全です。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Eccube/Tests/Service/AgentCommerce/Catalog/Acp/AcpFeedClientTest.php`
around lines 63 - 325, Add a unit test in AcpFeedClientTest that covers
AcpFeedClientInterface::pushFullReplacement(): mirror the existing
upsertProducts tests by using $this->createClient([...]) with a jsonResponse
(e.g. returning ['id'=>'feed_1','accepted'=>true]), call
$client->pushFullReplacement('feed_1', $products) (use $this->validProduct('p1')
to build payload), assert the returned accepted boolean, assert a single
outbound request was recorded, and assert the recorded request uses the
full-replacement HTTP method (PUT) to the feed products endpoint
(self::BASE_URL.'/feeds/feed_1/products') with the expected body shape
(['products'=>$products]); also add a test case that verifies pre-send
validation rejects invalid product payloads and that no request is recorded
(analogous to testUpsertProductsRejectsInvalidPayloadBeforeSending).
tests/Eccube/Tests/Service/AgentCommerce/AddressMappingServiceTest.php (1)

86-161: ⚡ Quick win

CustomerAddress 分岐のテストを追加してください。

Line 81 のシグネチャは CustomerAddress も受け付けますが、現状は Customer / Shipping しか検証していません。CustomerAddress の1ケースを追加して、3系統すべてのマッピング回帰を防ぐのが安全です。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Eccube/Tests/Service/AgentCommerce/AddressMappingServiceTest.php`
around lines 86 - 161, Add a new unit test in AddressMappingServiceTest that
covers the CustomerAddress branch by instantiating a CustomerAddress object,
setting the same fields used in existing tests
(name01/name02/kana/company/postal/pref/addr/country/phone as applicable),
calling $this->service->toAddressArray($customerAddress) and asserting the
mapped keys (family_name, given_name, family_name_kana, given_name_kana,
company, postal_code, region, address1/address2, country, phone) match the
source values; mirror the pattern of testToAddressArrayFromCustomer and
testToAddressArrayFromShipping so all three input types (Customer, Shipping,
CustomerAddress) are covered and prevent regression of the CustomerAddress
mapping branch.
src/Eccube/Service/AgentCommerce/Discovery/EmptyPaymentHandlerRegistry.php (1)

40-43: ⚡ Quick win

同一 reverse-domain キーの衝突がサイレント上書きされています

現在は後勝ちで上書きされるため、プラグイン競合時の原因追跡が難しくなります。重複キー検知時に例外または警告ログを出す実装にしておくと安全です。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Eccube/Service/AgentCommerce/Discovery/EmptyPaymentHandlerRegistry.php`
around lines 40 - 43, The current merge loop in EmptyPaymentHandlerRegistry
(iterating $this->registries and calling collect()) silently overwrites
duplicate keys; update the merge logic in the method containing that loop to
detect duplicates by checking isset($handlers[$key]) before assignment and
either throw a meaningful exception (e.g., DuplicateHandlerKeyException) or emit
a warning via the existing logger with details (conflicting key, originating
registry class or identifier and previous registry) so plugin conflicts are
visible; include the registry identifier when reporting to aid debugging and
ensure behavior is consistent across all $this->registries iterations.
tests/Eccube/Tests/Web/AgentCommerce/UcpDiscoveryControllerTest.php (1)

95-107: ⚡ Quick win

services{} 直列化確認が不足しています

このテスト名は services/payment_handlers 両方を対象にしていますが、raw 文字列での否定検証は payment_handlers のみです。"services":[] も同様に否定アサートを追加して、回帰検知を揃えてください。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Eccube/Tests/Web/AgentCommerce/UcpDiscoveryControllerTest.php` around
lines 95 - 107, In testServicesAndPaymentHandlersAreObjects in
UcpDiscoveryControllerTest (the test method that fetches agent_ucp_discovery and
inspects $raw), add the same raw-string negative assertion used for
payment_handlers to also assert that '"services":[]' does not appear; i.e.,
after the existing assertStringNotContainsString for payment_handlers, add a
matching assertStringNotContainsString('"services":[]', $raw, 'ucp.services MUST
serialize as a JSON object {} (not an array []) even when empty') so the test
verifies both services and payment_handlers serialize as JSON objects rather
than arrays.
src/Eccube/Service/AgentCommerce/Catalog/Ucp/UcpCatalogResponseBuilder.php (1)

36-36: ⚡ Quick win

UCP バージョン定数の重複を解消してください。

UCP_VERSION 定数が UcpCatalogResponseBuilder::UCP_VERSIONUcpProfileBuilder::UCP_VERSION の両方で '2026-04-08' として定義されています。バージョンが変更される際に複数箇所の更新が必要となり、不整合のリスクがあります。

共通の定数クラス(例: UcpConstantsAgentCommerceConstants)を作成し、バージョンを一元管理することを推奨します。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Eccube/Service/AgentCommerce/Catalog/Ucp/UcpCatalogResponseBuilder.php`
at line 36, UCP_VERSION is duplicated in UcpCatalogResponseBuilder::UCP_VERSION
and UcpProfileBuilder::UCP_VERSION; create a single shared constant class (e.g.,
UcpConstants or AgentCommerceConstants) with public const UCP_VERSION =
'2026-04-08' and replace both usages to reference that centralized constant
(e.g., UcpConstants::UCP_VERSION), updating any use-sites and imports to remove
the duplicated definitions.
src/Eccube/Service/AgentCommerce/Discovery/UcpProfileBuilder.php (1)

99-105: ⚖️ Poor tradeoff

エンドポイント導出ロジックに潜在的な脆弱性があります。

Line 105 の preg_replace('#/search$#', '', $searchUrl) は、生成された検索 URL の末尾から /search を削除してカタログサービスのベース URL を導出していますが、以下の問題があります:

  1. ルート名 'agent_ucp_catalog_search' から生成される URL が /search で終わらない場合(例: ルート定義変更、ロケールプレフィックス付与、クエリパラメータ追加など)、正規表現がマッチせず $searchUrl がそのまま endpoint として使用される
  2. このフォールバック動作により、誤った endpoint(/catalog/search など)が profile に含まれる可能性がある

より堅牢な実装として、カタログサービスのベースパスを専用のルート(例: 'agent_ucp_catalog_base')として定義するか、または文字列操作ではなく URL パース(parse_url/dirname)を使用することを推奨します。

代替実装案

案1: 専用ベースルートの定義

# routing
agent_ucp_catalog_base:
    path: /catalog
    # no controller, metadata only

agent_ucp_catalog_search:
    path: /catalog/search
    # ...
$endpoint = $this->urlGenerator->generate(
    'agent_ucp_catalog_base',
    [],
    UrlGeneratorInterface::ABSOLUTE_URL
);

案2: URL パース(現行コードの改善)

$searchUrl = $this->urlGenerator->generate(
    'agent_ucp_catalog_search',
    [],
    UrlGeneratorInterface::ABSOLUTE_URL
);
$parsed = parse_url($searchUrl);
$pathParts = explode('/', trim($parsed['path'] ?? '', '/'));
array_pop($pathParts); // remove 'search'
$basePath = '/' . implode('/', $pathParts);
$endpoint = ($parsed['scheme'] ?? 'https') . '://' 
          . ($parsed['host'] ?? '') 
          . $basePath;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Eccube/Service/AgentCommerce/Discovery/UcpProfileBuilder.php` around
lines 99 - 105, The current endpoint derivation using preg_replace('`#/search`$#',
'', $searchUrl) can fail when the generated URL from
urlGenerator->generate('agent_ucp_catalog_search', ...) does not literally end
with "/search"; update the code to derive a robust base endpoint by either
generating a dedicated base route (use
urlGenerator->generate('agent_ucp_catalog_base', ...,
UrlGeneratorInterface::ABSOLUTE_URL) instead of manipulating the search URL) or,
if keeping a single route, parse the generated $searchUrl with parse_url() and
remove the last path segment (the 'search' segment) to build $endpoint from
scheme, host and the trimmed path; replace the preg_replace usage and ensure
$endpoint always contains the true catalog base URL.
src/Eccube/Service/AgentCommerce/Catalog/Ucp/UcpCatalogProductSerializer.php (1)

208-210: ⚡ Quick win

コメントの記述が不正確です。

Line 209 のコメント「DTO が空 (全 null) の場合でも plain を空文字で出力すると minProperties:1 を満たさない」は論理的に誤りです。実際には ['plain' => ''] を返すことで minProperties:1(最低1つのプロパティ)の制約は満たされます

正しい意図は「DTO が空の場合でも、UCP の minProperties:1 制約を満たすために、最低限 plain を空文字として出力する」だと思われます。コメントを修正してください。

修正案
-     * UCP では Product / Variant の description は必須かつ minProperties:1。
-     * DTO が空 (全 null) の場合でも plain を空文字で出力すると minProperties:1 を満たさない
-     * ため、最低 1 形式 (plain) を保証する。html / markdown も値があれば付与する。
+     * UCP では Product / Variant の description は必須かつ minProperties:1。
+     * DTO が空 (全 null) の場合でも、minProperties:1 制約を満たすために
+     * 最低 1 形式 (plain を空文字) を保証する。html / markdown も値があれば付与する。
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Eccube/Service/AgentCommerce/Catalog/Ucp/UcpCatalogProductSerializer.php`
around lines 208 - 210, Update the inaccurate inline comment in
UcpCatalogProductSerializer (the block explaining UCP description minProperties
behavior) to state that when the DTO is empty we emit ['plain' => ''] to satisfy
UCP's minProperties:1 requirement; replace the current line claiming that
emitting an empty plain string does not satisfy the constraint with wording that
clearly says we output an empty 'plain' property to ensure at least one property
is present, and that html/markdown are added only if non-null.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/coverage.yml:
- Around line 68-71: Replace the tag-only clone in the "Fetch UCP spec schemas
(tag v2026-04-08)" step with a commit-SHA pinned workflow: clone the repo
(shallow), checkout the specific commit SHA
bb409d7489f14b3425510dd6a14166bc7b959409, then verify HEAD equals that SHA
(using git rev-parse HEAD) and fail the job if it does not match; update any
references to the original git clone invocation to use the new
checkout-and-verify flow so the fetched UCP spec is immutably pinned.

In `@app/DoctrineMigrations/Version20260604120000.php`:
- Around line 48-55: The current down() unconditionally deletes all rows from
mtb_country_iso_code which can destroy pre-existing master data; change down()
to be irreversible instead of deleting data: replace the DELETE logic in the
down() method (method name: down, constant: self::NAME, table:
mtb_country_iso_code) with throwing an IrreversibleMigrationException
(Doctrine\Migrations\IrreversibleMigrationException) and include a brief message
stating the migration cannot be safely rolled back to avoid accidental data
loss.

In `@app/keystore/.htaccess`:
- Around line 1-2: 既存の古いディレクティブ (order allow,deny と deny from all) のみだと Apache
2.4 以降の環境で拒否が効かない可能性があるので、互換性のために同じファイルに Apache 2.4 用のディレクティブ Require all denied
を併記してください — 該当箇所は .htaccess 内の "order allow,deny" / "deny from all" のブロックで、そこに
"Require all denied" を追加して mod_access_compat が無効な環境でも確実にアクセスを拒否できるようにしてください。

In `@src/Eccube/Command/AgentCommerce/AcpFeedPushCommand.php`:
- Around line 81-95: The current flow in AcpFeedPushCommand uses
$this->feedClient->getFeedProducts($feedId) then immediately calls
$this->feedClient->upsertProducts($feedId, $products), which re-uploads remote
data and prevents any local changes from being applied; either add a guard to
fail fast while diffing is unimplemented or switch to using locally-generated
upsert payloads. Concretely, replace the no-op path by (a) detecting when
$products came from the remote feed and calling $io->error(...) and return
Command::FAILURE to prevent accidental success, or (b) call a local generator
(e.g. createLocalProductsForUpsert or similar) and pass its result to
upsertProducts instead of $products; update the success/warning messages
accordingly and reference getFeedProducts and upsertProducts to locate the
change.

In `@src/Eccube/Controller/Admin/AgentCommerce/AgentCommerceController.php`:
- Around line 58-59: ucpCatalogCacheWarmup() calls buildDefaultSearchBody()
which uses json_encode(..., JSON_THROW_ON_ERROR) and currently doesn't catch
JsonException; wrap the call to buildDefaultSearchBody() (from
ucpCatalogCacheWarmup()) in a try/catch(JsonException $e) and on catch log the
error via the controller logger (or process logger) and perform an appropriate
redirect/flash error response instead of letting the exception bubble, or
alternatively remove JSON_THROW_ON_ERROR in buildDefaultSearchBody() if you
prefer silent failure; ensure the catch includes the exception message and
context so failures during json_encode are handled gracefully.
- Around line 161-165: The call to buildDefaultSearchBody() can throw
JsonException (it uses JSON_THROW_ON_ERROR) and ucpCatalogCacheWarmup()/the loop
calling $this->ucpCatalogCache->warmup(...) currently doesn't catch it, causing
a 500; wrap the buildDefaultSearchBody() invocation (or the surrounding warmup
loop in AgentCommerceController::ucpCatalogCacheWarmup or the method that
contains this snippet) in a try/catch that catches JsonException (and Exception
as fallback), translate the error into a user-facing admin message (e.g. add
flash/error or use the controller's message display mechanism) and skip/abort
the warmup gracefully instead of rethrowing so the admin UI shows the message
rather than a 500; ensure you reference buildDefaultSearchBody and
$this->ucpCatalogCache->warmup in the fix.

In `@src/Eccube/Controller/AgentCommerce/UcpCatalogController.php`:
- Around line 238-246: decodeCursor currently accepts arbitrarily large offsets
which allows expensive paging; fix it by clamping the parsed offset to a safe
maximum: add a MAX_OFFSET constant (e.g. MAX_OFFSET) near the class and in
decodeCursor parse the offset as you already do, ensure non-negative, then
return min(parsedOffset, self::MAX_OFFSET) (keeping the existing base64/offset
validation). Also ensure callers that call setFirstResult use the clamped value
returned from decodeCursor (reference decodeCursor and any use sites that call
setFirstResult).
- Around line 119-137: The handler allows an unbounded ids array which can cause
excessive DB work; modify the logic in UcpCatalogController (the block using
$ids, $seen, and resolveProductByIdentifier) to first validate and normalize
inputs (cast/filter to strings/ints), deduplicate the identifiers (unique array)
before any calls to resolveProductByIdentifier, enforce a configurable maximum
count (reject or trim and return an error/status when exceeded), and then
iterate only over the limited, deduplicated list to call
resolveProductByIdentifier and build $products while preserving the existing
checks for null product and duplicate product IDs.

In `@src/Eccube/Resource/locale/messages.en.yaml`:
- Line 1210: Replace the inconsistent English label value for the key
admin.setting.shop.shop.agent_commerce: currently set to "Agentic Commerce",
change it to "Agent Commerce" so it matches other usages (e.g., the navigation
label and the entry at line ~1150); update the YAML value only, preserving the
key and surrounding formatting.

In `@src/Eccube/Service/AgentCommerce/Catalog/Acp/AcpFeedClient.php`:
- Around line 185-196: 現在のレスポンス処理は json_decode が失敗した場合に空配列へフォールバックしているため、2xx
でも壊れたレスポンスを成功扱いしている問題があります。AcpFeedClient の該当処理で $content が空でない場合は json_decode
の戻り値と json_last_error()/json_last_error_msg() を確認し、配列でない・デコードエラーが発生したら空配列へ落とさずに
toTransportException(またはプロジェクトで定義されている AcpFeedTransportException
を作成して投げる)を使って例外を投げるように修正してください。エラー投げる際は $status、デコードエラー情報、$method、$path
を渡して異常を明示するようにしてください。

In `@src/Eccube/Service/AgentCommerce/Catalog/Acp/AcpFeedGenerator.php`:
- Around line 60-69: In generateProductsJsonl() in AcpFeedGenerator (where you
iterate catalogProvider->provideDisplayProducts(), mapProduct() and call
serializer->serialize()), validate each serialized Product against the feed
schema (schema.feed.json) before appending/writing the JSONL line: after mapping
and serializing the DTO, run the schema validator on the resulting
object/string, and if validation fails, skip that product and log the validation
errors (include product id/sku/context) instead of emitting the line; ensure the
validator is used consistently for the full-replacement (全置換) flow so only
schema-conformant product lines are written.

In `@src/Eccube/Service/AgentCommerce/Catalog/Ucp/UcpCatalogCache.php`:
- Around line 85-91: UcpCatalogCache::set() と clear() で Symfony の
FilesystemAdapter::save()/clear()
の戻り値を無視しているため永続化/削除失敗を検知できません。UcpCatalogCache::set(string $key, string $value) と
UcpCatalogCache::clear() 内で $this->adapter->save($item) と
$this->adapter->clear() の戻り値をチェックし、false の場合はロギング(例:
$this->logger->error(...))するか例外を投げるようにしてください。合わせて UcpCatalogCacheClearCommand と
UcpCatalogController の呼び出し側で返り値/例外を検知して成功/失敗のレスポンスや出力を切り替える(失敗時はエラーメッセージを表示し
exit コード/HTTP ステータスを適切に設定)ように修正してください。

In
`@src/Eccube/Service/AgentCommerce/Catalog/Ucp/UcpCatalogProductSerializer.php`:
- Around line 157-161: The empty-variants fallback in
UcpCatalogProductSerializer (the branch returning ['min'=> $zero, 'max'=>
$zero]) hardcodes 'JPY'; change it to use the shop default currency from
injected BaseInfo/shop settings instead: add a dependency (e.g., BaseInfo or a
Settings service) to UcpCatalogProductSerializer (constructor/property), obtain
the default currency via the appropriate method (e.g.,
getDefaultCurrency()/getDefaultCurrencyCode()) and use that value for 'currency'
in the $zero fallback so the serializer no longer hardcodes 'JPY' for empty
$variants.

In `@src/Eccube/Service/AgentCommerce/Security/FilesystemKeyStore.php`:
- Around line 59-64: The write() method in FilesystemKeyStore currently ignores
failures when creating the directory, writing the PEM and setting permissions;
update it to check mkdir() return (when directory doesn't exist), verify
file_put_contents($path, $pem) !== false, and confirm chmod($path, 0600)
returned true, and if any step fails throw a meaningful exception (e.g.,
RuntimeException or the store's existing exception type) so callers can detect
that the key was not saved; keep the same path/$pem variables and ensure the
thrown message includes the failing operation and $path for debugging.

In `@tests/Eccube/Tests/Service/AgentCommerce/Catalog/Acp/AcpFeedClientTest.php`:
- Line 259: 現在のアサーション uses case-sensitive comparison ('Authorization: Bearer
'.self::API_KEY) against $this->normalizedHeaders($call), which can false-fail
if header case differs; update the test in AcpFeedClientTest to perform a
case-insensitive check instead (e.g. normalize both header names/values to
lower-case or use a case-insensitive search) targeting the assertion line that
uses assertContains and the helper normalizedHeaders($call) so it verifies
presence of the Bearer token regardless of header case.

In `@tests/Eccube/Tests/Service/AgentCommerce/README.md`:
- Around line 13-19: The README currently contradicts ACP/UCP packaging: update
the text so it clearly states that UCP schemas are not bundled while ACP is
bundled as a runtime resource; specifically modify the paragraph that mentions
"ACP / UCP の JSON Schema は**リポジトリに同梱しません**" to say UCP is not bundled but ACP is
included for runtime, and ensure the sections referencing
AcpFeedSchemaContractTest / AcpFeedConformanceTest and the resource path
src/Eccube/Resource/AgentCommerce/Acp/schema.feed.json explicitly state that ACP
is packaged with the product for runtime use; apply the same clarification to
the later duplicate lines (the lines around the second mention at 48-49) so both
places consistently state "UCP: not bundled" and "ACP: bundled as runtime
resource."

In `@tests/Eccube/Tests/Service/AgentCommerce/Schema/SchemaValidatorTrait.php`:
- Around line 118-121: The code in SchemaValidatorTrait that reads the ACP
schema (using projectRoot(), json_decode and accessing $bundle->{'$id'}) assumes
the file exists and decodes correctly; validate the file_get_contents result and
json_decode output, check json_last_error(), ensure $bundle is an object and
isset($bundle->{'$id'}) before assigning acpBundleId and calling
SchemaStorage->addSchema; if any check fails, throw or log a descriptive
RuntimeException (e.g., "ACP schema file missing", "Invalid JSON in ACP schema:
<json error>", or "ACP schema missing $id") so failures are explicit and
SchemaStorage->addSchema is only called with a valid bundle.
- Around line 100-103: In SchemaValidatorTrait (around the block that calls
file_get_contents($file->getPathname()), json_decode and $storage->addSchema),
stop silently skipping bad input and fail-fast: check that file_get_contents did
not return false, decode produced a valid object (use
json_last_error()/json_last_error_msg()), and that $schema has the required $id
($schema->{'$id'}); if any check fails throw a clear RuntimeException (include
$file->getPathname() and the json error or missing-$id detail) so consumers see
why schema loading failed rather than silently leaving SchemaStorage incomplete.

---

Nitpick comments:
In
`@src/Eccube/Service/AgentCommerce/Catalog/Ucp/UcpCatalogProductSerializer.php`:
- Around line 208-210: Update the inaccurate inline comment in
UcpCatalogProductSerializer (the block explaining UCP description minProperties
behavior) to state that when the DTO is empty we emit ['plain' => ''] to satisfy
UCP's minProperties:1 requirement; replace the current line claiming that
emitting an empty plain string does not satisfy the constraint with wording that
clearly says we output an empty 'plain' property to ensure at least one property
is present, and that html/markdown are added only if non-null.

In `@src/Eccube/Service/AgentCommerce/Catalog/Ucp/UcpCatalogResponseBuilder.php`:
- Line 36: UCP_VERSION is duplicated in UcpCatalogResponseBuilder::UCP_VERSION
and UcpProfileBuilder::UCP_VERSION; create a single shared constant class (e.g.,
UcpConstants or AgentCommerceConstants) with public const UCP_VERSION =
'2026-04-08' and replace both usages to reference that centralized constant
(e.g., UcpConstants::UCP_VERSION), updating any use-sites and imports to remove
the duplicated definitions.

In `@src/Eccube/Service/AgentCommerce/Discovery/EmptyPaymentHandlerRegistry.php`:
- Around line 40-43: The current merge loop in EmptyPaymentHandlerRegistry
(iterating $this->registries and calling collect()) silently overwrites
duplicate keys; update the merge logic in the method containing that loop to
detect duplicates by checking isset($handlers[$key]) before assignment and
either throw a meaningful exception (e.g., DuplicateHandlerKeyException) or emit
a warning via the existing logger with details (conflicting key, originating
registry class or identifier and previous registry) so plugin conflicts are
visible; include the registry identifier when reporting to aid debugging and
ensure behavior is consistent across all $this->registries iterations.

In `@src/Eccube/Service/AgentCommerce/Discovery/UcpProfileBuilder.php`:
- Around line 99-105: The current endpoint derivation using
preg_replace('`#/search`$#', '', $searchUrl) can fail when the generated URL from
urlGenerator->generate('agent_ucp_catalog_search', ...) does not literally end
with "/search"; update the code to derive a robust base endpoint by either
generating a dedicated base route (use
urlGenerator->generate('agent_ucp_catalog_base', ...,
UrlGeneratorInterface::ABSOLUTE_URL) instead of manipulating the search URL) or,
if keeping a single route, parse the generated $searchUrl with parse_url() and
remove the last path segment (the 'search' segment) to build $endpoint from
scheme, host and the trimmed path; replace the preg_replace usage and ensure
$endpoint always contains the true catalog base URL.

In `@tests/Eccube/Tests/Service/AgentCommerce/AddressMappingServiceTest.php`:
- Around line 86-161: Add a new unit test in AddressMappingServiceTest that
covers the CustomerAddress branch by instantiating a CustomerAddress object,
setting the same fields used in existing tests
(name01/name02/kana/company/postal/pref/addr/country/phone as applicable),
calling $this->service->toAddressArray($customerAddress) and asserting the
mapped keys (family_name, given_name, family_name_kana, given_name_kana,
company, postal_code, region, address1/address2, country, phone) match the
source values; mirror the pattern of testToAddressArrayFromCustomer and
testToAddressArrayFromShipping so all three input types (Customer, Shipping,
CustomerAddress) are covered and prevent regression of the CustomerAddress
mapping branch.

In `@tests/Eccube/Tests/Service/AgentCommerce/Catalog/Acp/AcpFeedClientTest.php`:
- Around line 63-325: Add a unit test in AcpFeedClientTest that covers
AcpFeedClientInterface::pushFullReplacement(): mirror the existing
upsertProducts tests by using $this->createClient([...]) with a jsonResponse
(e.g. returning ['id'=>'feed_1','accepted'=>true]), call
$client->pushFullReplacement('feed_1', $products) (use $this->validProduct('p1')
to build payload), assert the returned accepted boolean, assert a single
outbound request was recorded, and assert the recorded request uses the
full-replacement HTTP method (PUT) to the feed products endpoint
(self::BASE_URL.'/feeds/feed_1/products') with the expected body shape
(['products'=>$products]); also add a test case that verifies pre-send
validation rejects invalid product payloads and that no request is recorded
(analogous to testUpsertProductsRejectsInvalidPayloadBeforeSending).

In `@tests/Eccube/Tests/Web/AgentCommerce/UcpDiscoveryControllerTest.php`:
- Around line 95-107: In testServicesAndPaymentHandlersAreObjects in
UcpDiscoveryControllerTest (the test method that fetches agent_ucp_discovery and
inspects $raw), add the same raw-string negative assertion used for
payment_handlers to also assert that '"services":[]' does not appear; i.e.,
after the existing assertStringNotContainsString for payment_handlers, add a
matching assertStringNotContainsString('"services":[]', $raw, 'ucp.services MUST
serialize as a JSON object {} (not an array []) even when empty') so the test
verifies both services and payment_handlers serialize as JSON objects rather
than arrays.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 268efb1b-8e0c-4144-b096-cc3278845309

📥 Commits

Reviewing files that changed from the base of the PR and between 9820448 and 0c0cdc0.

⛔ Files ignored due to path filters (5)
  • composer.lock is excluded by !**/*.lock
  • src/Eccube/Resource/doctrine/import_csv/en/dtb_base_info.csv is excluded by !**/*.csv
  • src/Eccube/Resource/doctrine/import_csv/en/mtb_country_iso_code.csv is excluded by !**/*.csv
  • src/Eccube/Resource/doctrine/import_csv/ja/dtb_base_info.csv is excluded by !**/*.csv
  • src/Eccube/Resource/doctrine/import_csv/ja/mtb_country_iso_code.csv is excluded by !**/*.csv
📒 Files selected for processing (83)
  • .github/workflows/coverage.yml
  • .github/workflows/unit-test.yml
  • .gitignore
  • .htaccess
  • app/DoctrineMigrations/Version20260604120000.php
  • app/config/eccube/packages/eccube_nav.yaml
  • app/config/eccube/packages/framework.yaml
  • app/config/eccube/services.yaml
  • app/config/eccube/services_test.yaml
  • app/keystore/.gitkeep
  • app/keystore/.htaccess
  • composer.json
  • src/Eccube/Command/AgentCommerce/AcpFeedPushCommand.php
  • src/Eccube/Command/AgentCommerce/UcpCatalogCacheClearCommand.php
  • src/Eccube/Command/AgentCommerce/UcpCatalogCacheWarmupCommand.php
  • src/Eccube/Controller/Admin/AgentCommerce/AgentCommerceController.php
  • src/Eccube/Controller/AgentCommerce/UcpCatalogController.php
  • src/Eccube/Controller/AgentCommerce/UcpDiscoveryController.php
  • src/Eccube/Entity/BaseInfo.php
  • src/Eccube/Entity/Master/CountryIsoCode.php
  • src/Eccube/EventListener/AgentCommerce/CatalogCacheInvalidationListener.php
  • src/Eccube/Form/Type/Admin/ShopMasterType.php
  • src/Eccube/Repository/Master/CountryIsoCodeRepository.php
  • src/Eccube/Resource/AgentCommerce/Acp/schema.feed.json
  • src/Eccube/Resource/AgentCommerce/README.md
  • src/Eccube/Resource/doctrine/import_csv/en/definition.yml
  • src/Eccube/Resource/doctrine/import_csv/ja/definition.yml
  • src/Eccube/Resource/locale/messages.en.yaml
  • src/Eccube/Resource/locale/messages.ja.yaml
  • src/Eccube/Resource/template/admin/Content/AgentCommerce/index.twig
  • src/Eccube/Resource/template/admin/Setting/Shop/shop_master.twig
  • src/Eccube/Service/AgentCommerce/AddressMappingService.php
  • src/Eccube/Service/AgentCommerce/Catalog/Acp/AcpFeedClient.php
  • src/Eccube/Service/AgentCommerce/Catalog/Acp/AcpFeedClientInterface.php
  • src/Eccube/Service/AgentCommerce/Catalog/Acp/AcpFeedGenerator.php
  • src/Eccube/Service/AgentCommerce/Catalog/Acp/AcpFeedProductSerializer.php
  • src/Eccube/Service/AgentCommerce/Catalog/Acp/AcpFeedValidator.php
  • src/Eccube/Service/AgentCommerce/Catalog/AgentCatalogBarcodeDto.php
  • src/Eccube/Service/AgentCommerce/Catalog/AgentCatalogDescriptionDto.php
  • src/Eccube/Service/AgentCommerce/Catalog/AgentCatalogItemDto.php
  • src/Eccube/Service/AgentCommerce/Catalog/AgentCatalogMediaDto.php
  • src/Eccube/Service/AgentCommerce/Catalog/AgentCatalogOptionDto.php
  • src/Eccube/Service/AgentCommerce/Catalog/AgentCatalogVariantDto.php
  • src/Eccube/Service/AgentCommerce/Catalog/AvailabilityStatus.php
  • src/Eccube/Service/AgentCommerce/Catalog/CatalogMapper.php
  • src/Eccube/Service/AgentCommerce/Catalog/CatalogProvider.php
  • src/Eccube/Service/AgentCommerce/Catalog/CatalogProviderInterface.php
  • src/Eccube/Service/AgentCommerce/Catalog/Exception/AcpFeedException.php
  • src/Eccube/Service/AgentCommerce/Catalog/Exception/AcpFeedTransportException.php
  • src/Eccube/Service/AgentCommerce/Catalog/Exception/AcpFeedValidationException.php
  • src/Eccube/Service/AgentCommerce/Catalog/ProductReferenceResolver.php
  • src/Eccube/Service/AgentCommerce/Catalog/ProductReferenceResolverInterface.php
  • src/Eccube/Service/AgentCommerce/Catalog/Ucp/UcpCatalogCache.php
  • src/Eccube/Service/AgentCommerce/Catalog/Ucp/UcpCatalogProductSerializer.php
  • src/Eccube/Service/AgentCommerce/Catalog/Ucp/UcpCatalogResponseBuilder.php
  • src/Eccube/Service/AgentCommerce/Discovery/EmptyPaymentHandlerRegistry.php
  • src/Eccube/Service/AgentCommerce/Discovery/PaymentHandlerRegistryInterface.php
  • src/Eccube/Service/AgentCommerce/Discovery/UcpProfileBuilder.php
  • src/Eccube/Service/AgentCommerce/MinorUnitConverter.php
  • src/Eccube/Service/AgentCommerce/Security/AgentCommerceMessageSignerInterface.php
  • src/Eccube/Service/AgentCommerce/Security/AgentCommerceScopeRegistry.php
  • src/Eccube/Service/AgentCommerce/Security/FilesystemKeyStore.php
  • src/Eccube/Service/AgentCommerce/Security/KeyStoreInterface.php
  • src/Eccube/Service/AgentCommerce/Security/UcpMessageSigner.php
  • tests/Eccube/Tests/Service/AgentCommerce/AddressMappingServiceTest.php
  • tests/Eccube/Tests/Service/AgentCommerce/BaseInfoAgentCommerceFlagsTest.php
  • tests/Eccube/Tests/Service/AgentCommerce/Catalog/Acp/AcpFeedClientTest.php
  • tests/Eccube/Tests/Service/AgentCommerce/Catalog/Acp/AcpFeedProductSerializerTest.php
  • tests/Eccube/Tests/Service/AgentCommerce/Catalog/CatalogMapperTest.php
  • tests/Eccube/Tests/Service/AgentCommerce/Catalog/Ucp/UcpCatalogProductSerializerTest.php
  • tests/Eccube/Tests/Service/AgentCommerce/Conformance/AcpFeedConformanceTest.php
  • tests/Eccube/Tests/Service/AgentCommerce/Conformance/AgentCommerceBaseConformanceTest.php
  • tests/Eccube/Tests/Service/AgentCommerce/Conformance/UcpCatalogConformanceTest.php
  • tests/Eccube/Tests/Service/AgentCommerce/Conformance/UcpDiscoveryConformanceTest.php
  • tests/Eccube/Tests/Service/AgentCommerce/MinorUnitConverterTest.php
  • tests/Eccube/Tests/Service/AgentCommerce/README.md
  • tests/Eccube/Tests/Service/AgentCommerce/Schema/AcpFeedSchemaContractTest.php
  • tests/Eccube/Tests/Service/AgentCommerce/Schema/SchemaValidatorTrait.php
  • tests/Eccube/Tests/Service/AgentCommerce/Schema/UcpCatalogSchemaContractTest.php
  • tests/Eccube/Tests/Service/AgentCommerce/Security/AgentCommerceScopeRegistryTest.php
  • tests/Eccube/Tests/Service/AgentCommerce/Security/UcpMessageSignerTest.php
  • tests/Eccube/Tests/Web/AgentCommerce/UcpCatalogControllerTest.php
  • tests/Eccube/Tests/Web/AgentCommerce/UcpDiscoveryControllerTest.php

Comment thread .github/workflows/coverage.yml
Comment thread app/DoctrineMigrations/Version20260604120000.php
Comment thread app/keystore/.htaccess
Comment thread src/Eccube/Command/AgentCommerce/AcpFeedPushCommand.php
Comment thread src/Eccube/Service/AgentCommerce/Security/FilesystemKeyStore.php
Comment thread tests/Eccube/Tests/Service/AgentCommerce/README.md

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/coverage.yml:
- Line 8: The `code-quality: write` permission scope on line 8 is invalid and
not recognized by GitHub Actions, which will cause workflow validation to fail.
Replace the `code-quality` scope with a valid permission scope such as `checks`
or `contents` depending on what the workflow actually needs to accomplish. Refer
to the list of valid scopes provided in the comment: actions, artifact-metadata,
attestations, checks, contents, deployments, discussions, id-token, issues,
models, packages, pages, pull-requests, repository-projects, security-events,
statuses.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b79c1237-83d4-4de5-9df9-0e8d2a8970b9

📥 Commits

Reviewing files that changed from the base of the PR and between 0c0cdc0 and 4f69656.

⛔ Files ignored due to path filters (1)
  • composer.lock is excluded by !**/*.lock
📒 Files selected for processing (3)
  • .github/workflows/coverage.yml
  • .github/workflows/unit-test.yml
  • composer.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • composer.json

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/coverage.yml:
- Line 8: The `code-quality: write` permission scope on line 8 is invalid and
not recognized by GitHub Actions, which will cause workflow validation to fail.
Replace the `code-quality` scope with a valid permission scope such as `checks`
or `contents` depending on what the workflow actually needs to accomplish. Refer
to the list of valid scopes provided in the comment: actions, artifact-metadata,
attestations, checks, contents, deployments, discussions, id-token, issues,
models, packages, pages, pull-requests, repository-projects, security-events,
statuses.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b79c1237-83d4-4de5-9df9-0e8d2a8970b9

📥 Commits

Reviewing files that changed from the base of the PR and between 0c0cdc0 and 4f69656.

⛔ Files ignored due to path filters (1)
  • composer.lock is excluded by !**/*.lock
📒 Files selected for processing (3)
  • .github/workflows/coverage.yml
  • .github/workflows/unit-test.yml
  • composer.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • composer.json
🛑 Comments failed to post (1)
.github/workflows/coverage.yml (1)

8-8: ⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 期待結果: unknown permission scope エラーが消えること
actionlint .github/workflows/coverage.yml

Repository: EC-CUBE/ec-cube

Length of output: 1584


code-quality は無効な permission スコープです。ワークフローの検証に失敗します。

Line 8 の code-quality: write は GitHub Actions で認識されない permission スコープです。有効なスコープ(checkscontents など)に置き換えてください。利用可能なスコープ一覧:actions、artifact-metadata、attestations、checks、contents、deployments、discussions、id-token、issues、models、packages、pages、pull-requests、repository-projects、security-events、statuses

🧰 Tools
🪛 actionlint (1.7.12)

[error] 8-8: unknown permission scope "code-quality". all available permission scopes are "actions", "artifact-metadata", "attestations", "checks", "contents", "deployments", "discussions", "id-token", "issues", "models", "packages", "pages", "pull-requests", "repository-projects", "security-events", "statuses"

(permissions)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/coverage.yml at line 8, The `code-quality: write`
permission scope on line 8 is invalid and not recognized by GitHub Actions,
which will cause workflow validation to fail. Replace the `code-quality` scope
with a valid permission scope such as `checks` or `contents` depending on what
the workflow actually needs to accomplish. Refer to the list of valid scopes
provided in the comment: actions, artifact-metadata, attestations, checks,
contents, deployments, discussions, id-token, issues, models, packages, pages,
pull-requests, repository-projects, security-events, statuses.

Source: Linters/SAST tools

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant