| @@ -633,8 +633,23 @@ | ||
| 633 | 633 | if ( ! $order ) { |
| 634 | 634 | return $payload; |
| 635 | 635 | } |
| 636 | 636 | |
| 637 | + // `{ id, code: null }` is the POS's deletion marker for a synced coupon line — | |
| 638 | + // the `<key>: null` shape wc/v3's item_is_null() honours for every other line | |
| 639 | + // collection, and the one remove_omitted_order_items() mints for them. wc/v3 | |
| 640 | + // keys coupon_lines by code instead, so the marker must be dropped here: the | |
| 641 | + // remaining codes then differ from the stored set and the remove-and-reapply | |
| 642 | + // below removes the coupon, or match it (a retried push) and nothing is sent. | |
| 643 | + $payload['coupon_lines'] = array_values( | |
| 644 | + array_filter( | |
| 645 | + $payload['coupon_lines'], | |
| 646 | + static function ( $line ) { | |
| 647 | + return ! self::is_coupon_deletion_marker( $line ); | |
| 648 | + } | |
| 649 | + ) | |
| 650 | + ); | |
| 651 | + | |
| 637 | 652 | $requested_codes = array(); |
| 638 | 653 | $all_lines_valid = true; |
| 639 | 654 | foreach ( $payload['coupon_lines'] as $line ) { |
| 640 | 655 | $code = is_array( $line ) ? ( $line['code'] ?? null ) : null; |
| @@ -667,6 +682,29 @@ | ||
| 667 | 682 | } |
| 668 | 683 | $payload['coupon_lines'] = array_values( $payload['coupon_lines'] ); |
| 669 | 684 | |
| 670 | 685 | return $payload; |
| 686 | + } | |
| 687 | + | |
| 688 | + /** | |
| 689 | + * Whether a posted coupon line is a deletion marker: a synced line (a canonical | |
| 690 | + * positive integer id, as an int or a digit string) whose `code` key is present | |
| 691 | + * and null. Anything else — no `code` key, an empty code, a missing, zero, | |
| 692 | + * negative, fractional or exponent id — is malformed and still reaches wc/v3, | |
| 693 | + * whose own validation answers ("Coupon code is required" / "Coupon item ID is | |
| 694 | + * readonly"). | |
| 695 | + * | |
| 696 | + * @param mixed $line A posted coupon line. | |
| 697 | + * | |
| 698 | + * @return bool | |
| 699 | + */ | |
| 700 | + public static function is_coupon_deletion_marker( $line ): bool { | |
| 701 | + if ( ! is_array( $line ) || ! array_key_exists( 'code', $line ) || null !== $line['code'] ) { | |
| 702 | + return false; | |
| 703 | + } | |
| 704 | + $id = $line['id'] ?? null; | |
| 705 | + if ( is_int( $id ) ) { | |
| 706 | + return $id > 0; | |
| 707 | + } | |
| 708 | + return is_string( $id ) && ctype_digit( $id ) && (int) $id > 0; | |
| 671 | 709 | } |
| 672 | 710 | } |