Use DomainPricingLogic in ExpandRecurringBillingEventsAction (#1687)

* Inject DomainPricingLogic to action file

* Remove attempt to inject

* Merge conflict

* Fix non static to static issue

* Merge branch 'master' into at_internal/Expand
This commit is contained in:
Ben McIlwain 2022-06-29 17:39:15 -04:00 committed by GitHub
parent cd4770dd5b
commit 9665ddbe70
5 changed files with 315 additions and 17 deletions

View file

@ -26,7 +26,6 @@ import static google.registry.persistence.transaction.QueryComposer.Comparator.E
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm; import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm; import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.persistence.transaction.TransactionManagerUtil.transactIfJpaTm; import static google.registry.persistence.transaction.TransactionManagerUtil.transactIfJpaTm;
import static google.registry.pricing.PricingEngineProxy.getDomainRenewCost;
import static google.registry.util.CollectionUtils.union; import static google.registry.util.CollectionUtils.union;
import static google.registry.util.DateTimeUtils.START_OF_TIME; import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static google.registry.util.DateTimeUtils.earliestOf; import static google.registry.util.DateTimeUtils.earliestOf;
@ -38,6 +37,7 @@ import com.google.common.collect.Range;
import com.google.common.collect.Streams; import com.google.common.collect.Streams;
import com.google.common.flogger.FluentLogger; import com.google.common.flogger.FluentLogger;
import google.registry.config.RegistryConfig.Config; import google.registry.config.RegistryConfig.Config;
import google.registry.flows.domain.DomainPricingLogic;
import google.registry.model.ImmutableObject; import google.registry.model.ImmutableObject;
import google.registry.model.billing.BillingEvent; import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingEvent.Flag; import google.registry.model.billing.BillingEvent.Flag;
@ -61,7 +61,6 @@ import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import javax.inject.Inject; import javax.inject.Inject;
import org.joda.money.Money;
import org.joda.time.DateTime; import org.joda.time.DateTime;
/** /**
@ -89,6 +88,7 @@ public class ExpandRecurringBillingEventsAction implements Runnable {
@Inject @Parameter(PARAM_DRY_RUN) boolean isDryRun; @Inject @Parameter(PARAM_DRY_RUN) boolean isDryRun;
@Inject @Parameter(PARAM_CURSOR_TIME) Optional<DateTime> cursorTimeParam; @Inject @Parameter(PARAM_CURSOR_TIME) Optional<DateTime> cursorTimeParam;
@Inject DomainPricingLogic domainPricingLogic;
@Inject Response response; @Inject Response response;
@Inject ExpandRecurringBillingEventsAction() {} @Inject ExpandRecurringBillingEventsAction() {}
@ -156,7 +156,8 @@ public class ExpandRecurringBillingEventsAction implements Runnable {
continue; continue;
} }
int billingEventsSaved = int billingEventsSaved =
expandBillingEvent(recurring, executeTime, cursorTime, isDryRun); expandBillingEvent(
recurring, executeTime, cursorTime, isDryRun, domainPricingLogic);
batchBillingEventsSaved += billingEventsSaved; batchBillingEventsSaved += billingEventsSaved;
if (billingEventsSaved > 0) { if (billingEventsSaved > 0) {
expandedDomains.add(recurring.getTargetId()); expandedDomains.add(recurring.getTargetId());
@ -231,7 +232,11 @@ public class ExpandRecurringBillingEventsAction implements Runnable {
} }
private static int expandBillingEvent( private static int expandBillingEvent(
Recurring recurring, DateTime executeTime, DateTime cursorTime, boolean isDryRun) { Recurring recurring,
DateTime executeTime,
DateTime cursorTime,
boolean isDryRun,
DomainPricingLogic domainPricingLogic) {
ImmutableSet.Builder<OneTime> syntheticOneTimesBuilder = new ImmutableSet.Builder<>(); ImmutableSet.Builder<OneTime> syntheticOneTimesBuilder = new ImmutableSet.Builder<>();
final Registry tld = Registry.get(getTldFromDomainName(recurring.getTargetId())); final Registry tld = Registry.get(getTldFromDomainName(recurring.getTargetId()));
@ -295,13 +300,16 @@ public class ExpandRecurringBillingEventsAction implements Runnable {
historyEntriesBuilder.add(historyEntry); historyEntriesBuilder.add(historyEntry);
DateTime eventTime = billingTime.minus(tld.getAutoRenewGracePeriodLength()); DateTime eventTime = billingTime.minus(tld.getAutoRenewGracePeriodLength());
// Determine the cost for a one-year renewal.
Money renewCost = getDomainRenewCost(recurring.getTargetId(), eventTime, 1);
syntheticOneTimesBuilder.add( syntheticOneTimesBuilder.add(
new OneTime.Builder() new OneTime.Builder()
.setBillingTime(billingTime) .setBillingTime(billingTime)
.setRegistrarId(recurring.getRegistrarId()) .setRegistrarId(recurring.getRegistrarId())
.setCost(renewCost) // Determine the cost for a one-year renewal.
.setCost(
domainPricingLogic
.getRenewPrice(tld, recurring.getTargetId(), eventTime, 1, recurring)
.getRenewCost())
.setEventTime(eventTime) .setEventTime(eventTime)
.setFlags(union(recurring.getFlags(), Flag.SYNTHETIC)) .setFlags(union(recurring.getFlags(), Flag.SYNTHETIC))
.setParent(historyEntry) .setParent(historyEntry)

View file

@ -50,8 +50,7 @@ public class DomainPricingCustomLogic extends BaseFlowCustomLogic {
/** A hook that customizes the renew price. */ /** A hook that customizes the renew price. */
@SuppressWarnings("unused") @SuppressWarnings("unused")
public FeesAndCredits customizeRenewPrice(RenewPriceParameters priceParameters) public FeesAndCredits customizeRenewPrice(RenewPriceParameters priceParameters) {
throws EppException {
return priceParameters.feesAndCredits(); return priceParameters.feesAndCredits();
} }

View file

@ -51,10 +51,12 @@ import org.joda.time.DateTime;
*/ */
public final class DomainPricingLogic { public final class DomainPricingLogic {
@Inject DomainPricingCustomLogic customLogic; private final DomainPricingCustomLogic customLogic;
@Inject @Inject
DomainPricingLogic() {} public DomainPricingLogic(DomainPricingCustomLogic customLogic) {
this.customLogic = customLogic;
}
/** /**
* Returns a new create price for the pricer. * Returns a new create price for the pricer.
@ -105,13 +107,12 @@ public final class DomainPricingLogic {
} }
/** Returns a new renewal cost for the pricer. */ /** Returns a new renewal cost for the pricer. */
FeesAndCredits getRenewPrice( public FeesAndCredits getRenewPrice(
Registry registry, Registry registry,
String domainName, String domainName,
DateTime dateTime, DateTime dateTime,
int years, int years,
@Nullable Recurring recurringBillingEvent) @Nullable Recurring recurringBillingEvent) {
throws EppException {
checkArgument(years > 0, "Number of years must be positive"); checkArgument(years > 0, "Number of years must be positive");
Money renewCost; Money renewCost;
boolean isRenewCostPremiumPrice; boolean isRenewCostPremiumPrice;

View file

@ -15,6 +15,8 @@
package google.registry.batch; package google.registry.batch;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.billing.BillingEvent.RenewalPriceBehavior.NONPREMIUM;
import static google.registry.model.billing.BillingEvent.RenewalPriceBehavior.SPECIFIED;
import static google.registry.model.common.Cursor.CursorType.RECURRING_BILLING; import static google.registry.model.common.Cursor.CursorType.RECURRING_BILLING;
import static google.registry.model.domain.Period.Unit.YEARS; import static google.registry.model.domain.Period.Unit.YEARS;
import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_AUTORENEW; import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_AUTORENEW;
@ -38,6 +40,8 @@ import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap; import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.googlecode.objectify.Key; import com.googlecode.objectify.Key;
import google.registry.flows.custom.DomainPricingCustomLogic;
import google.registry.flows.domain.DomainPricingLogic;
import google.registry.model.billing.BillingEvent; import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingEvent.Flag; import google.registry.model.billing.BillingEvent.Flag;
import google.registry.model.billing.BillingEvent.OneTime; import google.registry.model.billing.BillingEvent.OneTime;
@ -89,6 +93,8 @@ public class ExpandRecurringBillingEventsActionTest {
action.clock = clock; action.clock = clock;
action.cursorTimeParam = Optional.empty(); action.cursorTimeParam = Optional.empty();
action.batchSize = 2; action.batchSize = 2;
action.domainPricingLogic =
new DomainPricingLogic(new DomainPricingCustomLogic(null, null, null));
createTld("tld"); createTld("tld");
domain = domain =
persistResource( persistResource(
@ -729,6 +735,112 @@ public class ExpandRecurringBillingEventsActionTest {
assertCursorAt(currentTestTime); assertCursorAt(currentTestTime);
} }
@TestOfyAndSql
void testSuccess_expandMultipleEvents_anchorTenant() throws Exception {
persistResource(
Registry.get("tld")
.asBuilder()
.setPremiumList(persistPremiumList("tld2", USD, "example,USD 100"))
.build());
recurring = persistResource(recurring.asBuilder().setRenewalPriceBehavior(NONPREMIUM).build());
BillingEvent.Recurring recurring2 =
persistResource(
recurring
.asBuilder()
.setEventTime(recurring.getEventTime().plusMonths(3))
.setId(3L)
.build());
action.cursorTimeParam = Optional.of(START_OF_TIME);
runAction();
List<DomainHistory> persistedEntries =
getHistoryEntriesOfType(domain, DOMAIN_AUTORENEW, DomainHistory.class);
assertThat(persistedEntries).hasSize(2);
assertHistoryEntryMatches(
domain,
persistedEntries.get(0),
"TheRegistrar",
DateTime.parse("2000-02-19T00:00:00Z"),
true);
BillingEvent.OneTime expected =
defaultOneTimeBuilder()
.setParent(persistedEntries.get(0))
.setCancellationMatchingBillingEvent(recurring.createVKey())
.build();
assertHistoryEntryMatches(
domain,
persistedEntries.get(1),
"TheRegistrar",
DateTime.parse("2000-05-20T00:00:00Z"),
true);
BillingEvent.OneTime expected2 =
defaultOneTimeBuilder()
.setBillingTime(DateTime.parse("2000-05-20T00:00:00Z"))
.setEventTime(DateTime.parse("2000-04-05T00:00:00Z"))
.setParent(persistedEntries.get(1))
.setCancellationMatchingBillingEvent(recurring2.createVKey())
.build();
assertBillingEventsForResource(domain, expected, expected2, recurring, recurring2);
assertCursorAt(currentTestTime);
}
@TestOfyAndSql
void testSuccess_expandMultipleEvents_premiumDomain_internalRegistration() throws Exception {
persistResource(
Registry.get("tld")
.asBuilder()
.setPremiumList(persistPremiumList("tld2", USD, "example,USD 100"))
.build());
recurring =
persistResource(
recurring
.asBuilder()
.setRenewalPriceBehavior(SPECIFIED)
.setRenewalPrice(Money.of(USD, 4))
.build());
BillingEvent.Recurring recurring2 =
persistResource(
recurring
.asBuilder()
.setEventTime(recurring.getEventTime().plusMonths(3))
.setId(3L)
.setRenewalPriceBehavior(SPECIFIED)
.setRenewalPrice(Money.of(USD, 4))
.build());
action.cursorTimeParam = Optional.of(START_OF_TIME);
runAction();
List<DomainHistory> persistedEntries =
getHistoryEntriesOfType(domain, DOMAIN_AUTORENEW, DomainHistory.class);
assertThat(persistedEntries).hasSize(2);
assertHistoryEntryMatches(
domain,
persistedEntries.get(0),
"TheRegistrar",
DateTime.parse("2000-02-19T00:00:00Z"),
true);
BillingEvent.OneTime expected =
defaultOneTimeBuilder()
.setCost(Money.of(USD, 4))
.setParent(persistedEntries.get(0))
.setCancellationMatchingBillingEvent(recurring.createVKey())
.build();
assertHistoryEntryMatches(
domain,
persistedEntries.get(1),
"TheRegistrar",
DateTime.parse("2000-05-20T00:00:00Z"),
true);
BillingEvent.OneTime expected2 =
defaultOneTimeBuilder()
.setCost(Money.of(USD, 4))
.setBillingTime(DateTime.parse("2000-05-20T00:00:00Z"))
.setEventTime(DateTime.parse("2000-04-05T00:00:00Z"))
.setParent(persistedEntries.get(1))
.setCancellationMatchingBillingEvent(recurring2.createVKey())
.build();
assertBillingEventsForResource(domain, expected, expected2, recurring, recurring2);
assertCursorAt(currentTestTime);
}
@TestOfyAndSql @TestOfyAndSql
void testSuccess_premiumDomain() throws Exception { void testSuccess_premiumDomain() throws Exception {
persistResource( persistResource(
@ -749,6 +861,87 @@ public class ExpandRecurringBillingEventsActionTest {
assertCursorAt(currentTestTime); assertCursorAt(currentTestTime);
} }
@TestOfyAndSql
void testSuccess_premiumDomain_forAnchorTenant() throws Exception {
persistResource(
Registry.get("tld")
.asBuilder()
.setPremiumList(persistPremiumList("tld2", USD, "example,USD 100"))
.build());
recurring = persistResource(recurring.asBuilder().setRenewalPriceBehavior(NONPREMIUM).build());
action.cursorTimeParam = Optional.of(START_OF_TIME);
runAction();
DomainHistory persistedEntry =
getOnlyHistoryEntryOfType(domain, DOMAIN_AUTORENEW, DomainHistory.class);
assertHistoryEntryMatches(
domain, persistedEntry, "TheRegistrar", DateTime.parse("2000-02-19T00:00:00Z"), true);
BillingEvent.OneTime expected =
defaultOneTimeBuilder().setParent(persistedEntry).setCost(Money.of(USD, 11)).build();
assertBillingEventsForResource(domain, expected, recurring);
assertCursorAt(currentTestTime);
}
@TestOfyAndSql
void testSuccess_standardDomain_forAnchorTenant() throws Exception {
recurring = persistResource(recurring.asBuilder().setRenewalPriceBehavior(NONPREMIUM).build());
action.cursorTimeParam = Optional.of(START_OF_TIME);
runAction();
DomainHistory persistedEntry =
getOnlyHistoryEntryOfType(domain, DOMAIN_AUTORENEW, DomainHistory.class);
assertHistoryEntryMatches(
domain, persistedEntry, "TheRegistrar", DateTime.parse("2000-02-19T00:00:00Z"), true);
BillingEvent.OneTime expected = defaultOneTimeBuilder().setParent(persistedEntry).build();
assertBillingEventsForResource(domain, expected, recurring);
assertCursorAt(currentTestTime);
}
@TestOfyAndSql
void testSuccess_premiumDomain_forInternalRegistration() throws Exception {
persistResource(
Registry.get("tld")
.asBuilder()
.setPremiumList(persistPremiumList("tld2", USD, "example,USD 100"))
.build());
recurring =
persistResource(
recurring
.asBuilder()
.setRenewalPriceBehavior(SPECIFIED)
.setRenewalPrice(Money.of(USD, 20))
.build());
action.cursorTimeParam = Optional.of(START_OF_TIME);
runAction();
DomainHistory persistedEntry =
getOnlyHistoryEntryOfType(domain, DOMAIN_AUTORENEW, DomainHistory.class);
assertHistoryEntryMatches(
domain, persistedEntry, "TheRegistrar", DateTime.parse("2000-02-19T00:00:00Z"), true);
BillingEvent.OneTime expected =
defaultOneTimeBuilder().setParent(persistedEntry).setCost(Money.of(USD, 20)).build();
assertBillingEventsForResource(domain, expected, recurring);
assertCursorAt(currentTestTime);
}
@TestOfyAndSql
void testSuccess_standardDomain_forInternalRegistration() throws Exception {
recurring =
persistResource(
recurring
.asBuilder()
.setRenewalPriceBehavior(SPECIFIED)
.setRenewalPrice(Money.of(USD, 2))
.build());
action.cursorTimeParam = Optional.of(START_OF_TIME);
runAction();
DomainHistory persistedEntry =
getOnlyHistoryEntryOfType(domain, DOMAIN_AUTORENEW, DomainHistory.class);
assertHistoryEntryMatches(
domain, persistedEntry, "TheRegistrar", DateTime.parse("2000-02-19T00:00:00Z"), true);
BillingEvent.OneTime expected =
defaultOneTimeBuilder().setParent(persistedEntry).setCost(Money.of(USD, 2)).build();
assertBillingEventsForResource(domain, expected, recurring);
assertCursorAt(currentTestTime);
}
@TestOfyAndSql @TestOfyAndSql
void testSuccess_varyingRenewPrices() throws Exception { void testSuccess_varyingRenewPrices() throws Exception {
clock.setTo(currentTestTime); clock.setTo(currentTestTime);
@ -793,6 +986,102 @@ public class ExpandRecurringBillingEventsActionTest {
assertCursorAt(currentTestTime); assertCursorAt(currentTestTime);
} }
@TestOfyAndSql
void testSuccess_varyingRenewPrices_anchorTenant() throws Exception {
clock.setTo(currentTestTime);
persistResource(
Registry.get("tld")
.asBuilder()
.setPremiumList(persistPremiumList("tld2", USD, "example,USD 100"))
.setRenewBillingCostTransitions(
ImmutableSortedMap.of(
START_OF_TIME,
Money.of(USD, 8),
DateTime.parse("2000-06-01T00:00:00Z"),
Money.of(USD, 10)))
.build());
clock.setTo(DateTime.parse("2001-10-02T00:00:00Z"));
recurring = persistResource(recurring.asBuilder().setRenewalPriceBehavior(NONPREMIUM).build());
action.cursorTimeParam = Optional.of(START_OF_TIME);
runAction();
List<DomainHistory> persistedEntries =
getHistoryEntriesOfType(domain, DOMAIN_AUTORENEW, DomainHistory.class);
assertThat(persistedEntries).hasSize(2);
DateTime eventDate = DateTime.parse("2000-01-05T00:00:00Z");
DateTime billingDate = DateTime.parse("2000-02-19T00:00:00Z");
assertHistoryEntryMatches(domain, persistedEntries.get(0), "TheRegistrar", billingDate, true);
BillingEvent.OneTime cheaper =
defaultOneTimeBuilder()
.setBillingTime(billingDate)
.setEventTime(eventDate)
.setParent(persistedEntries.get(0))
.setCost(Money.of(USD, 8))
.build();
assertHistoryEntryMatches(
domain, persistedEntries.get(1), "TheRegistrar", billingDate.plusYears(1), true);
BillingEvent.OneTime expensive =
cheaper
.asBuilder()
.setCost(Money.of(USD, 10))
.setBillingTime(billingDate.plusYears(1))
.setEventTime(eventDate.plusYears(1))
.setParent(persistedEntries.get(1))
.build();
assertBillingEventsForResource(domain, recurring, cheaper, expensive);
assertCursorAt(currentTestTime);
}
@TestOfyAndSql
void testSuccess_varyingRenewPrices_internalRegistration() throws Exception {
clock.setTo(currentTestTime);
persistResource(
Registry.get("tld")
.asBuilder()
.setPremiumList(persistPremiumList("tld2", USD, "example,USD 100"))
.setRenewBillingCostTransitions(
ImmutableSortedMap.of(
START_OF_TIME,
Money.of(USD, 8),
DateTime.parse("2000-06-01T00:00:00Z"),
Money.of(USD, 10)))
.build());
clock.setTo(DateTime.parse("2001-10-02T00:00:00Z"));
recurring =
persistResource(
recurring
.asBuilder()
.setRenewalPriceBehavior(SPECIFIED)
.setRenewalPrice(Money.of(USD, 5))
.build());
action.cursorTimeParam = Optional.of(START_OF_TIME);
runAction();
List<DomainHistory> persistedEntries =
getHistoryEntriesOfType(domain, DOMAIN_AUTORENEW, DomainHistory.class);
assertThat(persistedEntries).hasSize(2);
DateTime eventDate = DateTime.parse("2000-01-05T00:00:00Z");
DateTime billingDate = DateTime.parse("2000-02-19T00:00:00Z");
assertHistoryEntryMatches(domain, persistedEntries.get(0), "TheRegistrar", billingDate, true);
BillingEvent.OneTime cheaper =
defaultOneTimeBuilder()
.setBillingTime(billingDate)
.setEventTime(eventDate)
.setParent(persistedEntries.get(0))
.setCost(Money.of(USD, 5))
.build();
assertHistoryEntryMatches(
domain, persistedEntries.get(1), "TheRegistrar", billingDate.plusYears(1), true);
BillingEvent.OneTime expensive =
cheaper
.asBuilder()
.setCost(Money.of(USD, 5))
.setBillingTime(billingDate.plusYears(1))
.setEventTime(eventDate.plusYears(1))
.setParent(persistedEntries.get(1))
.build();
assertBillingEventsForResource(domain, recurring, cheaper, expensive);
assertCursorAt(currentTestTime);
}
@TestOfyAndSql @TestOfyAndSql
void testFailure_cursorAfterExecutionTime() { void testFailure_cursorAfterExecutionTime() {
action.cursorTimeParam = Optional.of(clock.nowUtc().plusYears(1)); action.cursorTimeParam = Optional.of(clock.nowUtc().plusYears(1));

View file

@ -64,7 +64,7 @@ import org.mockito.Mock;
/** Unit tests for {@link DomainPricingLogic}. */ /** Unit tests for {@link DomainPricingLogic}. */
@DualDatabaseTest @DualDatabaseTest
public class DomainPricingLogicTest { public class DomainPricingLogicTest {
DomainPricingLogic domainPricingLogic = new DomainPricingLogic(); DomainPricingLogic domainPricingLogic;
@RegisterExtension @RegisterExtension
public final AppEngineExtension appEngine = public final AppEngineExtension appEngine =
@ -81,8 +81,9 @@ public class DomainPricingLogicTest {
void beforeEach() throws Exception { void beforeEach() throws Exception {
createTld("example"); createTld("example");
sessionMetadata = new HttpSessionMetadata(new FakeHttpSession()); sessionMetadata = new HttpSessionMetadata(new FakeHttpSession());
domainPricingLogic.customLogic = domainPricingLogic =
new DomainPricingCustomLogic(eppInput, sessionMetadata, flowMetadata); new DomainPricingLogic(
new DomainPricingCustomLogic(eppInput, sessionMetadata, flowMetadata));
registry = registry =
persistResource( persistResource(
Registry.get("example") Registry.get("example")