Skip to content

Commit 511b0cc

Browse files
committed
Migrate TMCH, SMD, Mark models, EPP InfoData, and Fee Extensions to use java.time.Instant
This commit completes the java.time migration for the Trademark Clearinghouse (TMCH), Signed Mark Data (SMD), and Mark models, the EPP Response & InfoData Objects, and the Fee Extension ecosystem, transitioning them from Joda-Time's DateTime to java.time.Instant. Changes made: 1. Core Models Migration: - ClaimsList: Converted tmdbGenerationTime to Instant. Removed deprecated toDateTime bridge methods and replaced them directly with Instant getters. - TmchCrl: Converted updated to Instant, utilizing tm().getTxTime() natively for accurate database transaction time injection. - SignedMark: Updated creationTime and expirationTime to use Instant. - SignedMarkRevocationList: Changed creationTime to Instant and the map values of revokes from DateTime to Instant. - Mark Models (ProtectedMark, Trademark, TreatyOrStatuteMark): Migrated all date properties (protectionDate, applicationDate, registrationDate, expirationDate, executionDate) to Instant. - PremiumPricingEngine & PricingEngineProxy: Migrated priceTime parameters and DomainPrices outputs to Instant. 2. Fee Extensions & EPP Response Models: - BaseFee & Fee: Converted validDateRange to Range<Instant> and eliminated getValidDateRangeInstant bridge methods. - All Fee Extension Versions (v06, v11, v12, stdv1): Migrated effectiveDate and notAfterDate fields to Instant. - DomainInfoData, HostInfoData, ContactInfoData, CreateData, Greeting, MessageQueueInfo: Migrated all date fields to Instant. 3. JAXB XML Adapter Updates: - Added and applied UtcInstantAdapter.class in package-info.java definitions (smd, mark, contact, eppoutput, fee*) to natively marshall/unmarshall Instant properties instead of DateTime. 4. Parsers & Flow Integrations: - SmdrlCsvParser & ClaimsListParser: Modified to invoke Instant.parse() when parsing external CSV artifacts. Added new strict header validation and row consistency checks to ClaimsListParser to ensure records match expected fields before parsing. - DomainPricingLogic: Updated to natively pass and compute Instant for all domain pricing, renewals, and restore logic. - DomainFlowTmchUtils and DomainCreateFlow: Adapted method signatures and callers to natively receive and validate against Instant instead of relying on conversions, updating chronological comparisons to leverage isBefore() and isAfter(). - Stripped out toDateTime() conversions previously necessary when mapping between core entities and these EPP response objects across multiple flow classes (e.g. DomainInfoFlow, HostInfoFlow, DomainCreateFlow, HostCreateFlow, PollRequestFlow, and HelloFlow). 5. Test Suite Refactoring: - Transformed org.joda.time.DateTime usage into java.time.Instant in associated tests (SmdrlCsvParserTest, TmchTestDataExpirationTest, UploadClaimsListCommandTest, SignedMarkRevocationListTest, DomainPricingLogicTest, etc.). - Replaced uses of fakeClock.nowUtc() with the idiomatic fakeClock.now(). - Fixed broken tests in UploadClaimsListCommandTest where invalid formats like "foo" previously generated Joda IllegalArgumentException on the first row, hiding the fact that subsequent header/body validation logic was never exercised. The tests now use a valid timestamp header row to properly trigger IllegalArgumentException for structural assertions, and assert DateTimeParseException correctly where applicable. 6. Refinement: - Added explicit guidelines to GEMINI.md to avoid calling toInstant() and toDateTime() when equivalent native alternatives like tm().getTxTime() exist. - Added guidelines on avoiding Python scripts for formatting in favor of ./gradlew javaIncrementalFormatApply. - Added guidelines on checking exceptions in test suites when migrating parsing logic.
1 parent 3de790f commit 511b0cc

77 files changed

Lines changed: 875 additions & 957 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

GEMINI.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ This document outlines foundational mandates, architectural patterns, and projec
1515

1616
## 2. Time and Precision Handling (java.time Migration)
1717

18-
- **Idiomatic java.time Usage:** Avoid redundant conversions between `Instant` and `DateTime`. If a field or parameter is an `Instant`, use it directly. Do not convert to `DateTime` just to call a deprecated method if an `Instant` alternative exists or can be easily created.
18+
- **Idiomatic java.time Usage:** Avoid redundant conversions between `Instant` and `DateTime`. If a field or parameter is an `Instant`, use it directly. Do not convert to `DateTime` just to call a deprecated method if an `Instant` alternative exists or can be easily created. Furthermore, you should not call `toInstant()` or `toDateTime()` conversion methods when not strictly necessary; always prefer to use an alternative method that returns the correct type if one exists (e.g. use `tm().getTxTime()` which returns an `Instant` instead of calling `tm().getTransactionTime().toInstant()`).
1919
- **Millisecond Precision:** Always truncate `Instant.now()` to milliseconds (using `.truncatedTo(ChronoUnit.MILLIS)`) to maintain consistency with Joda `DateTime` and the PostgreSQL schema (which enforces millisecond precision via JPA converters).
2020
- **Clock Injection:**
2121
- Avoid direct calls to `Instant.now()`, `DateTime.now()`, `ZonedDateTime.now()`, or `System.currentTimeMillis()`.
@@ -56,6 +56,7 @@ This document outlines foundational mandates, architectural patterns, and projec
5656
## Performance and Efficiency
5757
- **Turn Minimization:** Aim for "perfect" code in the first iteration. Iterative fixes for checkstyle or compilation errors consume significant context and time.
5858
- **Context Management:** Use sub-agents for batch refactoring or high-volume output tasks to keep the main session history lean and efficient.
59+
- **Code Formatting:** Do not write custom Python scripts or manual regex replacements to fix code formatting issues (e.g., unused imports, import ordering, line length). Instead, use the project's built-in formatting tools: run `./gradlew spotlessApply` to fix unused/unordered imports and `./gradlew javaIncrementalFormatApply` (or `google-java-format --replace <files>`) to automatically fix Java formatting and indentation errors.
5960

6061
## General Code Review Lessons & Avoidable Mistakes
6162
Based on historical PR reviews, avoid the following common mistakes:
@@ -90,8 +91,12 @@ This document captures high-level architectural patterns, lessons learned from l
9091
- **One Commit Per PR:** All changes for a single PR must be squashed into a single commit before merging.
9192
- **Default to Amend:** Once an initial commit is created for a PR, all subsequent functional changes should be amended into that same commit by default (`git commit --amend --no-edit`). This ensures the PR remains a single, clean unit of work throughout the development lifecycle.
9293
- **Commit Message Style:** Follow standard Git commit best practices. The subject line (first line) should be concise, capitalized, and **must not end with punctuation** (e.g., a period).
93-
- **Final Validation:** Always run `git status` as the final step before declaring a task complete to ensure all changes are committed and the working directory is clean.
94-
- **Commit Verification:** After any commit or amendment, explicitly verify the success of the operation (e.g., using `git status` and reviewing the diff). Never report a Git operation as "done" without having first successfully executed the command and confirmed the repository state.
94+
- **Strict Completion Verification:** You MUST NEVER declare a task, commit, or amendment as complete until you have explicitly verified that the workspace is clean. You MUST follow this exact sequence of actions across multiple conversational turns if necessary:
95+
1. Execute the `git commit` or `git commit --amend` command.
96+
2. Wait for the tool to return successfully.
97+
3. Execute `git status`.
98+
4. Wait for the tool to return and explicitly verify the output contains `nothing to commit, working tree clean` (or similar indication that no unstaged changes remain). If changes remain, stage them and amend the commit, then repeat this verification loop.
99+
5. **Only after** step 4 has successfully returned a clean working directory may you generate a text response to the user declaring that the task is complete.
95100
- **Diff Review:** Before finalizing a task, review the full diff (e.g., `git diff HEAD^`) to ensure all changes are functional and relevant. Identify and revert any formatting-only changes in files that do not contain functional updates to keep the commit focused.
96101

97102
## Refactoring & Migration Guardrails
@@ -116,6 +121,7 @@ This project treats Error Prone warnings as errors.
116121
## 🚫 Common Pitfalls to Avoid
117122

118123
- **Do not go in circles with the build:** If you see an `InlineMeSuggester` error, apply the suppression to **ALL** similar methods in that file and related files in one turn. Do not fix them one by one.
124+
- **Exception Conversion in Tests:** When migrating time types (e.g., from Joda `DateTime` to Java `Instant`), be extremely careful with tests that verify parsing failures (e.g., `assertThrows(IllegalArgumentException.class, ...)`). Joda's `DateTime.parse()` throws an `IllegalArgumentException` on failure, but `Instant.parse()` throws a `java.time.format.DateTimeParseException`. You must update the expected exception type in these tests to ensure they actually test the correct behavior, and verify the tests are not failing prematurely on the first line if it contains invalid data meant to be ignored.
119125
- Dagger/AutoValue corruption: If you modify a builder or a component incorrectly, Dagger will fail to generate code, leading to hundreds of "cannot find symbol" errors. If this happens, `git checkout` the last working state of the specific file and re-apply changes more surgically.
120126
- **`replace` tool context**: When using `replace` on large files (like `Tld.java` or `DomainBase.java`), provide significant surrounding context. These files have many similar method signatures (getters/setters) that can lead to incorrect replacements.
121127

core/src/main/java/google/registry/beam/billing/ExpandBillingRecurrencesPipeline.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,7 @@ private void expandOneRecurrence(
402402
.getRenewPrice(
403403
tld,
404404
billingRecurrence.getTargetId(),
405-
toDateTime(eventTime),
405+
eventTime,
406406
1,
407407
billingRecurrence,
408408
Optional.empty())

core/src/main/java/google/registry/flows/domain/DomainCreateFlow.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
5454
import static google.registry.util.DateTimeUtils.END_INSTANT;
5555
import static google.registry.util.DateTimeUtils.plusYears;
56+
import static google.registry.util.DateTimeUtils.toInstant;
5657

5758
import com.google.common.collect.ImmutableList;
5859
import com.google.common.collect.ImmutableSet;
@@ -313,7 +314,7 @@ public EppResponse run() throws EppException {
313314
// at this point so that we can verify it before the "after validation" extension point.
314315
signedMarkId =
315316
tmchUtils
316-
.verifySignedMarks(launchCreate.get().getSignedMarks(), domainLabel, now)
317+
.verifySignedMarks(launchCreate.get().getSignedMarks(), domainLabel, toInstant(now))
317318
.getId();
318319
}
319320
verifyNotBlockedByBsa(domainName, tld, now, allocationToken);
@@ -440,7 +441,9 @@ public EppResponse run() throws EppException {
440441
BeforeResponseReturnData responseData =
441442
flowCustomLogic.beforeResponse(
442443
BeforeResponseParameters.newBuilder()
443-
.setResData(DomainCreateData.create(targetId, now, registrationExpirationTime))
444+
.setResData(
445+
DomainCreateData.create(
446+
targetId, toInstant(now), toInstant(registrationExpirationTime)))
444447
.setResponseExtensions(createResponseExtensions(feeCreate, responseFeesAndCredits))
445448
.build());
446449
return responseBuilder

core/src/main/java/google/registry/flows/domain/DomainFlowTmchUtils.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,10 @@
3838
import java.security.cert.CertificateExpiredException;
3939
import java.security.cert.CertificateNotYetValidException;
4040
import java.security.cert.CertificateRevokedException;
41+
import java.time.Instant;
4142
import javax.xml.crypto.MarshalException;
4243
import javax.xml.crypto.dsig.XMLSignatureException;
4344
import javax.xml.parsers.ParserConfigurationException;
44-
import org.joda.time.DateTime;
4545
import org.xml.sax.SAXException;
4646

4747
/** TMCH utility functions for domain flows. */
@@ -55,7 +55,7 @@ public DomainFlowTmchUtils(TmchXmlSignature tmchXmlSignature) {
5555
}
5656

5757
public SignedMark verifySignedMarks(
58-
ImmutableList<AbstractSignedMark> signedMarks, String domainLabel, DateTime now)
58+
ImmutableList<AbstractSignedMark> signedMarks, String domainLabel, Instant now)
5959
throws EppException {
6060
if (signedMarks.size() > 1) {
6161
throw new TooManySignedMarksException();
@@ -75,7 +75,7 @@ public SignedMark verifySignedMarkValidForDomainLabel(SignedMark signedMark, Str
7575
return signedMark;
7676
}
7777

78-
public SignedMark verifyEncodedSignedMark(EncodedSignedMark encodedSignedMark, DateTime now)
78+
public SignedMark verifyEncodedSignedMark(EncodedSignedMark encodedSignedMark, Instant now)
7979
throws EppException {
8080
if (!encodedSignedMark.getEncoding().equals("base64")) {
8181
throw new Base64RequiredForEncodedSignedMarksException();

core/src/main/java/google/registry/flows/domain/DomainFlowUtils.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -504,7 +504,7 @@ public static BillingRecurrence.Builder newAutorenewBillingEvent(Domain domain)
504504
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
505505
.setTargetId(domain.getDomainName())
506506
.setRegistrarId(domain.getCurrentSponsorRegistrarId())
507-
.setEventTime(domain.getRegistrationExpirationDateTime());
507+
.setEventTime(domain.getRegistrationExpirationTime());
508508
}
509509

510510
/**
@@ -515,7 +515,7 @@ public static Autorenew.Builder newAutorenewPollMessage(Domain domain) {
515515
return new Autorenew.Builder()
516516
.setTargetId(domain.getDomainName())
517517
.setRegistrarId(domain.getCurrentSponsorRegistrarId())
518-
.setEventTime(domain.getRegistrationExpirationDateTime())
518+
.setEventTime(domain.getRegistrationExpirationTime())
519519
.setMsg("Domain was auto-renewed.");
520520
}
521521

@@ -659,7 +659,7 @@ static void handleFeeRequest(
659659
// process, don't count as expired for the purposes of requiring an added year of renewal on
660660
// restore because they can't be restored in the first place.
661661
boolean isExpired =
662-
domain.isPresent() && domain.get().getRegistrationExpirationDateTime().isBefore(now);
662+
domain.isPresent() && domain.get().getRegistrationExpirationTime().isBefore(now);
663663
fees = pricingLogic.getRestorePrice(tld, domainNameString, now, isExpired).getFees();
664664
}
665665
case TRANSFER -> {

core/src/main/java/google/registry/flows/domain/DomainInfoFlow.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -121,10 +121,10 @@ public EppResponse run() throws EppException {
121121
.setStatusValues(domain.getStatusValues())
122122
.setNameservers(
123123
hostsRequest.requestDelegated() ? domain.loadNameserverHostNames() : null)
124-
.setCreationTime(domain.getCreationTime())
125-
.setLastEppUpdateTime(domain.getLastEppUpdateDateTime())
126-
.setRegistrationExpirationTime(domain.getRegistrationExpirationDateTime())
127-
.setLastTransferTime(domain.getLastTransferTime());
124+
.setCreationTime(domain.getCreationTimeInstant())
125+
.setLastEppUpdateTime(domain.getLastEppUpdateTime())
126+
.setRegistrationExpirationTime(domain.getRegistrationExpirationTime())
127+
.setLastTransferTime(domain.getLastTransferTimeInstant());
128128

129129
// If authInfo is non-null, then the caller is authorized to see the full information since we
130130
// will have already verified the authInfo is valid.

core/src/main/java/google/registry/flows/domain/DomainPricingLogic.java

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,11 @@
4141
import google.registry.model.tld.Tld;
4242
import jakarta.inject.Inject;
4343
import java.math.RoundingMode;
44+
import java.time.Instant;
4445
import java.util.Optional;
4546
import javax.annotation.Nullable;
4647
import org.joda.money.CurrencyUnit;
4748
import org.joda.money.Money;
48-
import org.joda.time.DateTime;
4949

5050
/**
5151
* Provides pricing for create, renew, etc, operations, with call-outs that can be customized by
@@ -70,7 +70,7 @@ public DomainPricingLogic(DomainPricingCustomLogic customLogic) {
7070
public FeesAndCredits getCreatePrice(
7171
Tld tld,
7272
String domainName,
73-
DateTime dateTime,
73+
Instant dateTime,
7474
int years,
7575
boolean isAnchorTenant,
7676
boolean isSunriseCreate,
@@ -125,7 +125,7 @@ public FeesAndCredits getCreatePrice(
125125
public FeesAndCredits getRenewPrice(
126126
Tld tld,
127127
String domainName,
128-
DateTime dateTime,
128+
Instant dateTime,
129129
int years,
130130
@Nullable BillingRecurrence billingRecurrence,
131131
Optional<AllocationToken> allocationToken) {
@@ -194,7 +194,7 @@ public FeesAndCredits getRenewPrice(
194194

195195
/** Returns a new restore price for the pricer. */
196196
public FeesAndCredits getRestorePrice(
197-
Tld tld, String domainName, DateTime dateTime, boolean isExpired) throws EppException {
197+
Tld tld, String domainName, Instant dateTime, boolean isExpired) throws EppException {
198198
DomainPrices domainPrices = getPricesForDomainName(domainName, dateTime);
199199
FeesAndCredits.Builder feesAndCredits =
200200
new FeesAndCredits.Builder()
@@ -217,7 +217,7 @@ public FeesAndCredits getRestorePrice(
217217

218218
/** Returns a new transfer price for the pricer. */
219219
public FeesAndCredits getTransferPrice(
220-
Tld tld, String domainName, DateTime dateTime, @Nullable BillingRecurrence billingRecurrence)
220+
Tld tld, String domainName, Instant dateTime, @Nullable BillingRecurrence billingRecurrence)
221221
throws EppException {
222222
FeesAndCredits renewPrice =
223223
getRenewPrice(tld, domainName, dateTime, 1, billingRecurrence, Optional.empty());
@@ -239,7 +239,7 @@ public FeesAndCredits getTransferPrice(
239239
}
240240

241241
/** Returns a new update price for the pricer. */
242-
public FeesAndCredits getUpdatePrice(Tld tld, String domainName, DateTime dateTime)
242+
public FeesAndCredits getUpdatePrice(Tld tld, String domainName, Instant dateTime)
243243
throws EppException {
244244
CurrencyUnit currency = tld.getCurrency();
245245
BaseFee feeOrCredit = Fee.create(zeroInCurrency(currency), FeeType.UPDATE, false);
@@ -272,7 +272,7 @@ private Money getDomainCreateCostWithDiscount(
272272
private Money getDomainRenewCostWithDiscount(
273273
Tld tld,
274274
DomainPrices domainPrices,
275-
DateTime dateTime,
275+
Instant dateTime,
276276
int years,
277277
Optional<AllocationToken> allocationToken) {
278278
// Short-circuit if the user sent an anchor-tenant or otherwise NONPREMIUM-renewal token
@@ -349,7 +349,7 @@ private Money getDomainCostWithDiscount(
349349
}
350350

351351
private DomainPrices applyTokenToDomainPrices(
352-
DomainPrices domainPrices, Tld tld, DateTime dateTime, int years, AllocationToken token) {
352+
DomainPrices domainPrices, Tld tld, Instant dateTime, int years, AllocationToken token) {
353353
// Convert to nonpremium iff no premium charges are included (either in create or any renewal)
354354
boolean convertToNonPremium =
355355
token.getRegistrationBehavior().equals(RegistrationBehavior.NONPREMIUM_CREATE)

core/src/main/java/google/registry/flows/host/HostCreateFlow.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import static google.registry.model.reporting.HistoryEntry.Type.HOST_CREATE;
2626
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
2727
import static google.registry.util.CollectionUtils.isNullOrEmpty;
28+
import static google.registry.util.DateTimeUtils.toInstant;
2829

2930
import com.google.common.collect.ImmutableSet;
3031
import google.registry.config.RegistryConfig.Config;
@@ -141,7 +142,7 @@ public EppResponse run() throws EppException {
141142
requestHostDnsRefresh(targetId);
142143
}
143144
tm().insertAll(entitiesToInsert);
144-
return responseBuilder.setResData(HostCreateData.create(targetId, now)).build();
145+
return responseBuilder.setResData(HostCreateData.create(targetId, toInstant(now))).build();
145146
}
146147

147148
/** Subordinate hosts must have an ip address. */

core/src/main/java/google/registry/flows/host/HostInfoFlow.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
import static google.registry.flows.host.HostFlowUtils.validateHostName;
2020
import static google.registry.model.EppResourceUtils.isLinked;
2121
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
22-
import static google.registry.util.DateTimeUtils.toDateTime;
2322

2423
import com.google.common.collect.ImmutableSet;
2524
import google.registry.flows.EppException;
@@ -81,14 +80,14 @@ public EppResponse run() throws EppException {
8180
tm().loadByKey(host.getSuperordinateDomain()).cloneProjectedAtTime(now);
8281
hostInfoDataBuilder
8382
.setCurrentSponsorRegistrarId(superordinateDomain.getCurrentSponsorRegistrarId())
84-
.setLastTransferTime(toDateTime(host.computeLastTransferTime(superordinateDomain)));
83+
.setLastTransferTime(host.computeLastTransferTime(superordinateDomain));
8584
if (superordinateDomain.getStatusValues().contains(StatusValue.PENDING_TRANSFER)) {
8685
statusValues.add(StatusValue.PENDING_TRANSFER);
8786
}
8887
} else {
8988
hostInfoDataBuilder
9089
.setCurrentSponsorRegistrarId(host.getPersistedCurrentSponsorRegistrarId())
91-
.setLastTransferTime(host.getLastTransferTime());
90+
.setLastTransferTime(host.getLastTransferTimeInstant());
9291
}
9392
return responseBuilder
9493
.setResData(
@@ -98,9 +97,9 @@ public EppResponse run() throws EppException {
9897
.setStatusValues(statusValues.build())
9998
.setInetAddresses(host.getInetAddresses())
10099
.setCreationRegistrarId(host.getCreationRegistrarId())
101-
.setCreationTime(host.getCreationTime())
100+
.setCreationTime(host.getCreationTimeInstant())
102101
.setLastEppUpdateRegistrarId(host.getLastEppUpdateRegistrarId())
103-
.setLastEppUpdateTime(host.getLastEppUpdateDateTime())
102+
.setLastEppUpdateTime(host.getLastEppUpdateTime())
104103
.build())
105104
.build();
106105
}

core/src/main/java/google/registry/flows/poll/PollRequestFlow.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import static google.registry.model.eppoutput.Result.Code.SUCCESS_WITH_NO_MESSAGES;
2222
import static google.registry.model.poll.PollMessageExternalKeyConverter.makePollMessageExternalId;
2323
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
24+
import static google.registry.util.DateTimeUtils.toInstant;
2425

2526
import google.registry.flows.EppException;
2627
import google.registry.flows.EppException.ParameterValueSyntaxErrorException;
@@ -77,7 +78,7 @@ public EppResponse run() throws EppException {
7778
.setResultFromCode(SUCCESS_WITH_ACK_MESSAGE)
7879
.setMessageQueueInfo(
7980
new MessageQueueInfo.Builder()
80-
.setQueueDate(pollMessage.getEventTime())
81+
.setQueueDate(toInstant(pollMessage.getEventTime()))
8182
.setMsg(pollMessage.getMsg())
8283
.setQueueLength(getPollMessageCount(registrarId, now))
8384
.setMessageId(makePollMessageExternalId(pollMessage))

0 commit comments

Comments
 (0)