ACSCourierShippingProvider.php
3 months ago
AbstractShippingProvider.php
3 months ago
AmazonLogisticsShippingProvider.php
3 months ago
AnPostShippingProvider.php
3 months ago
ArasKargoShippingProvider.php
3 months ago
AustraliaPostShippingProvider.php
3 months ago
AzerpostShippingProvider.php
3 months ago
BartoliniBRTShippingProvider.php
3 months ago
BelpochtaShippingProvider.php
3 months ago
BpostShippingProvider.php
3 months ago
BulgarianPostsShippingProvider.php
3 months ago
CDEKShippingProvider.php
3 months ago
CTTShippingProvider.php
3 months ago
CanadaPostShippingProvider.php
3 months ago
CeskaPostaShippingProvider.php
3 months ago
ChronopostShippingProvider.php
3 months ago
CorreosShippingProvider.php
3 months ago
CustomShippingProvider.php
3 months ago
CyprusPostShippingProvider.php
3 months ago
DHLShippingProvider.php
3 months ago
DPDShippingProvider.php
3 months ago
DeutschePostShippingProvider.php
3 months ago
ELTAShippingProvider.php
3 months ago
EcontShippingProvider.php
3 months ago
EimskipShippingProvider.php
3 months ago
EvriHermesShippingProvider.php
3 months ago
FanCourierShippingProvider.php
3 months ago
FastwayShippingProvider.php
3 months ago
FedExShippingProvider.php
3 months ago
GLSShippingProvider.php
3 months ago
GenikiTaxydromikiShippingProvider.php
3 months ago
HayPostShippingProvider.php
3 months ago
HelthjemShippingProvider.php
3 months ago
HrvatskaPostaShippingProvider.php
3 months ago
InPostShippingProvider.php
3 months ago
IslandsposturShippingProvider.php
3 months ago
ItellaShippingProvider.php
3 months ago
KazpostShippingProvider.php
3 months ago
LaPosteColissimoShippingProvider.php
3 months ago
LasershipOntracShippingProvider.php
3 months ago
LatvijasPastsShippingProvider.php
3 months ago
LiechtensteinischePostShippingProvider.php
3 months ago
MPLShippingProvider.php
3 months ago
MRWShippingProvider.php
3 months ago
MagyarPostaShippingProvider.php
3 months ago
MakedonskaPostaShippingProvider.php
3 months ago
MaltaPostShippingProvider.php
3 months ago
MatkahuoltoShippingProvider.php
3 months ago
MondialRelayShippingProvider.php
3 months ago
NewZealandPostShippingProvider.php
3 months ago
NovaPoshtaShippingProvider.php
3 months ago
OmnivaShippingProvider.php
3 months ago
OsterreichischePostShippingProvider.php
3 months ago
ParcelForceShippingProvider.php
3 months ago
PocztaPolskaShippingProvider.php
3 months ago
PostLuxembourgShippingProvider.php
3 months ago
PostNLShippingProvider.php
3 months ago
PostNordShippingProvider.php
3 months ago
PostaMoldoveiShippingProvider.php
3 months ago
PostaRomanaShippingProvider.php
3 months ago
PosteItalianeShippingProvider.php
3 months ago
PosteSanMarinoShippingProvider.php
3 months ago
PostenNorgeBringShippingProvider.php
3 months ago
PurolatorShippingProvider.php
3 months ago
RoyalMailShippingProvider.php
3 months ago
RussianPostShippingProvider.php
3 months ago
SDAShippingProvider.php
3 months ago
SeurShippingProvider.php
3 months ago
SlovenskaPostaShippingProvider.php
3 months ago
SpeeDeeDeliveryShippingProvider.php
3 months ago
StarTrackShippingProvider.php
3 months ago
SwissPostShippingProvider.php
3 months ago
TollShippingProvider.php
3 months ago
UPSShippingProvider.php
3 months ago
USPSShippingProvider.php
3 months ago
UkrposhtaShippingProvider.php
3 months ago
UrgentCargusShippingProvider.php
3 months ago
YurticiKargoShippingProvider.php
3 months ago
ZasilkovnaShippingProvider.php
3 months ago
RoyalMailShippingProvider.php
230 lines
| 1 | <?php declare(strict_types=1); |
| 2 | |
| 3 | namespace Automattic\WooCommerce\Admin\Features\Fulfillments\Providers; |
| 4 | |
| 5 | use Automattic\WooCommerce\Admin\Features\Fulfillments\FulfillmentUtils; |
| 6 | |
| 7 | /** |
| 8 | * Royal Mail Shipping Provider class. |
| 9 | * |
| 10 | * Provides Royal Mail tracking number validation, supported countries, and tracking URL generation. |
| 11 | */ |
| 12 | class RoyalMailShippingProvider extends AbstractShippingProvider { |
| 13 | |
| 14 | /** |
| 15 | * Royal Mail tracking number patterns with enhanced service detection. |
| 16 | * |
| 17 | * @var array<string, array{patterns: array<int, string>, confidence: int}> |
| 18 | */ |
| 19 | private const TRACKING_PATTERNS = array( |
| 20 | 'GB' => array( // United Kingdom. |
| 21 | 'patterns' => array( |
| 22 | // UPU S10 international formats. |
| 23 | '/^[A-Z]{2}\d{9}GB$/', // International format: XX#########GB. |
| 24 | '/^[A-Z]{2}\d{7}GB$/', // Alternative international format: XX#######GB. |
| 25 | |
| 26 | // Domestic tracking formats. |
| 27 | '/^[A-Z]{1}\d{9}[A-Z]{1}$/', // Domestic format: X#########X. |
| 28 | '/^[A-Z]{2}\d{8}[A-Z]{2}$/', // Standard format: XX########XX. |
| 29 | '/^[A-Z]{2}\d{6}[A-Z]{2}$/', // Compact format: XX######XX. |
| 30 | |
| 31 | // Service-specific patterns. |
| 32 | '/^[A-Z]{4}\d{10}$/', // Special delivery format: XXXX##########. |
| 33 | '/^SD\d{8,12}$/', // Signed For service. |
| 34 | '/^SF\d{8,12}$/', // Special Delivery. |
| 35 | '/^RM\d{8,12}$/', // Royal Mail standard. |
| 36 | |
| 37 | // Digital tracking formats. |
| 38 | '/^\d{16}$/', // 16-digit returns label or digital. |
| 39 | '/^\d{14}$/', // 14-digit returns label. |
| 40 | '/^\d{13}$/', // 13-digit domestic/international tracking. |
| 41 | '/^\d{12}$/', // 12-digit domestic tracking. |
| 42 | '/^\d{11}$/', // 11-digit domestic tracking. |
| 43 | '/^\d{10}$/', // 10-digit legacy Parcelforce/RM. |
| 44 | '/^\d{9}$/', // 9-digit legacy Parcelforce/RM. |
| 45 | |
| 46 | // Parcelforce (Royal Mail Group). |
| 47 | '/^PF\d{8,12}$/', // Parcelforce Express. |
| 48 | '/^[A-Z]{2}\d{8}PF$/', // Parcelforce International. |
| 49 | '/^\d{13}$/', // Parcelforce Worldwide numeric. |
| 50 | |
| 51 | // International tracked services. |
| 52 | '/^IT\d{9}GB$/', // International Tracked. |
| 53 | '/^IE\d{9}GB$/', // International Economy. |
| 54 | '/^IS\d{9}GB$/', // International Standard. |
| 55 | |
| 56 | // Business services. |
| 57 | '/^BF\d{8,12}$/', // Business services. |
| 58 | '/^[A-Z]{3}\d{8,12}$/', // Three-letter business codes. |
| 59 | |
| 60 | // Legacy formats. |
| 61 | '/^[A-Z]{1}\d{8}[A-Z]{2}$/', // Legacy format: X########XX. |
| 62 | '/^[0-9]{9}[A-Z]{3}$/', // 9 digits + 3 letters. |
| 63 | ), |
| 64 | 'confidence' => 80, |
| 65 | ), |
| 66 | ); |
| 67 | |
| 68 | /** |
| 69 | * Get the key of the shipping provider. |
| 70 | * |
| 71 | * @return string |
| 72 | */ |
| 73 | public function get_key(): string { |
| 74 | return 'royal-mail'; |
| 75 | } |
| 76 | |
| 77 | /** |
| 78 | * Get the name of the shipping provider. |
| 79 | * |
| 80 | * @return string |
| 81 | */ |
| 82 | public function get_name(): string { |
| 83 | return 'Royal Mail'; |
| 84 | } |
| 85 | |
| 86 | /** |
| 87 | * Get the icon of the shipping provider. |
| 88 | * |
| 89 | * @return string |
| 90 | */ |
| 91 | public function get_icon(): string { |
| 92 | return esc_url( WC()->plugin_url() ) . '/assets/images/shipping_providers/royal-mail.png'; |
| 93 | } |
| 94 | |
| 95 | /** |
| 96 | * Get the countries this shipping provider can ship from. |
| 97 | * |
| 98 | * @return array List of country codes. |
| 99 | */ |
| 100 | public function get_shipping_from_countries(): array { |
| 101 | return array_keys( self::TRACKING_PATTERNS ); |
| 102 | } |
| 103 | |
| 104 | /** |
| 105 | * Get the countries this shipping provider can ship to. |
| 106 | * |
| 107 | * Royal Mail ships internationally, so we return a comprehensive list. |
| 108 | * |
| 109 | * @return array List of country codes. |
| 110 | */ |
| 111 | public function get_shipping_to_countries(): array { |
| 112 | return array( 'AD', 'AE', 'AF', 'AG', 'AI', 'AL', 'AM', 'AO', 'AQ', 'AR', 'AS', 'AT', 'AU', 'AW', 'AX', 'AZ', 'BA', 'BB', 'BD', 'BE', 'BF', 'BG', 'BH', 'BI', 'BJ', 'BL', 'BM', 'BN', 'BO', 'BQ', 'BR', 'BS', 'BT', 'BV', 'BW', 'BY', 'BZ', 'CA', 'CC', 'CD', 'CF', 'CG', 'CH', 'CI', 'CK', 'CL', 'CM', 'CN', 'CO', 'CR', 'CU', 'CV', 'CW', 'CX', 'CY', 'CZ', 'DE', 'DJ', 'DK', 'DM', 'DO', 'DZ', 'EC', 'EE', 'EG', 'EH', 'ER', 'ES', 'ET', 'FI', 'FJ', 'FK', 'FM', 'FO', 'FR', 'GA', 'GB', 'GD', 'GE', 'GF', 'GG', 'GH', 'GI', 'GL', 'GM', 'GN', 'GP', 'GQ', 'GR', 'GS', 'GT', 'GU', 'GW', 'GY', 'HK', 'HM', 'HN', 'HR', 'HT', 'HU', 'ID', 'IE', 'IL', 'IM', 'IN', 'IO', 'IQ', 'IR', 'IS', 'IT', 'JE', 'JM', 'JO', 'JP', 'KE', 'KG', 'KH', 'KI', 'KM', 'KN', 'KP', 'KR', 'KW', 'KY', 'KZ', 'LA', 'LB', 'LC', 'LI', 'LK', 'LR', 'LS', 'LT', 'LU', 'LV', 'LY', 'MA', 'MC', 'MD', 'ME', 'MF', 'MG', 'MH', 'MK', 'ML', 'MM', 'MN', 'MO', 'MP', 'MQ', 'MR', 'MS', 'MT', 'MU', 'MV', 'MW', 'MX', 'MY', 'MZ', 'NA', 'NC', 'NE', 'NF', 'NG', 'NI', 'NL', 'NO', 'NP', 'NR', 'NU', 'NZ', 'OM', 'PA', 'PE', 'PF', 'PG', 'PH', 'PK', 'PL', 'PM', 'PN', 'PR', 'PS', 'PT', 'PW', 'PY', 'QA', 'RE', 'RO', 'RS', 'RU', 'RW', 'SA', 'SB', 'SC', 'SD', 'SE', 'SG', 'SH', 'SI', 'SJ', 'SK', 'SL', 'SM', 'SN', 'SO', 'SR', 'SS', 'ST', 'SV', 'SX', 'SY', 'SZ', 'TC', 'TD', 'TF', 'TG', 'TH', 'TJ', 'TK', 'TL', 'TM', 'TN', 'TO', 'TR', 'TT', 'TV', 'TW', 'TZ', 'UA', 'UG', 'UM', 'US', 'UY', 'UZ', 'VA', 'VC', 'VE', 'VG', 'VI', 'VN', 'VU', 'WF', 'WS', 'YE', 'YT', 'ZA', 'ZM', 'ZW' ); |
| 113 | } |
| 114 | |
| 115 | /** |
| 116 | * Get the tracking URL for a given tracking number. |
| 117 | * |
| 118 | * @param string $tracking_number The tracking number. |
| 119 | * @return string The tracking URL. |
| 120 | */ |
| 121 | public function get_tracking_url( string $tracking_number ): string { |
| 122 | return 'https://www.royalmail.com/track-your-item#/tracking-results/' . rawurlencode( $tracking_number ); |
| 123 | } |
| 124 | |
| 125 | /** |
| 126 | * Validate tracking number against country-specific patterns. |
| 127 | * |
| 128 | * @param string $tracking_number The tracking number to validate. |
| 129 | * @param string $country_code The country code for the shipment. |
| 130 | * @return bool True if valid, false otherwise. |
| 131 | */ |
| 132 | private function validate_country_pattern( string $tracking_number, string $country_code ): bool { |
| 133 | if ( ! isset( self::TRACKING_PATTERNS[ $country_code ] ) ) { |
| 134 | return false; |
| 135 | } |
| 136 | |
| 137 | foreach ( self::TRACKING_PATTERNS[ $country_code ]['patterns'] as $pattern ) { |
| 138 | if ( preg_match( $pattern, $tracking_number ) ) { |
| 139 | return true; |
| 140 | } |
| 141 | } |
| 142 | return false; |
| 143 | } |
| 144 | |
| 145 | /** |
| 146 | * Try to parse a Royal Mail tracking number. |
| 147 | * |
| 148 | * @param string $tracking_number The tracking number to parse. |
| 149 | * @param string $shipping_from The country code of the shipping origin. |
| 150 | * @param string $shipping_to The country code of the shipping destination. |
| 151 | * @return array|null An array with 'url' and 'ambiguity_score' if valid, null otherwise. |
| 152 | */ |
| 153 | public function try_parse_tracking_number( |
| 154 | string $tracking_number, |
| 155 | string $shipping_from, |
| 156 | string $shipping_to |
| 157 | ): ?array { |
| 158 | if ( empty( $tracking_number ) || empty( $shipping_from ) || empty( $shipping_to ) ) { |
| 159 | return null; |
| 160 | } |
| 161 | |
| 162 | $normalized = strtoupper( preg_replace( '/\s+/', '', $tracking_number ) ); // Normalize input. |
| 163 | if ( empty( $normalized ) ) { |
| 164 | return null; |
| 165 | } |
| 166 | |
| 167 | $shipping_from = strtoupper( $shipping_from ); |
| 168 | $shipping_to = strtoupper( $shipping_to ); |
| 169 | |
| 170 | // Check if shipping from UK. |
| 171 | if ( 'GB' !== $shipping_from ) { |
| 172 | return null; |
| 173 | } |
| 174 | |
| 175 | // Check country-specific patterns with enhanced validation. |
| 176 | if ( $this->validate_country_pattern( $normalized, $shipping_from ) ) { |
| 177 | $confidence = self::TRACKING_PATTERNS[ $shipping_from ]['confidence']; |
| 178 | |
| 179 | // Apply UPU S10 validation for international formats. |
| 180 | if ( preg_match( '/^[A-Z]{2}\d{7,9}GB$/', $normalized ) ) { |
| 181 | if ( FulfillmentUtils::check_s10_upu_format( $normalized ) ) { |
| 182 | $confidence = min( 98, $confidence + 8 ); // Strong boost for valid UPU. |
| 183 | } |
| 184 | } |
| 185 | |
| 186 | // Apply check digit validation for numeric formats. |
| 187 | if ( preg_match( '/^\d{11,16}$/', $normalized ) ) { |
| 188 | if ( FulfillmentUtils::validate_mod10_check_digit( $normalized ) ) { |
| 189 | $confidence = min( 95, $confidence + 5 ); // Boost for valid check digit. |
| 190 | } |
| 191 | } |
| 192 | |
| 193 | // Service-specific confidence boosts. |
| 194 | if ( preg_match( '/^(SD|SF)\d+/', $normalized ) ) { |
| 195 | $confidence = min( 96, $confidence + 6 ); // Special Delivery/Signed For. |
| 196 | } elseif ( preg_match( '/^PF\d+/', $normalized ) ) { |
| 197 | $confidence = min( 94, $confidence + 4 ); // Parcelforce. |
| 198 | } elseif ( preg_match( '/^(IT|IE|IS)\d+GB$/', $normalized ) ) { |
| 199 | $confidence = min( 95, $confidence + 5 ); // International tracked services. |
| 200 | } elseif ( preg_match( '/^(RM|BF)\d+/', $normalized ) ) { |
| 201 | $confidence = min( 92, $confidence + 3 ); // Standard Royal Mail/Business. |
| 202 | } |
| 203 | |
| 204 | // Boost confidence for domestic shipments. |
| 205 | if ( 'GB' === $shipping_to ) { |
| 206 | $confidence = min( 95, $confidence + 8 ); |
| 207 | } |
| 208 | |
| 209 | // Boost confidence for common destinations (Europe). |
| 210 | $european_destinations = array( 'FR', 'DE', 'ES', 'IT', 'NL', 'BE', 'IE', 'AT', 'CH', 'PT', 'DK', 'SE', 'NO' ); |
| 211 | if ( in_array( $shipping_to, $european_destinations, true ) ) { |
| 212 | $confidence = min( 95, $confidence + 3 ); |
| 213 | } |
| 214 | |
| 215 | // Boost for other common destinations. |
| 216 | $common_destinations = array( 'US', 'CA', 'AU', 'NZ', 'JP', 'SG', 'HK' ); |
| 217 | if ( in_array( $shipping_to, $common_destinations, true ) ) { |
| 218 | $confidence = min( 93, $confidence + 2 ); |
| 219 | } |
| 220 | |
| 221 | return array( |
| 222 | 'url' => $this->get_tracking_url( $normalized ), |
| 223 | 'ambiguity_score' => $confidence, |
| 224 | ); |
| 225 | } |
| 226 | |
| 227 | return null; |
| 228 | } |
| 229 | } |
| 230 |