Add billing account map to Registrar entity

A CurrencyUnit-to-BillingAccountEntry map is persisted in the Registrar entity. It provides flexibility for billing systems that assign different account ids for accounts under different currencies of the same registrar.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=151022753
This commit is contained in:
jianglai 2017-03-23 10:42:05 -07:00 committed by Ben McIlwain
parent ab9b7c613d
commit d7e2009ddf
8 changed files with 225 additions and 39 deletions

View file

@ -41,6 +41,7 @@ import com.google.common.base.Predicates;
import com.google.common.base.Supplier;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Sets;
@ -48,12 +49,15 @@ import com.google.re2j.Pattern;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.Work;
import com.googlecode.objectify.annotation.Cache;
import com.googlecode.objectify.annotation.Embed;
import com.googlecode.objectify.annotation.Entity;
import com.googlecode.objectify.annotation.Id;
import com.googlecode.objectify.annotation.IgnoreSave;
import com.googlecode.objectify.annotation.Index;
import com.googlecode.objectify.annotation.Mapify;
import com.googlecode.objectify.annotation.Parent;
import com.googlecode.objectify.condition.IfNull;
import com.googlecode.objectify.mapper.Mapper;
import google.registry.model.Buildable;
import google.registry.model.CreateAutoTimestamp;
import google.registry.model.ImmutableObject;
@ -62,6 +66,7 @@ import google.registry.model.Jsonifiable;
import google.registry.model.UpdateAutoTimestamp;
import google.registry.model.annotations.ReportedOn;
import google.registry.model.common.EntityGroupRoot;
import google.registry.model.registrar.Registrar.BillingAccountEntry.CurrencyMapper;
import google.registry.util.CidrAddressBlock;
import google.registry.util.NonFinalForTesting;
import java.security.MessageDigest;
@ -74,6 +79,7 @@ import java.util.Map;
import java.util.Objects;
import java.util.Set;
import javax.annotation.Nullable;
import org.joda.money.CurrencyUnit;
import org.joda.time.DateTime;
/** Information about a registrar. */
@ -293,6 +299,39 @@ public class Registrar extends ImmutableObject implements Buildable, Jsonifiable
/** Identifier of registrar used in external billing system (e.g. Oracle). */
Long billingIdentifier;
/**
* Map of currency-to-billing account for the registrar.
*
* <p>A registrar can have different billing accounts that are denoted in different currencies.
* This provides flexibility for billing systems that require such distinction.
*/
@Nullable
@Mapify(CurrencyMapper.class)
Map<CurrencyUnit, BillingAccountEntry> billingAccountMap;
/** A billing account entry for this registrar, consisting of a currency and an account Id. */
@Embed
static class BillingAccountEntry extends ImmutableObject {
CurrencyUnit currency;
String accountId;
BillingAccountEntry() {};
BillingAccountEntry(CurrencyUnit currency, String accountId) {
this.accountId = accountId;
this.currency = currency;
}
/** Mapper to use for {@code @Mapify}. */
static class CurrencyMapper implements Mapper<CurrencyUnit, BillingAccountEntry> {
@Override
public CurrencyUnit getKey(BillingAccountEntry billingAccountEntry) {
return billingAccountEntry.currency;
}
}
}
/** URL of registrar's website. */
String url;
@ -370,6 +409,18 @@ public class Registrar extends ImmutableObject implements Buildable, Jsonifiable
return billingIdentifier;
}
public ImmutableMap<CurrencyUnit, String> getBillingAccountMap() {
if (billingAccountMap == null) {
return ImmutableMap.of();
}
ImmutableMap.Builder<CurrencyUnit, String> billingAccountMapBuilder =
new ImmutableMap.Builder<>();
for (Map.Entry<CurrencyUnit, BillingAccountEntry> entry : billingAccountMap.entrySet()) {
billingAccountMapBuilder.put(entry.getKey(), entry.getValue().accountId);
}
return billingAccountMapBuilder.build();
}
public DateTime getLastUpdateTime() {
return lastUpdateTime.getTimestamp();
}
@ -588,6 +639,21 @@ public class Registrar extends ImmutableObject implements Buildable, Jsonifiable
return this;
}
public Builder setBillingAccountMap(Map<CurrencyUnit, String> billingAccountMap) {
if (billingAccountMap == null) {
getInstance().billingAccountMap = null;
} else {
ImmutableMap.Builder<CurrencyUnit, BillingAccountEntry> billingAccountMapBuilder =
new ImmutableMap.Builder<>();
for (Map.Entry<CurrencyUnit, String> entry : billingAccountMap.entrySet()) {
CurrencyUnit key = entry.getKey();
billingAccountMapBuilder.put(key, new BillingAccountEntry(key, entry.getValue()));
}
getInstance().billingAccountMap = billingAccountMapBuilder.build();
}
return this;
}
public Builder setRegistrarName(String registrarName) {
getInstance().registrarName = registrarName;
return this;

View file

@ -32,6 +32,7 @@ import google.registry.model.billing.RegistrarBillingUtils;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.Registrar.BillingMethod;
import google.registry.model.registrar.RegistrarAddress;
import google.registry.tools.params.KeyValueMapParameter.CurrencyUnitToStringMap;
import google.registry.tools.params.OptionalLongParameter;
import google.registry.tools.params.OptionalPhoneNumberParameter;
import google.registry.tools.params.OptionalStringParameter;
@ -169,6 +170,18 @@ abstract class CreateOrUpdateRegistrarCommand extends MutatingCommand {
validateWith = OptionalLongParameter.class)
private Optional<Long> billingId;
@Nullable
@Parameter(
names = "--billing_account_map",
description =
"Registrar Billing Account key-value pairs (formatted as key=value[,key=value...]), "
+ "where key is a currency unit (USD, JPY, etc) and value is the registrar's billing "
+ "account id for that currency.",
converter = CurrencyUnitToStringMap.class,
validateWith = CurrencyUnitToStringMap.class
)
private Map<CurrencyUnit, String> billingAccountMap;
@Nullable
@Parameter(
names = "--billing_method",
@ -334,6 +347,9 @@ abstract class CreateOrUpdateRegistrarCommand extends MutatingCommand {
if (billingId != null) {
builder.setBillingIdentifier(billingId.orNull());
}
if (billingAccountMap != null) {
builder.setBillingAccountMap(billingAccountMap);
}
if (billingMethod != null) {
if (oldRegistrar != null && !billingMethod.equals(oldRegistrar.getBillingMethod())) {
Map<CurrencyUnit, Money> balances = RegistrarBillingUtils.loadBalance(oldRegistrar);

View file

@ -18,6 +18,7 @@ import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import java.util.Map;
import org.joda.money.CurrencyUnit;
/**
* Combined converter and validator class for key-value map JCommander argument strings.
@ -93,4 +94,17 @@ public abstract class KeyValueMapParameter<K, V>
return Integer.parseInt(value);
}
}
/** Combined converter and validator class for currency unit-to-string Map argument strings. */
public static class CurrencyUnitToStringMap extends KeyValueMapParameter<CurrencyUnit, String> {
@Override
protected CurrencyUnit parseKey(String rawKey) {
return CurrencyUnit.of(rawKey);
}
@Override
protected String parseValue(String value) {
return value;
}
}
}

View file

@ -27,6 +27,7 @@ import static google.registry.testing.DatastoreHelper.persistSimpleResource;
import static google.registry.testing.DatastoreHelper.persistSimpleResources;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import google.registry.model.EntityTestCase;
import google.registry.model.common.EntityGroupRoot;
@ -34,6 +35,7 @@ import google.registry.model.registrar.Registrar.State;
import google.registry.model.registrar.Registrar.Type;
import google.registry.testing.ExceptionRule;
import google.registry.util.CidrAddressBlock;
import org.joda.money.CurrencyUnit;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@ -50,45 +52,51 @@ public class RegistrarTest extends EntityTestCase {
public void setUp() throws Exception {
createTld("xn--q9jyb4c");
// Set up a new persisted registrar entity.
registrar = cloneAndSetAutoTimestamps(
new Registrar.Builder()
.setClientId("registrar")
.setRegistrarName("full registrar name")
.setType(Type.REAL)
.setState(State.PENDING)
.setAllowedTlds(ImmutableSet.of("xn--q9jyb4c"))
.setWhoisServer("whois.example.com")
.setBlockPremiumNames(true)
.setClientCertificate(SAMPLE_CERT, clock.nowUtc())
.setIpAddressWhitelist(ImmutableList.of(
CidrAddressBlock.create("192.168.1.1/31"),
CidrAddressBlock.create("10.0.0.1/8")))
.setPassword("foobar")
.setInternationalizedAddress(new RegistrarAddress.Builder()
.setStreet(ImmutableList.of("123 Example Boulevard"))
.setCity("Williamsburg")
.setState("NY")
.setZip("11211")
.setCountryCode("US")
.build())
.setLocalizedAddress(new RegistrarAddress.Builder()
.setStreet(ImmutableList.of("123 Example Boulevard."))
.setCity("Williamsburg")
.setState("NY")
.setZip("11211")
.setCountryCode("US")
.build())
.setPhoneNumber("+1.2125551212")
.setFaxNumber("+1.2125551213")
.setEmailAddress("contact-us@example.com")
.setUrl("http://www.example.com")
.setReferralUrl("http://www.example.com")
.setIcannReferralEmail("foo@example.com")
.setDriveFolderId("drive folder id")
.setIanaIdentifier(8L)
.setBillingIdentifier(5325L)
.setPhonePasscode("01234")
.build());
registrar =
cloneAndSetAutoTimestamps(
new Registrar.Builder()
.setClientId("registrar")
.setRegistrarName("full registrar name")
.setType(Type.REAL)
.setState(State.PENDING)
.setAllowedTlds(ImmutableSet.of("xn--q9jyb4c"))
.setWhoisServer("whois.example.com")
.setBlockPremiumNames(true)
.setClientCertificate(SAMPLE_CERT, clock.nowUtc())
.setIpAddressWhitelist(
ImmutableList.of(
CidrAddressBlock.create("192.168.1.1/31"),
CidrAddressBlock.create("10.0.0.1/8")))
.setPassword("foobar")
.setInternationalizedAddress(
new RegistrarAddress.Builder()
.setStreet(ImmutableList.of("123 Example Boulevard"))
.setCity("Williamsburg")
.setState("NY")
.setZip("11211")
.setCountryCode("US")
.build())
.setLocalizedAddress(
new RegistrarAddress.Builder()
.setStreet(ImmutableList.of("123 Example Boulevard."))
.setCity("Williamsburg")
.setState("NY")
.setZip("11211")
.setCountryCode("US")
.build())
.setPhoneNumber("+1.2125551212")
.setFaxNumber("+1.2125551213")
.setEmailAddress("contact-us@example.com")
.setUrl("http://www.example.com")
.setReferralUrl("http://www.example.com")
.setIcannReferralEmail("foo@example.com")
.setDriveFolderId("drive folder id")
.setIanaIdentifier(8L)
.setBillingIdentifier(5325L)
.setBillingAccountMap(
ImmutableMap.of(CurrencyUnit.USD, "abc123", CurrencyUnit.JPY, "789xyz"))
.setPhonePasscode("01234")
.build());
persistResource(registrar);
persistSimpleResources(ImmutableList.of(
new RegistrarContact.Builder()
@ -225,6 +233,14 @@ public class RegistrarTest extends EntityTestCase {
.build();
}
@Test
public void testSuccess_clearingBillingAccountMap() throws Exception {
registrar = registrar.asBuilder()
.setBillingAccountMap(null)
.build();
assertThat(registrar.getBillingAccountMap()).isEmpty();
}
@Test
public void testSuccess_ianaIdForInternal() throws Exception {
registrar.asBuilder().setType(Type.INTERNAL).setIanaIdentifier(9998L).build();

View file

@ -617,9 +617,14 @@ class google.registry.model.registrar.Registrar {
java.lang.String url;
java.lang.String whoisServer;
java.util.List<google.registry.util.CidrAddressBlock> ipAddressWhitelist;
java.util.Map<org.joda.money.CurrencyUnit, google.registry.model.registrar.Registrar$BillingAccountEntry> billingAccountMap;
java.util.Set<java.lang.String> allowedTlds;
org.joda.time.DateTime lastCertificateUpdateTime;
}
class google.registry.model.registrar.Registrar$BillingAccountEntry {
java.lang.String accountId;
org.joda.money.CurrencyUnit currency;
}
enum google.registry.model.registrar.Registrar$BillingMethod {
BRAINTREE;
EXTERNAL;

View file

@ -34,6 +34,7 @@ import google.registry.model.registrar.Registrar;
import google.registry.testing.CertificateSamples;
import google.registry.tools.ServerSideCommand.Connection;
import java.io.IOException;
import org.joda.money.CurrencyUnit;
import org.joda.time.DateTime;
import org.junit.Before;
import org.junit.Test;
@ -345,6 +346,25 @@ public class CreateRegistrarCommandTest extends CommandTestCase<CreateRegistrarC
assertThat(registrar.getBillingIdentifier()).isEqualTo(12345);
}
@Test
public void testSuccess_billingAccountMap() throws Exception {
runCommand(
"--name=blobio",
"--password=some_password",
"--registrar_type=REAL",
"--iana_id=8",
"--billing_account_map=USD=abc123,JPY=789xyz",
"--passcode=01234",
"--icann_referral_email=foo@bar.test",
"--force",
"clientz");
Registrar registrar = Registrar.loadByClientId("clientz");
assertThat(registrar).isNotNull();
assertThat(registrar.getBillingAccountMap())
.containsExactly(CurrencyUnit.USD, "abc123", CurrencyUnit.JPY, "789xyz");
}
@Test
public void testSuccess_streetAddress() throws Exception {
runCommand(

View file

@ -33,6 +33,7 @@ import google.registry.model.registrar.Registrar.BillingMethod;
import google.registry.model.registrar.Registrar.State;
import google.registry.model.registrar.Registrar.Type;
import google.registry.util.CidrAddressBlock;
import org.joda.money.CurrencyUnit;
import org.joda.money.Money;
import org.joda.time.DateTime;
import org.junit.Test;
@ -203,6 +204,14 @@ public class UpdateRegistrarCommandTest extends CommandTestCase<UpdateRegistrarC
assertThat(loadByClientId("NewRegistrar").getBillingIdentifier()).isEqualTo(12345);
}
@Test
public void testSuccess_billingAccountMap() throws Exception {
assertThat(loadByClientId("NewRegistrar").getBillingAccountMap()).isEmpty();
runCommand("--billing_account_map=USD=abc123,JPY=789xyz", "--force", "NewRegistrar");
assertThat(loadByClientId("NewRegistrar").getBillingAccountMap())
.containsExactly(CurrencyUnit.USD, "abc123", CurrencyUnit.JPY, "789xyz");
}
@Test
public void testSuccess_changeBillingMethodToBraintreeWhenBalanceIsZero() throws Exception {
createTlds("xn--q9jyb4c");

View file

@ -18,8 +18,11 @@ import static com.google.common.truth.Truth.assertThat;
import com.google.common.collect.ImmutableMap;
import google.registry.testing.ExceptionRule;
import google.registry.tools.params.KeyValueMapParameter.CurrencyUnitToStringMap;
import google.registry.tools.params.KeyValueMapParameter.StringToIntegerMap;
import google.registry.tools.params.KeyValueMapParameter.StringToStringMap;
import org.joda.money.CurrencyUnit;
import org.joda.money.IllegalCurrencyException;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -34,6 +37,7 @@ public class KeyValueMapParameterTest {
private final StringToStringMap stringToStringInstance = new StringToStringMap();
private final StringToIntegerMap stringToIntegerInstance = new StringToIntegerMap();
private final CurrencyUnitToStringMap currencyUnitToStringMap = new CurrencyUnitToStringMap();
@Test
public void testSuccess_convertStringToString_singleEntry() throws Exception {
@ -47,6 +51,12 @@ public class KeyValueMapParameterTest {
.isEqualTo(ImmutableMap.of("key", 1));
}
@Test
public void testSuccess_convertCurrencyUnitToString_singleEntry() throws Exception {
assertThat(currencyUnitToStringMap.convert("USD=123abc"))
.isEqualTo(ImmutableMap.of(CurrencyUnit.USD, "123abc"));
}
@Test
public void testSuccess_convertStringToString() throws Exception {
assertThat(stringToStringInstance.convert("key=foo,key2=bar"))
@ -59,6 +69,12 @@ public class KeyValueMapParameterTest {
.isEqualTo(ImmutableMap.of("key", 1, "key2", 2));
}
@Test
public void testSuccess_convertCurrencyUnitToString() throws Exception {
assertThat(currencyUnitToStringMap.convert("USD=123abc,JPY=xyz789"))
.isEqualTo(ImmutableMap.of(CurrencyUnit.USD, "123abc", CurrencyUnit.JPY, "xyz789"));
}
@Test
public void testSuccess_convertStringToString_empty() throws Exception {
assertThat(stringToStringInstance.convert("")).isEmpty();
@ -69,12 +85,23 @@ public class KeyValueMapParameterTest {
assertThat(stringToIntegerInstance.convert("")).isEmpty();
}
@Test
public void testSuccess_convertCurrencyUnitToString_empty() throws Exception {
assertThat(currencyUnitToStringMap.convert("")).isEmpty();
}
@Test
public void testFailure_convertStringToInteger_badType() throws Exception {
thrown.expect(NumberFormatException.class);
stringToIntegerInstance.convert("key=1,key2=foo");
}
@Test
public void testFailure_convertCurrencyUnitToString_badType() throws Exception {
thrown.expect(IllegalCurrencyException.class, "XYZ");
currencyUnitToStringMap.convert("USD=123abc,XYZ=xyz789");
}
@Test
public void testFailure_convertStringToString_badSeparator() throws Exception {
thrown.expect(IllegalArgumentException.class);
@ -87,6 +114,12 @@ public class KeyValueMapParameterTest {
stringToIntegerInstance.convert("key=1&key2=2");
}
@Test
public void testFailure_convertCurrencyUnitToString_badSeparator() throws Exception {
thrown.expect(IllegalArgumentException.class);
currencyUnitToStringMap.convert("USD=123abc&JPY=xyz789");
}
@Test
public void testFailure_convertStringToString_badFormat() throws Exception {
thrown.expect(IllegalArgumentException.class);
@ -98,4 +131,11 @@ public class KeyValueMapParameterTest {
thrown.expect(IllegalArgumentException.class);
stringToIntegerInstance.convert("foo");
}
@Test
public void testFailure_convertCurrencyUnitToString_badFormat() throws Exception {
thrown.expect(IllegalArgumentException.class);
currencyUnitToStringMap.convert("foo");
}
}