Skip to content

Update logic to clean up order lines#2296

Merged
alecritson merged 3 commits into1.xfrom
lun-350-cleanuporderlines-pipeline-action-logic-is-insufficient
Sep 23, 2025
Merged

Update logic to clean up order lines#2296
alecritson merged 3 commits into1.xfrom
lun-350-cleanuporderlines-pipeline-action-logic-is-insufficient

Conversation

@alecritson
Copy link
Collaborator

@alecritson alecritson commented Sep 23, 2025

This PR adds additional logic to the CleanUpOrderLines pipeline to catch any duplicate purchasable lines which may have slipped through the net due to differing quantities/meta values.

Summary by CodeRabbit

  • Bug Fixes

    • Order line cleanup now uses per-line signatures to precisely compare against cart lines, removing only truly mismatched lines and preserving matching product and shipping lines.
  • Tests

    • Added and expanded unit tests to validate persisted DB state (including same product with different metadata/prices) and replaced fragile in-memory checks with robust database assertions.

@vercel
Copy link

vercel bot commented Sep 23, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
lunar-docs Ready Ready Preview Comment Sep 23, 2025 9:22am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 23, 2025

Walkthrough

Order line cleanup now computes signatures (purchasableId + meta + qty) for cart and order lines and deletes order lines whose signature is not present in the cart. A private signature helper was added and tests were updated to assert persisted DB state, including a new case for identical purchasable_ids with differing meta/qty.

Changes

Cohort / File(s) Summary of changes
Order cleanup logic
packages/core/src/Pipelines/Order/Creation/CleanUpOrderLines.php
Replaced deletion-by-purchasable_id membership with per-line signature comparison (purchasableId + meta + qty); added private signature(string, array, int): string; iterate existing order lines and delete those without matching cart signatures; added inline comment.
Unit tests
tests/core/Unit/Pipelines/Order/Creation/CleanUpOrderLinesTest.php
Switched in-test expectations to database assertions; added a test ensuring lines with identical purchasable_ids but differing meta/qty are removed while correct lines and shipping lines remain; asserted persisted purchasable_ids, quantities, and meta.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The PR title "Update logic to clean up order lines" accurately and concisely summarizes the main change — refining the CleanUpOrderLines pipeline to better identify and remove order lines (including duplicates with differing meta/quantity). It is a short, single sentence and specific enough for a reviewer scanning history.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch lun-350-cleanuporderlines-pipeline-action-logic-is-insufficient

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
Contributor

@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 (6)
tests/core/Unit/Pipelines/Order/Creation/CleanUpOrderLinesTest.php (3)

95-102: Scope DB assertions to the current order to avoid false positives.

Include order_id in the where clauses for precision.

-    assertDatabaseHas((new OrderLine)->getTable(), [
-        'purchasable_id' => $purchasable->id,
-    ]);
+    assertDatabaseHas((new OrderLine)->getTable(), [
+        'order_id' => $order->id,
+        'purchasable_id' => $purchasable->id,
+    ]);

-    assertDatabaseMissing((new OrderLine)->getTable(), [
-        'purchasable_id' => $purchasableB->id,
-    ]);
+    assertDatabaseMissing((new OrderLine)->getTable(), [
+        'order_id' => $order->id,
+        'purchasable_id' => $purchasableB->id,
+    ]);

106-106: Clarify test title.

Make the intent explicit: differing quantity or meta.

- test('will remove lines with same purchasable ids when different', function () {
+ test('removes lines with same purchasable_id when quantity or meta differ', function () {

204-214: Prefer JSON path assertions over raw JSON string equality.

Asserting meta via a JSON path is more robust across drivers than string-matching json_encode output.

Option A (simple): use JSON path columns if supported in your test DB driver.

-    assertDatabaseHas((new OrderLine)->getTable(), [
-        'purchasable_id' => $purchasableB->id,
-        'quantity' => 5,
-        'meta' => json_encode(['foo' => 'bar']),
-    ]);
+    assertDatabaseHas((new OrderLine)->getTable(), [
+        'purchasable_id' => $purchasableB->id,
+        'quantity' => 5,
+        'meta->foo' => 'bar',
+    ]);

-    assertDatabaseMissing((new OrderLine)->getTable(), [
-        'purchasable_id' => $purchasableB->id,
-        'quantity' => 15,
-        'meta' => json_encode(['bar' => 'baz']),
-    ]);
+    assertDatabaseMissing((new OrderLine)->getTable(), [
+        'purchasable_id' => $purchasableB->id,
+        'quantity' => 15,
+        'meta->bar' => 'baz',
+    ]);

If SQLite JSON1 or your driver doesn’t support meta->key in where, keep your current approach or switch to a query using whereJsonContains for those checks.

packages/core/src/Pipelines/Order/Creation/CleanUpOrderLines.php (3)

20-23: Build a set for O(1) membership checks.

Converting the signatures array into a hash set avoids repeated linear in_array scans.

-        // Build a set of "signatures" that uniquely identify each cart line
-        $cartSignatures = $cart->lines->map(function ($line) {
-            return $this->signature($line->purchasable_id, (array) $line->meta, $line->quantity);
-        })->toArray();
+        // Build a set of "signatures" that uniquely identify each cart line
+        $cartSignatures = $cart->lines->map(function ($line) {
+            return $this->signature($line->purchasable_id, (array) $line->meta, $line->quantity);
+        })->toArray();
+        $cartSignatureSet = array_flip($cartSignatures);

25-35: Use the set for membership checks (avoid in_array).

Switch to isset on the flipped set for faster lookups.

-        $order->productLines->each(function ($orderLine) use ($cartSignatures) {
+        $order->productLines->each(function ($orderLine) use ($cartSignatureSet) {
             $sig = $this->signature(
                 $orderLine->purchasable_id,
                 (array) $orderLine->meta,
                 $orderLine->quantity,
             );
 
-            if (! in_array($sig, $cartSignatures, true)) {
+            if (! isset($cartSignatureSet[$sig])) {
                 $orderLine->delete();
             }
         });

40-47: Normalize meta recursively for stable signatures.

Sorting only top-level keys can produce mismatches if meta has nested structures. Normalize recursively before encoding.

-    private function signature(string $purchasableId, array $meta, int $qty): string
+    private function signature(string $purchasableId, array $meta, int $qty): string
     {
-        return md5(json_encode([
-            'id' => $purchasableId,
-            'meta' => collect($meta)->sortKeys()->toArray(),
-            'qty' => $qty,
-        ]));
+        return md5(json_encode([
+            'id' => (string) $purchasableId,
+            'meta' => $this->normalizeMeta($meta),
+            'qty' => (int) $qty,
+        ], JSON_UNESCAPED_SLASHES));
     }

Add this helper within the class:

private function normalizeMeta(mixed $value): mixed
{
    if (is_array($value)) {
        // Normalize associative arrays by key; preserve order for indexed arrays
        $isAssoc = array_keys($value) !== range(0, count($value) - 1);
        if ($isAssoc) {
            ksort($value);
        }
        foreach ($value as $k => $v) {
            $value[$k] = $this->normalizeMeta($v);
        }
        return $value;
    }
    if ($value instanceof \ArrayObject) {
        return $this->normalizeMeta((array) $value);
    }
    return $value;
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0c34f27 and d876582.

📒 Files selected for processing (2)
  • packages/core/src/Pipelines/Order/Creation/CleanUpOrderLines.php (2 hunks)
  • tests/core/Unit/Pipelines/Order/Creation/CleanUpOrderLinesTest.php (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
packages/core/src/Pipelines/Order/Creation/CleanUpOrderLines.php (2)
packages/core/src/Models/Order.php (3)
  • cart (93-96)
  • lines (98-101)
  • productLines (118-121)
packages/core/src/Models/Cart.php (1)
  • lines (212-215)
tests/core/Unit/Pipelines/Order/Creation/CleanUpOrderLinesTest.php (3)
packages/core/src/Models/OrderLine.php (1)
  • OrderLine (39-99)
packages/core/src/Base/Traits/HasModelExtending.php (1)
  • getTable (66-71)
packages/core/src/Pipelines/Order/Creation/CleanUpOrderLines.php (2)
  • CleanUpOrderLines (10-48)
  • handle (15-38)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (20)
  • GitHub Check: core - PHP 8.4 - L12.* ↑ E
  • GitHub Check: search - PHP 8.4 - L12.* ↑ E
  • GitHub Check: admin - PHP 8.4 - L12.* ↑ E
  • GitHub Check: stripe - PHP 8.4 - L11.* ↑
  • GitHub Check: stripe - PHP 8.4 - L12.* ↑ E
  • GitHub Check: admin - PHP 8.4 - L11.* ↑
  • GitHub Check: core - PHP 8.4 - L11.* ↑ E
  • GitHub Check: admin - PHP 8.4 - L11.* ↑ E
  • GitHub Check: core - PHP 8.4 - L11.* ↑
  • GitHub Check: search - PHP 8.3 - L12.* ↑
  • GitHub Check: shipping - PHP 8.4 - L11.* ↑
  • GitHub Check: admin - PHP 8.3 - L12.* ↑
  • GitHub Check: admin - PHP 8.3 - L12.* ↑ E
  • GitHub Check: admin - PHP 8.3 - L11.* ↑
  • GitHub Check: core - PHP 8.3 - L11.* ↑
  • GitHub Check: core - PHP 8.3 - L12.* ↑ E
  • GitHub Check: core - PHP 8.3 - L12.* ↑
  • GitHub Check: admin - PHP 8.3 - L11.* ↑ E
  • GitHub Check: core - PHP 8.3 - L11.* ↑ E
  • GitHub Check: fix-code-style
🔇 Additional comments (1)
tests/core/Unit/Pipelines/Order/Creation/CleanUpOrderLinesTest.php (1)

16-16: Good switch to DB assertions.

Importing Pest’s assertDatabaseHas/assertDatabaseMissing is appropriate for validating persisted state.

Copy link
Contributor

@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 (1)
tests/core/Unit/Pipelines/Order/Creation/CleanUpOrderLinesTest.php (1)

207-212: Prefer JSON path assertions over raw JSON encoding for meta

Using meta->key paths is more driver-agnostic (MySQL/Postgres/SQLite) and resilient to JSON normalization differences than comparing serialized JSON strings.

Apply this diff:

-    assertDatabaseHas((new OrderLine)->getTable(), [
-        'order_id' => $order->id,
-        'purchasable_id' => $purchasableB->id,
-        'quantity' => 5,
-        'meta' => json_encode(['foo' => 'bar']),
-    ]);
+    assertDatabaseHas((new OrderLine)->getTable(), [
+        'order_id' => $order->id,
+        'purchasable_id' => $purchasableB->id,
+        'quantity' => 5,
+        'meta->foo' => 'bar',
+    ]);

-    assertDatabaseMissing((new OrderLine)->getTable(), [
-        'order_id' => $order->id,
-        'purchasable_id' => $purchasableB->id,
-        'quantity' => 15,
-        'meta' => json_encode(['bar' => 'baz']),
-    ]);
+    assertDatabaseMissing((new OrderLine)->getTable(), [
+        'order_id' => $order->id,
+        'purchasable_id' => $purchasableB->id,
+        'quantity' => 15,
+        'meta->bar' => 'baz',
+    ]);

Also applies to: 214-219

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b30ca34 and 6d350a8.

📒 Files selected for processing (1)
  • tests/core/Unit/Pipelines/Order/Creation/CleanUpOrderLinesTest.php (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
tests/core/Unit/Pipelines/Order/Creation/CleanUpOrderLinesTest.php (6)
packages/core/src/Models/OrderLine.php (1)
  • OrderLine (39-99)
packages/core/src/Base/Traits/HasModelExtending.php (1)
  • getTable (66-71)
packages/core/src/Models/Cart.php (1)
  • Cart (62-620)
packages/core/src/DataTypes/ShippingOption.php (1)
  • ShippingOption (9-148)
packages/core/src/Models/CartAddress.php (1)
  • CartAddress (42-140)
packages/core/src/Pipelines/Order/Creation/CleanUpOrderLines.php (2)
  • CleanUpOrderLines (9-47)
  • handle (14-37)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: core - PHP 8.4 - L12.* ↑
  • GitHub Check: core - PHP 8.4 - L12.* ↑ E
  • GitHub Check: core - PHP 8.4 - L11.* ↑ E
🔇 Additional comments (2)
tests/core/Unit/Pipelines/Order/Creation/CleanUpOrderLinesTest.php (2)

17-19: LGTM: Using Pest DB assertions improves test robustness

Importing Pest’s assertDatabaseHas/assertDatabaseMissing is appropriate and aligns with the updated persistence-focused assertions.


97-106: LGTM: Correctly asserts persisted delta after cleanup

These DB assertions accurately verify that only the expected product line remains and the stray line is removed.

@alecritson alecritson merged commit 7586c34 into 1.x Sep 23, 2025
48 checks passed
@alecritson alecritson deleted the lun-350-cleanuporderlines-pipeline-action-logic-is-insufficient branch September 23, 2025 09:37
$purchasableIds = $cart->lines->pluck('purchasable_id');
// Build a set of "signatures" that uniquely identify each cart line
$cartSignatures = $cart->lines->map(function ($line) {
return $this->signature($line->purchasable_id, (array) $line->meta, $line->quantity);
Copy link
Contributor

Choose a reason for hiding this comment

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

should include purchasable_type in signature too

Copy link
Contributor

Choose a reason for hiding this comment

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

@alecritson can you do a PR to add that in? I think it makes sense.

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.

4 participants