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