diff --git a/core/src/main/java/google/registry/beam/billing/ExpandRecurringBillingEventsPipeline.java b/core/src/main/java/google/registry/beam/billing/ExpandRecurringBillingEventsPipeline.java index 8a4fabfea..f2ec8b7f9 100644 --- a/core/src/main/java/google/registry/beam/billing/ExpandRecurringBillingEventsPipeline.java +++ b/core/src/main/java/google/registry/beam/billing/ExpandRecurringBillingEventsPipeline.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.AllocationTokenInvalidForPremiumNameException; import google.registry.model.ImmutableObject; import google.registry.model.billing.BillingEvent.Cancellation; import google.registry.model.billing.BillingEvent.Flag; @@ -53,6 +54,7 @@ import google.registry.util.Clock; import google.registry.util.SystemClock; import java.io.Serializable; import java.math.BigInteger; +import java.util.Optional; import java.util.Set; import javax.inject.Singleton; import org.apache.beam.sdk.Pipeline; @@ -383,24 +385,31 @@ public class ExpandRecurringBillingEventsPipeline implements Serializable { // It is OK to always create a OneTime, even though the domain might be deleted or transferred // later during autorenew grace period, as a cancellation will always be written out in those // instances. - OneTime oneTime = - new OneTime.Builder() - .setBillingTime(billingTime) - .setRegistrarId(recurring.getRegistrarId()) - // Determine the cost for a one-year renewal. - .setCost( - domainPricingLogic - .getRenewPrice(tld, recurring.getTargetId(), eventTime, 1, recurring) - .getRenewCost()) - .setEventTime(eventTime) - .setFlags(union(recurring.getFlags(), Flag.SYNTHETIC)) - .setDomainHistory(historyEntry) - .setPeriodYears(1) - .setReason(recurring.getReason()) - .setSyntheticCreationTime(endTime) - .setCancellationMatchingBillingEvent(recurring) - .setTargetId(recurring.getTargetId()) - .build(); + OneTime oneTime = null; + try { + oneTime = + new OneTime.Builder() + .setBillingTime(billingTime) + .setRegistrarId(recurring.getRegistrarId()) + // Determine the cost for a one-year renewal. + .setCost( + domainPricingLogic + .getRenewPrice( + tld, recurring.getTargetId(), eventTime, 1, recurring, Optional.empty()) + .getRenewCost()) + .setEventTime(eventTime) + .setFlags(union(recurring.getFlags(), Flag.SYNTHETIC)) + .setDomainHistory(historyEntry) + .setPeriodYears(1) + .setReason(recurring.getReason()) + .setSyntheticCreationTime(endTime) + .setCancellationMatchingBillingEvent(recurring) + .setTargetId(recurring.getTargetId()) + .build(); + } catch (AllocationTokenInvalidForPremiumNameException e) { + // This should not be reached since we are not using an allocation token + return; + } results.add(oneTime); } results.add( diff --git a/core/src/main/java/google/registry/flows/domain/DomainCreateFlow.java b/core/src/main/java/google/registry/flows/domain/DomainCreateFlow.java index 4cf7c683c..8b47d6494 100644 --- a/core/src/main/java/google/registry/flows/domain/DomainCreateFlow.java +++ b/core/src/main/java/google/registry/flows/domain/DomainCreateFlow.java @@ -265,6 +265,7 @@ public final class DomainCreateFlow implements TransactionalFlow { validateLaunchCreateNotice(launchCreate.get().getNotice(), domainLabel, isSuperuser, now); } boolean isSunriseCreate = hasSignedMarks && (tldState == START_DATE_SUNRISE); + // TODO(sarahbot@): Add check for valid EPP actions on the token Optional allocationToken = allocationTokenFlowUtils.verifyAllocationTokenCreateIfPresent( command, diff --git a/core/src/main/java/google/registry/flows/domain/DomainFlowUtils.java b/core/src/main/java/google/registry/flows/domain/DomainFlowUtils.java index 697fb05de..6af91c55d 100644 --- a/core/src/main/java/google/registry/flows/domain/DomainFlowUtils.java +++ b/core/src/main/java/google/registry/flows/domain/DomainFlowUtils.java @@ -674,6 +674,8 @@ public class DomainFlowUtils { String feeClass = null; ImmutableList fees = ImmutableList.of(); switch (feeRequest.getCommandName()) { + // TODO(sarahbot@): Add check of valid EPP actions on token before passing the token to the + // fee request. case CREATE: // Don't return a create price for reserved names. if (isReserved(domainName, isSunrise) && !isAvailable) { @@ -698,7 +700,8 @@ public class DomainFlowUtils { builder.setAvailIfSupported(true); fees = pricingLogic - .getRenewPrice(registry, domainNameString, now, years, recurringBillingEvent) + .getRenewPrice( + registry, domainNameString, now, years, recurringBillingEvent, allocationToken) .getFees(); break; case RESTORE: 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 bdadfed81..937fe631f 100644 --- a/core/src/main/java/google/registry/flows/domain/DomainPricingLogic.java +++ b/core/src/main/java/google/registry/flows/domain/DomainPricingLogic.java @@ -34,6 +34,7 @@ import google.registry.model.domain.fee.BaseFee; import google.registry.model.domain.fee.BaseFee.FeeType; import google.registry.model.domain.fee.Fee; import google.registry.model.domain.token.AllocationToken; +import google.registry.model.domain.token.AllocationToken.TokenBehavior; import google.registry.model.pricing.PremiumPricingEngine.DomainPrices; import google.registry.model.tld.Registry; import java.math.RoundingMode; @@ -112,21 +113,22 @@ public final class DomainPricingLogic { String domainName, DateTime dateTime, int years, - @Nullable Recurring recurringBillingEvent) { + @Nullable Recurring recurringBillingEvent, + Optional allocationToken) + throws AllocationTokenInvalidForPremiumNameException { checkArgument(years > 0, "Number of years must be positive"); Money renewCost; + DomainPrices domainPrices = getPricesForDomainName(domainName, dateTime); boolean isRenewCostPremiumPrice; // recurring billing event is null if the domain is still available. Billing events are created // in the process of domain creation. if (recurringBillingEvent == null) { - DomainPrices domainPrices = getPricesForDomainName(domainName, dateTime); - renewCost = domainPrices.getRenewCost().multipliedBy(years); + renewCost = getDomainRenewCostWithDiscount(domainPrices, years, allocationToken); isRenewCostPremiumPrice = domainPrices.isPremium(); } else { switch (recurringBillingEvent.getRenewalPriceBehavior()) { case DEFAULT: - DomainPrices domainPrices = getPricesForDomainName(domainName, dateTime); - renewCost = domainPrices.getRenewCost().multipliedBy(years); + renewCost = getDomainRenewCostWithDiscount(domainPrices, years, allocationToken); isRenewCostPremiumPrice = domainPrices.isPremium(); break; // if the renewal price behavior is specified, then the renewal price should be the same @@ -136,6 +138,7 @@ public final class DomainPricingLogic { recurringBillingEvent.getRenewalPrice(), "Unexpected behavior: renewal price cannot be null when renewal behavior is" + " SPECIFIED"); + // Don't apply allocation token to renewal price when SPECIFIED renewCost = recurringBillingEvent.getRenewalPrice().get().multipliedBy(years); isRenewCostPremiumPrice = false; break; @@ -143,9 +146,11 @@ public final class DomainPricingLogic { // at standard price of domains at the time, even if the domain is premium case NONPREMIUM: renewCost = - Registry.get(getTldFromDomainName(domainName)) - .getStandardRenewCost(dateTime) - .multipliedBy(years); + getDomainCostWithDiscount( + false, + years, + allocationToken, + Registry.get(getTldFromDomainName(domainName)).getStandardRenewCost(dateTime)); isRenewCostPremiumPrice = false; break; default: @@ -202,7 +207,7 @@ public final class DomainPricingLogic { @Nullable Recurring recurringBillingEvent) throws EppException { FeesAndCredits renewPrice = - getRenewPrice(registry, domainName, dateTime, 1, recurringBillingEvent); + getRenewPrice(registry, domainName, dateTime, 1, recurringBillingEvent, Optional.empty()); return customLogic.customizeTransferPrice( TransferPriceParameters.newBuilder() .setFeesAndCredits( @@ -242,25 +247,40 @@ public final class DomainPricingLogic { private Money getDomainCreateCostWithDiscount( DomainPrices domainPrices, int years, Optional allocationToken) throws EppException { + return getDomainCostWithDiscount( + domainPrices.isPremium(), years, allocationToken, domainPrices.getCreateCost()); + } + + /** Returns the domain renew cost with allocation-token-related discounts applied. */ + private Money getDomainRenewCostWithDiscount( + DomainPrices domainPrices, int years, Optional allocationToken) + throws AllocationTokenInvalidForPremiumNameException { + return getDomainCostWithDiscount( + domainPrices.isPremium(), years, allocationToken, domainPrices.getRenewCost()); + } + + private Money getDomainCostWithDiscount( + boolean isPremium, int years, Optional allocationToken, Money oneYearCost) + throws AllocationTokenInvalidForPremiumNameException { if (allocationToken.isPresent() && allocationToken.get().getDiscountFraction() != 0.0 - && domainPrices.isPremium() + && isPremium && !allocationToken.get().shouldDiscountPremiums()) { throw new AllocationTokenInvalidForPremiumNameException(); } - Money oneYearCreateCost = domainPrices.getCreateCost(); - Money totalDomainCreateCost = oneYearCreateCost.multipliedBy(years); + Money totalDomainFlowCost = oneYearCost.multipliedBy(years); // Apply the allocation token discount, if applicable. - if (allocationToken.isPresent()) { + if (allocationToken.isPresent() + && allocationToken.get().getTokenBehavior().equals(TokenBehavior.DEFAULT)) { int discountedYears = Math.min(years, allocationToken.get().getDiscountYears()); Money discount = - oneYearCreateCost.multipliedBy( + oneYearCost.multipliedBy( discountedYears * allocationToken.get().getDiscountFraction(), RoundingMode.HALF_EVEN); - totalDomainCreateCost = totalDomainCreateCost.minus(discount); + totalDomainFlowCost = totalDomainFlowCost.minus(discount); } - return totalDomainCreateCost; + return totalDomainFlowCost; } /** An allocation token was provided that is invalid for premium domains. */ diff --git a/core/src/main/java/google/registry/flows/domain/DomainRenewFlow.java b/core/src/main/java/google/registry/flows/domain/DomainRenewFlow.java index ad7ef8311..118f421fd 100644 --- a/core/src/main/java/google/registry/flows/domain/DomainRenewFlow.java +++ b/core/src/main/java/google/registry/flows/domain/DomainRenewFlow.java @@ -172,6 +172,7 @@ public final class DomainRenewFlow implements TransactionalFlow { Renew command = (Renew) resourceCommand; // Loads the target resource if it exists Domain existingDomain = loadAndVerifyExistence(Domain.class, targetId, now); + // TODO(sarahbot@): Add check for valid EPP actions on the token Optional allocationToken = allocationTokenFlowUtils.verifyAllocationTokenIfPresent( existingDomain, @@ -198,7 +199,8 @@ public final class DomainRenewFlow implements TransactionalFlow { targetId, now, years, - existingRecurringBillingEvent); + existingRecurringBillingEvent, + allocationToken); validateFeeChallenge(feeRenew, feesAndCredits, false); flowCustomLogic.afterValidation( AfterValidationParameters.newBuilder() 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 fe0b19728..62edf70cc 100644 --- a/core/src/test/java/google/registry/flows/domain/DomainCreateFlowTest.java +++ b/core/src/test/java/google/registry/flows/domain/DomainCreateFlowTest.java @@ -1577,7 +1577,7 @@ class DomainCreateFlowTest extends ResourceFlowTestCase tm().loadAllOf(BillingEvent.OneTime.class))); + Iterables.getOnlyElement(DatabaseHelper.loadAllOf(BillingEvent.OneTime.class)); assertThat(billingEvent.getTargetId()).isEqualTo("example.tld"); assertThat(billingEvent.getCost()).isEqualTo(Money.of(USD, BigDecimal.valueOf(19.5))); } @@ -1627,7 +1627,7 @@ class DomainCreateFlowTest extends ResourceFlowTestCase tm().loadAllOf(BillingEvent.OneTime.class))); + Iterables.getOnlyElement(DatabaseHelper.loadAllOf(BillingEvent.OneTime.class)); assertThat(billingEvent.getTargetId()).isEqualTo("example.tld"); assertThat(billingEvent.getCost()).isEqualTo(expectedPrice); } @@ -1660,7 +1660,7 @@ class DomainCreateFlowTest extends ResourceFlowTestCase tm().loadAllOf(BillingEvent.OneTime.class))); + Iterables.getOnlyElement(DatabaseHelper.loadAllOf(BillingEvent.OneTime.class)); assertThat(billingEvent.getTargetId()).isEqualTo("rich.example"); // 1yr @ $100 + 2yrs @ $100 * (1 - 0.98) = $104 assertThat(billingEvent.getCost()).isEqualTo(Money.of(USD, 104.00)); @@ -1693,7 +1693,7 @@ class DomainCreateFlowTest extends ResourceFlowTestCase tm().loadAllOf(BillingEvent.OneTime.class))); + Iterables.getOnlyElement(DatabaseHelper.loadAllOf(BillingEvent.OneTime.class)); assertThat(billingEvent.getTargetId()).isEqualTo("rich.example"); // 2yrs @ $100 + 1yr @ $100 * (1 - 0.95555) = $204.44 assertThat(billingEvent.getCost()).isEqualTo(Money.of(USD, 204.44)); @@ -1862,7 +1862,7 @@ class DomainCreateFlowTest extends ResourceFlowTestCase tm().loadAllOf(BillingEvent.OneTime.class))); + Iterables.getOnlyElement(DatabaseHelper.loadAllOf(BillingEvent.OneTime.class)); assertThat(billingEvent.getTargetId()).isEqualTo("example.tld"); assertThat(billingEvent.getCost()).isEqualTo(Money.of(USD, BigDecimal.valueOf(19.5))); assertThat(billingEvent.getAllocationToken().get().getKey()).isEqualTo("abc123"); @@ -2058,7 +2058,7 @@ class DomainCreateFlowTest extends ResourceFlowTestCase tm().loadAllOf(BillingEvent.OneTime.class))); + Iterables.getOnlyElement(DatabaseHelper.loadAllOf(BillingEvent.OneTime.class)); assertThat(billingEvent.getTargetId()).isEqualTo("example.tld"); assertThat(billingEvent.getAllocationToken().get().getKey()).isEqualTo(token); return billingEvent; 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 f31c98387..98e40ef52 100644 --- a/core/src/test/java/google/registry/flows/domain/DomainPricingLogicTest.java +++ b/core/src/test/java/google/registry/flows/domain/DomainPricingLogicTest.java @@ -20,6 +20,7 @@ import static google.registry.model.billing.BillingEvent.RenewalPriceBehavior.DE import static google.registry.model.billing.BillingEvent.RenewalPriceBehavior.NONPREMIUM; import static google.registry.model.billing.BillingEvent.RenewalPriceBehavior.SPECIFIED; import static google.registry.model.domain.fee.BaseFee.FeeType.RENEW; +import static google.registry.model.domain.token.AllocationToken.TokenType.SINGLE_USE; import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_CREATE; import static google.registry.testing.DatabaseHelper.createTld; import static google.registry.testing.DatabaseHelper.persistPremiumList; @@ -32,11 +33,13 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSortedMap; +import com.google.common.collect.Iterables; import google.registry.flows.EppException; import google.registry.flows.FlowMetadata; import google.registry.flows.HttpSessionMetadata; import google.registry.flows.SessionMetadata; import google.registry.flows.custom.DomainPricingCustomLogic; +import google.registry.flows.domain.DomainPricingLogic.AllocationTokenInvalidForPremiumNameException; import google.registry.model.billing.BillingEvent; import google.registry.model.billing.BillingEvent.Reason; import google.registry.model.billing.BillingEvent.Recurring; @@ -44,6 +47,7 @@ import google.registry.model.billing.BillingEvent.RenewalPriceBehavior; 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.eppinput.EppInput; import google.registry.model.tld.Registry; import google.registry.persistence.transaction.JpaTestExtensions; @@ -133,7 +137,8 @@ public class DomainPricingLogicTest { void testGetDomainRenewPrice_oneYear_standardDomain_noBilling_isStandardPrice() throws EppException { assertThat( - domainPricingLogic.getRenewPrice(registry, "standard.example", clock.nowUtc(), 1, null)) + domainPricingLogic.getRenewPrice( + registry, "standard.example", clock.nowUtc(), 1, null, Optional.empty())) .isEqualTo( new FeesAndCredits.Builder() .setCurrency(USD) @@ -145,7 +150,8 @@ public class DomainPricingLogicTest { void testGetDomainRenewPrice_multiYear_standardDomain_noBilling_isStandardPrice() throws EppException { assertThat( - domainPricingLogic.getRenewPrice(registry, "standard.example", clock.nowUtc(), 5, null)) + domainPricingLogic.getRenewPrice( + registry, "standard.example", clock.nowUtc(), 5, null, Optional.empty())) .isEqualTo( new FeesAndCredits.Builder() .setCurrency(USD) @@ -157,7 +163,8 @@ public class DomainPricingLogicTest { void testGetDomainRenewPrice_oneYear_premiumDomain_noBilling_isPremiumPrice() throws EppException { assertThat( - domainPricingLogic.getRenewPrice(registry, "premium.example", clock.nowUtc(), 1, null)) + domainPricingLogic.getRenewPrice( + registry, "premium.example", clock.nowUtc(), 1, null, Optional.empty())) .isEqualTo( new FeesAndCredits.Builder() .setCurrency(USD) @@ -169,7 +176,8 @@ public class DomainPricingLogicTest { void testGetDomainRenewPrice_multiYear_premiumDomain_noBilling_isPremiumPrice() throws EppException { assertThat( - domainPricingLogic.getRenewPrice(registry, "premium.example", clock.nowUtc(), 5, null)) + domainPricingLogic.getRenewPrice( + registry, "premium.example", clock.nowUtc(), 5, null, Optional.empty())) .isEqualTo( new FeesAndCredits.Builder() .setCurrency(USD) @@ -186,7 +194,8 @@ public class DomainPricingLogicTest { clock.nowUtc(), 1, persistDomainAndSetRecurringBillingEvent( - "premium.example", DEFAULT, Optional.empty()))) + "premium.example", DEFAULT, Optional.empty()), + Optional.empty())) .isEqualTo( new FeesAndCredits.Builder() .setCurrency(USD) @@ -194,6 +203,58 @@ public class DomainPricingLogicTest { .build()); } + @Test + void testGetDomainRenewPrice_oneYear_premiumDomain_default_withToken_isPremiumPrice() + throws EppException { + AllocationToken allocationToken = + persistResource( + new AllocationToken.Builder() + .setToken("abc123") + .setTokenType(SINGLE_USE) + .setDiscountFraction(0.5) + .setDiscountPremiums(true) + .build()); + assertThat( + domainPricingLogic.getRenewPrice( + registry, + "premium.example", + clock.nowUtc(), + 1, + persistDomainAndSetRecurringBillingEvent( + "premium.example", DEFAULT, Optional.empty()), + Optional.of(allocationToken))) + .isEqualTo( + new FeesAndCredits.Builder() + .setCurrency(USD) + .addFeeOrCredit(Fee.create(Money.of(USD, 50).getAmount(), RENEW, true)) + .build()); + } + + @Test + void + testGetDomainRenewPrice_oneYear_premiumDomain_default_withTokenNotValidForPremiums_throwsException() + throws EppException { + AllocationToken allocationToken = + persistResource( + new AllocationToken.Builder() + .setToken("abc123") + .setTokenType(SINGLE_USE) + .setDiscountFraction(0.5) + .setDiscountPremiums(false) + .build()); + assertThrows( + AllocationTokenInvalidForPremiumNameException.class, + () -> + domainPricingLogic.getRenewPrice( + registry, + "premium.example", + clock.nowUtc(), + 1, + persistDomainAndSetRecurringBillingEvent( + "premium.example", DEFAULT, Optional.empty()), + Optional.of(allocationToken))); + } + @Test void testGetDomainRenewPrice_multiYear_premiumDomain_default_isPremiumCost() throws EppException { assertThat( @@ -203,7 +264,8 @@ public class DomainPricingLogicTest { clock.nowUtc(), 5, persistDomainAndSetRecurringBillingEvent( - "premium.example", DEFAULT, Optional.empty()))) + "premium.example", DEFAULT, Optional.empty()), + Optional.empty())) .isEqualTo( new FeesAndCredits.Builder() .setCurrency(USD) @@ -211,6 +273,60 @@ public class DomainPricingLogicTest { .build()); } + @Test + void testGetDomainRenewPrice_multiYear_premiumDomain_default_withToken_isPremiumPrice() + throws EppException { + AllocationToken allocationToken = + persistResource( + new AllocationToken.Builder() + .setToken("abc123") + .setTokenType(SINGLE_USE) + .setDiscountFraction(0.5) + .setDiscountPremiums(true) + .setDiscountYears(2) + .build()); + assertThat( + domainPricingLogic.getRenewPrice( + registry, + "premium.example", + clock.nowUtc(), + 5, + persistDomainAndSetRecurringBillingEvent( + "premium.example", DEFAULT, Optional.empty()), + Optional.of(allocationToken))) + .isEqualTo( + new FeesAndCredits.Builder() + .setCurrency(USD) + .addFeeOrCredit(Fee.create(Money.of(USD, 400).getAmount(), RENEW, true)) + .build()); + } + + @Test + void + testGetDomainRenewPrice_multiYear_premiumDomain_default_withTokenNotValidForPremiums_throwsException() + throws EppException { + AllocationToken allocationToken = + persistResource( + new AllocationToken.Builder() + .setToken("abc123") + .setTokenType(SINGLE_USE) + .setDiscountFraction(0.5) + .setDiscountPremiums(false) + .setDiscountYears(2) + .build()); + assertThrows( + AllocationTokenInvalidForPremiumNameException.class, + () -> + domainPricingLogic.getRenewPrice( + registry, + "premium.example", + clock.nowUtc(), + 5, + persistDomainAndSetRecurringBillingEvent( + "premium.example", DEFAULT, Optional.empty()), + Optional.of(allocationToken))); + } + @Test void testGetDomainRenewPrice_oneYear_standardDomain_default_isNonPremiumPrice() throws EppException { @@ -221,7 +337,8 @@ public class DomainPricingLogicTest { clock.nowUtc(), 1, persistDomainAndSetRecurringBillingEvent( - "premium.example", DEFAULT, Optional.empty()))) + "standard.example", DEFAULT, Optional.empty()), + Optional.empty())) .isEqualTo( new FeesAndCredits.Builder() .setCurrency(USD) @@ -229,6 +346,33 @@ public class DomainPricingLogicTest { .build()); } + @Test + void testGetDomainRenewPrice_oneYear_standardDomain_default_withToken_isDiscountedPrice() + throws EppException { + AllocationToken allocationToken = + persistResource( + new AllocationToken.Builder() + .setToken("abc123") + .setTokenType(SINGLE_USE) + .setDiscountFraction(0.5) + .setDiscountPremiums(false) + .build()); + assertThat( + domainPricingLogic.getRenewPrice( + registry, + "standard.example", + clock.nowUtc(), + 1, + persistDomainAndSetRecurringBillingEvent( + "standard.example", DEFAULT, Optional.empty()), + Optional.of(allocationToken))) + .isEqualTo( + new FeesAndCredits.Builder() + .setCurrency(USD) + .addFeeOrCredit(Fee.create(Money.of(USD, 5).getAmount(), RENEW, false)) + .build()); + } + @Test void testGetDomainRenewPrice_multiYear_standardDomain_default_isNonPremiumCost() throws EppException { @@ -239,7 +383,8 @@ public class DomainPricingLogicTest { clock.nowUtc(), 5, persistDomainAndSetRecurringBillingEvent( - "standard.example", DEFAULT, Optional.empty()))) + "standard.example", DEFAULT, Optional.empty()), + Optional.empty())) .isEqualTo( new FeesAndCredits.Builder() .setCurrency(USD) @@ -247,6 +392,34 @@ public class DomainPricingLogicTest { .build()); } + @Test + void testGetDomainRenewPrice_multiYear_standardDomain_default_withToken_isDiscountedPrice() + throws EppException { + AllocationToken allocationToken = + persistResource( + new AllocationToken.Builder() + .setToken("abc123") + .setTokenType(SINGLE_USE) + .setDiscountFraction(0.5) + .setDiscountPremiums(false) + .setDiscountYears(2) + .build()); + assertThat( + domainPricingLogic.getRenewPrice( + registry, + "standard.example", + clock.nowUtc(), + 5, + persistDomainAndSetRecurringBillingEvent( + "standard.example", DEFAULT, Optional.empty()), + Optional.of(allocationToken))) + .isEqualTo( + new FeesAndCredits.Builder() + .setCurrency(USD) + .addFeeOrCredit(Fee.create(Money.of(USD, 40).getAmount(), RENEW, false)) + .build()); + } + @Test void testGetDomainRenewPrice_oneYear_premiumDomain_anchorTenant_isNonPremiumPrice() throws EppException { @@ -257,7 +430,8 @@ public class DomainPricingLogicTest { clock.nowUtc(), 1, persistDomainAndSetRecurringBillingEvent( - "premium.example", NONPREMIUM, Optional.empty()))) + "premium.example", NONPREMIUM, Optional.empty()), + Optional.empty())) .isEqualTo( new FeesAndCredits.Builder() .setCurrency(USD) @@ -265,6 +439,34 @@ public class DomainPricingLogicTest { .build()); } + @Test + void + testGetDomainRenewPrice_oneYear_premiumDomain_anchorTenant__withToken_isDiscountedNonPremiumPrice() + throws EppException { + AllocationToken allocationToken = + persistResource( + new AllocationToken.Builder() + .setToken("abc123") + .setTokenType(SINGLE_USE) + .setDiscountFraction(0.5) + .setDiscountPremiums(false) + .build()); + assertThat( + domainPricingLogic.getRenewPrice( + registry, + "premium.example", + clock.nowUtc(), + 1, + persistDomainAndSetRecurringBillingEvent( + "premium.example", NONPREMIUM, Optional.empty()), + Optional.of(allocationToken))) + .isEqualTo( + new FeesAndCredits.Builder() + .setCurrency(USD) + .addFeeOrCredit(Fee.create(Money.of(USD, 5).getAmount(), RENEW, false)) + .build()); + } + @Test void testGetDomainRenewPrice_multiYear_premiumDomain_anchorTenant_isNonPremiumCost() throws EppException { @@ -275,7 +477,8 @@ public class DomainPricingLogicTest { clock.nowUtc(), 5, persistDomainAndSetRecurringBillingEvent( - "premium.example", NONPREMIUM, Optional.empty()))) + "premium.example", NONPREMIUM, Optional.empty()), + Optional.empty())) .isEqualTo( new FeesAndCredits.Builder() .setCurrency(USD) @@ -283,6 +486,35 @@ public class DomainPricingLogicTest { .build()); } + @Test + void + testGetDomainRenewPrice_multiYear_premiumDomain_anchorTenant__withToken_isDiscountedNonPremiumPrice() + throws EppException { + AllocationToken allocationToken = + persistResource( + new AllocationToken.Builder() + .setToken("abc123") + .setTokenType(SINGLE_USE) + .setDiscountFraction(0.5) + .setDiscountPremiums(false) + .setDiscountYears(2) + .build()); + assertThat( + domainPricingLogic.getRenewPrice( + registry, + "premium.example", + clock.nowUtc(), + 5, + persistDomainAndSetRecurringBillingEvent( + "premium.example", NONPREMIUM, Optional.empty()), + Optional.of(allocationToken))) + .isEqualTo( + new FeesAndCredits.Builder() + .setCurrency(USD) + .addFeeOrCredit(Fee.create(Money.of(USD, 40).getAmount(), RENEW, false)) + .build()); + } + @Test void testGetDomainRenewPrice_oneYear_standardDomain_anchorTenant_isNonPremiumPrice() throws EppException { @@ -293,7 +525,8 @@ public class DomainPricingLogicTest { clock.nowUtc(), 1, persistDomainAndSetRecurringBillingEvent( - "standard.example", NONPREMIUM, Optional.empty()))) + "standard.example", NONPREMIUM, Optional.empty()), + Optional.empty())) .isEqualTo( new FeesAndCredits.Builder() .setCurrency(USD) @@ -311,7 +544,8 @@ public class DomainPricingLogicTest { clock.nowUtc(), 5, persistDomainAndSetRecurringBillingEvent( - "standard.example", NONPREMIUM, Optional.empty()))) + "standard.example", NONPREMIUM, Optional.empty()), + Optional.empty())) .isEqualTo( new FeesAndCredits.Builder() .setCurrency(USD) @@ -329,7 +563,8 @@ public class DomainPricingLogicTest { clock.nowUtc(), 1, persistDomainAndSetRecurringBillingEvent( - "standard.example", SPECIFIED, Optional.of(Money.of(USD, 1))))) + "standard.example", SPECIFIED, Optional.of(Money.of(USD, 1))), + Optional.empty())) .isEqualTo( new FeesAndCredits.Builder() .setCurrency(USD) @@ -337,6 +572,71 @@ public class DomainPricingLogicTest { .build()); } + @Test + void + testGetDomainRenewPrice_oneYear_standardDomain_internalRegistration_withToken_isSpecifiedPrice() + throws EppException { + AllocationToken allocationToken = + persistResource( + new AllocationToken.Builder() + .setToken("abc123") + .setTokenType(SINGLE_USE) + .setDiscountFraction(0.5) + .setDiscountPremiums(false) + .build()); + assertThat( + domainPricingLogic.getRenewPrice( + registry, + "standard.example", + clock.nowUtc(), + 1, + persistDomainAndSetRecurringBillingEvent( + "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(Money.of(USD, 1).getAmount(), RENEW, false)) + .build()); + } + + @Test + void + testGetDomainRenewPrice_oneYear_standardDomain_internalRegistration_withToken_doesNotChangePriceBehavior() + throws EppException { + AllocationToken allocationToken = + persistResource( + new AllocationToken.Builder() + .setToken("abc123") + .setTokenType(SINGLE_USE) + .setDiscountFraction(0.5) + .setRenewalPriceBehavior(DEFAULT) + .setDiscountPremiums(false) + .build()); + assertThat( + domainPricingLogic.getRenewPrice( + registry, + "standard.example", + clock.nowUtc(), + 1, + persistDomainAndSetRecurringBillingEvent( + "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(Money.of(USD, 1).getAmount(), RENEW, false)) + .build()); + assertThat( + Iterables.getLast(DatabaseHelper.loadAllOf(BillingEvent.Recurring.class)) + .getRenewalPriceBehavior()) + .isEqualTo(SPECIFIED); + } + @Test void testGetDomainRenewPrice_multiYear_standardDomain_internalRegistration_isSpecifiedPrice() throws EppException { @@ -347,7 +647,36 @@ public class DomainPricingLogicTest { clock.nowUtc(), 5, persistDomainAndSetRecurringBillingEvent( - "standard.example", SPECIFIED, Optional.of(Money.of(USD, 1))))) + "standard.example", SPECIFIED, Optional.of(Money.of(USD, 1))), + Optional.empty())) + .isEqualTo( + new FeesAndCredits.Builder() + .setCurrency(USD) + .addFeeOrCredit(Fee.create(Money.of(USD, 5).getAmount(), RENEW, false)) + .build()); + } + + @Test + void + testGetDomainRenewPrice_multiYear_standardDomain_internalRegistration_withToken_isSpecifiedPrice() + throws EppException { + AllocationToken allocationToken = + persistResource( + new AllocationToken.Builder() + .setToken("abc123") + .setTokenType(SINGLE_USE) + .setDiscountFraction(0.5) + .setDiscountPremiums(false) + .build()); + assertThat( + domainPricingLogic.getRenewPrice( + registry, + "standard.example", + clock.nowUtc(), + 5, + persistDomainAndSetRecurringBillingEvent( + "standard.example", SPECIFIED, Optional.of(Money.of(USD, 1))), + Optional.of(allocationToken))) .isEqualTo( new FeesAndCredits.Builder() .setCurrency(USD) @@ -365,7 +694,8 @@ public class DomainPricingLogicTest { clock.nowUtc(), 1, persistDomainAndSetRecurringBillingEvent( - "premium.example", SPECIFIED, Optional.of(Money.of(USD, 17))))) + "premium.example", SPECIFIED, Optional.of(Money.of(USD, 17))), + Optional.empty())) .isEqualTo( new FeesAndCredits.Builder() .setCurrency(USD) @@ -383,7 +713,8 @@ public class DomainPricingLogicTest { clock.nowUtc(), 5, persistDomainAndSetRecurringBillingEvent( - "premium.example", SPECIFIED, Optional.of(Money.of(USD, 17))))) + "premium.example", SPECIFIED, Optional.of(Money.of(USD, 17))), + Optional.empty())) .isEqualTo( new FeesAndCredits.Builder() .setCurrency(USD) @@ -398,7 +729,7 @@ public class DomainPricingLogicTest { IllegalArgumentException.class, () -> domainPricingLogic.getRenewPrice( - registry, "standard.example", clock.nowUtc(), -1, null)); + registry, "standard.example", clock.nowUtc(), -1, null, Optional.empty())); assertThat(thrown).hasMessageThat().isEqualTo("Number of years must be positive"); } diff --git a/core/src/test/java/google/registry/flows/domain/DomainRenewFlowTest.java b/core/src/test/java/google/registry/flows/domain/DomainRenewFlowTest.java index 5ce4cdb9f..f6dae4366 100644 --- a/core/src/test/java/google/registry/flows/domain/DomainRenewFlowTest.java +++ b/core/src/test/java/google/registry/flows/domain/DomainRenewFlowTest.java @@ -49,6 +49,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSortedMap; +import com.google.common.collect.Iterables; import com.google.common.truth.Truth8; import google.registry.flows.EppException; import google.registry.flows.EppRequestSource; @@ -604,12 +605,21 @@ class DomainRenewFlowTest extends ResourceFlowTestCase .setToken("abc123") .setTokenType(SINGLE_USE) .setDomainName("example.tld") + .setDiscountFraction(0.5) + .setDiscountYears(1) .build()); runFlowAssertResponse( loadFile( "domain_renew_response.xml", ImmutableMap.of("DOMAIN", "example.tld", "EXDATE", "2002-04-03T22:00:00.0Z"))); assertThat(DatabaseHelper.loadByEntity(allocationToken).getRedemptionHistoryId()).isPresent(); + BillingEvent.OneTime billingEvent = + Iterables.getOnlyElement(DatabaseHelper.loadAllOf(BillingEvent.OneTime.class)); + assertThat(billingEvent.getTargetId()).isEqualTo("example.tld"); + assertThat(billingEvent.getAllocationToken().get().getKey()) + .isEqualTo(allocationToken.getToken()); + // Price is 50% off the first year only. Non-discounted price is $11. + assertThat(billingEvent.getCost()).isEqualTo(Money.of(USD, 16.5)); } @Test @@ -619,11 +629,20 @@ class DomainRenewFlowTest extends ResourceFlowTestCase ImmutableMap.of("DOMAIN", "example.tld", "YEARS", "2", "TOKEN", "abc123")); persistDomain(); persistResource( - new AllocationToken.Builder().setToken("abc123").setTokenType(UNLIMITED_USE).build()); + new AllocationToken.Builder() + .setToken("abc123") + .setTokenType(UNLIMITED_USE) + .setDiscountFraction(0.5) + .build()); runFlowAssertResponse( loadFile( "domain_renew_response.xml", ImmutableMap.of("DOMAIN", "example.tld", "EXDATE", "2002-04-03T22:00:00.0Z"))); + BillingEvent.OneTime billingEvent = + Iterables.getOnlyElement(DatabaseHelper.loadAllOf(BillingEvent.OneTime.class)); + assertThat(billingEvent.getTargetId()).isEqualTo("example.tld"); + assertThat(billingEvent.getAllocationToken().get().getKey()).isEqualTo("abc123"); + assertThat(billingEvent.getCost()).isEqualTo(Money.of(USD, 16.5)); clock.advanceOneMilli(); setEppInput( "domain_renew_allocationtoken.xml", @@ -633,6 +652,38 @@ class DomainRenewFlowTest extends ResourceFlowTestCase loadFile( "domain_renew_response.xml", ImmutableMap.of("DOMAIN", "other-example.tld", "EXDATE", "2002-04-03T22:00:00.0Z"))); + billingEvent = Iterables.getLast(DatabaseHelper.loadAllOf(BillingEvent.OneTime.class)); + assertThat(billingEvent.getTargetId()).isEqualTo("other-example.tld"); + assertThat(billingEvent.getAllocationToken().get().getKey()).isEqualTo("abc123"); + assertThat(billingEvent.getCost()).isEqualTo(Money.of(USD, 16.5)); + } + + @Test + void testSuccess_allocationTokenMultiYearDiscount() throws Exception { + setEppInput( + "domain_renew_allocationtoken.xml", + ImmutableMap.of("DOMAIN", "example.tld", "YEARS", "2", "TOKEN", "abc123")); + persistDomain(); + AllocationToken allocationToken = + persistResource( + new AllocationToken.Builder() + .setToken("abc123") + .setTokenType(SINGLE_USE) + .setDomainName("example.tld") + .setDiscountFraction(0.5) + .setDiscountYears(10) + .build()); + runFlowAssertResponse( + loadFile( + "domain_renew_response.xml", + ImmutableMap.of("DOMAIN", "example.tld", "EXDATE", "2002-04-03T22:00:00.0Z"))); + assertThat(DatabaseHelper.loadByEntity(allocationToken).getRedemptionHistoryId()).isPresent(); + BillingEvent.OneTime billingEvent = + Iterables.getOnlyElement(DatabaseHelper.loadAllOf(BillingEvent.OneTime.class)); + assertThat(billingEvent.getTargetId()).isEqualTo("example.tld"); + assertThat(billingEvent.getAllocationToken().get().getKey()) + .isEqualTo(allocationToken.getToken()); + assertThat(billingEvent.getCost()).isEqualTo(Money.of(USD, 11)); } @Test @@ -750,6 +801,7 @@ class DomainRenewFlowTest extends ResourceFlowTestCase .setToken("abc123") .setTokenType(SINGLE_USE) .setRedemptionHistoryId(historyEntryId) + .setDiscountFraction(0.5) .build()); clock.advanceOneMilli(); EppException thrown = diff --git a/core/src/test/resources/google/registry/flows/domain/domain_check_fee_multiple_commands_default_token_response_v06.xml b/core/src/test/resources/google/registry/flows/domain/domain_check_fee_multiple_commands_default_token_response_v06.xml index caa97f5c3..5ec9b04ae 100644 --- a/core/src/test/resources/google/registry/flows/domain/domain_check_fee_multiple_commands_default_token_response_v06.xml +++ b/core/src/test/resources/google/registry/flows/domain/domain_check_fee_multiple_commands_default_token_response_v06.xml @@ -24,7 +24,7 @@ USD renew 1 - 11.00 + 5.50 example1.tld diff --git a/core/src/test/resources/google/registry/flows/domain/domain_check_fee_multiple_commands_default_token_response_v12.xml b/core/src/test/resources/google/registry/flows/domain/domain_check_fee_multiple_commands_default_token_response_v12.xml index c2812ffe3..26e3a8845 100644 --- a/core/src/test/resources/google/registry/flows/domain/domain_check_fee_multiple_commands_default_token_response_v12.xml +++ b/core/src/test/resources/google/registry/flows/domain/domain_check_fee_multiple_commands_default_token_response_v12.xml @@ -28,7 +28,7 @@ 1 - 11.00 + 5.50