Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement invoice config support and new bus events to notify of upco…
…ming invoices. See #308
- Loading branch information
Showing
19 changed files
with
459 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
34 changes: 34 additions & 0 deletions
34
api/src/main/java/org/killbill/billing/events/InvoiceNotificationInternalEvent.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -0,0 +1,34 @@ | |||
/* | |||
* Copyright 2014-2015 Groupon, Inc | |||
* Copyright 2014-2015 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.events; | |||
|
|||
import java.math.BigDecimal; | |||
import java.util.UUID; | |||
|
|||
import org.joda.time.DateTime; | |||
import org.killbill.billing.catalog.api.Currency; | |||
|
|||
public interface InvoiceNotificationInternalEvent extends InvoiceInternalEvent { | |||
|
|||
public BigDecimal getAmountOwed(); | |||
This comment has been minimized.
Sorry, something went wrong. |
|||
|
|||
public Currency getCurrency(); | |||
|
|||
public DateTime getTargetDate(); | |||
|
|||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
64 changes: 64 additions & 0 deletions
64
beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestInvoiceNotifications.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -0,0 +1,64 @@ | |||
/* | |||
* Copyright 2014-2015 Groupon, Inc | |||
* Copyright 2014-2015 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.integration; | |||
|
|||
import org.joda.time.LocalDate; | |||
import org.killbill.billing.account.api.Account; | |||
import org.killbill.billing.account.api.AccountData; | |||
import org.killbill.billing.api.TestApiListener.NextEvent; | |||
import org.killbill.billing.catalog.api.BillingPeriod; | |||
import org.killbill.billing.catalog.api.ProductCategory; | |||
import org.killbill.billing.entitlement.api.DefaultEntitlement; | |||
import org.killbill.billing.platform.api.KillbillConfigSource; | |||
import org.testng.annotations.Test; | |||
|
|||
import com.google.common.collect.ImmutableMap; | |||
|
|||
public class TestInvoiceNotifications extends TestIntegrationBase { | |||
|
|||
@Override | |||
protected KillbillConfigSource getConfigSource() { | |||
ImmutableMap additionalProperties = new ImmutableMap.Builder() | |||
.put("org.killbill.invoice.dryRunNotificationSchedule", "7d") | |||
.build(); | |||
return getConfigSource("/beatrix.properties", additionalProperties); | |||
} | |||
|
|||
@Test(groups = "slow") | |||
public void testInvoiceNotificationBasic() throws Exception { | |||
|
|||
final AccountData accountData = getAccountData(1); | |||
final Account account = createAccountWithNonOsgiPaymentMethod(accountData); | |||
accountChecker.checkAccount(account.getId(), accountData, callContext); | |||
|
|||
// We take april as it has 30 days (easier to play with BCD) | |||
// Set clock to the initial start date - we implicitly assume here that the account timezone is UTC | |||
clock.setDay(new LocalDate(2012, 4, 1)); | |||
|
|||
final DefaultEntitlement bpSubscription = createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey", "Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.INVOICE); | |||
|
|||
// Move to end of trial => 2012, 5, 1 | |||
addDaysAndCheckForCompletion(30, NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT); | |||
|
|||
// Next invoice is scheduled for 2012, 6, 1 so we should have a NOTIFICATION event 7 days before, on 2012, 5, 25 | |||
addDaysAndCheckForCompletion(24, NextEvent.INVOICE_NOTIFICATION); | |||
|
|||
// And then verify the invoice is correctly generated | |||
addDaysAndCheckForCompletion(7, NextEvent.INVOICE, NextEvent.PAYMENT); | |||
} | |||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
|
@@ -38,14 +38,19 @@ | ||
import org.killbill.billing.account.api.AccountInternalApi; | import org.killbill.billing.account.api.AccountInternalApi; | ||
import org.killbill.billing.callcontext.InternalCallContext; | import org.killbill.billing.callcontext.InternalCallContext; | ||
import org.killbill.billing.callcontext.InternalTenantContext; | import org.killbill.billing.callcontext.InternalTenantContext; | ||
import org.killbill.billing.catalog.api.BillingActionPolicy; | |||
import org.killbill.billing.catalog.api.BillingMode; | import org.killbill.billing.catalog.api.BillingMode; | ||
import org.killbill.billing.catalog.api.CatalogApiException; | import org.killbill.billing.catalog.api.CatalogApiException; | ||
import org.killbill.billing.catalog.api.Currency; | import org.killbill.billing.catalog.api.Currency; | ||
import org.killbill.billing.catalog.api.PlanPhasePriceOverride; | |||
import org.killbill.billing.catalog.api.PlanPhaseSpecifier; | |||
import org.killbill.billing.catalog.api.Usage; | import org.killbill.billing.catalog.api.Usage; | ||
import org.killbill.billing.entitlement.api.SubscriptionEventType; | |||
import org.killbill.billing.events.BusInternalEvent; | import org.killbill.billing.events.BusInternalEvent; | ||
import org.killbill.billing.events.EffectiveSubscriptionInternalEvent; | import org.killbill.billing.events.EffectiveSubscriptionInternalEvent; | ||
import org.killbill.billing.events.InvoiceAdjustmentInternalEvent; | import org.killbill.billing.events.InvoiceAdjustmentInternalEvent; | ||
import org.killbill.billing.events.InvoiceInternalEvent; | import org.killbill.billing.events.InvoiceInternalEvent; | ||
import org.killbill.billing.events.InvoiceNotificationInternalEvent; | |||
import org.killbill.billing.invoice.api.DryRunArguments; | import org.killbill.billing.invoice.api.DryRunArguments; | ||
import org.killbill.billing.invoice.api.Invoice; | import org.killbill.billing.invoice.api.Invoice; | ||
import org.killbill.billing.invoice.api.InvoiceApiException; | import org.killbill.billing.invoice.api.InvoiceApiException; | ||
|
@@ -54,6 +59,7 @@ | ||
import org.killbill.billing.invoice.api.InvoiceNotifier; | import org.killbill.billing.invoice.api.InvoiceNotifier; | ||
import org.killbill.billing.invoice.api.user.DefaultInvoiceAdjustmentEvent; | import org.killbill.billing.invoice.api.user.DefaultInvoiceAdjustmentEvent; | ||
import org.killbill.billing.invoice.api.user.DefaultInvoiceCreationEvent; | import org.killbill.billing.invoice.api.user.DefaultInvoiceCreationEvent; | ||
import org.killbill.billing.invoice.api.user.DefaultInvoiceNotificationInternalEvent; | |||
import org.killbill.billing.invoice.api.user.DefaultNullInvoiceEvent; | import org.killbill.billing.invoice.api.user.DefaultNullInvoiceEvent; | ||
import org.killbill.billing.invoice.dao.InvoiceDao; | import org.killbill.billing.invoice.dao.InvoiceDao; | ||
import org.killbill.billing.invoice.dao.InvoiceItemModelDao; | import org.killbill.billing.invoice.dao.InvoiceItemModelDao; | ||
|
@@ -93,6 +99,8 @@ public class InvoiceDispatcher { | ||
private static final Logger log = LoggerFactory.getLogger(InvoiceDispatcher.class); | private static final Logger log = LoggerFactory.getLogger(InvoiceDispatcher.class); | ||
private static final int NB_LOCK_TRY = 5; | private static final int NB_LOCK_TRY = 5; | ||
|
|
||
private static final NullDryRunArguments NULL_DRY_RUN_ARGUMENTS = new NullDryRunArguments(); | |||
|
|||
private final InvoiceGenerator generator; | private final InvoiceGenerator generator; | ||
private final BillingInternalApi billingApi; | private final BillingInternalApi billingApi; | ||
private final AccountInternalApi accountApi; | private final AccountInternalApi accountApi; | ||
|
@@ -130,27 +138,49 @@ public InvoiceDispatcher(final InvoiceGenerator generator, | ||
this.clock = clock; | this.clock = clock; | ||
} | } | ||
|
|
||
public void processSubscription(final EffectiveSubscriptionInternalEvent transition, | public void processSubscriptionForInvoiceGeneration(final EffectiveSubscriptionInternalEvent transition, | ||
final InternalCallContext context) throws InvoiceApiException { | final InternalCallContext context) throws InvoiceApiException { | ||
final UUID subscriptionId = transition.getSubscriptionId(); | final UUID subscriptionId = transition.getSubscriptionId(); | ||
final DateTime targetDate = transition.getEffectiveTransitionTime(); | final DateTime targetDate = transition.getEffectiveTransitionTime(); | ||
processSubscription(subscriptionId, targetDate, context); | processSubscriptionForInvoiceGeneration(subscriptionId, targetDate, context); | ||
} | |||
|
|||
public void processSubscriptionForInvoiceGeneration(final UUID subscriptionId, final DateTime targetDate, final InternalCallContext context) throws InvoiceApiException { | |||
processSubscriptionInternal(subscriptionId, targetDate, false, context); | |||
} | |||
|
|||
public void processSubscriptionForInvoiceNotification(final UUID subscriptionId, final DateTime targetDate, final InternalCallContext context) throws InvoiceApiException { | |||
final Invoice dryRunInvoice = processSubscriptionInternal(subscriptionId, targetDate, true, context); | |||
if (dryRunInvoice != null && dryRunInvoice.getBalance().compareTo(BigDecimal.ZERO) > 0) { | |||
final InvoiceNotificationInternalEvent event = new DefaultInvoiceNotificationInternalEvent(dryRunInvoice.getAccountId(), dryRunInvoice.getBalance(), dryRunInvoice.getCurrency(), | |||
targetDate, context.getAccountRecordId(), context.getTenantRecordId(), context.getUserToken()); | |||
try { | |||
eventBus.post(event); | |||
} catch (EventBusException e) { | |||
log.error("Failed to post event " + event, e); | |||
} | |||
} | |||
} | } | ||
|
|
||
public void processSubscription(final UUID subscriptionId, final DateTime targetDate, final InternalCallContext context) throws InvoiceApiException { |
|
||
private Invoice processSubscriptionInternal(final UUID subscriptionId, final DateTime targetDate, final boolean dryRunForNotification, final InternalCallContext context) throws InvoiceApiException { | |||
try { | try { | ||
if (subscriptionId == null) { | if (subscriptionId == null) { | ||
log.error("Failed handling SubscriptionBase change.", new InvoiceApiException(ErrorCode.INVOICE_INVALID_TRANSITION)); | log.error("Failed handling SubscriptionBase change.", new InvoiceApiException(ErrorCode.INVOICE_INVALID_TRANSITION)); | ||
return; | return null; | ||
} | } | ||
final UUID accountId = subscriptionApi.getAccountIdFromSubscriptionId(subscriptionId, context); | final UUID accountId = subscriptionApi.getAccountIdFromSubscriptionId(subscriptionId, context); | ||
processAccount(accountId, targetDate, null, context); | final DryRunArguments dryRunArguments = dryRunForNotification ? NULL_DRY_RUN_ARGUMENTS : null; | ||
|
|||
return processAccount(accountId, targetDate, dryRunArguments, context); | |||
} catch (final SubscriptionBaseApiException e) { | } catch (final SubscriptionBaseApiException e) { | ||
log.error("Failed handling SubscriptionBase change.", | log.error("Failed handling SubscriptionBase change.", | ||
new InvoiceApiException(ErrorCode.INVOICE_NO_ACCOUNT_ID_FOR_SUBSCRIPTION_ID, subscriptionId.toString())); | new InvoiceApiException(ErrorCode.INVOICE_NO_ACCOUNT_ID_FOR_SUBSCRIPTION_ID, subscriptionId.toString())); | ||
return null; | |||
} | } | ||
} | } | ||
|
|
||
|
|||
public Invoice processAccount(final UUID accountId, final DateTime targetDate, | public Invoice processAccount(final UUID accountId, final DateTime targetDate, | ||
@Nullable final DryRunArguments dryRunArguments, final InternalCallContext context) throws InvoiceApiException { | @Nullable final DryRunArguments dryRunArguments, final InternalCallContext context) throws InvoiceApiException { | ||
GlobalLock lock = null; | GlobalLock lock = null; | ||
|
@@ -426,4 +456,35 @@ private void addInvoiceItemsToChargeThroughDates(final DateAndTimeZoneContext da | ||
} | } | ||
} | } | ||
} | } | ||
|
|||
private final static class NullDryRunArguments implements DryRunArguments { | |||
@Override | |||
public PlanPhaseSpecifier getPlanPhaseSpecifier() { | |||
return null; | |||
} | |||
@Override | |||
public SubscriptionEventType getAction() { | |||
return null; | |||
} | |||
@Override | |||
public UUID getSubscriptionId() { | |||
return null; | |||
} | |||
@Override | |||
public DateTime getEffectiveDate() { | |||
return null; | |||
} | |||
@Override | |||
public UUID getBundleId() { | |||
return null; | |||
} | |||
@Override | |||
public BillingActionPolicy getBillingActionPolicy() { | |||
return null; | |||
} | |||
@Override | |||
public List<PlanPhasePriceOverride> getPlanPhasePriceoverrides() { | |||
return null; | |||
This comment has been minimized.
Sorry, something went wrong.
This comment has been minimized.
Sorry, something went wrong.
sbrossie
Author
Member
|
|||
} | |||
} | |||
} | } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Nit: I would have named it
balance
to make it obvious it'sInvoice#getBalance
, which is computed in a very specific way by convention (seeInvoiceCalculatorUtils
).