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:
Maurice PreußandClaude Opus 5.5 committed 2026-10-25 17:18:05 +01:00
1 parent 8de737641e
commit 9613d31a8d
22 files changed
+720 -108

No files matched your search

+18 -8
View File
@@ -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);
}
}
}␍
+7 -1
View File
@@ -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);
}
}
}␍
+29
View File
@@ -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');
}
}
+85
View File
@@ -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');
}
}
+6 -7
View File
@@ -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);
}
}
+38 -38
View File
@@ -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);
}
}␍
+61
View File
@@ -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]);
}
}
+23
View File
@@ -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);
}
}
+32 -2
View File
@@ -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,
]);
}