| 1 |
<?php |
| 2 |
/** |
| 3 |
* Shared (donation_post_id, source_campaign_id) dedupe for the Charitable |
| 4 |
* import paths. |
| 5 |
* |
| 6 |
* Both the direct-DB (Donation_Mapper) and CSV (Csv_Parser) paths import the |
| 7 |
* same gift keyed on the (Charitable donation post, source campaign) pair, |
| 8 |
* which both writers store in the indexed `import_provenance` column. Keeping |
| 9 |
* the dedupe in one trait means the two paths can never drift apart on the key, |
| 10 |
* the recurring guard, or the unknown-campaign (0) handling. |
| 11 |
* |
| 12 |
* Per-row dedupe is a single indexed lookup on |
| 13 |
* idx_import_provenance (import_source, import_provenance) — not a scan + |
| 14 |
* JSON-decode of every prior imported row. Each import batch is a separate HTTP |
| 15 |
* request (fresh process), so there is no per-request set to preload; the |
| 16 |
* in-request $seen cache only short-circuits rows this same request already |
| 17 |
* wrote (a multi-campaign donation's repeated rows, or a re-uploaded CSV row). |
| 18 |
* |
| 19 |
* @package SureDonation |
| 20 |
* @since 1.5.1 |
| 21 |
*/ |
| 22 |
|
| 23 |
namespace SureDonation\Inc\Import\Charitable; |
| 24 |
|
| 25 |
use SureDonation\Inc\Database\Tables\Donations; |
| 26 |
|
| 27 |
// Exit if accessed directly. |
| 28 |
defined( 'ABSPATH' ) || exit; |
| 29 |
|
| 30 |
/** |
| 31 |
* Provenance_Dedupe trait. |
| 32 |
* |
| 33 |
* @since 1.5.1 |
| 34 |
*/ |
| 35 |
trait Provenance_Dedupe { |
| 36 |
|
| 37 |
/** |
| 38 |
* Keys ("<post>:<token>") written earlier in *this* request, so a repeated |
| 39 |
* row skips without a second DB round-trip. Not a full prior-import set — |
| 40 |
* that lives in the indexed column and is queried per row. |
| 41 |
* |
| 42 |
* @var array<string, bool> |
| 43 |
* @since 1.5.1 |
| 44 |
*/ |
| 45 |
protected $seen = []; |
| 46 |
|
| 47 |
/** |
| 48 |
* Stable dedupe key for a (donation post, source campaign) pair. |
| 49 |
* |
| 50 |
* Delegates to Donations::build_provenance_key() so the importer and the v6 |
| 51 |
* backfill derive byte-identical keys. The label is the blank-campaign-id |
| 52 |
* fallback (a CSV export that left the Campaign ID cell empty): keying on the |
| 53 |
* campaign title keeps the per-campaign rows of a multi-campaign donation |
| 54 |
* distinct instead of collapsing to "<post>:0" and being deduped away. |
| 55 |
* |
| 56 |
* @param int $donation_post_id Charitable donation post ID. |
| 57 |
* @param int $source_campaign_id Charitable campaign ID (0 when unknown). |
| 58 |
* @param string $campaign_label Campaign title/name fallback (optional). |
| 59 |
* @return string |
| 60 |
* @since 1.5.1 |
| 61 |
*/ |
| 62 |
protected function provenance_key( $donation_post_id, $source_campaign_id, $campaign_label = '' ) { |
| 63 |
return Donations::build_provenance_key( $donation_post_id, $source_campaign_id, $campaign_label ); |
| 64 |
} |
| 65 |
|
| 66 |
/** |
| 67 |
* Whether a (donation post, source campaign) pair has already been imported |
| 68 |
* — earlier in this request, or by any prior import (indexed column lookup). |
| 69 |
* |
| 70 |
* Restricted to one-time rows: a Pro subscription plan-post row (donation_type |
| 71 |
* = 'recurring') never masks a one-time donation that shares a post id. |
| 72 |
* |
| 73 |
* @param int $donation_post_id Charitable donation post ID. |
| 74 |
* @param int $source_campaign_id Charitable campaign ID (0 when unknown). |
| 75 |
* @param string $campaign_label Campaign title/name fallback (optional). |
| 76 |
* @return bool |
| 77 |
* @since 1.5.1 |
| 78 |
*/ |
| 79 |
protected function provenance_seen( $donation_post_id, $source_campaign_id, $campaign_label = '' ) { |
| 80 |
$key = $this->provenance_key( $donation_post_id, $source_campaign_id, $campaign_label ); |
| 81 |
if ( isset( $this->seen[ $key ] ) ) { |
| 82 |
return true; |
| 83 |
} |
| 84 |
|
| 85 |
global $wpdb; |
| 86 |
$table = $wpdb->prefix . 'suredonation_donations'; |
| 87 |
|
| 88 |
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Indexed per-row dedupe lookup during a migration; not cacheable. |
| 89 |
$found = $wpdb->get_var( |
| 90 |
$wpdb->prepare( |
| 91 |
'SELECT id FROM %i WHERE import_source = %s AND import_provenance = %s AND donation_type != %s LIMIT 1', |
| 92 |
$table, |
| 93 |
'charitable', |
| 94 |
$key, |
| 95 |
'recurring' |
| 96 |
) |
| 97 |
); |
| 98 |
|
| 99 |
return null !== $found; |
| 100 |
} |
| 101 |
|
| 102 |
/** |
| 103 |
* Record a (donation post, source campaign) pair as written this request so |
| 104 |
* a later row in the same request skips it. |
| 105 |
* |
| 106 |
* @param int $donation_post_id Charitable donation post ID. |
| 107 |
* @param int $source_campaign_id Charitable campaign ID (0 when unknown). |
| 108 |
* @param string $campaign_label Campaign title/name fallback (optional). |
| 109 |
* @return void |
| 110 |
* @since 1.5.1 |
| 111 |
*/ |
| 112 |
protected function mark_provenance_seen( $donation_post_id, $source_campaign_id, $campaign_label = '' ) { |
| 113 |
$this->seen[ $this->provenance_key( $donation_post_id, $source_campaign_id, $campaign_label ) ] = true; |
| 114 |
} |
| 115 |
} |
| 116 |
|