PluginProbe
WooCommerce / 11.1.0
WooCommerce v11.1.0
11.1.0 11.1.0-rc.2 11.1.0-rc.1 11.1.0-beta.2 11.1.0-beta.1 11.0.1 11.0.0 11.0.0-rc.3 11.0.0-rc.2 11.0.0-rc.1 11.0.0-beta.2 11.0.0-beta.1 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 All 648 releases
woocommerce / includes / rest-api / Controllers / Version3 / class-wc-rest-order-refunds-controller.php
class-wc-rest-order-refunds-controller.php
681 lines 25.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * REST API Order Refunds controller
4 *
5 * Handles requests to the /orders/<order_id>/refunds endpoint.
6 *
7 * @package WooCommerce\RestApi
8 * @since 2.6.0
9 */
10
11 defined( 'ABSPATH' ) || exit;
12
13 use Automattic\WooCommerce\Internal\RestApiParameterUtil;
14 use Automattic\WooCommerce\Internal\CostOfGoodsSold\CogsAwareTrait;
15 use Automattic\WooCommerce\Internal\RestApi\Routes\V4\Refunds\DataUtils;
16 use Automattic\WooCommerce\Internal\RestApi\Routes\V4\Refunds\Schema\RefundPreviewSchema;
17 use Automattic\WooCommerce\Utilities\MetaDataUtil;
18
19 /**
20 * REST API Order Refunds controller class.
21 *
22 * @package WooCommerce\RestApi
23 * @extends WC_REST_Order_Refunds_V2_Controller
24 */
25 class WC_REST_Order_Refunds_Controller extends WC_REST_Order_Refunds_V2_Controller {
26 use CogsAwareTrait;
27
28 /**
29 * Endpoint namespace.
30 *
31 * @var string
32 */
33 protected $namespace = 'wc/v3';
34
35 /**
36 * Register the routes for order refunds, including the refund preview route.
37 *
38 * The override is new in 11.1.0 even though the parent method is not, hence
39 * the tag per the convention for public methods.
40 *
41 * @return void
42 *
43 * @since 11.1.0
44 */
45 public function register_routes() {
46 parent::register_routes();
47
48 register_rest_route(
49 $this->namespace,
50 '/' . $this->rest_base . '/preview',
51 array(
52 'args' => array(
53 'order_id' => array(
54 'description' => __( 'The order ID.', 'woocommerce' ),
55 'type' => 'integer',
56 ),
57 ),
58 // permission_callback below intentionally uses the create-refund capability:
59 // preview is read-only but logically part of the refund-creation flow, so it
60 // requires the same capability. This prevents read-only-API clients from
61 // probing refund state on orders they cannot act on.
62 array(
63 'methods' => WP_REST_Server::CREATABLE,
64 'callback' => array( $this, 'preview_refund' ),
65 'permission_callback' => array( $this, 'create_item_permissions_check' ),
66 'args' => array(
67 'line_items' => $this->get_preview_line_items_arg_schema(),
68 ),
69 ),
70 'schema' => array( $this, 'get_public_preview_schema' ),
71 )
72 );
73 }
74
75 /**
76 * Preview a refund without creating it.
77 *
78 * Returns server-computed refund totals and per-line breakdowns for the
79 * requested line items, using the same calculation engine as the wc/v4
80 * refunds endpoints, so clients do not have to replicate tax, rounding,
81 * and currency-precision logic.
82 *
83 * @param WP_REST_Request $request Full details about the request.
84 * @return WP_REST_Response|WP_Error
85 *
86 * @phpstan-param WP_REST_Request<array<string, mixed>> $request
87 *
88 * @since 11.1.0
89 */
90 public function preview_refund( $request ) {
91 $order = wc_get_order( (int) $request['order_id'] );
92
93 // wc_get_order returns WC_Order|WC_Order_Refund|false; only a WC_Order
94 // (shop_order) is previewable here — refunds and missing IDs are rejected.
95 if ( ! $order instanceof WC_Order ) {
96 return new WP_Error( 'woocommerce_rest_invalid_order_id', __( 'Invalid order ID.', 'woocommerce' ), array( 'status' => 404 ) );
97 }
98
99 // The shared engine runs the whole pipeline: normalize, validate, build,
100 // and the aggregate guards. Its WP_Errors carry their HTTP status in the
101 // error data and use unprefixed codes (the engine is convention neutral);
102 // they are prefixed here at the v3 boundary so this endpoint follows the
103 // `woocommerce_rest_*` convention of the rest of the v3 surface.
104 $preview = $this->get_data_utils()->compute_refund_preview_or_error( $order, $request['line_items'], 'wc-rest-refunds' );
105
106 if ( is_wp_error( $preview ) ) {
107 return $this->prefix_error_code( $preview );
108 }
109
110 $preview = $this->add_preview_additional_fields( $preview, $request );
111
112 $response = rest_ensure_response( $preview );
113
114 /**
115 * Filters the refund preview response before it is returned, following the
116 * `woocommerce_rest_prepare_*` family contract. The preview is advisory:
117 * the create path re-validates independently, so filtered values cannot
118 * bypass the creation guards.
119 *
120 * @param WP_REST_Response $response The preview response. Its data carries
121 * breakdown, subtotal, tax, total, max_refundable.
122 * @param WC_Order $order The order the refund preview was computed for.
123 * @param WP_REST_Request $request The request.
124 *
125 * @since 11.1.0
126 */
127 return apply_filters( 'woocommerce_rest_prepare_order_refund_preview', $response, $order, $request );
128 }
129
130 /**
131 * Populate fields registered for the preview object type into a response.
132 *
133 * The stock add_additional_fields_to_object() resolves the object type from
134 * this controller's item schema (`order_refund`), so it would populate the
135 * wrong field set; the preview publishes its schema as
136 * `order_refund_preview` and must populate the fields registered for that
137 * type. Mirrors core's `_fields` handling: callbacks for fields the request
138 * excludes are not executed, so extension callbacks do not run for
139 * responses that will not carry their field. Runs before the response
140 * filter so filters see the complete payload.
141 *
142 * @param array $preview Preview response data.
143 * @param WP_REST_Request $request The request.
144 *
145 * @return array
146 *
147 * @phpstan-param WP_REST_Request<array<string, mixed>> $request
148 */
149 private function add_preview_additional_fields( array $preview, $request ): array {
150 $additional_fields = $this->get_additional_fields( 'order_refund_preview' );
151
152 if ( empty( $additional_fields ) ) {
153 return $preview;
154 }
155
156 $fields_for_response = $this->get_preview_fields_for_response( $request );
157
158 foreach ( $additional_fields as $field_name => $field_options ) {
159 if ( empty( $field_options['get_callback'] ) || ! is_callable( $field_options['get_callback'] ) ) {
160 continue;
161 }
162
163 if ( ! in_array( $field_name, $fields_for_response, true ) ) {
164 continue;
165 }
166
167 $preview[ $field_name ] = call_user_func( $field_options['get_callback'], $preview, $field_name, $request, 'order_refund_preview' );
168 }
169
170 return $preview;
171 }
172
173 /**
174 * Get the preview fields a request asks for, mirroring core's
175 * get_fields_for_response() against the preview schema instead of the
176 * controller's item schema.
177 *
178 * @param WP_REST_Request $request The request.
179 *
180 * @return string[]
181 *
182 * @phpstan-param WP_REST_Request<array<string, mixed>> $request
183 */
184 private function get_preview_fields_for_response( $request ): array {
185 $schema = $this->get_public_preview_schema();
186 $properties = isset( $schema['properties'] ) && is_array( $schema['properties'] ) ? $schema['properties'] : array();
187
188 // For back-compat, include any registered field with an empty schema, as
189 // core's get_fields_for_response() does: without a schema the field never
190 // reaches the published properties, but its callback must still run.
191 foreach ( $this->get_additional_fields( 'order_refund_preview' ) as $field_name => $field_options ) {
192 if ( is_null( $field_options['schema'] ) ) {
193 $properties[ $field_name ] = $field_options;
194 }
195 }
196
197 $fields = array_map( 'strval', array_keys( $properties ) );
198
199 if ( ! isset( $request['_fields'] ) || empty( $request['_fields'] ) ) {
200 return $fields;
201 }
202
203 $requested_fields = array_map(
204 static function ( $field ): string {
205 return trim( (string) $field );
206 },
207 wp_parse_list( $request['_fields'] )
208 );
209
210 if ( 0 === count( $requested_fields ) ) {
211 return $fields;
212 }
213
214 return array_values(
215 array_filter(
216 $fields,
217 function ( string $field ) use ( $requested_fields ): bool {
218 return rest_is_field_included( $field, $requested_fields );
219 }
220 )
221 );
222 }
223
224 /**
225 * Get the public schema for the refund preview endpoint.
226 *
227 * @return array
228 *
229 * @since 11.1.0
230 */
231 public function get_public_preview_schema() {
232 $schema = wc_get_container()->get( RefundPreviewSchema::class )->get_item_schema();
233 $schema['title'] = 'order_refund_preview';
234
235 // Like the sibling v3 schema getters: fields registered via
236 // register_rest_field() must appear in the published schema.
237 return $this->add_additional_fields_schema( $schema );
238 }
239
240 /**
241 * Get the argument schema for the preview route's line_items parameter.
242 *
243 * Shared with the wc/v4 preview endpoint (including the line_item_id key
244 * naming) so clients can send the same payload to both API versions and
245 * the accepted shape cannot drift between them.
246 *
247 * Note the two deliberate differences from this controller's create
248 * endpoint: the preview keys lines by `line_item_id` where the create
249 * uses `id`, and the preview's `refund_total` is tax-inclusive where the
250 * create's classic `refund_total` is net with taxes supplied separately
251 * via `refund_tax` (the compute_totals create shares the preview's
252 * tax-inclusive semantics).
253 *
254 * @return array
255 */
256 private function get_preview_line_items_arg_schema() {
257 return $this->get_data_utils()->get_preview_line_items_arg_schema();
258 }
259
260 /**
261 * Normalize one compute_totals line item to the shared engine's shape.
262 *
263 * Maps the create endpoint's public `id` key to the engine's `line_item_id`
264 * and validates/normalizes the scalar types. The REST schema cannot validate
265 * the line_items subtree (the property is readonly for backward
266 * compatibility), so without this check malformed values such as an array
267 * refund_total would reach the calculation engine and fail with a TypeError
268 * instead of a 400 response. Uses the same error codes as the engine's own
269 * validation, and casts numeric strings to their proper types.
270 *
271 * @param array $line_item Line item in the public request shape (id keys).
272 * @return array|WP_Error The normalized line item, or WP_Error on an invalid type.
273 *
274 * @since 11.1.0
275 */
276 private function normalize_line_item( array $line_item ) {
277 // The create endpoint documents `id`; the shared engine and the preview
278 // endpoint key lines by `line_item_id`, and either form is accepted here.
279 // A payload carrying both is rejected: silently preferring one could
280 // refund and restock a different line than the client intended.
281 if ( isset( $line_item['id'], $line_item['line_item_id'] ) ) {
282 return new WP_Error( 'woocommerce_rest_invalid_line_item', __( 'Specify the line item with either id or line_item_id, not both.', 'woocommerce' ), array( 'status' => 400 ) );
283 }
284
285 if ( isset( $line_item['id'] ) ) {
286 $line_item['line_item_id'] = $line_item['id'];
287 unset( $line_item['id'] );
288 }
289
290 // IDs must be whole numbers (rest_is_integer): silently truncating a
291 // fractional id such as 123.5 to 123 would target a different line or
292 // tax bucket than requested.
293 if ( isset( $line_item['line_item_id'] ) ) {
294 if ( ! rest_is_integer( $line_item['line_item_id'] ) ) {
295 return new WP_Error( 'woocommerce_rest_invalid_line_item', __( 'Line item id must be an integer.', 'woocommerce' ), array( 'status' => 400 ) );
296 }
297 $line_item['line_item_id'] = (int) $line_item['line_item_id'];
298 }
299
300 if ( isset( $line_item['quantity'] ) ) {
301 if ( ! rest_is_integer( $line_item['quantity'] ) ) {
302 return new WP_Error( 'woocommerce_rest_invalid_quantity', __( 'Quantity must be a whole number.', 'woocommerce' ), array( 'status' => 400 ) );
303 }
304 $line_item['quantity'] = (int) $line_item['quantity'];
305 }
306
307 if ( isset( $line_item['refund_total'] ) ) {
308 if ( ! is_numeric( $line_item['refund_total'] ) ) {
309 return new WP_Error( 'woocommerce_rest_invalid_refund_total', __( 'refund_total must be a number.', 'woocommerce' ), array( 'status' => 400 ) );
310 }
311 $line_item['refund_total'] = (float) $line_item['refund_total'];
312 }
313
314 if ( isset( $line_item['refund_tax'] ) ) {
315 if ( ! is_array( $line_item['refund_tax'] ) ) {
316 return new WP_Error( 'woocommerce_rest_invalid_line_item', __( 'refund_tax must be an array of objects with id and refund_total.', 'woocommerce' ), array( 'status' => 400 ) );
317 }
318 foreach ( $line_item['refund_tax'] as $index => $tax ) {
319 if ( ! is_array( $tax ) || ! isset( $tax['id'], $tax['refund_total'] ) || ! rest_is_integer( $tax['id'] ) || ! is_numeric( $tax['refund_total'] ) ) {
320 return new WP_Error( 'woocommerce_rest_invalid_line_item', __( 'refund_tax entries must be objects with an integer id and a numeric refund_total.', 'woocommerce' ), array( 'status' => 400 ) );
321 }
322 $line_item['refund_tax'][ $index ] = array(
323 'id' => (int) $tax['id'],
324 'refund_total' => (float) $tax['refund_total'],
325 );
326 }
327 }
328
329 return $line_item;
330 }
331
332 /**
333 * Get the shared refund calculation engine.
334 *
335 * DataUtils is the calculation/validation engine shared with the wc/v4
336 * refunds endpoints (the V4 segment in its namespace is historical); using
337 * it here keeps wc/v3 and wc/v4 refund math identical.
338 *
339 * @return DataUtils
340 */
341 private function get_data_utils(): DataUtils {
342 return wc_get_container()->get( DataUtils::class );
343 }
344
345 /**
346 * Prefix a shared-engine error code with `woocommerce_rest_`.
347 *
348 * DataUtils emits unprefixed codes (the wc/v4 convention). The wc/v3 surface
349 * uses `woocommerce_rest_*`, so errors crossing into a v3 response are
350 * renamed at this boundary. Codes that already carry the prefix pass through
351 * unchanged, and the message and data (including the HTTP status) are kept.
352 * An unprefixed engine error whose data carries no HTTP status is backfilled
353 * with 400, the same default the wc/v4 envelope applies, so it is not served
354 * as a 500. An already-prefixed code returns untouched above and so misses
355 * that backfill, which leaves no gap: every prefixed error reaching this
356 * endpoint is built by normalize_line_item() with an explicit status.
357 *
358 * @param WP_Error $error The error whose code should be prefixed.
359 *
360 * @return WP_Error
361 */
362 private function prefix_error_code( WP_Error $error ): WP_Error {
363 $code = (string) $error->get_error_code();
364
365 if ( str_starts_with( $code, 'woocommerce_rest_' ) ) {
366 return $error;
367 }
368
369 // Every DataUtils error site attaches array data; the guard below is here
370 // so a non-array payload cannot turn the $data['status'] write into a
371 // fatal. Replacing such a payload rather than nesting it mirrors the wc/v4
372 // envelope, which likewise reads a status only out of array data and drops
373 // the rest, so both versions answer an identical error identically.
374 $data = $error->get_error_data();
375 if ( ! is_array( $data ) ) {
376 $data = array();
377 }
378 if ( ! isset( $data['status'] ) ) {
379 $data['status'] = 400;
380 }
381
382 return new WP_Error( 'woocommerce_rest_' . $code, $error->get_error_message(), $data );
383 }
384
385 /**
386 * Prepares one object for create or update operation.
387 *
388 * @since 3.0.0
389 * @param WP_REST_Request $request Request object.
390 * @param bool $creating If is creating a new object.
391 * @return WP_Error|WC_Data The prepared item, or WP_Error object on failure.
392 */
393 protected function prepare_object_for_database( $request, $creating = false ) {
394 // The opt-in compute_totals mode routes through the shared wc/v4 refund
395 // calculation pipeline. It is a separate path so that requests without the
396 // flag behave exactly as before, including degenerate forms such as
397 // quantity-only line items producing a 0.00 refund. The schema declares
398 // compute_totals as boolean with a false default, so the REST layer has
399 // already sanitized the value by the time this runs.
400 if ( $creating && true === $request['compute_totals'] ) {
401 return $this->create_refund_with_computed_totals( $request );
402 }
403
404 RestApiParameterUtil::adjust_create_refund_request_parameters( $request );
405
406 $order = wc_get_order( (int) $request['order_id'] );
407
408 if ( ! $order ) {
409 return new WP_Error( 'woocommerce_rest_invalid_order_id', __( 'Invalid order ID.', 'woocommerce' ), 404 );
410 }
411
412 if ( 0 > $request['amount'] ) {
413 return new WP_Error( 'woocommerce_rest_invalid_order_refund', __( 'Refund amount must be greater than zero.', 'woocommerce' ), 400 );
414 }
415
416 // Create the refund.
417 $refund = wc_create_refund(
418 array(
419 'order_id' => $order->get_id(),
420 'amount' => $request['amount'],
421 'reason' => $request['reason'],
422 'line_items' => $request['line_items'],
423 'refund_payment' => $request['api_refund'],
424 'restock_items' => $request['api_restock'],
425 )
426 );
427
428 if ( is_wp_error( $refund ) ) {
429 return new WP_Error( 'woocommerce_rest_cannot_create_order_refund', $refund->get_error_message(), 500 );
430 }
431
432 if ( ! $refund ) {
433 return new WP_Error( 'woocommerce_rest_cannot_create_order_refund', __( 'Cannot create order refund, please try again.', 'woocommerce' ), 500 );
434 }
435
436 if ( ! empty( $request['meta_data'] ) ) {
437 MetaDataUtil::update( $request['meta_data'], $refund );
438 $refund->save_meta_data();
439 }
440
441 /**
442 * Filters an object before it is inserted via the REST API.
443 *
444 * The dynamic portion of the hook name, `$this->post_type`,
445 * refers to the object type slug.
446 *
447 * @param WC_Data $coupon Object object.
448 * @param WP_REST_Request $request Request object.
449 * @param bool $creating If is creating a new object.
450 */
451 return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $refund, $request, $creating );
452 }
453
454 /**
455 * Create a refund with server-computed per-line totals (compute_totals mode).
456 *
457 * Mirrors the wc/v4 refund creation pipeline: line items may omit refund_total
458 * (computed from quantity at the order's stored unit price, tax-inclusive,
459 * clamped to the remaining refundable amount), input is validated against the
460 * order's refund history, and the refund amount is derived from the line items
461 * unless an explicit amount override is supplied. Validation follows the same
462 * rules as the wc/v4 creation endpoint; error codes are prefixed with
463 * `woocommerce_rest_` at this v3 boundary like the rest of the v3 surface.
464 *
465 * @param WP_REST_Request $request Request object.
466 * @return WP_Error|WC_Data The created refund, or WP_Error object on failure.
467 *
468 * @phpstan-param WP_REST_Request<array<string, mixed>> $request
469 *
470 * @since 11.1.0
471 */
472 private function create_refund_with_computed_totals( $request ) {
473 $order = wc_get_order( (int) $request['order_id'] );
474
475 // wc_get_order can return a WC_Order_Refund for refund IDs — reject those
476 // here since refunds are not refundable themselves.
477 if ( ! $order instanceof WC_Order ) {
478 return new WP_Error( 'woocommerce_rest_invalid_order_id', __( 'Invalid order ID.', 'woocommerce' ), array( 'status' => 404 ) );
479 }
480
481 // Normalize each line to the engine's schema shape and validate value
482 // types here: the REST layer cannot, because the line_items schema
483 // property is readonly for backward compatibility, so its args are not
484 // registered.
485 $line_items = array();
486 foreach ( (array) ( $request['line_items'] ?? array() ) as $line_item ) {
487 if ( ! is_array( $line_item ) ) {
488 return new WP_Error( 'woocommerce_rest_invalid_line_item', __( 'Each line item must be an object.', 'woocommerce' ), array( 'status' => 400 ) );
489 }
490
491 $line_item = $this->normalize_line_item( $line_item );
492 if ( is_wp_error( $line_item ) ) {
493 return $line_item;
494 }
495
496 $line_items[] = $line_item;
497 }
498
499 // The shared engine runs the whole creation preparation: fill missing
500 // refund totals, validate against the order's refund history, convert to
501 // the internal wc_create_refund() format, resolve the amount, and apply
502 // the aggregate guards. Its WP_Errors carry their HTTP status in the
503 // error data and use unprefixed codes; they are prefixed here at the v3
504 // boundary like every other error the endpoint returns.
505 $prepared = $this->get_data_utils()->prepare_refund_creation_or_error(
506 $order,
507 $line_items,
508 $request->has_param( 'amount' ),
509 $request['amount'],
510 'wc-rest-refunds'
511 );
512
513 if ( is_wp_error( $prepared ) ) {
514 return $this->prefix_error_code( $prepared );
515 }
516
517 $line_item_data = $prepared['line_items'];
518 $refund_amount = $prepared['amount'];
519
520 // Mirror the resolved values back onto the request so the pre_insert filter
521 // below and any other downstream readers see the same internal-format
522 // line_items and amount the legacy path exposes after
523 // RestApiParameterUtil::adjust_create_refund_request_parameters().
524 $request->set_param( 'line_items', $line_item_data );
525 $request->set_param( 'amount', strval( $refund_amount ) );
526
527 $refund = wc_create_refund(
528 array(
529 'order_id' => $order->get_id(),
530 'amount' => $refund_amount,
531 'reason' => empty( $request['reason'] ) ? null : $request['reason'],
532 'line_items' => $line_item_data,
533 'refund_payment' => is_bool( $request['api_refund'] ) ? $request['api_refund'] : true,
534 'restock_items' => is_bool( $request['api_restock'] ) ? $request['api_restock'] : true,
535 )
536 );
537
538 // Same code and status as the legacy path above so a wc_create_refund
539 // failure looks identical to clients regardless of the compute_totals flag.
540 if ( is_wp_error( $refund ) ) {
541 return new WP_Error( 'woocommerce_rest_cannot_create_order_refund', $refund->get_error_message(), array( 'status' => 500 ) );
542 }
543
544 if ( ! $refund ) {
545 return new WP_Error( 'woocommerce_rest_cannot_create_order_refund', __( 'Cannot create order refund, please try again.', 'woocommerce' ), array( 'status' => 500 ) );
546 }
547
548 if ( ! empty( $request['meta_data'] ) ) {
549 MetaDataUtil::update( $request['meta_data'], $refund );
550 $refund->save_meta_data();
551 }
552
553 /**
554 * Filters an object before it is inserted via the REST API.
555 *
556 * The dynamic portion of the hook name, `$this->post_type`,
557 * refers to the object type slug.
558 *
559 * @param WC_Data $refund Object object.
560 * @param WP_REST_Request $request Request object.
561 * @param bool $creating If is creating a new object.
562 *
563 * @since 3.0.0
564 */
565 return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $refund, $request, true );
566 }
567
568 /**
569 * Get formatted item data.
570 * Invokes parents and then adds the proper Cost of Goods Sold information.
571 *
572 * @param WC_Data $data_object WC_Data instance.
573 * @return array
574 * @since 9.9.0
575 */
576 protected function get_formatted_item_data( $data_object ) {
577 $data = parent::get_formatted_item_data( $data_object );
578 if ( ! $this->cogs_is_enabled() ) {
579 return $data;
580 }
581
582 if ( $data_object instanceof WC_Abstract_Order && $data_object->has_cogs() ) {
583 $data['cost_of_goods_sold'] = array(
584 'value' => $data_object->get_cogs_total_value(),
585 );
586
587 foreach ( $data['line_items'] as $key => $line_item ) {
588 $cogs_value = $line_item['cogs_value'] ?? null;
589 if ( ! is_null( $cogs_value ) ) {
590 $data['line_items'][ $key ]['cost_of_goods_sold'] = array(
591 'value' => $cogs_value,
592 );
593 unset( $data['line_items'][ $key ]['cogs_value'] );
594 }
595 }
596 }
597 return $data;
598 }
599
600 /**
601 * Get the refund schema, conforming to JSON Schema.
602 *
603 * @return array
604 */
605 public function get_item_schema() {
606 $schema = parent::get_item_schema();
607
608 $schema['properties']['line_items']['items']['properties']['refund_total'] = array(
609 'description' => __( 'Amount that will be refunded for this line item (excluding taxes).', 'woocommerce' ),
610 'type' => 'number',
611 'context' => array( 'edit' ),
612 'readonly' => true,
613 );
614
615 $schema['properties']['line_items']['items']['properties']['taxes']['items']['properties']['refund_total'] = array(
616 'description' => __( 'Amount that will be refunded for this tax.', 'woocommerce' ),
617 'type' => 'number',
618 'context' => array( 'edit' ),
619 'readonly' => true,
620 );
621
622 $schema['properties']['api_restock'] = array(
623 'description' => __( 'When true, refunded items are restocked.', 'woocommerce' ),
624 'type' => 'boolean',
625 'context' => array( 'edit' ),
626 'default' => true,
627 );
628
629 $schema['properties']['compute_totals'] = array(
630 'description' => __( 'When true, the server computes per-line refund amounts from quantities using the order\'s stored prices and taxes, validating the request against the order\'s refund history. Defaults to false, which preserves the pre-existing behavior of this endpoint.', 'woocommerce' ),
631 'type' => 'boolean',
632 'context' => array( 'edit' ),
633 'default' => false,
634 );
635
636 if ( $this->cogs_is_enabled() ) {
637 $schema = $this->add_cogs_related_schema( $schema );
638 }
639
640 return $schema;
641 }
642
643 /**
644 * Add the Cost of Goods Sold related fields to the schema.
645 *
646 * @param array $schema The original schema.
647 * @return array The updated schema.
648 */
649 private function add_cogs_related_schema( array $schema ): array {
650 $schema['properties']['cost_of_goods_sold'] = array(
651 'description' => __( 'Cost of Goods Sold data.', 'woocommerce' ),
652 'type' => 'object',
653 'context' => array( 'view', 'edit' ),
654 'properties' => array(
655 'total_value' => array(
656 'description' => __( 'Total value of the Cost of Goods Sold for the refund.', 'woocommerce' ),
657 'type' => 'number',
658 'readonly' => true,
659 'context' => array( 'view', 'edit' ),
660 ),
661 ),
662 );
663
664 $schema['properties']['line_items']['items']['properties']['cost_of_goods_sold'] = array(
665 'description' => __( 'Cost of Goods Sold data. Only present for product refund line items.', 'woocommerce' ),
666 'type' => 'object',
667 'context' => array( 'view', 'edit' ),
668 'properties' => array(
669 'total_value' => array(
670 'description' => __( 'Value of the Cost of Goods Sold for the refund item.', 'woocommerce' ),
671 'type' => 'number',
672 'readonly' => true,
673 'context' => array( 'view', 'edit' ),
674 ),
675 ),
676 );
677
678 return $schema;
679 }
680 }
681