diff --git a/core/src/main/java/google/registry/beam/billing/ExpandBillingRecurrencesPipeline.java b/core/src/main/java/google/registry/beam/billing/ExpandBillingRecurrencesPipeline.java index 8c8461c15..cb867dd6b 100644 --- a/core/src/main/java/google/registry/beam/billing/ExpandBillingRecurrencesPipeline.java +++ b/core/src/main/java/google/registry/beam/billing/ExpandBillingRecurrencesPipeline.java @@ -36,6 +36,7 @@ import google.registry.config.RegistryConfig.ConfigModule; import google.registry.flows.custom.CustomLogicFactoryModule; import google.registry.flows.custom.CustomLogicModule; import google.registry.flows.domain.DomainPricingLogic; +import google.registry.flows.domain.DomainPricingLogic.AllocationTokenInvalidForCurrencyException; import google.registry.flows.domain.DomainPricingLogic.AllocationTokenInvalidForPremiumNameException; import google.registry.model.ImmutableObject; import google.registry.model.billing.BillingBase.Flag; @@ -414,7 +415,8 @@ public class ExpandBillingRecurrencesPipeline implements Serializable { .setCancellationMatchingBillingEvent(billingRecurrence) .setTargetId(billingRecurrence.getTargetId()) .build(); - } catch (AllocationTokenInvalidForPremiumNameException e) { + } catch (AllocationTokenInvalidForCurrencyException + | AllocationTokenInvalidForPremiumNameException e) { // This should not be reached since we are not using an allocation token return; } diff --git a/core/src/main/java/google/registry/flows/domain/DomainPricingLogic.java b/core/src/main/java/google/registry/flows/domain/DomainPricingLogic.java index 6478ed152..9b55e4a12 100644 --- a/core/src/main/java/google/registry/flows/domain/DomainPricingLogic.java +++ b/core/src/main/java/google/registry/flows/domain/DomainPricingLogic.java @@ -95,7 +95,7 @@ public final class DomainPricingLogic { false, tld.getCreateBillingCost(dateTime), domainPrices.getRenewCost()); } Money domainCreateCost = - getDomainCreateCostWithDiscount(domainPrices, years, allocationToken); + getDomainCreateCostWithDiscount(domainPrices, years, allocationToken, tld); // Apply a sunrise discount if configured and applicable if (isSunriseCreate) { domainCreateCost = @@ -134,7 +134,8 @@ public final class DomainPricingLogic { int years, @Nullable BillingRecurrence billingRecurrence, Optional allocationToken) - throws AllocationTokenInvalidForPremiumNameException { + throws AllocationTokenInvalidForCurrencyException, + AllocationTokenInvalidForPremiumNameException { checkArgument(years > 0, "Number of years must be positive"); Money renewCost; DomainPrices domainPrices = getPricesForDomainName(domainName, dateTime); @@ -172,7 +173,8 @@ public final class DomainPricingLogic { years, allocationToken, tld.getStandardRenewCost(dateTime), - Optional.empty()); + Optional.empty(), + tld); isRenewCostPremiumPrice = false; } default -> @@ -262,14 +264,15 @@ public final class DomainPricingLogic { /** Returns the domain create cost with allocation-token-related discounts applied. */ private Money getDomainCreateCostWithDiscount( - DomainPrices domainPrices, int years, Optional allocationToken) + DomainPrices domainPrices, int years, Optional allocationToken, Tld tld) throws EppException { return getDomainCostWithDiscount( domainPrices.isPremium(), years, allocationToken, domainPrices.getCreateCost(), - Optional.of(domainPrices.getRenewCost())); + Optional.of(domainPrices.getRenewCost()), + tld); } /** Returns the domain renew cost with allocation-token-related discounts applied. */ @@ -279,7 +282,8 @@ public final class DomainPricingLogic { DateTime dateTime, int years, Optional allocationToken) - throws AllocationTokenInvalidForPremiumNameException { + throws AllocationTokenInvalidForCurrencyException, + AllocationTokenInvalidForPremiumNameException { // Short-circuit if the user sent an anchor-tenant or otherwise NONPREMIUM-renewal token if (allocationToken.isPresent()) { AllocationToken token = allocationToken.get(); @@ -293,7 +297,8 @@ public final class DomainPricingLogic { years, allocationToken, domainPrices.getRenewCost(), - Optional.empty()); + Optional.empty(), + tld); } /** @@ -310,8 +315,10 @@ public final class DomainPricingLogic { int years, Optional allocationToken, Money firstYearCost, - Optional subsequentYearCost) - throws AllocationTokenInvalidForPremiumNameException { + Optional subsequentYearCost, + Tld tld) + throws AllocationTokenInvalidForCurrencyException, + AllocationTokenInvalidForPremiumNameException { checkArgument(years > 0, "Registration years to get cost for must be positive."); validateTokenForPossiblePremiumName(allocationToken, isPremium); Money totalDomainFlowCost = @@ -320,13 +327,31 @@ public final class DomainPricingLogic { // Apply the allocation token discount, if applicable. if (allocationToken.isPresent() && allocationToken.get().getTokenBehavior().equals(TokenBehavior.DEFAULT)) { - int discountedYears = Math.min(years, allocationToken.get().getDiscountYears()); - if (discountedYears > 0) { - var discount = - firstYearCost - .plus(subsequentYearCost.orElse(firstYearCost).multipliedBy(discountedYears - 1)) - .multipliedBy(allocationToken.get().getDiscountFraction(), RoundingMode.HALF_EVEN); - totalDomainFlowCost = totalDomainFlowCost.minus(discount); + if (allocationToken.get().getDiscountPrice().isPresent()) { + if (!tld.getCurrency() + .equals(allocationToken.get().getDiscountPrice().get().getCurrencyUnit())) { + throw new AllocationTokenInvalidForCurrencyException(); + } + + int nonDiscountedYears = Math.max(0, years - allocationToken.get().getDiscountYears()); + totalDomainFlowCost = + allocationToken + .get() + .getDiscountPrice() + .get() + .multipliedBy(allocationToken.get().getDiscountYears()) + .plus(subsequentYearCost.orElse(firstYearCost).multipliedBy(nonDiscountedYears)); + } else { + // Assumes token has discount fraction set. + int discountedYears = Math.min(years, allocationToken.get().getDiscountYears()); + if (discountedYears > 0) { + var discount = + firstYearCost + .plus(subsequentYearCost.orElse(firstYearCost).multipliedBy(discountedYears - 1)) + .multipliedBy( + allocationToken.get().getDiscountFraction(), RoundingMode.HALF_EVEN); + totalDomainFlowCost = totalDomainFlowCost.minus(discount); + } } } return totalDomainFlowCost; @@ -339,4 +364,10 @@ public final class DomainPricingLogic { super("Token not valid for premium name"); } } + + public static class AllocationTokenInvalidForCurrencyException extends CommandUseErrorException { + public AllocationTokenInvalidForCurrencyException() { + super("Token and domain currencies do not match."); + } + } } diff --git a/core/src/main/java/google/registry/flows/domain/token/AllocationTokenFlowUtils.java b/core/src/main/java/google/registry/flows/domain/token/AllocationTokenFlowUtils.java index 288034e3f..3e3c46bf2 100644 --- a/core/src/main/java/google/registry/flows/domain/token/AllocationTokenFlowUtils.java +++ b/core/src/main/java/google/registry/flows/domain/token/AllocationTokenFlowUtils.java @@ -156,7 +156,7 @@ public class AllocationTokenFlowUtils { Optional token, boolean isPremium) throws AllocationTokenInvalidForPremiumNameException { if (token.isPresent() - && token.get().getDiscountFraction() != 0.0 + && (token.get().getDiscountFraction() != 0.0 || token.get().getDiscountPrice().isPresent()) && isPremium && !token.get().shouldDiscountPremiums()) { throw new AllocationTokenInvalidForPremiumNameException(); @@ -288,6 +288,7 @@ public class AllocationTokenFlowUtils { super("Alloc token not in promo period"); } } + /** The allocation token is not valid for this TLD. */ public static class AllocationTokenNotValidForTldException extends AssociationProhibitsOperationException { 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 6a23f90c6..69bb38443 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 @@ -123,8 +123,8 @@ public class AllocationToken extends UpdateAutoTimestampEntity implements Builda * Bypasses the premium list to use the standard creation price. Does not affect the renewal * price. * - *

This cannot be specified along with a discount fraction, and any renewals (automatic or - * otherwise) will use the premium price for the domain if one exists. + *

This cannot be specified along with a discount fraction/price, and any renewals (automatic + * or otherwise) will use the premium price for the domain if one exists. * *

Tokens with this behavior must be tied to a single particular domain. */ @@ -248,6 +248,22 @@ public class AllocationToken extends UpdateAutoTimestampEntity implements Builda @AttributeOverride(name = "currency", column = @Column(name = "renewalPriceCurrency")) Money renewalPrice; + /** + * A discount that allows the setting of promotional prices. This field is different from {@code + * discountFraction} because the price set here is treated as the domain price, versus {@code + * discountFraction} that applies a fraction discount to the domain base price. + * + *

Prefer this method of discount when attempting to set a promotional price across TLDs with + * different base prices. + */ + @Nullable + @AttributeOverride( + name = "amount", + // Override Hibernate default (numeric(38,2)) to match real schema definition (numeric(19,2)). + column = @Column(name = "discountPriceAmount", precision = 19, scale = 2)) + @AttributeOverride(name = "currency", column = @Column(name = "discountPriceCurrency")) + Money discountPrice; + @Enumerated(EnumType.STRING) @Column(nullable = false) RegistrationBehavior registrationBehavior = RegistrationBehavior.DEFAULT; @@ -299,6 +315,10 @@ public class AllocationToken extends UpdateAutoTimestampEntity implements Builda return discountFraction; } + public Optional getDiscountPrice() { + return Optional.ofNullable(discountPrice); + } + public boolean shouldDiscountPremiums() { return discountPremiums; } @@ -406,8 +426,10 @@ public class AllocationToken extends UpdateAutoTimestampEntity implements Builda getInstance().discountFraction > 0 || !getInstance().discountPremiums, "Discount premiums can only be specified along with a discount fraction"); checkArgument( - getInstance().discountFraction > 0 || getInstance().discountYears == 1, - "Discount years can only be specified along with a discount fraction"); + getInstance().discountFraction > 0 + || getInstance().discountPrice != null + || getInstance().discountYears == 1, + "Discount years can only be specified along with a discount fraction/price"); if (getInstance().getTokenType().equals(REGISTER_BSA)) { checkArgumentNotNull( getInstance().domainName, "REGISTER_BSA tokens must be tied to a domain"); @@ -418,7 +440,7 @@ public class AllocationToken extends UpdateAutoTimestampEntity implements Builda } if (getInstance().registrationBehavior.equals(RegistrationBehavior.NONPREMIUM_CREATE)) { checkArgument( - getInstance().discountFraction == 0.0, + getInstance().discountFraction == 0.0 && getInstance().discountPrice == null, "NONPREMIUM_CREATE tokens cannot apply a discount"); checkArgumentNotNull( getInstance().domainName, "NONPREMIUM_CREATE tokens must be tied to a domain"); @@ -454,6 +476,12 @@ public class AllocationToken extends UpdateAutoTimestampEntity implements Builda "BULK_PRICING tokens must have exactly one allowed client registrar"); } + if (getInstance().discountFraction != 0.0) { + checkArgument( + getInstance().discountPrice == null, + "discountFraction and discountPrice can't be set together"); + } + if (getInstance().domainName != null) { try { DomainFlowUtils.validateDomainName(getInstance().domainName); @@ -559,5 +587,10 @@ public class AllocationToken extends UpdateAutoTimestampEntity implements Builda getInstance().registrationBehavior = registrationBehavior; return this; } + + public Builder setDiscountPrice(@Nullable Money discountPrice) { + getInstance().discountPrice = discountPrice; + return this; + } } } diff --git a/core/src/test/java/google/registry/flows/domain/DomainPricingLogicTest.java b/core/src/test/java/google/registry/flows/domain/DomainPricingLogicTest.java index dc274e393..5b358294d 100644 --- a/core/src/test/java/google/registry/flows/domain/DomainPricingLogicTest.java +++ b/core/src/test/java/google/registry/flows/domain/DomainPricingLogicTest.java @@ -28,6 +28,7 @@ import static google.registry.testing.DatabaseHelper.persistPremiumList; import static google.registry.testing.DatabaseHelper.persistResource; import static google.registry.util.DateTimeUtils.END_OF_TIME; import static google.registry.util.DateTimeUtils.START_OF_TIME; +import static org.joda.money.CurrencyUnit.JPY; import static org.joda.money.CurrencyUnit.USD; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -38,6 +39,7 @@ import google.registry.flows.EppException; import google.registry.flows.HttpSessionMetadata; import google.registry.flows.SessionMetadata; import google.registry.flows.custom.DomainPricingCustomLogic; +import google.registry.flows.domain.DomainPricingLogic.AllocationTokenInvalidForCurrencyException; import google.registry.flows.domain.DomainPricingLogic.AllocationTokenInvalidForPremiumNameException; import google.registry.model.billing.BillingBase.Reason; import google.registry.model.billing.BillingBase.RenewalPriceBehavior; @@ -46,6 +48,7 @@ import google.registry.model.domain.Domain; import google.registry.model.domain.DomainHistory; import google.registry.model.domain.fee.Fee; import google.registry.model.domain.token.AllocationToken; +import google.registry.model.domain.token.AllocationToken.RegistrationBehavior; import google.registry.model.eppinput.EppInput; import google.registry.model.tld.Tld; import google.registry.model.tld.Tld.TldState; @@ -151,6 +154,92 @@ public class DomainPricingLogicTest { .build()); } + @Test + void testGetDomainCreatePrice_discountPriceAllocationToken_oneYearCreate_appliesDiscount() + throws EppException { + AllocationToken allocationToken = + persistResource( + new AllocationToken.Builder() + .setToken("abc123") + .setTokenType(SINGLE_USE) + .setDomainName("default.example") + .setDiscountPrice(Money.of(USD, 5)) + .setDiscountYears(1) + .setRegistrationBehavior(RegistrationBehavior.DEFAULT) + .build()); + assertThat( + domainPricingLogic.getCreatePrice( + tld, + "default.example", + clock.nowUtc(), + 1, + false, + false, + Optional.of(allocationToken))) + .isEqualTo( + new FeesAndCredits.Builder() + .setCurrency(USD) + .addFeeOrCredit(Fee.create(new BigDecimal("5.00"), CREATE, false)) + .build()); + } + + @Test + void testGetDomainCreatePrice_discountPriceAllocationToken_multiYearCreate_appliesDiscount() + throws EppException { + AllocationToken allocationToken = + persistResource( + new AllocationToken.Builder() + .setToken("abc123") + .setTokenType(SINGLE_USE) + .setDomainName("default.example") + .setDiscountPrice(Money.of(USD, 5)) + .setDiscountYears(1) + .setRegistrationBehavior(RegistrationBehavior.DEFAULT) + .build()); + + // 3 year create should be 5 (discount price) + 10*2 (regular price) = 25. + assertThat( + domainPricingLogic.getCreatePrice( + tld, + "default.example", + clock.nowUtc(), + 3, + false, + false, + Optional.of(allocationToken))) + .isEqualTo( + new FeesAndCredits.Builder() + .setCurrency(USD) + .addFeeOrCredit(Fee.create(new BigDecimal("25.00"), CREATE, false)) + .build()); + } + + @Test + void + testGetDomainCreatePrice_withDiscountPriceToken_domainCurrencyDoesNotMatchTokensCurrency_throwsException() { + AllocationToken allocationToken = + persistResource( + new AllocationToken.Builder() + .setToken("abc123") + .setTokenType(SINGLE_USE) + .setDiscountPrice(Money.of(JPY, new BigDecimal("250"))) + .setDiscountPremiums(false) + .build()); + + // Domain's currency is not JPY (is USD). + assertThrows( + AllocationTokenInvalidForCurrencyException.class, + () -> + domainPricingLogic.getCreatePrice( + tld, + "default.example", + clock.nowUtc(), + 3, + false, + false, + Optional.of(allocationToken))); + } + @Test void testGetDomainRenewPrice_oneYear_standardDomain_noBilling_isStandardPrice() throws EppException { @@ -269,6 +358,54 @@ public class DomainPricingLogicTest { Optional.of(allocationToken))); } + @Test + void + testGetDomainRenewPrice_oneYear_premiumDomain_default_withDiscountPriceToken_throwsException() { + AllocationToken allocationToken = + persistResource( + new AllocationToken.Builder() + .setToken("abc123") + .setTokenType(SINGLE_USE) + .setDiscountPrice(Money.of(USD, 5)) + .setDiscountPremiums(false) + .build()); + assertThrows( + AllocationTokenInvalidForPremiumNameException.class, + () -> + domainPricingLogic.getRenewPrice( + tld, + "premium.example", + clock.nowUtc(), + 1, + persistDomainAndSetRecurrence("premium.example", DEFAULT, Optional.empty()), + Optional.of(allocationToken))); + } + + @Test + void + testGetDomainRenewPrice_withDiscountPriceToken_domainCurrencyDoesNotMatchTokensCurrency_throwsException() { + AllocationToken allocationToken = + persistResource( + new AllocationToken.Builder() + .setToken("abc123") + .setTokenType(SINGLE_USE) + .setDiscountPrice(Money.of(JPY, new BigDecimal("250"))) + .setDiscountPremiums(false) + .build()); + + // Domain's currency is not JPY (is USD). + assertThrows( + AllocationTokenInvalidForCurrencyException.class, + () -> + domainPricingLogic.getRenewPrice( + tld, + "default.example", + clock.nowUtc(), + 1, + persistDomainAndSetRecurrence("default.example", DEFAULT, Optional.empty()), + Optional.of(allocationToken))); + } + @Test void testGetDomainRenewPrice_multiYear_premiumDomain_default_isPremiumCost() throws EppException { assertThat( @@ -381,6 +518,33 @@ public class DomainPricingLogicTest { .build()); } + @Test + void + testGetDomainRenewPrice_oneYear_standardDomain_default_withDiscountPriceToken_isDiscountedPrice() + throws EppException { + AllocationToken allocationToken = + persistResource( + new AllocationToken.Builder() + .setToken("abc123") + .setTokenType(SINGLE_USE) + .setDiscountPrice(Money.of(USD, 1.5)) + .setDiscountPremiums(false) + .build()); + assertThat( + domainPricingLogic.getRenewPrice( + tld, + "standard.example", + clock.nowUtc(), + 1, + persistDomainAndSetRecurrence("standard.example", DEFAULT, Optional.empty()), + Optional.of(allocationToken))) + .isEqualTo( + new FeesAndCredits.Builder() + .setCurrency(USD) + .addFeeOrCredit(Fee.create(new BigDecimal("1.50"), RENEW, false)) + .build()); + } + @Test void testGetDomainRenewPrice_multiYear_standardDomain_default_isNonPremiumCost() throws EppException { @@ -426,6 +590,36 @@ public class DomainPricingLogicTest { .build()); } + @Test + void + testGetDomainRenewPrice_multiYear_standardDomain_default_withDiscountPriceToken_isDiscountedPrice() + throws EppException { + AllocationToken allocationToken = + persistResource( + new AllocationToken.Builder() + .setToken("discountPrice12345") + .setTokenType(SINGLE_USE) + .setDiscountPrice(Money.of(USD, 2.5)) + .setDiscountPremiums(false) + .setDiscountYears(2) + .build()); + + // 5 year create should be 2*2.5 (discount price) + 10*3 (regular price) = 35. + assertThat( + domainPricingLogic.getRenewPrice( + tld, + "standard.example", + clock.nowUtc(), + 5, + persistDomainAndSetRecurrence("standard.example", DEFAULT, Optional.empty()), + Optional.of(allocationToken))) + .isEqualTo( + new FeesAndCredits.Builder() + .setCurrency(USD) + .addFeeOrCredit(Fee.create(new BigDecimal("35.00"), RENEW, false)) + .build()); + } + @Test void testGetDomainRenewPrice_oneYear_premiumDomain_anchorTenant_isNonPremiumPrice() throws EppException { @@ -602,6 +796,36 @@ public class DomainPricingLogicTest { .build()); } + @Test + void + testGetDomainRenewPrice_oneYear_standardDomain_internalRegistration_withDiscountPriceToken_isSpecifiedPrice() + throws EppException { + AllocationToken allocationToken = + persistResource( + new AllocationToken.Builder() + .setToken("abc123") + .setTokenType(SINGLE_USE) + .setDiscountPrice(Money.of(USD, 0.5)) + .setDiscountPremiums(false) + .build()); + assertThat( + domainPricingLogic.getRenewPrice( + tld, + "standard.example", + clock.nowUtc(), + 1, + persistDomainAndSetRecurrence( + "standard.example", SPECIFIED, Optional.of(Money.of(USD, 1))), + Optional.of(allocationToken))) + + // The allocation token should not discount the speicifed price + .isEqualTo( + new FeesAndCredits.Builder() + .setCurrency(USD) + .addFeeOrCredit(Fee.create(new BigDecimal("1.00"), RENEW, false)) + .build()); + } + @Test void testGetDomainRenewPrice_oneYear_standardDomain_internalRegistration_withToken_doesNotChangePriceBehavior() @@ -684,6 +908,34 @@ public class DomainPricingLogicTest { .build()); } + @Test + void + testGetDomainRenewPrice_multiYear_standardDomain_internalRegistration_withDiscountPriceToken_isSpecifiedPrice() + throws EppException { + AllocationToken allocationToken = + persistResource( + new AllocationToken.Builder() + .setToken("abc123") + .setTokenType(SINGLE_USE) + .setDiscountPrice(Money.of(USD, 0.5)) + .setDiscountPremiums(false) + .build()); + assertThat( + domainPricingLogic.getRenewPrice( + tld, + "standard.example", + clock.nowUtc(), + 5, + persistDomainAndSetRecurrence( + "standard.example", SPECIFIED, Optional.of(Money.of(USD, 1))), + Optional.of(allocationToken))) + .isEqualTo( + new FeesAndCredits.Builder() + .setCurrency(USD) + .addFeeOrCredit(Fee.create(new BigDecimal("5.00"), RENEW, false)) + .build()); + } + @Test void testGetDomainRenewPrice_oneYear_premiumDomain_internalRegistration_isSpecifiedPrice() throws EppException { diff --git a/core/src/test/java/google/registry/model/domain/token/AllocationTokenTest.java b/core/src/test/java/google/registry/model/domain/token/AllocationTokenTest.java index aaa3c9160..6fbf1962f 100644 --- a/core/src/test/java/google/registry/model/domain/token/AllocationTokenTest.java +++ b/core/src/test/java/google/registry/model/domain/token/AllocationTokenTest.java @@ -571,7 +571,7 @@ public class AllocationTokenTest extends EntityTestCase { } @Test - void testBuild_discountYearsRequiresDiscountFraction() { + void testBuild_discountYearsRequiresDiscountFractionOrPrice() { IllegalArgumentException thrown = assertThrows( IllegalArgumentException.class, @@ -583,7 +583,7 @@ public class AllocationTokenTest extends EntityTestCase { .build()); assertThat(thrown) .hasMessageThat() - .isEqualTo("Discount years can only be specified along with a discount fraction"); + .isEqualTo("Discount years can only be specified along with a discount fraction/price"); } @Test @@ -669,6 +669,42 @@ public class AllocationTokenTest extends EntityTestCase { .build()); } + @Test + void testBuild_discountPriceCantBeSetWithDiscountFraction() { + IllegalArgumentException thrown = + assertThrows( + IllegalArgumentException.class, + () -> + new AllocationToken.Builder() + .setToken("abc") + .setTokenType(SINGLE_USE) + .setDiscountYears(2) + .setDiscountFraction(0.5) + .setDiscountPrice(Money.of(CurrencyUnit.USD, 5)) + .build()); + assertThat(thrown) + .hasMessageThat() + .isEqualTo("discountFraction and discountPrice can't be set together"); + } + + @Test + void testBuild_discountPriceCantBeSetWithNonPremiumCreateRegistrationBehavior() { + IllegalArgumentException thrown = + assertThrows( + IllegalArgumentException.class, + () -> + new AllocationToken.Builder() + .setToken("abc") + .setTokenType(SINGLE_USE) + .setRegistrationBehavior(RegistrationBehavior.NONPREMIUM_CREATE) + .setDiscountYears(2) + .setDiscountPrice(Money.of(CurrencyUnit.USD, 5)) + .build()); + assertThat(thrown) + .hasMessageThat() + .isEqualTo("NONPREMIUM_CREATE tokens cannot apply a discount"); + } + private void assertBadInitialTransition(TokenStatus status) { assertBadTransition( ImmutableSortedMap.naturalOrder() 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 2e516d4aa..893c287ea 100644 --- a/db/src/main/resources/sql/schema/db-schema.sql.generated +++ b/db/src/main/resources/sql/schema/db-schema.sql.generated @@ -21,6 +21,8 @@ creation_time timestamp(6) with time zone not null, discount_fraction float(53) not null, discount_premiums boolean not null, + discount_price_amount numeric(19,2), + discount_price_currency text, discount_years integer not null, domain_name text, redemption_domain_repo_id text,