Add the PackagePromotion table (#1745)

* Add the PackagePromotion table

* Add long id

* Add NOT NULL

* fix formatting

* make package price non null

* Add not nulls to java file

* Fix broken tests from merge conflicts
This commit is contained in:
sarahcaseybot 2022-08-24 14:16:34 -04:00 committed by GitHub
parent 3c0805def5
commit a6087bf328
8 changed files with 298 additions and 1 deletions

View file

@ -103,7 +103,10 @@ public class AllocationToken extends BackupGroupRoot implements Buildable {
ANCHOR_TENANT ANCHOR_TENANT
} }
/** Single-use tokens are invalid after use. Infinite-use tokens, predictably, are not. */ /**
* Single-use tokens are invalid after use. Infinite-use tokens, predictably, are not. Package
* tokens are used in package promotions.
*/
public enum TokenType { public enum TokenType {
PACKAGE, PACKAGE,
SINGLE_USE, SINGLE_USE,
@ -286,6 +289,10 @@ public class AllocationToken extends BackupGroupRoot implements Buildable {
getInstance().redemptionHistoryEntry == null getInstance().redemptionHistoryEntry == null
|| TokenType.SINGLE_USE.equals(getInstance().tokenType), || TokenType.SINGLE_USE.equals(getInstance().tokenType),
"Redemption history entry can only be specified for SINGLE_USE tokens"); "Redemption history entry can only be specified for SINGLE_USE tokens");
checkArgument(
getInstance().tokenType != TokenType.PACKAGE
|| getInstance().allowedClientIds.size() == 1,
"PACKAGE tokens must have exactly one allowed client registrar");
checkArgument( checkArgument(
getInstance().discountFraction > 0 || !getInstance().discountPremiums, getInstance().discountFraction > 0 || !getInstance().discountPremiums,
"Discount premiums can only be specified along with a discount fraction"); "Discount premiums can only be specified along with a discount fraction");

View file

@ -0,0 +1,157 @@
// Copyright 2022 The Nomulus 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 google.registry.model.domain.token;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.DateTimeUtils.END_OF_TIME;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import google.registry.model.Buildable;
import google.registry.model.ImmutableObject;
import google.registry.model.domain.token.AllocationToken.TokenType;
import google.registry.persistence.VKey;
import google.registry.persistence.converter.JodaMoneyType;
import java.util.Optional;
import javax.annotation.Nullable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import org.hibernate.annotations.Columns;
import org.hibernate.annotations.Type;
import org.joda.money.Money;
import org.joda.time.DateTime;
/** An entity representing a package promotion. */
@Entity
@javax.persistence.Table(indexes = {@javax.persistence.Index(columnList = "token")})
public class PackagePromotion extends ImmutableObject implements Buildable {
/** An autogenerated identifier for the package promotion. */
@Id long packagePromotionId;
/** The allocation token string for the package. */
@Column(nullable = false)
VKey<AllocationToken> token;
/** The maximum number of active domains the package allows at any given time. */
@Column(nullable = false)
int maxDomains;
/** The maximum number of domains that can be created in the package each year. */
@Column(nullable = false)
int maxCreates;
/** The annual price of the package. */
@Type(type = JodaMoneyType.TYPE_NAME)
@Columns(
columns = {
@Column(name = "package_price_amount", nullable = false),
@Column(name = "package_price_currency", nullable = false)
})
Money packagePrice;
/** The next billing date of the package. */
@Column(nullable = false)
DateTime nextBillingDate = END_OF_TIME;
/** Date the last warning email was sent that the package has exceeded the maxDomains limit. */
@Nullable DateTime lastNotificationSent;
public VKey<AllocationToken> getToken() {
return token;
}
public int getMaxDomains() {
return maxDomains;
}
public int getMaxCreates() {
return maxCreates;
}
public Money getPackagePrice() {
return packagePrice;
}
public DateTime getNextBillingDate() {
return nextBillingDate;
}
public Optional<DateTime> getLastNotificationSent() {
return Optional.ofNullable(lastNotificationSent);
}
@Override
public Builder asBuilder() {
return new Builder(clone(this));
}
/** A builder for constructing {@link PackagePromotion} objects, since they are immutable. */
public static class Builder extends Buildable.Builder<PackagePromotion> {
public Builder() {}
private Builder(PackagePromotion instance) {
super(instance);
}
@Override
public PackagePromotion build() {
checkArgumentNotNull(getInstance().token, "Allocation token must be specified");
AllocationToken allocationToken = tm().transact(() -> tm().loadByKey(getInstance().token));
checkArgument(
allocationToken.tokenType == TokenType.PACKAGE,
"Allocation token must be a PACKAGE type");
return super.build();
}
public Builder setToken(AllocationToken token) {
checkArgumentNotNull(token, "Allocation token must not be null");
checkArgument(
token.tokenType == TokenType.PACKAGE, "Allocation token must be a PACKAGE type");
getInstance().token = token.createVKey();
return this;
}
public Builder setMaxDomains(int maxDomains) {
checkArgumentNotNull(maxDomains, "maxDomains must not be null");
getInstance().maxDomains = maxDomains;
return this;
}
public Builder setMaxCreates(int maxCreates) {
checkArgumentNotNull(maxCreates, "maxCreates must not be null");
getInstance().maxCreates = maxCreates;
return this;
}
public Builder setPackagePrice(Money packagePrice) {
checkArgumentNotNull(packagePrice, "Package price must not be null");
getInstance().packagePrice = packagePrice;
return this;
}
public Builder setNextBillingDate(@Nullable DateTime nextBillingDate) {
checkArgumentNotNull(nextBillingDate, "Next billing date must not be null");
getInstance().nextBillingDate = nextBillingDate;
return this;
}
public Builder setLastNotificationSent(@Nullable DateTime lastNotificationSent) {
getInstance().lastNotificationSent = lastNotificationSent;
return this;
}
}
}

View file

@ -52,6 +52,7 @@
<class>google.registry.model.domain.secdns.DelegationSignerData</class> <class>google.registry.model.domain.secdns.DelegationSignerData</class>
<class>google.registry.model.domain.secdns.DomainDsDataHistory</class> <class>google.registry.model.domain.secdns.DomainDsDataHistory</class>
<class>google.registry.model.domain.token.AllocationToken</class> <class>google.registry.model.domain.token.AllocationToken</class>
<class>google.registry.model.domain.token.PackagePromotion</class>
<class>google.registry.model.host.HostHistory</class> <class>google.registry.model.host.HostHistory</class>
<class>google.registry.model.host.Host</class> <class>google.registry.model.host.Host</class>
<class>google.registry.model.poll.PollMessage</class> <class>google.registry.model.poll.PollMessage</class>

View file

@ -3136,6 +3136,7 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
new AllocationToken.Builder() new AllocationToken.Builder()
.setToken("abc123") .setToken("abc123")
.setTokenType(PACKAGE) .setTokenType(PACKAGE)
.setAllowedRegistrarIds(ImmutableSet.of("TheRegistrar"))
.setAllowedTlds(ImmutableSet.of("tld")) .setAllowedTlds(ImmutableSet.of("tld"))
.setRenewalPriceBehavior(SPECIFIED) .setRenewalPriceBehavior(SPECIFIED)
.build()); .build());

View file

@ -20,6 +20,7 @@ import static google.registry.model.domain.token.AllocationToken.TokenStatus.CAN
import static google.registry.model.domain.token.AllocationToken.TokenStatus.ENDED; import static google.registry.model.domain.token.AllocationToken.TokenStatus.ENDED;
import static google.registry.model.domain.token.AllocationToken.TokenStatus.NOT_STARTED; import static google.registry.model.domain.token.AllocationToken.TokenStatus.NOT_STARTED;
import static google.registry.model.domain.token.AllocationToken.TokenStatus.VALID; import static google.registry.model.domain.token.AllocationToken.TokenStatus.VALID;
import static google.registry.model.domain.token.AllocationToken.TokenType.PACKAGE;
import static google.registry.model.domain.token.AllocationToken.TokenType.SINGLE_USE; import static google.registry.model.domain.token.AllocationToken.TokenType.SINGLE_USE;
import static google.registry.model.domain.token.AllocationToken.TokenType.UNLIMITED_USE; import static google.registry.model.domain.token.AllocationToken.TokenType.UNLIMITED_USE;
import static google.registry.testing.DatabaseHelper.createTld; import static google.registry.testing.DatabaseHelper.createTld;
@ -33,6 +34,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap; import com.google.common.collect.ImmutableSortedMap;
import com.googlecode.objectify.Key; import com.googlecode.objectify.Key;
import google.registry.model.Buildable;
import google.registry.model.EntityTestCase; import google.registry.model.EntityTestCase;
import google.registry.model.billing.BillingEvent.RenewalPriceBehavior; import google.registry.model.billing.BillingEvent.RenewalPriceBehavior;
import google.registry.model.domain.Domain; import google.registry.model.domain.Domain;
@ -276,6 +278,20 @@ public class AllocationTokenTest extends EntityTestCase {
.isEqualTo("Domain name can only be specified for SINGLE_USE tokens"); .isEqualTo("Domain name can only be specified for SINGLE_USE tokens");
} }
@Test
void testBuild_onlyOneClientInPackage() {
Buildable.Builder builder =
new AllocationToken.Builder()
.setToken("foobar")
.setTokenType(PACKAGE)
.setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED)
.setAllowedRegistrarIds(ImmutableSet.of("foo", "bar"));
IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, builder::build);
assertThat(thrown)
.hasMessageThat()
.isEqualTo("PACKAGE tokens must have exactly one allowed client registrar");
}
@Test @Test
void testBuild_redemptionHistoryEntryOnlyInSingleUse() { void testBuild_redemptionHistoryEntryOnlyInSingleUse() {
Domain domain = persistActiveDomain("blahdomain.foo"); Domain domain = persistActiveDomain("blahdomain.foo");

View file

@ -0,0 +1,100 @@
// Copyright 2022 The Nomulus 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 google.registry.model.domain.token;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.loadByEntity;
import static google.registry.testing.DatabaseHelper.persistResource;
import static org.junit.jupiter.api.Assertions.assertThrows;
import google.registry.model.EntityTestCase;
import google.registry.model.billing.BillingEvent.RenewalPriceBehavior;
import google.registry.model.domain.token.AllocationToken.TokenType;
import org.joda.money.CurrencyUnit;
import org.joda.money.Money;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.testcontainers.shaded.com.google.common.collect.ImmutableSet;
/** Unit tests for {@link PackagePromotion}. */
public class PackagePromotionTest extends EntityTestCase {
public PackagePromotionTest() {
super(JpaEntityCoverageCheck.ENABLED);
}
@BeforeEach
void beforeEach() {
createTld("foo");
}
@Test
void testPersistence() {
AllocationToken token =
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(TokenType.PACKAGE)
.setCreationTimeForTest(DateTime.parse("2010-11-12T05:00:00Z"))
.setAllowedTlds(ImmutableSet.of("foo"))
.setAllowedRegistrarIds(ImmutableSet.of("TheRegistrar"))
.setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED)
.setDiscountFraction(1)
.build());
PackagePromotion packagePromotion =
persistResource(
new PackagePromotion.Builder()
.setToken(token)
.setPackagePrice(Money.of(CurrencyUnit.USD, 10000))
.setMaxCreates(40)
.setMaxDomains(10)
.setNextBillingDate(DateTime.parse("2011-11-12T05:00:00Z"))
.build());
assertThat(loadByEntity(packagePromotion)).isEqualTo(packagePromotion);
}
@Test
void testFail_tokenIsNotPackage() {
AllocationToken token =
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(TokenType.SINGLE_USE)
.setCreationTimeForTest(DateTime.parse("2010-11-12T05:00:00Z"))
.setAllowedTlds(ImmutableSet.of("foo"))
.setAllowedRegistrarIds(ImmutableSet.of("TheRegistrar"))
.setDiscountFraction(1)
.build());
IllegalArgumentException thrown =
assertThrows(
IllegalArgumentException.class,
() ->
persistResource(
new PackagePromotion.Builder()
.setToken(token)
.setPackagePrice(Money.of(CurrencyUnit.USD, 10000))
.setMaxCreates(40)
.setMaxDomains(10)
.setNextBillingDate(DateTime.parse("2011-11-12T05:00:00Z"))
.build()));
assertThat(thrown).hasMessageThat().isEqualTo("Allocation token must be a PACKAGE type");
}
}

View file

@ -21,6 +21,7 @@ import google.registry.model.common.CursorTest;
import google.registry.model.contact.ContactResourceTest; import google.registry.model.contact.ContactResourceTest;
import google.registry.model.domain.DomainSqlTest; import google.registry.model.domain.DomainSqlTest;
import google.registry.model.domain.token.AllocationTokenTest; import google.registry.model.domain.token.AllocationTokenTest;
import google.registry.model.domain.token.PackagePromotionTest;
import google.registry.model.history.ContactHistoryTest; import google.registry.model.history.ContactHistoryTest;
import google.registry.model.history.DomainHistoryTest; import google.registry.model.history.DomainHistoryTest;
import google.registry.model.history.HostHistoryTest; import google.registry.model.history.HostHistoryTest;
@ -88,6 +89,7 @@ import org.junit.runner.RunWith;
DomainHistoryTest.class, DomainHistoryTest.class,
HostHistoryTest.class, HostHistoryTest.class,
LockTest.class, LockTest.class,
PackagePromotionTest.class,
PollMessageTest.class, PollMessageTest.class,
PremiumListDaoTest.class, PremiumListDaoTest.class,
RdeRevisionTest.class, RdeRevisionTest.class,

View file

@ -491,6 +491,18 @@
primary key (resource_name, scope) primary key (resource_name, scope)
); );
create table "PackagePromotion" (
package_promotion_id int8 not null,
last_notification_sent timestamptz,
max_creates int4 not null,
max_domains int4 not null,
next_billing_date timestamptz not null,
package_price_amount numeric(19, 2) not null,
package_price_currency text not null,
token text not null,
primary key (package_promotion_id)
);
create table "PollMessage" ( create table "PollMessage" (
type text not null, type text not null,
poll_message_id int8 not null, poll_message_id int8 not null,
@ -797,6 +809,7 @@ create index IDX1iy7njgb7wjmj9piml4l2g0qi on "HostHistory" (history_registrar_id
create index IDXkkwbwcwvrdkkqothkiye4jiff on "HostHistory" (host_name); create index IDXkkwbwcwvrdkkqothkiye4jiff on "HostHistory" (host_name);
create index IDXknk8gmj7s47q56cwpa6rmpt5l on "HostHistory" (history_type); create index IDXknk8gmj7s47q56cwpa6rmpt5l on "HostHistory" (history_type);
create index IDX67qwkjtlq5q8dv6egtrtnhqi7 on "HostHistory" (history_modification_time); create index IDX67qwkjtlq5q8dv6egtrtnhqi7 on "HostHistory" (history_modification_time);
create index IDXlg6a5tp70nch9cp0gc11brc5o on "PackagePromotion" (token);
create index IDXe7wu46c7wpvfmfnj4565abibp on "PollMessage" (registrar_id); create index IDXe7wu46c7wpvfmfnj4565abibp on "PollMessage" (registrar_id);
create index IDXaydgox62uno9qx8cjlj5lauye on "PollMessage" (event_time); create index IDXaydgox62uno9qx8cjlj5lauye on "PollMessage" (event_time);
create index premiumlist_name_idx on "PremiumList" (name); create index premiumlist_name_idx on "PremiumList" (name);