1
0
mirror of https://github.com/google/nomulus synced 2026-07-15 04:22:24 +00:00

Compare commits

...

11 Commits

Author SHA1 Message Date
gbrodman 5fb95f38ed Don't always require contacts in CreateDomainCommand (#2755)
If contacts are optional, they should be optional in the command too.
2025-05-15 20:22:07 +00:00
gbrodman dfe8e24761 Add registrar_id col to password reset requests (#2756)
This is just so that we can add an additional layer of security on
verification
2025-05-15 20:13:27 +00:00
Juan Celhay bd30fcc81c Remove registrar id from invoice grouping key (#2749)
* Remove registrar id from invoice grouping key

* Fix formatting issues

* Update BillingEventTests
2025-05-13 20:29:25 +00:00
gbrodman 8cecc8d3a8 Use the primary DB for DomainInfoFlow (#2750)
This avoids potential replication lag issues when requesting info on
domains that were just created.
2025-05-13 18:00:30 +00:00
Pavlo Tkach c5a39bccc5 Add Console POC reminder front-end (#2754) 2025-05-12 20:14:56 +00:00
gbrodman a90a117341 Add SQL table for password resets (#2751)
We plan on using this for EPP password resets and registry lock password
resets for now.
2025-05-08 19:16:08 +00:00
Weimin Yu b40ad54daf Hardcode beam pipelines to use GKE for tasks (#2753) 2025-05-08 17:29:30 +00:00
Pavlo Tkach b4d239c329 Add console POC reminder backend support (#2747) 2025-04-30 14:15:43 +00:00
gbrodman daa7ab3bfa Disable primary-contact editing in console (#2745)
This is necessary because we'll use primary-contact emails as a way of
resetting passwords.

In the UI, don't allow editing of email address for primary contacts,
and don't allow addition/removal of the primary contact field
post-creation.

In the backend, make sure that all emails previously added still exist.
2025-04-29 17:32:29 +00:00
gbrodman 56cd2ad282 Change AllocationToken behavior in non-catastrophic situations (#2730)
We're changing the way that allocation tokens work in suboptimal (i.e. incorrect) situations in the domain check, creation, and renewal process. Currently, if a token is not applicable, in any way, to any of the operations (including when a check has multiple operations requested) we return some variation of "Allocation token not valid" for all of those options. We wish to allow for a more lenient process, where if a token is "not applicable" instead of "invalid", we just pass through that part of the request as if the token were not there.

Types of errors that will remain catastrophic, where we'll basically return a token error immediately in all cases:
- nonexistent or null token
- token is assigned to a particular domain and the request isn't for that domain
- token is not valid for this registrar
- token is a single-use token that has already been redeemed
- token has a promotional schedule and it's no longer valid

Types of errors that will now be a silent pass-through, as if the user did not issue a token:
- token is not allowed for this TLD
- token has a discount, is not valid for premium names, and the domain name is premium
- token does not allow the provided EPP action

Currently, the last three types of errors cause that generic "token invalid" message but in the future, we'll pass the requests through as if the user did not pass in a token. This does allow for a default token to apply to these requests if available, meaning that it's possible that a single DomainCheckFlow with multiple check requests could use the provided token for some check(s), and a default token for others.

The flip side of this is that if the user passes in a catastrophically invalid token (the first five error messages above), we will return that result to any/all checks that they request, even if there are other issues with that request (e.g. the domain is reserved or already registered).

See b/315504612 for more details and background
2025-04-23 15:09:37 +00:00
gbrodman 0472dda860 Remove transaction duration logging (#2748)
We suspected this could be a cause of optimistic locking failures
(because long transactions would lead to optimistic locks not being
released) but this didn't end up being the case. Let's remove this to
reduce log spam.
2025-04-22 18:53:21 +00:00
67 changed files with 2287 additions and 1935 deletions
+98 -10
View File
@@ -14,30 +14,71 @@
import { provideHttpClient } from '@angular/common/http';
import { provideHttpClientTesting } from '@angular/common/http/testing';
import { TestBed } from '@angular/core/testing';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { AppComponent } from './app.component';
import { MaterialModule } from './material.module';
import { BackendService } from './shared/services/backend.service';
import { AppRoutingModule } from './app-routing.module';
import { routes } from './app-routing.module';
import { AppModule } from './app.module';
import { PocReminderComponent } from './shared/components/pocReminder/pocReminder.component';
import { RouterModule } from '@angular/router';
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
import { UserData, UserDataService } from './shared/services/userData.service';
import { Registrar, RegistrarService } from './registrar/registrar.service';
import { MatSidenavModule } from '@angular/material/sidenav';
import { signal, WritableSignal } from '@angular/core';
describe('AppComponent', () => {
let component: AppComponent;
let fixture: ComponentFixture<AppComponent>;
let mockRegistrarService: {
registrar: WritableSignal<Partial<Registrar> | null | undefined>;
registrarId: WritableSignal<string>;
registrars: WritableSignal<Array<Partial<Registrar>>>;
};
let mockUserDataService: { userData: WritableSignal<Partial<UserData>> };
let mockSnackBar: jasmine.SpyObj<MatSnackBar>;
const dummyPocReminderComponent = class {}; // Dummy class for type checking
beforeEach(async () => {
mockRegistrarService = {
registrar: signal<Registrar | null | undefined>(undefined),
registrarId: signal('123'),
registrars: signal([]),
};
mockUserDataService = {
userData: signal({
globalRole: 'NONE',
}),
};
mockSnackBar = jasmine.createSpyObj('MatSnackBar', ['openFromComponent']);
await TestBed.configureTestingModule({
declarations: [AppComponent],
imports: [
MaterialModule,
BrowserAnimationsModule,
AppRoutingModule,
MatSidenavModule,
NoopAnimationsModule,
MatSnackBarModule,
AppModule,
RouterModule.forRoot(routes),
],
providers: [
BackendService,
{ provide: RegistrarService, useValue: mockRegistrarService },
{ provide: UserDataService, useValue: mockUserDataService },
{ provide: MatSnackBar, useValue: mockSnackBar },
{ provide: PocReminderComponent, useClass: dummyPocReminderComponent },
provideHttpClient(),
provideHttpClientTesting(),
],
}).compileComponents();
fixture = TestBed.createComponent(AppComponent);
component = fixture.componentInstance;
});
afterEach(() => {
jasmine.clock().uninstall();
});
it('should create the app', () => {
@@ -46,4 +87,51 @@ describe('AppComponent', () => {
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
describe('PoC Verification Reminder', () => {
beforeEach(() => {
jasmine.clock().install();
});
it('should open snackbar if lastPocVerificationDate is older than one year', fakeAsync(() => {
const MOCK_TODAY = new Date('2024-07-15T10:00:00.000Z');
jasmine.clock().mockDate(MOCK_TODAY);
const twoYearsAgo = new Date(MOCK_TODAY);
twoYearsAgo.setFullYear(MOCK_TODAY.getFullYear() - 2);
mockRegistrarService.registrar.set({
lastPocVerificationDate: twoYearsAgo.toISOString(),
});
fixture.detectChanges();
TestBed.flushEffects();
expect(mockSnackBar.openFromComponent).toHaveBeenCalledWith(
PocReminderComponent,
{
horizontalPosition: 'center',
verticalPosition: 'top',
duration: 1000000000,
}
);
}));
it('should NOT open snackbar if lastPocVerificationDate is within last year', fakeAsync(() => {
const MOCK_TODAY = new Date('2024-07-15T10:00:00.000Z');
jasmine.clock().mockDate(MOCK_TODAY);
const sixMonthsAgo = new Date(MOCK_TODAY);
sixMonthsAgo.setMonth(MOCK_TODAY.getMonth() - 6);
mockRegistrarService.registrar.set({
lastPocVerificationDate: sixMonthsAgo.toISOString(),
});
fixture.detectChanges();
TestBed.flushEffects();
expect(mockSnackBar.openFromComponent).not.toHaveBeenCalled();
}));
});
});
+25 -3
View File
@@ -12,13 +12,15 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { AfterViewInit, Component, ViewChild } from '@angular/core';
import { AfterViewInit, Component, effect, ViewChild } from '@angular/core';
import { MatSidenav } from '@angular/material/sidenav';
import { NavigationEnd, Router } from '@angular/router';
import { RegistrarService } from './registrar/registrar.service';
import { BreakPointObserverService } from './shared/services/breakPoint.service';
import { GlobalLoaderService } from './shared/services/globalLoader.service';
import { UserDataService } from './shared/services/userData.service';
import { MatSnackBar } from '@angular/material/snack-bar';
import { PocReminderComponent } from './shared/components/pocReminder/pocReminder.component';
@Component({
selector: 'app-root',
@@ -35,8 +37,28 @@ export class AppComponent implements AfterViewInit {
protected userDataService: UserDataService,
protected globalLoader: GlobalLoaderService,
protected breakpointObserver: BreakPointObserverService,
private router: Router
) {}
private router: Router,
private _snackBar: MatSnackBar
) {
effect(() => {
const registrar = this.registrarService.registrar();
const oneYearAgo = new Date();
oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
oneYearAgo.setHours(0, 0, 0, 0);
if (
registrar &&
registrar.lastPocVerificationDate &&
new Date(registrar.lastPocVerificationDate) < oneYearAgo &&
this.userDataService?.userData()?.globalRole === 'NONE'
) {
this._snackBar.openFromComponent(PocReminderComponent, {
horizontalPosition: 'center',
verticalPosition: 'top',
duration: 1000000000,
});
}
});
}
ngAfterViewInit() {
this.router.events.subscribe((event) => {
+2
View File
@@ -60,6 +60,7 @@ import { TldsComponent } from './tlds/tlds.component';
import { ForceFocusDirective } from './shared/directives/forceFocus.directive';
import RdapComponent from './settings/rdap/rdap.component';
import RdapEditComponent from './settings/rdap/rdapEdit.component';
import { PocReminderComponent } from './shared/components/pocReminder/pocReminder.component';
@NgModule({
declarations: [SelectedRegistrarWrapper],
@@ -86,6 +87,7 @@ export class SelectedRegistrarModule {}
RdapComponent,
RdapEditComponent,
ReasonDialogComponent,
PocReminderComponent,
RegistrarComponent,
RegistrarDetailsComponent,
RegistrarSelectorComponent,
@@ -57,7 +57,7 @@ export class NavigationComponent {
}
ngOnDestroy() {
this.subscription.unsubscribe();
this.subscription && this.subscription.unsubscribe();
}
getElementId(node: RouteWithIcon) {
@@ -71,6 +71,7 @@ export interface Registrar
registrarName: string;
registryLockAllowed?: boolean;
type?: string;
lastPocVerificationDate?: string;
}
@Injectable({
@@ -16,11 +16,7 @@ import { Component, effect, ViewEncapsulation } from '@angular/core';
import { MatTableDataSource } from '@angular/material/table';
import { take } from 'rxjs';
import { RegistrarService } from 'src/app/registrar/registrar.service';
import {
ContactService,
contactTypeToViewReadyContact,
ViewReadyContact,
} from './contact.service';
import { ContactService, ViewReadyContact } from './contact.service';
@Component({
selector: 'app-contact',
@@ -83,7 +83,7 @@ export class ContactService {
: contactTypeToViewReadyContact({
emailAddress: '',
name: '',
types: ['ADMIN'],
types: ['TECH'],
faxNumber: '',
phoneNumber: '',
registrarId: '',
@@ -56,6 +56,7 @@
[required]="true"
[(ngModel)]="contactService.contactInEdit.emailAddress"
[ngModelOptions]="{ standalone: true }"
[disabled]="emailAddressIsDisabled()"
/>
</mat-form-field>
@@ -85,14 +86,18 @@
<mat-icon color="accent">error</mat-icon>Required to select at least one
</p>
<div class="">
<mat-checkbox
<ng-container
*ngFor="let contactType of contactTypeToTextMap | keyvalue"
[checked]="checkboxIsChecked(contactType.key)"
(change)="checkboxOnChange($event, contactType.key)"
[disabled]="checkboxIsDisabled(contactType.key)"
>
{{ contactType.value }}
</mat-checkbox>
<mat-checkbox
*ngIf="shouldDisplayCheckbox(contactType.key)"
[checked]="checkboxIsChecked(contactType.key)"
(change)="checkboxOnChange($event, contactType.key)"
[disabled]="checkboxIsDisabled(contactType.key)"
>
{{ contactType.value }}
</mat-checkbox>
</ng-container>
</div>
</section>
@@ -82,6 +82,10 @@ export class ContactDetailsComponent {
});
}
shouldDisplayCheckbox(type: string) {
return type !== 'ADMIN' || this.checkboxIsChecked(type);
}
checkboxIsChecked(type: string) {
return this.contactService.contactInEdit.types.includes(
type as contactType
@@ -89,6 +93,9 @@ export class ContactDetailsComponent {
}
checkboxIsDisabled(type: string) {
if (type === 'ADMIN') {
return true;
}
return (
this.contactService.contactInEdit.types.length === 1 &&
this.contactService.contactInEdit.types[0] === (type as contactType)
@@ -105,4 +112,8 @@ export class ContactDetailsComponent {
);
}
}
emailAddressIsDisabled() {
return this.contactService.contactInEdit.types.includes('ADMIN');
}
}
@@ -0,0 +1,14 @@
<div class="console-app__pocReminder">
<p class="">
Please take a moment to complete annual review of
<a routerLink="/settings">contacts</a>.
</p>
<span matSnackBarActions>
<button mat-button matSnackBarAction (click)="confirmReviewed()">
Confirm reviewed
</button>
<button mat-button matSnackBarAction (click)="snackBarRef.dismiss()">
Close
</button>
</span>
</div>
@@ -0,0 +1,5 @@
.console-app__pocReminder {
a {
color: white !important;
}
}
@@ -0,0 +1,53 @@
// Copyright 2025 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.
import { Component } from '@angular/core';
import { MatSnackBar, MatSnackBarRef } from '@angular/material/snack-bar';
import { RegistrarService } from '../../../registrar/registrar.service';
import { HttpErrorResponse } from '@angular/common/http';
@Component({
selector: 'app-poc-reminder',
templateUrl: './pocReminder.component.html',
styleUrls: ['./pocReminder.component.scss'],
standalone: false,
})
export class PocReminderComponent {
constructor(
public snackBarRef: MatSnackBarRef<PocReminderComponent>,
private registrarService: RegistrarService,
private _snackBar: MatSnackBar
) {}
confirmReviewed() {
if (this.registrarService.registrar()) {
const todayMidnight = new Date();
todayMidnight.setHours(0, 0, 0, 0);
this.registrarService
// @ts-ignore - if check above won't allow empty object to be submitted
.updateRegistrar({
...this.registrarService.registrar(),
lastPocVerificationDate: todayMidnight.toISOString(),
})
.subscribe({
error: (err: HttpErrorResponse) => {
this._snackBar.open(err.error || err.message);
},
next: () => {
this.snackBarRef.dismiss();
},
});
}
}
}
+1
View File
@@ -61,6 +61,7 @@ def fragileTestPatterns = [
// Currently changes a global configuration parameter that for some reason
// results in timestamp inversions for other tests. TODO(mmuller): fix.
"google/registry/flows/host/HostInfoFlowTest.*",
"google/registry/beam/common/RegistryPipelineWorkerInitializerTest.*",
] + dockerIncompatibleTestPatterns
sourceSets {
@@ -172,7 +172,7 @@ public record BillingEvent(
.minusDays(1)
.toString(),
billingId(),
registrarId(),
"",
String.format("%s | TLD: %s | TERM: %d-year", action(), tld(), years()),
amount(),
currency(),
@@ -36,8 +36,6 @@ import google.registry.config.RegistryConfig.ConfigModule;
import google.registry.flows.custom.CustomLogicFactoryModule;
import google.registry.flows.custom.CustomLogicModule;
import google.registry.flows.domain.DomainPricingLogic;
import google.registry.flows.domain.DomainPricingLogic.AllocationTokenInvalidForCurrencyException;
import google.registry.flows.domain.DomainPricingLogic.AllocationTokenInvalidForPremiumNameException;
import google.registry.model.ImmutableObject;
import google.registry.model.billing.BillingBase.Flag;
import google.registry.model.billing.BillingCancellation;
@@ -389,38 +387,30 @@ public class ExpandBillingRecurrencesPipeline implements Serializable {
// It is OK to always create a OneTime, even though the domain might be deleted or transferred
// later during autorenew grace period, as a cancellation will always be written out in those
// instances.
BillingEvent billingEvent = null;
try {
billingEvent =
new BillingEvent.Builder()
.setBillingTime(billingTime)
.setRegistrarId(billingRecurrence.getRegistrarId())
// Determine the cost for a one-year renewal.
.setCost(
domainPricingLogic
.getRenewPrice(
tld,
billingRecurrence.getTargetId(),
eventTime,
1,
billingRecurrence,
Optional.empty())
.getRenewCost())
.setEventTime(eventTime)
.setFlags(union(billingRecurrence.getFlags(), Flag.SYNTHETIC))
.setDomainHistory(historyEntry)
.setPeriodYears(1)
.setReason(billingRecurrence.getReason())
.setSyntheticCreationTime(endTime)
.setCancellationMatchingBillingEvent(billingRecurrence)
.setTargetId(billingRecurrence.getTargetId())
.build();
} catch (AllocationTokenInvalidForCurrencyException
| AllocationTokenInvalidForPremiumNameException e) {
// This should not be reached since we are not using an allocation token
return;
}
results.add(billingEvent);
results.add(
new BillingEvent.Builder()
.setBillingTime(billingTime)
.setRegistrarId(billingRecurrence.getRegistrarId())
// Determine the cost for a one-year renewal.
.setCost(
domainPricingLogic
.getRenewPrice(
tld,
billingRecurrence.getTargetId(),
eventTime,
1,
billingRecurrence,
Optional.empty())
.getRenewCost())
.setEventTime(eventTime)
.setFlags(union(billingRecurrence.getFlags(), Flag.SYNTHETIC))
.setDomainHistory(historyEntry)
.setPeriodYears(1)
.setReason(billingRecurrence.getReason())
.setSyntheticCreationTime(endTime)
.setCancellationMatchingBillingEvent(billingRecurrence)
.setTargetId(billingRecurrence.getTargetId())
.build());
}
results.add(
billingRecurrence
@@ -40,6 +40,8 @@ public class RegistryPipelineWorkerInitializer implements JvmInitializer {
@Override
public void beforeProcessing(PipelineOptions options) {
// TODO(b/416299900): remove next line after GAE is removed.
System.setProperty("google.registry.jetty", "true");
RegistryPipelineOptions registryOptions = options.as(RegistryPipelineOptions.class);
RegistryEnvironment environment = registryOptions.getRegistryEnvironment();
if (environment == null || environment.equals(RegistryEnvironment.UNITTEST)) {
@@ -14,7 +14,6 @@
package google.registry.flows.domain;
import static com.google.common.base.Strings.emptyToNull;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
@@ -42,6 +41,7 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.common.flogger.FluentLogger;
import com.google.common.net.InternetDomainName;
import google.registry.config.RegistryConfig;
import google.registry.config.RegistryConfig.Config;
@@ -55,14 +55,7 @@ import google.registry.flows.annotations.ReportingSpec;
import google.registry.flows.custom.DomainCheckFlowCustomLogic;
import google.registry.flows.custom.DomainCheckFlowCustomLogic.BeforeResponseParameters;
import google.registry.flows.custom.DomainCheckFlowCustomLogic.BeforeResponseReturnData;
import google.registry.flows.domain.DomainPricingLogic.AllocationTokenInvalidForPremiumNameException;
import google.registry.flows.domain.token.AllocationTokenDomainCheckResults;
import google.registry.flows.domain.token.AllocationTokenFlowUtils;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotInPromotionException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForCommandException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForDomainException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForRegistrarException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForTldException;
import google.registry.model.EppResource;
import google.registry.model.ForeignKeyUtils;
import google.registry.model.billing.BillingRecurrence;
@@ -87,7 +80,6 @@ import google.registry.model.tld.Tld;
import google.registry.model.tld.Tld.TldState;
import google.registry.model.tld.label.ReservationType;
import google.registry.persistence.VKey;
import google.registry.pricing.PricingEngineProxy;
import google.registry.util.Clock;
import jakarta.inject.Inject;
import java.util.Collection;
@@ -131,6 +123,8 @@ import org.joda.time.DateTime;
@ReportingSpec(ActivityReportField.DOMAIN_CHECK)
public final class DomainCheckFlow implements TransactionalFlow {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private static final String STANDARD_FEE_RESPONSE_CLASS = "STANDARD";
private static final String STANDARD_PROMOTION_FEE_RESPONSE_CLASS = "STANDARD PROMOTION";
@@ -146,7 +140,6 @@ public final class DomainCheckFlow implements TransactionalFlow {
@Inject @Superuser boolean isSuperuser;
@Inject Clock clock;
@Inject EppResponse.Builder responseBuilder;
@Inject AllocationTokenFlowUtils allocationTokenFlowUtils;
@Inject DomainCheckFlowCustomLogic flowCustomLogic;
@Inject DomainPricingLogic pricingLogic;
@@ -195,36 +188,15 @@ public final class DomainCheckFlow implements TransactionalFlow {
existingDomains.size() == parsedDomains.size()
? ImmutableSet.of()
: getBsaBlockedDomains(parsedDomains.values(), now);
Optional<AllocationTokenExtension> allocationTokenExtension =
eppInput.getSingleExtension(AllocationTokenExtension.class);
Optional<AllocationTokenDomainCheckResults> tokenDomainCheckResults =
allocationTokenExtension.map(
tokenExtension ->
allocationTokenFlowUtils.checkDomainsWithToken(
ImmutableList.copyOf(parsedDomains.values()),
tokenExtension.getAllocationToken(),
registrarId,
now));
ImmutableList.Builder<DomainCheck> checksBuilder = new ImmutableList.Builder<>();
ImmutableSet.Builder<String> availableDomains = new ImmutableSet.Builder<>();
ImmutableMap<String, TldState> tldStates =
Maps.toMap(seenTlds, tld -> Tld.get(tld).getTldState(now));
ImmutableMap<InternetDomainName, String> domainCheckResults =
tokenDomainCheckResults
.map(AllocationTokenDomainCheckResults::domainCheckResults)
.orElse(ImmutableMap.of());
Optional<AllocationToken> allocationToken =
tokenDomainCheckResults.flatMap(AllocationTokenDomainCheckResults::token);
for (String domainName : domainNames) {
Optional<String> message =
getMessageForCheck(
parsedDomains.get(domainName),
existingDomains,
bsaBlockedDomainNames,
domainCheckResults,
tldStates,
allocationToken);
domainName, existingDomains, bsaBlockedDomainNames, tldStates, parsedDomains, now);
boolean isAvailable = message.isEmpty();
checksBuilder.add(DomainCheck.create(isAvailable, domainName, message.orElse(null)));
if (isAvailable) {
@@ -237,11 +209,7 @@ public final class DomainCheckFlow implements TransactionalFlow {
.setDomainChecks(checksBuilder.build())
.setResponseExtensions(
getResponseExtensions(
parsedDomains,
existingDomains,
availableDomains.build(),
now,
allocationToken))
parsedDomains, existingDomains, availableDomains.build(), now))
.setAsOfDate(now)
.build());
return responseBuilder
@@ -251,10 +219,39 @@ public final class DomainCheckFlow implements TransactionalFlow {
}
private Optional<String> getMessageForCheck(
String domainName,
ImmutableMap<String, VKey<Domain>> existingDomains,
ImmutableSet<InternetDomainName> bsaBlockedDomainNames,
ImmutableMap<String, TldState> tldStates,
ImmutableMap<String, InternetDomainName> parsedDomains,
DateTime now) {
InternetDomainName idn = parsedDomains.get(domainName);
Optional<AllocationToken> token;
try {
// Which token we use may vary based on the domain -- a provided token may be invalid for
// some domains, or there may be DEFAULT PROMO tokens only applicable on some domains
token =
AllocationTokenFlowUtils.loadTokenFromExtensionOrGetDefault(
registrarId,
now,
eppInput.getSingleExtension(AllocationTokenExtension.class),
Tld.get(idn.parent().toString()),
domainName,
FeeQueryCommandExtensionItem.CommandName.CREATE);
} catch (AllocationTokenFlowUtils.NonexistentAllocationTokenException
| AllocationTokenFlowUtils.AllocationTokenInvalidException e) {
// The provided token was catastrophically invalid in some way
logger.atInfo().withCause(e).log("Cannot load/use allocation token.");
return Optional.of(e.getMessage());
}
return getMessageForCheckWithToken(
idn, existingDomains, bsaBlockedDomainNames, tldStates, token);
}
private Optional<String> getMessageForCheckWithToken(
InternetDomainName domainName,
ImmutableMap<String, VKey<Domain>> existingDomains,
ImmutableSet<InternetDomainName> bsaBlockedDomains,
ImmutableMap<InternetDomainName, String> tokenCheckResults,
ImmutableMap<String, TldState> tldStates,
Optional<AllocationToken> allocationToken) {
if (existingDomains.containsKey(domainName.toString())) {
@@ -271,11 +268,6 @@ public final class DomainCheckFlow implements TransactionalFlow {
}
}
}
Optional<String> tokenResult =
Optional.ofNullable(emptyToNull(tokenCheckResults.get(domainName)));
if (tokenResult.isPresent()) {
return tokenResult;
}
if (isRegisterBsaCreate(domainName, allocationToken)
|| !bsaBlockedDomains.contains(domainName)) {
return Optional.empty();
@@ -290,8 +282,7 @@ public final class DomainCheckFlow implements TransactionalFlow {
ImmutableMap<String, InternetDomainName> domainNames,
ImmutableMap<String, VKey<Domain>> existingDomains,
ImmutableSet<String> availableDomains,
DateTime now,
Optional<AllocationToken> allocationToken)
DateTime now)
throws EppException {
Optional<FeeCheckCommandExtension> feeCheckOpt =
eppInput.getSingleExtension(FeeCheckCommandExtension.class);
@@ -309,84 +300,24 @@ public final class DomainCheckFlow implements TransactionalFlow {
RegistryConfig.getTieredPricingPromotionRegistrarIds().contains(registrarId);
for (FeeCheckCommandExtensionItem feeCheckItem : feeCheck.getItems()) {
for (String domainName : getDomainNamesToCheckForFee(feeCheckItem, domainNames.keySet())) {
Optional<AllocationToken> defaultToken =
DomainFlowUtils.checkForDefaultToken(
Tld.get(InternetDomainName.from(domainName).parent().toString()),
domainName,
feeCheckItem.getCommandName(),
registrarId,
now);
FeeCheckResponseExtensionItem.Builder<?> builder = feeCheckItem.createResponseBuilder();
Optional<Domain> domain = Optional.ofNullable(domainObjs.get(domainName));
Tld tld = Tld.get(domainNames.get(domainName).parent().toString());
Optional<AllocationToken> token;
try {
if (allocationToken.isPresent()) {
AllocationTokenFlowUtils.validateToken(
InternetDomainName.from(domainName),
allocationToken.get(),
feeCheckItem.getCommandName(),
registrarId,
PricingEngineProxy.isDomainPremium(domainName, now),
now);
}
handleFeeRequest(
feeCheckItem,
builder,
domainNames.get(domainName),
domain,
feeCheck.getCurrency(),
now,
pricingLogic,
allocationToken.isPresent() ? allocationToken : defaultToken,
availableDomains.contains(domainName),
recurrences.getOrDefault(domainName, null));
// In the case of a registrar that is running a tiered pricing promotion, we issue two
// responses for the CREATE fee check command: one (the default response) with the
// non-promotional price, and one (an extra STANDARD PROMO response) with the actual
// promotional price.
if (defaultToken.isPresent()
&& shouldUseTieredPricingPromotion
&& feeCheckItem
.getCommandName()
.equals(FeeQueryCommandExtensionItem.CommandName.CREATE)) {
// First, set the promotional (real) price under the STANDARD PROMO class
builder
.setClass(STANDARD_PROMOTION_FEE_RESPONSE_CLASS)
.setCommand(
FeeQueryCommandExtensionItem.CommandName.CUSTOM,
feeCheckItem.getPhase(),
feeCheckItem.getSubphase());
// Next, get the non-promotional price and set it as the standard response to the CREATE
// fee check command
FeeCheckResponseExtensionItem.Builder<?> nonPromotionalBuilder =
feeCheckItem.createResponseBuilder();
handleFeeRequest(
feeCheckItem,
nonPromotionalBuilder,
domainNames.get(domainName),
domain,
feeCheck.getCurrency(),
now,
pricingLogic,
allocationToken,
availableDomains.contains(domainName),
recurrences.getOrDefault(domainName, null));
responseItems.add(
nonPromotionalBuilder
.setClass(STANDARD_FEE_RESPONSE_CLASS)
.setDomainNameIfSupported(domainName)
.build());
}
responseItems.add(builder.setDomainNameIfSupported(domainName).build());
} catch (AllocationTokenInvalidForPremiumNameException
| AllocationTokenNotValidForCommandException
| AllocationTokenNotValidForDomainException
| AllocationTokenNotValidForRegistrarException
| AllocationTokenNotValidForTldException
| AllocationTokenNotInPromotionException e) {
// Allocation token is either not an active token or it is not valid for the EPP command,
// registrar, domain, or TLD.
Tld tld = Tld.get(InternetDomainName.from(domainName).parent().toString());
// The precise token to use for this fee request may vary based on the domain or even the
// precise command issued (some tokens may be valid only for certain actions)
token =
AllocationTokenFlowUtils.loadTokenFromExtensionOrGetDefault(
registrarId,
now,
eppInput.getSingleExtension(AllocationTokenExtension.class),
tld,
domainName,
feeCheckItem.getCommandName());
} catch (AllocationTokenFlowUtils.NonexistentAllocationTokenException
| AllocationTokenFlowUtils.AllocationTokenInvalidException e) {
// The provided token was catastrophically invalid in some way
responseItems.add(
builder
.setDomainNameIfSupported(domainName)
@@ -398,7 +329,60 @@ public final class DomainCheckFlow implements TransactionalFlow {
.setCurrencyIfSupported(tld.getCurrency())
.setClass("token-not-supported")
.build());
continue;
}
handleFeeRequest(
feeCheckItem,
builder,
domainNames.get(domainName),
domain,
feeCheck.getCurrency(),
now,
pricingLogic,
token,
availableDomains.contains(domainName),
recurrences.getOrDefault(domainName, null));
// In the case of a registrar that is running a tiered pricing promotion, we issue two
// responses for the CREATE fee check command: one (the default response) with the
// non-promotional price, and one (an extra STANDARD PROMO response) with the actual
// promotional price.
if (token
.map(t -> t.getTokenType().equals(AllocationToken.TokenType.DEFAULT_PROMO))
.orElse(false)
&& shouldUseTieredPricingPromotion
&& feeCheckItem
.getCommandName()
.equals(FeeQueryCommandExtensionItem.CommandName.CREATE)) {
// First, set the promotional (real) price under the STANDARD PROMO class
builder
.setClass(STANDARD_PROMOTION_FEE_RESPONSE_CLASS)
.setCommand(
FeeQueryCommandExtensionItem.CommandName.CUSTOM,
feeCheckItem.getPhase(),
feeCheckItem.getSubphase());
// Next, get the non-promotional price and set it as the standard response to the CREATE
// fee check command
FeeCheckResponseExtensionItem.Builder<?> nonPromotionalBuilder =
feeCheckItem.createResponseBuilder();
handleFeeRequest(
feeCheckItem,
nonPromotionalBuilder,
domainNames.get(domainName),
domain,
feeCheck.getCurrency(),
now,
pricingLogic,
Optional.empty(),
availableDomains.contains(domainName),
recurrences.getOrDefault(domainName, null));
responseItems.add(
nonPromotionalBuilder
.setClass(STANDARD_FEE_RESPONSE_CLASS)
.setDomainNameIfSupported(domainName)
.build());
}
responseItems.add(builder.setDomainNameIfSupported(domainName).build());
}
}
return ImmutableList.of(feeCheck.createResponse(responseItems.build()));
@@ -133,11 +133,8 @@ import org.joda.time.Duration;
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForRegistrarException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForTldException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AlreadyRedeemedAllocationTokenException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.InvalidAllocationTokenException}
* @error {@link AllocationTokenFlowUtils.NonexistentAllocationTokenException}
* @error {@link google.registry.flows.exceptions.OnlyToolCanPassMetadataException}
* @error {@link ResourceAlreadyExistsForThisClientException}
* @error {@link ResourceCreateContentionException}
@@ -205,7 +202,6 @@ import org.joda.time.Duration;
* @error {@link DomainFlowUtils.UnexpectedClaimsNoticeException}
* @error {@link DomainFlowUtils.UnsupportedFeeAttributeException}
* @error {@link DomainFlowUtils.UnsupportedMarkTypeException}
* @error {@link DomainPricingLogic.AllocationTokenInvalidForPremiumNameException}
*/
@ReportingSpec(ActivityReportField.DOMAIN_CREATE)
public final class DomainCreateFlow implements MutatingFlow {
@@ -221,7 +217,6 @@ public final class DomainCreateFlow implements MutatingFlow {
@Inject @Superuser boolean isSuperuser;
@Inject DomainHistory.Builder historyBuilder;
@Inject EppResponse.Builder responseBuilder;
@Inject AllocationTokenFlowUtils allocationTokenFlowUtils;
@Inject DomainCreateFlowCustomLogic flowCustomLogic;
@Inject DomainFlowTmchUtils tmchUtils;
@Inject DomainPricingLogic pricingLogic;
@@ -264,25 +259,20 @@ public final class DomainCreateFlow implements MutatingFlow {
}
boolean isSunriseCreate = hasSignedMarks && (tldState == START_DATE_SUNRISE);
Optional<AllocationToken> allocationToken =
allocationTokenFlowUtils.verifyAllocationTokenCreateIfPresent(
command,
tld,
AllocationTokenFlowUtils.loadTokenFromExtensionOrGetDefault(
registrarId,
now,
eppInput.getSingleExtension(AllocationTokenExtension.class));
boolean defaultTokenUsed = false;
if (allocationToken.isEmpty()) {
allocationToken =
DomainFlowUtils.checkForDefaultToken(
tld, command.getDomainName(), CommandName.CREATE, registrarId, now);
if (allocationToken.isPresent()) {
defaultTokenUsed = true;
}
}
eppInput.getSingleExtension(AllocationTokenExtension.class),
tld,
command.getDomainName(),
CommandName.CREATE);
boolean defaultTokenUsed =
allocationToken.map(t -> t.getTokenType().equals(TokenType.DEFAULT_PROMO)).orElse(false);
boolean isAnchorTenant =
isAnchorTenant(
domainName, allocationToken, eppInput.getSingleExtension(MetadataExtension.class));
verifyAnchorTenantValidPeriod(isAnchorTenant, years);
// Superusers can create reserved domains, force creations on domains that require a claims
// notice without specifying a claims key, ignore the registry phase, and override blocks on
// registering premium domains.
@@ -416,7 +406,7 @@ public final class DomainCreateFlow implements MutatingFlow {
entitiesToSave.add(domain, domainHistory);
if (allocationToken.isPresent() && allocationToken.get().getTokenType().isOneTimeUse()) {
entitiesToSave.add(
allocationTokenFlowUtils.redeemToken(
AllocationTokenFlowUtils.redeemToken(
allocationToken.get(), domainHistory.getHistoryEntryId()));
}
if (domain.shouldPublishToDns()) {
@@ -42,7 +42,6 @@ import static google.registry.model.tld.label.ReservationType.RESERVED_FOR_ANCHO
import static google.registry.model.tld.label.ReservationType.RESERVED_FOR_SPECIFIC_USE;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.pricing.PricingEngineProxy.isDomainPremium;
import static google.registry.util.CollectionUtils.isNullOrEmpty;
import static google.registry.util.CollectionUtils.nullToEmpty;
import static google.registry.util.DateTimeUtils.END_OF_TIME;
import static google.registry.util.DateTimeUtils.isAtOrAfter;
@@ -67,7 +66,6 @@ import com.google.common.collect.Sets;
import com.google.common.collect.Streams;
import com.google.common.net.InternetDomainName;
import google.registry.flows.EppException;
import google.registry.flows.EppException.AssociationProhibitsOperationException;
import google.registry.flows.EppException.AuthorizationErrorException;
import google.registry.flows.EppException.CommandUseErrorException;
import google.registry.flows.EppException.ObjectDoesNotExistException;
@@ -77,8 +75,6 @@ import google.registry.flows.EppException.ParameterValueSyntaxErrorException;
import google.registry.flows.EppException.RequiredParameterMissingException;
import google.registry.flows.EppException.StatusProhibitsOperationException;
import google.registry.flows.EppException.UnimplementedOptionException;
import google.registry.flows.domain.DomainPricingLogic.AllocationTokenInvalidForPremiumNameException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils;
import google.registry.flows.exceptions.ResourceHasClientUpdateProhibitedException;
import google.registry.model.EppResource;
import google.registry.model.billing.BillingBase.Flag;
@@ -101,7 +97,6 @@ import google.registry.model.domain.fee.BaseFee.FeeType;
import google.registry.model.domain.fee.Credit;
import google.registry.model.domain.fee.Fee;
import google.registry.model.domain.fee.FeeQueryCommandExtensionItem;
import google.registry.model.domain.fee.FeeQueryCommandExtensionItem.CommandName;
import google.registry.model.domain.fee.FeeQueryResponseExtensionItem;
import google.registry.model.domain.fee.FeeTransformCommandExtension;
import google.registry.model.domain.fee.FeeTransformResponseExtension;
@@ -1233,52 +1228,6 @@ public class DomainFlowUtils {
.getResultList();
}
/**
* Checks if there is a valid default token to be used for a domain create command.
*
* <p>If there is more than one valid default token for the registration, only the first valid
* token found on the TLD's default token list will be returned.
*/
public static Optional<AllocationToken> checkForDefaultToken(
Tld tld, String domainName, CommandName commandName, String registrarId, DateTime now)
throws EppException {
if (isNullOrEmpty(tld.getDefaultPromoTokens())) {
return Optional.empty();
}
Map<VKey<AllocationToken>, Optional<AllocationToken>> tokens =
AllocationToken.getAll(tld.getDefaultPromoTokens());
ImmutableList<Optional<AllocationToken>> tokenList =
tld.getDefaultPromoTokens().stream()
.map(tokens::get)
.filter(Optional::isPresent)
.collect(toImmutableList());
checkState(
!isNullOrEmpty(tokenList),
"Failure while loading default TLD promotions from the database");
// Check if any of the tokens are valid for this domain registration
for (Optional<AllocationToken> token : tokenList) {
try {
AllocationTokenFlowUtils.validateToken(
InternetDomainName.from(domainName),
token.get(),
commandName,
registrarId,
isDomainPremium(domainName, now),
now);
} catch (AssociationProhibitsOperationException
| StatusProhibitsOperationException
| AllocationTokenInvalidForPremiumNameException e) {
// Allocation token was not valid for this registration, continue to check the next token in
// the list
continue;
}
// Only use the first valid token in the list
return token;
}
// No valid default token found
return Optional.empty();
}
/** Resource linked to this domain does not exist. */
static class LinkedResourcesDoNotExistException extends ObjectDoesNotExistException {
public LinkedResourcesDoNotExistException(Class<?> type, ImmutableSet<String> resourceIds) {
@@ -31,6 +31,7 @@ import google.registry.flows.ExtensionManager;
import google.registry.flows.FlowModule.RegistrarId;
import google.registry.flows.FlowModule.Superuser;
import google.registry.flows.FlowModule.TargetId;
import google.registry.flows.MutatingFlow;
import google.registry.flows.TransactionalFlow;
import google.registry.flows.annotations.ReportingSpec;
import google.registry.flows.custom.DomainInfoFlowCustomLogic;
@@ -53,6 +54,8 @@ import google.registry.model.eppinput.ResourceCommand;
import google.registry.model.eppoutput.EppResponse;
import google.registry.model.eppoutput.EppResponse.ResponseExtension;
import google.registry.model.reporting.IcannReportingTypes.ActivityReportField;
import google.registry.persistence.IsolationLevel;
import google.registry.persistence.PersistenceModule;
import google.registry.util.Clock;
import jakarta.inject.Inject;
import java.util.Optional;
@@ -62,8 +65,12 @@ import org.joda.time.DateTime;
* An EPP flow that returns information about a domain.
*
* <p>The registrar that owns the domain, and any registrar presenting a valid authInfo for the
* domain, will get a rich result with all of the domain's fields. All other requests will be
* answered with a minimal result containing only basic information about the domain.
* domain, will get a rich result with all the domain's fields. All other requests will be answered
* with a minimal result containing only basic information about the domain.
*
* <p>This implements {@link MutatingFlow} instead of {@link TransactionalFlow} as a workaround so
* that the common workflow of "create domain, then immediately get domain info" does not run into
* replication lag issues where the info command claims the domain does not exist.
*
* @error {@link google.registry.flows.FlowUtils.NotLoggedInException}
* @error {@link google.registry.flows.FlowUtils.UnknownCurrencyEppException}
@@ -76,7 +83,8 @@ import org.joda.time.DateTime;
* @error {@link DomainFlowUtils.TransfersAreAlwaysForOneYearException}
*/
@ReportingSpec(ActivityReportField.DOMAIN_INFO)
public final class DomainInfoFlow implements TransactionalFlow {
@IsolationLevel(PersistenceModule.TransactionIsolationLevel.TRANSACTION_REPEATABLE_READ)
public final class DomainInfoFlow implements MutatingFlow {
@Inject ExtensionManager extensionManager;
@Inject ResourceCommand resourceCommand;
@@ -16,14 +16,13 @@ package google.registry.flows.domain;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.flows.domain.DomainFlowUtils.zeroInCurrency;
import static google.registry.flows.domain.token.AllocationTokenFlowUtils.validateTokenForPossiblePremiumName;
import static google.registry.flows.domain.token.AllocationTokenFlowUtils.discountTokenInvalidForPremiumName;
import static google.registry.pricing.PricingEngineProxy.getPricesForDomainName;
import static google.registry.util.PreconditionsUtils.checkArgumentPresent;
import com.google.common.net.InternetDomainName;
import google.registry.config.RegistryConfig;
import google.registry.flows.EppException;
import google.registry.flows.EppException.CommandUseErrorException;
import google.registry.flows.custom.DomainPricingCustomLogic;
import google.registry.flows.custom.DomainPricingCustomLogic.CreatePriceParameters;
import google.registry.flows.custom.DomainPricingCustomLogic.RenewPriceParameters;
@@ -129,9 +128,7 @@ public final class DomainPricingLogic {
DateTime dateTime,
int years,
@Nullable BillingRecurrence billingRecurrence,
Optional<AllocationToken> allocationToken)
throws AllocationTokenInvalidForCurrencyException,
AllocationTokenInvalidForPremiumNameException {
Optional<AllocationToken> allocationToken) {
checkArgument(years > 0, "Number of years must be positive");
Money renewCost;
DomainPrices domainPrices = getPricesForDomainName(domainName, dateTime);
@@ -260,8 +257,7 @@ public final class DomainPricingLogic {
/** Returns the domain create cost with allocation-token-related discounts applied. */
private Money getDomainCreateCostWithDiscount(
DomainPrices domainPrices, int years, Optional<AllocationToken> allocationToken, Tld tld)
throws EppException {
DomainPrices domainPrices, int years, Optional<AllocationToken> allocationToken, Tld tld) {
return getDomainCostWithDiscount(
domainPrices.isPremium(),
years,
@@ -277,9 +273,7 @@ public final class DomainPricingLogic {
DomainPrices domainPrices,
DateTime dateTime,
int years,
Optional<AllocationToken> allocationToken)
throws AllocationTokenInvalidForCurrencyException,
AllocationTokenInvalidForPremiumNameException {
Optional<AllocationToken> allocationToken) {
// Short-circuit if the user sent an anchor-tenant or otherwise NONPREMIUM-renewal token
if (allocationToken.isPresent()) {
AllocationToken token = allocationToken.get();
@@ -315,44 +309,41 @@ public final class DomainPricingLogic {
Optional<AllocationToken> allocationToken,
Money firstYearCost,
Optional<Money> subsequentYearCost,
Tld tld)
throws AllocationTokenInvalidForCurrencyException,
AllocationTokenInvalidForPremiumNameException {
Tld tld) {
checkArgument(years > 0, "Registration years to get cost for must be positive.");
validateTokenForPossiblePremiumName(allocationToken, isPremium);
Money totalDomainFlowCost =
firstYearCost.plus(subsequentYearCost.orElse(firstYearCost).multipliedBy(years - 1));
if (allocationToken.isEmpty()) {
return totalDomainFlowCost;
}
AllocationToken token = allocationToken.get();
if (discountTokenInvalidForPremiumName(token, isPremium)) {
return totalDomainFlowCost;
}
if (!token.getTokenBehavior().equals(TokenBehavior.DEFAULT)) {
return totalDomainFlowCost;
}
// Apply the allocation token discount, if applicable.
if (allocationToken.isPresent()
&& allocationToken.get().getTokenBehavior().equals(TokenBehavior.DEFAULT)) {
if (allocationToken.get().getDiscountPrice().isPresent()) {
if (!tld.getCurrency()
.equals(allocationToken.get().getDiscountPrice().get().getCurrencyUnit())) {
throw new AllocationTokenInvalidForCurrencyException();
}
int nonDiscountedYears = Math.max(0, years - allocationToken.get().getDiscountYears());
totalDomainFlowCost =
allocationToken
.get()
.getDiscountPrice()
.get()
.multipliedBy(allocationToken.get().getDiscountYears())
.plus(subsequentYearCost.orElse(firstYearCost).multipliedBy(nonDiscountedYears));
} else {
// Assumes token has discount fraction set.
int discountedYears = Math.min(years, allocationToken.get().getDiscountYears());
if (token.getDiscountPrice().isPresent()
&& tld.getCurrency().equals(token.getDiscountPrice().get().getCurrencyUnit())) {
int nonDiscountedYears = Math.max(0, years - token.getDiscountYears());
totalDomainFlowCost =
token
.getDiscountPrice()
.get()
.multipliedBy(token.getDiscountYears())
.plus(subsequentYearCost.orElse(firstYearCost).multipliedBy(nonDiscountedYears));
} else if (token.getDiscountFraction() > 0) {
int discountedYears = Math.min(years, token.getDiscountYears());
if (discountedYears > 0) {
var discount =
firstYearCost
.plus(subsequentYearCost.orElse(firstYearCost).multipliedBy(discountedYears - 1))
.multipliedBy(
allocationToken.get().getDiscountFraction(), RoundingMode.HALF_EVEN);
var discount =
firstYearCost
.plus(subsequentYearCost.orElse(firstYearCost).multipliedBy(discountedYears - 1))
.multipliedBy(token.getDiscountFraction(), RoundingMode.HALF_EVEN);
totalDomainFlowCost = totalDomainFlowCost.minus(discount);
}
}
}
return totalDomainFlowCost;
}
@@ -376,18 +367,4 @@ public final class DomainPricingLogic {
: domainPrices.getRenewCost();
return DomainPrices.create(isPremium, createCost, renewCost);
}
/** An allocation token was provided that is invalid for premium domains. */
public static class AllocationTokenInvalidForPremiumNameException
extends CommandUseErrorException {
public AllocationTokenInvalidForPremiumNameException() {
super("Token not valid for premium name");
}
}
public static class AllocationTokenInvalidForCurrencyException extends CommandUseErrorException {
public AllocationTokenInvalidForCurrencyException() {
super("Token and domain currencies do not match.");
}
}
}
@@ -31,7 +31,7 @@ import static google.registry.flows.domain.DomainFlowUtils.validateRegistrationP
import static google.registry.flows.domain.DomainFlowUtils.verifyRegistrarIsActive;
import static google.registry.flows.domain.DomainFlowUtils.verifyUnitIsYears;
import static google.registry.flows.domain.token.AllocationTokenFlowUtils.maybeApplyBulkPricingRemovalToken;
import static google.registry.flows.domain.token.AllocationTokenFlowUtils.verifyTokenAllowedOnDomain;
import static google.registry.flows.domain.token.AllocationTokenFlowUtils.verifyBulkTokenAllowedOnDomain;
import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_RENEW;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.DateTimeUtils.leapSafeAddYears;
@@ -124,15 +124,12 @@ import org.joda.time.Duration;
* @error {@link RemoveBulkPricingTokenOnNonBulkPricingDomainException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForDomainException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.InvalidAllocationTokenException}
* @error {@link AllocationTokenFlowUtils.NonexistentAllocationTokenException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotInPromotionException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForRegistrarException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForTldException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AlreadyRedeemedAllocationTokenException}
*/
@ReportingSpec(ActivityReportField.DOMAIN_RENEW)
@@ -154,7 +151,6 @@ public final class DomainRenewFlow implements MutatingFlow {
@Inject @Superuser boolean isSuperuser;
@Inject DomainHistory.Builder historyBuilder;
@Inject EppResponse.Builder responseBuilder;
@Inject AllocationTokenFlowUtils allocationTokenFlowUtils;
@Inject DomainRenewFlowCustomLogic flowCustomLogic;
@Inject DomainPricingLogic pricingLogic;
@Inject DomainRenewFlow() {}
@@ -174,22 +170,17 @@ public final class DomainRenewFlow implements MutatingFlow {
String tldStr = existingDomain.getTld();
Tld tld = Tld.get(tldStr);
Optional<AllocationToken> allocationToken =
allocationTokenFlowUtils.verifyAllocationTokenIfPresent(
existingDomain,
tld,
AllocationTokenFlowUtils.loadTokenFromExtensionOrGetDefault(
registrarId,
now,
CommandName.RENEW,
eppInput.getSingleExtension(AllocationTokenExtension.class));
boolean defaultTokenUsed = false;
if (allocationToken.isEmpty()) {
allocationToken =
DomainFlowUtils.checkForDefaultToken(
tld, existingDomain.getDomainName(), CommandName.RENEW, registrarId, now);
if (allocationToken.isPresent()) {
defaultTokenUsed = true;
}
}
eppInput.getSingleExtension(AllocationTokenExtension.class),
tld,
existingDomain.getDomainName(),
CommandName.RENEW);
boolean defaultTokenUsed =
allocationToken
.map(t -> t.getTokenType().equals(AllocationToken.TokenType.DEFAULT_PROMO))
.orElse(false);
verifyRenewAllowed(authInfo, existingDomain, command, allocationToken);
// If client passed an applicable static token this updates the domain
@@ -259,7 +250,7 @@ public final class DomainRenewFlow implements MutatingFlow {
newDomain, domainHistory, explicitRenewEvent, newAutorenewEvent, newAutorenewPollMessage);
if (allocationToken.isPresent() && allocationToken.get().getTokenType().isOneTimeUse()) {
entitiesToSave.add(
allocationTokenFlowUtils.redeemToken(
AllocationTokenFlowUtils.redeemToken(
allocationToken.get(), domainHistory.getHistoryEntryId()));
}
EntityChanges entityChanges =
@@ -327,7 +318,7 @@ public final class DomainRenewFlow implements MutatingFlow {
}
verifyUnitIsYears(command.getPeriod());
// We only allow __REMOVE_BULK_PRICING__ token on bulk pricing domains for now
verifyTokenAllowedOnDomain(existingDomain, allocationToken);
verifyBulkTokenAllowedOnDomain(existingDomain, allocationToken);
// If the date they specify doesn't match the expiration, fail. (This is an idempotence check).
if (!command.getCurrentExpirationDate().equals(
existingDomain.getRegistrationExpirationTime().toLocalDate())) {
@@ -54,7 +54,6 @@ import google.registry.model.billing.BillingRecurrence;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.GracePeriod;
import google.registry.model.domain.fee.FeeQueryCommandExtensionItem.CommandName;
import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.domain.rgp.GracePeriodStatus;
import google.registry.model.domain.token.AllocationTokenExtension;
@@ -94,15 +93,12 @@ import org.joda.time.DateTime;
* @error {@link DomainFlowUtils.NotAuthorizedForTldException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForDomainException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.InvalidAllocationTokenException}
* @error {@link AllocationTokenFlowUtils.NonexistentAllocationTokenException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotInPromotionException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForRegistrarException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForTldException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AlreadyRedeemedAllocationTokenException}
*/
@ReportingSpec(ActivityReportField.DOMAIN_TRANSFER_APPROVE)
@@ -116,7 +112,6 @@ public final class DomainTransferApproveFlow implements MutatingFlow {
@Inject DomainHistory.Builder historyBuilder;
@Inject EppResponse.Builder responseBuilder;
@Inject DomainPricingLogic pricingLogic;
@Inject AllocationTokenFlowUtils allocationTokenFlowUtils;
@Inject EppInput eppInput;
@Inject DomainTransferApproveFlow() {}
@@ -132,13 +127,8 @@ public final class DomainTransferApproveFlow implements MutatingFlow {
extensionManager.validate();
DateTime now = tm().getTransactionTime();
Domain existingDomain = loadAndVerifyExistence(Domain.class, targetId, now);
allocationTokenFlowUtils.verifyAllocationTokenIfPresent(
existingDomain,
Tld.get(existingDomain.getTld()),
registrarId,
now,
CommandName.TRANSFER,
eppInput.getSingleExtension(AllocationTokenExtension.class));
AllocationTokenFlowUtils.loadAllocationTokenFromExtension(
registrarId, targetId, now, eppInput.getSingleExtension(AllocationTokenExtension.class));
verifyOptionalAuthInfo(authInfo, existingDomain);
verifyHasPendingTransfer(existingDomain);
verifyResourceOwnership(registrarId, existingDomain);
@@ -58,7 +58,6 @@ import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainCommand.Transfer;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.Period;
import google.registry.model.domain.fee.FeeQueryCommandExtensionItem.CommandName;
import google.registry.model.domain.fee.FeeTransferCommandExtension;
import google.registry.model.domain.fee.FeeTransformResponseExtension;
import google.registry.model.domain.metadata.MetadataExtension;
@@ -123,15 +122,12 @@ import org.joda.time.DateTime;
* @error {@link DomainFlowUtils.UnsupportedFeeAttributeException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForDomainException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.InvalidAllocationTokenException}
* @error {@link AllocationTokenFlowUtils.NonexistentAllocationTokenException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotInPromotionException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForRegistrarException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForTldException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AlreadyRedeemedAllocationTokenException}
*/
@ReportingSpec(ActivityReportField.DOMAIN_TRANSFER_REQUEST)
@@ -154,7 +150,6 @@ public final class DomainTransferRequestFlow implements MutatingFlow {
@Inject AsyncTaskEnqueuer asyncTaskEnqueuer;
@Inject EppResponse.Builder responseBuilder;
@Inject DomainPricingLogic pricingLogic;
@Inject AllocationTokenFlowUtils allocationTokenFlowUtils;
@Inject DomainTransferRequestFlow() {}
@@ -170,12 +165,10 @@ public final class DomainTransferRequestFlow implements MutatingFlow {
extensionManager.validate();
DateTime now = tm().getTransactionTime();
Domain existingDomain = loadAndVerifyExistence(Domain.class, targetId, now);
allocationTokenFlowUtils.verifyAllocationTokenIfPresent(
existingDomain,
Tld.get(existingDomain.getTld()),
AllocationTokenFlowUtils.loadAllocationTokenFromExtension(
gainingClientId,
targetId,
now,
CommandName.TRANSFER,
eppInput.getSingleExtension(AllocationTokenExtension.class));
Optional<DomainTransferRequestSuperuserExtension> superuserExtension =
eppInput.getSingleExtension(DomainTransferRequestSuperuserExtension.class);
@@ -15,218 +15,97 @@
package google.registry.flows.domain.token;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.pricing.PricingEngineProxy.isDomainPremium;
import static google.registry.util.CollectionUtils.isNullOrEmpty;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.common.collect.ImmutableList;
import com.google.common.net.InternetDomainName;
import google.registry.flows.EppException;
import google.registry.flows.EppException.AssociationProhibitsOperationException;
import google.registry.flows.EppException.AuthorizationErrorException;
import google.registry.flows.EppException.StatusProhibitsOperationException;
import google.registry.flows.domain.DomainPricingLogic.AllocationTokenInvalidForPremiumNameException;
import google.registry.model.billing.BillingBase.RenewalPriceBehavior;
import google.registry.model.billing.BillingBase;
import google.registry.model.billing.BillingRecurrence;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainCommand;
import google.registry.model.domain.fee.FeeQueryCommandExtensionItem.CommandName;
import google.registry.model.domain.token.AllocationToken;
import google.registry.model.domain.token.AllocationToken.TokenBehavior;
import google.registry.model.domain.token.AllocationToken.TokenStatus;
import google.registry.model.domain.token.AllocationTokenExtension;
import google.registry.model.reporting.HistoryEntry.HistoryEntryId;
import google.registry.model.tld.Tld;
import google.registry.persistence.VKey;
import jakarta.inject.Inject;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.joda.time.DateTime;
/** Utility functions for dealing with {@link AllocationToken}s in domain flows. */
public class AllocationTokenFlowUtils {
@Inject
public AllocationTokenFlowUtils() {}
/**
* Checks if the allocation token applies to the given domain names, used for domain checks.
*
* @return A map of domain names to domain check error response messages. If a message is present
* for a a given domain then it does not validate with this allocation token; domains that do
* validate have blank messages (i.e. no error).
*/
public AllocationTokenDomainCheckResults checkDomainsWithToken(
List<InternetDomainName> domainNames, String token, String registrarId, DateTime now) {
// If the token is completely invalid, return the error message for all domain names
AllocationToken tokenEntity;
try {
tokenEntity = loadToken(token);
} catch (EppException e) {
return new AllocationTokenDomainCheckResults(
Optional.empty(), Maps.toMap(domainNames, ignored -> e.getMessage()));
}
// If the token is only invalid for some domain names (e.g. an invalid TLD), include those error
// results for only those domain names
ImmutableMap.Builder<InternetDomainName, String> resultsBuilder = new ImmutableMap.Builder<>();
for (InternetDomainName domainName : domainNames) {
try {
validateToken(
domainName,
tokenEntity,
CommandName.CREATE,
registrarId,
isDomainPremium(domainName.toString(), now),
now);
resultsBuilder.put(domainName, "");
} catch (EppException e) {
resultsBuilder.put(domainName, e.getMessage());
}
}
return new AllocationTokenDomainCheckResults(Optional.of(tokenEntity), resultsBuilder.build());
}
private AllocationTokenFlowUtils() {}
/** Redeems a SINGLE_USE {@link AllocationToken}, returning the redeemed copy. */
public AllocationToken redeemToken(AllocationToken token, HistoryEntryId redemptionHistoryId) {
public static AllocationToken redeemToken(
AllocationToken token, HistoryEntryId redemptionHistoryId) {
checkArgument(
token.getTokenType().isOneTimeUse(), "Only SINGLE_USE tokens can be marked as redeemed");
return token.asBuilder().setRedemptionHistoryId(redemptionHistoryId).build();
}
/**
* Validates a given token. The token could be invalid if it has allowed client IDs or TLDs that
* do not include this client ID / TLD, or if the token has a promotion that is not currently
* running, or the token is not valid for a premium name when necessary.
*
* @throws EppException if the token is invalid in any way
*/
public static void validateToken(
InternetDomainName domainName,
AllocationToken token,
CommandName commandName,
String registrarId,
boolean isPremium,
DateTime now)
throws EppException {
// Only tokens with default behavior require validation
if (!TokenBehavior.DEFAULT.equals(token.getTokenBehavior())) {
return;
}
validateTokenForPossiblePremiumName(Optional.of(token), isPremium);
if (!token.getAllowedEppActions().isEmpty()
&& !token.getAllowedEppActions().contains(commandName)) {
throw new AllocationTokenNotValidForCommandException();
}
if (!token.getAllowedRegistrarIds().isEmpty()
&& !token.getAllowedRegistrarIds().contains(registrarId)) {
throw new AllocationTokenNotValidForRegistrarException();
}
if (!token.getAllowedTlds().isEmpty()
&& !token.getAllowedTlds().contains(domainName.parent().toString())) {
throw new AllocationTokenNotValidForTldException();
}
if (token.getDomainName().isPresent()
&& !token.getDomainName().get().equals(domainName.toString())) {
throw new AllocationTokenNotValidForDomainException();
}
// Tokens without status transitions will just have a single-entry NOT_STARTED map, so only
// check the status transitions map if it's non-trivial.
if (token.getTokenStatusTransitions().size() > 1
&& !TokenStatus.VALID.equals(token.getTokenStatusTransitions().getValueAtTime(now))) {
throw new AllocationTokenNotInPromotionException();
}
}
/** Validates that the given token is valid for a premium name if the name is premium. */
public static void validateTokenForPossiblePremiumName(
Optional<AllocationToken> token, boolean isPremium)
throws AllocationTokenInvalidForPremiumNameException {
if (token.isPresent()
&& (token.get().getDiscountFraction() != 0.0 || token.get().getDiscountPrice().isPresent())
/** Don't apply discounts on premium domains if the token isn't configured that way. */
public static boolean discountTokenInvalidForPremiumName(
AllocationToken token, boolean isPremium) {
return (token.getDiscountFraction() != 0.0 || token.getDiscountPrice().isPresent())
&& isPremium
&& !token.get().shouldDiscountPremiums()) {
throw new AllocationTokenInvalidForPremiumNameException();
}
&& !token.shouldDiscountPremiums();
}
/** Loads a given token and validates that it is not redeemed */
private static AllocationToken loadToken(String token) throws EppException {
if (Strings.isNullOrEmpty(token)) {
// We load the token directly from the input XML. If it's null or empty we should throw
// an InvalidAllocationTokenException before the database load attempt fails.
// See https://tools.ietf.org/html/draft-ietf-regext-allocation-token-04#section-2.1
throw new InvalidAllocationTokenException();
}
Optional<AllocationToken> maybeTokenEntity = AllocationToken.maybeGetStaticTokenInstance(token);
if (maybeTokenEntity.isPresent()) {
return maybeTokenEntity.get();
}
// TODO(b/368069206): `reTransact` needed by tests only.
maybeTokenEntity =
tm().reTransact(() -> tm().loadByKeyIfPresent(VKey.create(AllocationToken.class, token)));
if (maybeTokenEntity.isEmpty()) {
throw new InvalidAllocationTokenException();
}
if (maybeTokenEntity.get().isRedeemed()) {
throw new AlreadyRedeemedAllocationTokenException();
}
return maybeTokenEntity.get();
}
/** Verifies and returns the allocation token if one is specified, otherwise does nothing. */
public Optional<AllocationToken> verifyAllocationTokenCreateIfPresent(
DomainCommand.Create command,
Tld tld,
/** Loads and verifies the allocation token if one is specified, otherwise does nothing. */
public static Optional<AllocationToken> loadAllocationTokenFromExtension(
String registrarId,
String domainName,
DateTime now,
Optional<AllocationTokenExtension> extension)
throws EppException {
throws NonexistentAllocationTokenException, AllocationTokenInvalidException {
if (extension.isEmpty()) {
return Optional.empty();
}
AllocationToken tokenEntity = loadToken(extension.get().getAllocationToken());
validateToken(
InternetDomainName.from(command.getDomainName()),
tokenEntity,
CommandName.CREATE,
registrarId,
isDomainPremium(command.getDomainName(), now),
now);
return Optional.of(tokenEntity);
return Optional.of(
loadAndValidateToken(extension.get().getAllocationToken(), registrarId, domainName, now));
}
/** Verifies and returns the allocation token if one is specified, otherwise does nothing. */
public Optional<AllocationToken> verifyAllocationTokenIfPresent(
Domain existingDomain,
Tld tld,
/**
* Loads the relevant token, if present, for the given extension + request.
*
* <p>This may be the allocation token provided in the request, if it is present and valid for the
* request. Otherwise, it may be a default allocation token if one is present and valid for the
* request.
*/
public static Optional<AllocationToken> loadTokenFromExtensionOrGetDefault(
String registrarId,
DateTime now,
CommandName commandName,
Optional<AllocationTokenExtension> extension)
throws EppException {
if (extension.isEmpty()) {
return Optional.empty();
Optional<AllocationTokenExtension> extension,
Tld tld,
String domainName,
CommandName commandName)
throws NonexistentAllocationTokenException, AllocationTokenInvalidException {
Optional<AllocationToken> fromExtension =
loadAllocationTokenFromExtension(registrarId, domainName, now, extension);
if (fromExtension.isPresent()
&& tokenIsValidAgainstDomain(
InternetDomainName.from(domainName), fromExtension.get(), commandName, now)) {
return fromExtension;
}
AllocationToken tokenEntity = loadToken(extension.get().getAllocationToken());
validateToken(
InternetDomainName.from(existingDomain.getDomainName()),
tokenEntity,
commandName,
registrarId,
isDomainPremium(existingDomain.getDomainName(), now),
now);
return Optional.of(tokenEntity);
return checkForDefaultToken(tld, domainName, commandName, registrarId, now);
}
public static void verifyTokenAllowedOnDomain(
/** Verifies that the given domain can have a bulk pricing token removed from it. */
public static void verifyBulkTokenAllowedOnDomain(
Domain domain, Optional<AllocationToken> allocationToken) throws EppException {
boolean domainHasBulkToken = domain.getCurrentBulkToken().isPresent();
boolean hasRemoveBulkPricingToken =
allocationToken.isPresent()
@@ -239,6 +118,11 @@ public class AllocationTokenFlowUtils {
}
}
/**
* Removes the bulk pricing token from the provided domain, if applicable.
*
* @param allocationToken the (possibly) REMOVE_BULK_PRICING token provided by the client.
*/
public static Domain maybeApplyBulkPricingRemovalToken(
Domain domain, Optional<AllocationToken> allocationToken) {
if (allocationToken.isEmpty()
@@ -249,7 +133,7 @@ public class AllocationTokenFlowUtils {
BillingRecurrence newBillingRecurrence =
tm().loadByKey(domain.getAutorenewBillingEvent())
.asBuilder()
.setRenewalPriceBehavior(RenewalPriceBehavior.DEFAULT)
.setRenewalPriceBehavior(BillingBase.RenewalPriceBehavior.DEFAULT)
.setRenewalPrice(null)
.build();
@@ -267,35 +151,139 @@ public class AllocationTokenFlowUtils {
.build();
}
/**
* Checks if the given token is valid for the given request.
*
* <p>Note that if the token is not valid, that is not a catastrophic error -- we may move on to
* trying a different token or skip token usage entirely.
*/
@VisibleForTesting
static boolean tokenIsValidAgainstDomain(
InternetDomainName domainName, AllocationToken token, CommandName commandName, DateTime now) {
if (discountTokenInvalidForPremiumName(token, isDomainPremium(domainName.toString(), now))) {
return false;
}
if (!token.getAllowedEppActions().isEmpty()
&& !token.getAllowedEppActions().contains(commandName)) {
return false;
}
if (!token.getAllowedTlds().isEmpty()
&& !token.getAllowedTlds().contains(domainName.parent().toString())) {
return false;
}
return token.getDomainName().isEmpty()
|| token.getDomainName().get().equals(domainName.toString());
}
/**
* Checks if there is a valid default token to be used for a domain create command.
*
* <p>If there is more than one valid default token for the registration, only the first valid
* token found on the TLD's default token list will be returned.
*/
private static Optional<AllocationToken> checkForDefaultToken(
Tld tld, String domainName, CommandName commandName, String registrarId, DateTime now) {
ImmutableList<VKey<AllocationToken>> tokensFromTld = tld.getDefaultPromoTokens();
if (isNullOrEmpty(tokensFromTld)) {
return Optional.empty();
}
Map<VKey<AllocationToken>, Optional<AllocationToken>> tokens =
AllocationToken.getAll(tokensFromTld);
checkState(
!isNullOrEmpty(tokens), "Failure while loading default TLD tokens from the database");
// Iterate over the list to maintain token ordering (since we return the first valid token)
ImmutableList<AllocationToken> tokenList =
tokensFromTld.stream()
.map(tokens::get)
.filter(Optional::isPresent)
.map(Optional::get)
.collect(toImmutableList());
// Check if any of the tokens are valid for this domain registration
for (AllocationToken token : tokenList) {
try {
validateTokenEntity(token, registrarId, domainName, now);
} catch (AllocationTokenInvalidException e) {
// Token is not valid for this registrar, etc. -- continue trying tokens
continue;
}
if (tokenIsValidAgainstDomain(InternetDomainName.from(domainName), token, commandName, now)) {
return Optional.of(token);
}
}
// No valid default token found
return Optional.empty();
}
/** Loads a given token and validates it against the registrar, time, etc */
private static AllocationToken loadAndValidateToken(
String token, String registrarId, String domainName, DateTime now)
throws NonexistentAllocationTokenException, AllocationTokenInvalidException {
if (Strings.isNullOrEmpty(token)) {
// We load the token directly from the input XML. If it's null or empty we should throw
// an NonexistentAllocationTokenException before the database load attempt fails.
// See https://tools.ietf.org/html/draft-ietf-regext-allocation-token-04#section-2.1
throw new NonexistentAllocationTokenException();
}
Optional<AllocationToken> maybeTokenEntity = AllocationToken.maybeGetStaticTokenInstance(token);
if (maybeTokenEntity.isPresent()) {
return maybeTokenEntity.get();
}
maybeTokenEntity = AllocationToken.get(VKey.create(AllocationToken.class, token));
if (maybeTokenEntity.isEmpty()) {
throw new NonexistentAllocationTokenException();
}
AllocationToken tokenEntity = maybeTokenEntity.get();
validateTokenEntity(tokenEntity, registrarId, domainName, now);
return tokenEntity;
}
private static void validateTokenEntity(
AllocationToken token, String registrarId, String domainName, DateTime now)
throws AllocationTokenInvalidException {
if (token.isRedeemed()) {
throw new AlreadyRedeemedAllocationTokenException();
}
if (!token.getAllowedRegistrarIds().isEmpty()
&& !token.getAllowedRegistrarIds().contains(registrarId)) {
throw new AllocationTokenNotValidForRegistrarException();
}
// Tokens without status transitions will just have a single-entry NOT_STARTED map, so only
// check the status transitions map if it's non-trivial.
if (token.getTokenStatusTransitions().size() > 1
&& !AllocationToken.TokenStatus.VALID.equals(
token.getTokenStatusTransitions().getValueAtTime(now))) {
throw new AllocationTokenNotInPromotionException();
}
if (token.getDomainName().isPresent() && !token.getDomainName().get().equals(domainName)) {
throw new AllocationTokenNotValidForDomainException();
}
}
// Note: exception messages should be <= 32 characters long for domain check results
/** The allocation token exists but is not valid, e.g. the wrong registrar. */
public abstract static class AllocationTokenInvalidException
extends StatusProhibitsOperationException {
AllocationTokenInvalidException(String message) {
super(message);
}
}
/** The allocation token is not currently valid. */
public static class AllocationTokenNotInPromotionException
extends StatusProhibitsOperationException {
extends AllocationTokenInvalidException {
AllocationTokenNotInPromotionException() {
super("Alloc token not in promo period");
}
}
/** The allocation token is not valid for this TLD. */
public static class AllocationTokenNotValidForTldException
extends AssociationProhibitsOperationException {
AllocationTokenNotValidForTldException() {
super("Alloc token invalid for TLD");
}
}
/** The allocation token is not valid for this domain. */
public static class AllocationTokenNotValidForDomainException
extends AssociationProhibitsOperationException {
AllocationTokenNotValidForDomainException() {
super("Alloc token invalid for domain");
}
}
/** The allocation token is not valid for this registrar. */
public static class AllocationTokenNotValidForRegistrarException
extends AssociationProhibitsOperationException {
extends AllocationTokenInvalidException {
AllocationTokenNotValidForRegistrarException() {
super("Alloc token invalid for client");
}
@@ -303,23 +291,23 @@ public class AllocationTokenFlowUtils {
/** The allocation token was already redeemed. */
public static class AlreadyRedeemedAllocationTokenException
extends AssociationProhibitsOperationException {
extends AllocationTokenInvalidException {
AlreadyRedeemedAllocationTokenException() {
super("Alloc token was already redeemed");
}
}
/** The allocation token is not valid for this EPP command. */
public static class AllocationTokenNotValidForCommandException
extends AssociationProhibitsOperationException {
AllocationTokenNotValidForCommandException() {
super("Allocation token not valid for the EPP command");
/** The allocation token is not valid for this domain. */
public static class AllocationTokenNotValidForDomainException
extends AllocationTokenInvalidException {
AllocationTokenNotValidForDomainException() {
super("Alloc token invalid for domain");
}
}
}
/** The allocation token is invalid. */
public static class InvalidAllocationTokenException extends AuthorizationErrorException {
InvalidAllocationTokenException() {
public static class NonexistentAllocationTokenException extends AuthorizationErrorException {
NonexistentAllocationTokenException() {
super("The allocation token is invalid");
}
}
@@ -403,6 +403,9 @@ public class Registrar extends UpdateAutoTimestampEntity implements Buildable, J
*/
DateTime lastExpiringFailoverCertNotificationSentDate = START_OF_TIME;
/** The time that the POCs have been reviewed last. */
@Expose DateTime lastPocVerificationDate = START_OF_TIME;
/** Telephone support passcode (5-digit numeric) */
String phonePasscode;
@@ -461,6 +464,10 @@ public class Registrar extends UpdateAutoTimestampEntity implements Buildable, J
return registrarName;
}
public DateTime getLastPocVerificationDate() {
return lastPocVerificationDate;
}
public Type getType() {
return type;
}
@@ -614,6 +621,7 @@ public class Registrar extends UpdateAutoTimestampEntity implements Buildable, J
.putString(
"lastExpiringFailoverCertNotificationSentDate",
lastExpiringFailoverCertNotificationSentDate)
.putString("lastPocVerificationDate", lastPocVerificationDate)
.put("registrarName", registrarName)
.put("type", type)
.put("state", state)
@@ -802,6 +810,12 @@ public class Registrar extends UpdateAutoTimestampEntity implements Buildable, J
return thisCastToDerived();
}
public B setLastPocVerificationDate(DateTime now) {
checkArgumentNotNull(now, "Registrar lastPocVerificationDate cannot be null");
getInstance().lastPocVerificationDate = now;
return thisCastToDerived();
}
private static String calculateHash(String clientCertificate) {
if (clientCertificate == null) {
return null;
@@ -276,10 +276,6 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
}
T result = work.call();
txn.commit();
long duration = clock.nowUtc().getMillis() - txnInfo.transactionTime.getMillis();
if (duration >= 100) {
logger.atInfo().log("Transaction duration: %d milliseconds", duration);
}
return result;
} catch (Throwable e) {
// Catch a Throwable here so even Errors would lead to a rollback.
@@ -16,6 +16,7 @@ package google.registry.tools;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Strings.isNullOrEmpty;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.pricing.PricingEngineProxy.getPricesForDomainName;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import static org.joda.time.DateTimeZone.UTC;
@@ -23,6 +24,7 @@ import static org.joda.time.DateTimeZone.UTC;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.google.template.soy.data.SoyMapData;
import google.registry.model.common.FeatureFlag;
import google.registry.model.pricing.PremiumPricingEngine.DomainPrices;
import google.registry.tools.soy.DomainCreateSoyInfo;
import google.registry.util.StringGenerator;
@@ -58,9 +60,15 @@ final class CreateDomainCommand extends CreateOrUpdateDomainCommand {
@Override
protected void initMutatingEppToolCommand() {
checkArgumentNotNull(registrant, "Registrant must be specified");
checkArgument(!admins.isEmpty(), "At least one admin must be specified");
checkArgument(!techs.isEmpty(), "At least one tech must be specified");
tm().transact(
() -> {
if (!FeatureFlag.isActiveNowOrElse(
FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_OPTIONAL, false)) {
checkArgumentNotNull(registrant, "Registrant must be specified");
checkArgument(!admins.isEmpty(), "At least one admin must be specified");
checkArgument(!techs.isEmpty(), "At least one tech must be specified");
}
});
if (isNullOrEmpty(password)) {
password = passwordGenerator.createString(PASSWORD_LENGTH);
}
@@ -17,6 +17,7 @@ package google.registry.ui.server.console;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.request.Action.Method.POST;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static google.registry.util.PreconditionsUtils.checkArgumentPresent;
import static org.apache.http.HttpStatus.SC_OK;
@@ -37,6 +38,7 @@ import google.registry.util.RegistryEnvironment;
import jakarta.inject.Inject;
import java.util.Optional;
import java.util.stream.Collectors;
import org.joda.time.DateTime;
@Action(
service = GaeService.DEFAULT,
@@ -88,17 +90,35 @@ public class ConsoleUpdateRegistrarAction extends ConsoleApiAction {
}
}
Registrar updatedRegistrar =
DateTime now = tm().getTransactionTime();
DateTime newLastPocVerificationDate =
registrarParam.getLastPocVerificationDate() == null
? START_OF_TIME
: registrarParam.getLastPocVerificationDate();
checkArgument(
newLastPocVerificationDate.isBefore(now),
"Invalid value of LastPocVerificationDate - value is in the future");
var updatedRegistrarBuilder =
existingRegistrar
.get()
.asBuilder()
.setAllowedTlds(
registrarParam.getAllowedTlds().stream()
.map(DomainNameUtils::canonicalizeHostname)
.collect(Collectors.toSet()))
.setRegistryLockAllowed(registrarParam.isRegistryLockAllowed())
.build();
.setLastPocVerificationDate(newLastPocVerificationDate);
if (user.getUserRoles()
.hasGlobalPermission(ConsolePermission.EDIT_REGISTRAR_DETAILS)) {
updatedRegistrarBuilder =
updatedRegistrarBuilder
.setAllowedTlds(
registrarParam.getAllowedTlds().stream()
.map(DomainNameUtils::canonicalizeHostname)
.collect(Collectors.toSet()))
.setRegistryLockAllowed(registrarParam.isRegistryLockAllowed())
.setLastPocVerificationDate(newLastPocVerificationDate);
}
var updatedRegistrar = updatedRegistrarBuilder.build();
tm().put(updatedRegistrar);
finishAndPersistConsoleUpdateHistory(
new ConsoleUpdateHistory.Builder()
@@ -169,6 +169,7 @@ public class ContactAction extends ConsoleApiAction {
throw new ContactRequirementException(t);
}
}
enforcePrimaryContactRestrictions(oldContactsByType, newContactsByType);
ensurePhoneNumberNotRemovedForContactTypes(oldContactsByType, newContactsByType, Type.TECH);
Optional<RegistrarPoc> domainWhoisAbuseContact =
getDomainWhoisVisibleAbuseContact(updatedContacts);
@@ -187,6 +188,23 @@ public class ContactAction extends ConsoleApiAction {
checkContactRegistryLockRequirements(existingContacts, updatedContacts);
}
private static void enforcePrimaryContactRestrictions(
Multimap<Type, RegistrarPoc> oldContactsByType,
Multimap<Type, RegistrarPoc> newContactsByType) {
ImmutableSet<String> oldAdminEmails =
oldContactsByType.get(Type.ADMIN).stream()
.map(RegistrarPoc::getEmailAddress)
.collect(toImmutableSet());
ImmutableSet<String> newAdminEmails =
newContactsByType.get(Type.ADMIN).stream()
.map(RegistrarPoc::getEmailAddress)
.collect(toImmutableSet());
if (!newAdminEmails.containsAll(oldAdminEmails)) {
throw new ContactRequirementException(
"Cannot remove or change the email address of primary contacts");
}
}
private static void checkContactRegistryLockRequirements(
ImmutableSet<RegistrarPoc> existingContacts, ImmutableSet<RegistrarPoc> updatedContacts) {
// Any contact(s) with new passwords must be allowed to set them
@@ -20,9 +20,9 @@
{@param domain: string}
{@param period: int}
{@param nameservers: list<string>}
{@param registrant: string}
{@param admins: list<string>}
{@param techs: list<string>}
{@param? registrant: string|null}
{@param? admins: list<string>|null}
{@param? techs: list<string>|null}
{@param password: string}
{@param? currency: string|null}
{@param? price: string|null}
@@ -45,13 +45,19 @@
{/for}
</domain:ns>
{/if}
<domain:registrant>{$registrant}</domain:registrant>
{for $admin in $admins}
<domain:contact type="admin">{$admin}</domain:contact>
{/for}
{for $tech in $techs}
<domain:contact type="tech">{$tech}</domain:contact>
{/for}
{if $registrant != null}
<domain:registrant>{$registrant}</domain:registrant>
{/if}
{if $admins != null}
{for $admin in $admins}
<domain:contact type="admin">{$admin}</domain:contact>
{/for}
{/if}
{if $techs != null}
{for $tech in $techs}
<domain:contact type="tech">{$tech}</domain:contact>
{/for}
{/if}
<domain:authInfo>
<domain:pw>{$password}</domain:pw>
</domain:authInfo>
@@ -85,7 +85,7 @@ class BillingEventTest {
assertThat(invoiceKey.startDate()).isEqualTo("2017-10-01");
assertThat(invoiceKey.endDate()).isEqualTo("2022-09-30");
assertThat(invoiceKey.productAccountKey()).isEqualTo("12345-CRRHELLO");
assertThat(invoiceKey.usageGroupingKey()).isEqualTo("myRegistrar");
assertThat(invoiceKey.usageGroupingKey()).isEqualTo("");
assertThat(invoiceKey.description()).isEqualTo("RENEW | TLD: test | TERM: 5-year");
assertThat(invoiceKey.unitPrice()).isEqualTo(20.5);
assertThat(invoiceKey.unitPriceCurrency()).isEqualTo("USD");
@@ -106,7 +106,7 @@ class BillingEventTest {
assertThat(invoiceKey.toCsv(3L))
.isEqualTo(
"2017-10-01,2022-09-30,12345-CRRHELLO,61.50,USD,10125,1,PURCHASE,"
+ "myRegistrar,3,RENEW | TLD: test | TERM: 5-year,20.50,USD,");
+ ",3,RENEW | TLD: test | TERM: 5-year,20.50,USD,");
}
@Test
@@ -116,7 +116,7 @@ class BillingEventTest {
assertThat(invoiceKey.toCsv(3L))
.isEqualTo(
"2017-10-01,,12345-CRRHELLO,61.50,USD,10125,1,PURCHASE,"
+ "myRegistrar,3,RENEW | TLD: test | TERM: 0-year,20.50,USD,");
+ ",3,RENEW | TLD: test | TERM: 0-year,20.50,USD,");
}
@Test
@@ -199,6 +199,36 @@ class InvoicingPipelineTest {
0,
"USD",
20.0,
""),
google.registry.beam.billing.BillingEvent.create(
15,
DateTime.parse("2017-10-02T00:00:00.0Z"),
DateTime.parse("2017-10-04T00:00:00.0Z"),
"theRegistrarCopy",
"234",
"",
"test",
"CREATE",
"mydomainfromanotherclient.test",
"REPO-ID",
5,
"JPY",
70.0,
""),
google.registry.beam.billing.BillingEvent.create(
16,
DateTime.parse("2017-10-04T00:00:00Z"),
DateTime.parse("2017-10-04T00:00:00Z"),
"theRegistrarCopy",
"234",
"",
"test",
"RENEW",
"mydomain2fromanotherclient.test",
"REPO-ID",
3,
"USD",
20.5,
""));
private static final ImmutableMap<String, ImmutableList<String>> EXPECTED_DETAILED_REPORT_MAP =
@@ -224,18 +254,26 @@ class InvoicingPipelineTest {
"invoice_details_2017-10_anotherRegistrar_test.csv",
ImmutableList.of(
"5,2017-10-04 00:00:00 UTC,2017-10-04 00:00:00 UTC,anotherRegistrar,789,,"
+ "test,CREATE,mydomain5.test,REPO-ID,1,USD,0.00,SUNRISE ANCHOR_TENANT"));
+ "test,CREATE,mydomain5.test,REPO-ID,1,USD,0.00,SUNRISE ANCHOR_TENANT"),
"invoice_details_2017-10_theRegistrarCopy_test.csv",
ImmutableList.of(
"15,2017-10-02 00:00:00 UTC,2017-10-04 00:00:00"
+ " UTC,theRegistrarCopy,234,,test,CREATE,mydomainfromanotherclient.test,REPO-ID,5,JPY,70.00,",
"16,2017-10-04 00:00:00 UTC,2017-10-04 00:00:00"
+ " UTC,theRegistrarCopy,234,,test,RENEW,mydomain2fromanotherclient.test,REPO-ID,3,USD,20.50,"));
private static final ImmutableList<String> EXPECTED_INVOICE_OUTPUT =
ImmutableList.of(
"2017-10-01,2020-09-30,234,41.00,USD,10125,1,PURCHASE,theRegistrar,2,"
"2017-10-01,2020-09-30,234,61.50,USD,10125,1,PURCHASE,,3,"
+ "RENEW | TLD: test | TERM: 3-year,20.50,USD,",
"2017-10-01,2022-09-30,234,70.00,JPY,10125,1,PURCHASE,theRegistrar,1,"
"2017-10-01,2022-09-30,234,70.00,JPY,10125,1,PURCHASE,,1,"
+ "CREATE | TLD: hello | TERM: 5-year,70.00,JPY,",
"2017-10-01,,234,20.00,USD,10125,1,PURCHASE,theRegistrar,1,"
"2017-10-01,,234,20.00,USD,10125,1,PURCHASE,,1,"
+ "SERVER_STATUS | TLD: test | TERM: 0-year,20.00,USD,",
"2017-10-01,2018-09-30,456,20.50,USD,10125,1,PURCHASE,bestdomains,1,"
+ "RENEW | TLD: test | TERM: 1-year,20.50,USD,116688");
"2017-10-01,2018-09-30,456,20.50,USD,10125,1,PURCHASE,,1,"
+ "RENEW | TLD: test | TERM: 1-year,20.50,USD,116688",
"2017-10-01,2022-09-30,234,70.00,JPY,10125,1,PURCHASE,,1,CREATE | TLD: test | TERM:"
+ " 5-year,70.00,JPY,");
private final InvoicingPipelineOptions options =
PipelineOptionsFactory.create().as(InvoicingPipelineOptions.class);
@@ -355,21 +393,21 @@ class InvoicingPipelineTest {
.isEqualTo(
"""
SELECT b, r FROM BillingEvent b
JOIN Registrar r ON b.clientId = r.registrarId
JOIN Domain d ON b.domainRepoId = d.repoId
JOIN Tld t ON t.tldStr = d.tld
LEFT JOIN BillingCancellation c ON b.id = c.billingEvent
LEFT JOIN BillingCancellation cr ON b.cancellationMatchingBillingEvent = cr.billingRecurrence
WHERE r.billingAccountMap IS NOT NULL
AND r.type = 'REAL'
AND t.invoicingEnabled IS TRUE
AND CAST(b.billingTime AS timestamp)
BETWEEN CAST('2017-10-01T00:00:00Z' AS timestamp)
AND CAST('2017-11-01T00:00:00Z' AS timestamp)
AND c.id IS NULL
AND cr.id IS NULL
""");
SELECT b, r FROM BillingEvent b
JOIN Registrar r ON b.clientId = r.registrarId
JOIN Domain d ON b.domainRepoId = d.repoId
JOIN Tld t ON t.tldStr = d.tld
LEFT JOIN BillingCancellation c ON b.id = c.billingEvent
LEFT JOIN BillingCancellation cr ON b.cancellationMatchingBillingEvent = cr.billingRecurrence
WHERE r.billingAccountMap IS NOT NULL
AND r.type = 'REAL'
AND t.invoicingEnabled IS TRUE
AND CAST(b.billingTime AS timestamp)
BETWEEN CAST('2017-10-01T00:00:00Z' AS timestamp)
AND CAST('2017-11-01T00:00:00Z' AS timestamp)
AND c.id IS NULL
AND cr.id IS NULL
""");
}
/** Returns the text contents of a file under the beamBucket/results directory. */
@@ -391,6 +429,13 @@ class InvoicingPipelineTest {
.setBillingAccountMap(ImmutableMap.of(JPY, "234", USD, "234"))
.build();
persistResource(registrar1);
Registrar registrar11 = persistNewRegistrar("theRegistrarCopy");
registrar11 =
registrar11
.asBuilder()
.setBillingAccountMap(ImmutableMap.of(JPY, "234", USD, "234"))
.build();
persistResource(registrar11);
Registrar registrar2 = persistNewRegistrar("bestdomains");
registrar2 =
registrar2
@@ -547,6 +592,21 @@ class InvoicingPipelineTest {
.setDomainHistory(domainHistoryRecurrence)
.build();
persistResource(cancellationRecurrence);
// Domains created for registrar with = key but != client id.
Domain domain14 = persistActiveDomain("mydomainfromanotherclient.test");
Domain domain15 = persistActiveDomain("mydomain2fromanotherclient.test");
persistBillingEvent(
15,
domain14,
registrar11,
Reason.CREATE,
5,
Money.ofMajor(JPY, 70),
DateTime.parse("2017-10-04T00:00:00.0Z"),
DateTime.parse("2017-10-02T00:00:00.0Z"));
persistBillingEvent(16, domain15, registrar11, Reason.RENEW, 3, Money.of(USD, 20.5));
}
private static DomainHistory persistDomainHistory(Domain domain, Registrar registrar) {
@@ -0,0 +1,36 @@
// Copyright 2025 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.beam.common;
import static com.google.common.truth.Truth.assertThat;
import google.registry.util.RegistryEnvironment;
import org.apache.beam.sdk.options.PipelineOptionsFactory;
import org.junit.jupiter.api.Test;
public class RegistryPipelineWorkerInitializerTest {
@Test
void test() {
RegistryPipelineOptions options =
PipelineOptionsFactory.fromArgs(
"--registryEnvironment=ALPHA", "--isolationOverride=TRANSACTION_SERIALIZABLE")
.withValidation()
.as(RegistryPipelineOptions.class);
new RegistryPipelineWorkerInitializer().beforeProcessing(options);
assertThat(RegistryEnvironment.isOnJetty()).isTrue();
System.clearProperty("google.registry.jetty");
}
}
@@ -58,7 +58,7 @@ public class FlowModuleTest {
@Test
void givenNonMutatingFlow_thenReplicaTmIsUsed() throws EppException {
String eppInputXmlFilename = "domain_info.xml";
String eppInputXmlFilename = "domain_check.xml";
FlowModule flowModule =
new FlowModule.Builder().setEppInput(getEppInput(eppInputXmlFilename)).build();
JpaTransactionManager tm =
@@ -211,8 +211,8 @@ class DomainCheckFlowTest extends ResourceCheckFlowTestCase<DomainCheckFlow, Dom
doCheckTest(
create(true, "example1.tld", null),
create(false, "example2.tld", "Alloc token invalid for domain"),
create(false, "reserved.tld", "Reserved"),
create(false, "specificuse.tld", "Reserved; alloc. token required"));
create(false, "reserved.tld", "Alloc token invalid for domain"),
create(false, "specificuse.tld", "Alloc token invalid for domain"));
}
@Test
@@ -230,8 +230,8 @@ class DomainCheckFlowTest extends ResourceCheckFlowTestCase<DomainCheckFlow, Dom
doCheckTest(
create(false, "example1.tld", "Blocked by a GlobalBlock service"),
create(false, "example2.tld", "Alloc token invalid for domain"),
create(false, "reserved.tld", "Reserved"),
create(false, "specificuse.tld", "Reserved; alloc. token required"));
create(false, "reserved.tld", "Alloc token invalid for domain"),
create(false, "specificuse.tld", "Alloc token invalid for domain"));
}
@Test
@@ -257,17 +257,6 @@ class DomainCheckFlowTest extends ResourceCheckFlowTestCase<DomainCheckFlow, Dom
create(true, "example3.tld", null));
}
@Test
void testSuccess_oneExists_allocationTokenIsInvalid() throws Exception {
setEppInput("domain_check_allocationtoken.xml");
persistActiveDomain("example1.tld");
doCheckTest(
create(false, "example1.tld", "In use"),
create(false, "example2.tld", "The allocation token is invalid"),
create(false, "reserved.tld", "Reserved"),
create(false, "specificuse.tld", "Reserved; alloc. token required"));
}
@Test
void testSuccess_oneExists_allocationTokenIsValid() throws Exception {
setEppInput("domain_check_allocationtoken.xml");
@@ -300,24 +289,6 @@ class DomainCheckFlowTest extends ResourceCheckFlowTestCase<DomainCheckFlow, Dom
runFlowAssertResponse(loadFile("domain_check_allocationtoken_fee_anchor_response.xml"));
}
@Test
void testSuccess_oneExists_allocationTokenIsRedeemed() throws Exception {
setEppInput("domain_check_allocationtoken.xml");
Domain domain = persistActiveDomain("example1.tld");
HistoryEntryId historyEntryId = new HistoryEntryId(domain.getRepoId(), 1L);
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setRedemptionHistoryId(historyEntryId)
.build());
doCheckTest(
create(false, "example1.tld", "In use"),
create(false, "example2.tld", "Alloc token was already redeemed"),
create(false, "reserved.tld", "Reserved"),
create(false, "specificuse.tld", "Reserved; alloc. token required"));
}
@Test
void testSuccess_oneExists_allocationTokenForReservedDomain() throws Exception {
setEppInput("domain_check_allocationtoken.xml");
@@ -329,9 +300,9 @@ class DomainCheckFlowTest extends ResourceCheckFlowTestCase<DomainCheckFlow, Dom
.setTokenType(SINGLE_USE)
.build());
doCheckTest(
create(false, "example1.tld", "In use"),
create(false, "example1.tld", "Alloc token invalid for domain"),
create(false, "example2.tld", "Alloc token invalid for domain"),
create(false, "reserved.tld", "Reserved"),
create(false, "reserved.tld", "Alloc token invalid for domain"),
create(true, "specificuse.tld", null));
}
@@ -350,23 +321,6 @@ class DomainCheckFlowTest extends ResourceCheckFlowTestCase<DomainCheckFlow, Dom
runFlowAssertResponse(loadFile("domain_check_allocationtoken_fee_specificuse_response.xml"));
}
@Test
void testSuccess_oneExists_allocationTokenForWrongDomain() throws Exception {
setEppInput("domain_check_allocationtoken.xml");
persistActiveDomain("example1.tld");
persistResource(
new AllocationToken.Builder()
.setDomainName("someotherdomain.tld")
.setToken("abc123")
.setTokenType(SINGLE_USE)
.build());
doCheckTest(
create(false, "example1.tld", "In use"),
create(false, "example2.tld", "Alloc token invalid for domain"),
create(false, "reserved.tld", "Reserved"),
create(false, "specificuse.tld", "Reserved; alloc. token required"));
}
@Test
void testSuccess_notOutOfDateToken_forSpecificDomain() throws Exception {
setEppInput("domain_check_allocationtoken.xml");
@@ -385,44 +339,10 @@ class DomainCheckFlowTest extends ResourceCheckFlowTestCase<DomainCheckFlow, Dom
doCheckTest(
create(false, "example1.tld", "Alloc token invalid for domain"),
create(false, "example2.tld", "Alloc token invalid for domain"),
create(false, "reserved.tld", "Reserved"),
create(false, "reserved.tld", "Alloc token invalid for domain"),
create(true, "specificuse.tld", null));
}
@Test
void testSuccess_outOfDateToken_forSpecificDomain() throws Exception {
setEppInput("domain_check_allocationtoken.xml");
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setDomainName("specificuse.tld")
.setTokenStatusTransitions(
ImmutableSortedMap.<DateTime, TokenStatus>naturalOrder()
.put(START_OF_TIME, TokenStatus.NOT_STARTED)
.put(clock.nowUtc().minusDays(2), TokenStatus.VALID)
.put(clock.nowUtc().minusDays(1), TokenStatus.ENDED)
.build())
.build());
doCheckTest(
create(false, "example1.tld", "Alloc token invalid for domain"),
create(false, "example2.tld", "Alloc token invalid for domain"),
create(false, "reserved.tld", "Reserved"),
create(false, "specificuse.tld", "Alloc token not in promo period"));
}
@Test
void testSuccess_nothingExists_reservationsOverrideInvalidAllocationTokens() throws Exception {
setEppInput("domain_check_reserved_allocationtoken.xml");
// Fill out these reasons
doCheckTest(
create(false, "collision.tld", "Cannot be delegated"),
create(false, "reserved.tld", "Reserved"),
create(false, "anchor.tld", "Reserved; alloc. token required"),
create(false, "allowedinsunrise.tld", "Reserved"),
create(false, "premiumcollision.tld", "Cannot be delegated"));
}
@Test
void testSuccess_allocationTokenPromotion_singleYear() throws Exception {
createTld("example");
@@ -500,7 +420,75 @@ class DomainCheckFlowTest extends ResourceCheckFlowTestCase<DomainCheckFlow, Dom
}
@Test
void testFailure_allocationTokenPromotion_PremiumsNotSet() throws Exception {
void testSuccess_allocationTokenInvalid_overridesOtherErrors() throws Exception {
setEppInput("domain_check_allocationtoken.xml");
persistActiveDomain("example1.tld");
doCheckTest(
create(false, "example1.tld", "The allocation token is invalid"),
create(false, "example2.tld", "The allocation token is invalid"),
create(false, "reserved.tld", "The allocation token is invalid"),
create(false, "specificuse.tld", "The allocation token is invalid"));
}
@Test
void testSuccess_allocationTokenForWrongDomain_overridesOtherConcerns() throws Exception {
setEppInput("domain_check_allocationtoken.xml");
persistActiveDomain("example1.tld");
persistResource(
new AllocationToken.Builder()
.setDomainName("someotherdomain.tld")
.setToken("abc123")
.setTokenType(SINGLE_USE)
.build());
doCheckTest(
create(false, "example1.tld", "Alloc token invalid for domain"),
create(false, "example2.tld", "Alloc token invalid for domain"),
create(false, "reserved.tld", "Alloc token invalid for domain"),
create(false, "specificuse.tld", "Alloc token invalid for domain"));
}
@Test
void testSuccess_outOfDateToken_overridesOtherIssues() throws Exception {
setEppInput("domain_check_allocationtoken.xml");
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setDomainName("specificuse.tld")
.setTokenStatusTransitions(
ImmutableSortedMap.<DateTime, TokenStatus>naturalOrder()
.put(START_OF_TIME, TokenStatus.NOT_STARTED)
.put(clock.nowUtc().minusDays(2), TokenStatus.VALID)
.put(clock.nowUtc().minusDays(1), TokenStatus.ENDED)
.build())
.build());
doCheckTest(
create(false, "example1.tld", "Alloc token not in promo period"),
create(false, "example2.tld", "Alloc token not in promo period"),
create(false, "reserved.tld", "Alloc token not in promo period"),
create(false, "specificuse.tld", "Alloc token not in promo period"));
}
@Test
void testSuccess_redeemedTokenOverridesOtherConcerns() throws Exception {
setEppInput("domain_check_allocationtoken.xml");
Domain domain = persistActiveDomain("example1.tld");
HistoryEntryId historyEntryId = new HistoryEntryId(domain.getRepoId(), 1L);
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setRedemptionHistoryId(historyEntryId)
.build());
doCheckTest(
create(false, "example1.tld", "Alloc token was already redeemed"),
create(false, "example2.tld", "Alloc token was already redeemed"),
create(false, "reserved.tld", "Alloc token was already redeemed"),
create(false, "specificuse.tld", "Alloc token was already redeemed"));
}
@Test
void testSuccess_allocationTokenPromotion_noPremium_stillPasses() throws Exception {
createTld("example");
persistResource(
new AllocationToken.Builder()
@@ -515,7 +503,7 @@ class DomainCheckFlowTest extends ResourceCheckFlowTestCase<DomainCheckFlow, Dom
ImmutableMap.of("DOMAIN", "rich.example"));
doCheckTest(
create(true, "example1.example", null),
create(false, "rich.example", "Token not valid for premium name"),
create(true, "rich.example", null),
create(true, "example3.example", null));
}
@@ -572,7 +560,8 @@ class DomainCheckFlowTest extends ResourceCheckFlowTestCase<DomainCheckFlow, Dom
doCheckTest(
create(false, "example1.tld", "Alloc token not in promo period"),
create(false, "example2.example", "Alloc token not in promo period"),
create(false, "reserved.tld", "Reserved"));
create(false, "reserved.tld", "Alloc token not in promo period"),
create(false, "rich.example", "Alloc token not in promo period"));
}
@Test
@@ -593,9 +582,10 @@ class DomainCheckFlowTest extends ResourceCheckFlowTestCase<DomainCheckFlow, Dom
.build());
setEppInput("domain_check_allocationtoken_fee.xml");
doCheckTest(
create(false, "example1.tld", "Alloc token invalid for TLD"),
create(true, "example1.tld", null),
create(true, "example2.example", null),
create(false, "reserved.tld", "Reserved"));
create(false, "reserved.tld", "Reserved"),
create(true, "rich.example", null));
}
@Test
@@ -618,7 +608,8 @@ class DomainCheckFlowTest extends ResourceCheckFlowTestCase<DomainCheckFlow, Dom
doCheckTest(
create(false, "example1.tld", "Alloc token invalid for client"),
create(false, "example2.example", "Alloc token invalid for client"),
create(false, "reserved.tld", "Reserved"));
create(false, "reserved.tld", "Alloc token invalid for client"),
create(false, "rich.example", "Alloc token invalid for client"));
}
@Test
@@ -999,6 +990,7 @@ class DomainCheckFlowTest extends ResourceCheckFlowTestCase<DomainCheckFlow, Dom
.setToken("abc123")
.setTokenType(UNLIMITED_USE)
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE, CommandName.TRANSFER))
.setDiscountFraction(0.1)
.build());
setEppInput("domain_check_fee_multiple_commands_allocationtoken_v06.xml");
runFlowAssertResponse(
@@ -1028,6 +1020,7 @@ class DomainCheckFlowTest extends ResourceCheckFlowTestCase<DomainCheckFlow, Dom
.setToken("abc123")
.setTokenType(UNLIMITED_USE)
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE, CommandName.TRANSFER))
.setDiscountFraction(0.1)
.build());
setEppInput("domain_check_fee_multiple_commands_allocationtoken_v12.xml");
runFlowAssertResponse(
@@ -141,13 +141,10 @@ import google.registry.flows.domain.DomainFlowUtils.TrailingDashException;
import google.registry.flows.domain.DomainFlowUtils.UnexpectedClaimsNoticeException;
import google.registry.flows.domain.DomainFlowUtils.UnsupportedFeeAttributeException;
import google.registry.flows.domain.DomainFlowUtils.UnsupportedMarkTypeException;
import google.registry.flows.domain.DomainPricingLogic.AllocationTokenInvalidForPremiumNameException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotInPromotionException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForDomainException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForRegistrarException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForTldException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AlreadyRedeemedAllocationTokenException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.InvalidAllocationTokenException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.NonexistentAllocationTokenException;
import google.registry.flows.exceptions.OnlyToolCanPassMetadataException;
import google.registry.flows.exceptions.ResourceAlreadyExistsForThisClientException;
import google.registry.flows.exceptions.ResourceCreateContentionException;
@@ -529,51 +526,10 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
"domain_create_allocationtoken.xml",
ImmutableMap.of("DOMAIN", "example.tld", "YEARS", "2"));
persistContactsAndHosts();
EppException thrown = assertThrows(InvalidAllocationTokenException.class, this::runFlow);
EppException thrown = assertThrows(NonexistentAllocationTokenException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_reservedDomainCreate_allocationTokenIsForADifferentDomain() {
// Try to register a reserved domain name with an allocation token valid for a different domain
// name.
setEppInput(
"domain_create_allocationtoken.xml", ImmutableMap.of("DOMAIN", "resdom.tld", "YEARS", "2"));
persistContactsAndHosts();
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setDomainName("otherdomain.tld")
.build());
clock.advanceOneMilli();
EppException thrown =
assertThrows(AllocationTokenNotValidForDomainException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
assertAllocationTokenWasNotRedeemed("abc123");
}
@Test
void testFailure_nonreservedDomainCreate_allocationTokenIsForADifferentDomain() {
// Try to register a non-reserved domain name with an allocation token valid for a different
// domain name.
setEppInput(
"domain_create_allocationtoken.xml",
ImmutableMap.of("DOMAIN", "example.tld", "YEARS", "2"));
persistContactsAndHosts();
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setDomainName("otherdomain.tld")
.build());
clock.advanceOneMilli();
EppException thrown =
assertThrows(AllocationTokenNotValidForDomainException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
assertAllocationTokenWasNotRedeemed("abc123");
}
@Test
void testFailure_alreadyRedemeedAllocationToken() {
setEppInput(
@@ -1704,32 +1660,6 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
assertThat(billingEvent.getCost()).isEqualTo(Money.of(USD, 204.44));
}
@Test
void testSuccess_promotionDoesNotApplyToPremiumPrice() {
// Discounts only apply to premium domains if the token is explicitly configured to allow it.
createTld("example");
persistContactsAndHosts();
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(UNLIMITED_USE)
.setDiscountFraction(0.5)
.setTokenStatusTransitions(
ImmutableSortedMap.<DateTime, TokenStatus>naturalOrder()
.put(START_OF_TIME, TokenStatus.NOT_STARTED)
.put(clock.nowUtc().plusMillis(1), TokenStatus.VALID)
.put(clock.nowUtc().plusSeconds(1), TokenStatus.ENDED)
.build())
.build());
clock.advanceOneMilli();
setEppInput(
"domain_create_premium_allocationtoken.xml",
ImmutableMap.of("YEARS", "2", "FEE", "193.50"));
assertAboutEppExceptions()
.that(assertThrows(AllocationTokenInvalidForPremiumNameException.class, this::runFlow))
.marshalsToXml();
}
@Test
void testSuccess_token_premiumDomainZeroPrice_noFeeExtension() throws Exception {
createTld("example");
@@ -1774,30 +1704,6 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
.marshalsToXml();
}
@Test
void testSuccess_promoTokenNotValidForTld() {
persistContactsAndHosts();
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(UNLIMITED_USE)
.setAllowedTlds(ImmutableSet.of("example"))
.setDiscountFraction(0.5)
.setTokenStatusTransitions(
ImmutableSortedMap.<DateTime, TokenStatus>naturalOrder()
.put(START_OF_TIME, TokenStatus.NOT_STARTED)
.put(clock.nowUtc().minusDays(1), TokenStatus.VALID)
.put(clock.nowUtc().plusDays(1), TokenStatus.ENDED)
.build())
.build());
setEppInput(
"domain_create_allocationtoken.xml",
ImmutableMap.of("DOMAIN", "example.tld", "YEARS", "2"));
assertAboutEppExceptions()
.that(assertThrows(AllocationTokenNotValidForTldException.class, this::runFlow))
.marshalsToXml();
}
@Test
void testSuccess_promoTokenNotValidForRegistrar() {
persistContactsAndHosts();
@@ -3671,22 +3577,6 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
assertThrows(MissingClaimsNoticeException.class, this::runFlow);
}
@Test
void testFailure_anchorTenant_mismatchedName_viaToken() throws Exception {
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setRegistrationBehavior(RegistrationBehavior.ANCHOR_TENANT)
.setDomainName("example.tld")
.build());
persistContactsAndHosts();
setEppInput(
"domain_create_allocationtoken.xml",
ImmutableMap.of("DOMAIN", "example-one.tld", "YEARS", "2"));
assertThrows(AllocationTokenNotValidForDomainException.class, this::runFlow);
}
@Test
void testSuccess_bulkToken_addsTokenToDomain() throws Exception {
AllocationToken token =
@@ -179,7 +179,7 @@ class DomainInfoFlowTest extends ResourceFlowTestCase<DomainInfoFlow, Domain> {
ImmutableMap<String, String> substitutions,
boolean expectHistoryAndBilling)
throws Exception {
assertMutatingFlow(false);
assertMutatingFlow(true);
String expected =
loadFile(expectedXmlFilename, updateSubstitutions(substitutions, "ROID", "2FF-TLD"));
if (inactive) {
@@ -28,7 +28,6 @@ import static google.registry.testing.DatabaseHelper.persistPremiumList;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.util.DateTimeUtils.END_OF_TIME;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static org.joda.money.CurrencyUnit.JPY;
import static org.joda.money.CurrencyUnit.USD;
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -39,8 +38,6 @@ import google.registry.flows.EppException;
import google.registry.flows.HttpSessionMetadata;
import google.registry.flows.SessionMetadata;
import google.registry.flows.custom.DomainPricingCustomLogic;
import google.registry.flows.domain.DomainPricingLogic.AllocationTokenInvalidForCurrencyException;
import google.registry.flows.domain.DomainPricingLogic.AllocationTokenInvalidForPremiumNameException;
import google.registry.model.billing.BillingBase.Reason;
import google.registry.model.billing.BillingBase.RenewalPriceBehavior;
import google.registry.model.billing.BillingRecurrence;
@@ -214,32 +211,6 @@ public class DomainPricingLogicTest {
.build());
}
@Test
void
testGetDomainCreatePrice_withDiscountPriceToken_domainCurrencyDoesNotMatchTokensCurrency_throwsException() {
AllocationToken allocationToken =
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setDiscountPrice(Money.of(JPY, new BigDecimal("250")))
.setDiscountPremiums(false)
.build());
// Domain's currency is not JPY (is USD).
assertThrows(
AllocationTokenInvalidForCurrencyException.class,
() ->
domainPricingLogic.getCreatePrice(
tld,
"default.example",
clock.nowUtc(),
3,
false,
false,
Optional.of(allocationToken)));
}
@Test
void testGetDomainRenewPrice_oneYear_standardDomain_noBilling_isStandardPrice()
throws EppException {
@@ -335,77 +306,6 @@ public class DomainPricingLogicTest {
.build());
}
@Test
void
testGetDomainRenewPrice_oneYear_premiumDomain_default_withTokenNotValidForPremiums_throwsException() {
AllocationToken allocationToken =
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setDiscountFraction(0.5)
.setDiscountPremiums(false)
.build());
assertThrows(
AllocationTokenInvalidForPremiumNameException.class,
() ->
domainPricingLogic.getRenewPrice(
tld,
"premium.example",
clock.nowUtc(),
1,
persistDomainAndSetRecurrence("premium.example", DEFAULT, Optional.empty()),
Optional.of(allocationToken)));
}
@Test
void
testGetDomainRenewPrice_oneYear_premiumDomain_default_withDiscountPriceToken_throwsException() {
AllocationToken allocationToken =
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setDiscountPrice(Money.of(USD, 5))
.setDiscountPremiums(false)
.build());
assertThrows(
AllocationTokenInvalidForPremiumNameException.class,
() ->
domainPricingLogic.getRenewPrice(
tld,
"premium.example",
clock.nowUtc(),
1,
persistDomainAndSetRecurrence("premium.example", DEFAULT, Optional.empty()),
Optional.of(allocationToken)));
}
@Test
void
testGetDomainRenewPrice_withDiscountPriceToken_domainCurrencyDoesNotMatchTokensCurrency_throwsException() {
AllocationToken allocationToken =
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setDiscountPrice(Money.of(JPY, new BigDecimal("250")))
.setDiscountPremiums(false)
.build());
// Domain's currency is not JPY (is USD).
assertThrows(
AllocationTokenInvalidForCurrencyException.class,
() ->
domainPricingLogic.getRenewPrice(
tld,
"default.example",
clock.nowUtc(),
1,
persistDomainAndSetRecurrence("default.example", DEFAULT, Optional.empty()),
Optional.of(allocationToken)));
}
@Test
void testGetDomainRenewPrice_multiYear_premiumDomain_default_isPremiumCost() throws EppException {
assertThat(
@@ -450,30 +350,6 @@ public class DomainPricingLogicTest {
.build());
}
@Test
void
testGetDomainRenewPrice_multiYear_premiumDomain_default_withTokenNotValidForPremiums_throwsException() {
AllocationToken allocationToken =
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setDiscountFraction(0.5)
.setDiscountPremiums(false)
.setDiscountYears(2)
.build());
assertThrows(
AllocationTokenInvalidForPremiumNameException.class,
() ->
domainPricingLogic.getRenewPrice(
tld,
"premium.example",
clock.nowUtc(),
5,
persistDomainAndSetRecurrence("premium.example", DEFAULT, Optional.empty()),
Optional.of(allocationToken)));
}
@Test
void testGetDomainRenewPrice_oneYear_standardDomain_default_isNonPremiumPrice()
throws EppException {
@@ -69,13 +69,12 @@ import google.registry.flows.domain.DomainFlowUtils.NotAuthorizedForTldException
import google.registry.flows.domain.DomainFlowUtils.RegistrarMustBeActiveForThisOperationException;
import google.registry.flows.domain.DomainFlowUtils.UnsupportedFeeAttributeException;
import google.registry.flows.domain.DomainRenewFlow.IncorrectCurrentExpirationDateException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotInPromotionException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForDomainException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForRegistrarException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForTldException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AlreadyRedeemedAllocationTokenException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.InvalidAllocationTokenException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.MissingRemoveBulkPricingTokenOnBulkPricingDomainException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.NonexistentAllocationTokenException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.RemoveBulkPricingTokenOnNonBulkPricingDomainException;
import google.registry.flows.exceptions.ResourceStatusProhibitsOperationException;
import google.registry.model.billing.BillingBase.Flag;
@@ -459,10 +458,14 @@ class DomainRenewFlowTest extends ResourceFlowTestCase<DomainRenewFlow, Domain>
ImmutableMap<String, String> customFeeMap =
updateSubstitutions(
FEE_06_MAP,
"NAME", "costly-renew.tld",
"PERIOD", "1",
"EX_DATE", "2001-04-03T22:00:00.0Z",
"FEE", "111.00");
"NAME",
"costly-renew.tld",
"PERIOD",
"1",
"EX_DATE",
"2001-04-03T22:00:00.0Z",
"FEE",
"111.00");
setEppInput("domain_renew_fee.xml", customFeeMap);
persistDomain();
doSuccessfulTest(
@@ -694,7 +697,7 @@ class DomainRenewFlowTest extends ResourceFlowTestCase<DomainRenewFlow, Domain>
"domain_renew_allocationtoken.xml",
ImmutableMap.of("DOMAIN", "example.tld", "YEARS", "2", "TOKEN", "abc123"));
persistDomain();
EppException thrown = assertThrows(InvalidAllocationTokenException.class, this::runFlow);
EppException thrown = assertThrows(NonexistentAllocationTokenException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@@ -711,9 +714,12 @@ class DomainRenewFlowTest extends ResourceFlowTestCase<DomainRenewFlow, Domain>
.setDomainName("otherdomain.tld")
.build());
clock.advanceOneMilli();
EppException thrown =
assertThrows(AllocationTokenNotValidForDomainException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
assertAboutEppExceptions()
.that(
assertThrows(
AllocationTokenFlowUtils.AllocationTokenNotValidForDomainException.class,
this::runFlow))
.marshalsToXml();
assertAllocationTokenWasNotRedeemed("abc123");
}
@@ -784,9 +790,10 @@ class DomainRenewFlowTest extends ResourceFlowTestCase<DomainRenewFlow, Domain>
.put(clock.nowUtc().plusDays(1), TokenStatus.ENDED)
.build())
.build());
assertAboutEppExceptions()
.that(assertThrows(AllocationTokenNotValidForTldException.class, this::runFlow))
.marshalsToXml();
runFlowAssertResponse(
loadFile(
"domain_renew_response.xml",
ImmutableMap.of("DOMAIN", "example.tld", "EXDATE", "2002-04-03T22:00:00.0Z")));
assertAllocationTokenWasNotRedeemed("abc123");
}
@@ -52,12 +52,11 @@ import google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException;
import google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException;
import google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException;
import google.registry.flows.domain.DomainFlowUtils.NotAuthorizedForTldException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotInPromotionException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForDomainException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForRegistrarException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForTldException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AlreadyRedeemedAllocationTokenException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.InvalidAllocationTokenException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.NonexistentAllocationTokenException;
import google.registry.flows.exceptions.NotPendingTransferException;
import google.registry.model.billing.BillingBase;
import google.registry.model.billing.BillingBase.Reason;
@@ -897,7 +896,7 @@ class DomainTransferApproveFlowTest
@Test
void testFailure_invalidAllocationToken() throws Exception {
setEppInput("domain_transfer_approve_allocation_token.xml");
EppException thrown = assertThrows(InvalidAllocationTokenException.class, this::runFlow);
EppException thrown = assertThrows(NonexistentAllocationTokenException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@@ -910,9 +909,12 @@ class DomainTransferApproveFlowTest
.setDomainName("otherdomain.tld")
.build());
setEppInput("domain_transfer_approve_allocation_token.xml");
EppException thrown =
assertThrows(AllocationTokenNotValidForDomainException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
assertAboutEppExceptions()
.that(
assertThrows(
AllocationTokenFlowUtils.AllocationTokenNotValidForDomainException.class,
this::runFlow))
.marshalsToXml();
}
@Test
@@ -971,8 +973,7 @@ class DomainTransferApproveFlowTest
.build())
.build());
setEppInput("domain_transfer_approve_allocation_token.xml");
EppException thrown = assertThrows(AllocationTokenNotValidForTldException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
runFlowAssertResponse(loadFile("domain_transfer_approve_response.xml"));
}
@Test
@@ -79,11 +79,9 @@ import google.registry.flows.domain.DomainFlowUtils.PremiumNameBlockedException;
import google.registry.flows.domain.DomainFlowUtils.RegistrarMustBeActiveForThisOperationException;
import google.registry.flows.domain.DomainFlowUtils.UnsupportedFeeAttributeException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotInPromotionException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForDomainException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForRegistrarException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForTldException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AlreadyRedeemedAllocationTokenException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.InvalidAllocationTokenException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.NonexistentAllocationTokenException;
import google.registry.flows.exceptions.AlreadyPendingTransferException;
import google.registry.flows.exceptions.InvalidTransferPeriodValueException;
import google.registry.flows.exceptions.MissingTransferRequestAuthInfoException;
@@ -505,7 +503,7 @@ class DomainTransferRequestFlowTest
implicitTransferTime,
transferCost,
originalGracePeriods,
/* expectTransferBillingEvent = */ true,
/* expectTransferBillingEvent= */ true,
extraExpectedBillingEvents);
assertPollMessagesEmitted(expectedExpirationTime, implicitTransferTime);
@@ -1859,22 +1857,7 @@ class DomainTransferRequestFlowTest
void testFailure_invalidAllocationToken() throws Exception {
setupDomain("example", "tld");
setEppInput("domain_transfer_request_allocation_token.xml", ImmutableMap.of("TOKEN", "abc123"));
EppException thrown = assertThrows(InvalidAllocationTokenException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_allocationTokenIsForDifferentName() throws Exception {
setupDomain("example", "tld");
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setDomainName("otherdomain.tld")
.build());
setEppInput("domain_transfer_request_allocation_token.xml", ImmutableMap.of("TOKEN", "abc123"));
EppException thrown =
assertThrows(AllocationTokenNotValidForDomainException.class, this::runFlow);
EppException thrown = assertThrows(NonexistentAllocationTokenException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@@ -1920,27 +1903,6 @@ class DomainTransferRequestFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_allocationTokenNotValidForTld() throws Exception {
setupDomain("example", "tld");
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(UNLIMITED_USE)
.setAllowedTlds(ImmutableSet.of("example"))
.setDiscountFraction(0.5)
.setTokenStatusTransitions(
ImmutableSortedMap.<DateTime, TokenStatus>naturalOrder()
.put(START_OF_TIME, TokenStatus.NOT_STARTED)
.put(clock.nowUtc().minusDays(1), TokenStatus.VALID)
.put(clock.nowUtc().plusDays(1), TokenStatus.ENDED)
.build())
.build());
setEppInput("domain_transfer_request_allocation_token.xml", ImmutableMap.of("TOKEN", "abc123"));
EppException thrown = assertThrows(AllocationTokenNotValidForTldException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_allocationTokenAlreadyRedeemed() throws Exception {
setupDomain("example", "tld");
@@ -19,31 +19,25 @@ import static google.registry.model.domain.token.AllocationToken.TokenStatus.CAN
import static google.registry.model.domain.token.AllocationToken.TokenStatus.ENDED;
import static google.registry.model.domain.token.AllocationToken.TokenStatus.NOT_STARTED;
import static google.registry.model.domain.token.AllocationToken.TokenStatus.VALID;
import static google.registry.model.domain.token.AllocationToken.TokenType.DEFAULT_PROMO;
import static google.registry.model.domain.token.AllocationToken.TokenType.SINGLE_USE;
import static google.registry.model.domain.token.AllocationToken.TokenType.UNLIMITED_USE;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.persistActiveDomain;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.EppExceptionSubject.assertAboutEppExceptions;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static org.joda.time.DateTimeZone.UTC;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.net.InternetDomainName;
import google.registry.flows.EppException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotInPromotionException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForCommandException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForRegistrarException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForTldException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.InvalidAllocationTokenException;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainCommand;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.NonexistentAllocationTokenException;
import google.registry.model.domain.fee.FeeQueryCommandExtensionItem.CommandName;
import google.registry.model.domain.token.AllocationToken;
import google.registry.model.domain.token.AllocationToken.TokenStatus;
@@ -52,7 +46,7 @@ import google.registry.model.reporting.HistoryEntry.HistoryEntryId;
import google.registry.model.tld.Tld;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
import google.registry.testing.DatabaseHelper;
import google.registry.testing.FakeClock;
import java.util.Optional;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
@@ -62,319 +56,322 @@ import org.junit.jupiter.api.extension.RegisterExtension;
/** Unit tests for {@link AllocationTokenFlowUtils}. */
class AllocationTokenFlowUtilsTest {
private final AllocationTokenFlowUtils flowUtils = new AllocationTokenFlowUtils();
private final FakeClock clock = new FakeClock(DateTime.parse("2025-01-10T01:00:00.000Z"));
@RegisterExtension
final JpaIntegrationTestExtension jpa =
new JpaTestExtensions.Builder().buildIntegrationTestExtension();
new JpaTestExtensions.Builder().withClock(clock).buildIntegrationTestExtension();
private final AllocationTokenExtension allocationTokenExtension =
mock(AllocationTokenExtension.class);
private Tld tld;
@BeforeEach
void beforeEach() {
createTld("tld");
tld = createTld("tld");
}
@Test
void test_validateToken_successfullyVerifiesValidTokenOnCreate() throws Exception {
void testSuccess_redeemsToken() {
HistoryEntryId historyEntryId = new HistoryEntryId("repoId", 10L);
assertThat(
AllocationTokenFlowUtils.redeemToken(singleUseTokenBuilder().build(), historyEntryId)
.getRedemptionHistoryId())
.hasValue(historyEntryId);
}
@Test
void testInvalidForPremiumName_validForPremium() {
AllocationToken token = singleUseTokenBuilder().setDiscountPremiums(true).build();
assertThat(AllocationTokenFlowUtils.discountTokenInvalidForPremiumName(token, true)).isFalse();
}
@Test
void testInvalidForPremiumName_notPremium() {
assertThat(
AllocationTokenFlowUtils.discountTokenInvalidForPremiumName(
singleUseTokenBuilder().build(), false))
.isFalse();
}
@Test
void testInvalidForPremiumName_invalidForPremium() {
assertThat(
AllocationTokenFlowUtils.discountTokenInvalidForPremiumName(
singleUseTokenBuilder().build(), true))
.isTrue();
}
@Test
void testSuccess_loadFromExtension() throws Exception {
AllocationToken token =
persistResource(
new AllocationToken.Builder()
.setToken("tokeN")
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE, CommandName.RESTORE))
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE))
.setTokenType(SINGLE_USE)
.build());
when(allocationTokenExtension.getAllocationToken()).thenReturn("tokeN");
assertThat(
flowUtils
.verifyAllocationTokenCreateIfPresent(
createCommand("blah.tld"),
Tld.get("tld"),
"TheRegistrar",
DateTime.now(UTC),
Optional.of(allocationTokenExtension))
.get())
.isEqualTo(token);
AllocationTokenFlowUtils.loadAllocationTokenFromExtension(
"TheRegistrar",
"example.tld",
clock.nowUtc(),
Optional.of(allocationTokenExtension)))
.hasValue(token);
}
@Test
void test_validateToken_successfullyVerifiesValidTokenExistingDomain() throws Exception {
void testSuccess_loadOrDefault_fromExtensionEvenWhenDefaultPresent() throws Exception {
persistDefaultToken();
AllocationToken token =
persistResource(
new AllocationToken.Builder()
.setToken("tokeN")
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE, CommandName.RENEW))
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE))
.setTokenType(SINGLE_USE)
.build());
when(allocationTokenExtension.getAllocationToken()).thenReturn("tokeN");
assertThat(
flowUtils
.verifyAllocationTokenIfPresent(
DatabaseHelper.newDomain("blah.tld"),
Tld.get("tld"),
"TheRegistrar",
DateTime.now(UTC),
CommandName.RENEW,
Optional.of(allocationTokenExtension))
.get())
.isEqualTo(token);
AllocationTokenFlowUtils.loadTokenFromExtensionOrGetDefault(
"TheRegistrar",
clock.nowUtc(),
Optional.of(allocationTokenExtension),
tld,
"example.tld",
CommandName.CREATE))
.hasValue(token);
}
void test_validateToken_emptyAllowedEppActions_successfullyVerifiesValidTokenExistingDomain()
throws Exception {
AllocationToken token =
persistResource(
new AllocationToken.Builder().setToken("tokeN").setTokenType(SINGLE_USE).build());
when(allocationTokenExtension.getAllocationToken()).thenReturn("tokeN");
@Test
void testSuccess_loadOrDefault_defaultWhenNonePresent() throws Exception {
AllocationToken defaultToken = persistDefaultToken();
assertThat(
flowUtils
.verifyAllocationTokenIfPresent(
DatabaseHelper.newDomain("blah.tld"),
Tld.get("tld"),
"TheRegistrar",
DateTime.now(UTC),
CommandName.RENEW,
Optional.of(allocationTokenExtension))
.get())
.isEqualTo(token);
AllocationTokenFlowUtils.loadTokenFromExtensionOrGetDefault(
"TheRegistrar",
clock.nowUtc(),
Optional.empty(),
tld,
"example.tld",
CommandName.CREATE))
.hasValue(defaultToken);
}
@Test
void test_validateTokenCreate_failsOnNonexistentToken() {
assertValidateCreateThrowsEppException(InvalidAllocationTokenException.class);
}
@Test
void test_validateTokenExistingDomain_failsOnNonexistentToken() {
assertValidateExistingDomainThrowsEppException(InvalidAllocationTokenException.class);
}
@Test
void test_validateTokenCreate_failsOnNullToken() {
assertAboutEppExceptions()
.that(
assertThrows(
InvalidAllocationTokenException.class,
() ->
flowUtils.verifyAllocationTokenCreateIfPresent(
createCommand("blah.tld"),
Tld.get("tld"),
"TheRegistrar",
DateTime.now(UTC),
Optional.of(allocationTokenExtension))))
.marshalsToXml();
}
@Test
void test_validateTokenExistingDomain_failsOnNullToken() {
assertAboutEppExceptions()
.that(
assertThrows(
InvalidAllocationTokenException.class,
() ->
flowUtils.verifyAllocationTokenIfPresent(
DatabaseHelper.newDomain("blah.tld"),
Tld.get("tld"),
"TheRegistrar",
DateTime.now(UTC),
CommandName.RENEW,
Optional.of(allocationTokenExtension))))
.marshalsToXml();
}
@Test
void test_validateTokenCreate_invalidForClientId() {
persistResource(
createOneMonthPromoTokenBuilder(DateTime.now(UTC).minusDays(1))
.setAllowedRegistrarIds(ImmutableSet.of("NewRegistrar"))
.build());
assertValidateCreateThrowsEppException(AllocationTokenNotValidForRegistrarException.class);
}
@Test
void test_validateTokenExistingDomain_invalidForClientId() {
persistResource(
createOneMonthPromoTokenBuilder(DateTime.now(UTC).minusDays(1))
.setAllowedRegistrarIds(ImmutableSet.of("NewRegistrar"))
.build());
assertValidateExistingDomainThrowsEppException(
AllocationTokenNotValidForRegistrarException.class);
}
@Test
void test_validateTokenCreate_invalidForTld() {
persistResource(
createOneMonthPromoTokenBuilder(DateTime.now(UTC).minusDays(1))
.setAllowedTlds(ImmutableSet.of("nottld"))
.build());
assertValidateCreateThrowsEppException(AllocationTokenNotValidForTldException.class);
}
@Test
void test_validateTokenExistingDomain_invalidForTld() {
persistResource(
createOneMonthPromoTokenBuilder(DateTime.now(UTC).minusDays(1))
.setAllowedTlds(ImmutableSet.of("nottld"))
.build());
assertValidateExistingDomainThrowsEppException(AllocationTokenNotValidForTldException.class);
}
@Test
void test_validateTokenCreate_beforePromoStart() {
persistResource(createOneMonthPromoTokenBuilder(DateTime.now(UTC).plusDays(1)).build());
assertValidateCreateThrowsEppException(AllocationTokenNotInPromotionException.class);
}
@Test
void test_validateTokenExistingDomain_beforePromoStart() {
persistResource(createOneMonthPromoTokenBuilder(DateTime.now(UTC).plusDays(1)).build());
assertValidateExistingDomainThrowsEppException(AllocationTokenNotInPromotionException.class);
}
@Test
void test_validateTokenCreate_afterPromoEnd() {
persistResource(createOneMonthPromoTokenBuilder(DateTime.now(UTC).minusMonths(2)).build());
assertValidateCreateThrowsEppException(AllocationTokenNotInPromotionException.class);
}
@Test
void test_validateTokenExistingDomain_afterPromoEnd() {
persistResource(createOneMonthPromoTokenBuilder(DateTime.now(UTC).minusMonths(2)).build());
assertValidateExistingDomainThrowsEppException(AllocationTokenNotInPromotionException.class);
}
@Test
void test_validateTokenCreate_promoCancelled() {
// the promo would be valid, but it was cancelled 12 hours ago
persistResource(
createOneMonthPromoTokenBuilder(DateTime.now(UTC).minusDays(1))
.setTokenStatusTransitions(
ImmutableSortedMap.<DateTime, TokenStatus>naturalOrder()
.put(START_OF_TIME, NOT_STARTED)
.put(DateTime.now(UTC).minusMonths(1), VALID)
.put(DateTime.now(UTC).minusHours(12), CANCELLED)
.build())
.build());
assertValidateCreateThrowsEppException(AllocationTokenNotInPromotionException.class);
}
@Test
void test_validateTokenExistingDomain_promoCancelled() {
// the promo would be valid, but it was cancelled 12 hours ago
persistResource(
createOneMonthPromoTokenBuilder(DateTime.now(UTC).minusDays(1))
.setTokenStatusTransitions(
ImmutableSortedMap.<DateTime, TokenStatus>naturalOrder()
.put(START_OF_TIME, NOT_STARTED)
.put(DateTime.now(UTC).minusMonths(1), VALID)
.put(DateTime.now(UTC).minusHours(12), CANCELLED)
.build())
.build());
assertValidateExistingDomainThrowsEppException(AllocationTokenNotInPromotionException.class);
}
@Test
void test_validateTokenCreate_invalidCommand() {
persistResource(
createOneMonthPromoTokenBuilder(DateTime.now(UTC).minusDays(1))
.setAllowedEppActions(ImmutableSet.of(CommandName.RENEW))
.build());
assertValidateCreateThrowsEppException(AllocationTokenNotValidForCommandException.class);
}
@Test
void test_validateTokenExistingDomain_invalidCommand() {
persistResource(
createOneMonthPromoTokenBuilder(DateTime.now(UTC).minusDays(1))
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE))
.build());
assertValidateExistingDomainThrowsEppException(
AllocationTokenNotValidForCommandException.class);
}
@Test
void test_checkDomainsWithToken_successfullyVerifiesValidToken() {
persistResource(
new AllocationToken.Builder().setToken("tokeN").setTokenType(SINGLE_USE).build());
assertThat(
flowUtils
.checkDomainsWithToken(
ImmutableList.of(
InternetDomainName.from("blah.tld"), InternetDomainName.from("blah2.tld")),
"tokeN",
"TheRegistrar",
DateTime.now(UTC))
.domainCheckResults())
.containsExactlyEntriesIn(
ImmutableMap.of(
InternetDomainName.from("blah.tld"), "", InternetDomainName.from("blah2.tld"), ""))
.inOrder();
}
@Test
void test_checkDomainsWithToken_showsFailureMessageForRedeemedToken() {
Domain domain = persistActiveDomain("example.tld");
HistoryEntryId historyEntryId = new HistoryEntryId(domain.getRepoId(), 1051L);
void testSuccess_loadOrDefault_defaultWhenTokenIsPresentButNotApplicable() throws Exception {
AllocationToken defaultToken = persistDefaultToken();
persistResource(
new AllocationToken.Builder()
.setToken("tokeN")
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE))
.setTokenType(SINGLE_USE)
.setRedemptionHistoryId(historyEntryId)
.setAllowedTlds(ImmutableSet.of("othertld"))
.build());
when(allocationTokenExtension.getAllocationToken()).thenReturn("tokeN");
assertThat(
flowUtils
.checkDomainsWithToken(
ImmutableList.of(
InternetDomainName.from("blah.tld"), InternetDomainName.from("blah2.tld")),
"tokeN",
"TheRegistrar",
DateTime.now(UTC))
.domainCheckResults())
.containsExactlyEntriesIn(
ImmutableMap.of(
InternetDomainName.from("blah.tld"),
"Alloc token was already redeemed",
InternetDomainName.from("blah2.tld"),
"Alloc token was already redeemed"))
.inOrder();
AllocationTokenFlowUtils.loadTokenFromExtensionOrGetDefault(
"TheRegistrar",
clock.nowUtc(),
Optional.of(allocationTokenExtension),
tld,
"example.tld",
CommandName.CREATE))
.hasValue(defaultToken);
}
private void assertValidateCreateThrowsEppException(Class<? extends EppException> clazz) {
@Test
void testValidAgainstDomain_validAllReasons() {
AllocationToken token = singleUseTokenBuilder().setDiscountPremiums(true).build();
assertThat(
AllocationTokenFlowUtils.tokenIsValidAgainstDomain(
InternetDomainName.from("rich.tld"), token, CommandName.CREATE, clock.nowUtc()))
.isTrue();
}
@Test
void testValidAgainstDomain_invalidPremium() {
AllocationToken token = singleUseTokenBuilder().build();
assertThat(
AllocationTokenFlowUtils.tokenIsValidAgainstDomain(
InternetDomainName.from("rich.tld"), token, CommandName.CREATE, clock.nowUtc()))
.isFalse();
}
@Test
void testValidAgainstDomain_invalidAction() {
AllocationToken token =
singleUseTokenBuilder().setAllowedEppActions(ImmutableSet.of(CommandName.RESTORE)).build();
assertThat(
AllocationTokenFlowUtils.tokenIsValidAgainstDomain(
InternetDomainName.from("domain.tld"), token, CommandName.CREATE, clock.nowUtc()))
.isFalse();
}
@Test
void testValidAgainstDomain_invalidTld() {
createTld("othertld");
AllocationToken token = singleUseTokenBuilder().build();
assertThat(
AllocationTokenFlowUtils.tokenIsValidAgainstDomain(
InternetDomainName.from("domain.othertld"),
token,
CommandName.CREATE,
clock.nowUtc()))
.isFalse();
}
@Test
void testValidAgainstDomain_invalidDomain() {
AllocationToken token = singleUseTokenBuilder().setDomainName("anchor.tld").build();
assertThat(
AllocationTokenFlowUtils.tokenIsValidAgainstDomain(
InternetDomainName.from("domain.tld"), token, CommandName.CREATE, clock.nowUtc()))
.isFalse();
}
@Test
void testFailure_redeemToken_nonSingleUse() {
assertThrows(
IllegalArgumentException.class,
() ->
AllocationTokenFlowUtils.redeemToken(
createOneMonthPromoTokenBuilder(clock.nowUtc()).build(),
new HistoryEntryId("repoId", 10L)));
}
@Test
void testFailure_loadFromExtension_nonexistentToken() {
assertLoadTokenFromExtensionThrowsException(NonexistentAllocationTokenException.class);
}
@Test
void testFailure_loadFromExtension_nullToken() {
when(allocationTokenExtension.getAllocationToken()).thenReturn(null);
assertLoadTokenFromExtensionThrowsException(NonexistentAllocationTokenException.class);
}
@Test
void testFailure_tokenInvalidForRegistrar() {
persistResource(
createOneMonthPromoTokenBuilder(clock.nowUtc().minusDays(1))
.setAllowedRegistrarIds(ImmutableSet.of("NewRegistrar"))
.build());
assertLoadTokenFromExtensionThrowsException(AllocationTokenNotValidForRegistrarException.class);
}
@Test
void testFailure_beforePromoStart() {
persistResource(createOneMonthPromoTokenBuilder(clock.nowUtc().plusDays(1)).build());
assertLoadTokenFromExtensionThrowsException(AllocationTokenNotInPromotionException.class);
}
@Test
void testFailure_afterPromoEnd() {
persistResource(createOneMonthPromoTokenBuilder(clock.nowUtc().minusMonths(2)).build());
assertLoadTokenFromExtensionThrowsException(AllocationTokenNotInPromotionException.class);
}
@Test
void testFailure_promoCancelled() {
// the promo would be valid, but it was cancelled 12 hours ago
persistResource(
createOneMonthPromoTokenBuilder(clock.nowUtc().minusDays(1))
.setTokenStatusTransitions(
ImmutableSortedMap.<DateTime, TokenStatus>naturalOrder()
.put(START_OF_TIME, NOT_STARTED)
.put(clock.nowUtc().minusMonths(1), VALID)
.put(clock.nowUtc().minusHours(12), CANCELLED)
.build())
.build());
assertLoadTokenFromExtensionThrowsException(AllocationTokenNotInPromotionException.class);
}
@Test
void testFailure_loadOrDefault_badTokenProvided() throws Exception {
when(allocationTokenExtension.getAllocationToken()).thenReturn("asdf");
assertThrows(
NonexistentAllocationTokenException.class,
() ->
AllocationTokenFlowUtils.loadTokenFromExtensionOrGetDefault(
"TheRegistrar",
clock.nowUtc(),
Optional.of(allocationTokenExtension),
tld,
"example.tld",
CommandName.CREATE));
}
@Test
void testFailure_loadOrDefault_noValidTokens() throws Exception {
assertThat(
AllocationTokenFlowUtils.loadTokenFromExtensionOrGetDefault(
"TheRegistrar",
clock.nowUtc(),
Optional.empty(),
tld,
"example.tld",
CommandName.CREATE))
.isEmpty();
}
@Test
void testFailure_loadOrDefault_badDomainName() throws Exception {
// Tokens tied to a domain should throw a catastrophic exception if used for a different domain
persistResource(singleUseTokenBuilder().setDomainName("someotherdomain.tld").build());
when(allocationTokenExtension.getAllocationToken()).thenReturn("tokeN");
assertThrows(
AllocationTokenFlowUtils.AllocationTokenNotValidForDomainException.class,
() ->
AllocationTokenFlowUtils.loadTokenFromExtensionOrGetDefault(
"TheRegistrar",
clock.nowUtc(),
Optional.of(allocationTokenExtension),
tld,
"example.tld",
CommandName.CREATE));
}
private AllocationToken persistDefaultToken() {
AllocationToken defaultToken =
persistResource(
new AllocationToken.Builder()
.setToken("defaultToken")
.setDiscountFraction(0.1)
.setAllowedTlds(ImmutableSet.of("tld"))
.setAllowedRegistrarIds(ImmutableSet.of("TheRegistrar"))
.setTokenType(DEFAULT_PROMO)
.build());
tld =
persistResource(
tld.asBuilder()
.setDefaultPromoTokens(ImmutableList.of(defaultToken.createVKey()))
.build());
return defaultToken;
}
private void assertLoadTokenFromExtensionThrowsException(Class<? extends EppException> clazz) {
assertAboutEppExceptions()
.that(
assertThrows(
clazz,
() ->
flowUtils.verifyAllocationTokenCreateIfPresent(
createCommand("blah.tld"),
Tld.get("tld"),
AllocationTokenFlowUtils.loadAllocationTokenFromExtension(
"TheRegistrar",
DateTime.now(UTC),
"example.tld",
clock.nowUtc(),
Optional.of(allocationTokenExtension))))
.marshalsToXml();
}
private void assertValidateExistingDomainThrowsEppException(Class<? extends EppException> clazz) {
assertAboutEppExceptions()
.that(
assertThrows(
clazz,
() ->
flowUtils.verifyAllocationTokenIfPresent(
DatabaseHelper.newDomain("blah.tld"),
Tld.get("tld"),
"TheRegistrar",
DateTime.now(UTC),
CommandName.RENEW,
Optional.of(allocationTokenExtension))))
.marshalsToXml();
}
private static DomainCommand.Create createCommand(String domainName) {
DomainCommand.Create command = mock(DomainCommand.Create.class);
when(command.getDomainName()).thenReturn(domainName);
return command;
private AllocationToken.Builder singleUseTokenBuilder() {
when(allocationTokenExtension.getAllocationToken()).thenReturn("tokeN");
return new AllocationToken.Builder()
.setTokenType(SINGLE_USE)
.setToken("tokeN")
.setAllowedTlds(ImmutableSet.of("tld"))
.setDiscountFraction(0.1)
.setAllowedRegistrarIds(ImmutableSet.of("TheRegistrar"));
}
private AllocationToken.Builder createOneMonthPromoTokenBuilder(DateTime promoStart) {
@@ -496,6 +496,28 @@ class RegistrarTest extends EntityTestCase {
.isEqualTo(fakeClock.nowUtc());
}
@Test
void testSuccess_setLastPocVerificationDate() {
assertThat(
registrar
.asBuilder()
.setLastPocVerificationDate(fakeClock.nowUtc())
.build()
.getLastPocVerificationDate())
.isEqualTo(fakeClock.nowUtc());
}
@Test
void testFailure_setLastPocVerificationDate_nullDate() {
IllegalArgumentException thrown =
assertThrows(
IllegalArgumentException.class,
() -> new Registrar.Builder().setLastPocVerificationDate(null).build());
assertThat(thrown)
.hasMessageThat()
.isEqualTo("Registrar lastPocVerificationDate cannot be null");
}
@Test
void testFailure_setLastExpiringFailoverCertNotificationSentDate_nullDate() {
IllegalArgumentException thrown =
@@ -15,16 +15,21 @@
package google.registry.tools;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_OPTIONAL;
import static google.registry.model.common.FeatureFlag.FeatureStatus.ACTIVE;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.persistPremiumList;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static org.joda.money.CurrencyUnit.JPY;
import static org.junit.jupiter.api.Assertions.assertThrows;
import com.beust.jcommander.ParameterException;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import google.registry.dns.writer.VoidDnsWriter;
import google.registry.model.common.FeatureFlag;
import google.registry.model.pricing.StaticPremiumListPricingEngine;
import google.registry.model.tld.Tld;
import google.registry.model.tld.label.PremiumListDao;
@@ -111,12 +116,15 @@ class CreateDomainCommandTest extends EppToolCommandTestCase<CreateDomainCommand
@Test
void testSuccess_minimal() throws Exception {
persistResource(
new FeatureFlag()
.asBuilder()
.setFeatureName(MINIMUM_DATASET_CONTACTS_OPTIONAL)
.setStatusMap(ImmutableSortedMap.of(START_OF_TIME, ACTIVE))
.build());
// Test that each optional field can be omitted. Also tests the auto-gen password.
runCommandForced(
"--client=NewRegistrar",
"--registrant=crr-admin",
"--admins=crr-admin",
"--techs=crr-tech",
"example.tld");
eppVerifier.verifySent("domain_create_minimal.xml");
}
@@ -131,7 +139,9 @@ class CreateDomainCommandTest extends EppToolCommandTestCase<CreateDomainCommand
"--techs=crr-tech",
"example.tld",
"example.abc");
eppVerifier.verifySent("domain_create_minimal.xml").verifySent("domain_create_minimal_abc.xml");
eppVerifier
.verifySent("domain_create_contacts.xml")
.verifySent("domain_create_contacts_abc.xml");
}
@Test
@@ -152,8 +162,8 @@ class CreateDomainCommandTest extends EppToolCommandTestCase<CreateDomainCommand
"example.tld",
"example.abc");
eppVerifier
.verifySent("domain_create_minimal.xml")
.verifySent("domain_create_minimal_abc.xml");
.verifySent("domain_create_contacts.xml")
.verifySent("domain_create_contacts_abc.xml");
}
@Test
@@ -192,9 +202,9 @@ class CreateDomainCommandTest extends EppToolCommandTestCase<CreateDomainCommand
"palladium.tld",
"example.abc");
eppVerifier
.verifySent("domain_create_minimal.xml")
.verifySent("domain_create_contacts.xml")
.verifySent("domain_create_palladium.xml")
.verifySent("domain_create_minimal_abc.xml");
.verifySent("domain_create_contacts_abc.xml");
assertInStdout(
"palladium.tld is premium at USD 877.00 per year; "
+ "sending total cost for 1 year(s) of USD 877.00.");
@@ -227,6 +237,19 @@ class CreateDomainCommandTest extends EppToolCommandTestCase<CreateDomainCommand
eppVerifier.verifySent("domain_create_token.xml");
}
@Test
void testSuccess_contactsStillRequired() throws Exception {
// Verify that if contacts are still required, the minimum+contacts request is sent
createTld("tld");
runCommandForced(
"--client=NewRegistrar",
"--registrant=crr-admin",
"--admins=crr-admin",
"--techs=crr-tech",
"example.tld");
eppVerifier.verifySent("domain_create_contacts.xml");
}
@Test
void testFailure_duplicateDomains() {
IllegalArgumentException thrown =
@@ -40,6 +40,7 @@ import google.registry.request.Action;
import google.registry.request.RequestModule;
import google.registry.request.auth.AuthResult;
import google.registry.testing.ConsoleApiParamsUtils;
import google.registry.testing.FakeClock;
import google.registry.testing.FakeResponse;
import google.registry.testing.SystemPropertyExtension;
import google.registry.tools.GsonUtils;
@@ -51,6 +52,7 @@ import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.util.Optional;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
@@ -59,7 +61,7 @@ import org.junit.jupiter.api.extension.RegisterExtension;
/** Tests for {@link google.registry.ui.server.console.ConsoleUpdateRegistrarAction}. */
class ConsoleUpdateRegistrarActionTest {
private static final Gson GSON = GsonUtils.provideGson();
private final FakeClock clock = new FakeClock(DateTime.parse("2025-01-01T00:00:00.000Z"));
private ConsoleApiParams consoleApiParams;
private FakeResponse response;
@@ -68,12 +70,17 @@ class ConsoleUpdateRegistrarActionTest {
private User user;
private static String registrarPostData =
"{\"registrarId\":\"%s\",\"allowedTlds\":[%s],\"registryLockAllowed\":%s}";
"{\"registrarId\":\"%s\",\"allowedTlds\":[%s],\"registryLockAllowed\":%s,"
+ " \"lastPocVerificationDate\":%s }";
@RegisterExtension
@Order(Integer.MAX_VALUE)
final SystemPropertyExtension systemPropertyExtension = new SystemPropertyExtension();
@RegisterExtension
final JpaTestExtensions.JpaIntegrationTestExtension jpa =
new JpaTestExtensions.Builder().withClock(clock).buildIntegrationTestExtension();
@BeforeEach
void beforeEach() throws Exception {
createTlds("app", "dev");
@@ -94,13 +101,16 @@ class ConsoleUpdateRegistrarActionTest {
consoleApiParams = createParams();
}
@RegisterExtension
final JpaTestExtensions.JpaIntegrationTestExtension jpa =
new JpaTestExtensions.Builder().buildIntegrationTestExtension();
@Test
void testSuccess_updatesRegistrar() throws IOException {
var action = createAction(String.format(registrarPostData, "TheRegistrar", "app, dev", false));
var action =
createAction(
String.format(
registrarPostData,
"TheRegistrar",
"app, dev",
false,
"\"2024-12-12T00:00:00.000Z\""));
action.run();
Registrar newRegistrar = Registrar.loadByRegistrarId("TheRegistrar").get();
assertThat(newRegistrar.getAllowedTlds()).containsExactly("app", "dev");
@@ -111,10 +121,44 @@ class ConsoleUpdateRegistrarActionTest {
assertThat(history.getDescription()).hasValue("TheRegistrar");
}
@Test
void testSuccess_updatesNullPocVerificationDate() throws IOException {
var action =
createAction(
String.format(registrarPostData, "TheRegistrar", "app, dev", false, "\"null\""));
action.run();
Registrar newRegistrar = Registrar.loadByRegistrarId("TheRegistrar").get();
assertThat(newRegistrar.getLastPocVerificationDate())
.isEqualTo(DateTime.parse("1970-01-01T00:00:00.000Z"));
}
@Test
void testFailure_pocVerificationInTheFuture() throws IOException {
var action =
createAction(
String.format(
registrarPostData,
"TheRegistrar",
"app, dev",
false,
"\"2025-02-01T00:00:00.000Z\""));
action.run();
assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_BAD_REQUEST);
assertThat((String) ((FakeResponse) consoleApiParams.response()).getPayload())
.contains("Invalid value of LastPocVerificationDate - value is in the future");
}
@Test
void testFails_missingWhoisContact() throws IOException {
RegistryEnvironment.PRODUCTION.setup(systemPropertyExtension);
var action = createAction(String.format(registrarPostData, "TheRegistrar", "app, dev", false));
var action =
createAction(
String.format(
registrarPostData,
"TheRegistrar",
"app, dev",
false,
"\"2024-12-12T00:00:00.000Z\""));
action.run();
assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_BAD_REQUEST);
assertThat((String) ((FakeResponse) consoleApiParams.response()).getPayload())
@@ -137,7 +181,14 @@ class ConsoleUpdateRegistrarActionTest {
.setVisibleInDomainWhoisAsAbuse(true)
.build();
persistResource(contact);
var action = createAction(String.format(registrarPostData, "TheRegistrar", "app, dev", false));
var action =
createAction(
String.format(
registrarPostData,
"TheRegistrar",
"app, dev",
false,
"\"2024-12-12T00:00:00.000Z\""));
action.run();
Registrar newRegistrar = Registrar.loadByRegistrarId("TheRegistrar").get();
assertThat(newRegistrar.getAllowedTlds()).containsExactly("app", "dev");
@@ -147,7 +198,14 @@ class ConsoleUpdateRegistrarActionTest {
@Test
void testSuccess_sendsEmail() throws AddressException, IOException {
var action = createAction(String.format(registrarPostData, "TheRegistrar", "app, dev", false));
var action =
createAction(
String.format(
registrarPostData,
"TheRegistrar",
"app, dev",
false,
"\"2024-12-12T00:00:00.000Z\""));
action.run();
verify(consoleApiParams.sendEmailUtils().gmailClient, times(1))
.sendEmail(
@@ -159,7 +217,9 @@ class ConsoleUpdateRegistrarActionTest {
"The following changes were made in registry unittest environment to the"
+ " registrar TheRegistrar by user user@registrarId.com:\n"
+ "\n"
+ "allowedTlds: null -> [app, dev]\n")
+ "allowedTlds: null -> [app, dev]\n"
+ "lastPocVerificationDate: 1970-01-01T00:00:00.000Z ->"
+ " 2024-12-12T00:00:00.000Z\n")
.setRecipients(ImmutableList.of(new InternetAddress("notification@test.example")))
.build());
}
@@ -18,6 +18,7 @@ import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.registrar.RegistrarPoc.Type.ABUSE;
import static google.registry.model.registrar.RegistrarPoc.Type.ADMIN;
import static google.registry.model.registrar.RegistrarPoc.Type.MARKETING;
import static google.registry.model.registrar.RegistrarPoc.Type.TECH;
import static google.registry.testing.DatabaseHelper.createAdminUser;
import static google.registry.testing.DatabaseHelper.insertInDb;
@@ -70,8 +71,9 @@ class ContactActionTest {
private Registrar testRegistrar;
private ConsoleApiParams consoleApiParams;
private RegistrarPoc testRegistrarPoc1;
private RegistrarPoc testRegistrarPoc2;
private RegistrarPoc adminPoc;
private RegistrarPoc techPoc;
private RegistrarPoc marketingPoc;
private static final Gson GSON = RequestModule.provideGson();
@@ -82,7 +84,7 @@ class ContactActionTest {
@BeforeEach
void beforeEach() {
testRegistrar = saveRegistrar("registrarId");
testRegistrarPoc1 =
adminPoc =
new RegistrarPoc.Builder()
.setRegistrar(testRegistrar)
.setName("Test Registrar 1")
@@ -94,19 +96,32 @@ class ContactActionTest {
.setVisibleInWhoisAsTech(false)
.setVisibleInDomainWhoisAsAbuse(false)
.build();
testRegistrarPoc2 =
testRegistrarPoc1
techPoc =
adminPoc
.asBuilder()
.setName("Test Registrar 2")
.setTypes(ImmutableSet.of(TECH))
.setVisibleInWhoisAsTech(true)
.setVisibleInWhoisAsAdmin(false)
.setEmailAddress("test.registrar2@example.com")
.setPhoneNumber("+1.1234567890")
.setFaxNumber("+1.1234567891")
.build();
marketingPoc =
adminPoc
.asBuilder()
.setName("Test Registrar 3")
.setTypes(ImmutableSet.of(MARKETING))
.setVisibleInWhoisAsAdmin(false)
.setEmailAddress("test.registrar3@example.com")
.setPhoneNumber("+1.1238675309")
.setFaxNumber("+1.1238675309")
.build();
}
@Test
void testSuccess_getContactInfo() throws IOException {
insertInDb(testRegistrarPoc1);
insertInDb(adminPoc);
ContactAction action =
createAction(
Action.Method.GET,
@@ -120,13 +135,13 @@ class ContactActionTest {
@Test
void testSuccess_noOp() throws IOException {
insertInDb(testRegistrarPoc1);
insertInDb(adminPoc);
ContactAction action =
createAction(
Action.Method.POST,
AuthResult.createUser(createAdminUser("email@email.com")),
testRegistrar.getRegistrarId(),
testRegistrarPoc1);
adminPoc);
action.run();
assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_OK);
verify(consoleApiParams.sendEmailUtils().gmailClient, never()).sendEmail(any());
@@ -134,8 +149,8 @@ class ContactActionTest {
@Test
void testSuccess_onlyContactsWithNonEmptyType() throws IOException {
testRegistrarPoc1 = testRegistrarPoc1.asBuilder().setTypes(ImmutableSet.of()).build();
insertInDb(testRegistrarPoc1);
adminPoc = adminPoc.asBuilder().setTypes(ImmutableSet.of()).build();
insertInDb(adminPoc);
ContactAction action =
createAction(
Action.Method.GET,
@@ -148,14 +163,14 @@ class ContactActionTest {
@Test
void testSuccess_postCreateContactInfo() throws IOException {
insertInDb(testRegistrarPoc1);
insertInDb(adminPoc);
ContactAction action =
createAction(
Action.Method.POST,
AuthResult.createUser(createAdminUser("email@email.com")),
testRegistrar.getRegistrarId(),
testRegistrarPoc1,
testRegistrarPoc2);
adminPoc,
techPoc);
action.run();
assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_OK);
assertThat(
@@ -168,14 +183,14 @@ class ContactActionTest {
@Test
void testSuccess_postUpdateContactInfo() throws IOException {
insertInDb(testRegistrarPoc1.asBuilder().setEmailAddress("incorrect@email.com").build());
insertInDb(techPoc.asBuilder().setEmailAddress("incorrect@email.com").build());
ContactAction action =
createAction(
Action.Method.POST,
AuthResult.createUser(createAdminUser("email@email.com")),
testRegistrar.getRegistrarId(),
testRegistrarPoc1,
testRegistrarPoc2);
adminPoc,
techPoc);
action.run();
assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_OK);
HashMap<String, String> testResult = new HashMap<>();
@@ -197,8 +212,8 @@ class ContactActionTest {
Action.Method.POST,
AuthResult.createUser(createAdminUser("email@email.com")),
testRegistrar.getRegistrarId(),
testRegistrarPoc1,
testRegistrarPoc2.asBuilder().setEmailAddress("test.registrar1@example.com").build());
adminPoc,
techPoc.asBuilder().setEmailAddress("test.registrar1@example.com").build());
action.run();
assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_BAD_REQUEST);
assertThat(((FakeResponse) consoleApiParams.response()).getPayload())
@@ -213,13 +228,13 @@ class ContactActionTest {
@Test
void testFailure_postUpdateContactInfo_requiredContactRemoved() throws IOException {
insertInDb(testRegistrarPoc1);
insertInDb(adminPoc);
ContactAction action =
createAction(
Action.Method.POST,
AuthResult.createUser(createAdminUser("email@email.com")),
testRegistrar.getRegistrarId(),
testRegistrarPoc1.asBuilder().setTypes(ImmutableSet.of(ABUSE)).build());
adminPoc.asBuilder().setTypes(ImmutableSet.of(ABUSE)).build());
action.run();
assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_BAD_REQUEST);
assertThat(((FakeResponse) consoleApiParams.response()).getPayload())
@@ -228,20 +243,19 @@ class ContactActionTest {
loadAllOf(RegistrarPoc.class).stream()
.filter(r -> r.registrarId.equals(testRegistrar.getRegistrarId()))
.collect(toImmutableList()))
.containsExactly(testRegistrarPoc1);
.containsExactly(adminPoc);
}
@Test
void testFailure_postUpdateContactInfo_phoneNumberRemoved() throws IOException {
testRegistrarPoc1 =
testRegistrarPoc1.asBuilder().setTypes(ImmutableSet.of(ADMIN, TECH)).build();
insertInDb(testRegistrarPoc1);
adminPoc = adminPoc.asBuilder().setTypes(ImmutableSet.of(ADMIN, TECH)).build();
insertInDb(adminPoc);
ContactAction action =
createAction(
Action.Method.POST,
AuthResult.createUser(createAdminUser("email@email.com")),
testRegistrar.getRegistrarId(),
testRegistrarPoc1
adminPoc
.asBuilder()
.setPhoneNumber(null)
.setTypes(ImmutableSet.of(ADMIN, TECH))
@@ -254,7 +268,7 @@ class ContactActionTest {
loadAllOf(RegistrarPoc.class).stream()
.filter(r -> r.registrarId.equals(testRegistrar.getRegistrarId()))
.collect(toImmutableList()))
.containsExactly(testRegistrarPoc1);
.containsExactly(adminPoc);
}
@Test
@@ -264,11 +278,7 @@ class ContactActionTest {
Action.Method.POST,
AuthResult.createUser(createAdminUser("email@email.com")),
testRegistrar.getRegistrarId(),
testRegistrarPoc1
.asBuilder()
.setPhoneNumber(null)
.setVisibleInDomainWhoisAsAbuse(true)
.build());
adminPoc.asBuilder().setPhoneNumber(null).setVisibleInDomainWhoisAsAbuse(true).build());
action.run();
assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_BAD_REQUEST);
assertThat(((FakeResponse) consoleApiParams.response()).getPayload())
@@ -282,14 +292,14 @@ class ContactActionTest {
@Test
void testFailure_postUpdateContactInfo_whoisContactPhoneNumberRemoved() throws IOException {
testRegistrarPoc1 = testRegistrarPoc1.asBuilder().setVisibleInDomainWhoisAsAbuse(true).build();
insertInDb(testRegistrarPoc1);
adminPoc = adminPoc.asBuilder().setVisibleInDomainWhoisAsAbuse(true).build();
insertInDb(adminPoc);
ContactAction action =
createAction(
Action.Method.POST,
AuthResult.createUser(createAdminUser("email@email.com")),
testRegistrar.getRegistrarId(),
testRegistrarPoc1.asBuilder().setVisibleInDomainWhoisAsAbuse(false).build());
adminPoc.asBuilder().setVisibleInDomainWhoisAsAbuse(false).build());
action.run();
assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_BAD_REQUEST);
assertThat(((FakeResponse) consoleApiParams.response()).getPayload())
@@ -298,7 +308,7 @@ class ContactActionTest {
loadAllOf(RegistrarPoc.class).stream()
.filter(r -> r.registrarId.equals(testRegistrar.getRegistrarId()))
.collect(toImmutableList()))
.containsExactly(testRegistrarPoc1);
.containsExactly(adminPoc);
}
@Test
@@ -309,7 +319,7 @@ class ContactActionTest {
Action.Method.POST,
AuthResult.createUser(createAdminUser("email@email.com")),
testRegistrar.getRegistrarId(),
testRegistrarPoc1
adminPoc
.asBuilder()
.setAllowedToSetRegistryLockPassword(true)
.setRegistryLockEmailAddress("lock@example.com")
@@ -327,22 +337,19 @@ class ContactActionTest {
@Test
void testFailure_postUpdateContactInfo_cannotModifyRegistryLockEmail() throws IOException {
testRegistrarPoc1 =
testRegistrarPoc1
adminPoc =
adminPoc
.asBuilder()
.setRegistryLockEmailAddress("lock@example.com")
.setAllowedToSetRegistryLockPassword(true)
.build();
insertInDb(testRegistrarPoc1);
insertInDb(adminPoc);
ContactAction action =
createAction(
Action.Method.POST,
AuthResult.createUser(createAdminUser("email@email.com")),
testRegistrar.getRegistrarId(),
testRegistrarPoc1
.asBuilder()
.setRegistryLockEmailAddress("unlock@example.com")
.build());
adminPoc.asBuilder().setRegistryLockEmailAddress("unlock@example.com").build());
action.run();
assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_BAD_REQUEST);
assertThat(((FakeResponse) consoleApiParams.response()).getPayload())
@@ -351,25 +358,25 @@ class ContactActionTest {
loadAllOf(RegistrarPoc.class).stream()
.filter(r -> r.registrarId.equals(testRegistrar.getRegistrarId()))
.collect(toImmutableList()))
.containsExactly(testRegistrarPoc1);
.containsExactly(adminPoc);
}
@Test
void testFailure_postUpdateContactInfo_cannotSetIsAllowedToSetRegistryLockPassword()
throws IOException {
testRegistrarPoc1 =
testRegistrarPoc1
adminPoc =
adminPoc
.asBuilder()
.setRegistryLockEmailAddress("lock@example.com")
.setAllowedToSetRegistryLockPassword(false)
.build();
insertInDb(testRegistrarPoc1);
insertInDb(adminPoc);
ContactAction action =
createAction(
Action.Method.POST,
AuthResult.createUser(createAdminUser("email@email.com")),
testRegistrar.getRegistrarId(),
testRegistrarPoc1.asBuilder().setAllowedToSetRegistryLockPassword(true).build());
adminPoc.asBuilder().setAllowedToSetRegistryLockPassword(true).build());
action.run();
assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_BAD_REQUEST);
assertThat(((FakeResponse) consoleApiParams.response()).getPayload())
@@ -378,18 +385,18 @@ class ContactActionTest {
loadAllOf(RegistrarPoc.class).stream()
.filter(r -> r.registrarId.equals(testRegistrar.getRegistrarId()))
.collect(toImmutableList()))
.containsExactly(testRegistrarPoc1);
.containsExactly(adminPoc);
}
@Test
void testSuccess_sendsEmail() throws IOException, AddressException {
insertInDb(testRegistrarPoc1.asBuilder().setEmailAddress("incorrect@email.com").build());
insertInDb(techPoc.asBuilder().setEmailAddress("incorrect@email.com").build());
ContactAction action =
createAction(
Action.Method.POST,
AuthResult.createUser(createAdminUser("email@email.com")),
testRegistrar.getRegistrarId(),
testRegistrarPoc1);
techPoc);
action.run();
assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_OK);
verify(consoleApiParams.sendEmailUtils().gmailClient, times(1))
@@ -404,44 +411,42 @@ class ContactActionTest {
+ "\n"
+ "contacts:\n"
+ " ADDED:\n"
+ " {name=Test Registrar 1,"
+ " emailAddress=test.registrar1@example.com, registrarId=registrarId,"
+ " registryLockEmailAddress=null, phoneNumber=+1.9999999999,"
+ " faxNumber=+1.9999999991, types=[ADMIN],"
+ " visibleInWhoisAsAdmin=true, visibleInWhoisAsTech=false,"
+ " {name=Test Registrar 2,"
+ " emailAddress=test.registrar2@example.com, registrarId=registrarId,"
+ " registryLockEmailAddress=null, phoneNumber=+1.1234567890,"
+ " faxNumber=+1.1234567891, types=[TECH],"
+ " visibleInWhoisAsAdmin=false, visibleInWhoisAsTech=true,"
+ " visibleInDomainWhoisAsAbuse=false,"
+ " allowedToSetRegistryLockPassword=false}\n"
+ " REMOVED:\n"
+ " {name=Test Registrar 1, emailAddress=incorrect@email.com,"
+ " {name=Test Registrar 2, emailAddress=incorrect@email.com,"
+ " registrarId=registrarId, registryLockEmailAddress=null,"
+ " phoneNumber=+1.9999999999, faxNumber=+1.9999999991, types=[ADMIN],"
+ " visibleInWhoisAsAdmin=true,"
+ " visibleInWhoisAsTech=false, visibleInDomainWhoisAsAbuse=false,"
+ " phoneNumber=+1.1234567890, faxNumber=+1.1234567891, types=[TECH],"
+ " visibleInWhoisAsAdmin=false,"
+ " visibleInWhoisAsTech=true, visibleInDomainWhoisAsAbuse=false,"
+ " allowedToSetRegistryLockPassword=false}\n"
+ " FINAL CONTENTS:\n"
+ " {name=Test Registrar 1,"
+ " emailAddress=test.registrar1@example.com, registrarId=registrarId,"
+ " registryLockEmailAddress=null, phoneNumber=+1.9999999999,"
+ " faxNumber=+1.9999999991, types=[ADMIN],"
+ " visibleInWhoisAsAdmin=true, visibleInWhoisAsTech=false,"
+ " {name=Test Registrar 2,"
+ " emailAddress=test.registrar2@example.com, registrarId=registrarId,"
+ " registryLockEmailAddress=null, phoneNumber=+1.1234567890,"
+ " faxNumber=+1.1234567891, types=[TECH],"
+ " visibleInWhoisAsAdmin=false, visibleInWhoisAsTech=true,"
+ " visibleInDomainWhoisAsAbuse=false,"
+ " allowedToSetRegistryLockPassword=false}\n")
.setRecipients(
ImmutableList.of(
new InternetAddress("notification@test.example"),
new InternetAddress("incorrect@email.com")))
.setRecipients(ImmutableList.of(new InternetAddress("notification@test.example")))
.build());
}
@Test
void testSuccess_postDeleteContactInfo() throws IOException {
insertInDb(testRegistrarPoc1);
insertInDb(adminPoc, techPoc, marketingPoc);
ContactAction action =
createAction(
Action.Method.POST,
AuthResult.createUser(createAdminUser("email@email.com")),
testRegistrar.getRegistrarId(),
testRegistrarPoc2);
adminPoc,
techPoc);
action.run();
assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_OK);
assertThat(
@@ -449,12 +454,12 @@ class ContactActionTest {
.filter(r -> r.registrarId.equals(testRegistrar.getRegistrarId()))
.map(r -> r.getName())
.collect(toImmutableList()))
.containsExactly("Test Registrar 2");
.containsExactly("Test Registrar 1", "Test Registrar 2");
}
@Test
void testFailure_postDeleteContactInfo_missingPermission() throws IOException {
insertInDb(testRegistrarPoc1);
insertInDb(adminPoc);
ContactAction action =
createAction(
Action.Method.POST,
@@ -469,11 +474,27 @@ class ContactActionTest {
.build())
.build()),
testRegistrar.getRegistrarId(),
testRegistrarPoc2);
techPoc);
action.run();
assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_FORBIDDEN);
}
@Test
void testFailure_changesAdminEmail() throws Exception {
insertInDb(adminPoc.asBuilder().setEmailAddress("oldemail@example.com").build());
ContactAction action =
createAction(
Action.Method.POST,
AuthResult.createUser(createAdminUser("email@email.com")),
testRegistrar.getRegistrarId(),
adminPoc);
action.run();
FakeResponse fakeResponse = (FakeResponse) consoleApiParams.response();
assertThat(fakeResponse.getStatus()).isEqualTo(400);
assertThat(fakeResponse.getPayload())
.isEqualTo("Cannot remove or change the email address of primary contacts");
}
private ContactAction createAction(
Action.Method method, AuthResult authResult, String registrarId, RegistrarPoc... contacts)
throws IOException {
@@ -16,13 +16,18 @@ package google.registry.webdriver;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.server.Fixture.BASIC;
import static google.registry.testing.DatabaseHelper.persistResource;
import com.google.common.collect.ImmutableMap;
import google.registry.model.console.GlobalRole;
import google.registry.model.console.RegistrarRole;
import google.registry.model.registrar.Registrar;
import google.registry.server.RegistryTestServer;
import java.util.List;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Timeout;
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.junitpioneer.jupiter.RetryingTest;
@@ -50,7 +55,15 @@ import org.openqa.selenium.WebElement;
*/
// The Selenium image only supports amd64 architecture.
@EnabledIfSystemProperty(named = "os.arch", matches = "amd64")
public class ConsoleScreenshotTest extends WebDriverTestCase {
@Timeout(120)
public class ConsoleScreenshotTest {
@RegisterExtension
static final DockerWebDriverExtension webDriverProvider = new DockerWebDriverExtension();
@RegisterExtension
final WebDriverPlusScreenDifferExtension driver =
new WebDriverPlusScreenDifferExtension(webDriverProvider::getWebDriver);
@RegisterExtension
final TestServerExtension server =
@@ -65,6 +78,10 @@ public class ConsoleScreenshotTest extends WebDriverTestCase {
@BeforeEach
void beforeEach() throws Exception {
server.setRegistrarRoles(ImmutableMap.of("TheRegistrar", RegistrarRole.ACCOUNT_MANAGER));
Registrar registrar = Registrar.loadByRegistrarId("TheRegistrar").get();
registrar =
registrar.asBuilder().setLastPocVerificationDate(DateTime.now(DateTimeZone.UTC)).build();
persistResource(registrar);
loadHomePage();
}
@@ -1,30 +0,0 @@
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.webdriver;
import org.junit.jupiter.api.Timeout;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Base class for tests that needs a {@link WebDriverPlusScreenDifferExtension}. */
@Timeout(120)
class WebDriverTestCase {
@RegisterExtension
static final DockerWebDriverExtension webDriverProvider = new DockerWebDriverExtension();
@RegisterExtension
final WebDriverPlusScreenDifferExtension driver =
new WebDriverPlusScreenDifferExtension(webDriverProvider::getWebDriver);
}
@@ -6,6 +6,7 @@
<domain:name>example1.tld</domain:name>
<domain:name>example2.example</domain:name>
<domain:name>reserved.tld</domain:name>
<domain:name>rich.example</domain:name>
</domain:check>
</check>
<extension>
@@ -34,6 +35,12 @@
<fee:command>create</fee:command>
<fee:period unit="y">1</fee:period>
</fee:domain>
<fee:domain>
<fee:name>rich.example</fee:name>
<fee:currency>USD</fee:currency>
<fee:command>create</fee:command>
<fee:period unit="y">1</fee:period>
</fee:domain>
<fee:domain>
<fee:name>example1.tld</fee:name>
<fee:currency>USD</fee:currency>
@@ -52,6 +59,12 @@
<fee:command>renew</fee:command>
<fee:period unit="y">1</fee:period>
</fee:domain>
<fee:domain>
<fee:name>rich.example</fee:name>
<fee:currency>USD</fee:currency>
<fee:command>renew</fee:command>
<fee:period unit="y">1</fee:period>
</fee:domain>
</fee:check>
</extension>
<clTRID>ABC-12345</clTRID>
@@ -17,6 +17,10 @@
<domain:name avail="false">reserved.tld</domain:name>
<domain:reason>Alloc token invalid for domain</domain:reason>
</domain:cd>
<domain:cd>
<domain:name avail="false">rich.example</domain:name>
<domain:reason>Alloc token invalid for domain</domain:reason>
</domain:cd>
</domain:chkData>
</resData>
<extension>
@@ -42,6 +46,13 @@
<fee:period unit="y">1</fee:period>
<fee:class>token-not-supported</fee:class>
</fee:cd>
<fee:cd>
<fee:name>rich.example</fee:name>
<fee:currency>USD</fee:currency>
<fee:command>create</fee:command>
<fee:period unit="y">1</fee:period>
<fee:class>token-not-supported</fee:class>
</fee:cd>
<fee:cd>
<fee:name>example1.tld</fee:name>
<fee:currency>USD</fee:currency>
@@ -64,6 +75,13 @@
<fee:period unit="y">1</fee:period>
<fee:class>token-not-supported</fee:class>
</fee:cd>
<fee:cd>
<fee:name>rich.example</fee:name>
<fee:currency>USD</fee:currency>
<fee:command>renew</fee:command>
<fee:period unit="y">1</fee:period>
<fee:class>token-not-supported</fee:class>
</fee:cd>
</fee:chkData>
</extension>
<trID>
@@ -16,6 +16,9 @@
<domain:name avail="false">reserved.tld</domain:name>
<domain:reason>Reserved</domain:reason>
</domain:cd>
<domain:cd>
<domain:name avail="true">rich.example</domain:name>
</domain:cd>
</domain:chkData>
</resData>
<extension>
@@ -41,26 +44,42 @@
<fee:period unit="y">1</fee:period>
<fee:class>reserved</fee:class>
</fee:cd>
<fee:cd>
<fee:name>rich.example</fee:name>
<fee:currency>USD</fee:currency>
<fee:command>create</fee:command>
<fee:period unit="y">1</fee:period>
<fee:fee description="create">100.00</fee:fee>
<fee:class>premium</fee:class>
</fee:cd>
<fee:cd>
<fee:name>example1.tld</fee:name>
<fee:currency>USD</fee:currency>
<fee:command>renew</fee:command>
<fee:period unit="y">1</fee:period>
<fee:class>token-not-supported</fee:class>
<fee:fee description="renew">11.00</fee:fee>
</fee:cd>
<fee:cd>
<fee:name>example2.example</fee:name>
<fee:currency>USD</fee:currency>
<fee:command>renew</fee:command>
<fee:period unit="y">1</fee:period>
<fee:class>token-not-supported</fee:class>
<fee:fee description="renew">11.00</fee:fee>
</fee:cd>
<fee:cd>
<fee:name>reserved.tld</fee:name>
<fee:currency>USD</fee:currency>
<fee:command>renew</fee:command>
<fee:period unit="y">1</fee:period>
<fee:class>token-not-supported</fee:class>
<fee:fee description="renew">11.00</fee:fee>
</fee:cd>
<fee:cd>
<fee:name>rich.example</fee:name>
<fee:currency>USD</fee:currency>
<fee:command>renew</fee:command>
<fee:period unit="y">1</fee:period>
<fee:fee description="renew">100.00</fee:fee>
<fee:class>premium</fee:class>
</fee:cd>
</fee:chkData>
</extension>
@@ -16,7 +16,7 @@
</domain:cd>
<domain:cd>
<domain:name avail="false">reserved.tld</domain:name>
<domain:reason>Reserved</domain:reason>
<domain:reason>Alloc token invalid for domain</domain:reason>
</domain:cd>
<domain:cd>
<domain:name avail="true">specificuse.tld</domain:name>
@@ -17,14 +17,14 @@
<fee:currency>USD</fee:currency>
<fee:command>create</fee:command>
<fee:period unit="y">1</fee:period>
<fee:fee description="create">13.00</fee:fee>
<fee:fee description="create">11.70</fee:fee>
</fee:cd>
<fee:cd xmlns:fee="urn:ietf:params:xml:ns:fee-0.6">
<fee:name>example1.tld</fee:name>
<fee:currency>USD</fee:currency>
<fee:command>renew</fee:command>
<fee:period unit="y">1</fee:period>
<fee:class>token-not-supported</fee:class>
<fee:fee description="renew">11.00</fee:fee>
</fee:cd>
<fee:cd xmlns:fee="urn:ietf:params:xml:ns:fee-0.6">
<fee:name>example1.tld</fee:name>
@@ -38,14 +38,14 @@
<fee:currency>USD</fee:currency>
<fee:command>restore</fee:command>
<fee:period unit="y">1</fee:period>
<fee:class>token-not-supported</fee:class>
<fee:fee description="restore">17.00</fee:fee>
</fee:cd>
<fee:cd xmlns:fee="urn:ietf:params:xml:ns:fee-0.6">
<fee:name>example1.tld</fee:name>
<fee:currency>USD</fee:currency>
<fee:command>update</fee:command>
<fee:period unit="y">1</fee:period>
<fee:class>token-not-supported</fee:class>
<fee:fee description="update">0.00</fee:fee>
</fee:cd>
</fee:chkData>
</extension>
@@ -19,7 +19,7 @@
</fee:object>
<fee:command name="create">
<fee:period unit="y">1</fee:period>
<fee:fee description="create">13.00</fee:fee>
<fee:fee description="create">11.70</fee:fee>
</fee:command>
</fee:cd>
<fee:cd>
@@ -28,7 +28,7 @@
</fee:object>
<fee:command name="renew">
<fee:period unit="y">1</fee:period>
<fee:class>token-not-supported</fee:class>
<fee:fee description="renew">11.00</fee:fee>
</fee:command>
</fee:cd>
<fee:cd>
@@ -46,7 +46,7 @@
</fee:object>
<fee:command name="restore">
<fee:period unit="y">1</fee:period>
<fee:class>token-not-supported</fee:class>
<fee:fee description="restore">17.00</fee:fee>
</fee:command>
</fee:cd>
<fee:cd>
@@ -55,7 +55,7 @@
</fee:object>
<fee:command name="update">
<fee:period unit="y">1</fee:period>
<fee:class>token-not-supported</fee:class>
<fee:fee description="update">0.00</fee:fee>
</fee:command>
</fee:cd>
</fee:chkData>
@@ -0,0 +1,11 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<command>
<check>
<domain:check
xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
<domain:name>%DOMAIN%</domain:name>
</domain:check>
</check>
<clTRID>ABC-12345</clTRID>
</command>
</epp>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<command>
<create>
<domain:create
xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
<domain:name>example.tld</domain:name>
<domain:period unit="y">1</domain:period>
<domain:registrant>crr-admin</domain:registrant>
<domain:contact type="admin">crr-admin</domain:contact>
<domain:contact type="tech">crr-tech</domain:contact>
<domain:authInfo>
<domain:pw>abcdefghijklmnop</domain:pw>
</domain:authInfo>
</domain:create>
</create>
<clTRID>RegistryTool</clTRID>
</command>
</epp>
@@ -6,9 +6,6 @@
xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
<domain:name>example.tld</domain:name>
<domain:period unit="y">1</domain:period>
<domain:registrant>crr-admin</domain:registrant>
<domain:contact type="admin">crr-admin</domain:contact>
<domain:contact type="tech">crr-tech</domain:contact>
<domain:authInfo>
<domain:pw>abcdefghijklmnop</domain:pw>
</domain:authInfo>
@@ -261,26 +261,26 @@ td.section {
</tr>
<tr>
<td class="property_name">generated on</td>
<td class="property_value">2025-04-18 20:02:20</td>
<td class="property_value">2025-05-15 19:22:21</td>
</tr>
<tr>
<td class="property_name">last flyway file</td>
<td id="lastFlywayFile" class="property_value">V192__add_last_poc_verification_date.sql</td>
<td id="lastFlywayFile" class="property_value">V194__password_reset_request_registrar.sql</td>
</tr>
</tbody>
</table>
<p>&nbsp;</p>
<p>&nbsp;</p>
<svg viewBox="0.00 0.00 4903.00 3666.00" 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 3662)">
<svg viewBox="0.00 0.00 4903.00 3732.00" 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 3728)">
<title>
SchemaCrawler_Diagram
</title>
<polygon fill="white" stroke="transparent" points="-4,4 -4,-3662 4899,-3662 4899,4 -4,4" />
<polygon fill="white" stroke="transparent" points="-4,4 -4,-3728 4899,-3728 4899,4 -4,4" />
<text text-anchor="start" x="4655" y="-29.8" font-family="Helvetica,sans-Serif" font-size="14.00">generated by</text>
<text text-anchor="start" x="4738" y="-29.8" font-family="Helvetica,sans-Serif" font-size="14.00">SchemaCrawler 16.25.2</text>
<text text-anchor="start" x="4654" y="-10.8" font-family="Helvetica,sans-Serif" font-size="14.00">generated on</text>
<text text-anchor="start" x="4738" y="-10.8" font-family="Helvetica,sans-Serif" font-size="14.00">2025-04-18 20:02:20</text>
<text text-anchor="start" x="4738" y="-10.8" font-family="Helvetica,sans-Serif" font-size="14.00">2025-05-15 19:22:21</text>
<polygon fill="none" stroke="#888888" points="4651,-4 4651,-44 4887,-44 4887,-4 4651,-4" /> <!-- allocationtoken_a08ccbef -->
<g id="node1" class="node">
<title>
@@ -404,7 +404,7 @@ td.section {
<polyline fill="none" stroke="black" points="769.5,-845.99 774.5,-845.99 " />
<text text-anchor="start" x="790" y="-849.8" font-family="Helvetica,sans-Serif" font-size="14.00">fk_billing_event_cancellation_matching_billing_recurrence_id</text>
</g> <!-- registrar_6e1503e3 -->
<g id="node35" class="node">
<g id="node36" class="node">
<title>
registrar_6e1503e3
</title>
@@ -765,7 +765,7 @@ td.section {
<polyline fill="none" stroke="black" points="209.49,-1035.98 213.47,-1032.96 " />
<text text-anchor="start" x="1269" y="-620.8" font-family="Helvetica,sans-Serif" font-size="14.00">fk_domain_transfer_losing_registrar_id</text>
</g> <!-- tld_f1fa57e2 -->
<g id="node46" class="node">
<g id="node47" class="node">
<title>
tld_f1fa57e2
</title>
@@ -1153,7 +1153,7 @@ td.section {
<text text-anchor="start" x="4742.5" y="-1956.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<polygon fill="none" stroke="#888888" points="4532,-1950.5 4532,-2047.5 4867,-2047.5 4867,-1950.5 4532,-1950.5" />
</g> <!-- user_f2216f01 -->
<g id="node48" class="node">
<g id="node49" class="node">
<title>
user_f2216f01
</title>
@@ -1900,74 +1900,87 @@ td.section {
<text text-anchor="start" x="4719.5" y="-2648.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4739.5" y="-2648.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<polygon fill="none" stroke="#888888" points="4551,-2642 4551,-2720 4848,-2720 4848,-2642 4551,-2642" />
</g> <!-- premiumentry_b0060b91 -->
</g> <!-- passwordresetrequest_8484e7b1 -->
<g id="node32" class="node">
<title>
passwordresetrequest_8484e7b1
</title>
<polygon fill="#e9c2f2" stroke="transparent" points="4554.5,-2766 4554.5,-2785 4770.5,-2785 4770.5,-2766 4554.5,-2766" />
<text text-anchor="start" x="4556.5" y="-2772.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."PasswordResetRequest"</text>
<polygon fill="#e9c2f2" stroke="transparent" points="4770.5,-2766 4770.5,-2785 4844.5,-2785 4844.5,-2766 4770.5,-2766" />
<text text-anchor="start" x="4805.5" y="-2771.8" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="4556.5" y="-2753.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">verification_code</text>
<text text-anchor="start" x="4718.5" y="-2752.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4772.5" y="-2752.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<polygon fill="none" stroke="#888888" points="4553.5,-2746 4553.5,-2786 4845.5,-2786 4845.5,-2746 4553.5,-2746" />
</g> <!-- premiumentry_b0060b91 -->
<g id="node33" class="node">
<title>
premiumentry_b0060b91
</title>
<polygon fill="#e9c2f2" stroke="transparent" points="4585.5,-2785 4585.5,-2804 4740.5,-2804 4740.5,-2785 4585.5,-2785" />
<text text-anchor="start" x="4587.5" y="-2791.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."PremiumEntry"</text>
<polygon fill="#e9c2f2" stroke="transparent" points="4740.5,-2785 4740.5,-2804 4814.5,-2804 4814.5,-2785 4740.5,-2785" />
<text text-anchor="start" x="4775.5" y="-2790.8" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="4587.5" y="-2772.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">revision_id</text>
<text text-anchor="start" x="4706.5" y="-2771.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4742.5" y="-2771.8" font-family="Helvetica,sans-Serif" font-size="14.00">int8 not null</text>
<text text-anchor="start" x="4587.5" y="-2753.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">domain_label</text>
<text text-anchor="start" x="4706.5" y="-2752.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4742.5" y="-2752.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<polygon fill="none" stroke="#888888" points="4584,-2746.5 4584,-2805.5 4815,-2805.5 4815,-2746.5 4584,-2746.5" />
<polygon fill="#e9c2f2" stroke="transparent" points="4585.5,-2851 4585.5,-2870 4740.5,-2870 4740.5,-2851 4585.5,-2851" />
<text text-anchor="start" x="4587.5" y="-2857.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."PremiumEntry"</text>
<polygon fill="#e9c2f2" stroke="transparent" points="4740.5,-2851 4740.5,-2870 4814.5,-2870 4814.5,-2851 4740.5,-2851" />
<text text-anchor="start" x="4775.5" y="-2856.8" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="4587.5" y="-2838.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">revision_id</text>
<text text-anchor="start" x="4706.5" y="-2837.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4742.5" y="-2837.8" font-family="Helvetica,sans-Serif" font-size="14.00">int8 not null</text>
<text text-anchor="start" x="4587.5" y="-2819.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">domain_label</text>
<text text-anchor="start" x="4706.5" y="-2818.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4742.5" y="-2818.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<polygon fill="none" stroke="#888888" points="4584,-2812.5 4584,-2871.5 4815,-2871.5 4815,-2812.5 4584,-2812.5" />
</g> <!-- premiumlist_7c3ea68b -->
<g id="node33" class="node">
<g id="node34" class="node">
<title>
premiumlist_7c3ea68b
</title>
<polygon fill="#e9c2f2" stroke="transparent" points="3921,-2784 3921,-2803 4066,-2803 4066,-2784 3921,-2784" />
<text text-anchor="start" x="3923" y="-2790.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."PremiumList"</text>
<polygon fill="#e9c2f2" stroke="transparent" points="4066,-2784 4066,-2803 4176,-2803 4176,-2784 4066,-2784" />
<text text-anchor="start" x="4137" y="-2789.8" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="3923" y="-2771.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">revision_id</text>
<text text-anchor="start" x="4029" y="-2770.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4068" y="-2770.8" font-family="Helvetica,sans-Serif" font-size="14.00">bigserial not null</text>
<text text-anchor="start" x="4029" y="-2751.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4068" y="-2751.8" font-family="Helvetica,sans-Serif" font-size="14.00">auto-incremented</text>
<text text-anchor="start" x="3923" y="-2732.8" font-family="Helvetica,sans-Serif" font-size="14.00">name</text>
<text text-anchor="start" x="4029" y="-2732.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4068" y="-2732.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<polygon fill="none" stroke="#888888" points="3919.5,-2726 3919.5,-2804 4176.5,-2804 4176.5,-2726 3919.5,-2726" />
<polygon fill="#e9c2f2" stroke="transparent" points="3921,-2850 3921,-2869 4066,-2869 4066,-2850 3921,-2850" />
<text text-anchor="start" x="3923" y="-2856.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."PremiumList"</text>
<polygon fill="#e9c2f2" stroke="transparent" points="4066,-2850 4066,-2869 4176,-2869 4176,-2850 4066,-2850" />
<text text-anchor="start" x="4137" y="-2855.8" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="3923" y="-2837.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">revision_id</text>
<text text-anchor="start" x="4029" y="-2836.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4068" y="-2836.8" font-family="Helvetica,sans-Serif" font-size="14.00">bigserial not null</text>
<text text-anchor="start" x="4029" y="-2817.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4068" y="-2817.8" font-family="Helvetica,sans-Serif" font-size="14.00">auto-incremented</text>
<text text-anchor="start" x="3923" y="-2798.8" font-family="Helvetica,sans-Serif" font-size="14.00">name</text>
<text text-anchor="start" x="4029" y="-2798.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4068" y="-2798.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<polygon fill="none" stroke="#888888" points="3919.5,-2792 3919.5,-2870 4176.5,-2870 4176.5,-2792 3919.5,-2792" />
</g> <!-- premiumentry_b0060b91&#45;&gt;premiumlist_7c3ea68b -->
<g id="edge36" class="edge">
<title>
premiumentry_b0060b91:w-&gt;premiumlist_7c3ea68b:e
</title>
<path fill="none" stroke="black" d="M4566.39,-2775C4403.38,-2775 4353.85,-2775 4187.11,-2775" />
<polygon fill="black" stroke="black" points="4574.5,-2775 4584.5,-2779.5 4579.5,-2775 4584.5,-2775 4584.5,-2775 4584.5,-2775 4579.5,-2775 4584.5,-2770.5 4574.5,-2775 4574.5,-2775" />
<ellipse fill="none" stroke="black" cx="4570.5" cy="-2775" rx="4" ry="4" />
<polygon fill="black" stroke="black" points="4178,-2780 4178,-2770 4180,-2770 4180,-2780 4178,-2780" />
<polyline fill="none" stroke="black" points="4177,-2775 4182,-2775 " />
<polygon fill="black" stroke="black" points="4183,-2780 4183,-2770 4185,-2770 4185,-2780 4183,-2780" />
<polyline fill="none" stroke="black" points="4182,-2775 4187,-2775 " />
<text text-anchor="start" x="4273.5" y="-2778.8" font-family="Helvetica,sans-Serif" font-size="14.00">fko0gw90lpo1tuee56l0nb6y6g5</text>
<path fill="none" stroke="black" d="M4566.39,-2841C4403.38,-2841 4353.85,-2841 4187.11,-2841" />
<polygon fill="black" stroke="black" points="4574.5,-2841 4584.5,-2845.5 4579.5,-2841 4584.5,-2841 4584.5,-2841 4584.5,-2841 4579.5,-2841 4584.5,-2836.5 4574.5,-2841 4574.5,-2841" />
<ellipse fill="none" stroke="black" cx="4570.5" cy="-2841" rx="4" ry="4" />
<polygon fill="black" stroke="black" points="4178,-2846 4178,-2836 4180,-2836 4180,-2846 4178,-2846" />
<polyline fill="none" stroke="black" points="4177,-2841 4182,-2841 " />
<polygon fill="black" stroke="black" points="4183,-2846 4183,-2836 4185,-2836 4185,-2846 4183,-2846" />
<polyline fill="none" stroke="black" points="4182,-2841 4187,-2841 " />
<text text-anchor="start" x="4273.5" y="-2844.8" font-family="Helvetica,sans-Serif" font-size="14.00">fko0gw90lpo1tuee56l0nb6y6g5</text>
</g> <!-- rderevision_83396864 -->
<g id="node34" class="node">
<g id="node35" class="node">
<title>
rderevision_83396864
</title>
<polygon fill="#e9c2f2" stroke="transparent" points="4589.5,-2890 4589.5,-2909 4732.5,-2909 4732.5,-2890 4589.5,-2890" />
<text text-anchor="start" x="4591.5" y="-2896.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."RdeRevision"</text>
<polygon fill="#e9c2f2" stroke="transparent" points="4732.5,-2890 4732.5,-2909 4810.5,-2909 4810.5,-2890 4732.5,-2890" />
<text text-anchor="start" x="4771.5" y="-2895.8" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="4591.5" y="-2877.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">tld</text>
<text text-anchor="start" x="4681.5" y="-2876.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4734.5" y="-2876.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<text text-anchor="start" x="4591.5" y="-2858.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">mode</text>
<text text-anchor="start" x="4681.5" y="-2857.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4734.5" y="-2857.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<text text-anchor="start" x="4591.5" y="-2839.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">"date"</text>
<text text-anchor="start" x="4681.5" y="-2838.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4734.5" y="-2838.8" font-family="Helvetica,sans-Serif" font-size="14.00">date not null</text>
<polygon fill="none" stroke="#888888" points="4588,-2832 4588,-2910 4811,-2910 4811,-2832 4588,-2832" />
<polygon fill="#e9c2f2" stroke="transparent" points="4589.5,-2956 4589.5,-2975 4732.5,-2975 4732.5,-2956 4589.5,-2956" />
<text text-anchor="start" x="4591.5" y="-2962.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."RdeRevision"</text>
<polygon fill="#e9c2f2" stroke="transparent" points="4732.5,-2956 4732.5,-2975 4810.5,-2975 4810.5,-2956 4732.5,-2956" />
<text text-anchor="start" x="4771.5" y="-2961.8" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="4591.5" y="-2943.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">tld</text>
<text text-anchor="start" x="4681.5" y="-2942.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4734.5" y="-2942.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<text text-anchor="start" x="4591.5" y="-2924.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">mode</text>
<text text-anchor="start" x="4681.5" y="-2923.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4734.5" y="-2923.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<text text-anchor="start" x="4591.5" y="-2905.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">"date"</text>
<text text-anchor="start" x="4681.5" y="-2904.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4734.5" y="-2904.8" font-family="Helvetica,sans-Serif" font-size="14.00">date not null</text>
<polygon fill="none" stroke="#888888" points="4588,-2898 4588,-2976 4811,-2976 4811,-2898 4588,-2898" />
</g> <!-- registrarpoc_ab47054d -->
<g id="node36" class="node">
<g id="node37" class="node">
<title>
registrarpoc_ab47054d
</title>
@@ -1996,7 +2009,7 @@ td.section {
<polyline fill="none" stroke="black" points="208.93,-1035.36 212.36,-1031.72 " />
<text text-anchor="start" x="246" y="-222.8" font-family="Helvetica,sans-Serif" font-size="14.00">fk_registrar_poc_registrar_id</text>
</g> <!-- registrarupdatehistory_8a38bed4 -->
<g id="node37" class="node">
<g id="node38" class="node">
<title>
registrarupdatehistory_8a38bed4
</title>
@@ -2028,7 +2041,7 @@ td.section {
<polyline fill="none" stroke="black" points="209,-1035.43 212.49,-1031.85 " />
<text text-anchor="start" x="231" y="-142.8" font-family="Helvetica,sans-Serif" font-size="14.00">fkregistrarupdatehistoryregistrarid</text>
</g> <!-- registrarpocupdatehistory_31e5d9aa -->
<g id="node38" class="node">
<g id="node39" class="node">
<title>
registrarpocupdatehistory_31e5d9aa
</title>
@@ -2076,205 +2089,205 @@ td.section {
<polyline fill="none" stroke="black" points="727.49,-165.72 732.48,-165.44 " />
<text text-anchor="start" x="851.5" y="-151.8" font-family="Helvetica,sans-Serif" font-size="14.00">fkregistrarpocupdatehistoryemailaddress</text>
</g> <!-- registrylock_ac88663e -->
<g id="node39" class="node">
<g id="node40" class="node">
<title>
registrylock_ac88663e
</title>
<polygon fill="#e9c2f2" stroke="transparent" points="4571.5,-3051 4571.5,-3070 4718.5,-3070 4718.5,-3051 4571.5,-3051" />
<text text-anchor="start" x="4573.5" y="-3057.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."RegistryLock"</text>
<polygon fill="#e9c2f2" stroke="transparent" points="4718.5,-3051 4718.5,-3070 4828.5,-3070 4828.5,-3051 4718.5,-3051" />
<text text-anchor="start" x="4789.5" y="-3056.8" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="4573.5" y="-3038.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">revision_id</text>
<text text-anchor="start" x="4699.5" y="-3037.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4720.5" y="-3037.8" font-family="Helvetica,sans-Serif" font-size="14.00">bigserial not null</text>
<text text-anchor="start" x="4699.5" y="-3018.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4720.5" y="-3018.8" font-family="Helvetica,sans-Serif" font-size="14.00">auto-incremented</text>
<text text-anchor="start" x="4573.5" y="-2999.8" font-family="Helvetica,sans-Serif" font-size="14.00">registrar_id</text>
<text text-anchor="start" x="4699.5" y="-2999.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4720.5" y="-2999.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<text text-anchor="start" x="4573.5" y="-2980.8" font-family="Helvetica,sans-Serif" font-size="14.00">repo_id</text>
<text text-anchor="start" x="4699.5" y="-2980.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4720.5" y="-2980.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<text text-anchor="start" x="4573.5" y="-2961.8" font-family="Helvetica,sans-Serif" font-size="14.00">verification_code</text>
<text text-anchor="start" x="4699.5" y="-2961.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4720.5" y="-2961.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<text text-anchor="start" x="4573.5" y="-2942.8" font-family="Helvetica,sans-Serif" font-size="14.00">relock_revision_id</text>
<text text-anchor="start" x="4699.5" y="-2942.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4720.5" y="-2942.8" font-family="Helvetica,sans-Serif" font-size="14.00">int8</text>
<polygon fill="none" stroke="#888888" points="4570,-2936.5 4570,-3071.5 4829,-3071.5 4829,-2936.5 4570,-2936.5" />
<polygon fill="#e9c2f2" stroke="transparent" points="4571.5,-3117 4571.5,-3136 4718.5,-3136 4718.5,-3117 4571.5,-3117" />
<text text-anchor="start" x="4573.5" y="-3123.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."RegistryLock"</text>
<polygon fill="#e9c2f2" stroke="transparent" points="4718.5,-3117 4718.5,-3136 4828.5,-3136 4828.5,-3117 4718.5,-3117" />
<text text-anchor="start" x="4789.5" y="-3122.8" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="4573.5" y="-3104.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">revision_id</text>
<text text-anchor="start" x="4699.5" y="-3103.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4720.5" y="-3103.8" font-family="Helvetica,sans-Serif" font-size="14.00">bigserial not null</text>
<text text-anchor="start" x="4699.5" y="-3084.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4720.5" y="-3084.8" font-family="Helvetica,sans-Serif" font-size="14.00">auto-incremented</text>
<text text-anchor="start" x="4573.5" y="-3065.8" font-family="Helvetica,sans-Serif" font-size="14.00">registrar_id</text>
<text text-anchor="start" x="4699.5" y="-3065.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4720.5" y="-3065.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<text text-anchor="start" x="4573.5" y="-3046.8" font-family="Helvetica,sans-Serif" font-size="14.00">repo_id</text>
<text text-anchor="start" x="4699.5" y="-3046.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4720.5" y="-3046.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<text text-anchor="start" x="4573.5" y="-3027.8" font-family="Helvetica,sans-Serif" font-size="14.00">verification_code</text>
<text text-anchor="start" x="4699.5" y="-3027.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4720.5" y="-3027.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<text text-anchor="start" x="4573.5" y="-3008.8" font-family="Helvetica,sans-Serif" font-size="14.00">relock_revision_id</text>
<text text-anchor="start" x="4699.5" y="-3008.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4720.5" y="-3008.8" font-family="Helvetica,sans-Serif" font-size="14.00">int8</text>
<polygon fill="none" stroke="#888888" points="4570,-3002.5 4570,-3137.5 4829,-3137.5 4829,-3002.5 4570,-3002.5" />
</g> <!-- registrylock_ac88663e&#45;&gt;registrylock_ac88663e -->
<g id="edge64" class="edge">
<title>
registrylock_ac88663e:w-&gt;registrylock_ac88663e:e
</title>
<path fill="none" stroke="black" d="M4554.85,-2953.77C4489.6,-2992.23 4503.78,-3093.5 4700,-3093.5 4902.83,-3093.5 4911.14,-3073.57 4838.29,-3045.63" />
<polygon fill="black" stroke="black" points="4562.44,-2950.23 4573.4,-2950.08 4566.97,-2948.11 4571.5,-2946 4571.5,-2946 4571.5,-2946 4566.97,-2948.11 4569.6,-2941.92 4562.44,-2950.23 4562.44,-2950.23" />
<ellipse fill="none" stroke="black" cx="4558.81" cy="-2951.92" rx="4" ry="4" />
<polygon fill="black" stroke="black" points="4827.7,-3047.04 4831.18,-3037.66 4833.05,-3038.36 4829.57,-3047.73 4827.7,-3047.04" />
<polyline fill="none" stroke="black" points="4828.5,-3042 4833.19,-3043.74 " />
<polygon fill="black" stroke="black" points="4832.39,-3048.77 4835.86,-3039.4 4837.74,-3040.09 4834.26,-3049.47 4832.39,-3048.77" />
<polyline fill="none" stroke="black" points="4833.19,-3043.74 4837.88,-3045.48 " />
<text text-anchor="start" x="4618" y="-3097.3" font-family="Helvetica,sans-Serif" font-size="14.00">fk2lhcwpxlnqijr96irylrh1707</text>
<path fill="none" stroke="black" d="M4554.85,-3019.77C4489.6,-3058.23 4503.78,-3159.5 4700,-3159.5 4902.83,-3159.5 4911.14,-3139.57 4838.29,-3111.63" />
<polygon fill="black" stroke="black" points="4562.44,-3016.23 4573.4,-3016.08 4566.97,-3014.11 4571.5,-3012 4571.5,-3012 4571.5,-3012 4566.97,-3014.11 4569.6,-3007.92 4562.44,-3016.23 4562.44,-3016.23" />
<ellipse fill="none" stroke="black" cx="4558.81" cy="-3017.92" rx="4" ry="4" />
<polygon fill="black" stroke="black" points="4827.7,-3113.04 4831.18,-3103.66 4833.05,-3104.36 4829.57,-3113.73 4827.7,-3113.04" />
<polyline fill="none" stroke="black" points="4828.5,-3108 4833.19,-3109.74 " />
<polygon fill="black" stroke="black" points="4832.39,-3114.77 4835.86,-3105.4 4837.74,-3106.09 4834.26,-3115.47 4832.39,-3114.77" />
<polyline fill="none" stroke="black" points="4833.19,-3109.74 4837.88,-3111.48 " />
<text text-anchor="start" x="4618" y="-3163.3" font-family="Helvetica,sans-Serif" font-size="14.00">fk2lhcwpxlnqijr96irylrh1707</text>
</g> <!-- reservedentry_1a7b8520 -->
<g id="node40" class="node">
<g id="node41" class="node">
<title>
reservedentry_1a7b8520
</title>
<polygon fill="#e9c2f2" stroke="transparent" points="4584.5,-3169 4584.5,-3188 4741.5,-3188 4741.5,-3169 4584.5,-3169" />
<text text-anchor="start" x="4586.5" y="-3175.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."ReservedEntry"</text>
<polygon fill="#e9c2f2" stroke="transparent" points="4741.5,-3169 4741.5,-3188 4815.5,-3188 4815.5,-3169 4741.5,-3169" />
<text text-anchor="start" x="4776.5" y="-3174.8" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="4586.5" y="-3156.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">revision_id</text>
<text text-anchor="start" x="4706.5" y="-3155.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4743.5" y="-3155.8" font-family="Helvetica,sans-Serif" font-size="14.00">int8 not null</text>
<text text-anchor="start" x="4586.5" y="-3137.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">domain_label</text>
<text text-anchor="start" x="4706.5" y="-3136.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4743.5" y="-3136.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<polygon fill="none" stroke="#888888" points="4583,-3130.5 4583,-3189.5 4816,-3189.5 4816,-3130.5 4583,-3130.5" />
<polygon fill="#e9c2f2" stroke="transparent" points="4584.5,-3235 4584.5,-3254 4741.5,-3254 4741.5,-3235 4584.5,-3235" />
<text text-anchor="start" x="4586.5" y="-3241.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."ReservedEntry"</text>
<polygon fill="#e9c2f2" stroke="transparent" points="4741.5,-3235 4741.5,-3254 4815.5,-3254 4815.5,-3235 4741.5,-3235" />
<text text-anchor="start" x="4776.5" y="-3240.8" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="4586.5" y="-3222.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">revision_id</text>
<text text-anchor="start" x="4706.5" y="-3221.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4743.5" y="-3221.8" font-family="Helvetica,sans-Serif" font-size="14.00">int8 not null</text>
<text text-anchor="start" x="4586.5" y="-3203.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">domain_label</text>
<text text-anchor="start" x="4706.5" y="-3202.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4743.5" y="-3202.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<polygon fill="none" stroke="#888888" points="4583,-3196.5 4583,-3255.5 4816,-3255.5 4816,-3196.5 4583,-3196.5" />
</g> <!-- reservedlist_b97c3f1c -->
<g id="node41" class="node">
<g id="node42" class="node">
<title>
reservedlist_b97c3f1c
</title>
<polygon fill="#e9c2f2" stroke="transparent" points="3920,-3168 3920,-3187 4066,-3187 4066,-3168 3920,-3168" />
<text text-anchor="start" x="3922" y="-3174.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."ReservedList"</text>
<polygon fill="#e9c2f2" stroke="transparent" points="4066,-3168 4066,-3187 4176,-3187 4176,-3168 4066,-3168" />
<text text-anchor="start" x="4137" y="-3173.8" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="3922" y="-3155.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">revision_id</text>
<text text-anchor="start" x="4029" y="-3154.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4068" y="-3154.8" font-family="Helvetica,sans-Serif" font-size="14.00">bigserial not null</text>
<text text-anchor="start" x="4029" y="-3135.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4068" y="-3135.8" font-family="Helvetica,sans-Serif" font-size="14.00">auto-incremented</text>
<text text-anchor="start" x="3922" y="-3116.8" font-family="Helvetica,sans-Serif" font-size="14.00">name</text>
<text text-anchor="start" x="4029" y="-3116.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4068" y="-3116.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<polygon fill="none" stroke="#888888" points="3919,-3110 3919,-3188 4177,-3188 4177,-3110 3919,-3110" />
<polygon fill="#e9c2f2" stroke="transparent" points="3920,-3234 3920,-3253 4066,-3253 4066,-3234 3920,-3234" />
<text text-anchor="start" x="3922" y="-3240.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."ReservedList"</text>
<polygon fill="#e9c2f2" stroke="transparent" points="4066,-3234 4066,-3253 4176,-3253 4176,-3234 4066,-3234" />
<text text-anchor="start" x="4137" y="-3239.8" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="3922" y="-3221.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">revision_id</text>
<text text-anchor="start" x="4029" y="-3220.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4068" y="-3220.8" font-family="Helvetica,sans-Serif" font-size="14.00">bigserial not null</text>
<text text-anchor="start" x="4029" y="-3201.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4068" y="-3201.8" font-family="Helvetica,sans-Serif" font-size="14.00">auto-incremented</text>
<text text-anchor="start" x="3922" y="-3182.8" font-family="Helvetica,sans-Serif" font-size="14.00">name</text>
<text text-anchor="start" x="4029" y="-3182.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4068" y="-3182.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<polygon fill="none" stroke="#888888" points="3919,-3176 3919,-3254 4177,-3254 4177,-3176 3919,-3176" />
</g> <!-- reservedentry_1a7b8520&#45;&gt;reservedlist_b97c3f1c -->
<g id="edge65" class="edge">
<title>
reservedentry_1a7b8520:w-&gt;reservedlist_b97c3f1c:e
</title>
<path fill="none" stroke="black" d="M4565.44,-3159C4402.83,-3159 4353.42,-3159 4187.08,-3159" />
<polygon fill="black" stroke="black" points="4573.5,-3159 4583.5,-3163.5 4578.5,-3159 4583.5,-3159 4583.5,-3159 4583.5,-3159 4578.5,-3159 4583.5,-3154.5 4573.5,-3159 4573.5,-3159" />
<ellipse fill="none" stroke="black" cx="4569.5" cy="-3159" rx="4" ry="4" />
<polygon fill="black" stroke="black" points="4178,-3164 4178,-3154 4180,-3154 4180,-3164 4178,-3164" />
<polyline fill="none" stroke="black" points="4177,-3159 4182,-3159 " />
<polygon fill="black" stroke="black" points="4183,-3164 4183,-3154 4185,-3154 4185,-3164 4183,-3164" />
<polyline fill="none" stroke="black" points="4182,-3159 4187,-3159 " />
<text text-anchor="start" x="4275" y="-3162.8" font-family="Helvetica,sans-Serif" font-size="14.00">fkgq03rk0bt1hb915dnyvd3vnfc</text>
<path fill="none" stroke="black" d="M4565.44,-3225C4402.83,-3225 4353.42,-3225 4187.08,-3225" />
<polygon fill="black" stroke="black" points="4573.5,-3225 4583.5,-3229.5 4578.5,-3225 4583.5,-3225 4583.5,-3225 4583.5,-3225 4578.5,-3225 4583.5,-3220.5 4573.5,-3225 4573.5,-3225" />
<ellipse fill="none" stroke="black" cx="4569.5" cy="-3225" rx="4" ry="4" />
<polygon fill="black" stroke="black" points="4178,-3230 4178,-3220 4180,-3220 4180,-3230 4178,-3230" />
<polyline fill="none" stroke="black" points="4177,-3225 4182,-3225 " />
<polygon fill="black" stroke="black" points="4183,-3230 4183,-3220 4185,-3220 4185,-3230 4183,-3230" />
<polyline fill="none" stroke="black" points="4182,-3225 4187,-3225 " />
<text text-anchor="start" x="4275" y="-3228.8" font-family="Helvetica,sans-Serif" font-size="14.00">fkgq03rk0bt1hb915dnyvd3vnfc</text>
</g> <!-- serversecret_6cc90f09 -->
<g id="node42" class="node">
<g id="node43" class="node">
<title>
serversecret_6cc90f09
</title>
<polygon fill="#e9c2f2" stroke="transparent" points="4590.5,-3236 4590.5,-3255 4735.5,-3255 4735.5,-3236 4590.5,-3236" />
<text text-anchor="start" x="4592.5" y="-3242.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."ServerSecret"</text>
<polygon fill="#e9c2f2" stroke="transparent" points="4735.5,-3236 4735.5,-3255 4809.5,-3255 4809.5,-3236 4735.5,-3236" />
<text text-anchor="start" x="4770.5" y="-3241.8" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="4592.5" y="-3223.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">id</text>
<text text-anchor="start" x="4669.5" y="-3222.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4737.5" y="-3222.8" font-family="Helvetica,sans-Serif" font-size="14.00">int8 not null</text>
<polygon fill="none" stroke="#888888" points="4589,-3216 4589,-3256 4810,-3256 4810,-3216 4589,-3216" />
<polygon fill="#e9c2f2" stroke="transparent" points="4590.5,-3302 4590.5,-3321 4735.5,-3321 4735.5,-3302 4590.5,-3302" />
<text text-anchor="start" x="4592.5" y="-3308.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."ServerSecret"</text>
<polygon fill="#e9c2f2" stroke="transparent" points="4735.5,-3302 4735.5,-3321 4809.5,-3321 4809.5,-3302 4735.5,-3302" />
<text text-anchor="start" x="4770.5" y="-3307.8" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="4592.5" y="-3289.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">id</text>
<text text-anchor="start" x="4669.5" y="-3288.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4737.5" y="-3288.8" font-family="Helvetica,sans-Serif" font-size="14.00">int8 not null</text>
<polygon fill="none" stroke="#888888" points="4589,-3282 4589,-3322 4810,-3322 4810,-3282 4589,-3282" />
</g> <!-- signedmarkrevocationentry_99c39721 -->
<g id="node43" class="node">
<g id="node44" class="node">
<title>
signedmarkrevocationentry_99c39721
</title>
<polygon fill="#e9c2f2" stroke="transparent" points="4539.5,-3321 4539.5,-3340 4785.5,-3340 4785.5,-3321 4539.5,-3321" />
<text text-anchor="start" x="4541.5" y="-3327.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."SignedMarkRevocationEntry"</text>
<polygon fill="#e9c2f2" stroke="transparent" points="4785.5,-3321 4785.5,-3340 4859.5,-3340 4859.5,-3321 4785.5,-3321" />
<text text-anchor="start" x="4820.5" y="-3326.8" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="4541.5" y="-3308.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">revision_id</text>
<text text-anchor="start" x="4698.5" y="-3307.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4787.5" y="-3307.8" font-family="Helvetica,sans-Serif" font-size="14.00">int8 not null</text>
<text text-anchor="start" x="4541.5" y="-3289.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">smd_id</text>
<text text-anchor="start" x="4698.5" y="-3288.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4787.5" y="-3288.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<polygon fill="none" stroke="#888888" points="4538.5,-3282.5 4538.5,-3341.5 4860.5,-3341.5 4860.5,-3282.5 4538.5,-3282.5" />
<polygon fill="#e9c2f2" stroke="transparent" points="4539.5,-3387 4539.5,-3406 4785.5,-3406 4785.5,-3387 4539.5,-3387" />
<text text-anchor="start" x="4541.5" y="-3393.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."SignedMarkRevocationEntry"</text>
<polygon fill="#e9c2f2" stroke="transparent" points="4785.5,-3387 4785.5,-3406 4859.5,-3406 4859.5,-3387 4785.5,-3387" />
<text text-anchor="start" x="4820.5" y="-3392.8" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="4541.5" y="-3374.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">revision_id</text>
<text text-anchor="start" x="4698.5" y="-3373.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4787.5" y="-3373.8" font-family="Helvetica,sans-Serif" font-size="14.00">int8 not null</text>
<text text-anchor="start" x="4541.5" y="-3355.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">smd_id</text>
<text text-anchor="start" x="4698.5" y="-3354.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4787.5" y="-3354.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<polygon fill="none" stroke="#888888" points="4538.5,-3348.5 4538.5,-3407.5 4860.5,-3407.5 4860.5,-3348.5 4538.5,-3348.5" />
</g> <!-- signedmarkrevocationlist_c5d968fb -->
<g id="node44" class="node">
<g id="node45" class="node">
<title>
signedmarkrevocationlist_c5d968fb
</title>
<polygon fill="#e9c2f2" stroke="transparent" points="3875,-3321 3875,-3340 4111,-3340 4111,-3321 3875,-3321" />
<text text-anchor="start" x="3877" y="-3327.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."SignedMarkRevocationList"</text>
<polygon fill="#e9c2f2" stroke="transparent" points="4111,-3321 4111,-3340 4221,-3340 4221,-3321 4111,-3321" />
<text text-anchor="start" x="4182" y="-3326.8" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="3877" y="-3308.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">revision_id</text>
<text text-anchor="start" x="4029" y="-3307.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4113" y="-3307.8" font-family="Helvetica,sans-Serif" font-size="14.00">bigserial not null</text>
<text text-anchor="start" x="4029" y="-3288.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4113" y="-3288.8" font-family="Helvetica,sans-Serif" font-size="14.00">auto-incremented</text>
<polygon fill="none" stroke="#888888" points="3874,-3282.5 3874,-3341.5 4222,-3341.5 4222,-3282.5 3874,-3282.5" />
<polygon fill="#e9c2f2" stroke="transparent" points="3875,-3387 3875,-3406 4111,-3406 4111,-3387 3875,-3387" />
<text text-anchor="start" x="3877" y="-3393.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."SignedMarkRevocationList"</text>
<polygon fill="#e9c2f2" stroke="transparent" points="4111,-3387 4111,-3406 4221,-3406 4221,-3387 4111,-3387" />
<text text-anchor="start" x="4182" y="-3392.8" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="3877" y="-3374.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">revision_id</text>
<text text-anchor="start" x="4029" y="-3373.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4113" y="-3373.8" font-family="Helvetica,sans-Serif" font-size="14.00">bigserial not null</text>
<text text-anchor="start" x="4029" y="-3354.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4113" y="-3354.8" font-family="Helvetica,sans-Serif" font-size="14.00">auto-incremented</text>
<polygon fill="none" stroke="#888888" points="3874,-3348.5 3874,-3407.5 4222,-3407.5 4222,-3348.5 3874,-3348.5" />
</g> <!-- signedmarkrevocationentry_99c39721&#45;&gt;signedmarkrevocationlist_c5d968fb -->
<g id="edge66" class="edge">
<title>
signedmarkrevocationentry_99c39721:w-&gt;signedmarkrevocationlist_c5d968fb:e
</title>
<path fill="none" stroke="black" d="M4520.16,-3311C4397.65,-3311 4358.34,-3311 4232.05,-3311" />
<polygon fill="black" stroke="black" points="4528.5,-3311 4538.5,-3315.5 4533.5,-3311 4538.5,-3311 4538.5,-3311 4538.5,-3311 4533.5,-3311 4538.5,-3306.5 4528.5,-3311 4528.5,-3311" />
<ellipse fill="none" stroke="black" cx="4524.5" cy="-3311" rx="4" ry="4" />
<polygon fill="black" stroke="black" points="4223,-3316 4223,-3306 4225,-3306 4225,-3316 4223,-3316" />
<polyline fill="none" stroke="black" points="4222,-3311 4227,-3311 " />
<polygon fill="black" stroke="black" points="4228,-3316 4228,-3306 4230,-3306 4230,-3316 4228,-3316" />
<polyline fill="none" stroke="black" points="4227,-3311 4232,-3311 " />
<text text-anchor="start" x="4284" y="-3314.8" font-family="Helvetica,sans-Serif" font-size="14.00">fk5ivlhvs3121yx2li5tqh54u4</text>
<path fill="none" stroke="black" d="M4520.16,-3377C4397.65,-3377 4358.34,-3377 4232.05,-3377" />
<polygon fill="black" stroke="black" points="4528.5,-3377 4538.5,-3381.5 4533.5,-3377 4538.5,-3377 4538.5,-3377 4538.5,-3377 4533.5,-3377 4538.5,-3372.5 4528.5,-3377 4528.5,-3377" />
<ellipse fill="none" stroke="black" cx="4524.5" cy="-3377" rx="4" ry="4" />
<polygon fill="black" stroke="black" points="4223,-3382 4223,-3372 4225,-3372 4225,-3382 4223,-3382" />
<polyline fill="none" stroke="black" points="4222,-3377 4227,-3377 " />
<polygon fill="black" stroke="black" points="4228,-3382 4228,-3372 4230,-3372 4230,-3382 4228,-3382" />
<polyline fill="none" stroke="black" points="4227,-3377 4232,-3377 " />
<text text-anchor="start" x="4284" y="-3380.8" font-family="Helvetica,sans-Serif" font-size="14.00">fk5ivlhvs3121yx2li5tqh54u4</text>
</g> <!-- spec11threatmatch_a61228a6 -->
<g id="node45" class="node">
<g id="node46" class="node">
<title>
spec11threatmatch_a61228a6
</title>
<polygon fill="#e9c2f2" stroke="transparent" points="4549.5,-3464 4549.5,-3483 4739.5,-3483 4739.5,-3464 4549.5,-3464" />
<text text-anchor="start" x="4551.5" y="-3470.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."Spec11ThreatMatch"</text>
<polygon fill="#e9c2f2" stroke="transparent" points="4739.5,-3464 4739.5,-3483 4849.5,-3483 4849.5,-3464 4739.5,-3464" />
<text text-anchor="start" x="4810.5" y="-3469.8" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="4551.5" y="-3451.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">id</text>
<text text-anchor="start" x="4679.5" y="-3450.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4741.5" y="-3450.8" font-family="Helvetica,sans-Serif" font-size="14.00">bigserial not null</text>
<text text-anchor="start" x="4679.5" y="-3431.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4741.5" y="-3431.8" font-family="Helvetica,sans-Serif" font-size="14.00">auto-incremented</text>
<text text-anchor="start" x="4551.5" y="-3412.8" font-family="Helvetica,sans-Serif" font-size="14.00">check_date</text>
<text text-anchor="start" x="4679.5" y="-3412.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4741.5" y="-3412.8" font-family="Helvetica,sans-Serif" font-size="14.00">date not null</text>
<text text-anchor="start" x="4551.5" y="-3393.8" font-family="Helvetica,sans-Serif" font-size="14.00">registrar_id</text>
<text text-anchor="start" x="4679.5" y="-3393.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4741.5" y="-3393.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<text text-anchor="start" x="4551.5" y="-3374.8" font-family="Helvetica,sans-Serif" font-size="14.00">tld</text>
<text text-anchor="start" x="4679.5" y="-3374.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4741.5" y="-3374.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<polygon fill="none" stroke="#888888" points="4548.5,-3368 4548.5,-3484 4850.5,-3484 4850.5,-3368 4548.5,-3368" />
<polygon fill="#e9c2f2" stroke="transparent" points="4549.5,-3530 4549.5,-3549 4739.5,-3549 4739.5,-3530 4549.5,-3530" />
<text text-anchor="start" x="4551.5" y="-3536.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."Spec11ThreatMatch"</text>
<polygon fill="#e9c2f2" stroke="transparent" points="4739.5,-3530 4739.5,-3549 4849.5,-3549 4849.5,-3530 4739.5,-3530" />
<text text-anchor="start" x="4810.5" y="-3535.8" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="4551.5" y="-3517.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">id</text>
<text text-anchor="start" x="4679.5" y="-3516.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4741.5" y="-3516.8" font-family="Helvetica,sans-Serif" font-size="14.00">bigserial not null</text>
<text text-anchor="start" x="4679.5" y="-3497.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4741.5" y="-3497.8" font-family="Helvetica,sans-Serif" font-size="14.00">auto-incremented</text>
<text text-anchor="start" x="4551.5" y="-3478.8" font-family="Helvetica,sans-Serif" font-size="14.00">check_date</text>
<text text-anchor="start" x="4679.5" y="-3478.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4741.5" y="-3478.8" font-family="Helvetica,sans-Serif" font-size="14.00">date not null</text>
<text text-anchor="start" x="4551.5" y="-3459.8" font-family="Helvetica,sans-Serif" font-size="14.00">registrar_id</text>
<text text-anchor="start" x="4679.5" y="-3459.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4741.5" y="-3459.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<text text-anchor="start" x="4551.5" y="-3440.8" font-family="Helvetica,sans-Serif" font-size="14.00">tld</text>
<text text-anchor="start" x="4679.5" y="-3440.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4741.5" y="-3440.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<polygon fill="none" stroke="#888888" points="4548.5,-3434 4548.5,-3550 4850.5,-3550 4850.5,-3434 4548.5,-3434" />
</g> <!-- tmchcrl_d282355 -->
<g id="node47" class="node">
<g id="node48" class="node">
<title>
tmchcrl_d282355
</title>
<polygon fill="#e9c2f2" stroke="transparent" points="4604.5,-3530 4604.5,-3549 4721.5,-3549 4721.5,-3530 4604.5,-3530" />
<text text-anchor="start" x="4606.5" y="-3536.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."TmchCrl"</text>
<polygon fill="#e9c2f2" stroke="transparent" points="4721.5,-3530 4721.5,-3549 4795.5,-3549 4795.5,-3530 4721.5,-3530" />
<text text-anchor="start" x="4756.5" y="-3535.8" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="4606.5" y="-3517.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">id</text>
<text text-anchor="start" x="4669.5" y="-3516.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4723.5" y="-3516.8" font-family="Helvetica,sans-Serif" font-size="14.00">int8 not null</text>
<polygon fill="none" stroke="#888888" points="4603,-3510 4603,-3550 4796,-3550 4796,-3510 4603,-3510" />
<polygon fill="#e9c2f2" stroke="transparent" points="4604.5,-3596 4604.5,-3615 4721.5,-3615 4721.5,-3596 4604.5,-3596" />
<text text-anchor="start" x="4606.5" y="-3602.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."TmchCrl"</text>
<polygon fill="#e9c2f2" stroke="transparent" points="4721.5,-3596 4721.5,-3615 4795.5,-3615 4795.5,-3596 4721.5,-3596" />
<text text-anchor="start" x="4756.5" y="-3601.8" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="4606.5" y="-3583.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">id</text>
<text text-anchor="start" x="4669.5" y="-3582.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4723.5" y="-3582.8" font-family="Helvetica,sans-Serif" font-size="14.00">int8 not null</text>
<polygon fill="none" stroke="#888888" points="4603,-3576 4603,-3616 4796,-3616 4796,-3576 4603,-3576" />
</g> <!-- userupdatehistory_24efd476 -->
<g id="node49" class="node">
<g id="node50" class="node">
<title>
userupdatehistory_24efd476
</title>
<polygon fill="#e9c2f2" stroke="transparent" points="4570.5,-3634 4570.5,-3653 4754.5,-3653 4754.5,-3634 4570.5,-3634" />
<text text-anchor="start" x="4572.5" y="-3640.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."UserUpdateHistory"</text>
<polygon fill="#e9c2f2" stroke="transparent" points="4754.5,-3634 4754.5,-3653 4828.5,-3653 4828.5,-3634 4754.5,-3634" />
<text text-anchor="start" x="4789.5" y="-3639.8" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="4572.5" y="-3621.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">history_revision_id</text>
<text text-anchor="start" x="4724.5" y="-3620.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4756.5" y="-3620.8" font-family="Helvetica,sans-Serif" font-size="14.00">int8 not null</text>
<text text-anchor="start" x="4572.5" y="-3601.8" font-family="Helvetica,sans-Serif" font-size="14.00">email_address</text>
<text text-anchor="start" x="4724.5" y="-3601.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4756.5" y="-3601.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<text text-anchor="start" x="4572.5" y="-3582.8" font-family="Helvetica,sans-Serif" font-size="14.00">history_acting_user</text>
<text text-anchor="start" x="4724.5" y="-3582.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4756.5" y="-3582.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<polygon fill="none" stroke="#888888" points="4569.5,-3576 4569.5,-3654 4829.5,-3654 4829.5,-3576 4569.5,-3576" />
<polygon fill="#e9c2f2" stroke="transparent" points="4570.5,-3700 4570.5,-3719 4754.5,-3719 4754.5,-3700 4570.5,-3700" />
<text text-anchor="start" x="4572.5" y="-3706.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."UserUpdateHistory"</text>
<polygon fill="#e9c2f2" stroke="transparent" points="4754.5,-3700 4754.5,-3719 4828.5,-3719 4828.5,-3700 4754.5,-3700" />
<text text-anchor="start" x="4789.5" y="-3705.8" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="4572.5" y="-3687.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">history_revision_id</text>
<text text-anchor="start" x="4724.5" y="-3686.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4756.5" y="-3686.8" font-family="Helvetica,sans-Serif" font-size="14.00">int8 not null</text>
<text text-anchor="start" x="4572.5" y="-3667.8" font-family="Helvetica,sans-Serif" font-size="14.00">email_address</text>
<text text-anchor="start" x="4724.5" y="-3667.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4756.5" y="-3667.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<text text-anchor="start" x="4572.5" y="-3648.8" font-family="Helvetica,sans-Serif" font-size="14.00">history_acting_user</text>
<text text-anchor="start" x="4724.5" y="-3648.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4756.5" y="-3648.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<polygon fill="none" stroke="#888888" points="4569.5,-3642 4569.5,-3720 4829.5,-3720 4829.5,-3642 4569.5,-3642" />
</g>
</g>
</svg>
@@ -4982,6 +4995,37 @@ td.section {
</tbody>
</table>
<p>&nbsp;</p>
<table>
<caption style="background-color: #E9C2F2;">
<span id="passwordresetrequest_8484e7b1" class="caption_name">public."PasswordResetRequest"</span> <span class="caption_description">[table]</span>
</caption>
<tbody>
<tr>
<td class="spacer"></td>
<td class="minwidth"><b><i>verification_code</i></b></td>
<td class="minwidth">text not null</td>
</tr>
<tr>
<td colspan="3"></td>
</tr>
<tr>
<td colspan="3" class="section">Primary Key</td>
</tr>
<tr>
<td colspan="3"></td>
</tr>
<tr>
<td colspan="2" class="name">"PasswordResetRequest_pkey"</td>
<td class="description right">[primary key]</td>
</tr>
<tr>
<td class="spacer"></td>
<td class="minwidth">verification_code</td>
<td class="minwidth"></td>
</tr>
</tbody>
</table>
<p>&nbsp;</p>
<table>
<caption style="background-color: #E9C2F2;">
<span id="pollmessage_614a523e" class="caption_name">public."PollMessage"</span> <span class="caption_description">[table]</span>
@@ -261,26 +261,26 @@ td.section {
</tr>
<tr>
<td class="property_name">generated on</td>
<td class="property_value">2025-04-18 20:02:17</td>
<td class="property_value">2025-05-15 19:22:16</td>
</tr>
<tr>
<td class="property_name">last flyway file</td>
<td id="lastFlywayFile" class="property_value">V192__add_last_poc_verification_date.sql</td>
<td id="lastFlywayFile" class="property_value">V194__password_reset_request_registrar.sql</td>
</tr>
</tbody>
</table>
<p>&nbsp;</p>
<p>&nbsp;</p>
<svg viewBox="0.00 0.00 5683.00 7966.00" 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 7962)">
<svg viewBox="0.00 0.00 5683.00 8146.00" 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 8142)">
<title>
SchemaCrawler_Diagram
</title>
<polygon fill="white" stroke="transparent" points="-4,4 -4,-7962 5679,-7962 5679,4 -4,4" />
<polygon fill="white" stroke="transparent" points="-4,4 -4,-8142 5679,-8142 5679,4 -4,4" />
<text text-anchor="start" x="5435" y="-29.8" font-family="Helvetica,sans-Serif" font-size="14.00">generated by</text>
<text text-anchor="start" x="5518" y="-29.8" font-family="Helvetica,sans-Serif" font-size="14.00">SchemaCrawler 16.25.2</text>
<text text-anchor="start" x="5434" y="-10.8" font-family="Helvetica,sans-Serif" font-size="14.00">generated on</text>
<text text-anchor="start" x="5518" y="-10.8" font-family="Helvetica,sans-Serif" font-size="14.00">2025-04-18 20:02:17</text>
<text text-anchor="start" x="5518" y="-10.8" font-family="Helvetica,sans-Serif" font-size="14.00">2025-05-15 19:22:16</text>
<polygon fill="none" stroke="#888888" points="5431,-4 5431,-44 5667,-44 5667,-4 5431,-4" /> <!-- allocationtoken_a08ccbef -->
<g id="node1" class="node">
<title>
@@ -488,7 +488,7 @@ td.section {
<polyline fill="none" stroke="black" points="1073.99,-2560.27 1078.99,-2560.54 " />
<text text-anchor="start" x="1160" y="-2740.8" font-family="Helvetica,sans-Serif" font-size="14.00">fk_billing_event_cancellation_matching_billing_recurrence_id</text>
</g> <!-- registrar_6e1503e3 -->
<g id="node35" class="node">
<g id="node36" class="node">
<title>
registrar_6e1503e3
</title>
@@ -1233,7 +1233,7 @@ td.section {
<polyline fill="none" stroke="black" points="447.33,-3053.27 450.66,-3049.54 " />
<text text-anchor="start" x="1639" y="-1837.8" font-family="Helvetica,sans-Serif" font-size="14.00">fk_domain_transfer_losing_registrar_id</text>
</g> <!-- tld_f1fa57e2 -->
<g id="node46" class="node">
<g id="node47" class="node">
<title>
tld_f1fa57e2
</title>
@@ -1999,7 +1999,7 @@ td.section {
<text text-anchor="start" x="5481" y="-5097.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<polygon fill="none" stroke="#888888" points="5270.5,-5091 5270.5,-5245 5605.5,-5245 5605.5,-5091 5270.5,-5091" />
</g> <!-- user_f2216f01 -->
<g id="node48" class="node">
<g id="node49" class="node">
<title>
user_f2216f01
</title>
@@ -3163,92 +3163,123 @@ td.section {
<text text-anchor="start" x="5443" y="-6189.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5463" y="-6189.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<polygon fill="none" stroke="#888888" points="5274.5,-6183 5274.5,-6375 5601.5,-6375 5601.5,-6183 5274.5,-6183" />
</g> <!-- premiumentry_b0060b91 -->
</g> <!-- passwordresetrequest_8484e7b1 -->
<g id="node32" class="node">
<title>
passwordresetrequest_8484e7b1
</title>
<polygon fill="#e9c2f2" stroke="transparent" points="5267,-6535 5267,-6554 5483,-6554 5483,-6535 5267,-6535" />
<text text-anchor="start" x="5269" y="-6541.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."PasswordResetRequest"</text>
<polygon fill="#e9c2f2" stroke="transparent" points="5483,-6535 5483,-6554 5609,-6554 5609,-6535 5483,-6535" />
<text text-anchor="start" x="5570" y="-6540.8" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="5269" y="-6521.8" font-family="Helvetica,sans-Serif" font-size="14.00">type</text>
<text text-anchor="start" x="5431" y="-6521.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5485" y="-6521.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<text text-anchor="start" x="5269" y="-6502.8" font-family="Helvetica,sans-Serif" font-size="14.00">request_time</text>
<text text-anchor="start" x="5431" y="-6502.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5485" y="-6502.8" font-family="Helvetica,sans-Serif" font-size="14.00">timestamptz not null</text>
<text text-anchor="start" x="5269" y="-6483.8" font-family="Helvetica,sans-Serif" font-size="14.00">requester</text>
<text text-anchor="start" x="5431" y="-6483.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5485" y="-6483.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<text text-anchor="start" x="5269" y="-6464.8" font-family="Helvetica,sans-Serif" font-size="14.00">fulfillment_time</text>
<text text-anchor="start" x="5431" y="-6464.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5485" y="-6464.8" font-family="Helvetica,sans-Serif" font-size="14.00">timestamptz</text>
<text text-anchor="start" x="5269" y="-6445.8" font-family="Helvetica,sans-Serif" font-size="14.00">destination_email</text>
<text text-anchor="start" x="5431" y="-6445.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5485" y="-6445.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<text text-anchor="start" x="5269" y="-6427.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">verification_code</text>
<text text-anchor="start" x="5431" y="-6426.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5485" y="-6426.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<text text-anchor="start" x="5269" y="-6407.8" font-family="Helvetica,sans-Serif" font-size="14.00">registrar_id</text>
<text text-anchor="start" x="5431" y="-6407.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5485" y="-6407.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<polygon fill="none" stroke="#888888" points="5266,-6401 5266,-6555 5610,-6555 5610,-6401 5266,-6401" />
</g> <!-- premiumentry_b0060b91 -->
<g id="node33" class="node">
<title>
premiumentry_b0060b91
</title>
<polygon fill="#e9c2f2" stroke="transparent" points="5291,-6459 5291,-6478 5446,-6478 5446,-6459 5291,-6459" />
<text text-anchor="start" x="5293" y="-6465.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."PremiumEntry"</text>
<polygon fill="#e9c2f2" stroke="transparent" points="5446,-6459 5446,-6478 5586,-6478 5586,-6459 5446,-6459" />
<text text-anchor="start" x="5547" y="-6464.8" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="5293" y="-6446.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">revision_id</text>
<text text-anchor="start" x="5412" y="-6445.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5448" y="-6445.8" font-family="Helvetica,sans-Serif" font-size="14.00">int8 not null</text>
<text text-anchor="start" x="5293" y="-6426.8" font-family="Helvetica,sans-Serif" font-size="14.00">price</text>
<text text-anchor="start" x="5412" y="-6426.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5448" y="-6426.8" font-family="Helvetica,sans-Serif" font-size="14.00">numeric(19, 2) not null</text>
<text text-anchor="start" x="5293" y="-6408.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">domain_label</text>
<text text-anchor="start" x="5412" y="-6407.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5448" y="-6407.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<polygon fill="none" stroke="#888888" points="5289.5,-6401 5289.5,-6479 5586.5,-6479 5586.5,-6401 5289.5,-6401" />
<polygon fill="#e9c2f2" stroke="transparent" points="5291,-6639 5291,-6658 5446,-6658 5446,-6639 5291,-6639" />
<text text-anchor="start" x="5293" y="-6645.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."PremiumEntry"</text>
<polygon fill="#e9c2f2" stroke="transparent" points="5446,-6639 5446,-6658 5586,-6658 5586,-6639 5446,-6639" />
<text text-anchor="start" x="5547" y="-6644.8" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="5293" y="-6626.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">revision_id</text>
<text text-anchor="start" x="5412" y="-6625.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5448" y="-6625.8" font-family="Helvetica,sans-Serif" font-size="14.00">int8 not null</text>
<text text-anchor="start" x="5293" y="-6606.8" font-family="Helvetica,sans-Serif" font-size="14.00">price</text>
<text text-anchor="start" x="5412" y="-6606.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5448" y="-6606.8" font-family="Helvetica,sans-Serif" font-size="14.00">numeric(19, 2) not null</text>
<text text-anchor="start" x="5293" y="-6588.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">domain_label</text>
<text text-anchor="start" x="5412" y="-6587.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5448" y="-6587.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<polygon fill="none" stroke="#888888" points="5289.5,-6581 5289.5,-6659 5586.5,-6659 5586.5,-6581 5289.5,-6581" />
</g> <!-- premiumlist_7c3ea68b -->
<g id="node33" class="node">
<g id="node34" class="node">
<title>
premiumlist_7c3ea68b
</title>
<polygon fill="#e9c2f2" stroke="transparent" points="4618,-6459 4618,-6478 4763,-6478 4763,-6459 4618,-6459" />
<text text-anchor="start" x="4620" y="-6465.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."PremiumList"</text>
<polygon fill="#e9c2f2" stroke="transparent" points="4763,-6459 4763,-6478 4873,-6478 4873,-6459 4763,-6459" />
<text text-anchor="start" x="4834" y="-6464.8" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="4620" y="-6446.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">revision_id</text>
<text text-anchor="start" x="4750" y="-6445.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4765" y="-6445.8" font-family="Helvetica,sans-Serif" font-size="14.00">bigserial not null</text>
<text text-anchor="start" x="4750" y="-6426.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4765" y="-6426.8" font-family="Helvetica,sans-Serif" font-size="14.00">auto-incremented</text>
<text text-anchor="start" x="4620" y="-6407.8" font-family="Helvetica,sans-Serif" font-size="14.00">creation_timestamp</text>
<text text-anchor="start" x="4750" y="-6407.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4765" y="-6407.8" font-family="Helvetica,sans-Serif" font-size="14.00">timestamptz</text>
<text text-anchor="start" x="4620" y="-6388.8" font-family="Helvetica,sans-Serif" font-size="14.00">name</text>
<text text-anchor="start" x="4750" y="-6388.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4765" y="-6388.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<text text-anchor="start" x="4620" y="-6369.8" font-family="Helvetica,sans-Serif" font-size="14.00">bloom_filter</text>
<text text-anchor="start" x="4750" y="-6369.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4765" y="-6369.8" font-family="Helvetica,sans-Serif" font-size="14.00">bytea not null</text>
<text text-anchor="start" x="4620" y="-6350.8" font-family="Helvetica,sans-Serif" font-size="14.00">currency</text>
<text text-anchor="start" x="4750" y="-6350.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4765" y="-6350.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<polygon fill="none" stroke="#888888" points="4616.5,-6344.5 4616.5,-6479.5 4873.5,-6479.5 4873.5,-6344.5 4616.5,-6344.5" />
<polygon fill="#e9c2f2" stroke="transparent" points="4618,-6639 4618,-6658 4763,-6658 4763,-6639 4618,-6639" />
<text text-anchor="start" x="4620" y="-6645.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."PremiumList"</text>
<polygon fill="#e9c2f2" stroke="transparent" points="4763,-6639 4763,-6658 4873,-6658 4873,-6639 4763,-6639" />
<text text-anchor="start" x="4834" y="-6644.8" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="4620" y="-6626.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">revision_id</text>
<text text-anchor="start" x="4750" y="-6625.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4765" y="-6625.8" font-family="Helvetica,sans-Serif" font-size="14.00">bigserial not null</text>
<text text-anchor="start" x="4750" y="-6606.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4765" y="-6606.8" font-family="Helvetica,sans-Serif" font-size="14.00">auto-incremented</text>
<text text-anchor="start" x="4620" y="-6587.8" font-family="Helvetica,sans-Serif" font-size="14.00">creation_timestamp</text>
<text text-anchor="start" x="4750" y="-6587.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4765" y="-6587.8" font-family="Helvetica,sans-Serif" font-size="14.00">timestamptz</text>
<text text-anchor="start" x="4620" y="-6568.8" font-family="Helvetica,sans-Serif" font-size="14.00">name</text>
<text text-anchor="start" x="4750" y="-6568.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4765" y="-6568.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<text text-anchor="start" x="4620" y="-6549.8" font-family="Helvetica,sans-Serif" font-size="14.00">bloom_filter</text>
<text text-anchor="start" x="4750" y="-6549.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4765" y="-6549.8" font-family="Helvetica,sans-Serif" font-size="14.00">bytea not null</text>
<text text-anchor="start" x="4620" y="-6530.8" font-family="Helvetica,sans-Serif" font-size="14.00">currency</text>
<text text-anchor="start" x="4750" y="-6530.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4765" y="-6530.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<polygon fill="none" stroke="#888888" points="4616.5,-6524.5 4616.5,-6659.5 4873.5,-6659.5 4873.5,-6524.5 4616.5,-6524.5" />
</g> <!-- premiumentry_b0060b91&#45;&gt;premiumlist_7c3ea68b -->
<g id="edge36" class="edge">
<title>
premiumentry_b0060b91:w-&gt;premiumlist_7c3ea68b:e
</title>
<path fill="none" stroke="black" d="M5271.77,-6450C5105.08,-6450 5054.58,-6450 4884.07,-6450" />
<polygon fill="black" stroke="black" points="5280,-6450 5290,-6454.5 5285,-6450 5290,-6450 5290,-6450 5290,-6450 5285,-6450 5290,-6445.5 5280,-6450 5280,-6450" />
<ellipse fill="none" stroke="black" cx="5276" cy="-6450" rx="4" ry="4" />
<polygon fill="black" stroke="black" points="4875,-6455 4875,-6445 4877,-6445 4877,-6455 4875,-6455" />
<polyline fill="none" stroke="black" points="4874,-6450 4879,-6450 " />
<polygon fill="black" stroke="black" points="4880,-6455 4880,-6445 4882,-6445 4882,-6455 4880,-6455" />
<polyline fill="none" stroke="black" points="4879,-6450 4884,-6450 " />
<text text-anchor="start" x="4970.5" y="-6453.8" font-family="Helvetica,sans-Serif" font-size="14.00">fko0gw90lpo1tuee56l0nb6y6g5</text>
<path fill="none" stroke="black" d="M5271.77,-6630C5105.08,-6630 5054.58,-6630 4884.07,-6630" />
<polygon fill="black" stroke="black" points="5280,-6630 5290,-6634.5 5285,-6630 5290,-6630 5290,-6630 5290,-6630 5285,-6630 5290,-6625.5 5280,-6630 5280,-6630" />
<ellipse fill="none" stroke="black" cx="5276" cy="-6630" rx="4" ry="4" />
<polygon fill="black" stroke="black" points="4875,-6635 4875,-6625 4877,-6625 4877,-6635 4875,-6635" />
<polyline fill="none" stroke="black" points="4874,-6630 4879,-6630 " />
<polygon fill="black" stroke="black" points="4880,-6635 4880,-6625 4882,-6625 4882,-6635 4880,-6635" />
<polyline fill="none" stroke="black" points="4879,-6630 4884,-6630 " />
<text text-anchor="start" x="4970.5" y="-6633.8" font-family="Helvetica,sans-Serif" font-size="14.00">fko0gw90lpo1tuee56l0nb6y6g5</text>
</g> <!-- rderevision_83396864 -->
<g id="node34" class="node">
<g id="node35" class="node">
<title>
rderevision_83396864
</title>
<polygon fill="#e9c2f2" stroke="transparent" points="5327,-6601 5327,-6620 5470,-6620 5470,-6601 5327,-6601" />
<text text-anchor="start" x="5329" y="-6607.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."RdeRevision"</text>
<polygon fill="#e9c2f2" stroke="transparent" points="5470,-6601 5470,-6620 5549,-6620 5549,-6601 5470,-6601" />
<text text-anchor="start" x="5510" y="-6606.8" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="5329" y="-6588.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">tld</text>
<text text-anchor="start" x="5455" y="-6587.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5472" y="-6587.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<text text-anchor="start" x="5329" y="-6569.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">mode</text>
<text text-anchor="start" x="5455" y="-6568.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5472" y="-6568.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<text text-anchor="start" x="5329" y="-6550.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">"date"</text>
<text text-anchor="start" x="5455" y="-6549.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5472" y="-6549.8" font-family="Helvetica,sans-Serif" font-size="14.00">date not null</text>
<text text-anchor="start" x="5329" y="-6530.8" font-family="Helvetica,sans-Serif" font-size="14.00">update_timestamp</text>
<text text-anchor="start" x="5455" y="-6530.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5472" y="-6530.8" font-family="Helvetica,sans-Serif" font-size="14.00">timestamptz</text>
<text text-anchor="start" x="5329" y="-6511.8" font-family="Helvetica,sans-Serif" font-size="14.00">revision</text>
<text text-anchor="start" x="5455" y="-6511.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5472" y="-6511.8" font-family="Helvetica,sans-Serif" font-size="14.00">int4 not null</text>
<polygon fill="none" stroke="#888888" points="5326,-6505 5326,-6621 5550,-6621 5550,-6505 5326,-6505" />
<polygon fill="#e9c2f2" stroke="transparent" points="5327,-6781 5327,-6800 5470,-6800 5470,-6781 5327,-6781" />
<text text-anchor="start" x="5329" y="-6787.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."RdeRevision"</text>
<polygon fill="#e9c2f2" stroke="transparent" points="5470,-6781 5470,-6800 5549,-6800 5549,-6781 5470,-6781" />
<text text-anchor="start" x="5510" y="-6786.8" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="5329" y="-6768.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">tld</text>
<text text-anchor="start" x="5455" y="-6767.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5472" y="-6767.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<text text-anchor="start" x="5329" y="-6749.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">mode</text>
<text text-anchor="start" x="5455" y="-6748.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5472" y="-6748.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<text text-anchor="start" x="5329" y="-6730.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">"date"</text>
<text text-anchor="start" x="5455" y="-6729.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5472" y="-6729.8" font-family="Helvetica,sans-Serif" font-size="14.00">date not null</text>
<text text-anchor="start" x="5329" y="-6710.8" font-family="Helvetica,sans-Serif" font-size="14.00">update_timestamp</text>
<text text-anchor="start" x="5455" y="-6710.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5472" y="-6710.8" font-family="Helvetica,sans-Serif" font-size="14.00">timestamptz</text>
<text text-anchor="start" x="5329" y="-6691.8" font-family="Helvetica,sans-Serif" font-size="14.00">revision</text>
<text text-anchor="start" x="5455" y="-6691.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5472" y="-6691.8" font-family="Helvetica,sans-Serif" font-size="14.00">int4 not null</text>
<polygon fill="none" stroke="#888888" points="5326,-6685 5326,-6801 5550,-6801 5550,-6685 5326,-6685" />
</g> <!-- registrarpoc_ab47054d -->
<g id="node36" class="node">
<g id="node37" class="node">
<title>
registrarpoc_ab47054d
</title>
@@ -3310,7 +3341,7 @@ td.section {
<polyline fill="none" stroke="black" points="446.71,-3052.8 449.42,-3048.6 " />
<text text-anchor="start" x="485" y="-1380.8" font-family="Helvetica,sans-Serif" font-size="14.00">fk_registrar_poc_registrar_id</text>
</g> <!-- registrarupdatehistory_8a38bed4 -->
<g id="node37" class="node">
<g id="node38" class="node">
<title>
registrarupdatehistory_8a38bed4
</title>
@@ -3492,7 +3523,7 @@ td.section {
<polyline fill="none" stroke="black" points="445.56,-3052.25 447.12,-3047.5 " />
<text text-anchor="start" x="470" y="-1110.8" font-family="Helvetica,sans-Serif" font-size="14.00">fkregistrarupdatehistoryregistrarid</text>
</g> <!-- registrarpocupdatehistory_31e5d9aa -->
<g id="node38" class="node">
<g id="node39" class="node">
<title>
registrarpocupdatehistory_31e5d9aa
</title>
@@ -3591,304 +3622,304 @@ td.section {
<polyline fill="none" stroke="black" points="1086,-1116.03 1091,-1116.05 " />
<text text-anchor="start" x="1221.5" y="-1192.8" font-family="Helvetica,sans-Serif" font-size="14.00">fkregistrarpocupdatehistoryemailaddress</text>
</g> <!-- registrylock_ac88663e -->
<g id="node39" class="node">
<g id="node40" class="node">
<title>
registrylock_ac88663e
</title>
<polygon fill="#e9c2f2" stroke="transparent" points="5296,-6933 5296,-6952 5455,-6952 5455,-6933 5296,-6933" />
<text text-anchor="start" x="5298" y="-6939.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."RegistryLock"</text>
<polygon fill="#e9c2f2" stroke="transparent" points="5455,-6933 5455,-6952 5581,-6952 5581,-6933 5455,-6933" />
<text text-anchor="start" x="5542" y="-6938.8" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="5298" y="-6920.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">revision_id</text>
<text text-anchor="start" x="5449" y="-6919.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5457" y="-6919.8" font-family="Helvetica,sans-Serif" font-size="14.00">bigserial not null</text>
<text text-anchor="start" x="5449" y="-6900.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5457" y="-6900.8" font-family="Helvetica,sans-Serif" font-size="14.00">auto-incremented</text>
<text text-anchor="start" x="5298" y="-6881.8" font-family="Helvetica,sans-Serif" font-size="14.00">lock_completion_time</text>
<text text-anchor="start" x="5449" y="-6881.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5457" y="-6881.8" font-family="Helvetica,sans-Serif" font-size="14.00">timestamptz</text>
<text text-anchor="start" x="5298" y="-6862.8" font-family="Helvetica,sans-Serif" font-size="14.00">lock_request_time</text>
<text text-anchor="start" x="5449" y="-6862.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5457" y="-6862.8" font-family="Helvetica,sans-Serif" font-size="14.00">timestamptz not null</text>
<text text-anchor="start" x="5298" y="-6843.8" font-family="Helvetica,sans-Serif" font-size="14.00">domain_name</text>
<text text-anchor="start" x="5449" y="-6843.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5457" y="-6843.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<text text-anchor="start" x="5298" y="-6824.8" font-family="Helvetica,sans-Serif" font-size="14.00">is_superuser</text>
<text text-anchor="start" x="5449" y="-6824.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5457" y="-6824.8" font-family="Helvetica,sans-Serif" font-size="14.00">bool not null</text>
<text text-anchor="start" x="5298" y="-6805.8" font-family="Helvetica,sans-Serif" font-size="14.00">registrar_id</text>
<text text-anchor="start" x="5449" y="-6805.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5457" y="-6805.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<text text-anchor="start" x="5298" y="-6786.8" font-family="Helvetica,sans-Serif" font-size="14.00">registrar_poc_id</text>
<text text-anchor="start" x="5449" y="-6786.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5457" y="-6786.8" font-family="Helvetica,sans-Serif" font-size="14.00">text</text>
<text text-anchor="start" x="5298" y="-6767.8" font-family="Helvetica,sans-Serif" font-size="14.00">repo_id</text>
<text text-anchor="start" x="5449" y="-6767.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5457" y="-6767.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<text text-anchor="start" x="5298" y="-6748.8" font-family="Helvetica,sans-Serif" font-size="14.00">verification_code</text>
<text text-anchor="start" x="5449" y="-6748.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5457" y="-6748.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<text text-anchor="start" x="5298" y="-6729.8" font-family="Helvetica,sans-Serif" font-size="14.00">unlock_request_time</text>
<text text-anchor="start" x="5449" y="-6729.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5457" y="-6729.8" font-family="Helvetica,sans-Serif" font-size="14.00">timestamptz</text>
<text text-anchor="start" x="5298" y="-6710.8" font-family="Helvetica,sans-Serif" font-size="14.00">unlock_completion_time</text>
<text text-anchor="start" x="5449" y="-6710.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5457" y="-6710.8" font-family="Helvetica,sans-Serif" font-size="14.00">timestamptz</text>
<text text-anchor="start" x="5298" y="-6691.8" font-family="Helvetica,sans-Serif" font-size="14.00">last_update_time</text>
<text text-anchor="start" x="5449" y="-6691.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5457" y="-6691.8" font-family="Helvetica,sans-Serif" font-size="14.00">timestamptz not null</text>
<text text-anchor="start" x="5298" y="-6672.8" font-family="Helvetica,sans-Serif" font-size="14.00">relock_revision_id</text>
<text text-anchor="start" x="5449" y="-6672.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5457" y="-6672.8" font-family="Helvetica,sans-Serif" font-size="14.00">int8</text>
<text text-anchor="start" x="5298" y="-6653.8" font-family="Helvetica,sans-Serif" font-size="14.00">relock_duration</text>
<text text-anchor="start" x="5449" y="-6653.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5457" y="-6653.8" font-family="Helvetica,sans-Serif" font-size="14.00">interval</text>
<polygon fill="none" stroke="#888888" points="5294.5,-6647 5294.5,-6953 5581.5,-6953 5581.5,-6647 5294.5,-6647" />
<polygon fill="#e9c2f2" stroke="transparent" points="5296,-7113 5296,-7132 5455,-7132 5455,-7113 5296,-7113" />
<text text-anchor="start" x="5298" y="-7119.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."RegistryLock"</text>
<polygon fill="#e9c2f2" stroke="transparent" points="5455,-7113 5455,-7132 5581,-7132 5581,-7113 5455,-7113" />
<text text-anchor="start" x="5542" y="-7118.8" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="5298" y="-7100.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">revision_id</text>
<text text-anchor="start" x="5449" y="-7099.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5457" y="-7099.8" font-family="Helvetica,sans-Serif" font-size="14.00">bigserial not null</text>
<text text-anchor="start" x="5449" y="-7080.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5457" y="-7080.8" font-family="Helvetica,sans-Serif" font-size="14.00">auto-incremented</text>
<text text-anchor="start" x="5298" y="-7061.8" font-family="Helvetica,sans-Serif" font-size="14.00">lock_completion_time</text>
<text text-anchor="start" x="5449" y="-7061.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5457" y="-7061.8" font-family="Helvetica,sans-Serif" font-size="14.00">timestamptz</text>
<text text-anchor="start" x="5298" y="-7042.8" font-family="Helvetica,sans-Serif" font-size="14.00">lock_request_time</text>
<text text-anchor="start" x="5449" y="-7042.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5457" y="-7042.8" font-family="Helvetica,sans-Serif" font-size="14.00">timestamptz not null</text>
<text text-anchor="start" x="5298" y="-7023.8" font-family="Helvetica,sans-Serif" font-size="14.00">domain_name</text>
<text text-anchor="start" x="5449" y="-7023.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5457" y="-7023.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<text text-anchor="start" x="5298" y="-7004.8" font-family="Helvetica,sans-Serif" font-size="14.00">is_superuser</text>
<text text-anchor="start" x="5449" y="-7004.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5457" y="-7004.8" font-family="Helvetica,sans-Serif" font-size="14.00">bool not null</text>
<text text-anchor="start" x="5298" y="-6985.8" font-family="Helvetica,sans-Serif" font-size="14.00">registrar_id</text>
<text text-anchor="start" x="5449" y="-6985.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5457" y="-6985.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<text text-anchor="start" x="5298" y="-6966.8" font-family="Helvetica,sans-Serif" font-size="14.00">registrar_poc_id</text>
<text text-anchor="start" x="5449" y="-6966.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5457" y="-6966.8" font-family="Helvetica,sans-Serif" font-size="14.00">text</text>
<text text-anchor="start" x="5298" y="-6947.8" font-family="Helvetica,sans-Serif" font-size="14.00">repo_id</text>
<text text-anchor="start" x="5449" y="-6947.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5457" y="-6947.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<text text-anchor="start" x="5298" y="-6928.8" font-family="Helvetica,sans-Serif" font-size="14.00">verification_code</text>
<text text-anchor="start" x="5449" y="-6928.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5457" y="-6928.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<text text-anchor="start" x="5298" y="-6909.8" font-family="Helvetica,sans-Serif" font-size="14.00">unlock_request_time</text>
<text text-anchor="start" x="5449" y="-6909.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5457" y="-6909.8" font-family="Helvetica,sans-Serif" font-size="14.00">timestamptz</text>
<text text-anchor="start" x="5298" y="-6890.8" font-family="Helvetica,sans-Serif" font-size="14.00">unlock_completion_time</text>
<text text-anchor="start" x="5449" y="-6890.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5457" y="-6890.8" font-family="Helvetica,sans-Serif" font-size="14.00">timestamptz</text>
<text text-anchor="start" x="5298" y="-6871.8" font-family="Helvetica,sans-Serif" font-size="14.00">last_update_time</text>
<text text-anchor="start" x="5449" y="-6871.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5457" y="-6871.8" font-family="Helvetica,sans-Serif" font-size="14.00">timestamptz not null</text>
<text text-anchor="start" x="5298" y="-6852.8" font-family="Helvetica,sans-Serif" font-size="14.00">relock_revision_id</text>
<text text-anchor="start" x="5449" y="-6852.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5457" y="-6852.8" font-family="Helvetica,sans-Serif" font-size="14.00">int8</text>
<text text-anchor="start" x="5298" y="-6833.8" font-family="Helvetica,sans-Serif" font-size="14.00">relock_duration</text>
<text text-anchor="start" x="5449" y="-6833.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5457" y="-6833.8" font-family="Helvetica,sans-Serif" font-size="14.00">interval</text>
<polygon fill="none" stroke="#888888" points="5294.5,-6827 5294.5,-7133 5581.5,-7133 5581.5,-6827 5294.5,-6827" />
</g> <!-- registrylock_ac88663e&#45;&gt;registrylock_ac88663e -->
<g id="edge64" class="edge">
<title>
registrylock_ac88663e:w-&gt;registrylock_ac88663e:e
</title>
<path fill="none" stroke="black" d="M5281.52,-6687.14C5203.21,-6759.94 5215.41,-6975 5438.5,-6975 5666.43,-6975 5674.22,-6963.81 5590.25,-6927.9" />
<polygon fill="black" stroke="black" points="5288.07,-6682.1 5298.74,-6679.57 5292.04,-6679.05 5296,-6676 5296,-6676 5296,-6676 5292.04,-6679.05 5293.26,-6672.43 5288.07,-6682.1 5288.07,-6682.1" />
<ellipse fill="none" stroke="black" cx="5284.9" cy="-6684.54" rx="4" ry="4" />
<polygon fill="black" stroke="black" points="5579.98,-6929 5583.87,-6919.78 5585.71,-6920.56 5581.82,-6929.77 5579.98,-6929" />
<polyline fill="none" stroke="black" points="5581,-6924 5585.61,-6925.94 " />
<polygon fill="black" stroke="black" points="5584.58,-6930.94 5588.47,-6921.73 5590.31,-6922.5 5586.43,-6931.72 5584.58,-6930.94" />
<polyline fill="none" stroke="black" points="5585.61,-6925.94 5590.21,-6927.89 " />
<text text-anchor="start" x="5356.5" y="-6978.8" font-family="Helvetica,sans-Serif" font-size="14.00">fk2lhcwpxlnqijr96irylrh1707</text>
<path fill="none" stroke="black" d="M5281.52,-6867.14C5203.21,-6939.94 5215.41,-7155 5438.5,-7155 5666.43,-7155 5674.22,-7143.81 5590.25,-7107.9" />
<polygon fill="black" stroke="black" points="5288.07,-6862.1 5298.74,-6859.57 5292.04,-6859.05 5296,-6856 5296,-6856 5296,-6856 5292.04,-6859.05 5293.26,-6852.43 5288.07,-6862.1 5288.07,-6862.1" />
<ellipse fill="none" stroke="black" cx="5284.9" cy="-6864.54" rx="4" ry="4" />
<polygon fill="black" stroke="black" points="5579.98,-7109 5583.87,-7099.78 5585.71,-7100.56 5581.82,-7109.77 5579.98,-7109" />
<polyline fill="none" stroke="black" points="5581,-7104 5585.61,-7105.94 " />
<polygon fill="black" stroke="black" points="5584.58,-7110.94 5588.47,-7101.73 5590.31,-7102.5 5586.43,-7111.72 5584.58,-7110.94" />
<polyline fill="none" stroke="black" points="5585.61,-7105.94 5590.21,-7107.89 " />
<text text-anchor="start" x="5356.5" y="-7158.8" font-family="Helvetica,sans-Serif" font-size="14.00">fk2lhcwpxlnqijr96irylrh1707</text>
</g> <!-- reservedentry_1a7b8520 -->
<g id="node40" class="node">
<g id="node41" class="node">
<title>
reservedentry_1a7b8520
</title>
<polygon fill="#e9c2f2" stroke="transparent" points="5323,-7089 5323,-7108 5480,-7108 5480,-7089 5323,-7089" />
<text text-anchor="start" x="5325" y="-7095.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."ReservedEntry"</text>
<polygon fill="#e9c2f2" stroke="transparent" points="5480,-7089 5480,-7108 5554,-7108 5554,-7089 5480,-7089" />
<text text-anchor="start" x="5515" y="-7094.8" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="5325" y="-7076.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">revision_id</text>
<text text-anchor="start" x="5451" y="-7075.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5482" y="-7075.8" font-family="Helvetica,sans-Serif" font-size="14.00">int8 not null</text>
<text text-anchor="start" x="5325" y="-7056.8" font-family="Helvetica,sans-Serif" font-size="14.00">comment</text>
<text text-anchor="start" x="5451" y="-7056.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5482" y="-7056.8" font-family="Helvetica,sans-Serif" font-size="14.00">text</text>
<text text-anchor="start" x="5325" y="-7037.8" font-family="Helvetica,sans-Serif" font-size="14.00">reservation_type</text>
<text text-anchor="start" x="5451" y="-7037.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5482" y="-7037.8" font-family="Helvetica,sans-Serif" font-size="14.00">int4 not null</text>
<text text-anchor="start" x="5325" y="-7019.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">domain_label</text>
<text text-anchor="start" x="5451" y="-7018.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5482" y="-7018.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<polygon fill="none" stroke="#888888" points="5321.5,-7012.5 5321.5,-7109.5 5554.5,-7109.5 5554.5,-7012.5 5321.5,-7012.5" />
<polygon fill="#e9c2f2" stroke="transparent" points="5323,-7269 5323,-7288 5480,-7288 5480,-7269 5323,-7269" />
<text text-anchor="start" x="5325" y="-7275.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."ReservedEntry"</text>
<polygon fill="#e9c2f2" stroke="transparent" points="5480,-7269 5480,-7288 5554,-7288 5554,-7269 5480,-7269" />
<text text-anchor="start" x="5515" y="-7274.8" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="5325" y="-7256.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">revision_id</text>
<text text-anchor="start" x="5451" y="-7255.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5482" y="-7255.8" font-family="Helvetica,sans-Serif" font-size="14.00">int8 not null</text>
<text text-anchor="start" x="5325" y="-7236.8" font-family="Helvetica,sans-Serif" font-size="14.00">comment</text>
<text text-anchor="start" x="5451" y="-7236.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5482" y="-7236.8" font-family="Helvetica,sans-Serif" font-size="14.00">text</text>
<text text-anchor="start" x="5325" y="-7217.8" font-family="Helvetica,sans-Serif" font-size="14.00">reservation_type</text>
<text text-anchor="start" x="5451" y="-7217.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5482" y="-7217.8" font-family="Helvetica,sans-Serif" font-size="14.00">int4 not null</text>
<text text-anchor="start" x="5325" y="-7199.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">domain_label</text>
<text text-anchor="start" x="5451" y="-7198.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5482" y="-7198.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<polygon fill="none" stroke="#888888" points="5321.5,-7192.5 5321.5,-7289.5 5554.5,-7289.5 5554.5,-7192.5 5321.5,-7192.5" />
</g> <!-- reservedlist_b97c3f1c -->
<g id="node41" class="node">
<g id="node42" class="node">
<title>
reservedlist_b97c3f1c
</title>
<polygon fill="#e9c2f2" stroke="transparent" points="4609,-7089 4609,-7108 4755,-7108 4755,-7089 4609,-7089" />
<text text-anchor="start" x="4611" y="-7095.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."ReservedList"</text>
<polygon fill="#e9c2f2" stroke="transparent" points="4755,-7089 4755,-7108 4881,-7108 4881,-7089 4755,-7089" />
<text text-anchor="start" x="4842" y="-7094.8" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="4611" y="-7076.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">revision_id</text>
<text text-anchor="start" x="4742" y="-7075.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4757" y="-7075.8" font-family="Helvetica,sans-Serif" font-size="14.00">bigserial not null</text>
<text text-anchor="start" x="4742" y="-7056.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4757" y="-7056.8" font-family="Helvetica,sans-Serif" font-size="14.00">auto-incremented</text>
<text text-anchor="start" x="4611" y="-7037.8" font-family="Helvetica,sans-Serif" font-size="14.00">creation_timestamp</text>
<text text-anchor="start" x="4742" y="-7037.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4757" y="-7037.8" font-family="Helvetica,sans-Serif" font-size="14.00">timestamptz not null</text>
<text text-anchor="start" x="4611" y="-7018.8" font-family="Helvetica,sans-Serif" font-size="14.00">name</text>
<text text-anchor="start" x="4742" y="-7018.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4757" y="-7018.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<polygon fill="none" stroke="#888888" points="4608,-7012.5 4608,-7109.5 4882,-7109.5 4882,-7012.5 4608,-7012.5" />
<polygon fill="#e9c2f2" stroke="transparent" points="4609,-7269 4609,-7288 4755,-7288 4755,-7269 4609,-7269" />
<text text-anchor="start" x="4611" y="-7275.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."ReservedList"</text>
<polygon fill="#e9c2f2" stroke="transparent" points="4755,-7269 4755,-7288 4881,-7288 4881,-7269 4755,-7269" />
<text text-anchor="start" x="4842" y="-7274.8" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="4611" y="-7256.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">revision_id</text>
<text text-anchor="start" x="4742" y="-7255.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4757" y="-7255.8" font-family="Helvetica,sans-Serif" font-size="14.00">bigserial not null</text>
<text text-anchor="start" x="4742" y="-7236.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4757" y="-7236.8" font-family="Helvetica,sans-Serif" font-size="14.00">auto-incremented</text>
<text text-anchor="start" x="4611" y="-7217.8" font-family="Helvetica,sans-Serif" font-size="14.00">creation_timestamp</text>
<text text-anchor="start" x="4742" y="-7217.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4757" y="-7217.8" font-family="Helvetica,sans-Serif" font-size="14.00">timestamptz not null</text>
<text text-anchor="start" x="4611" y="-7198.8" font-family="Helvetica,sans-Serif" font-size="14.00">name</text>
<text text-anchor="start" x="4742" y="-7198.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4757" y="-7198.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<polygon fill="none" stroke="#888888" points="4608,-7192.5 4608,-7289.5 4882,-7289.5 4882,-7192.5 4608,-7192.5" />
</g> <!-- reservedentry_1a7b8520&#45;&gt;reservedlist_b97c3f1c -->
<g id="edge65" class="edge">
<title>
reservedentry_1a7b8520:w-&gt;reservedlist_b97c3f1c:e
</title>
<path fill="none" stroke="black" d="M5303.81,-7080C5126.46,-7080 5073.26,-7080 4892.13,-7080" />
<polygon fill="black" stroke="black" points="5312,-7080 5322,-7084.5 5317,-7080 5322,-7080 5322,-7080 5322,-7080 5317,-7080 5322,-7075.5 5312,-7080 5312,-7080" />
<ellipse fill="none" stroke="black" cx="5308" cy="-7080" rx="4" ry="4" />
<polygon fill="black" stroke="black" points="4883,-7085 4883,-7075 4885,-7075 4885,-7085 4883,-7085" />
<polyline fill="none" stroke="black" points="4882,-7080 4887,-7080 " />
<polygon fill="black" stroke="black" points="4888,-7085 4888,-7075 4890,-7075 4890,-7085 4888,-7085" />
<polyline fill="none" stroke="black" points="4887,-7080 4892,-7080 " />
<text text-anchor="start" x="4972" y="-7083.8" font-family="Helvetica,sans-Serif" font-size="14.00">fkgq03rk0bt1hb915dnyvd3vnfc</text>
<path fill="none" stroke="black" d="M5303.81,-7260C5126.46,-7260 5073.26,-7260 4892.13,-7260" />
<polygon fill="black" stroke="black" points="5312,-7260 5322,-7264.5 5317,-7260 5322,-7260 5322,-7260 5322,-7260 5317,-7260 5322,-7255.5 5312,-7260 5312,-7260" />
<ellipse fill="none" stroke="black" cx="5308" cy="-7260" rx="4" ry="4" />
<polygon fill="black" stroke="black" points="4883,-7265 4883,-7255 4885,-7255 4885,-7265 4883,-7265" />
<polyline fill="none" stroke="black" points="4882,-7260 4887,-7260 " />
<polygon fill="black" stroke="black" points="4888,-7265 4888,-7255 4890,-7255 4890,-7265 4888,-7265" />
<polyline fill="none" stroke="black" points="4887,-7260 4892,-7260 " />
<text text-anchor="start" x="4972" y="-7263.8" font-family="Helvetica,sans-Serif" font-size="14.00">fkgq03rk0bt1hb915dnyvd3vnfc</text>
</g> <!-- serversecret_6cc90f09 -->
<g id="node42" class="node">
<g id="node43" class="node">
<title>
serversecret_6cc90f09
</title>
<polygon fill="#e9c2f2" stroke="transparent" points="5327,-7174 5327,-7193 5472,-7193 5472,-7174 5327,-7174" />
<text text-anchor="start" x="5329" y="-7180.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."ServerSecret"</text>
<polygon fill="#e9c2f2" stroke="transparent" points="5472,-7174 5472,-7193 5549,-7193 5549,-7174 5472,-7174" />
<text text-anchor="start" x="5510" y="-7179.8" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="5329" y="-7160.8" font-family="Helvetica,sans-Serif" font-size="14.00">secret</text>
<text text-anchor="start" x="5418" y="-7160.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5474" y="-7160.8" font-family="Helvetica,sans-Serif" font-size="14.00">uuid not null</text>
<text text-anchor="start" x="5329" y="-7142.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">id</text>
<text text-anchor="start" x="5418" y="-7141.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5474" y="-7141.8" font-family="Helvetica,sans-Serif" font-size="14.00">int8 not null</text>
<polygon fill="none" stroke="#888888" points="5326,-7135.5 5326,-7194.5 5550,-7194.5 5550,-7135.5 5326,-7135.5" />
<polygon fill="#e9c2f2" stroke="transparent" points="5327,-7354 5327,-7373 5472,-7373 5472,-7354 5327,-7354" />
<text text-anchor="start" x="5329" y="-7360.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."ServerSecret"</text>
<polygon fill="#e9c2f2" stroke="transparent" points="5472,-7354 5472,-7373 5549,-7373 5549,-7354 5472,-7354" />
<text text-anchor="start" x="5510" y="-7359.8" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="5329" y="-7340.8" font-family="Helvetica,sans-Serif" font-size="14.00">secret</text>
<text text-anchor="start" x="5418" y="-7340.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5474" y="-7340.8" font-family="Helvetica,sans-Serif" font-size="14.00">uuid not null</text>
<text text-anchor="start" x="5329" y="-7322.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">id</text>
<text text-anchor="start" x="5418" y="-7321.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5474" y="-7321.8" font-family="Helvetica,sans-Serif" font-size="14.00">int8 not null</text>
<polygon fill="none" stroke="#888888" points="5326,-7315.5 5326,-7374.5 5550,-7374.5 5550,-7315.5 5326,-7315.5" />
</g> <!-- signedmarkrevocationentry_99c39721 -->
<g id="node43" class="node">
<g id="node44" class="node">
<title>
signedmarkrevocationentry_99c39721
</title>
<polygon fill="#e9c2f2" stroke="transparent" points="5252,-7279 5252,-7298 5498,-7298 5498,-7279 5252,-7279" />
<text text-anchor="start" x="5254" y="-7285.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."SignedMarkRevocationEntry"</text>
<polygon fill="#e9c2f2" stroke="transparent" points="5498,-7279 5498,-7298 5624,-7298 5624,-7279 5498,-7279" />
<text text-anchor="start" x="5585" y="-7284.8" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="5254" y="-7266.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">revision_id</text>
<text text-anchor="start" x="5423" y="-7265.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5500" y="-7265.8" font-family="Helvetica,sans-Serif" font-size="14.00">int8 not null</text>
<text text-anchor="start" x="5254" y="-7246.8" font-family="Helvetica,sans-Serif" font-size="14.00">revocation_time</text>
<text text-anchor="start" x="5423" y="-7246.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5500" y="-7246.8" font-family="Helvetica,sans-Serif" font-size="14.00">timestamptz not null</text>
<text text-anchor="start" x="5254" y="-7228.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">smd_id</text>
<text text-anchor="start" x="5423" y="-7227.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5500" y="-7227.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<polygon fill="none" stroke="#888888" points="5251,-7221 5251,-7299 5625,-7299 5625,-7221 5251,-7221" />
<polygon fill="#e9c2f2" stroke="transparent" points="5252,-7459 5252,-7478 5498,-7478 5498,-7459 5252,-7459" />
<text text-anchor="start" x="5254" y="-7465.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."SignedMarkRevocationEntry"</text>
<polygon fill="#e9c2f2" stroke="transparent" points="5498,-7459 5498,-7478 5624,-7478 5624,-7459 5498,-7459" />
<text text-anchor="start" x="5585" y="-7464.8" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="5254" y="-7446.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">revision_id</text>
<text text-anchor="start" x="5423" y="-7445.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5500" y="-7445.8" font-family="Helvetica,sans-Serif" font-size="14.00">int8 not null</text>
<text text-anchor="start" x="5254" y="-7426.8" font-family="Helvetica,sans-Serif" font-size="14.00">revocation_time</text>
<text text-anchor="start" x="5423" y="-7426.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5500" y="-7426.8" font-family="Helvetica,sans-Serif" font-size="14.00">timestamptz not null</text>
<text text-anchor="start" x="5254" y="-7408.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">smd_id</text>
<text text-anchor="start" x="5423" y="-7407.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5500" y="-7407.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<polygon fill="none" stroke="#888888" points="5251,-7401 5251,-7479 5625,-7479 5625,-7401 5251,-7401" />
</g> <!-- signedmarkrevocationlist_c5d968fb -->
<g id="node44" class="node">
<g id="node45" class="node">
<title>
signedmarkrevocationlist_c5d968fb
</title>
<polygon fill="#e9c2f2" stroke="transparent" points="4572,-7279 4572,-7298 4808,-7298 4808,-7279 4572,-7279" />
<text text-anchor="start" x="4574" y="-7285.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."SignedMarkRevocationList"</text>
<polygon fill="#e9c2f2" stroke="transparent" points="4808,-7279 4808,-7298 4918,-7298 4918,-7279 4808,-7279" />
<text text-anchor="start" x="4879" y="-7284.8" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="4574" y="-7266.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">revision_id</text>
<text text-anchor="start" x="4731" y="-7265.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4810" y="-7265.8" font-family="Helvetica,sans-Serif" font-size="14.00">bigserial not null</text>
<text text-anchor="start" x="4731" y="-7246.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4810" y="-7246.8" font-family="Helvetica,sans-Serif" font-size="14.00">auto-incremented</text>
<text text-anchor="start" x="4574" y="-7227.8" font-family="Helvetica,sans-Serif" font-size="14.00">creation_time</text>
<text text-anchor="start" x="4731" y="-7227.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4810" y="-7227.8" font-family="Helvetica,sans-Serif" font-size="14.00">timestamptz</text>
<polygon fill="none" stroke="#888888" points="4571,-7221 4571,-7299 4919,-7299 4919,-7221 4571,-7221" />
<polygon fill="#e9c2f2" stroke="transparent" points="4572,-7459 4572,-7478 4808,-7478 4808,-7459 4572,-7459" />
<text text-anchor="start" x="4574" y="-7465.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."SignedMarkRevocationList"</text>
<polygon fill="#e9c2f2" stroke="transparent" points="4808,-7459 4808,-7478 4918,-7478 4918,-7459 4808,-7459" />
<text text-anchor="start" x="4879" y="-7464.8" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="4574" y="-7446.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">revision_id</text>
<text text-anchor="start" x="4731" y="-7445.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4810" y="-7445.8" font-family="Helvetica,sans-Serif" font-size="14.00">bigserial not null</text>
<text text-anchor="start" x="4731" y="-7426.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4810" y="-7426.8" font-family="Helvetica,sans-Serif" font-size="14.00">auto-incremented</text>
<text text-anchor="start" x="4574" y="-7407.8" font-family="Helvetica,sans-Serif" font-size="14.00">creation_time</text>
<text text-anchor="start" x="4731" y="-7407.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="4810" y="-7407.8" font-family="Helvetica,sans-Serif" font-size="14.00">timestamptz</text>
<polygon fill="none" stroke="#888888" points="4571,-7401 4571,-7479 4919,-7479 4919,-7401 4571,-7401" />
</g> <!-- signedmarkrevocationentry_99c39721&#45;&gt;signedmarkrevocationlist_c5d968fb -->
<g id="edge66" class="edge">
<title>
signedmarkrevocationentry_99c39721:w-&gt;signedmarkrevocationlist_c5d968fb:e
</title>
<path fill="none" stroke="black" d="M5232.98,-7270C5103.39,-7270 5062.36,-7270 4929.17,-7270" />
<polygon fill="black" stroke="black" points="5241,-7270 5251,-7274.5 5246,-7270 5251,-7270 5251,-7270 5251,-7270 5246,-7270 5251,-7265.5 5241,-7270 5241,-7270" />
<ellipse fill="none" stroke="black" cx="5237" cy="-7270" rx="4" ry="4" />
<polygon fill="black" stroke="black" points="4920,-7275 4920,-7265 4922,-7265 4922,-7275 4920,-7275" />
<polyline fill="none" stroke="black" points="4919,-7270 4924,-7270 " />
<polygon fill="black" stroke="black" points="4925,-7275 4925,-7265 4927,-7265 4927,-7275 4925,-7275" />
<polyline fill="none" stroke="black" points="4924,-7270 4929,-7270 " />
<text text-anchor="start" x="4981" y="-7273.8" font-family="Helvetica,sans-Serif" font-size="14.00">fk5ivlhvs3121yx2li5tqh54u4</text>
<path fill="none" stroke="black" d="M5232.98,-7450C5103.39,-7450 5062.36,-7450 4929.17,-7450" />
<polygon fill="black" stroke="black" points="5241,-7450 5251,-7454.5 5246,-7450 5251,-7450 5251,-7450 5251,-7450 5246,-7450 5251,-7445.5 5241,-7450 5241,-7450" />
<ellipse fill="none" stroke="black" cx="5237" cy="-7450" rx="4" ry="4" />
<polygon fill="black" stroke="black" points="4920,-7455 4920,-7445 4922,-7445 4922,-7455 4920,-7455" />
<polyline fill="none" stroke="black" points="4919,-7450 4924,-7450 " />
<polygon fill="black" stroke="black" points="4925,-7455 4925,-7445 4927,-7445 4927,-7455 4925,-7455" />
<polyline fill="none" stroke="black" points="4924,-7450 4929,-7450 " />
<text text-anchor="start" x="4981" y="-7453.8" font-family="Helvetica,sans-Serif" font-size="14.00">fk5ivlhvs3121yx2li5tqh54u4</text>
</g> <!-- spec11threatmatch_a61228a6 -->
<g id="node45" class="node">
<g id="node46" class="node">
<title>
spec11threatmatch_a61228a6
</title>
<polygon fill="#e9c2f2" stroke="transparent" points="5288,-7478 5288,-7497 5478,-7497 5478,-7478 5288,-7478" />
<text text-anchor="start" x="5290" y="-7484.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."Spec11ThreatMatch"</text>
<polygon fill="#e9c2f2" stroke="transparent" points="5478,-7478 5478,-7497 5588,-7497 5588,-7478 5478,-7478" />
<text text-anchor="start" x="5549" y="-7483.8" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="5290" y="-7465.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">id</text>
<text text-anchor="start" x="5432" y="-7464.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5480" y="-7464.8" font-family="Helvetica,sans-Serif" font-size="14.00">bigserial not null</text>
<text text-anchor="start" x="5432" y="-7445.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5480" y="-7445.8" font-family="Helvetica,sans-Serif" font-size="14.00">auto-incremented</text>
<text text-anchor="start" x="5290" y="-7426.8" font-family="Helvetica,sans-Serif" font-size="14.00">check_date</text>
<text text-anchor="start" x="5432" y="-7426.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5480" y="-7426.8" font-family="Helvetica,sans-Serif" font-size="14.00">date not null</text>
<text text-anchor="start" x="5290" y="-7407.8" font-family="Helvetica,sans-Serif" font-size="14.00">domain_name</text>
<text text-anchor="start" x="5432" y="-7407.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5480" y="-7407.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<text text-anchor="start" x="5290" y="-7388.8" font-family="Helvetica,sans-Serif" font-size="14.00">domain_repo_id</text>
<text text-anchor="start" x="5432" y="-7388.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5480" y="-7388.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<text text-anchor="start" x="5290" y="-7369.8" font-family="Helvetica,sans-Serif" font-size="14.00">registrar_id</text>
<text text-anchor="start" x="5432" y="-7369.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5480" y="-7369.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<text text-anchor="start" x="5290" y="-7350.8" font-family="Helvetica,sans-Serif" font-size="14.00">threat_types</text>
<text text-anchor="start" x="5432" y="-7350.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5480" y="-7350.8" font-family="Helvetica,sans-Serif" font-size="14.00">_text not null</text>
<text text-anchor="start" x="5290" y="-7331.8" font-family="Helvetica,sans-Serif" font-size="14.00">tld</text>
<text text-anchor="start" x="5432" y="-7331.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5480" y="-7331.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<polygon fill="none" stroke="#888888" points="5287,-7325.5 5287,-7498.5 5589,-7498.5 5589,-7325.5 5287,-7325.5" />
<polygon fill="#e9c2f2" stroke="transparent" points="5288,-7658 5288,-7677 5478,-7677 5478,-7658 5288,-7658" />
<text text-anchor="start" x="5290" y="-7664.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."Spec11ThreatMatch"</text>
<polygon fill="#e9c2f2" stroke="transparent" points="5478,-7658 5478,-7677 5588,-7677 5588,-7658 5478,-7658" />
<text text-anchor="start" x="5549" y="-7663.8" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="5290" y="-7645.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">id</text>
<text text-anchor="start" x="5432" y="-7644.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5480" y="-7644.8" font-family="Helvetica,sans-Serif" font-size="14.00">bigserial not null</text>
<text text-anchor="start" x="5432" y="-7625.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5480" y="-7625.8" font-family="Helvetica,sans-Serif" font-size="14.00">auto-incremented</text>
<text text-anchor="start" x="5290" y="-7606.8" font-family="Helvetica,sans-Serif" font-size="14.00">check_date</text>
<text text-anchor="start" x="5432" y="-7606.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5480" y="-7606.8" font-family="Helvetica,sans-Serif" font-size="14.00">date not null</text>
<text text-anchor="start" x="5290" y="-7587.8" font-family="Helvetica,sans-Serif" font-size="14.00">domain_name</text>
<text text-anchor="start" x="5432" y="-7587.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5480" y="-7587.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<text text-anchor="start" x="5290" y="-7568.8" font-family="Helvetica,sans-Serif" font-size="14.00">domain_repo_id</text>
<text text-anchor="start" x="5432" y="-7568.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5480" y="-7568.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<text text-anchor="start" x="5290" y="-7549.8" font-family="Helvetica,sans-Serif" font-size="14.00">registrar_id</text>
<text text-anchor="start" x="5432" y="-7549.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5480" y="-7549.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<text text-anchor="start" x="5290" y="-7530.8" font-family="Helvetica,sans-Serif" font-size="14.00">threat_types</text>
<text text-anchor="start" x="5432" y="-7530.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5480" y="-7530.8" font-family="Helvetica,sans-Serif" font-size="14.00">_text not null</text>
<text text-anchor="start" x="5290" y="-7511.8" font-family="Helvetica,sans-Serif" font-size="14.00">tld</text>
<text text-anchor="start" x="5432" y="-7511.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5480" y="-7511.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<polygon fill="none" stroke="#888888" points="5287,-7505.5 5287,-7678.5 5589,-7678.5 5589,-7505.5 5287,-7505.5" />
</g> <!-- tmchcrl_d282355 -->
<g id="node47" class="node">
<g id="node48" class="node">
<title>
tmchcrl_d282355
</title>
<polygon fill="#e9c2f2" stroke="transparent" points="5302,-7601 5302,-7620 5449,-7620 5449,-7601 5302,-7601" />
<text text-anchor="start" x="5304" y="-7607.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."TmchCrl"</text>
<polygon fill="#e9c2f2" stroke="transparent" points="5449,-7601 5449,-7620 5575,-7620 5575,-7601 5449,-7601" />
<text text-anchor="start" x="5536" y="-7606.8" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="5304" y="-7587.8" font-family="Helvetica,sans-Serif" font-size="14.00">certificate_revocations</text>
<text text-anchor="start" x="5443" y="-7587.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5451" y="-7587.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<text text-anchor="start" x="5304" y="-7568.8" font-family="Helvetica,sans-Serif" font-size="14.00">update_timestamp</text>
<text text-anchor="start" x="5443" y="-7568.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5451" y="-7568.8" font-family="Helvetica,sans-Serif" font-size="14.00">timestamptz not null</text>
<text text-anchor="start" x="5304" y="-7549.8" font-family="Helvetica,sans-Serif" font-size="14.00">url</text>
<text text-anchor="start" x="5443" y="-7549.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5451" y="-7549.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<text text-anchor="start" x="5304" y="-7531.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">id</text>
<text text-anchor="start" x="5443" y="-7530.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5451" y="-7530.8" font-family="Helvetica,sans-Serif" font-size="14.00">int8 not null</text>
<polygon fill="none" stroke="#888888" points="5300.5,-7524.5 5300.5,-7621.5 5575.5,-7621.5 5575.5,-7524.5 5300.5,-7524.5" />
<polygon fill="#e9c2f2" stroke="transparent" points="5302,-7781 5302,-7800 5449,-7800 5449,-7781 5302,-7781" />
<text text-anchor="start" x="5304" y="-7787.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."TmchCrl"</text>
<polygon fill="#e9c2f2" stroke="transparent" points="5449,-7781 5449,-7800 5575,-7800 5575,-7781 5449,-7781" />
<text text-anchor="start" x="5536" y="-7786.8" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="5304" y="-7767.8" font-family="Helvetica,sans-Serif" font-size="14.00">certificate_revocations</text>
<text text-anchor="start" x="5443" y="-7767.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5451" y="-7767.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<text text-anchor="start" x="5304" y="-7748.8" font-family="Helvetica,sans-Serif" font-size="14.00">update_timestamp</text>
<text text-anchor="start" x="5443" y="-7748.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5451" y="-7748.8" font-family="Helvetica,sans-Serif" font-size="14.00">timestamptz not null</text>
<text text-anchor="start" x="5304" y="-7729.8" font-family="Helvetica,sans-Serif" font-size="14.00">url</text>
<text text-anchor="start" x="5443" y="-7729.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5451" y="-7729.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<text text-anchor="start" x="5304" y="-7711.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">id</text>
<text text-anchor="start" x="5443" y="-7710.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5451" y="-7710.8" font-family="Helvetica,sans-Serif" font-size="14.00">int8 not null</text>
<polygon fill="none" stroke="#888888" points="5300.5,-7704.5 5300.5,-7801.5 5575.5,-7801.5 5575.5,-7704.5 5300.5,-7704.5" />
</g> <!-- userupdatehistory_24efd476 -->
<g id="node49" class="node">
<g id="node50" class="node">
<title>
userupdatehistory_24efd476
</title>
<polygon fill="#e9c2f2" stroke="transparent" points="5280,-7934 5280,-7953 5470,-7953 5470,-7934 5280,-7934" />
<text text-anchor="start" x="5282" y="-7940.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."UserUpdateHistory"</text>
<polygon fill="#e9c2f2" stroke="transparent" points="5470,-7934 5470,-7953 5596,-7953 5596,-7934 5470,-7934" />
<text text-anchor="start" x="5557" y="-7939.8" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="5282" y="-7921.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">history_revision_id</text>
<text text-anchor="start" x="5464" y="-7920.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5472" y="-7920.8" font-family="Helvetica,sans-Serif" font-size="14.00">int8 not null</text>
<text text-anchor="start" x="5282" y="-7901.8" font-family="Helvetica,sans-Serif" font-size="14.00">history_modification_time</text>
<text text-anchor="start" x="5464" y="-7901.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5472" y="-7901.8" font-family="Helvetica,sans-Serif" font-size="14.00">timestamptz not null</text>
<text text-anchor="start" x="5282" y="-7882.8" font-family="Helvetica,sans-Serif" font-size="14.00">history_method</text>
<text text-anchor="start" x="5464" y="-7882.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5472" y="-7882.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<text text-anchor="start" x="5282" y="-7863.8" font-family="Helvetica,sans-Serif" font-size="14.00">history_request_body</text>
<text text-anchor="start" x="5464" y="-7863.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5472" y="-7863.8" font-family="Helvetica,sans-Serif" font-size="14.00">text</text>
<text text-anchor="start" x="5282" y="-7844.8" font-family="Helvetica,sans-Serif" font-size="14.00">history_type</text>
<text text-anchor="start" x="5464" y="-7844.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5472" y="-7844.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<text text-anchor="start" x="5282" y="-7825.8" font-family="Helvetica,sans-Serif" font-size="14.00">history_url</text>
<text text-anchor="start" x="5464" y="-7825.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5472" y="-7825.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<text text-anchor="start" x="5282" y="-7806.8" font-family="Helvetica,sans-Serif" font-size="14.00">email_address</text>
<text text-anchor="start" x="5464" y="-7806.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5472" y="-7806.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<text text-anchor="start" x="5282" y="-7787.8" font-family="Helvetica,sans-Serif" font-size="14.00">registry_lock_password_hash</text>
<text text-anchor="start" x="5464" y="-7787.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5472" y="-7787.8" font-family="Helvetica,sans-Serif" font-size="14.00">text</text>
<text text-anchor="start" x="5282" y="-7768.8" font-family="Helvetica,sans-Serif" font-size="14.00">registry_lock_password_salt</text>
<text text-anchor="start" x="5464" y="-7768.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5472" y="-7768.8" font-family="Helvetica,sans-Serif" font-size="14.00">text</text>
<text text-anchor="start" x="5282" y="-7749.8" font-family="Helvetica,sans-Serif" font-size="14.00">global_role</text>
<text text-anchor="start" x="5464" y="-7749.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5472" y="-7749.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<text text-anchor="start" x="5282" y="-7730.8" font-family="Helvetica,sans-Serif" font-size="14.00">is_admin</text>
<text text-anchor="start" x="5464" y="-7730.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5472" y="-7730.8" font-family="Helvetica,sans-Serif" font-size="14.00">bool not null</text>
<text text-anchor="start" x="5282" y="-7711.8" font-family="Helvetica,sans-Serif" font-size="14.00">registrar_roles</text>
<text text-anchor="start" x="5464" y="-7711.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5472" y="-7711.8" font-family="Helvetica,sans-Serif" font-size="14.00">hstore</text>
<text text-anchor="start" x="5282" y="-7692.8" font-family="Helvetica,sans-Serif" font-size="14.00">update_timestamp</text>
<text text-anchor="start" x="5464" y="-7692.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5472" y="-7692.8" font-family="Helvetica,sans-Serif" font-size="14.00">timestamptz</text>
<text text-anchor="start" x="5282" y="-7673.8" font-family="Helvetica,sans-Serif" font-size="14.00">history_acting_user</text>
<text text-anchor="start" x="5464" y="-7673.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5472" y="-7673.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<text text-anchor="start" x="5282" y="-7654.8" font-family="Helvetica,sans-Serif" font-size="14.00">registry_lock_email_address</text>
<text text-anchor="start" x="5464" y="-7654.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5472" y="-7654.8" font-family="Helvetica,sans-Serif" font-size="14.00">text</text>
<polygon fill="none" stroke="#888888" points="5279,-7648 5279,-7954 5597,-7954 5597,-7648 5279,-7648" />
<polygon fill="#e9c2f2" stroke="transparent" points="5280,-8114 5280,-8133 5470,-8133 5470,-8114 5280,-8114" />
<text text-anchor="start" x="5282" y="-8120.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."UserUpdateHistory"</text>
<polygon fill="#e9c2f2" stroke="transparent" points="5470,-8114 5470,-8133 5596,-8133 5596,-8114 5470,-8114" />
<text text-anchor="start" x="5557" y="-8119.8" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="5282" y="-8101.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">history_revision_id</text>
<text text-anchor="start" x="5464" y="-8100.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5472" y="-8100.8" font-family="Helvetica,sans-Serif" font-size="14.00">int8 not null</text>
<text text-anchor="start" x="5282" y="-8081.8" font-family="Helvetica,sans-Serif" font-size="14.00">history_modification_time</text>
<text text-anchor="start" x="5464" y="-8081.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5472" y="-8081.8" font-family="Helvetica,sans-Serif" font-size="14.00">timestamptz not null</text>
<text text-anchor="start" x="5282" y="-8062.8" font-family="Helvetica,sans-Serif" font-size="14.00">history_method</text>
<text text-anchor="start" x="5464" y="-8062.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5472" y="-8062.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<text text-anchor="start" x="5282" y="-8043.8" font-family="Helvetica,sans-Serif" font-size="14.00">history_request_body</text>
<text text-anchor="start" x="5464" y="-8043.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5472" y="-8043.8" font-family="Helvetica,sans-Serif" font-size="14.00">text</text>
<text text-anchor="start" x="5282" y="-8024.8" font-family="Helvetica,sans-Serif" font-size="14.00">history_type</text>
<text text-anchor="start" x="5464" y="-8024.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5472" y="-8024.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<text text-anchor="start" x="5282" y="-8005.8" font-family="Helvetica,sans-Serif" font-size="14.00">history_url</text>
<text text-anchor="start" x="5464" y="-8005.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5472" y="-8005.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<text text-anchor="start" x="5282" y="-7986.8" font-family="Helvetica,sans-Serif" font-size="14.00">email_address</text>
<text text-anchor="start" x="5464" y="-7986.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5472" y="-7986.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<text text-anchor="start" x="5282" y="-7967.8" font-family="Helvetica,sans-Serif" font-size="14.00">registry_lock_password_hash</text>
<text text-anchor="start" x="5464" y="-7967.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5472" y="-7967.8" font-family="Helvetica,sans-Serif" font-size="14.00">text</text>
<text text-anchor="start" x="5282" y="-7948.8" font-family="Helvetica,sans-Serif" font-size="14.00">registry_lock_password_salt</text>
<text text-anchor="start" x="5464" y="-7948.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5472" y="-7948.8" font-family="Helvetica,sans-Serif" font-size="14.00">text</text>
<text text-anchor="start" x="5282" y="-7929.8" font-family="Helvetica,sans-Serif" font-size="14.00">global_role</text>
<text text-anchor="start" x="5464" y="-7929.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5472" y="-7929.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<text text-anchor="start" x="5282" y="-7910.8" font-family="Helvetica,sans-Serif" font-size="14.00">is_admin</text>
<text text-anchor="start" x="5464" y="-7910.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5472" y="-7910.8" font-family="Helvetica,sans-Serif" font-size="14.00">bool not null</text>
<text text-anchor="start" x="5282" y="-7891.8" font-family="Helvetica,sans-Serif" font-size="14.00">registrar_roles</text>
<text text-anchor="start" x="5464" y="-7891.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5472" y="-7891.8" font-family="Helvetica,sans-Serif" font-size="14.00">hstore</text>
<text text-anchor="start" x="5282" y="-7872.8" font-family="Helvetica,sans-Serif" font-size="14.00">update_timestamp</text>
<text text-anchor="start" x="5464" y="-7872.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5472" y="-7872.8" font-family="Helvetica,sans-Serif" font-size="14.00">timestamptz</text>
<text text-anchor="start" x="5282" y="-7853.8" font-family="Helvetica,sans-Serif" font-size="14.00">history_acting_user</text>
<text text-anchor="start" x="5464" y="-7853.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5472" y="-7853.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text>
<text text-anchor="start" x="5282" y="-7834.8" font-family="Helvetica,sans-Serif" font-size="14.00">registry_lock_email_address</text>
<text text-anchor="start" x="5464" y="-7834.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="5472" y="-7834.8" font-family="Helvetica,sans-Serif" font-size="14.00">text</text>
<polygon fill="none" stroke="#888888" points="5279,-7828 5279,-8134 5597,-8134 5597,-7828 5279,-7828" />
</g>
</g>
</svg>
@@ -9870,6 +9901,85 @@ td.section {
</tbody>
</table>
<p>&nbsp;</p>
<table>
<caption style="background-color: #E9C2F2;">
<span id="passwordresetrequest_8484e7b1" class="caption_name">public."PasswordResetRequest"</span> <span class="caption_description">[table]</span>
</caption>
<tbody>
<tr>
<td class="spacer"></td>
<td class="minwidth">type</td>
<td class="minwidth">text not null</td>
</tr>
<tr>
<td class="spacer"></td>
<td class="minwidth">request_time</td>
<td class="minwidth">timestamptz not null</td>
</tr>
<tr>
<td class="spacer"></td>
<td class="minwidth">requester</td>
<td class="minwidth">text not null</td>
</tr>
<tr>
<td class="spacer"></td>
<td class="minwidth">fulfillment_time</td>
<td class="minwidth">timestamptz</td>
</tr>
<tr>
<td class="spacer"></td>
<td class="minwidth">destination_email</td>
<td class="minwidth">text not null</td>
</tr>
<tr>
<td class="spacer"></td>
<td class="minwidth"><b><i>verification_code</i></b></td>
<td class="minwidth">text not null</td>
</tr>
<tr>
<td class="spacer"></td>
<td class="minwidth">registrar_id</td>
<td class="minwidth">text not null</td>
</tr>
<tr>
<td colspan="3"></td>
</tr>
<tr>
<td colspan="3" class="section">Primary Key</td>
</tr>
<tr>
<td colspan="3"></td>
</tr>
<tr>
<td colspan="2" class="name">"PasswordResetRequest_pkey"</td>
<td class="description right">[primary key]</td>
</tr>
<tr>
<td class="spacer"></td>
<td class="minwidth">verification_code</td>
<td class="minwidth"></td>
</tr>
<tr>
<td colspan="3"></td>
</tr>
<tr>
<td colspan="3" class="section">Indexes</td>
</tr>
<tr>
<td colspan="3"></td>
</tr>
<tr>
<td colspan="2" class="name">"PasswordResetRequest_pkey"</td>
<td class="description right">[unique index]</td>
</tr>
<tr>
<td class="spacer"></td>
<td class="minwidth">verification_code</td>
<td class="minwidth">ascending</td>
</tr>
</tbody>
</table>
<p>&nbsp;</p>
<table>
<caption style="background-color: #E9C2F2;">
<span id="pollmessage_614a523e" class="caption_name">public."PollMessage"</span> <span class="caption_description">[table]</span>
+2
View File
@@ -190,3 +190,5 @@ V189__remove_fk_consoleeppactionhistory.sql
V190__remove_fk_registrarupdatehistory.sql
V191__remove_fk_registrarpocupdatehistory.sql
V192__add_last_poc_verification_date.sql
V193__password_reset_request.sql
V194__password_reset_request_registrar.sql
@@ -0,0 +1,23 @@
-- Copyright 2025 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.
CREATE TABLE "PasswordResetRequest" (
type text NOT NULL,
request_time timestamptz NOT NULL,
requester text NOT NULL,
fulfillment_time timestamptz,
destination_email text NOT NULL,
verification_code text NOT NULL,
PRIMARY KEY (verification_code)
);
@@ -0,0 +1,17 @@
-- Copyright 2025 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 "PasswordResetRequest" ADD COLUMN registrar_id text;
UPDATE "PasswordResetRequest" SET registrar_id = '' WHERE registrar_id IS NULL;
ALTER TABLE "PasswordResetRequest" ALTER COLUMN registrar_id SET NOT NULL;
@@ -646,6 +646,7 @@
last_certificate_update_time timestamp(6) with time zone,
last_expiring_cert_notification_sent_date timestamp(6) with time zone,
last_expiring_failover_cert_notification_sent_date timestamp(6) with time zone,
last_poc_verification_date timestamp(6) with time zone,
localized_address_city text,
localized_address_country_code text,
localized_address_state text,
@@ -842,6 +842,21 @@ CREATE SEQUENCE public."Package_promotion_id_seq"
ALTER SEQUENCE public."Package_promotion_id_seq" OWNED BY public."PackagePromotion".package_promotion_id;
--
-- Name: PasswordResetRequest; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public."PasswordResetRequest" (
type text NOT NULL,
request_time timestamp with time zone NOT NULL,
requester text NOT NULL,
fulfillment_time timestamp with time zone,
destination_email text NOT NULL,
verification_code text NOT NULL,
registrar_id text NOT NULL
);
--
-- Name: PollMessage; Type: TABLE; Schema: public; Owner: -
--
@@ -1682,6 +1697,14 @@ ALTER TABLE ONLY public."PackagePromotion"
ADD CONSTRAINT "PackagePromotion_pkey" PRIMARY KEY (package_promotion_id);
--
-- Name: PasswordResetRequest PasswordResetRequest_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public."PasswordResetRequest"
ADD CONSTRAINT "PasswordResetRequest_pkey" PRIMARY KEY (verification_code);
--
-- Name: PollMessage PollMessage_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
@@ -61,9 +61,6 @@ public enum RegistryEnvironment {
/** Name of the environmental variable of the container name. */
private static final String CONTAINER_ENV = "CONTAINER_NAME";
private static final boolean ON_JETTY =
Boolean.parseBoolean(System.getProperty(JETTY_PROPERTY, "false"));
private static final boolean IS_CANARY =
System.getenv().getOrDefault(CONTAINER_ENV, "").endsWith("-canary");
@@ -100,8 +97,9 @@ public enum RegistryEnvironment {
return valueOf(Ascii.toUpperCase(System.getProperty(PROPERTY, UNITTEST.name())));
}
// TODO(b/416299900): remove method after GAE is removed.
public static boolean isOnJetty() {
return ON_JETTY;
return Boolean.parseBoolean(System.getProperty(JETTY_PROPERTY, "false"));
}
public static boolean isCanary() {