PluginProbe ʕ •ᴥ•ʔ
WooCommerce / 10.9.4
WooCommerce v10.9.4
10.9.4 10.9.3 10.9.2 10.9.1 10.9.0 10.9.0-rc.1 10.9.0-beta.2 10.9.0-beta.1 10.8.1 10.8.0 10.8.0-rc.1 10.8.0-beta.2 10.8.0-beta.1 7.8.0-beta.1 7.8.0-beta.2 7.8.0-rc.1 7.8.0-rc.2 7.8.1 7.8.2 7.8.3 7.8.4 7.9.0 7.9.0-beta.1 7.9.0-beta.2 7.9.0-rc.2 7.9.0-rc.3 7.9.1 7.9.2 8.0.0 8.0.0-beta.1 8.0.0-beta.2 8.0.0-rc.1 8.0.0-rc.2 8.0.1 8.0.2 8.0.3 8.0.4 8.0.5 8.1.0 8.1.0-beta.1 8.1.0-rc.1 8.1.0-rc.2 8.1.1 8.1.2 8.1.3 8.1.4 8.2.0 8.2.0-beta.1 8.2.0-rc.1 8.2.0-rc.2 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.3.0 8.3.0-beta.1 8.3.0-rc.1 8.3.0-rc.2 8.3.1 8.3.2 8.3.3 8.3.4 8.4.0 8.4.0-beta.1 8.4.0-rc.1 8.4.1 8.4.2 8.4.3 8.5.0 8.5.0-beta.1 8.5.0-rc.1 8.5.1 8.5.2 8.5.3 8.5.4 8.5.5 8.6.0 8.6.0-beta.1 8.6.0-rc.1 8.6.1 8.6.2 8.6.3 8.6.4 8.7.0 8.7.0-beta.1 8.7.0-beta.2 8.7.0-rc.1 8.7.1 8.7.2 8.7.3 8.8.0 8.8.0-beta.1 8.8.0-rc.1 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.8.6 8.8.7 8.9.0 8.9.0-beta.1 8.9.0-rc.1 8.9.1 8.9.2 8.9.3 8.9.4 8.9.5 9.0.0 9.0.0-beta.1 9.0.0-beta.2 9.0.0-rc.1 9.0.1 9.0.2 9.0.3 9.0.4 9.1.0 9.1.0-beta.1 9.1.0-rc.1 9.1.1 9.1.2 9.1.3 9.1.4 9.1.5 9.1.6 9.2.0 9.2.0-beta.1 9.2.0-rc.1 9.2.1 9.2.2 9.2.3 9.2.4 9.2.5 9.3.0 9.3.0-beta.1 9.3.0-rc.1 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.3.6 9.4.0 9.4.0-beta.1 9.4.0-beta.2 9.4.0-rc.1 9.4.0-rc.2 9.4.0-rc.3 9.4.0-rc.4 9.4.1 9.4.2 9.4.3 9.4.4 9.4.5 9.5.0 9.5.0-beta.1 9.5.0-beta.2 9.5.0-rc.1 9.5.1 9.5.2 9.5.3 9.5.4 9.6.0 9.6.0-beta.1 9.6.0-beta.2 9.6.0-rc.1 9.6.1 9.6.2 9.6.3 9.6.4 9.7.0 9.7.0-beta.1 9.7.0-rc.1 9.7.1 9.7.2 9.7.3 9.8.0 9.8.0-beta.1 9.8.0-rc.1 9.8.1 9.8.2 9.8.3 9.8.4 9.8.5 9.8.6 9.8.7 9.9.0 9.9.0-beta.1 9.9.0-rc.1 9.9.1 9.9.2 9.9.3 9.9.4 9.9.5 9.9.6 9.9.7 3.7.3 7.1.2 3.8.0 7.2.0 3.8.0-beta.1 7.2.0-beta.1 3.8.0-rc.1 7.2.0-beta.2 3.8.0-rc.2 7.2.0-rc.1 3.8.1 7.2.0-rc.2 3.8.2 7.2.1 3.8.3 7.2.2 3.9.0 7.2.3 3.9.0-beta.1 7.2.4 3.9.0-beta.2 7.3.0 3.9.0-rc.1 7.3.0-beta.1 3.9.0-rc.2 7.3.0-beta.2 3.9.0-rc.3 7.3.0-rc.1 3.9.0-rc.4 7.3.0-rc.2 3.9.1 7.3.1 3.9.2 7.4.0 3.9.3 7.4.0-beta.1 3.9.4 7.4.0-beta.2 3.9.5 7.4.0-rc.1 4.0.0 7.4.0-rc.2 4.0.0-beta.1 7.4.1 4.0.0-rc.1 7.4.2 4.0.0-rc.2 7.5.0 4.0.1 7.5.0-beta.1 4.0.2 7.5.0-beta.2 4.0.3 7.5.0-rc.1 4.0.4 7.5.1 4.1.0 7.5.2 4.1.0-beta.1 7.6.0 4.1.0-beta.2 7.6.0-beta.1 4.1.0-rc.1 7.6.0-beta.2 4.1.0-rc.2 7.6.0-rc.1 4.1.1 7.6.0-rc.2 4.1.2 7.6.0-rc.3 4.1.3 7.6.1 4.1.4 7.6.2 4.2.0 7.7.0 4.2.0-RC.1 7.7.0-beta.1 4.2.0-RC.2 7.7.0-beta.2 4.2.0-beta.1 7.7.0-rc.1 4.2.1 7.7.1 4.2.2 7.7.2 4.2.3 7.7.3 4.2.4 7.8.0 4.2.5 4.3.0 4.3.0-beta.1 4.3.0-rc.1 4.3.0-rc.2 4.3.0-rc.3 4.3.1 4.3.2 4.3.3 4.3.4 4.3.5 4.3.6 4.4.0 4.4.0-beta.1 4.4.0-rc.1 4.4.1 4.4.2 4.4.3 4.4.4 4.5.0 4.5.0-beta.1 4.5.0-rc.1 4.5.0-rc.3 4.5.1 4.5.2 4.5.3 4.5.4 4.5.5 4.6.0 4.6.0-beta.1 4.6.0-rc.1 4.6.1 4.6.2 4.6.3 4.6.4 4.6.5 4.7.0 4.7.0-beta.1 4.7.0-beta.2 4.7.0-rc.1 4.7.1 4.7.1-beta.1 4.7.2 4.7.3 4.7.4 4.8.0 4.8.0-beta.1 4.8.0-rc.1 4.8.0-rc.2 4.8.1 4.8.2 4.8.3 4.9.0 4.9.0-beta.1 4.9.0-rc.1 4.9.0-rc.2 4.9.1 4.9.2 4.9.3 4.9.4 4.9.5 5.0.0 5.0.0-beta.1 5.0.0-beta.2 5.0.0-rc.1 5.0.0-rc.2 5.0.0-rc.3 5.0.1 5.0.2 5.0.3 5.1.0 5.1.0-beta.1 5.1.0-rc.1 trunk 5.1.1 10.0.0 5.1.2 10.0.0-rc.1 5.1.3 10.0.0-rc.2 5.2.0 10.0.1 5.2.0-beta.1 10.0.2 5.2.0-rc.1 10.0.3 5.2.0-rc.2 10.0.4 5.2.1 10.0.5 5.2.2 10.0.6 5.2.3 10.1.0 5.2.4 10.1.0-rc.1 5.2.5 10.1.0-rc.2 5.3.0 10.1.0-rc.3 5.3.0-beta.1 10.1.0-rc.4 5.3.0-rc.1 10.1.1 5.3.0-rc.2 10.1.2 5.3.1 10.1.3 5.3.2 10.1.4 5.3.3 10.2.0 5.4.0 10.2.0-beta.1 5.4.0-beta.1 10.2.0-beta.2 5.4.0-rc.1 10.2.0-rc.1 5.4.1 10.2.1 5.4.2 10.2.2 5.4.3 10.2.3 5.4.4 10.2.4 5.4.5 10.3.0 5.5.0 10.3.0-beta.1 5.5.0-beta.1 10.3.0-beta.2 5.5.0-rc.1 10.3.0-rc.1 5.5.0-rc.2 10.3.0-rc.2 5.5.1 10.3.1 5.5.2 10.3.2 5.5.3 10.3.3 5.5.4 10.3.4 5.5.5 10.3.5 5.6.0 10.3.6 5.6.0-beta.1 10.3.7 5.6.0-rc.1 10.3.8 5.6.0-rc.2 10.4.0 5.6.1 10.4.0-beta.1 5.6.2 10.4.0-beta.2 5.6.3 10.4.0-rc.1 5.7.0 10.4.1 5.7.0-beta.1 10.4.2 5.7.0-rc.1 10.4.3 5.7.1 10.4.4 5.7.2 10.5.0 5.7.3 10.5.0-beta.1 5.8.0 10.5.0-beta.2 5.8.0-beta.1 10.5.0-rc.1 5.8.0-beta.2 10.5.0-rc.2 5.8.0-rc.1 10.5.0-rc.3 5.8.1 10.5.1 5.8.2 10.5.2 5.9.0 10.5.3 5.9.0-beta.1 10.6.0 5.9.0-rc.1 10.6.0-beta.1 5.9.0-rc.2 10.6.0-beta.2 5.9.1 10.6.0-rc.1 5.9.2 10.6.1 6.0.0 10.6.2 6.0.0-beta.1 10.7.0 6.0.0-rc.1 10.7.0-beta.1 6.0.1 10.7.0-beta.2 6.0.2 10.7.0-rc.1 6.1.0 3.0.0 6.1.0-beta.1 3.0.1 6.1.0-rc.1 3.0.2 6.1.0-rc.2 3.0.3 6.1.1 3.0.4 6.1.2 3.0.5 6.1.3 3.0.6 6.2.0 3.0.7 6.2.0-beta.1 3.0.8 6.2.0-rc.1 3.0.9 6.2.0-rc.2 3.1.0 6.2.1 3.1.1 6.2.2 3.1.2 6.2.3 3.2.0 6.3.0 3.2.1 6.3.0-beta.1 3.2.2 6.3.0-rc.1 3.2.3 6.3.0-rc.2 3.2.4 6.3.1 3.2.5 6.3.2 3.2.6 6.4.0 3.3.0 6.4.0-beta.1 3.3.1 6.4.0-rc.1 3.3.2 6.4.1 3.3.2-rc.1 6.4.2 3.3.3 6.5.0 3.3.4 6.5.0-beta.1 3.3.5 6.5.0-rc.1 3.3.6 6.5.0-rc.2 3.4.0 6.5.1 3.4.0-beta.1 6.5.2 3.4.0-rc.2 6.6.0 3.4.1 6.6.0-beta.1 3.4.2 6.6.0-rc.1 3.4.3 6.6.0-rc.2 3.4.4 6.6.1 3.4.5 6.6.2 3.4.6 6.7.0 3.4.7 6.7.0-beta.1 3.4.8 6.7.0-beta.2 3.5.0 6.7.0-rc.1 3.5.0-beta.1 6.7.1 3.5.0-rc.1 6.8.0 3.5.0-rc.2 6.8.0-beta.1 3.5.1 6.8.0-beta.2 3.5.10 6.8.0-rc.1 3.5.2 6.8.1 3.5.3 6.8.2 3.5.4 6.8.3 3.5.5 6.9.0 3.5.6 6.9.0-beta.1 3.5.7 6.9.0-beta.2 3.5.8 6.9.0-rc.1 3.5.9 6.9.1 3.6.0 6.9.2 3.6.0-beta.1 6.9.3 3.6.0-rc.1 6.9.4 3.6.0-rc.2 6.9.5 3.6.0-rc.3 7.0.0 3.6.1 7.0.0-beta.1 3.6.2 7.0.0-beta.2 3.6.3 7.0.0-beta.3 3.6.4 7.0.0-rc.1 3.6.5 7.0.0-rc.2 3.6.6 7.0.1 3.6.7 7.0.2 3.7.0 7.1.0 3.7.0-beta.1 7.1.0-beta.1 3.7.0-rc.1 7.1.0-beta.2 3.7.0-rc.2 7.1.0-rc.1 3.7.1 7.1.0-rc.2 3.7.2 7.1.1
woocommerce / src / Admin / Features / Fulfillments / FulfillmentUtils.php
woocommerce / src / Admin / Features / Fulfillments Last commit date
DataStore 1 month ago Providers 3 months ago Fulfillment.php 1 month ago FulfillmentException.php 3 months ago FulfillmentOrderNotes.php 3 months ago FulfillmentUtils.php 2 months ago FulfillmentsController.php 3 months ago FulfillmentsManager.php 2 months ago FulfillmentsRenderer.php 1 month ago FulfillmentsSettings.php 3 months ago FulfillmentsTracker.php 3 months ago OrderFulfillmentsRestController.php 1 month ago ShippingProviders.php 3 months ago
FulfillmentUtils.php
743 lines
1 <?php declare(strict_types=1);
2
3 namespace Automattic\WooCommerce\Admin\Features\Fulfillments;
4
5 use Automattic\WooCommerce\Admin\Features\Fulfillments\Providers\AbstractShippingProvider;
6 use WC_Order;
7
8 /**
9 * Class FulfillmentUtils
10 *
11 * Utility class for handling order fulfillments.
12 */
13 class FulfillmentUtils {
14
15 /**
16 * Get pending items for an order.
17 *
18 * @param WC_Order $order The order object.
19 * @param array $fulfillments An array of fulfillments to check.
20 * @param bool $without_refunds Whether to exclude refunded items from the pending items.
21 *
22 * @return array An array of pending items.
23 */
24 public static function get_pending_items( WC_Order $order, $fulfillments, $without_refunds = true ): array {
25 $items_in_fulfillments = self::get_all_items_of_fulfillments( $fulfillments );
26 $order_items = array_map(
27 function ( $item ) use ( $order, $without_refunds ) {
28 // Refunded item quantities are saved as negative values in the order.
29 return array(
30 'item_id' => $item->get_id(),
31 'item' => $item,
32 'qty' => $item->get_quantity() + ( $without_refunds ? $order->get_qty_refunded_for_item( $item->get_id() ) : 0 ),
33 );
34 },
35 $order->get_items() ?? array()
36 );
37
38 // If there are items in fulfillments, subtract their quantities from the order items.
39 if ( ! empty( $items_in_fulfillments ) ) {
40 foreach ( $order_items as $item_id => &$item ) {
41 if ( isset( $items_in_fulfillments[ $item_id ] ) ) {
42 $item['qty'] = $item['qty'] - $items_in_fulfillments[ $item_id ];
43 }
44 }
45 }
46
47 return array_filter(
48 $order_items,
49 function ( $item ) {
50 return $item['qty'] > 0; // Only return items with a positive quantity.
51 }
52 );
53 }
54
55 /**
56 * Get refunded items for an order.
57 *
58 * @param WC_Order $order The order object.
59 *
60 * @return array An array of refunded items with their IDs and quantities.
61 */
62 public static function get_refunded_items( WC_Order $order ): array {
63 $items_refunded = array();
64 foreach ( $order->get_items() as $item ) {
65 $items_refunded[ $item->get_id() ] = -1 * $order->get_qty_refunded_for_item( $item->get_id() );
66 }
67 return array_filter(
68 $items_refunded,
69 function ( $qty ) {
70 return $qty > 0; // Only include items that have been refunded.
71 }
72 );
73 }
74
75 /**
76 * Get order items for a fulfillment.
77 *
78 * @param WC_Order $order The order object.
79 * @param Fulfillment $fulfillment The fulfillment object.
80 *
81 * @return array An array of order items.
82 */
83 public static function get_fulfillment_items( WC_Order $order, Fulfillment $fulfillment ): array {
84 $fulfillment_items = array_combine(
85 array_column( $fulfillment->get_items(), 'item_id' ),
86 array_column( $fulfillment->get_items(), 'qty' )
87 );
88
89 $order_items = array_map(
90 function ( $item ) use ( $order ) {
91 return array(
92 'item_id' => $item->get_id(),
93 'item' => $item,
94 'qty' => $item->get_quantity() - $order->get_qty_refunded_for_item( $item ),
95 );
96 },
97 $order->get_items()
98 );
99
100 return array_map(
101 function ( $item ) use ( $fulfillment_items ) {
102 $item['qty'] = $fulfillment_items[ $item['item_id'] ];
103 return $item;
104 },
105 array_filter(
106 $order_items,
107 function ( $item ) use ( $fulfillment_items ) {
108 return isset( $fulfillment_items[ $item['item_id'] ] );
109 }
110 )
111 );
112 }
113
114 /**
115 * Check if an order has pending items.
116 *
117 * @param WC_Order $order The order object.
118 * @param array $fulfillments An array of fulfillments to check.
119 *
120 * @return bool True if there are pending items, false otherwise.
121 */
122 public static function has_pending_items( WC_Order $order, array $fulfillments ): bool {
123 $pending_items = self::get_pending_items( $order, $fulfillments );
124 return ! empty( $pending_items );
125 }
126
127 /**
128 * Get the fulfillment status of the entity. This runs like a computed property, where
129 * it checks the fulfillment status of each fulfillment attached to the order,
130 * and computes the overall fulfillment status of the order.
131 *
132 * @param WC_Order $order The order object.
133 * @param array $fulfillments An array of fulfillments to check.
134 *
135 * @return string The fulfillment status.
136 */
137 public static function calculate_order_fulfillment_status( WC_Order $order, $fulfillments = array() ): string {
138 $has_fulfillments = ! empty( $fulfillments );
139 if ( $has_fulfillments ) {
140 $pending_items = self::get_pending_items( $order, $fulfillments );
141
142 $all_fulfilled = true;
143 $some_fulfilled = false;
144
145 foreach ( $fulfillments as $fulfillment ) {
146 if ( ! $fulfillment->get_is_fulfilled() ) {
147 $all_fulfilled = false;
148 } else {
149 $some_fulfilled = true;
150 }
151 }
152
153 if ( $all_fulfilled && empty( $pending_items ) ) {
154 $status = 'fulfilled';
155 } elseif ( $some_fulfilled ) {
156 $status = 'partially_fulfilled';
157 } else {
158 $status = 'unfulfilled';
159 }
160 } else {
161 $status = 'no_fulfillments';
162 }
163
164 /**
165 * This filter allows plugins to modify the fulfillment status of an order.
166 *
167 * @since 10.1.0
168 *
169 * @param string $status The default fulfillment status.
170 * @param WC_Order $order The order object.
171 * @param array $fulfillments An array of fulfillments for the order.
172 */
173 return apply_filters(
174 'woocommerce_fulfillment_calculate_order_fulfillment_status',
175 $status,
176 $order,
177 $fulfillments
178 );
179 }
180
181 /**
182 * Get all items from the fulfillments.
183 *
184 * @param array $fulfillments An array of fulfillments.
185 *
186 * @return array An associative array of item IDs and their quantities.
187 */
188 public static function get_all_items_of_fulfillments( array $fulfillments ): array {
189 $items = array();
190 foreach ( $fulfillments as $fulfillment ) {
191 $fulfillment_items = $fulfillment->get_items();
192 foreach ( $fulfillment_items as $item ) {
193 if ( ! isset( $items[ $item['item_id'] ] ) ) {
194 $items[ $item['item_id'] ] = 0; // Initialize if not set.
195 }
196 // Sum the quantities for each item.
197 $items[ $item['item_id'] ] += $item['qty'];
198 }
199 }
200 return $items;
201 }
202
203 /**
204 * Get the HTML for the fulfillment tracking number.
205 *
206 * @param Fulfillment $fulfillment The fulfillment object.
207 *
208 * @return string The HTML for the tracking number.
209 */
210 public static function get_tracking_info_html( Fulfillment $fulfillment ): string {
211 $tracking_html = '';
212 $tracking_url = $fulfillment->get_tracking_url();
213 $tracking_number = $fulfillment->get_tracking_number();
214 if ( null !== $tracking_url && null !== $tracking_number ) {
215 $tracking_html .= '<a href="' . esc_url( $tracking_url ) . '" target="_blank" rel="noopener noreferrer">';
216 $tracking_html .= esc_html( $tracking_number );
217 $tracking_html .= '</a>';
218 } elseif ( ! empty( $tracking_number ) ) {
219 $tracking_html .= esc_html( $tracking_number );
220 } else {
221 $tracking_html .= '<span class="no-tracking">' . esc_html__( 'No tracking number available', 'woocommerce' ) . '</span>';
222 }
223 return $tracking_html;
224 }
225
226 /**
227 * Get the fulfillment status of an order.
228 *
229 * @param WC_Order $order The order object.
230 * @return string The fulfillment status.
231 */
232 public static function get_order_fulfillment_status( WC_Order $order ): string {
233 if ( ! $order instanceof WC_Order ) {
234 return 'no_fulfillments';
235 }
236
237 return $order->meta_exists( '_fulfillment_status' ) ? $order->get_meta( '_fulfillment_status', true ) : 'no_fulfillments';
238 }
239
240 /**
241 * Get the fulfillment status text for an order.
242 *
243 * @param WC_Order $order The order object.
244 *
245 * @return string The fulfillment status text.
246 */
247 public static function get_order_fulfillment_status_text( WC_Order $order ): string {
248 // Ensure the order is a valid WC_Order object.
249 if ( ! $order instanceof WC_Order ) {
250 return '';
251 }
252
253 // Check if the order meta exists for fulfillment status.
254 $fulfillment_status = self::get_order_fulfillment_status( $order );
255 $fulfillment_status_text = '';
256 switch ( $fulfillment_status ) {
257 case 'fulfilled':
258 $fulfillment_status_text = ' ' . __( 'It has been <mark class="fulfillment-status">Fulfilled</mark>.', 'woocommerce' );
259 break;
260 case 'partially_fulfilled':
261 $fulfillment_status_text = ' ' . __( 'It has been <mark class="fulfillment-status">Partially fulfilled</mark>.', 'woocommerce' );
262 break;
263 case 'unfulfilled':
264 $fulfillment_status_text = ' ' . __( 'It is currently <mark class="fulfillment-status">Unfulfilled</mark>.', 'woocommerce' );
265 break;
266 case 'no_fulfillments':
267 $fulfillment_status_text = ' ' . __( 'It has <mark class="fulfillment-status">no fulfillments</mark> yet.', 'woocommerce' );
268 break;
269 }
270
271 /**
272 * This filter allows plugins to modify the fulfillment status text for an order for their custom fulfillment statuses.
273 *
274 * @since 10.1.0
275 *
276 * @param string $fulfillment_status_text The default fulfillment status text.
277 * @param string $fulfillment_status The fulfillment status of the order.
278 * @param WC_Order $order The order object.
279 */
280 return apply_filters(
281 'woocommerce_fulfillment_order_fulfillment_status_text',
282 $fulfillment_status_text,
283 $fulfillment_status,
284 $order
285 );
286 }
287
288 /**
289 * Get the meta query for the order fulfillment status.
290 *
291 * @param array|string $statuses The fulfillment statuses, or single status.
292 * @return array The meta query.
293 */
294 public static function get_order_fulfillment_status_meta_query( $statuses ): array {
295 if ( is_string( $statuses ) ) {
296 $statuses = array( $statuses );
297 }
298
299 $valid_statuses = array_filter( $statuses, array( self::class, 'is_valid_order_fulfillment_status' ) );
300 if ( empty( $valid_statuses ) ) {
301 return array();
302 }
303
304 if ( in_array( 'no_fulfillments', $valid_statuses, true ) ) {
305 return array(
306 'relation' => 'OR',
307 array(
308 'key' => '_fulfillment_status',
309 'value' => $valid_statuses,
310 'compare' => 'IN',
311 ),
312 array(
313 'key' => '_fulfillment_status',
314 'compare' => 'NOT EXISTS',
315 ),
316 );
317 }
318
319 return array(
320 'key' => '_fulfillment_status',
321 'value' => $valid_statuses,
322 'compare' => 'IN',
323 );
324 }
325
326 /**
327 * Check if the given fulfillment status is valid.
328 *
329 * @param string|null $status The fulfillment status to check.
330 *
331 * @return bool True if the status is valid, false otherwise.
332 */
333 public static function is_valid_order_fulfillment_status( ?string $status ): bool {
334 if ( is_null( $status ) ) {
335 return false;
336 }
337 $order_fulfillment_statuses = self::get_order_fulfillment_statuses();
338 return in_array( $status, array_keys( $order_fulfillment_statuses ), true );
339 }
340
341 /**
342 * Check if the given fulfillment status is valid.
343 *
344 * @param string|null $status The fulfillment status to check.
345 *
346 * @return bool True if the status is valid, false otherwise.
347 */
348 public static function is_valid_fulfillment_status( ?string $status ): bool {
349 if ( is_null( $status ) ) {
350 return false;
351 }
352 $fulfillment_statuses = self::get_fulfillment_statuses();
353 return in_array( $status, array_keys( $fulfillment_statuses ), true );
354 }
355
356 /**
357 * Get the order fulfillment statuses.
358 *
359 * This method provides the order fulfillment statuses that can be used
360 * in the WooCommerce Fulfillments system. It can be filtered using the
361 * `woocommerce_fulfillment_order_fulfillment_statuses` filter.
362 *
363 * @return array An associative array of order fulfillment statuses.
364 */
365 public static function get_order_fulfillment_statuses(): array {
366 /**
367 * This filter allows plugins to modify the list of order fulfillment statuses.
368 * It can be used to add, remove, or change the order fulfillment statuses available in the
369 * WooCommerce Fulfillments system.
370 *
371 * @since 10.1.0
372 *
373 * @param array $order_fulfillment_statuses The default list of order fulfillment statuses.
374 */
375 return apply_filters(
376 'woocommerce_fulfillment_order_fulfillment_statuses',
377 self::get_default_order_fulfillment_statuses()
378 );
379 }
380
381 /**
382 * Get the fulfillment statuses.
383 *
384 * This method provides the fulfillment statuses that can be used
385 * in the WooCommerce Fulfillments system. It can be filtered using the
386 * `woocommerce_fulfillment_fulfillment_statuses` filter.
387 *
388 * @return array An associative array of fulfillment statuses.
389 */
390 public static function get_fulfillment_statuses(): array {
391 /**
392 * This filter allows plugins to modify the list of fulfillment statuses.
393 * It can be used to add, remove, or change the fulfillment statuses available in the
394 * WooCommerce Fulfillments system.
395 *
396 * @since 10.1.0
397 *
398 * @param array $fulfillment_statuses The default list of fulfillment statuses.
399 */
400 return apply_filters(
401 'woocommerce_fulfillment_fulfillment_statuses',
402 self::get_default_fulfillment_statuses()
403 );
404 }
405
406 /**
407 * Get the shipping providers.
408 *
409 * This method retrieves the shipping providers registered in the WooCommerce Fulfillments system.
410 * It can be filtered using the `woocommerce_fulfillment_shipping_providers` filter.
411 *
412 * Any class name strings in the filter result are resolved into AbstractShippingProvider instances
413 * via the DI container. Invalid entries are silently skipped.
414 *
415 * @return AbstractShippingProvider[] An associative array of shipping provider instances keyed by provider key.
416 */
417 public static function get_shipping_providers(): array {
418 /**
419 * This filter allows plugins to modify the list of shipping providers.
420 * It can be used to add, remove, or change the shipping providers available in the
421 * WooCommerce Fulfillments system.
422 *
423 * @since 10.1.0
424 *
425 * @param array $shipping_providers The default list of shipping providers.
426 */
427 $raw_providers = apply_filters(
428 'woocommerce_fulfillment_shipping_providers',
429 array()
430 );
431
432 if ( ! is_array( $raw_providers ) ) {
433 return array();
434 }
435
436 $resolved = array();
437 foreach ( $raw_providers as $provider ) {
438 if ( $provider instanceof AbstractShippingProvider ) {
439 $resolved[ $provider->get_key() ] = $provider;
440 } elseif ( is_string( $provider )
441 && class_exists( $provider )
442 && is_subclass_of( $provider, AbstractShippingProvider::class )
443 ) {
444 try {
445 $instance = wc_get_container()->get( $provider );
446 } catch ( \Throwable $e ) {
447 continue;
448 }
449 $resolved[ $instance->get_key() ] = $instance;
450 }
451 }
452
453 return $resolved;
454 }
455
456 /**
457 * Get the default order fulfillment statuses.
458 *
459 * This method provides the default order fulfillment statuses that can be used
460 * in the WooCommerce Fulfillments system. It can be filtered using the
461 * `woocommerce_fulfillment_order_fulfillment_statuses` filter.
462 *
463 * @return array An associative array of default order fulfillment statuses.
464 */
465 protected static function get_default_order_fulfillment_statuses(): array {
466 return array(
467 'fulfilled' => array(
468 'label' => __( 'Fulfilled', 'woocommerce' ),
469 'background_color' => '#C6E1C6',
470 'text_color' => '#13550F',
471 ),
472 'partially_fulfilled' => array(
473 'label' => __( 'Partially fulfilled', 'woocommerce' ),
474 'background_color' => '#C8D7E1',
475 'text_color' => '#003D66',
476 ),
477 'unfulfilled' => array(
478 'label' => __( 'Unfulfilled', 'woocommerce' ),
479 'background_color' => '#FBE5E5',
480 'text_color' => '#CC1818',
481 ),
482 'no_fulfillments' => array(
483 'label' => __( 'No fulfillments', 'woocommerce' ),
484 'background_color' => '#F0F0F0',
485 'text_color' => '#2F2F2F',
486 ),
487 );
488 }
489
490 /**
491 * Get the default fulfillment statuses.
492 *
493 * This method provides the default fulfillment statuses that can be used
494 * in the WooCommerce Fulfillments system. It can be filtered using the
495 * `woocommerce_fulfillment_fulfillment_statuses` filter.
496 *
497 * @return array An associative array of default fulfillment statuses.
498 */
499 protected static function get_default_fulfillment_statuses(): array {
500 return array(
501 'fulfilled' => array(
502 'label' => __( 'Fulfilled', 'woocommerce' ),
503 'is_fulfilled' => true,
504 'background_color' => '#C6E1C6',
505 'text_color' => '#13550F',
506 ),
507 'unfulfilled' => array(
508 'label' => __( 'Unfulfilled', 'woocommerce' ),
509 'is_fulfilled' => false,
510 'background_color' => '#FBE5E5',
511 'text_color' => '#CC1818',
512 ),
513 );
514 }
515
516 /**
517 * Calculate the S10 check digit for UPU tracking numbers.
518 *
519 * @param string $tracking_number The tracking number without the check digit.
520 *
521 * @return bool True if the check digit is valid, false otherwise.
522 */
523 public static function check_s10_upu_format( string $tracking_number ): bool {
524 if ( preg_match( '/^[A-Z]{2}\d{9}[A-Z]{2}$/', $tracking_number ) ) {
525 // The tracking number is in the UPU S10 format.
526 $tracking_number = substr( $tracking_number, 2, -2 );
527 } elseif ( ! preg_match( '/^\d{9}$/', $tracking_number ) ) {
528 // Ensure the tracking number is exactly 9 digits.
529 return false;
530 }
531
532 // Define the weights for the S10 check digit calculation.
533 $weights = array( 8, 6, 4, 2, 3, 5, 9, 7 );
534 $sum = 0;
535
536 // Calculate the weighted sum of the digits.
537 for ( $i = 0; $i < 8; $i++ ) {
538 $sum += $weights[ $i ] * (int) $tracking_number[ $i ];
539 }
540
541 // Calculate the check digit.
542 $check_digit = 11 - ( $sum % 11 );
543 if ( 10 === $check_digit ) {
544 $check_digit = 0;
545 } elseif ( 11 === $check_digit ) {
546 $check_digit = 5;
547 }
548
549 // Validate the check digit against the last digit of the tracking number.
550 return (int) $tracking_number[8] === $check_digit;
551 }
552
553 /**
554 * Validate UPS 1Z tracking number using Mod 10 check digit.
555 *
556 * @param string $tracking_number The UPS 1Z tracking number.
557 * @return bool True if valid, false otherwise.
558 */
559 public static function validate_ups_1z_check_digit( string $tracking_number ): bool {
560 if ( ! preg_match( '/^1Z[0-9A-Z]{15,16}$/', $tracking_number ) ) {
561 return false;
562 }
563
564 // Extract the trackable part (remove 1Z prefix).
565 $trackable = substr( $tracking_number, 2 );
566 $check_digit = (int) substr( $trackable, -1 );
567 $trackable = substr( $trackable, 0, -1 );
568
569 $sum = 0;
570 $odd_position = true;
571
572 // Process each character from right to left.
573 for ( $i = strlen( $trackable ) - 1; $i >= 0; $i-- ) {
574 $char = $trackable[ $i ];
575 $value = is_numeric( $char ) ? (int) $char : ord( $char ) - 55; // A=10, B=11, etc.
576
577 if ( $odd_position ) {
578 $value *= 2;
579 if ( $value > 9 ) {
580 $value = (int) ( $value / 10 ) + ( $value % 10 );
581 }
582 }
583
584 $sum += $value;
585 $odd_position = ! $odd_position;
586 }
587
588 $calculated_check = ( 10 - ( $sum % 10 ) ) % 10;
589 return $calculated_check === $check_digit;
590 }
591
592 /**
593 * Validate Mod 7 check digit for numeric tracking numbers.
594 *
595 * @param string $tracking_number The numeric tracking number.
596 * @return bool True if valid, false otherwise.
597 */
598 public static function validate_mod7_check_digit( string $tracking_number ): bool {
599 if ( ! preg_match( '/^\d+$/', $tracking_number ) || strlen( $tracking_number ) < 2 ) {
600 return false;
601 }
602
603 $check_digit = (int) substr( $tracking_number, -1 );
604 $number = substr( $tracking_number, 0, -1 );
605 $sum = 0;
606 $weights = array( 3, 1, 3, 1, 3, 1, 3 ); // Mod 7 weights.
607 $weight_index = 0;
608 // Process each digit from right to left.
609 for ( $i = strlen( $number ) - 1; $i >= 0; $i-- ) {
610 $digit = (int) $number[ $i ];
611 $sum += $digit * $weights[ $weight_index % count( $weights ) ];
612 ++$weight_index;
613 }
614 $calculated_check = $sum % 7;
615 if ( 0 === $calculated_check ) {
616 $calculated_check = 7; // If the sum is a multiple of 7, the check digit is 7.
617 }
618 return $calculated_check === $check_digit;
619 }
620
621 /**
622 * Validate Mod 10 check digit for numeric tracking numbers.
623 *
624 * @param string $tracking_number The numeric tracking number.
625 * @return bool True if valid, false otherwise.
626 */
627 public static function validate_mod10_check_digit( string $tracking_number ): bool {
628 if ( ! preg_match( '/^\d+$/', $tracking_number ) || strlen( $tracking_number ) < 2 ) {
629 return false;
630 }
631
632 $check_digit = (int) substr( $tracking_number, -1 );
633 $number = substr( $tracking_number, 0, -1 );
634
635 $sum = 0;
636 $odd_position = true;
637
638 // Process each digit from right to left.
639 for ( $i = strlen( $number ) - 1; $i >= 0; $i-- ) {
640 $digit = (int) $number[ $i ];
641
642 if ( $odd_position ) {
643 $digit *= 2;
644 if ( $digit > 9 ) {
645 $digit = (int) ( $digit / 10 ) + ( $digit % 10 );
646 }
647 }
648
649 $sum += $digit;
650 $odd_position = ! $odd_position;
651 }
652
653 $calculated_check = ( 10 - ( $sum % 10 ) ) % 10;
654 return $calculated_check === $check_digit;
655 }
656
657 /**
658 * Validate Mod 11 check digit for tracking numbers (used by DHL).
659 *
660 * @param string $tracking_number The tracking number.
661 * @return bool True if valid, false otherwise.
662 */
663 public static function validate_mod11_check_digit( string $tracking_number ): bool {
664 if ( ! preg_match( '/^\d+$/', $tracking_number ) || strlen( $tracking_number ) < 2 ) {
665 return false;
666 }
667
668 $check_digit = (int) substr( $tracking_number, -1 );
669 $number = substr( $tracking_number, 0, -1 );
670
671 $weights = array( 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 );
672 $sum = 0;
673 $weight_index = 0;
674
675 // Process each digit from right to left.
676 for ( $i = strlen( $number ) - 1; $i >= 0; $i-- ) {
677 $digit = (int) $number[ $i ];
678 $sum += $digit * $weights[ $weight_index % count( $weights ) ];
679 ++$weight_index;
680 }
681
682 $calculated_check = 11 - ( $sum % 11 );
683 if ( 10 === $calculated_check ) {
684 $calculated_check = 0;
685 } elseif ( 11 === $calculated_check ) {
686 $calculated_check = 5;
687 }
688
689 return $calculated_check === $check_digit;
690 }
691
692 /**
693 * Validate FedEx check digit for 12/14-digit tracking numbers.
694 *
695 * @param string $tracking_number The FedEx tracking number.
696 * @return bool True if valid, false otherwise.
697 */
698 public static function validate_fedex_check_digit( string $tracking_number ): bool {
699 if ( ! preg_match( '/^\d{12}$/', $tracking_number ) ) {
700 return false;
701 }
702 $digits = str_split( substr( $tracking_number, 0, 11 ) );
703 $multipliers = array( 3, 1, 7 );
704 $sum = 0;
705 $multiplier_index = 0;
706 for ( $i = 10; $i >= 0; $i-- ) {
707 $sum += $digits[ $i ] * $multipliers[ $multiplier_index ];
708 $multiplier_index = ( ++$multiplier_index ) % 3;
709 }
710 $check = $sum % 11;
711 if ( 10 === $check ) {
712 $check = 0;
713 }
714 return intval( $tracking_number[11] ) === $check;
715 }
716
717 /**
718 * Resolve the provider name for a fulfillment.
719 *
720 * For custom providers ("other"), the display name from _provider_name meta is used.
721 * For known providers, the slug from _shipment_provider meta is preferred, but the
722 * display name is used as a fallback when the slug is empty (e.g., when auto-lookup
723 * identified the provider but did not set the slug).
724 *
725 * @since 10.7.0
726 *
727 * @param Fulfillment $fulfillment The fulfillment object.
728 *
729 * @return string The resolved provider name.
730 */
731 public static function resolve_provider_name( Fulfillment $fulfillment ): string {
732 $shipment_provider = $fulfillment->get_shipment_provider() ?? '';
733 $provider_name = $fulfillment->get_meta( '_provider_name', true );
734 $provider_name = ! empty( $provider_name ) ? (string) $provider_name : '';
735
736 if ( 'other' === $shipment_provider ) {
737 return $provider_name;
738 }
739
740 return ! empty( $shipment_provider ) ? $shipment_provider : $provider_name;
741 }
742 }
743