mirror of
https://github.com/google/nomulus.git
synced 2025-04-30 03:57:51 +02:00
Add new columns to BillingEvent (#1573)
* Add new columns to BillingEvent.java * Improve PR and modifyJodaMoneyType to handle null currency in override * Add test cases for edge cases of nullSafeGet in JodaMoneyType * Improve assertions
This commit is contained in:
parent
430e136920
commit
f79e4740f6
6 changed files with 712 additions and 13 deletions
|
@ -126,7 +126,40 @@ public abstract class BillingEvent extends ImmutableObject
|
|||
* This flag will be added to any {@link OneTime} events that are created via, e.g., an
|
||||
* automated process to expand {@link Recurring} events.
|
||||
*/
|
||||
SYNTHETIC
|
||||
SYNTHETIC;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets of renewal price behaviors that can be applied to billing recurrences.
|
||||
*
|
||||
* <p>When a client renews a domain, they could be charged differently, depending on factors such
|
||||
* as the client type and the domain itself.
|
||||
*/
|
||||
public enum RenewalPriceBehavior {
|
||||
/**
|
||||
* This indicates the renewal price is the default price.
|
||||
*
|
||||
* <p>By default, if the domain is premium, then premium price will be used. Otherwise, the
|
||||
* standard price of the TLD will be used.
|
||||
*/
|
||||
DEFAULT,
|
||||
/**
|
||||
* This indicates the domain will be renewed at standard price even if it's a premium domain.
|
||||
*
|
||||
* <p>We chose to name this "NONPREMIUM" rather than simply "STANDARD" to avoid confusion
|
||||
* between "STANDARD" and "DEFAULT".
|
||||
*
|
||||
* <p>This price behavior is used with anchor tenants.
|
||||
*/
|
||||
NONPREMIUM,
|
||||
/**
|
||||
* This indicates that the renewalPrice in {@link BillingEvent.Recurring} will be used for
|
||||
* domain renewal.
|
||||
*
|
||||
* <p>The renewalPrice has a non-null value iff the price behavior is set to "SPECIFIED". This
|
||||
* behavior is used with internal registrations.
|
||||
*/
|
||||
SPECIFIED;
|
||||
}
|
||||
|
||||
/** Entity id. */
|
||||
|
@ -555,6 +588,22 @@ public abstract class BillingEvent extends ImmutableObject
|
|||
})
|
||||
TimeOfYear recurrenceTimeOfYear;
|
||||
|
||||
/**
|
||||
* The renewal price for domain renewal if and only if it's specified.
|
||||
*
|
||||
* <p>This price column remains null except when the renewal price behavior of the billing is
|
||||
* SPECIFIED. This column is used for internal registrations.
|
||||
*/
|
||||
@Nullable
|
||||
@Type(type = JodaMoneyType.TYPE_NAME)
|
||||
@Columns(
|
||||
columns = {@Column(name = "renewalPriceAmount"), @Column(name = "renewalPriceCurrency")})
|
||||
Money renewalPrice;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "renewalPriceBehavior", nullable = false)
|
||||
RenewalPriceBehavior renewalPriceBehavior = RenewalPriceBehavior.DEFAULT;
|
||||
|
||||
public DateTime getRecurrenceEndTime() {
|
||||
return recurrenceEndTime;
|
||||
}
|
||||
|
@ -563,6 +612,14 @@ public abstract class BillingEvent extends ImmutableObject
|
|||
return recurrenceTimeOfYear;
|
||||
}
|
||||
|
||||
public RenewalPriceBehavior getRenewalPriceBehavior() {
|
||||
return renewalPriceBehavior;
|
||||
}
|
||||
|
||||
public Optional<Money> getRenewalPrice() {
|
||||
return Optional.ofNullable(renewalPrice);
|
||||
}
|
||||
|
||||
@Override
|
||||
public VKey<Recurring> createVKey() {
|
||||
return VKey.create(Recurring.class, getId(), Key.create(this));
|
||||
|
@ -591,11 +648,26 @@ public abstract class BillingEvent extends ImmutableObject
|
|||
return this;
|
||||
}
|
||||
|
||||
public Builder setRenewalPriceBehavior(RenewalPriceBehavior renewalPriceBehavior) {
|
||||
getInstance().renewalPriceBehavior = renewalPriceBehavior;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setRenewalPrice(@Nullable Money renewalPrice) {
|
||||
getInstance().renewalPrice = renewalPrice;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Recurring build() {
|
||||
Recurring instance = getInstance();
|
||||
checkNotNull(instance.eventTime);
|
||||
checkNotNull(instance.reason);
|
||||
checkArgument(
|
||||
(instance.renewalPriceBehavior == RenewalPriceBehavior.SPECIFIED)
|
||||
^ (instance.renewalPrice == null),
|
||||
"Renewal price can have a value if and only if the renewal price behavior is"
|
||||
+ " SPECIFIED");
|
||||
instance.recurrenceTimeOfYear = TimeOfYear.fromDateTime(instance.eventTime);
|
||||
instance.recurrenceEndTime =
|
||||
Optional.ofNullable(instance.recurrenceEndTime).orElse(END_OF_TIME);
|
||||
|
|
|
@ -21,6 +21,7 @@ import java.sql.PreparedStatement;
|
|||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Objects;
|
||||
import javax.annotation.Nullable;
|
||||
import org.hibernate.HibernateException;
|
||||
import org.hibernate.engine.spi.SharedSessionContractImplementor;
|
||||
import org.hibernate.type.StandardBasicTypes;
|
||||
|
@ -116,20 +117,28 @@ public class JodaMoneyType implements CompositeUserType {
|
|||
return Objects.hashCode(x);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Object nullSafeGet(
|
||||
ResultSet rs, String[] names, SharedSessionContractImplementor session, Object owner)
|
||||
throws HibernateException, SQLException {
|
||||
BigDecimal amount = StandardBasicTypes.BIG_DECIMAL.nullSafeGet(rs, names[AMOUNT_ID], session);
|
||||
CurrencyUnit currencyUnit =
|
||||
CurrencyUnit.of(StandardBasicTypes.STRING.nullSafeGet(rs, names[CURRENCY_ID], session));
|
||||
if (amount != null && currencyUnit != null) {
|
||||
return Money.of(currencyUnit, amount.stripTrailingZeros());
|
||||
}
|
||||
if (amount == null && currencyUnit == null) {
|
||||
String currencyUnitString =
|
||||
StandardBasicTypes.STRING.nullSafeGet(rs, names[CURRENCY_ID], session);
|
||||
// It is allowable for a Money object to be null, but only if both the currency unit and the
|
||||
// amount are null
|
||||
if (amount == null && currencyUnitString == null) {
|
||||
return null;
|
||||
} else if (amount != null && currencyUnitString != null) {
|
||||
// CurrencyUnit.of() throws an IllegalCurrencyException for unknown currency, which means the
|
||||
// currency is valid if it returns a value
|
||||
return Money.of(CurrencyUnit.of(currencyUnitString), amount.stripTrailingZeros());
|
||||
} else {
|
||||
throw new HibernateException(
|
||||
String.format(
|
||||
"Mismatching null state between currency '%s' and amount '%s'",
|
||||
currencyUnitString, amount));
|
||||
}
|
||||
throw new HibernateException("Mismatching null state between currency and amount.");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -140,7 +149,7 @@ public class JodaMoneyType implements CompositeUserType {
|
|||
String currencyUnit = value == null ? null : ((Money) value).getCurrencyUnit().getCode();
|
||||
|
||||
if ((amount == null && currencyUnit != null) || (amount != null && currencyUnit == null)) {
|
||||
throw new HibernateException("Mismatching null state between currency and amount.");
|
||||
throw new HibernateException("Mismatching null state between currency and amount");
|
||||
}
|
||||
StandardBasicTypes.BIG_DECIMAL.nullSafeSet(st, amount, index, session);
|
||||
StandardBasicTypes.STRING.nullSafeSet(st, currencyUnit, index + 1, session);
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
package google.registry.model.billing;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static com.google.common.truth.Truth8.assertThat;
|
||||
import static google.registry.model.domain.token.AllocationToken.TokenType.UNLIMITED_USE;
|
||||
import static google.registry.model.ofy.ObjectifyService.auditedOfy;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
|
@ -36,6 +37,7 @@ import com.googlecode.objectify.Key;
|
|||
import google.registry.model.EntityTestCase;
|
||||
import google.registry.model.billing.BillingEvent.Flag;
|
||||
import google.registry.model.billing.BillingEvent.Reason;
|
||||
import google.registry.model.billing.BillingEvent.RenewalPriceBehavior;
|
||||
import google.registry.model.domain.DomainBase;
|
||||
import google.registry.model.domain.DomainHistory;
|
||||
import google.registry.model.domain.GracePeriod;
|
||||
|
@ -49,6 +51,7 @@ import google.registry.testing.TestOfyAndSql;
|
|||
import google.registry.testing.TestOfyOnly;
|
||||
import google.registry.testing.TestSqlOnly;
|
||||
import google.registry.util.DateTimeUtils;
|
||||
import java.math.BigDecimal;
|
||||
import org.joda.money.Money;
|
||||
import org.joda.time.DateTime;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
|
@ -474,4 +477,487 @@ public class BillingEventTest extends EntityTestCase {
|
|||
.setParent(domainHistory)
|
||||
.build();
|
||||
}
|
||||
|
||||
@TestOfyAndSql
|
||||
void testSuccess_defaultRenewalPriceBehavior_assertsIsDefault() {
|
||||
assertThat(recurring.getRenewalPriceBehavior()).isEqualTo(RenewalPriceBehavior.DEFAULT);
|
||||
assertThat(recurring.getRenewalPrice()).isEmpty();
|
||||
}
|
||||
|
||||
@TestOfyAndSql
|
||||
void testSuccess_getRenewalPriceBehavior_returnsRightBehavior() {
|
||||
BillingEvent.Recurring recurringEvent =
|
||||
persistResource(
|
||||
commonInit(
|
||||
new BillingEvent.Recurring.Builder()
|
||||
.setParent(domainHistory)
|
||||
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
|
||||
.setReason(Reason.RENEW)
|
||||
.setEventTime(now.plusYears(1))
|
||||
.setRenewalPriceBehavior(RenewalPriceBehavior.NONPREMIUM)
|
||||
.setRecurrenceEndTime(END_OF_TIME)));
|
||||
assertThat(recurringEvent.getRenewalPriceBehavior()).isEqualTo(RenewalPriceBehavior.NONPREMIUM);
|
||||
assertThat(recurringEvent.getRenewalPrice()).isEmpty();
|
||||
}
|
||||
|
||||
@TestOfyAndSql
|
||||
void testSuccess_setRenewalPriceBehaviorThenBuild_defaultToSpecified() {
|
||||
BillingEvent.Recurring recurringEvent =
|
||||
persistResource(
|
||||
commonInit(
|
||||
new BillingEvent.Recurring.Builder()
|
||||
.setParent(domainHistory)
|
||||
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
|
||||
.setReason(Reason.RENEW)
|
||||
.setEventTime(now.plusYears(1))
|
||||
.setRenewalPriceBehavior(RenewalPriceBehavior.DEFAULT)
|
||||
.setRecurrenceEndTime(END_OF_TIME)));
|
||||
assertThat(recurringEvent.getRenewalPriceBehavior()).isEqualTo(RenewalPriceBehavior.DEFAULT);
|
||||
assertThat(recurringEvent.getRenewalPrice()).isEmpty();
|
||||
BillingEvent.Recurring loadedEntity = loadByEntity(recurringEvent);
|
||||
assertThat(loadedEntity).isEqualTo(recurringEvent);
|
||||
persistResource(
|
||||
loadedEntity
|
||||
.asBuilder()
|
||||
.setRenewalPrice(Money.of(USD, 100))
|
||||
.setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED)
|
||||
.build());
|
||||
assertThat(loadByEntity(recurringEvent).getRenewalPriceBehavior())
|
||||
.isEqualTo(RenewalPriceBehavior.SPECIFIED);
|
||||
assertThat(loadByEntity(recurringEvent).getRenewalPrice()).hasValue(Money.of(USD, 100));
|
||||
}
|
||||
|
||||
@TestOfyAndSql
|
||||
void testSuccess_setRenewalPriceBehaviorThenBuild_defaultToNonPremium() {
|
||||
BillingEvent.Recurring recurringEvent =
|
||||
persistResource(
|
||||
commonInit(
|
||||
new BillingEvent.Recurring.Builder()
|
||||
.setParent(domainHistory)
|
||||
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
|
||||
.setReason(Reason.RENEW)
|
||||
.setEventTime(now.plusYears(1))
|
||||
.setRenewalPriceBehavior(RenewalPriceBehavior.DEFAULT)
|
||||
.setRecurrenceEndTime(END_OF_TIME)));
|
||||
assertThat(recurringEvent.getRenewalPriceBehavior()).isEqualTo(RenewalPriceBehavior.DEFAULT);
|
||||
assertThat(recurringEvent.getRenewalPrice()).isEmpty();
|
||||
BillingEvent.Recurring loadedEntity = loadByEntity(recurringEvent);
|
||||
assertThat(loadedEntity).isEqualTo(recurringEvent);
|
||||
persistResource(
|
||||
loadedEntity.asBuilder().setRenewalPriceBehavior(RenewalPriceBehavior.NONPREMIUM).build());
|
||||
assertThat(loadByEntity(recurringEvent).getRenewalPriceBehavior())
|
||||
.isEqualTo(RenewalPriceBehavior.NONPREMIUM);
|
||||
assertThat(loadByEntity(recurringEvent).getRenewalPrice()).isEmpty();
|
||||
}
|
||||
|
||||
@TestOfyAndSql
|
||||
void testSuccess_setRenewalPriceBehaviorThenBuild_nonPremiumToSpecified() {
|
||||
BillingEvent.Recurring recurringEvent =
|
||||
persistResource(
|
||||
commonInit(
|
||||
new BillingEvent.Recurring.Builder()
|
||||
.setParent(domainHistory)
|
||||
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
|
||||
.setReason(Reason.RENEW)
|
||||
.setEventTime(now.plusYears(1))
|
||||
.setRenewalPriceBehavior(RenewalPriceBehavior.NONPREMIUM)
|
||||
.setRecurrenceEndTime(END_OF_TIME)));
|
||||
assertThat(recurringEvent.getRenewalPriceBehavior()).isEqualTo(RenewalPriceBehavior.NONPREMIUM);
|
||||
assertThat(recurringEvent.getRenewalPrice()).isEmpty();
|
||||
BillingEvent.Recurring loadedEntity = loadByEntity(recurringEvent);
|
||||
assertThat(loadedEntity).isEqualTo(recurringEvent);
|
||||
persistResource(
|
||||
loadedEntity
|
||||
.asBuilder()
|
||||
.setRenewalPrice(Money.of(USD, 100))
|
||||
.setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED)
|
||||
.build());
|
||||
assertThat(loadByEntity(recurringEvent).getRenewalPriceBehavior())
|
||||
.isEqualTo(RenewalPriceBehavior.SPECIFIED);
|
||||
assertThat(loadByEntity(recurringEvent).getRenewalPrice()).hasValue(Money.of(USD, 100));
|
||||
}
|
||||
|
||||
@TestOfyAndSql
|
||||
void testSuccess_setRenewalPriceBehaviorThenBuild_nonPremiumToDefault() {
|
||||
BillingEvent.Recurring recurringEvent =
|
||||
persistResource(
|
||||
commonInit(
|
||||
new BillingEvent.Recurring.Builder()
|
||||
.setParent(domainHistory)
|
||||
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
|
||||
.setReason(Reason.RENEW)
|
||||
.setEventTime(now.plusYears(1))
|
||||
.setRenewalPriceBehavior(RenewalPriceBehavior.NONPREMIUM)
|
||||
.setRecurrenceEndTime(END_OF_TIME)));
|
||||
assertThat(recurringEvent.getRenewalPriceBehavior()).isEqualTo(RenewalPriceBehavior.NONPREMIUM);
|
||||
assertThat(recurringEvent.getRenewalPrice()).isEmpty();
|
||||
BillingEvent.Recurring loadedEntity = loadByEntity(recurringEvent);
|
||||
assertThat(loadedEntity).isEqualTo(recurringEvent);
|
||||
persistResource(
|
||||
loadedEntity.asBuilder().setRenewalPriceBehavior(RenewalPriceBehavior.DEFAULT).build());
|
||||
assertThat(loadByEntity(recurringEvent).getRenewalPriceBehavior())
|
||||
.isEqualTo(RenewalPriceBehavior.DEFAULT);
|
||||
assertThat(loadByEntity(recurringEvent).getRenewalPrice()).isEmpty();
|
||||
}
|
||||
|
||||
@TestOfyAndSql
|
||||
void testSuccess_setRenewalPriceBehaviorThenBuild_specifiedToDefault() {
|
||||
BillingEvent.Recurring recurringEvent =
|
||||
persistResource(
|
||||
commonInit(
|
||||
new BillingEvent.Recurring.Builder()
|
||||
.setParent(domainHistory)
|
||||
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
|
||||
.setReason(Reason.RENEW)
|
||||
.setEventTime(now.plusYears(1))
|
||||
.setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED)
|
||||
.setRenewalPrice(Money.of(USD, 100))
|
||||
.setRecurrenceEndTime(END_OF_TIME)));
|
||||
assertThat(recurringEvent.getRenewalPriceBehavior()).isEqualTo(RenewalPriceBehavior.SPECIFIED);
|
||||
assertThat(recurringEvent.getRenewalPrice()).hasValue(Money.of(USD, 100));
|
||||
BillingEvent.Recurring loadedEntity = loadByEntity(recurringEvent);
|
||||
assertThat(loadedEntity).isEqualTo(recurringEvent);
|
||||
persistResource(
|
||||
loadedEntity
|
||||
.asBuilder()
|
||||
.setRenewalPrice(null)
|
||||
.setRenewalPriceBehavior(RenewalPriceBehavior.DEFAULT)
|
||||
.build());
|
||||
assertThat(loadByEntity(recurringEvent).getRenewalPriceBehavior())
|
||||
.isEqualTo(RenewalPriceBehavior.DEFAULT);
|
||||
assertThat(loadByEntity(recurringEvent).getRenewalPrice()).isEmpty();
|
||||
}
|
||||
|
||||
@TestOfyAndSql
|
||||
void testSuccess_setRenewalPriceBehaviorThenBuild_specifiedToNonPremium() {
|
||||
BillingEvent.Recurring recurringEvent =
|
||||
persistResource(
|
||||
commonInit(
|
||||
new BillingEvent.Recurring.Builder()
|
||||
.setParent(domainHistory)
|
||||
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
|
||||
.setReason(Reason.RENEW)
|
||||
.setEventTime(now.plusYears(1))
|
||||
.setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED)
|
||||
.setRenewalPrice(Money.of(USD, 100))
|
||||
.setRecurrenceEndTime(END_OF_TIME)));
|
||||
assertThat(recurringEvent.getRenewalPriceBehavior()).isEqualTo(RenewalPriceBehavior.SPECIFIED);
|
||||
assertThat(recurringEvent.getRenewalPrice()).hasValue(Money.of(USD, 100));
|
||||
BillingEvent.Recurring loadedEntity = loadByEntity(recurringEvent);
|
||||
assertThat(loadedEntity).isEqualTo(recurringEvent);
|
||||
persistResource(
|
||||
loadedEntity
|
||||
.asBuilder()
|
||||
.setRenewalPrice(null)
|
||||
.setRenewalPriceBehavior(RenewalPriceBehavior.NONPREMIUM)
|
||||
.build());
|
||||
assertThat(loadByEntity(recurringEvent).getRenewalPriceBehavior())
|
||||
.isEqualTo(RenewalPriceBehavior.NONPREMIUM);
|
||||
assertThat(loadByEntity(recurringEvent).getRenewalPrice()).isEmpty();
|
||||
}
|
||||
|
||||
@TestOfyAndSql
|
||||
void testFailure_setRenewalPriceBehaviorThenBuild_defaultToSpecified_needRenewalPrice() {
|
||||
BillingEvent.Recurring recurringEvent =
|
||||
persistResource(
|
||||
commonInit(
|
||||
new BillingEvent.Recurring.Builder()
|
||||
.setParent(domainHistory)
|
||||
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
|
||||
.setReason(Reason.RENEW)
|
||||
.setEventTime(now.plusYears(1))
|
||||
.setRenewalPriceBehavior(RenewalPriceBehavior.DEFAULT)
|
||||
.setRecurrenceEndTime(END_OF_TIME)));
|
||||
assertThat(recurringEvent.getRenewalPriceBehavior()).isEqualTo(RenewalPriceBehavior.DEFAULT);
|
||||
assertThat(recurringEvent.getRenewalPrice()).isEmpty();
|
||||
BillingEvent.Recurring loadedEntity = loadByEntity(recurringEvent);
|
||||
assertThat(loadedEntity).isEqualTo(recurringEvent);
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() ->
|
||||
loadedEntity
|
||||
.asBuilder()
|
||||
.setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED)
|
||||
.build());
|
||||
assertThat(thrown)
|
||||
.hasMessageThat()
|
||||
.isEqualTo(
|
||||
"Renewal price can have a value if and only if the "
|
||||
+ "renewal price behavior is SPECIFIED");
|
||||
}
|
||||
|
||||
@TestOfyAndSql
|
||||
void testFailure_setRenewalPriceBehaviorThenBuild_defaultToPremium_noNeedToAddRenewalPrice() {
|
||||
BillingEvent.Recurring recurringEvent =
|
||||
persistResource(
|
||||
commonInit(
|
||||
new BillingEvent.Recurring.Builder()
|
||||
.setParent(domainHistory)
|
||||
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
|
||||
.setReason(Reason.RENEW)
|
||||
.setEventTime(now.plusYears(1))
|
||||
.setRenewalPriceBehavior(RenewalPriceBehavior.DEFAULT)
|
||||
.setRecurrenceEndTime(END_OF_TIME)));
|
||||
assertThat(recurringEvent.getRenewalPriceBehavior()).isEqualTo(RenewalPriceBehavior.DEFAULT);
|
||||
assertThat(recurringEvent.getRenewalPrice()).isEmpty();
|
||||
BillingEvent.Recurring loadedEntity = loadByEntity(recurringEvent);
|
||||
assertThat(loadedEntity).isEqualTo(recurringEvent);
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() ->
|
||||
loadedEntity
|
||||
.asBuilder()
|
||||
.setRenewalPriceBehavior(RenewalPriceBehavior.NONPREMIUM)
|
||||
.setRenewalPrice(Money.of(USD, 100))
|
||||
.build());
|
||||
assertThat(thrown)
|
||||
.hasMessageThat()
|
||||
.isEqualTo(
|
||||
"Renewal price can have a value if and only if the "
|
||||
+ "renewal price behavior is SPECIFIED");
|
||||
}
|
||||
|
||||
@TestOfyAndSql
|
||||
void testFailure_setRenewalPriceBehaviorThenBuild_nonPremiumToDefault_noNeedToAddRenewalPrice() {
|
||||
BillingEvent.Recurring recurringEvent =
|
||||
persistResource(
|
||||
commonInit(
|
||||
new BillingEvent.Recurring.Builder()
|
||||
.setParent(domainHistory)
|
||||
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
|
||||
.setReason(Reason.RENEW)
|
||||
.setEventTime(now.plusYears(1))
|
||||
.setRenewalPriceBehavior(RenewalPriceBehavior.NONPREMIUM)
|
||||
.setRecurrenceEndTime(END_OF_TIME)));
|
||||
assertThat(recurringEvent.getRenewalPriceBehavior()).isEqualTo(RenewalPriceBehavior.NONPREMIUM);
|
||||
assertThat(recurringEvent.getRenewalPrice()).isEmpty();
|
||||
BillingEvent.Recurring loadedEntity = loadByEntity(recurringEvent);
|
||||
assertThat(loadedEntity).isEqualTo(recurringEvent);
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() ->
|
||||
loadedEntity
|
||||
.asBuilder()
|
||||
.setRenewalPriceBehavior(RenewalPriceBehavior.DEFAULT)
|
||||
.setRenewalPrice(Money.of(USD, 100))
|
||||
.build());
|
||||
assertThat(thrown)
|
||||
.hasMessageThat()
|
||||
.isEqualTo(
|
||||
"Renewal price can have a value if and only if the "
|
||||
+ "renewal price behavior is SPECIFIED");
|
||||
}
|
||||
|
||||
@TestOfyAndSql
|
||||
void testFailure_setRenewalPriceBehaviorThenBuild_nonPremiumToSpecified_needRenewalPrice() {
|
||||
BillingEvent.Recurring recurringEvent =
|
||||
persistResource(
|
||||
commonInit(
|
||||
new BillingEvent.Recurring.Builder()
|
||||
.setParent(domainHistory)
|
||||
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
|
||||
.setReason(Reason.RENEW)
|
||||
.setEventTime(now.plusYears(1))
|
||||
.setRenewalPriceBehavior(RenewalPriceBehavior.NONPREMIUM)
|
||||
.setRecurrenceEndTime(END_OF_TIME)));
|
||||
assertThat(recurringEvent.getRenewalPriceBehavior()).isEqualTo(RenewalPriceBehavior.NONPREMIUM);
|
||||
assertThat(recurringEvent.getRenewalPrice()).isEmpty();
|
||||
BillingEvent.Recurring loadedEntity = loadByEntity(recurringEvent);
|
||||
assertThat(loadedEntity).isEqualTo(recurringEvent);
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() ->
|
||||
loadedEntity
|
||||
.asBuilder()
|
||||
.setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED)
|
||||
.build());
|
||||
assertThat(thrown)
|
||||
.hasMessageThat()
|
||||
.isEqualTo(
|
||||
"Renewal price can have a value if and only if the "
|
||||
+ "renewal price behavior is SPECIFIED");
|
||||
}
|
||||
|
||||
@TestOfyAndSql
|
||||
void testFailure_setRenewalPriceBehaviorThenBuild_specifiedToNonPremium_removeRenewalPrice() {
|
||||
BillingEvent.Recurring recurringEvent =
|
||||
persistResource(
|
||||
commonInit(
|
||||
new BillingEvent.Recurring.Builder()
|
||||
.setParent(domainHistory)
|
||||
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
|
||||
.setReason(Reason.RENEW)
|
||||
.setEventTime(now.plusYears(1))
|
||||
.setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED)
|
||||
.setRenewalPrice(Money.of(USD, 100))
|
||||
.setRecurrenceEndTime(END_OF_TIME)));
|
||||
assertThat(recurringEvent.getRenewalPriceBehavior()).isEqualTo(RenewalPriceBehavior.SPECIFIED);
|
||||
assertThat(recurringEvent.getRenewalPrice()).hasValue(Money.of(USD, 100));
|
||||
BillingEvent.Recurring loadedEntity = loadByEntity(recurringEvent);
|
||||
assertThat(loadedEntity).isEqualTo(recurringEvent);
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() ->
|
||||
loadedEntity
|
||||
.asBuilder()
|
||||
.setRenewalPriceBehavior(RenewalPriceBehavior.NONPREMIUM)
|
||||
.build());
|
||||
assertThat(thrown)
|
||||
.hasMessageThat()
|
||||
.isEqualTo(
|
||||
"Renewal price can have a value if and only if the "
|
||||
+ "renewal price behavior is SPECIFIED");
|
||||
}
|
||||
|
||||
@TestOfyAndSql
|
||||
void testFailure_setRenewalPriceBehaviorThenBuild_specifiedToDefault_removeRenewalPrice() {
|
||||
BillingEvent.Recurring recurringEvent =
|
||||
persistResource(
|
||||
commonInit(
|
||||
new BillingEvent.Recurring.Builder()
|
||||
.setParent(domainHistory)
|
||||
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
|
||||
.setReason(Reason.RENEW)
|
||||
.setEventTime(now.plusYears(1))
|
||||
.setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED)
|
||||
.setRenewalPrice(Money.of(USD, 100))
|
||||
.setRecurrenceEndTime(END_OF_TIME)));
|
||||
assertThat(recurringEvent.getRenewalPriceBehavior()).isEqualTo(RenewalPriceBehavior.SPECIFIED);
|
||||
assertThat(recurringEvent.getRenewalPrice()).hasValue(Money.of(USD, 100));
|
||||
BillingEvent.Recurring loadedEntity = loadByEntity(recurringEvent);
|
||||
assertThat(loadedEntity).isEqualTo(recurringEvent);
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() ->
|
||||
loadedEntity
|
||||
.asBuilder()
|
||||
.setRenewalPriceBehavior(RenewalPriceBehavior.DEFAULT)
|
||||
.build());
|
||||
assertThat(thrown)
|
||||
.hasMessageThat()
|
||||
.isEqualTo(
|
||||
"Renewal price can have a value if and only if the "
|
||||
+ "renewal price behavior is SPECIFIED");
|
||||
}
|
||||
|
||||
@TestOfyAndSql
|
||||
void testSuccess_buildWithDefaultRenewalBehavior() {
|
||||
BillingEvent.Recurring recurringEvent =
|
||||
persistResource(
|
||||
commonInit(
|
||||
new BillingEvent.Recurring.Builder()
|
||||
.setParent(domainHistory)
|
||||
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
|
||||
.setReason(Reason.RENEW)
|
||||
.setEventTime(now.plusYears(1))
|
||||
.setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED)
|
||||
.setRenewalPrice(Money.of(USD, BigDecimal.valueOf(100)))
|
||||
.setRecurrenceEndTime(END_OF_TIME)));
|
||||
assertThat(recurringEvent.getRenewalPriceBehavior()).isEqualTo(RenewalPriceBehavior.SPECIFIED);
|
||||
assertThat(recurringEvent.getRenewalPrice()).hasValue(Money.of(USD, 100));
|
||||
}
|
||||
|
||||
@TestOfyAndSql
|
||||
void testSuccess_buildWithNonPremiumRenewalBehavior() {
|
||||
BillingEvent.Recurring recurringEvent =
|
||||
persistResource(
|
||||
commonInit(
|
||||
new BillingEvent.Recurring.Builder()
|
||||
.setParent(domainHistory)
|
||||
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
|
||||
.setReason(Reason.RENEW)
|
||||
.setEventTime(now.plusYears(1))
|
||||
.setRenewalPriceBehavior(RenewalPriceBehavior.NONPREMIUM)
|
||||
.setRecurrenceEndTime(END_OF_TIME)));
|
||||
assertThat(recurringEvent.getRenewalPriceBehavior()).isEqualTo(RenewalPriceBehavior.NONPREMIUM);
|
||||
assertThat(loadByEntity(recurringEvent).getRenewalPrice()).isEmpty();
|
||||
}
|
||||
|
||||
@TestOfyAndSql
|
||||
void testSuccess_buildWithSpecifiedRenewalBehavior() {
|
||||
BillingEvent.Recurring recurringEvent =
|
||||
persistResource(
|
||||
commonInit(
|
||||
new BillingEvent.Recurring.Builder()
|
||||
.setParent(domainHistory)
|
||||
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
|
||||
.setReason(Reason.RENEW)
|
||||
.setEventTime(now.plusYears(1))
|
||||
.setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED)
|
||||
.setRenewalPrice(Money.of(USD, BigDecimal.valueOf(100)))
|
||||
.setRecurrenceEndTime(END_OF_TIME)));
|
||||
assertThat(recurringEvent.getRenewalPriceBehavior()).isEqualTo(RenewalPriceBehavior.SPECIFIED);
|
||||
assertThat(recurringEvent.getRenewalPrice()).hasValue(Money.of(USD, 100));
|
||||
}
|
||||
|
||||
@TestOfyAndSql
|
||||
void testFailure_buildWithSpecifiedRenewalBehavior_requiresNonNullRenewalPrice() {
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() ->
|
||||
new BillingEvent.Recurring.Builder()
|
||||
.setParent(domainHistory)
|
||||
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
|
||||
.setReason(Reason.RENEW)
|
||||
.setEventTime(now.plusYears(1))
|
||||
.setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED)
|
||||
.setRecurrenceEndTime(END_OF_TIME)
|
||||
.build());
|
||||
assertThat(thrown)
|
||||
.hasMessageThat()
|
||||
.isEqualTo(
|
||||
"Renewal price can have a value if and only if the "
|
||||
+ "renewal price behavior is SPECIFIED");
|
||||
}
|
||||
|
||||
@TestOfyAndSql
|
||||
void testFailure_buildWithNonPremiumRenewalBehavior_requiresNullRenewalPrice() {
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() ->
|
||||
new BillingEvent.Recurring.Builder()
|
||||
.setParent(domainHistory)
|
||||
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
|
||||
.setReason(Reason.RENEW)
|
||||
.setEventTime(now.plusYears(1))
|
||||
.setRenewalPriceBehavior(RenewalPriceBehavior.NONPREMIUM)
|
||||
.setRenewalPrice(Money.of(USD, BigDecimal.valueOf(100)))
|
||||
.setRecurrenceEndTime(END_OF_TIME)
|
||||
.build());
|
||||
assertThat(thrown)
|
||||
.hasMessageThat()
|
||||
.isEqualTo(
|
||||
"Renewal price can have a value if and only if the "
|
||||
+ "renewal price behavior is SPECIFIED");
|
||||
}
|
||||
|
||||
@TestOfyAndSql
|
||||
void testFailure_buildWithDefaultRenewalBehavior_requiresNullRenewalPrice() {
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() ->
|
||||
new BillingEvent.Recurring.Builder()
|
||||
.setParent(domainHistory)
|
||||
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
|
||||
.setReason(Reason.RENEW)
|
||||
.setEventTime(now.plusYears(1))
|
||||
.setRenewalPriceBehavior(RenewalPriceBehavior.DEFAULT)
|
||||
.setRenewalPrice(Money.of(USD, BigDecimal.valueOf(100)))
|
||||
.setRecurrenceEndTime(END_OF_TIME)
|
||||
.build());
|
||||
assertThat(thrown)
|
||||
.hasMessageThat()
|
||||
.isEqualTo(
|
||||
"Renewal price can have a value if and only if the "
|
||||
+ "renewal price behavior is SPECIFIED");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ package google.registry.persistence.converter;
|
|||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
|
||||
import static google.registry.testing.DatabaseHelper.insertInDb;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import google.registry.model.ImmutableObject;
|
||||
|
@ -23,6 +24,7 @@ import google.registry.model.replay.EntityTest.EntityForTesting;
|
|||
import google.registry.persistence.transaction.JpaTestExtensions;
|
||||
import google.registry.persistence.transaction.JpaTestExtensions.JpaUnitTestExtension;
|
||||
import java.math.BigDecimal;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
@ -34,9 +36,11 @@ import javax.persistence.FetchType;
|
|||
import javax.persistence.Id;
|
||||
import javax.persistence.JoinColumn;
|
||||
import javax.persistence.MapKeyColumn;
|
||||
import javax.persistence.PersistenceException;
|
||||
import org.hibernate.annotations.Columns;
|
||||
import org.hibernate.annotations.Type;
|
||||
import org.joda.money.CurrencyUnit;
|
||||
import org.joda.money.IllegalCurrencyException;
|
||||
import org.joda.money.Money;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
|
@ -65,7 +69,7 @@ public class JodaMoneyConverterTest {
|
|||
.createNativeQuery(
|
||||
"SELECT amount, currency FROM \"TestEntity\" WHERE name = 'id'")
|
||||
.getResultList());
|
||||
assertThat(result.size()).isEqualTo(1);
|
||||
assertThat(result).hasSize(1);
|
||||
// The amount property, when loaded as a raw value, has the same scale as the table column,
|
||||
// which is 2.
|
||||
assertThat(Arrays.asList((Object[]) result.get(0)))
|
||||
|
@ -91,7 +95,7 @@ public class JodaMoneyConverterTest {
|
|||
.createNativeQuery(
|
||||
"SELECT amount, currency FROM \"TestEntity\" WHERE name = 'id'")
|
||||
.getResultList());
|
||||
assertThat(result.size()).isEqualTo(1);
|
||||
assertThat(result).hasSize(1);
|
||||
/* The amount property, when loaded as a raw value, has the same scale as the table column,
|
||||
which is 2. */
|
||||
assertThat(Arrays.asList((Object[]) result.get(0)))
|
||||
|
@ -136,7 +140,7 @@ public class JodaMoneyConverterTest {
|
|||
"SELECT my_amount, my_currency, your_amount, your_currency FROM"
|
||||
+ " \"ComplexTestEntity\" WHERE name = 'id'")
|
||||
.getResultList());
|
||||
assertThat(result.size()).isEqualTo(1);
|
||||
assertThat(result).hasSize(1);
|
||||
assertThat(Arrays.asList((Object[]) result.get(0)))
|
||||
.containsExactly(
|
||||
BigDecimal.valueOf(100).setScale(2), "USD", BigDecimal.valueOf(80).setScale(2), "GBP")
|
||||
|
@ -153,7 +157,7 @@ public class JodaMoneyConverterTest {
|
|||
.getResultList());
|
||||
ComplexTestEntity persisted =
|
||||
jpaTm().transact(() -> jpaTm().getEntityManager().find(ComplexTestEntity.class, "id"));
|
||||
assertThat(result.size()).isEqualTo(1);
|
||||
assertThat(result).hasSize(1);
|
||||
|
||||
assertThat(Arrays.asList((Object[]) result.get(0)))
|
||||
.containsExactly(BigDecimal.valueOf(2000).setScale(2), "JPY")
|
||||
|
@ -164,6 +168,124 @@ public class JodaMoneyConverterTest {
|
|||
assertThat(persisted.moneyMap).containsExactlyEntriesIn(moneyMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implicit test cases for @override method @nullSafeGet when constructing {@link Money} object
|
||||
* with null/invalid column(s).
|
||||
*/
|
||||
@Test
|
||||
void testNullSafeGet_nullAmountNullCurrency_returnsNull() throws SQLException {
|
||||
jpaTm()
|
||||
.transact(
|
||||
() ->
|
||||
jpaTm()
|
||||
.getEntityManager()
|
||||
.createNativeQuery(
|
||||
"INSERT INTO \"TestEntity\" (name, amount, currency) VALUES('id', null,"
|
||||
+ " null)")
|
||||
.executeUpdate());
|
||||
List<?> result =
|
||||
jpaTm()
|
||||
.transact(
|
||||
() ->
|
||||
jpaTm()
|
||||
.getEntityManager()
|
||||
.createNativeQuery(
|
||||
"SELECT amount, currency FROM \"TestEntity\" WHERE name = 'id'")
|
||||
.getResultList());
|
||||
assertThat(result).hasSize(1);
|
||||
assertThat(Arrays.asList((Object[]) result.get(0))).containsExactly(null, null).inOrder();
|
||||
assertThat(
|
||||
jpaTm()
|
||||
.transact(
|
||||
() ->
|
||||
jpaTm()
|
||||
.getEntityManager()
|
||||
.createQuery("SELECT money FROM TestEntity WHERE name = 'id'")
|
||||
.getResultList())
|
||||
.get(0))
|
||||
.isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNullSafeGet_nullAMountValidCurrency_throwsHibernateException() throws SQLException {
|
||||
jpaTm()
|
||||
.transact(
|
||||
() ->
|
||||
jpaTm()
|
||||
.getEntityManager()
|
||||
.createNativeQuery(
|
||||
"INSERT INTO \"TestEntity\" (name, amount, currency) VALUES('id', null,"
|
||||
+ " 'USD')")
|
||||
.executeUpdate());
|
||||
List<?> result =
|
||||
jpaTm()
|
||||
.transact(
|
||||
() ->
|
||||
jpaTm()
|
||||
.getEntityManager()
|
||||
.createNativeQuery(
|
||||
"SELECT amount, currency FROM \"TestEntity\" WHERE name = 'id'")
|
||||
.getResultList());
|
||||
assertThat(Arrays.asList((Object[]) result.get(0))).containsExactly(null, "USD");
|
||||
// CurrencyUnit.of() throws HibernateException for invalid currency which leads to persistance
|
||||
// error
|
||||
PersistenceException thrown =
|
||||
assertThrows(
|
||||
PersistenceException.class,
|
||||
() ->
|
||||
jpaTm()
|
||||
.transact(
|
||||
() ->
|
||||
jpaTm()
|
||||
.getEntityManager()
|
||||
.createQuery("SELECT money FROM TestEntity WHERE name = 'id'")
|
||||
.getResultList()));
|
||||
assertThat(thrown)
|
||||
.hasMessageThat()
|
||||
.isEqualTo(
|
||||
"org.hibernate.HibernateException: Mismatching null state between currency 'USD' and"
|
||||
+ " amount 'null'");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNullSafeGet_nullAMountInValidCurrency_throwsIllegalCurrencyException()
|
||||
throws SQLException {
|
||||
jpaTm()
|
||||
.transact(
|
||||
() ->
|
||||
jpaTm()
|
||||
.getEntityManager()
|
||||
.createNativeQuery(
|
||||
"INSERT INTO \"TestEntity\" (name, amount, currency) VALUES('id', 100,"
|
||||
+ " 'INVALIDCURRENCY')")
|
||||
.executeUpdate());
|
||||
List<?> result =
|
||||
jpaTm()
|
||||
.transact(
|
||||
() ->
|
||||
jpaTm()
|
||||
.getEntityManager()
|
||||
.createNativeQuery(
|
||||
"SELECT amount, currency FROM \"TestEntity\" WHERE name = 'id'")
|
||||
.getResultList());
|
||||
assertThat(result).hasSize(1);
|
||||
assertThat(Arrays.asList((Object[]) result.get(0)))
|
||||
.containsExactly(BigDecimal.valueOf(100).setScale(2), "INVALIDCURRENCY")
|
||||
.inOrder();
|
||||
IllegalCurrencyException thrown =
|
||||
assertThrows(
|
||||
IllegalCurrencyException.class,
|
||||
() ->
|
||||
jpaTm()
|
||||
.transact(
|
||||
() ->
|
||||
jpaTm()
|
||||
.getEntityManager()
|
||||
.createQuery("SELECT money FROM TestEntity WHERE name = 'id'")
|
||||
.getResultList()));
|
||||
assertThat(thrown).hasMessageThat().isEqualTo("Unknown currency 'INVALIDCURRENCY'");
|
||||
}
|
||||
|
||||
// Override entity name to exclude outer-class name in table name. Not necessary if class is not
|
||||
// inner class.
|
||||
@Entity(name = "TestEntity")
|
||||
|
|
|
@ -65,13 +65,20 @@ class google.registry.model.billing.BillingEvent$Recurring {
|
|||
@Id java.lang.Long id;
|
||||
@Parent com.googlecode.objectify.Key<google.registry.model.domain.DomainHistory> parent;
|
||||
google.registry.model.billing.BillingEvent$Reason reason;
|
||||
google.registry.model.billing.BillingEvent$RenewalPriceBehavior renewalPriceBehavior;
|
||||
google.registry.model.common.TimeOfYear recurrenceTimeOfYear;
|
||||
java.lang.String clientId;
|
||||
java.lang.String targetId;
|
||||
java.util.Set<google.registry.model.billing.BillingEvent$Flag> flags;
|
||||
org.joda.money.Money renewalPrice;
|
||||
org.joda.time.DateTime eventTime;
|
||||
org.joda.time.DateTime recurrenceEndTime;
|
||||
}
|
||||
enum google.registry.model.billing.BillingEvent$RenewalPriceBehavior {
|
||||
DEFAULT;
|
||||
NONPREMIUM;
|
||||
SPECIFIED;
|
||||
}
|
||||
class google.registry.model.common.Cursor {
|
||||
@Id java.lang.String id;
|
||||
@Parent com.googlecode.objectify.Key<google.registry.model.common.EntityGroupRoot> parent;
|
||||
|
|
|
@ -79,6 +79,9 @@
|
|||
domain_name text not null,
|
||||
recurrence_end_time timestamptz,
|
||||
recurrence_time_of_year text,
|
||||
renewal_price_amount numeric(19, 2),
|
||||
renewal_price_currency text,
|
||||
renewal_price_behavior text not null,
|
||||
primary key (billing_recurrence_id)
|
||||
);
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue