diff --git a/src/Campaigns/CampaignDonationQuery.php b/src/Campaigns/CampaignDonationQuery.php index 3a65197b4f..7db4161b5e 100644 --- a/src/Campaigns/CampaignDonationQuery.php +++ b/src/Campaigns/CampaignDonationQuery.php @@ -94,6 +94,26 @@ public function countDonors(): int return $query->count('DISTINCT donorId.meta_value'); } + /** + * @unreleased + */ + public function getDonationsByDay(): array + { + $query = clone $this; + + $query->joinDonationMeta(DonationMetaKeys::AMOUNT, 'amount'); + $query->joinDonationMeta('_give_fee_donation_amount', 'intendedAmount'); + $query->select( + 'SUM(COALESCE(NULLIF(intendedAmount.meta_value,0), NULLIF(amount.meta_value,0), 0)) as amount' + ); + + $query->joinDonationMeta('_give_completed_date', 'completed'); + $query->select('DATE(completed.meta_value) as date'); + $query->groupBy('date'); + + return $query->getAll(); + } + /** * An opinionated join method for the donation meta table. * @unreleased diff --git a/src/Campaigns/DataTransferObjects/CampaignGoalData.php b/src/Campaigns/DataTransferObjects/CampaignGoalData.php index da845dfd8a..780de8c704 100644 --- a/src/Campaigns/DataTransferObjects/CampaignGoalData.php +++ b/src/Campaigns/DataTransferObjects/CampaignGoalData.php @@ -80,7 +80,10 @@ private function getActual(): int */ private function getPercentage(): float { - return round($this->actual / $this->campaign->goal * 100, 2); + $percentage = $this->campaign->goal + ? $this->actual / $this->campaign->goal + : 0; + return round($percentage * 100, 2); } /** diff --git a/src/Campaigns/Routes/GetCampaignRevenue.php b/src/Campaigns/Routes/GetCampaignRevenue.php new file mode 100644 index 0000000000..d21e5c9b9c --- /dev/null +++ b/src/Campaigns/Routes/GetCampaignRevenue.php @@ -0,0 +1,96 @@ + WP_REST_Server::READABLE, + 'callback' => [$this, 'handleRequest'], + 'permission_callback' => function () { + return current_user_can('manage_options'); + }, + ], + 'args' => [ + 'id' => [ + 'type' => 'integer', + 'required' => true, + 'sanitize_callback' => 'absint', + ], + ], + ] + ); + } + + /** + * @unreleased + * + * @throws Exception + */ + public function handleRequest($request): WP_REST_Response + { + + $campaign = Campaign::find($request->get_param('id')); + + $dates = $this->getDatesFromRange(new DateTime('-7 days'), new DateTime()); + + $query = new CampaignDonationQuery($campaign); + $query->between(new DateTime('-7 days'), new DateTime()); + $results = $query->getDonationsByDay(); + + foreach($results as $result) { + $dates[$result->date] = $result->amount; + } + + $data = []; + foreach($dates as $date => $amount) { + $data[] = [ + 'date' => $date, + 'amount' => $amount, + ]; + } + + return new WP_REST_Response($data, 200); + } + + public function getDatesFromRange(DateTimeInterface $startDate, DateTimeInterface $endDate): array + { + $period = new DatePeriod( + $startDate, + new DateInterval('P1D'), + $endDate + ); + + $dates = array_map(function($date) { + return $date->format('Y-m-d'); + }, iterator_to_array($period)); + + return array_fill_keys($dates, 0); + } +} diff --git a/src/Campaigns/ServiceProvider.php b/src/Campaigns/ServiceProvider.php index e46e6baa92..012be96503 100644 --- a/src/Campaigns/ServiceProvider.php +++ b/src/Campaigns/ServiceProvider.php @@ -56,6 +56,7 @@ private function registerRoutes() Hooks::addAction('rest_api_init', Routes\GetCampaignsListTable::class, 'registerRoute'); Hooks::addAction('rest_api_init', Routes\DeleteCampaignListTable::class, 'registerRoute'); Hooks::addAction('rest_api_init', Routes\GetCampaignStatistics::class, 'registerRoute'); + Hooks::addAction('rest_api_init', Routes\GetCampaignRevenue::class, 'registerRoute'); } /** @@ -138,7 +139,7 @@ private function setupCampaignForms() if ( ! defined('GIVE_IS_ALL_STATS_COLUMNS_ASYNC_ON_ADMIN_FORM_LIST_VIEWS')) { define('GIVE_IS_ALL_STATS_COLUMNS_ASYNC_ON_ADMIN_FORM_LIST_VIEWS', false); } - + Hooks::addAction('save_post_give_forms', AddCampaignFormFromRequest::class, 'optionBasedFormEditor', 10, 3); Hooks::addAction('givewp_donation_form_created', AddCampaignFormFromRequest::class, 'visualFormBuilder'); Hooks::addAction('givewp_campaign_created', CreateDefaultCampaignForm::class); diff --git a/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/RevenueChart.tsx b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/RevenueChart.tsx index cb3ff2820d..5beed0ab01 100644 --- a/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/RevenueChart.tsx +++ b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/RevenueChart.tsx @@ -1,8 +1,31 @@ -import React from "react"; +import React, {useEffect, useState} from "react"; import Chart from "react-apexcharts"; +import apiFetch from "@wordpress/api-fetch"; +import {addQueryArgs} from "@wordpress/url"; + +const campaignId = new URLSearchParams(window.location.search).get('id'); const RevenueChart = () => { + const [max, setMax] = useState(0); + const [categories, setCategories] = useState([]); + const [series, setSeries] = useState([{name: "Revenue", data: []}]); + + useEffect(() => { + apiFetch({path: addQueryArgs( '/give-api/v2/campaigns/' + campaignId +'/revenue' ) } ) + .then((data: {date: string, amount: number}[]) => { + + setMax(Math.max(...data.map(item => item.amount)) * 1.1) + + setCategories(data.map(item => item.date)) + + setSeries([{ + name: "Revenue", + data: data.map(item => item.amount) + }]) + }); + }, []) + const options = { chart: { id: "campaign-revenue", @@ -11,10 +34,11 @@ const RevenueChart = () => { }, }, xaxis: { - categories: ['Aug 06', 'Aug 07', 'Aug 08', 'Aug 09'] + categories, + type: 'datetime' as "datetime" | "category" | "numeric", }, yaxis: { - max: 200, + max, }, stroke: { color: ['#60a1e2'], @@ -51,13 +75,6 @@ const RevenueChart = () => { } }; - const series = [ - { - name: "Revenue", - data: [0, 100, 50, 150] - } - ]; - return ( <> assertEquals(2, $query->countDonors()); } + /** + * @unreleased + */ public function testCoalesceIntendedAmountWithoutRecoveredFees() { $campaign = Campaign::factory()->create(); @@ -116,4 +120,48 @@ public function testCoalesceIntendedAmountWithoutRecoveredFees() $this->assertEquals(10.00, $query->sumIntendedAmount()); } + + /** + * @unreleased + */ + public function testGetDonationsByDate() + { + $campaign = Campaign::factory()->create(); + $form = DonationForm::factory()->create(); + + $db = DB::table('give_campaign_forms'); + $db->insert(['form_id' => $form->id, 'campaign_id' => $campaign->id]); + + $donations = [ + Donation::factory()->create([ + 'formId' => $form->id, + 'status' => DonationStatus::COMPLETE(), + 'amount' => new Money(1000, 'USD'), + 'createdAt' => new DateTime('2021-01-01 00:00:00'), + ]), + Donation::factory()->create([ + 'formId' => $form->id, + 'status' => DonationStatus::COMPLETE(), + 'amount' => new Money(1000, 'USD'), + 'createdAt' => new DateTime('2021-01-02 00:00:00'), + ]), + Donation::factory()->create([ + 'formId' => $form->id, + 'status' => DonationStatus::COMPLETE(), + 'amount' => new Money(1000, 'USD'), + 'createdAt' => new DateTime('2021-01-02 00:00:00'), + ]), + ]; + + foreach($donations as $donation) { + give_update_meta($donation->id, '_give_completed_date', $donation->createdAt->format('Y-m-d H:i:s')); + } + + $query = new CampaignDonationQuery($campaign); + + $this->assertEquals([ + (object) ['date' => '2021-01-01', 'amount' => 10.00], + (object) ['date' => '2021-01-02', 'amount' => 20.00], + ], $query->getDonationsByDay()); + } } diff --git a/tests/Unit/Campaigns/DataTransferObjects/CampaignGoalDataTest.php b/tests/Unit/Campaigns/DataTransferObjects/CampaignGoalDataTest.php new file mode 100644 index 0000000000..347787071d --- /dev/null +++ b/tests/Unit/Campaigns/DataTransferObjects/CampaignGoalDataTest.php @@ -0,0 +1,39 @@ +create(['goal' => 0]); + + $form = DonationForm::factory()->create(); + DB::table('give_campaign_forms') + ->insert(['form_id' => $form->id, 'campaign_id' => $campaign->id]); + + Donation::factory()->create(['formId' => $form->id]); + + $goalData = new CampaignGoalData($campaign); + + $this->assertEquals(0.00, $goalData->percentage); + } +}