Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions docs/printnode/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,58 @@ $client->printJobs->create([...], opts: [

> {tip} The `$opts` argument is accepted in most method calls to the API when using the `Printing` facade as well.

### Integrator / Child Accounts

If your PrintNode account is an Integrator account, you can act on behalf of a child account by passing one of these keys in `$opts` (the client will translate them to the appropriate `X-Child-Account-*` headers):

- `child_account_by_id` → sets `X-Child-Account-By-Id`
- `child_account_by_email` → sets `X-Child-Account-By-Email`
- `child_account_by_creator_ref` → sets `X-Child-Account-By-CreatorRef`

Example: list printers for a child account identified by email:

```php
$client->printers->all(opts: [
'child_account_by_email' => '[email protected]',
]);
```

### Managing Accounts (Integrator only)

Use the `accounts` service for account creation and management:

```php
// Create a child account
$created = $client->accounts->create([
'Account' => [
'firstname' => '-',
'lastname' => '-',
'email' => '[email protected]',
'password' => 'securepassword',
'creatorRef' => 'customer-123',
],
'ApiKeys' => ['development', 'production'],
'Tags' => ['plan' => 'pro'],
]);

// Update a child account (targeted by email)
$updated = $client->accounts->update([
'email' => '[email protected]',
], opts: [
'child_account_by_email' => '[email protected]',
]);

// Suspend a child account
$client->accounts->setState('suspended', opts: [
'child_account_by_id' => 123,
]);

// Delete a child account
$client->accounts->delete(opts: [
'child_account_by_creator_ref' => 'customer-123',
]);
```

## Pagination Params

For requests that can be paginated, here are the supported array key values that can be sent through with a `$params` argument:
Expand Down
22 changes: 19 additions & 3 deletions src/Api/PrintNode/PrintNodeApiRequestor.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public function __construct(
$this->apiBase = $apiBase;
}

public function request(string $method, string $url, array $params = [], ?array $headers = []): array
public function request(string $method, string $url, array|string $params = [], ?array $headers = []): array
{
[$absoluteUrl, $headers, $params, $apiKey] = $this->prepareRequest($method, $url, $params, $headers);

Expand All @@ -48,10 +48,21 @@ public function request(string $method, string $url, array $params = [], ?array

$client = $this->httpClient()->withHeaders($headers);

// Send JSON for write operations when params are arrays
if (in_array(strtolower($method), ['post', 'put', 'patch'], true) && is_array($params)) {
$client = $client->asJson();
}

$response = match (strtolower($method)) {
'get' => $client->get($absoluteUrl, $params),
'get' => $client->get($absoluteUrl, is_array($params) ? $params : []),
'post' => $client->post($absoluteUrl, $params),
'delete' => $client->delete($absoluteUrl, $params),
'delete' => $client->delete($absoluteUrl, is_array($params) ? $params : []),
'put' => is_string($params)
? $client->withBody($params, 'application/json')->send('PUT', $absoluteUrl)
: $client->put($absoluteUrl, $params),
'patch' => is_string($params)
? $client->withBody($params, 'application/json')->send('PATCH', $absoluteUrl)
: $client->patch($absoluteUrl, $params),
};

$body = $this->interpretResponse($response);
Expand Down Expand Up @@ -160,6 +171,11 @@ private function interpretResponse(Response $response): array|int
);
}

// Some endpoints return no content (e.g., 204) - normalize to empty array
if ($response->status() === 204) {
return [];
}

return $response->json();
}

Expand Down
1 change: 1 addition & 0 deletions src/Api/PrintNode/PrintNodeClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
* @property-read \Rawilk\Printing\Api\PrintNode\Service\PrinterService $printers
* @property-read \Rawilk\Printing\Api\PrintNode\Service\PrintJobService $printJobs
* @property-read \Rawilk\Printing\Api\PrintNode\Service\WhoamiService $whoami
* @property-read \Rawilk\Printing\Api\PrintNode\Service\AccountService $accounts
*/
class PrintNodeClient extends BasePrintNodeClient
{
Expand Down
48 changes: 48 additions & 0 deletions src/Api/PrintNode/Service/AccountService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

declare(strict_types=1);

namespace Rawilk\Printing\Api\PrintNode\Service;

use Rawilk\Printing\Api\PrintNode\Resources\Whoami;
use Rawilk\Printing\Api\PrintNode\Util\RequestOptions;

class AccountService extends AbstractService
{
/**
* Create a child account (Integrator accounts only).
*
* @param array{Account: array<string, mixed>, ApiKeys?: array<int, string>, Tags?: array<string, mixed>} $params
*/
public function create(array $params, null|array|RequestOptions $opts = null): Whoami
{
return $this->request('post', '/account', $params, opts: $opts, expectedResource: Whoami::class);
}

/**
* Update a child account's profile (target via child account headers in $opts).
*/
public function update(array $params, null|array|RequestOptions $opts = null): Whoami
{
return $this->request('patch', '/account', $params, opts: $opts, expectedResource: Whoami::class);
}

/**
* Set account state for a child account. Send body as a JSON string: "active" or "suspended".
*/
public function setState(string $state, null|array|RequestOptions $opts = null): array
{
// The API expects a raw JSON string body, e.g., "active" or "suspended"
return $this->request('put', '/account/state', json_encode($state, JSON_THROW_ON_ERROR), opts: $opts);
}

/**
* Delete a child account. Target via child account headers in $opts.
* Returns an array of affected IDs per API conventions.
*/
public function delete(null|array|RequestOptions $opts = null): array
{
return $this->request('delete', '/account', [], opts: $opts);
}
}

2 changes: 2 additions & 0 deletions src/Api/PrintNode/Service/ServiceFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
* @property-read \Rawilk\Printing\Api\PrintNode\Service\PrinterService $printers
* @property-read \Rawilk\Printing\Api\PrintNode\Service\PrintJobService $printJobs
* @property-read \Rawilk\Printing\Api\PrintNode\Service\WhoamiService $whoami
* @property-read \Rawilk\Printing\Api\PrintNode\Service\AccountService $accounts
*/
class ServiceFactory
{
Expand All @@ -27,6 +28,7 @@ class ServiceFactory
'printers' => PrinterService::class,
'printJobs' => PrintJobService::class,
'whoami' => WhoamiService::class,
'accounts' => AccountService::class,
];

public function __construct(protected PrintNodeClientInterface $client)
Expand Down
16 changes: 16 additions & 0 deletions src/Api/PrintNode/Util/RequestOptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,22 @@ public static function parse(RequestOptions|array|string|null $options, bool $st
unset($options['api_base']);
}

// Integrator/Child Account impersonation headers
if (array_key_exists('child_account_by_id', $options)) {
$headers['X-Child-Account-By-Id'] = (string) $options['child_account_by_id'];
unset($options['child_account_by_id']);
}

if (array_key_exists('child_account_by_email', $options)) {
$headers['X-Child-Account-By-Email'] = (string) $options['child_account_by_email'];
unset($options['child_account_by_email']);
}

if (array_key_exists('child_account_by_creator_ref', $options)) {
$headers['X-Child-Account-By-CreatorRef'] = (string) $options['child_account_by_creator_ref'];
unset($options['child_account_by_creator_ref']);
}

if ($strict && ! empty($options)) {
$message = 'Got unexpected keys in options array: ' . implode(', ', array_keys($options));

Expand Down
84 changes: 84 additions & 0 deletions tests/Feature/Api/PrintNode/Service/AccountServiceTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php

declare(strict_types=1);

use Illuminate\Http\Client\Request;
use Illuminate\Support\Facades\Http;
use Rawilk\Printing\Api\PrintNode\PrintNodeClient;
use Rawilk\Printing\Api\PrintNode\Resources\Whoami;
use Rawilk\Printing\Api\PrintNode\Service\AccountService;
use Rawilk\Printing\Tests\Feature\Api\PrintNode\FakesPrintNodeRequests;

uses(FakesPrintNodeRequests::class);

beforeEach(function () {
Http::preventStrayRequests();
$this->fakeRequests();

$client = new PrintNodeClient(['api_key' => 'my-key']);
$this->service = new AccountService($client);
});

it('creates a child account', function () {
$this->fakeRequest('whoami');

$result = $this->service->create([
'Account' => [
'firstname' => '-',
'lastname' => '-',
'email' => '[email protected]',
'password' => 'password1234',
'creatorRef' => 'child-1',
],
'ApiKeys' => ['dev'],
'Tags' => ['tier' => 'premium'],
]);

expect($result)->toBeInstanceOf(Whoami::class);
});

it('updates a child account with headers', function () {
$this->fakeRequest('whoami', expectation: function (Request $request) {
expect($request->method())->toBe('PATCH')
->and($request->header('X-Child-Account-By-Email'))->toBe(['[email protected]']);
});

$result = $this->service->update([
'email' => '[email protected]',
], opts: [
'child_account_by_email' => '[email protected]',
]);

expect($result)->toBeInstanceOf(Whoami::class);
});

it('sets account state with raw body', function () {
$this->fakeRequest(function () {
return [];
}, code: 204, expectation: function (Request $request) {
expect($request->method())->toBe('PUT')
->and($request->url())->toEndWith('/account/state')
->and($request->body())->toBe('"suspended"');
});

$response = $this->service->setState('suspended', opts: [
'child_account_by_id' => 123,
]);

expect($response)->toBeArray()->toBeEmpty();
});

it('deletes a child account', function () {
$this->fakeRequest(function () {
return ['affected' => [123]];
}, expectation: function (Request $request) {
expect($request->method())->toBe('DELETE');
});

$response = $this->service->delete(opts: [
'child_account_by_creator_ref' => 'child-1',
]);

expect($response)->toBeArray();
});

4 changes: 3 additions & 1 deletion tests/Feature/Api/PrintNode/Service/ServiceFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Rawilk\Printing\Api\PrintNode\PrintNodeClient;
use Rawilk\Printing\Api\PrintNode\Service\ServiceFactory;
use Rawilk\Printing\Api\PrintNode\Service\AccountService;
use Rawilk\Printing\Api\PrintNode\Service\WhoamiService;

beforeEach(function () {
Expand All @@ -12,7 +13,8 @@
});

it('exposes properties for services', function () {
expect($this->serviceFactory->whoami)->toBeInstanceOf(WhoamiService::class);
expect($this->serviceFactory->whoami)->toBeInstanceOf(WhoamiService::class)
->and($this->serviceFactory->accounts)->toBeInstanceOf(AccountService::class);
});

test('multiple calls return the same instance', function () {
Expand Down
17 changes: 17 additions & 0 deletions tests/Feature/Api/PrintNode/Util/RequestOptionsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,23 @@
->apiBase->toBe('https://example.com');
});

it('parses child account header options', function () {
$opts = RequestOptions::parse([
'child_account_by_id' => 123,
'child_account_by_email' => '[email protected]',
'child_account_by_creator_ref' => 'child-ref',
]);

expect($opts)
->apiKey->toBeNull()
->headers->toEqualCanonicalizing([
'X-Child-Account-By-Id' => '123',
'X-Child-Account-By-Email' => '[email protected]',
'X-Child-Account-By-CreatorRef' => 'child-ref',
])
->apiBase->toBeNull();
});

it('can merge options', function () {
$baseOpts = RequestOptions::parse([
'api_key' => 'foo',
Expand Down