mirror of
https://github.com/google/nomulus.git
synced 2025-07-23 11:16:04 +02:00
mv com/google/domain/registry google/registry
This change renames directories in preparation for the great package rename. The repository is now in a broken state because the code itself hasn't been updated. However this should ensure that git correctly preserves history for each file.
This commit is contained in:
parent
a41677aea1
commit
5012893c1d
2396 changed files with 0 additions and 0 deletions
560
java/google/registry/model/billing/BillingEvent.java
Normal file
560
java/google/registry/model/billing/BillingEvent.java
Normal file
|
@ -0,0 +1,560 @@
|
|||
// Copyright 2016 The Domain Registry 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 com.google.domain.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 com.google.domain.registry.util.CollectionUtils.nullToEmptyImmutableCopy;
|
||||
import static com.google.domain.registry.util.CollectionUtils.union;
|
||||
import static com.google.domain.registry.util.DateTimeUtils.END_OF_TIME;
|
||||
|
||||
import com.google.common.base.Optional;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.domain.registry.model.Buildable;
|
||||
import com.google.domain.registry.model.ImmutableObject;
|
||||
import com.google.domain.registry.model.common.TimeOfYear;
|
||||
import com.google.domain.registry.model.domain.GracePeriod;
|
||||
import com.google.domain.registry.model.domain.rgp.GracePeriodStatus;
|
||||
import com.google.domain.registry.model.reporting.HistoryEntry;
|
||||
import com.google.domain.registry.model.transfer.TransferData.TransferServerApproveEntity;
|
||||
|
||||
import com.googlecode.objectify.Key;
|
||||
import com.googlecode.objectify.Ref;
|
||||
import com.googlecode.objectify.annotation.Entity;
|
||||
import com.googlecode.objectify.annotation.Id;
|
||||
import com.googlecode.objectify.annotation.IgnoreSave;
|
||||
import com.googlecode.objectify.annotation.Index;
|
||||
import com.googlecode.objectify.annotation.OnLoad;
|
||||
import com.googlecode.objectify.annotation.Parent;
|
||||
import com.googlecode.objectify.condition.IfNull;
|
||||
|
||||
import org.joda.money.Money;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/** A billable event in a domain's lifecycle. */
|
||||
public abstract class BillingEvent extends ImmutableObject
|
||||
implements Buildable, TransferServerApproveEntity {
|
||||
|
||||
/** The reason for the bill. */
|
||||
public enum Reason {
|
||||
CREATE,
|
||||
TRANSFER,
|
||||
RENEW,
|
||||
// TODO(b/27777398): Drop Reason.AUTO_RENEW after migration to Flag.AUTO_RENEW.
|
||||
AUTO_RENEW,
|
||||
RESTORE,
|
||||
SERVER_STATUS,
|
||||
ERROR
|
||||
}
|
||||
|
||||
/** Set of flags that can be applied to billing events. */
|
||||
public enum Flag {
|
||||
ALLOCATION,
|
||||
ANCHOR_TENANT,
|
||||
AUTO_RENEW,
|
||||
LANDRUSH,
|
||||
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
|
||||
}
|
||||
|
||||
/** Entity id. */
|
||||
@Id
|
||||
long id;
|
||||
|
||||
@Parent
|
||||
Key<HistoryEntry> parent;
|
||||
|
||||
/** The registrar to bill. */
|
||||
@Index
|
||||
String clientId;
|
||||
|
||||
/** When this event was created. For recurring events, this is also the recurrence start time. */
|
||||
@Index
|
||||
DateTime eventTime;
|
||||
|
||||
/** The reason for the bill. */
|
||||
Reason reason;
|
||||
|
||||
/** The fully qualified domain name of the domain that the bill is for. */
|
||||
String targetId;
|
||||
|
||||
Set<Flag> flags;
|
||||
|
||||
public String getClientId() {
|
||||
return clientId;
|
||||
}
|
||||
|
||||
public DateTime getEventTime() {
|
||||
return eventTime;
|
||||
}
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public Reason getReason() {
|
||||
return reason;
|
||||
}
|
||||
|
||||
public String getTargetId() {
|
||||
return targetId;
|
||||
}
|
||||
|
||||
public Key<HistoryEntry> getParentKey() {
|
||||
return parent;
|
||||
}
|
||||
|
||||
public ImmutableSet<Flag> getFlags() {
|
||||
return nullToEmptyImmutableCopy(flags);
|
||||
}
|
||||
|
||||
/** Override Buildable.asBuilder() to give this method stronger typing. */
|
||||
@Override
|
||||
public abstract Builder<?, ?> asBuilder();
|
||||
|
||||
/** An abstract builder for {@link BillingEvent}. */
|
||||
public abstract static class Builder<T extends BillingEvent, 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 setClientId(String clientId) {
|
||||
getInstance().clientId = clientId;
|
||||
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 = flags;
|
||||
return thisCastToDerived();
|
||||
}
|
||||
|
||||
public B setParent(HistoryEntry parent) {
|
||||
getInstance().parent = Key.create(parent);
|
||||
return thisCastToDerived();
|
||||
}
|
||||
|
||||
public B setParent(Key<HistoryEntry> parentKey) {
|
||||
getInstance().parent = parentKey;
|
||||
return thisCastToDerived();
|
||||
}
|
||||
|
||||
@Override
|
||||
public T build() {
|
||||
T instance = getInstance();
|
||||
checkNotNull(instance.reason);
|
||||
checkNotNull(instance.clientId);
|
||||
checkNotNull(instance.eventTime);
|
||||
checkNotNull(instance.targetId);
|
||||
checkNotNull(instance.parent);
|
||||
return super.build();
|
||||
}
|
||||
}
|
||||
|
||||
/** A one-time billable event. */
|
||||
@Entity
|
||||
public static class OneTime extends BillingEvent {
|
||||
|
||||
/** The billable value. */
|
||||
Money cost;
|
||||
|
||||
/** When the cost should be billed. */
|
||||
@Index
|
||||
DateTime billingTime;
|
||||
|
||||
/**
|
||||
* The period in years of the action being billed for, if applicable, otherwise null.
|
||||
* Used for financial reporting.
|
||||
*/
|
||||
@IgnoreSave(IfNull.class)
|
||||
Integer periodYears = null;
|
||||
|
||||
public Money getCost() {
|
||||
return cost;
|
||||
}
|
||||
|
||||
public DateTime getBillingTime() {
|
||||
return billingTime;
|
||||
}
|
||||
|
||||
public Integer getPeriodYears() {
|
||||
return periodYears;
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OneTime build() {
|
||||
OneTime instance = getInstance();
|
||||
checkNotNull(instance.billingTime);
|
||||
checkNotNull(instance.cost);
|
||||
checkState(!instance.cost.isNegative(), "Costs should be non-negative.");
|
||||
ImmutableSet<Reason> reasonsWithPeriods =
|
||||
Sets.immutableEnumSet(Reason.CREATE, Reason.RENEW, Reason.TRANSFER);
|
||||
checkState(
|
||||
reasonsWithPeriods.contains(instance.reason) == (instance.periodYears != null),
|
||||
"Period years must be set if and only if reason is CREATE, RENEW, or TRANSFER.");
|
||||
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
|
||||
public static class Recurring extends BillingEvent {
|
||||
|
||||
// TODO(b/27777398): Remove after migration is complete and Reason.AUTO_RENEW is removed.
|
||||
@OnLoad
|
||||
void setAutorenewFlag() {
|
||||
if (Reason.AUTO_RENEW.equals(reason)) {
|
||||
reason = Reason.RENEW;
|
||||
flags = union(getFlags(), Flag.AUTO_RENEW);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The billing event recurs every year between {@link #eventTime} and this time on the
|
||||
* [month, day, time] specified in {@link #recurrenceTimeOfYear}.
|
||||
*/
|
||||
@Index
|
||||
DateTime recurrenceEndTime;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
@Index
|
||||
TimeOfYear recurrenceTimeOfYear;
|
||||
|
||||
public DateTime getRecurrenceEndTime() {
|
||||
return recurrenceEndTime;
|
||||
}
|
||||
|
||||
public TimeOfYear getRecurrenceTimeOfYear() {
|
||||
return recurrenceTimeOfYear;
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Recurring build() {
|
||||
Recurring instance = getInstance();
|
||||
checkNotNull(instance.eventTime);
|
||||
checkNotNull(instance.reason);
|
||||
instance.recurrenceTimeOfYear = TimeOfYear.fromDateTime(instance.eventTime);
|
||||
instance.recurrenceEndTime =
|
||||
Optional.fromNullable(instance.recurrenceEndTime).or(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
|
||||
public static class Cancellation extends BillingEvent {
|
||||
|
||||
/** The billing time of the charge that is being cancelled. */
|
||||
@Index
|
||||
DateTime billingTime;
|
||||
|
||||
/** The one-time billing event to cancel, or null for autorenew cancellations. */
|
||||
@IgnoreSave(IfNull.class)
|
||||
Ref<BillingEvent.OneTime> refOneTime = null;
|
||||
|
||||
/** The recurring billing event to cancel, or null for non-autorenew cancellations. */
|
||||
@IgnoreSave(IfNull.class)
|
||||
Ref<BillingEvent.Recurring> refRecurring = null;
|
||||
|
||||
public DateTime getBillingTime() {
|
||||
return billingTime;
|
||||
}
|
||||
|
||||
public Ref<? extends BillingEvent> getEventRef() {
|
||||
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 entry, and with the
|
||||
* history entry's 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 BillingEvent.Cancellation forGracePeriod(
|
||||
GracePeriod gracePeriod, HistoryEntry historyEntry, String targetId) {
|
||||
checkArgument(gracePeriod.hasBillingEvent(),
|
||||
"Cannot create cancellation for grace period without billing event");
|
||||
BillingEvent.Cancellation.Builder builder = new BillingEvent.Cancellation.Builder()
|
||||
.setReason(checkNotNull(GRACE_PERIOD_TO_REASON.get(gracePeriod.getType())))
|
||||
.setTargetId(targetId)
|
||||
.setClientId(gracePeriod.getClientId())
|
||||
.setEventTime(historyEntry.getModificationTime())
|
||||
// The charge being cancelled will take place at the grace period's expiration time.
|
||||
.setBillingTime(gracePeriod.getExpirationTime())
|
||||
.setParent(historyEntry);
|
||||
// Set the grace period's billing event using the appropriate Cancellation builder method.
|
||||
if (gracePeriod.getOneTimeBillingEvent() != null) {
|
||||
builder.setOneTimeEventRef(gracePeriod.getOneTimeBillingEvent());
|
||||
} else if (gracePeriod.getRecurringBillingEvent() != null) {
|
||||
builder.setRecurringEventRef(gracePeriod.getRecurringBillingEvent());
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@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 setOneTimeEventRef(Ref<BillingEvent.OneTime> eventRef) {
|
||||
getInstance().refOneTime = eventRef;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setRecurringEventRef(Ref<BillingEvent.Recurring> eventRef) {
|
||||
getInstance().refRecurring = eventRef;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cancellation build() {
|
||||
Cancellation instance = getInstance();
|
||||
checkNotNull(instance.billingTime);
|
||||
checkNotNull(instance.reason);
|
||||
checkState((instance.refOneTime == null) != (instance.refRecurring == null),
|
||||
"Cancellations must have exactly one billing event ref set");
|
||||
return super.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An event representing a modification of an existing one-time billing event.
|
||||
*/
|
||||
@Entity
|
||||
public static class Modification extends BillingEvent {
|
||||
|
||||
/** The change in cost that should be applied to the original billing event. */
|
||||
Money cost;
|
||||
|
||||
/** The one-time billing event to modify. */
|
||||
Ref<BillingEvent.OneTime> eventRef;
|
||||
|
||||
/**
|
||||
* Description of the modification (and presumably why it was issued). This text may appear as a
|
||||
* line item on an invoice or report about such modifications.
|
||||
*/
|
||||
String description;
|
||||
|
||||
public Money getCost() {
|
||||
return cost;
|
||||
}
|
||||
|
||||
public Ref<BillingEvent.OneTime> getEventRef() {
|
||||
return eventRef;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder asBuilder() {
|
||||
return new Builder(clone(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new Modification billing event which is a refund of the given OneTime billing event
|
||||
* and that is parented off the given HistoryEntry.
|
||||
*
|
||||
* <p>Note that this method may appear to be unused most of the time, but it is kept around
|
||||
* because it is needed by one-off scrap tools that need to make billing adjustments.
|
||||
*/
|
||||
public static Modification createRefundFor(
|
||||
OneTime billingEvent, HistoryEntry historyEntry, String description) {
|
||||
return new Builder()
|
||||
.setClientId(billingEvent.getClientId())
|
||||
.setFlags(billingEvent.getFlags())
|
||||
.setReason(billingEvent.getReason())
|
||||
.setTargetId(billingEvent.getTargetId())
|
||||
.setEventRef(Ref.create(billingEvent))
|
||||
.setEventTime(historyEntry.getModificationTime())
|
||||
.setDescription(description)
|
||||
.setCost(billingEvent.getCost().negated())
|
||||
.setParent(historyEntry)
|
||||
.build();
|
||||
}
|
||||
|
||||
/** A builder for {@link Modification} since it is immutable. */
|
||||
public static class Builder extends BillingEvent.Builder<Modification, Builder> {
|
||||
|
||||
public Builder() {}
|
||||
|
||||
private Builder(Modification instance) {
|
||||
super(instance);
|
||||
}
|
||||
|
||||
public Builder setCost(Money cost) {
|
||||
getInstance().cost = cost;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setEventRef(Ref<BillingEvent.OneTime> eventRef) {
|
||||
getInstance().eventRef = eventRef;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setDescription(String description) {
|
||||
getInstance().description = description;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public Modification build() {
|
||||
Modification instance = getInstance();
|
||||
checkNotNull(instance.reason);
|
||||
checkNotNull(instance.eventRef);
|
||||
BillingEvent.OneTime billingEvent = instance.eventRef.get();
|
||||
checkArgument(Objects.equals(
|
||||
instance.cost.getCurrencyUnit(),
|
||||
billingEvent.cost.getCurrencyUnit()),
|
||||
"Referenced billing event is in a different currency");
|
||||
return super.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
212
java/google/registry/model/billing/RegistrarBillingEntry.java
Normal file
212
java/google/registry/model/billing/RegistrarBillingEntry.java
Normal file
|
@ -0,0 +1,212 @@
|
|||
// Copyright 2016 The Domain Registry 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 com.google.domain.registry.model.billing;
|
||||
|
||||
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 com.google.common.base.Strings.emptyToNull;
|
||||
import static com.google.common.base.Verify.verifyNotNull;
|
||||
|
||||
import com.google.domain.registry.model.Buildable;
|
||||
import com.google.domain.registry.model.ImmutableObject;
|
||||
import com.google.domain.registry.model.JsonMapBuilder;
|
||||
import com.google.domain.registry.model.Jsonifiable;
|
||||
import com.google.domain.registry.model.registrar.Registrar;
|
||||
|
||||
import com.googlecode.objectify.Key;
|
||||
import com.googlecode.objectify.annotation.Entity;
|
||||
import com.googlecode.objectify.annotation.Id;
|
||||
import com.googlecode.objectify.annotation.Index;
|
||||
import com.googlecode.objectify.annotation.Parent;
|
||||
|
||||
import org.joda.money.CurrencyUnit;
|
||||
import org.joda.money.Money;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Log of monthly invoices and payments for a Registrar customer.
|
||||
*
|
||||
* <p>This is a one-off single-entry bookkeeping system. There is a separate account for each
|
||||
* (registrar, currency) pair.
|
||||
*
|
||||
* <p>You should never update these entities once they've been inserted into datastore. If you need
|
||||
* to change something, add a correction entry.
|
||||
*/
|
||||
@Entity
|
||||
public class RegistrarBillingEntry extends ImmutableObject implements Jsonifiable {
|
||||
|
||||
@Parent
|
||||
Key<Registrar> parent;
|
||||
|
||||
/** Arbitrary unique identifier. */
|
||||
@Id
|
||||
long id;
|
||||
|
||||
/**
|
||||
* External transaction identifier or {@code null} if this is an invoice entry.
|
||||
*
|
||||
* <p>This is the ID or token that the payment gateway gives us, which represents the transaction
|
||||
* in their database.
|
||||
*/
|
||||
@Nullable
|
||||
String transactionId;
|
||||
|
||||
/**
|
||||
* Time at which this entry was created.
|
||||
*
|
||||
* <p>This value is unique and monotonic for a given ({@link #parent}, {@link #currency}) pair.
|
||||
*/
|
||||
@Index
|
||||
DateTime created;
|
||||
|
||||
/** Completely arbitrary description of payment. */
|
||||
String description;
|
||||
|
||||
/**
|
||||
* Currency of transaction.
|
||||
*
|
||||
* <p>This field is identical to {@code amount.getCurrencyUnit()} and is only here so it can be
|
||||
* indexed in datastore.
|
||||
*/
|
||||
@Index
|
||||
CurrencyUnit currency;
|
||||
|
||||
/**
|
||||
* Amount and currency of invoice or payment.
|
||||
*
|
||||
* <p>This field is positive for debits (e.g. monthly invoice entries) and negative for credits
|
||||
* (e.g. credit card payment transaction entries.)
|
||||
*/
|
||||
Money amount;
|
||||
|
||||
/**
|
||||
* Balance of account for this currency.
|
||||
*
|
||||
* <p>This is {@code amount + previous.balance}.
|
||||
*/
|
||||
Money balance;
|
||||
|
||||
public Key<Registrar> getParent() {
|
||||
return parent;
|
||||
}
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getTransactionId() {
|
||||
return transactionId;
|
||||
}
|
||||
|
||||
public DateTime getCreated() {
|
||||
return verifyNotNull(created, "created missing: %s", this);
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return verifyNotNull(description, "description missing: %s", this);
|
||||
}
|
||||
|
||||
public CurrencyUnit getCurrency() {
|
||||
return verifyNotNull(currency, "currency missing: %s", this);
|
||||
}
|
||||
|
||||
public Money getAmount() {
|
||||
return verifyNotNull(amount, "amount missing: %s", this);
|
||||
}
|
||||
|
||||
public Money getBalance() {
|
||||
return verifyNotNull(balance, "balance missing: %s", this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> toJsonMap() {
|
||||
return new JsonMapBuilder()
|
||||
.put("id", id)
|
||||
.put("transactionId", getTransactionId())
|
||||
.putString("created", getCreated())
|
||||
.put("description", getDescription())
|
||||
.putString("currency", getCurrency())
|
||||
.putString("amount", getAmount().getAmount())
|
||||
.putString("balance", getBalance().getAmount())
|
||||
.build();
|
||||
}
|
||||
|
||||
/** A builder for constructing a {@link RegistrarBillingEntry}, since it's immutable. */
|
||||
public static class Builder extends Buildable.Builder<RegistrarBillingEntry> {
|
||||
|
||||
@Nullable
|
||||
private RegistrarBillingEntry previous;
|
||||
|
||||
public Builder() {}
|
||||
|
||||
public Builder setParent(Registrar parent) {
|
||||
getInstance().parent = Key.create(parent);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setCreated(DateTime created) {
|
||||
getInstance().created = created;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setPrevious(@Nullable RegistrarBillingEntry previous) {
|
||||
this.previous = previous;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setTransactionId(@Nullable String transactionId) {
|
||||
getInstance().transactionId = transactionId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setDescription(String description) {
|
||||
getInstance().description = checkNotNull(emptyToNull(description));
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setAmount(Money amount) {
|
||||
checkArgument(!amount.isZero(), "Amount can't be zero");
|
||||
getInstance().amount = amount;
|
||||
getInstance().currency = amount.getCurrencyUnit();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RegistrarBillingEntry build() {
|
||||
checkNotNull(getInstance().parent, "parent");
|
||||
checkNotNull(getInstance().created, "created");
|
||||
checkNotNull(getInstance().description, "description");
|
||||
checkNotNull(getInstance().amount, "amount");
|
||||
if (previous == null) {
|
||||
getInstance().balance = getInstance().amount;
|
||||
} else {
|
||||
getInstance().balance = previous.balance.plus(getInstance().amount);
|
||||
checkState(getInstance().parent.equals(previous.parent),
|
||||
"Parent not same as previous:\nNew: %s\nPrevious: %s",
|
||||
getInstance(), previous);
|
||||
checkState(getInstance().created.isAfter(previous.created),
|
||||
"Created timestamp not after previous:\nNew: %s\nPrevious: %s",
|
||||
getInstance(), previous);
|
||||
}
|
||||
return cloneEmptyToNull(super.build());
|
||||
}
|
||||
}
|
||||
}
|
100
java/google/registry/model/billing/RegistrarBillingUtils.java
Normal file
100
java/google/registry/model/billing/RegistrarBillingUtils.java
Normal file
|
@ -0,0 +1,100 @@
|
|||
// Copyright 2016 The Domain Registry 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 com.google.domain.registry.model.billing;
|
||||
|
||||
import static com.google.domain.registry.model.ofy.ObjectifyService.ofy;
|
||||
|
||||
import com.google.common.base.Function;
|
||||
import com.google.common.base.Supplier;
|
||||
import com.google.common.collect.FluentIterable;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSortedSet;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.collect.Maps.EntryTransformer;
|
||||
import com.google.common.collect.Ordering;
|
||||
import com.google.domain.registry.model.registrar.Registrar;
|
||||
import com.google.domain.registry.model.registry.Registries;
|
||||
import com.google.domain.registry.model.registry.Registry;
|
||||
import com.google.domain.registry.util.CacheUtils;
|
||||
|
||||
import com.googlecode.objectify.cmd.Query;
|
||||
|
||||
import org.joda.money.CurrencyUnit;
|
||||
import org.joda.money.Money;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/** Utilities for managing the billing of {@link Registrar} customers. */
|
||||
public final class RegistrarBillingUtils {
|
||||
|
||||
private static final Supplier<ImmutableSortedSet<CurrencyUnit>> CURRENCIES_CACHE =
|
||||
CacheUtils.memoizeWithShortExpiration(
|
||||
new Supplier<ImmutableSortedSet<CurrencyUnit>>() {
|
||||
@Override
|
||||
public ImmutableSortedSet<CurrencyUnit> get() {
|
||||
return FluentIterable
|
||||
.from(Registries.getTlds())
|
||||
.transform(new Function<String, CurrencyUnit>() {
|
||||
@Override
|
||||
public CurrencyUnit apply(String tld) {
|
||||
return Registry.get(tld).getCurrency();
|
||||
}})
|
||||
.toSortedSet(Ordering.natural());
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns set of currencies in which registrars may be billed.
|
||||
*
|
||||
* <p>Each TLD has a currency associated with it. We don't do conversions. The registrar customer
|
||||
* gets a separate bill for each currency.
|
||||
*/
|
||||
public static ImmutableSortedSet<CurrencyUnit> getCurrencies() {
|
||||
return CURRENCIES_CACHE.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns query of {@link RegistrarBillingEntry} for each currency, most recent first.
|
||||
*
|
||||
* <p><b>Note:</b> Currency map keys are returned in sorted order, from {@link #getCurrencies()}.
|
||||
*/
|
||||
public static ImmutableMap<CurrencyUnit, Query<RegistrarBillingEntry>> getBillingEntryQueries(
|
||||
final Registrar registrar) {
|
||||
return Maps.toMap(getCurrencies(),
|
||||
new Function<CurrencyUnit, Query<RegistrarBillingEntry>>() {
|
||||
@Override
|
||||
public Query<RegistrarBillingEntry> apply(CurrencyUnit currency) {
|
||||
return ofy().load()
|
||||
.type(RegistrarBillingEntry.class)
|
||||
.ancestor(registrar)
|
||||
.filter("currency", currency)
|
||||
.order("-created");
|
||||
}});
|
||||
}
|
||||
|
||||
/** Returns amount of money registrar currently owes registry in each currency. */
|
||||
public static Map<CurrencyUnit, Money> loadBalance(Registrar registrar) {
|
||||
return Maps.transformEntries(getBillingEntryQueries(registrar),
|
||||
new EntryTransformer<CurrencyUnit, Query<RegistrarBillingEntry>, Money>() {
|
||||
@Override
|
||||
public Money transformEntry(
|
||||
CurrencyUnit currency, Query<RegistrarBillingEntry> query) {
|
||||
RegistrarBillingEntry entry = query.first().now();
|
||||
return entry != null ? entry.getBalance() : Money.zero(currency);
|
||||
}});
|
||||
}
|
||||
|
||||
private RegistrarBillingUtils() {}
|
||||
}
|
216
java/google/registry/model/billing/RegistrarCredit.java
Normal file
216
java/google/registry/model/billing/RegistrarCredit.java
Normal file
|
@ -0,0 +1,216 @@
|
|||
// Copyright 2016 The Domain Registry 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 com.google.domain.registry.model.billing;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static com.google.domain.registry.model.ofy.ObjectifyService.ofy;
|
||||
import static com.google.domain.registry.model.registry.Registries.assertTldExists;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.base.Optional;
|
||||
import com.google.common.collect.ComparisonChain;
|
||||
import com.google.common.collect.FluentIterable;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.Ordering;
|
||||
import com.google.domain.registry.model.Buildable;
|
||||
import com.google.domain.registry.model.ImmutableObject;
|
||||
import com.google.domain.registry.model.registrar.Registrar;
|
||||
import com.google.domain.registry.model.registry.Registry;
|
||||
|
||||
import com.googlecode.objectify.Ref;
|
||||
import com.googlecode.objectify.annotation.Entity;
|
||||
import com.googlecode.objectify.annotation.Id;
|
||||
import com.googlecode.objectify.annotation.Parent;
|
||||
|
||||
import org.joda.money.CurrencyUnit;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
/** A per-registrar billing credit, applied toward future charges for registrar activity. */
|
||||
@Entity
|
||||
public final class RegistrarCredit extends ImmutableObject implements Buildable {
|
||||
|
||||
/**
|
||||
* The type of credit represented. The ordering below determines the order in which credits of
|
||||
* of different types will be applied to an invoice charge.
|
||||
*/
|
||||
// Note: Right now the ordering is actually maintained manually via a hard-coded table in the
|
||||
// relevant billing query, so if adding a credit type here, add it there as well.
|
||||
// TODO(b/19031546): make the query automatically reflect the order in this enum.
|
||||
public enum CreditType {
|
||||
/** Credit awarded as an incentive to participate in sunrise/landrush auctions. */
|
||||
AUCTION("Auction Credit"),
|
||||
|
||||
/** Credit awarded as part of a promotional deal. */
|
||||
PROMOTION("Promotional Credit");
|
||||
|
||||
/** A descriptive name for a credit of this type. */
|
||||
private String descriptiveName;
|
||||
|
||||
CreditType(String descriptiveName) {
|
||||
this.descriptiveName = descriptiveName;
|
||||
}
|
||||
|
||||
public String getDescriptiveName() {
|
||||
return descriptiveName;
|
||||
}
|
||||
}
|
||||
|
||||
@Id
|
||||
long id;
|
||||
|
||||
/** The registrar to whom this credit belongs. */
|
||||
@Parent
|
||||
Ref<Registrar> parent;
|
||||
|
||||
/** The type of credit. */
|
||||
CreditType type;
|
||||
|
||||
/**
|
||||
* The time that this credit was created. If a registrar has multiple credits of a given type,
|
||||
* the older credits will be applied first.
|
||||
*/
|
||||
DateTime creationTime;
|
||||
|
||||
/** The currency in which the balance for this credit is stored. */
|
||||
CurrencyUnit currency;
|
||||
|
||||
/** The line item description to use when displaying this credit on an invoice. */
|
||||
String description;
|
||||
|
||||
/**
|
||||
* The TLD in which this credit applies.
|
||||
*
|
||||
* <p>For auction credits, this is also the TLD for which the relevant auctions occurred.
|
||||
*/
|
||||
String tld;
|
||||
|
||||
public Ref<Registrar> getParent() {
|
||||
return parent;
|
||||
}
|
||||
|
||||
public CreditType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public DateTime getCreationTime() {
|
||||
return creationTime;
|
||||
}
|
||||
|
||||
public CurrencyUnit getCurrency() {
|
||||
return currency;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public String getTld() {
|
||||
return tld;
|
||||
}
|
||||
|
||||
/** Returns a string representation of this credit. */
|
||||
public String getSummary() {
|
||||
String fields = Joiner.on(' ').join(type, creationTime, tld);
|
||||
return String.format("%s (%s/%d) - %s", description, parent.getKey().getName(), id, fields);
|
||||
}
|
||||
|
||||
/** Returns the default description for this {@link RegistrarCredit} instance. */
|
||||
private String getDefaultDescription() {
|
||||
return type.getDescriptiveName() + " for ." + tld;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder asBuilder() {
|
||||
return new Builder(clone(this));
|
||||
}
|
||||
|
||||
/** A Builder for {@link RegistrarCredit}. */
|
||||
public static class Builder extends Buildable.Builder<RegistrarCredit> {
|
||||
public Builder() {}
|
||||
|
||||
public Builder(RegistrarCredit instance) {
|
||||
super(instance);
|
||||
}
|
||||
|
||||
public Builder setParent(Registrar parent) {
|
||||
getInstance().parent = Ref.create(parent);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setType(CreditType type) {
|
||||
getInstance().type = type;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setCreationTime(DateTime creationTime) {
|
||||
getInstance().creationTime = creationTime;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setCurrency(CurrencyUnit currency) {
|
||||
getInstance().currency = currency;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setDescription(String description) {
|
||||
getInstance().description = description;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setTld(String tld) {
|
||||
getInstance().tld = tld;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RegistrarCredit build() {
|
||||
RegistrarCredit instance = getInstance();
|
||||
checkNotNull(instance.parent, "parent credit");
|
||||
checkNotNull(instance.type, "type");
|
||||
checkNotNull(instance.creationTime, "creationTime");
|
||||
checkNotNull(instance.currency, "currency");
|
||||
assertTldExists(checkNotNull(instance.tld, "tld"));
|
||||
checkArgument(
|
||||
Registry.get(instance.tld).getCurrency().equals(instance.currency),
|
||||
"Credits must be in the currency of the assigned TLD");
|
||||
instance.description =
|
||||
Optional.fromNullable(instance.description).or(instance.getDefaultDescription());
|
||||
return super.build();
|
||||
}
|
||||
}
|
||||
|
||||
/** Ordering that sorts credits first by type and then by creation time. */
|
||||
private static final Ordering<RegistrarCredit> CREDIT_PRIORITY_ORDERING =
|
||||
new Ordering<RegistrarCredit>() {
|
||||
@Override
|
||||
public int compare(RegistrarCredit left, RegistrarCredit right) {
|
||||
return ComparisonChain.start()
|
||||
.compare(left.type, right.type)
|
||||
.compare(left.creationTime, right.creationTime)
|
||||
.result();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Loads all RegistrarCredit entities for the given Registrar.
|
||||
*
|
||||
* <p>The resulting list sorts the credits first by type and then by creation time.
|
||||
*/
|
||||
public static ImmutableList<RegistrarCredit> loadAllForRegistrar(Registrar registrar) {
|
||||
return FluentIterable.from(ofy().load().type(RegistrarCredit.class).ancestor(registrar))
|
||||
.toSortedList(CREDIT_PRIORITY_ORDERING);
|
||||
}
|
||||
}
|
253
java/google/registry/model/billing/RegistrarCreditBalance.java
Normal file
253
java/google/registry/model/billing/RegistrarCreditBalance.java
Normal file
|
@ -0,0 +1,253 @@
|
|||
// Copyright 2016 The Domain Registry 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 com.google.domain.registry.model.billing;
|
||||
|
||||
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 com.google.domain.registry.model.ofy.ObjectifyService.ofy;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.base.Function;
|
||||
import com.google.common.base.Optional;
|
||||
import com.google.common.collect.ForwardingNavigableMap;
|
||||
import com.google.common.collect.ImmutableSortedMap;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.collect.Ordering;
|
||||
import com.google.domain.registry.model.Buildable;
|
||||
import com.google.domain.registry.model.ImmutableObject;
|
||||
|
||||
import com.googlecode.objectify.Key;
|
||||
import com.googlecode.objectify.Ref;
|
||||
import com.googlecode.objectify.annotation.Entity;
|
||||
import com.googlecode.objectify.annotation.Id;
|
||||
import com.googlecode.objectify.annotation.Parent;
|
||||
import com.googlecode.objectify.impl.ref.DeadRef;
|
||||
|
||||
import org.joda.money.Money;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* The balance of a {@link RegistrarCredit} at a given point in time.
|
||||
*
|
||||
* <p>A credit balance has two related times in addition to the monetary amount: the effective time,
|
||||
* which represents the time at which the amount becomes the actual credit balance; and the
|
||||
* written time, which represents the time at which this balance object was saved.
|
||||
*
|
||||
* <p>The active balance of a credit object before (at) any given point in time T can be found by
|
||||
* taking the balance object with the latest effective time that is before (before or at) T, and
|
||||
* breaking any ties by choosing the mostly recently written among those balances.
|
||||
*/
|
||||
@Entity
|
||||
public final class RegistrarCreditBalance extends ImmutableObject implements Buildable {
|
||||
|
||||
@Id
|
||||
long id;
|
||||
|
||||
/** The registrar credit object for which this represents a balance. */
|
||||
@Parent
|
||||
Ref<RegistrarCredit> parent;
|
||||
|
||||
/** The time at which this balance amount should become effective. */
|
||||
DateTime effectiveTime;
|
||||
|
||||
/**
|
||||
* The time at which this balance update was written.
|
||||
*
|
||||
* <p>Used to break ties in cases where there are multiple balances with the same effective time,
|
||||
* as the last written balance will take priority.
|
||||
*/
|
||||
DateTime writtenTime;
|
||||
|
||||
/** The monetary amount of credit balance remaining as of the effective time. */
|
||||
Money amount;
|
||||
|
||||
public Ref<RegistrarCredit> getParent() {
|
||||
return parent;
|
||||
}
|
||||
|
||||
public DateTime getEffectiveTime() {
|
||||
return effectiveTime;
|
||||
}
|
||||
|
||||
public DateTime getWrittenTime() {
|
||||
return writtenTime;
|
||||
}
|
||||
|
||||
public Money getAmount() {
|
||||
return amount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder asBuilder() {
|
||||
return new Builder(clone(this));
|
||||
}
|
||||
|
||||
/** A Builder for an {@link RegistrarCreditBalance}. */
|
||||
public static class Builder extends Buildable.Builder<RegistrarCreditBalance> {
|
||||
public Builder() {}
|
||||
|
||||
public Builder(RegistrarCreditBalance instance) {
|
||||
super(instance);
|
||||
}
|
||||
|
||||
public RegistrarCreditBalance.Builder setParent(RegistrarCredit parent) {
|
||||
// Use a DeadRef so that we can retrieve the actual instance provided later on in build().
|
||||
getInstance().parent = new DeadRef<>(Key.create(parent), parent);
|
||||
return this;
|
||||
}
|
||||
|
||||
public RegistrarCreditBalance.Builder setEffectiveTime(DateTime effectiveTime) {
|
||||
getInstance().effectiveTime = effectiveTime;
|
||||
return this;
|
||||
}
|
||||
|
||||
public RegistrarCreditBalance.Builder setWrittenTime(DateTime writtenTime) {
|
||||
getInstance().writtenTime = writtenTime;
|
||||
return this;
|
||||
}
|
||||
|
||||
public RegistrarCreditBalance.Builder setAmount(Money amount) {
|
||||
checkArgument(amount.isPositiveOrZero(), "Credit balance amount cannot be negative");
|
||||
getInstance().amount = amount;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RegistrarCreditBalance build() {
|
||||
RegistrarCreditBalance instance = getInstance();
|
||||
checkNotNull(instance.parent);
|
||||
checkNotNull(instance.effectiveTime);
|
||||
checkNotNull(instance.writtenTime);
|
||||
checkNotNull(instance.amount);
|
||||
RegistrarCredit credit = instance.parent.get();
|
||||
checkState(
|
||||
instance.amount.getCurrencyUnit().equals(credit.getCurrency()),
|
||||
"Currency of balance amount differs from credit currency (%s vs %s)",
|
||||
instance.amount.getCurrencyUnit(),
|
||||
credit.getCurrency());
|
||||
return super.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A map of maps representing the historical credit balance information for a given credit.
|
||||
*
|
||||
* <p>Specifically, this class provides a high-level view of the balances for a given credit
|
||||
* by in essence grouping them first by effective time and then by written time. This facilitates
|
||||
* the printing of a readable representation of a credit's balance history, and the retrieval of
|
||||
* the active balance at a given time (as described above on RegistrarCreditBalance).
|
||||
*/
|
||||
public static class BalanceMap
|
||||
extends ForwardingNavigableMap<DateTime, ImmutableSortedMap<DateTime, Money>> {
|
||||
|
||||
/**
|
||||
* Constructs a BalanceMap for the given registrar credit by loading all RegistrarCreditBalance
|
||||
* entities for the credit and then inserting them into a map of maps keyed first by effective
|
||||
* time and then by written time with the balance amount as the value.
|
||||
*/
|
||||
public static BalanceMap createForCredit(RegistrarCredit registrarCredit) {
|
||||
// Build up the data in a mutable map of maps.
|
||||
Map<DateTime, Map<DateTime, Money>> map = new HashMap<>();
|
||||
for (RegistrarCreditBalance balance :
|
||||
ofy().load().type(RegistrarCreditBalance.class).ancestor(registrarCredit)) {
|
||||
// Create the submap at this key if it doesn't exist already.
|
||||
Map<DateTime, Money> submap =
|
||||
Optional.fromNullable(map.get(balance.effectiveTime))
|
||||
.or(new HashMap<DateTime, Money>());
|
||||
submap.put(balance.writtenTime, balance.amount);
|
||||
map.put(balance.effectiveTime, submap);
|
||||
}
|
||||
// Wrap the mutable map of maps in an immutable BalanceMap.
|
||||
return new BalanceMap(map);
|
||||
}
|
||||
|
||||
/** The immutable map of maps used as the backing map. */
|
||||
private final ImmutableSortedMap<DateTime, ImmutableSortedMap<DateTime, Money>> delegate;
|
||||
|
||||
/**
|
||||
* Constructs an immutable BalanceMap from balance data provided as a map of maps.
|
||||
*
|
||||
* <p>The constructed BalanceMap delegates to an immutable copy of the provided map of maps.
|
||||
* This copy is created by first making a view of the map in which each submap is replaced by
|
||||
* an immutable copy, and then making an immutable copy of that view.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
BalanceMap(Map<DateTime, ? extends Map<DateTime, Money>> data) {
|
||||
delegate = ImmutableSortedMap.copyOf(
|
||||
Maps.transformValues(
|
||||
data,
|
||||
new Function<Map<DateTime, Money>, ImmutableSortedMap<DateTime, Money>>() {
|
||||
@Override
|
||||
public ImmutableSortedMap<DateTime, Money> apply(Map<DateTime, Money> map) {
|
||||
return ImmutableSortedMap.copyOf(map, Ordering.natural());
|
||||
}
|
||||
}),
|
||||
Ordering.natural());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ImmutableSortedMap<DateTime, ImmutableSortedMap<DateTime, Money>> delegate() {
|
||||
return delegate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the most recently written balance for the effective time corresponding to this entry,
|
||||
* or {@link Optional#absent()} if this entry is null.
|
||||
*/
|
||||
private Optional<Money> getMostRecentlyWrittenBalance(
|
||||
Map.Entry<DateTime, ImmutableSortedMap<DateTime, Money>> balancesAtEffectiveTime) {
|
||||
return balancesAtEffectiveTime == null
|
||||
? Optional.<Money>absent()
|
||||
// Don't use Optional.fromNullable() here since it's an error if there's a empty submap.
|
||||
: Optional.of(balancesAtEffectiveTime.getValue().lastEntry().getValue());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the active balance at a given time as described above on RegistrarCreditBalance, or
|
||||
* {@link Optional#absent()} if no balance was active at that time (i.e. the time provided is
|
||||
* before the first effectiveTime of any balance for the credit this BalanceMap represents).
|
||||
*/
|
||||
public Optional<Money> getActiveBalanceAtTime(DateTime time) {
|
||||
return getMostRecentlyWrittenBalance(delegate.floorEntry(time));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the active balance before a given time as described above on RegistrarCreditBalance,
|
||||
* or {@link Optional#absent()} if no balance was active before that time (i.e. the time
|
||||
* provided is before or at the first effectiveTime of any balance for the credit).
|
||||
*/
|
||||
public Optional<Money> getActiveBalanceBeforeTime(DateTime time) {
|
||||
return getMostRecentlyWrittenBalance(delegate.lowerEntry(time));
|
||||
}
|
||||
|
||||
/** Returns a string representation of this BalanceMap's data. */
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (Map.Entry<DateTime, ? extends Map<DateTime, Money>> entry : delegate.entrySet()) {
|
||||
builder.append(String.format(" - %s\n", entry.getKey()));
|
||||
for (Map.Entry<DateTime, Money> subEntry : entry.getValue().entrySet()) {
|
||||
builder.append(
|
||||
String.format(" - %s - %s\n", subEntry.getKey(), subEntry.getValue()));
|
||||
}
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue