Document elapsed periods and user accounts

This commit is contained in:
envoyr committed 2026-11-06 10:26:51 +00:00
1 parent 7813e8fbaa
commit 64ccb84987
3 files changed
+141 -2

No files matched your search

+14
View File
@@ -1,5 +1,19 @@
# Changelog # Changelog
## Unreleased
- `AppSubscription::checkout()` takes `billElapsedPeriods` and `maxNetAmount`: a subscription
started more than one period ago is billed for its elapsed periods too, up to the given net
amount (409 `elapsed_amount_exceeded` with `net_amount`, `elapsed_periods_not_allowed`). Calls
without the new arguments send the same request as before.
- `ApiException::body()` returns the JSON body of Anikeen ID's error response, e.g. its
`net_amount`.
- Add `AnikeenId::findUserByEmail()` for trusted apps: looks up an account by email address
(regardless of case) without creating one, null when there is none.
- `createUser()` documents `name`, `password_confirmation` and `send_invitation` (a newly created
account gets an invitation mail to set a password) and the response fields `created` and
`invitation_sent`.
## 3.1.0 (2026-11-05) ## 3.1.0 (2026-11-05)
- Add the scope `products:read` (`Scope::PRODUCTS_READ`) for read-only access to the product APIs - Add the scope `products:read` (`Scope::PRODUCTS_READ`) for read-only access to the product APIs
+64 -1
View File
@@ -514,6 +514,11 @@ $subscription = $subscriptions->create([
$subscription->checkout(); // bill the first period $subscription->checkout(); // bill the first period
$subscription->checkout(startsAt: $domain->registered_at); // already running: bill the period that started then $subscription->checkout(startsAt: $domain->registered_at); // already running: bill the period that started then
$subscription->checkout( // running for longer: also bill the periods that have ended
startsAt: $domain->registered_at,
billElapsedPeriods: true,
maxNetAmount: '15.57', // net ceiling, e.g. the amount the customer agreed to
);
$subscription->pause(); // expires when the paid period ends $subscription->pause(); // expires when the paid period ends
$subscription->resume(); // expired subscriptions start a new period $subscription->resume(); // expired subscriptions start a new period
$subscription->revoke(refund: false); // cancel $subscription->revoke(refund: false); // cancel
@@ -526,10 +531,44 @@ The user's Anikeen ID is read from the billable's `anikeen_id` attribute
(`AnikeenId::useUserIdField('…')` to change it). The app must be trusted by Anikeen ID; it sees the (`AnikeenId::useUserIdField('…')` to change it). The app must be trusted by Anikeen ID; it sees the
subscriptions it created (plus those of clients delegated to it). subscriptions it created (plus those of clients delegated to it).
#### Starting from an earlier date
`checkout(startsAt: …)` books a service that is already running, e.g. a domain registered before
the customer came to your app. If the first period from that date is still running, it is billed
now and renews when it ends. An older start is refused with `period_already_ended`, unless you
pass `billElapsedPeriods: true` with `maxNetAmount` (net, up to two decimals; both together, or
`checkout()` throws an `InvalidArgumentException` without sending anything):
- every period that has ended plus the running one is billed now, as one line with the full
service period. It lands on the user's collective order of the current month and is invoiced and
charged with the next monthly bill; nothing is back-dated.
- the start may be at most about three years ago (422 otherwise).
- if the periods come to more than `maxNetAmount`, nothing is booked: 409
`elapsed_amount_exceeded`, with the real amount in `$e->body()->net_amount` (e.g. `"20.76"`), so
you can show it and retry.
- 409 `elapsed_periods_not_allowed` when the subscription can't be billed this way (e.g. it ran
before, replaces another one, or is billed by the hour or day with a monthly cap).
```php
try {
$subscription->checkout(startsAt: $startsAt, billElapsedPeriods: true, maxNetAmount: $confirmed);
} catch (ApiException $e) {
if ($e->error === 'elapsed_amount_exceeded') {
$actual = $e->body()->net_amount; // ask again with the real amount
} else {
throw $e;
}
}
```
#### Error codes
Failures throw `Anikeen\Id\Exceptions\ApiException` with `status` (0 = not reachable), a stable Failures throw `Anikeen\Id\Exceptions\ApiException` with `status` (0 = not reachable), a stable
`error` code (`already_active`, `already_cancelled`, `not_active`, `already_paused`, `error` code (`already_active`, `already_cancelled`, `not_active`, `already_paused`,
`already_resumed`, `billing_address_missing`, `not_changeable`, `subscription_not_found`, `already_resumed`, `billing_address_missing`, `not_changeable`, `subscription_not_found`,
`idempotency_key_conflict`) and `isNotFound()`, `isConflict()`, `isTemporary()`: `idempotency_key_conflict`, `period_already_ended`, `elapsed_periods_not_allowed`,
`elapsed_amount_exceeded`), the error response via `body()` and `isNotFound()`, `isConflict()`,
`isTemporary()`:
```php ```php
try { try {
@@ -616,6 +655,29 @@ While a session is open, its subscriptions can't be checked out or repriced by t
`subscription.activated` / `subscription.cancelled` are still sent per subscription, before the `subscription.activated` / `subscription.cancelled` are still sent per subscription, before the
`checkout.*` event of the session. `checkout.*` event of the session.
### User accounts (trusted apps, no user token)
Trusted apps can look up and create Anikeen ID accounts with the app's own token. Email addresses
are matched regardless of case.
```php
$anikeenId = app(AnikeenId::class);
$account = $anikeenId->findUserByEmail('erika@example.com'); // null if no account uses it
$account?->data->id; // also name, email, email_verified, has_billing_address
$result = $anikeenId->createUser([ // finds the account of the email address or creates it
'name' => 'Erika Mustermann',
'email' => 'erika@example.com',
'send_invitation' => true, // a new account gets a mail with a link to set a password
]);
$result->data->created; // false: the account already existed (and is unchanged)
$result->data->invitation_sent; // only for an account created by this call without a password
```
`findUserByEmail()` changes nothing. It throws `ApiException` when the request fails for any reason
other than an unknown address.
## API authentication ## API authentication
@@ -833,6 +895,7 @@ public function sshKeysByUserId(string $userId): SshKeys
```php ```php
public function getAuthedUser(): Result public function getAuthedUser(): Result
public function createUser(array $attributes): Result public function createUser(array $attributes): Result
public function findUserByEmail(string $email): ?Result
public function findOrCreateSocialiteUser(string $provider, string $providerId, ?string $email = null, ?string $name = null): Result public function findOrCreateSocialiteUser(string $provider, string $providerId, ?string $email = null, ?string $name = null): Result
public function refreshToken(string $storedRefreshToken, string $scope = ''): Result public function refreshToken(string $storedRefreshToken, string $scope = ''): Result
public function isEmailExisting(string $email): Result public function isEmailExisting(string $email): Result
+63 -1
View File
@@ -514,6 +514,11 @@ $subscription = $subscriptions->create([
$subscription->checkout(); // bill the first period $subscription->checkout(); // bill the first period
$subscription->checkout(startsAt: $domain->registered_at); // already running: bill the period that started then $subscription->checkout(startsAt: $domain->registered_at); // already running: bill the period that started then
$subscription->checkout( // running for longer: also bill the periods that have ended
startsAt: $domain->registered_at,
billElapsedPeriods: true,
maxNetAmount: '15.57', // net ceiling, e.g. the amount the customer agreed to
);
$subscription->pause(); // expires when the paid period ends $subscription->pause(); // expires when the paid period ends
$subscription->resume(); // expired subscriptions start a new period $subscription->resume(); // expired subscriptions start a new period
$subscription->revoke(refund: false); // cancel $subscription->revoke(refund: false); // cancel
@@ -526,10 +531,44 @@ The user's Anikeen ID is read from the billable's `anikeen_id` attribute
(`AnikeenId::useUserIdField('…')` to change it). The app must be trusted by Anikeen ID; it sees the (`AnikeenId::useUserIdField('…')` to change it). The app must be trusted by Anikeen ID; it sees the
subscriptions it created (plus those of clients delegated to it). subscriptions it created (plus those of clients delegated to it).
#### Starting from an earlier date
`checkout(startsAt: …)` books a service that is already running, e.g. a domain registered before
the customer came to your app. If the first period from that date is still running, it is billed
now and renews when it ends. An older start is refused with `period_already_ended`, unless you
pass `billElapsedPeriods: true` with `maxNetAmount` (net, up to two decimals; both together, or
`checkout()` throws an `InvalidArgumentException` without sending anything):
- every period that has ended plus the running one is billed now, as one line with the full
service period. It lands on the user's collective order of the current month and is invoiced and
charged with the next monthly bill; nothing is back-dated.
- the start may be at most about three years ago (422 otherwise).
- if the periods come to more than `maxNetAmount`, nothing is booked: 409
`elapsed_amount_exceeded`, with the real amount in `$e->body()->net_amount` (e.g. `"20.76"`), so
you can show it and retry.
- 409 `elapsed_periods_not_allowed` when the subscription can't be billed this way (e.g. it ran
before, replaces another one, or is billed by the hour or day with a monthly cap).
```php
try {
$subscription->checkout(startsAt: $startsAt, billElapsedPeriods: true, maxNetAmount: $confirmed);
} catch (ApiException $e) {
if ($e->error === 'elapsed_amount_exceeded') {
$actual = $e->body()->net_amount; // ask again with the real amount
} else {
throw $e;
}
}
```
#### Error codes
Failures throw `Anikeen\Id\Exceptions\ApiException` with `status` (0 = not reachable), a stable Failures throw `Anikeen\Id\Exceptions\ApiException` with `status` (0 = not reachable), a stable
`error` code (`already_active`, `already_cancelled`, `not_active`, `already_paused`, `error` code (`already_active`, `already_cancelled`, `not_active`, `already_paused`,
`already_resumed`, `billing_address_missing`, `not_changeable`, `subscription_not_found`, `already_resumed`, `billing_address_missing`, `not_changeable`, `subscription_not_found`,
`idempotency_key_conflict`) and `isNotFound()`, `isConflict()`, `isTemporary()`: `idempotency_key_conflict`, `period_already_ended`, `elapsed_periods_not_allowed`,
`elapsed_amount_exceeded`), the error response via `body()` and `isNotFound()`, `isConflict()`,
`isTemporary()`:
```php ```php
try { try {
@@ -616,6 +655,29 @@ While a session is open, its subscriptions can't be checked out or repriced by t
`subscription.activated` / `subscription.cancelled` are still sent per subscription, before the `subscription.activated` / `subscription.cancelled` are still sent per subscription, before the
`checkout.*` event of the session. `checkout.*` event of the session.
### User accounts (trusted apps, no user token)
Trusted apps can look up and create Anikeen ID accounts with the app's own token. Email addresses
are matched regardless of case.
```php
$anikeenId = app(AnikeenId::class);
$account = $anikeenId->findUserByEmail('erika@example.com'); // null if no account uses it
$account?->data->id; // also name, email, email_verified, has_billing_address
$result = $anikeenId->createUser([ // finds the account of the email address or creates it
'name' => 'Erika Mustermann',
'email' => 'erika@example.com',
'send_invitation' => true, // a new account gets a mail with a link to set a password
]);
$result->data->created; // false: the account already existed (and is unchanged)
$result->data->invitation_sent; // only for an account created by this call without a password
```
`findUserByEmail()` changes nothing. It throws `ApiException` when the request fails for any reason
other than an unknown address.
## API authentication ## API authentication