Files
formipay/includes/Shipping/Shipping.php
dwindown 008188b790 feat: migrate shipping to form-level and integrate flags.json as single source of truth
Shipping Migration:
- Move shipping configuration from product-level to form-level
- Add form shipping tab in form settings (no_shipping, flat_rate, free_shipping)
- Update FlatRate to register at form level instead of product level
- Update checkout logic to read from form settings
- Support percentage-based flat rate calculation
- Simplify shipping method IDs (flat_rate, free_shipping)

Currency Flags Integration:
- Add formipay_get_all_currency_flags() to read from admin/assets/json/flags.json
- Remove hardcoded CURRENCY_FLAGS emoji map from VariationField.js
- Create CurrencyFlag component to render base64 flag images
- Localize currency_flags to window.formipayProductDetails
- Update shipping info display in admin order details

Benefits:
- Form-level shipping prevents multiplying shipping costs per product
- Single source of truth for currency flags (flags.json)
- Better support for future cart system
- Consistent with e-commerce standards
2026-04-23 08:12:40 +07:00

734 lines
28 KiB
PHP

<?php
namespace Formipay\Shipping;
use Formipay\Traits\SingletonTrait;
if ( ! defined( 'ABSPATH' ) ) exit;
/**
* Abstract Shipping Class - Core Carrier Extension System
*
* This class provides the hook system for carrier API extensions.
* Carriers like Rajaongkir, Biteship, etc. can extend Formipay shipping
* by registering themselves via the provided hooks.
*
* @phase 4 - Carrier API Extension System
*/
abstract class Shipping {
use SingletonTrait;
abstract public function add_shipping_method($shipping_methods);
abstract public function add_shipping_settings($fields);
abstract public function add_shipping_to_order_details($details, $form_id, $order_data);
protected function __construct() {
// Phase 1-3: Core shipping functionality
add_filter( 'formipay/global-settings', [$this, 'add_setting_shipping_menu'], 15 );
add_filter( 'formipay/form-config', [$this, 'add_form_shipping_config'], 75 );
add_filter( 'formipay/global-settings/tab:shipping', [$this, 'add_global_shipping_settings'], 15 );
// Phase 4: Carrier Extension Hooks
// Carrier registration - allows carriers to register themselves
add_filter( 'formipay/shipping/carriers', '__return_empty_array', 5 );
// Carrier API keys - inject into global shipping settings
add_filter( 'formipay/global-settings/tab:shipping', [$this, 'add_carrier_api_settings'], 20 );
// Checkout address fields - inject carrier-specific address fields
add_filter( 'formipay/checkout/shipping-address-fields', [$this, 'get_carrier_address_fields'], 10, 3 );
// Live rate fetching - allow carriers to provide real-time rates
add_filter( 'formipay/shipping/live-rates', '__return_empty_array', 10, 4 );
// AJAX handler for testing carrier connection
add_action( 'wp_ajax_formipay_test_carrier_connection', [$this, 'ajax_test_carrier_connection'], 10 );
add_action( 'wp_ajax_nopriv_formipay_test_carrier_connection', [$this, 'ajax_test_carrier_connection'], 10 );
// Phase 5: Checkout Integration
// AJAX endpoint for getting available shipping methods for checkout
add_action( 'wp_ajax_formipay_get_shipping_methods', [$this, 'ajax_get_shipping_methods'], 10 );
add_action( 'wp_ajax_nopriv_formipay_get_shipping_methods', [$this, 'ajax_get_shipping_methods'], 10 );
// AJAX endpoint for getting supported countries
add_action( 'wp_ajax_formipay_get_supported_countries', [$this, 'ajax_get_supported_countries'], 10 );
add_action( 'wp_ajax_nopriv_formipay_get_supported_countries', [$this, 'ajax_get_supported_countries'], 10 );
// Hook to add shipping cost to cart calculation
add_filter( 'formipay/checkout/cart/calculation', [$this, 'add_shipping_to_cart'], 10, 3 );
// Hook to add shipping data to order submission
add_filter( 'formipay/order/process-data', [$this, 'add_shipping_to_order_data'], 10, 2 );
}
public function add_setting_shipping_menu($fields){
$shipping_settings = [];
$shipping_settings = apply_filters( 'formipay/global-settings/tab:shipping', $shipping_settings );
if(!empty($shipping_settings)){
$fields['shipping'] = array(
'name' => __('Shipping', 'formipay'),
'fields' => $shipping_settings
);
}
return $fields;
}
/**
* Add global shipping settings fields
* This implements Phase 3 of the shipping module: Global Shipping Settings
*/
public function add_global_shipping_settings($fields) {
// Load countries from JSON file
$countries_json = FORMIPAY_PATH . 'admin/assets/json/country.json';
$countries = file_exists($countries_json) ? json_decode(file_get_contents($countries_json), true) : [];
$country_options = [];
if (is_array($countries)) {
foreach ($countries as $country) {
$code = $country['code'] ?? '';
$name = $country['name'] ?? '';
if ($code && $name) {
$country_options[$code] = $name;
}
}
}
// Store Origin Section
$fields['shipping_origin_group'] = array(
'type' => 'group_title',
'label' => __( 'Store Origin', 'formipay' ),
'description' => __( 'Your business location for shipping calculations', 'formipay' ),
'group' => 'started'
);
$fields['shipping_origin_country'] = array(
'type' => 'select',
'label' => __( 'Origin Country', 'formipay' ),
'options' => $country_options,
'searchable' => true,
'description' => __( 'Select the country where your products ship from', 'formipay' ),
);
$fields['shipping_weight_unit'] = array(
'type' => 'select',
'label' => __( 'Weight Unit', 'formipay' ),
'options' => array(
'kg' => __( 'Kilograms (kg)', 'formipay' ),
'g' => __( 'Grams (g)', 'formipay' ),
'lb' => __( 'Pounds (lb)', 'formipay' ),
'oz' => __( 'Ounces (oz)', 'formipay' ),
),
'value' => 'kg',
);
$fields['shipping_dimension_unit'] = array(
'type' => 'select',
'label' => __( 'Dimension Unit', 'formipay' ),
'options' => array(
'cm' => __( 'Centimeters (cm)', 'formipay' ),
'in' => __( 'Inches (in)', 'formipay' ),
'm' => __( 'Meters (m)', 'formipay' ),
),
'value' => 'cm',
'group' => 'ended'
);
// Shipping Calculation Method
$fields['shipping_calculation_group'] = array(
'type' => 'group_title',
'label' => __( 'Shipping Calculation', 'formipay' ),
'description' => __( 'How shipping costs are calculated for orders', 'formipay' ),
'group' => 'started'
);
$fields['shipping_calculation_method'] = array(
'type' => 'select',
'label' => __( 'Calculation Method', 'formipay' ),
'options' => array(
'per_order' => __( 'Per Order (single shipping fee for entire order)', 'formipay' ),
'per_item' => __( 'Per Item (shipping fee multiplied by quantity)', 'formipay' ),
),
'value' => 'per_order',
'group' => 'ended'
);
// Supported Destinations Section
$fields['shipping_destinations_group'] = array(
'type' => 'group_title',
'label' => __( 'Supported Destinations', 'formipay' ),
'description' => __( 'Configure which countries you ship to and their shipping rates', 'formipay' ),
'group' => 'started'
);
// Get enabled currencies for flat rate table
$formipay_settings = get_option('formipay_settings', []);
$enabled_currencies = [];
if (!empty($formipay_settings['multicurrencies']) && is_array($formipay_settings['multicurrencies'])) {
foreach ($formipay_settings['multicurrencies'] as $currency) {
if (isset($currency['currency'])) {
$parts = explode(':::', $currency['currency']);
$enabled_currencies[] = [
'code' => $parts[0] ?? '',
'title' => $parts[1] ?? '',
'symbol' => $parts[2] ?? $parts[0] ?? '',
];
}
}
}
// Fallback to default currency if multicurrency is not enabled
if (empty($enabled_currencies)) {
$default_currency = $formipay_settings['default_currency'] ?? 'IDR:::Indonesian rupiah:::Rp';
$parts = explode(':::', $default_currency);
$enabled_currencies[] = [
'code' => $parts[0] ?? 'IDR',
'title' => $parts[1] ?? 'Indonesian rupiah',
'symbol' => $parts[2] ?? 'Rp',
];
}
// Build currency amount fields for the repeater
$currency_fields = [];
foreach ($enabled_currencies as $curr) {
$code = $curr['code'];
$symbol = $curr['symbol'];
$currency_fields['flat_rate_' . $code] = array(
'type' => 'number',
'label' => sprintf(__( 'Flat Rate (%s)', 'formipay' ), $code),
'step' => 0.01,
'min' => 0,
'placeholder' => '0.00',
);
}
// Build free shipping threshold field (use primary currency)
$primary_currency = $enabled_currencies[0] ?? [];
$primary_symbol = $primary_currency['symbol'] ?? '';
$fields['shipping_destinations'] = array(
'type' => 'repeater',
'label' => __( 'Destinations', 'formipay' ),
'description' => __( 'Add countries you ship to and configure their shipping options', 'formipay' ),
'fields' => array_merge(
[
'country' => array(
'type' => 'select',
'label' => __('Country', 'formipay'),
'options' => $country_options,
'required' => true,
'searchable' => true,
'is_group_title' => true
),
'rate_source' => array(
'type' => 'select',
'label' => __('Rate Source', 'formipay'),
'options' => array(
'flat_rate' => __( 'Flat Rate', 'formipay' ),
// 'api' => __( 'Carrier API', 'formipay' ), // Phase 4
),
'value' => 'flat_rate',
),
],
$currency_fields,
[
'free_shipping_threshold' => array(
'type' => 'number',
'label' => sprintf(__( 'Free Shipping Threshold (%s)', 'formipay' ), $primary_symbol),
'description' => __( 'Order amount above which shipping is free. Leave empty to disable.', 'formipay' ),
'step' => 0.01,
'min' => 0,
'placeholder' => 'Empty = disabled',
),
]
)
);
// Note: Carrier API settings will be added in Phase 4
$fields['carrier_api_note'] = array(
'type' => 'notification_message',
'description' => __( '
<h3>Carrier API Integration</h3>
<p>Live carrier rates (Rajaongkir, Biteship, etc.) will be available in Phase 4 of the shipping module.</p>
<p>Currently, only Flat Rate and Free Shipping methods are available at the product level.</p>
', 'formipay' ),
);
return $fields;
}
/**
* Add shipping configuration to form settings
* This replaces product-level shipping with form-level shipping
*/
public function add_form_shipping_config($fields) {
$shipping_methods = apply_filters( 'formipay/form-settings/tab:shipping/method', [
'no_shipping' => [
'method' => __( 'No Shipping Required', 'formipay' )
],
'flat_rate' => [
'method' => __( 'Flat Rate', 'formipay' )
],
'free_shipping' => [
'method' => __( 'Free Shipping', 'formipay' )
]
] );
$shipping_options = [];
foreach($shipping_methods as $id => $shipping){
$label = $shipping['method'];
if(isset($shipping['courier'])){
$label .= ' - '.$shipping['courier'];
if(isset($shipping['service'])){
$label .= ' - '.$shipping['service'];
}
}
$shipping_options[$id] = $label;
}
// Main shipping configuration group
$shipping_config_group = [
'shipping_enable_group' => [
'type' => 'group_title',
'label' => __( 'Shipping Configuration', 'formipay' ),
'description' => __( 'Configure shipping options for this form. Shipping will be calculated based on form settings, not per-product.', 'formipay' ),
'group' => 'started'
],
'shipping_enabled' => [
'type' => 'radio',
'label' => __( 'Shipping Method', 'formipay' ),
'options' => $shipping_options,
'value' => 'no_shipping',
'description' => __( 'Select how shipping should be handled for orders from this form', 'formipay' ),
]
];
$shipping_config_group = apply_filters( 'formipay/form-settings/tab:shipping/group:config', $shipping_config_group );
$last_config_key = array_key_last($shipping_config_group);
$shipping_config_group[$last_config_key]['group'] = 'ended';
// Apply carrier-specific settings (Flat Rate, etc.)
$carrier_settings = apply_filters( 'formipay/form-settings/tab:shipping', [] );
$all_shipping_fields = array_merge($shipping_config_group, $carrier_settings);
$fields['formipay_form_settings']['shipping'] = [
'name' => __( 'Shipping', 'formipay' ),
'fields' => $all_shipping_fields
];
return $fields;
}
/**
* =============================================
* PHASE 4: CARRIER EXTENSION HOOKS
* =============================================
*/
/**
* Add carrier API settings to global shipping settings
* Carriers can hook into `formipay/global-settings/tab:shipping/carriers` to add their API key fields
*
* @param array $fields Existing shipping settings fields
* @return array Updated fields with carrier API settings
*/
public function add_carrier_api_settings($fields) {
// Get all registered carriers
$carriers = apply_filters('formipay/shipping/carriers', []);
if (empty($carriers)) {
// No carriers registered, add info message
$fields['carrier_api_info'] = array(
'type' => 'notification_message',
'description' => __( '
<h3>Carrier API Integration</h3>
<p>To enable live shipping rates, install a carrier extension plugin.</p>
<p>Extensions register themselves via the <code>formipay/shipping/carriers</code> filter.</p>
', 'formipay' ),
);
return $fields;
}
// Add carrier API settings section
$fields['carrier_api_group'] = array(
'type' => 'group_title',
'label' => __( 'Carrier API Keys', 'formipay' ),
'description' => __( 'Configure API credentials for live shipping rate calculation', 'formipay' ),
'group' => 'started'
);
// Allow carriers to inject their API key fields
$carrier_fields = apply_filters('formipay/global-settings/tab:shipping/carriers', []);
foreach ($carrier_fields as $key => $field) {
$fields[$key] = $field;
}
// Add test connection buttons for each carrier
foreach ($carriers as $carrier_id => $carrier) {
if (isset($carrier['test_connection']) && $carrier['test_connection']) {
$fields['test_connection_' . $carrier_id] = array(
'type' => 'html',
'label' => __( 'Test Connection', 'formipay' ),
'html' => sprintf(
'<button type="button" class="button formipay-test-connection" data-carrier="%s">%s</button>
<span class="formipay-connection-result" style="margin-left: 10px;"></span>',
esc_attr($carrier_id),
esc_html__('Test Connection', 'formipay')
),
'group' => ($carrier_id === array_key_last($carriers)) ? 'ended' : null,
);
}
}
return $fields;
}
/**
* Get carrier-specific address fields for checkout
* Carriers can hook into `formipay/checkout/address-fields/{carrier_id}` to provide their fields
*
* @param array $fields Current address fields
* @param string $carrier_id Carrier identifier (e.g., 'rajaongkir', 'biteship')
* @param string $country_code Destination country code
* @return array Address fields for this carrier
*/
public function get_carrier_address_fields($fields, $carrier_id, $country_code) {
// Get carrier-specific address fields
$carrier_fields = apply_filters('formipay/checkout/address-fields/' . $carrier_id, [], $country_code);
return array_merge($fields, $carrier_fields);
}
/**
* AJAX handler for testing carrier connection
* Carriers can hook into `formipay/test_carrier_connection/{carrier_id}` to handle the test
*/
public function ajax_test_carrier_connection() {
check_ajax_referer('formipay-admin', 'nonce', true);
if (!current_user_can('manage_options')) {
wp_send_json_error(['message' => __('Unauthorized', 'formipay')]);
}
$carrier_id = isset($_POST['carrier']) ? sanitize_text_field(wp_unslash($_POST['carrier'])) : '';
if (empty($carrier_id)) {
wp_send_json_error(['message' => __('Missing carrier ID', 'formipay')]);
}
// Allow carriers to handle their own connection test
$result = apply_filters('formipay/test_carrier_connection/' . $carrier_id, [
'success' => false,
'message' => __('Carrier does not implement connection test', 'formipay'),
], $_POST);
if ($result['success']) {
wp_send_json_success($result);
} else {
wp_send_json_error($result);
}
}
/**
* Helper method: Get registered carriers
* Returns all carriers that have registered via the filter
*
* @return array Registered carriers
*/
public static function get_registered_carriers() {
return apply_filters('formipay/shipping/carriers', []);
}
/**
* Helper method: Get carrier by ID
*
* @param string $carrier_id Carrier identifier
* @return array|null Carrier data or null if not found
*/
public static function get_carrier($carrier_id) {
$carriers = self::get_registered_carriers();
return $carriers[$carrier_id] ?? null;
}
/**
* Helper method: Check if carrier supports a country
*
* @param string $carrier_id Carrier identifier
* @param string $country_code Country code to check
* @return bool True if carrier supports the country
*/
public static function carrier_supports_country($carrier_id, $country_code) {
$carrier = self::get_carrier($carrier_id);
if (!$carrier) {
return false;
}
$supported_countries = $carrier['countries'] ?? [];
return in_array($country_code, $supported_countries, true);
}
/**
* Helper method: Fetch live rates from carrier
*
* @param string $carrier_id Carrier identifier
* @param array $params Rate request parameters (origin, destination, weight, dimensions)
* @return array Available rates with costs
*/
public static function fetch_live_rates($carrier_id, $params) {
$default_params = [
'origin_country' => '',
'origin_city' => '',
'origin_postcode' => '',
'destination_country' => '',
'destination_city' => '',
'destination_postcode' => '',
'weight' => 0,
'weight_unit' => 'kg',
'length' => 0,
'width' => 0,
'height' => 0,
'dimension_unit' => 'cm',
];
$params = wp_parse_args($params, $default_params);
return apply_filters('formipay/shipping/live-rates', [], $carrier_id, $params);
}
/**
* =============================================
* PHASE 5: CHECKOUT INTEGRATION
* =============================================
*/
/**
* AJAX: Get available shipping methods for a form
* Now reads from form-level shipping settings instead of global settings
*/
public function ajax_get_shipping_methods() {
check_ajax_referer('formipay-public', 'nonce', true);
$form_id = isset($_POST['form_id']) ? intval($_POST['form_id']) : 0;
$country_code = isset($_POST['country']) ? sanitize_text_field(wp_unslash($_POST['country'])) : '';
$currency = isset($_POST['currency']) ? sanitize_text_field(wp_unslash($_POST['currency'])) : '';
if (!$form_id) {
wp_send_json_error(['message' => __('Invalid request', 'formipay')]);
}
// Get form shipping settings
$form_settings = get_post_meta($form_id, 'formipay_form_settings', true);
$shipping_enabled = $form_settings['shipping_enabled'] ?? 'no_shipping';
$available_methods = [];
if ($shipping_enabled === 'flat_rate') {
// Get flat rate from form settings
$currency_code = $currency ?: 'IDR';
$rate_key = 'flat_rate_amount_' . $currency_code;
$flat_rate = floatval($form_settings[$rate_key] ?? 0);
$flat_rate_type = $form_settings['flat_rate_type'] ?? 'fixed';
// For percentage, we'll calculate on frontend based on cart total
// For now, store the type so frontend knows how to handle it
$available_methods[] = [
'id' => 'flat_rate',
'name' => __('Standard Shipping', 'formipay'),
'description' => $flat_rate_type === 'percentage'
? sprintf(__('%s%% of order total', 'formipay'), $flat_rate)
: __('Delivery in 3-5 business days', 'formipay'),
'cost' => $flat_rate,
'currency' => $currency_code,
'type' => $flat_rate_type,
];
} elseif ($shipping_enabled === 'free_shipping') {
// Free shipping from form settings
$free_label = $form_settings['free_shipping_label'] ?? __('Free Shipping', 'formipay');
$available_methods[] = [
'id' => 'free_shipping',
'name' => $free_label,
'description' => __('No shipping cost', 'formipay'),
'cost' => 0,
'currency' => $currency_code ?? '',
];
}
// If country-specific shipping is needed in future, add check here
// For now, form-level shipping applies to all countries
if (empty($available_methods)) {
wp_send_json_error([
'message' => __('Shipping is not available for this form', 'formipay'),
'methods' => []
]);
}
wp_send_json_success([
'methods' => $available_methods,
'default_method' => $available_methods[0]['id'],
]);
}
/**
* Add shipping cost to cart calculation
* Hooked into formipay/checkout/cart/calculation
*/
public function add_shipping_to_cart($cart, $form_id, $selected_currency) {
// Check if shipping is enabled for this form
$form_settings = get_post_meta($form_id, 'formipay_form_settings', true);
$shipping_enabled = $form_settings['shipping_enabled'] ?? 'no_shipping';
if ($shipping_enabled === 'no_shipping') {
return $cart;
}
// Get selected shipping method from POST data or session
$shipping_method = isset($_POST['shipping_method']) ? sanitize_text_field(wp_unslash($_POST['shipping_method'])) : '';
$shipping_country = isset($_POST['shipping_country']) ? sanitize_text_field(wp_unslash($_POST['shipping_country'])) : '';
if (empty($shipping_method) || empty($shipping_country)) {
return $cart;
}
// Parse shipping method ID to get cost
// Format: flat_rate, free_shipping, or {carrier}_{service}
if ($shipping_method === 'free_shipping') {
// Free shipping
$cart['shipping'] = [
'name' => __('Free Shipping', 'formipay'),
'cost' => 0,
];
} elseif ($shipping_method === 'flat_rate') {
// Flat rate - get cost from form settings
$currency_code = $selected_currency ?: 'IDR';
$rate_key = 'flat_rate_amount_' . $currency_code;
$flat_rate = floatval($form_settings[$rate_key] ?? 0);
// Check if percentage
$flat_rate_type = $form_settings['flat_rate_type'] ?? 'fixed';
if ($flat_rate_type === 'percentage') {
$subtotal = floatval($cart['subtotal'] ?? 0);
$flat_rate = ($subtotal * $flat_rate) / 100;
}
$cart['shipping'] = [
'name' => __('Standard Shipping', 'formipay'),
'cost' => $flat_rate,
];
// Recalculate totals
$cart['subtotal'] = floatval($cart['subtotal'] ?? 0);
$cart['tax'] = floatval($cart['tax'] ?? 0);
$cart['discount'] = floatval($cart['discount'] ?? 0);
$cart['grand'] = $cart['subtotal'] + $cart['tax'] + $cart['shipping']['cost'] - $cart['discount'];
}
return $cart;
}
/**
* Add shipping data to order submission
* Hooked into formipay/order/process-data
*/
public function add_shipping_to_order_data($form_data, $form_id) {
// Check if shipping is enabled for this form
$form_settings = get_post_meta($form_id, 'formipay_form_settings', true);
$shipping_enabled = $form_settings['shipping_enabled'] ?? 'no_shipping';
if ($shipping_enabled === 'no_shipping') {
return $form_data;
}
// Add shipping info to form data
if (isset($_POST['shipping_method'])) {
$form_data['shipping_method'] = sanitize_text_field(wp_unslash($_POST['shipping_method']));
}
if (isset($_POST['shipping_country'])) {
$form_data['shipping_country'] = sanitize_text_field(wp_unslash($_POST['shipping_country']));
}
return $form_data;
}
/**
* AJAX: Get supported shipping countries
* With form-level shipping, returns all available countries
*/
public function ajax_get_supported_countries() {
// Verify nonce
if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'formipay_public_nonce')) {
wp_send_json_error(['message' => __('Invalid security token', 'formipay')]);
}
$form_id = isset($_POST['form_id']) ? intval($_POST['form_id']) : 0;
if (!$form_id) {
wp_send_json_error(['message' => __('Invalid form ID', 'formipay')]);
}
// Load countries from JSON file
$countries_json = FORMIPAY_PATH . 'admin/assets/json/country.json';
$all_countries = file_exists($countries_json) ? json_decode(file_get_contents($countries_json), true) : [];
// Build country list
$countries = [];
if (is_array($all_countries)) {
foreach ($all_countries as $country) {
$code = $country['code'] ?? '';
$name = $country['name'] ?? '';
if ($code && $name) {
$countries[$code] = $name;
}
}
}
if (empty($countries)) {
wp_send_json_error([
'message' => __('No countries available', 'formipay'),
'countries' => []
]);
}
wp_send_json_success([
'countries' => $countries,
]);
}
}