diff --git a/core/src/main/java/google/registry/model/domain/token/AllocationToken.java b/core/src/main/java/google/registry/model/domain/token/AllocationToken.java index 618d15b9b..23d8b1b86 100644 --- a/core/src/main/java/google/registry/model/domain/token/AllocationToken.java +++ b/core/src/main/java/google/registry/model/domain/token/AllocationToken.java @@ -103,7 +103,10 @@ public class AllocationToken extends BackupGroupRoot implements Buildable { 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 { PACKAGE, SINGLE_USE, @@ -286,6 +289,10 @@ public class AllocationToken extends BackupGroupRoot implements Buildable { getInstance().redemptionHistoryEntry == null || TokenType.SINGLE_USE.equals(getInstance().tokenType), "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( getInstance().discountFraction > 0 || !getInstance().discountPremiums, "Discount premiums can only be specified along with a discount fraction"); diff --git a/core/src/main/java/google/registry/model/domain/token/PackagePromotion.java b/core/src/main/java/google/registry/model/domain/token/PackagePromotion.java new file mode 100644 index 000000000..a71d8e1d7 --- /dev/null +++ b/core/src/main/java/google/registry/model/domain/token/PackagePromotion.java @@ -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 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 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 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 { + 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; + } + } +} diff --git a/core/src/main/resources/META-INF/persistence.xml b/core/src/main/resources/META-INF/persistence.xml index 53a31e03f..ccbe28e1a 100644 --- a/core/src/main/resources/META-INF/persistence.xml +++ b/core/src/main/resources/META-INF/persistence.xml @@ -52,6 +52,7 @@ google.registry.model.domain.secdns.DelegationSignerData google.registry.model.domain.secdns.DomainDsDataHistory google.registry.model.domain.token.AllocationToken + google.registry.model.domain.token.PackagePromotion google.registry.model.host.HostHistory google.registry.model.host.Host google.registry.model.poll.PollMessage diff --git a/core/src/test/java/google/registry/flows/domain/DomainCreateFlowTest.java b/core/src/test/java/google/registry/flows/domain/DomainCreateFlowTest.java index 4eebf1c0d..e309d07ad 100644 --- a/core/src/test/java/google/registry/flows/domain/DomainCreateFlowTest.java +++ b/core/src/test/java/google/registry/flows/domain/DomainCreateFlowTest.java @@ -3136,6 +3136,7 @@ class DomainCreateFlowTest extends ResourceFlowTestCase + 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"); + } +} diff --git a/core/src/test/java/google/registry/schema/integration/SqlIntegrationTestSuite.java b/core/src/test/java/google/registry/schema/integration/SqlIntegrationTestSuite.java index 707fbb6be..f60920d3c 100644 --- a/core/src/test/java/google/registry/schema/integration/SqlIntegrationTestSuite.java +++ b/core/src/test/java/google/registry/schema/integration/SqlIntegrationTestSuite.java @@ -21,6 +21,7 @@ import google.registry.model.common.CursorTest; import google.registry.model.contact.ContactResourceTest; import google.registry.model.domain.DomainSqlTest; 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.DomainHistoryTest; import google.registry.model.history.HostHistoryTest; @@ -88,6 +89,7 @@ import org.junit.runner.RunWith; DomainHistoryTest.class, HostHistoryTest.class, LockTest.class, + PackagePromotionTest.class, PollMessageTest.class, PremiumListDaoTest.class, RdeRevisionTest.class, diff --git a/db/src/main/resources/sql/schema/db-schema.sql.generated b/db/src/main/resources/sql/schema/db-schema.sql.generated index f3782ff13..65efc7421 100644 --- a/db/src/main/resources/sql/schema/db-schema.sql.generated +++ b/db/src/main/resources/sql/schema/db-schema.sql.generated @@ -491,6 +491,18 @@ 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" ( type text 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 IDXknk8gmj7s47q56cwpa6rmpt5l on "HostHistory" (history_type); create index IDX67qwkjtlq5q8dv6egtrtnhqi7 on "HostHistory" (history_modification_time); +create index IDXlg6a5tp70nch9cp0gc11brc5o on "PackagePromotion" (token); create index IDXe7wu46c7wpvfmfnj4565abibp on "PollMessage" (registrar_id); create index IDXaydgox62uno9qx8cjlj5lauye on "PollMessage" (event_time); create index premiumlist_name_idx on "PremiumList" (name);