Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .fern/metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@
"inline-path-parameters": true,
"use-discriminated-unions": true
},
"sdkVersion": "44.0.0"
"sdkVersion": "44.1.0-rc.0"
}
2 changes: 2 additions & 0 deletions .fernignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ SquareWithLegacy.sln
src/Square.sln
src/Square/Square.Custom.props
src/Square/WebhooksHelper.cs
src/Square/Reporting/ReportingHelper.cs
src/Square/Core/Public/SquareApiException.cs
src/Square.Test/WebhooksHelperTests.cs
src/Square.Test/Unit/SquareErrorTest.cs
src/Square.Test/Unit/WebhooksHelperTests.cs
src/Square.Test/Unit/ReportingHelperTests.cs
src/Square.Test/Unit/MockServer/BatchDeleteTest.cs
src/Square.Test/Integration
src/Square.Test/Files
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ jobs:
continue-on-error: true
env:
TEST_SQUARE_TOKEN: ${{ secrets.TEST_SQUARE_TOKEN }}
TEST_SQUARE_REPORTING: ${{ secrets.TEST_SQUARE_REPORTING }}

- name: Publish to nuget.org
if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') && steps.build.outcome == 'success'
Expand Down
69 changes: 69 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,75 @@ public static async Task CheckWebhooksEvent(

In .NET 6 and above, there are also overloads using spans for allocation free webhook verification.

## Reporting API

The [Square Reporting API](https://developer.squareup.com/docs/reporting-api/overview) (beta)
exposes a Cube-based analytics layer through two endpoints. Call `GetMetadataAsync` to discover
the queryable schema (cubes, measures, dimensions, and segments), then `LoadAsync` to run a query:

```csharp
using Square;

var client = new SquareClient();

// Discover what you can query.
var metadata = await client.Reporting.GetMetadataAsync();

// Run a query.
var response = await client.Reporting.LoadAsync(
new LoadRequest
{
Query = new Query { Measures = new List<string> { "Orders.count" } },
}
);
```

### Polling with `LoadAndWaitAsync`

`/reporting/v1/load` is asynchronous: while a query is still being computed it returns an HTTP 200
whose body is `{ "error": "Continue wait" }` instead of results. The expected behavior is to re-send
the identical request, with backoff, until results arrive.

`LoadAndWaitAsync` owns that retry loop for you and returns the resolved `LoadResponse` — it never
hands back the "Continue wait" sentinel:

```csharp
using Square;

var response = await client.Reporting.LoadAndWaitAsync(
new LoadRequest
{
Query = new Query { Measures = new List<string> { "Orders.count" } },
}
);

// The resolved payload is exposed on the flat `Data` property.
var data = response.Data;
// ...
```

The backoff is configurable, and the loop honors a `CancellationToken`:

```csharp
var response = await client.Reporting.LoadAndWaitAsync(
request,
new LoadAndWaitOptions
{
MaxAttempts = 20, // give up after this many polls
InitialDelay = TimeSpan.FromSeconds(2), // delay before the first retry
MaxDelay = TimeSpan.FromSeconds(20), // upper bound on the backoff
BackoffFactor = 2, // multiplier applied after each attempt
},
cancellationToken
);
```

If the query does not resolve within `MaxAttempts`, a `SquareException` is thrown. The same logic is
also available as a static helper, `ReportingHelper.LoadAndWaitAsync(client.Reporting, request, ...)`.

> **Note:** The Reporting API is served only from production (`connect.squareup.com/reporting`); it is
> not available in the sandbox environment.

## Legacy SDK

While the new SDK has a lot of improvements, we at Square understand that it takes time to upgrade when there are breaking changes.
Expand Down
94 changes: 94 additions & 0 deletions reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -12050,6 +12050,100 @@ await client.Vendors.UpdateAsync(
</dl>


</dd>
</dl>
</details>

## Reporting
<details><summary><code>client.Reporting.<a href="/src/Square/Reporting/ReportingClient.cs">GetMetadataAsync</a>() -> MetadataResponse</code></summary>
<dl>
<dd>

#### 📝 Description

<dl>
<dd>

<dl>
<dd>

Describes the data available to query: the cubes, views, measures, dimensions, and segments you can reference in a reporting query. Call this first to discover the schema, then pass the members you need to `load`.
</dd>
</dl>
</dd>
</dl>

#### 🔌 Usage

<dl>
<dd>

<dl>
<dd>

```csharp
await client.Reporting.GetMetadataAsync();
```
</dd>
</dl>
</dd>
</dl>


</dd>
</dl>
</details>

<details><summary><code>client.Reporting.<a href="/src/Square/Reporting/ReportingClient.cs">LoadAsync</a>(LoadRequest { ... }) -> LoadResponse</code></summary>
<dl>
<dd>

#### 📝 Description

<dl>
<dd>

<dl>
<dd>

Runs a reporting query against the discovered schema and returns the aggregated results. Long-running queries may return a "Continue wait" response while processing — retry the same request until results are ready.
</dd>
</dl>
</dd>
</dl>

#### 🔌 Usage

<dl>
<dd>

<dl>
<dd>

```csharp
await client.Reporting.LoadAsync(new LoadRequest());
```
</dd>
</dl>
</dd>
</dl>

#### ⚙️ Parameters

<dl>
<dd>

<dl>
<dd>

**request:** `LoadRequest`

</dd>
</dl>
</dd>
</dl>


</dd>
</dl>
</details>
Expand Down
121 changes: 121 additions & 0 deletions src/Square.Test/Integration/ReportingTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
using System.Text.Json;
using NUnit.Framework;
using Square;

namespace Square.Test.Integration;

// The Reporting API is a beta, bespoke offering served ONLY from production
// (connect.squareup.com/reporting) — it is not routed on sandbox (returns 404 there).
// Validating it live therefore needs a production, reporting-provisioned access token,
// supplied via TEST_SQUARE_REPORTING (distinct from the sandbox TEST_SQUARE_TOKEN used by
// the rest of the integration suite). This suite is gated behind TEST_SQUARE_REPORTING and
// skips by default — keeping CI green. The endpoints are read-only (schema discovery +
// queries). The polling *logic* is covered without a live account in
// Unit/ReportingHelperTests.cs.
//
// Run it against a real prod account:
// TEST_SQUARE_REPORTING=<prod-reporting-token> \
// dotnet test --filter FullyQualifiedName~Integration.ReportingTests
// # override the host with TEST_SQUARE_BASE_URL=<url> if reporting moves.
[TestFixture]
public class ReportingTests
{
private SquareClient _client = null!;

[SetUp]
public void SetUp()
{
var token = Environment.GetEnvironmentVariable("TEST_SQUARE_REPORTING");
if (string.IsNullOrEmpty(token))
{
Assert.Ignore(
"Set TEST_SQUARE_REPORTING to a production, reporting-provisioned access token to run the reporting integration suite."
);
}
// Reporting only exists on production; allow overriding the host via TEST_SQUARE_BASE_URL.
var baseUrl =
Environment.GetEnvironmentVariable("TEST_SQUARE_BASE_URL") ?? SquareEnvironment.Production;
TestContext.Out.WriteLine(
$"[reporting] base URL: {baseUrl} -> {baseUrl}/reporting/v1/{{meta,load}}"
);
_client = new SquareClient(token!, new ClientOptions { BaseUrl = baseUrl });
}

// Resolves the first queryable measure from the live schema, e.g. "Orders.count".
private async Task<string> FirstMeasureNameAsync()
{
var metadata = await _client.Reporting.GetMetadataAsync();
var measure = metadata.Cubes?.FirstOrDefault()?.Measures.FirstOrDefault()?.Name;
if (string.IsNullOrEmpty(measure))
{
throw new Exception(
"No cubes/measures are available on the reporting schema for this account."
);
}
return measure!;
}

[Test]
public async Task GetMetadataReturnsTheQueryableSchema()
{
var metadata = await _client.Reporting.GetMetadataAsync();

Assert.That(metadata.Cubes, Is.Not.Null);
Assert.That(metadata.Cubes, Is.Not.Empty);

// Surface the live schema so a developer can see what is queryable.
var summary = (metadata.Cubes ?? new List<Cube>())
.Take(5)
.Select(cube => new
{
cube = cube.Name,
measures = cube.Measures.Take(5).Select(measure => measure.Name).ToList(),
});
TestContext.Out.WriteLine(
"Reporting schema (first 5 cubes): "
+ JsonSerializer.Serialize(summary, new JsonSerializerOptions { WriteIndented = true })
);
}

[Test]
public async Task LoadReturnsResultsOrTheContinueWaitSentinel()
{
var measure = await FirstMeasureNameAsync();
var response = await _client.Reporting.LoadAsync(
new LoadRequest { Query = new Query { Measures = new List<string> { measure } } }
);

if (response.AdditionalProperties.TryGetValue("error", out var sentinel))
{
// Documented async behavior: a still-processing query comes back as HTTP 200
// with { "error": "Continue wait" } instead of results.
Assert.That(sentinel.GetString(), Is.EqualTo("Continue wait"));
}
else
{
// Flat LoadResponse: a resolved query carries its payload on `Data`.
Assert.That(response.Data, Is.Not.Null);
}
}

[Test]
public async Task LoadAndWaitResolvesAQueryEndToEndWithoutSurfacingContinueWait()
{
var measure = await FirstMeasureNameAsync();

var response = await _client.Reporting.LoadAndWaitAsync(
new LoadRequest { Query = new Query { Measures = new List<string> { measure } } },
new LoadAndWaitOptions
{
MaxAttempts = 20,
InitialDelay = TimeSpan.FromSeconds(2),
MaxDelay = TimeSpan.FromSeconds(20),
}
);

// The polling helper must never hand back the raw "Continue wait" sentinel.
Assert.That(response.AdditionalProperties.ContainsKey("error"), Is.False);
// Flat LoadResponse: the resolved query carries its payload on `Data`.
Assert.That(response.Data, Is.Not.Null);
}
}
Loading
Loading