diff --git a/assets/wp/js/account-address.js b/assets/wp/js/account-address.js new file mode 100644 index 0000000..e343554 --- /dev/null +++ b/assets/wp/js/account-address.js @@ -0,0 +1,93 @@ +(function ($) { + "use strict"; + + function districtFields() { + return $( + "#billing_kiriof_destination_area, #shipping_kiriof_destination_area", + ); + } + + function districtNameField($field) { + var addressType = String($field.attr("id") || "").indexOf("shipping_") === 0 + ? "shipping" + : "billing"; + return $("#" + addressType + "_kiriof_destination_area_name"); + } + + function clearDistrict($field) { + $field.val("").trigger("change.select2"); + districtNameField($field).val(""); + } + + $(function () { + var select = $.fn.selectWoo || $.fn.select2; + var $fields = districtFields(); + + if (!$fields.length || !select || typeof kiriofAjax === "undefined") { + return; + } + + $fields.each(function () { + var $field = $(this); + select.call($field, { + minimumInputLength: 3, + placeholder: + typeof kiriofAccountAddress !== "undefined" + ? kiriofAccountAddress.selectOption + : "Select Option", + allowClear: true, + ajax: { + url: kiriofAjax.ajaxurl, + dataType: "json", + type: "POST", + delay: 250, + data: function (params) { + var term = params && params.term ? params.term : ""; + return { + action: "kiriminaja_subdistrict_search", + nonce: kiriofAjax.nonce, + term: term, + data: { term: term, search: term }, + }; + }, + processResults: function (response) { + var rows = response && response.success !== false && response.data + ? response.data + : []; + return { + results: $.map(rows, function (row) { + return { id: row.id, text: row.text }; + }), + }; + }, + cache: true, + }, + }); + + $field + .on("select2:select.kiriofAccountDistrict", function (event) { + var selected = event.params && event.params.data ? event.params.data : {}; + districtNameField($field).val(selected.text || ""); + }) + .on("select2:clear.kiriofAccountDistrict", function () { + districtNameField($field).val(""); + }); + }); + + $("#billing_postcode, #shipping_postcode").each(function () { + $(this).data("kiriofInitialPostcode", String($(this).val() || "")); + }).on( + "input.kiriofAccountDistrict change.kiriofAccountDistrict", + function () { + var currentPostcode = String($(this).val() || ""); + if (currentPostcode === String($(this).data("kiriofInitialPostcode") || "")) { + return; + } + var prefix = String(this.id || "").indexOf("shipping_") === 0 + ? "shipping" + : "billing"; + clearDistrict($("#" + prefix + "_kiriof_destination_area")); + }, + ); + }); +})(jQuery); diff --git a/inc/Base/Enqueue.php b/inc/Base/Enqueue.php index 36c992f..20536b2 100644 --- a/inc/Base/Enqueue.php +++ b/inc/Base/Enqueue.php @@ -60,6 +60,22 @@ function enqueueWp(){ KIRIOF_VERSION, array( 'in_footer' => true ) ); + if ( function_exists( 'is_account_page' ) && is_account_page() && function_exists( 'is_wc_endpoint_url' ) && is_wc_endpoint_url( 'edit-address' ) ) { + wp_enqueue_script( + 'kiriof-account-address', + KIRIOF_URL . 'assets/wp/js/account-address.js', + array( 'kiriof-script', 'jquery', 'select2' ), + KIRIOF_VERSION, + array( 'in_footer' => true ) + ); + wp_localize_script( + 'kiriof-account-address', + 'kiriofAccountAddress', + array( + 'selectOption' => __( 'Select Option', 'kiriminaja-official' ), + ) + ); + } // Localize script to pass ajax URL and nonce wp_localize_script( diff --git a/inc/Controllers/AccountAddressController.php b/inc/Controllers/AccountAddressController.php new file mode 100644 index 0000000..74a5d1f --- /dev/null +++ b/inc/Controllers/AccountAddressController.php @@ -0,0 +1,120 @@ +isEditAddressRequest( $address_type ) ) { + return $fields; + } + + $district = ( new CustomerDistrictService() )->get( get_current_user_id(), $address_type ); + $field_key = $address_type . '_kiriof_destination_area'; + $name_key = $address_type . '_kiriof_destination_area_name'; + $options = array( '' => __( 'Select Option', 'kiriminaja-official' ) ); + if ( '' !== $district['id'] && '' !== $district['name'] ) { + $options[ $district['id'] ] = $district['name']; + } + + $field = array( + 'label' => __( 'District', 'kiriminaja-official' ), + 'required' => true, + 'class' => array( 'form-row-wide' ), + 'type' => 'select', + 'options' => $options, + 'value' => $district['id'], + 'priority' => 61, + ); + $name_field = array( + 'type' => 'hidden', + 'value' => $district['name'], + 'priority' => 62, + ); + + $fields = $this->insertAfterPostcode( $fields, $field_key, $field ); + $fields = $this->insertAfterKey( $fields, $field_key, $name_key, $name_field ); + + return $fields; + } + + public function validateDistrict( int $user_id, string $address_type, array $address, $customer ): void { + unset( $user_id, $address, $customer ); + if ( ! in_array( $address_type, array( 'billing', 'shipping' ), true ) ) { + return; + } + + $district_id = $this->postedDistrictId( $address_type ); + $district_name = $this->postedDistrictName( $address_type ); + if ( $district_id < 1 || '' === $district_name ) { + wc_add_notice( __( 'Please select a District.', 'kiriminaja-official' ), 'error' ); + } + } + + public function saveDistrict( int $user_id, string $address_type ): void { + if ( ! in_array( $address_type, array( 'billing', 'shipping' ), true ) ) { + return; + } + + ( new CustomerDistrictService() )->save( + $user_id, + $address_type, + $this->postedDistrictId( $address_type ), + $this->postedDistrictName( $address_type ) + ); + } + + private function isEditAddressRequest( string $address_type = '' ): bool { + if ( ! function_exists( 'is_account_page' ) || ! is_account_page() || ! function_exists( 'is_wc_endpoint_url' ) || ! is_wc_endpoint_url( 'edit-address' ) ) { + return false; + } + if ( '' === $address_type ) { + return true; + } + + $requested_type = sanitize_key( (string) get_query_var( 'edit-address' ) ); + return '' === $requested_type || $requested_type === $address_type; + } + + private function postedDistrictId( string $address_type ): int { + $key = $address_type . '_kiriof_destination_area'; + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- WooCommerce verifies the account address nonce before this hook. + return isset( $_POST[ $key ] ) ? absint( wp_unslash( $_POST[ $key ] ) ) : 0; + } + + private function postedDistrictName( string $address_type ): string { + $key = $address_type . '_kiriof_destination_area_name'; + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- WooCommerce verifies the account address nonce before this hook. + return isset( $_POST[ $key ] ) ? sanitize_text_field( wp_unslash( $_POST[ $key ] ) ) : ''; + } + + private function insertAfterPostcode( array $fields, string $field_key, array $field ): array { + $postcode_key = 0 === strpos( $field_key, 'shipping_' ) ? 'shipping_postcode' : 'billing_postcode'; + return $this->insertAfterKey( $fields, $postcode_key, $field_key, $field ); + } + + private function insertAfterKey( array $fields, string $after_key, string $field_key, array $field ): array { + $result = array(); + foreach ( $fields as $key => $value ) { + $result[ $key ] = $value; + if ( $after_key === $key ) { + $result[ $field_key ] = $field; + } + } + if ( ! isset( $result[ $field_key ] ) ) { + $result[ $field_key ] = $field; + } + return $result; + } +} diff --git a/inc/Controllers/CheckoutController.php b/inc/Controllers/CheckoutController.php index f1e5c24..bd07def 100644 --- a/inc/Controllers/CheckoutController.php +++ b/inc/Controllers/CheckoutController.php @@ -505,6 +505,10 @@ function kiriof_add_checkout_nonce_field(){ } function add_custom_select_options_field_and_script($checkout) { + if ( ! is_cart() && ! is_checkout() ) { + return; + } + if ( ! $this->kiriof_cart_needs_shipping() ) { $this->kiriof_clear_logistics_session(); $this->kiriof_render_virtual_cart_district_cleanup(); @@ -1150,6 +1154,26 @@ function afterCheckoutBeforeCreated($order,$data ){ //save meta subdistrict shipping woocommerce if( isset($_POST['kiriof_shipping_destination_area']) && !empty($_POST['kiriof_shipping_destination_area']) ) $order->update_meta_data( '_shipping_kiriof_destination_area', sanitize_text_field(wp_unslash($_POST['kiriof_shipping_destination_area'])) ); if( isset($_POST['kiriof_shipping_destination_area_name']) && !empty($_POST['kiriof_shipping_destination_area_name']) ) $order->update_meta_data( '_shipping_kiriof_destination_name', sanitize_text_field(wp_unslash($_POST['kiriof_shipping_destination_area_name'])) ); + + if ( is_user_logged_in() ) { + $district_service = new \KiriminAjaOfficial\Services\CustomerDistrictService(); + if ( isset( $_POST['kiriof_destination_area'] ) ) { + $district_service->save( + get_current_user_id(), + 'billing', + wp_unslash( $_POST['kiriof_destination_area'] ), + isset( $_POST['kiriof_destination_area_name'] ) ? wp_unslash( $_POST['kiriof_destination_area_name'] ) : '' + ); + } + if ( ! empty( $_POST['ship_to_different_address'] ) ) { + $district_service->save( + get_current_user_id(), + 'shipping', + isset( $_POST['kiriof_shipping_destination_area'] ) ? wp_unslash( $_POST['kiriof_shipping_destination_area'] ) : '', + isset( $_POST['kiriof_shipping_destination_area_name'] ) ? wp_unslash( $_POST['kiriof_shipping_destination_area_name'] ) : '' + ); + } + } //save meta Insurance shipping woocommerce if( isset($data['kiriof_shipping_insurance']) && !empty($data['kiriof_shipping_insurance']) ) $order->update_meta_data( '_shipping_kiriof_insurance', sanitize_text_field( ( wp_unslash($data['kiriof_shipping_insurance']) == true ) ? 'yes' : '' ) ); @@ -1577,17 +1601,21 @@ private function kiriof_remove_fields_checkout($fields,$fields_selected){ } private function kiriof_add_field_subdistrict( $fields ){ $field_key = $this->field_destination_key; + $district_service = new \KiriminAjaOfficial\Services\CustomerDistrictService(); + $customer = isset( WC()->customer ) && WC()->customer instanceof \WC_Customer ? WC()->customer : get_current_user_id(); + $saved_billing = $district_service->get( $customer, 'billing' ); + $saved_shipping = $district_service->get( $customer, 'shipping' ); //billing session - $destination_id = WC()->session->get('destination_id') ?? ''; - $destination_name = WC()->session->get('destination_name') ?? ''; + $destination_id = WC()->session->get('destination_id') ?: $saved_billing['id']; + $destination_name = WC()->session->get('destination_name') ?: $saved_billing['name']; $options = array( '' => esc_html__( 'Select Option', 'kiriminaja-official' ) ); if ( ! empty( $destination_id ) ) { $options[ $destination_id ] = $destination_name; } //shipping session - $shipping_dest_id = WC()->session->get('shipping_destination_id') ?? ''; - $shipping_dest_name = WC()->session->get('shipping_destination_name') ?? ''; + $shipping_dest_id = WC()->session->get('shipping_destination_id') ?: $saved_shipping['id']; + $shipping_dest_name = WC()->session->get('shipping_destination_name') ?: $saved_shipping['name']; $shipping_options = array( '' => esc_html__( 'Select Option', 'kiriminaja-official' ) ); if ( ! empty( $shipping_dest_id ) ) { $shipping_options[ $shipping_dest_id ] = $shipping_dest_name; @@ -2018,16 +2046,18 @@ public function kiriof_store_api_update_checkout( $data ) { // meta, NOT from WC()->session, so session-only storage leaves the // field empty in the cart response and can cause empty shipping rates. if ( isset( WC()->customer ) && is_object( WC()->customer ) ) { - if ( $destination_id > 0 ) { - WC()->customer->update_meta_data( 'shipping_kiriminaja-official/kiriof_destination_area', (string) $destination_id ); - WC()->customer->update_meta_data( 'billing_kiriminaja-official/kiriof_destination_area', (string) $destination_id ); - } else { - WC()->customer->update_meta_data( 'shipping_kiriminaja-official/kiriof_destination_area', '' ); - WC()->customer->update_meta_data( 'billing_kiriminaja-official/kiriof_destination_area', '' ); - } - WC()->customer->update_meta_data( 'shipping_kiriminaja-official/kiriof_destination_area_name', $destination_name ); - WC()->customer->update_meta_data( 'billing_kiriminaja-official/kiriof_destination_area_name', $destination_name ); - WC()->customer->save_meta_data(); + ( new \KiriminAjaOfficial\Services\CustomerDistrictService() )->save( + WC()->customer, + 'shipping', + $destination_id, + $destination_name + ); + ( new \KiriminAjaOfficial\Services\CustomerDistrictService() )->save( + WC()->customer, + 'billing', + $destination_id, + $destination_name + ); } WC()->session->set( 'kiriof_insurance', $insurance ); diff --git a/inc/Init.php b/inc/Init.php index 3b3d2a5..562599c 100644 --- a/inc/Init.php +++ b/inc/Init.php @@ -25,6 +25,7 @@ public static function get_services(){ Controllers\TransactionProcessController::class, Controllers\ShippingDiscountCouponController::class, Controllers\CheckoutController::class, + Controllers\AccountAddressController::class, Controllers\TrackingFrontPageController::class, Controllers\EditOrderController::class, Controllers\CodAdjustmentController::class, @@ -51,4 +52,4 @@ public static function register_services(){ private static function instantiate($class ){ return new $class(); } -} \ No newline at end of file +} diff --git a/inc/Services/CustomerDistrictService.php b/inc/Services/CustomerDistrictService.php new file mode 100644 index 0000000..7e2ba50 --- /dev/null +++ b/inc/Services/CustomerDistrictService.php @@ -0,0 +1,86 @@ +normalizeAddressType( $address_type ); + $id = $this->readMeta( $customer, $address_type . '_' . self::FIELD_ID ); + $name = $this->readMeta( $customer, $address_type . '_' . self::FIELD_NAME ); + + if ( '' === $id ) { + $id = $this->readMeta( $customer, $address_type . '_kiriminaja-official/' . self::FIELD_ID ); + } + if ( '' === $name ) { + $name = $this->readMeta( $customer, $address_type . '_kiriminaja-official/' . self::FIELD_NAME ); + } + + return array( + 'id' => sanitize_text_field( $id ), + 'name' => sanitize_text_field( $name ), + ); + } + + /** + * Persist canonical and Blocks-compatible District metadata. + * + * @param \WC_Customer|int $customer Customer object or user ID. + * @param string $address_type Billing or shipping. + * @param string|int $district_id KiriminAja District ID. + * @param string $district_name District display label. + */ + public function save( $customer, string $address_type, $district_id, string $district_name ): void { + $address_type = $this->normalizeAddressType( $address_type ); + $district_id = absint( $district_id ); + $district_name = sanitize_text_field( $district_name ); + $values = array( + $address_type . '_' . self::FIELD_ID => $district_id > 0 ? (string) $district_id : '', + $address_type . '_' . self::FIELD_NAME => $district_name, + $address_type . '_kiriminaja-official/' . self::FIELD_ID => $district_id > 0 ? (string) $district_id : '', + $address_type . '_kiriminaja-official/' . self::FIELD_NAME => $district_name, + ); + + if ( $customer instanceof \WC_Customer ) { + foreach ( $values as $key => $value ) { + $customer->update_meta_data( $key, $value ); + } + $customer->save_meta_data(); + return; + } + + $user_id = absint( $customer ); + if ( $user_id < 1 ) { + return; + } + foreach ( $values as $key => $value ) { + update_user_meta( $user_id, $key, $value ); + } + } + + private function readMeta( $customer, string $key ): string { + if ( $customer instanceof \WC_Customer ) { + return (string) $customer->get_meta( $key, true ); + } + + $user_id = absint( $customer ); + return $user_id > 0 ? (string) get_user_meta( $user_id, $key, true ) : ''; + } + + private function normalizeAddressType( string $address_type ): string { + return 'shipping' === $address_type ? 'shipping' : 'billing'; + } +} diff --git a/lang/kiriminaja-official-id_ID.po b/lang/kiriminaja-official-id_ID.po index 12ddca0..6f88eaf 100644 --- a/lang/kiriminaja-official-id_ID.po +++ b/lang/kiriminaja-official-id_ID.po @@ -1252,6 +1252,10 @@ msgstr "Pilih Kecamatan" msgid "Select Option" msgstr "Pilih Opsi" +#: inc/Controllers/AccountAddressController.php +msgid "Please select a District." +msgstr "Silakan pilih Kecamatan." + #: templates/woocommerce/cart/shipping-calculator.php:30 msgid "Select a country / region…" msgstr "Pilih negara / wilayah…" diff --git a/tests/MyAccountDistrictTest.php b/tests/MyAccountDistrictTest.php new file mode 100644 index 0000000..b852d5e --- /dev/null +++ b/tests/MyAccountDistrictTest.php @@ -0,0 +1,74 @@ +assertStringContainsString("woocommerce_address_to_edit", $content); + $this->assertStringContainsString("woocommerce_after_save_address_validation", $content); + $this->assertStringContainsString("woocommerce_customer_save_address", $content); + $this->assertStringContainsString("is_wc_endpoint_url( 'edit-address' )", $content); + $this->assertStringContainsString("\$address_type . '_kiriof_destination_area'", $content); + } + + #[Test] + public function account_district_service_reads_and_writes_canonical_and_legacy_metadata(): void + { + $content = file_get_contents(PLUGIN_DIR . '/inc/Services/CustomerDistrictService.php'); + + $this->assertStringContainsString("\$address_type . '_' . self::FIELD_ID", $content); + $this->assertStringContainsString("\$address_type . '_kiriminaja-official/' . self::FIELD_ID", $content); + $this->assertStringContainsString("update_user_meta", $content); + $this->assertStringContainsString("update_meta_data", $content); + } + + #[Test] + public function account_district_script_supports_search_selection_and_postcode_invalidation(): void + { + $content = file_get_contents(PLUGIN_DIR . '/assets/wp/js/account-address.js'); + + $this->assertStringContainsString('kiriminaja_subdistrict_search', $content); + $this->assertStringContainsString('kiriofAjax.nonce', $content); + $this->assertStringContainsString('select2:select.kiriofAccountDistrict', $content); + $this->assertStringContainsString('select2:clear.kiriofAccountDistrict', $content); + $this->assertStringContainsString('#billing_postcode, #shipping_postcode', $content); + $this->assertStringContainsString('clearDistrict', $content); + } + + #[Test] + public function checkout_footer_district_renderer_is_not_global(): void + { + $content = file_get_contents(PLUGIN_DIR . '/inc/Controllers/CheckoutController.php'); + $start = strpos($content, 'function add_custom_select_options_field_and_script'); + $end = strpos($content, 'private function kiriof_render_virtual_cart_district_cleanup', $start); + $method = substr($content, $start, $end - $start); + + $this->assertStringContainsString('if ( ! is_cart() && ! is_checkout() )', $method); + } + + #[Test] + public function checkout_uses_customer_district_as_fallback_and_persists_canonical_meta(): void + { + $content = file_get_contents(PLUGIN_DIR . '/inc/Controllers/CheckoutController.php'); + + $this->assertStringContainsString("CustomerDistrictService()", $content); + $this->assertStringContainsString("\$district_service->get( \$customer, 'billing' )", $content); + $this->assertStringContainsString("\$district_service->get( \$customer, 'shipping' )", $content); + $this->assertStringContainsString("\$district_service->save(", $content); + } + + #[Test] + public function shipping_method_prefers_canonical_shipping_district_metadata(): void + { + $content = file_get_contents(PLUGIN_DIR . '/wc/KiriminajaShippingMethod.php'); + + $this->assertStringContainsString("'shipping_kiriof_destination_area'", $content); + $this->assertStringContainsString("'shipping_kiriminaja-official/kiriof_destination_area'", $content); + } +} diff --git a/tests/ShopVerseBlockCheckoutCompatibilityTest.php b/tests/ShopVerseBlockCheckoutCompatibilityTest.php index 1cf5c29..37bcc94 100644 --- a/tests/ShopVerseBlockCheckoutCompatibilityTest.php +++ b/tests/ShopVerseBlockCheckoutCompatibilityTest.php @@ -1901,7 +1901,9 @@ public function store_api_update_callback_does_not_overwrite_visible_block_postc $content = file_get_contents(PLUGIN_DIR . '/inc/Controllers/CheckoutController.php'); $start = strpos($content, 'public function kiriof_store_api_update_checkout'); $this->assertNotFalse($start, 'Store API update callback must exist'); - $methodBody = substr($content, $start, 6000); + $end = strpos($content, 'public function kiriof_ajax_session_save', $start); + $this->assertNotFalse($end, 'Store API update callback boundary must exist'); + $methodBody = substr($content, $start, $end - $start); $this->assertStringContainsString( "WC()->session->set( 'kiriof_checkout_postcode', \$postcode );", @@ -1928,7 +1930,9 @@ public function store_api_update_callback_recalculates_totals_after_block_destin $content = file_get_contents(PLUGIN_DIR . '/inc/Controllers/CheckoutController.php'); $start = strpos($content, 'public function kiriof_store_api_update_checkout'); $this->assertNotFalse($start, 'Store API update callback must exist'); - $methodBody = substr($content, $start, 6000); + $end = strpos($content, 'public function kiriof_ajax_session_save', $start); + $this->assertNotFalse($end, 'Store API update callback boundary must exist'); + $methodBody = substr($content, $start, $end - $start); $destinationPosition = strpos($methodBody, "WC()->session->set( 'kiriof_destination_area', \$destination_id );"); $calculatePosition = strpos($methodBody, 'WC()->cart->calculate_totals();'); diff --git a/wc/KiriminajaShippingMethod.php b/wc/KiriminajaShippingMethod.php index d8070d7..4be1c0b 100644 --- a/wc/KiriminajaShippingMethod.php +++ b/wc/KiriminajaShippingMethod.php @@ -114,6 +114,7 @@ public function calculate_shipping( $package = array() ){ try { if ( isset( WC()->customer ) && is_object( WC()->customer ) ) { $meta_keys = array( + 'shipping_kiriof_destination_area', 'shipping_kiriminaja-official/kiriof_destination_area', 'kiriminaja-official/kiriof_destination_area', '_wc_blocks_checkout_field_kiriminaja-official/kiriof_destination_area',