Skip to content

feat(agent-commerce): CheckoutSession 中核を実装 (#6777 トラックB前提・Phase 1b)#6825

Open
nanasess wants to merge 24 commits into
EC-CUBE:4.4from
nanasess:feature/agentic-commerce-checkout-core
Open

feat(agent-commerce): CheckoutSession 中核を実装 (#6777 トラックB前提・Phase 1b)#6825
nanasess wants to merge 24 commits into
EC-CUBE:4.4from
nanasess:feature/agentic-commerce-checkout-core

Conversation

@nanasess

@nanasess nanasess commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

概要(Overview・Refs Issue)

エージェントコマース (ACP/UCP) 対応の共通基盤 #6777 のうち、CheckoutSession 中核 (Phase 1b) を実装します。AI エージェント (ChatGPT / Gemini 等) 経由の購入フローの前提となる、プロトコル非依存のエンティティ・サービス・認証部品を提供します。

スタック PR: 本 PR は #6802 (Phase 1a 共通基盤) にスタックしています。#68024.4 に未マージのため、本 PR の diff には #6802 の変更が含まれます。#6802 マージ後に rebase し、本 PR は Phase 1b の差分のみになります。

方針(Policy)

  • 新規の購入フローは定義せず、通常購入と同一の eccube.purchase.flow.shopping を再利用します。これにより税計算・送料・代引手数料・ポイント・在庫引当・受注番号採番のロジックを完全共有し、エージェント経由でも通常購入と同じ購入処理を通します (PurchaseFlow 機構の拡充)。
  • CheckoutSessionCart/CartItem の代替ではなく、Cart をセッションレスに束ねる上位層として設計しました。会員購入は dtb_cart に永続化されますが、エージェントはブラウザセッション (Cookie) を持たないため、CheckoutSession.session_id (推測困難な公開識別子) が cart_key/セッションの代替になります。明細は Cart/CartItem を再利用し、確定時は通常購入と同様に pre_order_id 経由で Order を生成します。
  • 会員 ID 連携はゲスト購入を基準線とし、CustomerResolverInterface の標準実装は常に null (ゲスト) を返します。会員に紐づく OAuth2 トークンによる解決は eccube-api4 の機能追加 (Customer(会員) に紐づく OAuth2 authorization_code フロー (ID 連携) のサポート eccube-api4#189) に依存するため、差し替え可能な seam に留めています。
  • インバウンド認証は eccube-api4 の具象に依存させず、Symfony 標準 AccessTokenHandlerInterface 経由でトークン検証します (Agentic Commerce 向け client_credentials グラント有効化とプロトコル scope の登録 eccube-api4#188 が前提・未導入時は 503)。ユニットテストでは handler をスタブ化し、api4 を要求しません。
  • カラム追加・新規テーブルは ALTER/CREATE マイグレーションを書かず schema:update に委ねます (既存の EC-CUBE 規約・前例 Googleアナリティクス機能を追加 #4912 に準拠)。CheckoutSession は空テーブル始動・マスタデータ無しのため INSERT migration も不要です。
  • マッピングは 4.4 標準の PHP 8 attribute で記述 (XML は使用しません)。

実装に関する補足(Appendix)

追加した主なクラス:

区分 クラス
エンティティ CheckoutSession (+ CheckoutSessionRepository)、Orderagent_protocol/agent_id を NULL 許容で追加
PurchaseFlow 連携 AgentCheckoutPurchaseFlowAdapter (中立 DTO → Cart(永続) → OrderHelper → shopping flow 再計算 → 結果)
中立 DTO AgentCheckoutRequest / AgentCheckoutLineItem / AgentCheckoutAddress / AgentCheckoutResult / AgentCheckoutMessage(+Level)
配送 FulfillmentOptionMapperInterface + StandardFulfillmentOptionMapper (送料 DeliveryFee×Pref・代引手数料 PaymentOption→Payment::getCharge()・配送日数 DeliveryDuration の明細横断 max)
会員解決 CustomerResolverInterface + GuestCustomerResolver (標準はゲスト=null)
認証 AgentCommerceOAuth2Authenticator (Symfony AccessTokenHandlerInterface 経由・scope×protocol 照合・api4 未導入時 503)
例外/決済 AgentCheckoutException + AgentCheckoutErrorCode enum、AgentCheckoutPaymentHandlerInterface 共通基底

テスト(Test)

Important

本 PR 単体では curl 等での E2E テストはできません(設計上)。
本 PR は HTTP エンドポイント(Controller / ルート)を持たない中核サービス層であり、検証は PHPUnit で行います。curl で叩けるエージェント checkout の HTTP API(/checkout_sessions 等)は #6776 (ACP) / #6574 (UCP) で実装され、本 PR の中核サービスを呼び出します。手元で挙動を確認する場合は下記 PHPUnit を実行してください。

tests/Eccube/Tests/ 配下に以下を追加。AgentCommerce スイート 92 tests / 678 assertions / 0 失敗 / incomplete 3 (意図的)、PHPStan level 6 (src) No errors、php-cs-fixer 0 件。Order/PurchaseFlow の既存テストで回帰なしを確認済 (通常購入で agent カラム NULL)。

  • Layer 0 仕様適合 (AgentCheckoutCoreConformanceTest): status 正規化 (canceled 含む)、messages[] の error/warning/info 語彙、Order の agent 帰属。HTTP 2 系統変換・Idempotency は controller 層へ markTestIncomplete
  • Layer 2 Doctrine (CheckoutSessionTest): 状態遷移 (作成→更新→完了/canceled/expired)、json ラウンドトリップ、findExpiredschema:update でのテーブル+Order 2 カラム生成、通常購入 Order で両カラム NULL の回帰。
  • Layer 3 PurchaseFlow 連携 (AgentCheckoutPurchaseFlowAdapterTest / StandardFulfillmentOptionMapperTest): DTO→Cart→Order→shopping flow 再計算、在庫超過の messages[] 反映 (例外でない)、complete で受注番号採番、代引 charge・配送日数 max・お取り寄せ null。
  • Layer 4a OAuth2 (AgentCommerceOAuth2AuthenticatorTesteccube-api4 不要): handler スタブで 200/401/403、capability 越境 403、未導入 503。
  • Layer 4b OAuth2 (統合) は eccube-api4#188 landing 後に専用 CI ジョブで追加予定 (未導入は markTestSkipped)。

ローカル DB は SQLite。PostgreSQL/MySQL 固有の migration 移植性は CI マトリクスで担保します。

相談(Discussion)

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

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

レビュワー確認項目

  • 動作確認
  • コードレビュー
  • E2E/Unit テスト確認(テストの追加・変更が必要かどうか)
  • 互換性が保持されているか
  • セキュリティ上の問題がないか
    • 権限を超えた操作が可能にならないか
    • 不要なファイルアップロードがないか
    • 外部へ公開されるファイルや機能の追加ではないか
    • テンプレートでのエスケープ漏れがないか

Summary by CodeRabbit

  • 新機能
    • 管理画面にACP/UCPチェックアウト有効化トグルを追加
    • エージェント向けの見積/確定フロー、住所マッピング、配送選択肢の動的解決、通貨少数→整数変換を提供
  • セキュリティ
    • HTTPメッセージ署名/検証とOAuth2スコープ検証を追加
  • データベース更新
    • プロトコル/セッションステータス/国コードの初期マスタ投入に対応
  • 設定・保護
    • キーストア領域の保護と、アクセス制御対象ファイルの制限を強化
  • テスト
    • 主要機能(変換、署名、スコープ判定、フロー、マスタ連携)を網羅するテストを追加

追記 (2026-06-17): complete 状態機械化 + Idempotency を DB 一意制約ベースに追加

本 PR の中核に、その後の設計確定(#6777)に伴い以下を追加しました。

complete を「中断→再開」状態機械として実装

  • 追加認証 (EMV-3DS) / escalation はエラーでなく complete が返す正常な中間状態。決済ハンドラ結果型 PaymentOutcome (COMPLETED / REQUIRES_ACTION / PENDING / FAILED) を新設し、AgentCheckoutPaymentHandlerInterfaceauthorize/capturevoidPaymentOutcome に改訂。
  • AgentCheckoutCompletionService(状態機械オーケストレータ)を新設。冪等性・初回/再開フロー・在庫引当の保持/回収・トランザクション境界を core に集約(prepare 成功 + authorize COMPLETED 後にのみ capture する順序を保証)。
  • 正規化ステータスマスタに requires_action(6) / in_progress(7) を追加(CSV + 既存環境向け INSERT migration)。findExpired は終端 3 値のみ除外のため両者を回収対象に含む。
  • 在庫確保期限は eccube.yamleccube_agent_checkout_escalation_expire(既定 15 分・EMV-3DS の 10 分より大)。

Idempotency を DB 一意制約ベースで実装(マルチインスタンス対応)

検証

  • AgentCommerce + CheckoutSession Entity スイート 110 tests 0 失敗 (incomplete 3 意図的) / PHPStan level6 No errors / php-cs-fixer 0 / PurchaseFlow・OrderHelper 回帰 green。

nanasess and others added 10 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>
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>
- MinorUnitConverter: 不正な金額文字列 (abc / 1,000 / 1..2 等) を
  BCMath 呼び出し前に正規表現で弾き、ValueError を防止 (仕様の「不正は 0」契約遵守)
- FilesystemKeyStore: 鍵書き込み時の mkdir / file_put_contents 失敗を
  RuntimeException で検知し、サイレント失敗を防止
- Version20260604120000: down() を不可逆 migration として明示し、
  新規インストール (up() が no-op) 環境での import_csv 由来データ巻き添え削除を回避
- AddressMappingServiceTest: fopen 後に assertIsResource を追加し診断性を向上
- MinorUnitConverterTest: 不正・空入力の回帰テスト 8 ケースを追加

CodeRabbit 指摘のうち COUNT(*) 判定と (bool) キャストの 2 件は非妥当のため非対応。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
MinorUnitConverter::toMinorUnits の入力検証を、正規表現での形式チェックから
bcmul/bcadd の ValueError 捕捉へ変更する。

- BCMath が「受け付ける数値文字列形式」の唯一の権威であり、正規表現で再実装すると
  仕様の二重管理・他箇所への正規表現の拡散を招くため、判定を BCMath 自身へ委譲する
- 空文字 '' / '.' は BCMath が 0 として扱うため明示チェックも不要になり簡素化
- is_numeric は指数表記 (1e3) を true と判定するが BCMath では ValueError になり
  不適 (実証済み)。ValueError 捕捉なら指数表記・hex も正しく 0 に倒せる
- 回帰テストに指数表記 (1e3 / 1.5e3) と hex (0x1A) ケースを追加

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

CheckoutSessionを中心に、マスタデータ・エンティティ、住所・通貨・配送マッピング、鍵管理・署名、OAuth2認証・スコープ検証、購入フロー統合、管理UI、包括的テストを追加する変更です。

Changes

Agent Commerce機能追加

Layer / File(s) Summary
ストレージアクセス制御とkeystore保護
.gitignore, .htaccess, app/keystore/.htaccess
keystoreを.gitignoreで無視しつつ.gitkeep/.htaccessは追跡、.htaccessの保護パターンをweb.config/Dockerfile/.editorconfig/*.keyに拡張、keystoreディレクトリへのHTTPアクセスを拒否。
マスタデータ定義・リポジトリ・初期投入
src/Eccube/Entity/Master/CountryIsoCode.php, src/Eccube/Entity/Master/AgentProtocol.php, src/Eccube/Entity/Master/CheckoutSessionStatus.php, src/Eccube/Repository/Master/CountryIsoCodeRepository.php, src/Eccube/Repository/Master/AgentProtocolRepository.php, src/Eccube/Repository/Master/CheckoutSessionStatusRepository.php, app/DoctrineMigrations/Version20260604120000.php, app/DoctrineMigrations/Version20260611120000.php, src/Eccube/Resource/doctrine/import_csv/*/definition.yml
CountryIsoCode/AgentProtocol/CheckoutSessionStatusのマスタエンティティ定義、対応リポジトリ、国コード・チェックアウトセッション状態・エージェントプロトコルの冪等初期投入マイグレーション、CSVインポート定義の追加・並び替え。
CheckoutSessionと関連エンティティ拡張
src/Eccube/Entity/CheckoutSession.php, src/Eccube/Entity/Order.php, src/Eccube/Entity/Cart.php, src/Eccube/Entity/BaseInfo.php, src/Eccube/Form/Type/Admin/ShopMasterType.php, src/Eccube/Resource/template/admin/Setting/Shop/shop_master.twig, src/Eccube/Resource/locale/messages.en.yaml, src/Eccube/Resource/locale/messages.ja.yaml
CheckoutSessionエンティティ(ID・session_id・Protocol参照・agent_id・Status参照・currency_code・expires_at・JSON列・Cart/Order/Customer関連・create_date/update_date・isExpired判定)、OrderへのAgentProtocol参照・agent_id、CartへのagentOwned、BaseInfoへのACP/UCP/UCP_catalog_authフラグ、管理フォームのトグルスイッチ、設定UIカード、日英ローカライズを追加。
セッション取得・顧客解決契約
src/Eccube/Repository/CheckoutSessionRepository.php, src/Eccube/Service/AgentCommerce/CheckoutSession/CustomerResolverInterface.php, src/Eccube/Service/AgentCommerce/CheckoutSession/GuestCustomerResolver.php
CheckoutSessionRepositoryのfindOneBySessionId・findExpiredメソッド、CustomerResolverInterfaceとGuestCustomerResolver実装を追加。
チェックアウトDTO・エラーモデル・決済ハンドラ
src/Eccube/Service/AgentCommerce/CheckoutSession/AgentCheckoutRequest.php, src/Eccube/Service/AgentCommerce/CheckoutSession/AgentCheckoutAddress.php, src/Eccube/Service/AgentCommerce/CheckoutSession/AgentCheckoutLineItem.php, src/Eccube/Service/AgentCommerce/CheckoutSession/AgentCheckoutResult.php, src/Eccube/Service/AgentCommerce/CheckoutSession/AgentCheckoutMessage.php, src/Eccube/Service/AgentCommerce/CheckoutSession/AgentCheckoutMessageLevel.php, src/Eccube/Service/AgentCommerce/Exception/AgentCheckoutErrorCode.php, src/Eccube/Service/AgentCommerce/Exception/AgentCheckoutException.php, src/Eccube/Service/AgentCommerce/Payment/AgentCheckoutPaymentHandlerInterface.php
プロトコル非依存のチェックアウトDTO(Request/Address/LineItem/Result)、メッセージモデル(Message/MessageLevel)、プロトコル系エラーコード(ErrorCode)と例外(Exception)、決済ハンドラ契約を追加。
住所・通貨・配送マッピング
src/Eccube/Service/AgentCommerce/AddressMappingService.php, src/Eccube/Service/AgentCommerce/MinorUnitConverter.php, src/Eccube/Service/AgentCommerce/Fulfillment/FulfillmentOption.php, src/Eccube/Service/AgentCommerce/Fulfillment/FulfillmentPaymentOption.php, src/Eccube/Service/AgentCommerce/Fulfillment/FulfillmentOptionMapperInterface.php, src/Eccube/Service/AgentCommerce/Fulfillment/StandardFulfillmentOptionMapper.php
AddressMappingServiceでCustomer/Shipping→プロトコル非依存住所配列変換、numeric country ID→alpha2コード変換、MinorUnitConverterで通貨major/minor相互変換(round-half-up)、FulfillmentOptionMapperで配送先Pref・商品クラスから配送方法・送料・支払オプション・見積配送日数を解決。
鍵管理とUCP署名基盤
src/Eccube/Service/AgentCommerce/Security/KeyStoreInterface.php, src/Eccube/Service/AgentCommerce/Security/FilesystemKeyStore.php, src/Eccube/Service/AgentCommerce/Security/AgentCommerceMessageSignerInterface.php, src/Eccube/Service/AgentCommerce/Security/UcpMessageSigner.php
KeyStoreInterfaceとFilesystemKeyStoreでPEM秘密鍵をapp/keystore配下に保存・読出、UcpMessageSignerでES256(EC P-256)の署名・検証、JWKS公開(秘密パラメータ d 除外)、RFC 7638 Thumbprint生成、鍵ローテーション時のgrace公開鍵対応を実装。
OAuth2認証・スコープ正準化
src/Eccube/Service/AgentCommerce/Security/AgentCommerceScopeRegistry.php, src/Eccube/Service/AgentCommerce/Security/AgentCommerceOAuth2Authenticator.php
AgentCommerceScopeRegistryで<protocol>:<capability>正準化・検証・列挙・権限判定、AgentCommerceOAuth2AuthenticatorでAccessTokenHandler optional注入下でのtoken→UserBadge検証・scope対応チェック・protocol越境検出を実装。
DI設定(本番・テスト)
app/config/eccube/services.yaml, app/config/eccube/services_test.yaml
KeyStore/UcpMessageSigner/FulfillmentMapper/GuestResolverなどのサービス定義・インターフェイスエイリアス、テスト向けサービス公開設定・arguments再束縛を追加。
購入フロー統合とカート分離
src/Eccube/Service/AgentCommerce/AgentCheckoutPurchaseFlowAdapter.php, src/Eccube/Service/CartService.php, src/Eccube/Service/PurchaseFlow/Processor/PreOrderIdValidator.php
AgentCheckoutPurchaseFlowAdapterで中立DTOからCart/Orderを構築し既存shopping flowで税・送料・在庫を再計算・validate/prepare/commitを実行、agent_ownedカートでWeb側からの隔離、CartServiceのgetPersistedCarts/getSessionCartsで agent_owned=false フィルタを追加、PreOrderIdValidatorでagent注文をpre_order_id整合性チェック除外。
包括的テスト群
tests/Eccube/Tests/Entity/CheckoutSessionTest.php, tests/Eccube/Tests/Service/AgentCommerce/AddressMappingServiceTest.php, tests/Eccube/Tests/Service/AgentCommerce/AgentCheckoutPurchaseFlowAdapterTest.php, tests/Eccube/Tests/Service/AgentCommerce/BaseInfoAgentCommerceFlagsTest.php, tests/Eccube/Tests/Service/AgentCommerce/MinorUnitConverterTest.php, tests/Eccube/Tests/Service/AgentCommerce/Fulfillment/StandardFulfillmentOptionMapperTest.php, tests/Eccube/Tests/Service/AgentCommerce/Security/AgentCommerceScopeRegistryTest.php, tests/Eccube/Tests/Service/AgentCommerce/Security/AgentCommerceOAuth2AuthenticatorTest.php, tests/Eccube/Tests/Service/AgentCommerce/Security/UcpMessageSignerTest.php, tests/Eccube/Tests/Service/AgentCommerce/Conformance/AgentCheckoutCoreConformanceTest.php, tests/Eccube/Tests/Service/AgentCommerce/Conformance/AgentCommerceBaseConformanceTest.php
CheckoutSessionエンティティ永続化・マスタFK・検索・JSON・ステータス遷移・期限判定、AddressMappingServiceの国コード・地域・住所変換、AgentCheckoutPurchaseFlowAdapterの注文生成・確定・カート分離・エラーケース、MinorUnitConverterの通貨変換(JPY/USD/BHD)・round-half-up・ラウンドトリップ、OAuth2認証・スコープ検証、UcpMessageSignerの署名ラウンドトリップ・改ざん耐性・JWKS・鍵ローテーション、FulfillmentOptionMapperの配送解決、BaseInfoフラグデフォルト、コンフォーマンステスト(ステータス定数・メッセージレベル・order agent属性・通貨少額単位・公開鍵秘密パラメータ除外)を追加。

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45分

Possibly related issues

Poem

🐰 小さな兎の祝辞
鍵は隠し、署名は舞い、
セッション紡ぎ、注文は翔ける。
通貨は整数でぴしりと数え、
住所は整え、配送を選ぶ。
よきリリースを祝おう、ぴょん!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 43.81% 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
Title check ✅ Passed PR タイトルは「CheckoutSession 中核を実装」と明確で、このPRの主要な変更内容(CheckoutSession エンティティとそれに関連するコアコンポーネントの実装)を正確に反映しています。
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ 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 marked this pull request as ready for review June 11, 2026 09:07

@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: 7

🧹 Nitpick comments (7)
tests/Eccube/Tests/Entity/CheckoutSessionTest.php (1)

52-58: 💤 Low value

型安全性の小さな改善余地(任意)

find()null を返す可能性がありますが、@var で型を上書きしています。テストヘルパーとしては実用上問題ありませんが、より厳格にするなら self::assertNotNull($status) を追加することで、マスタデータ不在時に明確なテスト失敗メッセージを得られます。

現状でも十分機能するため、任意の改善です。

🤖 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/Entity/CheckoutSessionTest.php` around lines 52 - 58, The
helper findStatus() assumes statusRepository->find($id) never returns null but
overrides the type with `@var`; to improve type-safety add an assertion before
returning: call self::assertNotNull($status) (and optionally
self::assertInstanceOf(CheckoutSessionStatus::class, $status)) inside the
findStatus() method so the test fails clearly if the master data is missing and
the returned value can be safely treated as CheckoutSessionStatus.
tests/Eccube/Tests/Service/AgentCommerce/AddressMappingServiceTest.php (1)

166-184: 💤 Low value

ファイルハンドルのリソース管理を防御的に改善可能(任意)

fopen 後の assertIsResource が失敗した場合、理論上はファイルハンドルのクローズが保証されません。実際には fopen が失敗すると false を返すため fclose は不要ですが、より防御的には以下のいずれかが考えられます:

  1. assertIsResource の前に if で型チェックし、リソースでない場合は早期リターン
  2. try-finally でクローズを保証

現状でも実用上は問題ありませんが、テストコードの堅牢性向上として検討できます。

🤖 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 166 - 184, In loadCountryIdsFromCsv, make resource handling
defensive: after $handle = fopen(...), check is_resource($handle) and return an
empty array (or fail the test) before calling assertIsResource, or better wrap
the CSV processing loop in a try-finally so fclose($handle) is always called;
reference the $handle variable, the fopen/fgetcsv/fclose calls and the
assertIsResource assertion so you ensure fclose runs even if assertIsResource
throws.
tests/Eccube/Tests/Service/AgentCommerce/AgentCheckoutPurchaseFlowAdapterTest.php (1)

95-95: 💤 Low value

アサーションをより明示的に(任意)

assertNotInstanceOf(Customer::class, ...) は「Customer のインスタンスではない」ことを検証しますが、意図としては「null である」ことを確認したいと思われます。より明示的に assertNull($Order->getCustomer()) を使うと、テストの意図が明確になります。

現状でも機能的には正しく動作します。

🤖 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/AgentCheckoutPurchaseFlowAdapterTest.php`
at line 95, Replace the vague instance-type assertion with a null check: in
AgentCheckoutPurchaseFlowAdapterTest change the assertion that checks
$Order->getCustomer() (currently using assertNotInstanceOf(Customer::class,
...)) to assertNull($Order->getCustomer()) so the test explicitly verifies the
customer is null for guest purchases; update the assertion call where $Order and
getCustomer() are referenced.
src/Eccube/Service/AgentCommerce/Security/UcpMessageSigner.php (1)

207-238: 💤 Low value

コメントと実装が一致していません。

Lines 199-203 のコメントには「フォールバック (コメント)」として uncompressed point から座標を復元する実装に切り替える旨が記載されていますが、実際の実装(Lines 232-234)では RuntimeException を投げるだけで、フォールバックは実装されていません。

コメントには「ここではまず JWK 出力を解析し, 取得不能な場合に uncompressed point から座標を復元する」と書かれていますが、Lines 233 のコメント「phpseclib3 の EC 公開鍵は toString('JWK') で必ず x/y を返すため通常到達しない」という記述を考慮すると、フォールバック実装は実際には不要と判断されたようです。

コメントを実装に合わせて簡潔にし、フォールバックに関する記述を削除することを推奨します。

♻️ コメント簡潔化の提案
 /**
  * 公開鍵から JWK の座標 (x, y; base64url) を抽出する.
  *
- * 主: phpseclib の toString('JWK') を利用.
- * フォールバック (コメント): phpseclib のバージョン差で 'JWK' 出力のキー名が
- * 異なる / 取得できない場合は, getEncodedCoordinates() 等で得た
- * uncompressed point (0x04 || X(32) || Y(32)) を 32byte ずつに分割し,
- * それぞれ base64url する実装に切り替えること. ここではまず JWK 出力を解析し,
- * 取得不能な場合に uncompressed point から座標を復元する.
+ * phpseclib3 の toString('JWK') を利用して x, y 座標を抽出する.
+ * JWK は {keys:[{...}]} か単体 {x,y} のいずれもあり得るため両形式に対応.
  *
  * `@return` array{x: string, y: string}
  */
🤖 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/Security/UcpMessageSigner.php` around lines
207 - 238, The comment above extractCoordinates() claims a fallback will
reconstruct x/y from an uncompressed EC point, but the method actually only
parses JWK and throws a RuntimeException on failure; update the comment to
reflect the real behavior: state that the method parses
PublicKey::toString('JWK') for x/y and will throw if x or y are missing
(phpseclib3's toString('JWK') normally provides x/y), and remove any mention of
a fallback/uncompressed-point reconstruction; keep the function name
extractCoordinates and the thrown RuntimeException unchanged.
app/DoctrineMigrations/Version20260604120000.php (2)

45-45: 💤 Low value

可読性のため INSERT 文の分割を検討してください。

249件のレコードを1行の INSERT 文に含めているため、コードレビューやメンテナンスが困難です。複数の addSql() 呼び出しに分割することで可読性が向上します。

なお、これは一度きりのバックフィルマイグレーションであるため、影響は限定的です。

🤖 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 `@app/DoctrineMigrations/Version20260604120000.php` at line 45, The single-line
INSERT with 249 tuples in Version20260604120000::up() (the $this->addSql(...)
call) hurts readability and should be split into multiple smaller INSERT
statements; locate the long addSql("INSERT INTO mtb_country_iso_code ...")
invocation and replace it with several addSql() calls each inserting a subset
(e.g. batches of ~50 or one per logical region) or use multiple VALUES groups so
each call is short and easy to review, keeping the same table/columns and
discriminator_type values and preserving order/IDs.

40-40: ⚡ Quick win

テーブル名を識別子クォートで保護してください。

SQL文字列内でテーブル名を直接連結しています。self::NAME は定数のため実際の脆弱性リスクは低いですが、Doctrine DBAL の Connection::quoteIdentifier() を使用してテーブル識別子を適切にエスケープすることを推奨します。

🔒 推奨される修正
-        $count = $this->connection->fetchOne('SELECT COUNT(*) FROM '.self::NAME);
+        $count = $this->connection->fetchOne('SELECT COUNT(*) FROM '.$this->connection->quoteIdentifier(self::NAME));
🤖 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 `@app/DoctrineMigrations/Version20260604120000.php` at line 40, The SQL
concatenates self::NAME directly into the query passed to Connection::fetchOne;
wrap the table identifier using Doctrine DBAL's quoteIdentifier to escape it.
Replace the raw concatenation of self::NAME with a quoted identifier (e.g.
$this->connection->quoteIdentifier(self::NAME)) when building the 'SELECT
COUNT(*) FROM ...' SQL in the Version20260604120000 migration (where fetchOne is
called).
app/DoctrineMigrations/Version20260611120000.php (1)

47-47: ⚡ Quick win

テーブル名を識別子クォートで保護してください。

SQL文字列内でテーブル名を直接連結しています。$tableself::INSERTS のキーから取得されるため実際の脆弱性リスクは低いですが、Doctrine DBAL の Connection::quoteIdentifier() を使用してテーブル識別子を適切にエスケープすることを推奨します。

🔒 推奨される修正
-            $count = $this->connection->fetchOne('SELECT COUNT(*) FROM '.$table);
+            $count = $this->connection->fetchOne('SELECT COUNT(*) FROM '.$this->connection->quoteIdentifier($table));
🤖 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 `@app/DoctrineMigrations/Version20260611120000.php` at line 47, The query
concatenates $table directly into SQL in Version20260611120000 (the call to
$this->connection->fetchOne), so update that code to protect the table
identifier by passing $table through Doctrine DBAL's identifier-quoting method
(e.g. $this->connection->quoteIdentifier($table)) and use the quoted identifier
when building the SQL string; ensure you reference the same variable used from
self::INSERTS and replace the unquoted concatenation in the fetchOne call with
the quoted identifier.
🤖 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 `@src/Eccube/Entity/CheckoutSession.php`:
- Around line 361-364: The isExpired method currently uses a strict less-than
comparison which treats a session as valid at the exact moment expires_at ==
$now; change the boundary to use <= so the method returns true when expires_at
is equal to or before $now (update the comparison in isExpired to use <= with
the expires_at property).

In `@src/Eccube/Form/Type/Admin/ShopMasterType.php`:
- Around line 221-223: The admin UI is missing the ucp_catalog_requires_auth
field; add it to the ShopMasterType form and wire it through the admin template
and save flow: in ShopMasterType (class ShopMasterType) add
->add('ucp_catalog_requires_auth', ToggleSwitchType::class) so the form maps to
the BaseInfo::ucp_catalog_requires_auth property, then update
src/Eccube/Resource/template/admin/Setting/Shop/shop_master.twig to render the
new field alongside acp_checkout_enabled/ucp_checkout_enabled (use the same form
widget/label pattern), and ensure the controller or handler that processes and
persists the ShopMasterType form binds and flushes this property (the same save
path used for other ShopMasterType fields).

In `@src/Eccube/Service/AgentCommerce/CheckoutSession/AgentCheckoutLineItem.php`:
- Around line 24-27: The AgentCheckoutLineItem constructor allows invalid
values; add immediate lower-bound validation in the
AgentCheckoutLineItem::__construct to ensure productClassId > 0 and quantity > 0
and throw a clear exception (e.g., InvalidArgumentException) when violated so
invalid DTOs are never created; update the constructor to validate the public
int $productClassId and public int $quantity parameters and include descriptive
error messages mentioning the offending parameter.

In `@src/Eccube/Service/AgentCommerce/CheckoutSession/AgentCheckoutRequest.php`:
- Around line 28-35: The constructor of AgentCheckoutRequest currently accepts a
bare array for the lineItems parameter which can contain invalid types; update
the AgentCheckoutRequest::__construct to validate each element of $lineItems
using instanceof AgentCheckoutLineItem and throw a clear exception (e.g.,
InvalidArgumentException or TypeError) if any element is not an
AgentCheckoutLineItem so the contract is enforced at instantiation and
downstream code cannot receive invalid items.

In `@src/Eccube/Service/AgentCommerce/MinorUnitConverter.php`:
- Around line 67-71: The code in MinorUnitConverter currently swallows
\ValueError in the catch blocks and returns 0, causing silent fail-open
behavior; replace those catch blocks inside the MinorUnitConverter methods that
catch \ValueError (the catch at lines shown and the similar one around 107-118)
to throw a descriptive exception (e.g., \InvalidArgumentException) instead of
returning 0 so invalid amount/currency inputs fail-fast; include the original
input value and currency code in the exception message to aid debugging and let
callers handle the error.

In `@src/Eccube/Service/AgentCommerce/Security/FilesystemKeyStore.php`:
- Line 66: FilesystemKeyStore currently calls chmod($path, 0600) without
checking the return; update the code around the chmod($path, 0600) call (in
class FilesystemKeyStore, where the key file is written) to test the boolean
return and throw an exception (e.g., RuntimeException) if chmod returns false,
including the $path in the error message so callers know which file failed to
get secure permissions.
- Around line 54-67: In FilesystemKeyStore::write, avoid creating the key file
with default umask by temporarily setting umask(0077) before calling
file_put_contents and restoring the previous umask in a finally block so it is
always restored even on write failure; wrap the file_put_contents (and
subsequent chmod($path, 0600)) in a try/finally to ensure umask is reset and
preserve existing error handling that throws RuntimeException on failure,
referencing the write method and resolvePath to locate the change.

---

Nitpick comments:
In `@app/DoctrineMigrations/Version20260604120000.php`:
- Line 45: The single-line INSERT with 249 tuples in Version20260604120000::up()
(the $this->addSql(...) call) hurts readability and should be split into
multiple smaller INSERT statements; locate the long addSql("INSERT INTO
mtb_country_iso_code ...") invocation and replace it with several addSql() calls
each inserting a subset (e.g. batches of ~50 or one per logical region) or use
multiple VALUES groups so each call is short and easy to review, keeping the
same table/columns and discriminator_type values and preserving order/IDs.
- Line 40: The SQL concatenates self::NAME directly into the query passed to
Connection::fetchOne; wrap the table identifier using Doctrine DBAL's
quoteIdentifier to escape it. Replace the raw concatenation of self::NAME with a
quoted identifier (e.g. $this->connection->quoteIdentifier(self::NAME)) when
building the 'SELECT COUNT(*) FROM ...' SQL in the Version20260604120000
migration (where fetchOne is called).

In `@app/DoctrineMigrations/Version20260611120000.php`:
- Line 47: The query concatenates $table directly into SQL in
Version20260611120000 (the call to $this->connection->fetchOne), so update that
code to protect the table identifier by passing $table through Doctrine DBAL's
identifier-quoting method (e.g. $this->connection->quoteIdentifier($table)) and
use the quoted identifier when building the SQL string; ensure you reference the
same variable used from self::INSERTS and replace the unquoted concatenation in
the fetchOne call with the quoted identifier.

In `@src/Eccube/Service/AgentCommerce/Security/UcpMessageSigner.php`:
- Around line 207-238: The comment above extractCoordinates() claims a fallback
will reconstruct x/y from an uncompressed EC point, but the method actually only
parses JWK and throws a RuntimeException on failure; update the comment to
reflect the real behavior: state that the method parses
PublicKey::toString('JWK') for x/y and will throw if x or y are missing
(phpseclib3's toString('JWK') normally provides x/y), and remove any mention of
a fallback/uncompressed-point reconstruction; keep the function name
extractCoordinates and the thrown RuntimeException unchanged.

In `@tests/Eccube/Tests/Entity/CheckoutSessionTest.php`:
- Around line 52-58: The helper findStatus() assumes statusRepository->find($id)
never returns null but overrides the type with `@var`; to improve type-safety add
an assertion before returning: call self::assertNotNull($status) (and optionally
self::assertInstanceOf(CheckoutSessionStatus::class, $status)) inside the
findStatus() method so the test fails clearly if the master data is missing and
the returned value can be safely treated as CheckoutSessionStatus.

In `@tests/Eccube/Tests/Service/AgentCommerce/AddressMappingServiceTest.php`:
- Around line 166-184: In loadCountryIdsFromCsv, make resource handling
defensive: after $handle = fopen(...), check is_resource($handle) and return an
empty array (or fail the test) before calling assertIsResource, or better wrap
the CSV processing loop in a try-finally so fclose($handle) is always called;
reference the $handle variable, the fopen/fgetcsv/fclose calls and the
assertIsResource assertion so you ensure fclose runs even if assertIsResource
throws.

In
`@tests/Eccube/Tests/Service/AgentCommerce/AgentCheckoutPurchaseFlowAdapterTest.php`:
- Line 95: Replace the vague instance-type assertion with a null check: in
AgentCheckoutPurchaseFlowAdapterTest change the assertion that checks
$Order->getCustomer() (currently using assertNotInstanceOf(Customer::class,
...)) to assertNull($Order->getCustomer()) so the test explicitly verifies the
customer is null for guest purchases; update the assertion call where $Order and
getCustomer() are referenced.
🪄 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: 4a0572d0-a848-48fc-b308-fb61cc862268

📥 Commits

Reviewing files that changed from the base of the PR and between f2711b3 and 39f9453.

⛔ Files ignored due to path filters (8)
  • src/Eccube/Resource/doctrine/import_csv/en/dtb_base_info.csv is excluded by !**/*.csv
  • src/Eccube/Resource/doctrine/import_csv/en/mtb_agent_protocol.csv is excluded by !**/*.csv
  • src/Eccube/Resource/doctrine/import_csv/en/mtb_checkout_session_status.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_agent_protocol.csv is excluded by !**/*.csv
  • src/Eccube/Resource/doctrine/import_csv/ja/mtb_checkout_session_status.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 (61)
  • .gitignore
  • .htaccess
  • app/DoctrineMigrations/Version20260604120000.php
  • app/DoctrineMigrations/Version20260611120000.php
  • app/config/eccube/services.yaml
  • app/config/eccube/services_test.yaml
  • app/keystore/.gitkeep
  • app/keystore/.htaccess
  • src/Eccube/Entity/BaseInfo.php
  • src/Eccube/Entity/Cart.php
  • src/Eccube/Entity/CheckoutSession.php
  • src/Eccube/Entity/Master/AgentProtocol.php
  • src/Eccube/Entity/Master/CheckoutSessionStatus.php
  • src/Eccube/Entity/Master/CountryIsoCode.php
  • src/Eccube/Entity/Order.php
  • src/Eccube/Form/Type/Admin/ShopMasterType.php
  • src/Eccube/Repository/CheckoutSessionRepository.php
  • src/Eccube/Repository/Master/AgentProtocolRepository.php
  • src/Eccube/Repository/Master/CheckoutSessionStatusRepository.php
  • src/Eccube/Repository/Master/CountryIsoCodeRepository.php
  • 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/Setting/Shop/shop_master.twig
  • src/Eccube/Service/AgentCommerce/AddressMappingService.php
  • src/Eccube/Service/AgentCommerce/AgentCheckoutPurchaseFlowAdapter.php
  • src/Eccube/Service/AgentCommerce/CheckoutSession/AgentCheckoutAddress.php
  • src/Eccube/Service/AgentCommerce/CheckoutSession/AgentCheckoutLineItem.php
  • src/Eccube/Service/AgentCommerce/CheckoutSession/AgentCheckoutMessage.php
  • src/Eccube/Service/AgentCommerce/CheckoutSession/AgentCheckoutMessageLevel.php
  • src/Eccube/Service/AgentCommerce/CheckoutSession/AgentCheckoutRequest.php
  • src/Eccube/Service/AgentCommerce/CheckoutSession/AgentCheckoutResult.php
  • src/Eccube/Service/AgentCommerce/CheckoutSession/CustomerResolverInterface.php
  • src/Eccube/Service/AgentCommerce/CheckoutSession/GuestCustomerResolver.php
  • src/Eccube/Service/AgentCommerce/Exception/AgentCheckoutErrorCode.php
  • src/Eccube/Service/AgentCommerce/Exception/AgentCheckoutException.php
  • src/Eccube/Service/AgentCommerce/Fulfillment/FulfillmentOption.php
  • src/Eccube/Service/AgentCommerce/Fulfillment/FulfillmentOptionMapperInterface.php
  • src/Eccube/Service/AgentCommerce/Fulfillment/FulfillmentPaymentOption.php
  • src/Eccube/Service/AgentCommerce/Fulfillment/StandardFulfillmentOptionMapper.php
  • src/Eccube/Service/AgentCommerce/MinorUnitConverter.php
  • src/Eccube/Service/AgentCommerce/Payment/AgentCheckoutPaymentHandlerInterface.php
  • src/Eccube/Service/AgentCommerce/Security/AgentCommerceMessageSignerInterface.php
  • src/Eccube/Service/AgentCommerce/Security/AgentCommerceOAuth2Authenticator.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
  • src/Eccube/Service/CartService.php
  • tests/Eccube/Tests/Entity/CheckoutSessionTest.php
  • tests/Eccube/Tests/Service/AgentCommerce/AddressMappingServiceTest.php
  • tests/Eccube/Tests/Service/AgentCommerce/AgentCheckoutPurchaseFlowAdapterTest.php
  • tests/Eccube/Tests/Service/AgentCommerce/BaseInfoAgentCommerceFlagsTest.php
  • tests/Eccube/Tests/Service/AgentCommerce/Conformance/AgentCheckoutCoreConformanceTest.php
  • tests/Eccube/Tests/Service/AgentCommerce/Conformance/AgentCommerceBaseConformanceTest.php
  • tests/Eccube/Tests/Service/AgentCommerce/Fulfillment/StandardFulfillmentOptionMapperTest.php
  • tests/Eccube/Tests/Service/AgentCommerce/MinorUnitConverterTest.php
  • tests/Eccube/Tests/Service/AgentCommerce/Security/AgentCommerceOAuth2AuthenticatorTest.php
  • tests/Eccube/Tests/Service/AgentCommerce/Security/AgentCommerceScopeRegistryTest.php
  • tests/Eccube/Tests/Service/AgentCommerce/Security/UcpMessageSignerTest.php

Comment thread src/Eccube/Entity/CheckoutSession.php
Comment thread src/Eccube/Form/Type/Admin/ShopMasterType.php
Comment thread src/Eccube/Service/AgentCommerce/MinorUnitConverter.php
Comment thread src/Eccube/Service/AgentCommerce/Security/FilesystemKeyStore.php
Comment thread src/Eccube/Service/AgentCommerce/Security/FilesystemKeyStore.php Outdated
nanasess and others added 6 commits June 12, 2026 15:09
FilesystemKeyStore::write が file_put_contents で umask 既定 (通常 0644) の
パーミッションでファイルを作成し、その後 chmod(0600) するため、その間だけ
秘密鍵が group/other から読める瞬間が生じていた。書き込みの間だけ
umask(0077) に切り替え、作成時点から 0600 になるようにする。あわせて
chmod の戻り値を検査し失敗時に例外を投げる。

CodeRabbit レビュー指摘 (PR EC-CUBE#6825 経由・本ファイルは EC-CUBE#6802 由来) 対応。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
エージェントコマース (ACP/UCP) checkout の前提となる、プロトコル非依存の
CheckoutSession 中核を実装する。checkout エンドポイント本体は EC-CUBE#6776/EC-CUBE#6574 に委ね、
本コミットは再利用可能なエンティティ・サービス・認証部品とテストを提供する。

- CheckoutSession エンティティ + Repository を新規追加 (Cart をセッションレスに
  束ねる上位層・status 正規化(canceled 含む)・json 列・Cart/Order/Customer 関連)
- Order に agent_protocol / agent_id を NULL 許容で追加 (通常購入は両 NULL)
- AgentCheckoutPurchaseFlowAdapter: 中立 DTO → Cart(永続) → OrderHelper →
  既存 shopping flow 再利用で税・送料・在庫を再計算 (新規フローは作らない)
- FulfillmentOptionMapper: 送料(DeliveryFee×Pref)・代引手数料(PaymentOption→
  Payment::charge)・配送日数(DeliveryDuration の明細横断 max) を解決
- CustomerResolverInterface + GuestCustomerResolver (標準はゲスト=null。会員 ID
  連携は eccube-api4#189 landing 後に差し替える seam)
- AgentCommerceOAuth2Authenticator: Symfony 標準 AccessTokenHandlerInterface 経由で
  トークン検証 + scope×protocol 照合。eccube-api4 具象に依存せず、未導入時 503
- AgentCheckoutException + ErrorCode enum、AgentCheckoutPaymentHandlerInterface 共通基底
- カラム追加・新規テーブルは migration を書かず schema:update 方式 (既存規約に準拠)

テスト: Layer 0 (仕様適合) / Layer 2 (Doctrine) / Layer 3 (PurchaseFlow 連携) /
Layer 4a (OAuth2 ユニット・api4 不要)。AgentCommerce スイート 92 tests 0 失敗、
PHPStan level6 No errors。Layer 4b 統合は eccube-api4#188 landing 後。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ull 引数除去)

CI の rector ジョブ指摘を反映:
- DTO/値オブジェクトを readonly class 化 (ReadOnlyAnonymousClassRector ほか)
- テストの self::assert* を $this->assert* に統一 (PreferPHPUnitThisCallRector)
- null デフォルト引数への明示 null 渡しを除去 (RemoveNullArgOnNullDefaultParamRector)

PHPStan level6 No errors / AgentCommerce 92 tests 0 失敗を維持。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CheckoutSession.status / protocol および Order.agent_protocol を、文字列カラム +
クラス文字列定数から、Order/OrderStatus と同じ「マスタ + INTEGER ManyToOne」方式へ変更。
区分値を文字列で保存しない EC-CUBE コア慣習に統一する (protocol は CheckoutSession と
Order の双方で参照するため特にマスタ化が必須)。

- 新規マスタ mtb_checkout_session_status (CheckoutSessionStatus・INCOMPLETE=1..EXPIRED=5)
- 新規マスタ mtb_agent_protocol (AgentProtocol・ACP=1/UCP=2)
- 各 Repository、import_csv (ja/en・name は正準値で同一)、definition.yml 登録
- 既存インストール向け backfill の INSERT migration (冪等・down 非対応)
- CheckoutSession.status/protocol、Order.agent_protocol を ManyToOne 関連へ
- AgentCheckoutRequest.protocol(string) → protocolId(int)、Adapter で AgentProtocol 解決
- テストをマスタ参照へ更新 (status()/PHPUnit final 衝突回避で findStatus にリネーム)

PHPStan level6 No errors / AgentCommerce 93 tests 0 失敗 / Order・PurchaseFlow 回帰 green。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CheckoutSession が参照する Cart が、ログイン会員の Web カート解決
(CartService::getPersistedCarts は customer_id で全カートを取得) に混入し、
マージ・再計算・購入完了時の削除等で操作されてしまう問題を防ぐ。
Web 側の Cart 再生成に CheckoutSession を追従させるのは侵襲的で脆いため、
エージェント所有カートを Web から不可視・操作不可に隔離する方針とする。

- Cart に agent_owned (boolean, default false) を追加
- CartService::getPersistedCarts/getSessionCarts の検索条件に agent_owned=false を追加し、
  エージェント所有カートを Web カート解決から除外 (既存カートは全て false で無影響)
- Adapter はカートに agent_owned=true をセットし、customer_id は常に NULL に保つ
  (会員帰属は Order 側に持たせ、会員 ID 連携時もカート混入を防ぐ多層防御)
- agent_owned カラム追加は schema:update 方式 (migration 不要)
- 隔離の回帰テストを追加

PHPStan level6 No errors / AgentCommerce 85 tests 0 失敗 / CartService 回帰 green。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
expires_at と現在時刻が同値の瞬間だけ未失効になっていた `<` 判定を
`<=` に変更し、境界を期限切れ扱いにする。境界値の回帰テストを追加。

CodeRabbit レビュー指摘 (PR EC-CUBE#6825) 対応。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@nanasess nanasess force-pushed the feature/agentic-commerce-checkout-core branch from 39f9453 to 875720a Compare June 12, 2026 06:09
nanasess and others added 2 commits June 15, 2026 13:09
OAuth2 Authenticator テストのスタブ handler が、rector の
ReadOnlyAnonymousClassRector により `new readonly class` (匿名 readonly
クラス) へ変換されていた。匿名 readonly クラスは PHP 8.3+ 専用で、
サポート下限 (PHP 8.2) の CI で `syntax error, unexpected token "readonly"`
となり PHPUnit が失敗していた。

名前付き `final readonly class ScopeStubAccessTokenHandler` (8.2 で有効・
rector の匿名クラスルール対象外) へ切り出して恒久的に解消する。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CheckoutSessionTest::testJsonColumnsRoundTrip が MySQL CI で失敗していた。
MySQL のネイティブ JSON 型は格納時にオブジェクトのキー順序を保持しない
(PostgreSQL/SQLite は保持) ため、多キーの buyer_data に対する assertSame
(順序厳密) が順序差で落ちていた。buyer_data はキーで参照する map で順序に
意味はないため、assertEqualsCanonicalizing で順序非依存に検証する。

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

codecov Bot commented Jun 15, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 82.04545% with 79 lines in your changes missing coverage. Please review.
✅ Project coverage is 74.96%. Comparing base (3f76a03) to head (3d39bf0).
⚠️ Report is 100 commits behind head on 4.4.

Files with missing lines Patch % Lines
...vice/AgentCommerce/Security/FilesystemKeyStore.php 0.00% 20 Missing ⚠️
src/Eccube/Entity/CheckoutSession.php 59.57% 19 Missing ⚠️
...ervice/AgentCommerce/Security/UcpMessageSigner.php 86.20% 12 Missing ⚠️
src/Eccube/Entity/BaseInfo.php 33.33% 6 Missing ⚠️
...ntCommerce/CheckoutSession/AgentCheckoutResult.php 33.33% 6 Missing ⚠️
...ce/Fulfillment/StandardFulfillmentOptionMapper.php 90.56% 5 Missing ⚠️
...be/Service/AgentCommerce/AddressMappingService.php 90.32% 3 Missing ⚠️
...AgentCommerce/AgentCheckoutPurchaseFlowAdapter.php 96.05% 3 Missing ⚠️
...ccube/Service/AgentCommerce/MinorUnitConverter.php 92.00% 2 Missing ⚠️
...Commerce/CheckoutSession/GuestCustomerResolver.php 0.00% 1 Missing ⚠️
... and 2 more
Additional details and impacted files
@@             Coverage Diff              @@
##                4.4    #6825      +/-   ##
============================================
- Coverage     75.10%   74.96%   -0.14%     
============================================
  Files           483      485       +2     
  Lines         26279    24467    -1812     
============================================
- Hits          19736    18341    -1395     
+ Misses         6543     6126     -417     
Flag Coverage Δ
Unit 74.96% <82.04%> (-0.14%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

AgentCheckoutPurchaseFlowAdapter::complete() は validate→prepare→commit を一括実行して
いたが、EC-CUBE 標準の決済フロー (在庫引当・採番=prepare → 決済オーソリ → commit/rollback,
PaymentMethod が駆動) と順序が異なり、在庫検証前に決済しうる問題があった (sample-payment-plugin
CreditCard の apply/checkout と乖離)。

- complete() を廃止し prepare()/commit()/rollback() を個別公開。順序とトランザクション境界の
  制御は決済ハンドラ (PaymentMethod 相当) の責務とする。決済処理全体を DB トランザクションで
  囲まない (EMV-3DS 等の外部サイト遷移はリクエストを跨ぐため単一トランザクションで囲えない)。
- AgentCheckoutResult に紐付け元 Cart を追加 (CheckoutSession の cart_id 設定・
  pre_order_id 整合性チェック用)。
- PreOrderIdValidator::rollback はセッションレスなエージェント受注 (agent_protocol!=null) を
  適用外とする。操作中カートと受注の越境防止は CheckoutSession (推測不能な session_id +
  protocol 照合 + cart/order の pre_order_id 整合) が controller 層で担保する。

通常購入 (agent_protocol=null) は従来どおり。ローカル検証: AgentCommerce 95 tests / PurchaseFlow
256 tests / 0 失敗、PHPStan level6 No errors、php-cs-fixer 0 件。PG/MySQL マトリクス・E2E は CI。

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

@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

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/Eccube/Service/AgentCommerce/AgentCheckoutPurchaseFlowAdapter.php (1)

81-83: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

protocolId 未解決時に agent 判定が失われ、rollback 経路が破綻します。

Line 82 で find()null を返してもそのまま進むため、Order->getAgentProtocol()null の注文が作成されます。PreOrderIdValidator::rollback は agent protocol 非 null の場合のみ session チェックをスキップするため、この状態だとセッションレスのエージェント注文で rollback 時に BadRequestHttpException に落ちる経路が残ります。protocolId 指定時は「存在確認して未解決なら AgentCheckoutException」を必須化してください。

修正イメージ
-        if ($request->protocolId !== null) {
-            $Order->setAgentProtocol($this->agentProtocolRepository->find($request->protocolId));
-        }
+        if ($request->protocolId === null) {
+            throw new AgentCheckoutException(AgentCheckoutErrorCode::INVALID_PROTOCOL, 'protocolId is required');
+        }
+        $AgentProtocol = $this->agentProtocolRepository->find($request->protocolId);
+        if ($AgentProtocol === null) {
+            throw new AgentCheckoutException(AgentCheckoutErrorCode::INVALID_PROTOCOL, sprintf('unsupported protocolId: %d', $request->protocolId));
+        }
+        $Order->setAgentProtocol($AgentProtocol);
// src/Eccube/Service/AgentCommerce/Exception/AgentCheckoutErrorCode.php
case INVALID_PROTOCOL = 'invalid_protocol';
🤖 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/AgentCheckoutPurchaseFlowAdapter.php` around
lines 81 - 83, The current code in AgentCheckoutPurchaseFlowAdapter does not
validate that the protocolId actually exists when calling
agentProtocolRepository->find(), allowing null values to be set on Order which
breaks the rollback validation flow. Add validation after the find() call in the
protocolId block to check if the result is null, and if so, throw an
AgentCheckoutException instead of continuing. Additionally, add a new error code
INVALID_PROTOCOL to the AgentCheckoutErrorCode enum to represent this validation
failure. This ensures that when a protocolId is specified but doesn't exist, an
appropriate exception is raised rather than creating an Order with null agent
protocol that will fail during rollback in PreOrderIdValidator.
🤖 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.

Outside diff comments:
In `@src/Eccube/Service/AgentCommerce/AgentCheckoutPurchaseFlowAdapter.php`:
- Around line 81-83: The current code in AgentCheckoutPurchaseFlowAdapter does
not validate that the protocolId actually exists when calling
agentProtocolRepository->find(), allowing null values to be set on Order which
breaks the rollback validation flow. Add validation after the find() call in the
protocolId block to check if the result is null, and if so, throw an
AgentCheckoutException instead of continuing. Additionally, add a new error code
INVALID_PROTOCOL to the AgentCheckoutErrorCode enum to represent this validation
failure. This ensures that when a protocolId is specified but doesn't exist, an
appropriate exception is raised rather than creating an Order with null agent
protocol that will fail during rollback in PreOrderIdValidator.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d38d9c52-5a87-4e24-8773-f1c9866f824f

📥 Commits

Reviewing files that changed from the base of the PR and between 2c0bbf9 and 3d39bf0.

📒 Files selected for processing (4)
  • src/Eccube/Service/AgentCommerce/AgentCheckoutPurchaseFlowAdapter.php
  • src/Eccube/Service/AgentCommerce/CheckoutSession/AgentCheckoutResult.php
  • src/Eccube/Service/PurchaseFlow/Processor/PreOrderIdValidator.php
  • tests/Eccube/Tests/Service/AgentCommerce/AgentCheckoutPurchaseFlowAdapterTest.php
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/Eccube/Service/AgentCommerce/CheckoutSession/AgentCheckoutResult.php

nanasess and others added 5 commits June 17, 2026 16:19
…tionService)

追加認証 (EMV-3DS) / escalation はエラーでなく complete が返す正常な中間状態であり、
complete は冪等な単発呼び出しでなく状態に応じて複数回呼ばれる状態機械である (EC-CUBE#6777)。

- 決済ハンドラ結果型 PaymentOutcome (COMPLETED/REQUIRES_ACTION/PENDING/FAILED) を新設し、
  AgentCheckoutPaymentHandlerInterface の authorize/capture 戻り値を void から PaymentOutcome へ改訂
- AgentCheckoutCompletionService (状態機械オーケストレータ) を新設。冪等性・初回/再開フロー・
  在庫引当の保持/回収・トランザクション境界を core に集約 (prepare 成功 + authorize COMPLETED 後にのみ
  capture する順序を保証)
- 正規化ステータスマスタに requires_action(6)/in_progress(7) を追加 (CSV + 既存環境向け INSERT migration)。
  findExpired は終端 3 値のみ除外のため両者を自動的に回収対象に含む
- 在庫確保期限を eccube.yaml の eccube_agent_checkout_escalation_expire (既定 15 分・EMV-3DS の 10 分より大) で設定
- AgentCheckoutPaymentHandlerRegistry (core 中立・resolveForOrder) を新設

検証: AgentCommerce+CheckoutSession 104 tests 0 失敗 (incomplete 3 意図的) /
PHPStan level6 src No errors / php-cs-fixer 0 / PurchaseFlow・OrderHelper 回帰 green

Refs EC-CUBE#6777 EC-CUBE#6825

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Idempotency 基盤をプロトコル非依存のコア (EC-CUBE#6777) として実装する。従来の cache+Symfony Lock 方式は
ストアがノードローカル (SemaphoreStore/FlockStore・filesystem cache) でマルチインスタンス (AWS 等) では
越境・並行の二重実行を防げないため、DB の一意制約方式へ作り替える。

- Entity `AgentCheckoutIdempotency` (`dtb_agent_checkout_idempotency`・unique(idempotency_key, subject))
  + Repository。テーブルは空始動のため schema:update 方式 (migration ファイル不要)
- `AgentCheckoutIdempotencyStore` を DB ベースに実装: 予約 INSERT を一意制約で直列化し、
  並行時は処理中=409 / 完了済=リプレイ / 異パラメータ=409 を判定。compute 失敗時は予約を削除し再試行可能に。
  Symfony Lock は撤去 (ロック TTL 問題も解消)。subject (認証エージェント) で名前空間化し越境リプレイ防止
- `IdempotencyConflictException` をコアへ配置 (UCP/ACP 双方が利用)
- 単一の共有 DB だけで動作し、Redis 等の共有キャッシュ・分散ロックに非依存 (共有レンサバ〜AWS 全形態で正しい)

検証: AgentCommerce 110 tests 0 失敗 (incomplete 3 意図的) / PHPStan level6 No errors / php-cs-fixer 0

Refs EC-CUBE#6777

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CI rector が指摘した nullable instanceof アサート化・self→$this 呼び出し・
readonly プロパティ化を適用 (PreferPHPUnitThisCall /
AssertEmptyNullableObjectToAssertInstanceof /
AddInstanceofAssertForNullableInstance / ReadOnlyProperty)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ACP / UCP 両 checkout のレスポンス (注文 permalink・プライバシーポリシー /
利用規約リンク) で共用するプロトコル中立な店舗 URL 生成ユーティリティを共通基盤に置く。
標準ページ (help_privacy / help_agreement / mypage_history) から RequestContext
ベースの絶対 URL を実行時生成し、app/Customize で decoration 可能。

スタックする checkout PR (EC-CUBE#6574 UCP / EC-CUBE#6776 ACP) が重複実装せず本サービスを consume する。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ACP / UCP の postal address (region/state) から EC-CUBE の Pref を解決する
getPrefFromRegion() を共通基盤へ追加。Pref 名で逆引きし、ISO 3166-2:JP コード等は
app/Customize の拡張余地とする。スタックする checkout PR (EC-CUBE#6574 / EC-CUBE#6776) が共用する。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
nanasess added a commit to nanasess/ec-cube that referenced this pull request Jun 19, 2026
PR EC-CUBE#6843 のレビュー指摘のうち ACP 層所管の 6 件を修正:

- complete の decodeBody() を try 内へ移動。不正 JSON が 500 ではなく
  400 プロトコルエラーに変換され、create/update と一貫する
- findSession にセッション所有者 (agent_id) 照合を追加。他エージェントの
  セッションへの越境アクセスを存在秘匿で 404 拒否 (多層防御)
- line_items の id/quantity を正の整数のみ許可。非数値→0・負数の素通りを
  防ぎ、不正値は AgentCheckoutException → 400 に寄せる
- AcpMessageMapper の raw HTML 検知を包括化。任意タグ・HTML コメントを
  検知しつつ CommonMark の autolink は誤検知しない
- Discovery テストの max-age を固定値一致から >= 3600 の下限判定へ
- テスト Stub に declare(strict_types=1) を追加

越境遮断・不正値・不正 JSON・raw HTML 包括検知の回帰テストを追加。
B 群 (EC-CUBE#6825 checkout-core) / C 群 (EC-CUBE#6802 base) 所管の指摘は各 PR 側で対応。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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