Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
87.80% covered (warning)
87.80%
36 / 41
75.00% covered (warning)
75.00%
6 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
Background_Jobs_Actions_Handler
87.80% covered (warning)
87.80%
36 / 41
75.00% covered (warning)
75.00%
6 / 8
11.22
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
 add_action_scheduler_repeating_actions
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 update_exchange_rate
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 ensure_unused_addresses
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 single_ensure_unused_addresses
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 generate_new_addresses
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 check_new_addresses_for_transactions
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 check_assigned_addresses_for_transactions
70.00% covered (warning)
70.00%
7 / 10
0.00% covered (danger)
0.00%
0 / 1
3.24
1<?php
2/**
3 * Ensure:
4 * * the exchange rate is up-to-date
5 * * there are unused addresses available for orders
6 * * assigned addresses are checked for payments
7 *
8 * After new orders, wait 15 minutes and check for payments (TODO: check mempool too).
9 * While the destination address is waiting for payment, continue to schedule new checks every ten minutes (block generation time)
10 * Every hour, in case the previous check is not running correctly, check are there assigned Bitcoin addresses that we should check for transactions
11 * Schedule background job to generate new addresses as needed (fall below threshold defined elsewhere)
12 * After generating new addresses, check for existing transactions to ensure they are available to use
13 *
14 * @package    brianhenryie/bh-wp-bitcoin-gateway
15 */
16
17namespace BrianHenryIE\WP_Bitcoin_Gateway\Action_Scheduler;
18
19use BrianHenryIE\WP_Bitcoin_Gateway\API\Model\Exceptions\Rate_Limit_Exception;
20use BrianHenryIE\WP_Bitcoin_Gateway\API\Services\Bitcoin_Wallet_Service;
21use BrianHenryIE\WP_Bitcoin_Gateway\BH_WP_Bitcoin_Gateway;
22use DateInterval;
23use DateTimeImmutable;
24use Psr\Log\LoggerAwareTrait;
25use Psr\Log\LoggerInterface;
26
27/**
28 * Functions to handle `do_action` initiated from Action Scheduler.
29 *
30 * @see BH_WP_Bitcoin_Gateway::define_action_scheduler_hooks()
31 */
32class Background_Jobs_Actions_Handler implements Background_Jobs_Actions_Interface {
33    use LoggerAwareTrait;
34
35    /**
36     * Constructor
37     *
38     * @param API_Background_Jobs_Interface       $api Main plugin class.
39     * @param Bitcoin_Wallet_Service              $wallet_service Used to convert post_id into Bitcoin_Wallet and check are there addresses to check.
40     * @param Background_Jobs_Scheduler_Interface $background_jobs_scheduler Uses Action Scheduler `as_*` functions to invoke this class's functions during cron/background.
41     * @param LoggerInterface                     $logger PSR logger.
42     */
43    public function __construct(
44        protected API_Background_Jobs_Interface $api,
45        protected Bitcoin_Wallet_Service $wallet_service,
46        protected Background_Jobs_Scheduler_Interface $background_jobs_scheduler,
47        LoggerInterface $logger
48    ) {
49        $this->setLogger( $logger );
50    }
51
52    /**
53     * Using Action Scheduler's "schedule" hook, set up our own repeating jobs.
54     *
55     * @hooked action_scheduler_run_recurring_actions_schedule_hook
56     * @see \ActionScheduler_RecurringActionScheduler
57     *
58     * @used-by BH_WP_Bitcoin_Gateway::define_action_scheduler_hooks()
59     * @see Background_Jobs_Scheduler_Interface::schedule_check_for_assigned_addresses_repeating_action()
60     *
61     * @see https://crontab.guru/every-1-hour
62     * @see https://github.com/woocommerce/action-scheduler/issues/749
63     */
64    public function add_action_scheduler_repeating_actions(): void {
65        $this->background_jobs_scheduler->schedule_recurring_update_exchange_rate();
66        $this->background_jobs_scheduler->schedule_recurring_ensure_unused_addresses();
67        $this->background_jobs_scheduler->schedule_single_check_assigned_addresses_for_transactions();
68    }
69
70    /**
71     * Update the exchange rate for the WooCommerce store currency.
72     *
73     * @hooked Background_Jobs_Actions_Interface::UPDATE_EXCHANGE_RATE_HOOK
74     */
75    public function update_exchange_rate(): void {
76        $this->logger->debug( 'Starting update_exchange_rate() background job.' );
77
78        $result = $this->api->update_exchange_rate();
79
80        $this->logger->info(
81            'Finished update_exchange_rate() background job. Rate for {currency} changed to {new_value} from {old_value}.',
82            array(
83                'currency'  => $result->requested_exchange_rate_currency,
84                'new_value' => $result->updated_exchange_rate->rate->getAmount()->__toString(),
85                'old_value' => $result->updated_exchange_rate->previous_cached_exchange_rate?->rate->getAmount()->__toString() ?? '<null>',
86            )
87        );
88    }
89
90    /**
91     * @hooked Background_Jobs_Actions_Interface::RECURRING_ENSURE_UNUSED_ADDRESSES_HOOK
92     */
93    public function ensure_unused_addresses(): void {
94
95        $this->logger->debug( 'Starting ensure_unused_addresses() background job.' );
96
97        // TODO: return a meaningful result and log it.
98        $result = $this->api->ensure_unused_addresses();
99    }
100
101    /**
102     * Check a wallet (blockchain API calls) to make sure we have some unused payment addresses available for it.
103     *
104     * Used when a wallet is created and when a payment address is assigned to an order.
105     *
106     * @see Background_Jobs_Actions_Interface::single_ensure_unused_addresses()
107     * @see BH_WP_Bitcoin_Gateway::define_action_scheduler_hooks()
108     *
109     * @param int $wallet_post_id Deserialised argument passed from Action Scheduler.
110     */
111    public function single_ensure_unused_addresses( int $wallet_post_id ): void {
112        $this->logger->debug( 'Starting `single_ensure_unused_addresses()` background job for `wallet_post_id:{wallet_post_id}`.', array( 'wallet_post_id' => $wallet_post_id ) );
113
114        $wallet = $this->wallet_service->get_wallet_by_wp_post_id( $wallet_post_id );
115
116        $result = $this->api->ensure_unused_addresses_for_wallet_synchronously( $wallet );
117
118        $this->logger->info(
119            'Finished `single_ensure_unused_addresses()` background job for `wallet_post_id:{wallet_post_id}`.',
120            array_merge( array( 'wallet_post_id' => $wallet_post_id ), (array) $result )
121        );
122    }
123
124    /**
125     * When available addresses fall below a threshold, more are generated on a background job.
126     *
127     * @hooked bh_wp_bitcoin_gateway_generate_new_addresses
128     * @see self::GENERATE_NEW_ADDRESSES_HOOK
129     */
130    public function generate_new_addresses(): void {
131
132        $this->logger->debug( 'Starting generate_new_addresses() background job.' );
133
134        // TODO: return a meaningful result and log it.
135        $result = $this->api->generate_new_addresses();
136    }
137
138    /**
139     * After new addresses have been created, we check to see are they fresh/available to use.
140     * TODO It's not unlikely we'll hit 429 rate limits during this, so we'll loop through as many as we can,
141     * then schedule a new job when we're told to stop.
142     *
143     * @hooked {@see self::CHECK_NEW_ADDRESSES_TRANSACTIONS_HOOK}
144     */
145    public function check_new_addresses_for_transactions(): void {
146
147        $this->logger->debug( 'Starting check_new_addresses_for_transactions() background job.' );
148
149        try {
150            $result = $this->api->check_new_addresses_for_transactions();
151        } catch ( Rate_Limit_Exception $exception ) {
152            $this->background_jobs_scheduler->schedule_check_newly_generated_bitcoin_addresses_for_transactions(
153                $exception->get_reset_time()
154            );
155        }
156    }
157
158    /**
159     * Fetch all the addresses pending payments, ordered by last updated
160     * query the Blockchain API for updates,
161     * on rate-limit error, reschedule a check after the rate limit expires,
162     * reschedule another check in ten minutes if there are still addresses awaiting payment.
163     *
164     * If we have failed to check all the addresses that we should, so let's reschedule the check when
165     * the rate limit expires. The addresses that were successfully checked should have their updated
166     * time updated, so the next addresses in sequence will be the next checked.
167     * TODO: should the rescheduling be handled here or in the API class?
168     *
169     * @hooked {@see self::CHECK_ASSIGNED_ADDRESSES_TRANSACTIONS_HOOK}
170     */
171    public function check_assigned_addresses_for_transactions(): void {
172
173        $this->logger->info( 'Starting check_assigned_addresses_for_transactions() background job.' );
174
175        try {
176            $result = $this->api->check_assigned_addresses_for_payment();
177
178        } catch ( Rate_Limit_Exception $rate_limit_exception ) {
179            $this->background_jobs_scheduler->schedule_single_check_assigned_addresses_for_transactions(
180                $rate_limit_exception->get_reset_time()
181            );
182        }
183
184        // If we are still waiting for payments, schedule another check in ten minutes.
185        // TODO: Is this better placed in API class?
186        if ( $this->wallet_service->has_assigned_bitcoin_addresses() ) {
187            $this->background_jobs_scheduler->schedule_single_check_assigned_addresses_for_transactions(
188                new DateTimeImmutable( 'now' )->add( new DateInterval( 'PT10M' ) )
189            );
190        }
191    }
192}