Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
74.42% covered (warning)
74.42%
64 / 86
85.71% covered (warning)
85.71%
6 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
Background_Jobs_Scheduler
74.42% covered (warning)
74.42%
64 / 86
85.71% covered (warning)
85.71%
6 / 7
21.84
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
 schedule_recurring_update_exchange_rate
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
3
 schedule_recurring_ensure_unused_addresses
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
3
 schedule_single_ensure_unused_addresses
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
6
 schedule_single_check_assigned_addresses_for_transactions
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
4
 schedule_generate_new_addresses
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 schedule_check_newly_generated_bitcoin_addresses_for_transactions
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2/**
3 * Functions for background job for checking addresses, generating addresses, etc.
4 *
5 * Every hour Action Scheduler's `action_scheduler_run_recurring_actions_schedule_hook` runs. We hook into this to
6 * register our own recurring hourly job, and schedule a single job to check assigned addresses.
7 *
8 * The hourly job ensures there is always two addresses that are unused. I.e. previously generated
9 * addresses may have been used outside WordPress and it is difficult to use transaction data to confirm payment when
10 * transactions may not be for the purpose of the order.
11 *
12 * The single job checks for payments and TODO: the action reschedules itself if there are still unpaid addresses to check.
13 * This job may already be scheduled when an address is assigned to an order, or from its own self-rescheduling.
14 *
15 * TODO: If we see a wallet being used outside WordPress should we suggest setting up an exclusive wallet?
16 *
17 * @package    brianhenryie/bh-wp-bitcoin-gateway
18 */
19
20namespace BrianHenryIE\WP_Bitcoin_Gateway\Action_Scheduler;
21
22use BrianHenryIE\WP_Bitcoin_Gateway\API\Repositories\Bitcoin_Address_Repository;
23use BrianHenryIE\WP_Bitcoin_Gateway\API\Model\Wallet\Bitcoin_Wallet;
24use DateTimeImmutable;
25use DateTimeInterface;
26use Psr\Log\LoggerAwareTrait;
27use Psr\Log\LoggerInterface;
28
29/**
30 * Functions to schedule Action Scheduler jobs.
31 */
32class Background_Jobs_Scheduler implements Background_Jobs_Scheduler_Interface {
33    use LoggerAwareTrait;
34
35    /**
36     * Constructor
37     *
38     * @param Bitcoin_Address_Repository $bitcoin_address_repository Object to learn if there are addresses to act on.
39     * @param LoggerInterface            $logger PSR logger.
40     */
41    public function __construct(
42        protected Bitcoin_Address_Repository $bitcoin_address_repository,
43        LoggerInterface $logger
44    ) {
45        $this->setLogger( $logger );
46    }
47
48    /**
49     * Schedule a recurring job to update the exchange rate every hour.
50     *
51     * @used-by Background_Jobs_Actions_Interface::add_action_scheduler_repeating_actions()
52     */
53    public function schedule_recurring_update_exchange_rate(): void {
54        if ( as_has_scheduled_action( hook: Background_Jobs_Actions_Interface::UPDATE_EXCHANGE_RATE_HOOK ) ) {
55            $this->logger->debug(
56                message: 'Background_Jobs schedule_update_exchange_rate already scheduled.',
57            );
58            return;
59        }
60
61        $result = as_schedule_recurring_action(
62            timestamp: time(),
63            interval_in_seconds: constant( 'HOUR_IN_SECONDS' ),
64            hook: Background_Jobs_Actions_Interface::UPDATE_EXCHANGE_RATE_HOOK,
65            unique: true,
66        );
67
68        if ( 0 === $result ) {
69            $this->logger->error( 'Background_Jobs schedule_update_exchange_rate failed.' );
70            return;
71        }
72
73        $this->logger->info( 'Background_Jobs schedule_update_exchange_rate hourly job added.' );
74    }
75
76    /**
77     * This is hooked to Action Scheduler's repeating action and schedules the ensure_unused_addresses job for
78     * once/hour. To schedule it to run immediately, use the below method schedule_single...
79     *
80     * @see self::schedule_single_ensure_unused_addresses()
81     * @used-by Background_Jobs_Actions_Interface::add_action_scheduler_repeating_actions()
82     */
83    public function schedule_recurring_ensure_unused_addresses(): void {
84        if ( as_has_scheduled_action( hook: Background_Jobs_Actions_Interface::RECURRING_ENSURE_UNUSED_ADDRESSES_HOOK ) ) {
85            $this->logger->debug(
86                message: 'Background_Jobs schedule_ensure_unused_addresses already scheduled.',
87            );
88            return;
89        }
90
91        $result = as_schedule_recurring_action(
92            timestamp: time(),
93            interval_in_seconds: constant( 'HOUR_IN_SECONDS' ),
94            hook: Background_Jobs_Actions_Interface::RECURRING_ENSURE_UNUSED_ADDRESSES_HOOK,
95            unique: true,
96        );
97
98        if ( 0 === $result ) {
99            $this->logger->error( 'Background_Jobs schedule_ensure_unused_addresses failed.' );
100            return;
101        }
102
103        $this->logger->info( 'Background_Jobs schedule_ensure_unused_addresses hourly job added.' );
104    }
105
106    /**
107     * Schedule and immediate background job to check/generate unused addresses for the wallet.
108     *
109     * When a new wallet is made, this is immediately called to schedule a job to ensure there is one payable address
110     * ready. TODO: When an address is assigned, this is called to queue up the next one.
111     *
112     * Functions using `API::generate_new_addresses_for_wallet()` elsewhere can decide to synchronously call the API
113     * to check are addresses unused, if that's the case, this would exit quickly anyway.
114     *
115     * @see Background_Jobs_Actions_Handler::single_ensure_unused_addresses()
116     *
117     * @used-by API::generate_new_addresses_for_wallet()
118     *
119     * @param Bitcoin_Wallet $wallet The wallet that may need payment addresses generated.
120     */
121    public function schedule_single_ensure_unused_addresses( Bitcoin_Wallet $wallet ): void {
122        if ( as_has_scheduled_action(
123            hook: Background_Jobs_Actions_Interface::SINGLE_ENSURE_UNUSED_ADDRESSES_HOOK,
124            args: array(
125                'wallet_post_id' => $wallet->get_post_id(),
126            )
127        )
128        ) {
129            // This will probably never happen, but in case this were called from a loop by mistake.
130            $this->logger->debug(
131                'Background_Jobs schedule_single_ensure_unused_addresses unexpectedly already scheduled for {wallet_id} {wallet_xpub}.',
132                array(
133                    'wallet_id'   => $wallet->get_post_id(),
134                    'wallet_xpub' => $wallet->get_xpub(),
135                )
136            );
137            return;
138        }
139
140        $date_time = new DateTimeImmutable( 'now' );
141        $action_id = as_schedule_single_action(
142            timestamp: $date_time->getTimestamp(),
143            hook: Background_Jobs_Actions_Interface::SINGLE_ENSURE_UNUSED_ADDRESSES_HOOK,
144            args: array(
145                'wallet_post_id' => $wallet->get_post_id(),
146            )
147        );
148    }
149
150    /**
151     * Schedule the next check for transactions for assigned addresses.
152     *
153     * @hooked
154     *
155     * @used-by Background_Jobs_Actions_Interface::add_action_scheduler_repeating_actions()
156     * TODO: When a new order is placed, let's schedule a check (in ten minutes).
157     *
158     * @param ?DateTimeInterface $date_time In ten minutes for a regular check (time to generate a new block), or use the rate limit reset time.
159     * e.g. `new DateTimeImmutable( 'now' )->add( new DateInterval( 'PT15M' ) )`.
160     */
161    public function schedule_single_check_assigned_addresses_for_transactions(
162        ?DateTimeInterface $date_time = null
163    ): void {
164        if ( as_has_scheduled_action( Background_Jobs_Actions_Interface::CHECK_ASSIGNED_ADDRESSES_TRANSACTIONS_HOOK )
165            && ! doing_action( Background_Jobs_Actions_Interface::CHECK_ASSIGNED_ADDRESSES_TRANSACTIONS_HOOK ) ) {
166            return;
167        }
168
169        if ( ! $this->bitcoin_address_repository->has_assigned_bitcoin_addresses() ) {
170            return;
171        }
172
173        $date_time ??= new DateTimeImmutable( 'now' );
174        as_schedule_single_action(
175            timestamp: $date_time->getTimestamp(),
176            hook: Background_Jobs_Actions_Interface::CHECK_ASSIGNED_ADDRESSES_TRANSACTIONS_HOOK,
177        );
178    }
179
180    /**
181     * Schedule a background job to generate new addresses.
182     */
183    public function schedule_generate_new_addresses(): void {
184        as_schedule_single_action(
185            timestamp: time(),
186            hook: Background_Jobs_Actions_Interface::GENERATE_NEW_ADDRESSES_HOOK,
187            unique: true
188        );
189        // TODO: check was it already scheduled.
190        $this->logger->info( 'New generate new addresses background job scheduled.' );
191    }
192
193    /**
194     * Schedule a background job to check newly generated addresses to see do they have existing transactions.
195     * We will use unused addresses for orders and then consider all transactions seen as related to that order.
196     *
197     * This is a background job so when we hit a rate limit we can re-run later to complete.
198     *
199     * @see self::CHECK_NEW_ADDRESSES_TRANSACTIONS_HOOK
200     *
201     * @param ?DateTimeInterface $datetime Optional time, e.g. 429 reset time, or defaults to immediately.
202     */
203    public function schedule_check_newly_generated_bitcoin_addresses_for_transactions(
204        ?DateTimeInterface $datetime = null,
205    ): void {
206        if ( as_has_scheduled_action( hook: Background_Jobs_Actions_Interface::CHECK_NEW_ADDRESSES_TRANSACTIONS_HOOK )
207            && ! doing_action( hook_name: Background_Jobs_Actions_Interface::CHECK_NEW_ADDRESSES_TRANSACTIONS_HOOK ) ) {
208            /** @see https://github.com/woocommerce/action-scheduler/issues/903 */
209
210            $this->logger->info(
211                message: 'Background_Jobs::schedule_check_new_addresses_for_transactions already scheduled.',
212            );
213
214            return;
215        }
216
217        $datetime ??= new DateTimeImmutable( 'now' );
218
219        as_schedule_single_action(
220            timestamp: $datetime->getTimestamp(),
221            hook: Background_Jobs_Actions_Interface::CHECK_NEW_ADDRESSES_TRANSACTIONS_HOOK,
222        );
223
224        $this->logger->info(
225            message: 'Background_Jobs::schedule_check_new_addresses_for_transactions scheduled job at {datetime}.',
226            context: array(
227                'datetime' => $datetime->format( 'Y-m-d H:i:s' ),
228            )
229        );
230    }
231}