mirror of
https://github.com/anikeen-com/id.git
synced 2026-11-06 23:58:35 +00:00
Merge main (3.2.0) into feature/uptimr-billing-api
This commit is contained in:
8 files changed
+373
-16
No files matched your search
@@ -5,7 +5,9 @@ namespace Anikeen\Id\Concerns;
|
||||
|
||||
use Anikeen\Id\ApiOperations\Get;
|
||||
use Anikeen\Id\ApiOperations\Post;
|
||||
use Anikeen\Id\Exceptions\ApiException;
|
||||
use Anikeen\Id\Result;
|
||||
use Anikeen\Id\Support\AppRequest;
|
||||
use Throwable;
|
||||
|
||||
trait ManagesUsers
|
||||
@@ -24,20 +26,32 @@ trait ManagesUsers
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new user on behalf of the current user.
|
||||
* Finds the account of an email address or creates it (trusted apps, app token). The email
|
||||
* address is matched regardless of case; a new account stores it in lower case, unverified.
|
||||
* An existing account is returned unchanged (only a missing name is filled in).
|
||||
*
|
||||
* @param array{
|
||||
* first_name: null|string,
|
||||
* last_name: null|string,
|
||||
* username: null|string,
|
||||
* name?: null|string,
|
||||
* first_name?: null|string,
|
||||
* last_name?: null|string,
|
||||
* username?: null|string,
|
||||
* email: string,
|
||||
* password: null|string
|
||||
* password?: null|string,
|
||||
* password_confirmation?: null|string,
|
||||
* send_invitation?: bool
|
||||
* } $attributes The user data
|
||||
* - first_name: The first name (optional)
|
||||
* - last_name: The last name (optional)
|
||||
* - username: The username (optional)
|
||||
* - email: The email (required)
|
||||
* - password: The password (optional, can be reset by the user if not provided)
|
||||
* - name: The display name (optional)
|
||||
* - first_name: The first name (optional, joined with last_name into name)
|
||||
* - last_name: The last name (optional, joined with first_name into name)
|
||||
* - username: The username (optional)
|
||||
* - email: The email (required)
|
||||
* - password: The password (optional, can be reset by the user if not provided)
|
||||
* - password_confirmation: Defaults to password
|
||||
* - send_invitation: Send the user an invitation mail with a link to set a password
|
||||
* (optional, only when this call created the account without a
|
||||
* password, never for an existing account)
|
||||
* @return Result `data` is the user plus `created` (this call created the account) and
|
||||
* `invitation_sent` (the invitation mail was sent); 201 when created, 200 when it existed
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function createUser(array $attributes): Result
|
||||
@@ -57,6 +71,30 @@ trait ManagesUsers
|
||||
return $this->asApp()->post('v1/admin/users', $attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks up the account of an email address without creating or changing anything (trusted
|
||||
* apps, app token). The email address is matched regardless of case.
|
||||
*
|
||||
* `data` is `object{id: string, name: ?string, email: string, email_verified: bool,
|
||||
* has_billing_address: bool}`.
|
||||
*
|
||||
* @return Result|null null when no account uses the email address
|
||||
* @throws ApiException when the request fails for another reason
|
||||
*/
|
||||
public function findUserByEmail(string $email): ?Result
|
||||
{
|
||||
try {
|
||||
return AppRequest::send('POST', 'v1/admin/users/lookup', ['email' => $email]);
|
||||
} catch (ApiException $e) {
|
||||
// only Anikeen ID's own answer means "no account", not e.g. an unknown route
|
||||
if ($e->isNotFound() && $e->error === 'user_not_found') {
|
||||
return null;
|
||||
}
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the user of a social login or create it (trusted apps, app token).
|
||||
*
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace Anikeen\Id\Exceptions;
|
||||
|
||||
use Anikeen\Id\Result;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
@@ -44,6 +45,24 @@ class ApiException extends ResourceException
|
||||
return new static(sprintf('Anikeen ID is not reachable: %s', $previous->getMessage()), 0, null, $previous);
|
||||
}
|
||||
|
||||
/**
|
||||
* The JSON body of Anikeen ID's error response, null without one. Some errors carry details,
|
||||
* e.g. `net_amount` of "elapsed_amount_exceeded".
|
||||
*/
|
||||
public function body(): ?object
|
||||
{
|
||||
$previous = $this->getPrevious();
|
||||
$response = $previous !== null && method_exists($previous, 'getResponse') ? $previous->getResponse() : null;
|
||||
|
||||
if (!$response instanceof ResponseInterface) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$body = json_decode((string) $response->getBody());
|
||||
|
||||
return is_object($body) ? $body : null;
|
||||
}
|
||||
|
||||
public function isNotFound(): bool
|
||||
{
|
||||
return $this->status === 404;
|
||||
|
||||
@@ -6,6 +6,7 @@ use Anikeen\Id\Concerns\HasSubscriptionStatus;
|
||||
use Anikeen\Id\Exceptions\ApiException;
|
||||
use Anikeen\Id\Support\AppRequest;
|
||||
use DateTimeInterface;
|
||||
use InvalidArgumentException;
|
||||
use JsonSerializable;
|
||||
|
||||
/**
|
||||
@@ -129,15 +130,46 @@ class AppSubscription implements JsonSerializable
|
||||
* $startsAt (in the past) books a service that is already running, e.g. a domain registered
|
||||
* earlier: the first period runs from $startsAt, is billed now and renews when it ends.
|
||||
*
|
||||
* @throws ApiException 409 "already_active", "billing_address_missing", "period_already_ended", …
|
||||
* A $startsAt older than one period is refused ("period_already_ended") unless
|
||||
* $billElapsedPeriods is set: then the periods that have already ended are billed too, together
|
||||
* with the running one, as long as they come to at most $maxNetAmount (net, up to two
|
||||
* decimals) and $startsAt lies within the maximum age Anikeen ID accepts (about three years).
|
||||
* The lines land on the current month's collective order, which is charged on the 1st of the
|
||||
* next month. $billElapsedPeriods needs both $startsAt and $maxNetAmount.
|
||||
*
|
||||
* @throws ApiException 409 "already_active", "billing_address_missing", "period_already_ended",
|
||||
* "elapsed_periods_not_allowed" (the elapsed periods can't be billed for this subscription),
|
||||
* "elapsed_amount_exceeded" (they come to more than $maxNetAmount; the response body has
|
||||
* the booked `net_amount`), …; 422 invalid input (e.g. a start older than the maximum age)
|
||||
* @throws InvalidArgumentException $billElapsedPeriods without $startsAt or $maxNetAmount
|
||||
*/
|
||||
public function checkout(DateTimeInterface|string|null $startsAt = null): self
|
||||
{
|
||||
public function checkout(
|
||||
DateTimeInterface|string|null $startsAt = null,
|
||||
bool $billElapsedPeriods = false,
|
||||
float|string|null $maxNetAmount = null,
|
||||
): self {
|
||||
if ($startsAt instanceof DateTimeInterface) {
|
||||
$startsAt = $startsAt->format(DateTimeInterface::ATOM);
|
||||
}
|
||||
|
||||
return $this->send('PUT', '/checkout', $startsAt === null ? null : ['starts_at' => $startsAt]);
|
||||
if (!$billElapsedPeriods) {
|
||||
return $this->send('PUT', '/checkout', $startsAt === null ? null : ['starts_at' => $startsAt]);
|
||||
}
|
||||
|
||||
if ($startsAt === null || $startsAt === '') {
|
||||
throw new InvalidArgumentException('Billing elapsed periods needs a start date ($startsAt).');
|
||||
}
|
||||
|
||||
if ($maxNetAmount === null || $maxNetAmount === '') {
|
||||
throw new InvalidArgumentException('Billing elapsed periods needs a maximum net amount ($maxNetAmount).');
|
||||
}
|
||||
|
||||
return $this->send('PUT', '/checkout', [
|
||||
'starts_at' => $startsAt,
|
||||
'bill_elapsed_periods' => true,
|
||||
// floats from calculations may carry binary noise (e.g. 59.970000000000006)
|
||||
'max_net_amount' => is_float($maxNetAmount) ? round($maxNetAmount, 2) : $maxNetAmount,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user