ContiguousInArrearUsageInterval.java

212 lines | 8.73 kB Blame History Raw Download
/*
 * Copyright 2014 The Billing Project, Inc.
 *
 * Ning licenses this file to you 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 org.killbill.billing.invoice.usage;

import java.math.BigDecimal;
import java.util.List;
import java.util.UUID;

import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.LocalDate;
import org.killbill.billing.catalog.api.CatalogApiException;
import org.killbill.billing.catalog.api.Currency;
import org.killbill.billing.catalog.api.TieredBlock;
import org.killbill.billing.catalog.api.Usage;
import org.killbill.billing.invoice.api.InvoiceItem;
import org.killbill.billing.invoice.api.InvoiceItemType;
import org.killbill.billing.invoice.generator.BillingIntervalDetail;
import org.killbill.billing.invoice.model.UsageInvoiceItem;
import org.killbill.billing.junction.BillingEvent;
import org.killbill.billing.usage.api.RolledUpUsage;
import org.killbill.billing.usage.api.UsageUserApi;
import org.killbill.billing.util.callcontext.TenantContext;

import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;

import static org.killbill.billing.invoice.usage.SubscriptionConsumableInArrear.getTieredBlocks;
import static org.killbill.billing.invoice.usage.SubscriptionConsumableInArrear.localDateToEndOfDayInAccountTimezone;

public class ContiguousInArrearUsageInterval {

    private final List<LocalDate> transitionTimes;
    private final List<BillingEvent> billingEvents;

    private final Usage usage;
    private final String unitType;
    private final UsageUserApi usageApi;
    private final LocalDate targetDate;
    private final UUID invoiceId;
    private final TenantContext context;

    public ContiguousInArrearUsageInterval(final Usage usage, final UUID invoiceId, final String unitType, final UsageUserApi usageApi, final LocalDate targetDate, final TenantContext context) {
        this.usage = usage;
        this.invoiceId = invoiceId;
        this.unitType = unitType;
        this.usageApi = usageApi;
        this.targetDate = targetDate;
        this.context = context;
        this.billingEvents = Lists.newLinkedList();
        this.transitionTimes = Lists.newLinkedList();
    }

    public void addBillingEvent(final BillingEvent event) {
        billingEvents.add(event);
    }

    public Usage getUsage() {
        return usage;
    }

    public int getBCD() {
        return billingEvents.get(0).getBillCycleDayLocal();
    }

    public UUID getAccountId() {
        return billingEvents.get(0).getAccount().getId();
    }

    public UUID getBundleId() {
        return billingEvents.get(0).getSubscription().getBundleId();
    }

    public UUID getSubscriptionId() {
        return billingEvents.get(0).getSubscription().getId();
    }

    // STEPH_USAGE planName/phaseName,BCD,... might not be correct if we changed plan but Usage section was exactly similar
    public String getPlanName() {
        return billingEvents.get(0).getPlan().getName();
    }

    public String getPhaseName() {
        return billingEvents.get(0).getPlanPhase().getName();
    }

    public Currency getCurrency() {
        return billingEvents.get(0).getCurrency();
    }

    public DateTimeZone getAccountTimeZone() {
        return billingEvents.get(0).getTimeZone();
    }



    public ContiguousInArrearUsageInterval build(final boolean closedInterval) {

        Preconditions.checkState((!closedInterval && billingEvents.size() >= 1) ||
                                 (closedInterval && billingEvents.size() >= 2));

        final LocalDate startDate = new LocalDate(billingEvents.get(0).getEffectiveDate(), getAccountTimeZone());
        if (targetDate.isBefore(startDate)) {
            return this;
        }

        final LocalDate endDate = closedInterval ? new LocalDate(billingEvents.get(billingEvents.size() - 1), getAccountTimeZone()) : targetDate;

        final BillingIntervalDetail bid = new BillingIntervalDetail(startDate, endDate, targetDate, getBCD(), usage.getBillingPeriod());

        int numberOfPeriod = 0;
        // First billingCycleDate prior startDate
        LocalDate nextBillCycleDate = bid.getFutureBillingDateFor(numberOfPeriod);
        transitionTimes.add(startDate);
        while (!nextBillCycleDate.isAfter(endDate)) {
            if (nextBillCycleDate.isAfter(startDate)) {
                transitionTimes.add(nextBillCycleDate);
            }
            numberOfPeriod++;
            nextBillCycleDate = bid.getFutureBillingDateFor(numberOfPeriod);
        }
        return this;
    }

    public List<InvoiceItem> computeMissingItems(final List<InvoiceItem> existingUsage) throws CatalogApiException {

        final List<InvoiceItem> result = Lists.newLinkedList();

        final List<RolledUpUsage> rolledUpUsages = getRolledUpUsage();
        for (RolledUpUsage ru : rolledUpUsages) {
            final LocalDate startRolledUpDate = new LocalDate(ru.getStartTime(), getAccountTimeZone());
            final LocalDate endRolledUpDate = new LocalDate(ru.getEndTime(), getAccountTimeZone());
            final BigDecimal billedUsage = computeBilledUsage(startRolledUpDate, endRolledUpDate, existingUsage);
            final BigDecimal toBeBilledUsage = computeToBeBilledUsage(ru.getAmount());
            if (billedUsage.compareTo(toBeBilledUsage) < 0) {
                InvoiceItem item = new UsageInvoiceItem(invoiceId, getAccountId(), getBundleId(), getSubscriptionId(), getPlanName(),
                                                        getPhaseName(), startRolledUpDate, endRolledUpDate, toBeBilledUsage.subtract(billedUsage), getCurrency(), unitType);
                result.add(item);
            }
        }
        return result;
    }

    private List<RolledUpUsage> getRolledUpUsage() {

        final Iterable<DateTime> transitions = Iterables.transform(transitionTimes, new Function<LocalDate, DateTime>() {
            @Override
            public DateTime apply(final LocalDate input) {
                return localDateToEndOfDayInAccountTimezone(input, getAccountTimeZone());
            }
        });
        final List<RolledUpUsage> usagesForInterval = usageApi.getAllUsageForSubscription(getSubscriptionId(), unitType, ImmutableList.copyOf(transitions), context);
        return usagesForInterval;
    }

    private final BigDecimal computeToBeBilledUsage(final BigDecimal units) throws CatalogApiException {

        // STEPH_USAGE need to review catalog xml which defines block tiers, ...
        final int blockSize = 0x1000;
        final int nbBlocks = units.intValue() / blockSize + ((units.intValue() % blockSize == 0) ? 0 : 1);

        // STEPH_USAGE this is wrong should use from each tier.
        final List<TieredBlock> tieredBlocks = getTieredBlocks(usage, unitType);
        for (TieredBlock tier : tieredBlocks) {
            if (tier.getMax() >= units.doubleValue()) {
                return tier.getPrice().getPrice(getCurrency());
            }
        }
        // Return from last tier
        return tieredBlocks.get(tieredBlocks.size() - 1).getPrice().getPrice(getCurrency());
    }

    private final BigDecimal computeBilledUsage(final LocalDate startDate, final LocalDate endDate, final List<InvoiceItem> existingUsage) {
        final Iterable<InvoiceItem> filteredUsageForInterval = Iterables.filter(existingUsage, new Predicate<InvoiceItem>() {
            @Override
            public boolean apply(final InvoiceItem input) {
                // STEPH_USAGE what happens if we discover usage period that overlap (one side or both side) the [startDate, endDate] interval
                // STEPH_USAGE how to distinguish different usage charges (maybe different sections.) (needs to at least of the unitType in usage element
                return input.getInvoiceItemType() == InvoiceItemType.USAGE &&
                       input.getStartDate().compareTo(startDate) >= 0 &&
                       input.getEndDate().compareTo(endDate) <= 0;
            }
        });

        final BigDecimal billedAmount = BigDecimal.ZERO;
        for (InvoiceItem ii : filteredUsageForInterval) {
            billedAmount.add(ii.getAmount());
        }
        // Return the billed $ amount (not the # of units)
        return billedAmount;
    }

}