id = 'sdfw_deposits'; $this->label = __( 'Deposits', 'aawahid-simple-deposits-for-woocommerce' ); parent::__construct(); // Custom repeater field type + its sanitiser (WooCommerce core hooks). add_action( 'woocommerce_admin_field_sdfw_tiers', [ $this, 'output_tiers_field' ] ); add_filter( 'woocommerce_admin_settings_sanitize_option_sdfw_deposit_tiers', [ $this, 'sanitize_tiers_option' ], 10, 3 ); } /** * Own sections for this tab — modern WC override (WC 8.x+). * WooCommerce merges this with any filter-added sections via * the parent get_sections() method automatically. */ protected function get_own_sections(): array { return [ '' => __( 'General', 'aawahid-simple-deposits-for-woocommerce' ), 'gateways' => __( 'Eligible Gateways', 'aawahid-simple-deposits-for-woocommerce' ), ]; } /** * Backwards-compatible sections getter for WC versions that call * get_sections() directly instead of get_own_sections(). */ public function get_sections(): array { $sections = $this->get_own_sections(); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- 'woocommerce_get_sections_{$this->id}' is WC_Settings_Page's own required extensibility hook naming convention, identical to every core WC settings tab (e.g. woocommerce_get_sections_general); the tab id ('sdfw_deposits') is already part of the hook name. return apply_filters( 'woocommerce_get_sections_' . $this->id, $sections ); } /** * Field definitions for the current section. Only the General * section uses WooCommerce's standard settings-array renderer — * the Gateways section is rendered manually via output_gateway_fields() * because its checkboxes need non-standard field names (see output() * below), so get_settings() only needs to return real field * definitions for the General section. */ public function get_settings( $current_section = '' ): array { if ( 'gateways' === $current_section ) { return []; } $settings = $this->get_general_settings(); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- 'woocommerce_get_settings_{$this->id}' is WC_Settings_Page's own required extensibility hook naming convention, identical to every core WC settings tab; the tab id ('sdfw_deposits') is already part of the hook name. return apply_filters( 'woocommerce_get_settings_' . $this->id, $settings, $current_section ); } /** * General section: on/off switch, global amount, notice text. */ private function get_general_settings(): array { return [ [ 'title' => __( 'COD Partial Payment', 'aawahid-simple-deposits-for-woocommerce' ), 'type' => 'title', 'desc' => __( 'Collect a partial advance payment online and the remainder as Cash on Delivery. When a cart contains multiple products with different partial amounts, only the single highest amount is charged as the advance — never the sum.', 'aawahid-simple-deposits-for-woocommerce' ), 'id' => 'sdfw_general_title', ], [ 'title' => __( 'Enable Partial Payment', 'aawahid-simple-deposits-for-woocommerce' ), 'desc' => __( 'Enable COD partial payment collection site-wide', 'aawahid-simple-deposits-for-woocommerce' ), 'id' => 'sdfw_enabled', 'default' => 'yes', 'type' => 'checkbox', ], [ 'title' => __( 'Global Advance Amount', 'aawahid-simple-deposits-for-woocommerce' ), 'desc' => __( 'Fallback deposit (used when tiers are off or no tier matches) and the minimum order total for a deposit split. Orders below this amount must be paid in full online. Products with their own amount always use that instead.', 'aawahid-simple-deposits-for-woocommerce' ), 'id' => 'sdfw_global_amount', 'default' => '200', 'type' => 'number', 'desc_tip' => true, 'custom_attributes' => [ 'min' => '0', 'step' => '0.01', ], ], [ 'title' => __( 'Checkout Notice Text', 'aawahid-simple-deposits-for-woocommerce' ), 'desc' => __( 'Shown to the customer at checkout. Use {amount} as a placeholder for the formatted advance amount.', 'aawahid-simple-deposits-for-woocommerce' ), 'id' => 'sdfw_notice_text', 'default' => sdfw_get_default_notice_text(), 'type' => 'textarea', 'css' => 'min-width:400px; min-height: 75px;', 'desc_tip' => true, ], [ 'type' => 'sectionend', 'id' => 'sdfw_general_title', ], [ 'title' => __( 'Tiered Deposit Rules', 'aawahid-simple-deposits-for-woocommerce' ), 'type' => 'title', 'desc' => __( 'Set the deposit based on the order total. The matching tier replaces the Global Advance Amount; products with their own deposit amount still override it. Cart totals that fall in no tier use the Global Advance Amount.', 'aawahid-simple-deposits-for-woocommerce' ), 'id' => 'sdfw_tiers_title', ], [ 'title' => __( 'Enable Tiers', 'aawahid-simple-deposits-for-woocommerce' ), 'desc' => __( 'Choose the deposit amount by order total using the rules below', 'aawahid-simple-deposits-for-woocommerce' ), 'id' => 'sdfw_tiers_enabled', 'default' => 'no', 'type' => 'checkbox', ], [ 'title' => __( 'Deposit Tiers', 'aawahid-simple-deposits-for-woocommerce' ), 'id' => 'sdfw_deposit_tiers', 'type' => 'sdfw_tiers', ], [ 'type' => 'sectionend', 'id' => 'sdfw_tiers_title', ], [ 'title' => __( 'Deposit Processing Fee', 'aawahid-simple-deposits-for-woocommerce' ), 'type' => 'title', 'desc' => __( 'Optional extra fee charged only when the customer pays the deposit + COD split — never charged on full-payment orders or on orders forced into full payment by the minimum-order threshold. Added to the amount due on delivery; the deposit charged online is unaffected.', 'aawahid-simple-deposits-for-woocommerce' ), 'id' => 'sdfw_fee_title', ], [ 'title' => __( 'Enable Processing Fee', 'aawahid-simple-deposits-for-woocommerce' ), 'desc' => __( 'Charge an extra processing fee on deposit + COD orders', 'aawahid-simple-deposits-for-woocommerce' ), 'id' => 'sdfw_processing_fee_enabled', 'default' => 'no', 'type' => 'checkbox', ], [ 'title' => __( 'Fee Type', 'aawahid-simple-deposits-for-woocommerce' ), 'desc' => __( 'Whether the fee below is a flat currency amount or a percentage of the resolved deposit.', 'aawahid-simple-deposits-for-woocommerce' ), 'id' => 'sdfw_processing_fee_type', 'default' => 'flat', 'type' => 'select', 'options' => [ 'flat' => __( 'Flat amount', 'aawahid-simple-deposits-for-woocommerce' ), 'percent' => __( 'Percentage of deposit', 'aawahid-simple-deposits-for-woocommerce' ), ], 'desc_tip' => true, ], [ 'title' => __( 'Fee Amount', 'aawahid-simple-deposits-for-woocommerce' ), 'desc' => __( 'A flat currency amount, or a percentage (e.g. 5 = 5%) depending on Fee Type above.', 'aawahid-simple-deposits-for-woocommerce' ), 'id' => 'sdfw_processing_fee_amount', 'default' => '0', 'type' => 'number', 'desc_tip' => true, 'custom_attributes' => [ 'min' => '0', 'step' => '0.01', ], ], [ 'type' => 'sectionend', 'id' => 'sdfw_fee_title', ], [ 'title' => __( 'Uninstall', 'aawahid-simple-deposits-for-woocommerce' ), 'type' => 'title', 'desc' => __( 'Controls what happens to this plugin\'s settings and data if it is ever deleted (not just deactivated) from the Plugins screen.', 'aawahid-simple-deposits-for-woocommerce' ), 'id' => 'sdfw_uninstall_title', ], [ 'title' => __( 'Remove Data on Uninstall', 'aawahid-simple-deposits-for-woocommerce' ), 'desc' => __( 'Permanently delete all plugin settings and per-product deposit overrides when this plugin is deleted. Leave unchecked to keep them in case you reinstall later.', 'aawahid-simple-deposits-for-woocommerce' ), 'id' => 'sdfw_remove_data_on_uninstall', 'default' => 'no', 'type' => 'checkbox', ], [ 'type' => 'sectionend', 'id' => 'sdfw_uninstall_title', ], ]; } /** * Override save() for the gateways section only, since checkbox * arrays (sdfw_allowed_gateways[gateway_id]) need to be collapsed * into a single option value rather than saved as individual * woocommerce_* options the way WC_Settings_Page does by default. */ public function save(): void { global $current_section; if ( 'gateways' === $current_section ) { $this->save_gateway_settings(); return; } parent::save(); } /** * Persist the gateway checkboxes as a single array option, * sdfw_allowed_gateways, keyed by enabled gateway IDs. * * Note: the nonce ('woocommerce-settings') has already been * verified by WC_Admin_Settings::save() before this method is * ever reached, so no separate check_admin_referer() call is * needed here. */ private function save_gateway_settings(): void { $available_gateways = WC()->payment_gateways() ? WC()->payment_gateways()->payment_gateways() : []; $allowed = []; foreach ( $available_gateways as $gateway ) { if ( 'cod' === $gateway->id ) { continue; } $field_key = 'sdfw_allowed_gateways_' . $gateway->id; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce already verified by WC_Admin_Settings::save() before this method runs. if ( isset( $_POST[ $field_key ] ) ) { $allowed[] = $gateway->id; } } update_option( 'sdfw_allowed_gateways', $allowed ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- 'woocommerce_update_options_{$this->id}' is WC_Settings_Page's own required extensibility hook naming convention, identical to every core WC settings tab; the tab id ('sdfw_deposits') is already part of the hook name. do_action( 'woocommerce_update_options_' . $this->id ); } /** * WooCommerce's generate_checkbox_html() expects flat field IDs, * not array-style IDs, to round-trip correctly with its own * save() — so we render the gateway checkboxes with corrected * field names ourselves rather than fighting the default renderer. */ public function output(): void { global $current_section; if ( 'gateways' === $current_section ) { $this->output_gateway_fields(); return; } parent::output(); } /** * Custom render for the gateways section so each checkbox posts * as sdfw_allowed_gateways_{id} (matching save_gateway_settings()) * instead of the array-style name WC_Admin_Settings would expect. */ private function output_gateway_fields(): void { echo '

' . esc_html__( 'Eligible Online Gateways', 'aawahid-simple-deposits-for-woocommerce' ) . '

'; echo '

' . esc_html__( 'Choose which of your active online payment gateways are allowed to collect the advance payment (e.g. bKash, Nagad, SSLCommerz). Cash on Delivery should NOT be selected here — it is always used for the remaining balance.', 'aawahid-simple-deposits-for-woocommerce' ) . '

'; $available_gateways = WC()->payment_gateways() ? WC()->payment_gateways()->payment_gateways() : []; $allowed = (array) get_option( 'sdfw_allowed_gateways', [] ); if ( empty( $available_gateways ) ) { echo '

' . esc_html__( 'No payment gateways are currently active. Activate at least one online gateway (e.g. bKash, Nagad, SSLCommerz) in WooCommerce → Settings → Payments first.', 'aawahid-simple-deposits-for-woocommerce' ) . '

'; return; } echo ''; foreach ( $available_gateways as $gateway ) { if ( 'cod' === $gateway->id ) { continue; } $field_key = 'sdfw_allowed_gateways_' . $gateway->id; $checked = in_array( $gateway->id, $allowed, true ); ?> '; wp_nonce_field( 'woocommerce-settings' ); } // ──────────────────────────────────────────────────────────────── // Tiered deposit rules — repeater field // ──────────────────────────────────────────────────────────────── /** * Render the tier repeater (custom WC settings field type 'sdfw_tiers'). * * @param array $value Field definition from get_general_settings(). */ public function output_tiers_field( array $value ): void { $tiers = sdfw_get_tiers(); $symbol = get_woocommerce_currency_symbol(); ?> that admin.js clones when "+ Add Tier" is clicked. * * @param string $index Array key used in the input names. * @param float|string $min * @param float|string $max * @param float|string $amount */ private function render_tier_row( string $index, $min, $max, $amount ): void { $base = 'sdfw_deposit_tiers[' . $index . ']'; ?> $tiers[ $i ]['min'] ) { WC_Admin_Settings::add_error( __( 'Deposit tier ranges overlap. The first matching tier (lowest "Order total from") is used — please check your rules.', 'aawahid-simple-deposits-for-woocommerce' ) ); break; } } } return $tiers; } }
$tier ) { $this->render_tier_row( (string) $i, $tier['min'], $tier['max'], $tier['amount'] ); } ?>