mirror of
https://github.com/anikeen-com/id.git
synced 2026-10-31 20:59:32 +00:00
Read the orders that bill the app's subscriptions
$user->appOrders() and app(AnikeenId::class)->appOrders() read v1/client/orders with the app's own token (scope orders:client): all(), pending() and find(), paginated transparently, user scoped when called on a billable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9613d31a8d
commit
820f1b37cc
6 files changed
+208
No files matched your search
@@ -142,6 +142,20 @@ try {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### App orders (trusted apps, no user token)
|
||||||
|
|
||||||
|
Orders that bill the app's subscriptions, read with the app's token (scope `orders:client`):
|
||||||
|
|
||||||
|
```php
|
||||||
|
$team->owner->appOrders()->pending(); // unpaid orders of one user, oldest first
|
||||||
|
app(AnikeenId::class)->appOrders()->pending(); // of all users
|
||||||
|
$team->owner->appOrders()->find($orderId); // null if unknown or not this user's
|
||||||
|
```
|
||||||
|
|
||||||
|
Each order has `status` (`processing` = current month, still collecting; `pending` = closed and not
|
||||||
|
paid; `completed`; `cancelled`), the totals, `payment_attempts`, `next_payment_attempt_at` (null =
|
||||||
|
no more automatic retries) and `last_payment_error`.
|
||||||
|
|
||||||
### Change the default access token / refresh token field name
|
### Change the default access token / refresh token field name
|
||||||
|
|
||||||
If you access / refresh token fields differs from the default `anikeen_id_access_token` / `anikeen_id_refresh_token`, you can specify the field name in the `AppServiceProvider` boot method:
|
If you access / refresh token fields differs from the default `anikeen_id_access_token` / `anikeen_id_refresh_token`, you can specify the field name in the `AppServiceProvider` boot method:
|
||||||
@@ -362,6 +376,7 @@ public function shippingPriceList(): Result
|
|||||||
### ManagesUserBilling
|
### ManagesUserBilling
|
||||||
|
|
||||||
```php
|
```php
|
||||||
|
public function appOrders(): AppOrders
|
||||||
public function createOrderForUser(string $userId, array $attributes): Result
|
public function createOrderForUser(string $userId, array $attributes): Result
|
||||||
public function createTransactionForUser(string $userId, array $attributes): Result
|
public function createTransactionForUser(string $userId, array $attributes): Result
|
||||||
```
|
```
|
||||||
@@ -464,6 +479,7 @@ public function getBillable(): Billable
|
|||||||
```php
|
```php
|
||||||
public function subscriptions(): Subscriptions
|
public function subscriptions(): Subscriptions
|
||||||
public function appSubscriptions(): AppSubscriptions
|
public function appSubscriptions(): AppSubscriptions
|
||||||
|
public function appOrders(): AppOrders
|
||||||
public function setBillable(Billable|Model $billable): self
|
public function setBillable(Billable|Model $billable): self
|
||||||
public function getBillable(): Billable
|
public function getBillable(): Billable
|
||||||
```
|
```
|
||||||
|
|||||||
+14
@@ -142,6 +142,20 @@ try {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### App orders (trusted apps, no user token)
|
||||||
|
|
||||||
|
Orders that bill the app's subscriptions, read with the app's token (scope `orders:client`):
|
||||||
|
|
||||||
|
```php
|
||||||
|
$team->owner->appOrders()->pending(); // unpaid orders of one user, oldest first
|
||||||
|
app(AnikeenId::class)->appOrders()->pending(); // of all users
|
||||||
|
$team->owner->appOrders()->find($orderId); // null if unknown or not this user's
|
||||||
|
```
|
||||||
|
|
||||||
|
Each order has `status` (`processing` = current month, still collecting; `pending` = closed and not
|
||||||
|
paid; `completed`; `cancelled`), the totals, `payment_attempts`, `next_payment_attempt_at` (null =
|
||||||
|
no more automatic retries) and `last_payment_error`.
|
||||||
|
|
||||||
### Change the default access token / refresh token field name
|
### Change the default access token / refresh token field name
|
||||||
|
|
||||||
If you access / refresh token fields differs from the default `anikeen_id_access_token` / `anikeen_id_refresh_token`, you can specify the field name in the `AppServiceProvider` boot method:
|
If you access / refresh token fields differs from the default `anikeen_id_access_token` / `anikeen_id_refresh_token`, you can specify the field name in the `AppServiceProvider` boot method:
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace Anikeen\Id\Concerns;
|
namespace Anikeen\Id\Concerns;
|
||||||
|
|
||||||
|
use Anikeen\Id\Resources\AppOrders;
|
||||||
use Anikeen\Id\Resources\AppSubscriptions;
|
use Anikeen\Id\Resources\AppSubscriptions;
|
||||||
use Anikeen\Id\Resources\Subscriptions;
|
use Anikeen\Id\Resources\Subscriptions;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
@@ -35,4 +36,13 @@ trait ManagesSubscriptions
|
|||||||
{
|
{
|
||||||
return new AppSubscriptions($this);
|
return new AppSubscriptions($this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user's orders that bill the app's subscriptions, read with the app's own token
|
||||||
|
* (trusted apps only, scope orders:client), e.g. ->pending() for unpaid ones.
|
||||||
|
*/
|
||||||
|
public function appOrders(): AppOrders
|
||||||
|
{
|
||||||
|
return new AppOrders($this);
|
||||||
|
}
|
||||||
}␍
|
}␍
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace Anikeen\Id\Concerns;
|
namespace Anikeen\Id\Concerns;
|
||||||
|
|
||||||
|
use Anikeen\Id\Resources\AppOrders;
|
||||||
use Anikeen\Id\Result;
|
use Anikeen\Id\Result;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
|
|
||||||
@@ -10,6 +11,14 @@ use Throwable;
|
|||||||
*/
|
*/
|
||||||
trait ManagesUserBilling
|
trait ManagesUserBilling
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* Orders of all users that bill the app's subscriptions (e.g. ->pending() for unpaid ones).
|
||||||
|
*/
|
||||||
|
public function appOrders(): AppOrders
|
||||||
|
{
|
||||||
|
return new AppOrders();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an order for a user (VAT from the billing address). With `checkout`, it is paid right
|
* 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
|
* away: with the given `transaction` (a payment captured elsewhere) or by charging the user's
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Anikeen\Id\Resources;
|
||||||
|
|
||||||
|
use Anikeen\Id\AnikeenId;
|
||||||
|
use Anikeen\Id\Contracts\Billable;
|
||||||
|
use Anikeen\Id\Exceptions\ApiException;
|
||||||
|
use Anikeen\Id\Support\AppRequest;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Orders that bill the app's subscriptions, read with the app's own token (trusted apps,
|
||||||
|
* scope orders:client) - of one user ($user->appOrders()) or of all users
|
||||||
|
* (app(AnikeenId::class)->appOrders()). Nothing is requested until a method is called.
|
||||||
|
*
|
||||||
|
* Each order is an object with id, user_id, group, status (processing|pending|completed|cancelled),
|
||||||
|
* is_collective, net_total, vat_total, gross_total, currency_iso, created_at, paid_at,
|
||||||
|
* payment_attempts, next_payment_attempt_at and last_payment_error ({code, message} or null).
|
||||||
|
*/
|
||||||
|
class AppOrders
|
||||||
|
{
|
||||||
|
public function __construct(protected Billable|Model|null $billable = null)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All matching orders, oldest first.
|
||||||
|
*
|
||||||
|
* @param array{status?: string, group?: string} $filters
|
||||||
|
* @return list<object>
|
||||||
|
*
|
||||||
|
* @throws ApiException
|
||||||
|
*/
|
||||||
|
public function all(array $filters = []): array
|
||||||
|
{
|
||||||
|
$orders = [];
|
||||||
|
$page = 1;
|
||||||
|
|
||||||
|
do {
|
||||||
|
$result = AppRequest::send('GET', 'v1/client/orders', null, [
|
||||||
|
...$filters,
|
||||||
|
...($this->billable ? ['user_id' => $this->userId()] : []),
|
||||||
|
'per_page' => 100,
|
||||||
|
'page' => $page,
|
||||||
|
]);
|
||||||
|
|
||||||
|
array_push($orders, ...$result->data);
|
||||||
|
$lastPage = (int) ($result->meta->last_page ?? $page);
|
||||||
|
} while ($page++ < $lastPage);
|
||||||
|
|
||||||
|
return $orders;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closed orders that aren't paid (yet), oldest first.
|
||||||
|
*
|
||||||
|
* @return list<object>
|
||||||
|
*
|
||||||
|
* @throws ApiException
|
||||||
|
*/
|
||||||
|
public function pending(array $filters = []): array
|
||||||
|
{
|
||||||
|
return $this->all([...$filters, 'status' => 'pending']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws ApiException
|
||||||
|
*/
|
||||||
|
public function find(string $id): ?object
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$order = AppRequest::send('GET', sprintf('v1/client/orders/%s', $id))->data;
|
||||||
|
} catch (ApiException $e) {
|
||||||
|
if ($e->isNotFound()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !$this->billable || $order->user_id === $this->userId() ? $order : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function userId(): string
|
||||||
|
{
|
||||||
|
return (new AppSubscriptions($this->billable))->userId();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Anikeen\Id\Tests;
|
||||||
|
|
||||||
|
use Anikeen\Id\AnikeenId;
|
||||||
|
use Anikeen\Id\Billable;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class AppOrdersTest extends TestCase
|
||||||
|
{
|
||||||
|
private function owner(): Model
|
||||||
|
{
|
||||||
|
$owner = new class extends Model {
|
||||||
|
use Billable;
|
||||||
|
|
||||||
|
protected $guarded = [];
|
||||||
|
};
|
||||||
|
$owner->anikeen_id = '01JUSER';
|
||||||
|
|
||||||
|
return $owner;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function page(array $orders, int $page = 1, int $last = 1): array
|
||||||
|
{
|
||||||
|
return ['data' => $orders, 'meta' => ['current_page' => $page, 'last_page' => $last, 'total' => count($orders)], 'links' => ['next' => null]];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function requestQuery(int $index): array
|
||||||
|
{
|
||||||
|
parse_str($this->sent[$index]['request']->getUri()->getQuery(), $query);
|
||||||
|
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_pending_orders_of_a_user(): void
|
||||||
|
{
|
||||||
|
$this->respond(200, $this->page([['id' => 'o1', 'user_id' => '01JUSER', 'status' => 'pending', 'gross_total' => 11.9, 'next_payment_attempt_at' => null, 'payment_attempts' => 4]]));
|
||||||
|
|
||||||
|
$orders = $this->owner()->appOrders()->pending();
|
||||||
|
|
||||||
|
$this->assertSame('o1', $orders[0]->id);
|
||||||
|
$this->assertSame(4, $orders[0]->payment_attempts);
|
||||||
|
$request = $this->sent[0]['request'];
|
||||||
|
$this->assertSame('GET /api/v1/client/orders Bearer app-token', $request->getMethod() . ' ' . $request->getUri()->getPath() . ' ' . $request->getHeaderLine('Authorization'));
|
||||||
|
$this->assertSame(['status' => 'pending', 'user_id' => '01JUSER', 'per_page' => '100', 'page' => '1'], $this->requestQuery(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_pending_orders_of_all_users_follow_pages(): void
|
||||||
|
{
|
||||||
|
$this->respond(200, $this->page([['id' => 'o1', 'user_id' => 'a']], 1, 2));
|
||||||
|
$this->respond(200, $this->page([['id' => 'o2', 'user_id' => 'b']], 2, 2));
|
||||||
|
|
||||||
|
$orders = app(AnikeenId::class)->appOrders()->pending(['group' => 'team-1']);
|
||||||
|
|
||||||
|
$this->assertSame(['o1', 'o2'], array_map(fn ($o) => $o->id, $orders));
|
||||||
|
$this->assertSame(['group' => 'team-1', 'status' => 'pending', 'per_page' => '100', 'page' => '2'], $this->requestQuery(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_find_returns_null_for_unknown_and_other_users_orders(): void
|
||||||
|
{
|
||||||
|
$this->respond(404, ['message' => 'Order not found.', 'error' => 'order_not_found']);
|
||||||
|
$this->respond(200, ['data' => ['id' => 'o1', 'user_id' => 'someone-else']]);
|
||||||
|
$this->respond(200, ['data' => ['id' => 'o1', 'user_id' => '01JUSER']]);
|
||||||
|
|
||||||
|
$orders = $this->owner()->appOrders();
|
||||||
|
|
||||||
|
$this->assertNull($orders->find('unknown'));
|
||||||
|
$this->assertNull($orders->find('o1'));
|
||||||
|
$this->assertSame('o1', $orders->find('o1')->id);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user