diff --git a/modules/mollie_customers/mollie_customers.info.yml b/modules/mollie_customers/mollie_customers.info.yml new file mode 100644 index 0000000..a054ea4 --- /dev/null +++ b/modules/mollie_customers/mollie_customers.info.yml @@ -0,0 +1,11 @@ +name: Mollie Customers +description: Customers integration for Mollie. +package: Mollie for Drupal + +type: module +core: 8.x + +php: 7.0 + +dependencies: + - mollie:mollie diff --git a/modules/mollie_customers/mollie_customers.links.action.yml b/modules/mollie_customers/mollie_customers.links.action.yml new file mode 100644 index 0000000..0b47f9e --- /dev/null +++ b/modules/mollie_customers/mollie_customers.links.action.yml @@ -0,0 +1,11 @@ +entity.mollie_customer.add_form: + route_name: entity.mollie_customer.add_form + title: 'Add Mollie customer' + appears_on: + - entity.mollie_customer.collection + +entity.mollie_customer.delete_form: + route_name: entity.mollie_customer.delete_form + title: 'Delete' + appears_on: + - entity.mollie_customer.collection diff --git a/modules/mollie_customers/mollie_customers.links.menu.yml b/modules/mollie_customers/mollie_customers.links.menu.yml new file mode 100644 index 0000000..d449285 --- /dev/null +++ b/modules/mollie_customers/mollie_customers.links.menu.yml @@ -0,0 +1,5 @@ +entity.mollie_customers.collection: + title: 'Customers' + description: 'Manage customers made through Mollie for Drupal.' + parent: mollie.admin + route_name: entity.mollie_customer.collection diff --git a/modules/mollie_customers/mollie_customers.links.task.yml b/modules/mollie_customers/mollie_customers.links.task.yml new file mode 100644 index 0000000..9b62a77 --- /dev/null +++ b/modules/mollie_customers/mollie_customers.links.task.yml @@ -0,0 +1,15 @@ +entity.mollie_customers.collection: + title: 'Mollie customers' + route_name: entity.mollie_customers.collection + base_route: entity.mollie_customers.collection + +entity.mollie_customers.canonical: + route_name: entity.mollie_customers.canonical + base_route: entity.mollie_customers.canonical + title: View + +entity.mollie_customers.delete_form: + route_name: entity.mollie_customers.delete_form + base_route: entity.mollie_customers.canonical + title: Delete + weight: 10 diff --git a/modules/mollie_customers/mollie_customers.permissions.yml b/modules/mollie_customers/mollie_customers.permissions.yml new file mode 100644 index 0000000..de8fcf5 --- /dev/null +++ b/modules/mollie_customers/mollie_customers.permissions.yml @@ -0,0 +1,6 @@ +'access mollie customers overview': + title: 'Access Mollie for Drupal customers overview' + restrict access: TRUE + +permission_callbacks: + - Drupal\mollie_customers\CustomerPermissions::customerPermissions diff --git a/modules/mollie_customers/mollie_customers.routing.yml b/modules/mollie_customers/mollie_customers.routing.yml new file mode 100644 index 0000000..80676df --- /dev/null +++ b/modules/mollie_customers/mollie_customers.routing.yml @@ -0,0 +1,32 @@ +entity.mollie_customer.collection: + path: '/admin/mollie/customers' + defaults: + _entity_list: 'mollie_customer' + _title: 'Mollie customers' + requirements: + _permission: 'access mollie customers overview' + +entity.mollie_customer.canonical: + path: '/mollie/customer/{mollie_customer}' + defaults: + _entity_view: 'mollie_customer' + _title: 'Mollie customer' + _title_callback: 'Drupal\mollie_customers\Controller\CustomerController::customerTitle' + requirements: + _entity_access: 'mollie_customer.view' + +entity.mollie_customer.add_form: + path: '/mollie/customer/add' + defaults: + _entity_form: mollie_customer.add + _title: 'Add Mollie customer' + requirements: + _entity_create_access: 'mollie_customer' + +entity.mollie_customer.delete_form: + path: '/mollie/customer/{mollie_customer}/delete' + defaults: + _entity_form: mollie_customer.delete + _title: 'Delete' + requirements: + _entity_access: 'mollie_customer.delete' diff --git a/modules/mollie_customers/src/Controller/CustomerController.php b/modules/mollie_customers/src/Controller/CustomerController.php new file mode 100644 index 0000000..f35dddf --- /dev/null +++ b/modules/mollie_customers/src/Controller/CustomerController.php @@ -0,0 +1,28 @@ +entityTitle($mollie_customer); + } + +} diff --git a/modules/mollie_customers/src/Controller/CustomerListBuilder.php b/modules/mollie_customers/src/Controller/CustomerListBuilder.php new file mode 100644 index 0000000..88053ad --- /dev/null +++ b/modules/mollie_customers/src/Controller/CustomerListBuilder.php @@ -0,0 +1,102 @@ +dateFormatter = $dateFormatter; + } + + /** + * {@inheritdoc} + */ + public static function createInstance(ContainerInterface $container, EntityTypeInterface $entityType) { + return new static( + $entityType, + $container->get('entity.manager')->getStorage($entityType->id()), + $container->get('date.formatter') + ); + } + + /** + * {@inheritdoc} + */ + public function buildHeader() { + $header = []; + + $header['id'] = $this->t('Customer ID'); + $header['mode'] = $this->t('Mode'); + $header['name'] = $this->t('Name'); + $header['email'] = $this->t('Email'); + $header['created'] = $this->t('Created'); + + return $header + parent::buildHeader(); + } + + /** + * {@inheritdoc} + */ + public function buildRow(EntityInterface $entity) { + /** @var \Drupal\mollie_customers\Entity\Customer $entity */ + $row = []; + + $row['id'] = Link::fromTextAndUrl($entity->id(), $entity->toUrl()); + $row['mode'] = $entity->getMode(); + $row['name'] = $entity->getName(); + $row['email'] = $entity->getEmail(); + $row['created'] = $this->getFormattedDate($entity->getCreatedTime()); + + return $row + parent::buildRow($entity); + } + + /** + * @param string $date + * Date in ISO 8601 format. + * + * @return string + * Date formatted in medium date format. + */ + protected function getFormattedDate($date) { + $dateTime = new \DateTime($date); + return $this->dateFormatter + ->format($dateTime->format('U'), 'medium'); + } + +} diff --git a/modules/mollie_customers/src/CustomerAccessControlHandler.php b/modules/mollie_customers/src/CustomerAccessControlHandler.php new file mode 100644 index 0000000..1f68198 --- /dev/null +++ b/modules/mollie_customers/src/CustomerAccessControlHandler.php @@ -0,0 +1,39 @@ +entityTypeManager + ->getStorage('mollie_customer') + ->getEntityType(); + return $this->entityBasePermissions($entityType); + } + catch (InvalidPluginDefinitionException | PluginNotFoundException $e) { + watchdog_exception('mollie', $e); + } + + return []; + } + + /** + * Returns available operations on entities of a given type. + * + * @param \Drupal\Core\Entity\EntityTypeInterface $entityType + * Entity type. + * + * @return array + * Array with operations. + */ + protected function getAvailableOperations(EntityTypeInterface $entityType): array { + return array_merge(parent::getAvailableOperations($entityType), ['delete']); + } + +} diff --git a/modules/mollie_customers/src/Entity/Customer.php b/modules/mollie_customers/src/Entity/Customer.php new file mode 100644 index 0000000..86ea894 --- /dev/null +++ b/modules/mollie_customers/src/Entity/Customer.php @@ -0,0 +1,164 @@ +setLabel(t('ID')) + ->setDescription(t('The customer’s unique identifier.')) + ->setReadOnly(TRUE); + + // Customer created date. + $fields['created'] = BaseFieldDefinition::create('datetime') + ->setLabel(t('Created')) + ->setDescription(t('The customer’s date and time of creation, in ISO 8601 format.')) + ->setReadOnly(TRUE); + + // Customer name. + $fields['name'] = BaseFieldDefinition::create('string') + ->setLabel(t('Name')) + ->setDescription(t('The full name of the customer as provided when the customer was created.')) + ->setRequired(TRUE) + ->setDisplayOptions( + 'form', + [ + 'type' => 'string_textfield', + 'weight' => 1, + ] + ) + ->setDisplayOptions('view', [ + 'label' => 'inline', + 'type' => 'string', + 'weight' => 0, + ]); + + // Customer email. + $fields['email'] = BaseFieldDefinition::create('string') + ->setLabel(t('Email')) + ->setDescription(t('The email address of the customer as provided when the customer was created.')) + ->setRequired(TRUE) + ->setDisplayOptions( + 'form', + [ + 'type' => 'string_textfield', + 'weight' => 1, + ] + ) + ->setDisplayOptions( + 'view', + [ + 'label' => 'inline', + 'type' => 'email', + 'weight' => 0, + ] + ); + + // Customer changed time. + $fields['changed'] = BaseFieldDefinition::create('datetime') + ->setLabel(t('Changed')) + ->setReadOnly(TRUE); + + // Customer mode. + $fields['mode'] = BaseFieldDefinition::create('string') + ->setLabel(t('Mode')) + ->setDescription(t('The mode used to create this customer.')) + ->setReadOnly(TRUE); + + return $fields; + } + + /** + * {@inheritdoc} + */ + public function isTranslatable() { + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function getCreatedTime(): string { + return $this->get('created')->value; + } + + /** + * {@inheritdoc} + */ + public function getName(): string { + return $this->get('name')->value; + } + + /** + * {@inheritdoc} + */ + public function getEmail(): string { + return $this->get('email')->value; + } + + /** + * {@inheritdoc} + */ + public function getMode(): string { + return $this->get('mode')->value; + } + + /** + * @todo: Check if this is the correct way. + */ + public function delete() { + /** @var \Drupal\mollie_customers\Entity\CustomerStorage $customerStorage */ + $customerStorage = \Drupal::service('entity_type.manager')->getStorage('mollie_customer'); + $customers = $customerStorage->loadMultiple([$this->id()]); + return $customerStorage->doDeleteFieldItems($customers); + } + +} diff --git a/modules/mollie_customers/src/Entity/CustomerStorage.php b/modules/mollie_customers/src/Entity/CustomerStorage.php new file mode 100644 index 0000000..18b85b8 --- /dev/null +++ b/modules/mollie_customers/src/Entity/CustomerStorage.php @@ -0,0 +1,162 @@ +metadata); + + $values = [ + 'id' => $customer->id, + 'created' => $customer->createdAt, + 'name' => $customer->name, + 'email' => $customer->email, + 'mode' => $customer->mode, + 'changed' => $this->getCustomerChangedDate($customer), + ]; + + return Customer::create($values); + } + + return NULL; + } + + /** + * {@inheritdoc} + */ + protected function createCustomerFromEntity(EntityInterface $entity): void { + if ($entity instanceof Customer) { + $metadata = []; + + $values = [ + 'name' => $entity->getName(), + 'email' => $entity->getEmail(), + 'locale' => $this->getLocaleByCurrentContentLanguage(), + 'metadata' => Json::encode($metadata), + ]; + + try { + // Create a customer on the Mollie side. + $customer = $this->mollieApiClient->customers->create($values); + // Update the entity with the information added by Mollie. + // TODO: This might be incomplete. Look for a way to replace $entity. + $entity->setOriginalId($customer->id); + $entity->set('id', $customer->id); + } + catch (ApiException $e) { + watchdog_exception('mollie', $e); + throw new EntityStorageException('An error occurred while creating the customer.'); + } + } + } + + /** + * {@inheritdoc} + */ + protected function deleteCustomerFromEntity(EntityInterface $entity): void { + if ($entity instanceof Customer) { + + try { + // Delete a customer on the Mollie side. + $customer = $this->mollieApiClient->customers->delete($entity->id()); + } + catch (ApiException $e) { + watchdog_exception('mollie', $e); + throw new EntityStorageException('An error occurred while deleting the customer.'); + } + } + } + + /** + * Returns the date the customer last changed in ISO 8601 format. + * + * @param \Mollie\Api\Resources\Customer $customer + * Payment. + * + * @return string + * Date the customer last changed in ISO 8601 format. + */ + protected function getCustomerChangedDate(MollieCustomer $customer): string { + $changedDate = $customer->createdAt; + + foreach ($this->getCustomerDateFields() as $dateField) { + if (property_exists($customer, $dateField) + && $customer->{$dateField} > $changedDate) { + $changedDate = $customer->{$dateField}; + } + } + + return $changedDate; + } + + /** + * Returns the date fields known for customers. + * + * @return array + * Array of date field names. + */ + protected function getCustomerDateFields(): array { + return [ + 'createdAt', + ]; + } + + /** + * Returns an ISO locale code based on the current content language. + * + * Best effort to get a locale code to pass to Mollie. Drupal does not have + * a country for (anonymous) visitors by default so we cannot construct the + * locale from language code and country code. This implementation is based + * on the locales currently supported by Mollie. + * + * @return string + */ + protected function getLocaleByCurrentContentLanguage(): string { + $langcode = $this->languageManager + ->getCurrentLanguage(LanguageInterface::TYPE_CONTENT) + ->getId(); + + switch ($langcode) { + case 'en': + return 'en_US'; + + case 'ca': + return 'ca_ES'; + + case 'nb': + return 'nb_NO'; + + case 'sv': + return 'sv_SE'; + + case 'da': + return 'da_DK'; + + default: + $countryCode = strtoupper($langcode); + return "{$langcode}_{$countryCode}"; + } + } + +} diff --git a/modules/mollie_customers/src/Entity/CustomerStorageBase.php b/modules/mollie_customers/src/Entity/CustomerStorageBase.php new file mode 100644 index 0000000..806ae91 --- /dev/null +++ b/modules/mollie_customers/src/Entity/CustomerStorageBase.php @@ -0,0 +1,257 @@ +messenger = $messenger; + $this->mollieApiClient = $mollieConnector->getClient(); + $this->configFactory = $configFactory; + $this->languageManager = $languageManager; + } + + /** + * {@inheritdoc} + */ + public static function createInstance(ContainerInterface $container, EntityTypeInterface $entityType) { + return new static( + $entityType, + $container->get('entity.manager'), + $container->get('cache.entity'), + $container->get('messenger'), + $container->get('mollie.mollie'), + $container->get('config.factory'), + $container->get('language_manager'), + $container->get('entity.memory_cache') + ); + } + + /** + * Static cache for entities. + * + * @var \Drupal\Core\Entity\EntityInterface[] + */ + protected $loadedEntities = []; + + /** + * {@inheritdoc} + */ + public function doLoadMultiple(array $ids = NULL) { + $entities = []; + + foreach ($ids as $id) { + // Static caching. + if (isset($this->loadedEntities[$id])) { + $entities[$id] = $this->loadedEntities[$id]; + continue; + } + + try { + if (empty(static::RESOURCE_NAME)) { + throw new ApiException('The resource name is invalid.'); + } + + $customerBase = $this->mollieApiClient->{static::RESOURCE_NAME}->get($id); + + $entity = $this->createEntityFromCustomer($customerBase); + if (!is_null($entity)) { + $this->loadedEntities[$id] = $entity; + $entities[$id] = $this->loadedEntities[$id]; + } + } + catch (ApiException $e) { + watchdog_exception('mollie', $e); + } + } + + return $entities; + } + + /** + * {@inheritdoc} + */ + public function has($id, EntityInterface $entity) { + // TODO: Implement has() method. + } + + /** + * {@inheritdoc} + */ + public function purgeFieldItems(ContentEntityInterface $entity, FieldDefinitionInterface $field_definition) { + // TODO: Implement purgeFieldItems() method. + } + + /** + * {@inheritdoc} + */ + public function doDeleteRevisionFieldItems(ContentEntityInterface $revision) { + // TODO: Implement doDeleteRevisionFieldItems() method. + } + + /** + * {@inheritdoc} + */ + public function countFieldData($storage_definition, $as_bool = FALSE) { + // TODO: Implement countFieldData() method. + } + + /** + * {@inheritdoc} + */ + public function readFieldItemsToPurge(FieldDefinitionInterface $field_definition, $batch_size) { + // TODO: Implement readFieldItemsToPurge() method. + } + + /** + * {@inheritdoc} + */ + public function doLoadRevisionFieldItems($revision_id) { + // TODO: Implement doLoadRevisionFieldItems() method. + } + + /** + * {@inheritdoc} + */ + public function doDeleteFieldItems($entities) { + foreach ($entities as $entity) { + $this->deleteCustomerFromEntity($entity); + } + } + + /** + * {@inheritdoc} + */ + public function doSaveFieldItems(ContentEntityInterface $entity, array $names = []) { + $this->createCustomerFromEntity($entity); + } + + /** + * {@inheritdoc} + */ + public function getQueryServiceName() { + return 'mollie.mollie'; + } + + /** + * Returns an entity created from the data in a customer base object. + * + * This method should be implemented by storage classes for specific + * customer base types. + * + * @param \Mollie\Api\Resources\BaseResource $transaction + * Transaction object. + * + * @return \Drupal\Core\Entity\EntityInterface + */ + abstract protected function createEntityFromCustomer(BaseResource $transaction): ?EntityInterface; + + /** + * Creates a customer base object from an entity. + * + * This method should be implemented by storage classes for specific + * customer base types. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * Entity. + * + * @throws \Drupal\Core\Entity\EntityStorageException + */ + abstract protected function createCustomerFromEntity(EntityInterface $entity): void; + + /** + * Deletes a customer base object from an entity. + * + * This method should be implemented by storage classes for specific + * customer base types. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * Entity. + * + * @throws \Drupal\Core\Entity\EntityStorageException + */ + abstract protected function deleteCustomerFromEntity(EntityInterface $entity): void; + +} diff --git a/modules/mollie_customers/src/Entity/Query/CustomerQuery.php b/modules/mollie_customers/src/Entity/Query/CustomerQuery.php new file mode 100644 index 0000000..ea7e37c --- /dev/null +++ b/modules/mollie_customers/src/Entity/Query/CustomerQuery.php @@ -0,0 +1,63 @@ +count) { + return $this->getCustomersFromMollie()->count(); + } + + return $this->getCustomerIds(); + } + + /** + * Returns the IDs of the customers for the configured Mollie account. + * + * @return array + * Array with IDs of the customers for the configured Mollie account. + */ + protected function getCustomerIds(): array { + $customerIds = []; + + $customers = $this->getCustomersFromMollie(); + foreach ($customers as $customer) { + /** @var \Mollie\Api\Resources\Customer $customer */ + $customerIds[$customer->id] = $customer->id; + } + + return $customerIds; + } + + /** + * Returns the customers for the configured Mollie account. + * + * @return \Mollie\Api\Resources\CustomerCollection + * + * TODO: Add paging, sorting and parameters. + * TODO: Only return customers created by this module. + */ + protected function getCustomersFromMollie(): CustomerCollection { + try { + return $this->mollieApiClient->customers->page(); + } + catch (ApiException $e) { + watchdog_exception('mollie', $e); + } + + return new CustomerCollection($this->mollieApiClient, 0, []); + } + +} diff --git a/modules/mollie_customers/src/Entity/Query/CustomerQueryBase.php b/modules/mollie_customers/src/Entity/Query/CustomerQueryBase.php new file mode 100644 index 0000000..ebb2f90 --- /dev/null +++ b/modules/mollie_customers/src/Entity/Query/CustomerQueryBase.php @@ -0,0 +1,47 @@ +mollieApiClient = $mollieApiClient; + } + +} diff --git a/modules/mollie_customers/src/Form/CustomerDeleteForm.php b/modules/mollie_customers/src/Form/CustomerDeleteForm.php new file mode 100644 index 0000000..a5ee2e4 --- /dev/null +++ b/modules/mollie_customers/src/Form/CustomerDeleteForm.php @@ -0,0 +1,56 @@ +getEntity(); + $entity->delete(); + + $this->logger('mollie_customers')->notice('@type: deleted %title.', + [ + '@type' => $this->entity->bundle(), + '%title' => $this->entity->label(), + ]); + + $form_state->setRedirect('entity.mollie_customer.collection'); + } + +} diff --git a/modules/mollie_customers/src/Form/CustomerForm.php b/modules/mollie_customers/src/Form/CustomerForm.php new file mode 100644 index 0000000..c2485a0 --- /dev/null +++ b/modules/mollie_customers/src/Form/CustomerForm.php @@ -0,0 +1,90 @@ +uuid = $uuid; + $this->mollieApiClient = $mollieApiClient; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity.repository'), + $container->get('uuid'), + $container->get('mollie.mollie'), + $container->get('entity_type.bundle.info'), + $container->get('datetime.time') + ); + } + + /** + * {@inheritdoc} + */ + public function form(array $form, FormStateInterface $form_state) { + $form = parent::form($form, $form_state); + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $formState) { + parent::submitForm($form, $formState); + } + +} diff --git a/src/Controller/MollieEntityBaseController.php b/src/Controller/MollieEntityBaseController.php new file mode 100644 index 0000000..2c20fb8 --- /dev/null +++ b/src/Controller/MollieEntityBaseController.php @@ -0,0 +1,35 @@ +t( + '@type @label', + [ + '@type' => $entity->getEntityType()->getLabel(), + '@label' => $entity->label(), + ] + ); + } + +} diff --git a/src/Mollie.php b/src/Mollie.php index b028868..c0c889f 100644 --- a/src/Mollie.php +++ b/src/Mollie.php @@ -8,6 +8,7 @@ use Drupal\Core\Entity\Query\QueryInterface; use Drupal\Core\Messenger\MessengerInterface; use Drupal\Core\Site\Settings; use Drupal\mollie\Entity\Query\PaymentQuery; +use Drupal\mollie_customers\Entity\Query\CustomerQuery; use Mollie\Api\Exceptions\ApiException; use Mollie\Api\Exceptions\IncompatiblePlatform; use Mollie\Api\MollieApiClient; @@ -144,6 +145,14 @@ class Mollie { $this->getClient() ); } + if ($entityType->id() === 'mollie_customer') { + return new CustomerQuery( + $entityType, + $conjunction, + ['\Drupal\mollie\Entity\Query'], + $this->getClient() + ); + } return NULL; } diff --git a/src/MollieEntityBasePermissions.php b/src/MollieEntityBasePermissions.php new file mode 100644 index 0000000..ab38b5c --- /dev/null +++ b/src/MollieEntityBasePermissions.php @@ -0,0 +1,85 @@ +entityTypeManager = $entityTypeManager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity_type.manager') + ); + } + + /** + * Returns permissions for operations on entities of a given type. + * + * @param \Drupal\Core\Entity\EntityTypeInterface $entityType + * Entity type. + * + * @return array + * Array with permissions. + */ + protected function entityBasePermissions(EntityTypeInterface $entityType): array { + $permissions = []; + + foreach ($this->getAvailableOperations($entityType) as $operation) { + $permissions["$operation {$entityType->id()} entities"] = [ + 'title' => ucfirst( + $this->t('@operation @label entities', [ + '@operation' => $operation, + '@label' => $entityType->getLabel() + ]) + ), + ]; + } + + return $permissions; + } + + /** + * Returns available operations on entities of a given type. + * + * @param \Drupal\Core\Entity\EntityTypeInterface $entityType + * Entity type. + * + * @return array + * Array with operations. + */ + protected function getAvailableOperations(EntityTypeInterface $entityType): array { + return ['create', 'view']; + } + +}