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
27 changes: 26 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,32 @@ jobs:
restore-keys: ${{ runner.os }}-php-${{ matrix.php }}-

- name: Install dependencies
run: composer install --prefer-dist --no-interaction --no-progress
# phpnomad/phpstan-rules (dev-only static-analysis tooling, never loaded
# at test runtime) requires PHP ^8.2; the library itself supports 8.1.
# The 8.1 job ignores the platform check so the SDK + PHPUnit still run
# on the oldest supported PHP. The lint job runs on 8.3 with full checks.
run: composer install --prefer-dist --no-interaction --no-progress ${{ matrix.php == '8.1' && '--ignore-platform-req=php' || '' }}

- name: Run test suite
run: composer test

lint:
name: Lint (PHPStan)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
extensions: json
coverage: none
tools: composer:v2

- name: Install dependencies
run: composer install --prefer-dist --no-interaction --no-progress

- name: Run PHPStan
run: composer lint
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ after every change and leave them green:
```bash
composer install # ci.setup
composer test # ci.test
composer lint # ci.lint
```

What "done" means (PR audit, testing tiers, UAT proof) is defined in the KB:
Expand Down
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Typed taxonomy exports so integrations use SDK constants instead of magic
strings: `EventSlug` (built-in ingestion slugs) and the status vocabularies
`ConversionStatus`, `TransactionStatus`, `ObligationStatus`, `PayoutStatus`,
`FulfillmentStatus`, `OpportunityStatus`, `ApiKeyStatus`, and
`WebhookSubscriptionStatus`.
- `WebhookEventType`: added the missing `CREDIT_ISSUED`, `CREDIT_REDEEMED`,
`CURRENCY_CREATED`, and `CURRENCY_DELETED` events (also added to
`openapi.yaml`), matching the full set Siren dispatches.
- PHPStan static analysis (level 6) with the PHPNomad stack-elevator ruleset,
wired as `composer lint` and a CI lint job.

## [0.1.0] - 2026-07-11

### Added
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,13 @@ $secret = $subscription['signingSecret'];
- **Reconciliation readers** — paginated iterators over conversions, transactions, obligations, and payouts.
- **Automatic retries** — network errors, 429s, and 5xx are retried with exponential backoff on idempotent operations; management writes are never auto-retried.
- **Typed exceptions** — every error extends `Siren\Sdk\Exception\SirenException` and carries the status code, error code, and error data.
- **Typed taxonomy** — Siren's domain vocabulary as constants, so no magic strings cross the boundary: `WebhookEventType`, `EventSlug`, and the status vocabularies (`ConversionStatus`, `TransactionStatus`, `ObligationStatus`, `PayoutStatus`, `FulfillmentStatus`, `OpportunityStatus`, `ApiKeyStatus`, `WebhookSubscriptionStatus`).

```php
use Siren\Sdk\ConversionStatus;

$approved = $siren->conversions->list(['status' => ConversionStatus::APPROVED]);
```

### Configuration

Expand Down
5 changes: 4 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
"guzzlehttp/guzzle": "^7.8"
},
"require-dev": {
"phpnomad/phpstan-rules": "^2.1",
"phpstan/phpstan": "^2.1",
"phpunit/phpunit": "^10.5"
},
"autoload": {
Expand All @@ -45,7 +47,8 @@
}
},
"scripts": {
"test": "phpunit"
"test": "phpunit",
"lint": "phpstan analyse --no-progress"
},
"config": {
"sort-packages": true
Expand Down
1 change: 1 addition & 0 deletions navigator.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ initiative: siren
ci:
setup: composer install
test: composer test
lint: composer lint
knowledgeDependencies:
- initiative: phpnomad
description: "Org-wide engineering doctrine"
Expand Down
4 changes: 4 additions & 0 deletions openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,10 @@ components:
- conversion.rejected
- conversion.renewed
- coupon.applied
- credit.issued
- credit.redeemed
- currency.created
- currency.deleted
- distribution.completed
- engagement.awarded
- engagement.completed
Expand Down
17 changes: 17 additions & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
includes:
# PHPNomad layer-purity ruleset (Stack Elevator: Core -> Service -> Platform).
# This SDK is a flat library (Siren\Sdk, no Core/Service segments), so the
# layer rules have nothing to flag today — they are wired in so that any
# future layered namespace is checked from day one.
- vendor/phpnomad/phpstan-rules/stack-elevator.neon

parameters:
level: 6
paths:
- src
treatPhpDocTypesAsCertain: false
phpNomadStackElevator:
namespaceRoots:
- Siren
- Novatorius
- PHPNomad
18 changes: 18 additions & 0 deletions src/ApiKeyStatus.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace Siren\Sdk;

/**
* Statuses an API key can hold (the `apiKeys` resource).
*/
final class ApiKeyStatus
{
public const ACTIVE = 'active';
public const REVOKED = 'revoked';

private function __construct()
{
}
}
2 changes: 2 additions & 0 deletions src/ApiResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ public function getRawBody(): string
* The Siren API returns bare JSON — a plain array `[{...},{...}]` for list
* endpoints and a plain object `{...}` for create/read-by-id endpoints.
* There is no `{ "data": ... }` envelope to unwrap.
*
* @return array<int|string, mixed>
*/
public function json(): array
{
Expand Down
24 changes: 24 additions & 0 deletions src/ConversionStatus.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace Siren\Sdk;

/**
* Statuses a conversion can hold (the `conversions` reconciliation reader
* and `conversion.*` webhook payloads).
*/
final class ConversionStatus
{
public const PENDING = 'pending';
public const APPROVED = 'approved';
public const REJECTED = 'rejected';
public const EXPIRED = 'expired';

/** Soft-delete bucket written by the bulk delete action. */
public const DELETED = 'deleted';

private function __construct()
{
}
}
23 changes: 23 additions & 0 deletions src/EventSlug.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace Siren\Sdk;

/**
* URL slugs for the built-in ingestion event types (`POST /event/{slug}`).
*
* `Events::sale()`, `Events::refund()`, and `Events::siteVisited()` already
* use these internally; the constants exist for code that routes slugs
* dynamically (e.g. wrapping `Events::ingest()`).
*/
final class EventSlug
{
public const SALE = 'sale';
public const REFUND = 'refund';
public const SITE_VISITED = 'site-visited';

private function __construct()
{
}
}
3 changes: 3 additions & 0 deletions src/Exception/RateLimitException.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
/** Raised for HTTP 429 responses — too many requests. */
class RateLimitException extends SirenException
{
/**
* @param array<string, mixed>|null $errorData
*/
public function __construct(
string $message,
int $statusCode = 429,
Expand Down
9 changes: 8 additions & 1 deletion src/Exception/SirenException.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
*/
class SirenException extends \RuntimeException
{
/**
* @param array<string, mixed>|null $errorData
*/
public function __construct(
string $message,
private readonly int $statusCode = 0,
Expand All @@ -35,7 +38,11 @@ public function getErrorCode(): ?string
return $this->errorCode;
}

/** Structured `error.data` from the response body, when present. */
/**
* Structured `error.data` from the response body, when present.
*
* @return array<string, mixed>|null
*/
public function getErrorData(): ?array
{
return $this->errorData;
Expand Down
21 changes: 21 additions & 0 deletions src/FulfillmentStatus.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace Siren\Sdk;

/**
* Statuses a fulfillment can hold (`fulfillment.created` /
* `fulfillment.updated` webhook payloads).
*/
final class FulfillmentStatus
{
public const PENDING = 'pending';
public const PROCESSING = 'processing';
public const COMPLETE = 'complete';
public const FAILED = 'failed';

private function __construct()
{
}
}
1 change: 1 addition & 0 deletions src/ListResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ public function getTotal(): ?int
return $this->total;
}

/** @return \ArrayIterator<int, array<string, mixed>> */
public function getIterator(): \ArrayIterator
{
return new \ArrayIterator($this->data);
Expand Down
25 changes: 25 additions & 0 deletions src/ObligationStatus.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

namespace Siren\Sdk;

/**
* Statuses an obligation can hold (the `obligations` reconciliation reader
* and `obligation.*` webhook payloads).
*
* Note: Siren's machine paths (fulfillment generation, bulk actions) write
* `complete`, while its management REST surface accepts `fulfilled` — both
* appear in the wild, so both are listed here.
*/
final class ObligationStatus
{
public const PENDING = 'pending';
public const COMPLETE = 'complete';
public const FULFILLED = 'fulfilled';
public const CANCELLED = 'cancelled';

private function __construct()
{
}
}
23 changes: 23 additions & 0 deletions src/OpportunityStatus.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace Siren\Sdk;

/**
* Statuses an opportunity can hold (`opportunity.created` /
* `opportunity.invalidated` webhook payloads; the `trackingId` on a sale
* refers to an opportunity).
*/
final class OpportunityStatus
{
public const ACTIVE = 'active';
public const INACTIVE = 'inactive';

/** Set by Siren's invalidation service; never operator-settable. */
public const INVALID = 'invalid';

private function __construct()
{
}
}
21 changes: 21 additions & 0 deletions src/PayoutStatus.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace Siren\Sdk;

/**
* Statuses a payout can hold (the `payouts` reconciliation reader and
* `payout.*` webhook payloads).
*/
final class PayoutStatus
{
public const UNPAID = 'unpaid';
public const PROCESSING = 'processing';
public const PAID = 'paid';
public const FAILED = 'failed';

private function __construct()
{
}
}
20 changes: 20 additions & 0 deletions src/TransactionStatus.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace Siren\Sdk;

/**
* Statuses a transaction can hold (the `transactions` reconciliation reader
* and `transaction.*` webhook payloads).
*/
final class TransactionStatus
{
public const COMPLETE = 'complete';
public const CANCELLED = 'cancelled';
public const REFUNDED = 'refunded';

private function __construct()
{
}
}
4 changes: 4 additions & 0 deletions src/WebhookEventType.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ final class WebhookEventType
public const CONVERSION_REJECTED = 'conversion.rejected';
public const CONVERSION_RENEWED = 'conversion.renewed';
public const COUPON_APPLIED = 'coupon.applied';
public const CREDIT_ISSUED = 'credit.issued';
public const CREDIT_REDEEMED = 'credit.redeemed';
public const CURRENCY_CREATED = 'currency.created';
public const CURRENCY_DELETED = 'currency.deleted';
public const DISTRIBUTION_COMPLETED = 'distribution.completed';
public const ENGAGEMENT_AWARDED = 'engagement.awarded';
public const ENGAGEMENT_COMPLETED = 'engagement.completed';
Expand Down
19 changes: 19 additions & 0 deletions src/WebhookSubscriptionStatus.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace Siren\Sdk;

/**
* Statuses a webhook subscription can hold (the `webhooks` subscriptions
* resource).
*/
final class WebhookSubscriptionStatus
{
public const ACTIVE = 'active';
public const PAUSED = 'paused';

private function __construct()
{
}
}
Loading
Loading