PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.5
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.5
1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 1.3.20 1.3.19 trunk All 48 releases
fluent-cart / app / Modules / PaymentMethods / PayPalGateway / API / Webhook.php

Webhook.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.6.5, at app/Modules/PaymentMethods/PayPalGateway/API/Webhook.php

197 lines 8.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCart\App\Modules\PaymentMethods\PayPalGateway\API;
4
5 use FluentCart\App\Modules\PaymentMethods\PayPalGateway\PayPalSettingsBase;
6 use FluentCart\Framework\Support\Arr;
7
8 class Webhook
9 {
10 const EVENTS = [
11 ['name' => 'PAYMENT.SALE.COMPLETED'],
12 ['name' => 'PAYMENT.SALE.REFUNDED'], // recurring payment refund
13 ['name' => 'PAYMENT.CAPTURE.REFUNDED'], // one time payment refund
14 ['name' => 'PAYMENT.CAPTURE.COMPLETED'],
15 ['name' => 'BILLING.SUBSCRIPTION.CREATED'],
16 ['name' => 'BILLING.SUBSCRIPTION.ACTIVATED'],
17 ['name' => 'BILLING.SUBSCRIPTION.SUSPENDED'],
18 ['name' => 'BILLING.SUBSCRIPTION.CANCELLED'],
19 ['name' => 'BILLING.SUBSCRIPTION.EXPIRED'],
20 ['name' => 'BILLING.SUBSCRIPTION.PAYMENT.FAILED'],
21 ['name' => 'CUSTOMER.DISPUTE.CREATED'],
22 ['name' => 'CUSTOMER.DISPUTE.UPDATED'],
23 ['name' => 'CUSTOMER.DISPUTE.RESOLVED'],
24
25 // Extra for testing
26 ['name' => 'CHECKOUT.CHECKOUT.BUYER-APPROVED'],
27 ['name' => 'INVOICING.INVOICE.PAID'],
28 ['name' => 'CHECKOUT.ORDER.COMPLETED'],
29 ['name' => 'CHECKOUT.ORDER.PROCESSED'],
30 ['name' => 'CHECKOUT.ORDER.APPROVED'],
31 ['name' => 'PAYMENT.ORDER.CREATED']
32 ];
33
34 const WEBHOOK_ENDPOINT = '?fluent-cart=fct_payment_listener_ipn&method=paypal';
35
36 public static function getWebhookURL(): string
37 {
38 // return 'https://webhook.site/9f647f0d-3514-47b7-acca-95a2c168b917';
39 return trailingslashit(site_url()) . self::WEBHOOK_ENDPOINT;
40 }
41
42 public static function webhookInstruction(): string
43 {
44 $webhook_url = static::getWebhookURL();
45
46 $events = sprintf(
47 '<b>%1$s</b>
48 %2$s
49 <b>%3$s</b>
50 %4$s',
51 __('Payments and Payout:', 'fluent-cart'),
52 __('- Payment capture refunded | Payment sale completed | Payment sale refunded', 'fluent-cart'),
53 __('Billing Subscriptions:', 'fluent-cart'),
54 __('- Billing subscription activated | Billing subscription cancelled | Billing subscription created | Billing subscription expired | Billing subscription suspended | Billing subscription payment failed', 'fluent-cart')
55 );
56
57 return sprintf(
58 '<div>
59 <br/>
60 <h4>%1$s</h4>
61 <p>%2$s</p>
62 <br/>
63 <p>%3$s</p>
64 <p>%4$s <a href="https://developer.paypal.com/dashboard/applications/production" target="_blank">%5$s</a> > %6$s</p>
65 <p>%7$s <code class="copyable-content">%8$s</code></p>
66 <b>%9$s</b>
67 %10$s
68 <br/>
69 </div>
70 <br/>',
71 __('How to configure webhook?', 'fluent-cart'), // %1$s
72 __('You should configure your PayPal webhooks manually if you connected manually. Follow the instruction to Create webhook and paste the webhook ID below.', 'fluent-cart'), // %2$s
73 __('In your PayPal account:', 'fluent-cart'), // %3$s
74 __('Go to', 'fluent-cart'), // %4$s
75 __('PayPal developers', 'fluent-cart'), // %5$s
76 __('App & Credentials > Your App > Webhooks section > Add webhook', 'fluent-cart'), // %6$s
77 __('Enter The Webhook URL:', 'fluent-cart'), // %7$s
78 $webhook_url, // %8$s
79 __('Select these events', 'fluent-cart'), // %9$s
80 $events // %10$s
81 );
82 }
83
84 public function registerWebhook($mode, $url = '')
85 {
86 $webhookData = API::makeRequest('notifications/webhooks', 'v1', 'POST',
87 [
88 'url' => $url ? $url : static::getWebhookURL(),
89 'event_types' => static::EVENTS
90 ], $mode);
91
92 if (is_wp_error($webhookData)) {
93 // if ERROR: WEBHOOK_URL_ALREADY_EXISTS, then manage
94 return $this->maybeWebhookExists($webhookData, $mode);
95 }
96
97 static::parseAndUpdateSettings($webhookData, $mode);
98 return $webhookData;
99 }
100
101 public static function parseAndUpdateSettings($webhookData, $mode)
102 {
103 $data = [
104 $mode . '_webhook_id' => Arr::get($webhookData, 'id'),
105 $mode . '_webhook_events' => Arr::get($webhookData, 'event_types')
106 ];
107 (new PayPalSettingsBase())->updateNonSensitiveData($data);
108 }
109
110 public function maybeWebhookExists($webhookData, $mode)
111 {
112 $webhookURL = static::getWebhookURL();
113 if (Arr::get($webhookData->get_error_data(), 'name') === 'WEBHOOK_URL_ALREADY_EXISTS') {
114 $webhookData = API::makeRequest('notifications/webhooks', 'v1', 'GET', []);
115 if (is_wp_error($webhookData)) {
116 return $webhookData;
117 }
118
119 $webhooks = Arr::get($webhookData, 'webhooks');
120 $matched = array_filter($webhooks, function ($webhook) use ($webhookURL) {
121 return $webhook['url'] === $webhookURL;
122 });
123
124 if (isset($matched[0])) {
125 $matchedWebhook = static::maybeSyncWebhookEvents($matched[0], $mode);
126 if (!is_wp_error($matchedWebhook)) {
127 static::parseAndUpdateSettings($matchedWebhook, $mode);
128 }
129 }
130 }
131 return $webhookData;
132 }
133
134 public function maybeSetWebhook($mode)
135 {
136 $payPalSettings = new PayPalSettingsBase();
137 if (!$payPalSettings->getApiKey($mode)) {
138 return [
139 'status' => 'false',
140 'message' => __('No API key found for webhook setup. Please connect your PayPal account first.', 'fluent-cart')
141 ];
142 }
143
144 $webhookId = $payPalSettings->get($mode . '_webhook_id');
145
146 if ($webhookId) {
147 $webhookData = API::makeRequest('notifications/webhooks/' . $webhookId, 'v1', 'GET', []);
148 if (!is_wp_error($webhookData) && Arr::get($webhookData, 'id') === $webhookId) {
149 $synced = static::maybeSyncWebhookEvents($webhookData, $mode);
150 if (is_wp_error($synced)) {
151 return $webhookData; // sync failed, keep the last known-good local record
152 }
153 static::parseAndUpdateSettings($synced, $mode); // update webhook events
154 return $synced;
155 }
156 }
157
158 // If there is no Webhook ID found or webhook isn't found, register a new one
159 return (new Webhook())->registerWebhook($mode);
160 }
161
162 /**
163 * PayPal never adds newly introduced event types to an already-registered webhook
164 * on its own - registerWebhook() only sets event_types at creation time. Reconcile
165 * the remote subscription against our EVENTS list on every maybeSetWebhook() call
166 * so a store that connected before an event type was added still receives it.
167 */
168 public static function maybeSyncWebhookEvents($webhookData, $mode)
169 {
170 $remoteEventNames = array_column((array)Arr::get($webhookData, 'event_types', []), 'name');
171 $expectedEventNames = array_column(static::EVENTS, 'name');
172 $missing = array_diff($expectedEventNames, $remoteEventNames);
173
174 if (empty($missing)) {
175 return $webhookData;
176 }
177
178 $patched = API::makeRequest('notifications/webhooks/' . Arr::get($webhookData, 'id'), 'v1', 'PATCH', [
179 [
180 'op' => 'replace',
181 'path' => '/event_types',
182 'value' => static::EVENTS
183 ]
184 ], $mode);
185
186 if (is_wp_error($patched)) {
187 return $patched;
188 }
189
190 // PATCH answers 204 with no body (no id/event_types) - build the updated
191 // representation locally rather than persisting the empty response.
192 $webhookData['event_types'] = static::EVENTS;
193
194 return $webhookData;
195 }
196 }
197