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 / DataStore / FulfillmentsDataStore.php
woocommerce / src / Admin / Features / Fulfillments / DataStore Last commit date
FulfillmentsDataStore.php 1 month ago FulfillmentsDataStoreInterface.php 3 months ago
FulfillmentsDataStore.php
669 lines
1 <?php
2 /**
3 * Class FulfillmentsDataStore file.
4 *
5 * @package WooCommerce\DataStores
6 */
7
8 declare( strict_types=1 );
9
10 namespace Automattic\WooCommerce\Admin\Features\Fulfillments\DataStore;
11
12 use Automattic\WooCommerce\Admin\Features\Fulfillments\Fulfillment;
13 use Automattic\WooCommerce\Admin\Features\Fulfillments\FulfillmentUtils;
14 use WC_Meta_Data;
15
16 if ( ! defined( 'ABSPATH' ) ) {
17 exit;
18 }
19
20 /**
21 * WC Order Item Product Data Store
22 *
23 * @version 9.9.0
24 */
25 class FulfillmentsDataStore extends \WC_Data_Store_WP implements \WC_Object_Data_Store_Interface, FulfillmentsDataStoreInterface {
26
27 /**
28 * Method to create a new fulfillment in the database.
29 *
30 * @param Fulfillment $data The fulfillment object to create.
31 *
32 * @return void
33 *
34 * @throws \Exception If the fulfillment data is invalid.
35 * @throws \Exception If the fulfillment can't be created.
36 */
37 public function create( &$data ): void {
38 // Validate the fulfillment data.
39 if ( ! $data->get_entity_type() ) {
40 throw new \Exception( esc_html__( 'Invalid entity type.', 'woocommerce' ) );
41 }
42 if ( ! $data->get_entity_id() ) {
43 throw new \Exception( esc_html__( 'Invalid entity ID.', 'woocommerce' ) );
44 }
45 if ( ! FulfillmentUtils::is_valid_fulfillment_status( $data->get_status() ) ) {
46 throw new \Exception( esc_html__( 'Invalid fulfillment status.', 'woocommerce' ) );
47 }
48
49 $this->validate_items( $data );
50
51 // Set fulfillment properties.
52 $data->set_date_updated( current_time( 'mysql' ) );
53
54 /**
55 * Filter to modify the fulfillment data before it is created.
56 *
57 * @since 10.1.0
58 */
59 $data = apply_filters( 'woocommerce_fulfillment_before_create', $data );
60
61 $is_fulfill_action = $data->get_is_fulfilled();
62 // If the fulfillment is fulfilled, set the fulfilled date.
63 if ( $is_fulfill_action ) {
64 $data->set_date_fulfilled( current_time( 'mysql' ) );
65
66 /**
67 * Filter to modify the fulfillment data before it is fulfilled.
68 *
69 * @since 10.1.0
70 */
71 $data = apply_filters(
72 'woocommerce_fulfillment_before_fulfill',
73 $data
74 );
75 }
76
77 // Save the fulfillment to the database.
78 global $wpdb;
79 $rows_inserted = $wpdb->insert(
80 $wpdb->prefix . 'wc_order_fulfillments',
81 array(
82 'entity_type' => $data->get_entity_type(),
83 'entity_id' => $data->get_entity_id(),
84 'status' => $data->get_status() ?? 'unfulfilled',
85 'is_fulfilled' => $data->get_is_fulfilled() ? 1 : 0,
86 'date_updated' => $data->get_date_updated(),
87 'date_deleted' => $data->get_date_deleted(),
88 ),
89 array( '%s', '%s', '%s', '%d', '%s', '%s' )
90 );
91
92 // Check for errors.
93 if ( false === $rows_inserted ) {
94 throw new \Exception( esc_html__( 'Failed to insert fulfillment.', 'woocommerce' ) );
95 }
96
97 // Set the ID of the fulfillment object.
98 $data_id = $wpdb->insert_id;
99
100 $data->set_id( $data_id );
101
102 // Save the metadata for the fulfillment to the database.
103 $data->save_meta_data();
104
105 // Apply changes let's the object know that the current object reflects the database and no "changes" exist between the two.
106 $data->apply_changes();
107 $data->set_object_read( true );
108
109 if ( ! doing_action( 'woocommerce_fulfillment_after_create' ) ) {
110 /**
111 * Action to perform after a fulfillment is created.
112 *
113 * @param Fulfillment $data The fulfillment object that was created.
114 *
115 * @since 10.1.0
116 */
117 do_action( 'woocommerce_fulfillment_after_create', $data );
118 }
119
120 if ( $is_fulfill_action && ! doing_action( 'woocommerce_fulfillment_after_fulfill' ) ) {
121 /**
122 * Action to perform after a fulfillment is fulfilled.
123 *
124 * @since 10.1.0
125 */
126 do_action( 'woocommerce_fulfillment_after_fulfill', $data );
127 }
128 }
129
130 /**
131 * Method to read a fulfillment from the database.
132 *
133 * @param Fulfillment $data The fulfillment object to read.
134 *
135 * @return void
136 *
137 * @throws \Exception If the fulfillment data can't be read.
138 */
139 public function read( &$data ): void {
140 // Read the fulfillment from the database.
141 global $wpdb;
142
143 $data_id = $data->get_id();
144 $fulfillment_data = $wpdb->get_row(
145 $wpdb->prepare(
146 "SELECT * FROM {$wpdb->prefix}wc_order_fulfillments WHERE fulfillment_id = %d",
147 $data_id
148 ),
149 ARRAY_A
150 );
151
152 if ( empty( $fulfillment_data ) ) {
153 throw new \Exception( esc_html__( 'Fulfillment not found.', 'woocommerce' ) );
154 }
155
156 // Use set_props_from_storage() so already-UTC date columns are not re-normalized as site-local.
157 $data->set_props_from_storage( array_diff_key( $fulfillment_data, array( 'fulfillment_id' => true ) ) );
158 $data->set_id( (int) $fulfillment_data['fulfillment_id'] );
159 $data->read_meta_data( true );
160 $data->set_object_read( true );
161 $data->snapshot_meta();
162 }
163
164 /**
165 * Method to update an existing fulfillment in the database.
166 *
167 * @param Fulfillment $data The fulfillment object to update.
168 *
169 * @return void
170 *
171 * @throws \Exception If the fulfillment can't be updated.
172 */
173 public function update( &$data ): void {
174 // If the fulfillment is deleted, do nothing.
175 if ( $data->get_date_deleted() ) {
176 return;
177 }
178
179 // Update the fulfillment in the database.
180 $data_id = $data->get_id();
181
182 if ( ! FulfillmentUtils::is_valid_fulfillment_status( $data->get_status() ) ) {
183 throw new \Exception( esc_html__( 'Invalid fulfillment status.', 'woocommerce' ) );
184 }
185
186 $this->validate_items( $data );
187
188 /**
189 * Filter to modify the fulfillment data before it is updated.
190 *
191 * @param Fulfillment $data The fulfillment object that is being updated.
192 *
193 * @since 10.1.0
194 */
195 $data = apply_filters( 'woocommerce_fulfillment_before_update', $data );
196
197 // If the fulfillment is fulfilled, set the fulfilled date.
198 $is_fulfill_action = false;
199 if ( $data->get_is_fulfilled() && empty( $data->get_date_fulfilled() ) ) {
200 $is_fulfill_action = true;
201 $data->set_date_fulfilled( current_time( 'mysql' ) );
202
203 /**
204 * Filter to modify the fulfillment data before it is fulfilled.
205 *
206 * @param Fulfillment $data The fulfillment object that is being fulfilled.
207 *
208 * @since 10.1.0
209 */
210 $data = apply_filters(
211 'woocommerce_fulfillment_before_fulfill',
212 $data
213 );
214 }
215
216 global $wpdb;
217
218 // Capture changes and previous status before set_date_updated (which always
219 // changes) and before apply_changes resets the tracking.
220 $changes = $data->get_changes();
221 $previous_status = $data->get_data()['status'] ?? 'unfulfilled';
222
223 $data->set_date_updated( current_time( 'mysql' ) );
224
225 $wpdb->update(
226 $wpdb->prefix . 'wc_order_fulfillments',
227 array(
228 'entity_type' => $data->get_entity_type(),
229 'entity_id' => $data->get_entity_id(),
230 'status' => $data->get_status(),
231 'is_fulfilled' => $data->get_is_fulfilled() ? 1 : 0,
232 'date_updated' => $data->get_date_updated(),
233 'date_deleted' => $data->get_date_deleted(),
234 ),
235 array(
236 'fulfillment_id' => $data_id,
237 'date_deleted' => null,
238 ),
239 array( '%s', '%s', '%s', '%d', '%s', '%s' ),
240 array( '%d' )
241 );
242
243 // Check for errors.
244 if ( $wpdb->last_error ) {
245 throw new \Exception( esc_html__( 'Failed to update fulfillment.', 'woocommerce' ) );
246 }
247
248 // Update the metadata for the fulfillment.
249 $data->save_meta_data();
250 $data->apply_changes();
251
252 $data->set_object_read( true );
253
254 if ( ! doing_action( 'woocommerce_fulfillment_after_update' ) ) {
255 /**
256 * Action to perform after a fulfillment is updated.
257 *
258 * @param Fulfillment $data The fulfillment object that was updated.
259 * @param array $changes The changes that were applied, as returned by
260 * Fulfillment::get_changes() before save. Core data
261 * props at top level, meta changes under 'meta_data'.
262 * @param string $previous_status The fulfillment status before the update.
263 *
264 * @since 10.1.0
265 * @since 10.7.0 Added $changes and $previous_status parameters.
266 */
267 do_action( 'woocommerce_fulfillment_after_update', $data, $changes, $previous_status );
268 }
269
270 if ( $is_fulfill_action && ! doing_action( 'woocommerce_fulfillment_after_fulfill' ) ) {
271 /**
272 * Action to perform after a fulfillment is fulfilled.
273 *
274 * @param Fulfillment $data The fulfillment object that was fulfilled.
275 *
276 * @since 10.1.0
277 */
278 do_action( 'woocommerce_fulfillment_after_fulfill', $data );
279 }
280 }
281
282 /**
283 * Method to delete a fulfillment from the database.
284 *
285 * @param Fulfillment $data The fulfillment object to delete.
286 * @param array $args Optional arguments to pass to the delete method.
287 *
288 * @return void
289 *
290 * @throws \Exception If the fulfillment can't be deleted.
291 */
292 public function delete( &$data, $args = array() ): void {
293 // If the record is already deleted, do nothing.
294 if ( $data->get_date_deleted() ) {
295 return;
296 }
297
298 /**
299 * Filter to modify the fulfillment data before it is updated.
300 *
301 * @since 10.1.0
302 */
303 $data = apply_filters( 'woocommerce_fulfillment_before_delete', $data );
304
305 // Soft Delete the fulfillment from the database.
306 global $wpdb;
307
308 $data_id = $data->get_id();
309
310 // Route through the setter so the stored value is normalized to UTC,
311 // then read it back for the direct DB write below.
312 $data->set_date_deleted( current_time( 'mysql' ) );
313 $deletion_time = $data->get_date_deleted();
314
315 $wpdb->update(
316 $wpdb->prefix . 'wc_order_fulfillments',
317 array( 'date_deleted' => $deletion_time ),
318 array(
319 'fulfillment_id' => $data_id,
320 'date_deleted' => null,
321 ),
322 array( '%s' ),
323 array( '%d' )
324 );
325
326 // Check for errors.
327 if ( $wpdb->last_error ) {
328 throw new \Exception( esc_html__( 'Failed to delete fulfillment.', 'woocommerce' ) );
329 }
330 $data->apply_changes();
331 $data->set_object_read( true );
332
333 if ( ! doing_action( 'woocommerce_fulfillment_after_delete' ) ) {
334 /**
335 * Action to perform after a fulfillment is deleted.
336 *
337 * @since 10.1.0
338 */
339 do_action( 'woocommerce_fulfillment_after_delete', $data );
340 }
341
342 // Set the fulfillment object to a fresh state.
343 $data = new Fulfillment();
344 }
345
346 /**
347 * Method to read the metadata for a fulfillment.
348 *
349 * @param Fulfillment $data The fulfillment object to read.
350 * @return array
351 *
352 * @throws \Exception If the fulfillment is not saved.
353 */
354 public function read_meta( &$data ): array {
355 if ( ! $data->get_id() ) {
356 throw new \Exception( esc_html__( 'Invalid fulfillment.', 'woocommerce' ) );
357 }
358
359 // Read the metadata for the fulfillment.
360 global $wpdb;
361
362 $data_id = $data->get_id();
363 $meta_data = $wpdb->get_results(
364 $wpdb->prepare(
365 "SELECT * FROM {$wpdb->prefix}wc_order_fulfillment_meta WHERE fulfillment_id = %d",
366 $data_id
367 ),
368 OBJECT
369 );
370
371 return array_map(
372 function ( $meta ) {
373 $meta->meta_value = json_decode( $meta->meta_value, true ) ?? $meta->meta_value;
374 return $meta;
375 },
376 $meta_data
377 );
378 }
379
380 /**
381 * Method to delete the metadata for a fulfillment.
382 *
383 * @param Fulfillment $data The fulfillment object to delete.
384 * @param WC_Meta_Data $meta Meta object (containing at least ->id).
385 *
386 * @return void
387 *
388 * @throws \Exception If the fulfillment or meta is not saved.
389 */
390 public function delete_meta( &$data, $meta ): void {
391 // Check if the fulfillment and meta are saved.
392 $data_id = $data->get_id();
393
394 // Prevent deletion of metadata from a deleted fulfillment.
395 if ( $data->get_date_deleted() ) {
396 throw new \Exception( esc_html__( 'Cannot delete meta from a deleted fulfillment.', 'woocommerce' ) );
397 }
398
399 $meta_id = $meta->id;
400 if ( ! is_numeric( $data_id ) || $data_id <= 0 || ! is_numeric( $meta_id ) || $meta_id <= 0 ) {
401 throw new \Exception( esc_html__( 'Invalid fulfillment or meta.', 'woocommerce' ) );
402 }
403
404 // Delete the metadata for the fulfillment.
405 global $wpdb;
406
407 $wpdb->delete(
408 $wpdb->prefix . 'wc_order_fulfillment_meta',
409 array(
410 'fulfillment_id' => $data_id,
411 'meta_id' => $meta_id,
412 ),
413 array(
414 '%d',
415 '%d',
416 )
417 );
418 }
419
420 /**
421 * Method to add metadata for a fulfillment.
422 *
423 * @param Fulfillment $data The fulfillment object to save.
424 * @param WC_Meta_Data $meta Meta object (containing at least ->id).
425 * @return int meta ID or WP_Error on failure.
426 *
427 * @throws \Exception If the fulfillment or meta is not saved.
428 */
429 public function add_meta( &$data, $meta ): int {
430 // Add the metadata for the fulfillment.
431 global $wpdb;
432
433 // Prevent adding metadata to a deleted fulfillment.
434 if ( $data->get_date_deleted() ) {
435 throw new \Exception( esc_html__( 'Cannot add meta to a deleted fulfillment.', 'woocommerce' ) );
436 }
437
438 // Data ID can't be something wrong as this function is called after the meta is read.
439 // See WC_Data::save_meta_data().
440 $data_id = $data->get_id();
441
442 $wpdb->insert(
443 $wpdb->prefix . 'wc_order_fulfillment_meta',
444 array(
445 'fulfillment_id' => $data_id,
446 'meta_key' => $meta->key, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
447 'meta_value' => wp_json_encode( $meta->value ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
448 ),
449 array(
450 '%d',
451 '%s',
452 '%s',
453 )
454 );
455
456 // Note: There is no error check on WC_Data::save_meta_data(), and it expects us to return an ID in all cases.
457 // If there's an error, we should return null to indicate we didn't save it.
458 if ( $wpdb->last_error ) {
459 throw new \Exception( esc_html__( 'Failed to insert fulfillment meta.', 'woocommerce' ) );
460 }
461
462 return $wpdb->insert_id;
463 }
464
465 /**
466 * Method to save the metadata for a fulfillment.
467 *
468 * @param Fulfillment $data The fulfillment object to save.
469 * @param WC_Meta_Data $meta Meta object (containing at least ->id).
470 *
471 * @return int Number of rows updated.
472 *
473 * @throws \Exception If the fulfillment or meta is not saved.
474 */
475 public function update_meta( &$data, $meta ): int {
476 // Update the metadata for the fulfillment.
477 global $wpdb;
478
479 $data_id = $data->get_id();
480
481 // Prevent updating metadata for a deleted fulfillment.
482 if ( $data->get_date_deleted() ) {
483 throw new \Exception( esc_html__( 'Cannot update meta for a deleted fulfillment.', 'woocommerce' ) );
484 }
485
486 $rows_updated = $wpdb->update(
487 $wpdb->prefix . 'wc_order_fulfillment_meta',
488 array(
489 'meta_value' => wp_json_encode( $meta->value ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
490 ),
491 array(
492 'fulfillment_id' => $data_id,
493 'meta_id' => $meta->id,
494 'meta_key' => $meta->key, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
495 ),
496 array(
497 '%s',
498 ),
499 array(
500 '%d',
501 '%d',
502 '%s',
503 )
504 );
505
506 // Check for errors.
507 if ( $wpdb->last_error ) {
508 throw new \Exception( esc_html__( 'Failed to update fulfillment meta.', 'woocommerce' ) );
509 }
510
511 return $rows_updated;
512 }
513
514 /**
515 * Method to read the fulfillment data.
516 *
517 * @param string $entity_type The entity type.
518 * @param string $entity_id The entity ID.
519 * @param bool $with_deleted Whether to include deleted fulfillments in the results.
520 *
521 * @return Fulfillment[] Fulfillment object.
522 *
523 * @throws \Exception If the fulfillment data can't be read.
524 */
525 public function read_fulfillments( string $entity_type, string $entity_id, bool $with_deleted = false ): array {
526 // Read the fulfillment data from the database.
527 global $wpdb;
528
529 if ( ! $with_deleted ) {
530 $fulfillment_data = $wpdb->get_results(
531 $wpdb->prepare(
532 "SELECT * FROM {$wpdb->prefix}wc_order_fulfillments WHERE entity_type = %s AND entity_id = %s AND date_deleted IS NULL",
533 $entity_type,
534 $entity_id
535 ),
536 ARRAY_A
537 );
538 } else {
539 $fulfillment_data = $wpdb->get_results(
540 $wpdb->prepare(
541 "SELECT * FROM {$wpdb->prefix}wc_order_fulfillments WHERE entity_type = %s AND entity_id = %s",
542 $entity_type,
543 $entity_id
544 ),
545 ARRAY_A
546 );
547 }
548
549 if ( is_wp_error( $fulfillment_data ) ) {
550 throw new \Exception( esc_html__( 'Failed to read fulfillment data.', 'woocommerce' ) );
551 }
552
553 // Create Fulfillment objects from the data.
554 $fulfillments = array();
555 foreach ( $fulfillment_data as $data ) {
556 // Note: Don't initialize with ID, it will cause a re-read from the database.
557 // Set the ID directly after the object is created.
558 $fulfillment = new Fulfillment();
559 $fulfillment->set_id( $data['fulfillment_id'] );
560 // Use set_props_from_storage() so already-UTC date columns are not re-normalized as site-local.
561 $fulfillment->set_props_from_storage( $data );
562 $fulfillment->apply_changes();
563 $fulfillment->set_object_read( true );
564
565 // Read the metadata for the fulfillment.
566 $fulfillment->read_meta_data( true );
567 $fulfillment->snapshot_meta();
568
569 $fulfillments[] = $fulfillment;
570 }
571
572 return $fulfillments;
573 }
574
575 /**
576 * Hard-delete all fulfillment records (and their metadata) for a given entity.
577 *
578 * This is used when an order is permanently deleted to prevent orphaned rows.
579 *
580 * @since 10.7.0
581 *
582 * @param string $entity_type The entity type (e.g. 'WC_Order').
583 * @param string $entity_id The entity ID.
584 *
585 * @return int The number of fulfillment records deleted.
586 *
587 * @throws \RuntimeException If a database query fails.
588 * @throws \Throwable If the deletion fails.
589 */
590 public function delete_by_entity( string $entity_type, string $entity_id ): int {
591 global $wpdb;
592
593 wc_transaction_query( 'start' );
594
595 try {
596 // Delete metadata for all fulfillments belonging to this entity.
597 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table names are safe.
598 $result = $wpdb->query(
599 $wpdb->prepare(
600 "DELETE m FROM {$wpdb->prefix}wc_order_fulfillment_meta m INNER JOIN {$wpdb->prefix}wc_order_fulfillments f ON m.fulfillment_id = f.fulfillment_id WHERE f.entity_type = %s AND f.entity_id = %s",
601 $entity_type,
602 $entity_id
603 )
604 );
605
606 if ( false === $result ) {
607 throw new \RuntimeException( 'Failed to delete fulfillment metadata: ' . $wpdb->last_error );
608 }
609
610 // Delete the fulfillment records themselves.
611 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is safe.
612 $rows_deleted = $wpdb->query(
613 $wpdb->prepare(
614 "DELETE FROM {$wpdb->prefix}wc_order_fulfillments WHERE entity_type = %s AND entity_id = %s",
615 $entity_type,
616 $entity_id
617 )
618 );
619
620 if ( false === $rows_deleted ) {
621 throw new \RuntimeException( 'Failed to delete fulfillment records: ' . $wpdb->last_error );
622 }
623
624 wc_transaction_query( 'commit' );
625 } catch ( \Throwable $e ) {
626 wc_transaction_query( 'rollback' );
627 throw $e;
628 }
629
630 return (int) $rows_deleted;
631 }
632
633 /**
634 * Method to validate the items in a fulfillment.
635 *
636 * @param Fulfillment $data The fulfillment object to validate.
637 *
638 * @return void
639 *
640 * @throws \Exception If the fulfillment data is invalid.
641 */
642 private function validate_items( Fulfillment $data ): void {
643 $items = $data->get_meta( '_items', true );
644 if ( empty( $items ) ) {
645 throw new \Exception( esc_html__( 'The fulfillment should contain at least one item.', 'woocommerce' ) );
646 }
647
648 if ( ! is_array( $items ) ) {
649 throw new \Exception( esc_html__( 'The fulfillment items should be an array.', 'woocommerce' ) );
650 }
651
652 foreach ( $data->get_items() as $item ) {
653 if ( ! isset( $item['item_id'] )
654 // The item ID and qty should be set.
655 || ! isset( $item['qty'] )
656 // The item ID should be integers.
657 || ! is_int( $item['item_id'] )
658 // Allow the qty to be a float too.
659 || ( ! is_int( $item['qty'] ) && ! is_float( $item['qty'] ) )
660 // The item ID and qty should be greater than 0.
661 || $item['item_id'] <= 0
662 || $item['qty'] <= 0
663 ) {
664 throw new \Exception( esc_html__( 'Invalid item.', 'woocommerce' ) );
665 }
666 }
667 }
668 }
669