Skip to content

Commit 8b4d133

Browse files
davidbarrattclaude
andauthored
Remove local hash comparison for checkout sessions in WooCommerce (#4579)
* Remove local hash comparison for checkout sessions in WooCommerce The backend's idempotency system now handles deduplication of checkout session creation requests. This removes the need to store and compare hashes locally in WooCommerce order metadata. The checkout session will now always attempt to CREATE unless already COMPLETE, relying on the Idempotency-Key header (derived from the request body hash) to prevent duplicate sessions. Resolves: ENG-607 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Add to change log * Add tests for CheckoutSession::needs() method Tests the simplified needs() logic after removing local hash comparison: - Status COMPLETE returns NONE - Status OPEN/null returns CREATE - Dependencies (line_items/discounts) return DEPENDENCY - Dependencies checked before status 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Increment version --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 3c6ef10 commit 8b4d133

File tree

4 files changed

+144
-22
lines changed

4 files changed

+144
-22
lines changed

pay-with-flex.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
/**
33
* Plugin Name: Flex HSA/FSA Payments
44
* Description: Accept HSA/FSA payments directly in the checkout flow.
5-
* Version: 3.1.18
5+
* Version: 3.2.0
66
* Plugin URI: https://wordpress.org/plugins/pay-with-flex/
77
* Author: Flex
88
* Author URI: https://withflex.com/

readme.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,9 @@ Flex makes this process simple by offering asynchronous telehealth visits direct
5555

5656
== Changelog ==
5757

58-
= 3.1.18 =
58+
= 3.2.0 =
5959
* Added support for dynamic pricing.
60+
* Removed checkout session cache since idempotency system will prevent duplicate checkout sessions from being created.
6061

6162
= 3.1.17 =
6263
* Fixed a race condition where an order could be marked as `payment_complete` multiple times, once by the redirect and again by the webhook.

src/Resource/CheckoutSession/CheckoutSession.php

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@ class CheckoutSession extends Resource {
2222

2323
protected const KEY_STATUS = 'checkout_session_status';
2424
protected const KEY_REDIRECT_URL = 'checkout_session_redirect_url';
25-
protected const KEY_HASH = 'checkout_session_hash';
26-
protected const KEY_AMOUNT_TOTAL = 'checkout_session_amount_total';
2725
protected const KEY_TEST_MODE = 'checkout_session_test_mode';
2826

2927
/**
@@ -288,20 +286,13 @@ public function wc(): ?\WC_Order {
288286
*/
289287
public function apply_to( \WC_Order $order ): void {
290288
$order->set_transaction_id( $this->id ?? '' );
291-
$order->update_meta_data( self::META_PREFIX . self::KEY_HASH, $this->hash() );
292289

293290
if ( null === $this->redirect_url ) {
294291
$order->delete_meta_data( self::META_PREFIX . self::KEY_REDIRECT_URL );
295292
} else {
296293
$order->update_meta_data( self::META_PREFIX . self::KEY_REDIRECT_URL, $this->redirect_url );
297294
}
298295

299-
if ( null === $this->amount_total ) {
300-
$order->delete_meta_data( self::META_PREFIX . self::KEY_AMOUNT_TOTAL );
301-
} else {
302-
$order->update_meta_data( self::META_PREFIX . self::KEY_AMOUNT_TOTAL, strval( $this->amount_total ) );
303-
}
304-
305296
$status = $this->status?->value;
306297
if ( null === $status ) {
307298
$order->delete_meta_data( self::META_PREFIX . self::KEY_STATUS );
@@ -329,19 +320,11 @@ public function needs(): ResourceAction {
329320
return ResourceAction::DEPENDENCY;
330321
}
331322

332-
if ( null === $this->id ) {
333-
return ResourceAction::CREATE;
334-
}
335-
336-
if ( intval( $this->wc->get_meta( self::META_PREFIX . self::KEY_AMOUNT_TOTAL ) ) !== $this->amount_total ) {
337-
return ResourceAction::CREATE;
338-
}
339-
340-
if ( $this->wc->get_meta( self::META_PREFIX . self::KEY_HASH ) !== $this->hash() ) {
341-
return ResourceAction::CREATE;
323+
if ( Status::COMPLETE === $this->status ) {
324+
return ResourceAction::NONE;
342325
}
343326

344-
return ResourceAction::NONE;
327+
return ResourceAction::CREATE;
345328
}
346329

347330
/**
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
<?php
2+
/**
3+
* Tests for the CheckoutSession::needs() method
4+
*
5+
* @package Flex
6+
*/
7+
8+
declare(strict_types=1);
9+
10+
namespace Flex\Tests\Resource\CheckoutSession;
11+
12+
use Flex\Resource\CheckoutSession\CheckoutSession;
13+
use Flex\Resource\CheckoutSession\Discount;
14+
use Flex\Resource\CheckoutSession\LineItem;
15+
use Flex\Resource\CheckoutSession\Status;
16+
use Flex\Resource\ResourceAction;
17+
18+
/**
19+
* Test the CheckoutSession::needs() method.
20+
*
21+
* The needs() method determines what action should be taken for a checkout session:
22+
* - DEPENDENCY: If any line items or discounts need action
23+
* - NONE: If the status is COMPLETE
24+
* - CREATE: Otherwise (status is OPEN, null, or any other non-COMPLETE value)
25+
*/
26+
class CheckoutSessionTest extends \WP_UnitTestCase {
27+
28+
/**
29+
* Test that needs() returns NONE when status is COMPLETE.
30+
*/
31+
public function test_needs_returns_none_when_status_is_complete(): void {
32+
$checkout_session = new CheckoutSession(
33+
success_url: 'https://example.com/success',
34+
status: Status::COMPLETE,
35+
);
36+
37+
$this->assertSame( ResourceAction::NONE, $checkout_session->needs() );
38+
}
39+
40+
/**
41+
* Test that needs() returns CREATE when status is OPEN.
42+
*/
43+
public function test_needs_returns_create_when_status_is_open(): void {
44+
$checkout_session = new CheckoutSession(
45+
success_url: 'https://example.com/success',
46+
status: Status::OPEN,
47+
);
48+
49+
$this->assertSame( ResourceAction::CREATE, $checkout_session->needs() );
50+
}
51+
52+
/**
53+
* Test that needs() returns CREATE when status is null.
54+
*/
55+
public function test_needs_returns_create_when_status_is_null(): void {
56+
$checkout_session = new CheckoutSession(
57+
success_url: 'https://example.com/success',
58+
status: null,
59+
);
60+
61+
$this->assertSame( ResourceAction::CREATE, $checkout_session->needs() );
62+
}
63+
64+
/**
65+
* Test that needs() returns DEPENDENCY when a line item needs action.
66+
*/
67+
public function test_needs_returns_dependency_when_line_item_needs_action(): void {
68+
$line_item = $this->createStub( LineItem::class );
69+
$line_item->method( 'needs' )->willReturn( ResourceAction::CREATE );
70+
71+
$checkout_session = new CheckoutSession(
72+
success_url: 'https://example.com/success',
73+
line_items: array( $line_item ),
74+
status: Status::OPEN,
75+
);
76+
77+
$this->assertSame( ResourceAction::DEPENDENCY, $checkout_session->needs() );
78+
}
79+
80+
/**
81+
* Test that needs() returns DEPENDENCY when a discount needs action.
82+
*/
83+
public function test_needs_returns_dependency_when_discount_needs_action(): void {
84+
$discount = $this->createStub( Discount::class );
85+
$discount->method( 'needs' )->willReturn( ResourceAction::DEPENDENCY );
86+
87+
$checkout_session = new CheckoutSession(
88+
success_url: 'https://example.com/success',
89+
status: Status::OPEN,
90+
discounts: array( $discount ),
91+
);
92+
93+
$this->assertSame( ResourceAction::DEPENDENCY, $checkout_session->needs() );
94+
}
95+
96+
/**
97+
* Test that dependencies are checked before status.
98+
*
99+
* Even if the status is COMPLETE, if a line item needs action,
100+
* the result should be DEPENDENCY (not NONE).
101+
*/
102+
public function test_needs_checks_dependencies_before_status(): void {
103+
$line_item = $this->createStub( LineItem::class );
104+
$line_item->method( 'needs' )->willReturn( ResourceAction::DEPENDENCY );
105+
106+
$checkout_session = new CheckoutSession(
107+
success_url: 'https://example.com/success',
108+
line_items: array( $line_item ),
109+
status: Status::COMPLETE,
110+
);
111+
112+
$this->assertSame(
113+
ResourceAction::DEPENDENCY,
114+
$checkout_session->needs(),
115+
'Dependencies should be checked before status'
116+
);
117+
}
118+
119+
/**
120+
* Test that needs() returns NONE when status is COMPLETE and no dependencies need action.
121+
*/
122+
public function test_needs_returns_none_when_complete_with_satisfied_dependencies(): void {
123+
$line_item = $this->createStub( LineItem::class );
124+
$line_item->method( 'needs' )->willReturn( ResourceAction::NONE );
125+
126+
$discount = $this->createStub( Discount::class );
127+
$discount->method( 'needs' )->willReturn( ResourceAction::NONE );
128+
129+
$checkout_session = new CheckoutSession(
130+
success_url: 'https://example.com/success',
131+
line_items: array( $line_item ),
132+
status: Status::COMPLETE,
133+
discounts: array( $discount ),
134+
);
135+
136+
$this->assertSame( ResourceAction::NONE, $checkout_session->needs() );
137+
}
138+
}

0 commit comments

Comments
 (0)