Changeset 3749063
- Timestamp:
- 10/15/2026 05:50:24 PM (2 days ago)
- Location:
- mora-delivery-for-woocommerce
- Files:
-
- 2 deleted
- 34 edited
- 1 copied
-
tags/0.7.79 (copied) (copied from mora-delivery-for-woocommerce/trunk )
-
tags/0.7.79/README.md (deleted)
-
tags/0.7.79/includes/class-mora-admin-settings.php (modified) (1 diff)
-
tags/0.7.79/includes/class-mora-blocks-payment-method.php (modified) (2 diffs)
-
tags/0.7.79/includes/class-mora-checkout-experiment.php (modified) (1 diff)
-
tags/0.7.79/includes/class-mora-checkout.php (modified) (6 diffs)
-
tags/0.7.79/includes/class-mora-fields.php (modified) (4 diffs)
-
tags/0.7.79/includes/class-mora-leads-admin.php (modified) (9 diffs)
-
tags/0.7.79/includes/class-mora-leads.php (modified) (7 diffs)
-
tags/0.7.79/includes/class-mora-locator-block.php (modified) (1 diff)
-
tags/0.7.79/includes/class-mora-order-push.php (modified) (7 diffs)
-
tags/0.7.79/includes/class-mora-payment-gateway.php (modified) (19 diffs)
-
tags/0.7.79/includes/class-mora-paystack-driver.php (modified) (7 diffs)
-
tags/0.7.79/includes/class-mora-sentry-reporter.php (modified) (2 diffs)
-
tags/0.7.79/includes/class-mora-shipping-method.php (modified) (3 diffs)
-
tags/0.7.79/includes/class-mora-webhook-receiver.php (modified) (5 diffs)
-
tags/0.7.79/mora-delivery-for-woocommerce.php (modified) (2 diffs)
-
tags/0.7.79/readme.txt (modified) (3 diffs)
-
tags/0.7.79/uninstall.php (modified) (2 diffs)
-
trunk/README.md (deleted)
-
trunk/includes/class-mora-admin-settings.php (modified) (1 diff)
-
trunk/includes/class-mora-blocks-payment-method.php (modified) (2 diffs)
-
trunk/includes/class-mora-checkout-experiment.php (modified) (1 diff)
-
trunk/includes/class-mora-checkout.php (modified) (6 diffs)
-
trunk/includes/class-mora-fields.php (modified) (4 diffs)
-
trunk/includes/class-mora-leads-admin.php (modified) (9 diffs)
-
trunk/includes/class-mora-leads.php (modified) (7 diffs)
-
trunk/includes/class-mora-locator-block.php (modified) (1 diff)
-
trunk/includes/class-mora-order-push.php (modified) (7 diffs)
-
trunk/includes/class-mora-payment-gateway.php (modified) (19 diffs)
-
trunk/includes/class-mora-paystack-driver.php (modified) (7 diffs)
-
trunk/includes/class-mora-sentry-reporter.php (modified) (2 diffs)
-
trunk/includes/class-mora-shipping-method.php (modified) (3 diffs)
-
trunk/includes/class-mora-webhook-receiver.php (modified) (5 diffs)
-
trunk/mora-delivery-for-woocommerce.php (modified) (2 diffs)
-
trunk/readme.txt (modified) (3 diffs)
-
trunk/uninstall.php (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
mora-delivery-for-woocommerce/tags/0.7.79/includes/class-mora-admin-settings.php
r3746963 r3749063 400 400 } 401 401 self::$resolving_site_wide_credentials = true; 402 $scanned = self::scan_zone_instances_for_credentials();402 $scanned = self::scan_zone_instances_for_credentials(); 403 403 self::$resolving_site_wide_credentials = false; 404 404 if ( '' === $scanned['api_key'] ) { -
mora-delivery-for-woocommerce/tags/0.7.79/includes/class-mora-blocks-payment-method.php
r3746963 r3749063 69 69 public function initialize() { 70 70 $this->settings = get_option( 'woocommerce_' . $this->name . '_settings', array() ); 71 $gateways = WC()->payment_gateways() ? WC()->payment_gateways()->payment_gateways() : array();72 $this->gateway = isset( $gateways[ $this->name ] ) ? $gateways[ $this->name ] : null;71 $gateways = WC()->payment_gateways() ? WC()->payment_gateways()->payment_gateways() : array(); 72 $this->gateway = isset( $gateways[ $this->name ] ) ? $gateways[ $this->name ] : null; 73 73 } 74 74 … … 90 90 public function get_payment_method_data() { 91 91 return array( 92 'title' => $this->gateway ? $this->gateway->title : '',93 'description' => $this->gateway ? $this->gateway->description : '',92 'title' => $this->gateway ? $this->gateway->title : '', 93 'description' => $this->gateway ? $this->gateway->description : '', 94 94 // Static per page load (an admin setting, not cart state), safe 95 95 // for canMakePayment() to branch on directly; the live destination -
mora-delivery-for-woocommerce/tags/0.7.79/includes/class-mora-checkout-experiment.php
r3746963 r3749063 176 176 } 177 177 178 $due_today = (float) WC()->cart->get_shipping_total() + (float) WC()->cart->get_shipping_tax();178 $due_today = (float) WC()->cart->get_shipping_total() + (float) WC()->cart->get_shipping_tax(); 179 179 $due_on_delivery = (float) WC()->cart->get_total( 'edit' ) - $due_today; 180 180 if ( $due_on_delivery < 0 ) { -
mora-delivery-for-woocommerce/tags/0.7.79/includes/class-mora-checkout.php
r3746963 r3749063 35 35 class Mora_Checkout { 36 36 37 const NONCE_ACTION = 'mora_wc_set_coordinates';37 const NONCE_ACTION = 'mora_wc_set_coordinates'; 38 38 const COORDS_TRANSIENT_PREFIX = 'mora_coords_'; 39 39 40 40 // See get_places_api_key()'s own docblock for why this is cached at all, 41 41 // and why success and failure carry different TTLs. 42 const PLACES_KEY_TRANSIENT = 'mora_wc_places_key_cache';43 const PLACES_KEY_CACHE_TTL = 12 * HOUR_IN_SECONDS;42 const PLACES_KEY_TRANSIENT = 'mora_wc_places_key_cache'; 43 const PLACES_KEY_CACHE_TTL = 12 * HOUR_IN_SECONDS; 44 44 const PLACES_KEY_FAILURE_CACHE_TTL = 5 * MINUTE_IN_SECONDS; 45 45 … … 127 127 */ 128 128 public static function locator_script_data() { 129 $ng_states = WC()->countries->get_states( 'NG' ); 129 130 return array( 130 131 'placesApiKey' => self::get_places_api_key(), … … 136 137 // code => name list classic's own <select> options already reflect, 137 138 // sourced from the same place WooCommerce itself does. 138 'ngStates' => WC()->countries->get_states( 'NG' ) ?: array(),139 'ngStates' => $ng_states ? $ng_states : array(), 139 140 'i18n' => array( 140 141 // The widget is a locator (sets coordinates, city, state, LGA for … … 145 146 // phase adds a real, required field literally called "Landmark", so the 146 147 // widget can no longer describe itself the same way. 147 'locatorLabel' => __( 'Find your delivery address', 'mora-delivery-for-woocommerce' ),148 'locatorLabel' => __( 'Find your delivery address', 'mora-delivery-for-woocommerce' ), 148 149 // Measured on 20 real Lagos addresses: 18/20 resolved to door level when 149 150 // the query contained a house number. That instruction is the single most … … 153 154 // (New) release is not confirmed against Google's primary docs from this 154 155 // environment). 155 'locatorHint' => __( 'Start with your house number, like 12 Allen Avenue. We use this to work out your delivery fee.', 'mora-delivery-for-woocommerce' ),156 'locatorHint' => __( 'Start with your house number, like 12 Allen Avenue. We use this to work out your delivery fee.', 'mora-delivery-for-woocommerce' ), 156 157 // A generic example address, deliberately not the Mora hub's own street 157 158 // address (16 Adebiyi Street): that's a real, specific location tied to the … … 295 296 // two days for a guest. The pin has no reason to outlive the session it belongs to. 296 297 $ttl = is_user_logged_in() ? WEEK_IN_SECONDS : 2 * DAY_IN_SECONDS; 297 set_transient( $key, array( 298 'lat' => $lat, 299 'lon' => $lon, 300 'lga' => $lga, 301 'pinned' => $pinned, 302 ), $ttl ); 298 set_transient( 299 $key, 300 array( 301 'lat' => $lat, 302 'lon' => $lon, 303 'lga' => $lga, 304 'pinned' => $pinned, 305 ), 306 $ttl 307 ); 303 308 } 304 309 -
mora-delivery-for-woocommerce/tags/0.7.79/includes/class-mora-fields.php
r3746963 r3749063 99 99 'classic_type' => 'text', 100 100 'required_for_mora' => false, // always optional, Mora rate or not; see the class 101 // docblock for why this was changed.101 // docblock for why this was changed. 102 102 'label' => __( 'Landmark', 'mora-delivery-for-woocommerce' ), 103 103 'placeholder' => __( 'A shop, school or building we can find', 'mora-delivery-for-woocommerce' ), … … 189 189 190 190 /** 191 * WooCommerce's built-in pickup method ids: `local_pickup` on classic 192 * checkout, `pickup_location` on Blocks checkout (WooCommerce's newer 193 * Local Pickup). Both carry Mora's hub-pickup fee. 194 */ 195 const PICKUP_METHOD_IDS = array( 'local_pickup', 'pickup_location' ); 196 197 /** 198 * Whether a shipping method id is one of WooCommerce's pickup methods. 199 * 200 * @param string $method_id Shipping method id. 201 * @return bool 202 */ 203 public static function is_pickup_method( $method_id ) { 204 return in_array( $method_id, self::PICKUP_METHOD_IDS, true ); 205 } 206 207 /** 191 208 * has_mora_shipping_line()'s sibling for hub pickup: does this order use 192 * WooCommerce's own built-in Local Pickup method (method id193 * `local_pickup`,not a Mora-owned one). Mora_Order_Push::can_push()209 * one of WooCommerce's own built-in pickup methods (PICKUP_METHOD_IDS, 210 * not a Mora-owned one). Mora_Order_Push::can_push() 194 211 * reads this to also push a pickup order, alongside a Mora delivery 195 212 * one: the customer collects at the Mora hub instead of a rider … … 199 216 public static function has_local_pickup_line( $order ) { 200 217 foreach ( $order->get_items( 'shipping' ) as $item ) { 201 if ( 'local_pickup' === $item->get_method_id() ) {218 if ( self::is_pickup_method( $item->get_method_id() ) ) { 202 219 return true; 203 220 } … … 252 269 * Landmark is never enforced here, on any rate; see the class docblock for why. 253 270 */ 254 public static function validate_blocks_phone_and_landmark( $context, $result ) { 271 public static function validate_blocks_phone_and_landmark( $context, $result ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- $result is part of the hook's signature. 255 272 $chosen_methods = WC()->session ? WC()->session->get( 'chosen_shipping_methods', array() ) : array(); 256 273 if ( ! self::is_mora_rate_chosen( $chosen_methods ) ) { -
mora-delivery-for-woocommerce/tags/0.7.79/includes/class-mora-leads-admin.php
r3746963 r3749063 17 17 18 18 const EXPORT_ACTION = 'mora_wc_leads_export'; 19 20 // Shown in a list column with no value. 21 const EMPTY_CELL = '-'; 19 22 20 23 /** … … 80 83 } 81 84 82 public static function define_columns( $columns ) { 85 public static function define_columns( $columns ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found -- filter signature; the default columns are replaced, not extended. 83 86 return array( 84 87 'cb' => '<input type="checkbox" />', … … 95 98 96 99 /** 100 * A column value, or the empty-cell placeholder when it is empty. 101 * 102 * @param mixed $value Stored meta value. 103 * @return mixed 104 */ 105 private static function cell( $value ) { 106 return $value ? $value : self::EMPTY_CELL; 107 } 108 109 /** 97 110 * Every column but Cart is plain text, always esc_html()'d. The Cart 98 111 * column is the one exception: wc_price() returns real markup (a … … 107 120 case 'mora_lead_name': 108 121 $name = trim( get_post_meta( $post_id, Mora_Leads::META_FIRST_NAME, true ) . ' ' . get_post_meta( $post_id, Mora_Leads::META_LAST_NAME, true ) ); 109 echo esc_html( $name ?: '—');122 echo esc_html( self::cell( $name ) ); 110 123 break; 111 124 case 'mora_lead_phone': 112 echo esc_html( get_post_meta( $post_id, Mora_Leads::META_PHONE, true ) ?: '—');125 echo esc_html( self::cell( get_post_meta( $post_id, Mora_Leads::META_PHONE, true ) ) ); 113 126 break; 114 127 case 'mora_lead_email': 115 echo esc_html( get_post_meta( $post_id, Mora_Leads::META_EMAIL, true ) ?: '—');128 echo esc_html( self::cell( get_post_meta( $post_id, Mora_Leads::META_EMAIL, true ) ) ); 116 129 break; 117 130 case 'mora_lead_city': 118 echo esc_html( get_post_meta( $post_id, Mora_Leads::META_CITY, true ) ?: '—');131 echo esc_html( self::cell( get_post_meta( $post_id, Mora_Leads::META_CITY, true ) ) ); 119 132 break; 120 133 case 'mora_lead_cart': … … 124 137 break; 125 138 case 'mora_lead_products': 126 echo esc_html( get_post_meta( $post_id, Mora_Leads::META_PRODUCTS, true ) ?: '—');139 echo esc_html( self::cell( get_post_meta( $post_id, Mora_Leads::META_PRODUCTS, true ) ) ); 127 140 break; 128 141 case 'mora_lead_seen': 129 142 $last_seen = (int) get_post_meta( $post_id, Mora_Leads::META_LAST_SEEN, true ); 130 echo esc_html( $last_seen ? date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $last_seen ) : '—');143 echo esc_html( $last_seen ? date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $last_seen ) : self::EMPTY_CELL ); 131 144 break; 132 145 case 'mora_lead_status': 133 $status = get_post_meta( $post_id, Mora_Leads::META_STATUS, true );146 $status = get_post_meta( $post_id, Mora_Leads::META_STATUS, true ); 134 147 $order_id = (int) get_post_meta( $post_id, Mora_Leads::META_ORDER_ID, true ); 135 148 if ( Mora_Leads::STATUS_CONVERTED === $status && $order_id && function_exists( 'wc_get_order' ) && wc_get_order( $order_id ) ) { … … 148 161 * (a bare WooCommerce → Leads click, never a query string). 149 162 */ 150 public static function status_filter_views( $views ) { 163 public static function status_filter_views( $views ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found -- filter signature; the default views are replaced, not extended. 151 164 $current = isset( $_GET['lead_status'] ) ? sanitize_key( wp_unslash( $_GET['lead_status'] ) ) : 'open'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended 152 165 … … 159 172 $new_views = array(); 160 173 foreach ( $tabs as $key => $label ) { 161 $count = self::count_leads_for_status( $key );162 $url = add_query_arg(174 $count = self::count_leads_for_status( $key ); 175 $url = add_query_arg( 163 176 array( 164 177 'post_type' => Mora_Leads::POST_TYPE, … … 167 180 admin_url( 'edit.php' ) 168 181 ); 169 $class = $current === $key ? ' class="current"' : '';182 $class = $current === $key ? ' class="current"' : ''; 170 183 $new_views[ $key ] = sprintf( 171 184 '<a href="%s"%s>%s <span class="count">(%d)</span></a>', … … 347 360 // computed once, at capture time, and is what conversion matching 348 361 // also reads, so export and matching can never quietly disagree. 349 $phone = get_post_meta( $lead_id, Mora_Leads::META_PHONE_NORMALIZED, true );362 $phone = get_post_meta( $lead_id, Mora_Leads::META_PHONE_NORMALIZED, true ); 350 363 fputcsv( 351 364 $out, -
mora-delivery-for-woocommerce/tags/0.7.79/includes/class-mora-leads.php
r3746963 r3749063 45 45 const POST_TYPE = 'mora_lead'; 46 46 47 const META_SESSION_ID = '_mora_lead_session_id';48 const META_FIRST_NAME = '_mora_lead_first_name';49 const META_LAST_NAME = '_mora_lead_last_name';47 const META_SESSION_ID = '_mora_lead_session_id'; 48 const META_FIRST_NAME = '_mora_lead_first_name'; 49 const META_LAST_NAME = '_mora_lead_last_name'; 50 50 // As the customer typed it (local format, e.g. 08012345678): what the 51 51 // merchant searches by and sees in the admin list. META_PHONE_NORMALIZED … … 55 55 const META_PHONE = '_mora_lead_phone'; 56 56 const META_PHONE_NORMALIZED = '_mora_lead_phone_normalized'; 57 const META_EMAIL = '_mora_lead_email';58 const META_CITY = '_mora_lead_city';59 const META_STATE = '_mora_lead_state';60 const META_CART_COUNT = '_mora_lead_cart_count';61 const META_CART_TOTAL = '_mora_lead_cart_total';57 const META_EMAIL = '_mora_lead_email'; 58 const META_CITY = '_mora_lead_city'; 59 const META_STATE = '_mora_lead_state'; 60 const META_CART_COUNT = '_mora_lead_cart_count'; 61 const META_CART_TOTAL = '_mora_lead_cart_total'; 62 62 // "Name (SKU), Name (SKU)..." for the whole cart at capture time, not 63 63 // one line item: a lead can span multiple products, the same reason 64 64 // cart_count/cart_total summarize the whole cart rather than one item. 65 const META_PRODUCTS = '_mora_lead_products';66 const META_FIRST_SEEN = '_mora_lead_first_seen';67 const META_LAST_SEEN = '_mora_lead_last_seen';68 const META_STATUS = '_mora_lead_status';69 const META_ORDER_ID = '_mora_lead_order_id';65 const META_PRODUCTS = '_mora_lead_products'; 66 const META_FIRST_SEEN = '_mora_lead_first_seen'; 67 const META_LAST_SEEN = '_mora_lead_last_seen'; 68 const META_STATUS = '_mora_lead_status'; 69 const META_ORDER_ID = '_mora_lead_order_id'; 70 70 71 71 const STATUS_OPEN = 'open'; 72 72 const STATUS_CONVERTED = 'converted'; 73 73 74 const THROTTLE_SECONDS = 60;74 const THROTTLE_SECONDS = 60; 75 75 const RETENTION_DAYS_DEFAULT = 90; 76 const PURGE_CRON_HOOK = 'mora_wc_purge_leads';76 const PURGE_CRON_HOOK = 'mora_wc_purge_leads'; 77 77 // A daily cron deleting an unbounded set in one request will eventually 78 78 // time out; a capped, oldest-first batch clears a real backlog over a 79 79 // few days instead of failing outright on the day it's biggest. 80 const PURGE_BATCH_SIZE = 200;80 const PURGE_BATCH_SIZE = 200; 81 81 82 82 // Only these three WooCommerce order statuses mean "this order is real … … 177 177 * pollutes the leads list with the merchant's own sessions. 178 178 */ 179 public static function capture_from_store_api( $customer, $request ) { 179 public static function capture_from_store_api( $customer, $request ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- $request is part of the hook's signature. 180 180 if ( ! $customer instanceof WC_Customer ) { 181 181 return; … … 243 243 // Shipping-preferred-falls-back-to-billing, the same idiom 244 244 // Mora_Order_Push::build_payload() already uses for city/state. 245 'city' => $customer->get_shipping_city() ? : $customer->get_billing_city(),246 'state' => $customer->get_shipping_state() ? : $customer->get_billing_state(),245 'city' => $customer->get_shipping_city() ? $customer->get_shipping_city() : $customer->get_billing_city(), 246 'state' => $customer->get_shipping_state() ? $customer->get_shipping_state() : $customer->get_billing_state(), 247 247 'cart_count' => WC()->cart ? WC()->cart->get_cart_contents_count() : 0, 248 248 // 'edit' context: the raw numeric total, not a pre-formatted … … 272 272 continue; 273 273 } 274 $sku = $product->get_sku();274 $sku = $product->get_sku(); 275 275 $names[] = $sku ? $product->get_name() . ' (' . $sku . ')' : $product->get_name(); 276 276 } … … 422 422 } 423 423 424 public static function reopen_lead_for_order( $order_id, $order ) { 424 public static function reopen_lead_for_order( $order_id, $order ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- $order is part of the hook's signature. 425 425 $lead_ids = get_posts( 426 426 array( … … 582 582 * eraser keeps calling this until a page genuinely comes back empty. 583 583 */ 584 public static function privacy_eraser( $email_address, $page = 1 ) { 584 public static function privacy_eraser( $email_address, $page = 1 ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- eraser callback signature; $page deliberately unused, see above. 585 585 $per_page = 50; 586 586 $lead_ids = get_posts( -
mora-delivery-for-woocommerce/tags/0.7.79/includes/class-mora-locator-block.php
r3746963 r3749063 56 56 // Naming mirrors Mora_Payment_Gateway::COD_DISABLE_ACTION, the existing 57 57 // one-click-admin-notice pattern this feature follows. 58 const ADD_ACTION = 'mora_wc_add_locator_block';59 const RESULT_TRANSIENT_PREFIX = 'mora_wc_locator_result_';58 const ADD_ACTION = 'mora_wc_add_locator_block'; 59 const RESULT_TRANSIENT_PREFIX = 'mora_wc_locator_result_'; 60 60 61 61 public static function init() { -
mora-delivery-for-woocommerce/tags/0.7.79/includes/class-mora-order-push.php
r3748164 r3749063 120 120 return true; // hub pickup: no destination to validate 121 121 } 122 $country = $order->get_shipping_country() ?: $order->get_billing_country();122 $country = self::first_filled( $order->get_shipping_country(), $order->get_billing_country() ); 123 123 if ( 'NG' !== $country ) { 124 124 $reason = sprintf( … … 763 763 // A Mora delivery line always wins if present (matches can_push()'s own 764 764 // priority): only treat this as a hub-pickup order when there is no 765 // Mora line at all. WooCommerce's built-in Local Pickup method766 // ( `local_pickup`) is whatcarries the pickup fee in that case; the765 // Mora line at all. WooCommerce's built-in pickup method 766 // (Mora_Fields::PICKUP_METHOD_IDS) carries the pickup fee in that case; the 767 767 // customer collects at the Mora hub, so there is no delivery address 768 768 // to send at all. 769 $is_pickup = empty( $mora_lines ); 770 $expected_method = $is_pickup ? 'local_pickup' : 'mora'; 771 $fee_lines = $is_pickup 769 $is_pickup = empty( $mora_lines ); 770 $fee_lines = $is_pickup 772 771 ? array_values( 773 772 array_filter( 774 773 $shipping_lines, 775 774 function ( $item ) { 776 return 'local_pickup' === $item->get_method_id();775 return Mora_Fields::is_pickup_method( $item->get_method_id() ); 777 776 } 778 777 ) 779 778 ) 780 779 : $mora_lines; 781 $other_lines = array_values(780 $other_lines = array_values( 782 781 array_filter( 783 782 $shipping_lines, 784 function ( $item ) use ( $expected_method ) { 785 return $expected_method !== $item->get_method_id(); 783 function ( $item ) use ( $is_pickup ) { 784 $id = $item->get_method_id(); 785 return $is_pickup ? ! Mora_Fields::is_pickup_method( $id ) : 'mora' !== $id; 786 786 } 787 787 ) … … 801 801 } 802 802 803 $delivery_fee = 0.0;804 $shipping_tax = 0.0;803 $delivery_fee = 0.0; 804 $shipping_tax = 0.0; 805 805 foreach ( $fee_lines as $line ) { 806 806 $delivery_fee += (float) $line->get_total(); … … 832 832 // Mora delivery rate (Mora_Payment_Gateway::every_package_rate_or_ 833 833 // false()), never on Local Pickup. 834 $cod_amount = $is_mora_cod ? ( $order_total - $delivery_fee ) : 0.0;835 $state_code = $is_pickup ? null : ( $order->get_shipping_state() ?:$order->get_billing_state() );834 $cod_amount = $is_mora_cod ? ( $order_total - $delivery_fee ) : 0.0; 835 $state_code = $is_pickup ? null : self::first_filled( $order->get_shipping_state(), $order->get_billing_state() ); 836 836 837 837 return array( 838 838 'merchant_order_id' => (string) $order->get_id(), 839 839 'site_url' => home_url( '/' ), 840 'quote_id' => $order->get_meta( '_mora_quote_id' ) ?: null,840 'quote_id' => self::filled_or_null( $order->get_meta( '_mora_quote_id' ) ), 841 841 // Never null: OrderIn.placed_at is required and orders.placed_at is 842 842 // NOT NULL, so a null here is a guaranteed 422, and a 422 is never … … 858 858 'fulfillment_type' => $is_pickup ? 'pickup' : 'delivery', 859 859 // Not sent: delivery_fee_collected, state_name. Both derived in mora-api. 860 'customer_name' => trim( $order->get_shipping_first_name() . ' ' . $order->get_shipping_last_name() ) 861 ?: trim( $order->get_billing_first_name() . ' ' . $order->get_billing_last_name() ), 860 'customer_name' => self::first_filled( 861 trim( $order->get_shipping_first_name() . ' ' . $order->get_shipping_last_name() ), 862 trim( $order->get_billing_first_name() . ' ' . $order->get_billing_last_name() ) 863 ), 862 864 'customer_phone' => $order->get_billing_phone(), 863 'customer_phone_alt' => $order->get_meta( '_mora_phone_alt' ) ?: null,865 'customer_phone_alt' => self::filled_or_null( $order->get_meta( '_mora_phone_alt' ) ), 864 866 'customer_email' => $order->get_billing_email(), 865 867 // Every address/coordinate field below is null for a pickup order: … … 867 869 // delivery address to send at all (mora-api's own OrderIn requires 868 870 // these only when fulfillment_type is "delivery"). 869 'address_1' => $is_pickup ? null : ( $order->get_shipping_address_1() ?:$order->get_billing_address_1() ),870 'address_2' => $is_pickup ? null : ( $order->get_shipping_address_2() ?:$order->get_billing_address_2() ),871 'landmark' => $is_pickup ? null : ( $order->get_meta( '_mora_landmark' ) ?: null),872 'city' => $is_pickup ? null : ( $order->get_shipping_city() ?:$order->get_billing_city() ),871 'address_1' => $is_pickup ? null : self::first_filled( $order->get_shipping_address_1(), $order->get_billing_address_1() ), 872 'address_2' => $is_pickup ? null : self::first_filled( $order->get_shipping_address_2(), $order->get_billing_address_2() ), 873 'landmark' => $is_pickup ? null : self::filled_or_null( $order->get_meta( '_mora_landmark' ) ), 874 'city' => $is_pickup ? null : self::first_filled( $order->get_shipping_city(), $order->get_billing_city() ), 873 875 'state_code' => $state_code, 874 'lga' => $is_pickup ? null : ( $order->get_meta( '_mora_lga' ) ?: null),875 'pinned_address' => $is_pickup ? null : ( $order->get_meta( '_mora_pinned_address' ) ?: null),876 'lga' => $is_pickup ? null : self::filled_or_null( $order->get_meta( '_mora_lga' ) ), 877 'pinned_address' => $is_pickup ? null : self::filled_or_null( $order->get_meta( '_mora_pinned_address' ) ), 876 878 'lat' => ( ! $is_pickup && '' !== $order->get_meta( '_mora_lat' ) ) ? (float) $order->get_meta( '_mora_lat' ) : null, 877 879 'lon' => ( ! $is_pickup && '' !== $order->get_meta( '_mora_lon' ) ) ? (float) $order->get_meta( '_mora_lon' ) : null, … … 887 889 array_values( $order->get_items() ) 888 890 ), 889 'notes' => $order->get_customer_note() ?: null, 890 ); 891 'notes' => self::filled_or_null( $order->get_customer_note() ), 892 ); 893 } 894 895 /** 896 * The preferred value when it is truthy, otherwise the fallback. Same 897 * result as PHP's short ternary ( $preferred ?: $fallback ), which the 898 * coding standard disallows; both arguments are side-effect-free getters. 899 * 900 * @param mixed $preferred Value to use when truthy (e.g. a shipping field). 901 * @param mixed $fallback Value to use otherwise (e.g. the billing field). 902 * @return mixed 903 */ 904 private static function first_filled( $preferred, $fallback ) { 905 return $preferred ? $preferred : $fallback; 906 } 907 908 /** 909 * The value when it is truthy, otherwise null (an empty meta string is 910 * sent to mora-api as null, never as ""). 911 * 912 * @param mixed $value Value to check. 913 * @return mixed|null 914 */ 915 private static function filled_or_null( $value ) { 916 return $value ? $value : null; 891 917 } 892 918 -
mora-delivery-for-woocommerce/tags/0.7.79/includes/class-mora-payment-gateway.php
r3746963 r3749063 94 94 public function __construct() { 95 95 $this->id = 'mora_pay_on_delivery'; 96 $this->icon = '';97 $this->has_fields = false;98 $this->method_title = __( 'Mora Goods on Delivery', 'mora-delivery-for-woocommerce' );99 $this->method_description = __( 'Charges only the order\'s delivery/shipping fee at checkout through the merchant\'s own Paystack account; any goods balance is collected in cash on delivery. Available for any order regardless of shipping method, as long as this gateway is enabled and a delivery fee is present; optionally restricted to Lagos-only destinations below.', 'mora-delivery-for-woocommerce' );96 $this->icon = ''; 97 $this->has_fields = false; 98 $this->method_title = __( 'Mora Goods on Delivery', 'mora-delivery-for-woocommerce' ); 99 $this->method_description = __( 'Charges only the order\'s delivery/shipping fee at checkout through the merchant\'s own Paystack account; any goods balance is collected in cash on delivery. Available for any order regardless of shipping method, as long as this gateway is enabled and a delivery fee is present; optionally restricted to Lagos-only destinations below.', 'mora-delivery-for-woocommerce' ); 100 100 // Deliberately no 'refunds': a Paystack refund here is a manual action in 101 101 // the merchant's own PSP dashboard (README), not something this plugin … … 108 108 $this->title = $this->get_option( 'title' ); 109 109 $this->description = $this->get_option( 'description' ); 110 $this->enabled = $this->get_option( 'enabled', 'no' );110 $this->enabled = $this->get_option( 'enabled', 'no' ); 111 111 $this->balance_due_status = $this->get_option( 'balance_due_status', 'on-hold' ); 112 112 … … 125 125 public function init_form_fields() { 126 126 $this->form_fields = array( 127 'enabled' => array(127 'enabled' => array( 128 128 'title' => __( 'Enable', 'mora-delivery-for-woocommerce' ), 129 129 'type' => 'checkbox', … … 131 131 'default' => 'no', 132 132 ), 133 'title' => array(133 'title' => array( 134 134 'title' => __( 'Title', 'mora-delivery-for-woocommerce' ), 135 135 'type' => 'text', … … 138 138 'desc_tip' => true, 139 139 ), 140 'description' => array(140 'description' => array( 141 141 'title' => __( 'Description', 'mora-delivery-for-woocommerce' ), 142 142 'type' => 'textarea', 143 143 'default' => __( 'Pay the delivery fee now by card, bank transfer, or USSD. Any goods balance is paid to the rider on delivery.', 'mora-delivery-for-woocommerce' ), 144 144 ), 145 'restrict_to_lagos' => array(145 'restrict_to_lagos' => array( 146 146 'title' => __( 'Restrict to Lagos', 'mora-delivery-for-woocommerce' ), 147 147 'type' => 'checkbox', … … 150 150 'description' => __( 'Off by default: this gateway otherwise applies to any order regardless of shipping method or destination. Turn this on if the store delivers with Mora nationwide but only wants this fee-now/collect-on-delivery flow offered within Lagos; an interstate order with Mora chosen still delivers, it just pays in full upfront through another payment method.', 'mora-delivery-for-woocommerce' ), 151 151 ), 152 'balance_due_status' => array(152 'balance_due_status' => array( 153 153 'title' => __( 'Order status while a goods balance is due', 'mora-delivery-for-woocommerce' ), 154 154 'type' => 'select', … … 160 160 'description' => __( 'Used only while a goods balance remains after the delivery fee is collected; an order with nothing left owing always goes straight to Processing regardless of this setting. Choose Processing if this store\'s fulfilment/warehouse tooling triggers off that status rather than On hold.', 'mora-delivery-for-woocommerce' ), 161 161 ), 162 'hold_minutes' => array(162 'hold_minutes' => array( 163 163 'title' => __( 'Hold unpaid bank transfer and USSD orders for', 'mora-delivery-for-woocommerce' ), 164 164 'type' => 'number', … … 174 174 // no longer polled. Previously enforced nowhere, client or 175 175 // server side. 176 'custom_attributes' => array( 'min' => '1', 'max' => (string) self::MAX_POLL_MINUTES ), 176 'custom_attributes' => array( 177 'min' => '1', 178 'max' => (string) self::MAX_POLL_MINUTES, 179 ), 177 180 'description' => __( 'Minutes to hold an order open, and prevent WooCommerce\'s own unpaid-order sweeper from cancelling it, while waiting for a bank transfer or USSD payment to verify. After this, Mora marks the order failed itself.', 'mora-delivery-for-woocommerce' ), 178 181 'default' => '180', 179 182 'desc_tip' => true, 180 183 ), 181 'psp_driver' => array(184 'psp_driver' => array( 182 185 'title' => __( 'Payment provider', 'mora-delivery-for-woocommerce' ), 183 186 'type' => 'select', … … 187 190 'default' => 'paystack', 188 191 ), 189 'paystack_override_heading' => array(192 'paystack_override_heading' => array( 190 193 'title' => __( 'Paystack keys (optional)', 'mora-delivery-for-woocommerce' ), 191 194 'type' => 'title', 192 195 'description' => __( 'Leave these blank to use the keys already configured under WooCommerce > Settings > Payments > Paystack. Only fill these in if this store does not have that plugin installed.', 'mora-delivery-for-woocommerce' ), 193 196 ), 194 'paystack_override_testmode' => array(197 'paystack_override_testmode' => array( 195 198 'title' => __( 'Test mode', 'mora-delivery-for-woocommerce' ), 196 199 'type' => 'checkbox', … … 198 201 'default' => 'no', 199 202 ), 200 'paystack_override_test_secret_key' => array(203 'paystack_override_test_secret_key' => array( 201 204 'title' => __( 'Test secret key', 'mora-delivery-for-woocommerce' ), 202 205 'type' => 'password', 203 206 'default' => '', 204 207 ), 205 'paystack_override_live_secret_key' => array(208 'paystack_override_live_secret_key' => array( 206 209 'title' => __( 'Live secret key', 'mora-delivery-for-woocommerce' ), 207 210 'type' => 'password', … … 323 326 return false; 324 327 } 325 $state = WC()->customer->get_shipping_state() ?: WC()->customer->get_billing_state(); 328 $state = WC()->customer->get_shipping_state(); 329 if ( ! $state ) { 330 $state = WC()->customer->get_billing_state(); 331 } 326 332 return self::LAGOS_STATE_CODE === $state; 327 333 } … … 399 405 sprintf( 400 406 'Mora: %s initialize() failed for order #%d: %s', 401 $driver->id(), $order->get_id(), isset( $result['error'] ) ? $result['error'] : 'unknown error' 407 $driver->id(), 408 $order->get_id(), 409 isset( $result['error'] ) ? $result['error'] : 'unknown error' 402 410 ), 403 411 array( 'source' => 'mora' ) … … 418 426 $order->save(); 419 427 420 $order->add_order_note( sprintf( 421 'Mora: delivery-fee payment of %s started via %s (reference %s)%s.', 422 wc_price( $amount ), $driver->label(), $reference, 423 $driver->is_testmode() ? ' [TEST MODE]' : '' 424 ) ); 428 $order->add_order_note( 429 sprintf( 430 'Mora: delivery-fee payment of %s started via %s (reference %s)%s.', 431 wc_price( $amount ), 432 $driver->label(), 433 $reference, 434 $driver->is_testmode() ? ' [TEST MODE]' : '' 435 ) 436 ); 425 437 426 438 $this->schedule_poll( $order->get_id(), $reference, 0, 0 ); … … 589 601 sprintf( 590 602 'Mora: gave up polling reference %s for order #%d after %d minutes, still unpaid.', 591 $reference, $order_id, $elapsed_minutes 603 $reference, 604 $order_id, 605 $elapsed_minutes 592 606 ), 593 607 array( 'source' => 'mora' ) … … 649 663 sprintf( 650 664 'Mora: verified payment amount/currency mismatch for order #%d, reference %s: expected %s %.2f, got %s %.2f. Not completing.', 651 $order->get_id(), $reference, $expected_currency, $expected_amount, 652 isset( $result['currency'] ) ? $result['currency'] : '?', $result['amount_major'] 665 $order->get_id(), 666 $reference, 667 $expected_currency, 668 $expected_amount, 669 isset( $result['currency'] ) ? $result['currency'] : '?', 670 $result['amount_major'] 653 671 ), 654 672 array( 'source' => 'mora' ) … … 664 682 $order->update_meta_data( '_mora_psp_verified_at', current_time( 'mysql', true ) ); 665 683 $order->save(); 666 $order->add_order_note( sprintf( 667 'Mora: payment of %s verified against reference %s, but this order is already %s. The customer has paid and needs a refund or a new order. Not resurrected automatically.', 668 wc_price( $result['amount_major'] ), $reference, $order->get_status() 669 ) ); 684 $order->add_order_note( 685 sprintf( 686 'Mora: payment of %s verified against reference %s, but this order is already %s. The customer has paid and needs a refund or a new order. Not resurrected automatically.', 687 wc_price( $result['amount_major'] ), 688 $reference, 689 $order->get_status() 690 ) 691 ); 670 692 wc_get_logger()->error( 671 693 sprintf( 672 694 'Mora: verified payment against a cancelled/refunded/failed order #%d, reference %s.', 673 $order->get_id(), $reference 695 $order->get_id(), 696 $reference 674 697 ), 675 698 array( 'source' => 'mora' ) … … 701 724 // participates in, so noting "fee collected" ahead of a status change 702 725 // that might still fail would describe something that didn't happen. 703 $order->add_order_note( sprintf( 704 'Mora: delivery-fee payment verified via %s (reference %s). Collected %s; %s owed on delivery.', 705 $driver->label(), $reference, wc_price( $result['amount_major'] ), 706 $goods_balance > 0 ? wc_price( $goods_balance ) : __( 'nothing', 'mora-delivery-for-woocommerce' ) 707 ) ); 726 $order->add_order_note( 727 sprintf( 728 'Mora: delivery-fee payment verified via %s (reference %s). Collected %s; %s owed on delivery.', 729 $driver->label(), 730 $reference, 731 wc_price( $result['amount_major'] ), 732 $goods_balance > 0 ? wc_price( $goods_balance ) : __( 'nothing', 'mora-delivery-for-woocommerce' ) 733 ) 734 ); 708 735 } 709 736 … … 733 760 return; 734 761 } 735 echo '<p>' . esc_html( sprintf( 762 echo '<p>' . esc_html( 763 sprintf( 736 764 /* translators: %s: goods balance still owed, formatted as a price. */ 737 __( 'Your delivery fee has been paid. %s is still owed for the goods, payable to the rider on delivery.', 'mora-delivery-for-woocommerce' ), 738 wp_strip_all_tags( wc_price( (float) $balance ) ) 739 ) ) . '</p>'; 765 __( 'Your delivery fee has been paid. %s is still owed for the goods, payable to the rider on delivery.', 'mora-delivery-for-woocommerce' ), 766 wp_strip_all_tags( wc_price( (float) $balance ) ) 767 ) 768 ) . '</p>'; 740 769 } 741 770 … … 759 788 if ( isset( $_GET[ self::COD_DISABLED_FLAG ] ) && '1' === sanitize_text_field( wp_unslash( $_GET[ self::COD_DISABLED_FLAG ] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification 760 789 echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__( 761 'WooCommerce\'s stock Cash on Delivery gateway has been disabled.', 'mora-delivery-for-woocommerce' 790 'WooCommerce\'s stock Cash on Delivery gateway has been disabled.', 791 'mora-delivery-for-woocommerce' 762 792 ) . '</p></div>'; 763 793 } elseif ( $this->is_cod_enabled() ) { -
mora-delivery-for-woocommerce/tags/0.7.79/includes/class-mora-paystack-driver.php
r3746963 r3749063 77 77 if ( $has_override ) { 78 78 $key = $this->override_testmode ? $this->override_test_secret_key : $this->override_live_secret_key; 79 return array( 'key' => $key, 'testmode' => $this->override_testmode ); 80 } 81 82 $paystack_settings = get_option( 'woocommerce_paystack_settings', array() ); 79 return array( 80 'key' => $key, 81 'testmode' => $this->override_testmode, 82 ); 83 } 84 85 $paystack_settings = get_option( 'woocommerce_paystack_settings', array() ); 83 86 $inherited_testmode = isset( $paystack_settings['testmode'] ) && 'yes' === $paystack_settings['testmode']; 84 $key = $inherited_testmode87 $key = $inherited_testmode 85 88 ? ( isset( $paystack_settings['test_secret_key'] ) ? $paystack_settings['test_secret_key'] : '' ) 86 89 : ( isset( $paystack_settings['live_secret_key'] ) ? $paystack_settings['live_secret_key'] : '' ); 87 90 88 return array( 'key' => $key, 'testmode' => $inherited_testmode ); 91 return array( 92 'key' => $key, 93 'testmode' => $inherited_testmode, 94 ); 89 95 } 90 96 … … 92 98 $keys = $this->resolve_keys(); 93 99 if ( '' === trim( $keys['key'] ) ) { 94 return array( 'ok' => false, 'error' => 'Paystack secret key is not configured.' ); 100 return array( 101 'ok' => false, 102 'error' => 'Paystack secret key is not configured.', 103 ); 95 104 } 96 105 … … 119 128 120 129 if ( is_wp_error( $response ) ) { 121 return array( 'ok' => false, 'error' => $response->get_error_message() ); 130 return array( 131 'ok' => false, 132 'error' => $response->get_error_message(), 133 ); 122 134 } 123 135 … … 126 138 127 139 if ( 200 !== $status || empty( $data['status'] ) || empty( $data['data']['authorization_url'] ) ) { 128 return array( 'ok' => false, 'error' => $this->error_message( $data, $status ) ); 129 } 130 131 return array( 'ok' => true, 'redirect_url' => $data['data']['authorization_url'] ); 140 return array( 141 'ok' => false, 142 'error' => $this->error_message( $data, $status ), 143 ); 144 } 145 146 return array( 147 'ok' => true, 148 'redirect_url' => $data['data']['authorization_url'], 149 ); 132 150 } 133 151 … … 135 153 $keys = $this->resolve_keys(); 136 154 if ( '' === trim( $keys['key'] ) ) { 137 return array( 'ok' => false, 'error' => 'Paystack secret key is not configured.' ); 155 return array( 156 'ok' => false, 157 'error' => 'Paystack secret key is not configured.', 158 ); 138 159 } 139 160 … … 147 168 148 169 if ( is_wp_error( $response ) ) { 149 return array( 'ok' => false, 'error' => $response->get_error_message() ); 170 return array( 171 'ok' => false, 172 'error' => $response->get_error_message(), 173 ); 150 174 } 151 175 … … 154 178 155 179 if ( 200 !== $status || empty( $data['status'] ) || ! isset( $data['data'] ) ) { 156 return array( 'ok' => false, 'error' => $this->error_message( $data, $status ) ); 180 return array( 181 'ok' => false, 182 'error' => $this->error_message( $data, $status ), 183 ); 157 184 } 158 185 -
mora-delivery-for-woocommerce/tags/0.7.79/includes/class-mora-sentry-reporter.php
r3746963 r3749063 64 64 // elsewhere in this plugin (order meta, checkout fields). 65 65 const PII_KEYS = array( 66 'customer_name', 'customer_email', 'customer_phone', 'phone', 'email', 67 'address', 'address_1', 'address_2', 'billing_email', 'billing_phone', 68 'first_name', 'last_name', 66 'customer_name', 67 'customer_email', 68 'customer_phone', 69 'phone', 70 'email', 71 'address', 72 'address_1', 73 'address_2', 74 'billing_email', 75 'billing_phone', 76 'first_name', 77 'last_name', 69 78 ); 70 79 … … 107 116 $parsed['store_url'], 108 117 array( 109 'headers' => array(118 'headers' => array( 110 119 'Content-Type' => 'application/json', 111 120 'X-Sentry-Auth' => self::auth_header( $parsed['public_key'] ), 112 121 ), 113 'body' => wp_json_encode( $event ),114 'timeout' => self::TIMEOUT_SECONDS,115 'blocking' => false,122 'body' => wp_json_encode( $event ), 123 'timeout' => self::TIMEOUT_SECONDS, 124 'blocking' => false, 116 125 ) 117 126 ); -
mora-delivery-for-woocommerce/tags/0.7.79/includes/class-mora-shipping-method.php
r3746963 r3749063 73 73 $this->enabled = $this->get_option( 'enabled', 'yes' ); 74 74 75 $credentials = Mora_Admin_Settings::site_wide_credentials();75 $credentials = Mora_Admin_Settings::site_wide_credentials(); 76 76 $this->api_base_url = untrailingslashit( $credentials['api_base_url'] ); 77 $this->api_key = $credentials['api_key'];77 $this->api_key = $credentials['api_key']; 78 78 79 79 add_action( 'woocommerce_update_options_shipping_' . $this->id, array( $this, 'process_admin_options' ) ); … … 120 120 $locator_on = Mora_Admin_Settings::is_places_locator_enabled(); 121 121 $location = $locator_on ? Mora_Checkout::get_pinned_location() : array(); 122 $lat = isset( $location['lat'] ) ? $location['lat'] : null;123 $lon = isset( $location['lon'] ) ? $location['lon'] : null;122 $lat = isset( $location['lat'] ) ? $location['lat'] : null; 123 $lon = isset( $location['lon'] ) ? $location['lon'] : null; 124 124 125 125 $has_coordinates = ( null !== $lat && null !== $lon ); … … 129 129 // here either when there is a pin. 130 130 $address = $has_coordinates ? null : $this->customer_address_text( $package, $state_name ); 131 $quote = Mora_Api_Client::get_quote( $this->api_base_url, $this->api_key, $state_name, $lat, $lon, $address );131 $quote = Mora_Api_Client::get_quote( $this->api_base_url, $this->api_key, $state_name, $lat, $lon, $address ); 132 132 133 133 // Whether the customer has already given us an address to work from, -
mora-delivery-for-woocommerce/tags/0.7.79/includes/class-mora-webhook-receiver.php
r3746963 r3749063 79 79 if ( '' === $secret ) { 80 80 self::log_throttled( 'error', 'Mora: webhook received but no webhook secret is configured on this site.', 'no_secret' ); 81 return new WP_REST_Response( array( 'ok' => false, 'error' => 'webhook not configured' ), 501 ); 81 return new WP_REST_Response( 82 array( 83 'ok' => false, 84 'error' => 'webhook not configured', 85 ), 86 501 87 ); 82 88 } 83 89 … … 87 93 // number formatting). Same reasoning as the documented-but-unbuilt 88 94 // Paystack signature check elsewhere in this codebase. 89 $raw_body = $request->get_body();95 $raw_body = $request->get_body(); 90 96 $received_signature = (string) $request->get_header( 'x_mora_webhook_signature' ); 91 97 if ( '' === $received_signature ) { 92 return new WP_REST_Response( array( 'ok' => false, 'error' => 'missing signature' ), 401 ); 98 return new WP_REST_Response( 99 array( 100 'ok' => false, 101 'error' => 'missing signature', 102 ), 103 401 104 ); 93 105 } 94 106 … … 96 108 if ( ! hash_equals( $expected_signature, $received_signature ) ) { 97 109 self::log_throttled( 'warning', 'Mora: webhook signature did not match.', 'bad_signature' ); 98 return new WP_REST_Response( array( 'ok' => false, 'error' => 'invalid signature' ), 401 ); 110 return new WP_REST_Response( 111 array( 112 'ok' => false, 113 'error' => 'invalid signature', 114 ), 115 401 116 ); 99 117 } 100 118 101 119 $payload = json_decode( $raw_body, true ); 102 120 if ( ! is_array( $payload ) || empty( $payload['merchant_order_id'] ) || empty( $payload['status'] ) ) { 103 return new WP_REST_Response( array( 'ok' => false, 'error' => 'malformed payload' ), 400 ); 121 return new WP_REST_Response( 122 array( 123 'ok' => false, 124 'error' => 'malformed payload', 125 ), 126 400 127 ); 104 128 } 105 129 … … 113 137 array( 'source' => 'mora' ) 114 138 ); 115 return new WP_REST_Response( array( 'ok' => false, 'error' => 'order not found' ), 404 ); 139 return new WP_REST_Response( 140 array( 141 'ok' => false, 142 'error' => 'order not found', 143 ), 144 404 145 ); 116 146 } 117 147 … … 130 160 array( 'source' => 'mora' ) 131 161 ); 132 return new WP_REST_Response( array( 'ok' => false, 'error' => 'order_id mismatch' ), 409 ); 162 return new WP_REST_Response( 163 array( 164 'ok' => false, 165 'error' => 'order_id mismatch', 166 ), 167 409 168 ); 133 169 } 134 170 -
mora-delivery-for-woocommerce/tags/0.7.79/mora-delivery-for-woocommerce.php
r3748164 r3749063 3 3 * Plugin Name: Mora Delivery for WooCommerce 4 4 * Description: Live Mora delivery quotes at checkout, backed by mora-api's GET /v1/quote, a goods-on-delivery payment gateway that charges the delivery fee upfront, and pushes placed orders on to Mora. 5 * Version: 0.7.7 85 * Version: 0.7.79 6 6 * Requires at least: 6.0 7 7 * Requires PHP: 7.4 … … 21 21 } 22 22 23 define( 'MORA_WC_VERSION', '0.7.7 8' );23 define( 'MORA_WC_VERSION', '0.7.79' ); 24 24 define( 'MORA_WC_PLUGIN_FILE', __FILE__ ); 25 25 define( 'MORA_WC_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); -
mora-delivery-for-woocommerce/tags/0.7.79/readme.txt
r3748164 r3749063 5 5 Tested up to: 7.1 6 6 Requires PHP: 7.4 7 Stable tag: 0.7.7 87 Stable tag: 0.7.79 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 31 31 This plugin connects to the following services to provide its features. None of them run unless the matching feature is actually configured and in use. 32 32 33 **Mora Logistics API (mora-api)**: Mora's own backend, used to price delivery quotes , process the delivery-fee payment gateway's PSP verification, and push/sync placed orders. When a quote is requested, this plugin sends the destination state, and, where available, the customer's pinned coordinates or typed address, to `https://api.moralogistics.com`. When an order is placed, order, product, and customer contact details are sent there so Mora can fulfil the delivery. This is Mora's own first-party service, governed by the same merchant agreement a Mora Logistics merchant signs; see Mora Logistics' Terms of Service (https://moralogistics.com/terms) and Privacy Policy (https://moralogistics.com/privacy-policy).33 **Mora Logistics API (mora-api)**: Mora's own backend, used to price delivery quotes and to push and sync placed orders. When a quote is requested, this plugin sends the destination state, and, where available, the customer's pinned coordinates or typed address, to the Mora API address set in the plugin's settings (Mora's production service is `https://api.moralogistics.com`). When an order is placed, order, product, and customer contact details are sent there so Mora can fulfil the delivery. This is Mora's own first-party service, governed by the same merchant agreement a Mora Logistics merchant signs; see Mora Logistics' Terms of Service (https://moralogistics.com/terms) and Privacy Policy (https://moralogistics.com/privacy-policy). 34 34 35 35 **Paystack**: used only by this plugin's own payment gateway (optional, merchant-enabled), through the merchant's own Paystack account. The order's delivery-fee amount, currency, and a payment reference are sent to Paystack to process the charge (sent to `https://api.paystack.co` when the customer places an order with this gateway, and again to verify the payment); no card details pass through this plugin or site, Paystack's own hosted page handles that directly. See Paystack's Terms of Use (https://paystack.com/terms) and Merchant Privacy Policy (https://paystack.com/privacy/merchant). … … 71 71 72 72 == Changelog == 73 74 = 0.7.79 = 75 * Fixed: hub-pickup orders from the block checkout (WooCommerce Local Pickup) were never sent to Mora. They are now, like classic-checkout pickup orders. 76 * Docs: External Services no longer says payments are verified through Mora's API (they are verified directly with Paystack), and names the configured API address. 73 77 74 78 = 0.7.78 = -
mora-delivery-for-woocommerce/tags/0.7.79/uninstall.php
r3746963 r3749063 21 21 22 22 // This plugin's own site-wide options. 23 $ options = array(23 $mora_wc_options = array( 24 24 'mora_wc_places_api_key', 25 25 'mora_wc_reorder_core_fields', … … 38 38 'woocommerce_mora_settings', 39 39 ); 40 foreach ( $ options as $option ) {41 delete_option( $ option );40 foreach ( $mora_wc_options as $mora_wc_option ) { 41 delete_option( $mora_wc_option ); 42 42 } 43 43 -
mora-delivery-for-woocommerce/trunk/includes/class-mora-admin-settings.php
r3746963 r3749063 400 400 } 401 401 self::$resolving_site_wide_credentials = true; 402 $scanned = self::scan_zone_instances_for_credentials();402 $scanned = self::scan_zone_instances_for_credentials(); 403 403 self::$resolving_site_wide_credentials = false; 404 404 if ( '' === $scanned['api_key'] ) { -
mora-delivery-for-woocommerce/trunk/includes/class-mora-blocks-payment-method.php
r3746963 r3749063 69 69 public function initialize() { 70 70 $this->settings = get_option( 'woocommerce_' . $this->name . '_settings', array() ); 71 $gateways = WC()->payment_gateways() ? WC()->payment_gateways()->payment_gateways() : array();72 $this->gateway = isset( $gateways[ $this->name ] ) ? $gateways[ $this->name ] : null;71 $gateways = WC()->payment_gateways() ? WC()->payment_gateways()->payment_gateways() : array(); 72 $this->gateway = isset( $gateways[ $this->name ] ) ? $gateways[ $this->name ] : null; 73 73 } 74 74 … … 90 90 public function get_payment_method_data() { 91 91 return array( 92 'title' => $this->gateway ? $this->gateway->title : '',93 'description' => $this->gateway ? $this->gateway->description : '',92 'title' => $this->gateway ? $this->gateway->title : '', 93 'description' => $this->gateway ? $this->gateway->description : '', 94 94 // Static per page load (an admin setting, not cart state), safe 95 95 // for canMakePayment() to branch on directly; the live destination -
mora-delivery-for-woocommerce/trunk/includes/class-mora-checkout-experiment.php
r3746963 r3749063 176 176 } 177 177 178 $due_today = (float) WC()->cart->get_shipping_total() + (float) WC()->cart->get_shipping_tax();178 $due_today = (float) WC()->cart->get_shipping_total() + (float) WC()->cart->get_shipping_tax(); 179 179 $due_on_delivery = (float) WC()->cart->get_total( 'edit' ) - $due_today; 180 180 if ( $due_on_delivery < 0 ) { -
mora-delivery-for-woocommerce/trunk/includes/class-mora-checkout.php
r3746963 r3749063 35 35 class Mora_Checkout { 36 36 37 const NONCE_ACTION = 'mora_wc_set_coordinates';37 const NONCE_ACTION = 'mora_wc_set_coordinates'; 38 38 const COORDS_TRANSIENT_PREFIX = 'mora_coords_'; 39 39 40 40 // See get_places_api_key()'s own docblock for why this is cached at all, 41 41 // and why success and failure carry different TTLs. 42 const PLACES_KEY_TRANSIENT = 'mora_wc_places_key_cache';43 const PLACES_KEY_CACHE_TTL = 12 * HOUR_IN_SECONDS;42 const PLACES_KEY_TRANSIENT = 'mora_wc_places_key_cache'; 43 const PLACES_KEY_CACHE_TTL = 12 * HOUR_IN_SECONDS; 44 44 const PLACES_KEY_FAILURE_CACHE_TTL = 5 * MINUTE_IN_SECONDS; 45 45 … … 127 127 */ 128 128 public static function locator_script_data() { 129 $ng_states = WC()->countries->get_states( 'NG' ); 129 130 return array( 130 131 'placesApiKey' => self::get_places_api_key(), … … 136 137 // code => name list classic's own <select> options already reflect, 137 138 // sourced from the same place WooCommerce itself does. 138 'ngStates' => WC()->countries->get_states( 'NG' ) ?: array(),139 'ngStates' => $ng_states ? $ng_states : array(), 139 140 'i18n' => array( 140 141 // The widget is a locator (sets coordinates, city, state, LGA for … … 145 146 // phase adds a real, required field literally called "Landmark", so the 146 147 // widget can no longer describe itself the same way. 147 'locatorLabel' => __( 'Find your delivery address', 'mora-delivery-for-woocommerce' ),148 'locatorLabel' => __( 'Find your delivery address', 'mora-delivery-for-woocommerce' ), 148 149 // Measured on 20 real Lagos addresses: 18/20 resolved to door level when 149 150 // the query contained a house number. That instruction is the single most … … 153 154 // (New) release is not confirmed against Google's primary docs from this 154 155 // environment). 155 'locatorHint' => __( 'Start with your house number, like 12 Allen Avenue. We use this to work out your delivery fee.', 'mora-delivery-for-woocommerce' ),156 'locatorHint' => __( 'Start with your house number, like 12 Allen Avenue. We use this to work out your delivery fee.', 'mora-delivery-for-woocommerce' ), 156 157 // A generic example address, deliberately not the Mora hub's own street 157 158 // address (16 Adebiyi Street): that's a real, specific location tied to the … … 295 296 // two days for a guest. The pin has no reason to outlive the session it belongs to. 296 297 $ttl = is_user_logged_in() ? WEEK_IN_SECONDS : 2 * DAY_IN_SECONDS; 297 set_transient( $key, array( 298 'lat' => $lat, 299 'lon' => $lon, 300 'lga' => $lga, 301 'pinned' => $pinned, 302 ), $ttl ); 298 set_transient( 299 $key, 300 array( 301 'lat' => $lat, 302 'lon' => $lon, 303 'lga' => $lga, 304 'pinned' => $pinned, 305 ), 306 $ttl 307 ); 303 308 } 304 309 -
mora-delivery-for-woocommerce/trunk/includes/class-mora-fields.php
r3746963 r3749063 99 99 'classic_type' => 'text', 100 100 'required_for_mora' => false, // always optional, Mora rate or not; see the class 101 // docblock for why this was changed.101 // docblock for why this was changed. 102 102 'label' => __( 'Landmark', 'mora-delivery-for-woocommerce' ), 103 103 'placeholder' => __( 'A shop, school or building we can find', 'mora-delivery-for-woocommerce' ), … … 189 189 190 190 /** 191 * WooCommerce's built-in pickup method ids: `local_pickup` on classic 192 * checkout, `pickup_location` on Blocks checkout (WooCommerce's newer 193 * Local Pickup). Both carry Mora's hub-pickup fee. 194 */ 195 const PICKUP_METHOD_IDS = array( 'local_pickup', 'pickup_location' ); 196 197 /** 198 * Whether a shipping method id is one of WooCommerce's pickup methods. 199 * 200 * @param string $method_id Shipping method id. 201 * @return bool 202 */ 203 public static function is_pickup_method( $method_id ) { 204 return in_array( $method_id, self::PICKUP_METHOD_IDS, true ); 205 } 206 207 /** 191 208 * has_mora_shipping_line()'s sibling for hub pickup: does this order use 192 * WooCommerce's own built-in Local Pickup method (method id193 * `local_pickup`,not a Mora-owned one). Mora_Order_Push::can_push()209 * one of WooCommerce's own built-in pickup methods (PICKUP_METHOD_IDS, 210 * not a Mora-owned one). Mora_Order_Push::can_push() 194 211 * reads this to also push a pickup order, alongside a Mora delivery 195 212 * one: the customer collects at the Mora hub instead of a rider … … 199 216 public static function has_local_pickup_line( $order ) { 200 217 foreach ( $order->get_items( 'shipping' ) as $item ) { 201 if ( 'local_pickup' === $item->get_method_id() ) {218 if ( self::is_pickup_method( $item->get_method_id() ) ) { 202 219 return true; 203 220 } … … 252 269 * Landmark is never enforced here, on any rate; see the class docblock for why. 253 270 */ 254 public static function validate_blocks_phone_and_landmark( $context, $result ) { 271 public static function validate_blocks_phone_and_landmark( $context, $result ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- $result is part of the hook's signature. 255 272 $chosen_methods = WC()->session ? WC()->session->get( 'chosen_shipping_methods', array() ) : array(); 256 273 if ( ! self::is_mora_rate_chosen( $chosen_methods ) ) { -
mora-delivery-for-woocommerce/trunk/includes/class-mora-leads-admin.php
r3746963 r3749063 17 17 18 18 const EXPORT_ACTION = 'mora_wc_leads_export'; 19 20 // Shown in a list column with no value. 21 const EMPTY_CELL = '-'; 19 22 20 23 /** … … 80 83 } 81 84 82 public static function define_columns( $columns ) { 85 public static function define_columns( $columns ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found -- filter signature; the default columns are replaced, not extended. 83 86 return array( 84 87 'cb' => '<input type="checkbox" />', … … 95 98 96 99 /** 100 * A column value, or the empty-cell placeholder when it is empty. 101 * 102 * @param mixed $value Stored meta value. 103 * @return mixed 104 */ 105 private static function cell( $value ) { 106 return $value ? $value : self::EMPTY_CELL; 107 } 108 109 /** 97 110 * Every column but Cart is plain text, always esc_html()'d. The Cart 98 111 * column is the one exception: wc_price() returns real markup (a … … 107 120 case 'mora_lead_name': 108 121 $name = trim( get_post_meta( $post_id, Mora_Leads::META_FIRST_NAME, true ) . ' ' . get_post_meta( $post_id, Mora_Leads::META_LAST_NAME, true ) ); 109 echo esc_html( $name ?: '—');122 echo esc_html( self::cell( $name ) ); 110 123 break; 111 124 case 'mora_lead_phone': 112 echo esc_html( get_post_meta( $post_id, Mora_Leads::META_PHONE, true ) ?: '—');125 echo esc_html( self::cell( get_post_meta( $post_id, Mora_Leads::META_PHONE, true ) ) ); 113 126 break; 114 127 case 'mora_lead_email': 115 echo esc_html( get_post_meta( $post_id, Mora_Leads::META_EMAIL, true ) ?: '—');128 echo esc_html( self::cell( get_post_meta( $post_id, Mora_Leads::META_EMAIL, true ) ) ); 116 129 break; 117 130 case 'mora_lead_city': 118 echo esc_html( get_post_meta( $post_id, Mora_Leads::META_CITY, true ) ?: '—');131 echo esc_html( self::cell( get_post_meta( $post_id, Mora_Leads::META_CITY, true ) ) ); 119 132 break; 120 133 case 'mora_lead_cart': … … 124 137 break; 125 138 case 'mora_lead_products': 126 echo esc_html( get_post_meta( $post_id, Mora_Leads::META_PRODUCTS, true ) ?: '—');139 echo esc_html( self::cell( get_post_meta( $post_id, Mora_Leads::META_PRODUCTS, true ) ) ); 127 140 break; 128 141 case 'mora_lead_seen': 129 142 $last_seen = (int) get_post_meta( $post_id, Mora_Leads::META_LAST_SEEN, true ); 130 echo esc_html( $last_seen ? date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $last_seen ) : '—');143 echo esc_html( $last_seen ? date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $last_seen ) : self::EMPTY_CELL ); 131 144 break; 132 145 case 'mora_lead_status': 133 $status = get_post_meta( $post_id, Mora_Leads::META_STATUS, true );146 $status = get_post_meta( $post_id, Mora_Leads::META_STATUS, true ); 134 147 $order_id = (int) get_post_meta( $post_id, Mora_Leads::META_ORDER_ID, true ); 135 148 if ( Mora_Leads::STATUS_CONVERTED === $status && $order_id && function_exists( 'wc_get_order' ) && wc_get_order( $order_id ) ) { … … 148 161 * (a bare WooCommerce → Leads click, never a query string). 149 162 */ 150 public static function status_filter_views( $views ) { 163 public static function status_filter_views( $views ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found -- filter signature; the default views are replaced, not extended. 151 164 $current = isset( $_GET['lead_status'] ) ? sanitize_key( wp_unslash( $_GET['lead_status'] ) ) : 'open'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended 152 165 … … 159 172 $new_views = array(); 160 173 foreach ( $tabs as $key => $label ) { 161 $count = self::count_leads_for_status( $key );162 $url = add_query_arg(174 $count = self::count_leads_for_status( $key ); 175 $url = add_query_arg( 163 176 array( 164 177 'post_type' => Mora_Leads::POST_TYPE, … … 167 180 admin_url( 'edit.php' ) 168 181 ); 169 $class = $current === $key ? ' class="current"' : '';182 $class = $current === $key ? ' class="current"' : ''; 170 183 $new_views[ $key ] = sprintf( 171 184 '<a href="%s"%s>%s <span class="count">(%d)</span></a>', … … 347 360 // computed once, at capture time, and is what conversion matching 348 361 // also reads, so export and matching can never quietly disagree. 349 $phone = get_post_meta( $lead_id, Mora_Leads::META_PHONE_NORMALIZED, true );362 $phone = get_post_meta( $lead_id, Mora_Leads::META_PHONE_NORMALIZED, true ); 350 363 fputcsv( 351 364 $out, -
mora-delivery-for-woocommerce/trunk/includes/class-mora-leads.php
r3746963 r3749063 45 45 const POST_TYPE = 'mora_lead'; 46 46 47 const META_SESSION_ID = '_mora_lead_session_id';48 const META_FIRST_NAME = '_mora_lead_first_name';49 const META_LAST_NAME = '_mora_lead_last_name';47 const META_SESSION_ID = '_mora_lead_session_id'; 48 const META_FIRST_NAME = '_mora_lead_first_name'; 49 const META_LAST_NAME = '_mora_lead_last_name'; 50 50 // As the customer typed it (local format, e.g. 08012345678): what the 51 51 // merchant searches by and sees in the admin list. META_PHONE_NORMALIZED … … 55 55 const META_PHONE = '_mora_lead_phone'; 56 56 const META_PHONE_NORMALIZED = '_mora_lead_phone_normalized'; 57 const META_EMAIL = '_mora_lead_email';58 const META_CITY = '_mora_lead_city';59 const META_STATE = '_mora_lead_state';60 const META_CART_COUNT = '_mora_lead_cart_count';61 const META_CART_TOTAL = '_mora_lead_cart_total';57 const META_EMAIL = '_mora_lead_email'; 58 const META_CITY = '_mora_lead_city'; 59 const META_STATE = '_mora_lead_state'; 60 const META_CART_COUNT = '_mora_lead_cart_count'; 61 const META_CART_TOTAL = '_mora_lead_cart_total'; 62 62 // "Name (SKU), Name (SKU)..." for the whole cart at capture time, not 63 63 // one line item: a lead can span multiple products, the same reason 64 64 // cart_count/cart_total summarize the whole cart rather than one item. 65 const META_PRODUCTS = '_mora_lead_products';66 const META_FIRST_SEEN = '_mora_lead_first_seen';67 const META_LAST_SEEN = '_mora_lead_last_seen';68 const META_STATUS = '_mora_lead_status';69 const META_ORDER_ID = '_mora_lead_order_id';65 const META_PRODUCTS = '_mora_lead_products'; 66 const META_FIRST_SEEN = '_mora_lead_first_seen'; 67 const META_LAST_SEEN = '_mora_lead_last_seen'; 68 const META_STATUS = '_mora_lead_status'; 69 const META_ORDER_ID = '_mora_lead_order_id'; 70 70 71 71 const STATUS_OPEN = 'open'; 72 72 const STATUS_CONVERTED = 'converted'; 73 73 74 const THROTTLE_SECONDS = 60;74 const THROTTLE_SECONDS = 60; 75 75 const RETENTION_DAYS_DEFAULT = 90; 76 const PURGE_CRON_HOOK = 'mora_wc_purge_leads';76 const PURGE_CRON_HOOK = 'mora_wc_purge_leads'; 77 77 // A daily cron deleting an unbounded set in one request will eventually 78 78 // time out; a capped, oldest-first batch clears a real backlog over a 79 79 // few days instead of failing outright on the day it's biggest. 80 const PURGE_BATCH_SIZE = 200;80 const PURGE_BATCH_SIZE = 200; 81 81 82 82 // Only these three WooCommerce order statuses mean "this order is real … … 177 177 * pollutes the leads list with the merchant's own sessions. 178 178 */ 179 public static function capture_from_store_api( $customer, $request ) { 179 public static function capture_from_store_api( $customer, $request ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- $request is part of the hook's signature. 180 180 if ( ! $customer instanceof WC_Customer ) { 181 181 return; … … 243 243 // Shipping-preferred-falls-back-to-billing, the same idiom 244 244 // Mora_Order_Push::build_payload() already uses for city/state. 245 'city' => $customer->get_shipping_city() ? : $customer->get_billing_city(),246 'state' => $customer->get_shipping_state() ? : $customer->get_billing_state(),245 'city' => $customer->get_shipping_city() ? $customer->get_shipping_city() : $customer->get_billing_city(), 246 'state' => $customer->get_shipping_state() ? $customer->get_shipping_state() : $customer->get_billing_state(), 247 247 'cart_count' => WC()->cart ? WC()->cart->get_cart_contents_count() : 0, 248 248 // 'edit' context: the raw numeric total, not a pre-formatted … … 272 272 continue; 273 273 } 274 $sku = $product->get_sku();274 $sku = $product->get_sku(); 275 275 $names[] = $sku ? $product->get_name() . ' (' . $sku . ')' : $product->get_name(); 276 276 } … … 422 422 } 423 423 424 public static function reopen_lead_for_order( $order_id, $order ) { 424 public static function reopen_lead_for_order( $order_id, $order ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- $order is part of the hook's signature. 425 425 $lead_ids = get_posts( 426 426 array( … … 582 582 * eraser keeps calling this until a page genuinely comes back empty. 583 583 */ 584 public static function privacy_eraser( $email_address, $page = 1 ) { 584 public static function privacy_eraser( $email_address, $page = 1 ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- eraser callback signature; $page deliberately unused, see above. 585 585 $per_page = 50; 586 586 $lead_ids = get_posts( -
mora-delivery-for-woocommerce/trunk/includes/class-mora-locator-block.php
r3746963 r3749063 56 56 // Naming mirrors Mora_Payment_Gateway::COD_DISABLE_ACTION, the existing 57 57 // one-click-admin-notice pattern this feature follows. 58 const ADD_ACTION = 'mora_wc_add_locator_block';59 const RESULT_TRANSIENT_PREFIX = 'mora_wc_locator_result_';58 const ADD_ACTION = 'mora_wc_add_locator_block'; 59 const RESULT_TRANSIENT_PREFIX = 'mora_wc_locator_result_'; 60 60 61 61 public static function init() { -
mora-delivery-for-woocommerce/trunk/includes/class-mora-order-push.php
r3748164 r3749063 120 120 return true; // hub pickup: no destination to validate 121 121 } 122 $country = $order->get_shipping_country() ?: $order->get_billing_country();122 $country = self::first_filled( $order->get_shipping_country(), $order->get_billing_country() ); 123 123 if ( 'NG' !== $country ) { 124 124 $reason = sprintf( … … 763 763 // A Mora delivery line always wins if present (matches can_push()'s own 764 764 // priority): only treat this as a hub-pickup order when there is no 765 // Mora line at all. WooCommerce's built-in Local Pickup method766 // ( `local_pickup`) is whatcarries the pickup fee in that case; the765 // Mora line at all. WooCommerce's built-in pickup method 766 // (Mora_Fields::PICKUP_METHOD_IDS) carries the pickup fee in that case; the 767 767 // customer collects at the Mora hub, so there is no delivery address 768 768 // to send at all. 769 $is_pickup = empty( $mora_lines ); 770 $expected_method = $is_pickup ? 'local_pickup' : 'mora'; 771 $fee_lines = $is_pickup 769 $is_pickup = empty( $mora_lines ); 770 $fee_lines = $is_pickup 772 771 ? array_values( 773 772 array_filter( 774 773 $shipping_lines, 775 774 function ( $item ) { 776 return 'local_pickup' === $item->get_method_id();775 return Mora_Fields::is_pickup_method( $item->get_method_id() ); 777 776 } 778 777 ) 779 778 ) 780 779 : $mora_lines; 781 $other_lines = array_values(780 $other_lines = array_values( 782 781 array_filter( 783 782 $shipping_lines, 784 function ( $item ) use ( $expected_method ) { 785 return $expected_method !== $item->get_method_id(); 783 function ( $item ) use ( $is_pickup ) { 784 $id = $item->get_method_id(); 785 return $is_pickup ? ! Mora_Fields::is_pickup_method( $id ) : 'mora' !== $id; 786 786 } 787 787 ) … … 801 801 } 802 802 803 $delivery_fee = 0.0;804 $shipping_tax = 0.0;803 $delivery_fee = 0.0; 804 $shipping_tax = 0.0; 805 805 foreach ( $fee_lines as $line ) { 806 806 $delivery_fee += (float) $line->get_total(); … … 832 832 // Mora delivery rate (Mora_Payment_Gateway::every_package_rate_or_ 833 833 // false()), never on Local Pickup. 834 $cod_amount = $is_mora_cod ? ( $order_total - $delivery_fee ) : 0.0;835 $state_code = $is_pickup ? null : ( $order->get_shipping_state() ?:$order->get_billing_state() );834 $cod_amount = $is_mora_cod ? ( $order_total - $delivery_fee ) : 0.0; 835 $state_code = $is_pickup ? null : self::first_filled( $order->get_shipping_state(), $order->get_billing_state() ); 836 836 837 837 return array( 838 838 'merchant_order_id' => (string) $order->get_id(), 839 839 'site_url' => home_url( '/' ), 840 'quote_id' => $order->get_meta( '_mora_quote_id' ) ?: null,840 'quote_id' => self::filled_or_null( $order->get_meta( '_mora_quote_id' ) ), 841 841 // Never null: OrderIn.placed_at is required and orders.placed_at is 842 842 // NOT NULL, so a null here is a guaranteed 422, and a 422 is never … … 858 858 'fulfillment_type' => $is_pickup ? 'pickup' : 'delivery', 859 859 // Not sent: delivery_fee_collected, state_name. Both derived in mora-api. 860 'customer_name' => trim( $order->get_shipping_first_name() . ' ' . $order->get_shipping_last_name() ) 861 ?: trim( $order->get_billing_first_name() . ' ' . $order->get_billing_last_name() ), 860 'customer_name' => self::first_filled( 861 trim( $order->get_shipping_first_name() . ' ' . $order->get_shipping_last_name() ), 862 trim( $order->get_billing_first_name() . ' ' . $order->get_billing_last_name() ) 863 ), 862 864 'customer_phone' => $order->get_billing_phone(), 863 'customer_phone_alt' => $order->get_meta( '_mora_phone_alt' ) ?: null,865 'customer_phone_alt' => self::filled_or_null( $order->get_meta( '_mora_phone_alt' ) ), 864 866 'customer_email' => $order->get_billing_email(), 865 867 // Every address/coordinate field below is null for a pickup order: … … 867 869 // delivery address to send at all (mora-api's own OrderIn requires 868 870 // these only when fulfillment_type is "delivery"). 869 'address_1' => $is_pickup ? null : ( $order->get_shipping_address_1() ?:$order->get_billing_address_1() ),870 'address_2' => $is_pickup ? null : ( $order->get_shipping_address_2() ?:$order->get_billing_address_2() ),871 'landmark' => $is_pickup ? null : ( $order->get_meta( '_mora_landmark' ) ?: null),872 'city' => $is_pickup ? null : ( $order->get_shipping_city() ?:$order->get_billing_city() ),871 'address_1' => $is_pickup ? null : self::first_filled( $order->get_shipping_address_1(), $order->get_billing_address_1() ), 872 'address_2' => $is_pickup ? null : self::first_filled( $order->get_shipping_address_2(), $order->get_billing_address_2() ), 873 'landmark' => $is_pickup ? null : self::filled_or_null( $order->get_meta( '_mora_landmark' ) ), 874 'city' => $is_pickup ? null : self::first_filled( $order->get_shipping_city(), $order->get_billing_city() ), 873 875 'state_code' => $state_code, 874 'lga' => $is_pickup ? null : ( $order->get_meta( '_mora_lga' ) ?: null),875 'pinned_address' => $is_pickup ? null : ( $order->get_meta( '_mora_pinned_address' ) ?: null),876 'lga' => $is_pickup ? null : self::filled_or_null( $order->get_meta( '_mora_lga' ) ), 877 'pinned_address' => $is_pickup ? null : self::filled_or_null( $order->get_meta( '_mora_pinned_address' ) ), 876 878 'lat' => ( ! $is_pickup && '' !== $order->get_meta( '_mora_lat' ) ) ? (float) $order->get_meta( '_mora_lat' ) : null, 877 879 'lon' => ( ! $is_pickup && '' !== $order->get_meta( '_mora_lon' ) ) ? (float) $order->get_meta( '_mora_lon' ) : null, … … 887 889 array_values( $order->get_items() ) 888 890 ), 889 'notes' => $order->get_customer_note() ?: null, 890 ); 891 'notes' => self::filled_or_null( $order->get_customer_note() ), 892 ); 893 } 894 895 /** 896 * The preferred value when it is truthy, otherwise the fallback. Same 897 * result as PHP's short ternary ( $preferred ?: $fallback ), which the 898 * coding standard disallows; both arguments are side-effect-free getters. 899 * 900 * @param mixed $preferred Value to use when truthy (e.g. a shipping field). 901 * @param mixed $fallback Value to use otherwise (e.g. the billing field). 902 * @return mixed 903 */ 904 private static function first_filled( $preferred, $fallback ) { 905 return $preferred ? $preferred : $fallback; 906 } 907 908 /** 909 * The value when it is truthy, otherwise null (an empty meta string is 910 * sent to mora-api as null, never as ""). 911 * 912 * @param mixed $value Value to check. 913 * @return mixed|null 914 */ 915 private static function filled_or_null( $value ) { 916 return $value ? $value : null; 891 917 } 892 918 -
mora-delivery-for-woocommerce/trunk/includes/class-mora-payment-gateway.php
r3746963 r3749063 94 94 public function __construct() { 95 95 $this->id = 'mora_pay_on_delivery'; 96 $this->icon = '';97 $this->has_fields = false;98 $this->method_title = __( 'Mora Goods on Delivery', 'mora-delivery-for-woocommerce' );99 $this->method_description = __( 'Charges only the order\'s delivery/shipping fee at checkout through the merchant\'s own Paystack account; any goods balance is collected in cash on delivery. Available for any order regardless of shipping method, as long as this gateway is enabled and a delivery fee is present; optionally restricted to Lagos-only destinations below.', 'mora-delivery-for-woocommerce' );96 $this->icon = ''; 97 $this->has_fields = false; 98 $this->method_title = __( 'Mora Goods on Delivery', 'mora-delivery-for-woocommerce' ); 99 $this->method_description = __( 'Charges only the order\'s delivery/shipping fee at checkout through the merchant\'s own Paystack account; any goods balance is collected in cash on delivery. Available for any order regardless of shipping method, as long as this gateway is enabled and a delivery fee is present; optionally restricted to Lagos-only destinations below.', 'mora-delivery-for-woocommerce' ); 100 100 // Deliberately no 'refunds': a Paystack refund here is a manual action in 101 101 // the merchant's own PSP dashboard (README), not something this plugin … … 108 108 $this->title = $this->get_option( 'title' ); 109 109 $this->description = $this->get_option( 'description' ); 110 $this->enabled = $this->get_option( 'enabled', 'no' );110 $this->enabled = $this->get_option( 'enabled', 'no' ); 111 111 $this->balance_due_status = $this->get_option( 'balance_due_status', 'on-hold' ); 112 112 … … 125 125 public function init_form_fields() { 126 126 $this->form_fields = array( 127 'enabled' => array(127 'enabled' => array( 128 128 'title' => __( 'Enable', 'mora-delivery-for-woocommerce' ), 129 129 'type' => 'checkbox', … … 131 131 'default' => 'no', 132 132 ), 133 'title' => array(133 'title' => array( 134 134 'title' => __( 'Title', 'mora-delivery-for-woocommerce' ), 135 135 'type' => 'text', … … 138 138 'desc_tip' => true, 139 139 ), 140 'description' => array(140 'description' => array( 141 141 'title' => __( 'Description', 'mora-delivery-for-woocommerce' ), 142 142 'type' => 'textarea', 143 143 'default' => __( 'Pay the delivery fee now by card, bank transfer, or USSD. Any goods balance is paid to the rider on delivery.', 'mora-delivery-for-woocommerce' ), 144 144 ), 145 'restrict_to_lagos' => array(145 'restrict_to_lagos' => array( 146 146 'title' => __( 'Restrict to Lagos', 'mora-delivery-for-woocommerce' ), 147 147 'type' => 'checkbox', … … 150 150 'description' => __( 'Off by default: this gateway otherwise applies to any order regardless of shipping method or destination. Turn this on if the store delivers with Mora nationwide but only wants this fee-now/collect-on-delivery flow offered within Lagos; an interstate order with Mora chosen still delivers, it just pays in full upfront through another payment method.', 'mora-delivery-for-woocommerce' ), 151 151 ), 152 'balance_due_status' => array(152 'balance_due_status' => array( 153 153 'title' => __( 'Order status while a goods balance is due', 'mora-delivery-for-woocommerce' ), 154 154 'type' => 'select', … … 160 160 'description' => __( 'Used only while a goods balance remains after the delivery fee is collected; an order with nothing left owing always goes straight to Processing regardless of this setting. Choose Processing if this store\'s fulfilment/warehouse tooling triggers off that status rather than On hold.', 'mora-delivery-for-woocommerce' ), 161 161 ), 162 'hold_minutes' => array(162 'hold_minutes' => array( 163 163 'title' => __( 'Hold unpaid bank transfer and USSD orders for', 'mora-delivery-for-woocommerce' ), 164 164 'type' => 'number', … … 174 174 // no longer polled. Previously enforced nowhere, client or 175 175 // server side. 176 'custom_attributes' => array( 'min' => '1', 'max' => (string) self::MAX_POLL_MINUTES ), 176 'custom_attributes' => array( 177 'min' => '1', 178 'max' => (string) self::MAX_POLL_MINUTES, 179 ), 177 180 'description' => __( 'Minutes to hold an order open, and prevent WooCommerce\'s own unpaid-order sweeper from cancelling it, while waiting for a bank transfer or USSD payment to verify. After this, Mora marks the order failed itself.', 'mora-delivery-for-woocommerce' ), 178 181 'default' => '180', 179 182 'desc_tip' => true, 180 183 ), 181 'psp_driver' => array(184 'psp_driver' => array( 182 185 'title' => __( 'Payment provider', 'mora-delivery-for-woocommerce' ), 183 186 'type' => 'select', … … 187 190 'default' => 'paystack', 188 191 ), 189 'paystack_override_heading' => array(192 'paystack_override_heading' => array( 190 193 'title' => __( 'Paystack keys (optional)', 'mora-delivery-for-woocommerce' ), 191 194 'type' => 'title', 192 195 'description' => __( 'Leave these blank to use the keys already configured under WooCommerce > Settings > Payments > Paystack. Only fill these in if this store does not have that plugin installed.', 'mora-delivery-for-woocommerce' ), 193 196 ), 194 'paystack_override_testmode' => array(197 'paystack_override_testmode' => array( 195 198 'title' => __( 'Test mode', 'mora-delivery-for-woocommerce' ), 196 199 'type' => 'checkbox', … … 198 201 'default' => 'no', 199 202 ), 200 'paystack_override_test_secret_key' => array(203 'paystack_override_test_secret_key' => array( 201 204 'title' => __( 'Test secret key', 'mora-delivery-for-woocommerce' ), 202 205 'type' => 'password', 203 206 'default' => '', 204 207 ), 205 'paystack_override_live_secret_key' => array(208 'paystack_override_live_secret_key' => array( 206 209 'title' => __( 'Live secret key', 'mora-delivery-for-woocommerce' ), 207 210 'type' => 'password', … … 323 326 return false; 324 327 } 325 $state = WC()->customer->get_shipping_state() ?: WC()->customer->get_billing_state(); 328 $state = WC()->customer->get_shipping_state(); 329 if ( ! $state ) { 330 $state = WC()->customer->get_billing_state(); 331 } 326 332 return self::LAGOS_STATE_CODE === $state; 327 333 } … … 399 405 sprintf( 400 406 'Mora: %s initialize() failed for order #%d: %s', 401 $driver->id(), $order->get_id(), isset( $result['error'] ) ? $result['error'] : 'unknown error' 407 $driver->id(), 408 $order->get_id(), 409 isset( $result['error'] ) ? $result['error'] : 'unknown error' 402 410 ), 403 411 array( 'source' => 'mora' ) … … 418 426 $order->save(); 419 427 420 $order->add_order_note( sprintf( 421 'Mora: delivery-fee payment of %s started via %s (reference %s)%s.', 422 wc_price( $amount ), $driver->label(), $reference, 423 $driver->is_testmode() ? ' [TEST MODE]' : '' 424 ) ); 428 $order->add_order_note( 429 sprintf( 430 'Mora: delivery-fee payment of %s started via %s (reference %s)%s.', 431 wc_price( $amount ), 432 $driver->label(), 433 $reference, 434 $driver->is_testmode() ? ' [TEST MODE]' : '' 435 ) 436 ); 425 437 426 438 $this->schedule_poll( $order->get_id(), $reference, 0, 0 ); … … 589 601 sprintf( 590 602 'Mora: gave up polling reference %s for order #%d after %d minutes, still unpaid.', 591 $reference, $order_id, $elapsed_minutes 603 $reference, 604 $order_id, 605 $elapsed_minutes 592 606 ), 593 607 array( 'source' => 'mora' ) … … 649 663 sprintf( 650 664 'Mora: verified payment amount/currency mismatch for order #%d, reference %s: expected %s %.2f, got %s %.2f. Not completing.', 651 $order->get_id(), $reference, $expected_currency, $expected_amount, 652 isset( $result['currency'] ) ? $result['currency'] : '?', $result['amount_major'] 665 $order->get_id(), 666 $reference, 667 $expected_currency, 668 $expected_amount, 669 isset( $result['currency'] ) ? $result['currency'] : '?', 670 $result['amount_major'] 653 671 ), 654 672 array( 'source' => 'mora' ) … … 664 682 $order->update_meta_data( '_mora_psp_verified_at', current_time( 'mysql', true ) ); 665 683 $order->save(); 666 $order->add_order_note( sprintf( 667 'Mora: payment of %s verified against reference %s, but this order is already %s. The customer has paid and needs a refund or a new order. Not resurrected automatically.', 668 wc_price( $result['amount_major'] ), $reference, $order->get_status() 669 ) ); 684 $order->add_order_note( 685 sprintf( 686 'Mora: payment of %s verified against reference %s, but this order is already %s. The customer has paid and needs a refund or a new order. Not resurrected automatically.', 687 wc_price( $result['amount_major'] ), 688 $reference, 689 $order->get_status() 690 ) 691 ); 670 692 wc_get_logger()->error( 671 693 sprintf( 672 694 'Mora: verified payment against a cancelled/refunded/failed order #%d, reference %s.', 673 $order->get_id(), $reference 695 $order->get_id(), 696 $reference 674 697 ), 675 698 array( 'source' => 'mora' ) … … 701 724 // participates in, so noting "fee collected" ahead of a status change 702 725 // that might still fail would describe something that didn't happen. 703 $order->add_order_note( sprintf( 704 'Mora: delivery-fee payment verified via %s (reference %s). Collected %s; %s owed on delivery.', 705 $driver->label(), $reference, wc_price( $result['amount_major'] ), 706 $goods_balance > 0 ? wc_price( $goods_balance ) : __( 'nothing', 'mora-delivery-for-woocommerce' ) 707 ) ); 726 $order->add_order_note( 727 sprintf( 728 'Mora: delivery-fee payment verified via %s (reference %s). Collected %s; %s owed on delivery.', 729 $driver->label(), 730 $reference, 731 wc_price( $result['amount_major'] ), 732 $goods_balance > 0 ? wc_price( $goods_balance ) : __( 'nothing', 'mora-delivery-for-woocommerce' ) 733 ) 734 ); 708 735 } 709 736 … … 733 760 return; 734 761 } 735 echo '<p>' . esc_html( sprintf( 762 echo '<p>' . esc_html( 763 sprintf( 736 764 /* translators: %s: goods balance still owed, formatted as a price. */ 737 __( 'Your delivery fee has been paid. %s is still owed for the goods, payable to the rider on delivery.', 'mora-delivery-for-woocommerce' ), 738 wp_strip_all_tags( wc_price( (float) $balance ) ) 739 ) ) . '</p>'; 765 __( 'Your delivery fee has been paid. %s is still owed for the goods, payable to the rider on delivery.', 'mora-delivery-for-woocommerce' ), 766 wp_strip_all_tags( wc_price( (float) $balance ) ) 767 ) 768 ) . '</p>'; 740 769 } 741 770 … … 759 788 if ( isset( $_GET[ self::COD_DISABLED_FLAG ] ) && '1' === sanitize_text_field( wp_unslash( $_GET[ self::COD_DISABLED_FLAG ] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification 760 789 echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__( 761 'WooCommerce\'s stock Cash on Delivery gateway has been disabled.', 'mora-delivery-for-woocommerce' 790 'WooCommerce\'s stock Cash on Delivery gateway has been disabled.', 791 'mora-delivery-for-woocommerce' 762 792 ) . '</p></div>'; 763 793 } elseif ( $this->is_cod_enabled() ) { -
mora-delivery-for-woocommerce/trunk/includes/class-mora-paystack-driver.php
r3746963 r3749063 77 77 if ( $has_override ) { 78 78 $key = $this->override_testmode ? $this->override_test_secret_key : $this->override_live_secret_key; 79 return array( 'key' => $key, 'testmode' => $this->override_testmode ); 80 } 81 82 $paystack_settings = get_option( 'woocommerce_paystack_settings', array() ); 79 return array( 80 'key' => $key, 81 'testmode' => $this->override_testmode, 82 ); 83 } 84 85 $paystack_settings = get_option( 'woocommerce_paystack_settings', array() ); 83 86 $inherited_testmode = isset( $paystack_settings['testmode'] ) && 'yes' === $paystack_settings['testmode']; 84 $key = $inherited_testmode87 $key = $inherited_testmode 85 88 ? ( isset( $paystack_settings['test_secret_key'] ) ? $paystack_settings['test_secret_key'] : '' ) 86 89 : ( isset( $paystack_settings['live_secret_key'] ) ? $paystack_settings['live_secret_key'] : '' ); 87 90 88 return array( 'key' => $key, 'testmode' => $inherited_testmode ); 91 return array( 92 'key' => $key, 93 'testmode' => $inherited_testmode, 94 ); 89 95 } 90 96 … … 92 98 $keys = $this->resolve_keys(); 93 99 if ( '' === trim( $keys['key'] ) ) { 94 return array( 'ok' => false, 'error' => 'Paystack secret key is not configured.' ); 100 return array( 101 'ok' => false, 102 'error' => 'Paystack secret key is not configured.', 103 ); 95 104 } 96 105 … … 119 128 120 129 if ( is_wp_error( $response ) ) { 121 return array( 'ok' => false, 'error' => $response->get_error_message() ); 130 return array( 131 'ok' => false, 132 'error' => $response->get_error_message(), 133 ); 122 134 } 123 135 … … 126 138 127 139 if ( 200 !== $status || empty( $data['status'] ) || empty( $data['data']['authorization_url'] ) ) { 128 return array( 'ok' => false, 'error' => $this->error_message( $data, $status ) ); 129 } 130 131 return array( 'ok' => true, 'redirect_url' => $data['data']['authorization_url'] ); 140 return array( 141 'ok' => false, 142 'error' => $this->error_message( $data, $status ), 143 ); 144 } 145 146 return array( 147 'ok' => true, 148 'redirect_url' => $data['data']['authorization_url'], 149 ); 132 150 } 133 151 … … 135 153 $keys = $this->resolve_keys(); 136 154 if ( '' === trim( $keys['key'] ) ) { 137 return array( 'ok' => false, 'error' => 'Paystack secret key is not configured.' ); 155 return array( 156 'ok' => false, 157 'error' => 'Paystack secret key is not configured.', 158 ); 138 159 } 139 160 … … 147 168 148 169 if ( is_wp_error( $response ) ) { 149 return array( 'ok' => false, 'error' => $response->get_error_message() ); 170 return array( 171 'ok' => false, 172 'error' => $response->get_error_message(), 173 ); 150 174 } 151 175 … … 154 178 155 179 if ( 200 !== $status || empty( $data['status'] ) || ! isset( $data['data'] ) ) { 156 return array( 'ok' => false, 'error' => $this->error_message( $data, $status ) ); 180 return array( 181 'ok' => false, 182 'error' => $this->error_message( $data, $status ), 183 ); 157 184 } 158 185 -
mora-delivery-for-woocommerce/trunk/includes/class-mora-sentry-reporter.php
r3746963 r3749063 64 64 // elsewhere in this plugin (order meta, checkout fields). 65 65 const PII_KEYS = array( 66 'customer_name', 'customer_email', 'customer_phone', 'phone', 'email', 67 'address', 'address_1', 'address_2', 'billing_email', 'billing_phone', 68 'first_name', 'last_name', 66 'customer_name', 67 'customer_email', 68 'customer_phone', 69 'phone', 70 'email', 71 'address', 72 'address_1', 73 'address_2', 74 'billing_email', 75 'billing_phone', 76 'first_name', 77 'last_name', 69 78 ); 70 79 … … 107 116 $parsed['store_url'], 108 117 array( 109 'headers' => array(118 'headers' => array( 110 119 'Content-Type' => 'application/json', 111 120 'X-Sentry-Auth' => self::auth_header( $parsed['public_key'] ), 112 121 ), 113 'body' => wp_json_encode( $event ),114 'timeout' => self::TIMEOUT_SECONDS,115 'blocking' => false,122 'body' => wp_json_encode( $event ), 123 'timeout' => self::TIMEOUT_SECONDS, 124 'blocking' => false, 116 125 ) 117 126 ); -
mora-delivery-for-woocommerce/trunk/includes/class-mora-shipping-method.php
r3746963 r3749063 73 73 $this->enabled = $this->get_option( 'enabled', 'yes' ); 74 74 75 $credentials = Mora_Admin_Settings::site_wide_credentials();75 $credentials = Mora_Admin_Settings::site_wide_credentials(); 76 76 $this->api_base_url = untrailingslashit( $credentials['api_base_url'] ); 77 $this->api_key = $credentials['api_key'];77 $this->api_key = $credentials['api_key']; 78 78 79 79 add_action( 'woocommerce_update_options_shipping_' . $this->id, array( $this, 'process_admin_options' ) ); … … 120 120 $locator_on = Mora_Admin_Settings::is_places_locator_enabled(); 121 121 $location = $locator_on ? Mora_Checkout::get_pinned_location() : array(); 122 $lat = isset( $location['lat'] ) ? $location['lat'] : null;123 $lon = isset( $location['lon'] ) ? $location['lon'] : null;122 $lat = isset( $location['lat'] ) ? $location['lat'] : null; 123 $lon = isset( $location['lon'] ) ? $location['lon'] : null; 124 124 125 125 $has_coordinates = ( null !== $lat && null !== $lon ); … … 129 129 // here either when there is a pin. 130 130 $address = $has_coordinates ? null : $this->customer_address_text( $package, $state_name ); 131 $quote = Mora_Api_Client::get_quote( $this->api_base_url, $this->api_key, $state_name, $lat, $lon, $address );131 $quote = Mora_Api_Client::get_quote( $this->api_base_url, $this->api_key, $state_name, $lat, $lon, $address ); 132 132 133 133 // Whether the customer has already given us an address to work from, -
mora-delivery-for-woocommerce/trunk/includes/class-mora-webhook-receiver.php
r3746963 r3749063 79 79 if ( '' === $secret ) { 80 80 self::log_throttled( 'error', 'Mora: webhook received but no webhook secret is configured on this site.', 'no_secret' ); 81 return new WP_REST_Response( array( 'ok' => false, 'error' => 'webhook not configured' ), 501 ); 81 return new WP_REST_Response( 82 array( 83 'ok' => false, 84 'error' => 'webhook not configured', 85 ), 86 501 87 ); 82 88 } 83 89 … … 87 93 // number formatting). Same reasoning as the documented-but-unbuilt 88 94 // Paystack signature check elsewhere in this codebase. 89 $raw_body = $request->get_body();95 $raw_body = $request->get_body(); 90 96 $received_signature = (string) $request->get_header( 'x_mora_webhook_signature' ); 91 97 if ( '' === $received_signature ) { 92 return new WP_REST_Response( array( 'ok' => false, 'error' => 'missing signature' ), 401 ); 98 return new WP_REST_Response( 99 array( 100 'ok' => false, 101 'error' => 'missing signature', 102 ), 103 401 104 ); 93 105 } 94 106 … … 96 108 if ( ! hash_equals( $expected_signature, $received_signature ) ) { 97 109 self::log_throttled( 'warning', 'Mora: webhook signature did not match.', 'bad_signature' ); 98 return new WP_REST_Response( array( 'ok' => false, 'error' => 'invalid signature' ), 401 ); 110 return new WP_REST_Response( 111 array( 112 'ok' => false, 113 'error' => 'invalid signature', 114 ), 115 401 116 ); 99 117 } 100 118 101 119 $payload = json_decode( $raw_body, true ); 102 120 if ( ! is_array( $payload ) || empty( $payload['merchant_order_id'] ) || empty( $payload['status'] ) ) { 103 return new WP_REST_Response( array( 'ok' => false, 'error' => 'malformed payload' ), 400 ); 121 return new WP_REST_Response( 122 array( 123 'ok' => false, 124 'error' => 'malformed payload', 125 ), 126 400 127 ); 104 128 } 105 129 … … 113 137 array( 'source' => 'mora' ) 114 138 ); 115 return new WP_REST_Response( array( 'ok' => false, 'error' => 'order not found' ), 404 ); 139 return new WP_REST_Response( 140 array( 141 'ok' => false, 142 'error' => 'order not found', 143 ), 144 404 145 ); 116 146 } 117 147 … … 130 160 array( 'source' => 'mora' ) 131 161 ); 132 return new WP_REST_Response( array( 'ok' => false, 'error' => 'order_id mismatch' ), 409 ); 162 return new WP_REST_Response( 163 array( 164 'ok' => false, 165 'error' => 'order_id mismatch', 166 ), 167 409 168 ); 133 169 } 134 170 -
mora-delivery-for-woocommerce/trunk/mora-delivery-for-woocommerce.php
r3748164 r3749063 3 3 * Plugin Name: Mora Delivery for WooCommerce 4 4 * Description: Live Mora delivery quotes at checkout, backed by mora-api's GET /v1/quote, a goods-on-delivery payment gateway that charges the delivery fee upfront, and pushes placed orders on to Mora. 5 * Version: 0.7.7 85 * Version: 0.7.79 6 6 * Requires at least: 6.0 7 7 * Requires PHP: 7.4 … … 21 21 } 22 22 23 define( 'MORA_WC_VERSION', '0.7.7 8' );23 define( 'MORA_WC_VERSION', '0.7.79' ); 24 24 define( 'MORA_WC_PLUGIN_FILE', __FILE__ ); 25 25 define( 'MORA_WC_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); -
mora-delivery-for-woocommerce/trunk/readme.txt
r3748164 r3749063 5 5 Tested up to: 7.1 6 6 Requires PHP: 7.4 7 Stable tag: 0.7.7 87 Stable tag: 0.7.79 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 31 31 This plugin connects to the following services to provide its features. None of them run unless the matching feature is actually configured and in use. 32 32 33 **Mora Logistics API (mora-api)**: Mora's own backend, used to price delivery quotes , process the delivery-fee payment gateway's PSP verification, and push/sync placed orders. When a quote is requested, this plugin sends the destination state, and, where available, the customer's pinned coordinates or typed address, to `https://api.moralogistics.com`. When an order is placed, order, product, and customer contact details are sent there so Mora can fulfil the delivery. This is Mora's own first-party service, governed by the same merchant agreement a Mora Logistics merchant signs; see Mora Logistics' Terms of Service (https://moralogistics.com/terms) and Privacy Policy (https://moralogistics.com/privacy-policy).33 **Mora Logistics API (mora-api)**: Mora's own backend, used to price delivery quotes and to push and sync placed orders. When a quote is requested, this plugin sends the destination state, and, where available, the customer's pinned coordinates or typed address, to the Mora API address set in the plugin's settings (Mora's production service is `https://api.moralogistics.com`). When an order is placed, order, product, and customer contact details are sent there so Mora can fulfil the delivery. This is Mora's own first-party service, governed by the same merchant agreement a Mora Logistics merchant signs; see Mora Logistics' Terms of Service (https://moralogistics.com/terms) and Privacy Policy (https://moralogistics.com/privacy-policy). 34 34 35 35 **Paystack**: used only by this plugin's own payment gateway (optional, merchant-enabled), through the merchant's own Paystack account. The order's delivery-fee amount, currency, and a payment reference are sent to Paystack to process the charge (sent to `https://api.paystack.co` when the customer places an order with this gateway, and again to verify the payment); no card details pass through this plugin or site, Paystack's own hosted page handles that directly. See Paystack's Terms of Use (https://paystack.com/terms) and Merchant Privacy Policy (https://paystack.com/privacy/merchant). … … 71 71 72 72 == Changelog == 73 74 = 0.7.79 = 75 * Fixed: hub-pickup orders from the block checkout (WooCommerce Local Pickup) were never sent to Mora. They are now, like classic-checkout pickup orders. 76 * Docs: External Services no longer says payments are verified through Mora's API (they are verified directly with Paystack), and names the configured API address. 73 77 74 78 = 0.7.78 = -
mora-delivery-for-woocommerce/trunk/uninstall.php
r3746963 r3749063 21 21 22 22 // This plugin's own site-wide options. 23 $ options = array(23 $mora_wc_options = array( 24 24 'mora_wc_places_api_key', 25 25 'mora_wc_reorder_core_fields', … … 38 38 'woocommerce_mora_settings', 39 39 ); 40 foreach ( $ options as $option ) {41 delete_option( $ option );40 foreach ( $mora_wc_options as $mora_wc_option ) { 41 delete_option( $mora_wc_option ); 42 42 } 43 43
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)