> */ public static function events( $events ): array { $events = is_array( $events ) ? $events : array(); $events[] = self::event( self::EVENT_CREATED, __( 'WooCommerce: new order', '85pl-android-sms-api' ), '' ); foreach ( wc_get_order_statuses() as $key => $name ) { $status = self::status_slug( (string) $key ); $events[] = self::event( self::EVENT_STATUS . $status, /* translators: %s: order status name, e.g. "Completed". */ sprintf( __( 'WooCommerce: order status changed to "%s"', '85pl-android-sms-api' ), wp_strip_all_tags( (string) $name ) ), $status ); } return $events; } /** * Hook: a new order (drafts excluded by WooCommerce). * * @param int|string $order_id Order ID. * @param mixed $order Order. * @return void */ public static function order_created( $order_id, $order = null ): void { try { $order = self::order( $order_id, $order ); if ( null !== $order ) { Events::fire( self::EVENT_CREATED, self::context( $order ) ); } } catch ( \Throwable $e ) { Events::report( $e, self::EVENT_CREATED ); } } /** * Hook: order status changed. The new status is the dedupe suffix, so the same status set twice * (by hand and by the payment gateway) sends one SMS. * * @param int|string $order_id Order ID. * @param mixed $from Old status. * @param mixed $to New status (without "wc-"). * @param mixed $order Order. * @return void */ public static function status_changed( $order_id, $from, $to, $order = null ): void { $status = self::status_slug( (string) $to ); $event_id = self::EVENT_STATUS . $status; try { $order = self::order( $order_id, $order ); if ( null !== $order && '' !== $status ) { Events::fire( $event_id, self::context( $order ), $status ); } } catch ( \Throwable $e ) { Events::report( $e, $event_id ); } } /** * Filter: calling code of the store's country (WooCommerce > Settings > General). * * @param mixed $code Calling code so far. * @return string */ public static function store_country_code( $code ): string { $countries = function_exists( 'WC' ) ? WC()->countries : null; $store = $countries instanceof \WC_Countries ? self::country_code( (string) $countries->get_base_country() ) : null; return null !== $store ? $store : (string) $code; } /** * Template variables and the customer's number of an order. * * @param \WC_Order $order Order. * @return array */ public static function context( \WC_Order $order ): array { $tracking = self::tracking( $order ); return array( 'order_id' => $order->get_id(), 'imie' => $order->get_billing_first_name(), 'nazwisko' => $order->get_billing_last_name(), 'numer_zamowienia' => (string) $order->get_order_number(), 'kwota' => self::amount( $order ), 'status' => wc_get_order_status_name( $order->get_status() ), 'nazwa_sklepu' => wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ), 'numer_przesylki' => $tracking['number'], 'przewoznik' => $tracking['carrier'], 'link_przesylki' => $tracking['link'], 'link_zamowienia' => $order->get_customer_id() > 0 ? $order->get_view_order_url() : $order->get_checkout_order_received_url(), 'telefon' => self::phone( $order ), 'edit_url' => $order->get_edit_order_url(), ); } /** * Tracking number, carrier and link of an order. Our own meta comes first (ShipStation and others write * _tracking_number); otherwise the newest shipment of Advanced Shipment Tracking or WooCommerce Shipment * Tracking, which both keep _wc_shipment_tracking_items. * * @param \WC_Order $order Order. * @return array{number:string,carrier:string,link:string} */ private static function tracking( \WC_Order $order ): array { $number = (string) $order->get_meta( '_tracking_number' ); if ( '' !== $number ) { return array( 'number' => $number, 'carrier' => (string) $order->get_meta( '_tracking_provider' ), 'link' => '', ); } $items = $order->get_meta( '_wc_shipment_tracking_items' ); $items = is_array( $items ) ? array_values( array_filter( $items, 'is_array' ) ) : array(); if ( ! $items ) { return array( 'number' => '', 'carrier' => '', 'link' => '', ); } $item = $items[ count( $items ) - 1 ]; $formatted = self::formatted_tracking( $order, $item ); $carrier = (string) ( $formatted['formatted_tracking_provider'] ?? '' ); $link = (string) ( $formatted['formatted_tracking_link'] ?? '' ); return array( 'number' => (string) ( $item['tracking_number'] ?? '' ), 'carrier' => '' !== $carrier ? $carrier : (string) ( $item['custom_tracking_provider'] ?? $item['tracking_provider'] ?? '' ), 'link' => '' !== $link ? $link : (string) ( $item['custom_tracking_link'] ?? '' ), ); } /** * Carrier name and tracking link as the tracking plugin itself formats them (its carrier list). * * @param \WC_Order $order Order. * @param array $item One shipment. * @return array */ private static function formatted_tracking( \WC_Order $order, array $item ): array { $plugins = array( 'WC_Advanced_Shipment_Tracking_Actions', 'WC_Shipment_Tracking_Actions' ); foreach ( $plugins as $class ) { if ( ! class_exists( $class ) || ! method_exists( $class, 'get_instance' ) ) { continue; } try { $formatted = call_user_func( array( $class, 'get_instance' ) )->get_formatted_tracking_item( $order->get_id(), $item ); return is_array( $formatted ) ? $formatted : array(); } catch ( \Throwable $e ) { return array(); } } return array(); } /** * One event definition. * * @param string $id Event ID. * @param string $label Label. * @param string $status Order status the event is about ('' = any). * @return array */ private static function event( string $id, string $label, string $status ): array { return array( 'id' => $id, 'label' => $label, 'group' => self::GROUP, 'icon' => SMSPORTAL_URL_BASE . 'assets/integrations/woocommerce.png', 'variables' => self::variables(), 'customer_phone' => static function ( array $context ) { return isset( $context['telefon'] ) && is_string( $context['telefon'] ) ? $context['telefon'] : null; }, 'object' => static function ( array $context ): array { return array( 'type' => self::OBJECT_TYPE, 'id' => (string) ( $context['order_id'] ?? '' ), 'url' => (string) ( $context['edit_url'] ?? '' ), ); }, 'sample' => static function () use ( $status ) { return self::sample( $status ); }, ); } /** * Variables offered in the rule editor. * * @return array */ private static function variables(): array { return Variables::for_editor( array( 'imie' => __( 'First name', '85pl-android-sms-api' ), 'nazwisko' => __( 'Last name', '85pl-android-sms-api' ), 'numer_zamowienia' => __( 'Order number', '85pl-android-sms-api' ), 'kwota' => __( 'Order total', '85pl-android-sms-api' ), 'status' => __( 'Order status', '85pl-android-sms-api' ), 'nazwa_sklepu' => __( 'Store name', '85pl-android-sms-api' ), 'numer_przesylki' => __( 'Tracking number', '85pl-android-sms-api' ), 'przewoznik' => __( 'Carrier', '85pl-android-sms-api' ), 'link_przesylki' => __( 'Tracking link', '85pl-android-sms-api' ), 'link_zamowienia' => __( 'Order link', '85pl-android-sms-api' ), ) ); } /** * Context of the latest order (with the given status), for the rule preview. * * @param string $status Status slug, '' = any. * @return array|null */ private static function sample( string $status ): ?array { $query = array( 'limit' => 1, 'orderby' => 'date', 'order' => 'DESC', 'type' => 'shop_order', ); if ( '' !== $status ) { $query['status'] = $status; } $orders = wc_get_orders( $query ); $order = is_array( $orders ) ? reset( $orders ) : null; return $order instanceof \WC_Order ? self::context( $order ) : null; } /** * The order of a hook call (refunds and other order types are skipped). * * @param mixed $order_id Order ID. * @param mixed $order Order passed by the hook. * @return \WC_Order|null */ private static function order( $order_id, $order ): ?\WC_Order { if ( $order instanceof \WC_Order ) { return $order; } $order = absint( $order_id ) > 0 ? wc_get_order( absint( $order_id ) ) : null; return $order instanceof \WC_Order ? $order : null; } /** * Numbers of a person for the privacy tools: billing phone of the account with that e-mail * address and of its orders and the orders placed with that billing address. * * @param mixed $phones Numbers so far. * @param string $email E-mail address. * @return array */ public static function privacy_phones( $phones, $email ): array { $phones = is_array( $phones ) ? $phones : array(); $user = get_user_by( 'email', (string) $email ); if ( $user instanceof \WP_User ) { $raw = (string) get_user_meta( $user->ID, 'billing_phone', true ); if ( '' !== trim( $raw ) ) { $country = self::country_code( (string) get_user_meta( $user->ID, 'billing_country', true ) ); $phones[] = Phone::normalize( $raw, null !== $country ? $country : Phone::default_country_code() ); } } foreach ( self::orders_for_email( (string) $email ) as $order ) { $phones[] = self::phone( $order ); } return array_values( array_filter( $phones, 'is_string' ) ); } /** * Eraser step: delete the order notes with SMS replies (tagged by Replies) on the person's orders. * * @param mixed $removed Items removed so far. * @param string $email E-mail address. * @return int */ public static function privacy_erase( $removed, $email ): int { $removed = (int) $removed; foreach ( self::orders_for_email( (string) $email ) as $order ) { foreach ( wc_get_order_notes( array( 'order_id' => $order->get_id() ) ) as $note ) { if ( '' !== (string) get_comment_meta( (int) $note->id, Replies::NOTE_META, true ) && wc_delete_order_note( (int) $note->id ) ) { ++$removed; } } } return $removed; } /** * Orders of a person: placed with that billing e-mail address or by the account with that address. * * @param string $email E-mail address. * @return array */ private static function orders_for_email( string $email ): array { $queries = array( array( 'billing_email' => $email ) ); $user = get_user_by( 'email', $email ); if ( $user instanceof \WP_User ) { $queries[] = array( 'customer_id' => $user->ID ); } $orders = array(); foreach ( $queries as $query ) { $found = wc_get_orders( array_merge( $query, array( 'limit' => -1, 'type' => 'shop_order', ) ) ); foreach ( is_array( $found ) ? $found : array() as $order ) { if ( $order instanceof \WC_Order ) { $orders[ $order->get_id() ] = $order; } } } return array_values( $orders ); } /** * Customer's number in E.164: billing phone (shipping phone when empty), national numbers get the * calling code of the billing country, else of the store. * * @param \WC_Order $order Order. * @return string|null */ private static function phone( \WC_Order $order ): ?string { $raw = $order->get_billing_phone(); if ( '' === $raw && method_exists( $order, 'get_shipping_phone' ) ) { $raw = $order->get_shipping_phone(); } if ( '' === trim( (string) $raw ) ) { return null; } $country = self::country_code( $order->get_billing_country() ); return Phone::normalize( (string) $raw, null !== $country ? $country : Phone::default_country_code() ); } /** * Calling code of a country: our list first, then WooCommerce's. * * @param string $country Country code, also "PL:state" as stored in WooCommerce settings. * @return string|null Digits. */ private static function country_code( string $country ): ?string { $country = strtoupper( (string) strtok( $country, ':' ) ); if ( '' === $country ) { return null; } $code = Phone::calling_code( $country ); if ( null !== $code ) { return $code; } $countries = WC()->countries; $wc_code = $countries instanceof \WC_Countries ? $countries->get_country_calling_code( $country ) : ''; $digits = is_string( $wc_code ) ? (string) preg_replace( '/\D/', '', $wc_code ) : ''; return '' === $digits ? null : $digits; } /** * Order total as plain text, e.g. "123,45 zł". * * @param \WC_Order $order Order. * @return string */ private static function amount( \WC_Order $order ): string { $html = wc_price( (float) $order->get_total(), array( 'currency' => $order->get_currency() ) ); $text = html_entity_decode( wp_strip_all_tags( $html ), ENT_QUOTES, 'UTF-8' ); return trim( str_replace( "\u{00A0}", ' ', $text ) ); } /** * Status without the "wc-" prefix. * * @param string $status Status. * @return string */ private static function status_slug( string $status ): string { return 0 === strpos( $status, 'wc-' ) ? substr( $status, 3 ) : $status; } }