mirror of
https://github.com/google/nomulus.git
synced 2025-04-29 19:47:51 +02:00
Add SQL schema for GracePeriodHistory (#746)
* Add schema for GracePeriodHistory Rebase on HEAD Rebase on HEAD Rebase on HEAD and rename column Use OfyService to generate id Refactor GracePeriodsSubject Rebase on HEAD Remove GracePeriodSubject and GracePeriodsSubject Rebase on HEAD Rebase on HEAD Rebase on HEAD Add gracePeriodHistoryRevisionId and remove some foreign key * Rebase on HEAD
This commit is contained in:
parent
1349fa4cfc
commit
8625e44cfd
23 changed files with 4813 additions and 4091 deletions
|
@ -0,0 +1,47 @@
|
|||
// Copyright 2020 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.testing.truth;
|
||||
|
||||
import static com.google.common.truth.Truth.assertWithMessage;
|
||||
|
||||
import com.google.common.truth.Truth;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/** Utils class containing helper functions for {@link Truth}. */
|
||||
public class TruthUtils {
|
||||
|
||||
/** Asserts that both of the given objects are either null or nonnull. */
|
||||
public static void assertNullnessParity(@Nullable Object thisObj, @Nullable Object thatObj) {
|
||||
if (thisObj == null) {
|
||||
assertWithMessage("Expects both objects are null but thatObj is not null")
|
||||
.that(thatObj)
|
||||
.isNull();
|
||||
} else {
|
||||
assertWithMessage("Expects both objects are not null but thatObj is null")
|
||||
.that(thatObj)
|
||||
.isNotNull();
|
||||
}
|
||||
}
|
||||
|
||||
/** Asserts that both of the given objects are either null or nonnull. */
|
||||
public static void assertNullnessParity(
|
||||
@Nullable Object thisObj, @Nullable Object thatObj, String errorMessage) {
|
||||
if (thisObj == null) {
|
||||
assertWithMessage(errorMessage).that(thatObj).isNull();
|
||||
} else {
|
||||
assertWithMessage(errorMessage).that(thatObj).isNotNull();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -24,6 +24,7 @@ import com.googlecode.objectify.annotation.EntitySubclass;
|
|||
import com.googlecode.objectify.annotation.Ignore;
|
||||
import google.registry.model.ImmutableObject;
|
||||
import google.registry.model.domain.DomainHistory.DomainHistoryId;
|
||||
import google.registry.model.domain.GracePeriod.GracePeriodHistory;
|
||||
import google.registry.model.domain.secdns.DomainDsDataHistory;
|
||||
import google.registry.model.host.HostResource;
|
||||
import google.registry.model.reporting.DomainTransactionRecord;
|
||||
|
@ -118,6 +119,24 @@ public class DomainHistory extends HistoryEntry implements SqlEntity {
|
|||
})
|
||||
Set<DomainDsDataHistory> dsDataHistories;
|
||||
|
||||
@OneToMany(
|
||||
cascade = {CascadeType.ALL},
|
||||
fetch = FetchType.EAGER,
|
||||
orphanRemoval = true)
|
||||
@JoinColumns({
|
||||
@JoinColumn(
|
||||
name = "domainHistoryRevisionId",
|
||||
referencedColumnName = "historyRevisionId",
|
||||
insertable = false,
|
||||
updatable = false),
|
||||
@JoinColumn(
|
||||
name = "domainRepoId",
|
||||
referencedColumnName = "domainRepoId",
|
||||
insertable = false,
|
||||
updatable = false)
|
||||
})
|
||||
Set<GracePeriodHistory> gracePeriodHistories;
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
@Access(AccessType.PROPERTY)
|
||||
|
@ -205,6 +224,10 @@ public class DomainHistory extends HistoryEntry implements SqlEntity {
|
|||
return VKey.create(DomainBase.class, getDomainRepoId());
|
||||
}
|
||||
|
||||
public Set<GracePeriodHistory> getGracePeriodHistories() {
|
||||
return nullToEmptyImmutableCopy(gracePeriodHistories);
|
||||
}
|
||||
|
||||
/** Creates a {@link VKey} instance for this entity. */
|
||||
public VKey<DomainHistory> createVKey() {
|
||||
return VKey.create(
|
||||
|
@ -327,6 +350,10 @@ public class DomainHistory extends HistoryEntry implements SqlEntity {
|
|||
nullToEmptyImmutableCopy(instance.domainContent.getDsData()).stream()
|
||||
.map(dsData -> DomainDsDataHistory.createFrom(instance.id, dsData))
|
||||
.collect(toImmutableSet());
|
||||
instance.gracePeriodHistories =
|
||||
nullToEmptyImmutableCopy(instance.domainContent.getGracePeriods()).stream()
|
||||
.map(gracePeriod -> GracePeriodHistory.createFrom(instance.id, gracePeriod))
|
||||
.collect(toImmutableSet());
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
|
|
@ -17,7 +17,9 @@ package google.registry.model.domain;
|
|||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.googlecode.objectify.annotation.Embed;
|
||||
import com.googlecode.objectify.annotation.OnLoad;
|
||||
import google.registry.model.billing.BillingEvent;
|
||||
import google.registry.model.billing.BillingEvent.Recurring;
|
||||
import google.registry.model.domain.rgp.GracePeriodStatus;
|
||||
|
@ -25,7 +27,10 @@ import google.registry.model.ofy.ObjectifyService;
|
|||
import google.registry.persistence.VKey;
|
||||
import google.registry.schema.replay.DatastoreAndSqlEntity;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.persistence.Access;
|
||||
import javax.persistence.AccessType;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.Index;
|
||||
import javax.persistence.Table;
|
||||
import org.joda.time.DateTime;
|
||||
|
@ -41,20 +46,36 @@ import org.joda.time.DateTime;
|
|||
@Table(indexes = @Index(columnList = "domainRepoId"))
|
||||
public class GracePeriod extends GracePeriodBase implements DatastoreAndSqlEntity {
|
||||
|
||||
@Id
|
||||
@Access(AccessType.PROPERTY)
|
||||
@Override
|
||||
public long getGracePeriodId() {
|
||||
return super.getGracePeriodId();
|
||||
}
|
||||
|
||||
// TODO(b/169873747): Remove this method after explicitly re-saving all domain entities.
|
||||
@OnLoad
|
||||
void onLoad() {
|
||||
if (gracePeriodId == null) {
|
||||
gracePeriodId = ObjectifyService.allocateId();
|
||||
}
|
||||
}
|
||||
|
||||
private static GracePeriod createInternal(
|
||||
GracePeriodStatus type,
|
||||
String domainRepoId,
|
||||
DateTime expirationTime,
|
||||
String clientId,
|
||||
@Nullable VKey<BillingEvent.OneTime> billingEventOneTime,
|
||||
@Nullable VKey<BillingEvent.Recurring> billingEventRecurring) {
|
||||
@Nullable VKey<BillingEvent.Recurring> billingEventRecurring,
|
||||
@Nullable Long gracePeriodId) {
|
||||
checkArgument((billingEventOneTime == null) || (billingEventRecurring == null),
|
||||
"A grace period can have at most one billing event");
|
||||
checkArgument(
|
||||
(billingEventRecurring != null) == GracePeriodStatus.AUTO_RENEW.equals(type),
|
||||
"Recurring billing events must be present on (and only on) autorenew grace periods");
|
||||
GracePeriod instance = new GracePeriod();
|
||||
instance.id = ObjectifyService.allocateId();
|
||||
instance.gracePeriodId = gracePeriodId == null ? ObjectifyService.allocateId() : gracePeriodId;
|
||||
instance.type = checkArgumentNotNull(type);
|
||||
instance.domainRepoId = checkArgumentNotNull(domainRepoId);
|
||||
instance.expirationTime = checkArgumentNotNull(expirationTime);
|
||||
|
@ -79,7 +100,28 @@ public class GracePeriod extends GracePeriodBase implements DatastoreAndSqlEntit
|
|||
DateTime expirationTime,
|
||||
String clientId,
|
||||
@Nullable VKey<BillingEvent.OneTime> billingEventOneTime) {
|
||||
return createInternal(type, domainRepoId, expirationTime, clientId, billingEventOneTime, null);
|
||||
return createInternal(
|
||||
type, domainRepoId, expirationTime, clientId, billingEventOneTime, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a GracePeriod for an (optional) OneTime billing event and a given {@link
|
||||
* #gracePeriodId}.
|
||||
*
|
||||
* <p>Normal callers should always use {@link #forBillingEvent} instead, assuming they do not need
|
||||
* to avoid loading the BillingEvent from Datastore. This method should typically be called only
|
||||
* from test code to explicitly construct GracePeriods.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
public static GracePeriod create(
|
||||
GracePeriodStatus type,
|
||||
String domainRepoId,
|
||||
DateTime expirationTime,
|
||||
String clientId,
|
||||
@Nullable VKey<BillingEvent.OneTime> billingEventOneTime,
|
||||
@Nullable Long gracePeriodId) {
|
||||
return createInternal(
|
||||
type, domainRepoId, expirationTime, clientId, billingEventOneTime, null, gracePeriodId);
|
||||
}
|
||||
|
||||
/** Creates a GracePeriod for a Recurring billing event. */
|
||||
|
@ -91,13 +133,27 @@ public class GracePeriod extends GracePeriodBase implements DatastoreAndSqlEntit
|
|||
VKey<Recurring> billingEventRecurring) {
|
||||
checkArgumentNotNull(billingEventRecurring, "billingEventRecurring cannot be null");
|
||||
return createInternal(
|
||||
type, domainRepoId, expirationTime, clientId, null, billingEventRecurring);
|
||||
type, domainRepoId, expirationTime, clientId, null, billingEventRecurring, null);
|
||||
}
|
||||
|
||||
/** Creates a GracePeriod for a Recurring billing event and a given {@link #gracePeriodId}. */
|
||||
@VisibleForTesting
|
||||
public static GracePeriod createForRecurring(
|
||||
GracePeriodStatus type,
|
||||
String domainRepoId,
|
||||
DateTime expirationTime,
|
||||
String clientId,
|
||||
VKey<Recurring> billingEventRecurring,
|
||||
@Nullable Long gracePeriodId) {
|
||||
checkArgumentNotNull(billingEventRecurring, "billingEventRecurring cannot be null");
|
||||
return createInternal(
|
||||
type, domainRepoId, expirationTime, clientId, null, billingEventRecurring, gracePeriodId);
|
||||
}
|
||||
|
||||
/** Creates a GracePeriod with no billing event. */
|
||||
public static GracePeriod createWithoutBillingEvent(
|
||||
GracePeriodStatus type, String domainRepoId, DateTime expirationTime, String clientId) {
|
||||
return createInternal(type, domainRepoId, expirationTime, clientId, null, null);
|
||||
return createInternal(type, domainRepoId, expirationTime, clientId, null, null, null);
|
||||
}
|
||||
|
||||
/** Constructs a GracePeriod of the given type from the provided one-time BillingEvent. */
|
||||
|
@ -119,7 +175,6 @@ public class GracePeriod extends GracePeriodBase implements DatastoreAndSqlEntit
|
|||
*/
|
||||
public GracePeriod cloneAfterOfyLoad(String domainRepoId) {
|
||||
GracePeriod clone = clone(this);
|
||||
clone.id = ObjectifyService.allocateId();
|
||||
clone.domainRepoId = checkArgumentNotNull(domainRepoId);
|
||||
clone.restoreHistoryIds();
|
||||
return clone;
|
||||
|
@ -136,4 +191,49 @@ public class GracePeriod extends GracePeriodBase implements DatastoreAndSqlEntit
|
|||
clone.billingEventRecurring = recurring;
|
||||
return clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a clone of this {@link GracePeriod} with prepopulated {@link #gracePeriodId} generated
|
||||
* by {@link ObjectifyService#allocateId()}.
|
||||
*
|
||||
* <p>TODO(shicong): Figure out how to generate the id only when the entity is used for Cloud SQL.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
public GracePeriod cloneWithPrepopulatedId() {
|
||||
GracePeriod clone = clone(this);
|
||||
clone.gracePeriodId = ObjectifyService.allocateId();
|
||||
return clone;
|
||||
}
|
||||
|
||||
/** Entity class to represent a historic {@link GracePeriod}. */
|
||||
@Entity(name = "GracePeriodHistory")
|
||||
@Table(indexes = @Index(columnList = "domainRepoId"))
|
||||
static class GracePeriodHistory extends GracePeriodBase {
|
||||
@Id Long gracePeriodHistoryRevisionId;
|
||||
|
||||
/** ID for the associated {@link DomainHistory} entity. */
|
||||
Long domainHistoryRevisionId;
|
||||
|
||||
@Override
|
||||
@Access(AccessType.PROPERTY)
|
||||
public long getGracePeriodId() {
|
||||
return super.getGracePeriodId();
|
||||
}
|
||||
|
||||
static GracePeriodHistory createFrom(long historyRevisionId, GracePeriod gracePeriod) {
|
||||
GracePeriodHistory instance = new GracePeriodHistory();
|
||||
instance.gracePeriodHistoryRevisionId = ObjectifyService.allocateId();
|
||||
instance.domainHistoryRevisionId = historyRevisionId;
|
||||
instance.gracePeriodId = gracePeriod.gracePeriodId;
|
||||
instance.type = gracePeriod.type;
|
||||
instance.domainRepoId = gracePeriod.domainRepoId;
|
||||
instance.expirationTime = gracePeriod.expirationTime;
|
||||
instance.clientId = gracePeriod.clientId;
|
||||
instance.billingEventOneTime = gracePeriod.billingEventOneTime;
|
||||
instance.billingEventOneTimeHistoryId = gracePeriod.billingEventOneTimeHistoryId;
|
||||
instance.billingEventRecurring = gracePeriod.billingEventRecurring;
|
||||
instance.billingEventRecurringHistoryId = gracePeriod.billingEventRecurringHistoryId;
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,21 +26,23 @@ import google.registry.persistence.VKey;
|
|||
import java.lang.reflect.Field;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import javax.persistence.Access;
|
||||
import javax.persistence.AccessType;
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.EnumType;
|
||||
import javax.persistence.Enumerated;
|
||||
import javax.persistence.MappedSuperclass;
|
||||
import javax.persistence.Transient;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
/** Base class containing common fields and methods for {@link GracePeriod}. */
|
||||
@Embed
|
||||
@MappedSuperclass
|
||||
@Access(AccessType.FIELD)
|
||||
public class GracePeriodBase extends ImmutableObject {
|
||||
|
||||
/** Unique id required for hibernate representation. */
|
||||
@javax.persistence.Id
|
||||
@Ignore
|
||||
Long id;
|
||||
@Transient Long gracePeriodId;
|
||||
|
||||
/** Repository id for the domain which this grace period belongs to. */
|
||||
@Ignore
|
||||
|
@ -85,8 +87,8 @@ public class GracePeriodBase extends ImmutableObject {
|
|||
@Column(name = "billing_recurrence_history_id")
|
||||
Long billingEventRecurringHistoryId;
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
public long getGracePeriodId() {
|
||||
return gracePeriodId;
|
||||
}
|
||||
|
||||
public GracePeriodStatus getType() {
|
||||
|
@ -105,6 +107,12 @@ public class GracePeriodBase extends ImmutableObject {
|
|||
return clientId;
|
||||
}
|
||||
|
||||
/** This method is private because it is only used by Hibernate. */
|
||||
@SuppressWarnings("unused")
|
||||
private void setGracePeriodId(long gracePeriodId) {
|
||||
this.gracePeriodId = gracePeriodId;
|
||||
}
|
||||
|
||||
/** Returns true if this GracePeriod has an associated BillingEvent; i.e. if it's refundable. */
|
||||
public boolean hasBillingEvent() {
|
||||
return billingEventOneTime != null || billingEventRecurring != null;
|
||||
|
|
|
@ -46,6 +46,7 @@
|
|||
<class>google.registry.model.domain.DomainBase</class>
|
||||
<class>google.registry.model.domain.DomainHistory</class>
|
||||
<class>google.registry.model.domain.GracePeriod</class>
|
||||
<class>google.registry.model.domain.GracePeriod$GracePeriodHistory</class>
|
||||
<class>google.registry.model.domain.secdns.DelegationSignerData</class>
|
||||
<class>google.registry.model.domain.secdns.DomainDsDataHistory</class>
|
||||
<class>google.registry.model.domain.token.AllocationToken</class>
|
||||
|
|
|
@ -805,7 +805,7 @@ class EppLifecycleDomainTest extends EppTestCase {
|
|||
|
||||
// As the losing registrar, read the request poll message, and then ack it.
|
||||
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
|
||||
String messageId = "1-C-EXAMPLE-20-26-2001";
|
||||
String messageId = "1-C-EXAMPLE-18-24-2001";
|
||||
assertThatCommand("poll.xml")
|
||||
.atTime("2001-01-01T00:01:00Z")
|
||||
.hasResponse("poll_response_domain_transfer_request.xml", ImmutableMap.of("ID", messageId));
|
||||
|
@ -814,7 +814,7 @@ class EppLifecycleDomainTest extends EppTestCase {
|
|||
.hasResponse("poll_ack_response_empty.xml");
|
||||
|
||||
// Five days in the future, expect a server approval poll message to the loser, and ack it.
|
||||
messageId = "1-C-EXAMPLE-20-25-2001";
|
||||
messageId = "1-C-EXAMPLE-18-23-2001";
|
||||
assertThatCommand("poll.xml")
|
||||
.atTime("2001-01-06T00:01:00Z")
|
||||
.hasResponse(
|
||||
|
@ -826,7 +826,7 @@ class EppLifecycleDomainTest extends EppTestCase {
|
|||
assertThatLogoutSucceeds();
|
||||
|
||||
// Also expect a server approval poll message to the winner, with the transfer request trid.
|
||||
messageId = "1-C-EXAMPLE-20-24-2001";
|
||||
messageId = "1-C-EXAMPLE-18-22-2001";
|
||||
assertThatLoginSucceeds("TheRegistrar", "password2");
|
||||
assertThatCommand("poll.xml")
|
||||
.atTime("2001-01-06T00:02:00Z")
|
||||
|
|
|
@ -183,7 +183,8 @@ public abstract class FlowTestCase<F extends Flow> {
|
|||
entry.getKey().getDomainRepoId(),
|
||||
entry.getKey().getExpirationTime(),
|
||||
entry.getKey().getClientId(),
|
||||
null),
|
||||
null,
|
||||
1L),
|
||||
stripBillingEventId(entry.getValue()));
|
||||
}
|
||||
return builder.build();
|
||||
|
|
|
@ -436,7 +436,8 @@ class DomainDeleteFlowTest extends ResourceFlowTestCase<DomainDeleteFlow, Domain
|
|||
domain.getRepoId(),
|
||||
clock.nowUtc().plus(Registry.get("tld").getRedemptionGracePeriodLength()),
|
||||
"TheRegistrar",
|
||||
null));
|
||||
null,
|
||||
resource.getGracePeriods().iterator().next().getGracePeriodId()));
|
||||
assertDeletionPollMessageFor(resource, "Domain deleted.");
|
||||
}
|
||||
|
||||
|
@ -637,7 +638,8 @@ class DomainDeleteFlowTest extends ResourceFlowTestCase<DomainDeleteFlow, Domain
|
|||
domain.getRepoId(),
|
||||
clock.nowUtc().plus(Registry.get("tld").getRedemptionGracePeriodLength()),
|
||||
"TheRegistrar",
|
||||
null));
|
||||
null,
|
||||
domain.getGracePeriods().iterator().next().getGracePeriodId()));
|
||||
// The poll message (in the future) to the losing registrar for implicit ack should be gone.
|
||||
assertThat(getPollMessages("TheRegistrar", clock.nowUtc().plusMonths(1))).isEmpty();
|
||||
// The poll message in the future to the gaining registrar should be gone too, but there
|
||||
|
@ -1104,7 +1106,8 @@ class DomainDeleteFlowTest extends ResourceFlowTestCase<DomainDeleteFlow, Domain
|
|||
domain.getRepoId(),
|
||||
clock.nowUtc().plus(standardDays(15)),
|
||||
"TheRegistrar",
|
||||
null));
|
||||
null,
|
||||
resource.getGracePeriods().iterator().next().getGracePeriodId()));
|
||||
assertDeletionPollMessageFor(resource, "Deleted by registry administrator.");
|
||||
}
|
||||
|
||||
|
@ -1151,7 +1154,8 @@ class DomainDeleteFlowTest extends ResourceFlowTestCase<DomainDeleteFlow, Domain
|
|||
domain.getRepoId(),
|
||||
clock.nowUtc().plus(standardDays(15)),
|
||||
"TheRegistrar",
|
||||
null));
|
||||
null,
|
||||
resource.getGracePeriods().iterator().next().getGracePeriodId()));
|
||||
assertDeletionPollMessageFor(resource, "Deleted by registry administrator.");
|
||||
}
|
||||
|
||||
|
|
|
@ -87,8 +87,7 @@ public final class ImmutableObjectSubject extends Subject {
|
|||
}
|
||||
}
|
||||
|
||||
private static Map<Field, Object> filterFields(
|
||||
ImmutableObject original, String... ignoredFields) {
|
||||
public static Map<Field, Object> filterFields(ImmutableObject original, String... ignoredFields) {
|
||||
ImmutableSet<String> ignoredFieldSet = ImmutableSet.copyOf(ignoredFields);
|
||||
Map<Field, Object> originalFields = ModelUtils.getFieldValues(original);
|
||||
// don't use ImmutableMap or a stream->collect model since we can have nulls
|
||||
|
|
|
@ -24,7 +24,6 @@ import static google.registry.util.DateTimeUtils.END_OF_TIME;
|
|||
import static google.registry.util.DateTimeUtils.START_OF_TIME;
|
||||
import static org.joda.money.CurrencyUnit.USD;
|
||||
import static org.joda.time.DateTimeZone.UTC;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Sets;
|
||||
|
@ -118,7 +117,8 @@ public class DomainBaseSqlTest {
|
|||
LaunchNotice.create("tcnid", "validatorId", START_OF_TIME, START_OF_TIME))
|
||||
.setSmdId("smdid")
|
||||
.addGracePeriod(
|
||||
GracePeriod.create(GracePeriodStatus.ADD, "4-COM", END_OF_TIME, "registrar1", null))
|
||||
GracePeriod.create(
|
||||
GracePeriodStatus.ADD, "4-COM", END_OF_TIME, "registrar1", null, 100L))
|
||||
.build();
|
||||
|
||||
host =
|
||||
|
@ -240,7 +240,12 @@ public class DomainBaseSqlTest {
|
|||
.asBuilder()
|
||||
.addGracePeriod(
|
||||
GracePeriod.create(
|
||||
GracePeriodStatus.RENEW, "4-COM", END_OF_TIME, "registrar1", null))
|
||||
GracePeriodStatus.RENEW,
|
||||
"4-COM",
|
||||
END_OF_TIME,
|
||||
"registrar1",
|
||||
null,
|
||||
200L))
|
||||
.build();
|
||||
jpaTm().put(modified);
|
||||
});
|
||||
|
@ -249,38 +254,12 @@ public class DomainBaseSqlTest {
|
|||
.transact(
|
||||
() -> {
|
||||
DomainBase persisted = jpaTm().load(domain.createVKey());
|
||||
assertThat(persisted.getGracePeriods().size()).isEqualTo(2);
|
||||
persisted
|
||||
.getGracePeriods()
|
||||
.forEach(
|
||||
gracePeriod -> {
|
||||
assertThat(gracePeriod.id).isNotNull();
|
||||
if (gracePeriod.getType() == GracePeriodStatus.ADD) {
|
||||
assertAboutImmutableObjects()
|
||||
.that(gracePeriod)
|
||||
.isEqualExceptFields(
|
||||
GracePeriod.create(
|
||||
GracePeriodStatus.ADD,
|
||||
"4-COM",
|
||||
END_OF_TIME,
|
||||
"registrar1",
|
||||
null),
|
||||
"id");
|
||||
} else if (gracePeriod.getType() == GracePeriodStatus.RENEW) {
|
||||
assertAboutImmutableObjects()
|
||||
.that(gracePeriod)
|
||||
.isEqualExceptFields(
|
||||
GracePeriod.create(
|
||||
GracePeriodStatus.RENEW,
|
||||
"4-COM",
|
||||
END_OF_TIME,
|
||||
"registrar1",
|
||||
null),
|
||||
"id");
|
||||
} else {
|
||||
fail("Unexpected GracePeriod: " + gracePeriod);
|
||||
}
|
||||
});
|
||||
assertThat(persisted.getGracePeriods())
|
||||
.containsExactly(
|
||||
GracePeriod.create(
|
||||
GracePeriodStatus.ADD, "4-COM", END_OF_TIME, "registrar1", null, 100L),
|
||||
GracePeriod.create(
|
||||
GracePeriodStatus.RENEW, "4-COM", END_OF_TIME, "registrar1", null, 200L));
|
||||
assertEqualDomainExcept(persisted, "gracePeriods");
|
||||
});
|
||||
|
||||
|
@ -327,7 +306,12 @@ public class DomainBaseSqlTest {
|
|||
.asBuilder()
|
||||
.addGracePeriod(
|
||||
GracePeriod.create(
|
||||
GracePeriodStatus.ADD, "4-COM", END_OF_TIME, "registrar1", null))
|
||||
GracePeriodStatus.ADD,
|
||||
"4-COM",
|
||||
END_OF_TIME,
|
||||
"registrar1",
|
||||
null,
|
||||
100L))
|
||||
.build();
|
||||
jpaTm().put(modified);
|
||||
});
|
||||
|
@ -336,13 +320,10 @@ public class DomainBaseSqlTest {
|
|||
.transact(
|
||||
() -> {
|
||||
DomainBase persisted = jpaTm().load(domain.createVKey());
|
||||
assertThat(persisted.getGracePeriods().size()).isEqualTo(1);
|
||||
assertAboutImmutableObjects()
|
||||
.that(persisted.getGracePeriods().iterator().next())
|
||||
.isEqualExceptFields(
|
||||
assertThat(persisted.getGracePeriods())
|
||||
.containsExactly(
|
||||
GracePeriod.create(
|
||||
GracePeriodStatus.ADD, "4-COM", END_OF_TIME, "registrar1", null),
|
||||
"id");
|
||||
GracePeriodStatus.ADD, "4-COM", END_OF_TIME, "registrar1", null, 100L));
|
||||
assertEqualDomainExcept(persisted, "gracePeriods");
|
||||
});
|
||||
}
|
||||
|
|
|
@ -413,7 +413,8 @@ public class DomainBaseTest extends EntityTestCase {
|
|||
.plusDays(1)
|
||||
.plus(Registry.get("com").getTransferGracePeriodLength()),
|
||||
"winner",
|
||||
transferBillingEvent.createVKey()));
|
||||
transferBillingEvent.createVKey(),
|
||||
afterTransfer.getGracePeriods().iterator().next().getGracePeriodId()));
|
||||
// If we project after the grace period expires all should be the same except the grace period.
|
||||
DomainBase afterGracePeriod =
|
||||
domain.cloneProjectedAtTime(
|
||||
|
@ -653,7 +654,8 @@ public class DomainBaseTest extends EntityTestCase {
|
|||
.plusYears(2)
|
||||
.plus(Registry.get("com").getAutoRenewGracePeriodLength()),
|
||||
renewedThreeTimes.getCurrentSponsorClientId(),
|
||||
renewedThreeTimes.autorenewBillingEvent));
|
||||
renewedThreeTimes.autorenewBillingEvent,
|
||||
renewedThreeTimes.getGracePeriods().iterator().next().getGracePeriodId()));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -24,6 +24,7 @@ import static google.registry.testing.DatastoreHelper.createTld;
|
|||
import static google.registry.testing.DatastoreHelper.newContactResourceWithRoid;
|
||||
import static google.registry.testing.DatastoreHelper.newDomainBase;
|
||||
import static google.registry.testing.DatastoreHelper.newHostResourceWithRoid;
|
||||
import static google.registry.util.DateTimeUtils.END_OF_TIME;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
|
@ -33,7 +34,9 @@ import google.registry.model.contact.ContactResource;
|
|||
import google.registry.model.domain.DomainBase;
|
||||
import google.registry.model.domain.DomainContent;
|
||||
import google.registry.model.domain.DomainHistory;
|
||||
import google.registry.model.domain.GracePeriod;
|
||||
import google.registry.model.domain.Period;
|
||||
import google.registry.model.domain.rgp.GracePeriodStatus;
|
||||
import google.registry.model.domain.secdns.DelegationSignerData;
|
||||
import google.registry.model.eppcommon.Trid;
|
||||
import google.registry.model.host.HostResource;
|
||||
|
@ -55,7 +58,7 @@ public class DomainHistoryTest extends EntityTestCase {
|
|||
|
||||
@TestSqlOnly
|
||||
void testPersistence() {
|
||||
DomainBase domain = createDomainWithContactsAndHosts();
|
||||
DomainBase domain = addGracePeriodForSql(createDomainWithContactsAndHosts());
|
||||
DomainHistory domainHistory = createDomainHistory(domain);
|
||||
jpaTm().transact(() -> jpaTm().insert(domainHistory));
|
||||
|
||||
|
@ -70,7 +73,7 @@ public class DomainHistoryTest extends EntityTestCase {
|
|||
|
||||
@TestSqlOnly
|
||||
void testLegacyPersistence_nullResource() {
|
||||
DomainBase domain = createDomainWithContactsAndHosts();
|
||||
DomainBase domain = addGracePeriodForSql(createDomainWithContactsAndHosts());
|
||||
DomainHistory domainHistory =
|
||||
createDomainHistory(domain).asBuilder().setDomainContent(null).build();
|
||||
jpaTm().transact(() -> jpaTm().insert(domainHistory));
|
||||
|
@ -146,6 +149,17 @@ public class DomainHistoryTest extends EntityTestCase {
|
|||
return domain;
|
||||
}
|
||||
|
||||
private static DomainBase addGracePeriodForSql(DomainBase domainBase) {
|
||||
return domainBase
|
||||
.asBuilder()
|
||||
.setGracePeriods(
|
||||
ImmutableSet.of(
|
||||
GracePeriod.create(
|
||||
GracePeriodStatus.ADD, "domainRepoId", END_OF_TIME, "clientId", null)
|
||||
.cloneWithPrepopulatedId()))
|
||||
.build();
|
||||
}
|
||||
|
||||
static void assertDomainHistoriesEqual(DomainHistory one, DomainHistory two) {
|
||||
assertAboutImmutableObjects()
|
||||
.that(one)
|
||||
|
|
|
@ -118,7 +118,12 @@ public class LegacyHistoryObjectTest extends EntityTestCase {
|
|||
assertAboutImmutableObjects()
|
||||
.that(legacyHistoryEntry)
|
||||
.isEqualExceptFields(
|
||||
fromObjectify, "domainContent", "domainRepoId", "nsHosts", "dsDataHistories");
|
||||
fromObjectify,
|
||||
"domainContent",
|
||||
"domainRepoId",
|
||||
"nsHosts",
|
||||
"dsDataHistories",
|
||||
"gracePeriodHistories");
|
||||
assertThat(fromObjectify instanceof DomainHistory).isTrue();
|
||||
DomainHistory legacyDomainHistory = (DomainHistory) fromObjectify;
|
||||
|
||||
|
@ -136,7 +141,8 @@ public class LegacyHistoryObjectTest extends EntityTestCase {
|
|||
"domainTransactionRecords",
|
||||
"otherClientId",
|
||||
"nsHosts",
|
||||
"dsDataHistories");
|
||||
"dsDataHistories",
|
||||
"gracePeriodHistories");
|
||||
assertThat(nullToEmpty(legacyDomainHistory.getNsHosts()))
|
||||
.isEqualTo(nullToEmpty(legacyHistoryFromSql.getNsHosts()));
|
||||
}
|
||||
|
|
|
@ -124,19 +124,19 @@ class DedupeRecurringBillingEventIdsCommandTest
|
|||
@Test
|
||||
void testResaveAssociatedDomainAndOneTimeBillingEventCorrectly() throws Exception {
|
||||
assertThat(recurring1.getId()).isEqualTo(recurring2.getId());
|
||||
GracePeriod gracePeriod =
|
||||
GracePeriod.createForRecurring(
|
||||
GracePeriodStatus.AUTO_RENEW,
|
||||
domain1.getRepoId(),
|
||||
now.plusDays(45),
|
||||
"a registrar",
|
||||
recurring1.createVKey());
|
||||
domain1 =
|
||||
persistResource(
|
||||
domain1
|
||||
.asBuilder()
|
||||
.setAutorenewBillingEvent(recurring1.createVKey())
|
||||
.setGracePeriods(
|
||||
ImmutableSet.of(
|
||||
GracePeriod.createForRecurring(
|
||||
GracePeriodStatus.AUTO_RENEW,
|
||||
domain1.getRepoId(),
|
||||
now.plusDays(45),
|
||||
"a registrar",
|
||||
recurring1.createVKey())))
|
||||
.setGracePeriods(ImmutableSet.of(gracePeriod))
|
||||
.setTransferData(
|
||||
new DomainTransferData.Builder()
|
||||
.setServerApproveAutorenewEvent(recurring1.createVKey())
|
||||
|
@ -201,7 +201,8 @@ class DedupeRecurringBillingEventIdsCommandTest
|
|||
domain1.getRepoId(),
|
||||
now.plusDays(45),
|
||||
"a registrar",
|
||||
newRecurring.createVKey()));
|
||||
newRecurring.createVKey(),
|
||||
gracePeriod.getGracePeriodId()));
|
||||
assertThat(persistedDomain.getTransferData().getServerApproveAutorenewEvent())
|
||||
.isEqualTo(newRecurring.createVKey());
|
||||
assertThat(persistedDomain.getTransferData().getServerApproveEntities())
|
||||
|
|
|
@ -108,7 +108,7 @@ class EppLifecycleToolsTest extends EppTestCase {
|
|||
.atTime("2001-06-08T00:00:00Z")
|
||||
.hasResponse("poll_response_unrenew.xml");
|
||||
|
||||
assertThatCommand("poll_ack.xml", ImmutableMap.of("ID", "1-8-TLD-23-24-2001"))
|
||||
assertThatCommand("poll_ack.xml", ImmutableMap.of("ID", "1-8-TLD-19-20-2001"))
|
||||
.atTime("2001-06-08T00:00:01Z")
|
||||
.hasResponse("poll_ack_response_empty.xml");
|
||||
|
||||
|
@ -129,7 +129,7 @@ class EppLifecycleToolsTest extends EppTestCase {
|
|||
.hasResponse(
|
||||
"poll_response_autorenew.xml",
|
||||
ImmutableMap.of(
|
||||
"ID", "1-8-TLD-23-26-2003",
|
||||
"ID", "1-8-TLD-19-22-2003",
|
||||
"QDATE", "2003-06-01T00:02:00Z",
|
||||
"DOMAIN", "example.tld",
|
||||
"EXDATE", "2004-06-01T00:02:00Z"));
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<result code="1301">
|
||||
<msg>Command completed successfully; ack to dequeue</msg>
|
||||
</result>
|
||||
<msgQ count="1" id="1-8-TLD-23-24-2001">
|
||||
<msgQ count="1" id="1-8-TLD-19-20-2001">
|
||||
<qDate>2001-06-07T00:00:00Z</qDate>
|
||||
<msg>Domain example.tld was unrenewed by 3 years; now expires at 2003-06-01T00:02:00.000Z.</msg>
|
||||
</msgQ>
|
||||
|
|
|
@ -273,6 +273,7 @@ class google.registry.model.domain.DomainHistory {
|
|||
java.lang.String clientId;
|
||||
java.lang.String otherClientId;
|
||||
java.lang.String reason;
|
||||
java.util.Set<google.registry.model.domain.GracePeriod$GracePeriodHistory> gracePeriodHistories;
|
||||
java.util.Set<google.registry.model.domain.secdns.DomainDsDataHistory> dsDataHistories;
|
||||
java.util.Set<google.registry.model.reporting.DomainTransactionRecord> domainTransactionRecords;
|
||||
org.joda.time.DateTime modificationTime;
|
||||
|
@ -281,6 +282,17 @@ class google.registry.model.domain.GracePeriod {
|
|||
google.registry.model.domain.rgp.GracePeriodStatus type;
|
||||
google.registry.persistence.VKey<google.registry.model.billing.BillingEvent$OneTime> billingEventOneTime;
|
||||
google.registry.persistence.VKey<google.registry.model.billing.BillingEvent$Recurring> billingEventRecurring;
|
||||
java.lang.Long gracePeriodId;
|
||||
java.lang.String clientId;
|
||||
org.joda.time.DateTime expirationTime;
|
||||
}
|
||||
class google.registry.model.domain.GracePeriod$GracePeriodHistory {
|
||||
google.registry.model.domain.rgp.GracePeriodStatus type;
|
||||
google.registry.persistence.VKey<google.registry.model.billing.BillingEvent$OneTime> billingEventOneTime;
|
||||
google.registry.persistence.VKey<google.registry.model.billing.BillingEvent$Recurring> billingEventRecurring;
|
||||
java.lang.Long domainHistoryRevisionId;
|
||||
java.lang.Long gracePeriodHistoryRevisionId;
|
||||
java.lang.Long gracePeriodId;
|
||||
java.lang.String clientId;
|
||||
org.joda.time.DateTime expirationTime;
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -72,3 +72,4 @@ V71__create_kms_secret.sql
|
|||
V72__add_missing_foreign_keys.sql
|
||||
V73__singleton_entities.sql
|
||||
V74__sql_replay_checkpoint.sql
|
||||
V75__add_grace_period_history.sql
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
-- Copyright 2020 The Nomulus Authors. All Rights Reserved.
|
||||
--
|
||||
-- Licensed under the Apache License, Version 2.0 (the "License");
|
||||
-- you may not use this file except in compliance with the License.
|
||||
-- You may obtain a copy of the License at
|
||||
--
|
||||
-- http://www.apache.org/licenses/LICENSE-2.0
|
||||
--
|
||||
-- Unless required by applicable law or agreed to in writing, software
|
||||
-- distributed under the License is distributed on an "AS IS" BASIS,
|
||||
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
-- See the License for the specific language governing permissions and
|
||||
-- limitations under the License.
|
||||
|
||||
alter table "GracePeriod" rename column "id" to "grace_period_id";
|
||||
|
||||
create table "GracePeriodHistory" (
|
||||
grace_period_history_revision_id int8 not null,
|
||||
billing_event_id int8,
|
||||
billing_event_history_id int8,
|
||||
billing_recurrence_id int8,
|
||||
billing_recurrence_history_id int8,
|
||||
registrar_id text not null,
|
||||
domain_repo_id text not null,
|
||||
expiration_time timestamptz not null,
|
||||
type text not null,
|
||||
domain_history_revision_id int8,
|
||||
grace_period_id int8 not null,
|
||||
primary key (grace_period_history_revision_id)
|
||||
);
|
||||
|
||||
alter table if exists "GracePeriodHistory"
|
||||
add constraint FK7w3cx8d55q8bln80e716tr7b8
|
||||
foreign key (domain_repo_id, domain_history_revision_id)
|
||||
references "DomainHistory";
|
||||
|
||||
create index IDXd01j17vrpjxaerxdmn8bwxs7s on "GracePeriodHistory" (domain_repo_id);
|
|
@ -394,7 +394,7 @@
|
|||
);
|
||||
|
||||
create table "GracePeriod" (
|
||||
id int8 not null,
|
||||
grace_period_id int8 not null,
|
||||
billing_event_id int8,
|
||||
billing_event_history_id int8,
|
||||
billing_recurrence_id int8,
|
||||
|
@ -403,7 +403,22 @@
|
|||
domain_repo_id text not null,
|
||||
expiration_time timestamptz not null,
|
||||
type text not null,
|
||||
primary key (id)
|
||||
primary key (grace_period_id)
|
||||
);
|
||||
|
||||
create table "GracePeriodHistory" (
|
||||
grace_period_history_revision_id int8 not null,
|
||||
billing_event_id int8,
|
||||
billing_event_history_id int8,
|
||||
billing_recurrence_id int8,
|
||||
billing_recurrence_history_id int8,
|
||||
registrar_id text not null,
|
||||
domain_repo_id text not null,
|
||||
expiration_time timestamptz not null,
|
||||
type text not null,
|
||||
domain_history_revision_id int8,
|
||||
grace_period_id int8 not null,
|
||||
primary key (grace_period_history_revision_id)
|
||||
);
|
||||
|
||||
create table "Host" (
|
||||
|
@ -749,6 +764,7 @@ create index IDXaro1omfuaxjwmotk3vo00trwm on "DomainHistory" (history_registrar_
|
|||
create index IDXsu1nam10cjes9keobapn5jvxj on "DomainHistory" (history_type);
|
||||
create index IDX6w3qbtgce93cal2orjg1tw7b7 on "DomainHistory" (history_modification_time);
|
||||
create index IDXj1mtx98ndgbtb1bkekahms18w on "GracePeriod" (domain_repo_id);
|
||||
create index IDXd01j17vrpjxaerxdmn8bwxs7s on "GracePeriodHistory" (domain_repo_id);
|
||||
create index IDXfg2nnjlujxo6cb9fha971bq2n on "HostHistory" (creation_time);
|
||||
create index IDX1iy7njgb7wjmj9piml4l2g0qi on "HostHistory" (history_registrar_id);
|
||||
create index IDXkkwbwcwvrdkkqothkiye4jiff on "HostHistory" (host_name);
|
||||
|
@ -806,6 +822,11 @@ create index spec11threatmatch_check_date_idx on "Spec11ThreatMatch" (check_date
|
|||
foreign key (domain_repo_id)
|
||||
references "Domain";
|
||||
|
||||
alter table if exists "GracePeriodHistory"
|
||||
add constraint FK7w3cx8d55q8bln80e716tr7b8
|
||||
foreign key (domain_repo_id, domain_history_revision_id)
|
||||
references "DomainHistory";
|
||||
|
||||
alter table if exists "PremiumEntry"
|
||||
add constraint FKo0gw90lpo1tuee56l0nb6y6g5
|
||||
foreign key (revision_id)
|
||||
|
|
|
@ -524,7 +524,7 @@ ALTER SEQUENCE public."DomainTransactionRecord_id_seq" OWNED BY public."DomainTr
|
|||
--
|
||||
|
||||
CREATE TABLE public."GracePeriod" (
|
||||
id bigint NOT NULL,
|
||||
grace_period_id bigint NOT NULL,
|
||||
billing_event_id bigint,
|
||||
billing_recurrence_id bigint,
|
||||
registrar_id text NOT NULL,
|
||||
|
@ -536,6 +536,25 @@ CREATE TABLE public."GracePeriod" (
|
|||
);
|
||||
|
||||
|
||||
--
|
||||
-- Name: GracePeriodHistory; Type: TABLE; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE TABLE public."GracePeriodHistory" (
|
||||
grace_period_history_revision_id bigint NOT NULL,
|
||||
billing_event_id bigint,
|
||||
billing_event_history_id bigint,
|
||||
billing_recurrence_id bigint,
|
||||
billing_recurrence_history_id bigint,
|
||||
registrar_id text NOT NULL,
|
||||
domain_repo_id text NOT NULL,
|
||||
expiration_time timestamp with time zone NOT NULL,
|
||||
type text NOT NULL,
|
||||
domain_history_revision_id bigint,
|
||||
grace_period_id bigint NOT NULL
|
||||
);
|
||||
|
||||
|
||||
--
|
||||
-- Name: Host; Type: TABLE; Schema: public; Owner: -
|
||||
--
|
||||
|
@ -1212,12 +1231,20 @@ ALTER TABLE ONLY public."Domain"
|
|||
ADD CONSTRAINT "Domain_pkey" PRIMARY KEY (repo_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: GracePeriodHistory GracePeriodHistory_pkey; Type: CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public."GracePeriodHistory"
|
||||
ADD CONSTRAINT "GracePeriodHistory_pkey" PRIMARY KEY (grace_period_history_revision_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: GracePeriod GracePeriod_pkey; Type: CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public."GracePeriod"
|
||||
ADD CONSTRAINT "GracePeriod_pkey" PRIMARY KEY (id);
|
||||
ADD CONSTRAINT "GracePeriod_pkey" PRIMARY KEY (grace_period_id);
|
||||
|
||||
|
||||
--
|
||||
|
@ -1536,6 +1563,13 @@ CREATE INDEX idxaydgox62uno9qx8cjlj5lauye ON public."PollMessage" USING btree (e
|
|||
CREATE INDEX idxbn8t4wp85fgxjl8q4ctlscx55 ON public."Contact" USING btree (current_sponsor_registrar_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: idxd01j17vrpjxaerxdmn8bwxs7s; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE INDEX idxd01j17vrpjxaerxdmn8bwxs7s ON public."GracePeriodHistory" USING btree (domain_repo_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: idxe7wu46c7wpvfmfnj4565abibp; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
@ -1816,6 +1850,14 @@ ALTER TABLE ONLY public."ClaimsEntry"
|
|||
ADD CONSTRAINT fk6sc6at5hedffc0nhdcab6ivuq FOREIGN KEY (revision_id) REFERENCES public."ClaimsList"(revision_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: GracePeriodHistory fk7w3cx8d55q8bln80e716tr7b8; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public."GracePeriodHistory"
|
||||
ADD CONSTRAINT fk7w3cx8d55q8bln80e716tr7b8 FOREIGN KEY (domain_repo_id, domain_history_revision_id) REFERENCES public."DomainHistory"(domain_repo_id, history_revision_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: Contact fk93c185fx7chn68uv7nl6uv2s0; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
|
Loading…
Add table
Reference in a new issue