Skip to content

Commit

Permalink
overdue: allow email notifications
Browse files Browse the repository at this point in the history
Add option to send emails when the overdue state change.
For now, these emails are not localized.

Signed-off-by: Pierre-Alexandre Meyer <pierre@ning.com>
  • Loading branch information
Pierre-Alexandre Meyer committed Sep 22, 2012
1 parent bb536ab commit c494c0f
Show file tree
Hide file tree
Showing 14 changed files with 317 additions and 44 deletions.
26 changes: 26 additions & 0 deletions api/src/main/java/com/ning/billing/overdue/EmailNotification.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright 2010-2012 Ning, 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 com.ning.billing.overdue;

public interface EmailNotification {

public String getSubject();

public String getTemplateName();

public Boolean isHTML();
}
3 changes: 2 additions & 1 deletion api/src/main/java/com/ning/billing/overdue/OverdueState.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@

import com.ning.billing.junction.api.Blockable;


public interface OverdueState<T extends Blockable> {

public String getName();
Expand All @@ -40,4 +39,6 @@ public interface OverdueState<T extends Blockable> {
public Period getReevaluationInterval() throws OverdueApiException;

public Condition<T> getCondition();

public EmailNotification getEnterStateEmailNotification();
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,8 @@
import java.util.List;

public interface EmailSender {
public void sendSecureEmail(List<String> to, List<String> cc, String subject, String htmlBody) throws IOException, EmailApiException;

public void sendHTMLEmail(List<String> to, List<String> cc, String subject, String htmlBody) throws IOException, EmailApiException;

public void sendPlainTextEmail(List<String> to, List<String> cc, String subject, String body) throws IOException, EmailApiException;
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ public void notify(final Account account, final Invoice invoice) throws InvoiceA

final EmailSender sender = new DefaultEmailSender(config);
try {
sender.sendSecureEmail(to, cc, subject, htmlBody);
sender.sendHTMLEmail(to, cc, subject, htmlBody);
} catch (EmailApiException e) {
throw new InvoiceApiException(e, ErrorCode.EMAIL_SENDING_FAILED);
} catch (IOException e) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright 2010-2012 Ning, 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 com.ning.billing.overdue.applicator;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import com.ning.billing.account.api.Account;
import com.ning.billing.junction.api.Blockable;
import com.ning.billing.overdue.OverdueState;
import com.ning.billing.overdue.config.api.BillingState;
import com.ning.billing.util.email.templates.TemplateEngine;

import com.google.inject.Inject;

public class OverdueEmailGenerator {

private final TemplateEngine templateEngine;

@Inject
public OverdueEmailGenerator(final TemplateEngine templateEngine) {
this.templateEngine = templateEngine;
}

public <T extends Blockable> String generateEmail(final Account account, final BillingState<T> billingState,
final T overdueable, final OverdueState<T> nextOverdueState) throws IOException {
final Map<String, Object> data = new HashMap<String, Object>();

// TODO raw objects for now. We eventually should respect the account locale and support translations
data.put("account", account);
data.put("billingState", billingState);
data.put("overdueable", overdueable);
data.put("nextOverdueState", nextOverdueState);

// TODO single template for all languages for now
return templateEngine.executeTemplate(nextOverdueState.getEnterStateEmailNotification().getTemplateName(), data);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@

package com.ning.billing.overdue.applicator;

import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;

import org.joda.time.DateTime;
import org.joda.time.LocalDate;
Expand All @@ -27,12 +29,15 @@

import com.ning.billing.ErrorCode;
import com.ning.billing.account.api.Account;
import com.ning.billing.account.api.AccountApiException;
import com.ning.billing.account.api.AccountUserApi;
import com.ning.billing.catalog.api.ActionPolicy;
import com.ning.billing.entitlement.api.user.EntitlementUserApi;
import com.ning.billing.entitlement.api.user.EntitlementUserApiException;
import com.ning.billing.entitlement.api.user.Subscription;
import com.ning.billing.entitlement.api.user.SubscriptionBundle;
import com.ning.billing.junction.api.Blockable;
import com.ning.billing.junction.api.Blockable.Type;
import com.ning.billing.junction.api.BlockingApi;
import com.ning.billing.junction.api.BlockingApiException;
import com.ning.billing.junction.api.DefaultBlockingState;
Expand All @@ -50,8 +55,14 @@
import com.ning.billing.util.callcontext.CallOrigin;
import com.ning.billing.util.callcontext.UserType;
import com.ning.billing.util.clock.Clock;
import com.ning.billing.util.email.DefaultEmailSender;
import com.ning.billing.util.email.EmailApiException;
import com.ning.billing.util.email.EmailConfig;
import com.ning.billing.util.email.EmailSender;

import com.google.common.collect.ImmutableList;
import com.google.inject.Inject;
import com.samskivert.mustache.MustacheException;

public class OverdueStateApplicator<T extends Blockable> {

Expand All @@ -63,16 +74,23 @@ public class OverdueStateApplicator<T extends Blockable> {
private final Clock clock;
private final OverdueCheckPoster poster;
private final Bus bus;
private final AccountUserApi accountUserApi;
private final EntitlementUserApi entitlementUserApi;
private final CallContextFactory factory;
private final OverdueEmailGenerator overdueEmailGenerator;
private final EmailSender emailSender;

@Inject
public OverdueStateApplicator(final BlockingApi accessApi, final EntitlementUserApi entitlementUserApi, final Clock clock,
final OverdueCheckPoster poster, final Bus bus, final CallContextFactory factory) {
public OverdueStateApplicator(final BlockingApi accessApi, final AccountUserApi accountUserApi, final EntitlementUserApi entitlementUserApi,
final Clock clock, final OverdueCheckPoster poster, final OverdueEmailGenerator overdueEmailGenerator,
final EmailConfig config, final Bus bus, final CallContextFactory factory) {
this.blockingApi = accessApi;
this.accountUserApi = accountUserApi;
this.entitlementUserApi = entitlementUserApi;
this.clock = clock;
this.poster = poster;
this.overdueEmailGenerator = overdueEmailGenerator;
this.emailSender = new DefaultEmailSender(config);
this.bus = bus;
this.factory = factory;
}
Expand All @@ -91,13 +109,16 @@ public void apply(final OverdueState<T> firstOverdueState, final BillingState<T>
}

if (previousOverdueStateName.equals(nextOverdueState.getName())) {
return; //That's it we are done...
return; // That's it, we are done...
}

storeNewState(overdueable, nextOverdueState);

cancelSubscriptionsIfRequired(overdueable, nextOverdueState);

sendEmailIfRequired(billingState, overdueable, nextOverdueState);

// Add entry in notification queue
final Period reevaluationInterval = nextOverdueState.getReevaluationInterval();
if (!nextOverdueState.isClearState()) {
createFutureNotification(overdueable, clock.getUTCNow().plus(reevaluationInterval));
Expand Down Expand Up @@ -158,7 +179,7 @@ private void cancelSubscriptionsIfRequired(final T blockable, final OverdueState
return;
}
try {
ActionPolicy actionPolicy = null;
final ActionPolicy actionPolicy;
switch (nextOverdueState.getSubscriptionCancellationPolicy()) {
case END_OF_TERM:
actionPolicy = ActionPolicy.END_OF_TERM;
Expand Down Expand Up @@ -194,4 +215,62 @@ private void computeSubscriptionsToCancel(final T blockable, final List<Subscrip
}
}
}

private void sendEmailIfRequired(final BillingState<T> billingState, final T overdueable, final OverdueState<T> nextOverdueState) {
// Note: we don't want to fail the full refresh call because sending the email failed.
// That's the reason why we catch all exceptions here.
// The alternative would be to: throw new OverdueApiException(e, ErrorCode.EMAIL_SENDING_FAILED);

// If sending is not configured, skip
if (nextOverdueState.getEnterStateEmailNotification() == null) {
return;
}

// Retrieve the account
final Account account;
final Type overdueableType = Blockable.Type.get(overdueable);
try {
if (Type.SUBSCRIPTION.equals(overdueableType)) {
final UUID bundleId = ((Subscription) overdueable).getBundleId();
final SubscriptionBundle bundle = entitlementUserApi.getBundleFromId(bundleId);
account = accountUserApi.getAccountById(bundle.getAccountId());
} else if (Type.SUBSCRIPTION_BUNDLE.equals(overdueableType)) {
final UUID bundleId = ((SubscriptionBundle) overdueable).getId();
final SubscriptionBundle bundle = entitlementUserApi.getBundleFromId(bundleId);
account = accountUserApi.getAccountById(bundle.getAccountId());
} else if (Type.ACCOUNT.equals(overdueableType)) {
account = (Account) overdueable;
} else {
log.warn("Unable to retrieve account for overdueable {} (type {})", overdueable.getId(), overdueableType);
return;
}
} catch (EntitlementUserApiException e) {
log.warn(String.format("Unable to retrieve account for overdueable %s (type %s)", overdueable.getId(), overdueableType), e);
return;
} catch (AccountApiException e) {
log.warn(String.format("Unable to retrieve account for overdueable %s (type %s)", overdueable.getId(), overdueableType), e);
return;
}

final List<String> to = ImmutableList.<String>of(account.getEmail());
// TODO - should we look at the account CC: list?
final List<String> cc = ImmutableList.<String>of();
final String subject = nextOverdueState.getEnterStateEmailNotification().getSubject();

try {
// Generate and send the email
final String emailBody = overdueEmailGenerator.generateEmail(account, billingState, overdueable, nextOverdueState);
if (nextOverdueState.getEnterStateEmailNotification().isHTML()) {
emailSender.sendHTMLEmail(to, cc, subject, emailBody);
} else {
emailSender.sendPlainTextEmail(to, cc, subject, emailBody);
}
} catch (IOException e) {
log.warn(String.format("Unable to generate or send overdue notification email for account %s and overdueable %s", account.getId(), overdueable.getId()), e);
} catch (EmailApiException e) {
log.warn(String.format("Unable to send overdue notification email for account %s and overdueable %s", account.getId(), overdueable.getId()), e);
} catch (MustacheException e) {
log.warn(String.format("Unable to generate overdue notification email for account %s and overdueable %s", account.getId(), overdueable.getId()), e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright 2010-2012 Ning, 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 com.ning.billing.overdue.config;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;

import com.ning.billing.overdue.EmailNotification;

@XmlAccessorType(XmlAccessType.NONE)
public class DefaultEmailNotification implements EmailNotification {

@XmlElement(required = true, name = "subject")
private String subject;

@XmlElement(required = true, name = "templateName")
private String templateName;

@XmlElement(required = false, name = "isHTML")
private Boolean isHTML = false;

@Override
public String getSubject() {
return subject;
}

@Override
public String getTemplateName() {
return templateName;
}

@Override
public Boolean isHTML() {
return isHTML;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import com.ning.billing.ErrorCode;
import com.ning.billing.catalog.api.TimeUnit;
import com.ning.billing.junction.api.Blockable;
import com.ning.billing.overdue.EmailNotification;
import com.ning.billing.overdue.OverdueApiException;
import com.ning.billing.overdue.OverdueCancellationPolicicy;
import com.ning.billing.overdue.OverdueState;
Expand Down Expand Up @@ -64,8 +65,10 @@ public class DefaultOverdueState<T extends Blockable> extends ValidatingConfig<O
@XmlElement(required = false, name = "autoReevaluationInterval")
private DefaultDuration autoReevaluationInterval;

@XmlElement(required = false, name = "enterStateEmailNotification")
private DefaultEmailNotification enterStateEmailNotification;

//Other actions could include
// - send email
// - trigger payment retry?
// - add tagStore to bundle/account
// - set payment failure email template
Expand Down Expand Up @@ -163,4 +166,9 @@ public ValidationErrors validate(final OverdueConfig root,
public int getDaysBetweenPaymentRetries() {
return 8;
}

@Override
public EmailNotification getEnterStateEmailNotification() {
return enterStateEmailNotification;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import com.ning.billing.overdue.OverdueService;
import com.ning.billing.overdue.OverdueUserApi;
import com.ning.billing.overdue.api.DefaultOverdueUserApi;
import com.ning.billing.overdue.applicator.OverdueEmailGenerator;
import com.ning.billing.overdue.service.DefaultOverdueService;
import com.ning.billing.overdue.service.ExtendedOverdueService;
import com.ning.billing.overdue.wrapper.OverdueWrapperFactory;
Expand All @@ -42,6 +43,7 @@ protected void configure() {
// internal bindings
installOverdueService();
installOverdueWrapperFactory();
installOverdueEmail();

final OverdueProperties config = new ConfigurationObjectFactory(System.getProperties()).build(OverdueProperties.class);
bind(OverdueProperties.class).toInstance(config);
Expand All @@ -58,6 +60,10 @@ protected void installOverdueWrapperFactory() {
bind(OverdueWrapperFactory.class).asEagerSingleton();
}

protected void installOverdueEmail() {
bind(OverdueEmailGenerator.class).asEagerSingleton();
}

@Override
public void installOverdueUserApi() {
bind(OverdueUserApi.class).to(DefaultOverdueUserApi.class).asEagerSingleton();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,15 @@
import com.ning.billing.overdue.wrapper.OverdueWrapperFactory;
import com.ning.billing.util.bus.BusService;
import com.ning.billing.util.clock.ClockMock;
import com.ning.billing.util.email.EmailModule;
import com.ning.billing.util.email.templates.TemplateModule;
import com.ning.billing.util.glue.CallContextModule;
import com.ning.billing.util.glue.NotificationQueueModule;
import com.ning.billing.util.notificationq.NotificationQueueService.NotificationQueueAlreadyExists;

@Guice(modules = {DefaultOverdueModule.class, OverdueListenerTesterModule.class, MockClockModule.class, ApplicatorMockJunctionModule.class, CallContextModule.class, CatalogModule.class, MockInvoiceModule.class, MockPaymentModule.class, NotificationQueueModule.class, TestDbiModule.class})
@Guice(modules = {DefaultOverdueModule.class, OverdueListenerTesterModule.class, MockClockModule.class, ApplicatorMockJunctionModule.class,
CallContextModule.class, CatalogModule.class, MockInvoiceModule.class, MockPaymentModule.class, NotificationQueueModule.class,
EmailModule.class, TemplateModule.class, TestDbiModule.class})
public abstract class OverdueTestBase extends OverdueTestSuiteWithEmbeddedDB {
protected final String configXml =
"<overdueConfig>" +
Expand Down
Loading

0 comments on commit c494c0f

Please sign in to comment.