Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
8.82% covered (danger)
8.82%
6 / 68
21.43% covered (danger)
21.43%
3 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
Details_Formatter
8.82% covered (danger)
8.82%
6 / 68
21.43% covered (danger)
21.43%
3 / 14
538.38
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_btc_total_formatted
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 format_money_to_bitcoin
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 get_btc_exchange_rate_formatted
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 get_wc_order_status_formatted
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 get_last_checked_time_formatted
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
30
 get_btc_address_derivation_path_sequence_number
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 get_xpub_js_span
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 get_exchange_rate_url
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 get_btc_amount_received_formatted
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 get_friendly_status
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 to_array
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
6
 camel_case_keys
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 as_camel_case
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Takes the order details and return HTML and plain strings for output.
4 *
5 * @package brianhenryie/bh-wp-bitcoin-gateway
6 */
7
8namespace BrianHenryIE\WP_Bitcoin_Gateway\Integrations\WooCommerce;
9
10use BrianHenryIE\WP_Bitcoin_Gateway\Brick\Money\Exception\UnknownCurrencyException;
11use BrianHenryIE\WP_Bitcoin_Gateway\Brick\Money\Money;
12use BrianHenryIE\WP_Bitcoin_Gateway\Integrations\WooCommerce\Helpers\WC_Order_Meta_Helper;
13use BrianHenryIE\WP_Bitcoin_Gateway\Integrations\WooCommerce\Model\WC_Bitcoin_Order;
14use NumberFormatter;
15
16/**
17 * Parse the values of WC_Bitcoin_Order into HTML and strings for output.
18 */
19class Details_Formatter {
20
21    /**
22     * Constructor
23     *
24     * @param WC_Bitcoin_Order     $bitcoin_order The order we are about to print.
25     * @param WC_Order_Meta_Helper $order_meta_helper Set/get typed metadata.
26     */
27    public function __construct(
28        protected WC_Bitcoin_Order $bitcoin_order,
29        protected WC_Order_Meta_Helper $order_meta_helper,
30    ) {
31    }
32
33    /**
34     * ฿ U+0E3F THAI CURRENCY SYMBOL BAHT, decimal: 3647, HTML: &#3647;, UTF-8: 0xE0 0xB8 0xBF, block: Thai.
35     */
36    public function get_btc_total_formatted(): string {
37        $btc_price = $this->order_meta_helper->get_btc_total_price( $this->bitcoin_order->get_wc_order() );
38        return $btc_price ? $this->format_money_to_bitcoin( $btc_price ) : '';
39    }
40
41    /**
42     * Returns "฿ 0.00001234" style formatted Bitcoin amount.
43     *
44     * @param Money $money The amount to format.
45     *
46     * @return string
47     */
48    protected function format_money_to_bitcoin( Money $money ): string {
49        $btc_symbol = '฿';
50        $pattern    = '¤#,##0.000000000000000000';
51
52        $btc_formatter = new NumberFormatter( 'en_US', NumberFormatter::DECIMAL );
53        /**
54         * "Bitcoin has 8 decimal places."
55         *
56         * @see https://bitcoin.stackexchange.com/a/31934
57         */
58        $btc_formatter->setAttribute( NumberFormatter::FRACTION_DIGITS, 8 );
59
60        $formatted = $money->formatWith( $btc_formatter );
61
62        return $btc_symbol . ' ' . wc_trim_zeros( $formatted );
63    }
64
65    /**
66     * TODO: This should display the store currency value for one Bitcoin at the time of order. Currently ~"90817.00".
67     */
68    public function get_btc_exchange_rate_formatted(): string {
69        $exchange_rate = $this->order_meta_helper->get_exchange_rate( $this->bitcoin_order->get_wc_order() );
70        if ( ! $exchange_rate ) {
71            // TODO: log.
72            return '';
73        }
74        $exchange_rate_float = $exchange_rate->getAmount()->toFloat();
75        return $this->bitcoin_order->get_currency() . ' ' . wc_price( $exchange_rate_float, array( 'currency' => $this->bitcoin_order->get_currency() ) );
76    }
77
78    /**
79     * Get the pretty formatted WooCommerce order status.
80     */
81    public function get_wc_order_status_formatted(): ?string {
82        /** @var array<string, string> $wc_order_statuses */
83        $wc_order_statuses = wc_get_order_statuses();
84
85        return $wc_order_statuses[ 'wc-' . $this->bitcoin_order->get_status() ] ?? null;
86    }
87
88    /**
89     * "Never"|the time it was last checked. TODO: this is using `order->get_last_checked_time` which is not
90     * updated when the Bitcoin_Address itself has last been checked for transactions.
91     */
92    public function get_last_checked_time_formatted(): string {
93        if ( is_null( $this->bitcoin_order->get_last_checked_time() ) ) {
94            return __( 'Never', 'bh-wp-bitcoin-gateway' );
95        }
96        /**
97         * @see https://www.php.net/manual/datetime.format.php
98         * @var string $date_format
99         */
100        $date_format = get_option( 'date_format' ) ?: 'Y-m-d'; // TODO: What is a good default here?
101        /**
102         * @see https://www.php.net/manual/datetime.format.php
103         * @var string $time_format
104         */
105        $time_format = get_option( 'time_format' ) ?: 'H:i:s'; // TODO: default format.
106        $timezone    = wp_timezone_string();
107        // $last_checked_time is in UTC... change it to local time.?
108        // The server time is not local time... maybe use their address?
109        // @see https://stackoverflow.com/tags/timezone/info
110
111        $last_checked = $this->bitcoin_order->get_last_checked_time();
112        // @phpstan-ignore-next-line For some reason PHPStan isn't convinced this can be null.
113        if ( is_null( $last_checked ) ) {
114            return __( 'Never', 'bh-wp-bitcoin-gateway' );
115        }
116
117        $date_time_formatted = $last_checked->format( $date_format . ', ' . $time_format );
118
119        return $date_time_formatted . ' ' . $timezone;
120    }
121
122    /**
123     * The index of the derived address being used. TODO: no point displaying this to customers.
124     */
125    public function get_btc_address_derivation_path_sequence_number(): string {
126        $sequence_number = $this->bitcoin_order->get_address()->get_derivation_path_sequence_number();
127        return "{$sequence_number}";
128    }
129
130    /**
131     * Get a clickable HTML element, to copy the payment address to the clipboard when clicked.
132     */
133    public function get_xpub_js_span(): string {
134        $payment_address                  = $this->bitcoin_order->get_address()->get_raw_address();
135        $payment_address_friendly_display = substr( $payment_address, 0, 7 ) . ' ... ' . substr( $payment_address, - 3, 3 );
136        return "<span style=\"border-bottom: 1px dashed #999; word-wrap: break-word\" onclick=\"this.innerText = this.innerText === '{$payment_address}' ? '{$payment_address_friendly_display}' : '{$payment_address}';\" title=\"{$payment_address}\"'>{$payment_address_friendly_display}</span>";
137    }
138
139    /**
140     *  Add a link showing the exchange rate around the time of the order ( -12 hours to +12 hours after payment).
141     */
142    public function get_exchange_rate_url(): string {
143        /**
144         * This supposedly could be null, but I can't imagine a scenario where WooCommerce returns an order object
145         * that doesn't have a DateTime for created.
146         *
147         * @var \DateTimeInterface $date_created
148         */
149        $date_created = $this->bitcoin_order->get_date_created();
150        $from         = $date_created->getTimestamp() - ( DAY_IN_SECONDS / 2 );
151        if ( ! is_null( $this->bitcoin_order->get_date_paid() ) ) {
152            $to = $this->bitcoin_order->get_date_paid()->getTimestamp() + ( DAY_IN_SECONDS / 2 );
153        } else {
154            $to = $from + DAY_IN_SECONDS;
155        }
156        return "https://www.blockchain.com/prices/BTC?from={$from}&to={$to}&timeSpan=custom&scale=0&style=line";
157    }
158
159    /**
160     * String as ฿0.00123.
161     *
162     * @throws UnknownCurrencyException If 'BTC' has not been added to `brick/money`.
163     */
164    public function get_btc_amount_received_formatted(): string {
165
166        // TODO: An address doesn't know how many confirmations an order wants.
167        // e.g. there could be dynamic number of confirmations based on order total.
168
169        return $this->format_money_to_bitcoin(
170            $this->bitcoin_order->get_address()->get_amount_received() ?? Money::of( 0, 'BTC' )
171        );
172    }
173
174    /**
175     * @return string 'Awaiting Payment'|'Partly Paid'|'Paid'
176     */
177    public function get_friendly_status(): string {
178
179        // If the order is not marked paid, but has transactions, it is partly-paid.
180        switch ( true ) {
181            case $this->bitcoin_order->is_paid():
182                $result = __( 'Paid', 'bh-wp-bitcoin-gateway' );
183                break;
184            case $this->bitcoin_order->get_address()->get_amount_received()?->isGreaterThan( Money::of( 0, 'BTC' ) ):
185                $result = __( 'Partly Paid', 'bh-wp-bitcoin-gateway' );
186                break;
187            default:
188                $result = __( 'Awaiting Payment', 'bh-wp-bitcoin-gateway' );
189        }
190
191        return $result;
192    }
193
194    /**
195     * Get all values this class creates as an array.
196     *
197     * Probably passed to a template.
198     *
199     * probably: array{btc_total_formatted:string, btc_exchange_rate_formatted:string, order_status_before_formatted:string, order_status_formatted:string|null, btc_amount_received_formatted:string, last_checked_time_formatted:string}.
200     * maybe: camel case keys.
201     *
202     * @param bool $as_camel_case Default to snake_case but the array keys can be camelCase too.
203     *
204     * @return array<string, string|null>
205     */
206    public function to_array( bool $as_camel_case = false ): array {
207
208        $result                                  = array();
209        $result['btc_total_formatted']           = $this->get_btc_total_formatted();
210        $result['btc_exchange_rate_formatted']   = $this->get_btc_exchange_rate_formatted();
211        $result['order_status_formatted']        = $this->get_wc_order_status_formatted();
212        $result['btc_amount_received_formatted'] = $this->get_btc_amount_received_formatted();
213        $result['last_checked_time_formatted']   = $this->get_last_checked_time_formatted();
214        $result['btc_address_derivation_path_sequence_number'] = $this->get_btc_address_derivation_path_sequence_number();
215        $result['parent_wallet_xpub_html']                     = $this->get_xpub_js_span();
216        $result['exchange_rate_url']                           = $this->get_exchange_rate_url();
217        $result['payment_status']                              = $this->get_friendly_status();
218        $result['payment_address']                             = $this->bitcoin_order->get_address()->get_raw_address();
219
220        return $as_camel_case
221            ? self::camel_case_keys( $result )
222            : $result;
223    }
224
225    /**
226     * Convert an array's keys to camelCase, presumably to output for JavaScript.
227     *
228     * @param array<string,mixed> $array_with_keys Array to update.
229     * @return array<string,mixed>
230     */
231    public static function camel_case_keys( array $array_with_keys ): array {
232        foreach ( $array_with_keys as $key => $value ) {
233            $new_key                     = self::as_camel_case( $key );
234            $array_with_keys[ $new_key ] = $value;
235            unset( $array_with_keys[ $key ] );
236        }
237        return $array_with_keys;
238    }
239
240    /**
241     * Map the array keys to camelCase for JavaScript use.
242     *
243     * @param string $variable_name Snake_case variable name.
244     *
245     * @return string CamelCase variable name.
246     */
247    protected static function as_camel_case( string $variable_name ): string {
248        return lcfirst( str_replace( ' ', '', ucwords( str_replace( '_', ' ', $variable_name ) ) ) );
249    }
250}