/* * Copyright 2010-2013 Ning, Inc. * Copyright 2014-2016 Groupon, Inc * Copyright 2014-2016 The Billing Project, LLC * * The Billing Project 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.beatrix.util; import java.math.BigDecimal; import java.util.List; import java.util.UUID; import org.joda.time.LocalDate; import org.killbill.billing.entitlement.api.DefaultEntitlement; import org.killbill.billing.entitlement.api.EntitlementApi; import org.killbill.billing.entitlement.api.EntitlementApiException; import org.killbill.billing.invoice.api.Invoice; import org.killbill.billing.invoice.api.InvoiceApiException; import org.killbill.billing.invoice.api.InvoiceItem; import org.killbill.billing.invoice.api.InvoiceItemType; import org.killbill.billing.invoice.api.InvoiceUserApi; import org.killbill.billing.subscription.api.SubscriptionBase; import org.killbill.billing.util.callcontext.CallContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.testng.Assert; import com.google.common.base.Predicate; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.inject.Inject; import static org.testng.Assert.assertNull; import static org.testng.Assert.assertTrue; import static org.testng.Assert.fail; public class InvoiceChecker { private static final Logger log = LoggerFactory.getLogger(InvoiceChecker.class); private final InvoiceUserApi invoiceUserApi; private final EntitlementApi entitlementApi; private final AuditChecker auditChecker; @Inject public InvoiceChecker(final InvoiceUserApi invoiceUserApi, final EntitlementApi entitlementApi, final AuditChecker auditChecker) { this.invoiceUserApi = invoiceUserApi; this.entitlementApi = entitlementApi; this.auditChecker = auditChecker; } public Invoice checkInvoice(final UUID accountId, final int invoiceOrderingNumber, final CallContext context, final ExpectedInvoiceItemCheck... expected) throws InvoiceApiException { return checkInvoice(accountId, invoiceOrderingNumber, context, ImmutableList.<ExpectedInvoiceItemCheck>copyOf(expected)); } public Invoice checkInvoice(final UUID accountId, final int invoiceOrderingNumber, final CallContext context, final List<ExpectedInvoiceItemCheck> expected) throws InvoiceApiException { final List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(accountId, false, context); //Assert.assertEquals(invoices.size(), invoiceOrderingNumber); final Invoice invoice = invoices.get(invoiceOrderingNumber - 1); checkInvoice(invoice.getId(), context, expected); return invoice; } public void checkInvoice(final UUID invoiceId, final CallContext context, final List<ExpectedInvoiceItemCheck> expected) throws InvoiceApiException { final Invoice invoice = invoiceUserApi.getInvoice(invoiceId, context); Assert.assertNotNull(invoice); checkInvoice(invoice, context, expected); } public void checkInvoiceNoAudits(final Invoice invoice, final CallContext context, final List<ExpectedInvoiceItemCheck> expected) throws InvoiceApiException { final List<InvoiceItem> actual = invoice.getInvoiceItems(); Assert.assertEquals(actual.size(), expected.size(), String.format("Expected items: %s, actual items: %s", expected, actual)); for (final ExpectedInvoiceItemCheck cur : expected) { boolean found = false; // First try to find exact match; this is necessary because the for loop below might encounter a similar item -- for instance // same type, same dates, but different amount and choke. final InvoiceItem foundItem = Iterables.tryFind(actual, new Predicate<InvoiceItem>() { @Override public boolean apply(final InvoiceItem input) { if (input.getInvoiceItemType() != cur.getType() || (cur.shouldCheckDates() && input.getStartDate().compareTo(cur.getStartDate()) != 0)) { return false; } if (input.getAmount().compareTo(cur.getAmount()) != 0) { return false; } if (!cur.shouldCheckDates() || (cur.getEndDate() == null && input.getEndDate() == null) || (cur.getEndDate() != null && input.getEndDate() != null && cur.getEndDate().compareTo(input.getEndDate()) == 0)) { return true; } return false; } }).orNull(); if (foundItem != null) { continue; } // If we could not find it, we still loop again, so that error message helps to debug when there is a 'similar' item. for (final InvoiceItem in : actual) { // Match first on type and start date if (in.getInvoiceItemType() != cur.getType() || (cur.shouldCheckDates() && in.getStartDate().compareTo(cur.getStartDate()) != 0)) { continue; } if (in.getAmount().compareTo(cur.getAmount()) != 0) { log.info(String.format("Found item type = %s and startDate = %s but amount differ (actual = %s, expected = %s) ", cur.getType(), cur.getStartDate(), in.getAmount(), cur.getAmount())); continue; } if (!cur.shouldCheckDates() || (cur.getEndDate() == null && in.getEndDate() == null) || (cur.getEndDate() != null && in.getEndDate() != null && cur.getEndDate().compareTo(in.getEndDate()) == 0)) { found = true; break; } log.info(String.format("Found item type = %s and startDate = %s, amount = %s but endDate differ (actual = %s, expected = %s) ", cur.getType(), cur.getStartDate(), in.getAmount(), in.getEndDate(), cur.getEndDate())); } if (!found) { final StringBuilder debugBuilder = new StringBuilder(); debugBuilder.append(String.format("Invoice id=[%s], targetDate=[%s]", invoice.getId(), invoice.getTargetDate())); for (final InvoiceItem actualInvoiceItem : actual) { debugBuilder.append(String.format("\n type=[%s] startDate=[%s] endDate=[%s] amount=[%s]", actualInvoiceItem.getInvoiceItemType(), actualInvoiceItem.getStartDate(), actualInvoiceItem.getEndDate(), actualInvoiceItem.getAmount())); } final String failureMessage = String.format("Failed to find invoice item type = %s and startDate = %s, amount = %s, endDate = %s for invoice id %s\n%s", cur.getType(), cur.getStartDate(), cur.getAmount(), cur.getEndDate(), invoice.getId(), debugBuilder.toString()); Assert.fail(failureMessage); } } } public void checkInvoice(final Invoice invoice, final CallContext context, final List<ExpectedInvoiceItemCheck> expected) throws InvoiceApiException { checkInvoiceNoAudits(invoice, context, expected); auditChecker.checkInvoiceCreated(invoice, context); } public void checkNullChargedThroughDate(final UUID entitlementId, final CallContext context) { checkChargedThroughDate(entitlementId, null, context); } public void checkChargedThroughDate(final UUID entitlementId, final LocalDate expectedLocalCTD, final CallContext context) { try { final DefaultEntitlement entitlement = (DefaultEntitlement) entitlementApi.getEntitlementForId(entitlementId, context); final SubscriptionBase subscription = entitlement.getSubscriptionBase(); if (expectedLocalCTD == null) { assertNull(subscription.getChargedThroughDate()); } else { final String msg = String.format("Checking CTD for entitlement %s : expectedLocalCTD = %s, got %s", entitlementId, expectedLocalCTD, subscription.getChargedThroughDate().toLocalDate()); assertTrue(expectedLocalCTD.compareTo(subscription.getChargedThroughDate().toLocalDate()) == 0, msg); } } catch (final EntitlementApiException e) { fail("Failed to retrieve entitlement for " + entitlementId); } } public static class ExpectedInvoiceItemCheck { private final boolean checkDates; private final LocalDate startDate; private final LocalDate endDate; private final InvoiceItemType type; private final BigDecimal amount; public ExpectedInvoiceItemCheck(final InvoiceItemType type, final BigDecimal amount, final boolean checkDates) { this.checkDates = checkDates; this.type = type; this.startDate = null; this.endDate = null; this.amount = amount; } public ExpectedInvoiceItemCheck(final InvoiceItemType type, final BigDecimal amount) { this(type, amount, false); } public ExpectedInvoiceItemCheck(final LocalDate startDate, final LocalDate endDate, final InvoiceItemType type, final BigDecimal amount, final boolean checkDates) { this.checkDates = checkDates; this.startDate = startDate; this.endDate = endDate; this.type = type; this.amount = amount; } public ExpectedInvoiceItemCheck(final LocalDate startDate, final LocalDate endDate, final InvoiceItemType type, final BigDecimal amount) { this(startDate, endDate, type, amount, true); } public boolean shouldCheckDates() { return checkDates; } public LocalDate getStartDate() { return startDate; } public LocalDate getEndDate() { return endDate; } public InvoiceItemType getType() { return type; } public BigDecimal getAmount() { return amount; } @Override public String toString() { final StringBuilder sb = new StringBuilder("ExpectedInvoiceItemCheck{"); sb.append("checkDates=").append(checkDates); sb.append(", startDate=").append(startDate); sb.append(", endDate=").append(endDate); sb.append(", type=").append(type); sb.append(", amount=").append(amount); sb.append('}'); return sb.toString(); } } }