| @@ -153,8 +153,96 @@ | ||
| 153 | 153 | ); |
| 154 | 154 | } |
| 155 | 155 | |
| 156 | 156 | /** |
| 157 | + * Assemble the template-facing `store` section from the POS store object. | |
| 158 | + * | |
| 159 | + * Every field is read through get_store_value(), so partial store objects — | |
| 160 | + * including the bare \stdClass used for orders whose store has since been | |
| 161 | + * deleted — fall back field by field instead of fataling. Blank values count | |
| 162 | + * as "not set" for the fields that accept a fallback, which is how both | |
| 163 | + * builders have always treated empty store settings. | |
| 164 | + * | |
| 165 | + * @param array<string,mixed> $fallbacks Optional per-field fallbacks. Recognised keys: | |
| 166 | + * `id` (int, default 0), | |
| 167 | + * `name` (string, default site title), | |
| 168 | + * `opening_hours` (array, default empty — renders null), | |
| 169 | + * `opening_hours_notes`, `personal_notes`, | |
| 170 | + * `policies_and_conditions` and `footer_imprint` | |
| 171 | + * (string|null, default null). | |
| 172 | + * | |
| 173 | + * @return array<string,mixed> | |
| 174 | + */ | |
| 175 | + public function build_store_section( array $fallbacks = array() ): array { | |
| 176 | + $address = array( | |
| 177 | + 'address_1' => (string) $this->get_store_value( 'get_store_address', '' ), | |
| 178 | + 'address_2' => (string) $this->get_store_value( 'get_store_address_2', '' ), | |
| 179 | + 'city' => (string) $this->get_store_value( 'get_store_city', '' ), | |
| 180 | + 'state' => (string) $this->get_store_value( 'get_store_state', '' ), | |
| 181 | + 'postcode' => (string) $this->get_store_value( 'get_store_postcode', '' ), | |
| 182 | + 'country' => (string) $this->get_store_value( 'get_store_country', '' ), | |
| 183 | + ); | |
| 184 | + | |
| 185 | + $tax_ids = $this->get_store_value( 'get_tax_ids', array() ); | |
| 186 | + if ( ! \is_array( $tax_ids ) ) { | |
| 187 | + $tax_ids = array(); | |
| 188 | + } | |
| 189 | + | |
| 190 | + // Resolved lazily so the bloginfo filters only run when they are actually needed. | |
| 191 | + $name = $this->resolve_optional_text( 'get_name', null ); | |
| 192 | + if ( null === $name ) { | |
| 193 | + $name = isset( $fallbacks['name'] ) ? (string) $fallbacks['name'] : (string) get_bloginfo( 'name' ); | |
| 194 | + } | |
| 195 | + | |
| 196 | + $store = array( | |
| 197 | + 'id' => (int) $this->get_store_value( 'get_id', $fallbacks['id'] ?? 0 ), | |
| 198 | + 'name' => $name, | |
| 199 | + // Structured address parts mirror customer.billing_address — templates that | |
| 200 | + // want country-specific layouts compose from these. address_lines[] is the | |
| 201 | + // pre-formatted default for templates that just iterate, composed via | |
| 202 | + // WC_Countries::get_formatted_address() so per-country layouts are honoured. | |
| 203 | + 'address' => $address, | |
| 204 | + 'address_lines' => self::compose_address_lines( $address ), | |
| 205 | + 'tax_ids' => self::with_store_tax_id_labels( $tax_ids, $this->resolve_locale() ), | |
| 206 | + 'phone' => (string) $this->get_store_value( 'get_phone', '' ), | |
| 207 | + 'email' => (string) $this->get_store_value( 'get_email', '' ), | |
| 208 | + 'logo' => Store_Logo_Resolver::resolve( $this->pos_store ), | |
| 209 | + ); | |
| 210 | + | |
| 211 | + $opening_hours = $this->get_store_value( 'get_opening_hours', array() ); | |
| 212 | + $has_structured_hours = \is_array( $opening_hours ) && ! empty( $opening_hours ); | |
| 213 | + $has_legacy_hours = \is_string( $opening_hours ) && '' !== trim( $opening_hours ); | |
| 214 | + if ( ! $has_structured_hours && ! $has_legacy_hours ) { | |
| 215 | + $opening_hours = $fallbacks['opening_hours'] ?? array(); | |
| 216 | + $has_structured_hours = \is_array( $opening_hours ) && ! empty( $opening_hours ); | |
| 217 | + } | |
| 218 | + | |
| 219 | + if ( $has_structured_hours ) { | |
| 220 | + // Same locale the order timestamps use, so both render in one convention. | |
| 221 | + $hours_locale = $this->resolve_locale(); | |
| 222 | + $store['opening_hours'] = Opening_Hours_Formatter::format_compact( $opening_hours, $hours_locale ); | |
| 223 | + $store['opening_hours_vertical'] = Opening_Hours_Formatter::format_vertical( $opening_hours, $hours_locale ); | |
| 224 | + $store['opening_hours_inline'] = Opening_Hours_Formatter::format_inline( $opening_hours, $hours_locale ); | |
| 225 | + } elseif ( $has_legacy_hours ) { | |
| 226 | + // Legacy free-text hours have no per-day structure to reformat. | |
| 227 | + $store['opening_hours'] = $opening_hours; | |
| 228 | + $store['opening_hours_vertical'] = null; | |
| 229 | + $store['opening_hours_inline'] = null; | |
| 230 | + } else { | |
| 231 | + $store['opening_hours'] = null; | |
| 232 | + $store['opening_hours_vertical'] = null; | |
| 233 | + $store['opening_hours_inline'] = null; | |
| 234 | + } | |
| 235 | + | |
| 236 | + $store['opening_hours_notes'] = $this->resolve_optional_text( 'get_opening_hours_notes', $fallbacks['opening_hours_notes'] ?? null ); | |
| 237 | + $store['personal_notes'] = $this->resolve_optional_text( 'get_personal_notes', $fallbacks['personal_notes'] ?? null ); | |
| 238 | + $store['policies_and_conditions'] = $this->resolve_optional_text( 'get_policies_and_conditions', $fallbacks['policies_and_conditions'] ?? null ); | |
| 239 | + $store['footer_imprint'] = $this->resolve_optional_text( 'get_footer_imprint', $fallbacks['footer_imprint'] ?? null ); | |
| 240 | + | |
| 241 | + return $store; | |
| 242 | + } | |
| 243 | + | |
| 244 | + /** | |
| 157 | 245 | * Build price, currency, and locale presentation hints for renderers. |
| 158 | 246 | * |
| 159 | 247 | * @param string $currency Currency code. |
| 160 | 248 | * @param bool|null $prices_include_tax Optional precomputed prices-include-tax flag. |
| @@ -244,22 +332,21 @@ | ||
| 244 | 332 | * @param string $locale Receipt locale. |
| 245 | 333 | * @return array<int,array<string,mixed>> |
| 246 | 334 | */ |
| 247 | 335 | public static function with_store_tax_id_labels( array $tax_ids, string $locale = '' ): array { |
| 248 | - $labels = Receipt_I18n_Labels::get_labels( $locale ); | |
| 336 | + return Receipt_Sections::label_tax_ids( $tax_ids, 'store', $locale ); | |
| 337 | + } | |
| 249 | 338 | |
| 250 | - return array_map( | |
| 251 | - static function ( array $tax_id ) use ( $labels ): array { | |
| 252 | - if ( ! empty( $tax_id['label'] ) ) { | |
| 253 | - return $tax_id; | |
| 254 | - } | |
| 339 | + /** | |
| 340 | + * Resolve an optional free-text store field, treating a blank value as unset. | |
| 341 | + * | |
| 342 | + * @param string $getter Store getter method. | |
| 343 | + * @param string|null $fallback Value used when the store has nothing set. | |
| 344 | + * | |
| 345 | + * @return string|null | |
| 346 | + */ | |
| 347 | + private function resolve_optional_text( string $getter, ?string $fallback ): ?string { | |
| 348 | + $value = (string) $this->get_store_value( $getter, '' ); | |
| 255 | 349 | |
| 256 | - $type = isset( $tax_id['type'] ) ? (string) $tax_id['type'] : 'other'; | |
| 257 | - $key = 'store_tax_id_label_' . $type; | |
| 258 | - $tax_id['label'] = $labels[ $key ] ?? $labels['store_tax_id_label_other']; | |
| 259 | - | |
| 260 | - return $tax_id; | |
| 261 | - }, | |
| 262 | - $tax_ids | |
| 263 | - ); | |
| 350 | + return '' !== $value ? $value : $fallback; | |
| 264 | 351 | } |
| 265 | 352 | } |