1
0
mirror of https://github.com/google/nomulus synced 2026-05-25 01:01:57 +00:00

Compare commits

..

6 Commits

Author SHA1 Message Date
Ben McIlwain
e4312322dc Add a no-async actions DB migration phase (#1579)
* Add a no-async actions DB migration phase

This needs to be set several hours prior to entering the READONLY stage. This is
not a read-only stage; all synchronous actions under Datastore (such as domain
creates) will continue to succeed. The only thing that will fail is host
deletes, host renames, and contact deletes, as these three actions require a
mapreduce to run before they are complete, and we don't want mapreduces hanging
around and executing during what is supposed to be a short duration READONLY
period.
2022-04-01 16:55:51 -04:00
gbrodman
24dfaf6406 Use UrlFetch for RDE and default TLS (1.2) for other URL connections (#1578)
* Use UrlFetch for RDE and default TLS (1.2) for other URL connections

This removes the TLS 1.3-settings in the module providers and,
essentially, reverts the changes in #1535 only to the RdeReporter and
RdeReportActionTest
2022-03-31 14:08:28 -04:00
Rachel Guan
7afb8fa343 Add default value to renewal_price_behavior (#1575)
* Add default value to renewal_price_behavior

* Change DEFAULT_PRICE to DEFAULT
2022-03-31 12:27:32 -04:00
Michael Muller
02b3f7b505 Fix a few references to "Datastore" in comments (#1576)
* Fix a few references to "Datastore" in comments

Fix references to Datastore in the comments of classes that are now SQL-only.
2022-03-30 15:17:38 -04:00
Lai Jiang
25342aa480 Make a best effort guess on the RDE folder name (prefix) when not provided. (#1574)
We have a cron job that runs the RDE upload action every 4 hours for all
TLD. Normally this should be a no-op beacuse a RDE upload is scheduled
after RDE staging is completed, and when it fails with non-2XX status it
will retry. However if for some reason it failed due to 20X status (like
waiting for the SFTP cursor), it will not retry but rely on the cron job to
catch up.

With the BEAM RDE pipeline every staging job saves all its deposits in a
uniquely named folder to avoid the need to use a lock, which is not
practical in BEAM. However the cron job has no way of knowing what the
prefixes are for each TLD so it will fail in SQL mode.

In this PR we implemented a logic to guess what the prefix should be and
use it, if we are in SQL mode and a prefix is not provided.

<!-- Reviewable:start -->
---
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/google/nomulus/1574)
<!-- Reviewable:end -->
2022-03-30 11:36:24 -04:00
Rachel Guan
3ef1e6c6a4 Add renewal columns in BillingRecurrence (#1568)
* Add renewal columns in BillingRecurrence

* Change from event to recurrence in file name
2022-03-28 17:42:01 -04:00
32 changed files with 646 additions and 268 deletions

View File

@@ -25,6 +25,7 @@ import static google.registry.model.ResourceTransferUtils.handlePendingTransferO
import static google.registry.model.eppoutput.Result.Code.SUCCESS;
import static google.registry.model.eppoutput.Result.Code.SUCCESS_WITH_ACTION_PENDING;
import static google.registry.model.transfer.TransferStatus.SERVER_CANCELLED;
import static google.registry.persistence.transaction.TransactionManagerFactory.assertAsyncActionsAreAllowed;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.google.common.collect.ImmutableSet;
@@ -94,6 +95,7 @@ public final class ContactDeleteFlow implements TransactionalFlow {
extensionManager.register(MetadataExtension.class);
validateRegistrarIsLoggedIn(registrarId);
extensionManager.validate();
assertAsyncActionsAreAllowed();
DateTime now = tm().getTransactionTime();
checkLinkedDomains(targetId, now, ContactResource.class, DomainBase::getReferencedContacts);
ContactResource existingContact = loadAndVerifyExistence(ContactResource.class, targetId, now);

View File

@@ -22,6 +22,7 @@ import static google.registry.flows.ResourceFlowUtils.verifyResourceOwnership;
import static google.registry.flows.host.HostFlowUtils.validateHostName;
import static google.registry.model.eppoutput.Result.Code.SUCCESS;
import static google.registry.model.eppoutput.Result.Code.SUCCESS_WITH_ACTION_PENDING;
import static google.registry.persistence.transaction.TransactionManagerFactory.assertAsyncActionsAreAllowed;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.google.common.collect.ImmutableSet;
@@ -96,6 +97,7 @@ public final class HostDeleteFlow implements TransactionalFlow {
extensionManager.register(MetadataExtension.class);
validateRegistrarIsLoggedIn(registrarId);
extensionManager.validate();
assertAsyncActionsAreAllowed();
DateTime now = tm().getTransactionTime();
validateHostName(targetId);
checkLinkedDomains(targetId, now, HostResource.class, DomainBase::getNameservers);

View File

@@ -28,6 +28,7 @@ import static google.registry.flows.host.HostFlowUtils.verifySuperordinateDomain
import static google.registry.flows.host.HostFlowUtils.verifySuperordinateDomainOwnership;
import static google.registry.model.index.ForeignKeyIndex.loadAndGetKey;
import static google.registry.model.reporting.HistoryEntry.Type.HOST_UPDATE;
import static google.registry.persistence.transaction.TransactionManagerFactory.assertAsyncActionsAreAllowed;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.CollectionUtils.isNullOrEmpty;
@@ -136,6 +137,9 @@ public final class HostUpdateFlow implements TransactionalFlow {
validateHostName(targetId);
HostResource existingHost = loadAndVerifyExistence(HostResource.class, targetId, now);
boolean isHostRename = suppliedNewHostName != null;
if (isHostRename) {
assertAsyncActionsAreAllowed();
}
String oldHostName = targetId;
String newHostName = firstNonNull(suppliedNewHostName, oldHostName);
DomainBase oldSuperordinateDomain =

View File

@@ -30,6 +30,7 @@ import google.registry.model.annotations.DeleteAfterMigration;
import google.registry.model.common.TimedTransitionProperty.TimedTransition;
import google.registry.model.replay.SqlOnlyEntity;
import java.time.Duration;
import java.util.Arrays;
import javax.persistence.Entity;
import javax.persistence.PersistenceException;
import org.joda.time.DateTime;
@@ -62,11 +63,28 @@ public class DatabaseMigrationStateSchedule extends CrossTldSingleton implements
* not the phase is read-only.
*/
public enum MigrationState {
/** Datastore is the only DB being used. */
DATASTORE_ONLY(PrimaryDatabase.DATASTORE, false, ReplayDirection.NO_REPLAY),
/** Datastore is the primary DB, with changes replicated to Cloud SQL. */
DATASTORE_PRIMARY(PrimaryDatabase.DATASTORE, false, ReplayDirection.DATASTORE_TO_SQL),
/** Datastore is the primary DB, with replication, and async actions are disallowed. */
DATASTORE_PRIMARY_NO_ASYNC(PrimaryDatabase.DATASTORE, false, ReplayDirection.DATASTORE_TO_SQL),
/** Datastore is the primary DB, with replication, and all mutating actions are disallowed. */
DATASTORE_PRIMARY_READ_ONLY(PrimaryDatabase.DATASTORE, true, ReplayDirection.DATASTORE_TO_SQL),
/**
* Cloud SQL is the primary DB, with replication back to Datastore, and all mutating actions are
* disallowed.
*/
SQL_PRIMARY_READ_ONLY(PrimaryDatabase.CLOUD_SQL, true, ReplayDirection.SQL_TO_DATASTORE),
/** Cloud SQL is the primary DB, with changes replicated to Datastore. */
SQL_PRIMARY(PrimaryDatabase.CLOUD_SQL, false, ReplayDirection.SQL_TO_DATASTORE),
/** Cloud SQL is the only DB being used. */
SQL_ONLY(PrimaryDatabase.CLOUD_SQL, false, ReplayDirection.NO_REPLAY);
private final PrimaryDatabase primaryDatabase;
@@ -146,11 +164,17 @@ public class DatabaseMigrationStateSchedule extends CrossTldSingleton implements
.putAll(
MigrationState.DATASTORE_PRIMARY,
MigrationState.DATASTORE_ONLY,
MigrationState.DATASTORE_PRIMARY_NO_ASYNC)
.putAll(
MigrationState.DATASTORE_PRIMARY_NO_ASYNC,
MigrationState.DATASTORE_ONLY,
MigrationState.DATASTORE_PRIMARY,
MigrationState.DATASTORE_PRIMARY_READ_ONLY)
.putAll(
MigrationState.DATASTORE_PRIMARY_READ_ONLY,
MigrationState.DATASTORE_ONLY,
MigrationState.DATASTORE_PRIMARY,
MigrationState.DATASTORE_PRIMARY_NO_ASYNC,
MigrationState.SQL_PRIMARY_READ_ONLY,
MigrationState.SQL_PRIMARY)
.putAll(
@@ -165,10 +189,9 @@ public class DatabaseMigrationStateSchedule extends CrossTldSingleton implements
MigrationState.SQL_ONLY,
MigrationState.SQL_PRIMARY_READ_ONLY,
MigrationState.SQL_PRIMARY);
// In addition, we can always transition from a state to itself (useful when updating the map).
for (MigrationState migrationState : MigrationState.values()) {
builder.put(migrationState, migrationState);
}
Arrays.stream(MigrationState.values()).forEach(state -> builder.put(state, state));
return builder.build();
}
@@ -246,7 +269,7 @@ public class DatabaseMigrationStateSchedule extends CrossTldSingleton implements
* A provided map of transitions may be valid by itself (i.e. it shifts states properly, doesn't
* skip states, and doesn't backtrack incorrectly) while still being invalid. In addition to the
* transitions in the map being valid, the single transition from the current map at the current
* time to the new map at the current time time must also be valid.
* time to the new map at the current time must also be valid.
*/
private static void validateTransitionAtCurrentTime(
TimedTransitionProperty<MigrationState, MigrationStateTransition> newTransitions) {

View File

@@ -70,8 +70,7 @@ public class ClaimsList extends ImmutableObject implements SqlOnlyEntity {
*
* <p>Note that the value of this field is parsed from the claims list file(See this <a
* href="https://tools.ietf.org/html/draft-lozano-tmch-func-spec-08#section-6.1">RFC</>), it is
* the DNL List creation datetime from the rfc. Since this field has been used by Datastore, we
* cannot change its name until we finish the migration.
* the DNL List creation datetime from the rfc.
*
* <p>TODO(b/177567432): Rename this field to tmdbGenerationTime.
*/

View File

@@ -47,7 +47,7 @@ public final class TmchCrl extends CrossTldSingleton implements SqlOnlyEntity {
* Change the singleton to a new ASCII-armored X.509 CRL.
*
* <p>Please do not call this function unless your CRL is properly formatted, signed by the root,
* and actually newer than the one currently in Datastore.
* and actually newer than the one currently in the database.
*/
public static void set(final String crl, final String url) {
jpaTm()

View File

@@ -44,6 +44,7 @@ import google.registry.request.Modules.DatastoreServiceModule;
import google.registry.request.Modules.Jackson2Module;
import google.registry.request.Modules.NetHttpTransportModule;
import google.registry.request.Modules.UrlConnectionServiceModule;
import google.registry.request.Modules.UrlFetchServiceModule;
import google.registry.request.Modules.UrlFetchTransportModule;
import google.registry.request.Modules.UserServiceModule;
import google.registry.request.auth.AuthModule;
@@ -81,6 +82,7 @@ import javax.inject.Singleton;
SheetsServiceModule.class,
StackdriverModule.class,
UrlConnectionServiceModule.class,
UrlFetchServiceModule.class,
UrlFetchTransportModule.class,
UserServiceModule.class,
VoidDnsWriterModule.class,

View File

@@ -15,6 +15,7 @@
package google.registry.persistence.transaction;
import static com.google.common.base.Preconditions.checkState;
import static google.registry.model.common.DatabaseMigrationStateSchedule.MigrationState.DATASTORE_PRIMARY_NO_ASYNC;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import static org.joda.time.DateTimeZone.UTC;
@@ -23,6 +24,7 @@ import com.google.appengine.api.utils.SystemProperty.Environment.Value;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Suppliers;
import google.registry.config.RegistryEnvironment;
import google.registry.model.annotations.DeleteAfterMigration;
import google.registry.model.common.DatabaseMigrationStateSchedule;
import google.registry.model.common.DatabaseMigrationStateSchedule.PrimaryDatabase;
import google.registry.model.ofy.DatastoreTransactionManager;
@@ -198,6 +200,22 @@ public final class TransactionManagerFactory {
}
}
/**
* Asserts that async actions (contact/host deletes and host renames) are allowed.
*
* <p>These are allowed at all times except during the {@link
* DatabaseMigrationStateSchedule.MigrationState#DATASTORE_PRIMARY_NO_ASYNC} stage. Note that
* {@link ReadOnlyModeException} may well be thrown during other read-only stages inside the
* transaction manager; this method specifically checks only async actions.
*/
@DeleteAfterMigration
public static void assertAsyncActionsAreAllowed() {
if (DatabaseMigrationStateSchedule.getValueAtTime(DateTime.now(UTC))
.equals(DATASTORE_PRIMARY_NO_ASYNC)) {
throw new ReadOnlyModeException();
}
}
/** Registry is currently undergoing maintenance and is in read-only mode. */
public static class ReadOnlyModeException extends IllegalStateException {
public ReadOnlyModeException() {

View File

@@ -14,24 +14,25 @@
package google.registry.rde;
import static google.registry.request.UrlConnectionUtils.getResponseBytes;
import static google.registry.request.UrlConnectionUtils.setBasicAuth;
import static google.registry.request.UrlConnectionUtils.setPayload;
import static com.google.appengine.api.urlfetch.FetchOptions.Builder.validateCertificate;
import static com.google.appengine.api.urlfetch.HTTPMethod.PUT;
import static com.google.common.io.BaseEncoding.base64;
import static com.google.common.net.HttpHeaders.AUTHORIZATION;
import static com.google.common.net.HttpHeaders.CONTENT_TYPE;
import static google.registry.util.DomainNameUtils.canonicalizeDomainName;
import static java.nio.charset.StandardCharsets.UTF_8;
import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import static javax.servlet.http.HttpServletResponse.SC_OK;
import com.google.api.client.http.HttpMethods;
import com.google.appengine.api.urlfetch.HTTPHeader;
import com.google.appengine.api.urlfetch.HTTPRequest;
import com.google.appengine.api.urlfetch.HTTPResponse;
import com.google.appengine.api.urlfetch.URLFetchService;
import com.google.common.flogger.FluentLogger;
import com.google.common.net.MediaType;
import google.registry.config.RegistryConfig.Config;
import google.registry.keyring.api.KeyModule.Key;
import google.registry.request.HttpException.InternalServerErrorException;
import google.registry.request.UrlConnectionService;
import google.registry.util.Retrier;
import google.registry.util.UrlConnectionException;
import google.registry.xjc.XjcXmlTransformer;
import google.registry.xjc.iirdea.XjcIirdeaResponseElement;
import google.registry.xjc.iirdea.XjcIirdeaResult;
@@ -39,7 +40,6 @@ import google.registry.xjc.rdeheader.XjcRdeHeader;
import google.registry.xjc.rdereport.XjcRdeReportReport;
import google.registry.xml.XmlException;
import java.io.ByteArrayInputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.SocketTimeoutException;
import java.net.URL;
@@ -58,10 +58,10 @@ public class RdeReporter {
* @see <a href="http://tools.ietf.org/html/draft-lozano-icann-registry-interfaces-05#section-4">
* ICANN Registry Interfaces - Interface details</a>
*/
private static final MediaType MEDIA_TYPE = MediaType.XML_UTF_8;
private static final String REPORT_MIME = "text/xml";
@Inject Retrier retrier;
@Inject UrlConnectionService urlConnectionService;
@Inject URLFetchService urlFetchService;
@Inject @Config("rdeReportUrlPrefix") String reportUrlPrefix;
@Inject @Key("icannReportingPassword") String password;
@@ -76,24 +76,29 @@ public class RdeReporter {
// Send a PUT request to ICANN's HTTPS server.
URL url = makeReportUrl(header.getTld(), report.getId());
String username = header.getTld() + "_ry";
String token = base64().encode(String.format("%s:%s", username, password).getBytes(UTF_8));
final HTTPRequest req = new HTTPRequest(url, PUT, validateCertificate().setDeadline(60d));
req.addHeader(new HTTPHeader(CONTENT_TYPE, REPORT_MIME));
req.addHeader(new HTTPHeader(AUTHORIZATION, "Basic " + token));
req.setPayload(reportBytes);
logger.atInfo().log("Sending report:\n%s", new String(reportBytes, UTF_8));
byte[] responseBytes =
HTTPResponse rsp =
retrier.callWithRetry(
() -> {
HttpURLConnection connection = urlConnectionService.createConnection(url);
connection.setRequestMethod(HttpMethods.PUT);
setBasicAuth(connection, username, password);
setPayload(connection, reportBytes, MEDIA_TYPE.toString());
int responseCode = connection.getResponseCode();
if (responseCode == SC_OK || responseCode == SC_BAD_REQUEST) {
return getResponseBytes(connection);
HTTPResponse rsp1 = urlFetchService.fetch(req);
switch (rsp1.getResponseCode()) {
case SC_OK:
case SC_BAD_REQUEST:
break;
default:
throw new RuntimeException("PUT failed");
}
throw new UrlConnectionException("PUT failed", connection);
return rsp1;
},
SocketTimeoutException.class);
// Ensure the XML response is valid.
XjcIirdeaResult result = parseResult(responseBytes);
XjcIirdeaResult result = parseResult(rsp.getContent());
if (result.getCode().getValue() != 1000) {
logger.atWarning().log(
"PUT rejected: %d %s\n%s",

View File

@@ -32,6 +32,7 @@ import static java.util.Arrays.asList;
import com.google.cloud.storage.BlobId;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Ordering;
import com.google.common.flogger.FluentLogger;
import com.google.common.io.ByteStreams;
import com.jcraft.jsch.JSch;
@@ -122,7 +123,6 @@ public final class RdeUploadAction implements Runnable, EscrowTask {
@Inject @Key("rdeSigningKey") PGPKeyPair signingKey;
@Inject @Key("rdeStagingDecryptionKey") PGPPrivateKey stagingDecryptionKey;
@Inject RdeUploadAction() {}
@Override
public void run() {
logger.atInfo().log("Attempting to acquire RDE upload lock for TLD '%s'.", tld);
@@ -140,6 +140,27 @@ public final class RdeUploadAction implements Runnable, EscrowTask {
@Override
public void runWithLock(final DateTime watermark) throws Exception {
// If a prefix is not provided, but we are in SQL mode, try to determine the prefix. This should
// only happen when the RDE upload cron job runs to catch up any un-retried (i. e. expected)
// RDE failures.
if (prefix.isEmpty() && !tm().isOfy()) {
// The prefix is always in the format of: rde-2022-02-21t00-00-00z-2022-02-21t00-07-33z, where
// the first datetime is the watermark and the second one is the time when the RDE beam job
// launched. We search for the latest folder that starts with "rde-[watermark]".
String partialPrefix =
String.format("rde-%s", watermark.toString("yyyy-MM-dd't'HH-mm-ss'z'"));
String latestFilenameSuffix =
gcsUtils.listFolderObjects(bucket, partialPrefix).stream()
.max(Ordering.natural())
.orElse(null);
if (latestFilenameSuffix == null) {
throw new NoContentException(
String.format("RDE deposit for TLD %s on %s does not exist", tld, watermark));
}
int firstSlashPosition = latestFilenameSuffix.indexOf('/');
prefix =
Optional.of(partialPrefix + latestFilenameSuffix.substring(0, firstSlashPosition + 1));
}
logger.atInfo().log("Verifying readiness to upload the RDE deposit.");
Optional<Cursor> cursor =
transactIfJpaTm(() -> tm().loadByKeyIfPresent(Cursor.createVKey(RDE_STAGING, tld)));
@@ -241,9 +262,9 @@ public final class RdeUploadAction implements Runnable, EscrowTask {
.setSignatureOutput(sigOut, signingKey)
.setFileMetadata(nameWithoutPrefix, xmlLength, watermark)
.build()) {
long bytesCopied = ByteStreams.copy(ghostrydeDecoder, rydeEncoder);
long bytesCopied = ByteStreams.copy(ghostrydeDecoder, rydeEncoder);
logger.atInfo().log("Uploaded %,d bytes to path '%s'.", bytesCopied, rydeFilename);
}
}
String sigFilename = nameWithoutPrefix + ".sig";
BlobId sigGcsFilename = BlobId.of(bucket, name + ".sig");
byte[] signature = sigOut.toByteArray();

View File

@@ -23,14 +23,14 @@ import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.urlfetch.URLFetchService;
import com.google.appengine.api.urlfetch.URLFetchServiceFactory;
import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;
import dagger.Module;
import dagger.Provides;
import java.net.HttpURLConnection;
import javax.inject.Singleton;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
/** Dagger modules for App Engine services and other vendor classes. */
public final class Modules {
@@ -51,16 +51,18 @@ public final class Modules {
public static final class UrlConnectionServiceModule {
@Provides
static UrlConnectionService provideUrlConnectionService() {
return url -> {
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
if (connection instanceof HttpsURLConnection) {
HttpsURLConnection httpsConnection = (HttpsURLConnection) connection;
SSLContext tls13Context = SSLContext.getInstance("TLSv1.3");
tls13Context.init(null, null, null);
httpsConnection.setSSLSocketFactory(tls13Context.getSocketFactory());
}
return connection;
};
return url -> (HttpURLConnection) url.openConnection();
}
}
/** Dagger module for {@link URLFetchService}. */
@Module
public static final class UrlFetchServiceModule {
private static final URLFetchService fetchService = URLFetchServiceFactory.getURLFetchService();
@Provides
static URLFetchService provideUrlFetchService() {
return fetchService;
}
}

View File

@@ -41,8 +41,8 @@ import javax.inject.Inject;
* Helper methods for accessing ICANN's TMCH root certificate and revocation list.
*
* <p>There are two CRLs, a real one for the production environment and a pilot one for
* non-production environments. The Datastore singleton {@link TmchCrl} entity is used to cache this
* CRL once loaded and will always contain the proper one corresponding to the environment.
* non-production environments. The singleton {@link TmchCrl} entity is used to cache this CRL once
* loaded and will always contain the proper one corresponding to the environment.
*
* <p>The CRTs do not change and are included as files in the codebase that are not refreshed. They
* were downloaded from https://ca.icann.org/tmch.crt and https://ca.icann.org/tmch_pilot.crt
@@ -66,7 +66,7 @@ public final class TmchCertificateAuthority {
}
/**
* A cached supplier that loads the CRL from Datastore or chooses a default value.
* A cached supplier that loads the CRL from the database or chooses a default value.
*
* <p>We keep the cache here rather than caching TmchCrl in the model, because loading the CRL
* string into an X509CRL instance is expensive and should itself be cached.
@@ -132,7 +132,7 @@ public final class TmchCertificateAuthority {
}
/**
* Update to the latest TMCH X.509 certificate revocation list and save it to Datastore.
* Update to the latest TMCH X.509 certificate revocation list and save it to the database.
*
* <p>Your ASCII-armored CRL must be signed by the current ICANN root certificate.
*

View File

@@ -40,7 +40,7 @@ public final class TmchCrlAction implements Runnable {
@Inject TmchCertificateAuthority tmchCertificateAuthority;
@Inject TmchCrlAction() {}
/** Synchronously fetches latest ICANN TMCH CRL and saves it to Datastore. */
/** Synchronously fetches latest ICANN TMCH CRL and saves it to the database. */
@Override
public void run() {
try {

View File

@@ -40,6 +40,7 @@ import google.registry.rde.RdeModule;
import google.registry.request.Modules.DatastoreServiceModule;
import google.registry.request.Modules.Jackson2Module;
import google.registry.request.Modules.UrlConnectionServiceModule;
import google.registry.request.Modules.UrlFetchServiceModule;
import google.registry.request.Modules.UserServiceModule;
import google.registry.tools.AuthModule.LocalCredentialModule;
import google.registry.tools.javascrap.CompareEscrowDepositsCommand;
@@ -80,6 +81,7 @@ import javax.inject.Singleton;
RequestFactoryModule.class,
SecretManagerModule.class,
UrlConnectionServiceModule.class,
UrlFetchServiceModule.class,
UserServiceModule.class,
UtilsModule.class,
VoidDnsWriterModule.class,

View File

@@ -254,6 +254,15 @@ class ContactDeleteFlowTest extends ResourceFlowTestCase<ContactDeleteFlow, Cont
assertIcannReportingActivityFieldLogged("srs-cont-delete");
}
@TestOfyOnly
void testModification_duringNoAsyncPhase() throws Exception {
persistActiveContact(getUniqueIdFromCommand());
DatabaseHelper.setMigrationScheduleToDatastorePrimaryNoAsync(clock);
EppException thrown = assertThrows(ReadOnlyModeEppException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
DatabaseHelper.removeDatabaseMigrationSchedule();
}
@TestOfyOnly
void testModification_duringReadOnlyPhase() throws Exception {
persistActiveContact(getUniqueIdFromCommand());

View File

@@ -840,6 +840,15 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
doSuccessfulTest();
}
@TestOfyOnly
void testSuccess_inNoAsyncPhase() throws Exception {
DatabaseHelper.setMigrationScheduleToDatastorePrimaryNoAsync(clock);
persistContactsAndHosts();
runFlowAssertResponse(
loadFile("domain_create_response_noasync.xml", ImmutableMap.of("DOMAIN", "example.tld")));
DatabaseHelper.removeDatabaseMigrationSchedule();
}
@TestOfyAndSql
void testSuccess_maxNumberOfNameservers() throws Exception {
setEppInput("domain_create_13_nameservers.xml");

View File

@@ -358,6 +358,15 @@ class HostDeleteFlowTest extends ResourceFlowTestCase<HostDeleteFlow, HostResour
DatabaseHelper.removeDatabaseMigrationSchedule();
}
@TestOfyOnly
void testModification_duringNoAsyncPhase() {
persistActiveHost("ns1.example.tld");
DatabaseHelper.setMigrationScheduleToDatastorePrimaryNoAsync(clock);
EppException thrown = assertThrows(ReadOnlyModeEppException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
DatabaseHelper.removeDatabaseMigrationSchedule();
}
private void assertOfyDeleteSuccess(String registrarId, String clientTrid, boolean isSuperuser)
throws Exception {
HostResource deletedHost = reloadResourceByForeignKey();

View File

@@ -1355,7 +1355,43 @@ class HostUpdateFlowTest extends ResourceFlowTestCase<HostUpdateFlow, HostResour
}
@TestOfyOnly
void testModification_duringReadOnlyPhase() throws Exception {
void testSuccess_nonHostRename_inNoAsyncPhase_succeeds() throws Exception {
setEppInput("host_update_name_unchanged.xml");
createTld("tld");
DatabaseHelper.setMigrationScheduleToDatastorePrimaryNoAsync(clock);
DomainBase domain = persistActiveDomain("example.tld");
HostResource oldHost = persistActiveSubordinateHost(oldHostName(), domain);
clock.advanceOneMilli();
runFlowAssertResponse(loadFile("generic_success_response.xml"));
// The example xml doesn't do a host rename, so reloading the host should work.
assertAboutHosts()
.that(reloadResourceByForeignKey())
.hasLastSuperordinateChange(oldHost.getLastSuperordinateChange())
.and()
.hasSuperordinateDomain(domain.createVKey())
.and()
.hasPersistedCurrentSponsorRegistrarId("TheRegistrar")
.and()
.hasLastTransferTime(null)
.and()
.hasOnlyOneHistoryEntryWhich()
.hasType(HistoryEntry.Type.HOST_UPDATE);
assertDnsTasksEnqueued("ns1.example.tld");
DatabaseHelper.removeDatabaseMigrationSchedule();
}
@TestOfyOnly
void testRename_duringNoAsyncPhase_fails() throws Exception {
createTld("tld");
persistActiveSubordinateHost(oldHostName(), persistActiveDomain("example.tld"));
DatabaseHelper.setMigrationScheduleToDatastorePrimaryNoAsync(clock);
EppException thrown = assertThrows(ReadOnlyModeEppException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
DatabaseHelper.removeDatabaseMigrationSchedule();
}
@TestOfyOnly
void testModification_duringReadOnlyPhase_fails() throws Exception {
createTld("tld");
persistActiveSubordinateHost(oldHostName(), persistActiveDomain("example.tld"));
DatabaseHelper.setMigrationScheduleToDatastorePrimaryReadOnly(clock);

View File

@@ -17,6 +17,7 @@ package google.registry.model.common;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.common.DatabaseMigrationStateSchedule.MigrationState.DATASTORE_ONLY;
import static google.registry.model.common.DatabaseMigrationStateSchedule.MigrationState.DATASTORE_PRIMARY;
import static google.registry.model.common.DatabaseMigrationStateSchedule.MigrationState.DATASTORE_PRIMARY_NO_ASYNC;
import static google.registry.model.common.DatabaseMigrationStateSchedule.MigrationState.DATASTORE_PRIMARY_READ_ONLY;
import static google.registry.model.common.DatabaseMigrationStateSchedule.MigrationState.SQL_ONLY;
import static google.registry.model.common.DatabaseMigrationStateSchedule.MigrationState.SQL_PRIMARY;
@@ -71,10 +72,12 @@ public class DatabaseMigrationStateScheduleTest extends EntityTestCase {
runValidTransition(DATASTORE_ONLY, DATASTORE_PRIMARY);
runValidTransition(DATASTORE_PRIMARY, DATASTORE_ONLY);
runValidTransition(DATASTORE_PRIMARY, DATASTORE_PRIMARY_READ_ONLY);
runValidTransition(DATASTORE_PRIMARY, DATASTORE_PRIMARY_NO_ASYNC);
runValidTransition(DATASTORE_PRIMARY_NO_ASYNC, DATASTORE_PRIMARY_READ_ONLY);
runValidTransition(DATASTORE_PRIMARY_READ_ONLY, DATASTORE_ONLY);
runValidTransition(DATASTORE_PRIMARY_READ_ONLY, DATASTORE_PRIMARY);
runValidTransition(DATASTORE_PRIMARY_READ_ONLY, DATASTORE_PRIMARY_NO_ASYNC);
runValidTransition(DATASTORE_PRIMARY_READ_ONLY, SQL_PRIMARY_READ_ONLY);
runValidTransition(DATASTORE_PRIMARY_READ_ONLY, SQL_PRIMARY);
@@ -94,6 +97,7 @@ public class DatabaseMigrationStateScheduleTest extends EntityTestCase {
runInvalidTransition(DATASTORE_ONLY, SQL_PRIMARY);
runInvalidTransition(DATASTORE_ONLY, SQL_ONLY);
runInvalidTransition(DATASTORE_PRIMARY, DATASTORE_PRIMARY_READ_ONLY);
runInvalidTransition(DATASTORE_PRIMARY, SQL_PRIMARY_READ_ONLY);
runInvalidTransition(DATASTORE_PRIMARY, SQL_PRIMARY);
runInvalidTransition(DATASTORE_PRIMARY, SQL_ONLY);
@@ -124,7 +128,8 @@ public class DatabaseMigrationStateScheduleTest extends EntityTestCase {
ImmutableSortedMap.<DateTime, MigrationState>naturalOrder()
.put(START_OF_TIME, DATASTORE_ONLY)
.put(startTime.plusHours(1), DATASTORE_PRIMARY)
.put(startTime.plusHours(2), DATASTORE_PRIMARY_READ_ONLY)
.put(startTime.plusHours(2), DATASTORE_PRIMARY_NO_ASYNC)
.put(startTime.plusHours(3), DATASTORE_PRIMARY_READ_ONLY)
.build();
assertThat(
assertThrows(
@@ -163,7 +168,8 @@ public class DatabaseMigrationStateScheduleTest extends EntityTestCase {
fakeClock.setTo(START_OF_TIME.plusDays(1));
AllocationToken token =
new AllocationToken.Builder().setToken("token").setTokenType(TokenType.SINGLE_USE).build();
runValidTransition(DATASTORE_PRIMARY, DATASTORE_PRIMARY_READ_ONLY);
runValidTransition(DATASTORE_PRIMARY, DATASTORE_PRIMARY_NO_ASYNC);
runValidTransition(DATASTORE_PRIMARY_NO_ASYNC, DATASTORE_PRIMARY_READ_ONLY);
assertThrows(ReadOnlyModeException.class, () -> persistResource(token));
runValidTransition(DATASTORE_PRIMARY_READ_ONLY, SQL_PRIMARY_READ_ONLY);
assertThrows(ReadOnlyModeException.class, () -> persistResource(token));

View File

@@ -366,8 +366,9 @@ public class ReplicateToDatastoreActionTest {
ImmutableSortedMap.<DateTime, MigrationState>naturalOrder()
.put(START_OF_TIME, MigrationState.DATASTORE_ONLY)
.put(START_OF_TIME.plusHours(1), MigrationState.DATASTORE_PRIMARY)
.put(START_OF_TIME.plusHours(2), MigrationState.DATASTORE_PRIMARY_READ_ONLY)
.put(START_OF_TIME.plusHours(3), MigrationState.SQL_PRIMARY)
.put(START_OF_TIME.plusHours(2), MigrationState.DATASTORE_PRIMARY_NO_ASYNC)
.put(START_OF_TIME.plusHours(3), MigrationState.DATASTORE_PRIMARY_READ_ONLY)
.put(START_OF_TIME.plusHours(4), MigrationState.SQL_PRIMARY)
.put(now.plusHours(1), MigrationState.SQL_PRIMARY_READ_ONLY)
.put(now.plusHours(2), MigrationState.DATASTORE_PRIMARY_READ_ONLY)
.put(now.plusHours(3), MigrationState.DATASTORE_PRIMARY)

View File

@@ -47,7 +47,9 @@ public class DatabaseMigrationScheduleTransitionConverterTest {
MigrationState.DATASTORE_ONLY,
DateTime.parse("2001-01-01T00:00:00.0Z"),
MigrationState.DATASTORE_PRIMARY,
DateTime.parse("2002-01-01T00:00:00.0Z"),
DateTime.parse("2002-01-01T01:00:00.0Z"),
MigrationState.DATASTORE_PRIMARY_NO_ASYNC,
DateTime.parse("2002-01-01T02:00:00.0Z"),
MigrationState.DATASTORE_PRIMARY_READ_ONLY,
DateTime.parse("2002-01-02T00:00:00.0Z"),
MigrationState.SQL_PRIMARY,

View File

@@ -14,6 +14,7 @@
package google.registry.rde;
import static com.google.appengine.api.urlfetch.HTTPMethod.PUT;
import static com.google.common.net.MediaType.PLAIN_TEXT_UTF_8;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.common.Cursor.CursorType.RDE_REPORT;
@@ -28,13 +29,20 @@ import static javax.servlet.http.HttpServletResponse.SC_OK;
import static org.joda.time.Duration.standardDays;
import static org.joda.time.Duration.standardSeconds;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import com.google.appengine.api.urlfetch.HTTPHeader;
import com.google.appengine.api.urlfetch.HTTPRequest;
import com.google.appengine.api.urlfetch.HTTPResponse;
import com.google.appengine.api.urlfetch.URLFetchService;
import com.google.cloud.storage.BlobId;
import com.google.cloud.storage.contrib.nio.testing.LocalStorageHelper;
import com.google.common.base.Ascii;
import com.google.common.collect.ImmutableMap;
import com.google.common.io.ByteSource;
import google.registry.gcs.GcsUtils;
import google.registry.model.common.Cursor;
@@ -49,21 +57,20 @@ import google.registry.testing.FakeClock;
import google.registry.testing.FakeKeyringModule;
import google.registry.testing.FakeResponse;
import google.registry.testing.FakeSleeper;
import google.registry.testing.FakeUrlConnectionService;
import google.registry.testing.TestOfyAndSql;
import google.registry.util.Retrier;
import google.registry.xjc.XjcXmlTransformer;
import google.registry.xjc.rdereport.XjcRdeReportReport;
import google.registry.xml.XmlException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.net.HttpURLConnection;
import java.net.SocketTimeoutException;
import java.util.Map;
import java.util.Optional;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.mockito.ArgumentCaptor;
/** Unit tests for {@link RdeReportAction}. */
@DualDatabaseTest
@@ -82,21 +89,20 @@ public class RdeReportActionTest {
private final FakeResponse response = new FakeResponse();
private final EscrowTaskRunner runner = mock(EscrowTaskRunner.class);
private final URLFetchService urlFetchService = mock(URLFetchService.class);
private final ArgumentCaptor<HTTPRequest> request = ArgumentCaptor.forClass(HTTPRequest.class);
private final HTTPResponse httpResponse = mock(HTTPResponse.class);
private final PGPPublicKey encryptKey =
new FakeKeyringModule().get().getRdeStagingEncryptionKey();
private final GcsUtils gcsUtils = new GcsUtils(LocalStorageHelper.getOptions());
private final BlobId reportFile =
BlobId.of("tub", "test_2006-06-06_full_S1_R0-report.xml.ghostryde");
private final HttpURLConnection httpUrlConnection = mock(HttpURLConnection.class);
private final FakeUrlConnectionService urlConnectionService =
new FakeUrlConnectionService(httpUrlConnection);
private final ByteArrayOutputStream connectionOutputStream = new ByteArrayOutputStream();
private RdeReportAction createAction() {
RdeReporter reporter = new RdeReporter();
reporter.reportUrlPrefix = "https://rde-report.example";
reporter.urlFetchService = urlFetchService;
reporter.password = "foo";
reporter.urlConnectionService = urlConnectionService;
reporter.retrier = new Retrier(new FakeSleeper(new FakeClock()), 3);
RdeReportAction action = new RdeReportAction();
action.gcsUtils = gcsUtils;
@@ -121,7 +127,6 @@ public class RdeReportActionTest {
Cursor.create(RDE_UPLOAD, DateTime.parse("2006-06-07TZ"), Registry.get("test")));
gcsUtils.createFromBytes(reportFile, Ghostryde.encode(REPORT_XML.read(), encryptKey));
tm().transact(() -> RdeRevision.saveRevision("test", DateTime.parse("2006-06-06TZ"), FULL, 0));
when(httpUrlConnection.getOutputStream()).thenReturn(connectionOutputStream);
}
@TestOfyAndSql
@@ -137,22 +142,24 @@ public class RdeReportActionTest {
@TestOfyAndSql
void testRunWithLock() throws Exception {
when(httpUrlConnection.getResponseCode()).thenReturn(SC_OK);
when(httpUrlConnection.getInputStream()).thenReturn(IIRDEA_GOOD_XML.openStream());
when(httpResponse.getResponseCode()).thenReturn(SC_OK);
when(httpResponse.getContent()).thenReturn(IIRDEA_GOOD_XML.read());
when(urlFetchService.fetch(request.capture())).thenReturn(httpResponse);
createAction().runWithLock(loadRdeReportCursor());
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getContentType()).isEqualTo(PLAIN_TEXT_UTF_8);
assertThat(response.getPayload()).isEqualTo("OK test 2006-06-06T00:00:00.000Z\n");
// Verify the HTTP request was correct.
verify(httpUrlConnection).setRequestMethod("PUT");
assertThat(httpUrlConnection.getURL().getProtocol()).isEqualTo("https");
assertThat(httpUrlConnection.getURL().getPath()).endsWith("/test/20101017001");
verify(httpUrlConnection).setRequestProperty("Content-Type", "text/xml; charset=utf-8");
verify(httpUrlConnection).setRequestProperty("Authorization", "Basic dGVzdF9yeTpmb28=");
assertThat(request.getValue().getMethod()).isSameInstanceAs(PUT);
assertThat(request.getValue().getURL().getProtocol()).isEqualTo("https");
assertThat(request.getValue().getURL().getPath()).endsWith("/test/20101017001");
Map<String, String> headers = mapifyHeaders(request.getValue().getHeaders());
assertThat(headers).containsEntry("CONTENT_TYPE", "text/xml");
assertThat(headers).containsEntry("AUTHORIZATION", "Basic dGVzdF9yeTpmb28=");
// Verify the payload XML was the same as what's in testdata/report.xml.
XjcRdeReportReport report = parseReport(connectionOutputStream.toByteArray());
XjcRdeReportReport report = parseReport(request.getValue().getPayload());
assertThat(report.getId()).isEqualTo("20101017001");
assertThat(report.getCrDate()).isEqualTo(DateTime.parse("2010-10-17T00:15:00.0Z"));
assertThat(report.getWatermark()).isEqualTo(DateTime.parse("2010-10-17T00:00:00Z"));
@@ -160,8 +167,9 @@ public class RdeReportActionTest {
@TestOfyAndSql
void testRunWithLock_withPrefix() throws Exception {
when(httpUrlConnection.getResponseCode()).thenReturn(SC_OK);
when(httpUrlConnection.getInputStream()).thenReturn(IIRDEA_GOOD_XML.openStream());
when(httpResponse.getResponseCode()).thenReturn(SC_OK);
when(httpResponse.getContent()).thenReturn(IIRDEA_GOOD_XML.read());
when(urlFetchService.fetch(request.capture())).thenReturn(httpResponse);
RdeReportAction action = createAction();
action.prefix = Optional.of("job-name/");
gcsUtils.delete(reportFile);
@@ -174,14 +182,15 @@ public class RdeReportActionTest {
assertThat(response.getPayload()).isEqualTo("OK test 2006-06-06T00:00:00.000Z\n");
// Verify the HTTP request was correct.
verify(httpUrlConnection).setRequestMethod("PUT");
assertThat(httpUrlConnection.getURL().getProtocol()).isEqualTo("https");
assertThat(httpUrlConnection.getURL().getPath()).endsWith("/test/20101017001");
verify(httpUrlConnection).setRequestProperty("Content-Type", "text/xml; charset=utf-8");
verify(httpUrlConnection).setRequestProperty("Authorization", "Basic dGVzdF9yeTpmb28=");
assertThat(request.getValue().getMethod()).isSameInstanceAs(PUT);
assertThat(request.getValue().getURL().getProtocol()).isEqualTo("https");
assertThat(request.getValue().getURL().getPath()).endsWith("/test/20101017001");
Map<String, String> headers = mapifyHeaders(request.getValue().getHeaders());
assertThat(headers).containsEntry("CONTENT_TYPE", "text/xml");
assertThat(headers).containsEntry("AUTHORIZATION", "Basic dGVzdF9yeTpmb28=");
// Verify the payload XML was the same as what's in testdata/report.xml.
XjcRdeReportReport report = parseReport(connectionOutputStream.toByteArray());
XjcRdeReportReport report = parseReport(request.getValue().getPayload());
assertThat(report.getId()).isEqualTo("20101017001");
assertThat(report.getCrDate()).isEqualTo(DateTime.parse("2010-10-17T00:15:00.0Z"));
assertThat(report.getWatermark()).isEqualTo(DateTime.parse("2010-10-17T00:00:00Z"));
@@ -194,8 +203,9 @@ public class RdeReportActionTest {
PGPPublicKey encryptKey = new FakeKeyringModule().get().getRdeStagingEncryptionKey();
gcsUtils.createFromBytes(newReport, Ghostryde.encode(REPORT_XML.read(), encryptKey));
tm().transact(() -> RdeRevision.saveRevision("test", DateTime.parse("2006-06-06TZ"), FULL, 1));
when(httpUrlConnection.getResponseCode()).thenReturn(SC_OK);
when(httpUrlConnection.getInputStream()).thenReturn(IIRDEA_GOOD_XML.openStream());
when(httpResponse.getResponseCode()).thenReturn(SC_OK);
when(httpResponse.getContent()).thenReturn(IIRDEA_GOOD_XML.read());
when(urlFetchService.fetch(request.capture())).thenReturn(httpResponse);
createAction().runWithLock(loadRdeReportCursor());
assertThat(response.getStatus()).isEqualTo(200);
}
@@ -228,8 +238,9 @@ public class RdeReportActionTest {
@TestOfyAndSql
void testRunWithLock_badRequest_throws500WithErrorInfo() throws Exception {
when(httpUrlConnection.getResponseCode()).thenReturn(SC_BAD_REQUEST);
when(httpUrlConnection.getInputStream()).thenReturn(IIRDEA_BAD_XML.openStream());
when(httpResponse.getResponseCode()).thenReturn(SC_BAD_REQUEST);
when(httpResponse.getContent()).thenReturn(IIRDEA_BAD_XML.read());
when(urlFetchService.fetch(request.capture())).thenReturn(httpResponse);
InternalServerErrorException thrown =
assertThrows(
InternalServerErrorException.class,
@@ -240,17 +251,18 @@ public class RdeReportActionTest {
@TestOfyAndSql
void testRunWithLock_fetchFailed_throwsRuntimeException() throws Exception {
class ExpectedThrownException extends RuntimeException {}
when(httpUrlConnection.getResponseCode()).thenThrow(new ExpectedThrownException());
when(urlFetchService.fetch(any(HTTPRequest.class))).thenThrow(new ExpectedThrownException());
assertThrows(
ExpectedThrownException.class, () -> createAction().runWithLock(loadRdeReportCursor()));
}
@TestOfyAndSql
void testRunWithLock_socketTimeout_doesRetry() throws Exception {
when(httpUrlConnection.getInputStream()).thenReturn(IIRDEA_GOOD_XML.openStream());
when(httpUrlConnection.getResponseCode())
when(httpResponse.getResponseCode()).thenReturn(SC_OK);
when(httpResponse.getContent()).thenReturn(IIRDEA_GOOD_XML.read());
when(urlFetchService.fetch(request.capture()))
.thenThrow(new SocketTimeoutException())
.thenReturn(SC_OK);
.thenReturn(httpResponse);
createAction().runWithLock(loadRdeReportCursor());
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getContentType()).isEqualTo(PLAIN_TEXT_UTF_8);
@@ -261,6 +273,14 @@ public class RdeReportActionTest {
return loadByKey(Cursor.createVKey(RDE_REPORT, "test")).getCursorTime();
}
private static ImmutableMap<String, String> mapifyHeaders(Iterable<HTTPHeader> headers) {
ImmutableMap.Builder<String, String> builder = new ImmutableMap.Builder<>();
for (HTTPHeader header : headers) {
builder.put(Ascii.toUpperCase(header.getName().replace('-', '_')), header.getValue());
}
return builder.build();
}
private static XjcRdeReportReport parseReport(byte[] data) {
try {
return XjcXmlTransformer.unmarshal(XjcRdeReportReport.class, new ByteArrayInputStream(data));

View File

@@ -69,6 +69,8 @@ import google.registry.testing.FakeSleeper;
import google.registry.testing.GpgSystemCommandExtension;
import google.registry.testing.Lazies;
import google.registry.testing.TestOfyAndSql;
import google.registry.testing.TestOfyOnly;
import google.registry.testing.TestSqlOnly;
import google.registry.testing.sftp.SftpServerExtension;
import google.registry.util.Retrier;
import java.io.File;
@@ -91,6 +93,7 @@ public class RdeUploadActionTest {
private static final ByteSource REPORT_XML = RdeTestData.loadBytes("report.xml");
private static final ByteSource DEPOSIT_XML = RdeTestData.loadBytes("deposit_full.xml");
private static final String JOB_PREFIX = "rde-2010-10-17t00-00-00z";
private static final BlobId GHOSTRYDE_FILE =
BlobId.of("bucket", "tld_2010-10-17_full_S1_R0.xml.ghostryde");
@@ -99,11 +102,11 @@ public class RdeUploadActionTest {
private static final BlobId REPORT_FILE =
BlobId.of("bucket", "tld_2010-10-17_full_S1_R0-report.xml.ghostryde");
private static final BlobId GHOSTRYDE_FILE_WITH_PREFIX =
BlobId.of("bucket", "job-name/tld_2010-10-17_full_S1_R0.xml.ghostryde");
BlobId.of("bucket", JOB_PREFIX + "-job-name/tld_2010-10-17_full_S1_R0.xml.ghostryde");
private static final BlobId LENGTH_FILE_WITH_PREFIX =
BlobId.of("bucket", "job-name/tld_2010-10-17_full_S1_R0.xml.length");
BlobId.of("bucket", JOB_PREFIX + "-job-name/tld_2010-10-17_full_S1_R0.xml.length");
private static final BlobId REPORT_FILE_WITH_PREFIX =
BlobId.of("bucket", "job-name/tld_2010-10-17_full_S1_R0-report.xml.ghostryde");
BlobId.of("bucket", JOB_PREFIX + "-job-name/tld_2010-10-17_full_S1_R0-report.xml.ghostryde");
private static final BlobId GHOSTRYDE_R1_FILE =
BlobId.of("bucket", "tld_2010-10-17_full_S1_R1.xml.ghostryde");
@@ -181,7 +184,6 @@ public class RdeUploadActionTest {
return jschSpy;
}
@BeforeEach
void beforeEach() throws Exception {
// Force "development" mode so we don't try to really connect to GCS.
@@ -194,6 +196,13 @@ public class RdeUploadActionTest {
gcsUtils.createFromBytes(LENGTH_R1_FILE, Long.toString(DEPOSIT_XML.size()).getBytes(UTF_8));
gcsUtils.createFromBytes(REPORT_FILE, Ghostryde.encode(REPORT_XML.read(), encryptKey));
gcsUtils.createFromBytes(REPORT_R1_FILE, Ghostryde.encode(REPORT_XML.read(), encryptKey));
gcsUtils.createFromBytes(
GHOSTRYDE_FILE_WITH_PREFIX, Ghostryde.encode(DEPOSIT_XML.read(), encryptKey));
gcsUtils.createFromBytes(
LENGTH_FILE_WITH_PREFIX, Long.toString(DEPOSIT_XML.size()).getBytes(UTF_8));
gcsUtils.createFromBytes(
REPORT_FILE_WITH_PREFIX, Ghostryde.encode(REPORT_XML.read(), encryptKey));
tm().transact(
() -> {
RdeRevision.saveRevision("lol", DateTime.parse("2010-10-17TZ"), FULL, 0);
@@ -284,13 +293,36 @@ public class RdeUploadActionTest {
assertThat(thrown).hasMessageThat().contains("The crow flies in square circles.");
}
@TestOfyAndSql
@TestSqlOnly
void testRunWithLock_cannotGuessPrefix() throws Exception {
int port = sftpd.serve("user", "password", folder);
URI uploadUrl = URI.create(String.format("sftp://user:password@localhost:%d/", port));
DateTime stagingCursor = DateTime.parse("2010-10-18TZ");
DateTime uploadCursor = DateTime.parse("2010-10-17TZ");
persistResource(Cursor.create(RDE_STAGING, stagingCursor, Registry.get("tld")));
gcsUtils.delete(GHOSTRYDE_FILE_WITH_PREFIX);
gcsUtils.delete(LENGTH_FILE_WITH_PREFIX);
gcsUtils.delete(REPORT_FILE_WITH_PREFIX);
RdeUploadAction action = createAction(uploadUrl);
NoContentException thrown =
assertThrows(NoContentException.class, () -> action.runWithLock(uploadCursor));
assertThat(thrown)
.hasMessageThat()
.isEqualTo("RDE deposit for TLD tld on 2010-10-17T00:00:00.000Z does not exist");
cloudTasksHelper.assertNoTasksEnqueued("rde-upload");
assertThat(folder.list()).isEmpty();
}
@TestOfyOnly
void testRunWithLock_copiesOnGcs() throws Exception {
int port = sftpd.serve("user", "password", folder);
URI uploadUrl = URI.create(String.format("sftp://user:password@localhost:%d/", port));
DateTime stagingCursor = DateTime.parse("2010-10-18TZ");
DateTime uploadCursor = DateTime.parse("2010-10-17TZ");
persistResource(Cursor.create(RDE_STAGING, stagingCursor, Registry.get("tld")));
gcsUtils.delete(GHOSTRYDE_FILE_WITH_PREFIX);
gcsUtils.delete(LENGTH_FILE_WITH_PREFIX);
gcsUtils.delete(REPORT_FILE_WITH_PREFIX);
createAction(uploadUrl).runWithLock(uploadCursor);
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getContentType()).isEqualTo(PLAIN_TEXT_UTF_8);
@@ -306,7 +338,7 @@ public class RdeUploadActionTest {
.isEqualTo(Files.toByteArray(new File(folder, sigFilename)));
}
@TestOfyAndSql
@TestSqlOnly
void testRunWithLock_copiesOnGcs_withPrefix() throws Exception {
int port = sftpd.serve("user", "password", folder);
URI uploadUrl = URI.create(String.format("sftp://user:password@localhost:%d/", port));
@@ -314,16 +346,10 @@ public class RdeUploadActionTest {
DateTime uploadCursor = DateTime.parse("2010-10-17TZ");
persistResource(Cursor.create(RDE_STAGING, stagingCursor, Registry.get("tld")));
RdeUploadAction action = createAction(uploadUrl);
action.prefix = Optional.of("job-name/");
action.prefix = Optional.of(JOB_PREFIX + "-job-name/");
gcsUtils.delete(GHOSTRYDE_FILE);
gcsUtils.createFromBytes(
GHOSTRYDE_FILE_WITH_PREFIX, Ghostryde.encode(DEPOSIT_XML.read(), encryptKey));
gcsUtils.delete(LENGTH_FILE);
gcsUtils.createFromBytes(
LENGTH_FILE_WITH_PREFIX, Long.toString(DEPOSIT_XML.size()).getBytes(UTF_8));
gcsUtils.delete(REPORT_FILE);
gcsUtils.createFromBytes(
REPORT_FILE_WITH_PREFIX, Ghostryde.encode(REPORT_XML.read(), encryptKey));
action.runWithLock(uploadCursor);
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getContentType()).isEqualTo(PLAIN_TEXT_UTF_8);
@@ -331,9 +357,49 @@ public class RdeUploadActionTest {
cloudTasksHelper.assertNoTasksEnqueued("rde-upload");
// Assert that both files are written to SFTP and GCS, and that the contents are identical.
String rydeFilename = "tld_2010-10-17_full_S1_R0.ryde";
String rydeGcsFilename = "job-name/tld_2010-10-17_full_S1_R0.ryde";
String rydeGcsFilename = JOB_PREFIX + "-job-name/tld_2010-10-17_full_S1_R0.ryde";
String sigFilename = "tld_2010-10-17_full_S1_R0.sig";
String sigGcsFilename = "job-name/tld_2010-10-17_full_S1_R0.sig";
String sigGcsFilename = JOB_PREFIX + "-job-name/tld_2010-10-17_full_S1_R0.sig";
assertThat(folder.list()).asList().containsExactly(rydeFilename, sigFilename);
assertThat(gcsUtils.readBytesFrom(BlobId.of("bucket", rydeGcsFilename)))
.isEqualTo(Files.toByteArray(new File(folder, rydeFilename)));
assertThat(gcsUtils.readBytesFrom(BlobId.of("bucket", sigGcsFilename)))
.isEqualTo(Files.toByteArray(new File(folder, sigFilename)));
}
@TestSqlOnly
void testRunWithLock_copiesOnGcs_withoutPrefix() throws Exception {
int port = sftpd.serve("user", "password", folder);
URI uploadUrl = URI.create(String.format("sftp://user:password@localhost:%d/", port));
DateTime stagingCursor = DateTime.parse("2010-10-18TZ");
DateTime uploadCursor = DateTime.parse("2010-10-17TZ");
persistResource(Cursor.create(RDE_STAGING, stagingCursor, Registry.get("tld")));
RdeUploadAction action = createAction(uploadUrl);
gcsUtils.delete(GHOSTRYDE_FILE);
gcsUtils.delete(LENGTH_FILE);
gcsUtils.delete(REPORT_FILE);
// Add a folder that is alphabetically before the desired folder and fill it will nonsense data.
// It should NOT be picked up.
BlobId ghostrydeFileWithPrefixBefore =
BlobId.of("bucket", JOB_PREFIX + "-job-nama/tld_2010-10-17_full_S1_R0.xml.ghostryde");
BlobId lengthFileWithPrefixBefore =
BlobId.of("bucket", JOB_PREFIX + "-job-nama/tld_2010-10-17_full_S1_R0.xml.length");
BlobId reportFileWithPrefixBefore =
BlobId.of(
"bucket", JOB_PREFIX + "-job-nama/tld_2010-10-17_full_S1_R0-report.xml.ghostryde");
gcsUtils.createFromBytes(ghostrydeFileWithPrefixBefore, "foo".getBytes(UTF_8));
gcsUtils.createFromBytes(lengthFileWithPrefixBefore, "bar".getBytes(UTF_8));
gcsUtils.createFromBytes(reportFileWithPrefixBefore, "baz".getBytes(UTF_8));
action.runWithLock(uploadCursor);
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getContentType()).isEqualTo(PLAIN_TEXT_UTF_8);
assertThat(response.getPayload()).isEqualTo("OK tld 2010-10-17T00:00:00.000Z\n");
cloudTasksHelper.assertNoTasksEnqueued("rde-upload");
// Assert that both files are written to SFTP and GCS, and that the contents are identical.
String rydeFilename = "tld_2010-10-17_full_S1_R0.ryde";
String rydeGcsFilename = JOB_PREFIX + "-job-name/tld_2010-10-17_full_S1_R0.ryde";
String sigFilename = "tld_2010-10-17_full_S1_R0.sig";
String sigGcsFilename = JOB_PREFIX + "-job-name/tld_2010-10-17_full_S1_R0.sig";
assertThat(folder.list()).asList().containsExactly(rydeFilename, sigFilename);
assertThat(gcsUtils.readBytesFrom(BlobId.of("bucket", rydeGcsFilename)))
.isEqualTo(Files.toByteArray(new File(folder, rydeFilename)));
@@ -349,6 +415,19 @@ public class RdeUploadActionTest {
DateTime stagingCursor = DateTime.parse("2010-10-18TZ");
DateTime uploadCursor = DateTime.parse("2010-10-17TZ");
persistSimpleResource(Cursor.create(RDE_STAGING, stagingCursor, Registry.get("tld")));
BlobId ghostrydeR1FileWithPrefix =
BlobId.of("bucket", JOB_PREFIX + "-job-name/tld_2010-10-17_full_S1_R1.xml.ghostryde");
BlobId lengthR1FileWithPrefix =
BlobId.of("bucket", JOB_PREFIX + "-job-name/tld_2010-10-17_full_S1_R1.xml.length");
BlobId reportR1FileWithPrefix =
BlobId.of(
"bucket", JOB_PREFIX + "-job-name/tld_2010-10-17_full_S1_R1-report.xml.ghostryde");
gcsUtils.createFromBytes(
ghostrydeR1FileWithPrefix, Ghostryde.encode(DEPOSIT_XML.read(), encryptKey));
gcsUtils.createFromBytes(
lengthR1FileWithPrefix, Long.toString(DEPOSIT_XML.size()).getBytes(UTF_8));
gcsUtils.createFromBytes(
reportR1FileWithPrefix, Ghostryde.encode(REPORT_XML.read(), encryptKey));
createAction(uploadUrl).runWithLock(uploadCursor);
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getContentType()).isEqualTo(PLAIN_TEXT_UTF_8);

View File

@@ -1429,6 +1429,33 @@ public class DatabaseHelper {
return entity;
}
/**
* Sets a DATASTORE_PRIMARY_NO_ASYNC state on the {@link DatabaseMigrationStateSchedule}.
*
* <p>In order to allow for tests to manipulate the clock how they need, we start the transitions
* one millisecond after the clock's current time (in case the clock's current value is
* START_OF_TIME). We then advance the clock one second so that we're in the
* DATASTORE_PRIMARY_READ_ONLY phase.
*
* <p>We must use the current time, otherwise the setting of the migration state will fail due to
* an invalid transition.
*/
public static void setMigrationScheduleToDatastorePrimaryNoAsync(FakeClock fakeClock) {
DateTime now = fakeClock.nowUtc();
jpaTm()
.transact(
() ->
DatabaseMigrationStateSchedule.set(
ImmutableSortedMap.of(
START_OF_TIME,
MigrationState.DATASTORE_ONLY,
now.plusMillis(1),
MigrationState.DATASTORE_PRIMARY,
now.plusMillis(2),
MigrationState.DATASTORE_PRIMARY_NO_ASYNC)));
fakeClock.advanceBy(Duration.standardSeconds(1));
}
/**
* Sets a DATASTORE_PRIMARY_READ_ONLY state on the {@link DatabaseMigrationStateSchedule}.
*
@@ -1452,6 +1479,8 @@ public class DatabaseHelper {
now.plusMillis(1),
MigrationState.DATASTORE_PRIMARY,
now.plusMillis(2),
MigrationState.DATASTORE_PRIMARY_NO_ASYNC,
now.plusMillis(3),
MigrationState.DATASTORE_PRIMARY_READ_ONLY)));
fakeClock.advanceBy(Duration.standardSeconds(1));
}
@@ -1478,8 +1507,10 @@ public class DatabaseHelper {
now.plusMillis(1),
MigrationState.DATASTORE_PRIMARY,
now.plusMillis(2),
MigrationState.DATASTORE_PRIMARY_READ_ONLY,
MigrationState.DATASTORE_PRIMARY_NO_ASYNC,
now.plusMillis(3),
MigrationState.DATASTORE_PRIMARY_READ_ONLY,
now.plusMillis(4),
MigrationState.SQL_PRIMARY)));
fakeClock.advanceBy(Duration.standardSeconds(1));
}

View File

@@ -54,10 +54,12 @@ public class GetDatabaseMigrationStateCommandTest
now.plusHours(1),
MigrationState.DATASTORE_PRIMARY,
now.plusHours(2),
MigrationState.DATASTORE_PRIMARY_READ_ONLY,
MigrationState.DATASTORE_PRIMARY_NO_ASYNC,
now.plusHours(3),
MigrationState.SQL_PRIMARY,
MigrationState.DATASTORE_PRIMARY_READ_ONLY,
now.plusHours(4),
MigrationState.SQL_PRIMARY,
now.plusHours(5),
MigrationState.SQL_ONLY);
jpaTm().transact(() -> DatabaseMigrationStateSchedule.set(transitions));
runCommand();

View File

@@ -64,14 +64,21 @@ public class SetDatabaseMigrationStateCommandTest
void testSuccess_fullSchedule() throws Exception {
DateTime now = fakeClock.nowUtc();
DateTime datastorePrimary = now.plusHours(1);
DateTime datastorePrimaryReadOnly = now.plusHours(2);
DateTime sqlPrimary = now.plusHours(3);
DateTime sqlOnly = now.plusHours(4);
DateTime datastorePrimaryNoAsync = now.plusHours(2);
DateTime datastorePrimaryReadOnly = now.plusHours(3);
DateTime sqlPrimary = now.plusHours(4);
DateTime sqlOnly = now.plusHours(5);
runCommandForced(
String.format(
"--migration_schedule=%s=DATASTORE_ONLY,%s=DATASTORE_PRIMARY,"
+ "%s=DATASTORE_PRIMARY_READ_ONLY,%s=SQL_PRIMARY,%s=SQL_ONLY",
START_OF_TIME, datastorePrimary, datastorePrimaryReadOnly, sqlPrimary, sqlOnly));
+ "%s=DATASTORE_PRIMARY_NO_ASYNC,%s=DATASTORE_PRIMARY_READ_ONLY,"
+ "%s=SQL_PRIMARY,%s=SQL_ONLY",
START_OF_TIME,
datastorePrimary,
datastorePrimaryNoAsync,
datastorePrimaryReadOnly,
sqlPrimary,
sqlOnly));
assertThat(DatabaseMigrationStateSchedule.get().toValueMap())
.containsExactlyEntriesIn(
ImmutableSortedMap.of(
@@ -79,6 +86,8 @@ public class SetDatabaseMigrationStateCommandTest
MigrationState.DATASTORE_ONLY,
datastorePrimary,
MigrationState.DATASTORE_PRIMARY,
datastorePrimaryNoAsync,
MigrationState.DATASTORE_PRIMARY_NO_ASYNC,
datastorePrimaryReadOnly,
MigrationState.DATASTORE_PRIMARY_READ_ONLY,
sqlPrimary,
@@ -110,8 +119,9 @@ public class SetDatabaseMigrationStateCommandTest
runCommandForced(
String.format(
"--migration_schedule=%s=DATASTORE_ONLY,%s=DATASTORE_PRIMARY,"
+ "%s=DATASTORE_PRIMARY_READ_ONLY,%s=DATASTORE_PRIMARY",
START_OF_TIME, now.plusHours(1), now.plusHours(2), now.plusHours(3)));
+ "%s=DATASTORE_PRIMARY_NO_ASYNC,%s=DATASTORE_PRIMARY_READ_ONLY,"
+ "%s=DATASTORE_PRIMARY",
START_OF_TIME, now.plusHours(1), now.plusHours(2), now.plusHours(3), now.plusHours(4)));
assertThat(DatabaseMigrationStateSchedule.get().toValueMap())
.containsExactlyEntriesIn(
ImmutableSortedMap.of(
@@ -120,8 +130,10 @@ public class SetDatabaseMigrationStateCommandTest
now.plusHours(1),
MigrationState.DATASTORE_PRIMARY,
now.plusHours(2),
MigrationState.DATASTORE_PRIMARY_READ_ONLY,
MigrationState.DATASTORE_PRIMARY_NO_ASYNC,
now.plusHours(3),
MigrationState.DATASTORE_PRIMARY_READ_ONLY,
now.plusHours(4),
MigrationState.DATASTORE_PRIMARY));
}
@@ -152,9 +164,12 @@ public class SetDatabaseMigrationStateCommandTest
() ->
runCommandForced(
String.format(
"--migration_schedule=%s=DATASTORE_ONLY,"
+ "%s=DATASTORE_PRIMARY,%s=DATASTORE_PRIMARY_READ_ONLY",
START_OF_TIME, now.minusHours(2), now.minusHours(1)))))
"--migration_schedule=%s=DATASTORE_ONLY,%s=DATASTORE_PRIMARY,"
+ "%s=DATASTORE_PRIMARY_NO_ASYNC,%s=DATASTORE_PRIMARY_READ_ONLY",
START_OF_TIME,
now.minusHours(3),
now.minusHours(2),
now.minusHours(1)))))
.hasMessageThat()
.isEqualTo(
"Cannot transition from current state-as-of-now DATASTORE_ONLY "

View File

@@ -0,0 +1,19 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<response>
<result code="1000">
<msg>Command completed successfully</msg>
</result>
<resData>
<domain:creData
xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
<domain:name>%DOMAIN%</domain:name>
<domain:crDate>1999-04-03T22:00:01.0Z</domain:crDate>
<domain:exDate>2001-04-03T22:00:01.0Z</domain:exDate>
</domain:creData>
</resData>
<trID>
<clTRID>ABC-12345</clTRID>
<svTRID>server-trid</svTRID>
</trID>
</response>
</epp>

View File

@@ -261,11 +261,11 @@ td.section {
</tr>
<tr>
<td class="property_name">generated on</td>
<td class="property_value">2022-03-16 18:17:28.401054</td>
<td class="property_value">2022-03-30 20:53:20.574091</td>
</tr>
<tr>
<td class="property_name">last flyway file</td>
<td id="lastFlywayFile" class="property_value">V114__add_allocation_token_indexes.sql</td>
<td id="lastFlywayFile" class="property_value">V115__add_renewal_columns_to_billing_recurrence.sql</td>
</tr>
</tbody>
</table>
@@ -284,7 +284,7 @@ td.section {
generated on
</text>
<text text-anchor="start" x="4055.5" y="-10.8" font-family="Helvetica,sans-Serif" font-size="14.00">
2022-03-16 18:17:28.401054
2022-03-30 20:53:20.574091
</text>
<polygon fill="none" stroke="#888888" points="3968,-4 3968,-44 4233,-44 4233,-4 3968,-4" /> <!-- allocationtoken_a08ccbef -->
<g id="node1" class="node">

View File

@@ -261,11 +261,11 @@ td.section {
</tr>
<tr>
<td class="property_name">generated on</td>
<td class="property_value">2022-03-16 18:17:27.007916</td>
<td class="property_value">2022-03-30 20:53:18.15953</td>
</tr>
<tr>
<td class="property_name">last flyway file</td>
<td id="lastFlywayFile" class="property_value">V114__add_allocation_token_indexes.sql</td>
<td id="lastFlywayFile" class="property_value">V115__add_renewal_columns_to_billing_recurrence.sql</td>
</tr>
</tbody>
</table>
@@ -274,19 +274,19 @@ td.section {
<svg viewbox="0.00 0.00 4949.02 4890.50" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="erDiagram" style="overflow: hidden; width: 100%; height: 800px"> <g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 4886.5)">
<title>SchemaCrawler_Diagram</title>
<polygon fill="white" stroke="transparent" points="-4,4 -4,-4886.5 4945.02,-4886.5 4945.02,4 -4,4" />
<text text-anchor="start" x="4672.52" y="-29.8" font-family="Helvetica,sans-Serif" font-size="14.00">
<text text-anchor="start" x="4680.52" y="-29.8" font-family="Helvetica,sans-Serif" font-size="14.00">
generated by
</text>
<text text-anchor="start" x="4755.52" y="-29.8" font-family="Helvetica,sans-Serif" font-size="14.00">
<text text-anchor="start" x="4763.52" y="-29.8" font-family="Helvetica,sans-Serif" font-size="14.00">
SchemaCrawler 16.10.1
</text>
<text text-anchor="start" x="4671.52" y="-10.8" font-family="Helvetica,sans-Serif" font-size="14.00">
<text text-anchor="start" x="4679.52" y="-10.8" font-family="Helvetica,sans-Serif" font-size="14.00">
generated on
</text>
<text text-anchor="start" x="4755.52" y="-10.8" font-family="Helvetica,sans-Serif" font-size="14.00">
2022-03-16 18:17:27.007916
<text text-anchor="start" x="4763.52" y="-10.8" font-family="Helvetica,sans-Serif" font-size="14.00">
2022-03-30 20:53:18.15953
</text>
<polygon fill="none" stroke="#888888" points="4668.02,-4 4668.02,-44 4933.02,-44 4933.02,-4 4668.02,-4" /> <!-- allocationtoken_a08ccbef -->
<polygon fill="none" stroke="#888888" points="4676.02,-4 4676.02,-44 4933.02,-44 4933.02,-4 4676.02,-4" /> <!-- allocationtoken_a08ccbef -->
<g id="node1" class="node">
<title>allocationtoken_a08ccbef</title>
<polygon fill="#ebcef2" stroke="transparent" points="3100.5,-651.5 3100.5,-670.5 3299.5,-670.5 3299.5,-651.5 3100.5,-651.5" />
@@ -558,40 +558,64 @@ td.section {
</g> <!-- billingrecurrence_5fa2cb01 -->
<g id="node6" class="node">
<title>billingrecurrence_5fa2cb01</title>
<polygon fill="#ebcef2" stroke="transparent" points="3114.5,-1104.5 3114.5,-1123.5 3291.5,-1123.5 3291.5,-1104.5 3114.5,-1104.5" />
<text text-anchor="start" x="3116.5" y="-1111.3" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">
<polygon fill="#ebcef2" stroke="transparent" points="3114.5,-1161.5 3114.5,-1180.5 3291.5,-1180.5 3291.5,-1161.5 3114.5,-1161.5" />
<text text-anchor="start" x="3116.5" y="-1168.3" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">
public.BillingRecurrence
</text>
<polygon fill="#ebcef2" stroke="transparent" points="3291.5,-1104.5 3291.5,-1123.5 3417.5,-1123.5 3417.5,-1104.5 3291.5,-1104.5" />
<text text-anchor="start" x="3378.5" y="-1110.3" font-family="Helvetica,sans-Serif" font-size="14.00">
<polygon fill="#ebcef2" stroke="transparent" points="3291.5,-1161.5 3291.5,-1180.5 3417.5,-1180.5 3417.5,-1161.5 3291.5,-1161.5" />
<text text-anchor="start" x="3378.5" y="-1167.3" font-family="Helvetica,sans-Serif" font-size="14.00">
[table]
</text>
<text text-anchor="start" x="3116.5" y="-1092.3" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">
<text text-anchor="start" x="3116.5" y="-1149.3" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">
billing_recurrence_id
</text>
<text text-anchor="start" x="3285.5" y="-1148.3" font-family="Helvetica,sans-Serif" font-size="14.00">
</text>
<text text-anchor="start" x="3293.5" y="-1148.3" font-family="Helvetica,sans-Serif" font-size="14.00">
int8 not null
</text>
<text text-anchor="start" x="3116.5" y="-1129.3" font-family="Helvetica,sans-Serif" font-size="14.00">
registrar_id
</text>
<text text-anchor="start" x="3285.5" y="-1129.3" font-family="Helvetica,sans-Serif" font-size="14.00">
</text>
<text text-anchor="start" x="3293.5" y="-1129.3" font-family="Helvetica,sans-Serif" font-size="14.00">
text not null
</text>
<text text-anchor="start" x="3116.5" y="-1110.3" font-family="Helvetica,sans-Serif" font-size="14.00">
domain_history_revision_id
</text>
<text text-anchor="start" x="3285.5" y="-1110.3" font-family="Helvetica,sans-Serif" font-size="14.00">
</text>
<text text-anchor="start" x="3293.5" y="-1110.3" font-family="Helvetica,sans-Serif" font-size="14.00">
int8 not null
</text>
<text text-anchor="start" x="3116.5" y="-1091.3" font-family="Helvetica,sans-Serif" font-size="14.00">
domain_repo_id
</text>
<text text-anchor="start" x="3285.5" y="-1091.3" font-family="Helvetica,sans-Serif" font-size="14.00">
</text>
<text text-anchor="start" x="3293.5" y="-1091.3" font-family="Helvetica,sans-Serif" font-size="14.00">
int8 not null
text not null
</text>
<text text-anchor="start" x="3116.5" y="-1072.3" font-family="Helvetica,sans-Serif" font-size="14.00">
registrar_id
event_time
</text>
<text text-anchor="start" x="3285.5" y="-1072.3" font-family="Helvetica,sans-Serif" font-size="14.00">
</text>
<text text-anchor="start" x="3293.5" y="-1072.3" font-family="Helvetica,sans-Serif" font-size="14.00">
text not null
timestamptz not null
</text>
<text text-anchor="start" x="3116.5" y="-1053.3" font-family="Helvetica,sans-Serif" font-size="14.00">
domain_history_revision_id
flags
</text>
<text text-anchor="start" x="3285.5" y="-1053.3" font-family="Helvetica,sans-Serif" font-size="14.00">
</text>
<text text-anchor="start" x="3293.5" y="-1053.3" font-family="Helvetica,sans-Serif" font-size="14.00">
int8 not null
_text
</text>
<text text-anchor="start" x="3116.5" y="-1034.3" font-family="Helvetica,sans-Serif" font-size="14.00">
domain_repo_id
reason
</text>
<text text-anchor="start" x="3285.5" y="-1034.3" font-family="Helvetica,sans-Serif" font-size="14.00">
</text>
@@ -599,31 +623,31 @@ td.section {
text not null
</text>
<text text-anchor="start" x="3116.5" y="-1015.3" font-family="Helvetica,sans-Serif" font-size="14.00">
event_time
domain_name
</text>
<text text-anchor="start" x="3285.5" y="-1015.3" font-family="Helvetica,sans-Serif" font-size="14.00">
</text>
<text text-anchor="start" x="3293.5" y="-1015.3" font-family="Helvetica,sans-Serif" font-size="14.00">
timestamptz not null
text not null
</text>
<text text-anchor="start" x="3116.5" y="-996.3" font-family="Helvetica,sans-Serif" font-size="14.00">
flags
recurrence_end_time
</text>
<text text-anchor="start" x="3285.5" y="-996.3" font-family="Helvetica,sans-Serif" font-size="14.00">
</text>
<text text-anchor="start" x="3293.5" y="-996.3" font-family="Helvetica,sans-Serif" font-size="14.00">
_text
timestamptz
</text>
<text text-anchor="start" x="3116.5" y="-977.3" font-family="Helvetica,sans-Serif" font-size="14.00">
reason
recurrence_time_of_year
</text>
<text text-anchor="start" x="3285.5" y="-977.3" font-family="Helvetica,sans-Serif" font-size="14.00">
</text>
<text text-anchor="start" x="3293.5" y="-977.3" font-family="Helvetica,sans-Serif" font-size="14.00">
text not null
text
</text>
<text text-anchor="start" x="3116.5" y="-958.3" font-family="Helvetica,sans-Serif" font-size="14.00">
domain_name
renewal_price_behavior
</text>
<text text-anchor="start" x="3285.5" y="-958.3" font-family="Helvetica,sans-Serif" font-size="14.00">
</text>
@@ -631,33 +655,33 @@ td.section {
text not null
</text>
<text text-anchor="start" x="3116.5" y="-939.3" font-family="Helvetica,sans-Serif" font-size="14.00">
recurrence_end_time
renewal_price_currency
</text>
<text text-anchor="start" x="3285.5" y="-939.3" font-family="Helvetica,sans-Serif" font-size="14.00">
</text>
<text text-anchor="start" x="3293.5" y="-939.3" font-family="Helvetica,sans-Serif" font-size="14.00">
timestamptz
text
</text>
<text text-anchor="start" x="3116.5" y="-920.3" font-family="Helvetica,sans-Serif" font-size="14.00">
recurrence_time_of_year
renewal_price_amount
</text>
<text text-anchor="start" x="3285.5" y="-920.3" font-family="Helvetica,sans-Serif" font-size="14.00">
</text>
<text text-anchor="start" x="3293.5" y="-920.3" font-family="Helvetica,sans-Serif" font-size="14.00">
text
numeric(19, 2)
</text>
<polygon fill="none" stroke="#888888" points="3113,-914 3113,-1125 3418,-1125 3418,-914 3113,-914" />
<polygon fill="none" stroke="#888888" points="3113,-913.5 3113,-1181.5 3418,-1181.5 3418,-913.5 3113,-913.5" />
</g> <!-- billingevent_a57d1815&#45;&gt;billingrecurrence_5fa2cb01 -->
<g id="edge7" class="edge">
<title>billingevent_a57d1815:w-&gt;billingrecurrence_5fa2cb01:e</title>
<path fill="none" stroke="black" d="M3871.97,-834.71C3793.31,-872.06 3491.34,-1066.17 3428.33,-1092.53" />
<polygon fill="black" stroke="black" points="3879.6,-831.92 3890.54,-832.73 3884.3,-830.21 3889,-828.5 3889,-828.5 3889,-828.5 3884.3,-830.21 3887.46,-824.27 3879.6,-831.92 3879.6,-831.92" />
<ellipse fill="none" stroke="black" cx="3875.85" cy="-833.29" rx="4" ry="4" />
<polygon fill="black" stroke="black" points="3420.9,-1100 3418.01,-1090.42 3419.93,-1089.85 3422.82,-1099.42 3420.9,-1100" />
<polyline fill="none" stroke="black" points="3418.5,-1095.5 3423.29,-1094.06 " />
<polygon fill="black" stroke="black" points="3425.69,-1098.55 3422.8,-1088.98 3424.71,-1088.4 3427.6,-1097.98 3425.69,-1098.55" />
<polyline fill="none" stroke="black" points="3423.29,-1094.06 3428.07,-1092.61 " />
<text text-anchor="start" x="3470" y="-1072.3" font-family="Helvetica,sans-Serif" font-size="14.00">
<path fill="none" stroke="black" d="M3872.44,-835.57C3794.18,-879.8 3491.62,-1117.68 3428.23,-1149.08" />
<polygon fill="black" stroke="black" points="3879.8,-832.43 3890.77,-832.64 3884.4,-830.46 3889,-828.5 3889,-828.5 3889,-828.5 3884.4,-830.46 3887.23,-824.36 3879.8,-832.43 3879.8,-832.43" />
<ellipse fill="none" stroke="black" cx="3876.12" cy="-834" rx="4" ry="4" />
<polygon fill="black" stroke="black" points="3421.1,-1156.89 3417.78,-1147.45 3419.67,-1146.79 3422.99,-1156.22 3421.1,-1156.89" />
<polyline fill="none" stroke="black" points="3418.5,-1152.5 3423.22,-1150.84 " />
<polygon fill="black" stroke="black" points="3425.82,-1155.23 3422.5,-1145.79 3424.39,-1145.13 3427.71,-1154.56 3425.82,-1155.23" />
<polyline fill="none" stroke="black" points="3423.22,-1150.84 3427.93,-1149.18 " />
<text text-anchor="start" x="3470" y="-1123.3" font-family="Helvetica,sans-Serif" font-size="14.00">
fk_billing_event_cancellation_matching_billing_recurrence_id
</text>
</g> <!-- domainhistory_a54cc226 -->
@@ -1235,20 +1259,20 @@ td.section {
</g> <!-- billingevent_a57d1815&#45;&gt;domainhistory_a54cc226 -->
<g id="edge28" class="edge">
<title>billingevent_a57d1815:w-&gt;domainhistory_a54cc226:e</title>
<path fill="none" stroke="black" d="M3871.35,-957.82C3846.78,-940.16 3876.08,-877.25 3837,-852.5 3694.56,-762.28 3243.05,-846.56 3079,-885.5 2903.78,-927.09 2810.85,-892.28 2703,-1036.5 2668.91,-1082.09 2729.25,-1281.26 2686.9,-1305.98" />
<path fill="none" stroke="black" d="M3871.35,-957.82C3846.78,-940.16 3876.08,-877.25 3837,-852.5 3694.56,-762.28 3240.38,-836.69 3079,-885.5 2896.41,-940.73 2809.71,-934.38 2703,-1092.5 2678.07,-1129.44 2718.1,-1281.84 2686.62,-1305.43" />
<polygon fill="black" stroke="black" points="3879.33,-959.94 3887.85,-966.85 3884.17,-961.22 3889,-962.5 3889,-962.5 3889,-962.5 3884.17,-961.22 3890.15,-958.15 3879.33,-959.94 3879.33,-959.94" />
<ellipse fill="none" stroke="black" cx="3875.47" cy="-958.91" rx="4" ry="4" />
<polygon fill="black" stroke="black" points="2679.2,-1313.1 2676.74,-1303.41 2678.67,-1302.91 2681.14,-1312.61 2679.2,-1313.1" />
<polyline fill="none" stroke="black" points="2677,-1308.5 2681.85,-1307.27 " />
<polygon fill="black" stroke="black" points="2684.05,-1311.86 2681.58,-1302.17 2683.52,-1301.68 2685.99,-1311.37 2684.05,-1311.86" />
<polyline fill="none" stroke="black" points="2681.85,-1307.27 2686.69,-1306.03 " />
<polygon fill="black" stroke="black" points="2679.47,-1312.96 2676.43,-1303.43 2678.34,-1302.82 2681.38,-1312.35 2679.47,-1312.96" />
<polyline fill="none" stroke="black" points="2677,-1308.5 2681.76,-1306.98 " />
<polygon fill="black" stroke="black" points="2684.24,-1311.44 2681.2,-1301.91 2683.1,-1301.3 2686.14,-1310.83 2684.24,-1311.44" />
<polyline fill="none" stroke="black" points="2681.76,-1306.98 2686.53,-1305.46 " />
<text text-anchor="start" x="3169" y="-889.3" font-family="Helvetica,sans-Serif" font-size="14.00">
fk_billing_event_domain_history
</text>
</g> <!-- billingevent_a57d1815&#45;&gt;domainhistory_a54cc226 -->
<g id="edge29" class="edge">
<title>billingevent_a57d1815:w-&gt;domainhistory_a54cc226:e</title>
<path fill="none" stroke="black" d="M3870.71,-981.82C3570.79,-992.47 3573.6,-1268.04 3470,-1562.5 3457.19,-1598.92 3475.72,-1879.04 3452,-1909.5 3237.94,-2184.33 2927.98,-1874.53 2703,-2140.5 2677.3,-2170.88 2713.06,-2307.49 2686.73,-2331.8" />
<path fill="none" stroke="black" d="M3870.74,-981.82C3578.68,-992.17 3573.17,-1253.62 3470,-1539.5 3456.03,-1578.22 3477.24,-1876.99 3452,-1909.5 3238.38,-2184.68 2927.98,-1874.53 2703,-2140.5 2677.3,-2170.88 2713.06,-2307.49 2686.73,-2331.8" />
<polygon fill="black" stroke="black" points="3879,-981.67 3889.08,-986 3884,-981.59 3889,-981.5 3889,-981.5 3889,-981.5 3884,-981.59 3888.92,-977 3879,-981.67 3879,-981.67" />
<ellipse fill="none" stroke="black" cx="3875" cy="-981.74" rx="4" ry="4" />
<polygon fill="black" stroke="black" points="2679.71,-2339.82 2676.16,-2330.47 2678.03,-2329.76 2681.58,-2339.11 2679.71,-2339.82" />
@@ -1261,22 +1285,22 @@ td.section {
</g> <!-- billingevent_a57d1815&#45;&gt;domainhistory_a54cc226 -->
<g id="edge30" class="edge">
<title>billingevent_a57d1815:w-&gt;domainhistory_a54cc226:e</title>
<path fill="none" stroke="black" d="M3871.1,-959.11C3831.58,-940.14 3889.42,-846.54 3837,-808.5 3734.78,-734.32 2785.1,-791.52 2703,-887.5 2674.68,-920.61 2717.05,-1256.03 2685.49,-1303.04" />
<polygon fill="black" stroke="black" points="3879.17,-960.64 3888.16,-966.92 3884.09,-961.57 3889,-962.5 3889,-962.5 3889,-962.5 3884.09,-961.57 3889.84,-958.08 3879.17,-960.64 3879.17,-960.64" />
<ellipse fill="none" stroke="black" cx="3875.24" cy="-959.9" rx="4" ry="4" />
<polygon fill="black" stroke="black" points="2680.55,-1312.16 2675.14,-1303.75 2676.82,-1302.67 2682.23,-1311.08 2680.55,-1312.16" />
<path fill="none" stroke="black" d="M3871.16,-959.2C3830.74,-940.3 3889.97,-845.31 3837,-806.5 3735.09,-731.83 2784.41,-792.89 2703,-889.5 2675.06,-922.66 2716.91,-1256.31 2685.46,-1303.07" />
<polygon fill="black" stroke="black" points="3879.17,-960.68 3888.18,-966.92 3884.08,-961.59 3889,-962.5 3889,-962.5 3889,-962.5 3884.08,-961.59 3889.82,-958.08 3879.17,-960.68 3879.17,-960.68" />
<ellipse fill="none" stroke="black" cx="3875.23" cy="-959.95" rx="4" ry="4" />
<polygon fill="black" stroke="black" points="2680.54,-1312.17 2675.14,-1303.75 2676.82,-1302.67 2682.23,-1311.09 2680.54,-1312.17" />
<polyline fill="none" stroke="black" points="2677,-1308.5 2681.21,-1305.8 " />
<polygon fill="black" stroke="black" points="2684.75,-1309.46 2679.34,-1301.05 2681.02,-1299.97 2686.43,-1308.38 2684.75,-1309.46" />
<polyline fill="none" stroke="black" points="2681.21,-1305.8 2685.41,-1303.09 " />
<polygon fill="black" stroke="black" points="2684.75,-1309.46 2679.35,-1301.05 2681.03,-1299.97 2686.43,-1308.38 2684.75,-1309.46" />
<polyline fill="none" stroke="black" points="2681.21,-1305.8 2685.41,-1303.1 " />
<text text-anchor="start" x="3159" y="-803.3" font-family="Helvetica,sans-Serif" font-size="14.00">
fk_billing_event_recurrence_history
</text>
</g> <!-- billingevent_a57d1815&#45;&gt;domainhistory_a54cc226 -->
<g id="edge31" class="edge">
<title>billingevent_a57d1815:w-&gt;domainhistory_a54cc226:e</title>
<path fill="none" stroke="black" d="M3871.29,-737.96C3845.51,-755.37 3875.89,-819.21 3837,-846.5 3770,-893.51 3525.93,-817.75 3470,-877.5 3432.56,-917.5 3489.68,-1823.73 3452,-1863.5 3423.42,-1893.66 3119.71,-1882.19 3079,-1890.5 2905.9,-1925.84 2811.49,-1880.06 2703,-2019.5 2683.27,-2044.86 2707.21,-2286.85 2684.84,-2329.19" />
<polygon fill="black" stroke="black" points="3879.3,-735.94 3890.1,-737.86 3884.15,-734.72 3889,-733.5 3889,-733.5 3889,-733.5 3884.15,-734.72 3887.9,-729.14 3879.3,-735.94 3879.3,-735.94" />
<ellipse fill="none" stroke="black" cx="3875.42" cy="-736.92" rx="4" ry="4" />
<path fill="none" stroke="black" d="M3871.4,-737.92C3845.73,-755.17 3875.62,-818.42 3837,-845.5 3769.97,-892.5 3525.86,-817.66 3470,-877.5 3432.61,-917.55 3489.68,-1823.73 3452,-1863.5 3423.42,-1893.66 3119.71,-1882.19 3079,-1890.5 2905.9,-1925.84 2811.49,-1880.06 2703,-2019.5 2683.27,-2044.86 2707.21,-2286.85 2684.84,-2329.19" />
<polygon fill="black" stroke="black" points="3879.3,-735.93 3890.09,-737.86 3884.15,-734.72 3889,-733.5 3889,-733.5 3889,-733.5 3884.15,-734.72 3887.91,-729.14 3879.3,-735.93 3879.3,-735.93" />
<ellipse fill="none" stroke="black" cx="3875.42" cy="-736.91" rx="4" ry="4" />
<polygon fill="black" stroke="black" points="2680.91,-2338.77 2674.64,-2330.98 2676.2,-2329.72 2682.47,-2337.52 2680.91,-2338.77" />
<polyline fill="none" stroke="black" points="2677,-2335.5 2680.9,-2332.37 " />
<polygon fill="black" stroke="black" points="2684.81,-2335.63 2678.54,-2327.84 2680.1,-2326.59 2686.37,-2334.38 2684.81,-2335.63" />
@@ -1675,14 +1699,14 @@ td.section {
</g> <!-- billingevent_a57d1815&#45;&gt;registrar_6e1503e3 -->
<g id="edge51" class="edge">
<title>billingevent_a57d1815:w-&gt;registrar_6e1503e3:e</title>
<path fill="none" stroke="black" d="M3871.61,-994.55C3858.04,-981.95 3861.38,-952.77 3837,-940.5 3763.94,-903.72 3529.31,-968.82 3470,-912.5 3424.51,-869.3 3498.83,-814.24 3452,-772.5 3261.21,-602.45 1407.57,-744.75 1152,-742.5 848.9,-739.84 686.12,-527.97 470,-740.5 440.59,-769.42 483.09,-2162.55 449.17,-2284.93" />
<path fill="none" stroke="black" d="M3871.61,-994.55C3858.04,-981.95 3861.38,-952.77 3837,-940.5 3763.94,-903.72 3529.29,-968.85 3470,-912.5 3424.21,-868.98 3499.1,-813.61 3452,-771.5 3261.46,-601.15 1407.58,-738.73 1152,-736.5 848.9,-733.85 686.11,-522.96 470,-735.5 440.5,-764.52 483.2,-2162.13 449.19,-2284.91" />
<polygon fill="black" stroke="black" points="3879.54,-997.26 3887.54,-1004.76 3884.27,-998.88 3889,-1000.5 3889,-1000.5 3889,-1000.5 3884.27,-998.88 3890.46,-996.24 3879.54,-997.26 3879.54,-997.26" />
<ellipse fill="none" stroke="black" cx="3875.75" cy="-995.97" rx="4" ry="4" />
<polygon fill="black" stroke="black" points="448.8,-2295.23 440.24,-2290.06 441.27,-2288.35 449.83,-2293.52 448.8,-2295.23" />
<polyline fill="none" stroke="black" points="444,-2293.5 446.59,-2289.22 " />
<polygon fill="black" stroke="black" points="451.38,-2290.95 442.82,-2285.78 443.86,-2284.07 452.42,-2289.24 451.38,-2290.95" />
<polyline fill="none" stroke="black" points="446.59,-2289.22 449.17,-2284.94 " />
<text text-anchor="start" x="1980" y="-714.3" font-family="Helvetica,sans-Serif" font-size="14.00">
<text text-anchor="start" x="1980" y="-709.3" font-family="Helvetica,sans-Serif" font-size="14.00">
fk_billing_event_registrar_id
</text>
</g> <!-- billingcancellation_6eedf614 -->
@@ -1820,26 +1844,26 @@ td.section {
</g> <!-- billingcancellation_6eedf614&#45;&gt;billingevent_a57d1815 -->
<g id="edge3" class="edge">
<title>billingcancellation_6eedf614:w-&gt;billingevent_a57d1815:e</title>
<path fill="none" stroke="black" d="M752.36,-895.1C744.62,-868.93 752.39,-812.49 765.5,-799.5 793.83,-771.42 1084.61,-799.33 1124.5,-799.5 1156.83,-799.64 3427.57,-798.33 3452,-819.5 3492.09,-854.24 3434.86,-897.75 3470,-937.5 3595.53,-1079.49 3692.75,-1027.09 3880.5,-1053 4062.99,-1078.18 4162.81,-1181.31 4295,-1053 4302.35,-1045.87 4307.11,-1032.71 4304.52,-1025.18" />
<path fill="none" stroke="black" d="M752.36,-895.1C744.62,-868.93 752.39,-812.49 765.5,-799.5 793.83,-771.42 1084.61,-799.36 1124.5,-799.5 1156.83,-799.61 3427.6,-794.29 3452,-815.5 3493.36,-851.46 3433.8,-896.35 3470,-937.5 3595.19,-1079.8 3692.75,-1027.09 3880.5,-1053 4062.99,-1078.18 4162.81,-1181.31 4295,-1053 4302.35,-1045.87 4307.11,-1032.71 4304.52,-1025.18" />
<polygon fill="black" stroke="black" points="757.79,-901.09 761.16,-911.52 761.14,-904.79 764.5,-908.5 764.5,-908.5 764.5,-908.5 761.14,-904.79 767.84,-905.48 757.79,-901.09 757.79,-901.09" />
<ellipse fill="none" stroke="black" cx="755.1" cy="-898.12" rx="4" ry="4" />
<polygon fill="black" stroke="black" points="4294.06,-1024.22 4299.61,-1015.89 4301.27,-1017 4295.72,-1025.32 4294.06,-1024.22" />
<polyline fill="none" stroke="black" points="4296,-1019.5 4300.16,-1022.27 " />
<polygon fill="black" stroke="black" points="4298.22,-1026.99 4303.77,-1018.67 4305.43,-1019.78 4299.88,-1028.1 4298.22,-1026.99" />
<polyline fill="none" stroke="black" points="4300.16,-1022.27 4304.32,-1025.05 " />
<text text-anchor="start" x="2354" y="-808.3" font-family="Helvetica,sans-Serif" font-size="14.00">
<text text-anchor="start" x="2354" y="-805.3" font-family="Helvetica,sans-Serif" font-size="14.00">
fk_billing_cancellation_billing_event_id
</text>
</g> <!-- billingcancellation_6eedf614&#45;&gt;billingrecurrence_5fa2cb01 -->
<g id="edge6" class="edge">
<title>billingcancellation_6eedf614:w-&gt;billingrecurrence_5fa2cb01:e</title>
<path fill="none" stroke="black" d="M753.63,-874.92C748.83,-852.14 755.22,-809.66 765.5,-799.5 779.68,-785.48 1105.71,-792.82 1124.5,-799.5 1149.06,-808.23 1145.65,-827.19 1170,-836.5 1391.72,-921.29 2000.25,-870.23 2237,-887.5 2436.54,-902.06 2488.35,-894.68 2685,-931.5 2855.54,-963.43 2928.25,-924.78 3061,-1036.5 3095.83,-1065.81 3066.51,-1104.69 3105,-1129 3222.42,-1203.18 3317.84,-1225.73 3417.5,-1129 3424.85,-1121.87 3429.61,-1108.71 3427.02,-1101.18" />
<path fill="none" stroke="black" d="M753.63,-874.92C748.83,-852.14 755.22,-809.66 765.5,-799.5 779.68,-785.48 1105.71,-792.82 1124.5,-799.5 1149.06,-808.23 1145.65,-827.19 1170,-836.5 1391.72,-921.29 2000.25,-870.23 2237,-887.5 2436.54,-902.06 2491.21,-881.76 2685,-931.5 2861.08,-976.69 2930.08,-966.38 3061,-1092.5 3093.93,-1124.22 3066.37,-1161.04 3105,-1185.5 3134.34,-1204.07 3392.58,-1209.68 3417.5,-1185.5 3424.74,-1178.48 3429.43,-1165.51 3426.89,-1158.1" />
<polygon fill="black" stroke="black" points="758.52,-881.48 760.89,-892.19 761.51,-885.49 764.5,-889.5 764.5,-889.5 764.5,-889.5 761.51,-885.49 768.11,-886.81 758.52,-881.48 758.52,-881.48" />
<ellipse fill="none" stroke="black" cx="756.13" cy="-878.28" rx="4" ry="4" />
<polygon fill="black" stroke="black" points="3416.56,-1100.22 3422.11,-1091.89 3423.77,-1093 3418.22,-1101.32 3416.56,-1100.22" />
<polyline fill="none" stroke="black" points="3418.5,-1095.5 3422.66,-1098.27 " />
<polygon fill="black" stroke="black" points="3420.72,-1102.99 3426.27,-1094.67 3427.93,-1095.78 3422.38,-1104.1 3420.72,-1102.99" />
<polyline fill="none" stroke="black" points="3422.66,-1098.27 3426.82,-1101.05 " />
<polygon fill="black" stroke="black" points="3416.56,-1157.21 3422.11,-1148.89 3423.77,-1150 3418.22,-1158.32 3416.56,-1157.21" />
<polyline fill="none" stroke="black" points="3418.5,-1152.5 3422.66,-1155.27 " />
<polygon fill="black" stroke="black" points="3420.72,-1159.99 3426.27,-1151.67 3427.93,-1152.78 3422.38,-1161.1 3420.72,-1159.99" />
<polyline fill="none" stroke="black" points="3422.66,-1155.27 3426.82,-1158.05 " />
<text text-anchor="start" x="1932.5" y="-891.3" font-family="Helvetica,sans-Serif" font-size="14.00">
fk_billing_cancellation_billing_recurrence_id
</text>
@@ -2353,13 +2377,13 @@ td.section {
</g> <!-- domain_6c51cffa&#45;&gt;billingevent_a57d1815 -->
<g id="edge4" class="edge">
<title>domain_6c51cffa:w-&gt;billingevent_a57d1815:e</title>
<path fill="none" stroke="black" d="M1447.77,-1896.98C1438.07,-1804.62 1443.43,-1378.05 1453,-1368.5 1457.14,-1364.37 1869.79,-1364.44 1874,-1368.5 1894.36,-1388.15 1882.79,-1850.75 1892,-1877.5 1981.42,-2137.15 2010.34,-2252.79 2255,-2377.5 2314.25,-2407.7 3397.45,-2415.54 3452,-2377.5 3877.03,-2081.12 3513.1,-1672.85 3855,-1283.5 4000.67,-1117.61 4177.91,-1240.16 4295,-1053 4300.25,-1044.61 4305.53,-1032.59 4304.13,-1025.46" />
<path fill="none" stroke="black" d="M1447.77,-1896.98C1438.07,-1804.62 1443.43,-1378.05 1453,-1368.5 1457.14,-1364.37 1869.79,-1364.44 1874,-1368.5 1894.36,-1388.15 1882.79,-1850.75 1892,-1877.5 1981.42,-2137.15 2010.34,-2252.79 2255,-2377.5 2314.25,-2407.7 3397.21,-2415.19 3452,-2377.5 3864.83,-2093.47 3535.36,-1710.42 3855,-1324.5 4001.57,-1147.53 4180.05,-1251.97 4295,-1053 4299.89,-1044.54 4305.21,-1032.75 4304.08,-1025.65" />
<polygon fill="black" stroke="black" points="1449.66,-1904.78 1447.63,-1915.56 1450.83,-1909.64 1452,-1914.5 1452,-1914.5 1452,-1914.5 1450.83,-1909.64 1456.37,-1913.44 1449.66,-1904.78 1449.66,-1904.78" />
<ellipse fill="none" stroke="black" cx="1448.72" cy="-1900.89" rx="4" ry="4" />
<polygon fill="black" stroke="black" points="4293.85,-1024.12 4299.76,-1016.06 4301.38,-1017.24 4295.46,-1025.31 4293.85,-1024.12" />
<polyline fill="none" stroke="black" points="4296,-1019.5 4300.03,-1022.46 " />
<polygon fill="black" stroke="black" points="4297.88,-1027.08 4303.8,-1019.02 4305.41,-1020.2 4299.5,-1028.26 4297.88,-1027.08" />
<polyline fill="none" stroke="black" points="4300.03,-1022.46 4304.06,-1025.41 " />
<polygon fill="black" stroke="black" points="4293.76,-1024.08 4299.83,-1016.13 4301.42,-1017.34 4295.36,-1025.3 4293.76,-1024.08" />
<polyline fill="none" stroke="black" points="4296,-1019.5 4299.98,-1022.53 " />
<polygon fill="black" stroke="black" points="4297.74,-1027.11 4303.8,-1019.16 4305.39,-1020.37 4299.33,-1028.33 4297.74,-1027.11" />
<polyline fill="none" stroke="black" points="4299.98,-1022.53 4303.95,-1025.56 " />
<text text-anchor="start" x="2773.5" y="-2407.3" font-family="Helvetica,sans-Serif" font-size="14.00">
fk_domain_transfer_billing_event_id
</text>
@@ -2379,26 +2403,26 @@ td.section {
</g> <!-- domain_6c51cffa&#45;&gt;billingrecurrence_5fa2cb01 -->
<g id="edge8" class="edge">
<title>domain_6c51cffa:w-&gt;billingrecurrence_5fa2cb01:e</title>
<path fill="none" stroke="black" d="M1446.25,-1631.23C1438.3,-1571.65 1443.75,-1377.85 1453,-1368.5 1469.45,-1351.87 1857.21,-1378.78 1874,-1362.5 1905.94,-1331.53 1860.83,-994.25 1892,-962.5 2005.26,-847.15 2093.42,-944.61 2255,-939.5 2446.02,-933.45 2494,-932.9 2685,-939.5 2726.85,-940.95 3028.66,-938.89 3061,-965.5 3119.11,-1013.31 3045.27,-1083.22 3105,-1129 3215.23,-1213.49 3317.84,-1225.73 3417.5,-1129 3424.85,-1121.87 3429.61,-1108.71 3427.02,-1101.18" />
<path fill="none" stroke="black" d="M1446.25,-1631.23C1438.3,-1571.65 1443.75,-1377.85 1453,-1368.5 1469.45,-1351.87 1857.21,-1378.78 1874,-1362.5 1905.94,-1331.53 1860.83,-994.25 1892,-962.5 2005.26,-847.15 2093.42,-944.61 2255,-939.5 2446.02,-933.45 2494,-932.9 2685,-939.5 2726.85,-940.95 3029.55,-937.85 3061,-965.5 3135.89,-1031.34 3027.89,-1122.28 3105,-1185.5 3158.7,-1229.53 3367.66,-1233.86 3417.5,-1185.5 3424.74,-1178.48 3429.43,-1165.51 3426.89,-1158.1" />
<polygon fill="black" stroke="black" points="1448.84,-1639.01 1447.73,-1649.92 1450.42,-1643.76 1452,-1648.5 1452,-1648.5 1452,-1648.5 1450.42,-1643.76 1456.27,-1647.08 1448.84,-1639.01 1448.84,-1639.01" />
<ellipse fill="none" stroke="black" cx="1447.58" cy="-1635.22" rx="4" ry="4" />
<polygon fill="black" stroke="black" points="3416.56,-1100.22 3422.11,-1091.89 3423.77,-1093 3418.22,-1101.32 3416.56,-1100.22" />
<polyline fill="none" stroke="black" points="3418.5,-1095.5 3422.66,-1098.27 " />
<polygon fill="black" stroke="black" points="3420.72,-1102.99 3426.27,-1094.67 3427.93,-1095.78 3422.38,-1104.1 3420.72,-1102.99" />
<polyline fill="none" stroke="black" points="3422.66,-1098.27 3426.82,-1101.05 " />
<polygon fill="black" stroke="black" points="3416.56,-1157.21 3422.11,-1148.89 3423.77,-1150 3418.22,-1158.32 3416.56,-1157.21" />
<polyline fill="none" stroke="black" points="3418.5,-1152.5 3422.66,-1155.27 " />
<polygon fill="black" stroke="black" points="3420.72,-1159.99 3426.27,-1151.67 3427.93,-1152.78 3422.38,-1161.1 3420.72,-1159.99" />
<polyline fill="none" stroke="black" points="3422.66,-1155.27 3426.82,-1158.05 " />
<text text-anchor="start" x="2372" y="-943.3" font-family="Helvetica,sans-Serif" font-size="14.00">
fk_domain_billing_recurrence_id
</text>
</g> <!-- domain_6c51cffa&#45;&gt;billingrecurrence_5fa2cb01 -->
<g id="edge9" class="edge">
<title>domain_6c51cffa:w-&gt;billingrecurrence_5fa2cb01:e</title>
<path fill="none" stroke="black" d="M1447.83,-1877.7C1438.61,-1786.75 1443.86,-1377.76 1453,-1368.5 1461.22,-1360.18 1864.62,-1369.48 1874,-1362.5 1895.32,-1346.64 1877.51,-1327.76 1892,-1305.5 2009.34,-1125.26 2053.81,-1061.52 2255,-985.5 2344.39,-951.73 2590.74,-969.84 2685,-985.5 2879.59,-1017.83 2910.41,-1096.67 3105,-1129 3242.01,-1151.76 3317.84,-1225.73 3417.5,-1129 3424.85,-1121.87 3429.61,-1108.71 3427.02,-1101.18" />
<path fill="none" stroke="black" d="M1447.83,-1877.7C1438.61,-1786.75 1443.86,-1377.76 1453,-1368.5 1461.22,-1360.18 1864.62,-1369.48 1874,-1362.5 1895.32,-1346.64 1877.51,-1327.76 1892,-1305.5 2009.34,-1125.26 2053.81,-1061.52 2255,-985.5 2344.39,-951.73 2591.79,-964.44 2685,-985.5 2886.67,-1031.06 2903.33,-1139.94 3105,-1185.5 3240.47,-1216.11 3317.81,-1282.21 3417.5,-1185.5 3424.74,-1178.48 3429.43,-1165.51 3426.89,-1158.1" />
<polygon fill="black" stroke="black" points="1449.72,-1885.76 1447.62,-1896.53 1450.86,-1890.63 1452,-1895.5 1452,-1895.5 1452,-1895.5 1450.86,-1890.63 1456.38,-1894.47 1449.72,-1885.76 1449.72,-1885.76" />
<ellipse fill="none" stroke="black" cx="1448.81" cy="-1881.87" rx="4" ry="4" />
<polygon fill="black" stroke="black" points="3416.56,-1100.22 3422.11,-1091.89 3423.77,-1093 3418.22,-1101.32 3416.56,-1100.22" />
<polyline fill="none" stroke="black" points="3418.5,-1095.5 3422.66,-1098.27 " />
<polygon fill="black" stroke="black" points="3420.72,-1102.99 3426.27,-1094.67 3427.93,-1095.78 3422.38,-1104.1 3420.72,-1102.99" />
<polyline fill="none" stroke="black" points="3422.66,-1098.27 3426.82,-1101.05 " />
<polygon fill="black" stroke="black" points="3416.56,-1157.21 3422.11,-1148.89 3423.77,-1150 3418.22,-1158.32 3416.56,-1157.21" />
<polyline fill="none" stroke="black" points="3418.5,-1152.5 3422.66,-1155.27 " />
<polygon fill="black" stroke="black" points="3420.72,-1159.99 3426.27,-1151.67 3427.93,-1152.78 3422.38,-1161.1 3420.72,-1159.99" />
<polyline fill="none" stroke="black" points="3422.66,-1155.27 3426.82,-1158.05 " />
<text text-anchor="start" x="2345.5" y="-989.3" font-family="Helvetica,sans-Serif" font-size="14.00">
fk_domain_transfer_billing_recurrence_id
</text>
@@ -2881,53 +2905,53 @@ td.section {
</g> <!-- domain_6c51cffa&#45;&gt;contact_8de8cb16 -->
<g id="edge13" class="edge">
<title>domain_6c51cffa:w-&gt;contact_8de8cb16:e</title>
<path fill="none" stroke="black" d="M1436.91,-2058.53C1416.8,-2097.51 1454.86,-2238.31 1426,-2269.5 1331.6,-2371.52 1257.66,-2333.49 1121.82,-2331.57" />
<polygon fill="black" stroke="black" points="1443.67,-2054.04 1454.49,-2052.25 1447.84,-2051.27 1452,-2048.5 1452,-2048.5 1452,-2048.5 1447.84,-2051.27 1449.51,-2044.75 1443.67,-2054.04 1443.67,-2054.04" />
<ellipse fill="none" stroke="black" cx="1440.34" cy="-2056.25" rx="4" ry="4" />
<polygon fill="black" stroke="black" points="1112.46,-2336.51 1112.54,-2326.51 1114.54,-2326.52 1114.46,-2336.52 1112.46,-2336.51" />
<polyline fill="none" stroke="black" points="1111.5,-2331.5 1116.5,-2331.54 " />
<polygon fill="black" stroke="black" points="1117.46,-2336.54 1117.54,-2326.54 1119.54,-2326.56 1119.46,-2336.56 1117.46,-2336.54" />
<polyline fill="none" stroke="black" points="1116.5,-2331.54 1121.5,-2331.57 " />
<text text-anchor="start" x="1218" y="-2344.3" font-family="Helvetica,sans-Serif" font-size="14.00">
<path fill="none" stroke="black" d="M1437.49,-2059.39C1421.3,-2096.02 1449.83,-2213.94 1426,-2241.5 1333.13,-2348.9 1260.37,-2332.36 1121.61,-2331.53" />
<polygon fill="black" stroke="black" points="1444,-2054.5 1454.7,-2052.1 1448,-2051.5 1452,-2048.5 1452,-2048.5 1452,-2048.5 1448,-2051.5 1449.3,-2044.9 1444,-2054.5 1444,-2054.5" />
<ellipse fill="none" stroke="black" cx="1440.8" cy="-2056.91" rx="4" ry="4" />
<polygon fill="black" stroke="black" points="1112.48,-2336.5 1112.52,-2326.5 1114.52,-2326.51 1114.48,-2336.51 1112.48,-2336.5" />
<polyline fill="none" stroke="black" points="1111.5,-2331.5 1116.5,-2331.52 " />
<polygon fill="black" stroke="black" points="1117.48,-2336.52 1117.52,-2326.52 1119.52,-2326.52 1119.48,-2336.52 1117.48,-2336.52" />
<polyline fill="none" stroke="black" points="1116.5,-2331.52 1121.5,-2331.53 " />
<text text-anchor="start" x="1218" y="-2337.3" font-family="Helvetica,sans-Serif" font-size="14.00">
fk_domain_admin_contact
</text>
</g> <!-- domain_6c51cffa&#45;&gt;contact_8de8cb16 -->
<g id="edge14" class="edge">
<title>domain_6c51cffa:w-&gt;contact_8de8cb16:e</title>
<path fill="none" stroke="black" d="M1438.78,-2042.24C1428.56,-2074.05 1443.62,-2155.85 1426,-2175.5 1346.85,-2263.77 1266.83,-2182.08 1170,-2250.5 1136.85,-2273.92 1152.53,-2322.2 1121.55,-2330.33" />
<polygon fill="black" stroke="black" points="1444.8,-2036.44 1455.12,-2032.74 1448.4,-2032.97 1452,-2029.5 1452,-2029.5 1452,-2029.5 1448.4,-2032.97 1448.88,-2026.26 1444.8,-2036.44 1444.8,-2036.44" />
<ellipse fill="none" stroke="black" cx="1441.92" cy="-2039.21" rx="4" ry="4" />
<polygon fill="black" stroke="black" points="1113.07,-2336.35 1111.91,-2326.42 1113.9,-2326.19 1115.06,-2336.12 1113.07,-2336.35" />
<polyline fill="none" stroke="black" points="1111.5,-2331.5 1116.47,-2330.92 " />
<polygon fill="black" stroke="black" points="1118.04,-2335.77 1116.88,-2325.84 1118.87,-2325.61 1120.03,-2335.54 1118.04,-2335.77" />
<polyline fill="none" stroke="black" points="1116.47,-2330.92 1121.43,-2330.34 " />
<text text-anchor="start" x="1219.5" y="-2254.3" font-family="Helvetica,sans-Serif" font-size="14.00">
<path fill="none" stroke="black" d="M1436.82,-2039.75C1433.13,-2052.92 1439.64,-2075.83 1426,-2090.5 1338.85,-2184.27 1261.26,-2132.73 1170,-2222.5 1133.48,-2258.42 1161.95,-2322.02 1121.56,-2330.55" />
<polygon fill="black" stroke="black" points="1443.71,-2035.1 1454.52,-2033.23 1447.86,-2032.3 1452,-2029.5 1452,-2029.5 1452,-2029.5 1447.86,-2032.3 1449.48,-2025.77 1443.71,-2035.1 1443.71,-2035.1" />
<ellipse fill="none" stroke="black" cx="1440.4" cy="-2037.33" rx="4" ry="4" />
<polygon fill="black" stroke="black" points="1112.97,-2336.38 1112.02,-2326.43 1114.01,-2326.24 1114.96,-2336.19 1112.97,-2336.38" />
<polyline fill="none" stroke="black" points="1111.5,-2331.5 1116.48,-2331.03 " />
<polygon fill="black" stroke="black" points="1117.95,-2335.91 1117,-2325.96 1118.99,-2325.77 1119.94,-2335.72 1117.95,-2335.91" />
<polyline fill="none" stroke="black" points="1116.48,-2331.03 1121.46,-2330.55 " />
<text text-anchor="start" x="1219.5" y="-2226.3" font-family="Helvetica,sans-Serif" font-size="14.00">
fk_domain_billing_contact
</text>
</g> <!-- domain_6c51cffa&#45;&gt;contact_8de8cb16 -->
<g id="edge15" class="edge">
<title>domain_6c51cffa:w-&gt;contact_8de8cb16:e</title>
<path fill="none" stroke="black" d="M1435.21,-2017.96C1432.76,-2020.09 1429.99,-2022.4 1426,-2024.5 1312.75,-2084.18 1253.31,-2059.31 1170,-2156.5 1119.02,-2215.97 1186.86,-2321.47 1121.62,-2330.83" />
<polygon fill="black" stroke="black" points="1442.86,-2014.56 1453.83,-2014.61 1447.43,-2012.53 1452,-2010.5 1452,-2010.5 1452,-2010.5 1447.43,-2012.53 1450.17,-2006.39 1442.86,-2014.56 1442.86,-2014.56" />
<ellipse fill="none" stroke="black" cx="1439.2" cy="-2016.18" rx="4" ry="4" />
<polygon fill="black" stroke="black" points="1112.83,-2336.42 1112.17,-2326.44 1114.16,-2326.31 1114.82,-2336.29 1112.83,-2336.42" />
<polyline fill="none" stroke="black" points="1111.5,-2331.5 1116.49,-2331.17 " />
<polygon fill="black" stroke="black" points="1117.82,-2336.09 1117.16,-2326.12 1119.15,-2325.98 1119.81,-2335.96 1117.82,-2336.09" />
<polyline fill="none" stroke="black" points="1116.49,-2331.17 1121.48,-2330.84 " />
<text text-anchor="start" x="1209" y="-2160.3" font-family="Helvetica,sans-Serif" font-size="14.00">
<path fill="none" stroke="black" d="M1438.06,-2022.15C1435.66,-2028.05 1433.31,-2034.74 1426,-2038.5 1324,-2091 1254.81,-1994.24 1170,-2071.5 1085.09,-2148.85 1221.23,-2320.68 1121.54,-2331.01" />
<polygon fill="black" stroke="black" points="1444.33,-2016.91 1454.89,-2013.95 1448.16,-2013.71 1452,-2010.5 1452,-2010.5 1452,-2010.5 1448.16,-2013.71 1449.11,-2007.05 1444.33,-2016.91 1444.33,-2016.91" />
<ellipse fill="none" stroke="black" cx="1441.26" cy="-2019.48" rx="4" ry="4" />
<polygon fill="black" stroke="black" points="1112.74,-2336.45 1112.26,-2326.46 1114.25,-2326.36 1114.74,-2336.35 1112.74,-2336.45" />
<polyline fill="none" stroke="black" points="1111.5,-2331.5 1116.49,-2331.26 " />
<polygon fill="black" stroke="black" points="1117.74,-2336.2 1117.25,-2326.21 1119.25,-2326.12 1119.73,-2336.11 1117.74,-2336.2" />
<polyline fill="none" stroke="black" points="1116.49,-2331.26 1121.49,-2331.01 " />
<text text-anchor="start" x="1209" y="-2075.3" font-family="Helvetica,sans-Serif" font-size="14.00">
fk_domain_registrant_contact
</text>
</g> <!-- domain_6c51cffa&#45;&gt;contact_8de8cb16 -->
<g id="edge16" class="edge">
<title>domain_6c51cffa:w-&gt;contact_8de8cb16:e</title>
<path fill="none" stroke="black" d="M1433.71,-1991.01C1320.23,-1985.02 1259.53,-1926.75 1170,-2005.5 1062.17,-2100.35 1249.16,-2320.41 1121.67,-2331.1" />
<polygon fill="black" stroke="black" points="1442,-1991.23 1451.88,-1996 1447,-1991.37 1452,-1991.5 1452,-1991.5 1452,-1991.5 1447,-1991.37 1452.12,-1987 1442,-1991.23 1442,-1991.23" />
<ellipse fill="none" stroke="black" cx="1438" cy="-1991.13" rx="4" ry="4" />
<polygon fill="black" stroke="black" points="1112.7,-2336.46 1112.3,-2326.46 1114.3,-2326.38 1114.7,-2336.38 1112.7,-2336.46" />
<path fill="none" stroke="black" d="M1433.99,-1991.11C1319.81,-1986.26 1258.29,-1938.53 1170,-2019.5 1068.66,-2112.44 1243.3,-2320.58 1121.62,-2331.09" />
<polygon fill="black" stroke="black" points="1442,-1991.29 1451.9,-1996 1447,-1991.39 1452,-1991.5 1452,-1991.5 1452,-1991.5 1447,-1991.39 1452.1,-1987 1442,-1991.29 1442,-1991.29" />
<ellipse fill="none" stroke="black" cx="1438" cy="-1991.2" rx="4" ry="4" />
<polygon fill="black" stroke="black" points="1112.7,-2336.46 1112.3,-2326.46 1114.29,-2326.38 1114.7,-2336.37 1112.7,-2336.46" />
<polyline fill="none" stroke="black" points="1111.5,-2331.5 1116.5,-2331.3 " />
<polygon fill="black" stroke="black" points="1117.69,-2336.26 1117.3,-2326.27 1119.3,-2326.19 1119.69,-2336.18 1117.69,-2336.26" />
<polyline fill="none" stroke="black" points="1116.5,-2331.3 1121.49,-2331.1 " />
<text text-anchor="start" x="1224" y="-2009.3" font-family="Helvetica,sans-Serif" font-size="14.00">
<polygon fill="black" stroke="black" points="1117.7,-2336.25 1117.29,-2326.26 1119.29,-2326.18 1119.7,-2336.17 1117.7,-2336.25" />
<polyline fill="none" stroke="black" points="1116.5,-2331.3 1121.49,-2331.09 " />
<text text-anchor="start" x="1224" y="-2023.3" font-family="Helvetica,sans-Serif" font-size="14.00">
fk_domain_tech_contact
</text>
</g> <!-- domain_6c51cffa&#45;&gt;registrar_6e1503e3 -->
@@ -2959,9 +2983,9 @@ td.section {
</g> <!-- domain_6c51cffa&#45;&gt;registrar_6e1503e3 -->
<g id="edge61" class="edge">
<title>domain_6c51cffa:w-&gt;registrar_6e1503e3:e</title>
<path fill="none" stroke="black" d="M1434.49,-2357.19C1432.17,-2358.1 1429.51,-2358.94 1426,-2359.5 1305.56,-2378.57 1273.9,-2370.39 1152,-2373.5 964.06,-2378.3 916.36,-2388.95 729,-2373.5 612.71,-2363.91 568.78,-2392.62 470,-2330.5 456.97,-2322.31 460.43,-2304.97 453.75,-2297.29" />
<polygon fill="black" stroke="black" points="1442.34,-2355.09 1453.16,-2356.85 1447.17,-2353.79 1452,-2352.5 1452,-2352.5 1452,-2352.5 1447.17,-2353.79 1450.84,-2348.15 1442.34,-2355.09 1442.34,-2355.09" />
<ellipse fill="none" stroke="black" cx="1438.48" cy="-2356.12" rx="4" ry="4" />
<path fill="none" stroke="black" d="M1433.85,-2352.6C1315.05,-2354.01 1279.32,-2369.05 1152,-2373.5 964.11,-2380.07 916.36,-2388.95 729,-2373.5 612.71,-2363.91 568.78,-2392.62 470,-2330.5 456.97,-2322.31 460.43,-2304.97 453.75,-2297.29" />
<polygon fill="black" stroke="black" points="1442,-2352.56 1452.03,-2357 1447,-2352.53 1452,-2352.5 1452,-2352.5 1452,-2352.5 1447,-2352.53 1451.97,-2348 1442,-2352.56 1442,-2352.56" />
<ellipse fill="none" stroke="black" cx="1438" cy="-2352.58" rx="4" ry="4" />
<polygon fill="black" stroke="black" points="443.12,-2298.52 446.75,-2289.2 448.61,-2289.93 444.98,-2299.25 443.12,-2298.52" />
<polyline fill="none" stroke="black" points="444,-2293.5 448.66,-2295.31 " />
<polygon fill="black" stroke="black" points="447.78,-2300.34 451.4,-2291.02 453.27,-2291.74 449.64,-2301.06 447.78,-2300.34" />
@@ -3440,9 +3464,9 @@ td.section {
</g> <!-- graceperiod_cd3b2e8f&#45;&gt;domain_6c51cffa -->
<g id="edge23" class="edge">
<title>graceperiod_cd3b2e8f:w-&gt;domain_6c51cffa:e</title>
<path fill="none" stroke="black" d="M4559.94,-1763.61C4556.06,-1770.43 4553.72,-1779.71 4545,-1786.5 4104.83,-2129.53 3988.27,-2250.12 3452,-2404.5 3326.23,-2440.71 2928.48,-2447.46 1876.07,-2447.5" />
<polygon fill="black" stroke="black" points="4566.87,-1759.55 4577.77,-1758.38 4571.19,-1757.03 4575.5,-1754.5 4575.5,-1754.5 4575.5,-1754.5 4571.19,-1757.03 4573.23,-1750.62 4566.87,-1759.55 4566.87,-1759.55" />
<ellipse fill="none" stroke="black" cx="4563.42" cy="-1761.58" rx="4" ry="4" />
<path fill="none" stroke="black" d="M4559.93,-1763.92C4556.08,-1771.21 4554.03,-1781.25 4545,-1788.5 4110.16,-2137.57 3987.96,-2250.61 3452,-2404.5 3325.9,-2440.71 2927.48,-2447.46 1876.06,-2447.5" />
<polygon fill="black" stroke="black" points="4566.95,-1759.68 4577.83,-1758.35 4571.22,-1757.09 4575.5,-1754.5 4575.5,-1754.5 4575.5,-1754.5 4571.22,-1757.09 4573.17,-1750.65 4566.95,-1759.68 4566.95,-1759.68" />
<ellipse fill="none" stroke="black" cx="4563.52" cy="-1761.75" rx="4" ry="4" />
<polygon fill="black" stroke="black" points="1867,-2452.5 1867,-2442.5 1869,-2442.5 1869,-2452.5 1867,-2452.5" />
<polyline fill="none" stroke="black" points="1866,-2447.5 1871,-2447.5 " />
<polygon fill="black" stroke="black" points="1872,-2452.5 1872,-2442.5 1874,-2442.5 1874,-2452.5 1872,-2452.5" />
@@ -3453,20 +3477,20 @@ td.section {
</g> <!-- graceperiod_cd3b2e8f&#45;&gt;billingrecurrence_5fa2cb01 -->
<g id="edge10" class="edge">
<title>graceperiod_cd3b2e8f:w-&gt;billingrecurrence_5fa2cb01:e</title>
<path fill="none" stroke="black" d="M4557.23,-1792.28C3979.41,-1778.36 4012.26,-1103.48 3428.88,-1095.57" />
<polygon fill="black" stroke="black" points="4565.5,-1792.38 4575.45,-1797 4570.5,-1792.44 4575.5,-1792.5 4575.5,-1792.5 4575.5,-1792.5 4570.5,-1792.44 4575.55,-1788 4565.5,-1792.38 4565.5,-1792.38" />
<ellipse fill="none" stroke="black" cx="4561.5" cy="-1792.33" rx="4" ry="4" />
<polygon fill="black" stroke="black" points="3419.47,-1100.51 3419.53,-1090.51 3421.53,-1090.52 3421.47,-1100.52 3419.47,-1100.51" />
<polyline fill="none" stroke="black" points="3418.5,-1095.5 3423.5,-1095.53 " />
<polygon fill="black" stroke="black" points="3424.47,-1100.54 3424.53,-1090.54 3426.53,-1090.55 3426.47,-1100.55 3424.47,-1100.54" />
<polyline fill="none" stroke="black" points="3423.5,-1095.53 3428.5,-1095.57 " />
<text text-anchor="start" x="3977.5" y="-1745.3" font-family="Helvetica,sans-Serif" font-size="14.00">
<path fill="none" stroke="black" d="M4557.19,-1792.29C3991.5,-1779.21 3999.86,-1159.82 3428.66,-1152.56" />
<polygon fill="black" stroke="black" points="4565.5,-1792.39 4575.45,-1797 4570.5,-1792.44 4575.5,-1792.5 4575.5,-1792.5 4575.5,-1792.5 4570.5,-1792.44 4575.55,-1788 4565.5,-1792.39 4565.5,-1792.39" />
<ellipse fill="none" stroke="black" cx="4561.5" cy="-1792.34" rx="4" ry="4" />
<polygon fill="black" stroke="black" points="3419.47,-1157.51 3419.53,-1147.51 3421.53,-1147.52 3421.47,-1157.52 3419.47,-1157.51" />
<polyline fill="none" stroke="black" points="3418.5,-1152.5 3423.5,-1152.53 " />
<polygon fill="black" stroke="black" points="3424.47,-1157.54 3424.53,-1147.54 3426.53,-1147.55 3426.47,-1157.55 3424.47,-1157.54" />
<polyline fill="none" stroke="black" points="3423.5,-1152.53 3428.5,-1152.56 " />
<text text-anchor="start" x="3977.5" y="-1749.3" font-family="Helvetica,sans-Serif" font-size="14.00">
fk_grace_period_billing_recurrence_id
</text>
</g> <!-- graceperiod_cd3b2e8f&#45;&gt;registrar_6e1503e3 -->
<g id="edge65" class="edge">
<title>graceperiod_cd3b2e8f:w-&gt;registrar_6e1503e3:e</title>
<path fill="none" stroke="black" d="M4562.37,-1761.11C4494.36,-1645.72 4352,-762.28 4329,-710.5 4254.48,-542.72 4276.58,-371.5 4093,-371.5 4093,-371.5 4093,-371.5 939.5,-371.5 698.57,-371.5 592.12,-434.81 470,-642.5 447.75,-680.34 486.28,-2157.92 449.43,-2284.83" />
<path fill="none" stroke="black" d="M4562.37,-1761.11C4494.36,-1645.72 4352,-762.28 4329,-710.5 4254.48,-542.72 4276.58,-371.5 4093,-371.5 4093,-371.5 4093,-371.5 939.5,-371.5 699.67,-371.5 592.38,-431.24 470,-637.5 447.53,-675.37 486.39,-2157.5 449.45,-2284.8" />
<polygon fill="black" stroke="black" points="4568.23,-1766.64 4572.41,-1776.77 4571.86,-1770.07 4575.5,-1773.5 4575.5,-1773.5 4575.5,-1773.5 4571.86,-1770.07 4578.59,-1770.23 4568.23,-1766.64 4568.23,-1766.64" />
<ellipse fill="none" stroke="black" cx="4565.32" cy="-1763.89" rx="4" ry="4" />
<polygon fill="black" stroke="black" points="448.77,-2295.31 440.29,-2290 441.35,-2288.3 449.83,-2293.61 448.77,-2295.31" />
@@ -3479,22 +3503,22 @@ td.section {
</g> <!-- billingrecurrence_5fa2cb01&#45;&gt;domainhistory_a54cc226 -->
<g id="edge32" class="edge">
<title>billingrecurrence_5fa2cb01:w-&gt;domainhistory_a54cc226:e</title>
<path fill="none" stroke="black" d="M3095.23,-1038.63C2907.08,-1041.28 2833.35,-1086.69 2703,-1232.5 2682,-1256 2705.64,-1297.24 2686.9,-1306.58" />
<polygon fill="black" stroke="black" points="3103.5,-1038.57 3113.53,-1043 3108.5,-1038.53 3113.5,-1038.5 3113.5,-1038.5 3113.5,-1038.5 3108.5,-1038.53 3113.47,-1034 3103.5,-1038.57 3103.5,-1038.57" />
<ellipse fill="none" stroke="black" cx="3099.5" cy="-1038.6" rx="4" ry="4" />
<polygon fill="black" stroke="black" points="2678.93,-1313.22 2677.03,-1303.4 2678.99,-1303.02 2680.9,-1312.84 2678.93,-1313.22" />
<polyline fill="none" stroke="black" points="2677,-1308.5 2681.91,-1307.55 " />
<polygon fill="black" stroke="black" points="2683.84,-1312.27 2681.94,-1302.45 2683.9,-1302.07 2685.8,-1311.89 2683.84,-1312.27" />
<polyline fill="none" stroke="black" points="2681.91,-1307.55 2686.82,-1306.6 " />
<path fill="none" stroke="black" d="M3095.24,-1095.51C2915.84,-1095.74 2835.11,-1101.38 2703,-1232.5 2680.63,-1254.7 2705.32,-1296.93 2686.85,-1306.53" />
<polygon fill="black" stroke="black" points="3103.5,-1095.51 3113.5,-1100 3108.5,-1095.5 3113.5,-1095.5 3113.5,-1095.5 3113.5,-1095.5 3108.5,-1095.5 3113.5,-1091 3103.5,-1095.51 3103.5,-1095.51" />
<ellipse fill="none" stroke="black" cx="3099.5" cy="-1095.51" rx="4" ry="4" />
<polygon fill="black" stroke="black" points="2678.96,-1313.21 2677,-1303.4 2678.96,-1303.01 2680.92,-1312.81 2678.96,-1313.21" />
<polyline fill="none" stroke="black" points="2677,-1308.5 2681.9,-1307.52 " />
<polygon fill="black" stroke="black" points="2683.86,-1312.22 2681.9,-1302.42 2683.86,-1302.03 2685.83,-1311.83 2683.86,-1312.22" />
<polyline fill="none" stroke="black" points="2681.9,-1307.52 2686.81,-1306.54 " />
<text text-anchor="start" x="2769.5" y="-1236.3" font-family="Helvetica,sans-Serif" font-size="14.00">
fk_billing_recurrence_domain_history
</text>
</g> <!-- billingrecurrence_5fa2cb01&#45;&gt;domainhistory_a54cc226 -->
<g id="edge33" class="edge">
<title>billingrecurrence_5fa2cb01:w-&gt;domainhistory_a54cc226:e</title>
<path fill="none" stroke="black" d="M3102.02,-1071.66C3082.53,-1124.69 3079.92,-1309.46 3061,-1327.5 2944.63,-1438.44 2809.84,-1259.36 2703,-1379.5 2669.52,-1417.15 2723.93,-2243 2684.35,-2328.38" />
<polygon fill="black" stroke="black" points="3107.2,-1065.27 3116.99,-1060.33 3110.35,-1061.38 3113.5,-1057.5 3113.5,-1057.5 3113.5,-1057.5 3110.35,-1061.38 3110.01,-1054.67 3107.2,-1065.27 3107.2,-1065.27" />
<ellipse fill="none" stroke="black" cx="3104.68" cy="-1068.37" rx="4" ry="4" />
<path fill="none" stroke="black" d="M3095.6,-1117.76C3064.47,-1132.06 3090.72,-1190.68 3079,-1239.5 3069.68,-1278.32 3090.03,-1300.1 3061,-1327.5 2944.09,-1437.87 2809.84,-1259.36 2703,-1379.5 2669.52,-1417.15 2723.93,-2243 2684.35,-2328.38" />
<polygon fill="black" stroke="black" points="3103.66,-1116.29 3114.31,-1118.93 3108.58,-1115.4 3113.5,-1114.5 3113.5,-1114.5 3113.5,-1114.5 3108.58,-1115.4 3112.69,-1110.07 3103.66,-1116.29 3103.66,-1116.29" />
<ellipse fill="none" stroke="black" cx="3099.73" cy="-1117.01" rx="4" ry="4" />
<polygon fill="black" stroke="black" points="2681.2,-2338.4 2674.24,-2331.21 2675.68,-2329.82 2682.63,-2337.01 2681.2,-2338.4" />
<polyline fill="none" stroke="black" points="2677,-2335.5 2680.59,-2332.02 " />
<polygon fill="black" stroke="black" points="2684.79,-2334.92 2677.83,-2327.73 2679.27,-2326.34 2686.23,-2333.53 2684.79,-2334.92" />
@@ -3505,14 +3529,14 @@ td.section {
</g> <!-- billingrecurrence_5fa2cb01&#45;&gt;registrar_6e1503e3 -->
<g id="edge52" class="edge">
<title>billingrecurrence_5fa2cb01:w-&gt;registrar_6e1503e3:e</title>
<path fill="none" stroke="black" d="M3095.63,-1073.04C3058.72,-1054.83 3108.75,-969.53 3061,-931.5 2929.31,-826.61 2852.65,-900.94 2685,-885.5 2005.23,-822.91 1834.31,-811.62 1152,-790.5 964.09,-784.68 902.76,-718.72 729,-790.5 578.83,-852.53 533.89,-899.11 470,-1048.5 457.09,-1078.69 476.43,-2173.33 448.84,-2284.43" />
<polygon fill="black" stroke="black" points="3103.68,-1074.6 3112.64,-1080.92 3108.59,-1075.55 3113.5,-1076.5 3113.5,-1076.5 3113.5,-1076.5 3108.59,-1075.55 3114.36,-1072.08 3103.68,-1074.6 3103.68,-1074.6" />
<ellipse fill="none" stroke="black" cx="3099.76" cy="-1073.84" rx="4" ry="4" />
<path fill="none" stroke="black" d="M3095.37,-1131.04C3037.15,-1112.09 3125,-988.61 3061,-932.5 2963.4,-846.93 848.96,-740.95 729,-790.5 578.83,-852.53 533.89,-899.11 470,-1048.5 457.09,-1078.69 476.43,-2173.33 448.84,-2284.43" />
<polygon fill="black" stroke="black" points="3103.59,-1132.15 3112.89,-1137.96 3108.55,-1132.83 3113.5,-1133.5 3113.5,-1133.5 3113.5,-1133.5 3108.55,-1132.83 3114.11,-1129.04 3103.59,-1132.15 3103.59,-1132.15" />
<ellipse fill="none" stroke="black" cx="3099.63" cy="-1131.62" rx="4" ry="4" />
<polygon fill="black" stroke="black" points="448.88,-2294.97 440.06,-2290.26 441,-2288.5 449.82,-2293.21 448.88,-2294.97" />
<polyline fill="none" stroke="black" points="444,-2293.5 446.35,-2289.09 " />
<polygon fill="black" stroke="black" points="451.24,-2290.56 442.41,-2285.85 443.36,-2284.09 452.18,-2288.8 451.24,-2290.56" />
<polyline fill="none" stroke="black" points="446.35,-2289.09 448.71,-2284.68 " />
<text text-anchor="start" x="1559" y="-823.3" font-family="Helvetica,sans-Serif" font-size="14.00">
<text text-anchor="start" x="1559" y="-813.3" font-family="Helvetica,sans-Serif" font-size="14.00">
fk_billing_recurrence_registrar_id
</text>
</g> <!-- claimsentry_105da9f1 -->
@@ -4496,13 +4520,13 @@ td.section {
</g> <!-- pollmessage_614a523e&#45;&gt;contact_8de8cb16 -->
<g id="edge17" class="edge">
<title>pollmessage_614a523e:w-&gt;contact_8de8cb16:e</title>
<path fill="none" stroke="black" d="M3845.83,-2876.83C3828.57,-2857.9 3869.78,-2796.41 3837,-2766.5 3709.55,-2650.19 3623.75,-2733.08 3452,-2716.5 2760.49,-2649.73 2576.61,-2735.61 1892,-2617.5 1687.94,-2582.29 1586,-2640.23 1444,-2489.5 1418.71,-2462.66 1452.46,-2434.19 1426,-2408.5 1342.89,-2327.81 1280.04,-2395.69 1170,-2359.5 1146.25,-2351.69 1141.38,-2336.37 1121.7,-2332.44" />
<path fill="none" stroke="black" d="M3845.83,-2876.83C3828.57,-2857.9 3869.78,-2796.41 3837,-2766.5 3709.55,-2650.19 3623.75,-2733.08 3452,-2716.5 2760.49,-2649.73 2576.61,-2735.61 1892,-2617.5 1687.94,-2582.29 1586,-2640.23 1444,-2489.5 1418.71,-2462.66 1452.12,-2434.53 1426,-2408.5 1343.51,-2326.28 1281.93,-2384.69 1170,-2352.5 1147.08,-2345.91 1140.72,-2335.05 1121.64,-2332.2" />
<polygon fill="black" stroke="black" points="3853.5,-2879.36 3861.59,-2886.77 3858.25,-2880.93 3863,-2882.5 3863,-2882.5 3863,-2882.5 3858.25,-2880.93 3864.41,-2878.23 3853.5,-2879.36 3853.5,-2879.36" />
<ellipse fill="none" stroke="black" cx="3849.71" cy="-2878.11" rx="4" ry="4" />
<polygon fill="black" stroke="black" points="1112.04,-2336.57 1112.95,-2326.61 1114.94,-2326.8 1114.03,-2336.75 1112.04,-2336.57" />
<polyline fill="none" stroke="black" points="1111.5,-2331.5 1116.48,-2331.96 " />
<polygon fill="black" stroke="black" points="1117.02,-2337.03 1117.93,-2327.07 1119.92,-2327.25 1119.01,-2337.21 1117.02,-2337.03" />
<polyline fill="none" stroke="black" points="1116.48,-2331.96 1121.46,-2332.41 " />
<polygon fill="black" stroke="black" points="1112.15,-2336.56 1112.84,-2326.58 1114.84,-2326.72 1114.15,-2336.69 1112.15,-2336.56" />
<polyline fill="none" stroke="black" points="1111.5,-2331.5 1116.49,-2331.84 " />
<polygon fill="black" stroke="black" points="1117.14,-2336.9 1117.83,-2326.93 1119.83,-2327.06 1119.14,-2337.04 1117.14,-2336.9" />
<polyline fill="none" stroke="black" points="1116.49,-2331.84 1121.48,-2332.19 " />
<text text-anchor="start" x="2367" y="-2690.3" font-family="Helvetica,sans-Serif" font-size="14.00">
fk_poll_message_contact_repo_id
</text>
@@ -4535,7 +4559,7 @@ td.section {
</g> <!-- pollmessage_614a523e&#45;&gt;domainhistory_a54cc226 -->
<g id="edge42" class="edge">
<title>pollmessage_614a523e:w-&gt;domainhistory_a54cc226:e</title>
<path fill="none" stroke="black" d="M3851.94,-2830.23C3834.77,-2776.71 3842.85,-2589.68 3837,-2563.5 3732.52,-2095.75 3567.96,-2018.66 3470,-1549.5 3463.65,-1519.09 3474.66,-1291.75 3452,-1270.5 3331.06,-1157.11 3244.61,-1263.03 3079,-1270.5 2911.38,-1278.06 2869.56,-1284.18 2703,-1304.5 2695.75,-1305.39 2691.97,-1306.73 2687.3,-1307.61" />
<path fill="none" stroke="black" d="M3851.94,-2830.23C3834.76,-2776.71 3842.81,-2589.69 3837,-2563.5 3732.38,-2092.36 3568.5,-2013.96 3470,-1541.5 3463.84,-1511.96 3474.03,-1291.12 3452,-1270.5 3330.96,-1157.23 3244.61,-1263.03 3079,-1270.5 2911.38,-1278.06 2869.56,-1284.18 2703,-1304.5 2695.75,-1305.39 2691.97,-1306.73 2687.3,-1307.61" />
<polygon fill="black" stroke="black" points="3856.88,-2836.59 3859.44,-2847.26 3859.94,-2840.55 3863,-2844.5 3863,-2844.5 3863,-2844.5 3859.94,-2840.55 3866.56,-2841.74 3856.88,-2836.59 3856.88,-2836.59" />
<ellipse fill="none" stroke="black" cx="3854.43" cy="-2833.43" rx="4" ry="4" />
<polygon fill="black" stroke="black" points="2678.43,-1313.4 2677.56,-1303.43 2679.56,-1303.26 2680.42,-1313.22 2678.43,-1313.4" />
@@ -4970,14 +4994,14 @@ td.section {
</g> <!-- pollmessage_614a523e&#45;&gt;registrar_6e1503e3 -->
<g id="edge72" class="edge">
<title>pollmessage_614a523e:w-&gt;registrar_6e1503e3:e</title>
<path fill="none" stroke="black" d="M3857.11,-2541.44C3836.5,-2410.85 3844.9,-1574.11 3837,-1549.5 3748.12,-1272.49 3578.16,-1277.56 3470,-1007.5 3452.07,-962.73 3487.54,-933.1 3452,-900.5 3445.76,-894.78 2245.45,-803.03 2237,-802.5 1755.24,-772.2 1634.59,-764.39 1152,-753.5 964.05,-749.26 916.42,-738.76 729,-753.5 612.81,-762.64 546.46,-706.54 470,-794.5 443.91,-824.52 481.93,-2164.55 449.11,-2284.88" />
<path fill="none" stroke="black" d="M3857.07,-2541.3C3836.34,-2409.67 3845.04,-1566.28 3837,-1541.5 3748.1,-1267.59 3577.99,-1274.46 3470,-1007.5 3451.92,-962.8 3487.41,-933.24 3452,-900.5 3421.45,-872.26 3120.43,-871.28 3079,-867.5 2903.85,-851.53 2860.18,-846.15 2685,-830.5 2485.97,-812.71 2436.56,-802.8 2237,-792.5 1876.97,-773.92 1786.47,-782.81 1426,-777.5 1304.23,-775.71 1273.78,-775.17 1152,-774.5 1000.38,-773.67 573.96,-684.13 470,-794.5 442.73,-823.45 481.82,-2164.45 449.11,-2284.87" />
<polygon fill="black" stroke="black" points="3859.74,-2549.05 3858.75,-2559.97 3861.37,-2553.77 3863,-2558.5 3863,-2558.5 3863,-2558.5 3861.37,-2553.77 3867.25,-2557.03 3859.74,-2549.05 3859.74,-2549.05" />
<ellipse fill="none" stroke="black" cx="3858.43" cy="-2545.27" rx="4" ry="4" />
<polygon fill="black" stroke="black" points="448.81,-2295.19 440.21,-2290.09 441.23,-2288.37 449.83,-2293.47 448.81,-2295.19" />
<polyline fill="none" stroke="black" points="444,-2293.5 446.55,-2289.2 " />
<polygon fill="black" stroke="black" points="451.36,-2290.89 442.76,-2285.79 443.78,-2284.07 452.38,-2289.17 451.36,-2290.89" />
<polyline fill="none" stroke="black" points="446.55,-2289.2 449.1,-2284.9 " />
<text text-anchor="start" x="1896" y="-806.3" font-family="Helvetica,sans-Serif" font-size="14.00">
<polygon fill="black" stroke="black" points="451.36,-2290.88 442.75,-2285.79 443.77,-2284.07 452.38,-2289.16 451.36,-2290.88" />
<polyline fill="none" stroke="black" points="446.55,-2289.2 449.09,-2284.89 " />
<text text-anchor="start" x="1896" y="-796.3" font-family="Helvetica,sans-Serif" font-size="14.00">
fk_poll_message_transfer_response_losing_registrar_id
</text>
</g> <!-- cursor_6af40e8c -->
@@ -7368,6 +7392,21 @@ td.section {
<td class="minwidth">recurrence_time_of_year</td>
<td class="minwidth">text</td>
</tr>
<tr>
<td class="spacer"></td>
<td class="minwidth">renewal_price_behavior</td>
<td class="minwidth">text not null</td>
</tr>
<tr>
<td class="spacer"></td>
<td class="minwidth">renewal_price_currency</td>
<td class="minwidth">text</td>
</tr>
<tr>
<td class="spacer"></td>
<td class="minwidth">renewal_price_amount</td>
<td class="minwidth">numeric(19, 2)</td>
</tr>
<tr>
<td colspan="3"></td>
</tr>

View File

@@ -112,3 +112,4 @@ V111__add_billingcancellation_missing_indexes.sql
V112__add_billingrecurrence_missing_indexes.sql
V113__add_host_missing_indexes.sql
V114__add_allocation_token_indexes.sql
V115__add_renewal_columns_to_billing_recurrence.sql

View File

@@ -0,0 +1,17 @@
-- Copyright 2022 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 "BillingRecurrence" add column renewal_price_behavior text default 'DEFAULT' not null;
alter table "BillingRecurrence" add column renewal_price_currency text;
alter table "BillingRecurrence" add column renewal_price_amount numeric(19, 2);

View File

@@ -116,7 +116,10 @@ CREATE TABLE public."BillingRecurrence" (
reason text NOT NULL,
domain_name text NOT NULL,
recurrence_end_time timestamp with time zone,
recurrence_time_of_year text
recurrence_time_of_year text,
renewal_price_behavior text DEFAULT 'DEFAULT'::text NOT NULL,
renewal_price_currency text,
renewal_price_amount numeric(19,2)
);