Files
id/src/Id/Resources/AppSubscriptions.php
T
Maurice PreußandClaude Opus 5.5 c2493b76fd Document monthly_cap, predecessor_id and expected_monthly_price
Subscriptions (user token and app token) and checkout-session subscriptions
take a net monthly cap and the subscription they replace (a resize shares
the cap within the month). Attributes are passed through unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VbpgPaLqhBzo1AXHNs9Cx5
2026-11-03 12:43:17 +01:00

121 lines
3.8 KiB
PHP

<?php
namespace Anikeen\Id\Resources;
use Anikeen\Id\AnikeenId;
use Anikeen\Id\Contracts\Billable;
use Anikeen\Id\Exceptions\ApiException;
use Anikeen\Id\Support\AppRequest;
use Illuminate\Database\Eloquent\Model;
use InvalidArgumentException;
/**
* Subscriptions of a user, managed with the app's own token (client credentials, scope
* `subscriptions:client`) instead of the user's token, so they keep working when the user's
* login expired. The app has to be trusted by the ID service. Nothing is requested until a
* method is called.
*/
class AppSubscriptions
{
public function __construct(protected Billable|Model $billable)
{
}
/**
* The user's Anikeen ID (see AnikeenId::useUserIdField()).
*/
public function userId(): string
{
$userId = $this->billable->{AnikeenId::getUserIdField()} ?? null;
if (empty($userId)) {
throw new InvalidArgumentException(sprintf('The billable has no Anikeen ID in [%s].', AnikeenId::getUserIdField()));
}
return (string) $userId;
}
/**
* Create a subscription for the user (it is billed only after checkout()).
*
* @param array{
* group?: null|string,
* name: string,
* description?: null|string,
* unit: string,
* price: float|string,
* monthly_cap?: null|float|string,
* predecessor_id?: null|string,
* vat_rate?: float,
* payload?: null|array,
* ends_at?: null|string,
* idempotency_key?: null|string
* } $attributes With an idempotency_key (max. 191 characters), a retried call returns the
* subscription created by the first one instead of a duplicate.
* monthly_cap: net, at most this per calendar month (hour and day units only,
* at most 2 decimals, not below price; null = no cap).
* predecessor_id: the subscription this one replaces (a resize; same user and
* app, checked out, not cancelled): both share the monthly cap within a month.
*
* @throws ApiException
*/
public function create(array $attributes): AppSubscription
{
return new AppSubscription(AppRequest::send('POST', 'v1/client/subscriptions', [
...$attributes,
'user_id' => $this->userId(),
])->data);
}
/**
* Find one of the user's subscriptions; null when it doesn't exist (or belongs to someone else).
*
* @throws ApiException
*/
public function find(string $id): ?AppSubscription
{
try {
$subscription = new AppSubscription(AppRequest::send('GET', sprintf('v1/client/subscriptions/%s', $id))->data);
} catch (ApiException $e) {
if ($e->isNotFound()) {
return null;
}
throw $e;
}
return $subscription->user_id === $this->userId() ? $subscription : null;
}
/**
* All of the user's subscriptions, optionally filtered.
*
* @param array{group?: string, active?: bool, cancelled?: bool} $filters
* @return list<AppSubscription>
*
* @throws ApiException
*/
public function all(array $filters = []): array
{
$subscriptions = [];
$page = 1;
do {
$result = AppRequest::send('GET', 'v1/client/subscriptions', null, [
...array_map(fn ($value) => is_bool($value) ? (int) $value : $value, $filters),
'user_id' => $this->userId(),
'per_page' => 100,
'page' => $page,
]);
foreach ($result->data as $data) {
$subscriptions[] = new AppSubscription($data);
}
$lastPage = (int) ($result->meta->last_page ?? $page);
} while ($page++ < $lastPage);
return $subscriptions;
}
}