In the Recurly JS module, when a new subscription is being processed the function recurlyjs_subscribe_form_submit() is called. This function has some code in it which creates a new account in Recurly. If the recurly module is being used in conjunction with the 'user' entity type there is some entity type specific code that allows the username and email address to be copied from the $user object to the new Recurly account. Otherwise, the account is basically just default/blank data.

This is a bit of a regression from previous versions of the module (and older Recurly JS API versions) where it appears like first name, last name, email and other data added as part of the billing information when creating a new subscription where also automatically applied to the account in Recurly. So, if you entered, "John Smith " into the old billing information forms the account in Recurly would get those values. This isn't the case anymore. When you create a new subscription the account's name is just the account code instead of the first/last entered in the billing form.

I think we can maybe hard-code first name, last name into module. Since they are required fields on the billing information form no matter what your AVS requirements are. This would require removing the post render callback that removes the 'name' attribute from those elements and then getting it out of $form_state and populating the new Recurly account object.

It would also be nice if there was a hook invoked that other modules could use which would allow them to populate things like the email address, and username of a Recurly object.

In my use case a subscription is linked to a custom membership entity. And there's a one-one relationship between the membership entity and a user account in Drupal. So a user is required to create a user account first, and I then have their username + email address. But, there's currently no way for me to inject them into the Recurly account when it's created.

Proposed resolution

Add hook_recurlyjs_account_alter(Recurly_Account $account, $entity, $plan_code) and invoke it from within recurlyjs_subscribe_form_submit().

Consider moving this into an implementation of the new hook:

    if ($entity_type == 'user') {
      $recurly_account->email = $entity->mail;
      $recurly_account->username = $entity->name;
    }
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

eojthebrave created an issue. See original summary.

walangitan’s picture

@eojthebrave: Confirmed that this is an issue. I believe that this issue was first introduced in the update from V2 to V3 (and subsequently V4) of recurly.js and first addressed here for a default installation, but there is still more to do.

I think that the proposed resolution is a great course of action. Extending the creation/modification of a subscription so that the defaults can be overridden from other entities via hook. I'll look into translating the proposed resolution into a patch.

eojthebrave’s picture

Another, similar issue that I'm running into is the need to make use of the $subscription object created in recurlyjs_subscribe_form_submit() after the form is submitted. My use-case is wanting to log an event/transaction in Google Analytics. In previous versions of the module I added a submit handler to the form, and used the $form_state['recurly_status'] object that was added when validating the JS token. That code doesn't exist anymore though.

My suggestion in this case is to add something like the following:

// Allow other modules the chance to alter the new Recurly_Subscription object
// before it is saved.
drupal_alter('recurlyjs_subscription', $subscription, $entity, $plan_code);

$subscription->create();

// Allow other modules to react to the new subscription being created.
module_invoke_all('recurlyjs_new_subscription', $subscription, $entity);

If you remove the not necessary call to $recurly_account->create() (See #2854667: Display error message for declined credit cards) this will allow someone to modify the Recurly_Account, and Recurly_Subscription objects prior to saving them.

walangitan’s picture

Splitting up recurly_dme_modifications.patch into their corresponding issues. This work by eojthebrave tackles the proposed solution. I believe this can be the base from which we include a solution to resolve #3 as well.

eojthebrave’s picture

In my version I ended up re-writing this to use hook_recurlyjs_subscription_alter(), and hook_recurlyjs_new_subscription().

I've attached my updated recurlyjs.api.php file.

And here's what I've got for the submit handler at the moment. This also includes some code that's not really related to this issue, like catching some validation errors for example. However, what it does do is remove the extra call to $recurly_account->create() which isn't necessary, so we can have a single point to alter the entire subscription object before it's saved. And then invoke the alter, and new_subscription hooks. Hopefully this is helpful.

/**
 * Submit handler for recurlyjs_subscribe_form().
 */
function recurlyjs_subscribe_form_submit($form, &$form_state) {
  $entity_type = $form['#entity_type'];
  $entity = $form['#entity'];
  $plan_code = $form['#plan_code'];
  $currency = $form['#currency'];
  $recurly_token = isset($form_state['values']['recurly-token']) ? $form_state['values']['recurly-token'] : NULL;
  $coupon_code = isset($form_state['values']['coupon_code']) ? $form_state['values']['coupon_code'] : NULL;

  list($entity_id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
  $recurly_account = recurly_account_load(array('entity_type' => $entity_type, 'entity_id' => $entity_id));

  if (!$recurly_account) {
    $recurly_account = new Recurly_Account();

    $recurly_account->first_name = check_plain($form_state['values']['first_name']);
    $recurly_account->last_name = check_plain($form_state['values']['last_name']);

    if ($entity_type == 'user') {
      $recurly_account->email = $entity->mail;
      $recurly_account->username = $entity->name;
    }

    // Account code is the only property required for Recurly account creation.
    // https://dev.recurly.com/docs/create-an-account.
    $recurly_account->account_code = $entity_type . '-' . $entity_id;
  }

  $subscription = new Recurly_Subscription();
  $subscription->account = $recurly_account;
  $subscription->plan_code = $plan_code;
  $subscription->currency = $currency;
  $subscription->coupon_code = $coupon_code;

  // Allow other modules the chance to alter the new Recurly Subscription object
  // before it is saved.
  drupal_alter('recurlyjs_subscription', $subscription, $entity, $plan_code);

  // Billing info is based on the token we retrieved from the Recurly JS API
  // and should only contain the token in this case. We add this after the above
  // alter hook to ensure it's not modified.
  $subscription->account->billing_info = new Recurly_BillingInfo();
  $subscription->account->billing_info->token_id = $recurly_token;

  try {
    // This saves all of the data assembled above in addition to creating a new
    // subscription record.
    $subscription->create();
  }
  catch (Recurly_ValidationError $e) {
    // There was an error validating information in the form. For example,
    // credit card was declined. We don't need to log these in Drupal, you can
    // find the errors logged within Recurly.
    drupal_set_message(t('<strong>Unable to create subscription:</strong><br/>@error', array('@error' => $e->getMessage())), 'error');
    $form_state['rebuild'] = TRUE;
    return;
  }
  catch (Recurly_Error $e) {
    // Catch any non-validation errors. This will be things like unable to
    // contact Recurly API, or lower level errors. Display a generic message to
    // the user letting them know there was an error and then log the detailed
    // version. There's probably nothing a user can do to correct these errors
    // so we don't need to display the details.
    watchdog('recurlyjs', 'Unable to create subscription: @error', array('@error' => $e->getMessage()));
    drupal_set_message(t('An error occured while trying to create your subscription. Please contact a site administrator.'));
    $form_state['rebuild'] = TRUE;
    return;
  }

  // Allow other modules to react to the new subscription being created.
  module_invoke_all('recurlyjs_new_subscription', $subscription, $entity);

  drupal_set_message(t('Account upgraded to @plan!', array('@plan' => $subscription->plan->name)));

  // Save the account locally immediately so that subscriber information may
  // be retrieved when the user is directed back to the /subscription tab.
  try {
    $account = $subscription->account->get();
    recurly_account_save($account, $entity_type, $entity_id);
  }
  catch (Recurly_Error $e) {
    watchdog('recurly', 'New subscriber account could not be retreived from Recurly. Received the following error: @error', array('@error' => $e->getMessage()));
  }

  $form_state['redirect'] = $entity_type . '/' . $entity_id . '/subscription';
}
markdorison’s picture

markdorison’s picture

Priority: Normal » Major
Status: Active » Needs work
markdorison’s picture

Status: Needs work » Needs review
FileSize
6.04 KB

I have combined the patch and code in #5 into a single patch so it can be more easily reviewed.

adamzimmermann’s picture

I added an instance of both hooks defined in recurlyjs.pages.inc and I confirmed that they are being triggered when I submit the form at /user/1/subscription/signup/test-plan-2. If I need to perform additional tests I can, just let me know what I should be testing.

I also updated the patch to adding a space after the : in the error message.

- Unable to create subscription:@error
+ Unable to create subscription: @error

markdorison’s picture

Status: Needs review » Needs work

Patch no longer applies cleanly.

adamzimmermann’s picture

Re-rolled the patch.

markdorison’s picture

Status: Needs work » Needs review

markdorison’s picture

Status: Needs review » Fixed
markdorison’s picture

Version: 7.x-3.x-dev » 8.x-1.x-dev
Status: Fixed » Patch (to be ported)
adamzimmermann’s picture

Assigned: Unassigned » adamzimmermann
adamzimmermann’s picture

Status: Patch (to be ported) » Needs review
FileSize
12.2 KB

This replicates the functionality from D7, but it is done with events instead of hooks. I did some limited testing and it seemed to work fine, but additional testing would be great.

adamzimmermann’s picture

Assigned: adamzimmermann » Unassigned
colan’s picture

Status: Needs review » Needs work

In the updated README, I can see "recurlyjs" in the example code. So does this change only apply if you're using RecurlyJS and not Hosted Pages? Will Hosted Pages still work the old way? It would be helpful to clarify in provided comments.

adamzimmermann’s picture

Status: Needs work » Needs review
FileSize
11.75 KB

Good catch. Yes, the events are only fired from the recurlyjs module. I reverted my changes to the recurly README, and added them to the recurlyjs README.

colan’s picture

Status: Needs review » Needs work

Thanks for that.

+++ b/modules/recurlyjs/src/Form/RecurlyJsSubscribeForm.php
@@ -2,7 +2,11 @@
+use Drupal\Component\Utility\SafeMarkup;

@@ -71,23 +75,40 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
+      $recurly_account->first_name = SafeMarkup::checkPlain($form_state->getValue('first_name'));
+      $recurly_account->last_name = SafeMarkup::checkPlain($form_state->getValue('last_name'));

This calls are deprecated. See #2868491: Replace deprecated usage of SafeMarkup::checkPlain() for details. Sorry about not catching this earlier.

adamzimmermann’s picture

Status: Needs work » Needs review
FileSize
11.72 KB

Ah, yes. When I reviewed the patch on that other issue I thought it looked familiar. Good catch. Updated the patch.

markdorison’s picture

Status: Needs review » Fixed

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.