mirror of
https://github.com/google/nomulus
synced 2026-09-12 02:56:26 +00:00
Refactor / rename Billing object classes (#1993)
This includes renaming the billing classes to match the SQL table names, as well as splitting them out into their own separate top-level classes. The rest of the changes are mostly renaming variables and comments etc. We now use `BillingBase` as the name of the common billing superclass, because one-time events are called BillingEvents
This commit is contained in:
@@ -105,16 +105,15 @@ public class BatchModule {
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Parameter(ExpandRecurringBillingEventsAction.PARAM_START_TIME)
|
||||
@Parameter(ExpandBillingRecurrencesAction.PARAM_START_TIME)
|
||||
static Optional<DateTime> provideStartTime(HttpServletRequest req) {
|
||||
return extractOptionalDatetimeParameter(
|
||||
req, ExpandRecurringBillingEventsAction.PARAM_START_TIME);
|
||||
return extractOptionalDatetimeParameter(req, ExpandBillingRecurrencesAction.PARAM_START_TIME);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Parameter(ExpandRecurringBillingEventsAction.PARAM_END_TIME)
|
||||
@Parameter(ExpandBillingRecurrencesAction.PARAM_END_TIME)
|
||||
static Optional<DateTime> provideEndTime(HttpServletRequest req) {
|
||||
return extractOptionalDatetimeParameter(req, ExpandRecurringBillingEventsAction.PARAM_END_TIME);
|
||||
return extractOptionalDatetimeParameter(req, ExpandBillingRecurrencesAction.PARAM_END_TIME);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@@ -124,9 +123,9 @@ public class BatchModule {
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Parameter(ExpandRecurringBillingEventsAction.PARAM_ADVANCE_CURSOR)
|
||||
@Parameter(ExpandBillingRecurrencesAction.PARAM_ADVANCE_CURSOR)
|
||||
static boolean provideAdvanceCursor(HttpServletRequest req) {
|
||||
return extractBooleanParameter(req, ExpandRecurringBillingEventsAction.PARAM_ADVANCE_CURSOR);
|
||||
return extractBooleanParameter(req, ExpandBillingRecurrencesAction.PARAM_ADVANCE_CURSOR);
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
||||
+11
-11
@@ -29,11 +29,11 @@ import com.google.api.services.dataflow.model.LaunchFlexTemplateRequest;
|
||||
import com.google.api.services.dataflow.model.LaunchFlexTemplateResponse;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import google.registry.beam.billing.ExpandRecurringBillingEventsPipeline;
|
||||
import google.registry.beam.billing.ExpandBillingRecurrencesPipeline;
|
||||
import google.registry.config.RegistryConfig.Config;
|
||||
import google.registry.config.RegistryEnvironment;
|
||||
import google.registry.model.billing.BillingEvent.OneTime;
|
||||
import google.registry.model.billing.BillingEvent.Recurring;
|
||||
import google.registry.model.billing.BillingEvent;
|
||||
import google.registry.model.billing.BillingRecurrence;
|
||||
import google.registry.model.common.Cursor;
|
||||
import google.registry.request.Action;
|
||||
import google.registry.request.Parameter;
|
||||
@@ -46,20 +46,20 @@ import javax.inject.Inject;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
/**
|
||||
* An action that kicks off a {@link ExpandRecurringBillingEventsPipeline} dataflow job to expand
|
||||
* {@link Recurring} billing events into synthetic {@link OneTime} events.
|
||||
* An action that kicks off a {@link ExpandBillingRecurrencesPipeline} dataflow job to expand {@link
|
||||
* BillingRecurrence} billing events into synthetic {@link BillingEvent} events.
|
||||
*/
|
||||
@Action(
|
||||
service = Action.Service.BACKEND,
|
||||
path = "/_dr/task/expandRecurringBillingEvents",
|
||||
path = "/_dr/task/expandBillingRecurrences",
|
||||
auth = Auth.AUTH_INTERNAL_OR_ADMIN)
|
||||
public class ExpandRecurringBillingEventsAction implements Runnable {
|
||||
public class ExpandBillingRecurrencesAction implements Runnable {
|
||||
|
||||
public static final String PARAM_START_TIME = "startTime";
|
||||
public static final String PARAM_END_TIME = "endTime";
|
||||
public static final String PARAM_ADVANCE_CURSOR = "advanceCursor";
|
||||
|
||||
private static final String PIPELINE_NAME = "expand_recurring_billing_events_pipeline";
|
||||
private static final String PIPELINE_NAME = "expand_billing_recurrences_pipeline";
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
@Inject Clock clock;
|
||||
@@ -97,7 +97,7 @@ public class ExpandRecurringBillingEventsAction implements Runnable {
|
||||
@Inject Response response;
|
||||
|
||||
@Inject
|
||||
ExpandRecurringBillingEventsAction() {}
|
||||
ExpandBillingRecurrencesAction() {}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
@@ -133,7 +133,7 @@ public class ExpandRecurringBillingEventsAction implements Runnable {
|
||||
.put("advanceCursor", Boolean.toString(advanceCursor))
|
||||
.build());
|
||||
logger.atInfo().log(
|
||||
"Launching recurring billing event expansion pipeline for event time range [%s, %s)%s.",
|
||||
"Launching billing recurrence expansion pipeline for event time range [%s, %s)%s.",
|
||||
startTime,
|
||||
endTime,
|
||||
isDryRun ? " in dry run mode" : advanceCursor ? "" : " without advancing the cursor");
|
||||
@@ -152,7 +152,7 @@ public class ExpandRecurringBillingEventsAction implements Runnable {
|
||||
response.setStatus(SC_OK);
|
||||
response.setPayload(
|
||||
String.format(
|
||||
"Launched recurring billing event expansion pipeline: %s",
|
||||
"Launched billing recurrence expansion pipeline: %s",
|
||||
launchResponse.getJob().getId()));
|
||||
} catch (IOException e) {
|
||||
logger.atWarning().withCause(e).log("Pipeline Launch failed");
|
||||
@@ -64,7 +64,7 @@ public abstract class BillingEvent implements Serializable {
|
||||
"amount",
|
||||
"flags");
|
||||
|
||||
/** Returns the unique ID for the {@code OneTime} associated with this event. */
|
||||
/** Returns the unique ID for the {@code BillingEvent} associated with this event. */
|
||||
abstract long id();
|
||||
|
||||
/** Returns the UTC DateTime this event becomes billable. */
|
||||
|
||||
+82
-70
@@ -38,10 +38,10 @@ import google.registry.flows.custom.CustomLogicModule;
|
||||
import google.registry.flows.domain.DomainPricingLogic;
|
||||
import google.registry.flows.domain.DomainPricingLogic.AllocationTokenInvalidForPremiumNameException;
|
||||
import google.registry.model.ImmutableObject;
|
||||
import google.registry.model.billing.BillingEvent.Cancellation;
|
||||
import google.registry.model.billing.BillingEvent.Flag;
|
||||
import google.registry.model.billing.BillingEvent.OneTime;
|
||||
import google.registry.model.billing.BillingEvent.Recurring;
|
||||
import google.registry.model.billing.BillingBase.Flag;
|
||||
import google.registry.model.billing.BillingCancellation;
|
||||
import google.registry.model.billing.BillingEvent;
|
||||
import google.registry.model.billing.BillingRecurrence;
|
||||
import google.registry.model.common.Cursor;
|
||||
import google.registry.model.domain.Domain;
|
||||
import google.registry.model.domain.DomainHistory;
|
||||
@@ -77,48 +77,49 @@ import org.apache.beam.sdk.values.PDone;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
/**
|
||||
* Definition of a Dataflow Flex pipeline template, which expands {@link Recurring} to {@link
|
||||
* OneTime} when an autorenew occurs within the given time frame.
|
||||
* Definition of a Dataflow Flex pipeline template, which expands {@link BillingRecurrence} to
|
||||
* {@link BillingEvent} when an autorenew occurs within the given time frame.
|
||||
*
|
||||
* <p>This pipeline works in three stages:
|
||||
*
|
||||
* <ul>
|
||||
* <li>Gather the {@link Recurring}s that are in scope for expansion. The exact condition of
|
||||
* {@link Recurring}s to include can be found in {@link #getRecurringsInScope(Pipeline)}.
|
||||
* <li>Expand the {@link Recurring}s to {@link OneTime} (and corresponding {@link DomainHistory})
|
||||
* that fall within the [{@link #startTime}, {@link #endTime}) window, excluding those that
|
||||
* are already present (to make this pipeline idempotent when running with the same parameters
|
||||
* multiple times, either in parallel or in sequence). The {@link Recurring} is also updated
|
||||
* with the information on when it was last expanded, so it would not be in scope for
|
||||
* expansion until at least a year later.
|
||||
* <li>Gather the {@link BillingRecurrence}s that are in scope for expansion. The exact condition
|
||||
* of {@link BillingRecurrence}s to include can be found in {@link
|
||||
* #getRecurrencesInScope(Pipeline)}.
|
||||
* <li>Expand the {@link BillingRecurrence}s to {@link BillingEvent} (and corresponding {@link
|
||||
* DomainHistory}) that fall within the [{@link #startTime}, {@link #endTime}) window,
|
||||
* excluding those that are already present (to make this pipeline idempotent when running
|
||||
* with the same parameters multiple times, either in parallel or in sequence). The {@link
|
||||
* BillingRecurrence} is also updated with the information on when it was last expanded, so it
|
||||
* would not be in scope for expansion until at least a year later.
|
||||
* <li>If the cursor for billing events should be advanced, advance it to {@link #endTime} after
|
||||
* all of the expansions in the previous step is done, only when it is currently at {@link
|
||||
* #startTime}.
|
||||
* </ul>
|
||||
*
|
||||
* <p>Note that the creation of new {@link OneTime} and {@link DomainHistory} is done speculatively
|
||||
* as soon as its event time is in scope for expansion (i.e. within the window of operation). If a
|
||||
* domain is subsequently cancelled during the autorenew grace period, a {@link Cancellation} would
|
||||
* have been created to cancel the {@link OneTime} out. Similarly, a {@link DomainHistory} for the
|
||||
* delete will be created which negates the effect of the speculatively created {@link
|
||||
* DomainHistory}, specifically for the transaction records. Both the {@link OneTime} and {@link
|
||||
* DomainHistory} will only be used (and cancelled out) when the billing time becomes effective,
|
||||
* which is after the grace period, when the cancellations would have been written, if need be. This
|
||||
* is no different from what we do with manual renewals or normal creates, where entities are always
|
||||
* created for the action regardless of whether their effects will be negated later due to
|
||||
* subsequent actions within respective grace periods.
|
||||
* <p>Note that the creation of new {@link BillingEvent} and {@link DomainHistory} is done
|
||||
* speculatively as soon as its event time is in scope for expansion (i.e. within the window of
|
||||
* operation). If a domain is subsequently cancelled during the autorenew grace period, a {@link
|
||||
* BillingCancellation} would have been created to cancel the {@link BillingEvent} out. Similarly, a
|
||||
* {@link DomainHistory} for the delete will be created which negates the effect of the
|
||||
* speculatively created {@link DomainHistory}, specifically for the transaction records. Both the
|
||||
* {@link BillingEvent} and {@link DomainHistory} will only be used (and cancelled out) when the
|
||||
* billing time becomes effective, which is after the grace period, when the cancellations would
|
||||
* have been written, if need be. This is no different from what we do with manual renewals or
|
||||
* normal creates, where entities are always created for the action regardless of whether their
|
||||
* effects will be negated later due to subsequent actions within respective grace periods.
|
||||
*
|
||||
* <p>To stage this template locally, run {@code ./nom_build :core:sBP --environment=alpha \
|
||||
* --pipeline=expandBilling}.
|
||||
*
|
||||
* <p>Then, you can run the staged template via the API client library, gCloud or a raw REST call.
|
||||
*
|
||||
* @see Cancellation#forGracePeriod
|
||||
* @see BillingCancellation#forGracePeriod
|
||||
* @see google.registry.flows.domain.DomainFlowUtils#createCancelingRecords
|
||||
* @see <a href="https://cloud.google.com/dataflow/docs/guides/templates/using-flex-templates">Using
|
||||
* Flex Templates</a>
|
||||
*/
|
||||
public class ExpandRecurringBillingEventsPipeline implements Serializable {
|
||||
public class ExpandBillingRecurrencesPipeline implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = -5827984301386630194L;
|
||||
|
||||
@@ -128,7 +129,7 @@ public class ExpandRecurringBillingEventsPipeline implements Serializable {
|
||||
|
||||
static {
|
||||
PipelineComponent pipelineComponent =
|
||||
DaggerExpandRecurringBillingEventsPipeline_PipelineComponent.create();
|
||||
DaggerExpandBillingRecurrencesPipeline_PipelineComponent.create();
|
||||
domainPricingLogic = pipelineComponent.domainPricingLogic();
|
||||
batchSize = pipelineComponent.batchSize();
|
||||
}
|
||||
@@ -139,8 +140,8 @@ public class ExpandRecurringBillingEventsPipeline implements Serializable {
|
||||
private final DateTime endTime;
|
||||
private final boolean isDryRun;
|
||||
private final boolean advanceCursor;
|
||||
private final Counter recurringsInScopeCounter =
|
||||
Metrics.counter("ExpandBilling", "Recurrings in scope for expansion");
|
||||
private final Counter recurrencesInScopeCounter =
|
||||
Metrics.counter("ExpandBilling", "Recurrences in scope for expansion");
|
||||
// Note that this counter is only accurate when running in dry run mode. Because SQL persistence
|
||||
// is a side effect and not idempotent, a transaction to save OneTimes could be successful but the
|
||||
// transform that contains it could be still be retried, rolling back the counter increment. The
|
||||
@@ -150,8 +151,7 @@ public class ExpandRecurringBillingEventsPipeline implements Serializable {
|
||||
private final Counter oneTimesToExpandCounter =
|
||||
Metrics.counter("ExpandBilling", "OneTimes that would be expanded");
|
||||
|
||||
ExpandRecurringBillingEventsPipeline(
|
||||
ExpandRecurringBillingEventsPipelineOptions options, Clock clock) {
|
||||
ExpandBillingRecurrencesPipeline(ExpandBillingRecurrencesPipelineOptions options, Clock clock) {
|
||||
startTime = DateTime.parse(options.getStartTime());
|
||||
endTime = DateTime.parse(options.getEndTime());
|
||||
checkArgument(
|
||||
@@ -170,16 +170,16 @@ public class ExpandRecurringBillingEventsPipeline implements Serializable {
|
||||
}
|
||||
|
||||
void setupPipeline(Pipeline pipeline) {
|
||||
PCollection<KV<Integer, Long>> recurringIds = getRecurringsInScope(pipeline);
|
||||
PCollection<Void> expanded = expandRecurrings(recurringIds);
|
||||
PCollection<KV<Integer, Long>> recurrenceIds = getRecurrencesInScope(pipeline);
|
||||
PCollection<Void> expanded = expandRecurrences(recurrenceIds);
|
||||
if (!isDryRun && advanceCursor) {
|
||||
advanceCursor(expanded);
|
||||
}
|
||||
}
|
||||
|
||||
PCollection<KV<Integer, Long>> getRecurringsInScope(Pipeline pipeline) {
|
||||
PCollection<KV<Integer, Long>> getRecurrencesInScope(Pipeline pipeline) {
|
||||
return pipeline.apply(
|
||||
"Read all Recurrings in scope",
|
||||
"Read all Recurrences in scope",
|
||||
// Use native query because JPQL does not support timestamp arithmetics.
|
||||
RegistryJpaIO.read(
|
||||
"SELECT billing_recurrence_id "
|
||||
@@ -203,7 +203,7 @@ public class ExpandRecurringBillingEventsPipeline implements Serializable {
|
||||
endTime.minusYears(1)),
|
||||
true,
|
||||
(BigInteger id) -> {
|
||||
recurringsInScopeCounter.inc();
|
||||
recurrencesInScopeCounter.inc();
|
||||
// Note that because all elements are mapped to the same dummy key, the next
|
||||
// batching transform will effectively be serial. This however does not matter for
|
||||
// our use case because the elements were obtained from a SQL read query, which
|
||||
@@ -222,13 +222,13 @@ public class ExpandRecurringBillingEventsPipeline implements Serializable {
|
||||
.withCoder(KvCoder.of(VarIntCoder.of(), VarLongCoder.of())));
|
||||
}
|
||||
|
||||
private PCollection<Void> expandRecurrings(PCollection<KV<Integer, Long>> recurringIds) {
|
||||
return recurringIds
|
||||
private PCollection<Void> expandRecurrences(PCollection<KV<Integer, Long>> recurrenceIds) {
|
||||
return recurrenceIds
|
||||
.apply(
|
||||
"Group into batches",
|
||||
GroupIntoBatches.<Integer, Long>ofSize(batchSize).withShardedKey())
|
||||
.apply(
|
||||
"Expand and save Recurrings into OneTimes and corresponding DomainHistories",
|
||||
"Expand and save Recurrences into OneTimes and corresponding DomainHistories",
|
||||
MapElements.into(voids())
|
||||
.via(
|
||||
element -> {
|
||||
@@ -237,7 +237,7 @@ public class ExpandRecurringBillingEventsPipeline implements Serializable {
|
||||
() -> {
|
||||
ImmutableSet.Builder<ImmutableObject> results =
|
||||
new ImmutableSet.Builder<>();
|
||||
ids.forEach(id -> expandOneRecurring(id, results));
|
||||
ids.forEach(id -> expandOneRecurrence(id, results));
|
||||
if (!isDryRun) {
|
||||
tm().putAll(results.build());
|
||||
}
|
||||
@@ -246,14 +246,16 @@ public class ExpandRecurringBillingEventsPipeline implements Serializable {
|
||||
}));
|
||||
}
|
||||
|
||||
private void expandOneRecurring(Long recurringId, ImmutableSet.Builder<ImmutableObject> results) {
|
||||
Recurring recurring = tm().loadByKey(Recurring.createVKey(recurringId));
|
||||
private void expandOneRecurrence(
|
||||
Long recurrenceId, ImmutableSet.Builder<ImmutableObject> results) {
|
||||
BillingRecurrence billingRecurrence =
|
||||
tm().loadByKey(BillingRecurrence.createVKey(recurrenceId));
|
||||
|
||||
// Determine the complete set of EventTimes this recurring event should expand to within
|
||||
// Determine the complete set of EventTimes this recurrence event should expand to within
|
||||
// [max(recurrenceLastExpansion + 1 yr, startTime), min(recurrenceEndTime, endTime)).
|
||||
//
|
||||
// This range should always be legal for recurrings that are returned from the query. However,
|
||||
// it is possible that the recurring has changed between when the read transformation occurred
|
||||
// This range should always be legal for recurrences that are returned from the query. However,
|
||||
// it is possible that the recurrence has changed between when the read transformation occurred
|
||||
// and now. This could be caused by some out-of-process mutations (such as a domain deletion
|
||||
// closing out a previously open-ended recurrence), or more subtly, Beam could execute the same
|
||||
// work multiple times due to transient communication issues between workers and the scheduler.
|
||||
@@ -264,23 +266,25 @@ public class ExpandRecurringBillingEventsPipeline implements Serializable {
|
||||
// to work on the same batch. The second worker would see a new recurrence_last_expansion that
|
||||
// causes the range to be illegal.
|
||||
//
|
||||
// The best way to handle any unexpected behavior is to simply drop the recurring from
|
||||
// The best way to handle any unexpected behavior is to simply drop the recurrence from
|
||||
// expansion, if its new state still calls for an expansion, it would be picked up the next time
|
||||
// the pipeline runs.
|
||||
ImmutableSet<DateTime> eventTimes;
|
||||
try {
|
||||
eventTimes =
|
||||
ImmutableSet.copyOf(
|
||||
recurring
|
||||
billingRecurrence
|
||||
.getRecurrenceTimeOfYear()
|
||||
.getInstancesInRange(
|
||||
Range.closedOpen(
|
||||
latestOf(recurring.getRecurrenceLastExpansion().plusYears(1), startTime),
|
||||
earliestOf(recurring.getRecurrenceEndTime(), endTime))));
|
||||
latestOf(
|
||||
billingRecurrence.getRecurrenceLastExpansion().plusYears(1),
|
||||
startTime),
|
||||
earliestOf(billingRecurrence.getRecurrenceEndTime(), endTime))));
|
||||
} catch (IllegalArgumentException e) {
|
||||
return;
|
||||
}
|
||||
Domain domain = tm().loadByKey(Domain.createVKey(recurring.getDomainRepoId()));
|
||||
Domain domain = tm().loadByKey(Domain.createVKey(billingRecurrence.getDomainRepoId()));
|
||||
Tld tld = Tld.get(domain.getTld());
|
||||
|
||||
// Find the times for which the OneTime billing event are already created, making this expansion
|
||||
@@ -292,7 +296,7 @@ public class ExpandRecurringBillingEventsPipeline implements Serializable {
|
||||
"SELECT eventTime FROM BillingEvent WHERE cancellationMatchingBillingEvent ="
|
||||
+ " :key",
|
||||
DateTime.class)
|
||||
.setParameter("key", recurring.createVKey())
|
||||
.setParameter("key", billingRecurrence.createVKey())
|
||||
.getResultList());
|
||||
|
||||
Set<DateTime> eventTimesToExpand = difference(eventTimes, existingEventTimes);
|
||||
@@ -301,7 +305,7 @@ public class ExpandRecurringBillingEventsPipeline implements Serializable {
|
||||
return;
|
||||
}
|
||||
|
||||
DateTime recurrenceLastExpansionTime = recurring.getRecurrenceLastExpansion();
|
||||
DateTime recurrenceLastExpansionTime = billingRecurrence.getRecurrenceLastExpansion();
|
||||
|
||||
// Create new OneTime and DomainHistory for EventTimes that needs to be expanded.
|
||||
for (DateTime eventTime : eventTimesToExpand) {
|
||||
@@ -333,7 +337,7 @@ public class ExpandRecurringBillingEventsPipeline implements Serializable {
|
||||
DomainHistory historyEntry =
|
||||
new DomainHistory.Builder()
|
||||
.setBySuperuser(false)
|
||||
.setRegistrarId(recurring.getRegistrarId())
|
||||
.setRegistrarId(billingRecurrence.getRegistrarId())
|
||||
.setModificationTime(tm().getTransactionTime())
|
||||
.setDomain(domain)
|
||||
.setPeriod(Period.create(1, YEARS))
|
||||
@@ -385,35 +389,43 @@ public class ExpandRecurringBillingEventsPipeline 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.
|
||||
OneTime oneTime = null;
|
||||
BillingEvent billingEvent = null;
|
||||
try {
|
||||
oneTime =
|
||||
new OneTime.Builder()
|
||||
billingEvent =
|
||||
new BillingEvent.Builder()
|
||||
.setBillingTime(billingTime)
|
||||
.setRegistrarId(recurring.getRegistrarId())
|
||||
.setRegistrarId(billingRecurrence.getRegistrarId())
|
||||
// Determine the cost for a one-year renewal.
|
||||
.setCost(
|
||||
domainPricingLogic
|
||||
.getRenewPrice(
|
||||
tld, recurring.getTargetId(), eventTime, 1, recurring, Optional.empty())
|
||||
tld,
|
||||
billingRecurrence.getTargetId(),
|
||||
eventTime,
|
||||
1,
|
||||
billingRecurrence,
|
||||
Optional.empty())
|
||||
.getRenewCost())
|
||||
.setEventTime(eventTime)
|
||||
.setFlags(union(recurring.getFlags(), Flag.SYNTHETIC))
|
||||
.setFlags(union(billingRecurrence.getFlags(), Flag.SYNTHETIC))
|
||||
.setDomainHistory(historyEntry)
|
||||
.setPeriodYears(1)
|
||||
.setReason(recurring.getReason())
|
||||
.setReason(billingRecurrence.getReason())
|
||||
.setSyntheticCreationTime(endTime)
|
||||
.setCancellationMatchingBillingEvent(recurring)
|
||||
.setTargetId(recurring.getTargetId())
|
||||
.setCancellationMatchingBillingEvent(billingRecurrence)
|
||||
.setTargetId(billingRecurrence.getTargetId())
|
||||
.build();
|
||||
} catch (AllocationTokenInvalidForPremiumNameException e) {
|
||||
// This should not be reached since we are not using an allocation token
|
||||
return;
|
||||
}
|
||||
results.add(oneTime);
|
||||
results.add(billingEvent);
|
||||
}
|
||||
results.add(
|
||||
recurring.asBuilder().setRecurrenceLastExpansion(recurrenceLastExpansionTime).build());
|
||||
billingRecurrence
|
||||
.asBuilder()
|
||||
.setRecurrenceLastExpansion(recurrenceLastExpansionTime)
|
||||
.build());
|
||||
}
|
||||
|
||||
private PDone advanceCursor(PCollection<Void> persisted) {
|
||||
@@ -456,11 +468,11 @@ public class ExpandRecurringBillingEventsPipeline implements Serializable {
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
PipelineOptionsFactory.register(ExpandRecurringBillingEventsPipelineOptions.class);
|
||||
ExpandRecurringBillingEventsPipelineOptions options =
|
||||
PipelineOptionsFactory.register(ExpandBillingRecurrencesPipelineOptions.class);
|
||||
ExpandBillingRecurrencesPipelineOptions options =
|
||||
PipelineOptionsFactory.fromArgs(args)
|
||||
.withValidation()
|
||||
.as(ExpandRecurringBillingEventsPipelineOptions.class);
|
||||
.as(ExpandBillingRecurrencesPipelineOptions.class);
|
||||
// Hardcode the transaction level to be at serializable we do not want concurrent runs of the
|
||||
// pipeline for the same window to create duplicate OneTimes. This ensures that the set of
|
||||
// existing OneTimes do not change by the time new OneTimes are inserted within a transaction.
|
||||
@@ -468,7 +480,7 @@ public class ExpandRecurringBillingEventsPipeline implements Serializable {
|
||||
// Per PostgreSQL, serializable isolation level does not introduce any blocking beyond that
|
||||
// present in repeatable read other than some overhead related to monitoring possible
|
||||
// serializable anomalies. Therefore, in most cases, since each worker of the same job works on
|
||||
// a different set of recurrings, it is not possible for their execution order to affect
|
||||
// a different set of recurrences, it is not possible for their execution order to affect
|
||||
// serialization outcome, and the performance penalty should be minimum when using serializable
|
||||
// compared to using repeatable read.
|
||||
//
|
||||
@@ -480,7 +492,7 @@ public class ExpandRecurringBillingEventsPipeline implements Serializable {
|
||||
// See: https://www.postgresql.org/docs/current/transaction-iso.html
|
||||
options.setIsolationOverride(TransactionIsolationLevel.TRANSACTION_SERIALIZABLE);
|
||||
Pipeline pipeline = Pipeline.create(options);
|
||||
new ExpandRecurringBillingEventsPipeline(options, new SystemClock()).run(pipeline);
|
||||
new ExpandBillingRecurrencesPipeline(options, new SystemClock()).run(pipeline);
|
||||
}
|
||||
|
||||
@Singleton
|
||||
+1
-1
@@ -18,7 +18,7 @@ import google.registry.beam.common.RegistryPipelineOptions;
|
||||
import org.apache.beam.sdk.options.Default;
|
||||
import org.apache.beam.sdk.options.Description;
|
||||
|
||||
public interface ExpandRecurringBillingEventsPipelineOptions extends RegistryPipelineOptions {
|
||||
public interface ExpandBillingRecurrencesPipelineOptions extends RegistryPipelineOptions {
|
||||
@Description(
|
||||
"The inclusive lower bound of on the range of event times that will be expanded, in ISO 8601"
|
||||
+ " format")
|
||||
@@ -22,8 +22,8 @@ import google.registry.beam.billing.BillingEvent.InvoiceGroupingKey;
|
||||
import google.registry.beam.billing.BillingEvent.InvoiceGroupingKey.InvoiceGroupingKeyCoder;
|
||||
import google.registry.beam.common.RegistryJpaIO;
|
||||
import google.registry.beam.common.RegistryJpaIO.Read;
|
||||
import google.registry.model.billing.BillingEvent.Flag;
|
||||
import google.registry.model.billing.BillingEvent.OneTime;
|
||||
import google.registry.model.billing.BillingBase.Flag;
|
||||
import google.registry.model.billing.BillingEvent;
|
||||
import google.registry.model.registrar.Registrar;
|
||||
import google.registry.persistence.PersistenceModule.TransactionIsolationLevel;
|
||||
import google.registry.reporting.billing.BillingModule;
|
||||
@@ -86,29 +86,30 @@ public class InvoicingPipeline implements Serializable {
|
||||
|
||||
void setupPipeline(Pipeline pipeline) {
|
||||
options.setIsolationOverride(TransactionIsolationLevel.TRANSACTION_READ_COMMITTED);
|
||||
PCollection<BillingEvent> billingEvents = readFromCloudSql(options, pipeline);
|
||||
PCollection<google.registry.beam.billing.BillingEvent> billingEvents =
|
||||
readFromCloudSql(options, pipeline);
|
||||
saveInvoiceCsv(billingEvents, options);
|
||||
saveDetailedCsv(billingEvents, options);
|
||||
}
|
||||
|
||||
static PCollection<BillingEvent> readFromCloudSql(
|
||||
static PCollection<google.registry.beam.billing.BillingEvent> readFromCloudSql(
|
||||
InvoicingPipelineOptions options, Pipeline pipeline) {
|
||||
Read<Object[], BillingEvent> read =
|
||||
RegistryJpaIO.<Object[], BillingEvent>read(
|
||||
Read<Object[], google.registry.beam.billing.BillingEvent> read =
|
||||
RegistryJpaIO.<Object[], google.registry.beam.billing.BillingEvent>read(
|
||||
makeCloudSqlQuery(options.getYearMonth()), false, row -> parseRow(row).orElse(null))
|
||||
.withCoder(SerializableCoder.of(BillingEvent.class));
|
||||
.withCoder(SerializableCoder.of(google.registry.beam.billing.BillingEvent.class));
|
||||
|
||||
PCollection<BillingEvent> billingEventsWithNulls =
|
||||
PCollection<google.registry.beam.billing.BillingEvent> billingEventsWithNulls =
|
||||
pipeline.apply("Read BillingEvents from Cloud SQL", read);
|
||||
|
||||
// Remove null billing events
|
||||
return billingEventsWithNulls.apply(Filter.by(Objects::nonNull));
|
||||
}
|
||||
|
||||
private static Optional<BillingEvent> parseRow(Object[] row) {
|
||||
OneTime oneTime = (OneTime) row[0];
|
||||
private static Optional<google.registry.beam.billing.BillingEvent> parseRow(Object[] row) {
|
||||
BillingEvent billingEvent = (BillingEvent) row[0];
|
||||
Registrar registrar = (Registrar) row[1];
|
||||
CurrencyUnit currency = oneTime.getCost().getCurrencyUnit();
|
||||
CurrencyUnit currency = billingEvent.getCost().getCurrencyUnit();
|
||||
if (!registrar.getBillingAccountMap().containsKey(currency)) {
|
||||
logger.atSevere().log(
|
||||
"Registrar %s does not have a product account key for the currency unit: %s",
|
||||
@@ -117,37 +118,40 @@ public class InvoicingPipeline implements Serializable {
|
||||
}
|
||||
|
||||
return Optional.of(
|
||||
BillingEvent.create(
|
||||
oneTime.getId(),
|
||||
oneTime.getBillingTime(),
|
||||
oneTime.getEventTime(),
|
||||
google.registry.beam.billing.BillingEvent.create(
|
||||
billingEvent.getId(),
|
||||
billingEvent.getBillingTime(),
|
||||
billingEvent.getEventTime(),
|
||||
registrar.getRegistrarId(),
|
||||
registrar.getBillingAccountMap().get(currency),
|
||||
registrar.getPoNumber().orElse(""),
|
||||
DomainNameUtils.getTldFromDomainName(oneTime.getTargetId()),
|
||||
oneTime.getReason().toString(),
|
||||
oneTime.getTargetId(),
|
||||
oneTime.getDomainRepoId(),
|
||||
Optional.ofNullable(oneTime.getPeriodYears()).orElse(0),
|
||||
oneTime.getCost().getCurrencyUnit().toString(),
|
||||
oneTime.getCost().getAmount().doubleValue(),
|
||||
DomainNameUtils.getTldFromDomainName(billingEvent.getTargetId()),
|
||||
billingEvent.getReason().toString(),
|
||||
billingEvent.getTargetId(),
|
||||
billingEvent.getDomainRepoId(),
|
||||
Optional.ofNullable(billingEvent.getPeriodYears()).orElse(0),
|
||||
billingEvent.getCost().getCurrencyUnit().toString(),
|
||||
billingEvent.getCost().getAmount().doubleValue(),
|
||||
String.join(
|
||||
" ", oneTime.getFlags().stream().map(Flag::toString).collect(toImmutableSet()))));
|
||||
" ",
|
||||
billingEvent.getFlags().stream().map(Flag::toString).collect(toImmutableSet()))));
|
||||
}
|
||||
|
||||
/** Transform that converts a {@code BillingEvent} into an invoice CSV row. */
|
||||
private static class GenerateInvoiceRows
|
||||
extends PTransform<PCollection<BillingEvent>, PCollection<String>> {
|
||||
extends PTransform<
|
||||
PCollection<google.registry.beam.billing.BillingEvent>, PCollection<String>> {
|
||||
|
||||
private static final long serialVersionUID = -8090619008258393728L;
|
||||
|
||||
@Override
|
||||
public PCollection<String> expand(PCollection<BillingEvent> input) {
|
||||
public PCollection<String> expand(
|
||||
PCollection<google.registry.beam.billing.BillingEvent> input) {
|
||||
return input
|
||||
.apply(
|
||||
"Map to invoicing key",
|
||||
MapElements.into(TypeDescriptor.of(InvoiceGroupingKey.class))
|
||||
.via(BillingEvent::getInvoiceGroupingKey))
|
||||
.via(google.registry.beam.billing.BillingEvent::getInvoiceGroupingKey))
|
||||
.apply(
|
||||
"Filter out free events", Filter.by((InvoiceGroupingKey key) -> key.unitPrice() != 0))
|
||||
.setCoder(new InvoiceGroupingKeyCoder())
|
||||
@@ -161,7 +165,8 @@ public class InvoicingPipeline implements Serializable {
|
||||
|
||||
/** Saves the billing events to a single overall invoice CSV file. */
|
||||
static void saveInvoiceCsv(
|
||||
PCollection<BillingEvent> billingEvents, InvoicingPipelineOptions options) {
|
||||
PCollection<google.registry.beam.billing.BillingEvent> billingEvents,
|
||||
InvoicingPipelineOptions options) {
|
||||
billingEvents
|
||||
.apply("Generate overall invoice rows", new GenerateInvoiceRows())
|
||||
.apply(
|
||||
@@ -182,16 +187,17 @@ public class InvoicingPipeline implements Serializable {
|
||||
|
||||
/** Saves the billing events to detailed report CSV files keyed by registrar-tld pairs. */
|
||||
static void saveDetailedCsv(
|
||||
PCollection<BillingEvent> billingEvents, InvoicingPipelineOptions options) {
|
||||
PCollection<google.registry.beam.billing.BillingEvent> billingEvents,
|
||||
InvoicingPipelineOptions options) {
|
||||
String yearMonth = options.getYearMonth();
|
||||
billingEvents.apply(
|
||||
"Write detailed report for each registrar-tld pair",
|
||||
FileIO.<String, BillingEvent>writeDynamic()
|
||||
FileIO.<String, google.registry.beam.billing.BillingEvent>writeDynamic()
|
||||
.to(
|
||||
String.format(
|
||||
"%s/%s/%s",
|
||||
options.getBillingBucketUrl(), BillingModule.INVOICES_DIRECTORY, yearMonth))
|
||||
.by(BillingEvent::getDetailedReportGroupingKey)
|
||||
.by(google.registry.beam.billing.BillingEvent::getDetailedReportGroupingKey)
|
||||
.withNumShards(1)
|
||||
.withDestinationCoder(StringUtf8Coder.of())
|
||||
.withNaming(
|
||||
@@ -200,8 +206,8 @@ public class InvoicingPipeline implements Serializable {
|
||||
String.format(
|
||||
"%s_%s_%s.csv", BillingModule.DETAIL_REPORT_PREFIX, yearMonth, key))
|
||||
.via(
|
||||
Contextful.fn(BillingEvent::toCsv),
|
||||
TextIO.sink().withHeader(BillingEvent.getHeader())));
|
||||
Contextful.fn(google.registry.beam.billing.BillingEvent::toCsv),
|
||||
TextIO.sink().withHeader(google.registry.beam.billing.BillingEvent.getHeader())));
|
||||
}
|
||||
|
||||
/** Create the Cloud SQL query for a given yearMonth at runtime. */
|
||||
|
||||
+6
-5
@@ -80,12 +80,13 @@
|
||||
</task>
|
||||
|
||||
<task>
|
||||
<url><![CDATA[/_dr/task/expandRecurringBillingEvents?advanceCursor]]></url>
|
||||
<name>expandRecurringBillingEvents</name>
|
||||
<url><![CDATA[/_dr/task/expandBillingRecurrences?advanceCursor]]></url>
|
||||
<name>expandBillingRecurrences</name>
|
||||
<description>
|
||||
This job runs an action that creates synthetic OneTime billing events from Recurring billing
|
||||
events. Events are created for all instances of Recurring billing events that should exist
|
||||
between the RECURRING_BILLING cursor's time and the execution time of the action.
|
||||
This job runs an action that creates synthetic one-time billing events
|
||||
from billing recurrences. Events are created for all recurrences that
|
||||
should exist between the RECURRING_BILLING cursor's time and the execution
|
||||
time of the action.
|
||||
</description>
|
||||
<schedule>0 3 * * *</schedule>
|
||||
</task>
|
||||
|
||||
@@ -246,10 +246,10 @@
|
||||
<url-pattern>/_dr/task/refreshDnsOnHostRename</url-pattern>
|
||||
</servlet-mapping>
|
||||
|
||||
<!-- Action to expand recurring billing events into OneTimes. -->
|
||||
<!-- Action to expand BillingRecurrences into BillingEvents. -->
|
||||
<servlet-mapping>
|
||||
<servlet-name>backend-servlet</servlet-name>
|
||||
<url-pattern>/_dr/task/expandRecurringBillingEvents</url-pattern>
|
||||
<url-pattern>/_dr/task/expandBillingRecurrences</url-pattern>
|
||||
</servlet-mapping>
|
||||
|
||||
<!-- Background action to delete domains past end of autorenewal. -->
|
||||
|
||||
Vendored
+7
-6
@@ -122,12 +122,13 @@
|
||||
</task>
|
||||
|
||||
<task>
|
||||
<url><![CDATA[/_dr/task/expandRecurringBillingEvents?advanceCursor]]></url>
|
||||
<name>expandRecurringBillingEvents</name>
|
||||
<url><![CDATA[/_dr/task/expandBillingRecurrences?advanceCursor]]></url>
|
||||
<name>expandBillingRecurrences</name>
|
||||
<description>
|
||||
This job runs an action that creates synthetic OneTime billing events from Recurring billing
|
||||
events. Events are created for all instances of Recurring billing events that should exist
|
||||
between the RECURRING_BILLING cursor's time and the execution time of the action.
|
||||
This job runs an action that creates synthetic one-time billing events
|
||||
from billing recurrences. Events are created for all recurrences that
|
||||
should exist between the RECURRING_BILLING cursor's time and the execution
|
||||
time of the action.
|
||||
</description>
|
||||
<schedule>0 3 * * *</schedule>
|
||||
</task>
|
||||
@@ -253,7 +254,7 @@
|
||||
reports to the associated registrars' drive folders.
|
||||
See GenerateInvoicesAction for more details.
|
||||
</description>
|
||||
<!--WARNING: This must occur AFTER expandRecurringBillingEvents and AFTER exportSnapshot, as
|
||||
<!--WARNING: This must occur AFTER expandBillingRecurrences and AFTER exportSnapshot, as
|
||||
it uses Bigquery as the source of truth for billable events. ExportSnapshot usually takes
|
||||
about 2 hours to complete, so we give 11 hours to be safe. Normally, we give 24+ hours (see
|
||||
icannReportingStaging), but the invoicing team prefers receiving the e-mail on the first of
|
||||
|
||||
+6
-5
@@ -82,12 +82,13 @@
|
||||
</task>
|
||||
|
||||
<task>
|
||||
<url><![CDATA[/_dr/task/expandRecurringBillingEvents?advanceCursor]]></url>
|
||||
<name>expandRecurringBillingEvents</name>
|
||||
<url><![CDATA[/_dr/task/expandBillingRecurrences?advanceCursor]]></url>
|
||||
<name>expandBillingRecurrences</name>
|
||||
<description>
|
||||
This job runs an action that creates synthetic OneTime billing events from Recurring billing
|
||||
events. Events are created for all instances of Recurring billing events that should exist
|
||||
between the RECURRING_BILLING cursor's time and the execution time of the action.
|
||||
This job runs an action that creates synthetic one-time billing events
|
||||
from billing recurrences. Events are created for all recurrences that
|
||||
should exist between the RECURRING_BILLING cursor's time and the execution
|
||||
time of the action.
|
||||
</description>
|
||||
<schedule>0 3 * * *</schedule>
|
||||
</task>
|
||||
|
||||
@@ -59,7 +59,7 @@ import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTok
|
||||
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForTldException;
|
||||
import google.registry.model.EppResource;
|
||||
import google.registry.model.ForeignKeyUtils;
|
||||
import google.registry.model.billing.BillingEvent;
|
||||
import google.registry.model.billing.BillingRecurrence;
|
||||
import google.registry.model.domain.Domain;
|
||||
import google.registry.model.domain.DomainCommand.Check;
|
||||
import google.registry.model.domain.fee.FeeCheckCommandExtension;
|
||||
@@ -271,8 +271,7 @@ public final class DomainCheckFlow implements Flow {
|
||||
new ImmutableList.Builder<>();
|
||||
ImmutableMap<String, Domain> domainObjs =
|
||||
loadDomainsForRestoreChecks(feeCheck, domainNames, existingDomains);
|
||||
ImmutableMap<String, BillingEvent.Recurring> recurrences =
|
||||
loadRecurrencesForDomains(domainObjs);
|
||||
ImmutableMap<String, BillingRecurrence> recurrences = loadRecurrencesForDomains(domainObjs);
|
||||
|
||||
for (FeeCheckCommandExtensionItem feeCheckItem : feeCheck.getItems()) {
|
||||
for (String domainName : getDomainNamesToCheckForFee(feeCheckItem, domainNames.keySet())) {
|
||||
@@ -377,16 +376,15 @@ public final class DomainCheckFlow implements Flow {
|
||||
Maps.transformEntries(existingDomainsToLoad, (k, v) -> (Domain) loadedDomains.get(v)));
|
||||
}
|
||||
|
||||
private ImmutableMap<String, BillingEvent.Recurring> loadRecurrencesForDomains(
|
||||
private ImmutableMap<String, BillingRecurrence> loadRecurrencesForDomains(
|
||||
ImmutableMap<String, Domain> domainObjs) {
|
||||
return tm().transact(
|
||||
() -> {
|
||||
ImmutableMap<VKey<? extends BillingEvent.Recurring>, BillingEvent.Recurring>
|
||||
recurrences =
|
||||
tm().loadByKeys(
|
||||
domainObjs.values().stream()
|
||||
.map(Domain::getAutorenewBillingEvent)
|
||||
.collect(toImmutableSet()));
|
||||
ImmutableMap<VKey<? extends BillingRecurrence>, BillingRecurrence> recurrences =
|
||||
tm().loadByKeys(
|
||||
domainObjs.values().stream()
|
||||
.map(Domain::getAutorenewBillingEvent)
|
||||
.collect(toImmutableSet()));
|
||||
return ImmutableMap.copyOf(
|
||||
Maps.transformValues(
|
||||
domainObjs, d -> recurrences.get(d.getAutorenewBillingEvent())));
|
||||
|
||||
@@ -77,11 +77,11 @@ import google.registry.flows.domain.token.AllocationTokenFlowUtils;
|
||||
import google.registry.flows.exceptions.ResourceAlreadyExistsForThisClientException;
|
||||
import google.registry.flows.exceptions.ResourceCreateContentionException;
|
||||
import google.registry.model.ImmutableObject;
|
||||
import google.registry.model.billing.BillingBase.Flag;
|
||||
import google.registry.model.billing.BillingBase.Reason;
|
||||
import google.registry.model.billing.BillingBase.RenewalPriceBehavior;
|
||||
import google.registry.model.billing.BillingEvent;
|
||||
import google.registry.model.billing.BillingEvent.Flag;
|
||||
import google.registry.model.billing.BillingEvent.Reason;
|
||||
import google.registry.model.billing.BillingEvent.Recurring;
|
||||
import google.registry.model.billing.BillingEvent.RenewalPriceBehavior;
|
||||
import google.registry.model.billing.BillingRecurrence;
|
||||
import google.registry.model.domain.Domain;
|
||||
import google.registry.model.domain.DomainCommand;
|
||||
import google.registry.model.domain.DomainCommand.Create;
|
||||
@@ -348,8 +348,8 @@ public final class DomainCreateFlow implements TransactionalFlow {
|
||||
HistoryEntryId domainHistoryId = new HistoryEntryId(repoId, historyRevisionId);
|
||||
historyBuilder.setRevisionId(historyRevisionId);
|
||||
// Bill for the create.
|
||||
BillingEvent.OneTime createBillingEvent =
|
||||
createOneTimeBillingEvent(
|
||||
BillingEvent createBillingEvent =
|
||||
createBillingEvent(
|
||||
tld,
|
||||
isAnchorTenant,
|
||||
isSunriseCreate,
|
||||
@@ -360,7 +360,7 @@ public final class DomainCreateFlow implements TransactionalFlow {
|
||||
allocationToken,
|
||||
now);
|
||||
// Create a new autorenew billing event and poll message starting at the expiration time.
|
||||
BillingEvent.Recurring autorenewBillingEvent =
|
||||
BillingRecurrence autorenewBillingEvent =
|
||||
createAutorenewBillingEvent(
|
||||
domainHistoryId,
|
||||
registrationExpirationTime,
|
||||
@@ -572,7 +572,7 @@ public final class DomainCreateFlow implements TransactionalFlow {
|
||||
return historyBuilder.setType(DOMAIN_CREATE).setPeriod(period).setDomain(domain).build();
|
||||
}
|
||||
|
||||
private BillingEvent.OneTime createOneTimeBillingEvent(
|
||||
private BillingEvent createBillingEvent(
|
||||
Tld tld,
|
||||
boolean isAnchorTenant,
|
||||
boolean isSunriseCreate,
|
||||
@@ -594,7 +594,7 @@ public final class DomainCreateFlow implements TransactionalFlow {
|
||||
// it if it's reserved for other reasons.
|
||||
flagsBuilder.add(Flag.RESERVED);
|
||||
}
|
||||
return new BillingEvent.OneTime.Builder()
|
||||
return new BillingEvent.Builder()
|
||||
.setReason(Reason.CREATE)
|
||||
.setTargetId(targetId)
|
||||
.setRegistrarId(registrarId)
|
||||
@@ -612,11 +612,11 @@ public final class DomainCreateFlow implements TransactionalFlow {
|
||||
.build();
|
||||
}
|
||||
|
||||
private Recurring createAutorenewBillingEvent(
|
||||
private BillingRecurrence createAutorenewBillingEvent(
|
||||
HistoryEntryId domainHistoryId,
|
||||
DateTime registrationExpirationTime,
|
||||
RenewalPriceInfo renewalpriceInfo) {
|
||||
return new BillingEvent.Recurring.Builder()
|
||||
return new BillingRecurrence.Builder()
|
||||
.setReason(Reason.RENEW)
|
||||
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
|
||||
.setTargetId(targetId)
|
||||
@@ -640,9 +640,9 @@ public final class DomainCreateFlow implements TransactionalFlow {
|
||||
.build();
|
||||
}
|
||||
|
||||
private static BillingEvent.OneTime createEapBillingEvent(
|
||||
FeesAndCredits feesAndCredits, BillingEvent.OneTime createBillingEvent) {
|
||||
return new BillingEvent.OneTime.Builder()
|
||||
private static BillingEvent createEapBillingEvent(
|
||||
FeesAndCredits feesAndCredits, BillingEvent createBillingEvent) {
|
||||
return new BillingEvent.Builder()
|
||||
.setReason(Reason.FEE_EARLY_ACCESS)
|
||||
.setTargetId(createBillingEvent.getTargetId())
|
||||
.setRegistrarId(createBillingEvent.getRegistrarId())
|
||||
@@ -671,7 +671,7 @@ public final class DomainCreateFlow implements TransactionalFlow {
|
||||
|
||||
/**
|
||||
* Determines the {@link RenewalPriceBehavior} and the renewal price that needs be stored in the
|
||||
* {@link Recurring} billing events.
|
||||
* {@link BillingRecurrence} billing events.
|
||||
*
|
||||
* <p>By default, the renewal price is calculated during the process of renewal. Renewal price
|
||||
* should be the createCost if and only if the renewal price behavior in the {@link
|
||||
@@ -697,7 +697,7 @@ public final class DomainCreateFlow implements TransactionalFlow {
|
||||
}
|
||||
}
|
||||
|
||||
/** A class to store renewal info used in {@link Recurring} billing events. */
|
||||
/** A class to store renewal info used in {@link BillingRecurrence} billing events. */
|
||||
@AutoValue
|
||||
public abstract static class RenewalPriceInfo {
|
||||
static DomainCreateFlow.RenewalPriceInfo create(
|
||||
|
||||
@@ -61,8 +61,9 @@ import google.registry.flows.custom.DomainDeleteFlowCustomLogic.BeforeResponseRe
|
||||
import google.registry.flows.custom.DomainDeleteFlowCustomLogic.BeforeSaveParameters;
|
||||
import google.registry.flows.custom.EntityChanges;
|
||||
import google.registry.model.ImmutableObject;
|
||||
import google.registry.model.billing.BillingCancellation;
|
||||
import google.registry.model.billing.BillingEvent;
|
||||
import google.registry.model.billing.BillingEvent.Recurring;
|
||||
import google.registry.model.billing.BillingRecurrence;
|
||||
import google.registry.model.domain.Domain;
|
||||
import google.registry.model.domain.DomainHistory;
|
||||
import google.registry.model.domain.GracePeriod;
|
||||
@@ -232,15 +233,15 @@ public final class DomainDeleteFlow implements TransactionalFlow {
|
||||
// No cancellation is written if the grace period was not for a billable event.
|
||||
if (gracePeriod.hasBillingEvent()) {
|
||||
entitiesToSave.add(
|
||||
BillingEvent.Cancellation.forGracePeriod(gracePeriod, now, domainHistoryId, targetId));
|
||||
if (gracePeriod.getOneTimeBillingEvent() != null) {
|
||||
BillingCancellation.forGracePeriod(gracePeriod, now, domainHistoryId, targetId));
|
||||
if (gracePeriod.getBillingEvent() != null) {
|
||||
// Take the amount of registration time being refunded off the expiration time.
|
||||
// This can be either add grace periods or renew grace periods.
|
||||
BillingEvent.OneTime oneTime = tm().loadByKey(gracePeriod.getOneTimeBillingEvent());
|
||||
newExpirationTime = newExpirationTime.minusYears(oneTime.getPeriodYears());
|
||||
} else if (gracePeriod.getRecurringBillingEvent() != null) {
|
||||
BillingEvent billingEvent = tm().loadByKey(gracePeriod.getBillingEvent());
|
||||
newExpirationTime = newExpirationTime.minusYears(billingEvent.getPeriodYears());
|
||||
} else if (gracePeriod.getBillingRecurrence() != null) {
|
||||
// Take 1 year off the registration if in the autorenew grace period (no need to load the
|
||||
// recurring billing event; all autorenews are for 1 year).
|
||||
// recurrence billing event; all autorenews are for 1 year).
|
||||
newExpirationTime = newExpirationTime.minusYears(1);
|
||||
}
|
||||
}
|
||||
@@ -252,11 +253,12 @@ public final class DomainDeleteFlow implements TransactionalFlow {
|
||||
buildDomainHistory(newDomain, tld, now, durationUntilDelete, inAddGracePeriod);
|
||||
handlePendingTransferOnDelete(existingDomain, newDomain, now, domainHistory);
|
||||
// Close the autorenew billing event and poll message. This may delete the poll message. Store
|
||||
// the updated recurring billing event, we'll need it later and can't reload it.
|
||||
Recurring existingRecurring = tm().loadByKey(existingDomain.getAutorenewBillingEvent());
|
||||
BillingEvent.Recurring recurringBillingEvent =
|
||||
// the updated recurrence billing event, we'll need it later and can't reload it.
|
||||
BillingRecurrence existingBillingRecurrence =
|
||||
tm().loadByKey(existingDomain.getAutorenewBillingEvent());
|
||||
BillingRecurrence billingRecurrence =
|
||||
updateAutorenewRecurrenceEndTime(
|
||||
existingDomain, existingRecurring, now, domainHistory.getHistoryEntryId());
|
||||
existingDomain, existingBillingRecurrence, now, domainHistory.getHistoryEntryId());
|
||||
// If there's a pending transfer, the gaining client's autorenew billing
|
||||
// event and poll message will already have been deleted in
|
||||
// ResourceDeleteFlow since it's listed in serverApproveEntities.
|
||||
@@ -280,7 +282,7 @@ public final class DomainDeleteFlow implements TransactionalFlow {
|
||||
? SUCCESS_WITH_ACTION_PENDING
|
||||
: SUCCESS)
|
||||
.setResponseExtensions(
|
||||
getResponseExtensions(recurringBillingEvent, existingDomain, now))
|
||||
getResponseExtensions(billingRecurrence, existingDomain, now))
|
||||
.build());
|
||||
persistEntityChanges(entityChanges);
|
||||
return responseBuilder
|
||||
@@ -380,7 +382,7 @@ public final class DomainDeleteFlow implements TransactionalFlow {
|
||||
|
||||
@Nullable
|
||||
private ImmutableList<FeeTransformResponseExtension> getResponseExtensions(
|
||||
BillingEvent.Recurring recurringBillingEvent, Domain existingDomain, DateTime now) {
|
||||
BillingRecurrence billingRecurrence, Domain existingDomain, DateTime now) {
|
||||
FeeTransformResponseExtension.Builder feeResponseBuilder = getDeleteResponseBuilder();
|
||||
if (feeResponseBuilder == null) {
|
||||
return ImmutableList.of();
|
||||
@@ -388,7 +390,7 @@ public final class DomainDeleteFlow implements TransactionalFlow {
|
||||
ImmutableList.Builder<Credit> creditsBuilder = new ImmutableList.Builder<>();
|
||||
for (GracePeriod gracePeriod : existingDomain.getGracePeriods()) {
|
||||
if (gracePeriod.hasBillingEvent()) {
|
||||
Money cost = getGracePeriodCost(recurringBillingEvent, gracePeriod, now);
|
||||
Money cost = getGracePeriodCost(billingRecurrence, gracePeriod, now);
|
||||
creditsBuilder.add(Credit.create(
|
||||
cost.negated().getAmount(), FeeType.CREDIT, gracePeriod.getType().getXmlName()));
|
||||
feeResponseBuilder.setCurrency(checkNotNull(cost.getCurrencyUnit()));
|
||||
@@ -402,14 +404,14 @@ public final class DomainDeleteFlow implements TransactionalFlow {
|
||||
}
|
||||
|
||||
private Money getGracePeriodCost(
|
||||
BillingEvent.Recurring recurringBillingEvent, GracePeriod gracePeriod, DateTime now) {
|
||||
BillingRecurrence billingRecurrence, GracePeriod gracePeriod, DateTime now) {
|
||||
if (gracePeriod.getType() == GracePeriodStatus.AUTO_RENEW) {
|
||||
// If we updated the autorenew billing event, reuse it.
|
||||
DateTime autoRenewTime =
|
||||
recurringBillingEvent.getRecurrenceTimeOfYear().getLastInstanceBeforeOrAt(now);
|
||||
billingRecurrence.getRecurrenceTimeOfYear().getLastInstanceBeforeOrAt(now);
|
||||
return getDomainRenewCost(targetId, autoRenewTime, 1);
|
||||
}
|
||||
return tm().loadByKey(checkNotNull(gracePeriod.getOneTimeBillingEvent())).getCost();
|
||||
return tm().loadByKey(checkNotNull(gracePeriod.getBillingEvent())).getCost();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
||||
@@ -77,10 +77,9 @@ import google.registry.flows.EppException.UnimplementedOptionException;
|
||||
import google.registry.flows.domain.token.AllocationTokenFlowUtils;
|
||||
import google.registry.flows.exceptions.ResourceHasClientUpdateProhibitedException;
|
||||
import google.registry.model.EppResource;
|
||||
import google.registry.model.billing.BillingEvent;
|
||||
import google.registry.model.billing.BillingEvent.Flag;
|
||||
import google.registry.model.billing.BillingEvent.Reason;
|
||||
import google.registry.model.billing.BillingEvent.Recurring;
|
||||
import google.registry.model.billing.BillingBase.Flag;
|
||||
import google.registry.model.billing.BillingBase.Reason;
|
||||
import google.registry.model.billing.BillingRecurrence;
|
||||
import google.registry.model.contact.Contact;
|
||||
import google.registry.model.domain.DesignatedContact;
|
||||
import google.registry.model.domain.DesignatedContact.Type;
|
||||
@@ -556,8 +555,8 @@ public class DomainFlowUtils {
|
||||
* Fills in a builder with the data needed for an autorenew billing event for this domain. This
|
||||
* does not copy over the id of the current autorenew billing event.
|
||||
*/
|
||||
public static BillingEvent.Recurring.Builder newAutorenewBillingEvent(Domain domain) {
|
||||
return new BillingEvent.Recurring.Builder()
|
||||
public static BillingRecurrence.Builder newAutorenewBillingEvent(Domain domain) {
|
||||
return new BillingRecurrence.Builder()
|
||||
.setReason(Reason.RENEW)
|
||||
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
|
||||
.setTargetId(domain.getDomainName())
|
||||
@@ -584,11 +583,11 @@ public class DomainFlowUtils {
|
||||
* (if opening the message interval). This may cause an autorenew billing event to have an end
|
||||
* time earlier than its event time (i.e. if it's being ended before it was ever triggered).
|
||||
*
|
||||
* <p>Returns the new autorenew recurring billing event.
|
||||
* <p>Returns the new autorenew recurrence billing event.
|
||||
*/
|
||||
public static Recurring updateAutorenewRecurrenceEndTime(
|
||||
public static BillingRecurrence updateAutorenewRecurrenceEndTime(
|
||||
Domain domain,
|
||||
Recurring existingRecurring,
|
||||
BillingRecurrence existingBillingRecurrence,
|
||||
DateTime newEndTime,
|
||||
@Nullable HistoryEntryId historyId) {
|
||||
Optional<PollMessage.Autorenew> autorenewPollMessage =
|
||||
@@ -623,9 +622,10 @@ public class DomainFlowUtils {
|
||||
tm().put(updatedAutorenewPollMessage);
|
||||
}
|
||||
|
||||
Recurring newRecurring = existingRecurring.asBuilder().setRecurrenceEndTime(newEndTime).build();
|
||||
tm().put(newRecurring);
|
||||
return newRecurring;
|
||||
BillingRecurrence newBillingRecurrence =
|
||||
existingBillingRecurrence.asBuilder().setRecurrenceEndTime(newEndTime).build();
|
||||
tm().put(newBillingRecurrence);
|
||||
return newBillingRecurrence;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -642,7 +642,7 @@ public class DomainFlowUtils {
|
||||
DomainPricingLogic pricingLogic,
|
||||
Optional<AllocationToken> allocationToken,
|
||||
boolean isAvailable,
|
||||
@Nullable Recurring recurringBillingEvent)
|
||||
@Nullable BillingRecurrence billingRecurrence)
|
||||
throws EppException {
|
||||
DateTime now = currentDate;
|
||||
// Use the custom effective date specified in the fee check request, if there is one.
|
||||
@@ -698,7 +698,7 @@ public class DomainFlowUtils {
|
||||
fees =
|
||||
pricingLogic
|
||||
.getRenewPrice(
|
||||
tld, domainNameString, now, years, recurringBillingEvent, allocationToken)
|
||||
tld, domainNameString, now, years, billingRecurrence, allocationToken)
|
||||
.getFees();
|
||||
break;
|
||||
case RESTORE:
|
||||
@@ -724,9 +724,7 @@ public class DomainFlowUtils {
|
||||
}
|
||||
builder.setAvailIfSupported(true);
|
||||
fees =
|
||||
pricingLogic
|
||||
.getTransferPrice(tld, domainNameString, now, recurringBillingEvent)
|
||||
.getFees();
|
||||
pricingLogic.getTransferPrice(tld, domainNameString, now, billingRecurrence).getFees();
|
||||
break;
|
||||
case UPDATE:
|
||||
builder.setAvailIfSupported(true);
|
||||
|
||||
@@ -29,7 +29,7 @@ import google.registry.flows.custom.DomainPricingCustomLogic.RenewPriceParameter
|
||||
import google.registry.flows.custom.DomainPricingCustomLogic.RestorePriceParameters;
|
||||
import google.registry.flows.custom.DomainPricingCustomLogic.TransferPriceParameters;
|
||||
import google.registry.flows.custom.DomainPricingCustomLogic.UpdatePriceParameters;
|
||||
import google.registry.model.billing.BillingEvent.Recurring;
|
||||
import google.registry.model.billing.BillingRecurrence;
|
||||
import google.registry.model.domain.fee.BaseFee;
|
||||
import google.registry.model.domain.fee.BaseFee.FeeType;
|
||||
import google.registry.model.domain.fee.Fee;
|
||||
@@ -113,20 +113,20 @@ public final class DomainPricingLogic {
|
||||
String domainName,
|
||||
DateTime dateTime,
|
||||
int years,
|
||||
@Nullable Recurring recurringBillingEvent,
|
||||
@Nullable BillingRecurrence billingRecurrence,
|
||||
Optional<AllocationToken> allocationToken)
|
||||
throws AllocationTokenInvalidForPremiumNameException {
|
||||
checkArgument(years > 0, "Number of years must be positive");
|
||||
Money renewCost;
|
||||
DomainPrices domainPrices = getPricesForDomainName(domainName, dateTime);
|
||||
boolean isRenewCostPremiumPrice;
|
||||
// recurring billing event is null if the domain is still available. Billing events are created
|
||||
// recurrence is null if the domain is still available. Billing events are created
|
||||
// in the process of domain creation.
|
||||
if (recurringBillingEvent == null) {
|
||||
if (billingRecurrence == null) {
|
||||
renewCost = getDomainRenewCostWithDiscount(domainPrices, years, allocationToken);
|
||||
isRenewCostPremiumPrice = domainPrices.isPremium();
|
||||
} else {
|
||||
switch (recurringBillingEvent.getRenewalPriceBehavior()) {
|
||||
switch (billingRecurrence.getRenewalPriceBehavior()) {
|
||||
case DEFAULT:
|
||||
renewCost = getDomainRenewCostWithDiscount(domainPrices, years, allocationToken);
|
||||
isRenewCostPremiumPrice = domainPrices.isPremium();
|
||||
@@ -135,11 +135,11 @@ public final class DomainPricingLogic {
|
||||
// as the creation price, which is stored in the billing event as the renewal price
|
||||
case SPECIFIED:
|
||||
checkArgumentPresent(
|
||||
recurringBillingEvent.getRenewalPrice(),
|
||||
billingRecurrence.getRenewalPrice(),
|
||||
"Unexpected behavior: renewal price cannot be null when renewal behavior is"
|
||||
+ " SPECIFIED");
|
||||
// Don't apply allocation token to renewal price when SPECIFIED
|
||||
renewCost = recurringBillingEvent.getRenewalPrice().get().multipliedBy(years);
|
||||
renewCost = billingRecurrence.getRenewalPrice().get().multipliedBy(years);
|
||||
isRenewCostPremiumPrice = false;
|
||||
break;
|
||||
// if the renewal price behavior is nonpremium, it means that the domain should be renewed
|
||||
@@ -157,7 +157,7 @@ public final class DomainPricingLogic {
|
||||
throw new IllegalArgumentException(
|
||||
String.format(
|
||||
"Unknown RenewalPriceBehavior enum value: %s",
|
||||
recurringBillingEvent.getRenewalPriceBehavior()));
|
||||
billingRecurrence.getRenewalPriceBehavior()));
|
||||
}
|
||||
}
|
||||
return customLogic.customizeRenewPrice(
|
||||
@@ -200,10 +200,10 @@ public final class DomainPricingLogic {
|
||||
|
||||
/** Returns a new transfer price for the pricer. */
|
||||
FeesAndCredits getTransferPrice(
|
||||
Tld tld, String domainName, DateTime dateTime, @Nullable Recurring recurringBillingEvent)
|
||||
Tld tld, String domainName, DateTime dateTime, @Nullable BillingRecurrence billingRecurrence)
|
||||
throws EppException {
|
||||
FeesAndCredits renewPrice =
|
||||
getRenewPrice(tld, domainName, dateTime, 1, recurringBillingEvent, Optional.empty());
|
||||
getRenewPrice(tld, domainName, dateTime, 1, billingRecurrence, Optional.empty());
|
||||
return customLogic.customizeTransferPrice(
|
||||
TransferPriceParameters.newBuilder()
|
||||
.setFeesAndCredits(
|
||||
|
||||
@@ -54,10 +54,9 @@ import google.registry.flows.custom.DomainRenewFlowCustomLogic.BeforeSaveParamet
|
||||
import google.registry.flows.custom.EntityChanges;
|
||||
import google.registry.flows.domain.token.AllocationTokenFlowUtils;
|
||||
import google.registry.model.ImmutableObject;
|
||||
import google.registry.model.billing.BillingBase.Reason;
|
||||
import google.registry.model.billing.BillingEvent;
|
||||
import google.registry.model.billing.BillingEvent.OneTime;
|
||||
import google.registry.model.billing.BillingEvent.Reason;
|
||||
import google.registry.model.billing.BillingEvent.Recurring;
|
||||
import google.registry.model.billing.BillingRecurrence;
|
||||
import google.registry.model.domain.Domain;
|
||||
import google.registry.model.domain.DomainCommand.Renew;
|
||||
import google.registry.model.domain.DomainHistory;
|
||||
@@ -203,7 +202,7 @@ public final class DomainRenewFlow implements TransactionalFlow {
|
||||
validateRegistrationPeriod(now, newExpirationTime);
|
||||
Optional<FeeRenewCommandExtension> feeRenew =
|
||||
eppInput.getSingleExtension(FeeRenewCommandExtension.class);
|
||||
Recurring existingRecurringBillingEvent =
|
||||
BillingRecurrence existingBillingRecurrence =
|
||||
tm().loadByKey(existingDomain.getAutorenewBillingEvent());
|
||||
FeesAndCredits feesAndCredits =
|
||||
pricingLogic.getRenewPrice(
|
||||
@@ -211,7 +210,7 @@ public final class DomainRenewFlow implements TransactionalFlow {
|
||||
targetId,
|
||||
now,
|
||||
years,
|
||||
existingRecurringBillingEvent,
|
||||
existingBillingRecurrence,
|
||||
allocationToken);
|
||||
validateFeeChallenge(feeRenew, feesAndCredits, defaultTokenUsed);
|
||||
flowCustomLogic.afterValidation(
|
||||
@@ -223,15 +222,15 @@ public final class DomainRenewFlow implements TransactionalFlow {
|
||||
HistoryEntryId domainHistoryId = createHistoryEntryId(existingDomain);
|
||||
historyBuilder.setRevisionId(domainHistoryId.getRevisionId());
|
||||
// Bill for this explicit renew itself.
|
||||
BillingEvent.OneTime explicitRenewEvent =
|
||||
BillingEvent explicitRenewEvent =
|
||||
createRenewBillingEvent(
|
||||
tldStr, feesAndCredits.getTotalCost(), years, domainHistoryId, allocationToken, now);
|
||||
// Create a new autorenew billing event and poll message starting at the new expiration time.
|
||||
BillingEvent.Recurring newAutorenewEvent =
|
||||
BillingRecurrence newAutorenewEvent =
|
||||
newAutorenewBillingEvent(existingDomain)
|
||||
.setEventTime(newExpirationTime)
|
||||
.setRenewalPrice(existingRecurringBillingEvent.getRenewalPrice().orElse(null))
|
||||
.setRenewalPriceBehavior(existingRecurringBillingEvent.getRenewalPriceBehavior())
|
||||
.setRenewalPrice(existingBillingRecurrence.getRenewalPrice().orElse(null))
|
||||
.setRenewalPriceBehavior(existingBillingRecurrence.getRenewalPriceBehavior())
|
||||
.setDomainHistoryId(domainHistoryId)
|
||||
.build();
|
||||
PollMessage.Autorenew newAutorenewPollMessage =
|
||||
@@ -240,8 +239,8 @@ public final class DomainRenewFlow implements TransactionalFlow {
|
||||
.setDomainHistoryId(domainHistoryId)
|
||||
.build();
|
||||
// End the old autorenew billing event and poll message now. This may delete the poll message.
|
||||
Recurring existingRecurring = tm().loadByKey(existingDomain.getAutorenewBillingEvent());
|
||||
updateAutorenewRecurrenceEndTime(existingDomain, existingRecurring, now, domainHistoryId);
|
||||
updateAutorenewRecurrenceEndTime(
|
||||
existingDomain, existingBillingRecurrence, now, domainHistoryId);
|
||||
Domain newDomain =
|
||||
existingDomain
|
||||
.asBuilder()
|
||||
@@ -338,14 +337,14 @@ public final class DomainRenewFlow implements TransactionalFlow {
|
||||
}
|
||||
}
|
||||
|
||||
private OneTime createRenewBillingEvent(
|
||||
private BillingEvent createRenewBillingEvent(
|
||||
String tld,
|
||||
Money renewCost,
|
||||
int years,
|
||||
HistoryEntryId domainHistoryId,
|
||||
Optional<AllocationToken> allocationToken,
|
||||
DateTime now) {
|
||||
return new BillingEvent.OneTime.Builder()
|
||||
return new BillingEvent.Builder()
|
||||
.setReason(Reason.RENEW)
|
||||
.setTargetId(targetId)
|
||||
.setRegistrarId(registrarId)
|
||||
|
||||
@@ -45,9 +45,9 @@ import google.registry.flows.FlowModule.TargetId;
|
||||
import google.registry.flows.TransactionalFlow;
|
||||
import google.registry.flows.annotations.ReportingSpec;
|
||||
import google.registry.model.ImmutableObject;
|
||||
import google.registry.model.billing.BillingBase.Reason;
|
||||
import google.registry.model.billing.BillingEvent;
|
||||
import google.registry.model.billing.BillingEvent.OneTime;
|
||||
import google.registry.model.billing.BillingEvent.Reason;
|
||||
import google.registry.model.billing.BillingRecurrence;
|
||||
import google.registry.model.domain.Domain;
|
||||
import google.registry.model.domain.DomainCommand.Update;
|
||||
import google.registry.model.domain.DomainHistory;
|
||||
@@ -161,7 +161,7 @@ public final class DomainRestoreRequestFlow implements TransactionalFlow {
|
||||
entitiesToSave.add(
|
||||
createRestoreBillingEvent(domainHistoryId, feesAndCredits.getRestoreCost(), now));
|
||||
|
||||
BillingEvent.Recurring autorenewEvent =
|
||||
BillingRecurrence autorenewEvent =
|
||||
newAutorenewBillingEvent(existingDomain)
|
||||
.setEventTime(newExpirationTime)
|
||||
.setRecurrenceEndTime(END_OF_TIME)
|
||||
@@ -231,7 +231,7 @@ public final class DomainRestoreRequestFlow implements TransactionalFlow {
|
||||
private static Domain performRestore(
|
||||
Domain existingDomain,
|
||||
DateTime newExpirationTime,
|
||||
BillingEvent.Recurring autorenewEvent,
|
||||
BillingRecurrence autorenewEvent,
|
||||
PollMessage.Autorenew autorenewPollMessage,
|
||||
DateTime now,
|
||||
String registrarId) {
|
||||
@@ -252,19 +252,19 @@ public final class DomainRestoreRequestFlow implements TransactionalFlow {
|
||||
.build();
|
||||
}
|
||||
|
||||
private OneTime createRenewBillingEvent(
|
||||
private BillingEvent createRenewBillingEvent(
|
||||
HistoryEntryId domainHistoryId, Money renewCost, DateTime now) {
|
||||
return prepareBillingEvent(domainHistoryId, renewCost, now).setReason(Reason.RENEW).build();
|
||||
}
|
||||
|
||||
private BillingEvent.OneTime createRestoreBillingEvent(
|
||||
private BillingEvent createRestoreBillingEvent(
|
||||
HistoryEntryId domainHistoryId, Money restoreCost, DateTime now) {
|
||||
return prepareBillingEvent(domainHistoryId, restoreCost, now).setReason(Reason.RESTORE).build();
|
||||
}
|
||||
|
||||
private OneTime.Builder prepareBillingEvent(
|
||||
private BillingEvent.Builder prepareBillingEvent(
|
||||
HistoryEntryId domainHistoryId, Money cost, DateTime now) {
|
||||
return new BillingEvent.OneTime.Builder()
|
||||
return new BillingEvent.Builder()
|
||||
.setTargetId(targetId)
|
||||
.setRegistrarId(registrarId)
|
||||
.setEventTime(now)
|
||||
|
||||
@@ -45,11 +45,12 @@ import google.registry.flows.TransactionalFlow;
|
||||
import google.registry.flows.annotations.ReportingSpec;
|
||||
import google.registry.flows.domain.token.AllocationTokenFlowUtils;
|
||||
import google.registry.model.ImmutableObject;
|
||||
import google.registry.model.billing.BillingBase.Flag;
|
||||
import google.registry.model.billing.BillingBase.Reason;
|
||||
import google.registry.model.billing.BillingBase.RenewalPriceBehavior;
|
||||
import google.registry.model.billing.BillingCancellation;
|
||||
import google.registry.model.billing.BillingEvent;
|
||||
import google.registry.model.billing.BillingEvent.Flag;
|
||||
import google.registry.model.billing.BillingEvent.Reason;
|
||||
import google.registry.model.billing.BillingEvent.Recurring;
|
||||
import google.registry.model.billing.BillingEvent.RenewalPriceBehavior;
|
||||
import google.registry.model.billing.BillingRecurrence;
|
||||
import google.registry.model.domain.Domain;
|
||||
import google.registry.model.domain.DomainHistory;
|
||||
import google.registry.model.domain.GracePeriod;
|
||||
@@ -149,16 +150,18 @@ public final class DomainTransferApproveFlow implements TransactionalFlow {
|
||||
String gainingRegistrarId = transferData.getGainingRegistrarId();
|
||||
// Create a transfer billing event for 1 year, unless the superuser extension was used to set
|
||||
// the transfer period to zero. There is not a transfer cost if the transfer period is zero.
|
||||
Recurring existingRecurring = tm().loadByKey(existingDomain.getAutorenewBillingEvent());
|
||||
BillingRecurrence existingBillingRecurrence =
|
||||
tm().loadByKey(existingDomain.getAutorenewBillingEvent());
|
||||
HistoryEntryId domainHistoryId = createHistoryEntryId(existingDomain);
|
||||
historyBuilder.setRevisionId(domainHistoryId.getRevisionId());
|
||||
boolean hasPackageToken = existingDomain.getCurrentPackageToken().isPresent();
|
||||
Money renewalPrice = hasPackageToken ? null : existingRecurring.getRenewalPrice().orElse(null);
|
||||
Optional<BillingEvent.OneTime> billingEvent =
|
||||
Money renewalPrice =
|
||||
hasPackageToken ? null : existingBillingRecurrence.getRenewalPrice().orElse(null);
|
||||
Optional<BillingEvent> billingEvent =
|
||||
transferData.getTransferPeriod().getValue() == 0
|
||||
? Optional.empty()
|
||||
: Optional.of(
|
||||
new BillingEvent.OneTime.Builder()
|
||||
new BillingEvent.Builder()
|
||||
.setReason(Reason.TRANSFER)
|
||||
.setTargetId(targetId)
|
||||
.setRegistrarId(gainingRegistrarId)
|
||||
@@ -170,9 +173,9 @@ public final class DomainTransferApproveFlow implements TransactionalFlow {
|
||||
targetId,
|
||||
transferData.getTransferRequestTime(),
|
||||
// When removing a domain from a package it should return to the
|
||||
// default recurring billing behavior so the existing recurring
|
||||
// default recurrence billing behavior so the existing recurrence
|
||||
// billing event should not be passed in.
|
||||
hasPackageToken ? null : existingRecurring)
|
||||
hasPackageToken ? null : existingBillingRecurrence)
|
||||
.getRenewCost())
|
||||
.setEventTime(now)
|
||||
.setBillingTime(now.plus(Tld.get(tldStr).getTransferGracePeriodLength()))
|
||||
@@ -192,18 +195,18 @@ public final class DomainTransferApproveFlow implements TransactionalFlow {
|
||||
// still needs to be charged for the auto-renew.
|
||||
if (billingEvent.isPresent()) {
|
||||
entitiesToSave.add(
|
||||
BillingEvent.Cancellation.forGracePeriod(
|
||||
autorenewGrace, now, domainHistoryId, targetId));
|
||||
BillingCancellation.forGracePeriod(autorenewGrace, now, domainHistoryId, targetId));
|
||||
}
|
||||
}
|
||||
// Close the old autorenew event and poll message at the transfer time (aka now). This may end
|
||||
// up deleting the poll message.
|
||||
updateAutorenewRecurrenceEndTime(existingDomain, existingRecurring, now, domainHistoryId);
|
||||
updateAutorenewRecurrenceEndTime(
|
||||
existingDomain, existingBillingRecurrence, now, domainHistoryId);
|
||||
DateTime newExpirationTime =
|
||||
computeExDateForApprovalTime(existingDomain, now, transferData.getTransferPeriod());
|
||||
// Create a new autorenew event starting at the expiration time.
|
||||
BillingEvent.Recurring autorenewEvent =
|
||||
new BillingEvent.Recurring.Builder()
|
||||
BillingRecurrence autorenewEvent =
|
||||
new BillingRecurrence.Builder()
|
||||
.setReason(Reason.RENEW)
|
||||
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
|
||||
.setTargetId(targetId)
|
||||
@@ -212,7 +215,7 @@ public final class DomainTransferApproveFlow implements TransactionalFlow {
|
||||
.setRenewalPriceBehavior(
|
||||
hasPackageToken
|
||||
? RenewalPriceBehavior.DEFAULT
|
||||
: existingRecurring.getRenewalPriceBehavior())
|
||||
: existingBillingRecurrence.getRenewalPriceBehavior())
|
||||
.setRenewalPrice(renewalPrice)
|
||||
.setRecurrenceEndTime(END_OF_TIME)
|
||||
.setDomainHistoryId(domainHistoryId)
|
||||
@@ -248,12 +251,10 @@ public final class DomainTransferApproveFlow implements TransactionalFlow {
|
||||
.setGracePeriods(
|
||||
billingEvent
|
||||
.map(
|
||||
oneTime ->
|
||||
event ->
|
||||
ImmutableSet.of(
|
||||
GracePeriod.forBillingEvent(
|
||||
GracePeriodStatus.TRANSFER,
|
||||
existingDomain.getRepoId(),
|
||||
oneTime)))
|
||||
GracePeriodStatus.TRANSFER, existingDomain.getRepoId(), event)))
|
||||
.orElseGet(ImmutableSet::of))
|
||||
.setLastEppUpdateTime(now)
|
||||
.setLastEppUpdateRegistrarId(registrarId)
|
||||
|
||||
@@ -39,7 +39,7 @@ import google.registry.flows.FlowModule.Superuser;
|
||||
import google.registry.flows.FlowModule.TargetId;
|
||||
import google.registry.flows.TransactionalFlow;
|
||||
import google.registry.flows.annotations.ReportingSpec;
|
||||
import google.registry.model.billing.BillingEvent.Recurring;
|
||||
import google.registry.model.billing.BillingRecurrence;
|
||||
import google.registry.model.domain.Domain;
|
||||
import google.registry.model.domain.DomainHistory;
|
||||
import google.registry.model.domain.metadata.MetadataExtension;
|
||||
@@ -117,9 +117,10 @@ public final class DomainTransferCancelFlow implements TransactionalFlow {
|
||||
targetId, newDomain.getTransferData(), null, domainHistoryId));
|
||||
// Reopen the autorenew event and poll message that we closed for the implicit transfer. This
|
||||
// may recreate the autorenew poll message if it was deleted when the transfer request was made.
|
||||
Recurring existingRecurring = tm().loadByKey(existingDomain.getAutorenewBillingEvent());
|
||||
BillingRecurrence existingBillingRecurrence =
|
||||
tm().loadByKey(existingDomain.getAutorenewBillingEvent());
|
||||
updateAutorenewRecurrenceEndTime(
|
||||
existingDomain, existingRecurring, END_OF_TIME, domainHistory.getHistoryEntryId());
|
||||
existingDomain, existingBillingRecurrence, END_OF_TIME, domainHistory.getHistoryEntryId());
|
||||
// Delete the billing event and poll messages that were written in case the transfer would have
|
||||
// been implicitly server approved.
|
||||
tm().delete(existingDomain.getTransferData().getServerApproveEntities());
|
||||
|
||||
@@ -41,7 +41,7 @@ import google.registry.flows.FlowModule.Superuser;
|
||||
import google.registry.flows.FlowModule.TargetId;
|
||||
import google.registry.flows.TransactionalFlow;
|
||||
import google.registry.flows.annotations.ReportingSpec;
|
||||
import google.registry.model.billing.BillingEvent.Recurring;
|
||||
import google.registry.model.billing.BillingRecurrence;
|
||||
import google.registry.model.domain.Domain;
|
||||
import google.registry.model.domain.DomainHistory;
|
||||
import google.registry.model.domain.metadata.MetadataExtension;
|
||||
@@ -116,9 +116,10 @@ public final class DomainTransferRejectFlow implements TransactionalFlow {
|
||||
targetId, newDomain.getTransferData(), null, now, domainHistoryId));
|
||||
// Reopen the autorenew event and poll message that we closed for the implicit transfer. This
|
||||
// may end up recreating the poll message if it was deleted upon the transfer request.
|
||||
Recurring existingRecurring = tm().loadByKey(existingDomain.getAutorenewBillingEvent());
|
||||
BillingRecurrence existingBillingRecurrence =
|
||||
tm().loadByKey(existingDomain.getAutorenewBillingEvent());
|
||||
updateAutorenewRecurrenceEndTime(
|
||||
existingDomain, existingRecurring, END_OF_TIME, domainHistory.getHistoryEntryId());
|
||||
existingDomain, existingBillingRecurrence, END_OF_TIME, domainHistory.getHistoryEntryId());
|
||||
// Delete the billing event and poll messages that were written in case the transfer would have
|
||||
// been implicitly server approved.
|
||||
tm().delete(existingDomain.getTransferData().getServerApproveEntities());
|
||||
|
||||
@@ -52,7 +52,7 @@ import google.registry.flows.exceptions.InvalidTransferPeriodValueException;
|
||||
import google.registry.flows.exceptions.ObjectAlreadySponsoredException;
|
||||
import google.registry.flows.exceptions.TransferPeriodMustBeOneYearException;
|
||||
import google.registry.flows.exceptions.TransferPeriodZeroAndFeeTransferExtensionException;
|
||||
import google.registry.model.billing.BillingEvent.Recurring;
|
||||
import google.registry.model.billing.BillingRecurrence;
|
||||
import google.registry.model.domain.Domain;
|
||||
import google.registry.model.domain.DomainCommand.Transfer;
|
||||
import google.registry.model.domain.DomainHistory;
|
||||
@@ -195,13 +195,14 @@ public final class DomainTransferRequestFlow implements TransactionalFlow {
|
||||
throw new TransferPeriodZeroAndFeeTransferExtensionException();
|
||||
}
|
||||
// If the period is zero, then there is no fee for the transfer.
|
||||
Recurring existingRecurring = tm().loadByKey(existingDomain.getAutorenewBillingEvent());
|
||||
BillingRecurrence existingBillingRecurrence =
|
||||
tm().loadByKey(existingDomain.getAutorenewBillingEvent());
|
||||
Optional<FeesAndCredits> feesAndCredits;
|
||||
if (period.getValue() == 0) {
|
||||
feesAndCredits = Optional.empty();
|
||||
} else if (!existingDomain.getCurrentPackageToken().isPresent()) {
|
||||
feesAndCredits =
|
||||
Optional.of(pricingLogic.getTransferPrice(tld, targetId, now, existingRecurring));
|
||||
Optional.of(pricingLogic.getTransferPrice(tld, targetId, now, existingBillingRecurrence));
|
||||
} else {
|
||||
// If existing domain is in a package, calculate the transfer price with default renewal price
|
||||
// behavior
|
||||
@@ -243,7 +244,7 @@ public final class DomainTransferRequestFlow implements TransactionalFlow {
|
||||
serverApproveNewExpirationTime,
|
||||
domainHistoryId,
|
||||
existingDomain,
|
||||
existingRecurring,
|
||||
existingBillingRecurrence,
|
||||
trid,
|
||||
gainingClientId,
|
||||
feesAndCredits.map(FeesAndCredits::getTotalCost),
|
||||
@@ -274,7 +275,7 @@ public final class DomainTransferRequestFlow implements TransactionalFlow {
|
||||
// cloneProjectedAtTime() will replace these old autorenew entities with the server approve ones
|
||||
// that we've created in this flow and stored in pendingTransferData.
|
||||
updateAutorenewRecurrenceEndTime(
|
||||
existingDomain, existingRecurring, automaticTransferTime, domainHistoryId);
|
||||
existingDomain, existingBillingRecurrence, automaticTransferTime, domainHistoryId);
|
||||
Domain newDomain =
|
||||
existingDomain
|
||||
.asBuilder()
|
||||
|
||||
@@ -20,11 +20,12 @@ import static google.registry.util.DateTimeUtils.END_OF_TIME;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import google.registry.model.billing.BillingBase.Flag;
|
||||
import google.registry.model.billing.BillingBase.Reason;
|
||||
import google.registry.model.billing.BillingBase.RenewalPriceBehavior;
|
||||
import google.registry.model.billing.BillingCancellation;
|
||||
import google.registry.model.billing.BillingEvent;
|
||||
import google.registry.model.billing.BillingEvent.Flag;
|
||||
import google.registry.model.billing.BillingEvent.Reason;
|
||||
import google.registry.model.billing.BillingEvent.Recurring;
|
||||
import google.registry.model.billing.BillingEvent.RenewalPriceBehavior;
|
||||
import google.registry.model.billing.BillingRecurrence;
|
||||
import google.registry.model.domain.Domain;
|
||||
import google.registry.model.domain.GracePeriod;
|
||||
import google.registry.model.domain.Period;
|
||||
@@ -66,8 +67,8 @@ public final class DomainTransferUtils {
|
||||
// Unless superuser sets period to 0, add a transfer billing event.
|
||||
transferDataBuilder.setServerApproveBillingEvent(
|
||||
serverApproveEntities.stream()
|
||||
.filter(BillingEvent.OneTime.class::isInstance)
|
||||
.map(BillingEvent.OneTime.class::cast)
|
||||
.filter(BillingEvent.class::isInstance)
|
||||
.map(BillingEvent.class::cast)
|
||||
.collect(onlyElement())
|
||||
.createVKey());
|
||||
}
|
||||
@@ -75,8 +76,8 @@ public final class DomainTransferUtils {
|
||||
.setTransferStatus(TransferStatus.PENDING)
|
||||
.setServerApproveAutorenewEvent(
|
||||
serverApproveEntities.stream()
|
||||
.filter(BillingEvent.Recurring.class::isInstance)
|
||||
.map(BillingEvent.Recurring.class::cast)
|
||||
.filter(BillingRecurrence.class::isInstance)
|
||||
.map(BillingRecurrence.class::cast)
|
||||
.collect(onlyElement())
|
||||
.createVKey())
|
||||
.setServerApproveAutorenewPollMessage(
|
||||
@@ -110,7 +111,7 @@ public final class DomainTransferUtils {
|
||||
DateTime serverApproveNewExpirationTime,
|
||||
HistoryEntryId domainHistoryId,
|
||||
Domain existingDomain,
|
||||
Recurring existingRecurring,
|
||||
BillingRecurrence existingBillingRecurrence,
|
||||
Trid trid,
|
||||
String gainingRegistrarId,
|
||||
Optional<Money> transferCost,
|
||||
@@ -146,12 +147,12 @@ public final class DomainTransferUtils {
|
||||
.add(
|
||||
createGainingClientAutorenewEvent(
|
||||
existingDomain.getCurrentPackageToken().isPresent()
|
||||
? existingRecurring
|
||||
? existingBillingRecurrence
|
||||
.asBuilder()
|
||||
.setRenewalPriceBehavior(RenewalPriceBehavior.DEFAULT)
|
||||
.setRenewalPrice(null)
|
||||
.build()
|
||||
: existingRecurring,
|
||||
: existingBillingRecurrence,
|
||||
serverApproveNewExpirationTime,
|
||||
domainHistoryId,
|
||||
targetId,
|
||||
@@ -246,21 +247,21 @@ public final class DomainTransferUtils {
|
||||
.build();
|
||||
}
|
||||
|
||||
private static BillingEvent.Recurring createGainingClientAutorenewEvent(
|
||||
Recurring existingRecurring,
|
||||
private static BillingRecurrence createGainingClientAutorenewEvent(
|
||||
BillingRecurrence existingBillingRecurrence,
|
||||
DateTime serverApproveNewExpirationTime,
|
||||
HistoryEntryId domainHistoryId,
|
||||
String targetId,
|
||||
String gainingRegistrarId) {
|
||||
return new BillingEvent.Recurring.Builder()
|
||||
return new BillingRecurrence.Builder()
|
||||
.setReason(Reason.RENEW)
|
||||
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
|
||||
.setTargetId(targetId)
|
||||
.setRegistrarId(gainingRegistrarId)
|
||||
.setEventTime(serverApproveNewExpirationTime)
|
||||
.setRecurrenceEndTime(END_OF_TIME)
|
||||
.setRenewalPriceBehavior(existingRecurring.getRenewalPriceBehavior())
|
||||
.setRenewalPrice(existingRecurring.getRenewalPrice().orElse(null))
|
||||
.setRenewalPriceBehavior(existingBillingRecurrence.getRenewalPriceBehavior())
|
||||
.setRenewalPrice(existingBillingRecurrence.getRenewalPrice().orElse(null))
|
||||
.setDomainHistoryId(domainHistoryId)
|
||||
.build();
|
||||
}
|
||||
@@ -281,7 +282,7 @@ public final class DomainTransferUtils {
|
||||
* <p>For details on the policy justification, see b/19430703#comment17 and <a
|
||||
* href="https://www.icann.org/news/advisory-2002-06-06-en">this ICANN advisory</a>.
|
||||
*/
|
||||
private static Optional<BillingEvent.Cancellation> createOptionalAutorenewCancellation(
|
||||
private static Optional<BillingCancellation> createOptionalAutorenewCancellation(
|
||||
DateTime automaticTransferTime,
|
||||
DateTime now,
|
||||
HistoryEntryId domainHistoryId,
|
||||
@@ -294,8 +295,7 @@ public final class DomainTransferUtils {
|
||||
domainAtTransferTime.getGracePeriodsOfType(GracePeriodStatus.AUTO_RENEW), null);
|
||||
if (autorenewGracePeriod != null && transferCost.isPresent()) {
|
||||
return Optional.of(
|
||||
BillingEvent.Cancellation.forGracePeriod(
|
||||
autorenewGracePeriod, now, domainHistoryId, targetId)
|
||||
BillingCancellation.forGracePeriod(autorenewGracePeriod, now, domainHistoryId, targetId)
|
||||
.asBuilder()
|
||||
.setEventTime(automaticTransferTime)
|
||||
.build());
|
||||
@@ -303,14 +303,14 @@ public final class DomainTransferUtils {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
private static BillingEvent.OneTime createTransferBillingEvent(
|
||||
private static BillingEvent createTransferBillingEvent(
|
||||
DateTime automaticTransferTime,
|
||||
HistoryEntryId domainHistoryId,
|
||||
String targetId,
|
||||
String gainingRegistrarId,
|
||||
Tld registry,
|
||||
Money transferCost) {
|
||||
return new BillingEvent.OneTime.Builder()
|
||||
return new BillingEvent.Builder()
|
||||
.setReason(Reason.TRANSFER)
|
||||
.setTargetId(targetId)
|
||||
.setRegistrarId(gainingRegistrarId)
|
||||
|
||||
@@ -64,8 +64,8 @@ import google.registry.flows.custom.EntityChanges;
|
||||
import google.registry.flows.domain.DomainFlowUtils.MissingRegistrantException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.NameserversNotSpecifiedForTldWithNameserverAllowListException;
|
||||
import google.registry.model.ImmutableObject;
|
||||
import google.registry.model.billing.BillingBase.Reason;
|
||||
import google.registry.model.billing.BillingEvent;
|
||||
import google.registry.model.billing.BillingEvent.Reason;
|
||||
import google.registry.model.domain.DesignatedContact;
|
||||
import google.registry.model.domain.Domain;
|
||||
import google.registry.model.domain.DomainCommand.Update;
|
||||
@@ -187,7 +187,7 @@ public final class DomainUpdateFlow implements TransactionalFlow {
|
||||
}
|
||||
ImmutableSet.Builder<ImmutableObject> entitiesToSave = new ImmutableSet.Builder<>();
|
||||
entitiesToSave.add(newDomain, domainHistory);
|
||||
Optional<BillingEvent.OneTime> statusUpdateBillingEvent =
|
||||
Optional<BillingEvent> statusUpdateBillingEvent =
|
||||
createBillingEventForStatusUpdates(existingDomain, newDomain, domainHistory, now);
|
||||
statusUpdateBillingEvent.ifPresent(entitiesToSave::add);
|
||||
Optional<PollMessage.OneTime> serverStatusUpdatePollMessage =
|
||||
@@ -324,7 +324,7 @@ public final class DomainUpdateFlow implements TransactionalFlow {
|
||||
}
|
||||
|
||||
/** Some status updates cost money. Bill only once no matter how many of them are changed. */
|
||||
private Optional<BillingEvent.OneTime> createBillingEventForStatusUpdates(
|
||||
private Optional<BillingEvent> createBillingEventForStatusUpdates(
|
||||
Domain existingDomain, Domain newDomain, DomainHistory historyEntry, DateTime now) {
|
||||
Optional<MetadataExtension> metadataExtension =
|
||||
eppInput.getSingleExtension(MetadataExtension.class);
|
||||
@@ -334,7 +334,7 @@ public final class DomainUpdateFlow implements TransactionalFlow {
|
||||
if (statusValue.isChargedStatus()) {
|
||||
// Only charge once.
|
||||
return Optional.of(
|
||||
new BillingEvent.OneTime.Builder()
|
||||
new BillingEvent.Builder()
|
||||
.setReason(Reason.SERVER_STATUS)
|
||||
.setTargetId(targetId)
|
||||
.setRegistrarId(registrarId)
|
||||
|
||||
@@ -26,8 +26,8 @@ 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.model.billing.BillingEvent.Recurring;
|
||||
import google.registry.model.billing.BillingEvent.RenewalPriceBehavior;
|
||||
import google.registry.model.billing.BillingBase.RenewalPriceBehavior;
|
||||
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;
|
||||
@@ -236,16 +236,16 @@ public class AllocationTokenFlowUtils {
|
||||
return domain;
|
||||
}
|
||||
|
||||
Recurring newRecurringBillingEvent =
|
||||
BillingRecurrence newBillingRecurrence =
|
||||
tm().loadByKey(domain.getAutorenewBillingEvent())
|
||||
.asBuilder()
|
||||
.setRenewalPriceBehavior(RenewalPriceBehavior.DEFAULT)
|
||||
.setRenewalPrice(null)
|
||||
.build();
|
||||
|
||||
// the Recurring billing event is reloaded later in the renew flow, so we synchronize changed
|
||||
// RecurringBillingEvent with storage manually
|
||||
tm().put(newRecurringBillingEvent);
|
||||
// the Recurrence is reloaded later in the renew flow, so we synchronize changed
|
||||
// Recurrences with storage manually
|
||||
tm().put(newBillingRecurrence);
|
||||
tm().getEntityManager().flush();
|
||||
tm().getEntityManager().clear();
|
||||
|
||||
@@ -253,7 +253,7 @@ public class AllocationTokenFlowUtils {
|
||||
return domain
|
||||
.asBuilder()
|
||||
.setCurrentPackageToken(null)
|
||||
.setAutorenewBillingEvent(newRecurringBillingEvent.createVKey())
|
||||
.setAutorenewBillingEvent(newBillingRecurrence.createVKey())
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
// Copyright 2017 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.model.billing;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static google.registry.util.CollectionUtils.forceEmptyToNull;
|
||||
import static google.registry.util.CollectionUtils.nullToEmptyImmutableCopy;
|
||||
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import google.registry.model.Buildable;
|
||||
import google.registry.model.ImmutableObject;
|
||||
import google.registry.model.UnsafeSerializable;
|
||||
import google.registry.model.annotations.IdAllocation;
|
||||
import google.registry.model.domain.DomainHistory;
|
||||
import google.registry.model.reporting.HistoryEntry.HistoryEntryId;
|
||||
import google.registry.model.transfer.TransferData.TransferServerApproveEntity;
|
||||
import google.registry.persistence.VKey;
|
||||
import java.util.Set;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.EnumType;
|
||||
import javax.persistence.Enumerated;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.MappedSuperclass;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
/** A billable event in a domain's lifecycle. */
|
||||
@MappedSuperclass
|
||||
public abstract class BillingBase extends ImmutableObject
|
||||
implements Buildable, TransferServerApproveEntity, UnsafeSerializable {
|
||||
|
||||
/** The reason for the bill, which maps 1:1 to skus in go/registry-billing-skus. */
|
||||
public enum Reason {
|
||||
CREATE(true),
|
||||
@Deprecated // DO NOT USE THIS REASON. IT REMAINS BECAUSE OF HISTORICAL DATA. SEE b/31676071.
|
||||
ERROR(false),
|
||||
FEE_EARLY_ACCESS(true),
|
||||
RENEW(true),
|
||||
RESTORE(true),
|
||||
SERVER_STATUS(false),
|
||||
TRANSFER(true);
|
||||
|
||||
private final boolean requiresPeriod;
|
||||
|
||||
Reason(boolean requiresPeriod) {
|
||||
this.requiresPeriod = requiresPeriod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether billing events with this reason have a period years associated with them.
|
||||
*
|
||||
* <p>Note that this is an "if an only if" condition.
|
||||
*/
|
||||
public boolean hasPeriodYears() {
|
||||
return requiresPeriod;
|
||||
}
|
||||
}
|
||||
|
||||
/** Set of flags that can be applied to billing events. */
|
||||
public enum Flag {
|
||||
ALLOCATION,
|
||||
ANCHOR_TENANT,
|
||||
AUTO_RENEW,
|
||||
/** Landrush billing events are historical only and are no longer created. */
|
||||
LANDRUSH,
|
||||
/**
|
||||
* This flag is used on create {@link BillingEvent} billing events for domains that were
|
||||
* reserved.
|
||||
*
|
||||
* <p>This can happen when allocation tokens are used or superusers override a domain
|
||||
* reservation. These cases can need special handling in billing/invoicing. Anchor tenants will
|
||||
* never have this flag applied; they will have ANCHOR_TENANT instead.
|
||||
*/
|
||||
RESERVED,
|
||||
SUNRISE,
|
||||
/**
|
||||
* This flag will be added to any {@link BillingEvent} events that are created via, e.g., an
|
||||
* automated process to expand {@link BillingRecurrence} events.
|
||||
*/
|
||||
SYNTHETIC
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets of renewal price behaviors that can be applied to billing recurrences.
|
||||
*
|
||||
* <p>When a client renews a domain, they could be charged differently, depending on factors such
|
||||
* as the client type and the domain itself.
|
||||
*/
|
||||
public enum RenewalPriceBehavior {
|
||||
/**
|
||||
* This indicates the renewal price is the default price.
|
||||
*
|
||||
* <p>By default, if the domain is premium, then premium price will be used. Otherwise, the
|
||||
* standard price of the TLD will be used.
|
||||
*/
|
||||
DEFAULT,
|
||||
/**
|
||||
* This indicates the domain will be renewed at standard price even if it's a premium domain.
|
||||
*
|
||||
* <p>We chose to name this "NONPREMIUM" rather than simply "STANDARD" to avoid confusion
|
||||
* between "STANDARD" and "DEFAULT".
|
||||
*
|
||||
* <p>This price behavior is used with anchor tenants.
|
||||
*/
|
||||
NONPREMIUM,
|
||||
/**
|
||||
* This indicates that the renewalPrice in {@link BillingRecurrence} will be used for domain
|
||||
* renewal.
|
||||
*
|
||||
* <p>The renewalPrice has a non-null value iff the price behavior is set to "SPECIFIED". This
|
||||
* behavior is used with internal registrations.
|
||||
*/
|
||||
SPECIFIED
|
||||
}
|
||||
|
||||
/** Entity id. */
|
||||
@IdAllocation @Id Long id;
|
||||
|
||||
/** The registrar to bill. */
|
||||
@Column(name = "registrarId", nullable = false)
|
||||
String clientId;
|
||||
|
||||
/** Revision id of the entry in DomainHistory table that ths bill belongs to. */
|
||||
@Column(nullable = false)
|
||||
Long domainHistoryRevisionId;
|
||||
|
||||
/** ID of the EPP resource that the bill is for. */
|
||||
@Column(nullable = false)
|
||||
String domainRepoId;
|
||||
|
||||
/** When this event was created. For recurrence events, this is also the recurrence start time. */
|
||||
@Column(nullable = false)
|
||||
DateTime eventTime;
|
||||
|
||||
/** The reason for the bill. */
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false)
|
||||
Reason reason;
|
||||
|
||||
/** The fully qualified domain name of the domain that the bill is for. */
|
||||
@Column(name = "domain_name", nullable = false)
|
||||
String targetId;
|
||||
|
||||
@Nullable Set<Flag> flags;
|
||||
|
||||
public String getRegistrarId() {
|
||||
return clientId;
|
||||
}
|
||||
|
||||
public long getDomainHistoryRevisionId() {
|
||||
return domainHistoryRevisionId;
|
||||
}
|
||||
|
||||
public String getDomainRepoId() {
|
||||
return domainRepoId;
|
||||
}
|
||||
|
||||
public DateTime getEventTime() {
|
||||
return eventTime;
|
||||
}
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public Reason getReason() {
|
||||
return reason;
|
||||
}
|
||||
|
||||
public String getTargetId() {
|
||||
return targetId;
|
||||
}
|
||||
|
||||
public HistoryEntryId getHistoryEntryId() {
|
||||
return new HistoryEntryId(domainRepoId, domainHistoryRevisionId);
|
||||
}
|
||||
|
||||
public ImmutableSet<Flag> getFlags() {
|
||||
return nullToEmptyImmutableCopy(flags);
|
||||
}
|
||||
|
||||
@Override
|
||||
public abstract VKey<? extends BillingBase> createVKey();
|
||||
|
||||
/** Override Buildable.asBuilder() to give this method stronger typing. */
|
||||
@Override
|
||||
public abstract Builder<?, ?> asBuilder();
|
||||
|
||||
/** An abstract builder for {@link BillingBase}. */
|
||||
public abstract static class Builder<T extends BillingBase, B extends Builder<?, ?>>
|
||||
extends GenericBuilder<T, B> {
|
||||
|
||||
protected Builder() {}
|
||||
|
||||
protected Builder(T instance) {
|
||||
super(instance);
|
||||
}
|
||||
|
||||
public B setReason(Reason reason) {
|
||||
getInstance().reason = reason;
|
||||
return thisCastToDerived();
|
||||
}
|
||||
|
||||
public B setId(long id) {
|
||||
getInstance().id = id;
|
||||
return thisCastToDerived();
|
||||
}
|
||||
|
||||
public B setRegistrarId(String registrarId) {
|
||||
getInstance().clientId = registrarId;
|
||||
return thisCastToDerived();
|
||||
}
|
||||
|
||||
public B setEventTime(DateTime eventTime) {
|
||||
getInstance().eventTime = eventTime;
|
||||
return thisCastToDerived();
|
||||
}
|
||||
|
||||
public B setTargetId(String targetId) {
|
||||
getInstance().targetId = targetId;
|
||||
return thisCastToDerived();
|
||||
}
|
||||
|
||||
public B setFlags(ImmutableSet<Flag> flags) {
|
||||
getInstance().flags = forceEmptyToNull(checkArgumentNotNull(flags, "flags"));
|
||||
return thisCastToDerived();
|
||||
}
|
||||
|
||||
public B setDomainHistoryId(HistoryEntryId domainHistoryId) {
|
||||
getInstance().domainHistoryRevisionId = domainHistoryId.getRevisionId();
|
||||
getInstance().domainRepoId = domainHistoryId.getRepoId();
|
||||
return thisCastToDerived();
|
||||
}
|
||||
|
||||
public B setDomainHistory(DomainHistory domainHistory) {
|
||||
return setDomainHistoryId(domainHistory.getHistoryEntryId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public T build() {
|
||||
T instance = getInstance();
|
||||
checkNotNull(instance.reason, "Reason must be set");
|
||||
checkNotNull(instance.clientId, "Registrar ID must be set");
|
||||
checkNotNull(instance.eventTime, "Event time must be set");
|
||||
checkNotNull(instance.targetId, "Target ID must be set");
|
||||
checkNotNull(instance.domainHistoryRevisionId, "Domain History Revision ID must be set");
|
||||
checkNotNull(instance.domainRepoId, "Domain Repo ID must be set");
|
||||
return super.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
// Copyright 2023 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.model.billing;
|
||||
|
||||
import static com.google.common.base.MoreObjects.firstNonNull;
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import google.registry.model.domain.GracePeriod;
|
||||
import google.registry.model.domain.rgp.GracePeriodStatus;
|
||||
import google.registry.model.reporting.HistoryEntry.HistoryEntryId;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.persistence.WithVKey;
|
||||
import javax.persistence.AttributeOverride;
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Index;
|
||||
import javax.persistence.Table;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
/**
|
||||
* An event representing a cancellation of one of the other two billable event types.
|
||||
*
|
||||
* <p>This is implemented as a separate event rather than a bit on BillingEvent in order to preserve
|
||||
* the immutability of billing events.
|
||||
*/
|
||||
@Entity
|
||||
@Table(
|
||||
indexes = {
|
||||
@Index(columnList = "registrarId"),
|
||||
@Index(columnList = "eventTime"),
|
||||
@Index(columnList = "domainRepoId"),
|
||||
@Index(columnList = "billingTime"),
|
||||
@Index(columnList = "billing_event_id"),
|
||||
@Index(columnList = "billing_recurrence_id")
|
||||
})
|
||||
@AttributeOverride(name = "id", column = @Column(name = "billing_cancellation_id"))
|
||||
@WithVKey(Long.class)
|
||||
public class BillingCancellation extends BillingBase {
|
||||
|
||||
/** The billing time of the charge that is being cancelled. */
|
||||
DateTime billingTime;
|
||||
|
||||
/** The one-time billing event to cancel, or null for autorenew cancellations. */
|
||||
@Column(name = "billing_event_id")
|
||||
VKey<BillingEvent> billingEvent;
|
||||
|
||||
/** The Recurrence to cancel, or null for non-autorenew cancellations. */
|
||||
@Column(name = "billing_recurrence_id")
|
||||
VKey<BillingRecurrence> billingRecurrence;
|
||||
|
||||
public DateTime getBillingTime() {
|
||||
return billingTime;
|
||||
}
|
||||
|
||||
public VKey<? extends BillingBase> getEventKey() {
|
||||
return firstNonNull(billingEvent, billingRecurrence);
|
||||
}
|
||||
|
||||
/** The mapping from billable grace period types to originating billing event reasons. */
|
||||
static final ImmutableMap<GracePeriodStatus, Reason> GRACE_PERIOD_TO_REASON =
|
||||
ImmutableMap.of(
|
||||
GracePeriodStatus.ADD, Reason.CREATE,
|
||||
GracePeriodStatus.AUTO_RENEW, Reason.RENEW,
|
||||
GracePeriodStatus.RENEW, Reason.RENEW,
|
||||
GracePeriodStatus.TRANSFER, Reason.TRANSFER);
|
||||
|
||||
/**
|
||||
* Creates a cancellation billing event (parented on the provided history key, and with the
|
||||
* corresponding event time) that will cancel out the provided grace period's billing event, using
|
||||
* the supplied targetId and deriving other metadata (clientId, billing time, and the cancellation
|
||||
* reason) from the grace period.
|
||||
*/
|
||||
public static google.registry.model.billing.BillingCancellation forGracePeriod(
|
||||
GracePeriod gracePeriod,
|
||||
DateTime eventTime,
|
||||
HistoryEntryId domainHistoryId,
|
||||
String targetId) {
|
||||
checkArgument(
|
||||
gracePeriod.hasBillingEvent(),
|
||||
"Cannot create cancellation for grace period without billing event");
|
||||
Builder builder =
|
||||
new Builder()
|
||||
.setReason(checkNotNull(GRACE_PERIOD_TO_REASON.get(gracePeriod.getType())))
|
||||
.setTargetId(targetId)
|
||||
.setRegistrarId(gracePeriod.getRegistrarId())
|
||||
.setEventTime(eventTime)
|
||||
// The charge being cancelled will take place at the grace period's expiration time.
|
||||
.setBillingTime(gracePeriod.getExpirationTime())
|
||||
.setDomainHistoryId(domainHistoryId);
|
||||
// Set the grace period's billing event using the appropriate Cancellation builder method.
|
||||
if (gracePeriod.getBillingEvent() != null) {
|
||||
builder.setBillingEvent(gracePeriod.getBillingEvent());
|
||||
} else if (gracePeriod.getBillingRecurrence() != null) {
|
||||
builder.setBillingRecurrence(gracePeriod.getBillingRecurrence());
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public VKey<google.registry.model.billing.BillingCancellation> createVKey() {
|
||||
return createVKey(getId());
|
||||
}
|
||||
|
||||
public static VKey<google.registry.model.billing.BillingCancellation> createVKey(long id) {
|
||||
return VKey.create(google.registry.model.billing.BillingCancellation.class, id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder asBuilder() {
|
||||
return new Builder(clone(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder for {@link google.registry.model.billing.BillingCancellation} since it is immutable.
|
||||
*/
|
||||
public static class Builder
|
||||
extends BillingBase.Builder<google.registry.model.billing.BillingCancellation, Builder> {
|
||||
|
||||
public Builder() {}
|
||||
|
||||
private Builder(google.registry.model.billing.BillingCancellation instance) {
|
||||
super(instance);
|
||||
}
|
||||
|
||||
public Builder setBillingTime(DateTime billingTime) {
|
||||
getInstance().billingTime = billingTime;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setBillingEvent(VKey<BillingEvent> billingEvent) {
|
||||
getInstance().billingEvent = billingEvent;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setBillingRecurrence(VKey<BillingRecurrence> billingRecurrence) {
|
||||
getInstance().billingRecurrence = billingRecurrence;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public google.registry.model.billing.BillingCancellation build() {
|
||||
google.registry.model.billing.BillingCancellation instance = getInstance();
|
||||
checkNotNull(instance.billingTime, "Must set billing time");
|
||||
checkNotNull(instance.reason, "Must set reason");
|
||||
checkState(
|
||||
(instance.billingEvent == null) != (instance.billingRecurrence == null),
|
||||
"Cancellations must have exactly one billing event key set");
|
||||
return super.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
// Copyright 2023 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.
|
||||
@@ -14,740 +14,197 @@
|
||||
|
||||
package google.registry.model.billing;
|
||||
|
||||
import static com.google.common.base.MoreObjects.firstNonNull;
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
import static google.registry.util.CollectionUtils.forceEmptyToNull;
|
||||
import static google.registry.util.CollectionUtils.nullToEmptyImmutableCopy;
|
||||
import static google.registry.util.DateTimeUtils.END_OF_TIME;
|
||||
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import google.registry.model.Buildable;
|
||||
import google.registry.model.ImmutableObject;
|
||||
import google.registry.model.UnsafeSerializable;
|
||||
import google.registry.model.annotations.IdAllocation;
|
||||
import google.registry.model.common.TimeOfYear;
|
||||
import google.registry.model.domain.DomainHistory;
|
||||
import google.registry.model.domain.GracePeriod;
|
||||
import google.registry.model.domain.rgp.GracePeriodStatus;
|
||||
import google.registry.model.domain.token.AllocationToken;
|
||||
import google.registry.model.reporting.HistoryEntry.HistoryEntryId;
|
||||
import google.registry.model.transfer.TransferData.TransferServerApproveEntity;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.persistence.WithVKey;
|
||||
import google.registry.persistence.converter.JodaMoneyType;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.persistence.AttributeOverride;
|
||||
import javax.persistence.AttributeOverrides;
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Embedded;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.EnumType;
|
||||
import javax.persistence.Enumerated;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.Index;
|
||||
import javax.persistence.MappedSuperclass;
|
||||
import javax.persistence.Table;
|
||||
import org.hibernate.annotations.Columns;
|
||||
import org.hibernate.annotations.Type;
|
||||
import org.joda.money.Money;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
/** A billable event in a domain's lifecycle. */
|
||||
@MappedSuperclass
|
||||
public abstract class BillingEvent extends ImmutableObject
|
||||
implements Buildable, TransferServerApproveEntity, UnsafeSerializable {
|
||||
/** A one-time billable event. */
|
||||
@Entity
|
||||
@Table(
|
||||
indexes = {
|
||||
@Index(columnList = "registrarId"),
|
||||
@Index(columnList = "eventTime"),
|
||||
@Index(columnList = "billingTime"),
|
||||
@Index(columnList = "syntheticCreationTime"),
|
||||
@Index(columnList = "domainRepoId"),
|
||||
@Index(columnList = "allocationToken"),
|
||||
@Index(columnList = "cancellation_matching_billing_recurrence_id")
|
||||
})
|
||||
@AttributeOverride(name = "id", column = @Column(name = "billing_event_id"))
|
||||
@WithVKey(Long.class)
|
||||
public class BillingEvent extends BillingBase {
|
||||
|
||||
/** The reason for the bill, which maps 1:1 to skus in go/registry-billing-skus. */
|
||||
public enum Reason {
|
||||
CREATE(true),
|
||||
@Deprecated // DO NOT USE THIS REASON. IT REMAINS BECAUSE OF HISTORICAL DATA. SEE b/31676071.
|
||||
ERROR(false),
|
||||
FEE_EARLY_ACCESS(true),
|
||||
RENEW(true),
|
||||
RESTORE(true),
|
||||
SERVER_STATUS(false),
|
||||
TRANSFER(true);
|
||||
/** The billable value. */
|
||||
@Type(type = JodaMoneyType.TYPE_NAME)
|
||||
@Columns(columns = {@Column(name = "cost_amount"), @Column(name = "cost_currency")})
|
||||
Money cost;
|
||||
|
||||
private final boolean requiresPeriod;
|
||||
|
||||
Reason(boolean requiresPeriod) {
|
||||
this.requiresPeriod = requiresPeriod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether billing events with this reason have a period years associated with them.
|
||||
*
|
||||
* <p>Note that this is an "if an only if" condition.
|
||||
*/
|
||||
public boolean hasPeriodYears() {
|
||||
return requiresPeriod;
|
||||
}
|
||||
}
|
||||
|
||||
/** Set of flags that can be applied to billing events. */
|
||||
public enum Flag {
|
||||
ALLOCATION,
|
||||
ANCHOR_TENANT,
|
||||
AUTO_RENEW,
|
||||
/** Landrush billing events are historical only and are no longer created. */
|
||||
LANDRUSH,
|
||||
/**
|
||||
* This flag is used on create {@link OneTime} billing events for domains that were reserved.
|
||||
*
|
||||
* <p>This can happen when allocation tokens are used or superusers override a domain
|
||||
* reservation. These cases can need special handling in billing/invoicing. Anchor tenants will
|
||||
* never have this flag applied; they will have ANCHOR_TENANT instead.
|
||||
*/
|
||||
RESERVED,
|
||||
SUNRISE,
|
||||
/**
|
||||
* This flag will be added to any {@link OneTime} events that are created via, e.g., an
|
||||
* automated process to expand {@link Recurring} events.
|
||||
*/
|
||||
SYNTHETIC
|
||||
}
|
||||
/** When the cost should be billed. */
|
||||
DateTime billingTime;
|
||||
|
||||
/**
|
||||
* Sets of renewal price behaviors that can be applied to billing recurrences.
|
||||
*
|
||||
* <p>When a client renews a domain, they could be charged differently, depending on factors such
|
||||
* as the client type and the domain itself.
|
||||
* The period in years of the action being billed for, if applicable, otherwise null. Used for
|
||||
* financial reporting.
|
||||
*/
|
||||
public enum RenewalPriceBehavior {
|
||||
/**
|
||||
* This indicates the renewal price is the default price.
|
||||
*
|
||||
* <p>By default, if the domain is premium, then premium price will be used. Otherwise, the
|
||||
* standard price of the TLD will be used.
|
||||
*/
|
||||
DEFAULT,
|
||||
/**
|
||||
* This indicates the domain will be renewed at standard price even if it's a premium domain.
|
||||
*
|
||||
* <p>We chose to name this "NONPREMIUM" rather than simply "STANDARD" to avoid confusion
|
||||
* between "STANDARD" and "DEFAULT".
|
||||
*
|
||||
* <p>This price behavior is used with anchor tenants.
|
||||
*/
|
||||
NONPREMIUM,
|
||||
/**
|
||||
* This indicates that the renewalPrice in {@link Recurring} will be used for domain renewal.
|
||||
*
|
||||
* <p>The renewalPrice has a non-null value iff the price behavior is set to "SPECIFIED". This
|
||||
* behavior is used with internal registrations.
|
||||
*/
|
||||
SPECIFIED
|
||||
Integer periodYears;
|
||||
|
||||
/**
|
||||
* For {@link Flag#SYNTHETIC} events, when this event was persisted to the database (i.e. the
|
||||
* cursor position at the time the recurrence expansion job was last run). In the event a job
|
||||
* needs to be undone, a query on this field will return the complete set of potentially bad
|
||||
* events.
|
||||
*/
|
||||
DateTime syntheticCreationTime;
|
||||
|
||||
/**
|
||||
* For {@link Flag#SYNTHETIC} events, a {@link VKey} to the {@link BillingRecurrence} from which
|
||||
* this {@link google.registry.model.billing.BillingEvent} was created. This is needed in order to
|
||||
* properly match billing events against {@link BillingCancellation}s.
|
||||
*/
|
||||
@Column(name = "cancellation_matching_billing_recurrence_id")
|
||||
VKey<BillingRecurrence> cancellationMatchingBillingEvent;
|
||||
|
||||
/**
|
||||
* For {@link Flag#SYNTHETIC} events, the {@link DomainHistory} revision ID of the {@link
|
||||
* BillingRecurrence} from which this {@link google.registry.model.billing.BillingEvent} was
|
||||
* created. This is needed in order to recreate the {@link VKey} when reading from SQL.
|
||||
*/
|
||||
@Column(name = "recurrence_history_revision_id")
|
||||
Long recurrenceHistoryRevisionId;
|
||||
|
||||
/**
|
||||
* The {@link AllocationToken} used in the creation of this event, or null if one was not used.
|
||||
*/
|
||||
@Nullable VKey<AllocationToken> allocationToken;
|
||||
|
||||
public Money getCost() {
|
||||
return cost;
|
||||
}
|
||||
|
||||
/** Entity id. */
|
||||
@IdAllocation @Id Long id;
|
||||
|
||||
/** The registrar to bill. */
|
||||
@Column(name = "registrarId", nullable = false)
|
||||
String clientId;
|
||||
|
||||
/** Revision id of the entry in DomainHistory table that ths bill belongs to. */
|
||||
@Column(nullable = false)
|
||||
Long domainHistoryRevisionId;
|
||||
|
||||
/** ID of the EPP resource that the bill is for. */
|
||||
@Column(nullable = false)
|
||||
String domainRepoId;
|
||||
|
||||
/** When this event was created. For recurring events, this is also the recurrence start time. */
|
||||
@Column(nullable = false)
|
||||
DateTime eventTime;
|
||||
|
||||
/** The reason for the bill. */
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false)
|
||||
Reason reason;
|
||||
|
||||
/** The fully qualified domain name of the domain that the bill is for. */
|
||||
@Column(name = "domain_name", nullable = false)
|
||||
String targetId;
|
||||
|
||||
@Nullable Set<Flag> flags;
|
||||
|
||||
public String getRegistrarId() {
|
||||
return clientId;
|
||||
public DateTime getBillingTime() {
|
||||
return billingTime;
|
||||
}
|
||||
|
||||
public long getDomainHistoryRevisionId() {
|
||||
return domainHistoryRevisionId;
|
||||
public Integer getPeriodYears() {
|
||||
return periodYears;
|
||||
}
|
||||
|
||||
public String getDomainRepoId() {
|
||||
return domainRepoId;
|
||||
public DateTime getSyntheticCreationTime() {
|
||||
return syntheticCreationTime;
|
||||
}
|
||||
|
||||
public DateTime getEventTime() {
|
||||
return eventTime;
|
||||
public VKey<BillingRecurrence> getCancellationMatchingBillingEvent() {
|
||||
return cancellationMatchingBillingEvent;
|
||||
}
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
public Long getRecurrenceHistoryRevisionId() {
|
||||
return recurrenceHistoryRevisionId;
|
||||
}
|
||||
|
||||
public Reason getReason() {
|
||||
return reason;
|
||||
}
|
||||
|
||||
public String getTargetId() {
|
||||
return targetId;
|
||||
}
|
||||
|
||||
public HistoryEntryId getHistoryEntryId() {
|
||||
return new HistoryEntryId(domainRepoId, domainHistoryRevisionId);
|
||||
}
|
||||
|
||||
public ImmutableSet<Flag> getFlags() {
|
||||
return nullToEmptyImmutableCopy(flags);
|
||||
public Optional<VKey<AllocationToken>> getAllocationToken() {
|
||||
return Optional.ofNullable(allocationToken);
|
||||
}
|
||||
|
||||
@Override
|
||||
public abstract VKey<? extends BillingEvent> createVKey();
|
||||
public VKey<google.registry.model.billing.BillingEvent> createVKey() {
|
||||
return createVKey(getId());
|
||||
}
|
||||
|
||||
public static VKey<google.registry.model.billing.BillingEvent> createVKey(long id) {
|
||||
return VKey.create(google.registry.model.billing.BillingEvent.class, id);
|
||||
}
|
||||
|
||||
/** Override Buildable.asBuilder() to give this method stronger typing. */
|
||||
@Override
|
||||
public abstract Builder<?, ?> asBuilder();
|
||||
public Builder asBuilder() {
|
||||
return new Builder(clone(this));
|
||||
}
|
||||
|
||||
/** An abstract builder for {@link BillingEvent}. */
|
||||
public abstract static class Builder<T extends BillingEvent, B extends Builder<?, ?>>
|
||||
extends GenericBuilder<T, B> {
|
||||
/** A builder for {@link google.registry.model.billing.BillingEvent} since it is immutable. */
|
||||
public static class Builder
|
||||
extends BillingBase.Builder<google.registry.model.billing.BillingEvent, Builder> {
|
||||
|
||||
protected Builder() {}
|
||||
public Builder() {}
|
||||
|
||||
protected Builder(T instance) {
|
||||
private Builder(google.registry.model.billing.BillingEvent instance) {
|
||||
super(instance);
|
||||
}
|
||||
|
||||
public B setReason(Reason reason) {
|
||||
getInstance().reason = reason;
|
||||
return thisCastToDerived();
|
||||
public Builder setCost(Money cost) {
|
||||
getInstance().cost = cost;
|
||||
return this;
|
||||
}
|
||||
|
||||
public B setId(long id) {
|
||||
getInstance().id = id;
|
||||
return thisCastToDerived();
|
||||
public Builder setPeriodYears(Integer periodYears) {
|
||||
checkNotNull(periodYears);
|
||||
checkArgument(periodYears > 0);
|
||||
getInstance().periodYears = periodYears;
|
||||
return this;
|
||||
}
|
||||
|
||||
public B setRegistrarId(String registrarId) {
|
||||
getInstance().clientId = registrarId;
|
||||
return thisCastToDerived();
|
||||
public Builder setBillingTime(DateTime billingTime) {
|
||||
getInstance().billingTime = billingTime;
|
||||
return this;
|
||||
}
|
||||
|
||||
public B setEventTime(DateTime eventTime) {
|
||||
getInstance().eventTime = eventTime;
|
||||
return thisCastToDerived();
|
||||
public Builder setSyntheticCreationTime(DateTime syntheticCreationTime) {
|
||||
getInstance().syntheticCreationTime = syntheticCreationTime;
|
||||
return this;
|
||||
}
|
||||
|
||||
public B setTargetId(String targetId) {
|
||||
getInstance().targetId = targetId;
|
||||
return thisCastToDerived();
|
||||
public Builder setCancellationMatchingBillingEvent(
|
||||
BillingRecurrence cancellationMatchingBillingEvent) {
|
||||
getInstance().cancellationMatchingBillingEvent =
|
||||
cancellationMatchingBillingEvent.createVKey();
|
||||
getInstance().recurrenceHistoryRevisionId =
|
||||
cancellationMatchingBillingEvent.getDomainHistoryRevisionId();
|
||||
return this;
|
||||
}
|
||||
|
||||
public B setFlags(ImmutableSet<Flag> flags) {
|
||||
getInstance().flags = forceEmptyToNull(checkArgumentNotNull(flags, "flags"));
|
||||
return thisCastToDerived();
|
||||
}
|
||||
|
||||
public B setDomainHistoryId(HistoryEntryId domainHistoryId) {
|
||||
getInstance().domainHistoryRevisionId = domainHistoryId.getRevisionId();
|
||||
getInstance().domainRepoId = domainHistoryId.getRepoId();
|
||||
return thisCastToDerived();
|
||||
}
|
||||
|
||||
public B setDomainHistory(DomainHistory domainHistory) {
|
||||
return setDomainHistoryId(domainHistory.getHistoryEntryId());
|
||||
public Builder setAllocationToken(@Nullable VKey<AllocationToken> allocationToken) {
|
||||
getInstance().allocationToken = allocationToken;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public T build() {
|
||||
T instance = getInstance();
|
||||
checkNotNull(instance.reason, "Reason must be set");
|
||||
checkNotNull(instance.clientId, "Registrar ID must be set");
|
||||
checkNotNull(instance.eventTime, "Event time must be set");
|
||||
checkNotNull(instance.targetId, "Target ID must be set");
|
||||
checkNotNull(instance.domainHistoryRevisionId, "Domain History Revision ID must be set");
|
||||
checkNotNull(instance.domainRepoId, "Domain Repo ID must be set");
|
||||
public google.registry.model.billing.BillingEvent build() {
|
||||
google.registry.model.billing.BillingEvent instance = getInstance();
|
||||
checkNotNull(instance.billingTime);
|
||||
checkNotNull(instance.cost);
|
||||
checkState(!instance.cost.isNegative(), "Costs should be non-negative.");
|
||||
// TODO(mcilwain): Enforce this check on all billing events (not just more recent ones)
|
||||
// post-migration after we add the missing period years values in SQL.
|
||||
if (instance.eventTime.isAfter(DateTime.parse("2019-01-01T00:00:00Z"))) {
|
||||
checkState(
|
||||
instance.reason.hasPeriodYears() == (instance.periodYears != null),
|
||||
"Period years must be set if and only if reason is "
|
||||
+ "CREATE, FEE_EARLY_ACCESS, RENEW, RESTORE or TRANSFER.");
|
||||
}
|
||||
checkState(
|
||||
instance.getFlags().contains(Flag.SYNTHETIC) == (instance.syntheticCreationTime != null),
|
||||
"Synthetic creation time must be set if and only if the SYNTHETIC flag is set.");
|
||||
checkState(
|
||||
instance.getFlags().contains(Flag.SYNTHETIC)
|
||||
== (instance.cancellationMatchingBillingEvent != null),
|
||||
"Cancellation matching billing event must be set if and only if the SYNTHETIC flag "
|
||||
+ "is set.");
|
||||
return super.build();
|
||||
}
|
||||
}
|
||||
|
||||
/** A one-time billable event. */
|
||||
@Entity(name = "BillingEvent")
|
||||
@Table(
|
||||
indexes = {
|
||||
@Index(columnList = "registrarId"),
|
||||
@Index(columnList = "eventTime"),
|
||||
@Index(columnList = "billingTime"),
|
||||
@Index(columnList = "syntheticCreationTime"),
|
||||
@Index(columnList = "domainRepoId"),
|
||||
@Index(columnList = "allocationToken"),
|
||||
@Index(columnList = "cancellation_matching_billing_recurrence_id")
|
||||
})
|
||||
@AttributeOverride(name = "id", column = @Column(name = "billing_event_id"))
|
||||
@WithVKey(Long.class)
|
||||
public static class OneTime extends BillingEvent {
|
||||
|
||||
/** The billable value. */
|
||||
@Type(type = JodaMoneyType.TYPE_NAME)
|
||||
@Columns(columns = {@Column(name = "cost_amount"), @Column(name = "cost_currency")})
|
||||
Money cost;
|
||||
|
||||
/** When the cost should be billed. */
|
||||
DateTime billingTime;
|
||||
|
||||
/**
|
||||
* The period in years of the action being billed for, if applicable, otherwise null. Used for
|
||||
* financial reporting.
|
||||
*/
|
||||
Integer periodYears;
|
||||
|
||||
/**
|
||||
* For {@link Flag#SYNTHETIC} events, when this event was persisted to the database (i.e. the
|
||||
* cursor position at the time the recurrence expansion job was last run). In the event a job
|
||||
* needs to be undone, a query on this field will return the complete set of potentially bad
|
||||
* events.
|
||||
*/
|
||||
DateTime syntheticCreationTime;
|
||||
|
||||
/**
|
||||
* For {@link Flag#SYNTHETIC} events, a {@link VKey} to the {@link Recurring} from which this
|
||||
* {@link OneTime} was created. This is needed in order to properly match billing events against
|
||||
* {@link Cancellation}s.
|
||||
*/
|
||||
@Column(name = "cancellation_matching_billing_recurrence_id")
|
||||
VKey<Recurring> cancellationMatchingBillingEvent;
|
||||
|
||||
/**
|
||||
* For {@link Flag#SYNTHETIC} events, the {@link DomainHistory} revision ID of the {@link
|
||||
* Recurring} from which this {@link OneTime} was created. This is needed in order to recreate
|
||||
* the {@link VKey} when reading from SQL.
|
||||
*/
|
||||
@Column(name = "recurrence_history_revision_id")
|
||||
Long recurringEventHistoryRevisionId;
|
||||
|
||||
/**
|
||||
* The {@link AllocationToken} used in the creation of this event, or null if one was not used.
|
||||
*/
|
||||
@Nullable VKey<AllocationToken> allocationToken;
|
||||
|
||||
public Money getCost() {
|
||||
return cost;
|
||||
}
|
||||
|
||||
public DateTime getBillingTime() {
|
||||
return billingTime;
|
||||
}
|
||||
|
||||
public Integer getPeriodYears() {
|
||||
return periodYears;
|
||||
}
|
||||
|
||||
public DateTime getSyntheticCreationTime() {
|
||||
return syntheticCreationTime;
|
||||
}
|
||||
|
||||
public VKey<Recurring> getCancellationMatchingBillingEvent() {
|
||||
return cancellationMatchingBillingEvent;
|
||||
}
|
||||
|
||||
public Long getRecurringEventHistoryRevisionId() {
|
||||
return recurringEventHistoryRevisionId;
|
||||
}
|
||||
|
||||
public Optional<VKey<AllocationToken>> getAllocationToken() {
|
||||
return Optional.ofNullable(allocationToken);
|
||||
}
|
||||
|
||||
@Override
|
||||
public VKey<OneTime> createVKey() {
|
||||
return createVKey(getId());
|
||||
}
|
||||
|
||||
public static VKey<OneTime> createVKey(long id) {
|
||||
return VKey.create(OneTime.class, id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder asBuilder() {
|
||||
return new Builder(clone(this));
|
||||
}
|
||||
|
||||
/** A builder for {@link OneTime} since it is immutable. */
|
||||
public static class Builder extends BillingEvent.Builder<OneTime, Builder> {
|
||||
|
||||
public Builder() {}
|
||||
|
||||
private Builder(OneTime instance) {
|
||||
super(instance);
|
||||
}
|
||||
|
||||
public Builder setCost(Money cost) {
|
||||
getInstance().cost = cost;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setPeriodYears(Integer periodYears) {
|
||||
checkNotNull(periodYears);
|
||||
checkArgument(periodYears > 0);
|
||||
getInstance().periodYears = periodYears;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setBillingTime(DateTime billingTime) {
|
||||
getInstance().billingTime = billingTime;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setSyntheticCreationTime(DateTime syntheticCreationTime) {
|
||||
getInstance().syntheticCreationTime = syntheticCreationTime;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setCancellationMatchingBillingEvent(
|
||||
Recurring cancellationMatchingBillingEvent) {
|
||||
getInstance().cancellationMatchingBillingEvent =
|
||||
cancellationMatchingBillingEvent.createVKey();
|
||||
getInstance().recurringEventHistoryRevisionId =
|
||||
cancellationMatchingBillingEvent.getDomainHistoryRevisionId();
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setAllocationToken(@Nullable VKey<AllocationToken> allocationToken) {
|
||||
getInstance().allocationToken = allocationToken;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OneTime build() {
|
||||
OneTime instance = getInstance();
|
||||
checkNotNull(instance.billingTime);
|
||||
checkNotNull(instance.cost);
|
||||
checkState(!instance.cost.isNegative(), "Costs should be non-negative.");
|
||||
// TODO(mcilwain): Enforce this check on all billing events (not just more recent ones)
|
||||
// post-migration after we add the missing period years values in SQL.
|
||||
if (instance.eventTime.isAfter(DateTime.parse("2019-01-01T00:00:00Z"))) {
|
||||
checkState(
|
||||
instance.reason.hasPeriodYears() == (instance.periodYears != null),
|
||||
"Period years must be set if and only if reason is "
|
||||
+ "CREATE, FEE_EARLY_ACCESS, RENEW, RESTORE or TRANSFER.");
|
||||
}
|
||||
checkState(
|
||||
instance.getFlags().contains(Flag.SYNTHETIC)
|
||||
== (instance.syntheticCreationTime != null),
|
||||
"Synthetic creation time must be set if and only if the SYNTHETIC flag is set.");
|
||||
checkState(
|
||||
instance.getFlags().contains(Flag.SYNTHETIC)
|
||||
== (instance.cancellationMatchingBillingEvent != null),
|
||||
"Cancellation matching billing event must be set if and only if the SYNTHETIC flag "
|
||||
+ "is set.");
|
||||
return super.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A recurring billable event.
|
||||
*
|
||||
* <p>Unlike {@link OneTime} events, these do not store an explicit cost, since the cost of the
|
||||
* recurring event might change and each time we bill for it, we need to bill at the current cost,
|
||||
* not the value that was in use at the time the recurrence was created.
|
||||
*/
|
||||
@Entity(name = "BillingRecurrence")
|
||||
@Table(
|
||||
indexes = {
|
||||
@Index(columnList = "registrarId"),
|
||||
@Index(columnList = "eventTime"),
|
||||
@Index(columnList = "domainRepoId"),
|
||||
@Index(columnList = "recurrenceEndTime"),
|
||||
@Index(columnList = "recurrenceLastExpansion"),
|
||||
@Index(columnList = "recurrence_time_of_year")
|
||||
})
|
||||
@AttributeOverride(name = "id", column = @Column(name = "billing_recurrence_id"))
|
||||
@WithVKey(Long.class)
|
||||
public static class Recurring extends BillingEvent {
|
||||
|
||||
/**
|
||||
* The billing event recurs every year between {@link #eventTime} and this time on the [month,
|
||||
* day, time] specified in {@link #recurrenceTimeOfYear}.
|
||||
*/
|
||||
DateTime recurrenceEndTime;
|
||||
|
||||
/**
|
||||
* The most recent {@link DateTime} when this recurrence was expanded.
|
||||
*
|
||||
* <p>We only bother checking recurrences for potential expansion if this is at least one year
|
||||
* in the past. If it's more recent than that, it means that the recurrence was already expanded
|
||||
* too recently to need to be checked again (as domains autorenew each year).
|
||||
*/
|
||||
@Column(nullable = false)
|
||||
DateTime recurrenceLastExpansion;
|
||||
|
||||
/**
|
||||
* The eventTime recurs every year on this [month, day, time] between {@link #eventTime} and
|
||||
* {@link #recurrenceEndTime}, inclusive of the start but not of the end.
|
||||
*
|
||||
* <p>This field is denormalized from {@link #eventTime} to allow for an efficient index, but it
|
||||
* always has the same data as that field.
|
||||
*
|
||||
* <p>Note that this is a recurrence of the event time, not the billing time. The billing time
|
||||
* can be calculated by adding the relevant grace period length to this date. The reason for
|
||||
* this requirement is that the event time recurs on a {@link org.joda.time.Period} schedule
|
||||
* (same day of year, which can be 365 or 366 days later) which is what {@link TimeOfYear} can
|
||||
* model, whereas the billing time is a fixed {@link org.joda.time.Duration} later.
|
||||
*/
|
||||
@Embedded
|
||||
@AttributeOverrides(
|
||||
@AttributeOverride(name = "timeString", column = @Column(name = "recurrence_time_of_year")))
|
||||
TimeOfYear recurrenceTimeOfYear;
|
||||
|
||||
/**
|
||||
* The renewal price for domain renewal if and only if it's specified.
|
||||
*
|
||||
* <p>This price column remains null except when the renewal price behavior of the billing is
|
||||
* SPECIFIED. This column is used for internal registrations.
|
||||
*/
|
||||
@Nullable
|
||||
@Type(type = JodaMoneyType.TYPE_NAME)
|
||||
@Columns(
|
||||
columns = {@Column(name = "renewalPriceAmount"), @Column(name = "renewalPriceCurrency")})
|
||||
Money renewalPrice;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "renewalPriceBehavior", nullable = false)
|
||||
RenewalPriceBehavior renewalPriceBehavior = RenewalPriceBehavior.DEFAULT;
|
||||
|
||||
public DateTime getRecurrenceEndTime() {
|
||||
return recurrenceEndTime;
|
||||
}
|
||||
|
||||
public DateTime getRecurrenceLastExpansion() {
|
||||
return recurrenceLastExpansion;
|
||||
}
|
||||
|
||||
public TimeOfYear getRecurrenceTimeOfYear() {
|
||||
return recurrenceTimeOfYear;
|
||||
}
|
||||
|
||||
public RenewalPriceBehavior getRenewalPriceBehavior() {
|
||||
return renewalPriceBehavior;
|
||||
}
|
||||
|
||||
public Optional<Money> getRenewalPrice() {
|
||||
return Optional.ofNullable(renewalPrice);
|
||||
}
|
||||
|
||||
@Override
|
||||
public VKey<Recurring> createVKey() {
|
||||
return createVKey(getId());
|
||||
}
|
||||
|
||||
public static VKey<Recurring> createVKey(Long id) {
|
||||
return VKey.create(Recurring.class, id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder asBuilder() {
|
||||
return new Builder(clone(this));
|
||||
}
|
||||
|
||||
/** A builder for {@link Recurring} since it is immutable. */
|
||||
public static class Builder extends BillingEvent.Builder<Recurring, Builder> {
|
||||
|
||||
public Builder() {}
|
||||
|
||||
private Builder(Recurring instance) {
|
||||
super(instance);
|
||||
}
|
||||
|
||||
public Builder setRecurrenceEndTime(DateTime recurrenceEndTime) {
|
||||
getInstance().recurrenceEndTime = recurrenceEndTime;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setRecurrenceLastExpansion(DateTime recurrenceLastExpansion) {
|
||||
getInstance().recurrenceLastExpansion = recurrenceLastExpansion;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setRenewalPriceBehavior(RenewalPriceBehavior renewalPriceBehavior) {
|
||||
getInstance().renewalPriceBehavior = renewalPriceBehavior;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setRenewalPrice(@Nullable Money renewalPrice) {
|
||||
getInstance().renewalPrice = renewalPrice;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Recurring build() {
|
||||
Recurring instance = getInstance();
|
||||
checkNotNull(instance.eventTime);
|
||||
checkNotNull(instance.reason);
|
||||
// Don't require recurrenceLastExpansion to be individually set on every new Recurrence.
|
||||
// The correct default value if not otherwise set is the event time of the recurrence minus
|
||||
// 1 year.
|
||||
instance.recurrenceLastExpansion =
|
||||
Optional.ofNullable(instance.recurrenceLastExpansion)
|
||||
.orElse(instance.eventTime.minusYears(1));
|
||||
checkArgument(
|
||||
instance.renewalPriceBehavior == RenewalPriceBehavior.SPECIFIED
|
||||
^ instance.renewalPrice == null,
|
||||
"Renewal price can have a value if and only if the renewal price behavior is"
|
||||
+ " SPECIFIED");
|
||||
instance.recurrenceTimeOfYear = TimeOfYear.fromDateTime(instance.eventTime);
|
||||
instance.recurrenceEndTime =
|
||||
Optional.ofNullable(instance.recurrenceEndTime).orElse(END_OF_TIME);
|
||||
return super.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An event representing a cancellation of one of the other two billable event types.
|
||||
*
|
||||
* <p>This is implemented as a separate event rather than a bit on BillingEvent in order to
|
||||
* preserve the immutability of billing events.
|
||||
*/
|
||||
@Entity(name = "BillingCancellation")
|
||||
@Table(
|
||||
indexes = {
|
||||
@Index(columnList = "registrarId"),
|
||||
@Index(columnList = "eventTime"),
|
||||
@Index(columnList = "domainRepoId"),
|
||||
@Index(columnList = "billingTime"),
|
||||
@Index(columnList = "billing_event_id"),
|
||||
@Index(columnList = "billing_recurrence_id")
|
||||
})
|
||||
@AttributeOverride(name = "id", column = @Column(name = "billing_cancellation_id"))
|
||||
@WithVKey(Long.class)
|
||||
public static class Cancellation extends BillingEvent {
|
||||
|
||||
/** The billing time of the charge that is being cancelled. */
|
||||
DateTime billingTime;
|
||||
|
||||
/**
|
||||
* The one-time billing event to cancel, or null for autorenew cancellations.
|
||||
*
|
||||
* <p>Although the type is {@link VKey} the name "ref" is preserved for historical reasons.
|
||||
*/
|
||||
@Column(name = "billing_event_id")
|
||||
VKey<OneTime> refOneTime;
|
||||
|
||||
/**
|
||||
* The recurring billing event to cancel, or null for non-autorenew cancellations.
|
||||
*
|
||||
* <p>Although the type is {@link VKey} the name "ref" is preserved for historical reasons.
|
||||
*/
|
||||
@Column(name = "billing_recurrence_id")
|
||||
VKey<Recurring> refRecurring;
|
||||
|
||||
public DateTime getBillingTime() {
|
||||
return billingTime;
|
||||
}
|
||||
|
||||
public VKey<? extends BillingEvent> getEventKey() {
|
||||
return firstNonNull(refOneTime, refRecurring);
|
||||
}
|
||||
|
||||
/** The mapping from billable grace period types to originating billing event reasons. */
|
||||
static final ImmutableMap<GracePeriodStatus, Reason> GRACE_PERIOD_TO_REASON =
|
||||
ImmutableMap.of(
|
||||
GracePeriodStatus.ADD, Reason.CREATE,
|
||||
GracePeriodStatus.AUTO_RENEW, Reason.RENEW,
|
||||
GracePeriodStatus.RENEW, Reason.RENEW,
|
||||
GracePeriodStatus.TRANSFER, Reason.TRANSFER);
|
||||
|
||||
/**
|
||||
* Creates a cancellation billing event (parented on the provided history key, and with the
|
||||
* corresponding event time) that will cancel out the provided grace period's billing event,
|
||||
* using the supplied targetId and deriving other metadata (clientId, billing time, and the
|
||||
* cancellation reason) from the grace period.
|
||||
*/
|
||||
public static Cancellation forGracePeriod(
|
||||
GracePeriod gracePeriod,
|
||||
DateTime eventTime,
|
||||
HistoryEntryId domainHistoryId,
|
||||
String targetId) {
|
||||
checkArgument(
|
||||
gracePeriod.hasBillingEvent(),
|
||||
"Cannot create cancellation for grace period without billing event");
|
||||
Builder builder =
|
||||
new Builder()
|
||||
.setReason(checkNotNull(GRACE_PERIOD_TO_REASON.get(gracePeriod.getType())))
|
||||
.setTargetId(targetId)
|
||||
.setRegistrarId(gracePeriod.getRegistrarId())
|
||||
.setEventTime(eventTime)
|
||||
// The charge being cancelled will take place at the grace period's expiration time.
|
||||
.setBillingTime(gracePeriod.getExpirationTime())
|
||||
.setDomainHistoryId(domainHistoryId);
|
||||
// Set the grace period's billing event using the appropriate Cancellation builder method.
|
||||
if (gracePeriod.getOneTimeBillingEvent() != null) {
|
||||
builder.setOneTimeEventKey(gracePeriod.getOneTimeBillingEvent());
|
||||
} else if (gracePeriod.getRecurringBillingEvent() != null) {
|
||||
builder.setRecurringEventKey(gracePeriod.getRecurringBillingEvent());
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public VKey<Cancellation> createVKey() {
|
||||
return createVKey(getId());
|
||||
}
|
||||
|
||||
public static VKey<Cancellation> createVKey(long id) {
|
||||
return VKey.create(Cancellation.class, id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder asBuilder() {
|
||||
return new Builder(clone(this));
|
||||
}
|
||||
|
||||
/** A builder for {@link Cancellation} since it is immutable. */
|
||||
public static class Builder extends BillingEvent.Builder<Cancellation, Builder> {
|
||||
|
||||
public Builder() {}
|
||||
|
||||
private Builder(Cancellation instance) {
|
||||
super(instance);
|
||||
}
|
||||
|
||||
public Builder setBillingTime(DateTime billingTime) {
|
||||
getInstance().billingTime = billingTime;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setOneTimeEventKey(VKey<OneTime> eventKey) {
|
||||
getInstance().refOneTime = eventKey;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setRecurringEventKey(VKey<Recurring> eventKey) {
|
||||
getInstance().refRecurring = eventKey;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cancellation build() {
|
||||
Cancellation instance = getInstance();
|
||||
checkNotNull(instance.billingTime, "Must set billing time");
|
||||
checkNotNull(instance.reason, "Must set reason");
|
||||
checkState(
|
||||
(instance.refOneTime == null) != (instance.refRecurring == null),
|
||||
"Cancellations must have exactly one billing event key set");
|
||||
return super.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
// Copyright 2023 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.model.billing;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static google.registry.util.DateTimeUtils.END_OF_TIME;
|
||||
|
||||
import google.registry.model.common.TimeOfYear;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.persistence.WithVKey;
|
||||
import google.registry.persistence.converter.JodaMoneyType;
|
||||
import java.util.Optional;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.persistence.AttributeOverride;
|
||||
import javax.persistence.AttributeOverrides;
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Embedded;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.EnumType;
|
||||
import javax.persistence.Enumerated;
|
||||
import javax.persistence.Index;
|
||||
import javax.persistence.Table;
|
||||
import org.hibernate.annotations.Columns;
|
||||
import org.hibernate.annotations.Type;
|
||||
import org.joda.money.Money;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
/**
|
||||
* A recurring billable event.
|
||||
*
|
||||
* <p>Unlike {@link BillingEvent} events, these do not store an explicit cost, since the cost of the
|
||||
* recurring event might change and each time we bill for it, we need to bill at the current cost,
|
||||
* not the value that was in use at the time the recurrence was created.
|
||||
*/
|
||||
@Entity
|
||||
@Table(
|
||||
indexes = {
|
||||
@Index(columnList = "registrarId"),
|
||||
@Index(columnList = "eventTime"),
|
||||
@Index(columnList = "domainRepoId"),
|
||||
@Index(columnList = "recurrenceEndTime"),
|
||||
@Index(columnList = "recurrenceLastExpansion"),
|
||||
@Index(columnList = "recurrence_time_of_year")
|
||||
})
|
||||
@AttributeOverride(name = "id", column = @Column(name = "billing_recurrence_id"))
|
||||
@WithVKey(Long.class)
|
||||
public class BillingRecurrence extends BillingBase {
|
||||
|
||||
/**
|
||||
* The billing event recurs every year between {@link #eventTime} and this time on the [month,
|
||||
* day, time] specified in {@link #recurrenceTimeOfYear}.
|
||||
*/
|
||||
DateTime recurrenceEndTime;
|
||||
|
||||
/**
|
||||
* The most recent {@link DateTime} when this recurrence was expanded.
|
||||
*
|
||||
* <p>We only bother checking recurrences for potential expansion if this is at least one year in
|
||||
* the past. If it's more recent than that, it means that the recurrence was already expanded too
|
||||
* recently to need to be checked again (as domains autorenew each year).
|
||||
*/
|
||||
@Column(nullable = false)
|
||||
DateTime recurrenceLastExpansion;
|
||||
|
||||
/**
|
||||
* The eventTime recurs every year on this [month, day, time] between {@link #eventTime} and
|
||||
* {@link #recurrenceEndTime}, inclusive of the start but not of the end.
|
||||
*
|
||||
* <p>This field is denormalized from {@link #eventTime} to allow for an efficient index, but it
|
||||
* always has the same data as that field.
|
||||
*
|
||||
* <p>Note that this is a recurrence of the event time, not the billing time. The billing time can
|
||||
* be calculated by adding the relevant grace period length to this date. The reason for this
|
||||
* requirement is that the event time recurs on a {@link org.joda.time.Period} schedule (same day
|
||||
* of year, which can be 365 or 366 days later) which is what {@link TimeOfYear} can model,
|
||||
* whereas the billing time is a fixed {@link org.joda.time.Duration} later.
|
||||
*/
|
||||
@Embedded
|
||||
@AttributeOverrides(
|
||||
@AttributeOverride(name = "timeString", column = @Column(name = "recurrence_time_of_year")))
|
||||
TimeOfYear recurrenceTimeOfYear;
|
||||
|
||||
/**
|
||||
* The renewal price for domain renewal if and only if it's specified.
|
||||
*
|
||||
* <p>This price column remains null except when the renewal price behavior of the billing is
|
||||
* SPECIFIED. This column is used for internal registrations.
|
||||
*/
|
||||
@Nullable
|
||||
@Type(type = JodaMoneyType.TYPE_NAME)
|
||||
@Columns(columns = {@Column(name = "renewalPriceAmount"), @Column(name = "renewalPriceCurrency")})
|
||||
Money renewalPrice;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "renewalPriceBehavior", nullable = false)
|
||||
RenewalPriceBehavior renewalPriceBehavior = RenewalPriceBehavior.DEFAULT;
|
||||
|
||||
public DateTime getRecurrenceEndTime() {
|
||||
return recurrenceEndTime;
|
||||
}
|
||||
|
||||
public DateTime getRecurrenceLastExpansion() {
|
||||
return recurrenceLastExpansion;
|
||||
}
|
||||
|
||||
public TimeOfYear getRecurrenceTimeOfYear() {
|
||||
return recurrenceTimeOfYear;
|
||||
}
|
||||
|
||||
public RenewalPriceBehavior getRenewalPriceBehavior() {
|
||||
return renewalPriceBehavior;
|
||||
}
|
||||
|
||||
public Optional<Money> getRenewalPrice() {
|
||||
return Optional.ofNullable(renewalPrice);
|
||||
}
|
||||
|
||||
@Override
|
||||
public VKey<google.registry.model.billing.BillingRecurrence> createVKey() {
|
||||
return createVKey(getId());
|
||||
}
|
||||
|
||||
public static VKey<google.registry.model.billing.BillingRecurrence> createVKey(Long id) {
|
||||
return VKey.create(google.registry.model.billing.BillingRecurrence.class, id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder asBuilder() {
|
||||
return new Builder(clone(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder for {@link google.registry.model.billing.BillingRecurrence} since it is immutable.
|
||||
*/
|
||||
public static class Builder
|
||||
extends BillingBase.Builder<google.registry.model.billing.BillingRecurrence, Builder> {
|
||||
|
||||
public Builder() {}
|
||||
|
||||
private Builder(google.registry.model.billing.BillingRecurrence instance) {
|
||||
super(instance);
|
||||
}
|
||||
|
||||
public Builder setRecurrenceEndTime(DateTime recurrenceEndTime) {
|
||||
getInstance().recurrenceEndTime = recurrenceEndTime;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setRecurrenceLastExpansion(DateTime recurrenceLastExpansion) {
|
||||
getInstance().recurrenceLastExpansion = recurrenceLastExpansion;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setRenewalPriceBehavior(RenewalPriceBehavior renewalPriceBehavior) {
|
||||
getInstance().renewalPriceBehavior = renewalPriceBehavior;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setRenewalPrice(@Nullable Money renewalPrice) {
|
||||
getInstance().renewalPrice = renewalPrice;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public google.registry.model.billing.BillingRecurrence build() {
|
||||
google.registry.model.billing.BillingRecurrence instance = getInstance();
|
||||
checkNotNull(instance.eventTime);
|
||||
checkNotNull(instance.reason);
|
||||
// Don't require recurrenceLastExpansion to be individually set on every new Recurrence.
|
||||
// The correct default value if not otherwise set is the event time of the recurrence minus
|
||||
// 1 year.
|
||||
instance.recurrenceLastExpansion =
|
||||
Optional.ofNullable(instance.recurrenceLastExpansion)
|
||||
.orElse(instance.eventTime.minusYears(1));
|
||||
checkArgument(
|
||||
instance.renewalPriceBehavior == RenewalPriceBehavior.SPECIFIED
|
||||
^ instance.renewalPrice == null,
|
||||
"Renewal price can have a value if and only if the renewal price behavior is"
|
||||
+ " SPECIFIED");
|
||||
instance.recurrenceTimeOfYear = TimeOfYear.fromDateTime(instance.eventTime);
|
||||
instance.recurrenceEndTime =
|
||||
Optional.ofNullable(instance.recurrenceEndTime).orElse(END_OF_TIME);
|
||||
return super.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,7 +44,7 @@ import com.google.gson.annotations.Expose;
|
||||
import google.registry.flows.ResourceFlowUtils;
|
||||
import google.registry.model.EppResource;
|
||||
import google.registry.model.EppResource.ResourceWithTransferData;
|
||||
import google.registry.model.billing.BillingEvent.Recurring;
|
||||
import google.registry.model.billing.BillingRecurrence;
|
||||
import google.registry.model.contact.Contact;
|
||||
import google.registry.model.domain.launch.LaunchNotice;
|
||||
import google.registry.model.domain.rgp.GracePeriodStatus;
|
||||
@@ -200,7 +200,7 @@ public class DomainBase extends EppResource
|
||||
* should be created, and this field should be updated to point to the new one.
|
||||
*/
|
||||
@Column(name = "billing_recurrence_id")
|
||||
VKey<Recurring> autorenewBillingEvent;
|
||||
VKey<BillingRecurrence> autorenewBillingEvent;
|
||||
|
||||
/**
|
||||
* The recurring poll message associated with this domain's autorenewals.
|
||||
@@ -286,7 +286,7 @@ public class DomainBase extends EppResource
|
||||
return deletePollMessage;
|
||||
}
|
||||
|
||||
public VKey<Recurring> getAutorenewBillingEvent() {
|
||||
public VKey<BillingRecurrence> getAutorenewBillingEvent() {
|
||||
return autorenewBillingEvent;
|
||||
}
|
||||
|
||||
@@ -520,7 +520,7 @@ public class DomainBase extends EppResource
|
||||
builder
|
||||
.setRegistrationExpirationTime(newExpirationTime)
|
||||
.addGracePeriod(
|
||||
GracePeriod.createForRecurring(
|
||||
GracePeriod.createForRecurrence(
|
||||
GracePeriodStatus.AUTO_RENEW,
|
||||
domain.getRepoId(),
|
||||
lastAutorenewTime.plus(Tld.get(domain.getTld()).getAutoRenewGracePeriodLength()),
|
||||
@@ -847,7 +847,7 @@ public class DomainBase extends EppResource
|
||||
return thisCastToDerived();
|
||||
}
|
||||
|
||||
public B setAutorenewBillingEvent(VKey<Recurring> autorenewBillingEvent) {
|
||||
public B setAutorenewBillingEvent(VKey<BillingRecurrence> autorenewBillingEvent) {
|
||||
getInstance().autorenewBillingEvent = autorenewBillingEvent;
|
||||
return thisCastToDerived();
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import google.registry.model.billing.BillingEvent;
|
||||
import google.registry.model.billing.BillingEvent.Recurring;
|
||||
import google.registry.model.billing.BillingRecurrence;
|
||||
import google.registry.model.domain.rgp.GracePeriodStatus;
|
||||
import google.registry.model.reporting.HistoryEntry.HistoryEntryId;
|
||||
import google.registry.persistence.VKey;
|
||||
@@ -60,23 +60,23 @@ public class GracePeriod extends GracePeriodBase {
|
||||
String domainRepoId,
|
||||
DateTime expirationTime,
|
||||
String registrarId,
|
||||
@Nullable VKey<BillingEvent.OneTime> billingEventOneTime,
|
||||
@Nullable VKey<BillingEvent.Recurring> billingEventRecurring,
|
||||
@Nullable VKey<BillingEvent> billingEvent,
|
||||
@Nullable VKey<BillingRecurrence> billingRecurrence,
|
||||
@Nullable Long gracePeriodId) {
|
||||
checkArgument(
|
||||
billingEventOneTime == null || billingEventRecurring == null,
|
||||
billingEvent == null || billingRecurrence == null,
|
||||
"A grace period can have at most one billing event");
|
||||
checkArgument(
|
||||
(billingEventRecurring != null) == GracePeriodStatus.AUTO_RENEW.equals(type),
|
||||
"Recurring billing events must be present on (and only on) autorenew grace periods");
|
||||
(billingRecurrence != null) == GracePeriodStatus.AUTO_RENEW.equals(type),
|
||||
"BillingRecurrences must be present on (and only on) autorenew grace periods");
|
||||
GracePeriod instance = new GracePeriod();
|
||||
instance.gracePeriodId = gracePeriodId == null ? allocateId() : gracePeriodId;
|
||||
instance.type = checkArgumentNotNull(type);
|
||||
instance.domainRepoId = checkArgumentNotNull(domainRepoId);
|
||||
instance.expirationTime = checkArgumentNotNull(expirationTime);
|
||||
instance.clientId = checkArgumentNotNull(registrarId);
|
||||
instance.billingEventOneTime = billingEventOneTime;
|
||||
instance.billingEventRecurring = billingEventRecurring;
|
||||
instance.billingEvent = billingEvent;
|
||||
instance.billingRecurrence = billingRecurrence;
|
||||
return instance;
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ public class GracePeriod extends GracePeriodBase {
|
||||
String domainRepoId,
|
||||
DateTime expirationTime,
|
||||
String registrarId,
|
||||
@Nullable VKey<BillingEvent.OneTime> billingEventOneTime) {
|
||||
@Nullable VKey<BillingEvent> billingEventOneTime) {
|
||||
return createInternal(
|
||||
type, domainRepoId, expirationTime, registrarId, billingEventOneTime, null, null);
|
||||
}
|
||||
@@ -111,7 +111,7 @@ public class GracePeriod extends GracePeriodBase {
|
||||
String domainRepoId,
|
||||
DateTime expirationTime,
|
||||
String registrarId,
|
||||
@Nullable VKey<BillingEvent.OneTime> billingEventOneTime,
|
||||
@Nullable VKey<BillingEvent> billingEventOneTime,
|
||||
@Nullable Long gracePeriodId) {
|
||||
return createInternal(
|
||||
type, domainRepoId, expirationTime, registrarId, billingEventOneTime, null, gracePeriodId);
|
||||
@@ -123,40 +123,40 @@ public class GracePeriod extends GracePeriodBase {
|
||||
history.domainRepoId,
|
||||
history.expirationTime,
|
||||
history.clientId,
|
||||
history.billingEventOneTime,
|
||||
history.billingEventRecurring,
|
||||
history.billingEvent,
|
||||
history.billingRecurrence,
|
||||
history.gracePeriodId);
|
||||
}
|
||||
|
||||
/** Creates a GracePeriod for a Recurring billing event. */
|
||||
public static GracePeriod createForRecurring(
|
||||
/** Creates a GracePeriod for a Recurrence billing event. */
|
||||
public static GracePeriod createForRecurrence(
|
||||
GracePeriodStatus type,
|
||||
String domainRepoId,
|
||||
DateTime expirationTime,
|
||||
String registrarId,
|
||||
VKey<Recurring> billingEventRecurring) {
|
||||
checkArgumentNotNull(billingEventRecurring, "billingEventRecurring cannot be null");
|
||||
VKey<BillingRecurrence> billingEventRecurrence) {
|
||||
checkArgumentNotNull(billingEventRecurrence, "billingEventRecurrence cannot be null");
|
||||
return createInternal(
|
||||
type, domainRepoId, expirationTime, registrarId, null, billingEventRecurring, null);
|
||||
type, domainRepoId, expirationTime, registrarId, null, billingEventRecurrence, null);
|
||||
}
|
||||
|
||||
/** Creates a GracePeriod for a Recurring billing event and a given {@link #gracePeriodId}. */
|
||||
/** Creates a GracePeriod for a Recurrence billing event and a given {@link #gracePeriodId}. */
|
||||
@VisibleForTesting
|
||||
public static GracePeriod createForRecurring(
|
||||
public static GracePeriod createForRecurrence(
|
||||
GracePeriodStatus type,
|
||||
String domainRepoId,
|
||||
DateTime expirationTime,
|
||||
String registrarId,
|
||||
VKey<Recurring> billingEventRecurring,
|
||||
VKey<BillingRecurrence> billingEventRecurrence,
|
||||
@Nullable Long gracePeriodId) {
|
||||
checkArgumentNotNull(billingEventRecurring, "billingEventRecurring cannot be null");
|
||||
checkArgumentNotNull(billingEventRecurrence, "billingEventRecurrence cannot be null");
|
||||
return createInternal(
|
||||
type,
|
||||
domainRepoId,
|
||||
expirationTime,
|
||||
registrarId,
|
||||
null,
|
||||
billingEventRecurring,
|
||||
billingEventRecurrence,
|
||||
gracePeriodId);
|
||||
}
|
||||
|
||||
@@ -168,7 +168,7 @@ public class GracePeriod extends GracePeriodBase {
|
||||
|
||||
/** Constructs a GracePeriod of the given type from the provided one-time BillingEvent. */
|
||||
public static GracePeriod forBillingEvent(
|
||||
GracePeriodStatus type, String domainRepoId, BillingEvent.OneTime billingEvent) {
|
||||
GracePeriodStatus type, String domainRepoId, BillingEvent billingEvent) {
|
||||
return create(
|
||||
type,
|
||||
domainRepoId,
|
||||
@@ -205,8 +205,8 @@ public class GracePeriod extends GracePeriodBase {
|
||||
instance.domainRepoId = gracePeriod.domainRepoId;
|
||||
instance.expirationTime = gracePeriod.expirationTime;
|
||||
instance.clientId = gracePeriod.clientId;
|
||||
instance.billingEventOneTime = gracePeriod.billingEventOneTime;
|
||||
instance.billingEventRecurring = gracePeriod.billingEventRecurring;
|
||||
instance.billingEvent = gracePeriod.billingEvent;
|
||||
instance.billingRecurrence = gracePeriod.billingRecurrence;
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ package google.registry.model.domain;
|
||||
import google.registry.model.ImmutableObject;
|
||||
import google.registry.model.UnsafeSerializable;
|
||||
import google.registry.model.billing.BillingEvent;
|
||||
import google.registry.model.billing.BillingRecurrence;
|
||||
import google.registry.model.domain.rgp.GracePeriodStatus;
|
||||
import google.registry.persistence.VKey;
|
||||
import javax.persistence.Access;
|
||||
@@ -56,21 +57,21 @@ public class GracePeriodBase extends ImmutableObject implements UnsafeSerializab
|
||||
/**
|
||||
* The one-time billing event corresponding to the action that triggered this grace period, or
|
||||
* null if not applicable. Not set for autorenew grace periods (which instead use the field {@code
|
||||
* billingEventRecurring}) or for redemption grace periods (since deletes have no cost).
|
||||
* billingEventRecurrence}) or for redemption grace periods (since deletes have no cost).
|
||||
*/
|
||||
// NB: Would @IgnoreSave(IfNull.class), but not allowed for @Embed collections.
|
||||
@Access(AccessType.FIELD)
|
||||
@Column(name = "billing_event_id")
|
||||
VKey<BillingEvent.OneTime> billingEventOneTime = null;
|
||||
VKey<BillingEvent> billingEvent = null;
|
||||
|
||||
/**
|
||||
* The recurring billing event corresponding to the action that triggered this grace period, if
|
||||
* applicable - i.e. if the action was an autorenew - or null in all other cases.
|
||||
* The Recurrence corresponding to the action that triggered this grace period, if applicable -
|
||||
* i.e. if the action was an autorenew - or null in all other cases.
|
||||
*/
|
||||
// NB: Would @IgnoreSave(IfNull.class), but not allowed for @Embed collections.
|
||||
@Access(AccessType.FIELD)
|
||||
@Column(name = "billing_recurrence_id")
|
||||
VKey<BillingEvent.Recurring> billingEventRecurring = null;
|
||||
VKey<BillingRecurrence> billingRecurrence = null;
|
||||
|
||||
public long getGracePeriodId() {
|
||||
return gracePeriodId;
|
||||
@@ -100,22 +101,22 @@ public class GracePeriodBase extends ImmutableObject implements UnsafeSerializab
|
||||
|
||||
/** Returns true if this GracePeriod has an associated BillingEvent; i.e. if it's refundable. */
|
||||
public boolean hasBillingEvent() {
|
||||
return billingEventOneTime != null || billingEventRecurring != null;
|
||||
return billingEvent != null || billingRecurrence != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the one time billing event. The value will only be non-null if the type of this grace
|
||||
* period is not AUTO_RENEW.
|
||||
*/
|
||||
public VKey<BillingEvent.OneTime> getOneTimeBillingEvent() {
|
||||
return billingEventOneTime;
|
||||
public VKey<BillingEvent> getBillingEvent() {
|
||||
return billingEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the recurring billing event. The value will only be non-null if the type of this grace
|
||||
* period is AUTO_RENEW.
|
||||
* Returns the Recurrence. The value will only be non-null if the type of this grace period is
|
||||
* AUTO_RENEW.
|
||||
*/
|
||||
public VKey<BillingEvent.Recurring> getRecurringBillingEvent() {
|
||||
return billingEventRecurring;
|
||||
public VKey<BillingRecurrence> getBillingRecurrence() {
|
||||
return billingRecurrence;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ import google.registry.model.Buildable;
|
||||
import google.registry.model.CacheUtils;
|
||||
import google.registry.model.CreateAutoTimestamp;
|
||||
import google.registry.model.UpdateAutoTimestampEntity;
|
||||
import google.registry.model.billing.BillingEvent.RenewalPriceBehavior;
|
||||
import google.registry.model.billing.BillingBase.RenewalPriceBehavior;
|
||||
import google.registry.model.common.TimedTransitionProperty;
|
||||
import google.registry.model.domain.fee.FeeQueryCommandExtensionItem.CommandName;
|
||||
import google.registry.model.reporting.HistoryEntry.HistoryEntryId;
|
||||
|
||||
@@ -17,6 +17,7 @@ package google.registry.model.reporting;
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
|
||||
|
||||
import google.registry.batch.ExpandBillingRecurrencesAction;
|
||||
import google.registry.model.Buildable;
|
||||
import google.registry.model.EppResource;
|
||||
import google.registry.model.ImmutableObject;
|
||||
@@ -81,7 +82,7 @@ public abstract class HistoryEntry extends ImmutableObject
|
||||
DOMAIN_ALLOCATE,
|
||||
/**
|
||||
* Used for domain registration autorenews explicitly logged by {@link
|
||||
* google.registry.batch.ExpandRecurringBillingEventsAction}.
|
||||
* ExpandBillingRecurrencesAction}.
|
||||
*/
|
||||
DOMAIN_AUTORENEW,
|
||||
DOMAIN_CREATE,
|
||||
|
||||
@@ -17,7 +17,9 @@ package google.registry.model.transfer;
|
||||
import static google.registry.util.CollectionUtils.isNullOrEmpty;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import google.registry.model.billing.BillingCancellation;
|
||||
import google.registry.model.billing.BillingEvent;
|
||||
import google.registry.model.billing.BillingRecurrence;
|
||||
import google.registry.model.domain.Period;
|
||||
import google.registry.model.domain.Period.Unit;
|
||||
import google.registry.model.poll.PollMessage;
|
||||
@@ -69,7 +71,7 @@ public class DomainTransferData extends TransferData {
|
||||
DateTime transferredRegistrationExpirationTime;
|
||||
|
||||
@Column(name = "transfer_billing_cancellation_id")
|
||||
public VKey<BillingEvent.Cancellation> billingCancellationId;
|
||||
public VKey<BillingCancellation> billingCancellationId;
|
||||
|
||||
/**
|
||||
* The regular one-time billing event that will be charged for a server-approved transfer.
|
||||
@@ -78,7 +80,7 @@ public class DomainTransferData extends TransferData {
|
||||
* being transferred is not a domain.
|
||||
*/
|
||||
@Column(name = "transfer_billing_event_id")
|
||||
VKey<BillingEvent.OneTime> serverApproveBillingEvent;
|
||||
VKey<BillingEvent> serverApproveBillingEvent;
|
||||
|
||||
/**
|
||||
* The autorenew billing event that should be associated with this resource after the transfer.
|
||||
@@ -87,7 +89,7 @@ public class DomainTransferData extends TransferData {
|
||||
* being transferred is not a domain.
|
||||
*/
|
||||
@Column(name = "transfer_billing_recurrence_id")
|
||||
VKey<BillingEvent.Recurring> serverApproveAutorenewEvent;
|
||||
VKey<BillingRecurrence> serverApproveAutorenewEvent;
|
||||
|
||||
/**
|
||||
* The autorenew poll message that should be associated with this resource after the transfer.
|
||||
@@ -120,12 +122,12 @@ public class DomainTransferData extends TransferData {
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public VKey<BillingEvent.OneTime> getServerApproveBillingEvent() {
|
||||
public VKey<BillingEvent> getServerApproveBillingEvent() {
|
||||
return serverApproveBillingEvent;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public VKey<BillingEvent.Recurring> getServerApproveAutorenewEvent() {
|
||||
public VKey<BillingRecurrence> getServerApproveAutorenewEvent() {
|
||||
return serverApproveAutorenewEvent;
|
||||
}
|
||||
|
||||
@@ -176,9 +178,9 @@ public class DomainTransferData extends TransferData {
|
||||
domainTransferData.billingCancellationId = null;
|
||||
} else {
|
||||
domainTransferData.billingCancellationId =
|
||||
(VKey<BillingEvent.Cancellation>)
|
||||
(VKey<BillingCancellation>)
|
||||
serverApproveEntities.stream()
|
||||
.filter(k -> k.getKind().equals(BillingEvent.Cancellation.class))
|
||||
.filter(k -> k.getKind().equals(BillingCancellation.class))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
@@ -204,14 +206,13 @@ public class DomainTransferData extends TransferData {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setServerApproveBillingEvent(
|
||||
VKey<BillingEvent.OneTime> serverApproveBillingEvent) {
|
||||
public Builder setServerApproveBillingEvent(VKey<BillingEvent> serverApproveBillingEvent) {
|
||||
getInstance().serverApproveBillingEvent = serverApproveBillingEvent;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setServerApproveAutorenewEvent(
|
||||
VKey<BillingEvent.Recurring> serverApproveAutorenewEvent) {
|
||||
VKey<BillingRecurrence> serverApproveAutorenewEvent) {
|
||||
getInstance().serverApproveAutorenewEvent = serverApproveAutorenewEvent;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import google.registry.batch.CannedScriptExecutionAction;
|
||||
import google.registry.batch.DeleteExpiredDomainsAction;
|
||||
import google.registry.batch.DeleteLoadTestDataAction;
|
||||
import google.registry.batch.DeleteProberDataAction;
|
||||
import google.registry.batch.ExpandRecurringBillingEventsAction;
|
||||
import google.registry.batch.ExpandBillingRecurrencesAction;
|
||||
import google.registry.batch.RelockDomainAction;
|
||||
import google.registry.batch.ResaveAllEppResourcesPipelineAction;
|
||||
import google.registry.batch.ResaveEntityAction;
|
||||
@@ -115,7 +115,7 @@ interface BackendRequestComponent {
|
||||
|
||||
DeleteProberDataAction deleteProberDataAction();
|
||||
|
||||
ExpandRecurringBillingEventsAction expandRecurringBillingEventsAction();
|
||||
ExpandBillingRecurrencesAction expandBillingRecurrencesAction();
|
||||
|
||||
ExportDomainListsAction exportDomainListsAction();
|
||||
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
|
||||
package google.registry.persistence.converter;
|
||||
|
||||
import google.registry.model.billing.BillingEvent.Flag;
|
||||
import google.registry.model.billing.BillingBase.Flag;
|
||||
import java.util.Set;
|
||||
import javax.persistence.AttributeConverter;
|
||||
import javax.persistence.Converter;
|
||||
|
||||
@@ -26,8 +26,8 @@ import com.google.common.collect.Sets;
|
||||
import google.registry.batch.CloudTasksUtils;
|
||||
import google.registry.batch.RelockDomainAction;
|
||||
import google.registry.config.RegistryConfig.Config;
|
||||
import google.registry.model.billing.BillingBase.Reason;
|
||||
import google.registry.model.billing.BillingEvent;
|
||||
import google.registry.model.billing.BillingEvent.Reason;
|
||||
import google.registry.model.domain.Domain;
|
||||
import google.registry.model.domain.DomainHistory;
|
||||
import google.registry.model.domain.RegistryLock;
|
||||
@@ -397,8 +397,8 @@ public final class DomainLockUtils {
|
||||
tm().update(domain);
|
||||
tm().insert(domainHistory);
|
||||
if (!lock.isSuperuser()) { // admin actions shouldn't affect billing
|
||||
BillingEvent.OneTime oneTime =
|
||||
new BillingEvent.OneTime.Builder()
|
||||
BillingEvent billingEvent =
|
||||
new BillingEvent.Builder()
|
||||
.setReason(Reason.SERVER_STATUS)
|
||||
.setTargetId(domain.getForeignKey())
|
||||
.setRegistrarId(domain.getCurrentSponsorRegistrarId())
|
||||
@@ -407,7 +407,7 @@ public final class DomainLockUtils {
|
||||
.setBillingTime(now)
|
||||
.setDomainHistory(domainHistory)
|
||||
.build();
|
||||
tm().insert(oneTime);
|
||||
tm().insert(billingEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ package google.registry.tools;
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
||||
import static com.google.common.collect.Sets.difference;
|
||||
import static google.registry.model.billing.BillingEvent.RenewalPriceBehavior.DEFAULT;
|
||||
import static google.registry.model.billing.BillingBase.RenewalPriceBehavior.DEFAULT;
|
||||
import static google.registry.model.domain.token.AllocationToken.TokenType.DEFAULT_PROMO;
|
||||
import static google.registry.model.domain.token.AllocationToken.TokenType.PACKAGE;
|
||||
import static google.registry.model.domain.token.AllocationToken.TokenType.SINGLE_USE;
|
||||
@@ -39,7 +39,7 @@ import com.google.common.collect.ImmutableSortedMap;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Streams;
|
||||
import com.google.common.io.Files;
|
||||
import google.registry.model.billing.BillingEvent.RenewalPriceBehavior;
|
||||
import google.registry.model.billing.BillingBase.RenewalPriceBehavior;
|
||||
import google.registry.model.domain.fee.FeeQueryCommandExtensionItem.CommandName;
|
||||
import google.registry.model.domain.token.AllocationToken;
|
||||
import google.registry.model.domain.token.AllocationToken.RegistrationBehavior;
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
package google.registry.tools;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import google.registry.tools.javascrap.CreateCancellationsForOneTimesCommand;
|
||||
import google.registry.tools.javascrap.CreateCancellationsForBillingEventsCommand;
|
||||
|
||||
/** Container class to create and run remote commands against a server instance. */
|
||||
public final class RegistryTool {
|
||||
@@ -35,7 +35,9 @@ public final class RegistryTool {
|
||||
.put("convert_idn", ConvertIdnCommand.class)
|
||||
.put("count_domains", CountDomainsCommand.class)
|
||||
.put("create_anchor_tenant", CreateAnchorTenantCommand.class)
|
||||
.put("create_cancellations_for_one_times", CreateCancellationsForOneTimesCommand.class)
|
||||
.put(
|
||||
"create_cancellations_for_billing_events",
|
||||
CreateCancellationsForBillingEventsCommand.class)
|
||||
.put("create_cdns_tld", CreateCdnsTld.class)
|
||||
.put("create_contact", CreateContactCommand.class)
|
||||
.put("create_domain", CreateDomainCommand.class)
|
||||
|
||||
@@ -40,7 +40,6 @@ import google.registry.request.Modules.UrlConnectionServiceModule;
|
||||
import google.registry.request.Modules.UrlFetchServiceModule;
|
||||
import google.registry.request.Modules.UserServiceModule;
|
||||
import google.registry.tools.AuthModule.LocalCredentialModule;
|
||||
import google.registry.tools.javascrap.CreateCancellationsForOneTimesCommand;
|
||||
import google.registry.util.UtilsModule;
|
||||
import google.registry.whois.NonCachingWhoisModule;
|
||||
import javax.annotation.Nullable;
|
||||
@@ -92,8 +91,6 @@ interface RegistryToolComponent {
|
||||
|
||||
void inject(CreateAnchorTenantCommand command);
|
||||
|
||||
void inject(CreateCancellationsForOneTimesCommand command);
|
||||
|
||||
void inject(CreateCdnsTld command);
|
||||
|
||||
void inject(CreateContactCommand command);
|
||||
|
||||
@@ -32,8 +32,7 @@ import com.google.common.collect.ImmutableMultimap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Sets;
|
||||
import google.registry.model.ForeignKeyUtils;
|
||||
import google.registry.model.billing.BillingEvent;
|
||||
import google.registry.model.billing.BillingEvent.Recurring;
|
||||
import google.registry.model.billing.BillingRecurrence;
|
||||
import google.registry.model.domain.Domain;
|
||||
import google.registry.model.domain.DomainHistory;
|
||||
import google.registry.model.domain.Period;
|
||||
@@ -205,7 +204,7 @@ class UnrenewDomainCommand extends ConfirmingCommand {
|
||||
.setEventTime(now)
|
||||
.build();
|
||||
// Create a new autorenew billing event and poll message starting at the new expiration time.
|
||||
BillingEvent.Recurring newAutorenewEvent =
|
||||
BillingRecurrence newAutorenewEvent =
|
||||
newAutorenewBillingEvent(domain)
|
||||
.setEventTime(newExpirationTime)
|
||||
.setDomainHistory(domainHistory)
|
||||
@@ -216,9 +215,9 @@ class UnrenewDomainCommand extends ConfirmingCommand {
|
||||
.setHistoryEntry(domainHistory)
|
||||
.build();
|
||||
// End the old autorenew billing event and poll message now.
|
||||
Recurring existingRecurring = tm().loadByKey(domain.getAutorenewBillingEvent());
|
||||
BillingRecurrence existingBillingRecurrence = tm().loadByKey(domain.getAutorenewBillingEvent());
|
||||
updateAutorenewRecurrenceEndTime(
|
||||
domain, existingRecurring, now, domainHistory.getHistoryEntryId());
|
||||
domain, existingBillingRecurrence, now, domainHistory.getHistoryEntryId());
|
||||
Domain newDomain =
|
||||
domain
|
||||
.asBuilder()
|
||||
|
||||
@@ -28,7 +28,7 @@ import com.google.common.base.Joiner;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.ImmutableSortedMap;
|
||||
import google.registry.model.billing.BillingEvent.RenewalPriceBehavior;
|
||||
import google.registry.model.billing.BillingBase.RenewalPriceBehavior;
|
||||
import google.registry.model.domain.fee.FeeQueryCommandExtensionItem.CommandName;
|
||||
import google.registry.model.domain.token.AllocationToken;
|
||||
import google.registry.model.domain.token.AllocationToken.RegistrationBehavior;
|
||||
|
||||
@@ -25,8 +25,8 @@ import com.google.common.base.Joiner;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import google.registry.model.EppResourceUtils;
|
||||
import google.registry.model.billing.BillingEvent.Recurring;
|
||||
import google.registry.model.billing.BillingEvent.RenewalPriceBehavior;
|
||||
import google.registry.model.billing.BillingBase.RenewalPriceBehavior;
|
||||
import google.registry.model.billing.BillingRecurrence;
|
||||
import google.registry.model.domain.Domain;
|
||||
import google.registry.model.domain.DomainHistory;
|
||||
import google.registry.model.reporting.HistoryEntry;
|
||||
@@ -38,7 +38,7 @@ import org.joda.money.Money;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
/**
|
||||
* Command to update {@link Recurring} billing events with a new behavior and/or price.
|
||||
* Command to update {@link BillingRecurrence} billing events with a new behavior and/or price.
|
||||
*
|
||||
* <p>More specifically, this closes the existing recurrence object and creates a new, similar,
|
||||
* object as well as a corresponding synthetic {@link DomainHistory} object. This is done to
|
||||
@@ -85,11 +85,11 @@ public class UpdateRecurrenceCommand extends ConfirmingCommand {
|
||||
+ " SPECIFIED");
|
||||
}
|
||||
}
|
||||
ImmutableMap<Domain, Recurring> domainsAndRecurrings =
|
||||
tm().transact(this::loadDomainsAndRecurrings);
|
||||
ImmutableMap<Domain, BillingRecurrence> domainsAndRecurrences =
|
||||
tm().transact(this::loadDomainsAndRecurrences);
|
||||
if (renewalPriceBehavior == null) {
|
||||
// Allow users to specify only a price only if all renewals are already SPECIFIED
|
||||
domainsAndRecurrings.forEach(
|
||||
domainsAndRecurrences.forEach(
|
||||
(d, r) ->
|
||||
checkArgument(
|
||||
r.getRenewalPriceBehavior().equals(RenewalPriceBehavior.SPECIFIED),
|
||||
@@ -103,41 +103,45 @@ public class UpdateRecurrenceCommand extends ConfirmingCommand {
|
||||
"Update the following with behavior %s%s?\n%s",
|
||||
renewalPriceBehavior,
|
||||
specifiedPriceString,
|
||||
Joiner.on('\n').withKeyValueSeparator(':').join(domainsAndRecurrings));
|
||||
Joiner.on('\n').withKeyValueSeparator(':').join(domainsAndRecurrences));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String execute() throws Exception {
|
||||
ImmutableList<Recurring> newRecurrings = tm().transact(this::internalExecute);
|
||||
return "Updated new recurring(s): " + newRecurrings;
|
||||
ImmutableList<BillingRecurrence> newBillingRecurrences = tm().transact(this::internalExecute);
|
||||
return "Updated new recurrence(s): " + newBillingRecurrences;
|
||||
}
|
||||
|
||||
private ImmutableList<Recurring> internalExecute() {
|
||||
ImmutableMap<Domain, Recurring> domainsAndRecurrings = loadDomainsAndRecurrings();
|
||||
private ImmutableList<BillingRecurrence> internalExecute() {
|
||||
ImmutableMap<Domain, BillingRecurrence> domainsAndRecurrences = loadDomainsAndRecurrences();
|
||||
DateTime now = tm().getTransactionTime();
|
||||
ImmutableList.Builder<Recurring> resultBuilder = new ImmutableList.Builder<>();
|
||||
domainsAndRecurrings.forEach(
|
||||
(domain, existingRecurring) -> {
|
||||
// Make a new history ID to break the (recurring, history, domain) circular dep chain
|
||||
ImmutableList.Builder<BillingRecurrence> resultBuilder = new ImmutableList.Builder<>();
|
||||
domainsAndRecurrences.forEach(
|
||||
(domain, existingRecurrence) -> {
|
||||
// Make a new history ID to break the (recurrence, history, domain) circular dep chain
|
||||
long newHistoryId = allocateId();
|
||||
HistoryEntryId newDomainHistoryId = new HistoryEntryId(domain.getRepoId(), newHistoryId);
|
||||
Recurring endingNow = existingRecurring.asBuilder().setRecurrenceEndTime(now).build();
|
||||
Recurring.Builder newRecurringBuilder =
|
||||
existingRecurring
|
||||
BillingRecurrence endingNow =
|
||||
existingRecurrence.asBuilder().setRecurrenceEndTime(now).build();
|
||||
BillingRecurrence.Builder newRecurrenceBuilder =
|
||||
existingRecurrence
|
||||
.asBuilder()
|
||||
// set the ID to be 0 (null) to create a new object
|
||||
.setId(0)
|
||||
.setDomainHistoryId(newDomainHistoryId);
|
||||
if (renewalPriceBehavior != null) {
|
||||
newRecurringBuilder.setRenewalPriceBehavior(renewalPriceBehavior);
|
||||
newRecurringBuilder.setRenewalPrice(null);
|
||||
newRecurrenceBuilder.setRenewalPriceBehavior(renewalPriceBehavior);
|
||||
newRecurrenceBuilder.setRenewalPrice(null);
|
||||
}
|
||||
if (specifiedRenewalPrice != null) {
|
||||
newRecurringBuilder.setRenewalPrice(specifiedRenewalPrice);
|
||||
newRecurrenceBuilder.setRenewalPrice(specifiedRenewalPrice);
|
||||
}
|
||||
Recurring newRecurring = newRecurringBuilder.build();
|
||||
BillingRecurrence newBillingRecurrence = newRecurrenceBuilder.build();
|
||||
Domain newDomain =
|
||||
domain.asBuilder().setAutorenewBillingEvent(newRecurring.createVKey()).build();
|
||||
domain
|
||||
.asBuilder()
|
||||
.setAutorenewBillingEvent(newBillingRecurrence.createVKey())
|
||||
.build();
|
||||
DomainHistory newDomainHistory =
|
||||
new DomainHistory.Builder()
|
||||
.setRevisionId(newDomainHistoryId.getRevisionId())
|
||||
@@ -149,14 +153,14 @@ public class UpdateRecurrenceCommand extends ConfirmingCommand {
|
||||
.setType(HistoryEntry.Type.SYNTHETIC)
|
||||
.setModificationTime(now)
|
||||
.build();
|
||||
tm().putAll(endingNow, newRecurring, newDomain, newDomainHistory);
|
||||
resultBuilder.add(newRecurring);
|
||||
tm().putAll(endingNow, newBillingRecurrence, newDomain, newDomainHistory);
|
||||
resultBuilder.add(newBillingRecurrence);
|
||||
});
|
||||
return resultBuilder.build();
|
||||
}
|
||||
|
||||
private ImmutableMap<Domain, Recurring> loadDomainsAndRecurrings() {
|
||||
ImmutableMap.Builder<Domain, Recurring> result = new ImmutableMap.Builder<>();
|
||||
private ImmutableMap<Domain, BillingRecurrence> loadDomainsAndRecurrences() {
|
||||
ImmutableMap.Builder<Domain, BillingRecurrence> result = new ImmutableMap.Builder<>();
|
||||
DateTime now = tm().getTransactionTime();
|
||||
for (String domainName : mainParameters) {
|
||||
Domain domain =
|
||||
@@ -183,12 +187,12 @@ public class UpdateRecurrenceCommand extends ConfirmingCommand {
|
||||
"Domain %s autorenew ended prior to now at %s",
|
||||
domainName,
|
||||
endTime));
|
||||
Recurring recurring = tm().loadByKey(domain.getAutorenewBillingEvent());
|
||||
BillingRecurrence billingRecurrence = tm().loadByKey(domain.getAutorenewBillingEvent());
|
||||
checkArgument(
|
||||
recurring.getRecurrenceEndTime().equals(END_OF_TIME),
|
||||
billingRecurrence.getRecurrenceEndTime().equals(END_OF_TIME),
|
||||
"Domain %s's recurrence's end date is not END_OF_TIME",
|
||||
domainName);
|
||||
result.put(domain, recurring);
|
||||
result.put(domain, billingRecurrence);
|
||||
}
|
||||
return result.build();
|
||||
}
|
||||
|
||||
+39
-34
@@ -19,9 +19,9 @@ import static google.registry.persistence.transaction.TransactionManagerFactory.
|
||||
import com.beust.jcommander.Parameter;
|
||||
import com.beust.jcommander.Parameters;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import google.registry.model.billing.BillingBase;
|
||||
import google.registry.model.billing.BillingCancellation;
|
||||
import google.registry.model.billing.BillingEvent;
|
||||
import google.registry.model.billing.BillingEvent.Cancellation;
|
||||
import google.registry.model.billing.BillingEvent.OneTime;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.persistence.transaction.QueryComposer.Comparator;
|
||||
import google.registry.tools.ConfirmingCommand;
|
||||
@@ -29,14 +29,16 @@ import google.registry.tools.params.LongParameter;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Command to create {@link Cancellation}s for specified {@link OneTime} billing events.
|
||||
* Command to create {@link BillingCancellation}s for specified {@link BillingEvent} billing events.
|
||||
*
|
||||
* <p>This can be used to fix situations where we've inadvertently billed registrars. It's generally
|
||||
* easier and better to issue cancellations within the Nomulus system than to attempt to issue
|
||||
* refunds after the fact.
|
||||
*/
|
||||
@Parameters(separators = " =", commandDescription = "Manually create Cancellations for OneTimes.")
|
||||
public class CreateCancellationsForOneTimesCommand extends ConfirmingCommand {
|
||||
@Parameters(
|
||||
separators = " =",
|
||||
commandDescription = "Manually create Cancellations for BillingEvents.")
|
||||
public class CreateCancellationsForBillingEventsCommand extends ConfirmingCommand {
|
||||
|
||||
@Parameter(
|
||||
description = "Space-delimited billing event ID(s) to cancel",
|
||||
@@ -44,80 +46,83 @@ public class CreateCancellationsForOneTimesCommand extends ConfirmingCommand {
|
||||
validateWith = LongParameter.class)
|
||||
private List<Long> mainParameters;
|
||||
|
||||
private ImmutableSet<OneTime> oneTimesToCancel;
|
||||
private ImmutableSet<BillingEvent> billingEventsToCancel;
|
||||
|
||||
@Override
|
||||
protected void init() {
|
||||
ImmutableSet.Builder<Long> missingIdsBuilder = new ImmutableSet.Builder<>();
|
||||
ImmutableSet.Builder<Long> alreadyCancelledIdsBuilder = new ImmutableSet.Builder<>();
|
||||
ImmutableSet.Builder<OneTime> oneTimesBuilder = new ImmutableSet.Builder<>();
|
||||
ImmutableSet.Builder<BillingEvent> billingEventsBuilder = new ImmutableSet.Builder<>();
|
||||
tm().transact(
|
||||
() -> {
|
||||
for (Long billingEventId : ImmutableSet.copyOf(mainParameters)) {
|
||||
VKey<OneTime> key = VKey.create(OneTime.class, billingEventId);
|
||||
VKey<BillingEvent> key = VKey.create(BillingEvent.class, billingEventId);
|
||||
if (tm().exists(key)) {
|
||||
OneTime oneTime = tm().loadByKey(key);
|
||||
if (alreadyCancelled(oneTime)) {
|
||||
BillingEvent billingEvent = tm().loadByKey(key);
|
||||
if (alreadyCancelled(billingEvent)) {
|
||||
alreadyCancelledIdsBuilder.add(billingEventId);
|
||||
} else {
|
||||
oneTimesBuilder.add(oneTime);
|
||||
billingEventsBuilder.add(billingEvent);
|
||||
}
|
||||
} else {
|
||||
missingIdsBuilder.add(billingEventId);
|
||||
}
|
||||
}
|
||||
});
|
||||
oneTimesToCancel = oneTimesBuilder.build();
|
||||
System.out.printf("Found %d OneTime(s) to cancel\n", oneTimesToCancel.size());
|
||||
billingEventsToCancel = billingEventsBuilder.build();
|
||||
System.out.printf("Found %d BillingEvent(s) to cancel\n", billingEventsToCancel.size());
|
||||
ImmutableSet<Long> missingIds = missingIdsBuilder.build();
|
||||
if (!missingIds.isEmpty()) {
|
||||
System.out.printf("Missing OneTime(s) for IDs %s\n", missingIds);
|
||||
System.out.printf("Missing BillingEvent(s) for IDs %s\n", missingIds);
|
||||
}
|
||||
ImmutableSet<Long> alreadyCancelledIds = alreadyCancelledIdsBuilder.build();
|
||||
if (!alreadyCancelledIds.isEmpty()) {
|
||||
System.out.printf(
|
||||
"The following OneTime IDs were already cancelled: %s\n", alreadyCancelledIds);
|
||||
"The following BillingEvent IDs were already cancelled: %s\n", alreadyCancelledIds);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String prompt() {
|
||||
return String.format("Create cancellations for %d OneTime(s)?", oneTimesToCancel.size());
|
||||
return String.format(
|
||||
"Create cancellations for %d BillingEvent(s)?", billingEventsToCancel.size());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String execute() throws Exception {
|
||||
int cancelledOneTimes = 0;
|
||||
for (OneTime oneTime : oneTimesToCancel) {
|
||||
cancelledOneTimes +=
|
||||
int cancelledBillingEvents = 0;
|
||||
for (BillingEvent billingEvent : billingEventsToCancel) {
|
||||
cancelledBillingEvents +=
|
||||
tm().transact(
|
||||
() -> {
|
||||
if (alreadyCancelled(oneTime)) {
|
||||
if (alreadyCancelled(billingEvent)) {
|
||||
System.out.printf(
|
||||
"OneTime %d already cancelled, this is unexpected.\n", oneTime.getId());
|
||||
"BillingEvent %d already cancelled, this is unexpected.\n",
|
||||
billingEvent.getId());
|
||||
return 0;
|
||||
}
|
||||
tm().put(
|
||||
new Cancellation.Builder()
|
||||
.setOneTimeEventKey(oneTime.createVKey())
|
||||
.setBillingTime(oneTime.getBillingTime())
|
||||
.setDomainHistoryId(oneTime.getHistoryEntryId())
|
||||
.setRegistrarId(oneTime.getRegistrarId())
|
||||
.setEventTime(oneTime.getEventTime())
|
||||
.setReason(BillingEvent.Reason.ERROR)
|
||||
.setTargetId(oneTime.getTargetId())
|
||||
new BillingCancellation.Builder()
|
||||
.setBillingEvent(billingEvent.createVKey())
|
||||
.setBillingTime(billingEvent.getBillingTime())
|
||||
.setDomainHistoryId(billingEvent.getHistoryEntryId())
|
||||
.setRegistrarId(billingEvent.getRegistrarId())
|
||||
.setEventTime(billingEvent.getEventTime())
|
||||
.setReason(BillingBase.Reason.ERROR)
|
||||
.setTargetId(billingEvent.getTargetId())
|
||||
.build());
|
||||
System.out.printf(
|
||||
"Added Cancellation for OneTime with ID %d\n", oneTime.getId());
|
||||
"Added BillingCancellation for BillingEvent with ID %d\n",
|
||||
billingEvent.getId());
|
||||
return 1;
|
||||
});
|
||||
}
|
||||
return String.format("Created %d Cancellation event(s)", cancelledOneTimes);
|
||||
return String.format("Created %d BillingCancellation event(s)", cancelledBillingEvents);
|
||||
}
|
||||
|
||||
private boolean alreadyCancelled(OneTime oneTime) {
|
||||
return tm().createQueryComposer(Cancellation.class)
|
||||
.where("refOneTime", Comparator.EQ, oneTime.getId())
|
||||
private boolean alreadyCancelled(BillingEvent billingEvent) {
|
||||
return tm().createQueryComposer(BillingCancellation.class)
|
||||
.where("billingEvent", Comparator.EQ, billingEvent.getId())
|
||||
.first()
|
||||
.isPresent();
|
||||
}
|
||||
@@ -38,9 +38,9 @@
|
||||
|
||||
<mapping-file>META-INF/orm.xml</mapping-file>
|
||||
|
||||
<class>google.registry.model.billing.BillingEvent$Cancellation</class>
|
||||
<class>google.registry.model.billing.BillingEvent$OneTime</class>
|
||||
<class>google.registry.model.billing.BillingEvent$Recurring</class>
|
||||
<class>google.registry.model.billing.BillingCancellation</class>
|
||||
<class>google.registry.model.billing.BillingEvent</class>
|
||||
<class>google.registry.model.billing.BillingRecurrence</class>
|
||||
<class>google.registry.model.common.Cursor</class>
|
||||
<class>google.registry.model.common.DatabaseMigrationStateSchedule</class>
|
||||
<class>google.registry.model.common.DnsRefreshRequest</class>
|
||||
@@ -105,9 +105,9 @@
|
||||
<class>google.registry.persistence.converter.TransferServerApproveEntitySetConverter</class>
|
||||
|
||||
<!-- Generated converters for VKey -->
|
||||
<class>google.registry.model.billing.VKeyConverter_Cancellation</class>
|
||||
<class>google.registry.model.billing.VKeyConverter_OneTime</class>
|
||||
<class>google.registry.model.billing.VKeyConverter_Recurring</class>
|
||||
<class>google.registry.model.billing.VKeyConverter_BillingCancellation</class>
|
||||
<class>google.registry.model.billing.VKeyConverter_BillingEvent</class>
|
||||
<class>google.registry.model.billing.VKeyConverter_BillingRecurrence</class>
|
||||
<class>google.registry.model.contact.VKeyConverter_Contact</class>
|
||||
<class>google.registry.model.domain.VKeyConverter_Domain</class>
|
||||
<class>google.registry.model.domain.token.VKeyConverter_AllocationToken</class>
|
||||
|
||||
+2
-2
@@ -29,8 +29,8 @@ 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.refOneTime
|
||||
LEFT JOIN BillingCancellation cr ON b.cancellationMatchingBillingEvent = cr.refRecurring
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user