1
0
mirror of https://github.com/google/nomulus synced 2026-01-03 03:35:42 +00:00

Change billing for multi-year domain creation (#2446)

* Change billing for multi-year domain creation

From the second year on, charge the renewal price.

See b/322833077
This commit is contained in:
Weimin Yu
2024-05-29 13:19:54 -04:00
committed by GitHub
parent 589041b3ed
commit b3e67e58b5
16 changed files with 87 additions and 55 deletions

View File

@@ -159,7 +159,11 @@ public final class DomainPricingLogic {
case NONPREMIUM -> {
renewCost =
getDomainCostWithDiscount(
false, years, allocationToken, tld.getStandardRenewCost(dateTime));
false,
years,
allocationToken,
tld.getStandardRenewCost(dateTime),
Optional.empty());
isRenewCostPremiumPrice = false;
}
default ->
@@ -252,7 +256,11 @@ public final class DomainPricingLogic {
DomainPrices domainPrices, int years, Optional<AllocationToken> allocationToken)
throws EppException {
return getDomainCostWithDiscount(
domainPrices.isPremium(), years, allocationToken, domainPrices.getCreateCost());
domainPrices.isPremium(),
years,
allocationToken,
domainPrices.getCreateCost(),
Optional.of(domainPrices.getRenewCost()));
}
/** Returns the domain renew cost with allocation-token-related discounts applied. */
@@ -272,24 +280,45 @@ public final class DomainPricingLogic {
}
}
return getDomainCostWithDiscount(
domainPrices.isPremium(), years, allocationToken, domainPrices.getRenewCost());
domainPrices.isPremium(),
years,
allocationToken,
domainPrices.getRenewCost(),
Optional.empty());
}
/**
* Returns the domain creation or renewal cost for the given number of {@code years}.
*
* <p>For domain creation, {@code firstYearCost} is the creation cost while {@code
* subsequentYearCost} is the single-year renewal cost (which is guaranteed to be present).
*
* <p>For domain renewal, {@code firstYearCost} is the single-year renewal cost and {@code
* subsequentYearCost} should be empty.
*/
private Money getDomainCostWithDiscount(
boolean isPremium, int years, Optional<AllocationToken> allocationToken, Money oneYearCost)
boolean isPremium,
int years,
Optional<AllocationToken> allocationToken,
Money firstYearCost,
Optional<Money> subsequentYearCost)
throws AllocationTokenInvalidForPremiumNameException {
checkArgument(years > 0, "Registration years to get cost for must be positive.");
validateTokenForPossiblePremiumName(allocationToken, isPremium);
Money totalDomainFlowCost = oneYearCost.multipliedBy(years);
Money totalDomainFlowCost =
firstYearCost.plus(subsequentYearCost.orElse(firstYearCost).multipliedBy(years - 1));
// Apply the allocation token discount, if applicable.
if (allocationToken.isPresent()
&& allocationToken.get().getTokenBehavior().equals(TokenBehavior.DEFAULT)) {
int discountedYears = Math.min(years, allocationToken.get().getDiscountYears());
Money discount =
oneYearCost.multipliedBy(
discountedYears * allocationToken.get().getDiscountFraction(),
RoundingMode.HALF_EVEN);
totalDomainFlowCost = totalDomainFlowCost.minus(discount);
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;
}

View File

@@ -297,7 +297,7 @@ public class EppTestCase {
.setReason(Reason.CREATE)
.setTargetId(domain.getDomainName())
.setRegistrarId(domain.getCurrentSponsorRegistrarId())
.setCost(Money.parse("USD 26.00"))
.setCost(Money.parse("USD 24.00"))
.setPeriodYears(2)
.setEventTime(createTime)
.setBillingTime(createTime.plus(Tld.get(domain.getTld()).getAddGracePeriodLength()))

View File

@@ -514,14 +514,17 @@ class DomainCheckFlowTest extends ResourceCheckFlowTestCase<DomainCheckFlow, Dom
.build());
setEppInput(
"domain_check_allocationtoken_promotion.xml", ImmutableMap.of("DOMAIN", "single.tld"));
// 1-yr: 13 * .556
// 2-yr: (13 + 11) * .556
// 5-yr: 2-yr-cost + 3 * 11
runFlowAssertResponse(
loadFile(
"domain_check_allocationtoken_promotion_response.xml",
new ImmutableMap.Builder<String, String>()
.put("DOMAIN", "single.tld")
.put("COST_1YR", "7.23")
.put("COST_2YR", "14.46")
.put("COST_5YR", "53.46")
.put("COST_2YR", "13.34")
.put("COST_5YR", "46.34")
.put("FEE_CLASS", "")
.build()));
}

View File

@@ -295,7 +295,7 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
BigDecimal createCost =
isDomainPremium(getUniqueIdFromCommand(), clock.nowUtc())
? BigDecimal.valueOf(200)
: BigDecimal.valueOf(26);
: BigDecimal.valueOf(24);
if (isAnchorTenant) {
createCost = BigDecimal.ZERO;
}
@@ -687,7 +687,7 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
doSuccessfulTest(
"tld",
"domain_create_response_fee.xml",
ImmutableMap.of("FEE_VERSION", "0.6", "FEE", "26.00"));
ImmutableMap.of("FEE_VERSION", "0.6", "FEE", "24.00"));
}
@Test
@@ -697,7 +697,7 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
doSuccessfulTest(
"tld",
"domain_create_response_fee.xml",
ImmutableMap.of("FEE_VERSION", "0.11", "FEE", "26.00"));
ImmutableMap.of("FEE_VERSION", "0.11", "FEE", "24.00"));
}
@Test
@@ -707,7 +707,7 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
doSuccessfulTest(
"tld",
"domain_create_response_fee.xml",
ImmutableMap.of("FEE_VERSION", "0.12", "FEE", "26.00"));
ImmutableMap.of("FEE_VERSION", "0.12", "FEE", "24.00"));
}
@Test
@@ -717,7 +717,7 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
doSuccessfulTest(
"tld",
"domain_create_response_fee.xml",
ImmutableMap.of("FEE_VERSION", "0.6", "FEE", "26.00"));
ImmutableMap.of("FEE_VERSION", "0.6", "FEE", "24.00"));
}
@Test
@@ -727,7 +727,7 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
doSuccessfulTest(
"tld",
"domain_create_response_fee.xml",
ImmutableMap.of("FEE_VERSION", "0.11", "FEE", "26.00"));
ImmutableMap.of("FEE_VERSION", "0.11", "FEE", "24.00"));
}
@Test
@@ -737,7 +737,7 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
doSuccessfulTest(
"tld",
"domain_create_response_fee.xml",
ImmutableMap.of("FEE_VERSION", "0.12", "FEE", "26.00"));
ImmutableMap.of("FEE_VERSION", "0.12", "FEE", "24.00"));
}
@Test
@@ -1114,14 +1114,14 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
.setDefaultPromoTokens(ImmutableList.of(defaultToken.createVKey()))
.setCreateBillingCostTransitions(ImmutableSortedMap.of(START_OF_TIME, Money.of(USD, 8)))
.build());
// Expects fee of $26
// Expects fee of $24
setEppInput("domain_create_fee.xml", ImmutableMap.of("FEE_VERSION", "0.6", "CURRENCY", "USD"));
persistContactsAndHosts();
// $12 is equal to 50% off the first year registration and 0% 0ff the 2nd year
// $15 is 50% off the first year registration ($8) and 0% 0ff the 2nd year (renewal at $11)
runFlowAssertResponse(
loadFile(
"domain_create_response_fee.xml",
ImmutableMap.of("FEE_VERSION", "0.6", "FEE", "12.00")));
ImmutableMap.of("FEE_VERSION", "0.6", "FEE", "15.00")));
}
@Test
@@ -1142,7 +1142,7 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
.setCreateBillingCostTransitions(
ImmutableSortedMap.of(START_OF_TIME, Money.of(USD, 100)))
.build());
// Expects fee of $26
// Expects fee of $24
setEppInput("domain_create_fee.xml", ImmutableMap.of("FEE_VERSION", "0.6", "CURRENCY", "USD"));
persistContactsAndHosts();
EppException thrown = assertThrows(FeesMismatchException.class, this::runFlow);
@@ -1180,14 +1180,14 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
.setDefaultPromoTokens(ImmutableList.of(defaultToken.createVKey()))
.setCreateBillingCostTransitions(ImmutableSortedMap.of(START_OF_TIME, Money.of(USD, 8)))
.build());
// Expects fee of $26
// Expects fee of $24
setEppInput("domain_create_fee.xml", ImmutableMap.of("FEE_VERSION", "0.11", "CURRENCY", "USD"));
persistContactsAndHosts();
// $12 is equal to 50% off the first year registration and 0% 0ff the 2nd year
runFlowAssertResponse(
loadFile(
"domain_create_response_fee.xml",
ImmutableMap.of("FEE_VERSION", "0.11", "FEE", "12.00")));
ImmutableMap.of("FEE_VERSION", "0.11", "FEE", "15.00")));
}
@Test
@@ -1208,7 +1208,7 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
.setCreateBillingCostTransitions(
ImmutableSortedMap.of(START_OF_TIME, Money.of(USD, 100)))
.build());
// Expects fee of $26
// Expects fee of $24
setEppInput("domain_create_fee.xml", ImmutableMap.of("FEE_VERSION", "0.11", "CURRENCY", "USD"));
persistContactsAndHosts();
EppException thrown = assertThrows(FeesMismatchException.class, this::runFlow);
@@ -1246,14 +1246,14 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
.setDefaultPromoTokens(ImmutableList.of(defaultToken.createVKey()))
.setCreateBillingCostTransitions(ImmutableSortedMap.of(START_OF_TIME, Money.of(USD, 8)))
.build());
// Expects fee of $26
// Expects fee of $24
setEppInput("domain_create_fee.xml", ImmutableMap.of("FEE_VERSION", "0.12", "CURRENCY", "USD"));
persistContactsAndHosts();
// $12 is equal to 50% off the first year registration and 0% 0ff the 2nd year
runFlowAssertResponse(
loadFile(
"domain_create_response_fee.xml",
ImmutableMap.of("FEE_VERSION", "0.12", "FEE", "12.00")));
ImmutableMap.of("FEE_VERSION", "0.12", "FEE", "15.00")));
}
@Test
@@ -1274,7 +1274,7 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
.setCreateBillingCostTransitions(
ImmutableSortedMap.of(START_OF_TIME, Money.of(USD, 100)))
.build());
// Expects fee of $26
// Expects fee of $24
setEppInput("domain_create_fee.xml", ImmutableMap.of("FEE_VERSION", "0.12", "CURRENCY", "USD"));
persistContactsAndHosts();
EppException thrown = assertThrows(FeesMismatchException.class, this::runFlow);
@@ -1615,20 +1615,20 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
BillingEvent billingEvent =
Iterables.getOnlyElement(DatabaseHelper.loadAllOf(BillingEvent.class));
assertThat(billingEvent.getTargetId()).isEqualTo("example.tld");
assertThat(billingEvent.getCost()).isEqualTo(Money.of(USD, BigDecimal.valueOf(19.5)));
assertThat(billingEvent.getCost()).isEqualTo(Money.of(USD, BigDecimal.valueOf(17.5)));
}
@Test
void testSuccess_allocationToken_multiYearDiscount_maxesAtTokenDiscountYears() throws Exception {
// 2yrs @ $13 + 3yrs @ $13 * (1 - 0.73) = $36.53
runTest_allocationToken_multiYearDiscount(false, 0.73, 3, Money.of(USD, 36.53));
// ($13 + $11 + $11) * (1 - 0.73) + 2 * $11 =
runTest_allocationToken_multiYearDiscount(false, 0.73, 3, Money.of(USD, 31.45));
}
@Test
void testSuccess_allocationToken_multiYearDiscount_maxesAtNumRegistrationYears()
throws Exception {
// 5yrs @ $13 * (1 - 0.276) = $47.06
runTest_allocationToken_multiYearDiscount(false, 0.276, 10, Money.of(USD, 47.06));
// ($13 + 4 * $11) * (1 - 0.276) = $41.27
runTest_allocationToken_multiYearDiscount(false, 0.276, 10, Money.of(USD, 41.27));
}
void runTest_allocationToken_multiYearDiscount(
@@ -1900,7 +1900,7 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
BillingEvent billingEvent =
Iterables.getOnlyElement(DatabaseHelper.loadAllOf(BillingEvent.class));
assertThat(billingEvent.getTargetId()).isEqualTo("example.tld");
assertThat(billingEvent.getCost()).isEqualTo(Money.of(USD, BigDecimal.valueOf(19.5)));
assertThat(billingEvent.getCost()).isEqualTo(Money.of(USD, BigDecimal.valueOf(17.5)));
assertThat(billingEvent.getAllocationToken().get().getKey()).isEqualTo("abc123");
}
@@ -2014,7 +2014,7 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
ImmutableList.of(defaultToken1.createVKey(), defaultToken2.createVKey()))
.build());
BillingEvent billingEvent = runTest_defaultToken("bbbbb");
assertThat(billingEvent.getCost()).isEqualTo(Money.of(USD, BigDecimal.valueOf(19.5)));
assertThat(billingEvent.getCost()).isEqualTo(Money.of(USD, BigDecimal.valueOf(17.5)));
}
@Test
@@ -2045,7 +2045,7 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
.build());
DatabaseHelper.deleteResource(defaultToken1);
BillingEvent billingEvent = runTest_defaultToken("bbbbb");
assertThat(billingEvent.getCost()).isEqualTo(Money.of(USD, BigDecimal.valueOf(19.5)));
assertThat(billingEvent.getCost()).isEqualTo(Money.of(USD, BigDecimal.valueOf(17.5)));
}
@Test
@@ -3102,7 +3102,7 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
new ImmutableMap.Builder<String, String>()
.put("FEE_VERSION", "0.6")
.put("DESCRIPTION_1", "create")
.put("FEE_1", "26")
.put("FEE_1", "24")
.put("DESCRIPTION_2", "Early Access Period")
.put("FEE_2", "100")
.put("DESCRIPTION_3", "renew")
@@ -3111,7 +3111,7 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
persistContactsAndHosts();
setEapForTld("tld");
EppException thrown = assertThrows(FeesMismatchException.class, this::runFlow);
assertThat(thrown).hasMessageThat().contains("expected total of USD 126.00");
assertThat(thrown).hasMessageThat().contains("expected total of USD 124.00");
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@@ -3122,7 +3122,7 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
new ImmutableMap.Builder<String, String>()
.put("FEE_VERSION", "0.6")
.put("DESCRIPTION_1", "create")
.put("FEE_1", "26")
.put("FEE_1", "24")
.put("DESCRIPTION_2", "Early Access Period")
.put("FEE_2", "55")
.put("DESCRIPTION_3", "Early Access Period")
@@ -3142,7 +3142,7 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
new ImmutableMap.Builder<String, String>()
.put("FEE_VERSION", "0.6")
.put("DESCRIPTION_1", "create")
.put("FEE_1", "26")
.put("FEE_1", "24")
.put("DESCRIPTION_2", "Early Access Period")
.put("FEE_2", "55")
.put("DESCRIPTION_3", "Early Access Period")

View File

@@ -711,7 +711,7 @@ class DomainInfoFlowTest extends ResourceFlowTestCase<DomainInfoFlow, Domain> {
"COMMAND", "create",
"DESCRIPTION", "create",
"PERIOD", "2",
"FEE", "26.00"),
"FEE", "24.00"),
true);
}

View File

@@ -145,8 +145,8 @@ public class DomainPricingLogicTest {
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
// 13 * 2 * 0.85 == 22.1
.addFeeOrCredit(Fee.create(Money.of(USD, 22.1).getAmount(), CREATE, false))
// (13 + 11) * 0.85 == 20.40
.addFeeOrCredit(Fee.create(Money.of(USD, 20.4).getAmount(), CREATE, false))
.build());
}

View File

@@ -32,7 +32,7 @@
<fee:currency>USD</fee:currency>
<fee:command>create</fee:command>
<fee:period unit="y">2</fee:period>
<fee:fee description="create">26.00</fee:fee>
<fee:fee description="create">24.00</fee:fee>
<fee:fee description="Early Access Period, fee expires: 2010-01-02T10:00:00.002Z">100.00</fee:fee>
</fee:cd>
</fee:chkData>

View File

@@ -31,7 +31,7 @@
<fee:currency>USD</fee:currency>
<fee:command>create</fee:command>
<fee:period unit="y">2</fee:period>
<fee:fee description="create">19.50</fee:fee>
<fee:fee description="create">17.50</fee:fee>
</fee:cd>
</fee:chkData>
</extension>

View File

@@ -31,7 +31,7 @@
<fee:currency>USD</fee:currency>
<fee:command>create</fee:command>
<fee:period unit="y">2</fee:period>
<fee:fee description="create">26.00</fee:fee>
<fee:fee description="create">24.00</fee:fee>
</fee:cd>
</fee:chkData>
</extension>

View File

@@ -20,7 +20,7 @@
<extension>
<fee:create xmlns:fee="urn:ietf:params:xml:ns:fee-%FEE_VERSION%">
<fee:currency>USD</fee:currency>
<fee:fee description="%DESCRIPTION_1%">26.00</fee:fee>
<fee:fee description="%DESCRIPTION_1%">24.00</fee:fee>
<fee:fee description="%DESCRIPTION_2%">100.00</fee:fee>
</fee:create>
</extension>

View File

@@ -20,7 +20,7 @@
<extension>
<fee:create xmlns:fee="urn:ietf:params:xml:ns:fee-%FEE_VERSION%">
<fee:currency>%CURRENCY%</fee:currency>
<fee:fee>26.00</fee:fee>
<fee:fee>24.00</fee:fee>
</fee:create>
</extension>
<clTRID>ABC-12345</clTRID>

View File

@@ -20,7 +20,7 @@
<extension>
<fee:create xmlns:fee="urn:ietf:params:xml:ns:fee-%FEE_VERSION%">
<fee:currency>USD</fee:currency>
<fee:fee refundable="true" grace-period="P0D" applied="immediate">26.00</fee:fee>
<fee:fee refundable="true" grace-period="P0D" applied="immediate">24.00</fee:fee>
</fee:create>
</extension>
<clTRID>ABC-12345</clTRID>

View File

@@ -14,7 +14,7 @@
<extension>
<fee:creData xmlns:fee="urn:ietf:params:xml:ns:fee-%FEE_VERSION%">
<fee:currency>USD</fee:currency>
<fee:fee description="create">26.00</fee:fee>
<fee:fee description="create">24.00</fee:fee>
<fee:fee description="Early Access Period, fee expires: 1999-04-04T22:00:00.024Z">100.00</fee:fee>
</fee:creData>
</extension>

View File

@@ -15,7 +15,7 @@
<extension>
<fee:create xmlns:fee="urn:ietf:params:xml:ns:fee-0.6">
<fee:currency>USD</fee:currency>
<fee:fee description="create">26.00</fee:fee>
<fee:fee description="create">24.00</fee:fee>
<fee:fee description="Early Access Period">100.00</fee:fee>
</fee:create>
</extension>

View File

@@ -13,7 +13,7 @@
<extension>
<fee:creData xmlns:fee="urn:ietf:params:xml:ns:fee-0.6">
<fee:currency>USD</fee:currency>
<fee:fee description="create">26.00</fee:fee>
<fee:fee description="create">24.00</fee:fee>
<fee:fee description="Early Access Period, fee expires: 2000-06-02T00:00:00.000Z">100.00</fee:fee>
</fee:creData>
</extension>

View File

@@ -7,7 +7,7 @@
<fee:delData
xmlns:fee="urn:ietf:params:xml:ns:fee-0.6">
<fee:currency>USD</fee:currency>
<fee:credit description="addPeriod credit">-26.00</fee:credit>
<fee:credit description="addPeriod credit">-24.00</fee:credit>
</fee:delData>
</extension>
<trID>