Skip to content

Commit 517be96

Browse files
author
Jon Waldstein
committed
refactor: update schema for campaign revenue and stats endpoints
1 parent 0312449 commit 517be96

File tree

4 files changed

+217
-33
lines changed

4 files changed

+217
-33
lines changed

src/API/REST/V3/Routes/Campaigns/GetCampaignRevenue.php

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -203,36 +203,36 @@ public function mapResultsByDate(array $results, string $groupBy): array
203203
foreach ($results as $result) {
204204
$date = $this->getFormattedDateFromGroupBy($groupBy, date_create($result->date_created));
205205

206-
$resultMap[$date] = $result->amount;
206+
$resultMap[$date] = (float)$result->amount;
207207
}
208208

209209
return $resultMap;
210210
}
211211

212212
/**
213+
* @unreleased update schema to match actual response
213214
* @since 4.10.0
214215
*/
215216
public function getSchema(): array
216217
{
217218
return [
218219
'title' => 'campaign-revenue',
219220
'description' => esc_html__('Provides daily revenue data for a specific campaign.', 'give'),
220-
'type' => 'object',
221-
'properties' => [
222-
'date' => [
223-
'type' => 'string',
224-
'format' => 'date',
225-
'description' => esc_html__('The date for the revenue entry (YYYY-MM-DD).', 'give'),
226-
'required' => true
227-
],
228-
'amount' => [
229-
'oneOf' => [
230-
[ 'type' => 'number' ],
231-
[ 'type' => 'string' ],
221+
'type' => 'array',
222+
'readonly' => true,
223+
'items' => [
224+
'type' => 'object',
225+
'properties' => [
226+
'date' => [
227+
'type' => 'string',
228+
'format' => 'date',
229+
'description' => esc_html__('The date for the revenue entry (YYYY-MM-DD).', 'give'),
232230
],
233-
'description' => esc_html__('The amount of revenue received on the given date.', 'give'),
234-
'required' => true
235-
],
231+
'amount' => [
232+
'type' => ['integer', 'number'],
233+
'description' => esc_html__('The amount of revenue received on the given date.', 'give'),
234+
],
235+
]
236236
]
237237
];
238238
}

src/API/REST/V3/Routes/Campaigns/GetCampaignStatistics.php

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -107,16 +107,23 @@ public function getSchema(): array
107107
return [
108108
'title' => 'givewp/campaign-statistics',
109109
'description' => esc_html__('Provides statistics for a specific campaign.', 'give'),
110-
'type' => 'object',
111-
'properties' => [
112-
'id' => [
113-
'type' => 'integer',
114-
'description' => esc_html__('The Campaign ID.', 'give'),
115-
'required' => true,
116-
],
117-
'rangeInDays' => [
118-
'type' => 'integer',
119-
'description' => esc_html__('The date range in days.', 'give'),
110+
'type' => 'array',
111+
'readonly' => true,
112+
'items' => [
113+
'type' => 'object',
114+
'properties' => [
115+
'amountRaised' => [
116+
'type' => ['integer', 'number'],
117+
'description' => esc_html__('The amount raised for the campaign.', 'give'),
118+
],
119+
'donationCount' => [
120+
'type' => 'integer',
121+
'description' => esc_html__('The number of donations for the campaign.', 'give'),
122+
],
123+
'donorCount' => [
124+
'type' => 'integer',
125+
'description' => esc_html__('The number of donors for the campaign.', 'give'),
126+
],
120127
],
121128
],
122129
];

tests/Unit/API/REST/V3/Routes/Campaigns/CampaignSchemaTest.php

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,21 @@
22

33
namespace Give\Tests\Unit\API\REST\V3\Routes\Campaigns;
44

5+
use DateTime;
56
use Exception;
67
use Give\API\REST\V3\Routes\Campaigns\ValueObjects\CampaignRoute;
78
use Give\Campaigns\Models\Campaign;
89
use Give\Donations\Models\Donation;
10+
use Give\Donations\ValueObjects\DonationStatus;
911
use WP_REST_Server;
1012
use Give\Tests\RestApiTestCase;
1113
use Give\Tests\TestTraits\RefreshDatabase;
1214
use Give\Tests\TestTraits\HasDefaultWordPressUsers;
1315
use Give\Tests\Unit\API\REST\V3\SchemaValidationTrait;
1416

17+
/**
18+
* @unreleased
19+
*/
1520
class CampaignSchemaTest extends RestApiTestCase
1621
{
1722
use RefreshDatabase;
@@ -52,4 +57,111 @@ public function testCampaignSchemaShouldMatchActualResponse()
5257
// Validate enum values match schema
5358
$this->validateEnumValues($schema, $actualData);
5459
}
60+
61+
/**
62+
* @unreleased
63+
*/
64+
public function testCampaignCollectionSchemaShouldMatchActualResponse()
65+
{
66+
/** @var Campaign[] $campaigns */
67+
$campaign = Campaign::factory()->count(3)->create();
68+
69+
// Get the schema via OPTIONS request
70+
$schemaRoute = '/' . CampaignRoute::NAMESPACE . '/' . CampaignRoute::CAMPAIGNS;
71+
$schemaRequest = $this->createRequest('OPTIONS', $schemaRoute, [], 'administrator');
72+
$schemaResponse = $this->dispatchRequest($schemaRequest);
73+
$schemaJson = json_encode($schemaResponse->get_data());
74+
$schema = json_decode($schemaJson, true);
75+
76+
// Get actual collection data
77+
$dataRoute = '/' . CampaignRoute::NAMESPACE . '/' . CampaignRoute::CAMPAIGNS;
78+
$dataRequest = $this->createRequest(WP_REST_Server::READABLE, $dataRoute, [], 'administrator');
79+
$dataResponse = $this->dispatchRequest($dataRequest);
80+
$actualDataJson = json_encode($dataResponse->get_data());
81+
$actualData = json_decode($actualDataJson, true);
82+
83+
// Assert that we have data in the collection
84+
$this->assertNotEmpty($actualData, 'Collection should contain at least one campaign');
85+
$this->assertIsArray($actualData, 'Collection should be an array');
86+
87+
// Validate first item in collection
88+
if (!empty($actualData)) {
89+
$firstItem = $actualData[0];
90+
$this->validateSchemaProperties($schema, $firstItem);
91+
$this->validateDataTypes($schema, $firstItem);
92+
$this->validateEnumValues($schema, $firstItem);
93+
}
94+
}
95+
96+
/**
97+
* @unreleased
98+
*/
99+
public function testCampaignStatisticsSchemaShouldMatchActualResponse()
100+
{
101+
$campaign = Campaign::factory()->create();
102+
$donations = Donation::factory()->count(3)->create([
103+
'campaignId' => $campaign->id,
104+
'formId' => $campaign->defaultFormId,
105+
'status' => DonationStatus::COMPLETE(),
106+
]);
107+
108+
// Get the schema via OPTIONS request
109+
$schemaRoute = '/' . CampaignRoute::NAMESPACE . '/' . CampaignRoute::CAMPAIGNS . '/' . $campaign->id . '/statistics';
110+
$schemaRequest = $this->createRequest('OPTIONS', $schemaRoute, [], 'administrator');
111+
$schemaResponse = $this->dispatchRequest($schemaRequest);
112+
$schemaJson = json_encode($schemaResponse->get_data());
113+
$schema = json_decode($schemaJson, true);
114+
115+
// Get actual campaign statistics data
116+
$dataRoute = '/' . CampaignRoute::NAMESPACE . '/' . CampaignRoute::CAMPAIGNS . '/' . $campaign->id . '/statistics';
117+
$dataRequest = $this->createRequest(WP_REST_Server::READABLE, $dataRoute, [], 'administrator');
118+
$dataResponse = $this->dispatchRequest($dataRequest);
119+
$actualDataJson = json_encode($dataResponse->get_data());
120+
$actualData = json_decode($actualDataJson, true);
121+
122+
// Validate that all required schema properties exist in actual response
123+
$this->validateSchemaProperties($schema, $actualData);
124+
125+
// Validate data types match schema
126+
$this->validateDataTypes($schema, $actualData);
127+
128+
// Validate enum values match schema
129+
$this->validateEnumValues($schema, $actualData);
130+
}
131+
132+
/**
133+
* @unreleased
134+
*/
135+
public function testCampaignRevenueSchemaShouldMatchActualResponse()
136+
{
137+
$campaign = Campaign::factory()->create();
138+
$donations = Donation::factory()->count(3)->create([
139+
'campaignId' => $campaign->id,
140+
'formId' => $campaign->defaultFormId,
141+
'status' => DonationStatus::COMPLETE(),
142+
]);
143+
144+
// Get the schema via OPTIONS request
145+
$schemaRoute = '/' . CampaignRoute::NAMESPACE . '/' . CampaignRoute::CAMPAIGNS . '/' . $campaign->id . '/revenue';
146+
$schemaRequest = $this->createRequest('OPTIONS', $schemaRoute, [], 'administrator');
147+
$schemaResponse = $this->dispatchRequest($schemaRequest);
148+
$schemaJson = json_encode($schemaResponse->get_data());
149+
$schema = json_decode($schemaJson, true);
150+
151+
// Get actual campaign revenue data
152+
$dataRoute = '/' . CampaignRoute::NAMESPACE . '/' . CampaignRoute::CAMPAIGNS . '/' . $campaign->id . '/revenue';
153+
$dataRequest = $this->createRequest(WP_REST_Server::READABLE, $dataRoute, [], 'administrator');
154+
$dataResponse = $this->dispatchRequest($dataRequest);
155+
$actualDataJson = json_encode($dataResponse->get_data());
156+
$actualData = json_decode($actualDataJson, true);
157+
158+
// Validate that all required schema properties exist in actual response
159+
$this->validateSchemaProperties($schema, $actualData);
160+
161+
// Validate data types match schema
162+
$this->validateDataTypes($schema, $actualData);
163+
164+
// Validate enum values match schema
165+
$this->validateEnumValues($schema, $actualData);
166+
}
55167
}

tests/Unit/API/REST/V3/SchemaValidationTrait.php

Lines changed: 72 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,65 @@
88
trait SchemaValidationTrait
99
{
1010
/**
11+
* Resolve the effective schema node for validation.
12+
* Accepts an OPTIONS response wrapper or a raw schema fragment.
13+
* If the schema describes an array of objects, returns the item schema.
14+
*
15+
* @unreleased
16+
*/
17+
private function resolveSchemaNode(array $schema): array
18+
{
19+
$node = isset($schema['schema']) ? $schema['schema'] : $schema;
20+
21+
if (isset($node['type']) && $node['type'] === 'array' && isset($node['items']) && is_array($node['items'])) {
22+
$node = $node['items'];
23+
}
24+
25+
return $node;
26+
}
27+
28+
/**
29+
* Get properties from the resolved schema node.
30+
*
31+
* @unreleased
32+
*/
33+
private function getSchemaProperties(array $schema): array
34+
{
35+
$node = $this->resolveSchemaNode($schema);
36+
return $node['properties'] ?? [];
37+
}
38+
39+
/**
40+
* Get required properties from the resolved schema node.
41+
*
42+
* @unreleased
43+
*/
44+
private function getRequiredProperties(array $schema): array
45+
{
46+
$node = $this->resolveSchemaNode($schema);
47+
return $node['required'] ?? [];
48+
}
49+
50+
/**
51+
* @unreleased Validate schema properties for both objects and arrays.
1152
* @since 4.9.0
1253
*/
1354
private function validateSchemaProperties($schema, $actualData)
1455
{
15-
if (!isset($schema['schema']['properties'])) {
16-
$this->fail('Schema does not contain properties');
56+
// If the response is a list (indexed array), validate each item against the same schema
57+
if ($this->getActualType($actualData) === 'array') {
58+
foreach ($actualData as $item) {
59+
$this->validateSchemaProperties($schema, $item);
60+
}
61+
return;
62+
}
63+
64+
$schemaProperties = $this->getSchemaProperties($schema);
65+
if (empty($schemaProperties)) {
66+
$this->fail('Schema does not contain properties for object validation');
1767
}
1868

19-
$schemaProperties = $schema['schema']['properties'];
20-
$requiredProperties = $schema['schema']['required'] ?? [];
69+
$requiredProperties = $this->getRequiredProperties($schema);
2170

2271
// Check that all required properties exist
2372
foreach ($requiredProperties as $requiredProperty) {
@@ -44,12 +93,20 @@ private function validateSchemaProperties($schema, $actualData)
4493
}
4594

4695
/**
47-
* @since 4.11.0 Add support for nullable property.
96+
* @unreleased Validate data types for both objects and arrays, with support for nullable properties.
4897
* @since 4.9.0
4998
*/
5099
private function validateDataTypes($schema, $actualData)
51100
{
52-
$schemaProperties = $schema['schema']['properties'];
101+
// If the response is a list (indexed array), validate each item against the same schema
102+
if ($this->getActualType($actualData) === 'array') {
103+
foreach ($actualData as $item) {
104+
$this->validateDataTypes($schema, $item);
105+
}
106+
return;
107+
}
108+
109+
$schemaProperties = $this->getSchemaProperties($schema);
53110

54111
foreach ($actualData as $property => $value) {
55112
if (!isset($schemaProperties[$property])) {
@@ -94,7 +151,15 @@ private function validateDataTypes($schema, $actualData)
94151
*/
95152
private function validateEnumValues($schema, $actualData)
96153
{
97-
$schemaProperties = $schema['schema']['properties'];
154+
// If the response is a list (indexed array), validate each item against the same schema
155+
if ($this->getActualType($actualData) === 'array') {
156+
foreach ($actualData as $item) {
157+
$this->validateEnumValues($schema, $item);
158+
}
159+
return;
160+
}
161+
162+
$schemaProperties = $this->getSchemaProperties($schema);
98163

99164
foreach ($actualData as $property => $value) {
100165
if (!isset($schemaProperties[$property]['enum'])) {

0 commit comments

Comments
 (0)