class-wc-payments-webhook-reliability-service.php
6.42 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
<?php
/**
* WC_Payments_Webhook_Reliability_Service class
*
* @package WooCommerce\Payments
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use WCPay\Exceptions\API_Exception;
use WCPay\Exceptions\Invalid_Webhook_Data_Exception;
use WCPay\Logger;
/**
* Improve webhook reliability by fetching failed events from the server,
* then process them with ActionScheduler
*/
class WC_Payments_Webhook_Reliability_Service {
const CONTINUOUS_FETCH_FLAG_EVENTS_LIST = 'has_more';
const CONTINUOUS_FETCH_FLAG_ACCOUNT_DATA = 'has_more_failed_events';
const WEBHOOK_FETCH_EVENTS_ACTION = 'wcpay_webhook_fetch_events';
const WEBHOOK_PROCESS_EVENT_ACTION = 'wcpay_webhook_process_event';
/**
* Client for making requests to the WooCommerce Payments API.
*
* @var WC_Payments_API_Client
*/
private $payments_api_client;
/**
* WC_Payments_Action_Scheduler_Service.
*
* @var WC_Payments_Action_Scheduler_Service
*/
private $action_scheduler_service;
/**
* Webhook Processing Service.
*
* @var WC_Payments_Webhook_Processing_Service
*/
private $webhook_processing_service;
/**
* WC_Payments_Webhook_Reliability_Service constructor.
*
* @param WC_Payments_API_Client $payments_api_client WooCommerce Payments API client.
* @param WC_Payments_Action_Scheduler_Service $action_scheduler_service Wrapper for ActionScheduler service.
* @param WC_Payments_Webhook_Processing_Service $webhook_processing_service WC_Payments_Webhook_Processing_Service instance.
*/
public function __construct(
WC_Payments_API_Client $payments_api_client,
WC_Payments_Action_Scheduler_Service $action_scheduler_service,
WC_Payments_Webhook_Processing_Service $webhook_processing_service
) {
$this->payments_api_client = $payments_api_client;
$this->action_scheduler_service = $action_scheduler_service;
$this->webhook_processing_service = $webhook_processing_service;
add_action( 'woocommerce_payments_account_refreshed', [ $this, 'maybe_schedule_fetch_events' ] );
add_action( self::WEBHOOK_FETCH_EVENTS_ACTION, [ $this, 'fetch_events_and_schedule_processing_jobs' ] );
add_action( self::WEBHOOK_PROCESS_EVENT_ACTION, [ $this, 'process_event' ] );
}
/**
* During the account data refresh, check the relevant flag to remaining failed events on the WooCommerce Payments server,
* and decide whether scheduling a job to fetch them.
*
* @param mixed|array $account Account data retrieved from WooCommerce Payments server.
*
* @return void
*/
public function maybe_schedule_fetch_events( $account ) {
if ( ! is_array( $account ) ) {
return;
}
if ( $account[ self::CONTINUOUS_FETCH_FLAG_ACCOUNT_DATA ] ?? false ) {
$this->schedule_fetch_events();
}
}
/**
* Fetch failed events from the WooCommerce Payments server through ActionScheduler.
*
* @return void
*/
public function fetch_events_and_schedule_processing_jobs() {
try {
$payload = $this->payments_api_client->get_failed_webhook_events();
} catch ( API_Exception $e ) {
Logger::error( 'Can not fetch failed events from the server. Error:' . $e->getMessage() );
return;
}
if ( $payload[ self::CONTINUOUS_FETCH_FLAG_EVENTS_LIST ] ?? false ) {
$this->schedule_fetch_events();
}
// Save the data, and schedule a job for each event.
$events = $payload['data'] ?? [];
foreach ( $events as $event ) {
if ( ! isset( $event['id'] ) ) {
Logger::error( 'Event ID does not exist. Event data: ' . var_export( $event, true ) ); // phpcs:disable WordPress.PHP.DevelopmentFunctions.error_log_var_export
continue;
}
$this->set_event_data( $event );
$this->schedule_process_event( $event['id'] );
}
}
/**
* Process an event through ActionScheduler.
*
* @param string $event_id Event ID.
*
* @return void
*/
public function process_event( string $event_id ) {
Logger::info( 'Start processing event: ' . $event_id );
$event_data = $this->get_event_data( $event_id );
$this->delete_event_data( $event_id );
if ( null === $event_data ) {
Logger::error( 'Stop processing as no data available for event: ' . $event_id );
return;
}
try {
$this->webhook_processing_service->process( $event_data );
Logger::info( 'Successfully processed event ' . $event_id );
} catch ( Invalid_Webhook_Data_Exception $e ) {
Logger::error( 'Failed processing event ' . $event_id . '. Reason: ' . $e->getMessage() );
}
}
/**
* Schedule a job to process an event later.
*
* @param string $event_id Event ID.
*
* @return void
*/
private function schedule_process_event( string $event_id ) {
$this->action_scheduler_service->schedule_job( time(), self::WEBHOOK_PROCESS_EVENT_ACTION, [ 'event_id' => $event_id ] );
Logger::info( 'Successfully schedule a job to processing event: ' . $event_id );
}
/**
* Schedule a job to fetch failed events.
*
* @return void
*/
private function schedule_fetch_events() {
$this->action_scheduler_service->schedule_job( time(), self::WEBHOOK_FETCH_EVENTS_ACTION );
Logger::info( 'Successfully schedule a job to fetch failed events from the server.' );
}
/**
* Get the transient name to interact with the storage.
*
* @param string $event_id Event ID.
*
* @return string
*/
private function get_transient_name_for_event_id( string $event_id ): string {
// Use md5 to overcome the limit of transient name (172 characters) while Stripe event ID can be up to 255.
return 'wcpay_failed_event_' . md5( $event_id );
}
/**
* Save the event data.
*
* @param array $event_data Event data.
*
* @return bool True if the value was set, false otherwise.
*/
public function set_event_data( array $event_data ) {
if ( ! isset( $event_data['id'] ) ) {
return false;
}
return set_transient( $this->get_transient_name_for_event_id( $event_data['id'] ), $event_data, DAY_IN_SECONDS );
}
/**
* Delete the event data.
*
* @param string $event_id Event ID.
*
* @return bool True if the event data is deleted, false otherwise.
*/
public function delete_event_data( string $event_id ): bool {
return delete_transient( $this->get_transient_name_for_event_id( $event_id ) );
}
/**
* Retrieve the event data. Return null if the data does not exist.
*
* @param string $event_id Event ID.
*
* @return ?array
*/
public function get_event_data( string $event_id ) {
$data = get_transient( $this->get_transient_name_for_event_id( $event_id ) );
return false === $data ? null : $data;
}
}