mirror of
https://github.com/google/nomulus.git
synced 2025-04-30 03:57:51 +02:00
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:
parent
3c0805def5
commit
a6087bf328
8 changed files with 298 additions and 1 deletions
|
@ -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");
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -52,6 +52,7 @@
|
|||
<class>google.registry.model.domain.secdns.DelegationSignerData</class>
|
||||
<class>google.registry.model.domain.secdns.DomainDsDataHistory</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.Host</class>
|
||||
<class>google.registry.model.poll.PollMessage</class>
|
||||
|
|
|
@ -3136,6 +3136,7 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
|
|||
new AllocationToken.Builder()
|
||||
.setToken("abc123")
|
||||
.setTokenType(PACKAGE)
|
||||
.setAllowedRegistrarIds(ImmutableSet.of("TheRegistrar"))
|
||||
.setAllowedTlds(ImmutableSet.of("tld"))
|
||||
.setRenewalPriceBehavior(SPECIFIED)
|
||||
.build());
|
||||
|
|
|
@ -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.NOT_STARTED;
|
||||
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.UNLIMITED_USE;
|
||||
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.ImmutableSortedMap;
|
||||
import com.googlecode.objectify.Key;
|
||||
import google.registry.model.Buildable;
|
||||
import google.registry.model.EntityTestCase;
|
||||
import google.registry.model.billing.BillingEvent.RenewalPriceBehavior;
|
||||
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");
|
||||
}
|
||||
|
||||
@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
|
||||
void testBuild_redemptionHistoryEntryOnlyInSingleUse() {
|
||||
Domain domain = persistActiveDomain("blahdomain.foo");
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Add table
Reference in a new issue