recurly-8.x-1.x-dev/recurly.module

recurly.module
<?php

/**
 * @file
 * Recurly.
 *
 * Uses Recurly's PHP client library to interact with their API and integrate it
 * with Drupal user accounts.
 */

use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Url;

/**
 * Implements hook_help().
 */
function recurly_help($route_name, RouteMatchInterface $route_match) {
  switch ($route_name) {
    case 'recurly.subscription_plans_overview':
      return '<p>' . t('Plans should be defined and updated at Recurly.com itself. The order and enabled state of a plan will affect the built-in signup pages.') . '</p>';
  }
}

/**
 * Implements hook_theme().
 */
function recurly_theme() {
  $items['recurly_subscription_summary'] = [
    'variables' => [
      'plan_code' => NULL,
      'plan_name' => NULL,
      'state_array' => NULL,
      'state_status' => NULL,
      'period_end_header' => NULL,
      'cost' => NULL,
      'quantity' => NULL,
      'add_ons' => [],
      'start_date' => NULL,
      'end_date' => NULL,
      'current_period_start' => NULL,
      'current_period_ends_at' => NULL,
      'total' => NULL,
      'subscription_links' => [],
      'message' => NULL,
      'subscription' => NULL,
      'account' => NULL,
      'custom_properties' => [],
    ],
    'template' => 'recurly-subscription-summary',
  ];
  $items['recurly_credit_card_information'] = [
    'variables' => [
      'first_name' => NULL,
      'last_name' => NULL,
      'card_type' => NULL,
      'exp_date' => NULL,
      'last_four' => NULL,
      'card_num_masked' => NULL,
    ],
    'template' => 'recurly-credit-card-information',
  ];
  $items['recurly_invoice'] = [
    'variables' => [
      'invoice' => NULL,
      'invoice_account' => NULL,
      'entity_type' => NULL,
      'entity' => NULL,
      'error_message' => NULL,
    ],
    'template' => 'recurly-invoice',
  ];
  $items['recurly_invoice_list'] = [
    'variables' => [
      'invoices' => NULL,
      'entity_type' => NULL,
      'entity' => NULL,
      'per_page' => NULL,
      'total' => NULL,
    ],
    'template' => 'recurly-invoice-list',
  ];
  $items['recurly_subscription_plan_select'] = [
    'variables' => [
      'plans' => NULL,
      'entity_type' => NULL,
      'entity' => NULL,
      'currency' => NULL,
      'mode' => 'signup',
      'subscriptions' => NULL,
      'subscription_id' => NULL,
    ],
    'template' => 'recurly-subscription-plan-select',
    // The $mode of "change" or "signup" may be appended to the template name.
    'pattern' => 'recurly_subscription_plan_select__',
  ];
  $items['recurly_subscription_cancel_confirm'] = [
    'render element' => 'form',
    'template' => 'recurly-subscription-cancel-confirm',
  ];
  $items['recurly_subscription_price_interval'] = [
    'variables' => [
      'time_length' => NULL,
      'amount' => [],
      'time_unit' => NULL,
      'time_indicator' => NULL,
    ],
    'template' => 'recurly-subscription-price-interval',
  ];

  return $items;
}

/**
 * Implements hook_libraries_info().
 */
function recurly_libraries_info() {
  $libraries['recurly'] = [
    'name' => 'Recurly',
    'vendor url' => 'https://github.com/recurly/recurly-client-php',
    'download url' => 'https://github.com/recurly/recurly-client-php/releases/latest',
    'path' => 'lib',
    'version' => '2.*',
    'files' => [
      'php' => ['recurly.php'],
    ],
  ];
  return $libraries;
}

/**
 * Implements hook_entity_type_alter().
 */
function recurly_entity_type_alter(array &$entity_types) {
  \Drupal::service('recurly.entity_type')->entityTypeAlter($entity_types);
}

/**
 * Implements hook_entity_update().
 */
function recurly_entity_update(EntityInterface $entity) {
  return \Drupal::service('recurly.entity_operations')->entityUpdate($entity);
}

/**
 * Implements hook_entity_delete().
 */
function recurly_entity_delete(EntityInterface $entity) {
  return \Drupal::service('recurly.entity_operations')->entityDelete($entity);
}

/**
 * Implements hook_user_cancel().
 *
 * Cancel a Recurly account when the user account is canceled. It's important to
 * note this hook is *not* called if the user account is just being straight-up
 * deleted, which is fine because hook_entity_delete() will be called for that
 * situation.
 */
function recurly_user_cancel($edit, $account, $method) {
  $entity_type = \Drupal::config('recurly.settings')->get('recurly_entity_type');
  if ($entity_type === 'user') {
    // Check for a local account first, no need to attempt to close an account
    // if we don't have any information about it.
    $local_account = recurly_account_load([
      'entity_type' => $entity_type,
      'entity_id' => $account->id(),
    ], TRUE);
    if ($local_account) {
      $recurly_account = recurly_account_load([
        'entity_type' => $entity_type,
        'entity_id' => $account->id(),
      ]);
      recurly_account_close($recurly_account);
    }
  }
}

/**
 * Loads a Recurly account record based on the given conditions.
 *
 * @param array $conditions
 *   An associative array of values to look for in the conditions of the query;
 *   normally used to look-up on account_code or uid.
 * @param bool $local
 *   Boolean indicating whether or not to only return local data; defaults to
 *   FALSE, meaning it will attempt to load the full linked account object.
 *
 * @return bool|object
 *   The fully loaded account object from Recurly if possible. If Recurly cannot
 *   be accessed, just returns an object representing the data stored locally.
 *   If the account no longer exists at Recurly, the returned object will
 *   include an additional 'orphaned' property set to TRUE. Returns FALSE if no
 *   data can be found locally at all.
 */
function recurly_account_load(array $conditions = [], $local = FALSE) {
  // Create a base select query object.
  $query = \Drupal::database()->select('recurly_account', 'ra')->fields('ra');

  // Add conditions to it based on the passed array.
  foreach ($conditions as $key => $value) {
    $query->condition($key, $value);
  }

  // Retrieve data if available.
  $data = $query->execute()->fetchObject();

  // Bail now if no data was returned.
  if (empty($data)) {
    return FALSE;
  }

  // If we only want local data, return it now.
  if ($local) {
    return $data;
  }

  // Attempt to load the full account from Recurly.
  $client = Drupal::service('recurly.client')->getClientFromSettings();
  if ($client) {
    try {
      $recurly_account = \Recurly_Account::get($data->account_code, $client);
    }
    catch (\Recurly_NotFoundError $e) {
      // Return the orphaned data if no account was found at Recurly.
      $data->orphaned = TRUE;
      return $data;
    }

    // If any data has changed remotely, update it locally now.
    if ($recurly_account->state != $data->status) {
      recurly_account_save($recurly_account, $data->entity_type, $data->entity_id);
    }

    return $recurly_account;
  }

  return FALSE;
}

/**
 * Saves an account record.
 *
 * Optionally exporting the saved data to Recurly as a new account or update
 * request as necessary.
 *
 * @param object $recurly_account
 *   The Recurly account object to save.
 * @param string $entity_type
 *   The entity type with which this account is associated.
 * @param int $entity_id
 *   The ID of the entity with which this account is associated.
 * @param bool $export
 *   Boolean indicating whether or not the saved account information should also
 *   be exported to Recurly by either creating a new account or updating an
 *   existing account based on the saved values using the Recurly API.
 *
 * @return mixed
 *   FALSE on failure of either the local save or optional export or
 *   STATUS_INSERT or STATUS_UPDATE indicating the type of query performed to
 *   save the account information locally.
 */
function recurly_account_save($recurly_account, $entity_type, $entity_id, $export = FALSE) {
  // First attempt to save the data at Recurly if specified. Failing an export
  // will prevent local data from being saved so you don't end up with a local
  // record that does not match a record at Recurly.
  if ($export) {
    // Check to see if the record already exists.
    try {
      \Recurly_Account::get($recurly_account->account_code);
      // If it does than update the account now.
      try {
        $recurly_account->update();
      }
      catch (\Recurly_NotFoundError $e) {
        watchdog_exception('recurly', $e);
        return FALSE;
      }
    }
    catch (Exception $e) {
      // Try and create the new account at Recurly now.
      try {
        $recurly_account->create();
      }
      catch (\Recurly_NotFoundError $e) {
        watchdog_exception('recurly', $e);
        return FALSE;
      }
    }
  }

  // Generate an array of data to save.
  $fields = [
    'entity_type' => $entity_type,
    'entity_id' => $entity_id,
    'updated' => \Drupal::time()->getRequestTime(),
  ];

  // Add the status based on whatever data we have available.
  if (!empty($recurly_account->state)) {
    $fields['status'] = $recurly_account->state;
  }
  elseif (!empty($recurly_account->status)) {
    $fields['status'] = $recurly_account->status;
  }
  else {
    $fields['status'] = 'active';
  }

  // Execute a merge query that will either insert a new record or update an
  // existing record accordingly.
  return \Drupal::database()->merge('recurly_account')
    ->key(['account_code' => $recurly_account->account_code])
    ->fields($fields)
    ->execute();
}

/**
 * Cancel a remote Recurly account.
 */
function recurly_account_close($recurly_account, $cancelation_method = NULL) {
  if (empty($cancelation_method)) {
    $cancelation_method = \Drupal::config('recurly.settings')->get('recurly_subscription_cancel_behavior');
  }

  if (empty($recurly_account->orphaned)) {
    try {
      // By default, closing an account will cancel all the subscriptions in
      // that account. If refunding accounts upon cancellation, we must manually
      // terminate each active subscription.
      if ($cancelation_method !== 'cancel') {
        $subscription_list = \Recurly_SubscriptionList::getForAccount($recurly_account->account_code);
        foreach ($subscription_list as $subscription) {
          if ($subscription->state === 'active') {
            $username = $recurly_account->username ? $recurly_account->username : $recurly_account->account_code;
            if ($cancelation_method === 'terminate_prorated') {
              \Drupal::messenger()->addMessage(t('Prorated refund for @plan refunded to @username.', [
                '@plan' => $subscription->plan->name,
                '@username' => $username,
              ]));
              $subscription->terminateAndPartialRefund();
            }
            else {
              \Drupal::messenger()->addMessage(t('Full refund for @plan refunded to @username.', [
                '@plan' => $subscription->plan->name,
                '@username' => $username,
              ]));
              $subscription->terminateAndRefund();
            }
          }
        }
      }
      // Then close the account.
      $recurly_account->close();
    }
    catch (\Recurly_Error $e) {
      // Throw the highest level alert. Failure could result in accounts getting
      // charged after the Drupal account is deleted.
      \Drupal::logger('recurly')->alert('A Recurly account with the account code @code was intended to be closed, but may still open! The Recurly API returned the error "@error".', [
        '@code' => $recurly_account->account_code,
        '@error' => $e->getMessage(),
      ]);
      return FALSE;
    }
  }

  return TRUE;
}

/**
 * Delete a Recurly database record and the account on Recurly.com.
 */
function recurly_account_delete($recurly_account, $cancelation_method = NULL) {
  recurly_account_close($recurly_account, $cancelation_method);
  \Drupal::database()->delete('recurly_account')
    ->condition('account_code', $recurly_account->account_code)
    ->execute();
}

/**
 * Check if an account has any active subscriptions.
 *
 * @return bool
 *   TRUE if the user has an active subscription, or FALSE if no
 *   active subscriptions are located.
 */
function recurly_account_has_active_subscriptions($account_code) {
  return count(recurly_account_get_subscriptions($account_code, 'active')) > 0 ? TRUE : FALSE;
}

/**
 * Get a list of active subscriptions for a particular account code.
 */
function recurly_account_get_subscriptions($account_code, $state) {
  static $accounts;

  if (!isset($accounts[$account_code])) {
    $accounts[$account_code] = [];

    $client = Drupal::service('recurly.client')->getClientFromSettings();
    $subscription_list = $client ? \Recurly_SubscriptionList::getForAccount($account_code, ['per_page' => 200], $client) : [];
    $accounts[$account_code] = ['active' => [], 'expired' => []];
    foreach ($subscription_list as $subscription) {
      if ($subscription->state !== 'expired') {
        $accounts[$account_code]['active'][$subscription->uuid] = $subscription;
      }
      else {
        $accounts[$account_code]['expired'][$subscription->uuid] = $subscription;
      }
    }
  }

  if ($state === 'active') {
    return $accounts[$account_code]['active'];
  }
  elseif ($state === 'expired') {
    return $accounts[$account_code]['expired'];
  }

  // Otherwise return all subscriptions.
  return $accounts[$account_code];
}

/**
 * Returns an array of subscription plan objects for the current account.
 *
 * Retrieves them from a local cache if possible.
 *
 * @param bool $reset_cache
 *   Boolean indicating whether or not to reset the subscription plan cache when
 *   retrieving plans.
 *
 * @return array|\Recurly_PlanList
 *   An array of subscription plan objects.
 */
function recurly_subscription_plans($reset_cache = FALSE) {
  static $plans;

  // If we haven't specified a cache reset, attempt to retrieve plans from the
  // cache before getting them from Recurly.
  if (!$reset_cache && isset($plans)) {
    return $plans;
  }

  // Retrieve the subscription plans from Recurly.
  $client = Drupal::service('recurly.client')->getClientFromSettings();
  return $client ? \Recurly_PlanList::get(NULL, $client) : [];
}

/**
 * Determines if subscription plans have been configured.
 *
 * Retrieves them from a local cache if possible.
 *
 * @param bool $reset_cache
 *   Boolean indicating whether or not to reset the subscription plan cache when
 *   retrieving plans.
 *
 * @return bool
 *   TRUE if there is at least one plan configured, else FALSE.
 */
function recurly_subscription_plans_configured($reset_cache = FALSE) {
  return !empty(recurly_subscription_plans($reset_cache));
}

/**
 * Determine if a Recurly subscription object is currently in a trial.
 */
function recurly_subscription_in_trial($subscription) {
  if ($subscription->trial_started_at && $subscription->trial_ends_at) {
    $subscription->trial_started_at->setTimezone(new DateTimeZone('UTC'));
    $subscription->trial_ends_at->setTimezone(new DateTimeZone('UTC'));
    $start = $subscription->trial_started_at->format('U');
    $end = $subscription->trial_ends_at->format('U');
    if (\Drupal::time()->getRequestTime() > $start && \Drupal::time()->getRequestTime() < $end) {
      return TRUE;
    }
  }
  return FALSE;
}

/**
 * Return a URL for a specified operation.
 *
 * This function should be used when generating links to operations that may
 * have variable locations, such as updating billing information or changing
 * plan levels.
 *
 * @param string $operation
 *   May be one of the following operations:
 *    - select_plan ($context contains account_code or entity_type/entity_id).
 *    - change_plan.
 *    - cancel.
 *    - update_billing ($context contains account_code).
 *    - subscribe ($context contains plan_code).
 *    - reactivate ($context contains account_code or entity_type/entity_id).
 * @param array $context
 *   An array of contextual information needed for generating the link.
 *
 * @return \Drupal\Core\Url
 *   A Drupal Url object containing a route and route parameters, or NULL if no
 *   module is available to handle the operation.
 */
function recurly_url($operation, array $context) {
  $urls = \Drupal::moduleHandler()->invokeAll('recurly_url_info', [
    $operation,
    $context,
  ]);

  $return_url = NULL;
  foreach ($urls as $url) {
    if ($url) {
      $return_url = $url;
    }
  }
  return $return_url;
}

/**
 * Implements hook_recurly_url_info().
 */
function recurly_recurly_url_info($operation, $context) {
  // Only provide URLs for built-in page types.
  $recurly_entity_type = \Drupal::config('recurly.settings')->get('recurly_entity_type');
  $context_entity_type = empty($context['entity_type']) ? NULL : $context['entity_type'];
  if (empty($recurly_entity_type) || $recurly_entity_type !== $context_entity_type) {
    return;
  }

  switch ($operation) {
    case 'select_plan':
      return Url::fromRoute("entity.$recurly_entity_type.recurly_signup", [
        $recurly_entity_type => $context['entity']->id(),
      ]);

    case 'change_plan':
      if (isset($context['plan_code'])) {
        return Url::fromRoute("entity.$recurly_entity_type.recurly_planchange", [
          $recurly_entity_type => $context['entity']->id(),
          'subscription_id' => $context['subscription']->uuid,
          'new_plan_code' => $context['plan_code'] ?? NULL,
        ]);
      }
      else {
        return Url::fromRoute("entity.$recurly_entity_type.recurly_change", [
          $recurly_entity_type => $context['entity']->id(),
        ]);
      }

    case 'cancel':
      return Url::fromRoute("entity.$recurly_entity_type.recurly_cancel", [
        $recurly_entity_type => $context['entity']->id(),
        'subscription_id' => $context['subscription']->uuid,
      ]);

    case 'reactivate':
      return Url::fromRoute("entity.$recurly_entity_type.recurly_reactivate", [
        $recurly_entity_type => $context['entity']->id(),
        'subscription_id' => $context['subscription']->uuid,
      ]);

    case 'redeem_coupon':
      return Url::fromRoute("entity.$recurly_entity_type.recurly_coupon", [
        $recurly_entity_type => $context['entity']->id(),
      ]);

    case 'quantity':
      return Url::fromRoute("entity.$recurly_entity_type.recurly_quantity", [
        $recurly_entity_type => $context['entity']->id(),
        'subscription_id' => $context['subscription']->uuid,
      ]);
  }
}

/**
 * Provide a list of currencies supported by Recurly.
 */
function recurly_currency_list() {
  $currencies = [
    'USD' => ['$', ' USD'],
    'AUD' => ['$', ' AUD'],
    'CAD' => ['$', ' CAD'],
    'EUR' => ['', ' €', ' ', ','],
    'GBP' => ['£', ''],
    'CZK' => ['', ' Kč', ' ', ','],
    'DKK' => ['', ' kr.', ' ', ','],
    'HUF' => ['', ' Ft', NULL, NULL, 0],
    'JPY' => ['¥', ''],
    'NOK' => ['', ' Nkr', ' ', ','],
    'NZD' => ['NZ$', ''],
    'PLN' => ['', ' zł', ' ', ','],
    'SGD' => ['S$', ''],
    'SEK' => ['', ' kr', ' ', ','],
    'CHF' => ['', ' Fr.', NULL, NULL, NULL, '0.05'],
    'ZAR' => ['R', ''],
  ];
  return $currencies;
}

/**
 * Calculate a prorated refund amount.
 */
function recurly_subscription_calculate_refund($subscription, $type = 'prorated') {
  if ($type == 'none' || recurly_subscription_in_trial($subscription)) {
    return 0;
  }

  $subscription->current_period_started_at->setTimezone(new DateTimeZone('UTC'));
  $subscription->current_period_ends_at->setTimezone(new DateTimeZone('UTC'));
  $start = $subscription->current_period_started_at->format('U');
  $end = $subscription->current_period_ends_at->format('U');
  $total_period_time = $end - $start;
  $remaining_time = $end - \Drupal::time()->getRequestTime();

  // Past due subscriptions get no refund.
  if ($remaining_time < 0) {
    return 0;
  }
  if ($type == 'full') {
    return $subscription->unit_amount_in_cents;
  }
  elseif ($type === 'prorated') {
    return $subscription->unit_amount_in_cents * $remaining_time / $total_period_time;
  }
}

/**
 * Implements hook_preprocess_recurly_subscription_plan_select().
 *
 * Shared preprocess function for the presentation of the signup & change page.
 */
function template_preprocess_recurly_subscription_plan_select(&$variables) {
  \Drupal::service('recurly.recurly_preprocess')->preprocessRecurlySubscriptionPlanSelect($variables);
}

/**
 * Implements hook_preprocess_recurly_subscription_cancel_confirm().
 */
function template_preprocess_recurly_subscription_cancel_confirm(&$variables) {
  \Drupal::service('recurly.recurly_preprocess')->preprocessRecurlySubscriptionCancelConfirm($variables);
}

/**
 * Implements hook_preprocess_recurly_invoice_list().
 */
function template_preprocess_recurly_invoice_list(&$variables) {
  \Drupal::service('recurly.recurly_preprocess')->preprocessRecurlyInvoiceList($variables);
}

/**
 * Implements hook_preprocess_recurly_invoice().
 */
function template_preprocess_recurly_invoice(&$variables) {
  \Drupal::service('recurly.recurly_preprocess')->preprocessRecurlyInvoice($variables);
}

/**
 * Implements hook_form_FORM_ID_alter().
 *
 * Alter the user edit form to redirect new users to plan selection.
 */
function recurly_form_user_form_alter(&$form, FormStateInterface $form_state, $form_id) {
  if (\Drupal::config('recurly.settings')->get('recurly_entity_type') === 'user') {
    $form['actions']['submit']['#submit'][] = 'recurly_user_edit_form_submit_redirect';
  }
}

/**
 * Redirects to the subscription selection page after setting user password.
 *
 * @param array $form
 *   The form definition.
 * @param \Drupal\Core\Form\FormStateInterface $form_state
 *   The current state of the form.
 */
function recurly_user_edit_form_submit_redirect(array $form, FormStateInterface $form_state) {
  if (!\Drupal::currentUser()->isAuthenticated() || !recurly_subscription_plans_configured()) {
    return;
  }
  $entity_type_id = \Drupal::config('recurly.settings')->get('recurly_entity_type');
  $authenticated_route_name = "entity.$entity_type_id.recurly_signup";

  try {
    $authenticated_route = \Drupal::service('router.route_provider')->getRouteByName($authenticated_route_name);
  }
  catch (RouteNotFoundException $e) {
    watchdog_exception('recurly', $e);
    return;
  }

  $form_state->setRedirect($authenticated_route_name, [
    'user' => \Drupal::currentUser()->id(),
  ], $authenticated_route->getOptions());
}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc