diff --git a/.github/workflows/reusable_release.yml b/.github/workflows/reusable_release.yml index b2539a0..6054b4d 100644 --- a/.github/workflows/reusable_release.yml +++ b/.github/workflows/reusable_release.yml @@ -124,6 +124,7 @@ jobs: name: Publish runs-on: ubuntu-latest needs: release + if: ${{ inputs.dry_run == false }} steps: - name: Download Package uses: actions/download-artifact@v4 diff --git a/assets/scripts/@types/index.d.ts b/assets/scripts/@types/index.d.ts index 99ea1f8..ae9d797 100644 --- a/assets/scripts/@types/index.d.ts +++ b/assets/scripts/@types/index.d.ts @@ -3,6 +3,7 @@ declare global { const _: _.UnderscoreStatic; interface Window { + wc_address_i18n_params: any; wp: { template: (id: string) => _.CompiledTemplate; }; diff --git a/assets/scripts/frontend/controllers/address-page.controller.ts b/assets/scripts/frontend/controllers/address-page.controller.ts index ff9b59e..fdb4a35 100644 --- a/assets/scripts/frontend/controllers/address-page.controller.ts +++ b/assets/scripts/frontend/controllers/address-page.controller.ts @@ -1,25 +1,77 @@ const $ = jQuery; export default class AddressPageController { - private $inputs: JQuery; + private selector = '.entity-type-control input'; + private locale: Record; public init(): void { - this.$inputs = $('.entity-type-control input[type="radio"]'); + this.locale = JSON.parse(window.wc_address_i18n_params.locale_fields); } public finalize(): void { - this.$inputs.on('click', ({ currentTarget }) => - this.toggleFields($(currentTarget)), + $(document.body).on('change refresh', this.selector, (e) => { + console.log('change refresh'); + this.toggleEntityType($(e.target)); + }); + + $(document.body).on('country_to_state_changing', () => { + window.setTimeout( + () => this.toggleEntityType($(`${this.selector}:checked`)), + 100, + ); + console.log('country_to_state_changing'); + }); + } + + private toggleEntityType($input: JQuery): void { + const isCompany = $input.val() === 'company'; + + console.log($input.is(':checked'), $input.val()); + + this.isRequired($('.entity-type-toggle'), isCompany); + } + + private isRequired( + $field: JQuery, + required: boolean, + ): void { + $field.find('input').prop({ + 'aria-required': required, + disabled: !required, + }); + + if (required) { + $field.find('label .optional').remove(); + $field.addClass('shown validate-required'); + + if ($field.find('label .required').length === 0) { + $field + .find('label') + .append( + `*`, + ); + } + + return; + } + + $field.find('label .required').remove(); + $field.removeClass( + 'shown validate-required woocommerce-invalid woocommerce-invalid-required-field', ); - this.toggleFields(this.$inputs.filter(':checked')); + + if ($field.find('label .optional').length === 0) { + $field + .find('label') + .append( + `${window.wc_address_i18n_params.i18n_optional_text}`, + ); + } } private toggleFields($toggle: JQuery): void { const isPerson = $toggle.attr('value') === 'person'; - $('.hide-if-person').toggleClass('shown', !isPerson).find('input').prop({ - required: !isPerson, - disabled: isPerson, - }); + $('.hide-if-person').toggleClass('shown', !isPerson); } } diff --git a/assets/styles/components/_billing_fields.scss b/assets/styles/components/_billing_fields.scss index 90296ca..285a6de 100644 --- a/assets/styles/components/_billing_fields.scss +++ b/assets/styles/components/_billing_fields.scss @@ -15,11 +15,11 @@ } } - .hide-if-person { - display: none; + .entity-type-toggle { + display: none !important; &.shown { - display: block; + display: block !important; } } } diff --git a/composer.json b/composer.json index b6aa19b..cb4c03b 100644 --- a/composer.json +++ b/composer.json @@ -20,13 +20,19 @@ }, "autoload": { "psr-4": { - "Oblak\\WooCommerce\\Serbian_Addons\\": "lib" + "Oblak\\WCSRB\\": "lib/" }, + "classmap": [ + "lib/Admin/", + "lib/Gateway/", + "lib/QR/" + ], "files": [ - "lib/Utils/wcsrb-core.php", - "lib/Utils/wcsrb-helpers.php", - "lib/Utils/wcsrb-payment-slip.php", - "lib/Utils/wcsrb-settings.php" + "lib/Functions/wcsrb-address-field-fns.php", + "lib/Functions/wcsrb-core.php", + "lib/Functions/wcsrb-helpers.php", + "lib/Functions/wcsrb-payment-slip.php", + "lib/Functions/wcsrb-settings.php" ] }, "config": { diff --git a/config/assets.php b/config/assets.php index e0e6888..675e0eb 100644 --- a/config/assets.php +++ b/config/assets.php @@ -23,6 +23,6 @@ 'base_uri' => plugins_url( 'dist', WCRS_PLUGIN_BASE ), 'id' => 'wcrs', 'manifest' => 'assets.php', - 'priority' => 50, + 'priority' => 500, 'version' => WCRS_VERSION, ); diff --git a/lib/Admin/Edit_User_Controller.php b/lib/Admin/Edit_User_Controller.php new file mode 100644 index 0000000..24bec75 --- /dev/null +++ b/lib/Admin/Edit_User_Controller.php @@ -0,0 +1,60 @@ + array( + 'description' => '', + 'label' => \__( 'Customer type', 'serbian-addons-for-woocommerce' ), + 'options' => \wcsrb_get_entity_types(), + 'type' => 'select', + ), + 'billing_company' => $fields['billing']['fields']['billing_company'], + 'billing_mb' => array( + 'description' => '', + 'label' => \__( 'Company Number', 'serbian-addons-for-woocommerce' ), + 'type' => 'text', + ), + 'billing_pib' => array( + 'description' => '', + 'label' => \__( 'Tax Number', 'serbian-addons-for-woocommerce' ), + 'type' => 'text', + ), + + ), + \array_slice( $fields['billing']['fields'], $company ), + ); + //phpcs:enable SlevomatCodingStandard.Arrays.AlphabeticallySortedByKeys.IncorrectKeyOrder + + return $fields; + } +} diff --git a/lib/Serbian_WooCommerce.php b/lib/App.php similarity index 83% rename from lib/Serbian_WooCommerce.php rename to lib/App.php index 220a03b..129bbc1 100644 --- a/lib/Serbian_WooCommerce.php +++ b/lib/App.php @@ -5,8 +5,10 @@ * @package Serbian Addons for WooCommerce */ -namespace Oblak\WooCommerce\Serbian_Addons; +namespace Oblak\WCSRB; +use Oblak\WCSRB\Services\Field_Validator; +use Oblak\WooCommerce\Serbian_Addons as Legacy; use Oblak\WP\Decorators\Action; use Oblak\WP\Decorators\Filter; use Oblak\WP\Traits\Hook_Processor_Trait; @@ -16,7 +18,7 @@ /** * Main plugin class */ -class Serbian_WooCommerce { +class App { use Hook_Processor_Trait; use Settings_API_Methods; use Singleton; @@ -29,6 +31,13 @@ class Serbian_WooCommerce { */ public string $version = WCRS_VERSION; + /** + * Field validator instance. + * + * @var Field_Validator + */ + protected Field_Validator $validator; + /** * Private constructor */ @@ -43,11 +52,12 @@ protected function __construct() { */ protected function get_dependencies(): array { return array( - Admin\Admin_Core::class, - Core\Template_Extender::class, - Checkout\Field_Customizer::class, - Checkout\Field_Validator::class, - Order\Field_Display::class, + Admin\Edit_User_Controller::class, + Core\Address_Display_Controller::class, + Core\Address_Field_Controller::class, + Core\Address_Validate_Controller::class, + Utils\Template_Extender::class, + Legacy\Admin\Admin_Core::class, ); } @@ -63,9 +73,9 @@ public function run_hooks() { */ #[Action( tag: 'plugins_loaded', priority: 1000 )] public function on_plugins_loaded() { - Core\Installer::instance()->init(); + Utils\Installer::instance()->init(); - $s = \load_plugin_textdomain( + \load_plugin_textdomain( domain: 'serbian-addons-for-woocommerce', plugin_rel_path: \dirname( WCRS_PLUGIN_BASE ) . '/languages', ); @@ -78,7 +88,7 @@ public function on_plugins_loaded() { public function load_plugin_settings() { try { $this->load_options( 'wcsrb_settings' ); - } catch ( \Exception ) { + } catch ( \Exception | \Error ) { \wc_get_logger()->critical( 'Failed to load plugin settings', array( @@ -133,7 +143,7 @@ public function declare_hpos_compatibility() { */ #[Filter( tag: 'woocommerce_payment_gateways', priority: 50 )] public function add_payment_gateways( $gateways ) { - $gateways[] = Gateway\Gateway_Payment_Slip::class; + $gateways[] = Legacy\Gateway\Gateway_Payment_Slip::class; return $gateways; } @@ -173,4 +183,13 @@ public function check_asset_necessity( bool $load, string $script ) { default => $load, }; } + + /** + * Gets the field validator instance. + * + * @return Field_Validator + */ + public function validator(): Field_Validator { + return $this->validator ??= new Field_Validator(); + } } diff --git a/lib/Checkout/Field_Customizer.php b/lib/Checkout/Field_Customizer.php deleted file mode 100644 index f97e8fc..0000000 --- a/lib/Checkout/Field_Customizer.php +++ /dev/null @@ -1,208 +0,0 @@ -get_settings( 'core', 'enabled_customer_types' ); - - $fields = $this->maybe_remove_fields( $fields ); - - $fields['billing_type'] = $this->add_billing_type_field( $enabled_type ); - - $fields = \array_merge( - $fields, - $this->maybe_add_company_fields( $enabled_type ), - ); - - // If the billing type is not both or company, remove the company field. - if ( ! \in_array( $enabled_type, array( 'both', 'company' ), true ) ) { - unset( $fields['billing_company'] ); - } else { // Else, addin some extra data. - $fields['billing_company']['class'][] = 'hide-if-person'; - $fields['billing_company']['required'] = true; - } - - return $fields; - } - - /** - * Modifies shipping fields to remove the unneded fields. - * - * @param array $fields Shipping fields. - * @return array Modified shipping fields - */ - #[Filter( tag: 'woocommerce_shipping_fields', priority: 'woocommerce_serbian_checkout_fields_priority' )] - public function modify_shipping_fields( $fields ) { - $fields = $this->maybe_remove_fields( $fields, 'shipping' ); - - return $fields; - } - - /** - * Removes unnecessary fields from the checkout ajax request - * - * @param array $fields Fields to modify. - * @return array Modified fields - */ - #[Filter( tag: 'woocommerce_shipping_fields', priority: 'woocommerce_serbian_checkout_fields_priority' )] - public function modify_ajax_checkout_fields( $fields ) { - if ( ! \wp_doing_ajax() ) { - return $fields; - } - - $checkout_customer_type = \xwp_fetch_post_var( 'billing_type', 'person' ); - - if ( 'person' === $checkout_customer_type ) { - unset( $fields['billing']['billing_company'] ); - unset( $fields['billing']['billing_mb'] ); - unset( $fields['billing']['billing_pib'] ); - } - - return $fields; - } - - /** - * Removes the fields that are not needed, and changes fields priority. - * - * @param array $fields Fields. - * @param string $type Field type - billing or shipping. - * @return array Modified fields - */ - private function maybe_remove_fields( $fields, $type = 'billing' ) { - $fields[ "{$type}_postcode" ]['priority'] = 81; - $fields[ "{$type}_city" ]['priority'] = 91; - $fields[ "{$type}_country" ]['priority'] = 91; - - $to_remove = \WCSRB()->get_settings( - 'core', - 'remove_unneeded_fields', - ) ? array( 'address_2', 'state' ) : array(); - - /** - * Filters the fields that should be removed from the checkout page - * - * @param array $to_remove Fields to remove - * @return array - * @since 1.3.0 - */ - $to_remove = \apply_filters( 'woocommerce_serbian_checkout_fields_to_remove', $to_remove ); - - foreach ( $to_remove as $field_name ) { - unset( $fields[ "{$type}_{$field_name}" ] ); - } - - return $fields; - } - - /** - * Adds the billing type field to the checkout page. - * - * Depending on the plugin settings, field can be a radio button or a hidden input - * - * @param string $enabled_type Enabled customer type. - * @return array Billing type field data. - * - * @since 1.3.0 - */ - private function add_billing_type_field( $enabled_type ) { - $billing_type = array( - 'class' => array( 'form-row-wide', 'entity-type-control', 'update_totals_on_change' ), - 'default' => 'person', - 'label' => \__( 'Customer type', 'serbian-addons-for-woocommerce' ), - 'options' => \wcsrb_get_entity_types(), - 'priority' => 21, - 'required' => true, - 'type' => 'radio', - ); - - if ( 'both' !== $enabled_type ) { - - $billing_type['type'] = 'hidden'; - $billing_type['default'] = $enabled_type; - $billing_type['description'] = \wcsrb_get_entity_types()[ $enabled_type ]; - - unset( $billing_type['options'] ); - - } - - return $billing_type; - } - - /** - * Add needed company fields if the customer can checkout as a company - * - * @param string $enabled_type Enabled customer type. - * @return array Company fields data. - */ - private function maybe_add_company_fields( $enabled_type ) { - if ( ! \in_array( $enabled_type, array( 'both', 'company' ), true ) ) { - return array(); - } - - $extra_fields = array( - 'billing_mb' => array( - 'class' => array( 'form-row-first', 'hide-if-person' ), - 'label' => \__( 'Company Number', 'serbian-addons-for-woocommerce' ), - 'placeholder' => \__( 'Enter MB', 'serbian-addons-for-woocommerce' ), - 'priority' => 31, - 'required' => true, - 'type' => 'text', - 'validate' => array( 'mb' ), - ), - 'billing_pib' => array( - 'class' => array( 'form-row-last', 'hide-if-person' ), - 'label' => \__( 'Tax Number', 'serbian-addons-for-woocommerce' ), - 'placeholder' => \__( 'Enter PIB', 'serbian-addons-for-woocommerce' ), - 'priority' => 32, - 'required' => true, - 'type' => 'text', - 'validate' => array( 'pib' ), - ), - ); - - if ( 'company' !== $enabled_type ) { - $extra_fields['billing_pib']['custom_attributes']['disabled'] = 'disabled'; - $extra_fields['billing_mb']['custom_attributes']['disabled'] = 'disabled'; - } - - return $extra_fields; - } -} diff --git a/lib/Checkout/Field_Validator.php b/lib/Checkout/Field_Validator.php deleted file mode 100644 index 567c256..0000000 --- a/lib/Checkout/Field_Validator.php +++ /dev/null @@ -1,161 +0,0 @@ -can_validate( $posted, $type ) ) { - return; - } - - $validators = $this->get_field_validators( \current_filter() ); - $notices = $this->filter_notices( \array_keys( $validators ) ); - - foreach ( $validators as $field => $args ) { - if ( $args['validator']( $posted[ $field ] ) ) { - continue; - } - - $notices['error'][] = array( - 'data' => array( - 'id' => $field, - ), - 'notice' => $args['message'], - ); - - $_POST[ $field ] = ''; - - } - \WC()->session->set( 'wc_notices', $notices ); - } - - - /** - * Adds custom validation to billing address field saving - * - * @param array $data Posted data. - * @param \WP_Error $error Error object. - */ - #[Action( 'woocommerce_after_checkout_validation', 0 )] - public function validate_checkout_fields( $data, $error ) { - $fields = $this->get_field_validators( \current_filter() ); - - foreach ( \array_keys( $fields ) as $field ) { - $error->remove( $field . '_required' ); - } - - if ( ! $this->can_validate( $data ) ) { - return; - } - - foreach ( $fields as $field => $args ) { - if ( $args['validator']( $data[ $field ] ) ) { - continue; - } - - $error->add( $args['code'], $args['message'], array( 'id' => $field ) ); - } - } - - /** - * Checks if the current address can be validated. - * - * @param array $fields Address fields. - * @param string $addr_type Address type being validated. - * @return bool - */ - protected function can_validate( array $fields, string $addr_type = 'billing' ): bool { - $type = $fields['billing_type'] ??= ''; - $country = $fields['billing_country'] ??= ''; - - return 'billing' === $addr_type && 'company' === $type && 'RS' === $country; - } - - /** - * Returns the field validators for the given action. - * - * @param string $action Action being performed. - * @return array - */ - protected function get_field_validators( string $action ) { - $args = array( - 'billing_company' => array( - 'code' => 'billing_company_required', - 'message' => \__( 'Company name is required', 'serbian-addons-for-woocommerce' ), - 'validator' => static fn( $val ) => '' !== $val, - ), - 'billing_mb' => array( - 'code' => 'billing_mb_validation', - 'message' => \__( 'Company number is invalid', 'serbian-addons-for-woocommerce' ), - 'validator' => '\Oblak\validateMB', - ), - 'billing_pib' => array( - 'code' => 'billing_pib_validation', - 'message' => \__( 'Company Tax Number is invalid', 'serbian-addons-for-woocommerce' ), - 'validator' => '\Oblak\validatePIB', - ), - ); - - /** - * Returns the validation arguments for the given action. - * - * @param array $args Validation arguments. - * @param string $action Action being performed. - * - * @return array - * - * @since 3.6.0 - */ - return \apply_filters( 'wcrs_field_validators', $args, $action ); - } - - /** - * Filters out notices for fields that have been validated. - * - * @param array $fields Fields that have been validated. - * @return array - */ - protected function filter_notices( array $fields ): array { - $notices = \WC()->session->get( 'wc_notices', array() ); - - $notices['error'] = \array_filter( - $notices['error'] ?? array(), - static fn( $e ) => ! \in_array( $e['data']['id'] ?? '', $fields, true ) - ); - - return $notices; - } -} diff --git a/lib/Core/Address_Display_Controller.php b/lib/Core/Address_Display_Controller.php new file mode 100644 index 0000000..450ede2 --- /dev/null +++ b/lib/Core/Address_Display_Controller.php @@ -0,0 +1,145 @@ + $formats Address formats. + * @return array + */ + #[Filter( 'woocommerce_localisation_address_formats', 'wcrs_localization_address_priority' )] + public function modify_address_format( $formats ) { + \add_filter( 'woocommerce_formatted_address_force_country_display', '__return_true' ); + + $formats['RS'] = "{name}\n{company}\n{mb}\n{pib}\n{address_1}\n{address_2}\n{postcode} {city}, {state} {country}"; + + if ( \WCSRB()->get_settings( 'core', 'remove_unneeded_fields' ) ) { + $formats['RS'] = \str_replace( array( '{state}', '{address_2}' ), '', $formats['RS'] ); + } + + return $formats; + } + + /** + * Adds custom replacements to the replacements array. + * + * Custom fields added are: + * - Type + * - Company Number + * - Tax Identification Number + * + * @param string[] $replacements Replacements array. + * @param array $args Address data. + * @return string[] Modified replacements array + */ + #[Filter( 'woocommerce_formatted_address_replacements', 99 )] + public function modify_address_replacements( $replacements, $args ) { + $replacements['{mb}'] = $args['mb'] ?? "\n"; + $replacements['{pib}'] = $args['pib'] ?? "\n"; + + return $replacements; + } + + /** + * Modifies the address data array to include neccecary company information. + * + * This is used in the My Account > Addresses page. + * + * @param array $fmtd Address data array. + * @param int $uid Customer ID. + * @param 'billing'|'shipping' $type Address type (billing or shipping). + * @return array + */ + #[Filter( 'woocommerce_my_account_my_address_formatted_address', 99 )] + public function modify_account_formatted_address( array $fmtd, int $uid, $type ) { + if ( 'billing' !== $type ) { + return $fmtd; + } + + return \array_merge( + $fmtd, + $this->get_replacement_values( new WC_Customer( $uid ) ), + ); + } + + /** + * Modifies the address data array to include neccecary company information. + * + * This is used for the order addresses. + * + * @param array $address Address data array. + * @param WC_Order $order Order object. + * @return array Modified address data array + */ + #[Filter( 'woocommerce_order_formatted_billing_address', 99 )] + public function modify_order_formatted_address( $address, $order ) { + return \array_merge( + $address, + $this->get_replacement_values( $order ), + ); + } + + /** + * Modifies the buyer name in the admin order page to include necessary company information + * + * @param string $buyer Buyer name. + * @param WC_Order $order Order object. + * @return string Modified Buyer name + */ + #[Filter( 'woocommerce_admin_order_buyer_name', 99 )] + public function modify_order_buyer_name( string $buyer, WC_Order $order ): string { + $data = \wcsrb_get_company_data( $order ); + + if ( 'RS' === $order->get_billing_country() && 'company' === $data['type'] ) { + $buyer = $order->get_billing_company(); + } + + return $buyer; + } + + /** + * Billing address modifier function + * + * Depending on the customer(user) type we add the needed rows to the address. + * If the customer is a company we prepend the number type before the number itself + * + * @param WC_Customer|WC_Order $target Customer or Order object. + * @return array + */ + protected function get_replacement_values( WC_Customer|WC_Order $target ): array { + $data = \wcsrb_get_company_data( $target ); + if ( 'company' !== $data['type'] ) { + return array(); + } + + return array( + 'first_name' => "\n", + 'last_name' => "\n", + 'mb' => \sprintf( + '%s: %s', + \_x( 'Company Number', 'Address display', 'serbian-addons-for-woocommerce' ), + $data['mb'] ?: "\n", + ), + 'pib' => \sprintf( + '%s: %s', + \_x( 'Tax Identification Number', 'Address display', 'serbian-addons-for-woocommerce' ), + $data['pib'] ?: "\n", + ), + + ); + } +} diff --git a/lib/Core/Address_Field_Controller.php b/lib/Core/Address_Field_Controller.php new file mode 100644 index 0000000..a7b3354 --- /dev/null +++ b/lib/Core/Address_Field_Controller.php @@ -0,0 +1,194 @@ + $fields Default address fields. + * @return array + */ + #[Filter( tag: 'woocommerce_default_address_fields', priority: 999999 )] + public function add_customer_type_field( array $fields ): array { + $enabled_type = \WCSRB()->get_settings( 'core', 'enabled_customer_types' ); + $type_field = array( + 'class' => array( 'form-row-wide', 'entity-type-control', 'update_totals_on_change', 'address-field' ), + 'default' => 'person', + 'label' => \__( 'Customer type', 'serbian-addons-for-woocommerce' ), + 'options' => \wcsrb_get_entity_types(), + 'priority' => 21, + 'required' => true, + 'type' => 'radio', + ); + + if ( 'both' !== $enabled_type ) { + + $type_field = \array_merge( + $type_field, + array( + 'default' => $enabled_type, + 'description' => \wcsrb_get_entity_types()[ $enabled_type ], + 'type' => 'hidden', + 'value' => $enabled_type, + ), + ); + + unset( $type_field['options'] ); + } + + $fields['type'] = $type_field; + + return $fields; + } + + /** + * Adds the extra fields to the default address fields + * + * @param array $fields Default address fields. + * @return array + */ + #[Filter( tag: 'woocommerce_default_address_fields', priority: 'wcsrb_address_fields_priority' )] + public function add_company_fields( array $fields ): array { + if ( \WCSRB()->get_settings( 'core', 'remove_unneeded_fields' ) ) { + unset( $fields['address_2'], $fields['state'] ); + } + + $fields['company']['class'][] = 'entity-type-toggle'; + + return \array_merge( $fields, \wcsrb_get_company_fields() ); + } + + /** + * Unsets I18n label for the customer type field. + * + * @param array $fields Default country locale fields. + * @return array + */ + #[Filter( tag: 'woocommerce_get_country_locale_default', priority: 999999 )] + public function modify_default_locale_field_data( array $fields ): array { + unset( $fields['type']['label'] ); + + return $fields; + } + + /** + * Set the JS locale fields data. + * + * Adds the hidden and required properties for all countries. + * All are set to be hidden and NOT required by default. + * + * @param array $locale Default locale fields data. + * @return array + */ + #[Filter( tag: 'woocommerce_get_country_locale', priority: 1000 )] + public function add_default_locale_field_data( array $locale ): array { + foreach ( $locale as &$fields ) { + $fields['company']['required'] = false; + $fields['type'] = array( + 'hidden' => true, + 'required' => false, + ); + $fields['mb'] = array( + 'hidden' => true, + 'required' => false, + ); + $fields['pib'] = array( + 'hidden' => true, + 'required' => false, + ); + } + + return $locale; + } + + /** + * Adds the custom locale field data + * + * We unhide the fields and enable them only if the company type is active. + * + * @param array $locale Default locale fields data. + * @return array + */ + #[Filter( tag: 'woocommerce_get_country_locale', priority: 1000 )] + public function add_custom_locale_field_data( array $locale ): array { + $company_active = \wcsrb_can_checkout_as( 'company' ); + $company_props = array( 'hidden' => ! $company_active ); + + // phpcs:disable SlevomatCodingStandard.Arrays.AlphabeticallySortedByKeys.IncorrectKeyOrder + $locale['RS'] = array( + 'type' => array( + 'required' => true, + 'hidden' => false, + ), + 'company' => \array_merge( + array( 'class' => array( 'form-row-wide', 'entity-type-toggle', 'shown' ) ), + $company_props, + ), + 'mb' => $company_props, + 'pib' => $company_props, + 'postcode' => array( + 'priority' => 81, + ), + 'city' => array( + 'priority' => 82, + ), + 'country' => array( + 'priority' => 91, + ), + ); + // phpcs:enable SlevomatCodingStandard.Arrays.AlphabeticallySortedByKeys.IncorrectKeyOrder + + return $locale; + } + + /** + * Modifies the locale field selectors + * + * @param array $selectors Field selectors. + * @return array + */ + #[Filter( tag: 'woocommerce_country_locale_field_selectors', priority: 99999 )] + public function locale_field_selectors( array $selectors ): array { + return \array_merge( + $selectors, + array( + 'company' => '#billing_company_field', + 'mb' => '#billing_mb_field', + 'pib' => '#billing_pib_field', + 'type' => '#billing_type_field', + ), + ); + } + + /** + * Modifies the billing fields to add the customer type and additional company fields + * + * @param array $fields Billing fields. + * @return array Modified billing fields + */ + #[Filter( tag: 'woocommerce_shipping_fields', priority: 'woocommerce_serbian_checkout_fields_priority' )] + public function modify_shipping_fields( array $fields ) { + $to_remove = array( 'company', 'mb', 'pib', 'type' ); + + return \xwp_array_diff_assoc( + $fields, + ...\array_map( static fn( $f ) => "shipping_{$f}", $to_remove ), + ); + } +} diff --git a/lib/Core/Address_Validate_Controller.php b/lib/Core/Address_Validate_Controller.php new file mode 100644 index 0000000..b5bc13b --- /dev/null +++ b/lib/Core/Address_Validate_Controller.php @@ -0,0 +1,47 @@ +validator()->validate_fields( \xwp_post_arr(), $type ) as $error ) { + \wc_add_notice( $error['message'], 'error', array( 'id' => $error['id'] ) ); + } + } + + + /** + * Validates the checkout fields. + * + * @param array $fields Address fields. + * @param \WP_Error $error Error object. + */ + #[Action( 'woocommerce_after_checkout_validation', 0 )] + public function validate_checkout( array $fields, \WP_Error $error ) { + foreach ( \WCSRB()->validator()->validate_fields( $fields, 'billing' ) as $err ) { + $error->add( $err['code'], $err['message'], array( 'id' => $err['id'] ) ); + } + } +} diff --git a/lib/Functions/wcsrb-address-field-fns.php b/lib/Functions/wcsrb-address-field-fns.php new file mode 100644 index 0000000..0cca998 --- /dev/null +++ b/lib/Functions/wcsrb-address-field-fns.php @@ -0,0 +1,96 @@ + array( + 'class' => array( 'form-row-first', 'address-field', 'entity-type-toggle', 'shown' ), + 'label' => \__( 'Company Number', 'serbian-addons-for-woocommerce' ), + 'placeholder' => \__( 'Enter MB', 'serbian-addons-for-woocommerce' ), + 'priority' => 31, + 'type' => 'text', + 'validate' => array( 'mb' ), + ), + 'pib' => array( + 'class' => array( 'form-row-last', 'address-field', 'entity-type-toggle', 'shown' ), + 'label' => \__( 'Tax Number', 'serbian-addons-for-woocommerce' ), + 'placeholder' => \__( 'Enter PIB', 'serbian-addons-for-woocommerce' ), + 'priority' => 32, + 'type' => 'text', + 'validate' => array( 'pib' ), + ), + ); + + /** + * Filters the company address fields. + * + * @param array $fields Company address fields. + * @return array + * + * @since 3.8.0 + */ + return apply_filters( 'wcsrb_company_address_fields', $fields ); +} + +/** + * Checks if the customer can checkout as a given type. + * + * @param 'person'|'company'|'both' $type Customer type. + * @return bool + */ +function wcsrb_can_checkout_as( string $type ): bool { + static $types; + + $types ??= WCSRB()->get_settings( 'core', 'enabled_customer_types' ); + + return 'both' === $types || $type === $types; +} + + +/** + * Get the customer type for the given customer. + * + * @param WC_Order|WC_Customer $target Customer ID or object. + * @return 'person'|'company' + */ +function wcsrb_get_customer_type( WC_Order|WC_Customer $target ): string { + $key = $target instanceof WC_Order ? '_billing_type' : 'billing_type'; + + // phpcs:ignore Universal + return $target->get_meta( $key, true ) ?: 'person'; +} + +/** + * Get the company data for the given customer. + * + * @param WC_Order|WC_Customer $target Customer ID or object. + * @return false|array{mb: string, pib: string, type: 'company'|'person'} + */ +function wcsrb_get_company_data( WC_Order|WC_Customer $target ): bool|array { + if ( 'company' !== wcsrb_get_customer_type( $target ) ) { + return array( + 'mb' => '', + 'pib' => '', + 'type' => 'person', + ); + } + + $key = $target instanceof WC_Order ? '_billing' : 'billing'; + + return array( + 'mb' => $target->get_meta( "{$key}_mb", true ), + 'pib' => $target->get_meta( "{$key}_pib", true ), + 'type' => 'company', + ); +} diff --git a/lib/Utils/wcsrb-core.php b/lib/Functions/wcsrb-core.php similarity index 89% rename from lib/Utils/wcsrb-core.php rename to lib/Functions/wcsrb-core.php index d7c583f..2ead635 100644 --- a/lib/Utils/wcsrb-core.php +++ b/lib/Functions/wcsrb-core.php @@ -6,15 +6,13 @@ * @subpackage Utils */ -use Oblak\WooCommerce\Serbian_Addons\Serbian_WooCommerce; - /** * Main Plugin Instance * - * @return Serbian_WooCommerce + * @return Oblak\WCSRB\App */ function WCSRB() { - return Serbian_WooCommerce::instance(); + return Oblak\WCSRB\App::instance(); } /** diff --git a/lib/Utils/wcsrb-helpers.php b/lib/Functions/wcsrb-helpers.php similarity index 100% rename from lib/Utils/wcsrb-helpers.php rename to lib/Functions/wcsrb-helpers.php diff --git a/lib/Utils/wcsrb-payment-slip.php b/lib/Functions/wcsrb-payment-slip.php similarity index 100% rename from lib/Utils/wcsrb-payment-slip.php rename to lib/Functions/wcsrb-payment-slip.php diff --git a/lib/Utils/wcsrb-settings.php b/lib/Functions/wcsrb-settings.php similarity index 100% rename from lib/Utils/wcsrb-settings.php rename to lib/Functions/wcsrb-settings.php diff --git a/lib/Order/Field_Display.php b/lib/Order/Field_Display.php deleted file mode 100644 index fe6bdb6..0000000 --- a/lib/Order/Field_Display.php +++ /dev/null @@ -1,200 +0,0 @@ -get_settings( 'core', 'remove_unneeded_fields' ) ) { - $formats['RS'] = \str_replace( array( '{state}', '{address_2}' ), '', $formats['RS'] ); - } - - return $formats; - } - - /** - * Adds custom replacements to the replacements array. - * - * Custom fields added are: - * - Type - * - Company Number - * - Tax Identification Number - * - * @param string[] $replacements Replacements array. - * @param array $args Address data. - * @return string[] Modified replacements array - */ - #[Filter( 'woocommerce_formatted_address_replacements', 99 )] - public function modify_address_replacements( $replacements, $args ) { - $replacements['{type}'] = $args['type'] ?? "\n"; - $replacements['{mb}'] = $args['mb'] ?? "\n"; - $replacements['{pib}'] = $args['pib'] ?? "\n"; - - return $replacements; - } - - /** - * Modifies the address data array to include neccecary company information. - * - * This is used in the My Account > Addresses page. - * - * @param array $address Address data array. - * @param int $customer_id Customer ID. - * @param string $address_type Address type (billing or shipping). - * @return array Modified address data array - */ - #[Filter( 'woocommerce_my_account_my_address_formatted_address', 99 )] - public function modify_account_formatted_address( $address, $customer_id, $address_type ) { - if ( 'billing' !== $address_type ) { - return $address; - } - - $customer = new WC_Customer( $customer_id ); - - // phpcs:ignore Universal.Operators.DisallowShortTernary.Found - $user_type = $customer->get_meta( 'billing_type', true ) ?: 'person'; - $company_num = $customer->get_meta( 'billing_mb', true ); - $company_tax = $customer->get_meta( 'billing_pib', true ); - - return $this->address_modifier( $address, $user_type, $company_num, $company_tax ); - } - - /** - * Modifies the address data array to include neccecary company information. - * - * This is used for the order addresses. - * - * @param array $address Address data array. - * @param WC_Order $order Order object. - * @return array Modified address data array - */ - #[Filter( 'woocommerce_order_formatted_billing_address', 99 )] - public function modify_order_formatted_address( $address, $order ) { - return $this->address_modifier( - $address, - $order->get_meta( '_billing_type', true ), - $order->get_meta( '_billing_mb', true ), - $order->get_meta( '_billing_pib', true ), - ); - } - - /** - * Billing address modifier function - * - * Depending on the customer(user) type we add the needed rows to the address. - * If the customer is a company we prepend the number type before the number itself - * - * @param array $address Billing address data array. - * @param string $type User type (person or company). - * @param string $company_number Company number. - * @param string $tax_number Company tax number. - * @return array Modified billing address data array. - */ - private function address_modifier( $address, $type, $company_number, $tax_number ) { - $address['type'] = $type; - $address['mb'] = "\n"; - $address['pib'] = "\n"; - - if ( 'company' !== $type ) { - return $address; - } - - $address['first_name'] = "\n"; - $address['last_name'] = "\n"; - - if ( $company_number ) { - $address['mb'] = \sprintf( - '%s: %s', - \_x( 'Company Number', 'Address display', 'serbian-addons-for-woocommerce' ), - $company_number, - ); - } - $address['pib'] = \sprintf( - '%s: %s', - \_x( 'Tax Identification Number', 'Address display', 'serbian-addons-for-woocommerce' ), - $tax_number, - ); - - return $address; - } - - /** - * Modifies the buyer name in the admin order page to include necessary company information - * - * @param string $buyer Buyer name. - * @param WC_Order $order Order object. - * @return string Modified Buyer name - */ - #[Filter( 'woocommerce_admin_order_buyer_name', 99 )] - public function modify_order_buyer_name( $buyer, $order ) { - return 'RS' === $order->get_billing_country() && 'company' === $order->get_meta( '_billing_type', true ) - ? $order->get_billing_company() - : $buyer; - } - - /** - * Adds the company information to the customer meta fields. - * - * @param array $fields Customer meta fields. - * @return array Modified customer meta fields - */ - #[Filter( 'woocommerce_customer_meta_fields' )] - public function modify_customer_meta_fields( array $fields ): array { - $billing = array(); - - foreach ( $fields['billing']['fields'] as $field => $args ) { - if ( 'billing_company' !== $field ) { - $billing[ $field ] = $args; - continue; - } - - $billing['billing_type'] = array( - 'label' => \__( 'Customer type', 'serbian-addons-for-woocommerce' ), - 'options' => \wcsrb_get_entity_types(), - 'type' => 'select', - ); - - $billing[ $field ] = $args; - - $billing['billing_mb'] = array( - 'label' => \__( 'Company Number', 'serbian-addons-for-woocommerce' ), - 'type' => 'text', - ); - - $billing['billing_pib'] = array( - 'label' => \__( 'Tax Number', 'serbian-addons-for-woocommerce' ), - 'type' => 'text', - ); - } - - $fields['billing']['fields'] = $billing; - - return $fields; - } -} diff --git a/lib/Services/Field_Validator.php b/lib/Services/Field_Validator.php new file mode 100644 index 0000000..63dca2a --- /dev/null +++ b/lib/Services/Field_Validator.php @@ -0,0 +1,141 @@ +company_active = \wcsrb_can_checkout_as( 'company' ); + } + + /** + * Validates the address fields. + * + * @param array $fields Address fields. + * @param 'billing'|'shipping' $type Address type being validated. + * @param array $address Address fields. + * @return null|array + */ + public function validate_fields( array $fields, string $type, ?array $address = null ): array { + if ( ! $this->needs_validation( $fields ) ) { + return array(); + } + + $address ??= \WC()->countries->get_address_fields( $fields[ "{$type}_country" ], $type . '_' ); + $errors = array(); + + foreach ( $this->get_field_validators() as $key => $args ) { + $errors[] = $this->validate_field( $fields[ $key ] ?? '', $key, $args, $address[ $key ] ); + + } + + //phpcs:ignore Universal.Operators.DisallowShortTernary.Found + return \array_filter( $errors ); + } + + /** + * Validates the given field. + * + * @param mixed $value Field value. + * @param string $key Field key. + * @param array $args Validation arguments. + * @param array $field Field data. + * @return ?array Error data if validation fails, null otherwise. + */ + protected function validate_field( mixed $value, string $key, array $args, array $field ): ?array { + if ( ! $value ) { + return array( + 'code' => "{$key}_required", + 'id' => $key, + // Translators: %s: Field label. + 'message' => \sprintf( \__( '%s is a required field.', 'woocommerce' ), $field['label'] ), + ); + } + + if ( ! $args['callback']( $value ) ) { + return array( + 'code' => $args['code'], + 'id' => $key, + 'message' => $args['message'], + ); + } + + return null; + } + + /** + * Returns the field validators for the given action. + * + * @return array + */ + protected function get_field_validators(): array { + $args = array( + 'billing_company' => array( + 'callback' => '__return_true', + 'code' => 'billing_company_required', + 'message' => \__( 'Company name is required', 'serbian-addons-for-woocommerce' ), + ), + 'billing_mb' => array( + 'callback' => '\Oblak\validateMB', + 'code' => 'billing_mb_validation', + 'message' => \__( 'Company number is invalid', 'serbian-addons-for-woocommerce' ), + ), + 'billing_pib' => array( + 'callback' => '\Oblak\validatePIB', + 'code' => 'billing_pib_validation', + 'message' => \__( 'Company Tax Number is invalid', 'serbian-addons-for-woocommerce' ), + ), + ); + + /** + * Returns the validation arguments for the given action. + * + * @param array $args Validation arguments. + * @return array + * + * @since 3.6.0 + */ + return \apply_filters( 'wcrs_field_validators', $args ); + } + + /** + * Checks if the current address can be validated. + * + * @param array $fields Address fields. + * @param 'billing'|'shipping' $address Address type being validated. + * @return bool + */ + protected function needs_validation( array $fields, string $address = 'billing' ): bool { + return $this->company_active && + 'billing' === $address && + 'company' === ( $fields['billing_type'] ?? '' ) && + 'RS' === ( $fields['billing_country'] ?? '' ); + } +} diff --git a/lib/Core/Installer.php b/lib/Utils/Installer.php similarity index 98% rename from lib/Core/Installer.php rename to lib/Utils/Installer.php index 4909221..54a607d 100644 --- a/lib/Core/Installer.php +++ b/lib/Utils/Installer.php @@ -6,7 +6,7 @@ * @subpackage Core */ -namespace Oblak\WooCommerce\Serbian_Addons\Core; +namespace Oblak\WCSRB\Utils; use Oblak\WP\Base_Plugin_Installer; diff --git a/lib/Core/Template_Extender.php b/lib/Utils/Template_Extender.php similarity index 91% rename from lib/Core/Template_Extender.php rename to lib/Utils/Template_Extender.php index 068692d..0610699 100644 --- a/lib/Core/Template_Extender.php +++ b/lib/Utils/Template_Extender.php @@ -5,7 +5,7 @@ * @package Serbian Addons for WooCommerce */ -namespace Oblak\WooCommerce\Serbian_Addons\Core; +namespace Oblak\WCSRB\Utils; use Oblak\WP\Decorators\Hookable; use XWC\Template\Customizer_Base; @@ -14,6 +14,7 @@ * Adds custom templates to WooCommerce. * * @since 2.3.0 + * @since 3.8.0 Moved from the `Core` namespace. */ #[Hookable( 'before_woocommerce_init', 99 )] class Template_Extender extends Customizer_Base {