mirror of
https://github.com/anikeen-com/id.git
synced 2026-11-01 05:09:35 +00:00
Bring the package up to date with the Anikeen ID API
Fixed (called endpoints that don't exist):
- charge(): POST v1/billing/charge (Stripe options aren't passed through anymore)
- Invoice::getInvoiceTemporaryUrl(): GET instead of a non-existent PUT
- OrderItems::create(): POST v1/orders/{order}/items
- createUser() / isEmailExisting(): admin endpoints with the app token
- sshKeysByUserId(): app endpoint; no longer returns the first user's keys for every user
- createOrderPreview(): country_iso goes into billing_address as the API expects
- public endpoints (products, order preview) work without a token
- SshKeys::find() results can be deleted
Added:
- Billable: sshKeys() (create, update, delete), billingOverview(), PaymentMethod::delete()
- AnikeenId: products(), boxes(), shipping labels (list, show, pdf, issue, ship, price list),
createOrderForUser(), createTransactionForUser(), findOrCreateSocialiteUser()
Requirements: PHP 8.2+, Laravel 11-13 (Laravel 10 and PHP 8.1 are end of life).
Docs generator handles union types; README regenerated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8de737641e
commit
9613d31a8d
22 files changed
+720
-108
No files matched your search
@@ -3,8 +3,11 @@
|
||||
namespace Anikeen\Id;
|
||||
|
||||
use Anikeen\Id\Concerns\ManagesPricing;
|
||||
use Anikeen\Id\Concerns\ManagesProducts;
|
||||
use Anikeen\Id\Concerns\ManagesShippingLabels;
|
||||
use Anikeen\Id\Concerns\ManagesSocialiteUsers;
|
||||
use Anikeen\Id\Concerns\ManagesSshKeys;
|
||||
use Anikeen\Id\Concerns\ManagesUserBilling;
|
||||
use Anikeen\Id\Concerns\ManagesUsers;
|
||||
use Anikeen\Id\Exceptions\RequestRequiresAuthenticationException;
|
||||
use Anikeen\Id\Exceptions\RequestRequiresClientIdException;
|
||||
@@ -21,6 +24,9 @@ class AnikeenId
|
||||
{
|
||||
use OauthTrait;
|
||||
use ManagesPricing;
|
||||
use ManagesProducts;
|
||||
use ManagesShippingLabels;
|
||||
use ManagesUserBilling;
|
||||
use ManagesSocialiteUsers;
|
||||
use ManagesSshKeys;
|
||||
use ManagesUsers;
|
||||
@@ -398,7 +404,8 @@ class AnikeenId
|
||||
'Client-ID' => $this->getClientId(),
|
||||
'Accept' => 'application/json',
|
||||
];
|
||||
if ($bearerToken = $useClientSecret ? $this->getClientSecret() : $this->getToken()) {
|
||||
// public endpoints (products, order preview, …) work without a token
|
||||
if ($bearerToken = $useClientSecret ? $this->getClientSecret() : $this->token) {
|
||||
$headers['Authorization'] = 'Bearer ' . $bearerToken;
|
||||
}
|
||||
if ($json) {
|
||||
|
||||
@@ -12,6 +12,7 @@ use Anikeen\Id\Concerns\ManagesProfile;
|
||||
use Anikeen\Id\Concerns\ManagesSubscriptions;
|
||||
use Anikeen\Id\Concerns\ManagesTaxation;
|
||||
use Anikeen\Id\Concerns\ManagesTransactions;
|
||||
use Anikeen\Id\Concerns\ManagesUserSshKeys;
|
||||
use Throwable;
|
||||
|
||||
trait Billable
|
||||
@@ -26,6 +27,7 @@ trait Billable
|
||||
use ManagesSubscriptions;
|
||||
use ManagesTaxation;
|
||||
use ManagesTransactions;
|
||||
use ManagesUserSshKeys;
|
||||
|
||||
/**
|
||||
* Get the currently authenticated user.
|
||||
|
||||
@@ -30,21 +30,31 @@ trait ManagesBalance
|
||||
}
|
||||
|
||||
/**
|
||||
* Charge given amount from bank to current user.
|
||||
* Billing overview of the current user: current_balance, current_charges, vat_rate,
|
||||
* has_billing_address, has_default_payment_method, open_orders.
|
||||
*
|
||||
* @param float $amount Amount to charge in euros.
|
||||
* @param string $paymentMethodId Payment method ID.
|
||||
* @param array $options Additional options for the charge.
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function charge(float $amount, string $paymentMethodId, array $options = []): Transaction
|
||||
public function billingOverview(): object
|
||||
{
|
||||
return $this->anikeenId()->request('GET', 'v1/billing')->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Charge given amount (deposit) from one of the current user's payment methods.
|
||||
*
|
||||
* @param float|string $amount Amount in euros (0.50 to 10,000, at most 2 decimals).
|
||||
* @param string $paymentMethodId Stripe payment method ID (pm_…).
|
||||
* @param array $options Ignored; Stripe options can no longer be passed through.
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function charge(float|string $amount, string $paymentMethodId, array $options = []): Transaction
|
||||
{
|
||||
return (new Transaction(fn() => $this->anikeenId()
|
||||
->request('POST', 'billing/charge', [
|
||||
->request('POST', 'v1/billing/charge', [
|
||||
'amount' => $amount,
|
||||
'payment_method_id' => $paymentMethodId,
|
||||
'options' => $options,
|
||||
])))
|
||||
->setBillable($this);
|
||||
}
|
||||
}
|
||||
}␍
|
||||
@@ -32,6 +32,12 @@ trait ManagesPricing
|
||||
*/
|
||||
public function createOrderPreview(array $attributes = []): Result
|
||||
{
|
||||
// the API expects the country as part of the billing address
|
||||
if (isset($attributes['country_iso']) && !isset($attributes['billing_address'])) {
|
||||
$attributes['billing_address'] = ['country_iso' => $attributes['country_iso']];
|
||||
unset($attributes['country_iso']);
|
||||
}
|
||||
|
||||
return $this->post('v1/orders/preview', $attributes);
|
||||
}
|
||||
}
|
||||
}␍
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Anikeen\Id\Concerns;
|
||||
|
||||
use Anikeen\Id\Result;
|
||||
use Throwable;
|
||||
|
||||
trait ManagesProducts
|
||||
{
|
||||
/**
|
||||
* Public product catalogue.
|
||||
*
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function products(): Result
|
||||
{
|
||||
return $this->request('GET', 'v1/products');
|
||||
}
|
||||
|
||||
/**
|
||||
* Shipping boxes (for shipping labels).
|
||||
*
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function boxes(): Result
|
||||
{
|
||||
return $this->request('GET', 'v1/boxes');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace Anikeen\Id\Concerns;
|
||||
|
||||
use Anikeen\Id\Result;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Shipping labels (Deutsche Post Internetmarke). Trusted apps only, sent with the app's token.
|
||||
*/
|
||||
trait ManagesShippingLabels
|
||||
{
|
||||
/**
|
||||
* Latest shipping labels (100 per page).
|
||||
*
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function shippingLabels(array $parameters = []): Result
|
||||
{
|
||||
return $this->asApp()->request('GET', 'v1/admin/shipping-labels', null, $parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function shippingLabel(string $id): Result
|
||||
{
|
||||
return $this->asApp()->request('GET', sprintf('v1/admin/shipping-labels/%s', $id));
|
||||
}
|
||||
|
||||
/**
|
||||
* The label as PDF (binary string), null if there is none.
|
||||
*
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function shippingLabelPdf(string $id): ?string
|
||||
{
|
||||
$result = $this->asApp()->request('GET', sprintf('v1/admin/shipping-labels/%s/pdf', $id));
|
||||
|
||||
return $result->success() ? (string) $result->response()->getBody() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Buy a shipping label; the postage is billed to the given user.
|
||||
*
|
||||
* @param array{
|
||||
* box: array,
|
||||
* weight: int,
|
||||
* address: array,
|
||||
* billed_user_id: string,
|
||||
* test?: bool
|
||||
* } $attributes
|
||||
* - box: barcode, id, common_name or size of the box
|
||||
* - weight: in grams
|
||||
* - address: recipient address
|
||||
* - billed_user_id: Anikeen ID of the user who pays the postage
|
||||
* - test: validate only, no real label is bought
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function issueShippingLabel(array $attributes): Result
|
||||
{
|
||||
return $this->asApp()->request('POST', 'v1/admin/shipping-labels/issue', $attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the label with the scanned matrix code as shipped.
|
||||
*
|
||||
* @param list<string> $hex Hex bytes of the scanned Internetmarke matrix code
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function shipShippingLabel(array $hex): Result
|
||||
{
|
||||
return $this->asApp()->request('POST', 'v1/admin/shipping-labels/ship', ['hex' => $hex]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prices of the available franking types.
|
||||
*
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function shippingPriceList(): Result
|
||||
{
|
||||
return $this->asApp()->request('GET', 'v1/admin/shipping-labels/price-list');
|
||||
}
|
||||
}
|
||||
@@ -12,17 +12,16 @@ trait ManagesSshKeys
|
||||
use Get;
|
||||
|
||||
/**
|
||||
* Get currently authed user with Bearer Token.
|
||||
* SSH keys of a user, read with the app's own token (trusted apps, scope ssh-keys:read),
|
||||
* e.g. to install them on a new server.
|
||||
*
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function sshKeysByUserId(string $sskKeyId): SshKeys
|
||||
public function sshKeysByUserId(string $userId): SshKeys
|
||||
{
|
||||
if (!isset($this->sshKeysCache)) {
|
||||
$this->sshKeysCache = SshKeys::builder(fn() => $this->get(sprintf('v1/users/%s/ssh-keys/json', $sskKeyId)))
|
||||
->setParent($this);
|
||||
}
|
||||
$app = $this->asApp();
|
||||
|
||||
return $this->sshKeysCache;
|
||||
return SshKeys::builder(fn() => $app->get(sprintf('v1/client/users/%s/ssh-keys', $userId)))
|
||||
->setParent($app);
|
||||
}
|
||||
}
|
||||
@@ -1,38 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace Anikeen\Id\Concerns;
|
||||
|
||||
use Anikeen\Id\Resources\AppSubscriptions;
|
||||
use Anikeen\Id\Resources\Subscriptions;
|
||||
use Throwable;
|
||||
|
||||
trait ManagesSubscriptions
|
||||
{
|
||||
use HasBillable;
|
||||
|
||||
/**
|
||||
* Get subscriptions from the current user.
|
||||
*
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function subscriptions(): Subscriptions
|
||||
{
|
||||
if (!isset($this->subscriptionsCache)) {
|
||||
$this->subscriptionsCache = Subscriptions::builder(fn() => $this->anikeenId()
|
||||
->request('GET', 'v1/subscriptions'))
|
||||
->setBillable($this);
|
||||
}
|
||||
|
||||
return $this->subscriptionsCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* The user's subscriptions, managed with the app's own token instead of the user's
|
||||
* (trusted apps only, scope subscriptions:client). Use this in jobs and anywhere the user's
|
||||
* login may have expired.
|
||||
*/
|
||||
public function appSubscriptions(): AppSubscriptions
|
||||
{
|
||||
return new AppSubscriptions($this);
|
||||
}
|
||||
}
|
||||
<?php
|
||||
|
||||
namespace Anikeen\Id\Concerns;
|
||||
|
||||
use Anikeen\Id\Resources\AppSubscriptions;
|
||||
use Anikeen\Id\Resources\Subscriptions;
|
||||
use Throwable;
|
||||
|
||||
trait ManagesSubscriptions
|
||||
{
|
||||
use HasBillable;
|
||||
|
||||
/**
|
||||
* Get subscriptions from the current user.
|
||||
*
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function subscriptions(): Subscriptions
|
||||
{
|
||||
if (!isset($this->subscriptionsCache)) {
|
||||
$this->subscriptionsCache = Subscriptions::builder(fn() => $this->anikeenId()
|
||||
->request('GET', 'v1/subscriptions'))
|
||||
->setBillable($this);
|
||||
}
|
||||
|
||||
return $this->subscriptionsCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* The user's subscriptions, managed with the app's own token instead of the user's
|
||||
* (trusted apps only, scope subscriptions:client). Use this in jobs and anywhere the user's
|
||||
* login may have expired.
|
||||
*/
|
||||
public function appSubscriptions(): AppSubscriptions
|
||||
{
|
||||
return new AppSubscriptions($this);
|
||||
}
|
||||
}␍
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace Anikeen\Id\Concerns;
|
||||
|
||||
use Anikeen\Id\Result;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Orders and payments on behalf of a user. Trusted apps only, sent with the app's token.
|
||||
*/
|
||||
trait ManagesUserBilling
|
||||
{
|
||||
/**
|
||||
* Create an order for a user (VAT from the billing address). With `checkout`, it is paid right
|
||||
* away: with the given `transaction` (a payment captured elsewhere) or by charging the user's
|
||||
* default payment method (402 when declined; the order stays pending and is retried).
|
||||
*
|
||||
* @param string $userId The user's Anikeen ID.
|
||||
* @param array{
|
||||
* group?: string,
|
||||
* billing_address_id?: string,
|
||||
* billing_address?: array,
|
||||
* shipping_address_id?: string,
|
||||
* shipping_address?: array,
|
||||
* items: array<array{type: string, name: string, description?: string, price?: float|string, gross_price?: float|string, unit: string, units: int}>,
|
||||
* checkout?: bool,
|
||||
* transaction_id?: string,
|
||||
* transaction?: array,
|
||||
* invoice_date?: string,
|
||||
* currency_iso?: string
|
||||
* } $attributes
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function createOrderForUser(string $userId, array $attributes): Result
|
||||
{
|
||||
return $this->asApp()->request('POST', 'v1/admin/orders', [...$attributes, 'user_id' => $userId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Book a payment of a user that was captured elsewhere (e.g. PayPal, Ko-fi). The same
|
||||
* provider/payment_intent can only be booked once (409 with the existing transaction_id).
|
||||
*
|
||||
* @param string $userId The user's Anikeen ID.
|
||||
* @param array{
|
||||
* payment_provider?: string,
|
||||
* payment_intent?: string,
|
||||
* status: string,
|
||||
* type: string,
|
||||
* amount: float|string,
|
||||
* currency_iso: string,
|
||||
* created_at: string,
|
||||
* group?: string,
|
||||
* invoice_id?: string
|
||||
* } $attributes
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function createTransactionForUser(string $userId, array $attributes): Result
|
||||
{
|
||||
return $this->asApp()->request('POST', 'v1/admin/transactions', [...$attributes, 'user_id' => $userId]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Anikeen\Id\Concerns;
|
||||
|
||||
use Anikeen\Id\Resources\SshKeys;
|
||||
use Throwable;
|
||||
|
||||
trait ManagesUserSshKeys
|
||||
{
|
||||
use HasBillable;
|
||||
|
||||
/**
|
||||
* SSH keys of the current user (user token).
|
||||
*
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function sshKeys(): SshKeys
|
||||
{
|
||||
$anikeenId = $this->anikeenId();
|
||||
|
||||
return SshKeys::builder(fn() => $anikeenId->get('v1/ssh-keys'))->setParent($anikeenId);
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,36 @@ trait ManagesUsers
|
||||
*/
|
||||
public function createUser(array $attributes): Result
|
||||
{
|
||||
return $this->post('v1/users', $attributes);
|
||||
// the API takes a single display name
|
||||
if (!isset($attributes['name']) && (isset($attributes['first_name']) || isset($attributes['last_name']))) {
|
||||
$attributes['name'] = trim(($attributes['first_name'] ?? '') . ' ' . ($attributes['last_name'] ?? '')) ?: null;
|
||||
}
|
||||
|
||||
if (isset($attributes['password']) && !isset($attributes['password_confirmation'])) {
|
||||
$attributes['password_confirmation'] = $attributes['password'];
|
||||
}
|
||||
|
||||
unset($attributes['first_name'], $attributes['last_name']);
|
||||
|
||||
// finds the user by email or creates it (trusted apps, app token)
|
||||
return $this->asApp()->post('v1/admin/users', $attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the user of a social login or create it (trusted apps, app token).
|
||||
*
|
||||
* @param string $provider The socialite driver, e.g. "twitch".
|
||||
* @param string $providerId The user's ID at the provider.
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function findOrCreateSocialiteUser(string $provider, string $providerId, ?string $email = null, ?string $name = null): Result
|
||||
{
|
||||
return $this->asApp()->post('v1/admin/users/socialite', array_filter([
|
||||
'provider' => $provider,
|
||||
'provider_id' => $providerId,
|
||||
'email' => $email,
|
||||
'name' => $name,
|
||||
], fn ($value) => $value !== null));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -67,7 +96,8 @@ trait ManagesUsers
|
||||
*/
|
||||
public function isEmailExisting(string $email): Result
|
||||
{
|
||||
return $this->post('v1/users/check', [
|
||||
// `data.exists` tells whether an account uses the email (trusted apps, app token)
|
||||
return $this->asApp()->post('v1/admin/users/check', [
|
||||
'email' => $email,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -17,11 +17,11 @@ class Invoice extends BaseResource
|
||||
*
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function getInvoiceTemporaryUrl(): string
|
||||
public function getInvoiceTemporaryUrl(): ?string
|
||||
{
|
||||
return $this->billable->anikeenId()
|
||||
->request('PUT', sprintf('v1/invoices/%s', $this->id))
|
||||
->request('GET', sprintf('v1/invoices/%s', $this->id))
|
||||
->data
|
||||
->temporary_url;
|
||||
->temporary_url ?? null;
|
||||
}
|
||||
}
|
||||
}␍
|
||||
@@ -28,12 +28,19 @@ class OrderItems extends BaseCollection
|
||||
* }>
|
||||
* } $attributes The order data:
|
||||
* - items: Array of order items, each with type, name, description, price, unit, and quantity
|
||||
*
|
||||
* Only trusted apps may add items, and only to draft orders. Returns the first created item.
|
||||
*
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function create(string $orderId, array $attributes = []): OrderItem
|
||||
{
|
||||
return (new OrderItem(fn() => $this->billable->anikeenId()
|
||||
->request('POST', sprintf('v1/orders/%s', $orderId), $attributes)))
|
||||
return (new OrderItem(fn() => tap($this->billable->anikeenId()
|
||||
->request('POST', sprintf('v1/orders/%s/items', $orderId), $attributes), function ($result) {
|
||||
if (is_array($result->data)) {
|
||||
$result->data = $result->data[0] ?? null;
|
||||
}
|
||||
})))
|
||||
->setBillable($this->billable)
|
||||
->setParent($this->parent);
|
||||
}
|
||||
@@ -48,4 +55,4 @@ class OrderItems extends BaseCollection
|
||||
->setBillable($this->billable)
|
||||
->setParent($this->parent);
|
||||
}
|
||||
}
|
||||
}␍
|
||||
@@ -10,4 +10,16 @@ use Anikeen\Id\Concerns\HasBillable;
|
||||
class PaymentMethod extends BaseResource
|
||||
{
|
||||
use HasBillable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove this payment method from the current user.
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function delete(): bool
|
||||
{
|
||||
return $this->billable->anikeenId()
|
||||
->request('DELETE', sprintf('v1/payment-methods/%s', $this->id))
|
||||
->success();
|
||||
}
|
||||
}␍
|
||||
@@ -17,8 +17,18 @@ class SshKey extends BaseResource
|
||||
*
|
||||
* @throws Throwable
|
||||
*/
|
||||
/**
|
||||
* Rename the SSH key.
|
||||
*
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function update(string $name): self
|
||||
{
|
||||
return (new self(fn() => $this->parent->put(sprintf('v1/ssh-keys/%s', $this->id), ['name' => $name])))->setParent($this->parent);
|
||||
}
|
||||
|
||||
public function delete(): bool
|
||||
{
|
||||
return $this->parent->delete(sprintf('v1/ssh-keys/%s', $this->id))->success();
|
||||
}
|
||||
}
|
||||
}␍
|
||||
@@ -29,6 +29,6 @@ class SshKeys extends BaseCollection
|
||||
*/
|
||||
public function find(string $id): ?SshKey
|
||||
{
|
||||
return (new SshKey(fn() => $this->parent->get(sprintf('v1/ssh-keys/%s', $id))));
|
||||
return (new SshKey(fn() => $this->parent->get(sprintf('v1/ssh-keys/%s', $id))))->setParent($this->parent);
|
||||
}
|
||||
}
|
||||
}␍
|
||||
Reference in new issue
Block a user