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:
Justine Tunney 2016-05-13 18:55:08 -04:00
parent a41677aea1
commit 5012893c1d
2396 changed files with 0 additions and 0 deletions

View 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();
}
}
}
}

View 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());
}
}
}

View 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() {}
}

View 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);
}
}

View 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();
}
}
}