diff --git a/java/google/registry/model/registrar/Registrar.java b/java/google/registry/model/registrar/Registrar.java index 15d9af040..c38d27950 100644 --- a/java/google/registry/model/registrar/Registrar.java +++ b/java/google/registry/model/registrar/Registrar.java @@ -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. + * + *

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 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 { + @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 getBillingAccountMap() { + if (billingAccountMap == null) { + return ImmutableMap.of(); + } + ImmutableMap.Builder billingAccountMapBuilder = + new ImmutableMap.Builder<>(); + for (Map.Entry 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 billingAccountMap) { + if (billingAccountMap == null) { + getInstance().billingAccountMap = null; + } else { + ImmutableMap.Builder billingAccountMapBuilder = + new ImmutableMap.Builder<>(); + for (Map.Entry 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; diff --git a/java/google/registry/tools/CreateOrUpdateRegistrarCommand.java b/java/google/registry/tools/CreateOrUpdateRegistrarCommand.java index bfac4097e..db10aab32 100644 --- a/java/google/registry/tools/CreateOrUpdateRegistrarCommand.java +++ b/java/google/registry/tools/CreateOrUpdateRegistrarCommand.java @@ -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 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 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 balances = RegistrarBillingUtils.loadBalance(oldRegistrar); diff --git a/java/google/registry/tools/params/KeyValueMapParameter.java b/java/google/registry/tools/params/KeyValueMapParameter.java index 792e84ce9..bf489353b 100644 --- a/java/google/registry/tools/params/KeyValueMapParameter.java +++ b/java/google/registry/tools/params/KeyValueMapParameter.java @@ -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 return Integer.parseInt(value); } } + + /** Combined converter and validator class for currency unit-to-string Map argument strings. */ + public static class CurrencyUnitToStringMap extends KeyValueMapParameter { + @Override + protected CurrencyUnit parseKey(String rawKey) { + return CurrencyUnit.of(rawKey); + } + + @Override + protected String parseValue(String value) { + return value; + } + } } diff --git a/javatests/google/registry/model/registrar/RegistrarTest.java b/javatests/google/registry/model/registrar/RegistrarTest.java index 393ca1f0d..a433274af 100644 --- a/javatests/google/registry/model/registrar/RegistrarTest.java +++ b/javatests/google/registry/model/registrar/RegistrarTest.java @@ -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(); diff --git a/javatests/google/registry/model/schema.txt b/javatests/google/registry/model/schema.txt index 67bd73e86..72a403387 100644 --- a/javatests/google/registry/model/schema.txt +++ b/javatests/google/registry/model/schema.txt @@ -617,9 +617,14 @@ class google.registry.model.registrar.Registrar { java.lang.String url; java.lang.String whoisServer; java.util.List ipAddressWhitelist; + java.util.Map billingAccountMap; java.util.Set 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; diff --git a/javatests/google/registry/tools/CreateRegistrarCommandTest.java b/javatests/google/registry/tools/CreateRegistrarCommandTest.java index 6482a595b..5aba5d291 100644 --- a/javatests/google/registry/tools/CreateRegistrarCommandTest.java +++ b/javatests/google/registry/tools/CreateRegistrarCommandTest.java @@ -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