\ No newline at end of file
diff --git a/includes/Admin/views/setup.php b/includes/Admin/views/setup.php
index d0aba45..2c1804a 100644
--- a/includes/Admin/views/setup.php
+++ b/includes/Admin/views/setup.php
@@ -1,98 +1,221 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/includes/Ajax.php b/includes/Ajax.php
index 8d2b21c..e92ee2b 100644
--- a/includes/Ajax.php
+++ b/includes/Ajax.php
@@ -1,212 +1,394 @@
- $cities->data,
- 'value' => apply_filters( 'pathao_selected_order_city_value', null, $order_id ),
- )
- );
- }
-
- /**
- * Generate token & save it.
- *
- * @return void
- */
- public function setup_pathao() {
- if ( ! isset( $_POST['client_id'], $_POST['client_secret'], $_POST['client_username'], $_POST['_wpnonce'], $_POST['client_password'], $_POST['sandbox_mode'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ), '_pathao_setup_nonce' ) || ! current_user_can( 'manage_options' ) ) {
- return;
- }
-
- $client_id = sanitize_text_field( wp_unslash( $_POST['client_id'] ) );
- $client_secret = sanitize_text_field( wp_unslash( $_POST['client_secret'] ) );
- $data = array(
- 'client_id' => $client_id,
- 'client_secret' => $client_secret,
- 'username' => sanitize_email( wp_unslash( $_POST['client_username'] ) ),
- 'password' => sanitize_text_field( wp_unslash( $_POST['client_password'] ) ),
- );
-
- update_option( 'pathao_sandbox_mode', 'true' === $_POST['sandbox_mode'] ? true : false );
-
- $res = PathaoAPI::generate_tokens( $data );
-
- if ( $res->success ) {
- update_option( 'pathao_client_id', $client_id );
- update_option( 'pathao_client_secret', $client_secret );
- update_option( 'pathao_access_token', $res->data->access_token );
- update_option( 'pathao_refresh_token', $res->data->refresh_token );
- wp_send_json(
- array(
- 'success' => true,
- 'access_token' => $res->data->access_token,
- 'refresh_token' => $res->data->refresh_token,
- )
- );
- }
-
- wp_send_json( $res );
- }
-
- /**
- * Get zones from pathao server.
- *
- * @return void
- */
- public function get_city_zones() {
- if ( ! isset( $_POST['nonce'], $_POST['order_id'], $_POST['city'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'pathao_send_order' ) ) {
- return;
- }
-
- $order_id = sanitize_text_field( wp_unslash( $_POST['order_id'] ) );
- $city = sanitize_text_field( wp_unslash( $_POST['city'] ) );
-
- $zones = PathaoAPI::get_zones( $city );
- $zones = $zones->success ? $zones->data : array();
-
- wp_send_json(
- array(
- 'zones' => $zones,
- 'value' => apply_filters( 'pathao_selected_order_zone_value', null, $order_id ),
- )
- );
- }
-
- /**
- * Get areas from pathao server.
- *
- * @return void
- */
- public function get_zone_areas() {
- if ( ! isset( $_POST['nonce'], $_POST['order_id'], $_POST['zone'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'pathao_send_order' ) ) {
- return;
- }
-
- $order_id = sanitize_text_field( wp_unslash( $_POST['order_id'] ) );
- $zone = sanitize_text_field( wp_unslash( $_POST['zone'] ) );
- $areas = PathaoAPI::get_areas( $zone );
- $areas = $areas->success ? $areas->data : array();
-
- wp_send_json(
- array(
- 'areas' => $areas,
- 'value' => apply_filters( 'pathao_selected_order_area_value', null, $order_id ),
- )
- );
- }
-
- /**
- * Send order to Pathao.
- */
- public function send_order_to_pathao() {
- if ( ! isset( $_POST['nonce'], $_POST['order_id'], $_POST['item_type'], $_POST['delivery_type'], $_POST['amount'], $_POST['item_weight'] ) ) {
- return;
- }
-
- if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'pathao_send_order' ) ) {
- wp_send_json(
- array(
- 'success' => false,
- 'errors' => array( __( 'Invalid nonce', 'sdevs_pathao' ) ),
- )
- );
- }
-
- $order_id = sanitize_text_field( wp_unslash( $_POST['order_id'] ) );
- $body = array();
-
- if ( ! empty( $_POST['city'] ) && ! empty( $_POST['zone'] ) ) {
- $body['recipient_city'] = sanitize_text_field( wp_unslash( $_POST['city'] ) );
- $body['recipient_zone'] = sanitize_text_field( wp_unslash( $_POST['zone'] ) );
- if ( ! empty( $_POST['area'] ) ) {
- $body['recipient_area'] = sanitize_text_field( wp_unslash( $_POST['area'] ) );
- }
- }
-
- if ( ! empty( $_POST['item_description'] ) ) {
- $body['item_description'] = trim( sanitize_text_field( wp_unslash( $_POST['item_description'] ) ) );
- }
-
- if ( ! empty( $_POST['special_instruction'] ) ) {
- $body['special_instruction'] = trim( sanitize_text_field( wp_unslash( $_POST['special_instruction'] ) ) );
- }
-
- if ( ! empty( $_POST['delivery_type'] ) ) {
- $body['delivery_type'] = sanitize_text_field( wp_unslash( $_POST['delivery_type'] ) );
- }
-
- if ( ! empty( $_POST['item_type'] ) ) {
- $body['item_type'] = sanitize_text_field( wp_unslash( $_POST['item_type'] ) );
- }
-
- if ( ! empty( $_POST['item_weight'] ) ) {
- $body['item_weight'] = sanitize_text_field( wp_unslash( $_POST['item_weight'] ) );
- }
-
- if ( ! empty( $_POST['amount'] ) ) {
- $body['amount_to_collect'] = sanitize_text_field( wp_unslash( $_POST['amount'] ) );
- }
-
- $res_data = PathaoAPI::send_order( $order_id, $body );
-
- if ( ! $res_data->success ) {
- wp_send_json(
- array(
- 'success' => false,
- 'errors' => $res_data->messages,
- )
- );
- }
-
- $order = wc_get_order( $order_id );
- $order->update_meta_data( '_pathao_consignment_id', $res_data->data->consignment_id );
- $order->update_meta_data( '_pathao_delivery_fee', $res_data->data->delivery_fee );
- $order->update_meta_data( '_pathao_order_status', $res_data->data->order_status );
- $order->save();
-
- do_action( 'pathao_order_created', $res_data->data );
-
- wp_send_json(
- array(
- 'success' => true,
- 'message' => 'Order sent to Pathao successfull.',
- )
- );
- }
-}
+ __('Permission denied', 'integration-of-pathao-for-woocommerce')], 403);
+ }
+
+ // Verify nonce from setup.php: wp_nonce_field( '_pathao_setup_nonce', '_wp_setup_nonce' );
+ if (empty($_POST['_wp_setup_nonce']) || ! wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_wp_setup_nonce'])), '_pathao_setup_nonce')) {
+ wp_send_json_error(['message' => __('Security check failed', 'integration-of-pathao-for-woocommerce')], 400);
+ }
+
+ $client_id = isset($_POST['client_id']) ? sanitize_text_field(wp_unslash($_POST['client_id'])) : '';
+ $client_secret = isset($_POST['client_secret']) ? sanitize_text_field(wp_unslash($_POST['client_secret'])) : '';
+ $username = isset($_POST['username']) ? sanitize_text_field(wp_unslash($_POST['username'])) : '';
+ $password = isset($_POST['password']) ? sanitize_text_field(wp_unslash($_POST['password'])) : '';
+ $sandbox_mode = ! empty($_POST['sandbox_mode']) ? 1 : 0;
+
+ if (! $client_id || ! $client_secret || ! $username || ! $password) {
+ wp_send_json_error(['message' => __('All fields are required', 'integration-of-pathao-for-woocommerce')]);
+ }
+
+ // Persist credentials & sandbox flag.
+ update_option('pathao_client_id', $client_id);
+ update_option('pathao_client_secret', $client_secret);
+ update_option('pathao_sandbox_mode', $sandbox_mode);
+
+ // Generate tokens using the service (password grant).
+ $res = PathaoAPI::generate_tokens(
+ [
+ 'username' => $username,
+ 'password' => $password,
+ 'grant_type' => 'password',
+ ]
+ );
+
+ if (empty($res->success)) {
+ $message = ! empty($res->messages[0]) ? $res->messages[0] : __('Token generation failed', 'integration-of-pathao-for-woocommerce');
+
+ wp_send_json_error(
+ [
+ 'message' => $message,
+ ]
+ );
+ }
+
+ wp_send_json_success(
+ [
+ 'message' => __('Token generated successfully', 'integration-of-pathao-for-woocommerce'),
+ 'access_token' => $res->data->access_token ?? '',
+ 'refresh_token' => $res->data->refresh_token ?? '',
+ 'sandbox_mode' => (int) $sandbox_mode,
+ ]
+ );
+ }
+
+
+
+
+
+ public function get_wc_order_info() {
+ check_ajax_referer( 'pathao_nonce', 'nonce' );
+
+ $order_id = absint( $_POST['order_id'] ?? 0 );
+ if ( ! $order_id ) {
+ wp_send_json_error( [ 'message' => 'Invalid order ID' ] );
+ }
+
+ $order = wc_get_order( $order_id );
+ if ( ! $order ) {
+ wp_send_json_error( [ 'message' => 'Order not found' ] );
+ }
+
+ // Payment status
+ $payment_status = $order->is_paid() ? 'Paid' : 'Unpaid';
+
+ // COD logic
+ $cod_amount = $order->is_paid() ? 0 : (float) $order->get_total();
+
+ // Order items
+ $items = [];
+ foreach ( $order->get_items() as $item ) {
+ $product = $item->get_product();
+ $items[] = [
+ 'name' => $item->get_name(),
+ 'image' => $product ? wp_get_attachment_image_url( $product->get_image_id(), 'thumbnail' ) : '',
+ ];
+ }
+
+ wp_send_json_success( [
+ 'order_number' => $order->get_order_number(),
+ 'total' => number_format((float) $order->get_total(), 2),
+ 'payment_status' => $payment_status,
+ 'cod_amount' => $cod_amount,
+ 'name' => $order->get_billing_first_name() . ' ' . $order->get_billing_last_name(),
+ 'phone' => $order->get_billing_phone(),
+ 'address' => $order->get_billing_address_1(),
+ 'quantity' => $order->get_item_count(),
+ 'weight' => 0.5, // default
+ 'items' => $items,
+ ] );
+ }
+
+
+ /* -------------------------------------------------------------------------
+ * SINGLE ORDER SEND
+ * ---------------------------------------------------------------------- */
+ public function send_order_to_pathao()
+ {
+
+ if (!current_user_can('manage_woocommerce')) {
+ wp_send_json_error(['message' => 'Permission denied'], 403);
+ }
+
+ check_ajax_referer('pathao_nonce', 'nonce');
+
+ parse_str($_POST['form'] ?? '', $form);
+
+ $order_id = absint($_POST['order_id'] ?? 0);
+ if (!$order_id) {
+ wp_send_json_error(['message' => 'Invalid order ID']);
+ }
+
+ $order = wc_get_order($order_id);
+ if (!$order) {
+ wp_send_json_error(['message' => 'Order not found']);
+ }
+
+ $settings = get_option('woocommerce_pathao_settings');
+ $store_id = absint($settings['store'] ?? 0);
+ if (!$store_id) {
+ wp_send_json_error(['message' => 'Pathao store not configured']);
+ }
+
+ foreach (['recipient_city', 'recipient_zone', 'recipient_area'] as $key) {
+ if (empty($form[$key])) {
+ wp_send_json_error(['message' => ucfirst(str_replace('_', ' ', $key)) . ' is required']);
+ }
+ }
+
+ $phone = sanitize_text_field($form['recipient_phone'] ?? $order->get_billing_phone());
+ if (strlen($phone) !== 11) {
+ wp_send_json_error(['message' => 'Valid phone required']);
+ }
+
+ $item_weight = max(0.5, (float) ($form['item_weight'] ?? 0.5));
+ $item_quantity = max(1, absint($form['item_quantity'] ?? $order->get_item_count()));
+
+ $amount_to_collect = $order->is_paid() ? 0 : (float) $order->get_total();
+
+ /* SAVE LOCATION FOR BULK USE */
+ $order->update_meta_data('_pathao_city', absint($form['recipient_city']));
+ $order->update_meta_data('_pathao_zone', absint($form['recipient_zone']));
+ $order->update_meta_data('_pathao_area', absint($form['recipient_area']));
+ $order->update_meta_data('_pathao_weight', $item_weight);
+ $order->save();
+
+ $payload = [
+ 'store_id' => $store_id,
+ 'merchant_order_id' => (string) $order_id,
+ 'recipient_name' => sanitize_text_field($form['recipient_name'] ?: $order->get_formatted_shipping_full_name()),
+ 'recipient_phone' => $phone,
+ 'recipient_address' => sanitize_textarea_field($form['recipient_address'] ?: $order->get_shipping_address_1()),
+ 'recipient_city' => absint($form['recipient_city']),
+ 'recipient_zone' => absint($form['recipient_zone']),
+ 'recipient_area' => absint($form['recipient_area']),
+ 'delivery_type' => absint($form['delivery_type'] ?? 48),
+ 'item_type' => absint($form['item_type'] ?? 2),
+ 'item_weight' => $item_weight,
+ 'item_quantity' => $item_quantity,
+ 'amount_to_collect' => $amount_to_collect,
+ 'special_instruction' => sanitize_textarea_field($form['special_instruction'] ?? ''),
+ 'item_description' => 'WooCommerce Order #' . $order_id,
+ ];
+
+ $api = new PathaoApiService();
+ $res = $api->send_order_with_payload($payload);
+
+ if (!empty($res->success) && !empty($res->data->consignment_id)) {
+ $order->update_meta_data('_pathao_consignment_id', $res->data->consignment_id);
+ $order->update_meta_data('_pathao_order_status', $res->data->order_status ?? 'Pending');
+ $order->save();
+
+ wp_send_json_success(['message' => 'Order sent to Pathao']);
+ }
+
+ wp_send_json_error(['message' => $res->messages[0] ?? 'Pathao failed']);
+ }
+
+
+
+
+ /* -------------------------------------------------------------------------
+ * GET CITIES
+ * ---------------------------------------------------------------------- */
+ public function get_cities()
+ {
+ check_ajax_referer('pathao_nonce', 'nonce');
+
+ $res = PathaoAPI::get_cities();
+
+ if (! empty($res->success) && ! empty($res->data)) {
+ wp_send_json_success([
+ 'cities' => $res->data
+ ]);
+ }
+
+ wp_send_json_error([
+ 'message' => ! empty($res->messages) ? implode(', ', $res->messages) : 'Failed to load cities',
+ 'cities' => []
+ ]);
+ }
+
+ public function get_city_zones()
+ {
+ check_ajax_referer('pathao_nonce', 'nonce');
+
+ $city_id = absint($_POST['city'] ?? 0);
+ if (! $city_id) {
+ wp_send_json_error(['message' => 'Invalid city id']);
+ }
+
+ $res = PathaoAPI::get_city_zones($city_id);
+
+ if (! empty($res->success) && ! empty($res->data)) {
+ wp_send_json_success([
+ 'zones' => $res->data
+ ]);
+ }
+
+
+ wp_send_json_error([
+ 'message' => ! empty($res->messages) ? implode(', ', $res->messages) : 'Failed to load zones',
+ 'zones' => []
+ ]);
+ }
+
+
+ public function get_zone_areas()
+ {
+ check_ajax_referer('pathao_nonce', 'nonce');
+
+ $zone_id = absint($_POST['zone'] ?? 0);
+ if (! $zone_id) {
+ wp_send_json_error(['message' => 'Invalid zone id']);
+ }
+
+ $res = PathaoAPI::get_areas($zone_id);
+
+ if (! empty($res->success) && ! empty($res->data)) {
+ wp_send_json_success([
+ 'areas' => $res->data
+ ]);
+ }
+
+ wp_send_json_error([
+ 'message' => ! empty($res->messages) ? implode(', ', $res->messages) : 'Failed to load areas',
+ 'areas' => []
+ ]);
+ }
+
+ public function price_calculation()
+ {
+ check_ajax_referer('pathao_nonce', 'nonce');
+
+ $api = new \ConversLabs\Pathao\Services\PathaoApiService();
+
+ // WooCommerce Pathao settings
+ $settings = get_option('woocommerce_pathao_settings');
+ $store_id = (int) ($settings['store'] ?? 0);
+
+ $args = [
+ 'store_id' => $store_id,
+ 'item_type' => absint($_POST['item_type'] ?? 2),
+ 'delivery_type' => absint($_POST['delivery_type'] ?? 48),
+ 'item_weight' => (float) ($_POST['item_weight'] ?? 0.5),
+ 'recipient_city' => absint($_POST['recipient_city'] ?? 0),
+ 'recipient_zone' => absint($_POST['recipient_zone'] ?? 0),
+ 'recipient_area' => absint($_POST['recipient_area'] ?? 0),
+ ];
+
+
+ // Basic validation
+ if (!$args['store_id']) {
+ wp_send_json_error(['message' => 'Store ID missing']);
+ }
+
+ if (!$args['recipient_city'] || !$args['recipient_zone']) {
+ wp_send_json_error(['message' => 'City and zone are required']);
+ }
+
+ $res = $api->price_calculation($args);
+
+ if (empty($res->success)) {
+ wp_send_json_error([
+ 'message' => $res->messages[0] ?? 'Price calculation failed'
+ ]);
+ }
+
+ wp_send_json_success($res->data);
+ }
+
+
+ public function sync_order_status()
+ {
+ check_ajax_referer('pathao_nonce', 'nonce');
+
+ if (!current_user_can('manage_woocommerce')) {
+ wp_send_json_error(['message' => 'Permission denied'], 403);
+ }
+
+ $orders = wc_get_orders([
+ 'limit' => 20,
+ 'meta_key' => '_pathao_consignment_id',
+ 'meta_compare' => 'EXISTS',
+ ]);
+
+ if (empty($orders)) {
+ wp_send_json_success(['message' => 'No Pathao orders to sync']);
+ }
+
+ $api = new \ConversLabs\Pathao\Services\PathaoApiService();
+ $updated = 0;
+
+ foreach ($orders as $order) {
+ $consignment_id = $order->get_meta('_pathao_consignment_id');
+
+ if (!$consignment_id) {
+ continue;
+ }
+
+ $res = $api->get_order_info($consignment_id);
+
+ if (!empty($res->success) && !empty($res->data->order_status_slug)) {
+ $order->update_meta_data('_pathao_order_status', $res->data->order_status_slug);
+ $order->save();
+ $updated++;
+ }
+ }
+
+ wp_send_json_success([
+ 'message' => sprintf('%d orders synced successfully', $updated)
+ ]);
+ }
+}
\ No newline at end of file
diff --git a/includes/Api.php b/includes/Api.php
index 2195db1..1c99911 100644
--- a/includes/Api.php
+++ b/includes/Api.php
@@ -1,102 +1,242 @@
- 'POST',
- 'callback' => array( $this, 'pathao_status_changed' ),
- 'permission_callback' => '__return_true',
- )
- );
- }
-
- /**
- * Receive webhook from pathao.
- *
- * @param WP_REST_Request $request Request.
- */
- public function pathao_status_changed( WP_REST_Request $request ) {
- $signature = $request->get_header( 'X-PATHAO-Signature' );
- $client_secret = get_option( 'pathao_client_secret' );
- if ( ! $client_secret || $signature !== $client_secret ) {
- return array(
- 'success' => false,
- 'message' => 'Invalid signature',
- );
- }
-
- $consignment_id = sanitize_text_field( $request->get_param( 'consignment_id' ) );
- $order_id = sanitize_text_field( $request->get_param( 'merchant_order_id' ) );
- $status = sanitize_text_field( $request->get_param( 'order_status_slug' ) );
-
- $order = wc_get_order( $order_id );
- if ( ! $order ) {
- return;
- }
-
- $order_consignment_id = $order->get_meta( '_pathao_consignment_id', true );
-
- if ( $consignment_id !== $order_consignment_id ) {
- return new WP_Error( 'invalid_consignment_id', 'Invalid consignment id.', array( 'status' => 400 ) );
- }
-
- if ( ! is_sdevs_pathao_pro_activated() ) {
- if ( 'Delivered' === $status ) {
- $order = wc_get_order( $order_id );
- $order->update_status( 'completed' );
- }
-
- if ( in_array( $status, array( 'Pickup_Failed', 'Pickup_Cancelled', 'Delivery_Failed' ), true ) ) {
- $order = wc_get_order( $order_id );
- $order->update_status( 'failed' );
- }
- }
-
- $order->update_meta_data( '_pathao_order_status', $status );
- $order->save();
-
- do_action(
- 'pathao_process_webhook',
- $status,
- $request->get_params()
- );
-
- return array( 'success' => true );
- }
-}
+logger = wc_get_logger();
+ add_action('rest_api_init', array($this, 'register_api'));
+ }
+
+ public function register_api() {
+ // Webhook endpoint for Pathao status updates
+ register_rest_route(
+ 'pathao/v1',
+ '/webhook',
+ array(
+ 'methods' => 'POST',
+ 'callback' => array($this, 'handle_webhook'),
+ 'permission_callback' => '__return_true',
+ )
+ );
+ }
+
+ /**
+ * Handle Pathao webhook requests
+ *
+ * Requirements per Pathao docs:
+ * - Must return status code 202
+ * - Must include header X-Pathao-Merchant-Webhook-Integration-Secret: f3992ecc-59da-4cbe-a049-a13da2018d51
+ * - Must respond within 10 seconds
+ * - Must validate X-PATHAO-Signature header
+ */
+ public function handle_webhook(WP_REST_Request $request) {
+ $start_time = microtime(true);
+
+ // Get webhook secret from options (user configured)
+ $webhook_secret = get_option('pathao_webhook_secret', '');
+
+ // Get signature from header
+ $signature = $request->get_header('X-PATHAO-Signature');
+
+ // Get raw body for signature verification
+ $raw_body = $request->get_body();
+
+ // Verify signature if webhook secret is configured
+ if (! empty($webhook_secret) && ! empty($signature)) {
+ $computed_signature = hash_hmac('sha256', $raw_body, $webhook_secret);
+ if (! hash_equals($computed_signature, $signature)) {
+ $this->logger->error('Pathao webhook: Invalid signature', array('source' => 'pathao-webhook'));
+ return new WP_REST_Response(
+ array('success' => false, 'message' => 'Invalid signature'),
+ 403,
+ array('X-Pathao-Merchant-Webhook-Integration-Secret' => self::WEBHOOK_SECRET_HEADER)
+ );
+ }
+ }
+
+ // Parse JSON body
+ $body = json_decode($raw_body, true);
+
+ if (json_last_error() !== JSON_ERROR_NONE) {
+ $this->logger->error('Pathao webhook: Invalid JSON', array('source' => 'pathao-webhook'));
+ return new WP_REST_Response(
+ array('success' => false, 'message' => 'Invalid JSON'),
+ 400,
+ array('X-Pathao-Merchant-Webhook-Integration-Secret' => self::WEBHOOK_SECRET_HEADER)
+ );
+ }
+
+ // Handle webhook integration test
+ if (! empty($body['event']) && $body['event'] === 'webhook_integration') {
+ $this->logger->info('Pathao webhook: Integration test received', array('source' => 'pathao-webhook'));
+ return new WP_REST_Response(
+ array('success' => true, 'message' => 'Webhook integration verified'),
+ 202,
+ array('X-Pathao-Merchant-Webhook-Integration-Secret' => self::WEBHOOK_SECRET_HEADER)
+ );
+ }
+
+ // Get event type
+ $event = ! empty($body['event']) ? sanitize_text_field($body['event']) : '';
+ $consignment_id = ! empty($body['consignment_id']) ? sanitize_text_field($body['consignment_id']) : '';
+ $merchant_order_id = ! empty($body['merchant_order_id']) ? sanitize_text_field($body['merchant_order_id']) : '';
+
+ if (empty($event) || empty($consignment_id)) {
+ $this->logger->warning('Pathao webhook: Missing required fields', array('source' => 'pathao-webhook', 'body' => $body));
+ return new WP_REST_Response(
+ array('success' => false, 'message' => 'Missing required fields'),
+ 400,
+ array('X-Pathao-Merchant-Webhook-Integration-Secret' => self::WEBHOOK_SECRET_HEADER)
+ );
+ }
+
+ // Log webhook received
+ $this->logger->info('Pathao webhook received', array(
+ 'source' => 'pathao-webhook',
+ 'event' => $event,
+ 'consignment_id' => $consignment_id,
+ 'merchant_order_id' => $merchant_order_id
+ ));
+
+ // Get WooCommerce order by merchant_order_id
+ $order_id = absint($merchant_order_id);
+ if (! $order_id) {
+ $this->logger->warning('Pathao webhook: Invalid merchant_order_id', array('source' => 'pathao-webhook', 'merchant_order_id' => $merchant_order_id));
+ return new WP_REST_Response(
+ array('success' => false, 'message' => 'Invalid merchant_order_id'),
+ 400,
+ array('X-Pathao-Merchant-Webhook-Integration-Secret' => self::WEBHOOK_SECRET_HEADER)
+ );
+ }
+
+ $order = wc_get_order($order_id);
+ if (! $order) {
+ $this->logger->warning('Pathao webhook: Order not found', array('source' => 'pathao-webhook', 'order_id' => $order_id));
+ return new WP_REST_Response(
+ array('success' => false, 'message' => 'Order not found'),
+ 404,
+ array('X-Pathao-Merchant-Webhook-Integration-Secret' => self::WEBHOOK_SECRET_HEADER)
+ );
+ }
+
+ // Verify consignment_id matches
+ $stored_consignment_id = $order->get_meta('_pathao_consignment_id');
+ if ($stored_consignment_id !== $consignment_id) {
+ $this->logger->warning('Pathao webhook: Consignment ID mismatch', array(
+ 'source' => 'pathao-webhook',
+ 'received' => $consignment_id,
+ 'stored' => $stored_consignment_id
+ ));
+ // Don't fail - might be a different consignment
+ }
+
+ // Update delivery fee if provided
+ if (isset($body['delivery_fee'])) {
+ $order->update_meta_data('_pathao_delivery_fee', (float) $body['delivery_fee']);
+ }
+
+ // Update store_id if provided
+ if (isset($body['store_id'])) {
+ $order->update_meta_data('_pathao_store_id', absint($body['store_id']));
+ }
+
+ // Handle different event types
+ $this->handle_event($order, $event, $body);
+
+ // Save order
+ $order->save();
+
+ // Trigger action for other plugins/themes
+ do_action('pathao_webhook_event', $event, $order, $body);
+
+ // Check if we're within 10 seconds (should be, but log if not)
+ $execution_time = microtime(true) - $start_time;
+ if ($execution_time > 10) {
+ $this->logger->warning('Pathao webhook: Execution time exceeded 10 seconds', array(
+ 'source' => 'pathao-webhook',
+ 'execution_time' => $execution_time
+ ));
+ }
+
+ // Return 202 Accepted with required header
+ return new WP_REST_Response(
+ array('success' => true, 'message' => 'Webhook processed'),
+ 202,
+ array('X-Pathao-Merchant-Webhook-Integration-Secret' => self::WEBHOOK_SECRET_HEADER)
+ );
+ }
+
+ /**
+ * Handle specific webhook events
+ */
+ private function handle_event($order, $event, $body) {
+ // Map event to order status slug
+ $status_slug_map = array(
+ 'order.created' => 'Pending',
+ 'order.updated' => 'Pending',
+ 'order.pickup-requested' => 'Pickup Requested',
+ 'order.assigned-for-pickup' => 'Assigned For Pickup',
+ 'order.picked' => 'Picked',
+ 'order.pickup-failed' => 'Pickup Failed',
+ 'order.pickup-cancelled' => 'Pickup Cancelled',
+ 'order.at-sorting-hub' => 'At Sorting Hub',
+ 'order.in-transit' => 'In Transit',
+ 'order.received-at-last-mile-hub' => 'Received at Last Mile Hub',
+ 'order.assigned-for-delivery' => 'Assigned for Delivery',
+ 'order.delivered' => 'Delivered',
+ 'order.partial-delivery' => 'Partial Delivery',
+ 'order.return' => 'Return',
+ 'order.delivery-failed' => 'Delivery Failed',
+ 'order.on-hold' => 'On Hold',
+ 'order.payment-invoice' => 'Payment Invoice',
+ 'order.paid-return' => 'Paid Return',
+ 'order.exchange' => 'Exchange',
+ );
+
+ // Get status slug
+ $status_slug = isset($status_slug_map[$event]) ? $status_slug_map[$event] : 'Unknown';
+
+ // Update order meta
+ $order->update_meta_data('_pathao_order_status', $status_slug);
+ $order->update_meta_data('_pathao_last_webhook_event', $event);
+ $order->update_meta_data('_pathao_last_webhook_time', current_time('mysql'));
+
+ // Update WooCommerce order status based on event (optional - can be disabled)
+ $update_wc_status = apply_filters('pathao_webhook_update_wc_order_status', true, $event, $order);
+
+ if ($update_wc_status) {
+ $wc_status_map = array(
+ 'order.delivered' => 'completed',
+ 'order.pickup-failed' => 'failed',
+ 'order.pickup-cancelled' => 'cancelled',
+ 'order.delivery-failed' => 'failed',
+ 'order.return' => 'refunded',
+ );
+
+ if (isset($wc_status_map[$event])) {
+ $new_status = $wc_status_map[$event];
+ $order->update_status($new_status, sprintf(__('Pathao webhook: %s', 'integration-of-pathao-for-woocommerce'), $status_slug));
+ }
+ }
+
+ // Log event
+ $this->logger->info('Pathao webhook event processed', array(
+ 'source' => 'pathao-webhook',
+ 'order_id' => $order->get_id(),
+ 'event' => $event,
+ 'status_slug' => $status_slug
+ ));
+ }
+}
diff --git a/includes/Assets.php b/includes/Assets.php
index 16af086..23b0b60 100644
--- a/includes/Assets.php
+++ b/includes/Assets.php
@@ -1,122 +1,94 @@
-register_scripts( $this->get_scripts() );
- $this->register_styles( $this->get_styles() );
- }
-
- /**
- * Register scripts
- *
- * @param array $scripts
- *
- * @since 1.0.0
- *
- * @return void
- */
- private function register_scripts( $scripts ) {
- foreach ( $scripts as $handle => $script ) {
- $deps = $script['deps'] ?? false;
- $in_footer = $script['in_footer'] ?? false;
- $version = $script['version'] ?? SDEVS_PATHAO_VERSION;
-
- wp_register_script( $handle, $script['src'], $deps, $version, $in_footer );
- }
- }
-
- /**
- * Register styles
- *
- * @param array $styles
- *
- * @since 1.0.0
- *
- * @return void
- */
- public function register_styles( $styles ) {
- foreach ( $styles as $handle => $style ) {
- $deps = $style['deps'] ?? false;
-
- wp_register_style( $handle, $style['src'], $deps, SDEVS_PATHAO_VERSION );
- }
- }
-
- /**
- * Get all registered scripts
- *
- * @since 1.0.0
- *
- * @return array
- */
- public function get_scripts() {
- $plugin_js_assets_path = SDEVS_PATHAO_ASSETS . '/js/';
-
- $scripts = array(
- 'pathao_toast_script' => array(
- 'src' => $plugin_js_assets_path . 'jquery.toast.min.js',
- 'deps' => array( 'jquery' ),
- 'in_footer' => true,
- ),
- 'pathao_admin_script' => array(
- 'src' => $plugin_js_assets_path . 'admin.js',
- 'deps' => array( 'jquery', 'pathao_toast_script' ),
- 'in_footer' => true,
- ),
- );
-
- return $scripts;
- }
-
- /**
- * Get registered styles
- *
- * @since 1.0.0
- *
- * @return array
- */
- public function get_styles() {
- $plugin_css_assets_path = SDEVS_PATHAO_ASSETS . '/css/';
-
- $styles = array(
- 'pathao_toast_styles' => array(
- 'src' => $plugin_css_assets_path . 'jquery.toast.min.css',
- ),
- );
-
- return $styles;
- }
-}
+ [
+ 'src' => $plugin_js_assets_path . 'jquery.toast.min.js',
+ 'deps' => ['jquery'],
+ 'in_footer' => true,
+ ],
+ 'pathao_admin_script' => [
+ 'src' => $plugin_js_assets_path . 'admin.js',
+ 'deps' => ['jquery'],
+ 'in_footer' => true,
+ ],
+ 'pathao_popup_script' => [
+ 'src' => $plugin_js_assets_path . 'popup.js',
+ 'deps' => ['jquery'],
+ 'in_footer' => true,
+ ],
+ ];
+ }
+
+ public function get_styles() {
+ $plugin_css_assets_path = SDEVS_PATHAO_ASSETS . '/css/';
+
+ return [
+ 'pathao_toast_styles' => [
+ 'src' => $plugin_css_assets_path . 'jquery.toast.min.css',
+ ],
+ 'pathao_styles' => [
+ 'src' => $plugin_css_assets_path . 'style.css',
+ ],
+ ];
+ }
+
+ public function register_scripts() {
+ foreach ($this->get_scripts() as $handle => $script) {
+ wp_register_script(
+ $handle,
+ $script['src'],
+ $script['deps'],
+ SDEVS_PATHAO_VERSION,
+ $script['in_footer']
+ );
+ }
+ }
+
+ public function register_styles() {
+ foreach ($this->get_styles() as $handle => $style) {
+ wp_register_style(
+ $handle,
+ $style['src'],
+ isset($style['deps']) ? $style['deps'] : [],
+ SDEVS_PATHAO_VERSION
+ );
+ }
+ }
+
+ public function enqueue_admin_assets() {
+
+ $this->register_scripts();
+ $this->register_styles();
+
+ wp_enqueue_script('pathao_admin_script');
+ wp_enqueue_script('pathao_popup_script');
+
+ wp_localize_script('pathao_admin_script', 'pathao_vars', [
+ 'ajax_url' => admin_url('admin-ajax.php'),
+ 'nonce' => wp_create_nonce('pathao_nonce'),
+ ]);
+
+ wp_localize_script('pathao_popup_script', 'pathao_vars', [
+ 'ajax_url' => admin_url('admin-ajax.php'),
+ 'nonce' => wp_create_nonce('pathao_nonce'),
+ ]);
+
+ foreach ($this->get_styles() as $handle => $style) {
+ wp_enqueue_style($handle);
+ }
+}
+
+
+}
diff --git a/includes/Facades/PathaoAPI.php b/includes/Facades/PathaoAPI.php
index c1e6610..b9ae811 100644
--- a/includes/Facades/PathaoAPI.php
+++ b/includes/Facades/PathaoAPI.php
@@ -1,32 +1,36 @@
-$name( ...$arguments );
- }
-}
+$name( ...$arguments );
+ }
+}
diff --git a/includes/Frontend.php b/includes/Frontend.php
index e409f88..88e5d9b 100644
--- a/includes/Frontend.php
+++ b/includes/Frontend.php
@@ -1,24 +1,24 @@
-dispatch_actions();
- new Cron();
- new Method();
- }
-
- /**
- * Dispatch and bind actions.
- *
- * @return void
- * @since 1.0.0
- */
- public function dispatch_actions() {
- include_once __DIR__ . '/Illuminate/class-pathao-method.php';
- }
-}
+dispatch_actions();
+ new Cron();
+ new Method();
+ }
+
+ /**
+ * Dispatch and bind actions.
+ *
+ * @return void
+ * @since 1.0.0
+ */
+ public function dispatch_actions() {
+ include_once __DIR__ . '/Illuminate/class-pathao-method.php';
+ }
+}
diff --git a/includes/Illuminate/Cron.php b/includes/Illuminate/Cron.php
index ec984eb..be086c2 100644
--- a/includes/Illuminate/Cron.php
+++ b/includes/Illuminate/Cron.php
@@ -1,37 +1,37 @@
-success ) {
- return;
- }
-
- $data = $res->data;
- update_option( 'pathao_access_token', $data->access_token );
- update_option( 'pathao_refresh_token', $data->refresh_token );
- }
-}
+success ) {
+ return;
+ }
+
+ $data = $res->data;
+ update_option( 'pathao_access_token', $data->access_token );
+ update_option( 'pathao_refresh_token', $data->refresh_token );
+ }
+}
diff --git a/includes/Illuminate/Method.php b/includes/Illuminate/Method.php
index e556d3d..dc246b5 100644
--- a/includes/Illuminate/Method.php
+++ b/includes/Illuminate/Method.php
@@ -1,24 +1,24 @@
-id = 'pathao';
- $this->method_title = __( 'Pathao', 'sdevs_pathao' );
- $this->method_description = __( 'Implement Pathao within WooCommerce fully effective way.', 'sdevs_pathao' );
-
- $this->availability = 'including';
- $this->countries = array( 'BD' );
-
- $this->enabled = is_sdevs_pathao_pro_activated() && in_array(
- $this->get_option( 'enabled' ),
- array(
- 'yes',
- 'yes_as_popup',
- ),
- true
- ) ? 'yes' : 'no';
- $this->title = $this->get_option( 'title' );
- $this->init();
- }
-
- /**
- * Init your settings
- *
- * @access public
- * @return void
- */
- public function init() {
- // Load the settings API.
- $this->init_form_fields(); // This is part of the settings API. Override the method to add your own settings.
- $this->init_settings(); // This is part of the settings API. Loads settings you previously init.
-
- // Save settings in admin if you have any defined.
- add_action(
- 'woocommerce_update_options_shipping_' . $this->id,
- array(
- $this,
- 'process_admin_options',
- )
- );
- }
-
- /**
- * Settings fields initialization.
- */
- public function init_form_fields() {
- $stores = PathaoAPI::get_stores();
- $stores = $stores->success ? $stores->data : array();
- $dropdown_stores = array();
- foreach ( $stores as $store ) {
- $dropdown_stores[ $store->id ] = $store->name;
- }
-
- $order_statuses = array();
- $wc_order_statuses = wc_get_order_statuses();
- foreach ( $wc_order_statuses as $status => $status_name ) {
- $order_statuses[ $status ] = $status_name;
- }
- if ( $this->get_option( 'store' ) === '' && count( $dropdown_stores ) > 0 ) {
- $this->update_option( 'enabled', 'yes' );
- $this->update_option( 'title', 'Pathao' );
- $this->update_option( 'store', array_key_first( $dropdown_stores ) );
- $this->update_option( 'area_field', 'display_required' );
- $this->update_option( 'delivery_type', 48 );
- $this->update_option( 'default_weight', 0.5 );
- $this->update_option( 'custom_order_status', 'yes' );
- $this->update_option( 'paid_order_status', 'wc-paid' );
- $this->update_option( 'at_the_sorting_hub_status', 'wc-in-shipment' );
- $this->update_option( 'pickup_failed_status', 'wc-processing' );
- $this->update_option( 'delivered_status', 'wc-completed' );
- $this->update_option( 'return_status', 'wc-processing' );
- $this->update_option( 'on_hold_status', 'wc-on-hold' );
- } elseif ( count( $dropdown_stores ) > 0 && ! array_key_exists( $this->get_option( 'store' ), $dropdown_stores ) ) {
- $this->update_option( 'store', array_key_first( $dropdown_stores ) );
- }
-
- $this->form_fields = array(
- 'enabled' => array(
- 'title' => __( 'Enable', 'sdevs_pathao' ),
- 'type' => 'select',
- 'description' => __( 'Enable this shipping.', 'sdevs_pathao' ),
- 'options' => array(
- 'yes' => 'Enable',
- 'yes_as_carrier' => 'Enable as Carrier',
- 'yes_as_popup' => 'Enable as Popup Checkout',
- 'no' => 'Disable',
- ),
- 'default' => is_sdevs_pathao_pro_activated() ? 'yes' : 'no',
- 'disabled' => ! is_sdevs_pathao_pro_activated(),
- ),
- 'title' => array(
- 'title' => __( 'Title', 'sdevs_pathao' ),
- 'type' => 'text',
- 'description' => __( 'Title to be display on site', 'sdevs_pathao' ),
- 'default' => 'Pathao',
- 'disabled' => ! is_sdevs_pathao_pro_activated(),
- 'required' => true,
- ),
- 'store' => array(
- 'title' => __( 'Store', 'sdevs_pathao' ),
- 'type' => 'select',
- 'class' => 'wc-enhanced-select',
- 'options' => $dropdown_stores,
- 'disabled' => count( $dropdown_stores ) === 0,
- 'description' => count( $dropdown_stores ) === 0 ? __( 'Please generate token at first !', 'sdevs_pathao' ) : null,
- ),
- 'area_field' => array(
- 'title' => __( 'Area Field', 'sdevs_pathao' ),
- 'type' => 'select',
- 'options' => array(
- 'display_required' => __( 'Display & Required', 'sdevs_pathao' ),
- 'display_no_required' => __( 'Display & Not Required', 'sdevs_pathao' ),
- 'not_display' => __( 'No Display', 'sdevs_pathao' ),
- ),
- 'default' => 'display_required',
- 'disabled' => ! is_sdevs_pathao_pro_activated(),
- ),
- 'delivery_type' => array(
- 'title' => __( 'Delivery Type', 'sdevs_pathao' ),
- 'type' => 'select',
- 'options' => array(
- 48 => __( 'Normal', 'sdevs_pathao' ),
- 12 => __( 'On Demand', 'sdevs_pathao' ),
- ),
- 'default' => 48,
- 'disabled' => ! is_sdevs_pathao_pro_activated(),
- ),
- 'default_weight' => array(
- 'title' => __( 'Default Item Weight (KG)', 'sdevs_pathao' ),
- 'type' => 'number',
- 'custom_attributes' => array(
- 'step' => '0.1',
- 'min' => '0.1',
- 'max' => '200.0',
- 'required' => 'required',
- ),
- 'description' => __( 'This value will be replaced when total weight of order is 0 ! Minimum 0.1 KG to Maximum 200 KG', 'sdevs_pathao' ),
- 'default' => 0.5,
- 'disabled' => ! is_sdevs_pathao_pro_activated(),
- ),
- 'custom_order_status' => array(
- 'title' => __( 'Custom Order Statuses', 'sdevs_pathao' ),
- 'type' => 'checkbox',
- 'options' => $order_statuses,
- 'description' => __( 'Enable helpfull statuses for order (In shipment, Paid, Shipment failed).', 'sdevs_pathao' ),
- 'default' => 'yes',
- 'disabled' => ! is_sdevs_pathao_pro_activated(),
- ),
- 'paid_order_status' => array(
- 'title' => __( 'Order Status For Paid', 'sdevs_pathao' ),
- 'type' => 'select',
- 'options' => $order_statuses,
- 'description' => __( 'When order is paid, the `Amount to Collect` will be 0.', 'sdevs_pathao' ),
- 'default' => 'wc-paid',
- 'disabled' => ! is_sdevs_pathao_pro_activated(),
- ),
- 'at_the_sorting_hub_status' => array(
- 'title' => __( 'Order Status For At the Sorting HUB', 'sdevs_pathao' ),
- 'type' => 'select',
- 'options' => $order_statuses,
- 'description' => __( 'When Pathao order status is At the Sorting HUB, WooCommerce Order status will be set this status !', 'sdevs_pathao' ),
- 'default' => 'wc-in-shipment',
- 'disabled' => ! is_sdevs_pathao_pro_activated(),
- ),
- 'pickup_failed_status' => array(
- 'title' => __( 'Order Status For Pickup Failed', 'sdevs_pathao' ),
- 'type' => 'select',
- 'options' => $order_statuses,
- 'description' => __( 'When Pathao order status is Pickup Failed, WooCommerce Order status will be set this status !', 'sdevs_pathao' ),
- 'default' => 'wc-processing',
- 'disabled' => ! is_sdevs_pathao_pro_activated(),
- ),
- 'delivered_status' => array(
- 'title' => __( 'Order Status For Delivered', 'sdevs_pathao' ),
- 'type' => 'select',
- 'options' => $order_statuses,
- 'description' => __( 'When Pathao order status is Delivered, WooCommerce Order status will be set this status !', 'sdevs_pathao' ),
- 'default' => 'wc-completed',
- 'disabled' => ! is_sdevs_pathao_pro_activated(),
- ),
- 'return_status' => array(
- 'title' => __( 'Order Status For Return', 'sdevs_pathao' ),
- 'type' => 'select',
- 'options' => $order_statuses,
- 'description' => __( 'When Pathao order status is Return, WooCommerce Order status will be set this status !', 'sdevs_pathao' ),
- 'default' => 'wc-processing',
- 'disabled' => ! is_sdevs_pathao_pro_activated(),
- ),
- 'on_hold_status' => array(
- 'title' => __( 'Order Status For On_Hold', 'sdevs_pathao' ),
- 'type' => 'select',
- 'options' => $order_statuses,
- 'description' => __( 'When Pathao order status is On_Hold, WooCommerce Order status will be set this status !', 'sdevs_pathao' ),
- 'default' => 'wc-on-hold',
- 'disabled' => ! is_sdevs_pathao_pro_activated(),
- ),
- );
- }
-
- /**
- * Calculate shipping function.
- *
- * @access public
- *
- * @param array $package Package.
- *
- * @return void
- */
- public function calculate_shipping( $package = array() ) {
- do_action( 'pathao_calculate_shipping', $package, $this );
- }
- }
- }
-}
-
-add_action( 'woocommerce_shipping_init', 'sdevs_pathao_shipping_method_init' );
+id = 'pathao';
+ $this->method_title = __( 'Pathao', 'integration-of-pathao-for-woocommerce' );
+ $this->method_description = __( 'Implement Pathao within WooCommerce fully effective way.', 'integration-of-pathao-for-woocommerce' );
+
+ $this->availability = 'including';
+ $this->countries = array( 'BD' );
+
+ $this->enabled = is_sdevs_pathao_pro_activated() && in_array(
+ $this->get_option( 'enabled' ),
+ array(
+ 'yes',
+ 'yes_as_popup',
+ ),
+ true
+ ) ? 'yes' : 'no';
+ $this->title = $this->get_option( 'title' );
+ $this->init();
+ }
+
+ /**
+ * Init your settings
+ *
+ * @access public
+ * @return void
+ */
+ public function init() {
+ // Load the settings API.
+ $this->init_form_fields(); // This is part of the settings API. Override the method to add your own settings.
+ $this->init_settings(); // This is part of the settings API. Loads settings you previously init.
+
+ // Save settings in admin if you have any defined.
+ add_action(
+ 'woocommerce_update_options_shipping_' . $this->id,
+ array(
+ $this,
+ 'process_admin_options',
+ )
+ );
+ }
+
+ /**
+ * Settings fields initialization.
+ */
+ public function init_form_fields() {
+ $stores = PathaoAPI::get_stores();
+ $stores = $stores->success ? $stores->data : array();
+ $dropdown_stores = array();
+ foreach ( $stores as $store ) {
+ $dropdown_stores[ $store->id ] = $store->name;
+ }
+
+ $order_statuses = array();
+ $wc_order_statuses = wc_get_order_statuses();
+ foreach ( $wc_order_statuses as $status => $status_name ) {
+ $order_statuses[ $status ] = $status_name;
+ }
+ if ( $this->get_option( 'store' ) === '' && count( $dropdown_stores ) > 0 ) {
+ $this->update_option( 'enabled', 'yes' );
+ $this->update_option( 'title', 'Pathao' );
+ $this->update_option( 'store', array_key_first( $dropdown_stores ) );
+ $this->update_option( 'area_field', 'display_required' );
+ $this->update_option( 'delivery_type', 48 );
+ $this->update_option( 'default_weight', 0.5 );
+ $this->update_option( 'custom_order_status', 'yes' );
+ $this->update_option( 'paid_order_status', 'wc-paid' );
+ $this->update_option( 'at_the_sorting_hub_status', 'wc-in-shipment' );
+ $this->update_option( 'pickup_failed_status', 'wc-processing' );
+ $this->update_option( 'delivered_status', 'wc-completed' );
+ $this->update_option( 'return_status', 'wc-processing' );
+ $this->update_option( 'on_hold_status', 'wc-on-hold' );
+ } elseif ( count( $dropdown_stores ) > 0 && ! array_key_exists( $this->get_option( 'store' ), $dropdown_stores ) ) {
+ $this->update_option( 'store', array_key_first( $dropdown_stores ) );
+ }
+
+ $this->form_fields = array(
+ 'enabled' => array(
+ 'title' => __( 'Enable', 'integration-of-pathao-for-woocommerce' ),
+ 'type' => 'select',
+ 'description' => __( 'Enable this shipping.', 'integration-of-pathao-for-woocommerce' ),
+ 'options' => array(
+ 'yes' => 'Enable',
+ 'yes_as_carrier' => 'Enable as Carrier',
+ 'yes_as_popup' => 'Enable as Popup Checkout',
+ 'no' => 'Disable',
+ ),
+ 'default' => is_sdevs_pathao_pro_activated() ? 'yes' : 'no',
+ 'disabled' => ! is_sdevs_pathao_pro_activated(),
+ ),
+ 'title' => array(
+ 'title' => __( 'Title', 'integration-of-pathao-for-woocommerce' ),
+ 'type' => 'text',
+ 'description' => __( 'Title to be display on site', 'integration-of-pathao-for-woocommerce' ),
+ 'default' => 'Pathao',
+ 'disabled' => ! is_sdevs_pathao_pro_activated(),
+ 'required' => true,
+ ),
+ 'store' => array(
+ 'title' => __( 'Store', 'integration-of-pathao-for-woocommerce' ),
+ 'type' => 'select',
+ 'class' => 'wc-enhanced-select',
+ 'options' => $dropdown_stores,
+ 'disabled' => count( $dropdown_stores ) === 0,
+ 'description' => count( $dropdown_stores ) === 0 ? __( 'Please generate token at first !', 'integration-of-pathao-for-woocommerce' ) : null,
+ ),
+ 'area_field' => array(
+ 'title' => __( 'Area Field', 'integration-of-pathao-for-woocommerce' ),
+ 'type' => 'select',
+ 'options' => array(
+ 'display_required' => __( 'Display & Required', 'integration-of-pathao-for-woocommerce' ),
+ 'display_no_required' => __( 'Display & Not Required', 'integration-of-pathao-for-woocommerce' ),
+ 'not_display' => __( 'No Display', 'integration-of-pathao-for-woocommerce' ),
+ ),
+ 'default' => 'display_required',
+ 'disabled' => ! is_sdevs_pathao_pro_activated(),
+ ),
+ 'delivery_type' => array(
+ 'title' => __( 'Delivery Type', 'integration-of-pathao-for-woocommerce' ),
+ 'type' => 'select',
+ 'options' => array(
+ 48 => __( 'Normal', 'integration-of-pathao-for-woocommerce' ),
+ 12 => __( 'On Demand', 'integration-of-pathao-for-woocommerce' ),
+ ),
+ 'default' => 48,
+ 'disabled' => ! is_sdevs_pathao_pro_activated(),
+ ),
+ 'default_weight' => array(
+ 'title' => __( 'Default Item Weight (KG)', 'integration-of-pathao-for-woocommerce' ),
+ 'type' => 'number',
+ 'custom_attributes' => array(
+ 'step' => '0.1',
+ 'min' => '0.1',
+ 'max' => '200.0',
+ 'required' => 'required',
+ ),
+ 'description' => __( 'This value will be replaced when total weight of order is 0 ! Minimum 0.1 KG to Maximum 200 KG', 'integration-of-pathao-for-woocommerce' ),
+ 'default' => 0.5,
+ 'disabled' => ! is_sdevs_pathao_pro_activated(),
+ ),
+ 'custom_order_status' => array(
+ 'title' => __( 'Custom Order Statuses', 'integration-of-pathao-for-woocommerce' ),
+ 'type' => 'checkbox',
+ 'options' => $order_statuses,
+ 'description' => __( 'Enable helpfull statuses for order (In shipment, Paid, Shipment failed).', 'integration-of-pathao-for-woocommerce' ),
+ 'default' => 'yes',
+ 'disabled' => ! is_sdevs_pathao_pro_activated(),
+ ),
+ 'paid_order_status' => array(
+ 'title' => __( 'Order Status For Paid', 'integration-of-pathao-for-woocommerce' ),
+ 'type' => 'select',
+ 'options' => $order_statuses,
+ 'description' => __( 'When order is paid, the `Amount to Collect` will be 0.', 'integration-of-pathao-for-woocommerce' ),
+ 'default' => 'wc-paid',
+ 'disabled' => ! is_sdevs_pathao_pro_activated(),
+ ),
+ 'at_the_sorting_hub_status' => array(
+ 'title' => __( 'Order Status For At the Sorting HUB', 'integration-of-pathao-for-woocommerce' ),
+ 'type' => 'select',
+ 'options' => $order_statuses,
+ 'description' => __( 'When Pathao order status is At the Sorting HUB, WooCommerce Order status will be set this status !', 'integration-of-pathao-for-woocommerce' ),
+ 'default' => 'wc-in-shipment',
+ 'disabled' => ! is_sdevs_pathao_pro_activated(),
+ ),
+ 'pickup_failed_status' => array(
+ 'title' => __( 'Order Status For Pickup Failed', 'integration-of-pathao-for-woocommerce' ),
+ 'type' => 'select',
+ 'options' => $order_statuses,
+ 'description' => __( 'When Pathao order status is Pickup Failed, WooCommerce Order status will be set this status !', 'integration-of-pathao-for-woocommerce' ),
+ 'default' => 'wc-processing',
+ 'disabled' => ! is_sdevs_pathao_pro_activated(),
+ ),
+ 'delivered_status' => array(
+ 'title' => __( 'Order Status For Delivered', 'integration-of-pathao-for-woocommerce' ),
+ 'type' => 'select',
+ 'options' => $order_statuses,
+ 'description' => __( 'When Pathao order status is Delivered, WooCommerce Order status will be set this status !', 'integration-of-pathao-for-woocommerce' ),
+ 'default' => 'wc-completed',
+ 'disabled' => ! is_sdevs_pathao_pro_activated(),
+ ),
+ 'return_status' => array(
+ 'title' => __( 'Order Status For Return', 'integration-of-pathao-for-woocommerce' ),
+ 'type' => 'select',
+ 'options' => $order_statuses,
+ 'description' => __( 'When Pathao order status is Return, WooCommerce Order status will be set this status !', 'integration-of-pathao-for-woocommerce' ),
+ 'default' => 'wc-processing',
+ 'disabled' => ! is_sdevs_pathao_pro_activated(),
+ ),
+ 'on_hold_status' => array(
+ 'title' => __( 'Order Status For On_Hold', 'integration-of-pathao-for-woocommerce' ),
+ 'type' => 'select',
+ 'options' => $order_statuses,
+ 'description' => __( 'When Pathao order status is On_Hold, WooCommerce Order status will be set this status !', 'integration-of-pathao-for-woocommerce' ),
+ 'default' => 'wc-on-hold',
+ 'disabled' => ! is_sdevs_pathao_pro_activated(),
+ ),
+ );
+ }
+
+ /**
+ * Calculate shipping function.
+ *
+ * @access public
+ *
+ * @param array $package Package.
+ *
+ * @return void
+ */
+ public function calculate_shipping( $package = array() ) {
+ do_action( 'pathao_calculate_shipping', $package, $this );
+ }
+ }
+ }
+}
+
+add_action( 'woocommerce_shipping_init', 'sdevs_pathao_shipping_method_init' );
diff --git a/includes/Installer.php b/includes/Installer.php
index af81ed4..79b0809 100644
--- a/includes/Installer.php
+++ b/includes/Installer.php
@@ -1,85 +1,85 @@
-add_version();
- $this->create_tables();
- }
-
- /**
- * Add time and version on DB.
- *
- * @since 1.0.0
- *
- * @return void
- */
- public function add_version() {
- $installed = get_option( 'pathao_installed' );
-
- if ( ! $installed ) {
- update_option( 'pathao_installed', time() );
- }
-
- update_option( 'pathao_version', SDEVS_PATHAO_VERSION );
-
- if ( ! wp_next_scheduled( 'pathao_refresh_token_cron' ) ) {
- wp_schedule_event( time(), 'twicedaily', 'pathao_refresh_token_cron' );
- }
- }
-
- /**
- * Create necessary database tables.
- *
- * @since 1.0.0
- *
- * @return void
- */
- public function create_tables() {
- if ( ! function_exists( 'dbDelta' ) ) {
- require_once ABSPATH . 'wp-admin/includes/upgrade.php';
- }
-
- $this->create_logs_table();
- }
-
- /**
- * Create logs table
- *
- * @return void
- */
- public function create_logs_table() {
- global $wpdb;
-
- $charset_collate = $wpdb->get_charset_collate();
- $table_name = $wpdb->prefix . 'pathao_logs';
-
- $schema = "CREATE TABLE IF NOT EXISTS `{$table_name}` (
- `id` INT(255) NOT NULL AUTO_INCREMENT,
- `order_id` INT(100) NOT NULL,
- `consignment_id` VARCHAR(100) NOT NULL,
- `order_status` VARCHAR(100) NOT NULL,
- `order_status_slug` VARCHAR(100) NOT NULL,
- `reason` VARCHAR(200) NULL,
- `updated_at` TIMESTAMP NOT NULL,
- PRIMARY KEY (`id`)
- ) $charset_collate";
-
- dbDelta( $schema );
- }
-}
+add_version();
+ $this->create_tables();
+ }
+
+ /**
+ * Add time and version on DB.
+ *
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function add_version() {
+ $installed = get_option( 'pathao_installed' );
+
+ if ( ! $installed ) {
+ update_option( 'pathao_installed', time() );
+ }
+
+ update_option( 'pathao_version', SDEVS_PATHAO_VERSION );
+
+ if ( ! wp_next_scheduled( 'pathao_refresh_token_cron' ) ) {
+ wp_schedule_event( time(), 'twicedaily', 'pathao_refresh_token_cron' );
+ }
+ }
+
+ /**
+ * Create necessary database tables.
+ *
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function create_tables() {
+ if ( ! function_exists( 'dbDelta' ) ) {
+ require_once ABSPATH . 'wp-admin/includes/upgrade.php';
+ }
+
+ $this->create_logs_table();
+ }
+
+ /**
+ * Create logs table
+ *
+ * @return void
+ */
+ public function create_logs_table() {
+ global $wpdb;
+
+ $charset_collate = $wpdb->get_charset_collate();
+ $table_name = $wpdb->prefix . 'pathao_logs';
+
+ $schema = "CREATE TABLE IF NOT EXISTS `{$table_name}` (
+ `id` INT(255) NOT NULL AUTO_INCREMENT,
+ `order_id` INT(100) NOT NULL,
+ `consignment_id` VARCHAR(100) NOT NULL,
+ `order_status` VARCHAR(100) NOT NULL,
+ `order_status_slug` VARCHAR(100) NOT NULL,
+ `reason` VARCHAR(200) NULL,
+ `updated_at` TIMESTAMP NOT NULL,
+ PRIMARY KEY (`id`)
+ ) $charset_collate";
+
+ dbDelta( $schema );
+ }
+}
diff --git a/includes/Services/PathaoApiService.php b/includes/Services/PathaoApiService.php
index c4a9f86..0668a92 100644
--- a/includes/Services/PathaoApiService.php
+++ b/includes/Services/PathaoApiService.php
@@ -1,445 +1,959 @@
-get_base_url() . $path,
- array_merge(
- array(
- 'headers' => array(
- 'Authorization' => 'Bearer ' . $this->get_access_token(),
- 'Accept' => 'application/json',
- ),
- ),
- $args
- )
- );
- }
-
- /**
- * Handle response errors.
- *
- * @param array $res Response.
- *
- * @return false|\stdClass
- */
- private function has_errors( $res ): false|\stdClass {
- $res_code = wp_remote_retrieve_response_code( $res );
- $data = new \stdClass();
- if ( 401 === $res_code ) {
- $data->success = false;
- $data->messages = array( __( 'Invalid access token !', 'sdevs_pathao' ) );
- return $data;
- } elseif ( 422 === $res_code ) {
- $res_body = wp_remote_retrieve_body( $res );
- $errors = json_decode( $res_body )->errors;
- $messages = array();
- array_walk_recursive(
- get_object_vars( $errors ),
- function ( $msg ) use ( &$messages ) {
- $messages[] = $msg;
- }
- );
-
- $data->success = false;
- $data->messages = $messages;
- return $data;
- } elseif ( 400 === $res_code ) {
- $data->success = false;
- $data->messages = array( __( 'The user credentials were incorrect.', 'sdevs_pathao' ) );
- return $data;
- } elseif ( 200 > $res_code || 299 < $res_code ) {
- $data->success = false;
- $data->messages = array( __( 'Something went wrong! Try again', 'sdevs_pathao' ) );
- return $data;
- }
-
- return false;
- }
-
- /**
- * Check if there any transient saved.
- *
- * @param string $key Transient Key.
- *
- * @return false|\stdClass
- */
- private function has_transient( $key ): false|\stdClass {
- $has_transient = get_transient( $key );
-
- if ( $has_transient ) {
- $data = new \stdClass();
- $data->success = true;
- $data->data = $has_transient;
- return $data;
- }
-
- return false;
- }
-
- /**
- * Get cities from pathao server.
- *
- * @return \stdClass
- */
- public function get_cities(): \stdClass {
- $transient_key = '_sdevs_pathao_cities';
-
- $has_transient = $this->has_transient( $transient_key );
- if ( $has_transient ) {
- return $has_transient;
- }
-
- $res = $this->request( 'wp_remote_get', 'aladdin/api/v1/city-list' );
-
- $has_errors = $this->has_errors( $res );
- if ( $has_errors ) {
- return $has_errors;
- }
-
- $encoded_body = wp_remote_retrieve_body( $res );
-
- $body = json_decode( $encoded_body );
- $data = new \stdClass();
- $data->success = true;
- $data->data = array();
- foreach ( $body->data->data as $city ) {
- $data->data[] = (object) array(
- 'id' => $city->city_id,
- 'name' => $city->city_name,
- );
- }
-
- // set transient for 12-hrs.
- set_transient( $transient_key, $data->data, 720 * 60 );
-
- return $data;
- }
-
- /**
- * Get zones from pathao server.
- *
- * @param int $city_id City Id.
- *
- * @return \stdClass
- */
- public function get_zones( int $city_id ): \stdClass {
- $transient_key = "_sdevs_pathao_city_{$city_id}_zones";
-
- $has_transient = $this->has_transient( $transient_key );
- if ( $has_transient ) {
- return $has_transient;
- }
-
- $res = $this->request( 'wp_remote_get', "/aladdin/api/v1/cities/$city_id/zone-list" );
-
- $has_errors = $this->has_errors( $res );
- if ( $has_errors ) {
- return $has_errors;
- }
-
- $encoded_body = wp_remote_retrieve_body( $res );
-
- $body = json_decode( $encoded_body );
- $data = new \stdClass();
- $data->success = true;
- $data->data = array();
- foreach ( $body->data->data as $zone ) {
- $data->data[] = (object) array(
- 'id' => $zone->zone_id,
- 'name' => $zone->zone_name,
- );
- }
-
- // set transient for 12-hrs.
- set_transient( $transient_key, $data->data, 720 * 60 );
-
- return $data;
- }
-
- /**
- * Get areas from pathao server.
- *
- * @param int $zone_id Zone Id.
- *
- * @return \stdClass
- */
- public function get_areas( int $zone_id ): \stdClass {
- $transient_key = "_sdevs_pathao_zone_{$zone_id}_areas";
-
- $has_transient = $this->has_transient( $transient_key );
- if ( $has_transient ) {
- return $has_transient;
- }
-
- $res = $this->request( 'wp_remote_get', "/aladdin/api/v1/zones/$zone_id/area-list" );
-
- $has_errors = $this->has_errors( $res );
- if ( $has_errors ) {
- return $has_errors;
- }
-
- $encoded_body = wp_remote_retrieve_body( $res );
-
- $body = json_decode( $encoded_body );
- $data = new \stdClass();
- $data->success = true;
- $data->data = array();
- foreach ( $body->data->data as $area ) {
- $data->data[] = (object) array(
- 'id' => $area->area_id,
- 'name' => $area->area_name,
- );
- }
-
- // set transient for 12-hrs.
- set_transient( $transient_key, $data->data, 720 * 60 );
-
- return $data;
- }
-
- /**
- * Get stores from pathao server.
- *
- * @return \stdClass
- */
- public function get_stores(): \stdClass {
- $transient_key = '_sdevs_pathao_stores';
-
- $has_transient = $this->has_transient( $transient_key );
- if ( $has_transient ) {
- return $has_transient;
- }
-
- $res = $this->request( 'wp_remote_get', 'aladdin/api/v1/stores' );
-
- $has_errors = $this->has_errors( $res );
- if ( $has_errors ) {
- return $has_errors;
- }
-
- $encoded_body = wp_remote_retrieve_body( $res );
-
- $body = json_decode( $encoded_body );
- $data = new \stdClass();
- $data->success = true;
- $data->data = array();
- foreach ( $body->data->data as $store ) {
- $data->data[] = (object) array(
- 'id' => $store->store_id,
- 'name' => $store->store_name,
- );
- }
-
- // set transient for 5-min.
- set_transient( $transient_key, $data->data, 5 * 60 );
-
- return $data;
- }
-
- /**
- * Send order to pathao.
- *
- * @param int $order_id Order Id.
- * @param array $args data for body.
- *
- * @return \stdClass
- */
- public function send_order( int $order_id, $args = array() ) {
- $order = wc_get_order( $order_id );
- $recipient_name = $order->get_formatted_shipping_full_name() !== ' ' ? $order->get_formatted_shipping_full_name() : $order->get_formatted_billing_full_name();
- $recipient_phone = $order->get_shipping_phone() !== '' ? $order->get_shipping_phone() : $order->get_billing_phone();
- $recipient_phone = substr( $recipient_phone, 0, 3 ) === '+88' ? str_replace( '+88', '', $recipient_phone ) : $recipient_phone;
- $recipient_address = $order->get_formatted_shipping_address() !== '' ? $order->get_formatted_shipping_address() : $order->get_formatted_billing_address();
-
- $item_weight = sdevs_pathao_get_totals_from_items( $order );
-
- $body = wp_parse_args(
- $args,
- array(
- 'store_id' => sdevs_pathao_store_id(),
- 'merchant_order_id' => $order_id,
- 'recipient_name' => $recipient_name,
- 'recipient_phone' => $recipient_phone,
- 'recipient_address' => $recipient_address,
- 'delivery_type' => apply_filters( 'sdevs_pathao_default_delivery_type', 48 ),
- 'item_type' => 2,
- 'item_description' => $item_weight->item_description,
- 'item_quantity' => $item_weight->quantity,
- 'item_weight' => $item_weight->weight,
- 'amount_to_collect' => $order->has_status( 'paid' ) ? 0 : round( (float) $order->get_total() ),
- )
- );
-
- $res = $this->request( 'wp_remote_post', 'aladdin/api/v1/orders', array( 'body' => $body ) );
-
- $has_errors = $this->has_errors( $res );
- if ( $has_errors ) {
- return $has_errors;
- }
-
- $body = wp_remote_retrieve_body( $res );
- $res_data = json_decode( $body )->data;
-
- $data = new \stdClass();
- $data->success = true;
- $data->data = (object) array(
- 'consignment_id' => $res_data->consignment_id,
- 'merchant_order_id' => $res_data->merchant_order_id,
- 'order_status' => $res_data->order_status,
- 'delivery_fee' => $res_data->delivery_fee,
- );
-
- return $data;
- }
-
- /**
- * Price calulation.
- *
- * @param array $args Arguments.
- *
- * @return \stdClass
- */
- public function price_calculation( $args ) {
- $body = wp_parse_args(
- $args,
- array(
- 'store_id' => sdevs_pathao_store_id(),
- 'item_type' => 2,
- 'delivery_type' => apply_filters( 'sdevs_pathao_default_delivery_type', 48 ),
- )
- );
-
- $res = $this->request( 'wp_remote_post', 'aladdin/api/v1/merchant/price-plan', array( 'body' => $body ) );
-
- $has_errors = $this->has_errors( $res );
- if ( $has_errors ) {
- return $has_errors;
- }
-
- $encoded_data = wp_remote_retrieve_body( $res );
- $decoded_data = json_decode( $encoded_data );
-
- $data = new \stdClass();
- $data->success = true;
- $data->data = (object) array(
- 'price' => $decoded_data->data->price,
- 'cod_enabled' => $decoded_data->data->cod_enabled,
- );
-
- return $data;
- }
-
- /**
- * Generate Token from pathao server.
- *
- * @param array $args Args.
- *
- * @return \stdClass
- */
- public function generate_tokens( $args ) {
- $body = wp_parse_args(
- $args,
- array(
- 'grant_type' => 'password',
- )
- );
-
- $res = $this->request( 'wp_remote_post', 'aladdin/api/v1/issue-token', array( 'body' => $body ) );
-
- $has_errors = $this->has_errors( $res );
- if ( $has_errors ) {
- return $has_errors;
- }
-
- $body = wp_remote_retrieve_body( $res );
- $res_data = json_decode( $body );
-
- $data = new \stdClass();
- $data->success = true;
- $data->data = (object) array(
- 'access_token' => $res_data->access_token,
- 'refresh_token' => $res_data->refresh_token,
- );
-
- return $data;
- }
-
- /**
- * Refresh tokens.
- *
- * @return \stdClass
- */
- public function refresh_tokens() {
- $client_id = get_option( 'pathao_client_id' );
- $client_secret = get_option( 'pathao_client_secret' );
- $refresh_token = get_option( 'pathao_refresh_token' );
-
- $data = new stdClass();
- if ( ! $client_id || ! $client_secret || ! $refresh_token ) {
- $data->success = false;
- $data->messages = array( __( 'Please generate tokens at first!', 'sdevs_pathao' ) );
- return;
- }
-
- $body = array(
- 'client_id' => $client_id,
- 'client_secret' => $client_secret,
- 'refresh_token' => $refresh_token,
- 'grant_type' => 'refresh_token',
- );
-
- $res = $this->request( 'wp_remote_post', 'aladdin/api/v1/issue-token', array( 'body' => $body ) );
-
- $has_errors = $this->has_errors( $res );
- if ( $has_errors ) {
- return $has_errors;
- }
-
- $body = wp_remote_retrieve_body( $res );
- $res_data = json_decode( $body );
-
- $data->success = true;
- $data->data = (object) array(
- 'access_token' => $res_data->access_token,
- 'refresh_token' => $res_data->refresh_token,
- );
-
- return $data;
- }
-}
+refresh_tokens();
+ if ($new && $new->success) {
+ update_option('pathao_access_token', $new->data->access_token);
+ update_option('pathao_refresh_token', $new->data->refresh_token);
+ return $new->data->access_token;
+ }
+ return false;
+ }
+
+ return $token;
+ }
+
+ /*-----------------------------------------
+ | REQUEST WRAPPER (AUTO TOKEN REFRESH)
+ ------------------------------------------*/
+ private function request($func, string $path, array $args = [])
+ {
+ $token = $this->ensure_access_token();
+
+ if (!$token) {
+ return ['error' => 'token_missing'];
+ }
+
+ $default_headers = [
+ 'Authorization' => 'Bearer ' . $token,
+ 'Content-Type' => 'application/json; charset=UTF-8',
+ 'Accept' => 'application/json',
+ ];
+
+ $final_args = $args;
+
+ // ✅ SAFE header merge (DO NOT overwrite Authorization)
+ $final_args['headers'] = array_merge(
+ $default_headers,
+ $args['headers'] ?? []
+ );
+
+ $url = $this->get_base_url() . ltrim($path, '/');
+
+ // Always call the requested endpoint; previously a hardcoded URL broke all requests.
+ $response = $func($url, $final_args);
+
+ // Token expired → refresh
+ if (wp_remote_retrieve_response_code($response) === 401) {
+ $new = $this->refresh_tokens();
+ if ($new && $new->success) {
+ update_option('pathao_access_token', $new->data->access_token);
+ update_option('pathao_refresh_token', $new->data->refresh_token);
+
+ $final_args['headers']['Authorization'] =
+ 'Bearer ' . $new->data->access_token;
+
+ $response = $func($url, $final_args);
+ }
+ }
+
+ return $response;
+ }
+
+
+
+ /*-----------------------------------------
+ | ERROR HANDLER
+ ------------------------------------------*/
+ private function has_errors($res)
+ {
+ $code = wp_remote_retrieve_response_code($res);
+ $data = new stdClass();
+
+ if ($code === 401) {
+ $data->success = false;
+ $data->messages = ['Unauthorized access token'];
+ return $data;
+ }
+
+ if ($code === 422) {
+ $body = json_decode(wp_remote_retrieve_body($res));
+ $messages = [];
+
+ if (!empty($body->errors)) {
+ foreach ($body->errors as $err) {
+ if (is_array($err)) $messages = array_merge($messages, $err);
+ else $messages[] = $err;
+ }
+ }
+
+ $data->success = false;
+ $data->messages = $messages;
+ return $data;
+ }
+
+ if ($code < 200 || $code > 299) {
+ $data->success = false;
+ $data->messages = ['Something went wrong'];
+ return $data;
+ }
+
+ return false;
+ }
+
+ /*-----------------------------------------
+ | TRANSIENT CHECK
+ ------------------------------------------*/
+ private function has_transient($key)
+ {
+ $saved = get_transient($key);
+
+ if ($saved) {
+ return (object)[
+ 'success' => true,
+ 'data' => $saved
+ ];
+ }
+
+ return false;
+ }
+
+ /*-----------------------------------------
+ | GET CITIES
+ ------------------------------------------*/
+ public function get_cities()
+ {
+ $transient_key = '_sdevs_pathao_cities';
+
+ if ($cached = $this->has_transient($transient_key)) {
+ return $cached;
+ }
+
+ $res = $this->request('wp_remote_get', 'aladdin/api/v1/city-list');
+
+ if ($err = $this->has_errors($res)) {
+ return $err;
+ }
+ $body = json_decode(wp_remote_retrieve_body($res));
+
+ if (!isset($body->data->data) || !is_array($body->data->data)) {
+ return (object)[
+ 'success' => false,
+ 'messages' => ['Cities data malformed']
+ ];
+ }
+ $list = [];
+ foreach ($body->data->data as $c) {
+ $list[] = (object)[
+ 'id' => $c->city_id,
+ 'name' => $c->city_name
+ ];
+ }
+ set_transient($transient_key, $list, 12 * HOUR_IN_SECONDS);
+
+ return (object)['success' => true, 'data' => $list];
+ }
+
+ /*-----------------------------------------
+ | GET ZONES BY CITY (NEW ENDPOINT)
+ | /aladdin/api/v1/cities/{city_id}/zone-list
+ ------------------------------------------*/
+ public function get_city_zones(int $city_id): stdClass
+ {
+ $key = "_sdevs_pathao_city_{$city_id}_zones";
+
+ if ($c = $this->has_transient($key)) {
+ return $c;
+ }
+
+ $res = $this->request(
+ 'wp_remote_get',
+ "aladdin/api/v1/cities/{$city_id}/zone-list"
+ );
+
+ if ($err = $this->has_errors($res)) {
+ return $err;
+ }
+
+ $body = json_decode(wp_remote_retrieve_body($res));
+
+ if (
+ empty($body->data->data) ||
+ !is_array($body->data->data)
+ ) {
+ return (object)[
+ 'success' => false,
+ 'messages' => ['Zone data malformed']
+ ];
+ }
+
+ $list = [];
+ foreach ($body->data->data as $z) {
+ $list[] = (object)[
+ 'id' => $z->zone_id,
+ 'name' => $z->zone_name,
+ ];
+ }
+
+ set_transient($key, $list, 12 * HOUR_IN_SECONDS);
+
+ return (object)[
+ 'success' => true,
+ 'data' => $list
+ ];
+ }
+
+ /*-----------------------------------------
+ | GET AREAS
+ ------------------------------------------*/
+ public function get_areas(int $zone_id): stdClass
+ {
+ $key = "_sdevs_pathao_zone_{$zone_id}_areas";
+
+ if ($c = $this->has_transient($key)) {
+ return $c;
+ }
+
+ $res = $this->request(
+ 'wp_remote_get',
+ "aladdin/api/v1/zones/{$zone_id}/area-list"
+ );
+
+ if ($err = $this->has_errors($res)) {
+ return $err;
+ }
+
+ $body = json_decode(wp_remote_retrieve_body($res));
+
+ if (
+ empty($body->data->data) ||
+ !is_array($body->data->data)
+ ) {
+ return (object)[
+ 'success' => false,
+ 'messages' => ['Area data malformed']
+ ];
+ }
+
+ $list = [];
+ foreach ($body->data->data as $a) {
+ $list[] = (object)[
+ 'id' => $a->area_id,
+ 'name' => $a->area_name,
+ ];
+ }
+
+ set_transient($key, $list, 12 * HOUR_IN_SECONDS);
+
+ return (object)['success' => true, 'data' => $list];
+ }
+
+ /*-----------------------------------------
+ | GET STORES
+ | Docs: GET {base_url}/aladdin/api/v1/stores
+ ------------------------------------------*/
+ public function get_stores(): stdClass
+ {
+ $key = '_pathao_store_id';
+
+ if ($c = $this->has_transient($key)) {
+ return $c;
+ }
+
+ $res = $this->request('wp_remote_get', 'aladdin/api/v1/stores');
+
+ if ($err = $this->has_errors($res)) {
+ return $err;
+ }
+
+ $body = json_decode(wp_remote_retrieve_body($res));
+
+ if (!isset($body->data->data) || !is_array($body->data->data)) {
+ return (object)[
+ 'success' => false,
+ 'messages' => ['Stores malformed'],
+ ];
+ }
+
+ $list = [];
+ foreach ($body->data->data as $s) {
+ $list[] = (object)[
+ 'id' => $s->store_id,
+ 'name' => $s->store_name,
+ ];
+ }
+
+ set_transient($key, $list, 5 * MINUTE_IN_SECONDS);
+
+ return (object)[
+ 'success' => true,
+ 'data' => $list,
+ ];
+ }
+
+ /*-----------------------------------------
+ | CREATE STORE
+ | Docs: POST {base_url}/aladdin/api/v1/stores
+ ------------------------------------------*/
+ public function create_store(array $payload): stdClass
+ {
+ // Required fields from official docs.
+ $required = [
+ 'name',
+ 'contact_name',
+ 'contact_number',
+ 'address',
+ 'city_id',
+ 'zone_id',
+ 'area_id',
+ ];
+
+ foreach ($required as $field) {
+ if (! isset($payload[$field]) || $payload[$field] === '') {
+ return (object) [
+ 'success' => false,
+ 'messages' => ["Missing required field: {$field}"],
+ ];
+ }
+ }
+
+ // Basic length validations to match docs.
+ if (strlen($payload['name']) < 3 || strlen($payload['name']) > 50) {
+ return (object) [
+ 'success' => false,
+ 'messages' => ['Store name length must be between 3 and 50 characters'],
+ ];
+ }
+
+ if (strlen($payload['contact_name']) < 3 || strlen($payload['contact_name']) > 50) {
+ return (object) [
+ 'success' => false,
+ 'messages' => ['Contact name length must be between 3 and 50 characters'],
+ ];
+ }
+
+ if (strlen($payload['contact_number']) !== 11) {
+ return (object) [
+ 'success' => false,
+ 'messages' => ['Contact number must be 11 characters'],
+ ];
+ }
+
+ if (! empty($payload['secondary_contact']) && strlen($payload['secondary_contact']) !== 11) {
+ return (object) [
+ 'success' => false,
+ 'messages' => ['Secondary contact number must be 11 characters'],
+ ];
+ }
+
+ if (strlen($payload['address']) < 15 || strlen($payload['address']) > 120) {
+ return (object) [
+ 'success' => false,
+ 'messages' => ['Address length must be between 15 and 120 characters'],
+ ];
+ }
+
+ // Send request.
+ $res = $this->request(
+ 'wp_remote_post',
+ 'aladdin/api/v1/stores',
+ [
+ 'body' => wp_json_encode($payload),
+ ]
+ );
+
+ if ($err = $this->has_errors($res)) {
+ return $err;
+ }
+
+ $body = json_decode(wp_remote_retrieve_body($res));
+
+ // Expecting data.store_name per docs.
+ $store_name = $body->data->store_name ?? null;
+
+ return (object) [
+ 'success' => true,
+ 'data' => (object) [
+ 'store_name' => $store_name,
+ 'raw' => $body,
+ ],
+ ];
+ }
+
+ /*-----------------------------------------
+ | ORDER PAYLOAD VALIDATOR
+ ------------------------------------------*/
+ private function validate_order_payload($p)
+ {
+ $required = [
+ 'store_id',
+ 'recipient_name',
+ 'recipient_address',
+ 'delivery_type',
+ 'item_type',
+ 'item_quantity',
+ 'item_weight',
+ 'amount_to_collect'
+ ];
+
+
+ foreach ($required as $f) {
+ if (!isset($p[$f]) || $p[$f] === '') {
+ return "Missing required field: {$f}";
+ }
+ }
+
+ if (strlen($p['recipient_phone']) != 11) {
+ return "Recipient phone must be 11 digits";
+ }
+
+ if (strlen($p['recipient_address']) < 10) {
+ return "Recipient address must be at least 10 characters";
+ }
+
+ return true;
+ }
+
+
+ private function map_delivery_type(\WC_Order $order): int
+ {
+ foreach ($order->get_shipping_methods() as $method) {
+ $method_id = $method->get_method_id();
+
+ // Adjust if you add more mappings later
+ if ($method_id === 'local_pickup') {
+ return 12; // Same city
+ }
+ }
+
+ return 48; // Normal delivery (default)
+ }
+
+
+ private function calculate_order_weight(\WC_Order $order): float
+ {
+ $weight = 0.0;
+
+ foreach ($order->get_items() as $item) {
+ $product = $item->get_product();
+ if (!$product) {
+ continue;
+ }
+
+ $product_weight = (float) $product->get_weight();
+ $qty = (int) $item->get_quantity();
+
+ $weight += ($product_weight * $qty);
+ }
+
+ // Pathao rule: minimum 0.5 KG
+ return max(0.5, round($weight, 2));
+ }
+ public function send_order_with_payload(array $payload): stdClass
+ {
+ // Basic validation
+ $required = [
+ 'store_id',
+ 'recipient_name',
+ 'recipient_phone',
+ 'recipient_address',
+ 'recipient_city',
+ 'recipient_zone',
+ 'recipient_area',
+ 'delivery_type',
+ 'item_type',
+ 'item_weight',
+ 'item_quantity',
+ 'amount_to_collect',
+ ];
+
+ foreach ($required as $field) {
+ if (!isset($payload[$field])) {
+ return (object)[
+ 'success' => false,
+ 'messages' => ["Missing field: {$field}"]
+ ];
+ }
+ }
+
+
+ if (strlen($payload['recipient_phone']) !== 11) {
+ return (object)[
+ 'success' => false,
+ 'messages' => ['Recipient phone must be 11 digits']
+ ];
+ }
+
+ $res = $this->request(
+ 'wp_remote_post',
+ 'aladdin/api/v1/orders',
+ [
+ 'body' => wp_json_encode($payload),
+ ]
+ );
+
+ if ($err = $this->has_errors($res)) {
+ return $err;
+ }
+
+ $body = json_decode(wp_remote_retrieve_body($res));
+
+ if (empty($body->data->consignment_id)) {
+ return (object)[
+ 'success' => false,
+ 'messages' => ['Consignment ID not returned'],
+ 'raw' => $body
+ ];
+ }
+
+ return (object)[
+ 'success' => true,
+ 'data' => $body->data,
+ ];
+ }
+
+
+ private function get_pathao_location(): array
+ {
+ return [
+ 'city' => (int) get_option('pathao_city_id', 0),
+ 'zone' => (int) get_option('pathao_zone_id', 0),
+ ];
+ }
+
+ private function get_item_type(): int
+ {
+ $settings = get_option('woocommerce_pathao_settings');
+
+ // 1 = Document, 2 = Parcel
+ return (int) ($settings['item_type'] ?? 2);
+ }
+
+
+
+
+ /*-----------------------------------------
+ | SEND ORDER TO PATHAO
+ ------------------------------------------*/
+ public function send_order(int $order_id): \stdClass
+ {
+ if (!function_exists('wc_get_order')) {
+ return (object)['success' => false, 'messages' => ['WooCommerce missing']];
+ }
+
+ $order = wc_get_order($order_id);
+ if (!$order) {
+ return (object)['success' => false, 'messages' => ['Invalid order']];
+ }
+
+ // Prevent duplicate Pathao order
+ $existing = $order->get_meta('_pathao_consignment_id');
+ if ($existing) {
+ return (object)[
+ 'success' => true,
+ 'messages' => ['Already sent to Pathao'],
+ 'consignment_id' => $existing
+ ];
+ }
+
+ $settings = get_option('woocommerce_pathao_settings');
+ $store_id = (int) ($settings['store'] ?? 0);
+
+ if (!$store_id) {
+ return (object)['success' => false, 'messages' => ['Store not configured']];
+ }
+
+ $city = absint($order->get_meta('_pathao_city'));
+ $zone = absint($order->get_meta('_pathao_zone'));
+ $area = absint($order->get_meta('_pathao_area'));
+
+ if (!$city || !$zone) {
+ return (object)[
+ 'success' => false,
+ 'messages' => ['Recipient city & zone missing']
+ ];
+ }
+
+ $payload = [
+ 'store_id' => $store_id,
+ 'merchant_order_id' => (string) $order_id,
+
+ 'recipient_name' => trim(
+ $order->get_shipping_first_name() . ' ' . $order->get_shipping_last_name()
+ ),
+ 'recipient_phone' => $order->get_billing_phone() ?: '01700000000',
+ 'recipient_address' => trim(
+ $order->get_shipping_address_1() . ', ' . $order->get_shipping_city()
+ ),
+
+ 'delivery_type' => $this->map_delivery_type($order),
+ 'item_type' => $this->get_item_type(),
+ 'item_quantity' => $order->get_item_count(),
+ 'item_weight' => $this->calculate_order_weight($order),
+
+ 'recipient_city' => $city,
+ 'recipient_zone' => $zone,
+
+ 'item_description' => "WooCommerce Order #{$order_id}",
+
+ 'amount_to_collect' => $order->get_payment_method() === 'cod'
+ ? (int) $order->get_total()
+ : 0,
+ ];
+
+ // Validate
+ $validate = $this->validate_order_payload($payload);
+ if ($validate !== true) {
+ return (object)['success' => false, 'messages' => [$validate]];
+ }
+
+ // Send request
+ $res = $this->request(
+ 'wp_remote_post',
+ 'aladdin/api/v1/orders',
+ ['body' => wp_json_encode($payload)]
+ );
+
+ if ($err = $this->has_errors($res)) {
+ return $err;
+ }
+
+ $body = json_decode(wp_remote_retrieve_body($res));
+
+ if (empty($body->data->consignment_id)) {
+ return (object)[
+ 'success' => false,
+ 'messages' => ['Consignment ID not returned'],
+ 'raw' => $body
+ ];
+ }
+
+ // Save meta
+ $order->update_meta_data('_pathao_consignment_id', $body->data->consignment_id);
+ $order->update_meta_data('_pathao_order_status', $body->data->order_status ?? 'Pending');
+ $order->save();
+
+ do_action('pathao_order_created', (object)[
+ 'merchant_order_id' => $order_id,
+ 'consignment_id' => $body->data->consignment_id,
+ 'order_status' => $body->data->order_status ?? '',
+ ]);
+
+ return (object)[
+ 'success' => true,
+ 'consignment_id' => $body->data->consignment_id,
+ 'order_status' => $body->data->order_status ?? '',
+ 'data' => $body
+ ];
+ }
+
+
+
+
+ public function get_order_by_merchant_order_id(string $merchant_order_id): stdClass
+ {
+ $res = $this->request(
+ 'wp_remote_get',
+ "aladdin/api/v1/orders?merchant_order_id={$merchant_order_id}"
+ );
+
+ if ($err = $this->has_errors($res)) {
+ return $err;
+ }
+
+ $body = json_decode(wp_remote_retrieve_body($res));
+
+ if (empty($body->data->data[0])) {
+ return (object)[
+ 'success' => false,
+ 'messages' => ['Order not created yet']
+ ];
+ }
+
+ return (object)[
+ 'success' => true,
+ 'data' => $body->data->data[0]
+ ];
+ }
+
+
+
+ /*-----------------------------------------
+ | GET ORDER SHORT INFO
+ ------------------------------------------*/
+ public function get_order_info(string $consignment_id): \stdClass
+ {
+ if (empty($consignment_id)) {
+ return (object) [
+ 'success' => false,
+ 'messages' => ['Consignment ID is required'],
+ ];
+ }
+
+ $response = $this->request(
+ 'wp_remote_get',
+ "aladdin/api/v1/orders/{$consignment_id}/info"
+ );
+
+ if (is_wp_error($response)) {
+ return (object) [
+ 'success' => false,
+ 'messages' => [$response->get_error_message()],
+ ];
+ }
+
+ $status_code = wp_remote_retrieve_response_code($response);
+ $body = json_decode(wp_remote_retrieve_body($response));
+
+ if ($status_code !== 200 || empty($body->data)) {
+ return (object) [
+ 'success' => false,
+ 'messages' => ['Invalid response from Pathao'],
+ 'raw' => $body,
+ ];
+ }
+
+ return (object) [
+ 'success' => true,
+ 'data' => (object) [
+ 'consignment_id' => $body->data->consignment_id ?? '',
+ 'merchant_order_id' => $body->data->merchant_order_id ?? '',
+ 'order_status' => $body->data->order_status ?? '',
+ 'order_status_slug' => $body->data->order_status_slug ?? '',
+ 'updated_at' => $body->data->updated_at ?? '',
+ 'invoice_id' => $body->data->invoice_id ?? null,
+ ],
+ ];
+ }
+
+ /*-----------------------------------------
+ | PRICE CALCULATION (MERCHANT)
+ ------------------------------------------*/
+ public function price_calculation($args)
+
+ // Get store ID from settings
+ $settings = get_option('woocommerce_pathao_settings');
+ $store_id = (int) ($settings['store'] ?? 0);
+
+ if (!$store_id) {
+ return (object)[
+ 'success' => false,
+ 'messages' => ['Store ID not configured']
+ ];
+ }
+
+ $body = wp_parse_args($args, [
+ 'store_id' => $store_id,
+ 'item_type' => 2,
+ 'delivery_type' => 48,
+ 'item_weight' => 0.5,
+ 'recipient_city' => 0,
+ 'recipient_zone' => 0,
+ ]);
+
+ // Hard validation (Pathao requirement)
+ if (!$body['recipient_city'] || !$body['recipient_zone']) {
+ return (object)[
+ 'success' => false,
+ 'messages' => ['City and zone are required']
+ ];
+ }
+
+ // Merchant endpoint + SOURCE header
+ $token = $this->ensure_access_token();
+ $res = $this->request(
+ 'wp_remote_post',
+ 'aladdin/api/v1/merchant/price-plan',
+ [
+ 'body' => wp_json_encode($body),
+ ]
+ );
+
+
+ // Transport / API errors
+ if ($err = $this->has_errors($res)) {
+ return $err;
+ }
+
+ $raw_body = wp_remote_retrieve_body($res);
+
+ $d = json_decode($raw_body);
+
+ if (empty($d->data)) {
+ return (object)[
+ 'success' => false,
+ 'messages' => ['Price data missing'],
+ 'raw' => $d,
+ ];
+ }
+
+ return (object)[
+ 'success' => true,
+ 'data' => (object)[
+ 'price' => $d->data->price ?? 0,
+ 'discount' => $d->data->discount ?? 0,
+ 'promo_discount' => $d->data->promo_discount ?? 0,
+ 'plan_id' => $d->data->plan_id ?? null,
+ 'cod_enabled' => $d->data->cod_enabled ?? 0,
+ 'cod_percentage' => $d->data->cod_percentage ?? 0,
+ 'additional_charge' => $d->data->additional_charge ?? 0,
+ 'final_price' => $d->data->final_price ?? 0,
+ ],
+ ];
+ }
+
+ /*-----------------------------------------
+ | TOKEN GENERATION (PASSWORD GRANT)
+ | Docs: POST {base_url}/aladdin/api/v1/issue-token
+ ------------------------------------------*/
+ public function generate_tokens($args)
+ {
+ $client_id = get_option('pathao_client_id');
+ $client_secret = get_option('pathao_client_secret');
+
+ $username = $args['username'] ?? '';
+ $password = $args['password'] ?? '';
+ $grant = $args['grant_type'] ?? 'password';
+
+ if (!$client_id || !$client_secret || !$username || !$password) {
+ return (object)[
+ 'success' => false,
+ 'messages' => ['client_id, client_secret, username and password are required to issue token'],
+ ];
+ }
+
+ $url = $this->get_base_url() . 'aladdin/api/v1/issue-token';
+
+ $body = [
+ 'client_id' => $client_id,
+ 'client_secret' => $client_secret,
+ 'grant_type' => $grant,
+ 'username' => $username,
+ 'password' => $password,
+ ];
+
+ $response = wp_remote_post($url, [
+ 'headers' => [
+ 'Content-Type' => 'application/json',
+ 'Accept' => 'application/json',
+ ],
+ 'body' => wp_json_encode($body),
+ ]);
+
+ if (is_wp_error($response)) {
+ return (object)[
+ 'success' => false,
+ 'messages' => [$response->get_error_message()],
+ ];
+ }
+
+ $code = wp_remote_retrieve_response_code($response);
+ if ($code !== 200) {
+ return (object)[
+ 'success' => false,
+ 'messages' => ['Token issue failed'],
+ ];
+ }
+
+ $d = json_decode(wp_remote_retrieve_body($response));
+
+ // Persist tokens for later use (as per docs: save access token & refresh token)
+ if (!empty($d->access_token)) {
+ update_option('pathao_access_token', $d->access_token);
+ }
+ if (!empty($d->refresh_token)) {
+ update_option('pathao_refresh_token', $d->refresh_token);
+ }
+
+ return (object)[
+ 'success' => true,
+ 'data' => (object)[
+ 'access_token' => $d->access_token ?? '',
+ 'refresh_token' => $d->refresh_token ?? '',
+ ],
+ ];
+ }
+
+ /*-----------------------------------------
+ | TOKEN REFRESH
+ ------------------------------------------*/
+ public function refresh_tokens()
+ {
+ $client_id = get_option('pathao_client_id');
+ $client_secret = get_option('pathao_client_secret');
+ $refresh_token = get_option('pathao_refresh_token');
+
+ if (!$client_id || !$client_secret || !$refresh_token) {
+ return (object)[
+ 'success' => false,
+ 'messages' => ['Token info missing']
+ ];
+ }
+
+ $url = $this->get_base_url() . 'aladdin/api/v1/issue-token';
+
+ $body = [
+ 'client_id' => $client_id,
+ 'client_secret' => $client_secret,
+ 'refresh_token' => $refresh_token,
+ 'grant_type' => 'refresh_token',
+ ];
+
+ $response = wp_remote_post($url, [
+ 'headers' => [
+ 'Content-Type' => 'application/json',
+ 'Accept' => 'application/json',
+ ],
+ 'body' => wp_json_encode($body),
+ ]);
+
+ if (is_wp_error($response)) {
+ return (object)[
+ 'success' => false,
+ 'messages' => [$response->get_error_message()]
+ ];
+ }
+
+ $code = wp_remote_retrieve_response_code($response);
+ if ($code !== 200) {
+ return (object)[
+ 'success' => false,
+ 'messages' => ['Token refresh failed'],
+ ];
+ }
+
+ $data = json_decode(wp_remote_retrieve_body($response));
+
+ return (object)[
+ 'success' => true,
+ 'data' => (object)[
+ 'access_token' => $data->access_token,
+ 'refresh_token' => $data->refresh_token,
+ ]
+ ];
+ }
+ }
diff --git a/includes/functions.php b/includes/functions.php
index 4e0ba4d..355d16f 100644
--- a/includes/functions.php
+++ b/includes/functions.php
@@ -1,93 +1,93 @@
-get_items() as $order_item ) {
- $product = $order_item->get_product();
- if ( ! $product->is_virtual() ) {
- array_push( $item_description, "{$order_item->get_name()}(x{$order_item->get_quantity()})" );
- $quantity += $order_item['quantity'];
- $total_weight += empty( $product->get_weight() ) ? 0 : intval( $product->get_weight() ) * $order_item['quantity'];
- }
- }
- $total_weight = floatval( max( $total_weight, 0.5 ) );
- $item_description = implode( ', ', $item_description );
-
- return (object) array(
- 'weight' => 0 === $total_weight ? apply_filters( 'sdevs_pathao_default_weight', 0.5 ) : $total_weight,
- 'item_description' => $item_description,
- 'quantity' => $quantity,
- );
-}
-
-/**
- * Check if HPOS enabled.
- */
-function sdevs_wc_order_hpos_enabled() {
- return function_exists( 'wc_get_container' ) ? wc_get_container()->get( CustomOrdersTableController::class )->custom_orders_table_usage_is_enabled() : false;
-}
+get_items() as $order_item ) {
+ $product = $order_item->get_product();
+ if ( ! $product->is_virtual() ) {
+ array_push( $item_description, "{$order_item->get_name()}(x{$order_item->get_quantity()})" );
+ $quantity += $order_item['quantity'];
+ $total_weight += empty( $product->get_weight() ) ? 0 : intval( $product->get_weight() ) * $order_item['quantity'];
+ }
+ }
+ $total_weight = floatval( max( $total_weight, 0.5 ) );
+ $item_description = implode( ', ', $item_description );
+
+ return (object) array(
+ 'weight' => 0 === $total_weight ? apply_filters( 'sdevs_pathao_default_weight', 0.5 ) : $total_weight,
+ 'item_description' => $item_description,
+ 'quantity' => $quantity,
+ );
+}
+
+/**
+ * Check if HPOS enabled.
+ */
+function sdevs_wc_order_hpos_enabled() {
+ return function_exists( 'wc_get_container' ) ? wc_get_container()->get( CustomOrdersTableController::class )->custom_orders_table_usage_is_enabled() : false;
+}
diff --git a/index.php b/index.php
index 0dc6337..594a69e 100644
--- a/index.php
+++ b/index.php
@@ -1,2 +1,2 @@
-define_constants();
-
- register_activation_hook( __FILE__, array( $this, 'activate' ) );
- register_deactivation_hook( __FILE__, array( $this, 'deactivate' ) );
-
- add_action( 'plugins_loaded', array( $this, 'init_plugin' ) );
- }
-
- /**
- * Initializes the Sdevs_pathao() class.
- *
- * Checks for an existing Sdevs_pathao() instance
- * and if it doesn't find one, creates it.
- *
- * @since 1.0.0
- *
- * @return Sdevs_pathao|bool
- */
- public static function init() {
- static $instance = false;
-
- if ( ! $instance ) {
- $instance = new Sdevs_pathao();
- }
-
- return $instance;
- }
-
- /**
- * Magic getter to bypass referencing plugin.
- *
- * @param $prop
- *
- * @since 1.0.0
- *
- * @return mixed
- */
- public function __get( $prop ) {
- if ( array_key_exists( $prop, $this->container ) ) {
- return $this->container[ $prop ];
- }
-
- return $this->{$prop};
- }
-
- /**
- * Magic isset to bypass referencing plugin.
- *
- * @param $prop
- *
- * @since 1.0.0
- *
- * @return mixed
- */
- public function __isset( $prop ) {
- return isset( $this->{$prop} ) || isset( $this->container[ $prop ] );
- }
-
- /**
- * Define the constants.
- *
- * @since 1.0.0
- *
- * @return void
- */
- public function define_constants() {
- define( 'SDEVS_PATHAO_VERSION', self::VERSION );
- define( 'SDEVS_PATHAO_FILE', __FILE__ );
- define( 'SDEVS_PATHAO_PATH', dirname( SDEVS_PATHAO_FILE ) );
- define( 'SDEVS_PATHAO_INCLUDES', __DIR__ . '/includes' );
- define( 'SDEVS_PATHAO_URL', plugins_url( '', SDEVS_PATHAO_FILE ) );
- define( 'SDEVS_PATHAO_ASSETS', SDEVS_PATHAO_URL . '/assets' );
- }
-
- /**
- * Load the plugin after all plugins are loaded.
- *
- * @since 1.0.0
- *
- * @return void
- */
- public function init_plugin() {
- $this->includes();
- $this->init_hooks();
- }
-
- /**
- * Placeholder for activation function.
- *
- * Nothing being called here yet.
- *
- * @since 1.0.0
- *
- * @return void
- */
- public function activate() {
- $installer = new SpringDevs\Pathao\Installer();
- $installer->run();
- }
-
- /**
- * Placeholder for deactivation function.
- *
- * Nothing being called here yet.
- *
- * @since 1.0.0
- */
- public function deactivate() {
- }
-
- /**
- * Include the required files.
- *
- * @since 1.0.0
- *
- * @return void
- */
- public function includes() {
- if ( $this->is_request( 'admin' ) ) {
- $this->container['admin'] = new SpringDevs\Pathao\Admin();
- }
-
- if ( $this->is_request( 'frontend' ) ) {
- $this->container['frontend'] = new SpringDevs\Pathao\Frontend();
- }
-
- if ( $this->is_request( 'ajax' ) ) {
- // require_once SDEVS_PATHAO_INCLUDES . '/class-ajax.php';
- }
- }
-
- /**
- * Initialize the hooks.
- *
- * @since 1.0.0
- *
- * @return void
- */
- public function init_hooks() {
- add_action( 'init', array( $this, 'init_classes' ) );
-
- // Localize our plugin.
- add_action( 'init', array( $this, 'localization_setup' ) );
- }
-
- /**
- * Instantiate the required classes.
- *
- * @since 1.0.0
- *
- * @return void
- */
- public function init_classes() {
- if ( $this->is_request( 'ajax' ) ) {
- $this->container['ajax'] = new SpringDevs\Pathao\Ajax();
- }
-
- $this->container['api'] = new SpringDevs\Pathao\Api();
- $this->container['assets'] = new SpringDevs\Pathao\Assets();
- }
-
- /**
- * Initialize plugin for localization.
- *
- * @uses load_plugin_textdomain()
- *
- * @since 1.0.0
- *
- * @return void
- */
- public function localization_setup() {
- load_plugin_textdomain( 'sdevs_pathao', false, dirname( plugin_basename( __FILE__ ) ) . '/languages/' );
- }
-
- /**
- * What type of request is this?
- *
- * @param string $type admin, ajax, cron or frontend.
- *
- * @since 1.0.0
- *
- * @return bool
- */
- private function is_request( $type ) {
- switch ( $type ) {
- case 'admin':
- return is_admin();
-
- case 'ajax':
- return defined( 'DOING_AJAX' );
-
- case 'rest':
- return defined( 'REST_REQUEST' );
-
- case 'cron':
- return defined( 'DOING_CRON' );
-
- case 'frontend':
- return ( ! is_admin() || defined( 'DOING_AJAX' ) ) && ! defined( 'DOING_CRON' );
- }
- }
-} // Sdevs_pathao
-
-/**
- * Initialize the main plugin.
- *
- * @since 1.0.0
- *
- * @return \Sdevs_pathao|bool
- */
-function sdevs_pathao() {
- return Sdevs_pathao::init();
-}
-
-/**
- * Kick-off the plugin.
- *
- * @since 1.0.0
- */
-sdevs_pathao();
+define_constants();
+
+ register_activation_hook(__FILE__, array($this, 'activate'));
+ register_deactivation_hook(__FILE__, array($this, 'deactivate'));
+
+ add_action('plugins_loaded', array($this, 'init_plugin'));
+ }
+
+ public static function init()
+ {
+ static $instance = false;
+ if (!$instance) {
+ $instance = new Sdevs_pathao();
+ }
+ return $instance;
+ }
+
+ public function __get($prop)
+ {
+ return $this->container[$prop] ?? $this->{$prop};
+ }
+
+ public function __isset($prop)
+ {
+ return isset($this->{$prop}) || isset($this->container[$prop]);
+ }
+
+ private function define_constants()
+ {
+ define('SDEVS_PATHAO_VERSION', self::VERSION);
+ define('SDEVS_PATHAO_FILE', __FILE__);
+ define('SDEVS_PATHAO_PATH', dirname(SDEVS_PATHAO_FILE));
+ define('SDEVS_PATHAO_INCLUDES', __DIR__ . '/includes');
+ define('SDEVS_PATHAO_URL', plugins_url('', SDEVS_PATHAO_FILE));
+ define('SDEVS_PATHAO_ASSETS', SDEVS_PATHAO_URL . '/assets');
+ }
+
+ public function init_plugin()
+ {
+ $this->includes();
+ $this->init_hooks();
+ }
+
+ public function activate()
+ {
+ $installer = new ConversLabs\Pathao\Installer();
+ $installer->run();
+ }
+
+ public function deactivate() {}
+
+ private function includes()
+ {
+ // Always include Ajax
+ require_once SDEVS_PATHAO_INCLUDES . '/Ajax.php';
+
+ if ($this->is_request('admin')) {
+ $this->container['admin'] = new ConversLabs\Pathao\Admin();
+ }
+ }
+
+ private function init_hooks()
+ {
+ add_action('init', array($this, 'init_classes'));
+ add_action('init', array($this, 'localization_setup'));
+ }
+
+ public function init_classes()
+ {
+ // Initialize Ajax as singleton
+ $this->container['ajax'] = \ConversLabs\Pathao\Ajax::init();
+
+ $this->container['api'] = new \ConversLabs\Pathao\Api();
+ $this->container['assets'] = new \ConversLabs\Pathao\Assets();
+ }
+
+ public function localization_setup()
+ {
+ load_plugin_textdomain(
+ 'integration-of-pathao-for-woocommerce',
+ false,
+ dirname(plugin_basename(__FILE__)) . '/languages/'
+ );
+ }
+
+ private function is_request($type)
+ {
+ switch ($type) {
+ case 'admin':
+ return is_admin();
+ case 'ajax':
+ return defined('DOING_AJAX');
+ case 'rest':
+ return defined('REST_REQUEST');
+ case 'cron':
+ return defined('DOING_CRON');
+ case 'frontend':
+ return (!is_admin() || defined('DOING_AJAX')) && !defined('DOING_CRON');
+ }
+ return false;
+ }
+
+
+
+}
+
+// Kickoff
+function sdevs_pathao()
+{
+ return Sdevs_pathao::init();
+}
+sdevs_pathao();
diff --git a/phpcs.xml b/phpcs.xml
deleted file mode 100644
index aa60a23..0000000
--- a/phpcs.xml
+++ /dev/null
@@ -1,47 +0,0 @@
-
-
- Generally-applicable sniffs for WordPress plugins.
-
-
- .
- /vendor/
- /node_modules/
- /tests/
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/readme.txt b/readme.txt
index 5742e8a..e15097b 100644
--- a/readme.txt
+++ b/readme.txt
@@ -1,125 +1,138 @@
-=== Pathao Integration for WooCommerce ===
-Contributors: springdevs, naminbd, ok9xnirab
-Donate link: http://springdevs.com/
-Tags: pathao, pathao-shipping, woocommerce-shipping, pathao courier, bd courier
-Requires at least: 4.0
-Tested up to: 6.6
-Stable tag: trunk
-Requires PHP: 7.0
-License: GPLv2 or later
-License URI: https://www.gnu.org/licenses/gpl-2.0.html
-
-Pathao courier integration for WooCommerce
-
-== Description ==
-
-Pathao integration for WooCommerce.
-Here we made a solution that will take all the information from the user and send that directly to Pathao. The information will directly interact with Pathao and calculate the delivery charge for each order. So you don’t need to worry about the delivery charge or call the customer if any change in the delivery charge. 👊👊
-
-[youtube https://youtu.be/nrdH7hWVTbc]
-
-
-## Features
-
-### Free
-
-1. Sent order to Pathao
-2. Track pathao order status from woocommerce order page
-
-### Premium
-
-Explore our [Premium Version here](https://springdevs.com/plugin/pathao).
-
-1. Register Pathao as a shipping method
-2. Automatically calculate shipping charge based on location
-3. Replace WooCommerce Checkout fields to support a Range of Locations
-4. Synchronise WooCommerce Order with Pathao Order
-5. Multi checkout support
-6. More Customizable.
-
-## Why should you choose us?
-1. Easy and simple installation process
-2. Fully automatic delivery charge calculation
-3. No need to worry about the delivery location
-4. No need to send data manually to Pathao
-5. Hassle-free one-click delivery solution
-
-
-
-
-== Installation ==
-
-= Installation from within WordPress =
-
-```
- 1. Visit 'Plugins > 'Add New'.
- 2. Search for 'Integration of Pathao for WooCommerce'.
- 3. Install and activate the 'Integration of Pathao for WooCommerce' plugin.
-
-= Manual installation =
-
- 1. Upload the entire `Integration of Pathao for WooCommerce` folder to the `/wp-content/plugins/` directory.
- 2. Visit 'Plugins'.
- 3. Activate the `Integration of Pathao for WooCommerce` plugin.
-```
-
-== Frequently Asked Questions ==
-=Is this plugin compatible with WooCommerce block pages ?=
-No, This plugin isn't fully compatible with WooCommerce block pages. You need to continue with shortcode pages.
-=Is it compatible with all WordPress themes ?=
-Compatibility with all themes is impossible, because there are too many, but generally if themes are developed according to WordPress and WooCommerce guidelines, **Pathao Integration for WooCommerce** is compatible with them.
-Sometimes, especially when new versions are released, it might only require some time for them to be all updated, but you can be sure that they will be tested and will be working in a few days.
-
-
-== Screenshots ==
-
-1. Settings page
-2. Setup plugin
-3. Send the order to Pathao
-4. Pathao order status
-
-== Changelog ==
-
-= 1.1 =
-* **New:** Pathao column on order list.
-* **Update:** City and Zone fields are totally optional now.
-* **Update:** Optimize perfromence.
-* **Update:** Rebuild the API part.
-
-= 1.0.9 =
-* **Update:** Autofill item description field.
-
-= 1.0.8 =
-
-* **Fix:** Error message doesn't display when success is false.
-* Ready for v1.3 pro version.
-
- = 1.0.7 =
-
-* Settings page updated.
-* Ready for v1.2 pro version.
-
- = 1.0.6 =
-
-* Item description field added.
-
- = 1.0.5 =
-
-* HPOS support added.
-* Several issues solved.
-
- = 1.0.4 =
-
-* Fix the wrong weight on the admin order
-
- = 1.0.3 =
-
-* `At_the_Sorting_HUB` status setting added.
-
- = 1.0.2 =
-
-* The ability to send an order again to Pathao if the order was canceled or failed.
-
- = 1.0.1 =
-
-* Initial release
+=== Pathao Integration for WooCommerce ===
+Contributors: converswp, shamsbd71
+Donate link: http://converslabs.com/
+https://converslabs.com/products/pathao
+Tags: pathao, pathao-shipping, woocommerce-shipping, pathao courier, bd courier
+Requires at least: 4.0
+Tested up to: 6.9
+Stable tag: 1.2
+Requires PHP: 7.0
+License: GPLv2 or later
+License URI: https://www.gnu.org/licenses/gpl-2.0.html
+
+Pathao courier integration for WooCommerce
+
+
+== Description ==
+
+Pathao integration for WooCommerce.
+Here we made a solution that will take all the information from the user and send that directly to Pathao. The information will directly interact with Pathao and calculate the delivery charge for each order. So you don’t need to worry about the delivery charge or call the customer if any change in the delivery charge. 👊👊
+
+[youtube https://youtu.be/nrdH7hWVTbc]
+
+
+## Features
+
+### Free
+
+1. Sent order to Pathao
+2. Track pathao order status from woocommerce order page
+
+### Premium
+
+Explore our [Premium Version here](https://converslabs.com/products/pathao/).
+
+1. Register Pathao as a shipping method
+2. Automatically calculate shipping charge based on location
+3. Replace WooCommerce Checkout fields to support a Range of Locations
+4. Synchronise WooCommerce Order with Pathao Order
+5. Multi checkout support
+6. More Customizable.
+
+## Why should you choose us?
+1. Easy and simple installation process
+2. Fully automatic delivery charge calculation
+3. No need to worry about the delivery location
+4. No need to send data manually to Pathao
+5. Hassle-free one-click delivery solution
+
+
+
+
+== Installation ==
+
+= Installation from within WordPress =
+
+```
+ 1. Visit 'Plugins > 'Add New'.
+ 2. Search for 'Integration of Pathao for WooCommerce'.
+ 3. Install and activate the 'Integration of Pathao for WooCommerce' plugin.
+
+= Manual installation =
+
+ 1. Upload the entire `Integration of Pathao for WooCommerce` folder to the `/wp-content/plugins/` directory.
+ 2. Visit 'Plugins'.
+ 3. Activate the `Integration of Pathao for WooCommerce` plugin.
+```
+
+== Frequently Asked Questions ==
+=Is this plugin compatible with WooCommerce block pages ?=
+No, This plugin isn't fully compatible with WooCommerce block pages. You need to continue with shortcode pages.
+=Is it compatible with all WordPress themes ?=
+Compatibility with all themes is impossible, because there are too many, but generally if themes are developed according to WordPress and WooCommerce guidelines, **Pathao Integration for WooCommerce** is compatible with them.
+Sometimes, especially when new versions are released, it might only require some time for them to be all updated, but you can be sure that they will be tested and will be working in a few days.
+
+
+== Screenshots ==
+
+1. Settings page
+2. Setup plugin
+3. Send the order to Pathao
+4. Pathao order status
+
+== Changelog ==
+
+= version 2.0 =
+* **feature:** implemented Pathao access token generation using password and refresh token flows.
+* **feature:** integrated dynamic fetching of City, Zone, and Area data from Pathao.
+* **feature:** added delivery price calculation API integration for merchants.
+* **improve:** enhanced admin popup form with better validation and order data handling.
+* **feature:** added support for special instructions in order creation.
+* **fix:** resolved multiple bugs related to API requests and data handling.
+* **refactor:** cleaned up and structured Pathao API service methods for better maintainability.
+* **chore:** applied formatting, cleanup, and compatibility improvements across the codebase.
+
+
+= 1.1 =
+* **New:** Pathao column on order list.
+* **Update:** City and Zone fields are totally optional now.
+* **Update:** Optimize perfromence.
+* **Update:** Rebuild the API part.
+
+= 1.0.9 =
+* **Update:** Autofill item description field.
+
+= 1.0.8 =
+
+* **Fix:** Error message doesn't display when success is false.
+* Ready for v1.3 pro version.
+
+ = 1.0.7 =
+
+* Settings page updated.
+* Ready for v1.2 pro version.
+
+ = 1.0.6 =
+
+* Item description field added.
+
+ = 1.0.5 =
+
+* HPOS support added.
+* Several issues solved.
+
+ = 1.0.4 =
+
+* Fix the wrong weight on the admin order
+
+ = 1.0.3 =
+
+* `At_the_Sorting_HUB` status setting added.
+
+ = 1.0.2 =
+
+* The ability to send an order again to Pathao if the order was canceled or failed.
+
+ = 1.0.1 =
+
+* Initial release