billwerk_subscriptions-1.x-dev/src/BillwerkDataObjectFactory.php
src/BillwerkDataObjectFactory.php
<?php
declare(strict_types=1);
namespace Drupal\billwerk_subscriptions;
use Drupal\billwerk_subscriptions\DataObject\BillwerkContract;
use Drupal\billwerk_subscriptions\DataObject\BillwerkContractComponentSubscription;
use Drupal\billwerk_subscriptions\DataObject\BillwerkContractSubscription;
use Drupal\billwerk_subscriptions\DataObject\BillwerkCustomer;
use Drupal\billwerk_subscriptions\Exception\CustomerExternalIdEmptyException;
use Drupal\billwerk_subscriptions\Exception\DataObjectException;
/**
* Factory for the Billwerk data objects (DTO).
*
* Loads the data objects holding the relevant values from Billwerk.
*
* Typically you won't need to interact with this factory, but instead use the
* Subscriber object.
*/
final class BillwerkDataObjectFactory {
/**
* Constructs a BillwerkDataObjectFactory object.
*/
public function __construct(
private readonly Api $api,
) {
}
/**
* Factory to build the full BillwerkContract with all its parts.
*
* @param string $contractId
* The Billwerk Contract ID.
*
* @throws \Drupal\billwerk_subscriptions\Exception\DataObjectException.
*
* @return \Drupal\billwerk_subscriptions\DataObject\BillwerkContract
* The Billwerk Contract.
*/
public function billwerkLoadBillwerkContract(string $contractId): BillwerkContract {
$contractArray = $this->api->getContract($contractId);
if (!empty($contractArray)) {
if ($contractArray['Id'] !== $contractId) {
throw new DataObjectException('Given contract ID did not match the returned result.');
}
if (empty($contractArray['CustomerId'])) {
throw new DataObjectException('No CustomerId found for contract ID: "' . $contractId . '"');
}
// Build the BillwerkContract:
$billwerkContract = new BillwerkContract(
$contractArray['Id'],
$contractArray['LifecycleStatus'],
$this->billwerkLoadBillwerkCustomer($contractArray['CustomerId']),
$this->billwerkLoadBillwerkContractSubscription($contractArray['Id']),
);
return $billwerkContract;
}
throw new DataObjectException('BillwerkContract could not be loaded for contract ID: "' . $contractId . '"');
}
/**
* Factory to load the BillwerkCustomer by the given $customerId.
*
* Used internally to build the full BillwerkContract data object, but also
* helpful standalone, if for example Billwerk only returns the CustomerId
* without other details.
*
* @param string $customerId
* The Billwerk Customer ID.
*
* @throws \Drupal\billwerk_subscriptions\Exception\DataObjectException.
*
* @return \Drupal\billwerk_subscriptions\DataObject\BillwerkCustomer
* The Billwerk Customer.
*/
public function billwerkLoadBillwerkCustomer(string $customerId): BillwerkCustomer {
$customerArray = $this->api->getCustomer($customerId);
if (!empty($customerArray)) {
if ($customerArray['Id'] !== $customerId) {
throw new DataObjectException('Given customer ID did not match the returned result.');
}
if (empty($customerArray['ExternalCustomerId'])) {
throw new CustomerExternalIdEmptyException("The ExternalCustomerId of the Billwerk Customer ID #{$customerId} is empty. ExternalCustomerId always has to match the Drupal user id (UID)!");
}
// Build the BillwerkContract:
$billwerkCustomer = new BillwerkCustomer(
$customerArray['Id'],
$customerArray['ExternalCustomerId'] ?? NULL,
$customerArray['EmailAddress'] ?? NULL,
$customerArray['Locale'] ?? NULL,
$customerArray['IsDeletable'] ?? FALSE,
$customerArray['IsLocked'] ?? FALSE,
$customerArray['Hidden'] ?? FALSE,
$customerArray['DeletedAt'] ?? NULL,
);
return $billwerkCustomer;
}
throw new DataObjectException('BillwerkCustomer by ID: "' . $customerId . '" did not return any data.');
}
/**
* Load the BillwerkContractSubscription part of the BillwerkContract.
*
* @param string $contractId
* The Billwerk Contract ID.
*
* @throws \Drupal\billwerk_subscriptions\Exception\DataObjectException.
*
* @return \Drupal\billwerk_subscriptions\DataObject\BillwerkContractSubscription
* The Billwerk Contract Subscription.
*/
protected function billwerkLoadBillwerkContractSubscription(string $contractId): BillwerkContractSubscription {
$contractSubscriptionsArray = $this->api->getContractSubscriptions($contractId);
if (!empty($contractSubscriptionsArray)) {
if ($contractSubscriptionsArray['Id'] !== $contractId) {
throw new DataObjectException('Given Contract ID did not match the returned result.');
}
if (empty($contractSubscriptionsArray['Phase'])) {
throw new DataObjectException('No Phase information found for contract ID: "' . $contractId . '" subscriptions.');
}
if (empty($contractSubscriptionsArray['Phase']['PlanVariantId'])) {
throw new DataObjectException("The given Billwerk Contract #{$contractId} has no (active) PlanVariantId. Billwerk Contracts may never be without a PlanVariantId.");
}
$componentSubscriptions = [];
if (!empty($contractSubscriptionsArray['ComponentSubscriptions'])) {
foreach ($contractSubscriptionsArray['ComponentSubscriptions'] as $componentSubscriptionArray) {
if ($componentSubscriptionArray['ContractId'] !== $contractId) {
throw new DataObjectException('Given Contract ID did not match the ContractId from the ComponentSubscription.');
}
$componentSubscriptions[$componentSubscriptionArray['Id']] = new BillwerkContractComponentSubscription(
$componentSubscriptionArray['Id'],
$componentSubscriptionArray['ComponentId'],
$componentSubscriptionArray['Quantity'],
$componentSubscriptionArray['StartDate'] ?? NULL,
$componentSubscriptionArray['BilledUntil'] ?? NULL,
$componentSubscriptionArray['EndDate'] ?? NULL,
);
}
}
// @codingStandardsIgnoreStart
// This way we could also support DiscountSubscriptions if needed later:
// $discountSubscriptions = [];
// if(!empty($contractSubscriptionsArray['DiscountSubscriptions'])){
// foreach($contractSubscriptionsArray['DiscountSubscriptions'] as $discountSubscriptionArray){
// $discountSubscriptions[$discountSubscriptionArray['Id']] = ...;
// }
// }.
// @improve: We currently need an additional API call because Billwerk
// does NOT provide the following standard values in the
// https://sandbox.billwerk.com/api/v1/contracts/{id}/subscriptions
// call!
// @see https://billwerk.readme.io/reference/contracts_getsubscriptions_id_timestamp_get
// @codingStandardsIgnoreEnd
// We made a support request to add them, so maybe one day this additional
// call can be removed and the values can just be retrieved from
// $contractSubscriptionsArray!
$contractDetailsArray = $this->api->getContract($contractId);
// Build the BillwerkContract:
$billwerkContract = new BillwerkContractSubscription(
$contractSubscriptionsArray['Phase']['Type'],
$contractSubscriptionsArray['Phase']['PlanVariantId'],
$contractSubscriptionsArray['Phase']['PlanId'],
$contractSubscriptionsArray['Phase']['Quantity'],
$contractSubscriptionsArray['Phase']['StartDate'],
$contractDetailsArray['EndDate'] ?? NULL,
$contractDetailsArray['TrialEndDate'] ?? NULL,
$contractDetailsArray['BilledUntil'] ?? NULL,
$contractDetailsArray['LastBillingDate'] ?? NULL,
$contractDetailsArray['NextBillingDate'] ?? NULL,
$contractDetailsArray['Currency'] ?? NULL,
$contractDetailsArray['Balance'] ?? NULL,
$componentSubscriptions
);
return $billwerkContract;
}
throw new DataObjectException('BillwerkContract could not be loaded for contract ID: "' . $contractId . '"');
}
}
