mirror of
https://github.com/anikeen-com/id.git
synced 2026-11-06 15:48:36 +00:00
Document elapsed periods and user accounts
This commit is contained in:
3 files changed
+141
-2
No files matched your search
@@ -1,5 +1,19 @@
|
||||
# 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)
|
||||
|
||||
- Add the scope `products:read` (`Scope::PRODUCTS_READ`) for read-only access to the product APIs
|
||||
|
||||
@@ -514,6 +514,11 @@ $subscription = $subscriptions->create([
|
||||
|
||||
$subscription->checkout(); // bill the first period
|
||||
$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->resume(); // expired subscriptions start a new period
|
||||
$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
|
||||
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
|
||||
`error` code (`already_active`, `already_cancelled`, `not_active`, `already_paused`,
|
||||
`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
|
||||
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
|
||||
`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
|
||||
|
||||
@@ -833,6 +895,7 @@ public function sshKeysByUserId(string $userId): SshKeys
|
||||
```php
|
||||
public function getAuthedUser(): 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 refreshToken(string $storedRefreshToken, string $scope = ''): Result
|
||||
public function isEmailExisting(string $email): Result
|
||||
|
||||
+63
-1
@@ -514,6 +514,11 @@ $subscription = $subscriptions->create([
|
||||
|
||||
$subscription->checkout(); // bill the first period
|
||||
$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->resume(); // expired subscriptions start a new period
|
||||
$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
|
||||
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
|
||||
`error` code (`already_active`, `already_cancelled`, `not_active`, `already_paused`,
|
||||
`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
|
||||
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
|
||||
`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
|
||||
|
||||
|
||||
Reference in new issue
Block a user