billwerk_subscriptions-1.x-dev/src/Api.php
src/Api.php
<?php
declare(strict_types=1);
namespace Drupal\billwerk_subscriptions;
use Drupal\Component\Serialization\Json;
use Drupal\billwerk_subscriptions\Exception\ApiException;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Psr7\Message;
use GuzzleHttp\Psr7\Request;
use Psr\Http\Client\ClientInterface;
/**
* API to interact with the SaaS API of Billwerk via REST.
*
* This service class provides the means to actually connect to Billwerk and do
* thing like:
* * Authorize against the SaaS api
* * Find and retrieve SaaS data
* * Send requests to the SaaS api
* * Handle responses and basic error situations.
*
* Originally inspired by
* https://github.com/billwerk/billwerkjs-jquery-php/blob/master/iteroapi.php
*/
class Api {
/**
* Amount of seconds the authentication will be cached.
*
* @var int
* Seconds until cache expiration.
*/
protected static $authCacheExpiration = 60 * 60;
const AUTH_PREFIX = 'Authorization: Bearer ';
const AUTH_CACHE_KEY = 'BillwerkAp::auth_token';
const RESPONSE_CACHE_NAME = 'BillwerkApi::get::requestCache';
const ERROR_STREAM_CACHE_NAME = 'BillwerkApi::getErrorStream';
/**
* The constructor.
*
* @param \GuzzleHttp\Client $httpClient
* The HTTP client.
* @param \Drupal\billwerk_subscriptions\Environment $environment
* The environment.
* @param \Drupal\billwerk_subscriptions\SettingsHelper $settingsHelper
* The settings helper.
* @param \Drupal\billwerk_subscriptions\LogHelper $logHelper
* The log helper.
* @param \Drupal\billwerk_subscriptions\CacheHelper $cacheHelper
* The cache helper.
*/
public function __construct(
protected readonly ClientInterface $httpClient,
protected readonly Environment $environment,
protected readonly SettingsHelper $settingsHelper,
protected readonly LogHelper $logHelper,
protected readonly CacheHelper $cacheHelper,
) {
}
/**
* Ensures there has been an authentication towards the API.
*/
protected function ensureAuthorization($environment = NULL): void {
if (empty($this->getAuthToken())) {
$client_id = $this->environment->getClientId();
$client_secret = $this->environment->getClientSecret();
$auth = $this->authorize($client_id, $client_secret);
$this->setAuthToken($auth);
}
}
/**
* Retrieves auth token from billwerk API.
*
* @param string $clientId
* The client ID to be used for authentication.
* @param string $clientSecret
* The client secret to be used for authentication.
*
* @return string
* The authorization token obtained.
*/
protected function authorize(string $clientId, string $clientSecret): string {
$data = ['grant_type' => 'client_credentials'];
try {
$response = $this->httpClient->request('POST', $this->environment->getApiUrl(Environment::API_OAUTH_PATH), [
'auth' => [$clientId, $clientSecret],
'headers' => [
'Content-Type' => 'application/x-www-form-urlencoded',
],
'form_params' => $data,
]);
}
catch (ClientException $e) {
$this->handleClientException($e);
throw new ApiException('Could not authorize with provider.');
}
$body = $response->getBody();
// Save auth token.
$token = json_decode($body->getContents());
return self::AUTH_PREFIX . $token->access_token;
}
/**
* Returns the current token for authentication.
*
* @return string|null
* The current authorization token.
*/
protected function getAuthToken(): ?string {
// Retrieve cached auth token.
$auth = $this->cacheHelper->load(self::AUTH_CACHE_KEY);
return $auth;
}
/**
* Stores the given token to be used as current authentication token.
*
* For not running into quota limits for auth token retrieval,
* we store a retrieved auth token in cache for some time (1h by default).
* Compare https://developer.billwerk.io/Docs/ApiIntroduction#authentication
* By choosing a cache duration that is less than the SaaS own expiration
* time, we practically circumvent the need for complex detection of
* invalid-auth responses, re-authentication and re-sending of quests.
*
* @param string $auth
* The authorization token to be set.
*/
protected function setAuthToken(string $auth): void {
// Cache auth token.
$this->cacheHelper->save(self::AUTH_CACHE_KEY, $auth, time() + self::$authCacheExpiration);
// Log retrieval/caching of new auth token:
$this->logHelper->debug(
__CLASS__ . '::' . __FUNCTION__ . ': Authentication token retrieved and cached for @time seconds.',
['@time' => self::$authCacheExpiration],
['auth token excerpt' => substr($auth, strlen(self::AUTH_PREFIX), 5) . '...' . substr($auth, -6)],
'api'
);
}
/**
* Issue a GET request to the API.
*
* Use drupal_static temporary static caching of responses during one Drupal
* execution, to minimize API calls.
*
* @param string $resource
* The URL of the resource to get.
*
* @return mixed
* The retrieved data / result.
*/
private function get(string $resource): mixed {
$cache_key = 'GET:' . $resource;
$cache = $this->settingsHelper->getApiResponseCacheEnabled();
$responseCache = &drupal_static(self::RESPONSE_CACHE_NAME, []);
$result = !$cache ? NULL : ($responseCache[$cache_key] ?? NULL);
$in_cache = !empty($result);
// If request is not in cache, perform real request.
if (!$in_cache) {
// Make sure we're authorized.
$this->ensureAuthorization();
// Assemble request options.
$token = str_replace('Authorization: Bearer ', '', $this->getAuthToken());
$guzzle_options = [
'headers' => [
'Authorization' => 'Bearer ' . $token,
'Accept' => 'application/json',
'Content-Type' => 'application/x-www-form-urlencoded',
],
'debug' => FALSE,
'stream' => FALSE,
];
// Send the request and get the response object.
$url = $this->environment->getApiUrl($resource);
$response = $this->httpClient->get($url, $guzzle_options);
$status = $response->getStatusCode();
// @improve check whether other status codes have to be considered ok:
$status_ok = $status == 200;
if ($status_ok) {
// Get json string from response.
$result = $response->getBody()->getContents();
// Cache response, if caching requests is enabled.
if ($cache) {
$responseCache[$cache_key] = $result;
}
}
// Log.
$this->logHelper->log(
__CLASS__ . '::' . __FUNCTION__ . ': Sending GET request to @url. Status @status "@reason". Result size in bytes: @size',
[
'@url' => $url,
'@status' => $status,
'@reason' => $response->getReasonPhrase(),
'@size' => strlen($result),
],
[],
'api',
$status_ok ? 'debug' : 'error'
);
}
// Convert result from json string to PHP structure.
return Json::decode($result);
}
/**
* Issue a POST request to the API.
*
* @param string $resource
* The URL of the resource to POST to.
* @param mixed $data
* The data to post.
*
* @return mixed
* The result of the operation.
*/
private function post(string $resource, $data = NULL): mixed {
$this->ensureAuthorization();
if (is_array($data) && empty($data)) {
// Billwerk expects objects instead of an empty array!
$data = new \stdClass();
}
$json = json_encode($data);
$token = str_replace('Authorization: Bearer ', '', $this->getAuthToken());
$headers = [
'Authorization' => 'Bearer ' . $token,
'Content-Type' => 'application/json',
'Accept' => 'application/json',
'Content-Length' => strlen($json),
];
$url = $this->environment->getApiUrl($resource);
$request = new Request('POST', $url, $headers, $json);
// No status code checks required, 4xx and 5xx will throw errors.
$response = $this->httpClient->send($request);
// Get json string from response.
return json_decode($response->getBody()->getContents());
}
/**
* Issue a PUT request to the API.
*
* @param string $resource
* The URL of the resource to put to.
* @param mixed $data
* The data to replace the resource with.
*
* @return mixed
* The result of the operation.
*/
private function put(string $resource, $data): mixed {
$this->ensureAuthorization();
if (is_array($data) && empty($data)) {
// Billwerk expects objects instead of an empty array!
$data = new \stdClass();
}
$json = json_encode($data);
$token = str_replace('Authorization: Bearer ', '', $this->getAuthToken());
$headers = [
'Authorization' => 'Bearer ' . $token,
'Content-Type' => 'application/json',
'Accept' => 'application/json',
'Content-Length' => strlen($json),
];
$url = $this->environment->getApiUrl($resource);
$request = new Request('PUT', $url, $headers, $json);
// No status code checks required, 4xx and 5xx will throw errors.
$response = $this->httpClient->send($request);
// Get json string from response.
return json_decode($response->getBody()->getContents());
}
/**
* Issue a PATCH request to the API.
*
* @param string $resource
* The URL of the resource to patch to.
* @param mixed $data
* The data to patch.
*
* @return mixed
* The result of the operation.
*/
private function patch(string $resource, $data): mixed {
$this->ensureAuthorization();
if (is_array($data) && empty($data)) {
// Billwerk expects objects instead of an empty array!
$data = new \stdClass();
}
$json = json_encode($data);
$token = str_replace('Authorization: Bearer ', '', $this->getAuthToken());
$headers = [
'Authorization' => 'Bearer ' . $token,
'Content-Type' => 'application/json',
'Accept' => 'application/json',
'Content-Length' => strlen($json),
];
$url = $this->environment->getApiUrl($resource);
$request = new Request('PATCH', $url, $headers, $json);
// No status code checks required, 4xx and 5xx will throw errors.
$response = $this->httpClient->send($request);
// Get json string from response:
return json_decode($response->getBody()->getContents());
}
/**
* Issue a DELETE request to the API.
*
* @param string $resource
* The URL of the resource to put to.
*
* @return mixed
* The result of the operation.
*/
private function delete(string $resource): mixed {
$this->ensureAuthorization();
$token = str_replace('Authorization: Bearer ', '', $this->getAuthToken());
$headers = [
'Authorization' => 'Bearer ' . $token,
'Content-Type' => 'application/json',
'Accept' => 'application/json',
];
$request = new Request('DELETE', $resource, $headers);
// No status code checks required, 4xx and 5xx will throw errors.
$response = $this->httpClient->send($request);
// Get json string from response:
return json_decode($response->getBody()->getContents());
}
/**
* Handles a client exception.
*
* @param \GuzzleHttp\Exception\ClientException $e
* The exception to handle.
*/
protected function handleClientException(ClientException $e): void {
// An exception was raised but there is an HTTP response body
// with the exception (in case of 404 and similar errors)
$request = $e->getRequest();
$response = $e->getResponse();
// @todo Do we need to remove credentials here from the request to not log them?
$this->logHelper->error('API Client Exception: "%response" (Statuscode: "%statuscode" %reasonphrase). Request: "%request".',
[
"%response" => Message::toString($response),
"%statuscode" => $response->getStatusCode(),
"%reasonphrase" => $response->getReasonPhrase(),
"%request" => Message::toString($request),
],
[],
'api');
}
/**
* Retrieves token for accessing hosted self service pages.
*
* @param string $contractId
* The contract to get the selfservice token for.
*
* @return array
* The selfservice token information array retrieved.
* Contains "Expiry", "Token", "Purpose", "Url".
*/
public function getSelfserviceTokenArray(string $contractId): array {
return $this->get(Environment::API_CONTRACTS_RESOURCE_PATH . "/{$contractId}/selfServiceToken");
}
/**
* Gets the individual selfservice Token for the $contractId.
*
* @param string $contractId
* The contract id to get the selfservice token for.
*
* @return ?string
* The selfservice token or NULL if not set.
*/
public function getSelfserviceToken(string $contractId): ?string {
$response = $this->getSelfserviceTokenArray($contractId);
return $response['Token'] ?? NULL;
}
/**
* Gets the individual selfservice URL for the $contractId.
*
* @param string $contractId
* The contract id to get the selfservice token for.
*
* @return ?string
* The selfservice URL.
*/
public function getSelfserviceUrl(string $contractId): ?string {
$response = $this->getSelfserviceTokenArray($contractId);
return $response['Url'] ?? NULL;
}
/**
* Fetch all contracts from API.
*
* @return array
* The contracts data as array.
*/
public function getContracts(): array {
return $this->get(Environment::API_CONTRACTS_RESOURCE_PATH);
}
/**
* Fetch contract by id from API.
*
* Example:
* ```
* {
* "Id": "599d51f881b1f00a28f7ae9e",
* "LastBillingDate": "2023-12-28T06:01:46.5518544Z",
* "NextBillingDate": "2023-12-28T06:01:46.5518548Z",
* "PlanId": "599d51f881b1f00a28f7ae9f",
* "CustomerId": "599d51f881b1f00a28f7ae9g",
* "IsDeletable": false,
* "LifecycleStatus": "Active",
* "CustomerName": "Marcellus Wallace",
* "CustomerIsLocked": false,
* "Phases": [
* {
* "Type": "Normal",
* "StartDate": "2023-12-28T06:01:46.5518566Z",
* "PlanVariantId": "599d51f881b1f00a28f7ae9h",
* "PlanId": "599d51f881b1f00a28f7ae9i",
* "InheritStartDate": false
* },
* {
* "Type": "Trial",
* "StartDate": "2023-12-28T06:01:46.5518575Z",
* "PlanVariantId": "599d51f881b1f00a28f7ae9j",
* "PlanId": "599d51f881b1f00a28f7ae9k",
* "InheritStartDate": false
* }
* ],
* "Balance": 0,
* "Currency": "EUR",
* "PlanGroupId": "599d51f881b1f00a28f7ae9l",
* "PaymentBearer": {
* "CardType": "Visa",
* "ExpiryMonth": 12,
* "ExpiryYear": 2020,
* "Holder": "Marcellus Wallace",
* "Last4": "1234",
* "Type": "CreditCard",
* "Country": "DE"
* },
* "PaymentProvider": "PayOne",
* "EscalationSuspended": false,
* "RecurringPaymentsPaused": false,
* "CurrentPhase": {
* "Type": "Normal",
* "StartDate": "2023-12-28T06:01:46.5518605Z",
* "PlanVariantId": "599d51f881b1f00a28f7ae9m",
* "PlanId": "599d51f881b1f00a28f7ae9n",
* "InheritStartDate": false
* },
* "PaymentProviderSupportRefunds": false,
* "BillingSuspended": false,
* "ThresholdBillingDisabled": false,
* "TimeGranularity": "Precise",
* "StartDate": "2023-12-28T06:01:46.5518610Z",
* "EndDate": "2023-12-28T06:01:46.5518612Z",
* "BilledUntil": "2023-12-28T06:01:46.5518614Z",
* "PlanVariantId": "599d51f881b1f00a28f7ae9o",
* "Notes": "NoteExample"
* }
* ```
*
* @param string $contractId
* The id of the contract to get.
*
* @return array
* The contract.
*/
public function getContract(string $contractId): array {
return $this->get(Environment::API_CONTRACTS_RESOURCE_PATH . "/{$contractId}");
}
/**
* Returns the contract changes information.
*
* @return array
* The contract changes.
*/
public function getContractChanges(): array {
return $this->get(Environment::API_CONTRACTCHANGES_RESOURCE_PATH);
}
/**
* Returns the contract change information.
*
* @param string $id
* The contract id.
*
* @return array
* The contract change.
*/
public function getContractChange(string $id): array {
return $this->get(Environment::API_CONTRACTCHANGES_RESOURCE_PATH . "/{$id}");
}
/**
* Returns the customers information.
*
* @return array
* The customers.
*/
public function getCustomers(): array {
return $this->get(Environment::API_CUSTOMERS_RESOURCE_PATH);
}
/**
* Fetch customer by id from API.
*
* Example response:
* ```
* {
* "Id": "5996a94681b200088ca84f1a",
* "CreatedAt": "2023-12-28T06:01:46.7935616Z",
* "IsDeletable": false,
* "DeletedAt": "0001-01-01T00:00:00.0000000Z",
* "IsLocked": false,
* "CustomerName": "Wallace, Marcellus",
* "CustomerSubName": "",
* "FirstName": "Marcellus",
* "LastName": "Wallace",
* "Language": "en-US",
* "EmailAddress": "marcellus@example.org",
* "AdditionalEmailAddresses": [
* {
* "EmailAddress": "marcellus02@example.com"
* },
* {
* "EmailAddress": "marcellus03@example.com"
* }
* ],
* "Address": {
* "Street": "Raymond Ave (Holly)",
* "HouseNumber": "145",
* "PostalCode": "91001",
* "City": "Pasadena",
* "Country": "CA"
* },
* "AdditionalAddresses": [
* {
* "FirstName": "Martin",
* "LastName": "Schmidt",
* "CompanyName": "Billwerk",
* "Street": "Schillergasse",
* "HouseNumber": "17",
* "PostalCode": "89278",
* "City": "Frankfurt am Main",
* "Country": "DE"
* }
* ],
* "Locale": "en-US",
* "CustomFields": {
* "CustomFieldName": "CustomFieldValue",
* "Name": "Value"
* },
* "DefaultBearerMedium": "Email",
* "CustomerType": "Consumer",
* "Hidden": false
* }
* ```
*
* @param string $customerId
* The id of the customer to get.
*
* @return array
* The customer DTO.
*/
public function getCustomer(string $customerId): array {
return $this->get(Environment::API_CUSTOMERS_RESOURCE_PATH . "/{$customerId}");
}
/**
* Replace customer data entirely in/via API.
*
* @param array $customerData
* The customer data to set from as array.
*
* @return array
* The result of the operation.
*/
public function putCustomer(array $customerData): array {
return $this->put(Environment::API_CUSTOMERS_RESOURCE_PATH . "/{$customerData['Id']}", $customerData);
}
/**
* Update single customer data in/via API.
*
* @param string $customerId
* The id of the customer to update.
* @param array $customerData
* The customer data to update from as array.
*
* @return array
* The result of the operation.
*/
public function patchCustomer(string $customerId, array $customerData): array {
return $this->patch(Environment::API_CUSTOMERS_RESOURCE_PATH . "/{$customerId}", $customerData);
}
/**
* Update single customer locked state in/via API.
*
* @param string $customerId
* The ID of the customer to update.
* @param bool $isLocked
* True if the user should be set locked, false to set unlocked.
*
* @throws \Exception
* If the operation fails.
*/
public function customerUpdateLockedState(string $customerId, bool $isLocked): void {
$isLockedString = $isLocked ? 'true' : 'false';
// Assuming $this->post() returns a result that can
// be returned by this function.
$this->post(Environment::API_CUSTOMERS_RESOURCE_PATH . "/updatelockedstate?id={$customerId}&isLocked={$isLockedString}");
}
/**
* Delete the customer.
*
* Deletes the customer and all its personal data in accordance with GDPR and
* accounting principles.
*
* If the customer had some contracts with ledger entry, billwerk will archive
* the invoices and other relevant data. To ensure compliance with data
* protection laws, customer data will be anonymized before archiving.
*
* @param string $customerId
* The id of the customer to delete.
*
* @see https://billwerk.readme.io/reference/contracts_delete_id_delete
*
* @return array
* The result of the operation.
*/
public function deleteCustomer(string $customerId): array {
return $this->delete(Environment::API_CUSTOMERS_RESOURCE_PATH . "/{$customerId}");
}
/**
* Fetch customer by email address (if existing)
*
* @param string $email
* The email address of the customer to get.
*
* @return array
* The customer DTO or NULL if the customer could not be found.
*/
public function getCustomerByEmail(string $email): array {
// @improve as soon as Billwerk offers a search for specific fields, try to use that.
// (the current search across multiple fields might turn up false positives)
$results = $this->searchCustomers($email, 1);
return empty($results) ? NULL : reset($results);
}
/**
* Fetch customer by their ExternalId.
*
* Typically, in the context of this module, the ExternalId is the Drupal User
* ID (UID).
*
* @param string $externalId
* The ExternalId to search the Billwerk Customers for.
*
* @return array
* The customer DTO or NULL if the customer could not be found.
*/
public function getCustomerByExternalId(string $externalId): ?array {
$querystring = "?externalId={$externalId}&take=1";
$results = $this->get(Environment::API_CUSTOMERS_SEARCH_RESOURCE_PATH . "/{$querystring}");
// Return the first result from the array:
return empty($results) ? NULL : reset($results);
}
/**
* Fetch customer from API by a search term.
*
* @param string $search
* The search term to filter customers on.
* @param int $limit
* An optional limit.
*
* @return array
* The customers data as array.
*/
public function searchCustomers(string $search, ?int $limit = NULL): array {
$querystring = "?search=$search";
if ($limit) {
$querystring .= '&take=' . $limit;
}
return $this->get(Environment::API_CUSTOMERS_SEARCH_RESOURCE_PATH . "/{$querystring}");
}
/**
* Returns the invoice information.
*
* @param string $id
* The id.
*
* @return array
* The invoice.
*/
public function getInvoice(string $id): array {
return $this->get(Environment::API_INVOICES_RESOURCE_PATH . "/{$id}");
}
/**
* Returns the invoice change information.
*
* @param string $id
* The id.
*
* @return array
* The Billwerk API response.
*/
public function getInvoiceUsage(string $id): array {
return $this->get(Environment::API_INVOICES_RESOURCE_PATH . "/{$id}/usage");
}
/**
* Gets a list of contracts for given customer id.
*
* @param string $customerId
* The customer id.
*
* @return array
* The contracts data as nested array.
*/
public function getCustomerContracts(string $customerId): array {
$url = Environment::API_CUSTOMERS_RESOURCE_PATH . "/{$customerId}/contracts";
return $this->get($url);
}
/**
* Returns the orders information.
*
* @return array
* The Billwerk API response.
*/
public function getOrders(): array {
return $this->get(Environment::API_ORDERS_RESOURCE_PATH);
}
/**
* Returns the order information.
*
* @param string $id
* The id.
*
* @return array
* The Billwerk API response.
*/
public function getOrder(string $id): array {
return $this->get(Environment::API_ORDERS_RESOURCE_PATH . "/{$id}");
}
/**
* Returns the PlanGroups (aka Component) information.
*
* @return array
* The Billwerk API response.
*/
public function getPlanGroups(): array {
return $this->get(Environment::API_PLANGROUPS_RESOURCE_PATH);
}
/**
* Returns the PlanGroup (aka Component) information.
*
* @param string $id
* The id.
*
* @return array
* The Billwerk API response.
*/
public function getPlanGroup(string $id): array {
return $this->get(Environment::API_PLANGROUPS_RESOURCE_PATH . "/{$id}");
}
/**
* Gets a list of plans.
*
* @return array
* The plans data as nested array.
*/
public function getPlans(): array {
return $this->get(Environment::API_PLANS_RESOURCE_PATH);
}
/**
* Returns the plan information.
*
* @param string $id
* The id.
*
* @return array
* The Billwerk API response.
*/
public function getPlan(string $id): array {
return $this->get(Environment::API_PLANS_RESOURCE_PATH . "/{$id}");
}
/**
* Gets a list of plan variants.
*
* @return array
* The plan variants data as nested array.
*/
public function getPlanVariants(): array {
return $this->get(Environment::API_PLANVARIANTS_RESOURCE_PATH);
}
/**
* Returns the plan variant information.
*
* @param string $id
* The id.
*
* @return array
* The Billwerk API response.
*/
public function getPlanVariant(string $id): array {
return $this->get(Environment::API_PLANVARIANTS_RESOURCE_PATH . "/{$id}");
}
/**
* Returns the pricelists information.
*
* @return array
* The Billwerk API response.
*/
public function getPriceLists(): array {
return $this->get(Environment::API_PRICELISTS_RESOURCE_PATH);
}
/**
* Returns the pricelist information.
*
* @param string $id
* The id.
*
* @return array
* The Billwerk API response.
*/
public function getPriceList(string $id): array {
return $this->get(Environment::API_PRICELISTS_RESOURCE_PATH . "/{id}");
}
/**
* Returns the hooks information.
*
* @return array
* The Billwerk API response.
*/
public function getWebhooks(): array {
return $this->get(Environment::API_WEBHOOKS_RESOURCE_PATH);
}
/**
* Returns the subscriptions information.
*
* @return array
* The Billwerk API response.
*/
public function getSubscriptions(): array {
return $this->get(Environment::API_SUBSCRIPTIONS_RESOURCE_PATH);
}
/**
* Returns the product information.
*
* @return array
* The Billwerk API response.
*/
public function getProductInfo(): array {
return $this->get(Environment::API_PRODUCTINFO_RESOURCE_PATH);
}
/**
* Create a new plan/component subscription or up/downgrade order.
*
* @param array $data
* The order data.
*
* @return ?\stdClass
* OrderDTO.
*/
public function createOrder(array $data): ?\stdClass {
return $this->post(Environment::API_ORDERS_RESOURCE_PATH, $data);
}
/**
* Create a new plan/component subscription or up/downgrade order preview.
*
* For example to show pricing information.
*
* @param array $data
* OrderDTO.
*
* @return ?\stdClass
* OrderDTO.
*/
public function createOrderPreview(array $data): ?\stdClass {
return $this->post(Environment::API_ORDERS_RESOURCE_PATH . '/preview', $data);
}
/**
* Process and finalize an order.
*
* @param string $orderId
* Order ID.
* @param array $data
* The order data.
*
* @return ?\stdClass
* OrderCommitDTO.
*/
public function commitOrder(string $orderId, array $data = []): ?\stdClass {
return $this->post(Environment::API_ORDERS_RESOURCE_PATH . "/{$orderId}/commit", $data);
}
/**
* Approve an order.
*
* @param string $orderId
* Order ID.
*
* @return ?\stdClass
* The result of the operation.
*/
public function approveOrder(string $orderId): ?\stdClass {
return $this->post(Environment::API_ORDERS_RESOURCE_PATH . "/{$orderId}/approve");
}
/**
* Decline an order.
*
* @param string $orderId
* Order ID.
*
* @return ?\stdClass
* The result of the operation.
*/
public function declineOrder(string $orderId): ?\stdClass {
return $this->post(Environment::API_ORDERS_RESOURCE_PATH . "/{$orderId}/decline");
}
/**
* Deletes the order.
*
* @param string $orderId
* Order ID.
*
* @return array
* The result of the operation.
*/
public function deleteOrder(string $orderId): array {
return $this->delete(Environment::API_ORDERS_RESOURCE_PATH . "/{$orderId}");
}
/**
* Create customer in/via API.
*
* @param array $customerData
* The customer data to create from as array.
*
* @return ?\stdClass
* The result of the operation.
*/
public function createCustomer(array $customerData): ?\stdClass {
return $this->post(Environment::API_CUSTOMERS_RESOURCE_PATH, $customerData);
}
/**
* Retrieves current subscriptions in the selected contract.
*
* @param string $contractId
* Contract ID.
*
* @return array
* The result of the operation.
*/
public function getContractComponentSubscriptions(string $contractId): array {
return $this->get(Environment::API_CONTRACTS_RESOURCE_PATH . "/{$contractId}/ComponentSubscriptions");
}
/**
* Create a new component subscription for this contract.
*
* @param string $contractId
* Contract ID.
* @param array $data
* ComponentSubscriptionCreateDTO.
*
* @return ?\stdClass
* The result of the operation.
*/
public function createContractComponentSubscription(string $contractId, array $data): ?\stdClass {
return $this->post(Environment::API_CONTRACTS_RESOURCE_PATH . "/{$contractId}/ComponentSubscriptions", $data);
}
/**
* Returns the component subscription.
*
* @param string $componentSubscriptionId
* Component subscription ID.
*
* @return array
* The result of the operation.
*/
public function getComponentSubscription(string $componentSubscriptionId): array {
return $this->get(Environment::API_COMPONENT_SUBSCRIPTIONS_RESOURCE_PATH . "/{$componentSubscriptionId}");
}
/**
* Retrieves all currently active subscriptions.
*
* Including plan variant, component subscriptions and discount subscriptions.
*
* Example response:
* ```
* {
* "Id": "658d0f4ac703e6a4d126704b",
* "Phase": {
* "Type": "Normal",
* "StartDate": "2023-06-28T06:01:46.5910722Z",
* "PlanVariantId": "5b20d2df81b1f00d1425280f",
* "PlanId": "5b20d2deba5c1e13409b15f2",
* "Quantity": 1,
* "InheritStartDate": false
* },
* "ComponentSubscriptions": [
* {
* "Id": "599d51f881b1f00a28f7ae9n",
* "ContractId": "599d51f881b1f00a28f7ae14",
* "CustomerId": "599d51f881b1f00a28f7asdf",
* "ComponentId": "599d51f881b1f00a28f7ae10",
* "Quantity": 1,
* "StartDate": "2023-11-28T06:01:46.5910686Z",
* "BilledUntil": "2024-01-28T06:01:46.5910665Z",
* "Status": "Active",
* "EndDate": "2024-06-28T06:01:46.5910681Z",
* "Memo": "Memoexample"
* },
* {
* "Id": "599d51f881b1f00a28f7ae8n",
* "ContractId": "599d51f881b1f00a28f7ae14",
* "CustomerId": "599d51f881b1f00a28f7asdf",
* "ComponentId": "599d51f881b1f00a28f7ae11",
* "Quantity": 0,
* "StartDate": "2023-11-28T06:01:46.5910695Z",
* "BilledUntil": "2024-01-28T06:01:46.5910692Z",
* "Status": "Active",
* "EndDate": "2024-06-28T06:01:46.5910694Z",
* "Memo": "Memoexample"
* }
* ],
* "DiscountSubscriptions": [
* {
* "Id": "599d51f881b1f00a28f7ae2m",
* "ContractId": "599d51f881b1f00a28f7ae14",
* "CouponCode": "50off",
* "CouponId": "599d51f881b1f00a28f7ae16",
* "DiscountId": "599d51f881b1f00a28f7ae18",
* "StartDate": "2023-12-28T06:01:46.5910710Z",
* "Status": "Active",
* "EndDate": "2024-06-28T06:01:46.5910708Z"
* },
* {
* "Id": "599d51f881b1f00a28f7ae2o",
* "ContractId": "599d51f881b1f00a28f7ae14",
* "CouponCode": "starter",
* "CouponId": "599d51f881b1f00a28f7ae17",
* "DiscountId": "599d51f881b1f00a28f7ae20",
* "StartDate": "2023-12-28T06:01:46.5910716Z",
* "Status": "Active",
* "EndDate": "2024-01-28T06:01:46.5910715Z"
* }
* ]
* }
* ```
*
* @param string $contractId
* Contract ID.
*
* @return array
* The result of the operation.
*/
public function getContractSubscriptions(string $contractId): array {
return $this->get(Environment::API_CONTRACTS_RESOURCE_PATH . "/{$contractId}/Subscriptions");
}
/**
* Retrieves the cancellation preview for this contract.
*
* Example response:
* ```
* {
* "NextPossibleCancellationDate": "2018-02-07T23:00:00Z",
* "EndDate": "2018-02-08T23:00:00Z",
* "Invoice": {...},
* "ContractAfter": {...}
* }
* ```
*
* @param string $contractId
* Contract ID.
*
* @return array
* The result of the operation.
*/
public function getContractCancellationPreview(string $contractId): array {
return $this->get(Environment::API_CONTRACTS_RESOURCE_PATH . "/{$contractId}/CancellationPreview");
}
/**
* Retrieves a list of all invoices / credit notes.
*
* @return array
* The result of the operation.
*/
public function getInvoices(): array {
return $this->get(Environment::API_INVOICES_RESOURCE_PATH);
}
/**
* Retrieves an invoice by id.
*
* @param string $invoiceId
* Invoice ID.
*
* @return array
* The result of the operation.
*/
public function getInvoiceById(string $invoiceId): array {
return $this->get(Environment::API_INVOICES_RESOURCE_PATH . "/{$invoiceId}");
}
/**
* Creates a file download token for the given invoice.
*
* @param string $invoiceId
* Invoice ID.
* @param array $data
* The download link data.
*/
public function getInvoiceDownloadLink(string $invoiceId, array $data): string {
return $this->post(Environment::API_INVOICES_RESOURCE_PATH . "/{$invoiceId}/downloadLink", $data);
}
/**
* Retrieves a list of all invoice drafts.
*
* @return array
* The result of the operation.
*/
public function getInvoiceDrafts(): array {
return $this->get(Environment::API_INVOICEDRAFTS_RESOURCE_PATH);
}
/**
* Retrieves a draft by id.
*
* @param string $invoiceDraftId
* Invoice Draft ID.
*
* @return array
* The result of the operation.
*/
public function getInvoiceDraft(string $invoiceDraftId): array {
return $this->get(Environment::API_INVOICEDRAFTS_RESOURCE_PATH . "/{$invoiceDraftId}");
}
/**
* Create a new webhook subscription.
*
* @param array $data
* HookDTO.
*
* @return ?\stdClass
* The result of the operation.
*/
public function createWebhook(array $data): ?\stdClass {
return $this->post(Environment::API_WEBHOOKS_RESOURCE_PATH, $data);
}
/**
* Deletes the webhook from the system.
*
* @param string $webhookId
* Webhook ID.
*
* @return array
* The result of the operation.
*/
public function deleteWebhook(string $webhookId): array {
return $this->delete(Environment::API_WEBHOOKS_RESOURCE_PATH . "/{$webhookId}");
}
/**
* Gets the Environment used for the current API calls.
*
* @return \Drupal\billwerk_subscriptions\Environment
* The environment.
*/
public function getEnvironment(): Environment {
return $this->environment;
}
/**
* Helper method to format a given $timestamp in the expected Billwerk format.
*
* Billwerk uses a certain date format, which we have to convert timestamps
* into. This method furthermore allows modifications like a
* low and high limit for the date range, because Billwerk is very strict
* about subscription dates and prolongation periods.
*
* @param string $timestamp
* The unix timestamp to format.
* @param string $min
* The minimum to round up to, if the $timestamp is lower.
* @param string $max
* The maximum to round down to, if the $timestamp is lower.
* @param bool $roundSeconds
* Round seconds to the nearest minute.
*
* @see https://support.billwerk.com/hc/en-us/articles/360020464720-What-to-consider-when-passign-a-date-and-times-
*
* @return string
* The formatted date time string in Billwerk expected ISO 8601 format
* format.
*/
public static function billwerkDateFormat(int $timestamp, ?string $min = NULL, ?string $max = NULL, bool $roundSeconds = FALSE): string {
if ($roundSeconds) {
$timestamp = round($timestamp / 60) * 60;
}
// Create a DateTime object from the timestamp.
$date = new \DateTimeImmutable('@' . $timestamp);
if ($min !== NULL) {
$minDate = self::billwerkParseDate($min);
if ($minDate > $date) {
$date = $minDate;
}
}
if ($max !== NULL) {
$maxDate = self::billwerkParseDate($max);
if ($maxDate < $date) {
$date = $maxDate;
}
}
// Set the timezone to UTC.
$date->setTimezone(new \DateTimeZone('UTC'));
// Output in ISO 8601 format.
return $date->format('Y-m-d\TH:i:s.u0\Z');
}
/**
* Parses a given Billwerk Date in the format Y-m-d\TH:i:s.u0\Z to a DateTime.
*
* @param string $date
* The date string to parse.
*
* @return \DateTimeImmutable
* The parsed date.
*/
public static function billwerkParseDate(string $date): \DateTimeImmutable {
return \DateTimeImmutable::createFromFormat('Y-m-d\TH:i:s.u0\Z', $date);
}
}
