Skip to content

Conversation

@ildyria
Copy link
Member

@ildyria ildyria commented Dec 3, 2025

Summary by CodeRabbit

  • New Features

    • Added a completion URL to checkout responses for clearer order confirmation.
  • Bug Fixes

    • Preserve transaction references in payment metadata for successful non-redirect payments.
    • Return refreshed order data on direct payment success instead of triggering background completion.
    • Allow marking orders with CLOSED status as deliverable (no-content response).
    • Ensure finalization flow consistently refreshes order state before redirecting.

✏️ Tip: You can customize this high-level summary in your review settings.

@ildyria ildyria requested a review from a team as a code owner December 3, 2025 17:02
@coderabbitai
Copy link

coderabbitai bot commented Dec 3, 2025

📝 Walkthrough

Walkthrough

This PR modifies checkout payment handling to capture DummyResponse transaction references and persist them, removes non-redirect redirect_url, returns CheckoutResource including order data and a new complete_url, expands allowed order statuses to include CLOSED for delivery marking, and adjusts related tests accordingly.

Changes

Cohort / File(s) Summary
Payment processing service
app/Actions/Shop/CheckoutService.php
Import and handle DummyResponse; on successful non-redirect responses capture transactionReference into session metadata and assign transaction_id on the Order, remove redirect_url from non-redirect DTO, retain redirect handling for FetchTransactionResponse, and improve error logging.
Controller & resource → checkout flow
app/Http/Controllers/Shop/CheckoutController.php, app/Http/Resources/Shop/CheckoutResource.php
Controller now refreshes order and returns CheckoutResource directly for non-redirect successes instead of conditional event dispatch; CheckoutResource constructor gains public readonly ?string $complete_url = null and responses include order: OrderResource::fromModel($order).
Order delivery request
app/Http/Requests/Order/MarkAsDeliveredOrderRequest.php
Authorization formatting unchanged; processValidatedValues() broadened to accept orders with status COMPLETED or CLOSED when locating the order.
Tests updated
tests/Unit/Actions/Shop/CheckoutServiceTest.php, tests/Webshop/Checkout/CheckoutFinalizeOrCancelControllerTest.php, tests/Webshop/OrderManagement/OrderControllerMarkAsDeliveredTest.php
Adjusted expectations for non-redirect payment DTO (no redirect_url, empty message), changed checkout finalize/cancel flow assertions to reflect deferred completion via provider redirect, and updated mark-as-delivered test to expect 204 for closed orders.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Pay extra attention to:
    • app/Actions/Shop/CheckoutService.php — correctness of transaction capture, session metadata keys, and Order persistence.
    • app/Http/Controllers/Shop/CheckoutController.php & app/Http/Resources/Shop/CheckoutResource.php — ensure API shape change (added complete_url and order payload) is backward-compatible where required.
    • Tests — verify expectations align with new flow (non-redirect payload and finalize/cancel behavior).

Poem

🐰
I hopped through code at dawn's first light,
Captured refs and fixed the flight.
No redirect URL in sight,
Orders refreshed — the path feels right.
A tiny hop, a ledger bright 🥕✨

Pre-merge checks

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 62.50% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between be5da84 and f5b8cea.

📒 Files selected for processing (6)
  • app/Actions/Shop/CheckoutService.php (2 hunks)
  • app/Http/Controllers/Shop/CheckoutController.php (1 hunks)
  • app/Http/Requests/Order/MarkAsDeliveredOrderRequest.php (2 hunks)
  • tests/Unit/Actions/Shop/CheckoutServiceTest.php (1 hunks)
  • tests/Webshop/Checkout/CheckoutFinalizeOrCancelControllerTest.php (1 hunks)
  • tests/Webshop/OrderManagement/OrderControllerMarkAsDeliveredTest.php (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/Http/Controllers/Shop/CheckoutController.php
🧰 Additional context used
📓 Path-based instructions (4)
**/*.php

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.php: Any new PHP file should contain the license header and have a single blank line after the opening PHP tag
Variable names should be in snake_case
Apply PSR-4 coding standard
in_array() should be used with true as the third parameter for strict comparison
Only use booleans in if statements, not integers or strings
Use strict comparison (===) instead of loose comparison (==)
Avoid code duplication in both if and else statements
Do not use empty()
Use the moneyphp/money library for handling monetary values and currencies in PHP
Never use floats or doubles to represent monetary values; use integers to represent the smallest currency unit (e.g., cents for USD)

Files:

  • tests/Webshop/OrderManagement/OrderControllerMarkAsDeliveredTest.php
  • app/Actions/Shop/CheckoutService.php
  • tests/Unit/Actions/Shop/CheckoutServiceTest.php
  • tests/Webshop/Checkout/CheckoutFinalizeOrCancelControllerTest.php
  • app/Http/Requests/Order/MarkAsDeliveredOrderRequest.php
tests/**/*.php

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Do not mock the database in tests; use the in-memory SQLite database instead

Files:

  • tests/Webshop/OrderManagement/OrderControllerMarkAsDeliveredTest.php
  • tests/Unit/Actions/Shop/CheckoutServiceTest.php
  • tests/Webshop/Checkout/CheckoutFinalizeOrCancelControllerTest.php
tests/Unit/**/*.php

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Tests in the tests/Unit directory should extend from AbstractTestCase

Files:

  • tests/Unit/Actions/Shop/CheckoutServiceTest.php
**/Http/Requests/**/*.php

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

In Requests, if a user is provided by the query parameter, it should be placed in $this->user2; $this->user is reserved for the user making the request

Files:

  • app/Http/Requests/Order/MarkAsDeliveredOrderRequest.php
🧠 Learnings (9)
📓 Common learnings
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3702
File: app/Actions/Shop/CheckoutService.php:143-147
Timestamp: 2025-09-21T20:19:42.669Z
Learning: In the CheckoutService, the Order.transaction_id field is intentionally overwritten with the gateway's transaction reference during payment completion via Order::markAsPaid(). This is the intended design per ildyria's confirmation, despite the transaction_id originally being a UUID used for return/cancel URLs.
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3810
File: app/Actions/Shop/CheckoutService.php:70-72
Timestamp: 2025-11-17T11:36:52.888Z
Learning: For Mollie and Stripe payment providers in the CheckoutService, the payment flow uses redirects rather than POST callbacks. When the user completes payment, they are redirected back to Lychee with query parameters in the URL (not POST data). The purchase parameters stored in the session are used with completePurchase() to verify the payment, and any necessary callback data comes from the query parameters in the redirect URL, which Omnipay handles automatically.
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3702
File: app/Actions/Shop/CheckoutService.php:143-147
Timestamp: 2025-09-21T20:07:17.804Z
Learning: In the CheckoutService, the Order.transaction_id field is intentionally overwritten with the gateway's transaction reference during payment completion via Order::markAsPaid(). This is the intended design despite the transaction_id originally being a UUID used for return/cancel URLs.
📚 Learning: 2025-09-13T11:30:21.765Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3683
File: tests/Feature_v2/Shop/CheckoutCreateSessionControllerTest.php:237-239
Timestamp: 2025-09-13T11:30:21.765Z
Learning: In tests/Feature_v2/Shop/CheckoutCreateSessionControllerTest.php, directly modifying the shared $this->test_order fixture in loops is an intentional design choice, not something that needs to be refactored to create fresh orders for each iteration.

Applied to files:

  • tests/Webshop/OrderManagement/OrderControllerMarkAsDeliveredTest.php
  • tests/Unit/Actions/Shop/CheckoutServiceTest.php
  • tests/Webshop/Checkout/CheckoutFinalizeOrCancelControllerTest.php
📚 Learning: 2025-11-17T11:36:52.888Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3810
File: app/Actions/Shop/CheckoutService.php:70-72
Timestamp: 2025-11-17T11:36:52.888Z
Learning: For Mollie and Stripe payment providers in the CheckoutService, the payment flow uses redirects rather than POST callbacks. When the user completes payment, they are redirected back to Lychee with query parameters in the URL (not POST data). The purchase parameters stored in the session are used with completePurchase() to verify the payment, and any necessary callback data comes from the query parameters in the redirect URL, which Omnipay handles automatically.

Applied to files:

  • app/Actions/Shop/CheckoutService.php
📚 Learning: 2025-09-21T20:19:42.669Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3702
File: app/Actions/Shop/CheckoutService.php:143-147
Timestamp: 2025-09-21T20:19:42.669Z
Learning: In the CheckoutService, the Order.transaction_id field is intentionally overwritten with the gateway's transaction reference during payment completion via Order::markAsPaid(). This is the intended design per ildyria's confirmation, despite the transaction_id originally being a UUID used for return/cancel URLs.

Applied to files:

  • app/Actions/Shop/CheckoutService.php
📚 Learning: 2025-09-21T20:08:02.626Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3702
File: app/Models/Order.php:123-137
Timestamp: 2025-09-21T20:08:02.626Z
Learning: In the Shop system Order model, the markAsPaid method intentionally overwrites the internal transaction_id with the payment provider's transaction reference when completing a payment. This is the intended behavior per ildyria's design decision.

Applied to files:

  • app/Actions/Shop/CheckoutService.php
📚 Learning: 2025-09-21T20:07:17.804Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3702
File: app/Actions/Shop/CheckoutService.php:143-147
Timestamp: 2025-09-21T20:07:17.804Z
Learning: In the CheckoutService, the Order.transaction_id field is intentionally overwritten with the gateway's transaction reference during payment completion via Order::markAsPaid(). This is the intended design despite the transaction_id originally being a UUID used for return/cancel URLs.

Applied to files:

  • app/Actions/Shop/CheckoutService.php
📚 Learning: 2025-09-13T13:58:16.970Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3683
File: app/Actions/Shop/CheckoutService.php:50-68
Timestamp: 2025-09-13T13:58:16.970Z
Learning: In the Shop system, Order models are always created via OrderService::createOrder() which automatically assigns a UUID to the transaction_id field during creation (line 42: 'transaction_id' => Str::uuid()). Therefore, transaction_id is never null or empty when orders reach the CheckoutService for payment processing.

Applied to files:

  • app/Actions/Shop/CheckoutService.php
📚 Learning: 2025-09-13T13:58:16.970Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3683
File: app/Actions/Shop/CheckoutService.php:50-68
Timestamp: 2025-09-13T13:58:16.970Z
Learning: In the Shop system, Order models are always created via OrderService::createOrder() which automatically assigns a UUID to the transaction_id field during creation. Therefore, transaction_id is never null or empty when orders reach the CheckoutService for payment processing.

Applied to files:

  • app/Actions/Shop/CheckoutService.php
📚 Learning: 2025-08-12T11:31:09.853Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3605
File: docs/Lifecycle-of-a-request-photo-upload.md:144-151
Timestamp: 2025-08-12T11:31:09.853Z
Learning: In Lychee, the BaseApiRequest class has been modified to change Laravel's default execution order - processValidatedValues() runs BEFORE authorize(), not after. This means that in Lychee request classes, $this->album and other processed properties are available during authorization checks, unlike standard Laravel behavior.

Applied to files:

  • app/Http/Requests/Order/MarkAsDeliveredOrderRequest.php
🧬 Code graph analysis (2)
tests/Webshop/OrderManagement/OrderControllerMarkAsDeliveredTest.php (1)
tests/Traits/CatchFailures.php (1)
  • assertNoContent (133-136)
tests/Webshop/Checkout/CheckoutFinalizeOrCancelControllerTest.php (2)
resources/js/lychee.d.ts (1)
  • PaymentStatusType (108-108)
tests/Traits/CatchFailures.php (1)
  • assertRedirect (143-146)
⏰ Context from checks skipped due to timeout of 180000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (19)
  • GitHub Check: 3️⃣ PHP dist / 8.4 - postgresql
  • GitHub Check: 3️⃣ PHP dist / 8.4 - mariadb
  • GitHub Check: 3️⃣ PHP dist / 8.4 - sqlite
  • GitHub Check: 3️⃣ PHP dist / 8.3 - sqlite
  • GitHub Check: 3️⃣ PHP dist / 8.3 - mariadb
  • GitHub Check: 2️⃣ PHP tests / 8.3 - sqlite -- Unit
  • GitHub Check: 2️⃣ PHP tests / 8.3 - postgresql -- ImageProcessing
  • GitHub Check: 2️⃣ PHP tests / 8.3 - sqlite -- Feature_v2
  • GitHub Check: 2️⃣ PHP tests / 8.3 - sqlite -- ImageProcessing
  • GitHub Check: 2️⃣ PHP tests / 8.3 - sqlite -- Install
  • GitHub Check: 2️⃣ PHP tests / 8.3 - sqlite -- Webshop
  • GitHub Check: 2️⃣ PHP tests / 8.3 - postgresql -- Webshop
  • GitHub Check: 2️⃣ PHP tests / 8.3 - mariadb -- Install
  • GitHub Check: 2️⃣ PHP tests / 8.3 - mariadb -- ImageProcessing
  • GitHub Check: 2️⃣ PHP tests / 8.3 - mariadb -- Webshop
  • GitHub Check: 2️⃣ PHP tests / 8.3 - postgresql -- Feature_v2
  • GitHub Check: 2️⃣ PHP tests / 8.3 - postgresql -- Unit
  • GitHub Check: 2️⃣ PHP tests / 8.3 - mariadb -- Feature_v2
  • GitHub Check: 2️⃣ PHP tests / 8.3 - mariadb -- Unit
🔇 Additional comments (8)
tests/Webshop/OrderManagement/OrderControllerMarkAsDeliveredTest.php (1)

232-232: LGTM! Idempotent operation for closed orders.

The change from assertNotFound to assertNoContent correctly reflects the updated behavior where marking an already-closed order as delivered succeeds idempotently with a 204 No Content response. This aligns with the expanded status allowlist in MarkAsDeliveredOrderRequest.

app/Http/Requests/Order/MarkAsDeliveredOrderRequest.php (2)

38-42: LGTM! Formatting improvement.

The reformatting of the array_diff() call improves readability without changing logic.


69-71: LGTM! Enables idempotent delivery marking.

Expanding the query to allow both COMPLETED and CLOSED statuses enables idempotent behavior—marking an already-closed order as delivered now succeeds gracefully. This aligns with the test expectations and PR objectives.

tests/Unit/Actions/Shop/CheckoutServiceTest.php (1)

132-133: LGTM! Test reflects updated non-redirect success behavior.

The updated assertions correctly expect an empty message and null redirect_url for successful non-redirect payments, aligning with the changes in CheckoutService that remove redirect_url from the non-redirect success CheckoutDTO.

tests/Webshop/Checkout/CheckoutFinalizeOrCancelControllerTest.php (1)

185-191: LGTM! Test reflects updated payment flow.

The updated flow correctly expects the order to remain in PROCESSING status after initial payment processing, then performs finalization via the Dummy provider. This aligns with the CheckoutService changes where successful non-redirect DummyResponse payments now require an explicit finalization step, making the test flow consistent with redirect-based payment providers.

app/Actions/Shop/CheckoutService.php (3)

107-113: LGTM! DummyResponse metadata capture enables finalization.

The special handling for DummyResponse correctly captures the transaction reference in session metadata, enabling the finalization flow. This makes the test provider's flow consistent with redirect-based providers like Mollie and Stripe.


119-122: LGTM! Non-redirect DTO structure updated.

The CheckoutDTO for successful non-redirect payments correctly omits redirect_url and uses is_redirect: false, aligning with the updated controller response structure that includes complete_url instead.


116-117: No changes needed—the code correctly follows the intended markAsPaid() pattern.

Lines 116-117 are part of an intentional two-step flow for non-redirect successful payments (currently only DummyResponse). The transaction_id is stored here with metadata in the session (lines 109-112), then when the client accesses the return URL, handlePaymentReturn() calls gateway->completePurchase() and properly invokes completePayment()markAsPaid() at line 188. The order remains in PROCESSING status by design during step 1, which is required by handlePaymentReturn()'s validation at line 183. This deferred finalization allows the test provider to work without redirect while maintaining the markAsPaid() pattern.

Likely an incorrect or invalid review comment.


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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (3)
app/Http/Resources/Shop/CheckoutResource.php (1)

19-26: New complete_url field is fine; ensure all call sites and TS types are aligned.

Adding public readonly ?string $complete_url = null fits the Data resource and the updated controller flow. Please ensure:

  • All CheckoutResource instantiations use named arguments or have been updated for the new parameter position.
  • The generated/checked TypeScript CheckoutResource type also includes complete_url so the frontend model matches this payload.
app/Http/Controllers/Shop/CheckoutController.php (1)

112-118: Including order in the generic process response is reasonable; consider payload impact.

Always attaching OrderResource::fromModel($order) (even for redirect flows) is useful, but it does increase response size. If the order has many items, ensure this remains acceptable for the checkout entrypoint; otherwise, consider returning a lighter representation here if needed later.

app/Actions/Shop/CheckoutService.php (1)

106-124: Non‑redirect success path and early transaction_id overwrite need cross‑flow validation.

The new logic for non‑redirect responses:

  • Stores metadata for DummyResponse in the session, mirroring redirect flows.
  • Sets $order->transaction_id to $response->getTransactionReference() and saves before finalize/completePayment() runs.

This is coherent with the Dummy/“no redirect” flow and works with CheckoutController::process() building complete_url after $order->refresh(). However, it does move the overwrite of transaction_id (from UUID to gateway reference) earlier than the previously documented behavior, at least for non‑redirect flows.

Please verify that:

  • No other code still relies on transaction_id remaining the original UUID between process() and finalize() for these flows.
  • The Dummy gateway’s completePurchase() indeed expects/uses the stored metadata as you’re now persisting it.

If everything is aligned, this centralizes behavior nicely for both redirect and dummy providers.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7cdf8f4 and be5da84.

📒 Files selected for processing (4)
  • app/Actions/Shop/CheckoutService.php (2 hunks)
  • app/Http/Controllers/Shop/CheckoutController.php (1 hunks)
  • app/Http/Requests/Order/MarkAsDeliveredOrderRequest.php (2 hunks)
  • app/Http/Resources/Shop/CheckoutResource.php (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.php

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.php: Any new PHP file should contain the license header and have a single blank line after the opening PHP tag
Variable names should be in snake_case
Apply PSR-4 coding standard
in_array() should be used with true as the third parameter for strict comparison
Only use booleans in if statements, not integers or strings
Use strict comparison (===) instead of loose comparison (==)
Avoid code duplication in both if and else statements
Do not use empty()
Use the moneyphp/money library for handling monetary values and currencies in PHP
Never use floats or doubles to represent monetary values; use integers to represent the smallest currency unit (e.g., cents for USD)

Files:

  • app/Http/Resources/Shop/CheckoutResource.php
  • app/Http/Controllers/Shop/CheckoutController.php
  • app/Http/Requests/Order/MarkAsDeliveredOrderRequest.php
  • app/Actions/Shop/CheckoutService.php
**/Http/Resources/**/*.php

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Resource classes should extend from Spatie Data instead of JsonResource

Files:

  • app/Http/Resources/Shop/CheckoutResource.php
**/Http/Requests/**/*.php

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

In Requests, if a user is provided by the query parameter, it should be placed in $this->user2; $this->user is reserved for the user making the request

Files:

  • app/Http/Requests/Order/MarkAsDeliveredOrderRequest.php
🧠 Learnings (10)
📓 Common learnings
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3810
File: app/Actions/Shop/CheckoutService.php:70-72
Timestamp: 2025-11-17T11:36:52.888Z
Learning: For Mollie and Stripe payment providers in the CheckoutService, the payment flow uses redirects rather than POST callbacks. When the user completes payment, they are redirected back to Lychee with query parameters in the URL (not POST data). The purchase parameters stored in the session are used with completePurchase() to verify the payment, and any necessary callback data comes from the query parameters in the redirect URL, which Omnipay handles automatically.
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3702
File: app/Actions/Shop/CheckoutService.php:143-147
Timestamp: 2025-09-21T20:19:42.669Z
Learning: In the CheckoutService, the Order.transaction_id field is intentionally overwritten with the gateway's transaction reference during payment completion via Order::markAsPaid(). This is the intended design per ildyria's confirmation, despite the transaction_id originally being a UUID used for return/cancel URLs.
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3702
File: app/Actions/Shop/CheckoutService.php:143-147
Timestamp: 2025-09-21T20:07:17.804Z
Learning: In the CheckoutService, the Order.transaction_id field is intentionally overwritten with the gateway's transaction reference during payment completion via Order::markAsPaid(). This is the intended design despite the transaction_id originally being a UUID used for return/cancel URLs.
📚 Learning: 2025-10-03T10:58:59.321Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3732
File: app/Http/Resources/Shop/OrderResource.php:62-64
Timestamp: 2025-10-03T10:58:59.321Z
Learning: In app/Http/Resources/Shop/OrderResource.php, the can_process_payment flag is intentionally set to false in order listings (via OrderService::getAll()) because items are explicitly excluded with without(['items']). This is by design: listings are lightweight, and the actual payment capability is only computed when fetching individual orders with full item data loaded.

Applied to files:

  • app/Http/Controllers/Shop/CheckoutController.php
📚 Learning: 2025-11-17T11:36:52.888Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3810
File: app/Actions/Shop/CheckoutService.php:70-72
Timestamp: 2025-11-17T11:36:52.888Z
Learning: For Mollie and Stripe payment providers in the CheckoutService, the payment flow uses redirects rather than POST callbacks. When the user completes payment, they are redirected back to Lychee with query parameters in the URL (not POST data). The purchase parameters stored in the session are used with completePurchase() to verify the payment, and any necessary callback data comes from the query parameters in the redirect URL, which Omnipay handles automatically.

Applied to files:

  • app/Http/Controllers/Shop/CheckoutController.php
  • app/Actions/Shop/CheckoutService.php
📚 Learning: 2025-11-23T23:44:13.346Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3810
File: app/Http/Controllers/Shop/CheckoutController.php:154-180
Timestamp: 2025-11-23T23:44:13.346Z
Learning: In the Lychee webshop, the OrderCompleted event should only be dispatched after payment is confirmed. The OFFLINE payment status (PaymentStatusType::OFFLINE) indicates that payment has not yet been confirmed, so OrderCompleted should NOT be dispatched when an order is marked as OFFLINE. The event is dispatched in the finalize() method for online payments after status reaches COMPLETED, and for offline orders, the event should be dispatched when the order is later confirmed as paid (likely through the markAsPaid transition).

Applied to files:

  • app/Http/Controllers/Shop/CheckoutController.php
📚 Learning: 2025-09-21T20:08:02.626Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3702
File: app/Models/Order.php:123-137
Timestamp: 2025-09-21T20:08:02.626Z
Learning: In the Shop system Order model, the markAsPaid method intentionally overwrites the internal transaction_id with the payment provider's transaction reference when completing a payment. This is the intended behavior per ildyria's design decision.

Applied to files:

  • app/Http/Controllers/Shop/CheckoutController.php
  • app/Actions/Shop/CheckoutService.php
📚 Learning: 2025-08-12T11:31:09.853Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3605
File: docs/Lifecycle-of-a-request-photo-upload.md:144-151
Timestamp: 2025-08-12T11:31:09.853Z
Learning: In Lychee, the BaseApiRequest class has been modified to change Laravel's default execution order - processValidatedValues() runs BEFORE authorize(), not after. This means that in Lychee request classes, $this->album and other processed properties are available during authorization checks, unlike standard Laravel behavior.

Applied to files:

  • app/Http/Requests/Order/MarkAsDeliveredOrderRequest.php
📚 Learning: 2025-09-21T20:19:42.669Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3702
File: app/Actions/Shop/CheckoutService.php:143-147
Timestamp: 2025-09-21T20:19:42.669Z
Learning: In the CheckoutService, the Order.transaction_id field is intentionally overwritten with the gateway's transaction reference during payment completion via Order::markAsPaid(). This is the intended design per ildyria's confirmation, despite the transaction_id originally being a UUID used for return/cancel URLs.

Applied to files:

  • app/Actions/Shop/CheckoutService.php
📚 Learning: 2025-09-21T20:07:17.804Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3702
File: app/Actions/Shop/CheckoutService.php:143-147
Timestamp: 2025-09-21T20:07:17.804Z
Learning: In the CheckoutService, the Order.transaction_id field is intentionally overwritten with the gateway's transaction reference during payment completion via Order::markAsPaid(). This is the intended design despite the transaction_id originally being a UUID used for return/cancel URLs.

Applied to files:

  • app/Actions/Shop/CheckoutService.php
📚 Learning: 2025-09-13T13:58:16.970Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3683
File: app/Actions/Shop/CheckoutService.php:50-68
Timestamp: 2025-09-13T13:58:16.970Z
Learning: In the Shop system, Order models are always created via OrderService::createOrder() which automatically assigns a UUID to the transaction_id field during creation (line 42: 'transaction_id' => Str::uuid()). Therefore, transaction_id is never null or empty when orders reach the CheckoutService for payment processing.

Applied to files:

  • app/Actions/Shop/CheckoutService.php
📚 Learning: 2025-09-13T13:58:16.970Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3683
File: app/Actions/Shop/CheckoutService.php:50-68
Timestamp: 2025-09-13T13:58:16.970Z
Learning: In the Shop system, Order models are always created via OrderService::createOrder() which automatically assigns a UUID to the transaction_id field during creation. Therefore, transaction_id is never null or empty when orders reach the CheckoutService for payment processing.

Applied to files:

  • app/Actions/Shop/CheckoutService.php
🧬 Code graph analysis (2)
app/Http/Controllers/Shop/CheckoutController.php (2)
resources/js/lychee.d.ts (2)
  • CheckoutResource (906-912)
  • OrderResource (943-956)
app/Http/Resources/Shop/OrderResource.php (1)
  • fromModel (48-72)
app/Http/Requests/Order/MarkAsDeliveredOrderRequest.php (4)
app/Models/OrderItem.php (1)
  • order (103-106)
app/Models/Order.php (1)
  • items (121-124)
app/Eloquent/FixedQueryBuilderTrait.php (2)
  • where (66-76)
  • orWhere (283-293)
resources/js/lychee.d.ts (1)
  • PaymentStatusType (108-108)
🔇 Additional comments (6)
app/Http/Requests/Order/MarkAsDeliveredOrderRequest.php (2)

37-44: Authorization item check remains correct and strict.

The array_diff(array_column($this->items, 'id'), $this->order->items->pluck('id')->toArray()) === 0 condition still cleanly enforces that all provided item IDs belong to the order; no issues here.


64-73: Status filter change to COMPLETED/CLOSED looks coherent; just confirm business intent.

Wrapping the status checks in a closure so the query becomes (status = COMPLETED OR status = CLOSED) AND id = :order_id is correct and avoids precedence pitfalls. Given this request “marks an order as delivered”, please double‑check that allowing both COMPLETED and CLOSED for retrieval matches the intended lifecycle (i.e. no need to restrict to one of them only).

app/Http/Controllers/Shop/CheckoutController.php (1)

100-109: Non‑redirect success branch is consistent with the finalize flow.

Returning a CheckoutResource with is_redirect = false, a complete_url pointing to shop.checkout.return, and a refreshed OrderResource keeps the non‑redirect (e.g. dummy) flow aligned with the existing finalize logic. Just confirm that the frontend is now expected to always follow complete_url on this branch instead of relying on the previously dispatched event alone.

app/Actions/Shop/CheckoutService.php (3)

23-25: DummyResponse import appears correct; verify against installed Omnipay Dummy version.

Using Omnipay\Dummy\Message\Response aliased as DummyResponse is the right shape for type checks here; just confirm it matches the actual class name in the omnipay/dummy version installed in this project.


84-106: Redirect branch metadata handling for FetchTransactionResponse looks sound.

Capturing $response->getMetadata(), injecting transactionReference, and persisting under metadata.{order_id} in the session gives handlePaymentReturn() what it needs for completePurchase(). The explicit RedirectResponseInterface guard is also a good safety check.


134-148: Exception handling and FAILED status update look appropriate.

Catching gateway exceptions, logging with order and provider context, transitioning the order to FAILED, and returning a generic user‑facing message is a good balance between observability and not leaking internal error details.

@codecov
Copy link

codecov bot commented Dec 3, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.26%. Comparing base (7cdf8f4) to head (f5b8cea).
⚠️ Report is 2 commits behind head on master.

🚀 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.

redirect_url: $redirect_url,
);
} elseif ($response->isSuccessful()) {
if ($response instanceof DummyResponse) {
Copy link
Member Author

Choose a reason for hiding this comment

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

For now, only Dummy is successful directly. Later consider the other cases but I expect most integration will have a redirection.


// Payment was successful
$this->completePayment($order, $response);
$order->transaction_id = $response->getTransactionReference();
Copy link
Member Author

Choose a reason for hiding this comment

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

We only save the transaction id to make sure that we do a proper finalization.

// If we have a success directly without redirection mark order as completed
if ($result->is_success && !$result->is_redirect) {
OrderCompleted::dispatchIf(Configs::getValueAsBool('webshop_auto_fulfill_enabled'), $order->id);
// This is a success we now need to complete the order.
Copy link
Member Author

Choose a reason for hiding this comment

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

This way the OrderCompleted event is only dispatched in the finalize step, not in the processing step.
Furthermore that clarifies that we have complete_url and redirect_url, showing different intent.

$order_id = intval($values['order_id'] ?? null);
$this->order = Order::query()
->where('status', '=', PaymentStatusType::COMPLETED)
->where(fn ($query) => $query
Copy link
Member Author

Choose a reason for hiding this comment

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

Mark as delivered can be used to provide links/update links even when the request is closed. hence the change.

Copy link
Contributor

@JasonMillward JasonMillward left a comment

Choose a reason for hiding this comment

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

Appreciate the comments

@ildyria ildyria merged commit 0c4cc54 into master Dec 4, 2025
48 checks passed
@ildyria ildyria deleted the webshop/more-backend-bugs branch December 4, 2025 11:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants