diff --git a/src/Campaigns/CampaignDonationQuery.php b/src/Campaigns/CampaignDonationQuery.php new file mode 100644 index 0000000000..3399ec2b2c --- /dev/null +++ b/src/Campaigns/CampaignDonationQuery.php @@ -0,0 +1,92 @@ +from('posts', 'donation'); + $this->where('post_type', 'give_payment'); + + // Include only valid statuses + $this->whereIn('donation.post_status', ['publish', 'give_subscription']); + + // Include only current payment "mode" + $this->joinDonationMeta(DonationMetaKeys::MODE, 'paymentMode'); + $this->where('paymentMode.meta_value', give_is_test_mode() ? 'test' : 'live'); + + // Include only forms associated with the Campaign. + $this->joinDonationMeta(DonationMetaKeys::FORM_ID, 'formId'); + $this->join(function (JoinQueryBuilder $builder) { + $builder->leftJoin('give_campaign_forms', 'campaign_forms') + ->on('campaign_forms.form_id', 'formId.meta_value'); + }); + $this->where('campaign_forms.campaign_id', $campaign->id); + } + + /** + * Returns a calculated sum of the intended amounts (without recovered fees) for the donations. + * + * @unreleased + * + * @return int|float + */ + public function sumIntendedAmount() + { + $this->joinDonationMeta(DonationMetaKeys::AMOUNT, 'amount'); + $this->joinDonationMeta('_give_fee_donation_amount', 'intendedAmount'); + return $this->sum( + /** + * The intended amount meta and the amount meta could either be 0 or NULL. + * So we need to use the NULLIF function to treat the 0 values as NULL. + * Then we coalesce the values to select the first non-NULL value. + * @link https://github.com/impress-org/givewp/pull/7411 + */ + 'COALESCE(NULLIF(intendedAmount.meta_value,0), NULLIF(amount.meta_value,0), 0)' + ); + } + + /** + * @unreleased + */ + public function countDonations(): int + { + return $this->count('donation.ID'); + } + + /** + * @unreleased + */ + public function countDonors(): int + { + $this->joinDonationMeta(DonationMetaKeys::DONOR_ID, 'donorId'); + return $this->count('DISTINCT donorId.meta_value'); + } + + /** + * An opinionated join method for the donation meta table. + * @unreleased + */ + protected function joinDonationMeta($key, $alias): self + { + $this->join(function (JoinQueryBuilder $builder) use ($key, $alias) { + $builder + ->leftJoin('give_donationmeta', $alias) + ->on('donation.ID', $alias . '.donation_id') + ->andOn($alias . '.meta_key', $key, true); + }); + return $this; + } +} diff --git a/tests/Unit/Campaigns/CampaignDonationQueryTest.php b/tests/Unit/Campaigns/CampaignDonationQueryTest.php new file mode 100644 index 0000000000..d8831b5348 --- /dev/null +++ b/tests/Unit/Campaigns/CampaignDonationQueryTest.php @@ -0,0 +1,119 @@ +create(); + $form = DonationForm::factory()->create(); + Donation::factory()->create([ + 'formId' => $form->id, + 'status' => DonationStatus::COMPLETE(), + 'amount' => new Money(1000, 'USD'), + ]); + Donation::factory()->create([ + 'formId' => $form->id, + 'status' => DonationStatus::COMPLETE(), + 'amount' => new Money(1000, 'USD'), + ]); + + $db = DB::table('give_campaign_forms'); + $db->insert(['form_id' => $form->id, 'campaign_id' => $campaign->id]); + + $query = new CampaignDonationQuery($campaign); + + $this->assertEquals(2, $query->countDonations()); + } + + /** + * @unreleased + */ + public function testSumCampaignDonations() + { + $campaign = Campaign::factory()->create(); + $form = DonationForm::factory()->create(); + Donation::factory()->create([ + 'formId' => $form->id, + 'status' => DonationStatus::COMPLETE(), + 'amount' => new Money(1000, 'USD'), + ]); + Donation::factory()->create([ + 'formId' => $form->id, + 'status' => DonationStatus::COMPLETE(), + 'amount' => new Money(1000, 'USD'), + ]); + + $db = DB::table('give_campaign_forms'); + $db->insert(['form_id' => $form->id, 'campaign_id' => $campaign->id]); + + $query = new CampaignDonationQuery($campaign); + + $this->assertEquals(20.00, $query->sumIntendedAmount()); + } + + /** + * @unreleased + */ + public function testCountCampaignDonors() + { + $campaign = Campaign::factory()->create(); + $form = DonationForm::factory()->create(); + Donation::factory()->create([ + 'formId' => $form->id, + 'status' => DonationStatus::COMPLETE(), + 'amount' => new Money(1000, 'USD'), + ]); + Donation::factory()->create([ + 'formId' => $form->id, + 'status' => DonationStatus::COMPLETE(), + 'amount' => new Money(1000, 'USD'), + ]); + + $db = DB::table('give_campaign_forms'); + $db->insert(['form_id' => $form->id, 'campaign_id' => $campaign->id]); + + $query = new CampaignDonationQuery($campaign); + + $this->assertEquals(2, $query->countDonors()); + } + + public function testCoalesceIntendedAmountWithoutRecoveredFees() + { + $campaign = Campaign::factory()->create(); + $form = DonationForm::factory()->create(); + + $db = DB::table('give_campaign_forms'); + $db->insert(['form_id' => $form->id, 'campaign_id' => $campaign->id]); + + $donation = Donation::factory()->create([ + 'formId' => $form->id, + 'status' => DonationStatus::COMPLETE(), + 'amount' => new Money(1070, 'USD'), + ]); + give_update_meta($donation->id, '_give_fee_donation_amount', 10.00); + + $query = new CampaignDonationQuery($campaign); + + $this->assertEquals(10.00, $query->sumIntendedAmount()); + } +}