feat(agent-commerce): CheckoutSession 中核を実装 (#6777 トラックB前提・Phase 1b)#6825
feat(agent-commerce): CheckoutSession 中核を実装 (#6777 トラックB前提・Phase 1b)#6825nanasess wants to merge 24 commits into
Conversation
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>
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughCheckoutSessionを中心に、マスタデータ・エンティティ、住所・通貨・配送マッピング、鍵管理・署名、OAuth2認証・スコープ検証、購入フロー統合、管理UI、包括的テストを追加する変更です。 ChangesAgent Commerce機能追加
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45分 Possibly related issues
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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は不要ですが、より防御的には以下のいずれかが考えられます:
assertIsResourceの前にifで型チェックし、リソースでない場合は早期リターン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文字列内でテーブル名を直接連結しています。
$tableはself::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
⛔ Files ignored due to path filters (8)
src/Eccube/Resource/doctrine/import_csv/en/dtb_base_info.csvis excluded by!**/*.csvsrc/Eccube/Resource/doctrine/import_csv/en/mtb_agent_protocol.csvis excluded by!**/*.csvsrc/Eccube/Resource/doctrine/import_csv/en/mtb_checkout_session_status.csvis excluded by!**/*.csvsrc/Eccube/Resource/doctrine/import_csv/en/mtb_country_iso_code.csvis excluded by!**/*.csvsrc/Eccube/Resource/doctrine/import_csv/ja/dtb_base_info.csvis excluded by!**/*.csvsrc/Eccube/Resource/doctrine/import_csv/ja/mtb_agent_protocol.csvis excluded by!**/*.csvsrc/Eccube/Resource/doctrine/import_csv/ja/mtb_checkout_session_status.csvis excluded by!**/*.csvsrc/Eccube/Resource/doctrine/import_csv/ja/mtb_country_iso_code.csvis excluded by!**/*.csv
📒 Files selected for processing (61)
.gitignore.htaccessapp/DoctrineMigrations/Version20260604120000.phpapp/DoctrineMigrations/Version20260611120000.phpapp/config/eccube/services.yamlapp/config/eccube/services_test.yamlapp/keystore/.gitkeepapp/keystore/.htaccesssrc/Eccube/Entity/BaseInfo.phpsrc/Eccube/Entity/Cart.phpsrc/Eccube/Entity/CheckoutSession.phpsrc/Eccube/Entity/Master/AgentProtocol.phpsrc/Eccube/Entity/Master/CheckoutSessionStatus.phpsrc/Eccube/Entity/Master/CountryIsoCode.phpsrc/Eccube/Entity/Order.phpsrc/Eccube/Form/Type/Admin/ShopMasterType.phpsrc/Eccube/Repository/CheckoutSessionRepository.phpsrc/Eccube/Repository/Master/AgentProtocolRepository.phpsrc/Eccube/Repository/Master/CheckoutSessionStatusRepository.phpsrc/Eccube/Repository/Master/CountryIsoCodeRepository.phpsrc/Eccube/Resource/doctrine/import_csv/en/definition.ymlsrc/Eccube/Resource/doctrine/import_csv/ja/definition.ymlsrc/Eccube/Resource/locale/messages.en.yamlsrc/Eccube/Resource/locale/messages.ja.yamlsrc/Eccube/Resource/template/admin/Setting/Shop/shop_master.twigsrc/Eccube/Service/AgentCommerce/AddressMappingService.phpsrc/Eccube/Service/AgentCommerce/AgentCheckoutPurchaseFlowAdapter.phpsrc/Eccube/Service/AgentCommerce/CheckoutSession/AgentCheckoutAddress.phpsrc/Eccube/Service/AgentCommerce/CheckoutSession/AgentCheckoutLineItem.phpsrc/Eccube/Service/AgentCommerce/CheckoutSession/AgentCheckoutMessage.phpsrc/Eccube/Service/AgentCommerce/CheckoutSession/AgentCheckoutMessageLevel.phpsrc/Eccube/Service/AgentCommerce/CheckoutSession/AgentCheckoutRequest.phpsrc/Eccube/Service/AgentCommerce/CheckoutSession/AgentCheckoutResult.phpsrc/Eccube/Service/AgentCommerce/CheckoutSession/CustomerResolverInterface.phpsrc/Eccube/Service/AgentCommerce/CheckoutSession/GuestCustomerResolver.phpsrc/Eccube/Service/AgentCommerce/Exception/AgentCheckoutErrorCode.phpsrc/Eccube/Service/AgentCommerce/Exception/AgentCheckoutException.phpsrc/Eccube/Service/AgentCommerce/Fulfillment/FulfillmentOption.phpsrc/Eccube/Service/AgentCommerce/Fulfillment/FulfillmentOptionMapperInterface.phpsrc/Eccube/Service/AgentCommerce/Fulfillment/FulfillmentPaymentOption.phpsrc/Eccube/Service/AgentCommerce/Fulfillment/StandardFulfillmentOptionMapper.phpsrc/Eccube/Service/AgentCommerce/MinorUnitConverter.phpsrc/Eccube/Service/AgentCommerce/Payment/AgentCheckoutPaymentHandlerInterface.phpsrc/Eccube/Service/AgentCommerce/Security/AgentCommerceMessageSignerInterface.phpsrc/Eccube/Service/AgentCommerce/Security/AgentCommerceOAuth2Authenticator.phpsrc/Eccube/Service/AgentCommerce/Security/AgentCommerceScopeRegistry.phpsrc/Eccube/Service/AgentCommerce/Security/FilesystemKeyStore.phpsrc/Eccube/Service/AgentCommerce/Security/KeyStoreInterface.phpsrc/Eccube/Service/AgentCommerce/Security/UcpMessageSigner.phpsrc/Eccube/Service/CartService.phptests/Eccube/Tests/Entity/CheckoutSessionTest.phptests/Eccube/Tests/Service/AgentCommerce/AddressMappingServiceTest.phptests/Eccube/Tests/Service/AgentCommerce/AgentCheckoutPurchaseFlowAdapterTest.phptests/Eccube/Tests/Service/AgentCommerce/BaseInfoAgentCommerceFlagsTest.phptests/Eccube/Tests/Service/AgentCommerce/Conformance/AgentCheckoutCoreConformanceTest.phptests/Eccube/Tests/Service/AgentCommerce/Conformance/AgentCommerceBaseConformanceTest.phptests/Eccube/Tests/Service/AgentCommerce/Fulfillment/StandardFulfillmentOptionMapperTest.phptests/Eccube/Tests/Service/AgentCommerce/MinorUnitConverterTest.phptests/Eccube/Tests/Service/AgentCommerce/Security/AgentCommerceOAuth2AuthenticatorTest.phptests/Eccube/Tests/Service/AgentCommerce/Security/AgentCommerceScopeRegistryTest.phptests/Eccube/Tests/Service/AgentCommerce/Security/UcpMessageSignerTest.php
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>
39f9453 to
875720a
Compare
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 Report❌ Patch coverage is 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
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
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>
There was a problem hiding this comment.
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
📒 Files selected for processing (4)
src/Eccube/Service/AgentCommerce/AgentCheckoutPurchaseFlowAdapter.phpsrc/Eccube/Service/AgentCommerce/CheckoutSession/AgentCheckoutResult.phpsrc/Eccube/Service/PurchaseFlow/Processor/PreOrderIdValidator.phptests/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
…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>
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>
概要(Overview・Refs Issue)
エージェントコマース (ACP/UCP) 対応の共通基盤 #6777 のうち、CheckoutSession 中核 (Phase 1b) を実装します。AI エージェント (ChatGPT / Gemini 等) 経由の購入フローの前提となる、プロトコル非依存のエンティティ・サービス・認証部品を提供します。
方針(Policy)
eccube.purchase.flow.shoppingを再利用します。これにより税計算・送料・代引手数料・ポイント・在庫引当・受注番号採番のロジックを完全共有し、エージェント経由でも通常購入と同じ購入処理を通します (PurchaseFlow機構の拡充)。CheckoutSessionはCart/CartItemの代替ではなく、Cart をセッションレスに束ねる上位層として設計しました。会員購入はdtb_cartに永続化されますが、エージェントはブラウザセッション (Cookie) を持たないため、CheckoutSession.session_id(推測困難な公開識別子) がcart_key/セッションの代替になります。明細はCart/CartItemを再利用し、確定時は通常購入と同様にpre_order_id経由でOrderを生成します。CustomerResolverInterfaceの標準実装は常に null (ゲスト) を返します。会員に紐づく OAuth2 トークンによる解決は eccube-api4 の機能追加 (Customer(会員) に紐づく OAuth2 authorization_code フロー (ID 連携) のサポート eccube-api4#189) に依存するため、差し替え可能な seam に留めています。AccessTokenHandlerInterface経由でトークン検証します (Agentic Commerce 向け client_credentials グラント有効化とプロトコル scope の登録 eccube-api4#188 が前提・未導入時は 503)。ユニットテストでは handler をスタブ化し、api4 を要求しません。schema:updateに委ねます (既存の EC-CUBE 規約・前例 Googleアナリティクス機能を追加 #4912 に準拠)。CheckoutSessionは空テーブル始動・マスタデータ無しのため INSERT migration も不要です。実装に関する補足(Appendix)
追加した主なクラス:
CheckoutSession(+CheckoutSessionRepository)、Orderにagent_protocol/agent_idを NULL 許容で追加AgentCheckoutPurchaseFlowAdapter(中立 DTO →Cart(永続) →OrderHelper→ shopping flow 再計算 → 結果)AgentCheckoutRequest/AgentCheckoutLineItem/AgentCheckoutAddress/AgentCheckoutResult/AgentCheckoutMessage(+Level)FulfillmentOptionMapperInterface+StandardFulfillmentOptionMapper(送料DeliveryFee×Pref・代引手数料PaymentOption→Payment::getCharge()・配送日数DeliveryDurationの明細横断 max)CustomerResolverInterface+GuestCustomerResolver(標準はゲスト=null)AgentCommerceOAuth2Authenticator(SymfonyAccessTokenHandlerInterface経由・scope×protocol 照合・api4 未導入時 503)AgentCheckoutException+AgentCheckoutErrorCodeenum、AgentCheckoutPaymentHandlerInterface共通基底Orderの 2 カラムは本体直接改変 (Customize trait でなく) ですが、通常購入では両 NULL で後方互換を担保します。AgentCheckoutResult.messages[]に反映します (ACP/UCP の「HTTP 200 + messages[]」系統に対応)。HTTP ステータスへの 2 系統変換は checkout controller (Agentic Commerce Protocol (ACP) の対応 #6776/ユニバーサル コマース プロトコル(UCP)の対応 #6574) の責務です。app/config/eccube/services_test.yamlで中核サービスをテスト時のみ public 化しています (consumer となる checkout controller が Agentic Commerce Protocol (ACP) の対応 #6776/ユニバーサル コマース プロトコル(UCP)の対応 #6574 まで存在しないため)。テスト(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)。AgentCheckoutCoreConformanceTest):status正規化 (canceled含む)、messages[] の error/warning/info 語彙、Order の agent 帰属。HTTP 2 系統変換・Idempotency は controller 層へmarkTestIncomplete。CheckoutSessionTest): 状態遷移 (作成→更新→完了/canceled/expired)、json ラウンドトリップ、findExpired、schema:updateでのテーブル+Order 2 カラム生成、通常購入 Order で両カラム NULL の回帰。AgentCheckoutPurchaseFlowAdapterTest/StandardFulfillmentOptionMapperTest): DTO→Cart→Order→shopping flow 再計算、在庫超過の messages[] 反映 (例外でない)、complete で受注番号採番、代引 charge・配送日数 max・お取り寄せ null。AgentCommerceOAuth2AuthenticatorTest・eccube-api4 不要): handler スタブで 200/401/403、capability 越境 403、未導入 503。markTestSkipped)。ローカル DB は SQLite。PostgreSQL/MySQL 固有の migration 移植性は CI マトリクスで担保します。
相談(Discussion)
CheckoutSession↔Cartを関連で持ち、エージェント取引をセッションレスに束ねる設計としています。会員購入のカート永続化を再利用する方針で問題ないかご確認ください。マイナーバージョン互換性保持のための制限事項チェックリスト
レビュワー確認項目
Summary by CodeRabbit
追記 (2026-06-17): complete 状態機械化 + Idempotency を DB 一意制約ベースに追加
本 PR の中核に、その後の設計確定(#6777)に伴い以下を追加しました。
complete を「中断→再開」状態機械として実装
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 分より大)。Idempotency を DB 一意制約ベースで実装(マルチインスタンス対応)
Entity\AgentCheckoutIdempotency(dtb_agent_checkout_idempotency・unique(idempotency_key, subject))+ Repository + DB ベースのAgentCheckoutIdempotencyStoreを追加。検証
No errors/ php-cs-fixer 0 / PurchaseFlow・OrderHelper 回帰 green。