| 1 |
<?php |
| 2 |
/** |
| 3 |
* Class Rest_API\V4\Label\Eligibility file. |
| 4 |
* |
| 5 |
* @package PostNLWooCommerce\Rest_API\V4\Label |
| 6 |
*/ |
| 7 |
|
| 8 |
declare( strict_types = 1 ); |
| 9 |
|
| 10 |
namespace PostNLWooCommerce\Rest_API\V4\Label; |
| 11 |
|
| 12 |
use PostNLWooCommerce\Helper\Product_Mapper\V4_Mapper; |
| 13 |
use PostNLWooCommerce\Utils; |
| 14 |
|
| 15 |
if ( ! defined( 'ABSPATH' ) ) { |
| 16 |
exit; |
| 17 |
} |
| 18 |
|
| 19 |
/** |
| 20 |
* Pure decision logic for whether an order is a shipment the V4 label service |
| 21 |
* handles — a domestic NL parcel, a domestic NL letterbox (mailbox parcel), or |
| 22 |
* an EU/ROW international parcel from NL/BE. Kept free of WooCommerce and |
| 23 |
* Order\Base so the gate — the highest-risk part of the flow — can be asserted |
| 24 |
* in isolation. |
| 25 |
* |
| 26 |
* @since 6.0.0 |
| 27 |
* @package PostNLWooCommerce\Rest_API\V4\Label |
| 28 |
*/ |
| 29 |
class Eligibility { |
| 30 |
|
| 31 |
/** |
| 32 |
* Resolve the V4 mapper result for an order's product combination. |
| 33 |
* |
| 34 |
* The selected options are passed through so a service-bearing combination |
| 35 |
* that keeps product 3085 (e.g. insured) resolves to a services row (or an |
| 36 |
* unknown combination) and is rejected by is_eligible(), rather than |
| 37 |
* silently masquerading as the base parcel. |
| 38 |
* |
| 39 |
* @param string $origin Origin country (store base). |
| 40 |
* @param string $destination Shipping zone (NL|BE|EU|ROW). |
| 41 |
* @param bool $is_pickup Whether a pickup point was selected. |
| 42 |
* @param array $backend_raw Raw backend feature flags ('yes' strings). |
| 43 |
* @param string $product_code Legacy resolved product code. |
| 44 |
* @return array V4_Mapper::map() result. |
| 45 |
*/ |
| 46 |
public static function resolve_mapped( string $origin, string $destination, bool $is_pickup, array $backend_raw, string $product_code ): array { |
| 47 |
return V4_Mapper::map( |
| 48 |
array( |
| 49 |
'origin' => $origin, |
| 50 |
'destination' => $destination, |
| 51 |
'flow' => $is_pickup ? 'pickup_points' : 'delivery_day', |
| 52 |
'options' => array_keys( Utils::get_selected_label_features( $backend_raw ) ), |
| 53 |
'legacy_product_code' => $product_code, |
| 54 |
) |
| 55 |
); |
| 56 |
} |
| 57 |
|
| 58 |
/** |
| 59 |
* Decide whether the collected signals describe a shipment the V4 service |
| 60 |
* handles — a domestic NL or EU/ROW international parcel (single- or |
| 61 |
* multi-collo), or a domestic NL letterbox (mailbox parcel 2928). |
| 62 |
* |
| 63 |
* @param array $signals { |
| 64 |
* Signal set assembled by Service::gather_signals(). |
| 65 |
* |
| 66 |
* @type int $num_labels Collo count (>= 1). |
| 67 |
* @type bool $is_delivery_day A delivery-day option was selected. |
| 68 |
* @type bool $is_pickup A pickup point was selected. |
| 69 |
* @type bool $has_return A return label/barcode is involved. |
| 70 |
* @type string $delivery_type 'Standard' or 'Evening'. |
| 71 |
* @type string $origin Origin country. |
| 72 |
* @type string $destination Shipping zone. |
| 73 |
* @type array $mapped V4_Mapper::map() result. |
| 74 |
* } |
| 75 |
* @return bool |
| 76 |
*/ |
| 77 |
public static function is_eligible( array $signals ): bool { |
| 78 |
// Multi-collo (num_labels > 1) is supported via multiple request items; |
| 79 |
// only a missing/invalid collo count is rejected here. |
| 80 |
if ( (int) ( $signals['num_labels'] ?? 1 ) < 1 ) { |
| 81 |
return false; |
| 82 |
} |
| 83 |
|
| 84 |
if ( ! empty( $signals['is_delivery_day'] ) || ! empty( $signals['is_pickup'] ) ) { |
| 85 |
return false; |
| 86 |
} |
| 87 |
|
| 88 |
if ( ! empty( $signals['has_return'] ) ) { |
| 89 |
return false; |
| 90 |
} |
| 91 |
|
| 92 |
if ( 'Standard' !== ( $signals['delivery_type'] ?? 'Standard' ) ) { |
| 93 |
return false; |
| 94 |
} |
| 95 |
|
| 96 |
$origin = (string) ( $signals['origin'] ?? '' ); |
| 97 |
$destination = (string) ( $signals['destination'] ?? '' ); |
| 98 |
|
| 99 |
// Domestic NL (tasks 18-19) or an EU/ROW international parcel from NL/BE |
| 100 |
// (task 20). NL<->BE cross-border stays on legacy for now. |
| 101 |
$is_domestic = ( 'NL' === $origin && 'NL' === $destination ); |
| 102 |
$is_international = in_array( $origin, array( 'NL', 'BE' ), true ) |
| 103 |
&& in_array( $destination, array( 'EU', 'ROW' ), true ); |
| 104 |
|
| 105 |
if ( ! $is_domestic && ! $is_international ) { |
| 106 |
return false; |
| 107 |
} |
| 108 |
|
| 109 |
// The mapper is authoritative: for a combination it marks as having a V4 |
| 110 |
// equivalent, its services array is the full V4 representation of every |
| 111 |
// selected option, so no separate product-option gate is needed. Only a |
| 112 |
// pickup DeliveryLocation is excluded here — that variant lands separately. |
| 113 |
$mapped = $signals['mapped'] ?? array(); |
| 114 |
$shipment_type = (string) ( $mapped['shipmentType'] ?? '' ); |
| 115 |
|
| 116 |
if ( empty( $mapped['has_v4_equivalent'] ) || ! empty( $mapped['deliveryLocation'] ) ) { |
| 117 |
return false; |
| 118 |
} |
| 119 |
|
| 120 |
// Letterbox (mailbox parcel 2928) is a domestic-NL-only variant. The 48h |
| 121 |
// variant (2948) never reaches here: it collapses onto the same 'letterbox' |
| 122 |
// option key but keeps product code 2948, so the mapper's product-code |
| 123 |
// mismatch guard drops it to has_v4_equivalent = false above. A parcel may |
| 124 |
// be domestic or EU/ROW international. |
| 125 |
if ( 'letterbox' === $shipment_type ) { |
| 126 |
return $is_domestic; |
| 127 |
} |
| 128 |
|
| 129 |
return 'parcel' === $shipment_type; |
| 130 |
} |
| 131 |
|
| 132 |
/** |
| 133 |
* Resolve the mapper's service placeholders into concrete request values. |
| 134 |
* |
| 135 |
* The matrix stores insuredValue as the '<order_total>' placeholder — a misnomer |
| 136 |
* kept for now to match V4_Mapper; the value substituted is the item subtotal, not |
| 137 |
* the order total. It is replaced here with the order's insured amount. All other |
| 138 |
* flags (deliveryConfirmation, statedAddressOnly, returnWhenNotHome) pass through |
| 139 |
* unchanged. minimalAgeCheck would too, but no matrix row emits it yet. |
| 140 |
* |
| 141 |
* The insured amount must remain the order item subtotal (WC_Order::get_subtotal), |
| 142 |
* matching the value the legacy Shipping\Client puts in the Amounts block. Do not |
| 143 |
* switch it to the order total — that would add tax/shipping and diverge from V1. |
| 144 |
* |
| 145 |
* @param array $mapped_services Services array from V4_Mapper::map(). |
| 146 |
* @param float $insured_value Amount to insure (order item subtotal). |
| 147 |
* @return array Concrete service flags for Request_Builder. |
| 148 |
*/ |
| 149 |
public static function resolve_services( array $mapped_services, float $insured_value ): array { |
| 150 |
if ( array_key_exists( 'insuredValue', $mapped_services ) ) { |
| 151 |
$mapped_services['insuredValue'] = $insured_value; |
| 152 |
} |
| 153 |
|
| 154 |
return $mapped_services; |
| 155 |
} |
| 156 |
} |
| 157 |
|