PluginProbe
Sendy / trunk
Sendy vtrunk
3.4.6 3.4.7 trunk 3.0 3.0.1 3.0.2 3.0.3 3.0.4 3.0.5 3.0.6 3.0.7 3.0.8 3.0.9 3.1.0 3.1.1 3.1.2 3.1.3 3.2.0 3.2.1 3.2.2 3.2.3 3.2.4 3.2.5 3.2.6 3.2.7 All 32 releases
sendy / lib / Modules / Orders / OrdersModule.php

OrdersModule.php in Sendy trunk, at lib/Modules/Orders/OrdersModule.php

338 lines 13.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Sendy\WooCommerce\Modules\Orders;
4
5 use GuzzleHttp\Exception\GuzzleException;
6 use Sendy\Api\ApiException;
7 use Sendy\Api\Exceptions\SendyException;
8 use Sendy\WooCommerce\ApiClientFactory;
9 use Sendy\WooCommerce\Enums\ProcessingMethod;
10 use Sendy\WooCommerce\Repositories\Shops;
11
12 abstract class OrdersModule
13 {
14 /**
15 * Initialize an order object from the meta box
16 *
17 * @param \WC_Order|null
18 * @return \WP_Post|\WC_Order|null
19 */
20 protected function init_order_object($metaboxObject)
21 {
22 if (is_a($metaboxObject, 'WP_Post')) {
23 return wc_get_order($metaboxObject->ID);
24 }
25
26 if (is_a($metaboxObject, 'WC_Order')) {
27 return $metaboxObject;
28 }
29
30 return null;
31 }
32
33 /**
34 * Determine if the order should be shipped to a pickup point
35 */
36 protected function is_pickup_point_delivery(\WC_Order $order): bool
37 {
38 return $order->meta_exists('_sendy_pickup_point_id');
39 }
40
41 /**
42 * Whether the orders list can only print labels that already exist
43 *
44 * With the Sendy processing method the portal creates and generates the
45 * shipments itself, so the orders list never has a new label to wait for.
46 */
47 protected function print_existing_labels_only(): bool
48 {
49 return get_option('sendy_processing_method') !== ProcessingMethod::WooCommerce;
50 }
51
52 /**
53 * Create a shipment for the order through the configured processing method
54 *
55 * @param string $shopId The UUID of the selected shop
56 * @param string $preferenceId The UUID of the selected shipping preference
57 * @param int $amount The amount of packages the shipment should contain
58 * @throws GuzzleException
59 */
60 protected function create_shipment(\WC_Order $order, string $shopId, string $preferenceId, int $amount): void
61 {
62 if ($this->print_existing_labels_only()) {
63 $this->create_shipment_with_smart_rules($order, false, $shopId);
64 } else {
65 $this->create_shipment_from_order($order, $shopId, $preferenceId, $amount);
66 }
67 }
68
69 /**
70 * Create a shipment and flash any failure as an admin notice
71 *
72 * The creation methods flash API errors themselves; this also catches what
73 * they throw outside that, so one failing order does not abort the batch.
74 */
75 protected function create_shipment_flashing_errors(\WC_Order $order, string $shopId, string $preferenceId, int $amount): void
76 {
77 try {
78 $this->create_shipment($order, $shopId, $preferenceId, $amount);
79 } catch (SendyException $exception) {
80 // translators: %1$s contains the ID of the order, %2$s the error message
81 sendy_flash_admin_notice('error', sprintf(
82 __('Error while creating shipment for order #%1$s: %2$s', 'sendy'),
83 $order->get_id(),
84 $exception->getMessage(),
85 ));
86 }
87 }
88
89 /**
90 * Remember the create-shipments modal values as the defaults for next time
91 */
92 protected function remember_previously_used(string $shopId, string $preferenceId, string $amount): void
93 {
94 update_option('sendy_previously_used_shop_id', $shopId);
95
96 // Only the WooCommerce processing method uses a preference and amount.
97 if (! $this->print_existing_labels_only()) {
98 update_option('sendy_previously_used_preference_id', $preferenceId);
99 update_option('sendy_previously_used_amount', $amount);
100 }
101 }
102
103 /**
104 * Send a 403 JSON response when the nonce or the capabilities do not check out
105 *
106 * @param bool $flashErrors Also flash the failure as an admin notice, for
107 * callers whose JS reloads the page on an error
108 */
109 protected function verify_ajax_request(string $nonceAction, bool $flashErrors = false): void
110 {
111 if (! check_ajax_referer($nonceAction, 'nonce', false)) {
112 $message = __('Nonce verification failed', 'sendy');
113 } elseif (! current_user_can('manage_woocommerce') || ! current_user_can('edit_shop_orders')) {
114 $message = __('You do not have sufficient permissions to access this page.', 'sendy');
115 } else {
116 return;
117 }
118
119 if ($flashErrors) {
120 sendy_flash_admin_notice('error', $message);
121 }
122
123 wp_send_json(['message' => $message], 403);
124 }
125
126 /**
127 * Create the order in the Sendy API
128 *
129 * @param string $shopId The UUID of the selected shop
130 * @param string $preferenceId The UUID of the selected shipping preference
131 * @param int $amount The amount of packages the shipment should contain
132 * @throws GuzzleException
133 */
134 protected function create_shipment_from_order(\WC_Order $order, string $shopId, string $preferenceId, int $amount): void
135 {
136 if ($order->meta_exists('_sendy_shipment_id')) {
137 // translators: %s The ID of the order
138 sendy_flash_admin_notice('notice', sprintf(__('Order #%s already has a shipment created', 'sendy'), $order->get_id()));
139 }
140
141 $request = [
142 'preference_id' => $preferenceId,
143 'shop_id' => $shopId,
144 'amount' => $amount,
145 'reference' => $order->get_order_number(),
146 'order_date' => $order->get_date_created()->format(\DateTimeInterface::RFC3339),
147 'options' => [],
148 ];
149
150 $this->addAddressToRequest($order, $request);
151
152 if ($this->is_pickup_point_delivery($order)) {
153 $request['options']['parcel_shop_id'] = $order->get_meta('_sendy_pickup_point_id');
154 }
155
156 if (get_option('sendy_import_weight')) {
157 $this->addWeightToRequest($order, $request);
158 }
159
160 if (get_option('sendy_import_products')) {
161 $this->addProductsToRequest($order, $request);
162 }
163
164 try {
165 $result = ApiClientFactory::buildConnectionUsingTokens()->shipment->createFromPreference($request);
166
167 $order->update_meta_data('_sendy_shipment_id', $result['uuid']);
168 $order->update_meta_data('_sendy_packages', $result['packages']);
169
170 if (get_option('sendy_mark_order_as_completed') === 'after-shipment-created') {
171 $order->set_status('completed', __('Sendy: Shipment created', 'sendy'));
172 }
173
174 $order->save();
175 } catch (ApiException $exception) {
176 sendy_flash_admin_notice('error', $this->parseException($exception, $order));
177 }
178 }
179
180 protected function create_shipment_with_smart_rules(\WC_Order $order, bool $executedInBackground = true, ?string $shopId = null): void
181 {
182 $firstShopId = array_keys((new Shops())->list())[0];
183 $shippingMethod = array_values($order->get_shipping_methods())[0];
184
185 $request = [
186 'shop_id' => $shopId ?? get_option('sendy_default_shop') ?? $firstShopId,
187 'amount' => 1,
188 'reference' => $order->get_order_number(),
189 'order_date' => $order->get_date_created()->format(\DateTimeInterface::RFC3339),
190 'options' => [],
191 'shippingMethodId' => $shippingMethod ? $shippingMethod->get_instance_id() : null,
192 ];
193
194 $this->addAddressToRequest($order, $request);
195
196 if ($this->is_pickup_point_delivery($order)) {
197 $request['options']['parcel_shop_id'] = $order->get_meta('_sendy_pickup_point_id');
198 }
199
200 if (get_option('sendy_import_weight')) {
201 $this->addWeightToRequest($order, $request);
202 }
203
204 if (get_option('sendy_import_products')) {
205 $this->addProductsToRequest($order, $request);
206 }
207
208 try {
209 $shipment = ApiClientFactory::buildConnectionUsingTokens()->shipment->createWithSmartRules($request);
210
211 $order->update_meta_data('_sendy_shipment_id', $shipment['uuid']);
212 $order->save();
213 } catch (ApiException $exception) {
214 $message = $this->parseException($exception, $order);
215
216 if ($executedInBackground) {
217 // TODO Implement a logger
218 } else {
219 sendy_flash_admin_notice('error', $message);
220 }
221 }
222 }
223
224 /**
225 * Add the shipping address to the request to create the shipment
226 */
227 private function addAddressToRequest(\WC_Order $order, &$request): void
228 {
229 $request['contact'] = sprintf(
230 '%s %s',
231 $order->get_shipping_first_name() ?? $order->get_billing_first_name(),
232 $order->get_shipping_last_name() ?? $order->get_billing_last_name(),
233 );
234
235 $address = $this->parseAddressFromOrder($order);
236
237 $request['company_name'] = $order->get_shipping_company() ?? $order->get_billing_company();
238 $request['country'] = $order->get_shipping_country() ?? $order->get_billing_country();
239 $request['street'] = $address->street;
240 $request['number'] = $address->number;
241 $request['addition'] = $address->addition;
242 $request['postal_code'] = $order->get_shipping_postcode() ?? $order->get_billing_postcode();
243 $request['city'] = $order->get_shipping_city() ?? $order->get_billing_city();
244 $request['phone'] = $order->get_shipping_phone() ?? $order->get_billing_phone();
245 $request['email'] = $order->get_billing_email();
246 }
247
248 /**
249 * Add the weight to the request to create a shipment
250 */
251 private function addWeightToRequest(\WC_Order $order, &$request): void
252 {
253 $weight = 0;
254
255 foreach ($order->get_items() as $item) {
256 $weight += (float) wc_get_weight($item->get_product()->get_weight(), 'kg') * $item->get_quantity();
257 }
258
259 if ($weight > 0) {
260 $request['weight'] = $weight;
261 }
262 }
263
264 /**
265 * Add the product data to the request to create a shipment
266 */
267 private function addProductsToRequest(\WC_Order $order, &$request): void
268 {
269 foreach ($order->get_items() as $item) {
270 $product = $item->get_product();
271
272 $request['products'][] = [
273 'description' => $item->get_name(),
274 'sku' => $product->get_sku(),
275 'quantity' => $item->get_quantity(),
276 'unit_price' => $item->get_subtotal() / $item->get_quantity(),
277 'unit_weight' => absint($product->get_weight()) > 0 ? wc_get_weight($product->get_weight(), 'kg') : null,
278 ];
279 }
280 }
281
282 /**
283 * Split the address into street, number and addition
284 *
285 * First it tries to extra the address from address_1. If no number can be found, it is assumed that the number and
286 * addition are stored in address_2. It will concatenate address_1 and address_2 and parse the address from the
287 * concatenated string.
288 *
289 * @return object{street: string, house_number:string|null, house_number_addition:string|null}
290 */
291 private function parseAddressFromOrder(\WC_Order $order): \stdClass
292 {
293 $address = sendy_parse_address($order->get_shipping_address_1() ?? $order->get_billing_address_1());
294
295 if (is_null($address->number)) {
296 return sendy_parse_address(sprintf(
297 '%s %s',
298 $order->get_shipping_address_1() ?? $order->get_billing_address_1(),
299 $order->get_shipping_address_2() ?? $order->get_billing_address_2(),
300 ));
301 }
302
303 return $address;
304 }
305
306 private function parseException(ApiException $exception, \WC_Order $order): string
307 {
308 if ($exception->getCode() === 500) {
309 // translators: %1$s should contain the ID of the order and %2$s the error
310 $message = sprintf(__('Error while creating shipment for order #%1$s: %2$s', 'sendy'), $order->get_id(), $exception->getMessage());
311 } else {
312 $statusCode = $exception->getCode();
313
314 if ($statusCode === 401) {
315 // translators: %s should contain the ID of the order
316 $message = sprintf(__('Error while creating shipment for order #%s: Authentication failed. Check the settings page to reconnect with Sendy.', 'sendy'), $order->get_id());
317 } elseif ($statusCode === 422) {
318 $errors = [];
319
320 foreach ($exception->getErrors() as $_ => $messages) {
321 $errors = array_merge($errors, $messages);
322 }
323
324 // translators: %1$s should contain the ID of the order and %2$s the error
325 $message = sprintf(__('Error while creating shipment for order #%1$s: %2$s', 'sendy'), $order->get_id(), implode("\n", $errors));
326 } elseif ($statusCode === 429) {
327 // translators: %s should contain the ID of the order
328 $message = sprintf(__('Error while creating shipment for order #%s: Too many requests. Please try again later.', 'sendy'), $order->get_id());
329 } else {
330 // translators: %s should contain the ID of the order
331 $message = sprintf(__('Error while creating shipment for order #%s: Unknown error.', 'sendy'), $order->get_id());
332 }
333 }
334
335 return $message;
336 }
337 }
338