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.base.Supplier;
import com.google.common.collect.FluentIterable; import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet; import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
@ -48,12 +49,15 @@ import com.google.re2j.Pattern;
import com.googlecode.objectify.Key; import com.googlecode.objectify.Key;
import com.googlecode.objectify.Work; import com.googlecode.objectify.Work;
import com.googlecode.objectify.annotation.Cache; import com.googlecode.objectify.annotation.Cache;
import com.googlecode.objectify.annotation.Embed;
import com.googlecode.objectify.annotation.Entity; import com.googlecode.objectify.annotation.Entity;
import com.googlecode.objectify.annotation.Id; import com.googlecode.objectify.annotation.Id;
import com.googlecode.objectify.annotation.IgnoreSave; import com.googlecode.objectify.annotation.IgnoreSave;
import com.googlecode.objectify.annotation.Index; import com.googlecode.objectify.annotation.Index;
import com.googlecode.objectify.annotation.Mapify;
import com.googlecode.objectify.annotation.Parent; import com.googlecode.objectify.annotation.Parent;
import com.googlecode.objectify.condition.IfNull; import com.googlecode.objectify.condition.IfNull;
import com.googlecode.objectify.mapper.Mapper;
import google.registry.model.Buildable; import google.registry.model.Buildable;
import google.registry.model.CreateAutoTimestamp; import google.registry.model.CreateAutoTimestamp;
import google.registry.model.ImmutableObject; import google.registry.model.ImmutableObject;
@ -62,6 +66,7 @@ import google.registry.model.Jsonifiable;
import google.registry.model.UpdateAutoTimestamp; import google.registry.model.UpdateAutoTimestamp;
import google.registry.model.annotations.ReportedOn; import google.registry.model.annotations.ReportedOn;
import google.registry.model.common.EntityGroupRoot; import google.registry.model.common.EntityGroupRoot;
import google.registry.model.registrar.Registrar.BillingAccountEntry.CurrencyMapper;
import google.registry.util.CidrAddressBlock; import google.registry.util.CidrAddressBlock;
import google.registry.util.NonFinalForTesting; import google.registry.util.NonFinalForTesting;
import java.security.MessageDigest; import java.security.MessageDigest;
@ -74,6 +79,7 @@ import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import org.joda.money.CurrencyUnit;
import org.joda.time.DateTime; import org.joda.time.DateTime;
/** Information about a registrar. */ /** 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). */ /** Identifier of registrar used in external billing system (e.g. Oracle). */
Long billingIdentifier; 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. */ /** URL of registrar's website. */
String url; String url;
@ -370,6 +409,18 @@ public class Registrar extends ImmutableObject implements Buildable, Jsonifiable
return billingIdentifier; 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() { public DateTime getLastUpdateTime() {
return lastUpdateTime.getTimestamp(); return lastUpdateTime.getTimestamp();
} }
@ -588,6 +639,21 @@ public class Registrar extends ImmutableObject implements Buildable, Jsonifiable
return this; 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) { public Builder setRegistrarName(String registrarName) {
getInstance().registrarName = registrarName; getInstance().registrarName = registrarName;
return this; 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;
import google.registry.model.registrar.Registrar.BillingMethod; import google.registry.model.registrar.Registrar.BillingMethod;
import google.registry.model.registrar.RegistrarAddress; import google.registry.model.registrar.RegistrarAddress;
import google.registry.tools.params.KeyValueMapParameter.CurrencyUnitToStringMap;
import google.registry.tools.params.OptionalLongParameter; import google.registry.tools.params.OptionalLongParameter;
import google.registry.tools.params.OptionalPhoneNumberParameter; import google.registry.tools.params.OptionalPhoneNumberParameter;
import google.registry.tools.params.OptionalStringParameter; import google.registry.tools.params.OptionalStringParameter;
@ -169,6 +170,18 @@ abstract class CreateOrUpdateRegistrarCommand extends MutatingCommand {
validateWith = OptionalLongParameter.class) validateWith = OptionalLongParameter.class)
private Optional<Long> billingId; 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 @Nullable
@Parameter( @Parameter(
names = "--billing_method", names = "--billing_method",
@ -334,6 +347,9 @@ abstract class CreateOrUpdateRegistrarCommand extends MutatingCommand {
if (billingId != null) { if (billingId != null) {
builder.setBillingIdentifier(billingId.orNull()); builder.setBillingIdentifier(billingId.orNull());
} }
if (billingAccountMap != null) {
builder.setBillingAccountMap(billingAccountMap);
}
if (billingMethod != null) { if (billingMethod != null) {
if (oldRegistrar != null && !billingMethod.equals(oldRegistrar.getBillingMethod())) { if (oldRegistrar != null && !billingMethod.equals(oldRegistrar.getBillingMethod())) {
Map<CurrencyUnit, Money> balances = RegistrarBillingUtils.loadBalance(oldRegistrar); 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.base.Strings;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import java.util.Map; import java.util.Map;
import org.joda.money.CurrencyUnit;
/** /**
* Combined converter and validator class for key-value map JCommander argument strings. * 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); 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 static google.registry.testing.DatastoreHelper.persistSimpleResources;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import google.registry.model.EntityTestCase; import google.registry.model.EntityTestCase;
import google.registry.model.common.EntityGroupRoot; 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.model.registrar.Registrar.Type;
import google.registry.testing.ExceptionRule; import google.registry.testing.ExceptionRule;
import google.registry.util.CidrAddressBlock; import google.registry.util.CidrAddressBlock;
import org.joda.money.CurrencyUnit;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
@ -50,7 +52,8 @@ public class RegistrarTest extends EntityTestCase {
public void setUp() throws Exception { public void setUp() throws Exception {
createTld("xn--q9jyb4c"); createTld("xn--q9jyb4c");
// Set up a new persisted registrar entity. // Set up a new persisted registrar entity.
registrar = cloneAndSetAutoTimestamps( registrar =
cloneAndSetAutoTimestamps(
new Registrar.Builder() new Registrar.Builder()
.setClientId("registrar") .setClientId("registrar")
.setRegistrarName("full registrar name") .setRegistrarName("full registrar name")
@ -60,18 +63,21 @@ public class RegistrarTest extends EntityTestCase {
.setWhoisServer("whois.example.com") .setWhoisServer("whois.example.com")
.setBlockPremiumNames(true) .setBlockPremiumNames(true)
.setClientCertificate(SAMPLE_CERT, clock.nowUtc()) .setClientCertificate(SAMPLE_CERT, clock.nowUtc())
.setIpAddressWhitelist(ImmutableList.of( .setIpAddressWhitelist(
ImmutableList.of(
CidrAddressBlock.create("192.168.1.1/31"), CidrAddressBlock.create("192.168.1.1/31"),
CidrAddressBlock.create("10.0.0.1/8"))) CidrAddressBlock.create("10.0.0.1/8")))
.setPassword("foobar") .setPassword("foobar")
.setInternationalizedAddress(new RegistrarAddress.Builder() .setInternationalizedAddress(
new RegistrarAddress.Builder()
.setStreet(ImmutableList.of("123 Example Boulevard")) .setStreet(ImmutableList.of("123 Example Boulevard"))
.setCity("Williamsburg") .setCity("Williamsburg")
.setState("NY") .setState("NY")
.setZip("11211") .setZip("11211")
.setCountryCode("US") .setCountryCode("US")
.build()) .build())
.setLocalizedAddress(new RegistrarAddress.Builder() .setLocalizedAddress(
new RegistrarAddress.Builder()
.setStreet(ImmutableList.of("123 Example Boulevard.")) .setStreet(ImmutableList.of("123 Example Boulevard."))
.setCity("Williamsburg") .setCity("Williamsburg")
.setState("NY") .setState("NY")
@ -87,6 +93,8 @@ public class RegistrarTest extends EntityTestCase {
.setDriveFolderId("drive folder id") .setDriveFolderId("drive folder id")
.setIanaIdentifier(8L) .setIanaIdentifier(8L)
.setBillingIdentifier(5325L) .setBillingIdentifier(5325L)
.setBillingAccountMap(
ImmutableMap.of(CurrencyUnit.USD, "abc123", CurrencyUnit.JPY, "789xyz"))
.setPhonePasscode("01234") .setPhonePasscode("01234")
.build()); .build());
persistResource(registrar); persistResource(registrar);
@ -225,6 +233,14 @@ public class RegistrarTest extends EntityTestCase {
.build(); .build();
} }
@Test
public void testSuccess_clearingBillingAccountMap() throws Exception {
registrar = registrar.asBuilder()
.setBillingAccountMap(null)
.build();
assertThat(registrar.getBillingAccountMap()).isEmpty();
}
@Test @Test
public void testSuccess_ianaIdForInternal() throws Exception { public void testSuccess_ianaIdForInternal() throws Exception {
registrar.asBuilder().setType(Type.INTERNAL).setIanaIdentifier(9998L).build(); 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 url;
java.lang.String whoisServer; java.lang.String whoisServer;
java.util.List<google.registry.util.CidrAddressBlock> ipAddressWhitelist; 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; java.util.Set<java.lang.String> allowedTlds;
org.joda.time.DateTime lastCertificateUpdateTime; 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 { enum google.registry.model.registrar.Registrar$BillingMethod {
BRAINTREE; BRAINTREE;
EXTERNAL; EXTERNAL;

View file

@ -34,6 +34,7 @@ import google.registry.model.registrar.Registrar;
import google.registry.testing.CertificateSamples; import google.registry.testing.CertificateSamples;
import google.registry.tools.ServerSideCommand.Connection; import google.registry.tools.ServerSideCommand.Connection;
import java.io.IOException; import java.io.IOException;
import org.joda.money.CurrencyUnit;
import org.joda.time.DateTime; import org.joda.time.DateTime;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
@ -345,6 +346,25 @@ public class CreateRegistrarCommandTest extends CommandTestCase<CreateRegistrarC
assertThat(registrar.getBillingIdentifier()).isEqualTo(12345); 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 @Test
public void testSuccess_streetAddress() throws Exception { public void testSuccess_streetAddress() throws Exception {
runCommand( 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.State;
import google.registry.model.registrar.Registrar.Type; import google.registry.model.registrar.Registrar.Type;
import google.registry.util.CidrAddressBlock; import google.registry.util.CidrAddressBlock;
import org.joda.money.CurrencyUnit;
import org.joda.money.Money; import org.joda.money.Money;
import org.joda.time.DateTime; import org.joda.time.DateTime;
import org.junit.Test; import org.junit.Test;
@ -203,6 +204,14 @@ public class UpdateRegistrarCommandTest extends CommandTestCase<UpdateRegistrarC
assertThat(loadByClientId("NewRegistrar").getBillingIdentifier()).isEqualTo(12345); 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 @Test
public void testSuccess_changeBillingMethodToBraintreeWhenBalanceIsZero() throws Exception { public void testSuccess_changeBillingMethodToBraintreeWhenBalanceIsZero() throws Exception {
createTlds("xn--q9jyb4c"); createTlds("xn--q9jyb4c");

View file

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