PluginProbe
ووسلام – همگام سازی ووکامرس و باسلام / 1.10.19
ووسلام – همگام سازی ووکامرس و باسلام v1.10.19
1.10.19 1.10.20 1.10.18 1.10.17 1.10.15 1.10.14 1.10.13 1.10.12 1.10.10 1.10.9 1.10.8 1.10.7 1.10.6 1.10.5 1.10.4 1.10.3 1.10.2 1.10.1 1.10.0 1.9.2 1.9.1 1.9.0 1.8.8 1.8.5 1.8.6 All 53 releases
← All changes | includes/Utilities/GetProvincesData.php +193 -41 1.8.61.10.19 View file →
@@ -39,10 +39,11 @@
39 39 ];
40 40
41 41 public static function getCodeByName(string $persianName): ?string
42 42 {
43 + $needle = self::normalizePersian($persianName);
43 44 foreach (self::$provinces as $code => $name) {
44 - if (trim($name) === trim($persianName)) return $code;
45 + if (self::normalizePersian($name) === $needle) return $code;
45 46 }
46 47
47 48 return null;
48 49 }
@@ -63,12 +64,74 @@
63 64 );
64 65 }
65 66
66 67 /**
67 - * Set order address with PWS compatibility
68 - * If PWS is active, sets Tapin state/city IDs as meta data
69 - * Otherwise, uses normal WooCommerce state/city fields.
68 + * Register IR states with WooCommerce when no other plugin has done so.
69 + * WooCommerce won't persist `billing_state` for a country that has registered
70 + * states unless the value matches a registered key — and it ships zero states
71 + * for IR. Without this filter, set_billing_state('یزد') is silently dropped
72 + * on stores that have no Persian shipping/state plugin active.
73 + *
74 + * Safe to call multiple times; if another plugin already provides IR states
75 + * we leave them alone.
70 76 */
77 + public static function registerIranStatesFilter(): void
78 + {
79 + static $registered = false;
80 + if ($registered) return;
81 + $registered = true;
82 +
83 + add_filter('woocommerce_states', function ($states) {
84 + if (!empty($states['IR'])) return $states;
85 +
86 + $states['IR'] = self::$provinces;
87 +
88 + return $states;
89 + }, 5);
90 + }
91 +
92 + /**
93 + * Whether PWS is running in Tapin mode (state IDs come from tapin.json, 1..31).
94 + * Otherwise PWS falls back to the `state_city` taxonomy term IDs.
95 + */
96 + private static function isTapinEnabled(): bool
97 + {
98 + if (!class_exists('\PWS_Tapin')) return false;
99 + if (!method_exists('\PWS_Tapin', 'is_enable')) return false;
100 +
101 + return (bool) call_user_func(['\PWS_Tapin', 'is_enable']);
102 + }
103 +
104 + /**
105 + * Normalize Persian/Arabic text so name comparisons survive small variations
106 + * (Arabic Yeh/Kaf, ZWNJ / half-space, repeated whitespace, BOM, etc.).
107 + */
108 + private static function normalizePersian(?string $value): string
109 + {
110 + if ($value === null) return '';
111 +
112 + $value = str_replace(
113 + ["\xEF\xBB\xBF", "\xE2\x80\x8C", "\xC2\xA0", 'ي', 'ك', 'ﮐ', 'ﮏ', 'ﯼ', 'ﯽ'],
114 + ['', ' ', ' ', 'ی', 'ک', 'ک', 'ک', 'ی', 'ی'],
115 + $value
116 + );
117 +
118 + $value = preg_replace('/\s+/u', ' ', $value);
119 +
120 + return trim((string) $value);
121 + }
122 +
123 + /**
124 + * Set order address with PWS compatibility.
125 + *
126 + * - When PWS+Tapin is active: store Tapin numeric state/city IDs in both the
127 + * WC billing_state/billing_city fields (so WC's state lookup resolves the
128 + * Persian name correctly) AND in PWS's `_billing_state_id`/`_billing_city_id`
129 + * meta (which Tapin label submission reads).
130 + * - When PWS is active without Tapin: resolve the `state_city` taxonomy
131 + * term IDs and use those.
132 + * - Otherwise fall back to plain Persian names.
133 + */
71 134 public static function setOrderAddress($order, $addressData, $type = 'billing')
72 135 {
73 136 if (!$order || !isset($addressData['province']) || !isset($addressData['city'])) {
74 137 return;
@@ -73,45 +136,59 @@
73 136 if (!$order || !isset($addressData['province']) || !isset($addressData['city'])) {
74 137 return;
75 138 }
76 139
77 - $province = trim($addressData['province']);
78 - $city = trim($addressData['city']);
140 + $province = trim((string) $addressData['province']);
141 + $city = trim((string) $addressData['city']);
79 142
143 + if ($province === '' && $city === '') {
144 + return;
145 + }
146 +
147 + // WooCommerce validates billing_state against the registered keys of
148 + // states['IR'], so the state value must be a valid KEY (numeric ID in
149 + // PWS, 3-letter code without PWS). The city field has no such
150 + // validation, so we always keep the human-readable Persian name there —
151 + // otherwise the admin order screen would render the raw term ID.
152 + $stateValue = $province;
153 + $cityValue = $city;
154 + $stateMetaId = null;
155 + $cityMetaId = null;
156 +
80 157 if (self::isPWSActive()) {
81 - // PWS is active - set Tapin IDs
82 - $state_id = self::getTapinStateIdByName($province);
83 - $city_id = self::getTapinCityIdByName($city, $state_id);
158 + if (self::isTapinEnabled()) {
159 + $stateMetaId = self::getTapinStateIdByName($province);
160 + $cityMetaId = self::getTapinCityIdByName($city, $stateMetaId);
161 + } else {
162 + $stateMetaId = self::getPwsStateTermIdByName($province);
163 + $cityMetaId = self::getPwsCityTermIdByName($city, $stateMetaId);
164 + }
84 165
85 - if ($state_id) {
86 - $order->update_meta_data("_{$type}_state_id", $state_id);
87 - // Also set the state name for WooCommerce
88 - if ($type === 'billing') {
89 - $order->set_billing_state($province);
90 - } else {
91 - $order->set_shipping_state($province);
92 - }
166 + if ($stateMetaId) $stateValue = (string) $stateMetaId;
167 + } else {
168 + // No PWS — make sure WC has IR states registered so it doesn't drop
169 + // the billing_state on save, then store the code instead of the name.
170 + self::registerIranStatesFilter();
171 + $code = self::getCodeByName($province);
172 + if ($code) {
173 + $stateValue = $code;
93 174 }
175 + }
94 176
95 - if ($city_id) {
96 - $order->update_meta_data("_{$type}_city_id", $city_id);
97 - // Also set the city name for WooCommerce
98 - if ($type === 'billing') {
99 - $order->set_billing_city($city);
100 - } else {
101 - $order->set_shipping_city($city);
102 - }
103 - }
177 + if ($type === 'billing') {
178 + $order->set_billing_state($stateValue);
179 + $order->set_billing_city($cityValue);
104 180 } else {
105 - // PWS is not active - use normal WooCommerce fields
106 - if ($type === 'billing') {
107 - $order->set_billing_state($province);
108 - $order->set_billing_city($city);
109 - } else {
110 - $order->set_shipping_state($province);
111 - $order->set_shipping_city($city);
112 - }
181 + $order->set_shipping_state($stateValue);
182 + $order->set_shipping_city($cityValue);
113 183 }
184 +
185 + if ($stateMetaId) {
186 + $order->update_meta_data("_{$type}_state_id", $stateMetaId);
187 + }
188 + if ($cityMetaId) {
189 + $order->update_meta_data("_{$type}_city_id", $cityMetaId);
190 + }
114 191 }
115 192
116 193 /**
117 194 * Get Tapin state ID by Persian name.
@@ -122,11 +199,15 @@
122 199
123 200 $states = call_user_func(['\PWS_Tapin', 'states']);
124 201 if (!$states) return null;
125 202
126 - $stateName = trim($stateName);
203 + $needle = self::normalizePersian($stateName);
204 + if ($needle === '') return null;
205 +
127 206 foreach ($states as $state_id => $state_title) {
128 - if (trim($state_title) === $stateName) return (int) $state_id;
207 + if (self::normalizePersian((string) $state_title) === $needle) {
208 + return (int) $state_id;
209 + }
129 210 }
130 211
131 212 return null;
132 213 }
@@ -132,19 +213,90 @@
132 213 }
133 214
134 215 /**
135 216 * Get Tapin city ID by Persian name.
217 + * If $stateId is provided the search is constrained to that state — this
218 + * avoids the failure mode where a null state_id lets PWS_Tapin::cities()
219 + * search across ALL cities and return the wrong match.
136 220 */
137 221 private static function getTapinCityIdByName($cityName, $stateId = null): ?int
138 222 {
139 223 if (!class_exists('\PWS_Tapin')) return null;
140 224
141 - $cities = call_user_func(['\PWS_Tapin', 'cities'], $stateId);
142 - if (!$cities) return null;
225 + $needle = self::normalizePersian($cityName);
226 + if ($needle === '') return null;
143 227
144 - $cityName = trim($cityName);
145 - foreach ($cities as $city_id => $city_title) {
146 - if (trim($city_title) === $cityName) return (int) $city_id;
228 + if ($stateId) {
229 + $cities = call_user_func(['\PWS_Tapin', 'cities'], $stateId);
230 + if (!$cities) return null;
231 +
232 + foreach ($cities as $city_id => $city_title) {
233 + if (self::normalizePersian((string) $city_title) === $needle) {
234 + return (int) $city_id;
235 + }
236 + }
237 +
238 + return null;
239 + }
240 +
241 + return null;
242 + }
243 +
244 + /**
245 + * Resolve a `state_city` taxonomy term ID for the given province name
246 + * (used when PWS is active but Tapin is not).
247 + */
248 + private static function getPwsStateTermIdByName($stateName): ?int
249 + {
250 + if (!taxonomy_exists('state_city')) return null;
251 +
252 + $needle = self::normalizePersian($stateName);
253 + if ($needle === '') return null;
254 +
255 + $terms = get_terms([
256 + 'taxonomy' => 'state_city',
257 + 'hide_empty' => false,
258 + 'parent' => 0,
259 + ]);
260 +
261 + if (is_wp_error($terms) || empty($terms)) return null;
262 +
263 + foreach ($terms as $term) {
264 + if (self::normalizePersian((string) $term->name) === $needle) {
265 + return (int) $term->term_id;
266 + }
267 + }
268 +
269 + return null;
270 + }
271 +
272 + /**
273 + * Resolve a `state_city` taxonomy term ID for the given city name.
274 + * Scoped to the province term when available.
275 + */
276 + private static function getPwsCityTermIdByName($cityName, $stateTermId = null): ?int
277 + {
278 + if (!taxonomy_exists('state_city')) return null;
279 +
280 + $needle = self::normalizePersian($cityName);
281 + if ($needle === '') return null;
282 +
283 + $args = [
284 + 'taxonomy' => 'state_city',
285 + 'hide_empty' => false,
286 + ];
287 +
288 + if ($stateTermId) {
289 + $args['parent'] = $stateTermId;
290 + }
291 +
292 + $terms = get_terms($args);
293 + if (is_wp_error($terms) || empty($terms)) return null;
294 +
295 + foreach ($terms as $term) {
296 + if (self::normalizePersian((string) $term->name) === $needle) {
297 + return (int) $term->term_id;
298 + }
147 299 }
148 300
149 301 return null;
150 302 }