Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
23.64% covered (danger)
23.64%
13 / 55
16.67% covered (danger)
16.67%
1 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
Bitcoin_Transaction_Repository
23.64% covered (danger)
23.64%
13 / 55
16.67% covered (danger)
16.67%
1 / 6
88.26
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_by_post_id
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_post_by_transaction_id
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 save_post
85.71% covered (warning)
85.71%
12 / 14
0.00% covered (danger)
0.00%
0 / 1
3.03
 save_new
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 associate_bitcoin_address_post_ids_to_transaction
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2/**
3 * Save new Bitcoin transactions in WordPress, and fetch them via xpub or post id.
4 *
5 * I had considered using taxonomies for the many-to-many relationship between Bitcoin_Addresses and Transactions
6 * but there's no real querying going on so post_meta on each end is probably adequate. This repository class will
7 * know about Bitcoin_Address postmeta that the Bitcoin address repository doesn't even know about!
8 *
9 * @package    brianhenryie/bh-wp-bitcoin-gateway
10 */
11
12namespace BrianHenryIE\WP_Bitcoin_Gateway\API\Repositories;
13
14use BrianHenryIE\WP_Bitcoin_Gateway\API\Model\Wallet\Bitcoin_Address;
15use BrianHenryIE\WP_Bitcoin_Gateway\API\Model\Payments\Bitcoin_Transaction;
16use BrianHenryIE\WP_Bitcoin_Gateway\API\Repositories\Factories\Bitcoin_Transaction_Factory;
17use BrianHenryIE\WP_Bitcoin_Gateway\API\Model\Payments\Bitcoin_Transaction_WP_Post_Interface;
18use BrianHenryIE\WP_Bitcoin_Gateway\API\Model\Exceptions\BH_WP_Bitcoin_Gateway_Exception;
19use BrianHenryIE\WP_Bitcoin_Gateway\API\Model\Payments\Transaction;
20use BrianHenryIE\WP_Bitcoin_Gateway\API\Repositories\Queries\Bitcoin_Transaction_Query;
21use BrianHenryIE\WP_Bitcoin_Gateway\WP_Includes\Post_BH_Bitcoin_Transaction;
22use RuntimeException;
23use WP_Post;
24
25/**
26 * Class for creating/getting `Bitcoin_Transaction` objects stored in wp_posts table.
27 *
28 * @see Bitcoin_Transaction_WP_Post_Interface
29 * @see Post_BH_Bitcoin_Transaction
30 *
31 * @phpstan-type WpUpdatePostArray array{ID?: int, post_author?: int, post_date?: string, post_date_gmt?: string, post_content?: string, post_content_filtered?: string, post_title?: string, post_excerpt?: string}
32 */
33class Bitcoin_Transaction_Repository extends WP_Post_Repository_Abstract {
34
35    /**
36     * Constructor.
37     *
38     * @param Bitcoin_Transaction_Factory $bitcoin_transaction_factory Factory for creating Bitcoin transaction objects.
39     */
40    public function __construct(
41        protected Bitcoin_Transaction_Factory $bitcoin_transaction_factory,
42    ) {
43    }
44
45    /**
46     * Given the id of the wp_posts row storing the bitcoin address, return the typed Bitcoin_Transaction object.
47     *
48     * @param int $post_id WordPress wp_posts ID.
49     *
50     * @throws BH_WP_Bitcoin_Gateway_Exception When the post_type of the post returned for the given post_id is not a Bitcoin_Transaction.
51     */
52    public function get_by_post_id( int $post_id ): Bitcoin_Transaction {
53        return $this->bitcoin_transaction_factory->get_by_wp_post_id( $post_id );
54    }
55
56    /**
57     * Get a WordPress post by transaction ID.
58     *
59     * @param string $tx_id The transaction ID to search for.
60     *
61     * @return WP_Post|null The WordPress post or null if not found.
62     * @throws RuntimeException When more than one post is unexpectedly found for the same transaction ID.
63     */
64    protected function get_post_by_transaction_id( string $tx_id ): ?WP_Post {
65
66        $query = new Bitcoin_Transaction_Query(
67            tx_id: $tx_id,
68        );
69
70        /** @var WP_Post[] $wp_posts */
71        $wp_posts = get_posts( $query->to_query_array() );
72
73        if ( empty( $wp_posts ) ) {
74            return null;
75        }
76
77        if ( count( $wp_posts ) === 1 ) {
78            return $wp_posts[0];
79        }
80
81        throw new RuntimeException( 'Unexpectedly found more than one post for txid: ' . $tx_id );
82    }
83
84    /**
85     * Save a transaction to WordPress posts table or return existing post.
86     *
87     * TODO: How to indicate if this was newly saved or already existed.
88     *
89     * @param Transaction       $transaction The blockchain transaction object to save or retrieve from WordPress posts.
90     * @param array<int,string> $bitcoin_addresses_indexed_by_post_ids Bitcoin addresses as post_id:bitcoin_address pairs.
91     *
92     * @throws RuntimeException When the transaction already exists in the database with a different post ID.
93     * @throws BH_WP_Bitcoin_Gateway_Exception When WordPress fails to create the new transaction post.
94     */
95    protected function save_post(
96        Transaction $transaction,
97        array $bitcoin_addresses_indexed_by_post_ids,
98    ): WP_Post {
99        $transaction_post = $this->get_post_by_transaction_id( $transaction->get_txid() );
100        // What if the transaction already exists? Potentially it is from a chain that has been discarded. When else might it be updated?
101        // (we will in a moment update a transaction's wp_post's meta to connect post ids for the relevant address).
102
103        if ( ! $transaction_post ) {
104            $insert_query = new Bitcoin_Transaction_Query(
105                transaction_object: $transaction,
106                block_height: $transaction->get_block_height(),
107                block_datetime: $transaction->get_block_time(),
108                updated_transaction_meta_bitcoin_address_post_ids: $bitcoin_addresses_indexed_by_post_ids,
109            );
110
111            /** @var WpUpdatePostArray $args */
112            $args = $insert_query->to_query_array();
113
114            $new_post_id = wp_insert_post( $args, true );
115
116            if ( is_wp_error( $new_post_id ) ) {
117                // TODO Log.
118                throw new BH_WP_Bitcoin_Gateway_Exception( 'WordPress failed to save new transaction.' );
119            }
120
121            return get_post( $new_post_id ); // @phpstan-ignore return.type
122        }
123
124        return $transaction_post;
125    }
126
127    /**
128     * Wrapper on wp_insert_post(), sets the address as the post_title and post_name.
129     *
130     * @param Transaction     $transaction A transaction from the blockchain API to save as a WordPress post.
131     * @param Bitcoin_Address $address The Bitcoin address that received funds in this transaction, used to create bidirectional links in post meta.
132     *
133     * @throws BH_WP_Bitcoin_Gateway_Exception When WordPress fails to create the transaction post or the address cannot be linked.
134     * @throws RuntimeException When multiple posts are found for the same transaction ID during the save operation.
135     */
136    public function save_new(
137        Transaction $transaction,
138        Bitcoin_Address $address,
139    ): Bitcoin_Transaction {
140
141        $transaction_post = $this->save_post(
142            transaction: $transaction,
143            bitcoin_addresses_indexed_by_post_ids: array(
144                $address->get_post_id() => $address->get_raw_address(),
145            )
146        );
147
148        // Using wp_post->ID here so it refreshes rather than just maps.
149        return $this->bitcoin_transaction_factory->get_by_wp_post_id( $transaction_post->ID );
150    }
151
152    /**
153     * Update transaction posts to reference a bitcoin address they are relevant to.
154     *
155     * @param Bitcoin_Address    $bitcoin_address The Bitcoin Address these transactions should be linked to.
156     * @param array<int, string> $transactions_post_ids Key/value: <wp_post_id, transaction_id>.
157     *
158     * @return void TODO: return something meaningful.
159     */
160    protected function associate_bitcoin_address_post_ids_to_transaction(
161        Bitcoin_Address $bitcoin_address,
162        array $transactions_post_ids,
163    ): void {
164
165        $bitcoin_address_post_id     = $bitcoin_address->get_post_id();
166        $bitcoin_address_raw_address = $bitcoin_address->get_raw_address();
167        unset( $bitcoin_address );
168
169        foreach ( $transactions_post_ids as $transaction_post_id => $transaction_id ) {
170
171            /** @var array<int,string> $existing_transaction_meta_bitcoin_address_post_ids */
172            $existing_transaction_meta_bitcoin_address_post_ids = get_post_meta(
173                post_id: $transaction_post_id,
174                key: Bitcoin_Transaction_WP_Post_Interface::BITCOIN_ADDRESSES_POST_IDS_META_KEY,
175                single: true
176            );
177
178            if ( empty( $existing_transaction_meta_bitcoin_address_post_ids ) ) {
179                $existing_transaction_meta_bitcoin_address_post_ids = array();
180            }
181
182            $updated_transaction_meta_bitcoin_address_post_ids = $existing_transaction_meta_bitcoin_address_post_ids;
183            $new_transaction_meta_bitcoin_address_post_ids     = array();
184
185            if ( ! isset( $existing_transaction_meta_bitcoin_address_post_ids[ $bitcoin_address_post_id ] ) ) {
186
187                $updated_transaction_meta_bitcoin_address_post_ids[ $bitcoin_address_post_id ] = $bitcoin_address_raw_address;
188                $new_transaction_meta_bitcoin_address_post_ids[ $bitcoin_address_post_id ]     = $bitcoin_address_raw_address;
189
190                $transaction = $this->bitcoin_transaction_factory->get_by_wp_post_id( $transaction_post_id );
191
192                // TODO: How best to handle errors in the loop & return all the results.
193                $this->update(
194                    model: $transaction,
195                    query:  new Bitcoin_Transaction_Query(
196                        updated_transaction_meta_bitcoin_address_post_ids: $updated_transaction_meta_bitcoin_address_post_ids
197                    )
198                );
199            }
200        }
201    }
202}