diff --git a/java/com/google/domain/registry/braintree/BUILD b/java/com/google/domain/registry/braintree/BUILD
index a7e1ebcbd..eab1e9fa0 100644
--- a/java/com/google/domain/registry/braintree/BUILD
+++ b/java/com/google/domain/registry/braintree/BUILD
@@ -6,8 +6,10 @@ java_library(
srcs = glob(["*.java"]),
visibility = ["//visibility:public"],
deps = [
+ "//java/com/google/common/base",
"//java/com/google/domain/registry/config",
"//java/com/google/domain/registry/keyring/api",
+ "//java/com/google/domain/registry/model",
"//third_party/java/braintree",
"//third_party/java/dagger",
"//third_party/java/jsr305_annotations",
diff --git a/java/com/google/domain/registry/braintree/BraintreeRegistrarSyncer.java b/java/com/google/domain/registry/braintree/BraintreeRegistrarSyncer.java
new file mode 100644
index 000000000..59159c277
--- /dev/null
+++ b/java/com/google/domain/registry/braintree/BraintreeRegistrarSyncer.java
@@ -0,0 +1,105 @@
+// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.domain.registry.braintree;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Verify.verify;
+
+import com.google.common.base.Optional;
+import com.google.common.base.VerifyException;
+import com.google.domain.registry.model.registrar.Registrar;
+import com.google.domain.registry.model.registrar.RegistrarContact;
+
+import com.braintreegateway.BraintreeGateway;
+import com.braintreegateway.Customer;
+import com.braintreegateway.CustomerRequest;
+import com.braintreegateway.Result;
+import com.braintreegateway.exceptions.NotFoundException;
+
+import javax.inject.Inject;
+
+/** Helper for creating Braintree customer entries for registrars. */
+public class BraintreeRegistrarSyncer {
+
+ private final BraintreeGateway braintree;
+
+ @Inject
+ BraintreeRegistrarSyncer(BraintreeGateway braintreeGateway) {
+ this.braintree = braintreeGateway;
+ }
+
+ /**
+ * Syncs {@code registrar} with Braintree customer entry, creating it if one doesn't exist.
+ *
+ *
The customer ID will be the same as {@link Registrar#getClientIdentifier()}.
+ *
+ *
Creating a customer object in Braintree's database is a necessary step in order to associate
+ * a payment with a registrar. The transaction will fail if the customer object doesn't exist.
+ *
+ * @throws IllegalArgumentException if {@code registrar} is not using BRAINTREE billing
+ * @throws VerifyException if the Braintree API returned a failure response
+ */
+ public void sync(Registrar registrar) throws VerifyException {
+ String id = registrar.getClientIdentifier();
+ checkArgument(registrar.getBillingMethod() == Registrar.BillingMethod.BRAINTREE,
+ "Registrar (%s) billing method (%s) is not BRAINTREE", id, registrar.getBillingMethod());
+ CustomerRequest request = createRequest(registrar);
+ Result result;
+ if (doesCustomerExist(id)) {
+ result = braintree.customer().update(id, request);
+ } else {
+ result = braintree.customer().create(request);
+ }
+ verify(result.isSuccess(),
+ "Failed to sync registrar (%s) to braintree customer: %s", id, result.getMessage());
+ }
+
+ private CustomerRequest createRequest(Registrar registrar) {
+ CustomerRequest result =
+ new CustomerRequest()
+ .id(registrar.getClientIdentifier())
+ .customerId(registrar.getClientIdentifier())
+ .company(registrar.getRegistrarName());
+ Optional contact = getBillingContact(registrar);
+ if (contact.isPresent()) {
+ result.email(contact.get().getEmailAddress());
+ result.phone(contact.get().getPhoneNumber());
+ result.fax(contact.get().getFaxNumber());
+ } else {
+ result.email(registrar.getEmailAddress());
+ result.phone(registrar.getPhoneNumber());
+ result.fax(registrar.getFaxNumber());
+ }
+ return result;
+ }
+
+ private Optional getBillingContact(Registrar registrar) {
+ for (RegistrarContact contact : registrar.getContacts()) {
+ if (contact.getTypes().contains(RegistrarContact.Type.BILLING)) {
+ return Optional.of(contact);
+ }
+ }
+ return Optional.absent();
+ }
+
+ private boolean doesCustomerExist(String id) {
+ try {
+ braintree.customer().find(id);
+ return true;
+ } catch (NotFoundException e) {
+ return false;
+ }
+ }
+}
diff --git a/java/com/google/domain/registry/ui/server/registrar/BUILD b/java/com/google/domain/registry/ui/server/registrar/BUILD
index 6702fe28c..252e4b09f 100644
--- a/java/com/google/domain/registry/ui/server/registrar/BUILD
+++ b/java/com/google/domain/registry/ui/server/registrar/BUILD
@@ -14,6 +14,7 @@ java_library(
"//java/com/google/common/collect",
"//java/com/google/common/io",
"//java/com/google/common/net",
+ "//java/com/google/domain/registry/braintree",
"//java/com/google/domain/registry/config",
"//java/com/google/domain/registry/export/sheet",
"//java/com/google/domain/registry/flows",
diff --git a/java/com/google/domain/registry/ui/server/registrar/RegistrarPaymentSetupAction.java b/java/com/google/domain/registry/ui/server/registrar/RegistrarPaymentSetupAction.java
index 4c380f954..3ca10089c 100644
--- a/java/com/google/domain/registry/ui/server/registrar/RegistrarPaymentSetupAction.java
+++ b/java/com/google/domain/registry/ui/server/registrar/RegistrarPaymentSetupAction.java
@@ -21,6 +21,7 @@ import static java.util.Arrays.asList;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableMap;
+import com.google.domain.registry.braintree.BraintreeRegistrarSyncer;
import com.google.domain.registry.config.ConfigModule.Config;
import com.google.domain.registry.config.RegistryEnvironment;
import com.google.domain.registry.model.registrar.Registrar;
@@ -76,6 +77,7 @@ import javax.inject.Inject;
public final class RegistrarPaymentSetupAction implements Runnable, JsonAction {
@Inject BraintreeGateway braintreeGateway;
+ @Inject BraintreeRegistrarSyncer customerSyncer;
@Inject JsonActionRunner jsonActionRunner;
@Inject Registrar registrar;
@Inject RegistryEnvironment environment;
@@ -93,6 +95,7 @@ public final class RegistrarPaymentSetupAction implements Runnable, JsonAction {
if (!json.isEmpty()) {
return JsonResponseHelper.create(ERROR, "JSON request object must be empty");
}
+
// payment.js is hard-coded to display a specific SOY error template when encountering the
// following error messages.
if (environment == RegistryEnvironment.SANDBOX) {
@@ -102,6 +105,10 @@ public final class RegistrarPaymentSetupAction implements Runnable, JsonAction {
// Registrar needs to contact support to have their billing bit flipped.
return JsonResponseHelper.create(ERROR, "not-using-cc-billing");
}
+
+ // In order to set the customerId field on the payment, the customer must exist.
+ customerSyncer.sync(registrar);
+
return JsonResponseHelper
.create(SUCCESS, "Success", asList(
ImmutableMap.of(
diff --git a/javatests/com/google/domain/registry/testing/ReflectiveFieldExtractor.java b/javatests/com/google/domain/registry/testing/ReflectiveFieldExtractor.java
new file mode 100644
index 000000000..2739dcf1e
--- /dev/null
+++ b/javatests/com/google/domain/registry/testing/ReflectiveFieldExtractor.java
@@ -0,0 +1,37 @@
+// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.domain.registry.testing;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import java.lang.reflect.Field;
+
+/** Utility class for extracting encapsulated contents of objects for testing. */
+public final class ReflectiveFieldExtractor {
+
+ /** Extracts private {@code fieldName} on {@code object} without public getter. */
+ public static T extractField(Class returnType, Object object, String fieldName)
+ throws NoSuchFieldException, SecurityException, IllegalAccessException {
+ Field field = object.getClass().getDeclaredField(fieldName);
+ field.setAccessible(true);
+ Object value = field.get(object);
+ checkArgument(value == null || returnType.isInstance(value));
+ @SuppressWarnings("unchecked")
+ T result = (T) value;
+ return result;
+ }
+
+ private ReflectiveFieldExtractor() {}
+}
diff --git a/javatests/com/google/domain/registry/ui/server/registrar/BUILD b/javatests/com/google/domain/registry/ui/server/registrar/BUILD
index 92589da94..a6eff5f47 100644
--- a/javatests/com/google/domain/registry/ui/server/registrar/BUILD
+++ b/javatests/com/google/domain/registry/ui/server/registrar/BUILD
@@ -13,6 +13,7 @@ java_library(
"//java/com/google/common/io",
"//java/com/google/common/net",
"//java/com/google/common/testing",
+ "//java/com/google/domain/registry/braintree",
"//java/com/google/domain/registry/config",
"//java/com/google/domain/registry/export/sheet",
"//java/com/google/domain/registry/model",
diff --git a/javatests/com/google/domain/registry/ui/server/registrar/RegistrarPaymentActionTest.java b/javatests/com/google/domain/registry/ui/server/registrar/RegistrarPaymentActionTest.java
index bb61bb766..bdd132dd4 100644
--- a/javatests/com/google/domain/registry/ui/server/registrar/RegistrarPaymentActionTest.java
+++ b/javatests/com/google/domain/registry/ui/server/registrar/RegistrarPaymentActionTest.java
@@ -14,8 +14,8 @@
package com.google.domain.registry.ui.server.registrar;
-import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.truth.Truth.assertThat;
+import static com.google.domain.registry.testing.ReflectiveFieldExtractor.extractField;
import static java.util.Arrays.asList;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.verify;
@@ -45,7 +45,6 @@ import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
-import java.lang.reflect.Field;
import java.math.BigDecimal;
/** Tests for {@link RegistrarPaymentAction}. */
@@ -444,14 +443,4 @@ public class RegistrarPaymentActionTest {
"field", "amount",
"results", asList());
}
-
- @SuppressWarnings("unchecked")
- private static T extractField(Class returnType, Object object, String fieldName)
- throws NoSuchFieldException, SecurityException, IllegalAccessException {
- Field field = object.getClass().getDeclaredField(fieldName);
- field.setAccessible(true);
- Object value = field.get(object);
- checkArgument(returnType.isInstance(value));
- return (T) value;
- }
}
diff --git a/javatests/com/google/domain/registry/ui/server/registrar/RegistrarPaymentSetupActionTest.java b/javatests/com/google/domain/registry/ui/server/registrar/RegistrarPaymentSetupActionTest.java
index 2d68d371c..3c41aaa84 100644
--- a/javatests/com/google/domain/registry/ui/server/registrar/RegistrarPaymentSetupActionTest.java
+++ b/javatests/com/google/domain/registry/ui/server/registrar/RegistrarPaymentSetupActionTest.java
@@ -16,9 +16,12 @@ package com.google.domain.registry.ui.server.registrar;
import static com.google.common.truth.Truth.assertThat;
import static java.util.Arrays.asList;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.google.common.collect.ImmutableMap;
+import com.google.domain.registry.braintree.BraintreeRegistrarSyncer;
import com.google.domain.registry.config.RegistryEnvironment;
import com.google.domain.registry.model.registrar.Registrar;
import com.google.domain.registry.testing.AppEngineRule;
@@ -49,11 +52,15 @@ public class RegistrarPaymentSetupActionTest {
@Mock
private ClientTokenGateway clientTokenGateway;
+ @Mock
+ private BraintreeRegistrarSyncer customerSyncer;
+
private final RegistrarPaymentSetupAction action = new RegistrarPaymentSetupAction();
@Before
public void before() throws Exception {
action.braintreeGateway = braintreeGateway;
+ action.customerSyncer = customerSyncer;
action.registrar =
Registrar.loadByClientId("TheRegistrar").asBuilder()
.setBillingMethod(Registrar.BillingMethod.BRAINTREE)
@@ -79,6 +86,7 @@ public class RegistrarPaymentSetupActionTest {
"token", blanketsOfSadness,
"currencies", asList("USD", "JPY"),
"brainframe", "/doodle")));
+ verify(customerSyncer).sync(eq(action.registrar));
}
@Test